Adds the harness plug-in kind (interface 1). packages/plugins/harness/pi and claude-code hold what core branched on: the adapter, thinking levels, preflight, version probe, sandbox mounts, builtins line, typed tool names, gate timeout, bundle files, adapter env, turn end and session-files path. Core reaches a module only through the loader. A session's route is the instance's harness, model and thinking vars, or the three fallback. vars; a missing one refuses no-route, and the system file's execution keys are never read for a session. The launcher checks the primary route, then the fallback: harness-off, harness-refused, thinking-unsupported, then preflight. A failed preflight falls back when a fallback is set, and any refusal of the fallback names the primary's failure too. Account checks run only when the route's module counts an account. The manifest records route, routeReason and harnessVersion, replacing piVersion and claudeVersion. pi gets --thinking, Claude Code --effort, both from MOSAIC_THINKING. The business file refuses a partial fallback. routes and routes.set are human-only bus verbs outside ORGANIZATION_KEYS; checkRoutes is shared by routes.set, instances.set and limits.set and checks only changed routes. The chief of staff changes an instance's model, and the assistant's whole vars (Q169), through limits.set, so its route write runs the same checks. A model outside launch.models refuses unknown-model, primary or fallback, and routes.set passes that code through. A new run binds only on a switched-on harness module name and records its transcript path, which the usage reader uses. Settings gains a Routing table with six selects per instance and loses the Role models fieldset; it posts changed rows to /api/routes. webui/src/bus.mjs is unchanged. Until Jason's `mosaic plugins set` adds "harness": ["claude-code"], a new launch refuses harness-off.
942 lines
57 KiB
Markdown
942 lines
57 KiB
Markdown
# Mosaic Console
|
||
|
||
Piece 4, #1507. The control board is the first and only WebUI screen.
|
||
|
||
## Run
|
||
|
||
Start the existing board in one terminal, then the WebUI in another:
|
||
|
||
```sh
|
||
node packages/control-board/src/cli.mjs serve
|
||
node packages/webui/src/cli.mjs serve
|
||
```
|
||
|
||
Open http://127.0.0.1:7330/. Optional `--port N` and
|
||
`--board http://127.0.0.1:7331` select another port or loopback board origin.
|
||
`--business ID` picks the business the bus views read; without it they read
|
||
the running bus host's business (`mosaic bus start <business>`).
|
||
Port 0 picks a free port. Ctrl-C stops each foreground server. No daemon,
|
||
installation, account, login or deployment is added. The preview mode below
|
||
checks one header a sign-in proxy sets; it signs nobody in itself.
|
||
|
||
Only HTTP loopback board origins are accepted. The WebUI binds to IPv4 loopback
|
||
by default, rejects a repeated Host (400), nonlocal Host and cross-origin
|
||
requests, sends no CORS
|
||
headers, refuses redirects and accepts only JSON object POST bodies up to 4096
|
||
bytes (16 KiB for row 76's send, below). Do not put the board behind a public proxy. Put the Console behind one
|
||
only in the preview mode below, with a proxy that meets its contract.
|
||
|
||
## Preview behind a sign-in proxy (row 74, #1568)
|
||
|
||
```sh
|
||
node packages/webui/src/cli.mjs serve --port 7332 \
|
||
--public-host NAME --auth-header HEADER --allow-user ID
|
||
```
|
||
|
||
The three flags go together; one or two refuse at start, before any config
|
||
is read. NAME is a DNS name, no scheme or port. HEADER is a header name other
|
||
than Host or Origin; Node lowercases incoming header names, so it matches
|
||
without regard to case (`X-Example-Uid` on the flag finds `x-example-uid`).
|
||
ID is the one user id, 1 to 256 visible ASCII characters without a comma,
|
||
and the value compare is exact. The Console still binds to loopback only. It
|
||
names no identity provider and has no login code: the proxy signs the user
|
||
in and limits sign-in to the right group.
|
||
|
||
In preview mode:
|
||
|
||
- A request whose Host is exactly NAME must carry HEADER once, with a value
|
||
equal to ID. Missing, different or repeated answers 401 `sign-in required`
|
||
before any route runs, so no bus read or board call is made. An Origin,
|
||
when sent, must be `https://NAME`.
|
||
- A request whose Host is loopback on the Console's port (`127.x`,
|
||
`localhost` or `[::1]`) is checked as before and needs no HEADER, so the
|
||
operator's browser on the host still works. Any other Host is 403.
|
||
- A request that carries HEADER with any Host but NAME answers 403 `proxy
|
||
Host refused`. The operator's browser never sends HEADER; a proxy that
|
||
rewrote Host does, and it fails on its first signed-in request instead of reaching
|
||
the loopback path unchecked.
|
||
- More than one Host header answers 400 before any other check, in this
|
||
mode or not. Node keeps the first, so a loopback Host followed by
|
||
`Host: NAME` would otherwise skip the sign-in check.
|
||
- Every method other than GET and HEAD answers 403 `read-only`, whatever the
|
||
Host. `/api/seen` and `/api/reply` never reach the board, so Seen and Reply
|
||
fail in the page with that refusal. The one exception is POST
|
||
`/api/bus/message` when the Console was started with `--send` (next
|
||
section).
|
||
- The GET routes are unchanged: the bus views, the Queue, Business and
|
||
Settings reads, the board panel and the assets. If the board is down, its
|
||
panel shows the existing error and the bus views still answer.
|
||
|
||
What the proxy must do. The Console trusts HEADER only because the proxy
|
||
controls it:
|
||
|
||
- It terminates TLS for NAME and is the only thing that reaches the port
|
||
from off the host.
|
||
- It strips HEADER from every client request and sets it only after the
|
||
identity provider signs the user in.
|
||
- It forwards only requests whose Host is NAME, and forwards that Host
|
||
unchanged. nginx rewrites Host to the upstream by default; set
|
||
`proxy_set_header Host $host;` (or the literal NAME). Caddy and Traefik
|
||
keep the client's Host by default.
|
||
- Any loopback Host on the Console's port, `localhost` included, is the
|
||
operator's path and skips the header check, so a proxy that forwarded a
|
||
client's `Host: 127.0.0.1:7332` would let anyone through without signing
|
||
in. A tunnel whose local end uses the same port number sends such a Host
|
||
and reaches the Console unchecked. Anything that reaches a tunnel's local
|
||
end can send a loopback Host without HEADER, so bind that end to loopback.
|
||
- Every hop on this host that reaches the loopback port is bound by this
|
||
contract too, not only the edge.
|
||
- It passes the Console's status and body through and doesn't rewrite
|
||
upstream errors (nginx's `proxy_intercept_errors` with an `error_page`,
|
||
for example). Under `--send` the page reads a 4xx other than 408 as "not
|
||
sent", so a proxy that turned the Console's 503 `outcome-unknown` into a
|
||
4xx or a redirect would report a message that may exist as not sent.
|
||
|
||
The local exposure doesn't change ("Data and boundaries"): any process on
|
||
the host can still connect to the port and send `Host: NAME` with HEADER and
|
||
ID, or just a loopback Host, and read what a reader capability reads. The
|
||
user id is not a secret; the sign-in at the edge is what keeps others out.
|
||
Under `--send` the first of those can also send (next section).
|
||
|
||
To run the Console as a systemd user unit, `scripts/webui-service.sh`
|
||
renders and installs one with these flags (row 75; `docs/TOOLS.md`
|
||
"Console service"). Jason starts and restarts it, and so may Mos under lead
|
||
decision 102, which accepts that sends from a Console Mos started enter the
|
||
trail as Jason. Any other agent starting it works around the human proof
|
||
(lead decisions 62 and 94). Without `--send` the unit is read-only. The script takes `--send` (row 83)
|
||
only with the three preview flags; otherwise it exits 1 and writes nothing.
|
||
With it, the unit's ExecStart ends with `--send`, and the Console needs the
|
||
system config at start: without it serve refuses with `--send needs the bus
|
||
configured` and exits 2, which the unit doesn't restart. `status` says
|
||
whether the installed unit sends.
|
||
|
||
The unit can run a read-only shared stack copy (rows 77 and 85), with
|
||
serve's --checkout naming the canonical checkout so the queue stays current.
|
||
webui-service.sh copy calls stack-copy.sh copy: the default directory is
|
||
mosaic-stack/copies/<full id>, and commits must be on both local and pushed
|
||
refactor. The copy includes bus/CLI/Console code, adapters and pinned npm
|
||
dependencies. install --copy ID refuses unless the bus's effective
|
||
ExecStart and WorkingDirectory name that same copy, including drop-ins.
|
||
It also refuses while the Console runs. With --auth-header X-authentik-uid,
|
||
allow-user must be the 64-character Authentik uid (decision 101). Status
|
||
names the copy and refactor drift. Files and directories are read-only;
|
||
copies are retained. Existing Console-only copies are not migrated or pruned.
|
||
|
||
The single update and rollback sequence is
|
||
[TOOLS.md, Pinned stack update](../../docs/TOOLS.md#pinned-stack-update).
|
||
A bus restart ends managed sessions, so no PM turn may be in flight.
|
||
Switching the Console, bus or wrapper, changing any unit, and starting or
|
||
stopping either service requires Mos's go for that live step. Agents build
|
||
and test fixtures; they stop before the live sequence.
|
||
|
||
## Message a role (row 76, #1570)
|
||
|
||
```sh
|
||
node packages/webui/src/cli.mjs serve --port 7332 \
|
||
--public-host NAME --auth-header HEADER --allow-user ID --send
|
||
```
|
||
|
||
`--send` is off by default. It goes only with the three preview flags and
|
||
refuses at start without them. It also needs the system config: without it
|
||
the start refuses with `--send needs the bus configured`, where a read-only
|
||
Console would still serve the board. A unit that sends comes from
|
||
`scripts/webui-service.sh install` with `--send` (above). With `--send` the
|
||
Inbox has a "Message a role" box: a role picker filled from the agents read,
|
||
a text box and Send. The message reaches the role as a message from the
|
||
human, class INFO,
|
||
through `message.send` on the human transport. It goes the same way as one
|
||
sent from the CLI, with class INFO where `mosaic talk` uses REQUEST (lead
|
||
decision 95). Decisions are still answered in a
|
||
terminal. There are no other message classes, no corrections and no seat
|
||
conversations here.
|
||
|
||
What `--send` grants: two kinds of client can send a human instruction to
|
||
any role while it runs.
|
||
|
||
- Anyone the proxy lets through as ID.
|
||
- Any local process that reaches the port and sets HEADER to ID with
|
||
`Host: NAME`. The user id is not a secret. This is the same class of
|
||
exposure as the human CLI (lead decision 62), accepted for the PoC by
|
||
lead decision 95.
|
||
|
||
The request is POST `/api/bus/message` with a JSON object `{to, body,
|
||
in_reply_to?}`. Nothing is sent unless all of these hold, each checked on
|
||
every request:
|
||
|
||
- Host is NAME and HEADER is ID, once: otherwise 401 `sign-in required`.
|
||
There is no loopback path. The operator's browser on `127.0.0.1` reads
|
||
but can't send, and a loopback Host with HEADER is 403 `proxy Host
|
||
refused`, as above.
|
||
- Origin is exactly `https://NAME`, else 403 `cross-origin request refused`.
|
||
- `Sec-Fetch-Site` is sent once and is exactly `same-origin`, else 403
|
||
`cross-site request refused`. Browsers set it, and page script can't.
|
||
- Content-Type is `application/json` and the body is at most 16384 bytes
|
||
and a JSON object with `to` and `body`, optionally `in_reply_to`, and no
|
||
other key. Anything else is 400 `invalid-request`.
|
||
|
||
The Console doesn't judge `to` or the body beyond that; the broker does,
|
||
with `message.send`'s own rules. Its body limit, 32768 characters, is past
|
||
the request limit, so the 16 KiB limit binds first. A broker refusal means
|
||
nothing was written. It answers `{ error, message, sent: false }` with the
|
||
read routes' statuses: 400 for `unknown-role`, `invalid-request`,
|
||
`message-not-found` and `request-too-large`, 403 for `human-required` and
|
||
`read-only`, 503 for `not-configured` and `no-bus-host`, 502 otherwise.
|
||
|
||
When the transport can't say what happened (a timeout, a child that exits
|
||
without a code, an answer that can't be read or that has no message id), the
|
||
message may have been written. The answer is 503 `outcome-unknown` with
|
||
`sent: "unknown"`. Nothing retries, in the server or in the page.
|
||
|
||
Each send that gets past the sign-in check writes one line to the Console's
|
||
stdout: the time, `send`, the user, the `to` value (JSON-quoted, at most 64
|
||
characters), then `id=<message id>` or `error=<code>`. The body is never
|
||
logged. A request refused at sign-in or by the shared Host and Origin
|
||
checks writes no line.
|
||
|
||
On the page:
|
||
|
||
- The draft, role and text, survives the ten-second refresh, kept in page
|
||
memory like a Reply draft. Reloading the page loses it. Send is disabled
|
||
while a send is pending.
|
||
- Sent shows the message id and links its trail, `#/trail/message/<id>`:
|
||
the message and its deliveries. A reply is not in the trail; it shows
|
||
under the receipt (below). The text is cleared unless it was edited
|
||
during the send.
|
||
- Not sent shows the broker's code and keeps the draft. A redirect counts
|
||
as not sent: the Console never redirects this route, so only the sign-in
|
||
proxy does, when its session has expired. The page asks for a reload to
|
||
sign in again, and signs out (row 95, below).
|
||
- Outcome unknown says the message may have been sent, keeps the draft, and
|
||
has no trail to link, since no id came back. Check the role's side, or
|
||
the bus trail from a terminal, before sending again. A 408 counts as
|
||
unknown, here and on the board's Reply, since a proxy can answer it
|
||
after forwarding the request (Envoy's `stream_idle_timeout`).
|
||
|
||
The role's reply comes back as a message to the human with `in_reply_to`
|
||
set to the sent message's id. Since row 81 (#1575) the Sent receipt lists
|
||
those replies, oldest first, each with its role, time, class, body and a
|
||
trail link. It reads them with the bus `replies` read on every render, so a
|
||
reply shows on the next ten-second refresh or Read again. "No reply yet"
|
||
means the read returned none; "could not be read" means it failed, and the
|
||
next refresh tries again. The bus returns at most 50; at 50 the receipt says
|
||
only the first 50 show. The list belongs to the last Sent receipt: a new
|
||
send starts its own, and reloading the page drops it.
|
||
|
||
The Console only shows replies. It never receives a message or marks one
|
||
delivered or read: `replies` writes no `deliveries` row. So Jason's next
|
||
`mosaic talk` in a terminal still prints each reply, within talk's limits:
|
||
|
||
- talk receives only after it sends its own message;
|
||
- `--wait 0` returns straight after the send and receives nothing;
|
||
- otherwise each receive returns every undelivered message to the human, up
|
||
to 100 a call. The broker records each one delivered when
|
||
`message.receive` returns it, so the next receive doesn't return it
|
||
again; talk prints each one and marks it read.
|
||
|
||
## Use
|
||
|
||
- Waiting on you uses the board's `waitingOnYou` flag across all projects.
|
||
- Select a project on the left to filter the session table. Counts show visible
|
||
rows out of the total when Hide offline or Hide seen hides anything.
|
||
- Select an agent to open its inspector. Arrow keys move between table agent
|
||
buttons, Enter opens, and Close or Escape in the inspector returns focus.
|
||
- Seen clears the row from Waiting on you until the board detects new activity.
|
||
The collapsed Seen section keeps those rows available; Unsee returns them.
|
||
- Reply appears only where the board's registration permits it. A failure shows
|
||
the board's stderr and keeps the draft. Transport success is the board's
|
||
`delivered` receipt, not proof that the seat processed the message.
|
||
- Refresh runs the existing board scan. Automatic refresh uses the board page's
|
||
ten-second interval; Pause stops it. Failed refreshes keep the last snapshot
|
||
with a warning. There are no automatic action retries.
|
||
- Palette and appearance come from `tokens.css` (see "Shell" below) and persist
|
||
in this browser when local storage is available. Appearance defaults to
|
||
System. Settings (row 54) shows the same control and nothing else that
|
||
changes.
|
||
- History opens a read-only conversation view for a seat, from its Waiting
|
||
card, its table row or its inspector (#1507, CHAT-02). It shows the whole
|
||
branch with nothing clipped. Tool calls, tool results and thinking start
|
||
collapsed. Session text is always shown as text: Markdown stays as source,
|
||
and terminal controls, bidi controls and marks show as visible symbols. Reply in the view
|
||
uses the same board reply path as the inspector.
|
||
- The view checks for new entries on the same ten-second refresh, and Pause
|
||
stops it. It never switches on its own. A fork, a newer session for the seat
|
||
or a rewritten file each shows a marker with a button to open the other
|
||
history. Reloading after a rewrite keeps the branch the view was on; if that
|
||
branch is gone, the view opens the latest one and says so. Session lists
|
||
older sessions for the seat, and is empty while the list is read or when
|
||
that read fails. Only repository Pi seats
|
||
have history; other harnesses say so.
|
||
|
||
Drafts and receipts stay in page memory, including across refresh, inspector
|
||
changes and a stale registration. Reloading or closing the page loses them.
|
||
A pending send stays disabled across refresh; new text typed during a send is
|
||
not cleared by the earlier send's success. If the proxy loses the response,
|
||
delivery may be unknown: inspect the seat before sending again.
|
||
|
||
## Inbox, tasks, agents and trails (slice 1 S5, #1522)
|
||
|
||
The Console sidebar adds Inbox, Tasks and Agents next to the control board,
|
||
which stays at `#/`. Routes: `#/inbox`, `#/inbox/<decision>`, `#/messages`, `#/tasks`,
|
||
`#/tasks/<vikunja:project/task>`, `#/agents`, `#/trail/task/<ref>`,
|
||
`#/trail/decision/<id>` and `#/trail/message/<id>` (row 76), with `?kind=` filters on a trail. The design
|
||
note is `agents/dewey/work/wui/SLICE1-VIEWS.md`.
|
||
|
||
- Every view reads the four verbs of the Q1 module (`packages/bus/src/views.mjs`,
|
||
lead decision 56): `inbox`, `tasks`, `agents` and `trail`. The CLI reads the
|
||
same functions, so both show the same broker data.
|
||
- Nothing writes but the "Message a role" box, the Settings update, the Assistant
|
||
section's three buttons and the spec and docs editor's Save, all under `--send` (above,
|
||
"Assistant settings" and "Spec and docs editor"). A decision shows `mosaic decide <id> <key>` for each choice,
|
||
with Copy, which only puts the command on the clipboard. It is answered in a
|
||
terminal (REQ-DEC-3, Q4). Seen is not set from the browser either. The
|
||
command has no `--business`, so `mosaic decide` uses the running bus host's
|
||
business, as the Console does by default. From a Console started with
|
||
`--business` for another business the copied command refuses (no bus host,
|
||
or no such decision) rather than answering elsewhere.
|
||
- What no Q1 verb returns is labelled where it would show, for example
|
||
"Bot names: not in the Q1 module" (lead decision 63). Nothing is guessed.
|
||
- A task's list row can only say its current state came from a Vikunja read,
|
||
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 task page's "Who may write" section lists each field with the verbs
|
||
that write it and the writer each verb checks: pm, the assigned role or any
|
||
role (row 61, lead decision 113). Whether a write also needs a resolved
|
||
decision is the role's authority, on the Business page. If the table read
|
||
fails, the section says why and the rest of the page still shows.
|
||
- 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,
|
||
open sections and the copy status survive a refresh.
|
||
- A failed read keeps the last good answer for that read and says it is old,
|
||
with the time it was read. A refusal (403, `not-configured`, `no-bus-host`,
|
||
or row 54's `queue-refused`) never shows kept rows (row 53), and it drops
|
||
every kept read: the Inbox count and the palette's decisions, tasks and
|
||
queue rows go too, until a read succeeds.
|
||
A page never read, or refused, shows the failure instead,
|
||
titled "Bus refused the read" (403: the bus refused the Console's read, for
|
||
example `human-required` from a Console started inside an agent run), "No bus to read" (no system
|
||
config, or no bus host and no `--business`) or "The read failed".
|
||
|
||
## Agent state (row 59, #1548)
|
||
|
||
`#/agents` adds State and Attention columns. State is the board's mark
|
||
(● working, ◐ waiting, – idle) from the bus `agents` verb, with the number
|
||
of messages and since when for a working role. Attention shows
|
||
"Input needed:" with the first line of the role's first open decision,
|
||
linked to `#/inbox/<id>`, a blocking chip, and "+N more" to the Inbox when
|
||
there are others. The page says what each state means. It writes nothing;
|
||
the server passes the rows through unchanged. How the bus derives the state
|
||
and its limits: `packages/bus/README.md`, "Agent state".
|
||
|
||
## Board: held roles and signing out (row 95, #1589)
|
||
|
||
The Board (`#/`) opens with **Held roles**: each role a run holds on the
|
||
bus, from the same bus `agents` read as `#/agents`, including a claim with
|
||
no address ("no address"). Each row shows the role, its state and attention
|
||
(as in "Agent state"), the address, the holding run with its harness, and
|
||
since when. With send on (`--send`, "Message a role"), each row has a
|
||
Message action: it opens `#/inbox` with the send form addressed to that role
|
||
and the caret in the message. Without send there is no action, and the
|
||
section says so. A refused or failed first read says why and shows no roles
|
||
rather than a guess; a failed later read keeps the last roles under a stale
|
||
warning.
|
||
|
||
The machine scan's sessions (Waiting on you, Seen, All sessions and the
|
||
project list) sit behind **Show machine sessions**, off by default. While
|
||
it is off the page makes no `/api/board` request and polls nothing for it.
|
||
The choice is kept in this browser's local storage under
|
||
`mosaic-console-board` (`{"machine":true}`), like the appearance; where
|
||
storage is refused it starts off on each load. Turning it off drops the
|
||
board data and closes an open conversation.
|
||
|
||
Every Console fetch the page makes uses `redirect: 'manual'`. The Console
|
||
never redirects, so a redirect answer (`opaqueredirect`) is the sign-in
|
||
proxy's: its session has ended. The first one, on a board read, a bus read,
|
||
a page read or a send, signs the page out:
|
||
|
||
- One line says "Signed out. Reload the page to sign in." No NetworkError,
|
||
failure banner or stale warning is shown for it.
|
||
- Everything drawn from a Console read is removed (the board, the held
|
||
roles, the bus views, an open conversation and the inspector), because
|
||
none of it can be refreshed. A read that was in flight is dropped when it
|
||
lands.
|
||
- Refresh and Pause are disabled; nothing polls and no route change reads.
|
||
- The send form, if open, keeps its draft and receipt so the text isn't
|
||
lost; a further Send is refused in the page ("signed out"). A board
|
||
Reply that meets the redirect is not sent, and its inspector closes with
|
||
the rest of the board, so its draft isn't shown again.
|
||
- Reloading the page goes through the sign-in proxy again.
|
||
|
||
How long a sign-in lasts is the edge's setting (ops-01), not the Console's.
|
||
|
||
## Shell (row 53, #1542)
|
||
|
||
The Console is restyled to `docs/design` (`IMPLEMENTING.md`). It is a restyle
|
||
only: no new data, route or write.
|
||
|
||
- `src/public/tokens.css` and `src/public/icons.svg` are byte copies of
|
||
`docs/design/tokens.css` and `docs/design/icons.svg`; `tests/shell.test.mjs`
|
||
fails if either differs. Edit `docs/design`, rebuild there, then copy.
|
||
- Colours come only from `tokens.css`, selected by `data-palette` and
|
||
`data-mode` on `<html>`. The page sets no colour inline. Appearance System
|
||
removes `data-mode`, so the OS light or dark setting applies; it is the
|
||
default, with Harbor. The choice is kept under `mosaic-console-appearance`.
|
||
- `shell.css` loads last and holds the restyle: radii `--r` 6px and `--r-lg`
|
||
10px, the type scale, tabular numbers, the command bar, section list, dense
|
||
table, inspector, status marks, class chips, toast, command palette and
|
||
not-found page. The earlier CSS files are unchanged.
|
||
- JetBrains Mono 400, 500 and 700 come from the official JetBrains release
|
||
(v2.304); `assets/fonts/jetbrains-mono-sources.txt` has the URL and sha256
|
||
of the archive and each file, and `jetbrains-mono-OFL.txt` the licence.
|
||
Manrope has no 800 file here, so 800 headings render at 700.
|
||
- Ctrl+K (or the search field) opens the palette over views, inbox decisions,
|
||
tasks and (row 54) queue rows from the last queue read; arrows move, Enter opens, Esc closes and returns focus. Arrow
|
||
keys also move between task links in the bus tables.
|
||
- Copy reports in a toast. If the clipboard is unavailable, the toast stays
|
||
until dismissed and the command is selected for Ctrl+C. When no toast shows,
|
||
the toast is an empty live region, visually hidden but still in the
|
||
accessibility tree, so the first copy is announced.
|
||
- The command bar shows, in UTC, when the board was scanned and the bus read,
|
||
and says when the last attempt failed. A board refusal drops the last scan
|
||
and everything drawn from it: the cards, the table, the inspector, the
|
||
project filter and tree, the counts, the footer and the scan line all say
|
||
the board was not read. A failed read with no scan held, after a refusal or
|
||
before the first scan, shows the board empty and says the read failed; it
|
||
never brings back rows from a refused scan. A refusal also closes an open
|
||
conversation and clears its history. If that removes what had focus, focus
|
||
moves to the page.
|
||
- From 760px the footer is a one-line status bar and the side column fits
|
||
between it and the command bar. Below 760px the section list is one strip
|
||
that scrolls inside itself.
|
||
|
||
## Queue, Business and Settings (row 54, #1543)
|
||
|
||
Three read-only views, in the section list and the palette. Each has the
|
||
same five states as the bus views: loading (the skeleton names the source),
|
||
normal, empty, a failed read over kept data (labelled stale, with its time),
|
||
and a refusal that shows nothing kept. The command bar names the source:
|
||
"Queue read …", "Business not read: refused". A refusal shows the code in
|
||
mono and the module's own message, redacted by the server.
|
||
|
||
- `#/queue` lists every row with its state, owner, reviewers, brief and last
|
||
update, filterable by state. `#/queue/<id>` adds the gate, issues, claim,
|
||
`after`, note and review rounds. Rows move only with
|
||
`scripts/mosaic queue move` in a terminal; the row page shows that command
|
||
with a Copy button and has no button that moves a row. The brief is shown
|
||
as its path and anchor, because Console serves no repository files.
|
||
- `#/business` shows the business named by `--business`, else the running
|
||
bus host's business: the human, the arbiters as instance and kind ("pm
|
||
(delivery), cto (technical)"), projects, launch rules, the vars
|
||
on an allowlist, each role instance with its holder and harness, the
|
||
authority of each instance for each action as the business package
|
||
classifies it (`resolveInstance`, `classify`), and credentials as
|
||
metadata only: service, account, role, expiry or rotate-by date, and a
|
||
state worked out from the date alone. A missing or invalid business file
|
||
is a `not-configured` refusal with the business module's own text.
|
||
- `#/settings` shows appearance (the row 53 control, kept in this browser),
|
||
the notifier binding by name only, and about: the Console's loopback
|
||
address, `RELEASE`, the business and the board URL. A Settings refusal
|
||
still shows appearance, because it needs no read.
|
||
|
||
Console never reads a token, a token file or a credential variable, and no
|
||
response, page or log carries one, a file path or a Discord channel, guild or
|
||
user id. The server redacts every message and string field before it answers
|
||
(`redactor` in `src/reads.mjs`): the config directory becomes `<config>`, the
|
||
data root `<dataRoot>`, any other absolute or `~/` path `<path>`, a 17 to 20
|
||
digit run `<id>`, and a JSON parse error's quote of the file is dropped. The
|
||
startup log goes through the same redactor. A path starts at the beginning
|
||
of the text or after a space, an opening bracket, a quote, `=`, `,`, `<` or
|
||
`:`, so `file:/x` and `file:///x` are redacted and `https://host/x` is not.
|
||
Known gap: a path stops at the first space, so a path containing a space is
|
||
redacted only up to that space and the rest shows. A bare `~` or `~user/`
|
||
is not treated as a path, and neither is a relative path.
|
||
|
||
## Spec and docs editor (row 148, #1642)
|
||
|
||
The Docs page (`#/docs`) lists, for each project of the business, spec/north-star.md and
|
||
the Markdown files directly under docs/. A file opens at `#/docs/<project>/<path>` with
|
||
its current commit. The page is a `<textarea>` that holds the Markdown source and a
|
||
preview beside it. Save writes exactly what was typed, as one local commit authored by
|
||
the business's human; the page then shows the new commit. Nothing is pushed.
|
||
|
||
Each save carries the commit the editor opened. If the file's last commit has moved on
|
||
since, the save refuses `stale-base`, nothing is written, and the page says to reload the
|
||
file and make the edit again. The draft stays in the box. Reload discards the draft and
|
||
reads the file again. A draft survives a refresh and a visit to another page; a file
|
||
with no unsaved change is read again when it is opened. The 10 s refresh is off on the
|
||
editor so it can't touch a draft. A file with a NUL character never opens: the project's
|
||
read refuses it (`project-read-content`), so the page has no draft to show. A file
|
||
with CRLF line ends saves with LF, and the page says so. The page does not create files
|
||
or open any other path. A tree with pending changes refuses the save (`project-dirty`).
|
||
|
||
The preview is built by `markdown-preview.js` with `createElement` and `textContent`
|
||
only. It never calls `innerHTML`, `outerHTML`, `insertAdjacentHTML` or `document.write`
|
||
(a test reads the file for all four), so it needs no sanitizer. Headings, paragraphs,
|
||
lists, block quotes, code blocks, inline code, emphasis and links render. Everything
|
||
else, raw HTML included, shows as the source text. A link is made only for an `http:`,
|
||
`https:` or relative target, with `rel="noopener noreferrer"`, and the target is shown
|
||
beside the link text. A target with a space, a control or bidirectional character, a
|
||
backslash or a leading `//` stays text. An image shows its alt text and URL and is
|
||
never loaded. Each render has a look-ahead budget of 32 characters examined per
|
||
character of text (at least 1 Mi): an opener with no closer searches at most 2000
|
||
characters, and the budget caps the total over a whole file. Once it is spent, every
|
||
later link, image, code span and emphasis shows as its source text, so a hostile file
|
||
can slow the editor by a fixed multiple of its length and no more. The preview vendors
|
||
nothing and the Content-Security-Policy is unchanged; the choice against a bundled editor
|
||
is recorded in `agents/researcher/work/queue-148/LIBRARY.md`. Row 154 later added one,
|
||
for the page's default view; this textarea and preview are now its Source view (see
|
||
"Visual editor").
|
||
|
||
Routes: GET `/api/project-files?project=` and GET `/api/project-file?project=&path=`
|
||
run the human transport like the other reads (400 for a bad query, 503 with no bus, 502
|
||
for an unusable answer). POST `/api/project-file` exists only under `--send` and has the
|
||
Settings boundary: the signed-in user's header, the exact `https://<public host>`
|
||
Origin, `Sec-Fetch-Site: same-origin`, a JSON body of at most 1 MiB with exactly the
|
||
string keys `project`, `path`, `base` and `content`. The 16 KiB limit of the message
|
||
route is unchanged. GET `/api/config` now also returns `edit`, true when saving is
|
||
available; without it the source is read-only and Save is off. An answer is
|
||
`{path, commit, at}`, a refusal `{error, message, sent:false}`, and an answer that is
|
||
lost or unusable is `sent:"unknown"`: the page keeps the draft, says the save may have
|
||
gone through, and sends nothing again. The log line names the project, path and
|
||
outcome, never the content.
|
||
|
||
Tests: `project-file.test.mjs` (the routes), `markdown-preview.test.mjs` (the sink
|
||
check, each element, the scheme rules, the look-ahead budget) and `docs-browser.test.mjs`
|
||
(markdown with `<script>`, `onerror` and `javascript:` runs nothing; the draft, each
|
||
save receipt, the stale-base message, sign-out and read-only mode). Those tests open the
|
||
Source view first (`source()` in the test file); their assertions are unchanged.
|
||
|
||
## Visual editor (row 154, #1648, Q168)
|
||
|
||
A file opens in a visual editor (ProseMirror) when the editor can write the file's own
|
||
bytes back with no edit; otherwise it opens in the Source view with a note. Visual and
|
||
Source are two buttons above the page. They edit one draft: a change in either shows in
|
||
the other, and the read, save, stale-base and read-only rules above are the same for
|
||
both. The editor is a view over the same `{project, path, base, content}` save.
|
||
|
||
The editor is built from reviewable source, not a prebuilt bundle:
|
||
|
||
- `third_party/editor/` is unmodified upstream TypeScript source of 19 packages
|
||
(ProseMirror, markdown-it and their dependencies), copied byte for byte from tagged
|
||
trees. Each package keeps its own license file and `package.json` beside its source;
|
||
`NOTICE` lists them, and `PROVENANCE.json` records the origin, tag, commit, upstream
|
||
tree and the git blob and sha256 of every file, the tree hash, and `localChanges`
|
||
(empty). `agents/researcher/work/queue-154/repro/import-sources.mjs` makes the
|
||
directory.
|
||
- `editor/` is the editor's own code: `model.mjs` (schema, parse, serialise), `view.mjs`
|
||
(toolbar, link bar, keys), `entry.mjs`, `editor.css` and `build.mjs`.
|
||
- `node editor/build.mjs` (`npm run build:editor`) bundles them with esbuild 0.28.2,
|
||
asserted exact, into `src/public/editor.js` and `editor.css`, one unminified script
|
||
with no source map, and writes `editor/BUILD.json` with the inputs and the sha256 of
|
||
each output. Two builds from different absolute paths give identical bytes. The built
|
||
files are committed, so a pinned copy needs no build step and no esbuild.
|
||
|
||
Rules the editor keeps from the preview: no raw HTML runs (it is kept as source text in a
|
||
tinted block), a link is made only for a target `window.MosaicMarkdown.linkTarget`
|
||
accepts (`http:`, `https:`, relative) and gets `rel="noopener noreferrer"`, and an image
|
||
is text (`[image: alt] url`), never an `<img>`, on the load path and on paste. The editor
|
||
adds no `innerHTML`-family call (a test reads `editor/*.mjs` for them; the bundle's
|
||
upstream code uses the DOM APIs and is not scanned) and no `eval`. The Content-Security-
|
||
Policy is unchanged: `editor.js` and `editor.css` are served from `'self'`.
|
||
|
||
Open and save with no edit writes the same bytes: the editor keeps each top-level block's
|
||
source and writes an untouched block back as it was. A block you change is written in
|
||
the editor's own form (`-` bullets, soft breaks as new lines, reference links as inline
|
||
links, `<` and `&` escaped). Tables, HTML blocks, front matter, indented code, setext
|
||
headings, strikethrough, link reference definitions, and a block that holds a link target
|
||
the gate refuses are kept as source blocks and not edited visually. Known limits: CRLF
|
||
saves as LF (as in row 148); an unedited file keeps Save off; an empty paragraph is
|
||
dropped when its neighbour is edited; copying an image inside the editor does not keep
|
||
it; two adjacent lists may merge when the file is read again; a redraw or a switch
|
||
between Visual and Source starts a new undo history.
|
||
|
||
Routes: GET `/editor.js` and GET `/editor.css`, static like the other public files.
|
||
|
||
Tests: `editor.test.mjs` (routes, the round trip over the goals review, a document with
|
||
every element and every repository doc, the source rules), `editor-provenance.test.mjs`
|
||
(the imported tree against `PROVENANCE.json` and its recorded local changes, license
|
||
files, the build record, and a rebuild that skips with a reason when esbuild 0.28.2 is
|
||
not installed) and `editor-browser.test.mjs` (hostile content on the load and the paste
|
||
path, the Console's CSP, edit and save, the link bar, read-only).
|
||
|
||
## Assistant settings (row 155, #1649)
|
||
|
||
Jason's ruling Q169 lets the human configure the assistant and its seat name from the
|
||
Console. Settings shows an Assistant section with one form per assistant instance, the
|
||
ones the Settings read lists under `assistants`:
|
||
|
||
- the seat name, read through `/api/bus/names`, with Save name. If the read fails the
|
||
section says the current name could not be read, and the field starts empty;
|
||
- `definition`, `vars` and `tracker` as text, the last two as JSON, with Save assistant
|
||
settings. It sends only the fields that changed, under the revision the page read, as
|
||
POST `/api/settings` with `assistants` (the business README's "Assistant settings"). An
|
||
empty tracker sends `null`, which removes it. `vars` and `tracker` that aren't JSON
|
||
objects are refused in the page and nothing is sent;
|
||
- Stop its runs, which ends every run of that assistant in the threads the human made.
|
||
|
||
Without `--send` every control is off. A refusal shows the bus's message and keeps the
|
||
draft; a lost answer says the change may have gone through. Nothing is sent again on its
|
||
own.
|
||
|
||
Routes: POST `/api/name` with exactly the string keys `instance` and `name` calls
|
||
`name.set`, and POST `/api/assistant-stop` with exactly `instance` calls `assistant.stop`.
|
||
Both exist only under `--send`, go through the human transport, and have the Settings
|
||
boundary: the signed-in user's header (401 without it), the exact `https://<public host>`
|
||
Origin and `Sec-Fetch-Site: same-origin` (403), and a JSON object of at most 4 KiB (400).
|
||
Any other method is 405. An answer is the bus's result with `at`. A bus refusal is
|
||
`{error, message, sent:false}` with the code's status, `not-held` and `name-taken` 409 and
|
||
`human-required` 403. A lost or unusable answer is 503 with `sent:"unknown"`. The log line
|
||
names the route, the user and the instance, and the outcome: `done` or the code. A Console
|
||
started inside an agent run gets `human-required` from the broker.
|
||
|
||
## Role instances (row 165, #1662)
|
||
|
||
Q171 (REQ-ROLE-3) lets the human add a role instance and change one's definition, vars and
|
||
launch membership from the Console. Settings shows a Role instances section, read through
|
||
`/api/bus/instances`: a table of each instance with its definition, whether it is launched
|
||
(or is the launcher) and its vars, then two forms.
|
||
|
||
- Add an instance: a name, a definition from the ones the writer accepts, vars as JSON and,
|
||
when the file has a launch block, a Launch it box.
|
||
- Change an instance: any instance except an assistant, which changes in the Assistant
|
||
section. The definition, the vars (replacing them whole) and launch can change; a field
|
||
left as Keep or empty is not sent.
|
||
|
||
Definitions that declare credentials are not offered. The section names them, with their
|
||
services, as hand edits of the business file, because the Console binds no credentials.
|
||
Each form sends one POST `/api/instances` under the revision the page read. When the answer
|
||
says a bus restart is needed, the receipt says so and that the Console does not restart it;
|
||
the page has no restart control. Without `--send` every control is off. A
|
||
refusal shows the code and the reason and keeps the draft; a lost answer says to read the
|
||
instances again. Nothing is sent again on its own.
|
||
|
||
Route: POST `/api/instances` exists only under `--send` and calls the human-only
|
||
`instances.set` through the human transport. It has the Settings boundary: the signed-in
|
||
user's header (401 without it), the exact `https://<public host>` Origin and
|
||
`Sec-Fetch-Site: same-origin` (403), and a JSON object of at most 16 KiB (400). It takes
|
||
exactly `revision` (64 hex), `instance`, `op` (`add` or `change`) and any of `definition`,
|
||
`vars` (an object) and `launch` (a boolean); any other key, `holder`, `credentials` and
|
||
`tracker` included, is 400 before the bus. Any other method is 405. An answer is the bus's
|
||
result with `at`. The bus sends a refusal's code alone, so the route adds the reason:
|
||
`{error, message, sent:false}`, where the message is the code, the reason and "Nothing
|
||
changed." `revision-conflict` and `organization-instance-exists` are 409,
|
||
`human-required` 403 and the other refusals 400. A lost or unusable answer is 503 with
|
||
`sent:"unknown"`. The log line names the user, the instance and the outcome (`done
|
||
restart=true|false` or the code), never the vars.
|
||
|
||
## Routing (row 65, #1558, mos-0840)
|
||
|
||
Settings shows a Routing section, read through `/api/bus/routes`: a table
|
||
with one row per instance in the business file and six selects, harness,
|
||
model, thinking, fallback harness, fallback model and fallback thinking.
|
||
It replaces the Agent limits form's old Role models fieldset, which set
|
||
only the model. The harness cells offer the harnesses `plugins.harness`
|
||
switches on, the model cells the `launch.models` keys, and each thinking
|
||
cell the levels its row's harness applies. A value the file holds that
|
||
isn't offered stays selected and says why: "(not switched on)", "(not in
|
||
the model table)" or "(not applied by <harness>)". A harness change redraws
|
||
the row, so its thinking cell offers the new harness's levels. With no
|
||
harness switched on, the section says every launch refuses `harness-off`
|
||
and that `mosaic plugins set` switches one on; the page doesn't change
|
||
`plugins`.
|
||
|
||
A fallback is set or cleared as a whole. Save sends one POST `/api/routes`
|
||
under the revision the page read, with only the changed rows. A fallback
|
||
whose three cells are `none` is sent as null, which clears it; one with
|
||
some cells set goes as it is, and the bus refuses it as partial. A
|
||
submit with nothing changed says "Nothing changed." and sends nothing. A
|
||
save says which instances it saved and that the next launch of each uses
|
||
the new route; running sessions keep theirs. A refusal shows the code and
|
||
the reason and keeps the draft. A lost answer says the outcome is unknown
|
||
and to read the routes again. "Read current routes and discard draft"
|
||
drops the draft. Without `--send` every select and Save are off. Nothing
|
||
is sent again on its own.
|
||
|
||
Routes:
|
||
|
||
- GET `/api/bus/routes` calls the human-only `routes` through the human
|
||
transport. A query string is 400, no bus 503, and an answer without a
|
||
64-hex revision, the `harnesses` and `models` lists and the `levels` and
|
||
`instances` objects 502.
|
||
- POST `/api/routes` exists only under `--send` and calls the human-only
|
||
`routes.set`. It has the Settings boundary: the signed-in user's header
|
||
(401 without it), the exact `https://<public host>` Origin and
|
||
`Sec-Fetch-Site: same-origin` (403), and a JSON object of at most 16 KiB
|
||
(400). It takes exactly `revision` (64 hex) and a non-empty `routes`
|
||
object; any other key is 400 before the bus. Any other method is 405.
|
||
The bus sends a refusal's code alone, so the route adds the reason:
|
||
`{error, message, sent:false}`, where the message is the code, the reason
|
||
and "Nothing changed." `revision-conflict` is 409, `human-required` 403,
|
||
and `harness-off`, `harness-refused`, `thinking-unsupported`,
|
||
`unknown-model`, `organization-partial-fallback`,
|
||
`organization-unknown-instance`, `organization-no-change` and
|
||
`invalid-request` 400. A lost or unusable answer is 503 with
|
||
`sent:"unknown"`. The log line names the user, the instances and the
|
||
outcome (`done` or the code), never the routes.
|
||
|
||
Neither route is in `organization.set`'s keys, so the chief of staff can't
|
||
reach them (`packages/business/README.md`, "Routes").
|
||
|
||
## Data and boundaries
|
||
|
||
GET `/api/board`, `/api/conversations` and `/api/conversation`, and POST
|
||
`/api/seen` and `/api/reply` proxy only those board paths. Only the two
|
||
conversation routes carry their query string; the board validates it. POST
|
||
bytes and upstream status/JSON are preserved. GET `/api/config` returns the
|
||
configured board URL for the page's error message. No scanner, registration,
|
||
session reader or transport is implemented here. The session reader is
|
||
`packages/conversation`, served by the board.
|
||
|
||
GET `/api/bus/inbox`, `/api/bus/tasks`, `/api/bus/agents`,
|
||
`/api/bus/trail?subject=<id>`, `/api/bus/decisions?list=roles|recent`,
|
||
`/api/bus/credentials`, `/api/bus/names` (row 155, each instance's seat name),
|
||
`/api/bus/instances` (row 165, the role instances and the definitions `add` accepts),
|
||
`/api/bus/replies?id=<message id>` and
|
||
`/api/bus/messages?limit=50&after=<message id>` are the bus
|
||
reads (`src/bus.mjs`). Any other method is 405. The subject and the id must
|
||
match the broker's identifier rule, and the list must be `roles` or
|
||
`recent`, else 400. `replies` (row 81, #1575) is the messages whose
|
||
`in_reply_to` is that id, for the Sent receipt; the bus refuses an id that
|
||
names no message (`message-not-found`, 400). `decisions` (row 57, #1546) is for the Inbox tabs "Routed to roles" and
|
||
"Recent"; "Human and reader paths" in `packages/bus/README.md` has what each
|
||
list holds. `credentials` (row 60, #1549) is the bus host's credential
|
||
status: service, account, role, date and state for each credential it
|
||
loaded, never a token, file path or variable name. No page uses it yet; the
|
||
Business view still shows the business file's metadata.
|
||
Each read runs the S4 human transport (`packages/bus/src/human-cli.mjs`) as
|
||
a child process, so a slow read doesn't stall the server. The bus checks
|
||
that child's ancestry, which ends at the Console process; it never sees the
|
||
browser or any other client of the port. So while the Console runs,
|
||
anything that can connect to its port (7330 by default) reads what a reader
|
||
capability reads ("Human and reader paths" in `packages/bus/README.md`):
|
||
the inbox, tasks, agents, trails, the two decision lists and the credential
|
||
status. That includes
|
||
a T3 seat using `curl` and a managed S6 session, since S6 doesn't confine
|
||
the network. Without `--send` no route
|
||
writes (Q4), so the exposure is reads only; with it, see "Message a role". Slice 1 doesn't change this
|
||
(Filbert F1 on #1522). Answers are `{ rows, at }`; failures are `{ error, message }`
|
||
with 403 for a refusal, 503 for no bus or no answer, and 502 for an answer
|
||
that can't be used. The bus needs the system config
|
||
(`~/.config/mosaic-dev/config.json`); without it the board still serves and
|
||
the bus routes answer `not-configured`.
|
||
|
||
GET `/api/queue`, `/api/queue/<id>`, `/api/business` and `/api/settings` are
|
||
row 54's reads (`src/reads.mjs`), under the same Host, Origin and CSP rules.
|
||
Any other method is 405, and a queue id that is not a positive integer is
|
||
400. The queue is read in a child process (`src/queue-read.mjs`) through
|
||
`rows()` from `packages/queue/src/store.mjs` (lead decision 83): the same
|
||
read as `queue list`, with no write and no network. Like `list`, it takes
|
||
the queue lock only to recheck a disagreement before reporting it. The child
|
||
imports the store inside its `try`, so a store that fails to load reports
|
||
"the queue read failed". Only the store's exit 2 (invalid data or refused)
|
||
forwards the child's text, as `queue-refused`; any other exit is the fixed
|
||
`read-failed` message "the queue read failed (exit N)", because that stderr
|
||
may be Node's own, with a `file://` path and a stack. The business
|
||
read uses `packages/business` in process. No read writes, calls out or adds a
|
||
dependency. Answers carry `at`; failures are `{ error, message }` with 503
|
||
for `not-configured`, `no-bus-host`, `queue-refused` and `read-failed`, 404
|
||
for a row that isn't there, 400 for a bad address and 502 otherwise. Settings
|
||
answers 200 even without a system config, so appearance always works; its
|
||
notifier part then says `not-configured`.
|
||
|
||
GET `/api/task-fields` is row 61's read (#1550): the task field table,
|
||
`packages/tasks/fields.json`, loaded and checked by `loadFields()` on each
|
||
request. It is repository data, so it answers without `--business` or a
|
||
system config. The answer is the table itself, with no `at`. A table that
|
||
fails its check is refused whole with 503 `read-failed` and the loader's
|
||
message, which names the file or the key at fault, never a path. Any other method is 405.
|
||
|
||
Console's shared CSS, Console CSS, brand.js and local Manrope fonts were copied
|
||
unchanged from `agents/dewey/work/wui/`. Font license and source URLs accompany
|
||
the files under `src/public/assets/fonts/`. `live.css` contains the live-page
|
||
adaptations; original mockups and unrelated pending design work stay untouched.
|
||
Unsupported mockup fields and controls are listed in `docs/plans/DEFERRED.md`.
|
||
The source tree is a project filter; no invented workspace registration tree
|
||
or mockup session hierarchy is presented as live data.
|
||
|
||
## Verify
|
||
|
||
```sh
|
||
node --test packages/webui/tests/
|
||
node --test packages/control-board/tests/ packages/seat/tests/ packages/ledger/tests/ packages/mosaic/tests/
|
||
WEBUI_EVIDENCE=/tmp/webui-evidence node --test packages/webui/tests/browser.test.mjs
|
||
WEBUI_EVIDENCE=/tmp/webui-evidence node --test packages/webui/tests/conversation.test.mjs
|
||
WEBUI_EVIDENCE=/tmp/webui-evidence node --test packages/webui/tests/bus-browser.test.mjs
|
||
WEBUI_EVIDENCE=/tmp/webui-evidence node --test packages/webui/tests/shell.test.mjs
|
||
node --test packages/webui/tests/reads.test.mjs packages/webui/tests/views.test.mjs
|
||
```
|
||
|
||
Node's test runner and installed `/usr/bin/chromium` are required. Set `CHROMIUM`
|
||
to another installed Chromium path. No npm download is needed. The copied CDP
|
||
helper starts a separate temporary browser profile and removes it on exit.
|
||
Tests use temporary board session/registration files and stub the board's
|
||
transport. They never send to real seats or read real session logs.
|
||
|
||
Browser tests exercise the rendered real board fixture, exact counts, project
|
||
filtering, keyboard return focus, Seen/Unsee, success/failure receipts, draft and
|
||
caret preservation, pending-send exclusion, storage denial, stale registration,
|
||
loading/empty/malformed/unreachable states and hostile text. Contrast is measured
|
||
on 330 rendered samples across ten palettes and three modes. Layouts are checked
|
||
at 320, 390, 768, 1440 and 2560px. Horizontal scrolling is intentional within the
|
||
dense table; the page itself must not overflow.
|
||
|
||
Conversation tests run the real board routes over temporary repository-layout
|
||
session files. They cover full-length answers, collapsed tools and thinking,
|
||
hostile content rendered inert, malformed-line and reconcile markers, forks, and
|
||
the return flow: a send from the view, a tool call and a delayed result while a
|
||
draft is typed, a peer message, a 4.5-million-character answer split into
|
||
continuation parts, and a relaunch mid-turn.
|
||
|
||
Row 95's tests (`board-held.test.mjs`) seed an in-process broker with one
|
||
claim that has an address and one that has none. They check the held roles
|
||
with the machine sessions off by default and no `/api/board` request, the
|
||
toggle on and off and its persistence across reloads, Message addressing
|
||
the send form to each role, no action without send, and that every fetch
|
||
asked for `redirect: 'manual'`. The tests record every 10 s poll timer the
|
||
page arms, filed by the script that armed it, and assert that none is armed
|
||
for the board while the toggle is off, even across Pause and Resume, and that
|
||
one is armed once it is on. Then a redirect answer on each of six
|
||
requests, the board read (`/api/board`), the `agents` read, `/api/conversations`,
|
||
`/api/seen`, `/api/queue` and the send: one line, no NetworkError, nothing
|
||
stale left, and no further request or new poll timer on a route change,
|
||
Refresh, the toggle, a forced Pause click or a poll tick, and the draft kept. In
|
||
that test the poll timers fire after 150 ms, so the check spans several
|
||
ticks. `browser-edge.test.mjs` fails in 5 s, and closes the browser and
|
||
servers, if the toggle makes no `/api/board` request. `send-browser.test.mjs`
|
||
also checks the sign-out after its redirect receipt and that a reload works
|
||
again. Since the held roles lead the Board,
|
||
the shell test's no-scroll check at 1440px uses a 1000px-tall window.
|
||
|
||
Bus tests (`bus.test.mjs`, `bus-browser.test.mjs`) run the routes and the
|
||
views against an in-process broker read through its reader session, so every
|
||
row has the broker's own shape. `humanCall` runs against a stub CLI script.
|
||
They cover each view, the gap labels, inert hostile text, Copy, the stale
|
||
fallback and each failure title, and that no route writes. They never start
|
||
the human CLI against a live bus.
|
||
|
||
Shell tests (`shell.test.mjs`) check the copies, the font hashes and the kept
|
||
strings, and in the browser at 400px: System mode under an emulated dark and
|
||
light OS, the empty, stale and refused states of the board and the bus views
|
||
(a failed first read drawn empty, a board refusal with the inspector open and a project picked, then a failed
|
||
read, a failed session list, and a refusal with a conversation open and where
|
||
focus lands after it), not found, the palette, the toast,
|
||
keyboard and visible focus, the section strip at 360 and 400px, no sideways
|
||
scroll, no script error and no request other than GET. A second test walks
|
||
the Board, Inbox, Tasks, Agents and a decision trail through loading (a held
|
||
read), normal, a failed read over kept rows, and a refusal by the bus
|
||
(`human-required`) and for want of one (`no-bus-host`). It then checks that a
|
||
refusal empties the palette and the Inbox count, from a view, from the
|
||
palette's own reads and from the board page, and that palette labels are text.
|
||
|
||
Row 54's tests share the seeded fixture in `reads-fixture.mjs`: a scratch
|
||
queue whose row note names an id and token paths shaped like real notes
|
||
(absolute, `~/` and after `file:`; the store refuses `<` in a note, so the
|
||
`<…>` case is in the redactor test), and a business,
|
||
notifier config and Discord binding that hold token files, an environment
|
||
variable name, an agent `model` var holding a path, and guild, channel, bot
|
||
and user ids. Row 9's After entry has the `{id, when}` shape the real queue
|
||
stores. `reads.test.mjs` checks each
|
||
route's body and status, the redactor, the credential state, methods and
|
||
Origin, and that no body carries any seeded value or the temporary directory;
|
||
that a `store.mjs` with a syntax error, or none, gives the fixed message with
|
||
no path; and that the child's timeout, output cap and non-2 exits hold, over a
|
||
fake child script.
|
||
`views.test.mjs` walks Queue, a queue row, Business and Settings through the
|
||
five states over a stub read, with each refusal code the view can meet, at
|
||
400px; checks that a refusal leaves no queue row in the palette, that no view
|
||
has a control but Copy and Read again, and that the row page shows the
|
||
`queue move` command, that the 10 s refresh keeps focus on a Settings mirror
|
||
select, and that the palette keeps its queue rows after its own inbox and tasks
|
||
reads answer; then, over the seeded fixture, that the Arbiters line reads the
|
||
business file's object, that neither the page nor
|
||
any response it read carries a seeded value, that a missing or invalid
|
||
business file shows the module's text, and that the startup log names no
|
||
config path.
|
||
|
||
`assistant.test.mjs` (row 155) checks both seat routes: the answer and the log line,
|
||
`--send` off, the Host, user, Origin and Fetch Metadata checks, GET, strict keys, the size
|
||
and content type, each refusal's status, the lost answer and the `names` read.
|
||
`settings-write.test.mjs` checks that an `assistants` patch reaches the human operation
|
||
unchanged and that the log names the user. `settings-form.test.mjs` walks the Assistant
|
||
section in a browser: off without `--send`, Nothing changed, only the changed field sent,
|
||
a client-side refusal, a refused post that keeps the draft with no retry, Save name and
|
||
Stop.
|
||
|
||
No root CI workflow is configured for this package. These local tests are not a
|
||
claim of CI, deployment, live-seat delivery or user acceptance.
|
||
|
||
## User test and rollback
|
||
|
||
Gate E belongs to Jason on Monday 2026-09-14. Use Console to find who is waiting,
|
||
open a registered repo seat, reply and see its next answer after a scan. Mark a
|
||
completion Seen and find it again in Seen. Record any reason to open the board's
|
||
own page or a repo seat terminal in DEFERRED.md. Pass is Jason's end-of-day
|
||
say-so, not a test result. Keep #1507 open pending that ruling.
|
||
|
||
Rollback requires only stopping the WebUI and using the unchanged board at 7331.
|
||
Revert the scoped WebUI commit to remove it; there is no data migration to undo.
|
||
Seen and reply actions already taken belong to the board and are not rolled back.
|
||
|
||
## Messages (row 94, #1588)
|
||
|
||
The Messages page shows every message addressed to the human, newest first,
|
||
including unsolicited messages and late replies. Each lists sender, class,
|
||
time, read state, literal body and a trail link. It polls at the existing
|
||
10-second cadence; pause and refresh work as on the other pages. Older
|
||
messages and Newest messages links page through the history. The nav count
|
||
is the whole feed's unread count, including messages beyond the visible
|
||
page. If that count cannot be read, it is hidden; a bus refusal clears kept
|
||
data as on the other pages. Reading this page marks nothing delivered or
|
||
read. `mosaic follow` or `mosaic talk --follow` prints messages and marks
|
||
them read in the terminal. The Console does not consume them.
|
||
|
||
The messages HTTP read accepts only limit (1–200, default 50) and after
|
||
(a message id cursor); malformed, duplicate or unknown query keys refuse
|
||
400 invalid-request. It returns rows plus total, unread, next and the
|
||
Console read time at. Older read routes retain their rows/at response.
|
||
The broker owns business scoping and cursor authorization.
|
||
|
||
`messages.test.mjs` exercises the route's paging/refusals and existing
|
||
route response shapes, then the page against a temporary broker with
|
||
a scheduled poll accelerated in the browser test. It proves literal
|
||
content, late unsolicited arrival, pagination, full unread count and
|
||
unchanged delivery receipts. No live bus data or service is used.
|
||
|
||
## Chat projects and threads (row 129, #1623)
|
||
|
||
Open **Chat** in the section navigation or command palette. The business project
|
||
catalog is shared; each project's thread list includes only the current human's
|
||
threads. Create a named project, optionally linked to a configured repository
|
||
key, then create a named thread. Names are set at creation; rename is not shipped.
|
||
No default project or thread is created implicitly. A thread has its own URL,
|
||
draft and conversation. This is separate from the Board's seat conversations.
|
||
|
||
The routes are `#/chat`, `#/chat/projects/UUID` and `#/chat/threads/UUID`.
|
||
The fixed `/api/chat/projects`, `/api/chat/threads` and
|
||
`/api/chat/threads/UUID/messages` endpoints list/create projects, list/create
|
||
threads and read/send messages. `POST /api/chat/threads/UUID/open` is the explicit
|
||
**Resume assistant** action. Reading a thread never opens it or acknowledges a
|
||
message. Sending uses `thread.send` with class REQUEST, as the human CLI does.
|
||
|
||
All four POST operations need `--send`, the named preview Host, the exact
|
||
single authenticated user header, the Console's HTTPS Origin, same-origin Fetch
|
||
Metadata and a strict JSON object within 16 KiB. The broker binds the business
|
||
human and enforces ownership. A same-business thread owned by someone else
|
||
returns 403 `thread-not-owned`; a missing/foreign thread returns 404. No browser
|
||
owner, business, capability, arbitrary bus verb or filesystem path is accepted.
|
||
Read-only mode disables creation, Resume and Send. Chat uses the existing human
|
||
transport; a later channel-module conversion has its own brief (sage-0724).
|
||
|
||
History starts at the oldest message, reads forward 100 at a time and retains a
|
||
bounded 200-message display window. **Read next messages** advances older
|
||
history; a full page means more may exist. Once caught up, polling uses the last
|
||
id and de-duplicates replies. An accepted receipt confirms the message only,
|
||
not that the assistant has answered. An uncertain result never retries: read
|
||
the list/history before explicitly trying again. Drafts are memory-only and
|
||
thread-scoped; sign-out clears private chat state. Bodies are inert text.
|