Every place the surface hands a person a command
Scoping for M16 (relay-248). The owner’s framing, 17 August 2026:
this tool will be used by people who aren’t engineers so it needs to be almost as dead simple as talking to claude directly; there shouldn’t be a ton of back and forth between a terminal and the browser. We need this DEAD simple.
This enumerates every command the human application renders, says what act is behind each one, and classifies it: a control already exists, a control is buildable, or genuinely terminal-only, with the reason. It is deliberately a count rather than an impression, because “the surface assumes a terminal” is either a small problem or a rewrite and nobody could tell which.
The answer is that it is small. Nine acts need a control. Six are file operations on the machine the server runs on. Five are not commands at all.
The count
Twenty-seven renderings of gate-cmd, the class the surface uses for a command
or a value shown as code. They fall into four groups.
| A | Not a command | 5 |
| B | A control already exists beside it | 7 |
| C | A control is buildable — the endpoint already exists | 9 |
| D | Operations on the database file | 6 |
The API has 57 write routes. That number is why group C is easy: almost everything a person is told to type is already reachable over HTTP, and the control simply was never built.
A — not a command (5)
Values shown in a code style because they are literals somebody copies, not instructions somebody runs. These are correct as they are; they only want a copy button.
Connect.tsx:96— the instance’s base URLConnect.tsx:118— an issued tokenConnect.tsx:230— an MCP client configuration blockOperations.tsx:51— the database pathOperations.tsx:211— the path an upgrade would write
B — a control already exists (7)
The command sits beside a button that does the same thing. This is the shape M16 wants everywhere: the control is the instruction, the command is the receipt. These want no change beyond moving the command behind an “as a command” disclosure.
Compose.tsx:516, Compose.tsx:532, Compose.tsx:788, Members.tsx:417,
Members.tsx:448, Policy.tsx:207, domain.tsx:532
C — buildable, and the endpoint is already there (9)
Each of these tells a person to type something the browser could already do.
| where | the act | the route that already exists |
|---|---|---|
Task.tsx:361 | tick a criterion | POST /tasks/:id/criteria/:n/check |
domain.tsx:493 | satisfy a gate requirement | …/criteria/:n/check, POST /tasks/:id/evidence |
Record.tsx:156 | dispute a closure — reopen it | POST /tasks/:id/transition |
Notifications.tsx:450 | change delivery preferences | PATCH /notifications/preferences |
Notifications.tsx:289 | retry a failed external write | POST /conflicts/:bindingId |
domain.tsx:1633 | fix a sync problem | POST /projects/:slug/connection |
domain.tsx:974 | an act gated behind a typed confirmation | POST /tasks/:id/transition |
Compose.tsx:1049 | use the field a derived fact points at | PATCH /tasks/:id |
Task.tsx:673 | the handoff block — what an agent would run next | — |
Task.tsx:361 is the one to lead with. “What has to be true” is the central
list on the task page and the product’s whole argument — completion is verified,
not asserted. Every unticked criterion renders a ○, which is a <span>, and
underneath it commission check relay-244 0. There is no control. A person
reading their own task list is shown the exact thing standing between them and
done, and handed a string they cannot run.
The last row is different from the rest: the handoff block is for an agent, and a person reading it is reading over the agent’s shoulder. It belongs behind a disclosure rather than being turned into buttons.
D — the database file (6)
Operations.tsx renders backup, restore and upgrade as commands: lines 75, 114,
162, 213, 232, 234.
These are not the same as group C and should not be swept in with it.
- Backup is already reachable and the page does not say so.
GET /backupexists and returns the whole estate as a JSON document with acontent-dispositionattachment header — a browser downloads it on a click. So the operations page renderscommission backupas a command next to a capability it already has. This is the cheapest win on the list: one link. (It is not the same artefact as the CLI’s backup, which copies the SQLite file in place on the server’s disk. Both are real backups; they restore by different routes, and whichever the button offers has to say which it is.) - Restore and upgrade are not. Both replace the database the running server has open, and the safe procedure stops the server first — which is not something the server can do to itself on behalf of a browser tab. A button that says “restore” and cannot honestly finish the job is worse than the command.
The honest shape here is probably: a backup button, and restore/upgrade staying commands with much better surrounding words about why they are commands.
The identity problem
Not a command rendering, and the one a person actually hits first. It is what
happened during the session that produced this document: the owner asked to look
at his own estate, and every judgement control was inert with the notice “claude
is an agent and may not approve or verify work”. Fixing it needed an agent to
stop the server and restart it with COMMISSION_ACTOR=blaze.
The mechanism is in src/server/mode.ts. authMode defaults to open, and its
comment defends that well:
A single-user install has one identity and no login, and making everyone run an account system to see their own board would be a tax on the people Commission is easiest for.
That reasoning is right, and it collides with the product’s own deployment model. Commission’s premise is that agents operate it and humans are asked only for judgment — so the process is very often started by an agent, which means the human’s browser inherits the agent’s identity and correctly refuses them every act that is theirs to perform. The two designs are individually sound and meet badly.
session mode exists, and web.tsx already serves the whole identity surface —
sign in, redeem a licence, authorise a device, accept an invite, sign out. So the
capability is built. What is missing is anything between “no identity at all” and
“run an account system”.
What signing in as yourself would take. In open mode there is no
authentication and therefore no security boundary to honour: the question is not
who may you be but which of the declared actors are you sitting as. The
actors are already declared in .commission.json and already carry kind, so
the smallest honest fix is a chooser in the account footer — pick a declared
human, stored per browser, used as the acting actor for that session. It is a
preference, not a credential, and it should say so plainly rather than looking
like a login. Anything stronger is session mode, which already exists.
What this means for M16
Nine controls to build, one of which — ticking a criterion — is on the page the product is most proud of. One download link for a backup that already works. One identity chooser. Five copy buttons. Two commands that stay commands (restore and upgrade) and want better words rather than buttons.
That is a milestone, not a rewrite, and the reason is the 57 write routes plus
GET /backup: the browser has been able to do nearly all of this the whole
time, and nobody wired the controls to it. The surface is not
terminal-dependent by design. It is terminal-dependent by omission, which is a
much better problem to have.
Suggested order, cheapest and most-seen first:
- tick a criterion, and satisfy a gate requirement (
Task.tsx,domain.tsx) - the identity chooser — it gates whether a person can use any of the rest
- the backup link
- dispute, notification preferences, sync fix, retry a failed write
- copy buttons on the five values
- move every surviving command behind an “as a command” disclosure
What actually happened
Written after the work, because three of the classifications above turned out to be wrong and the corrections are the useful part.
The backup link already existed. The doc calls it “the cheapest win on the
list: one link”, and Operations.tsx was already rendering
<a href={backup.download} download> beside the command. What was missing was
not the control but the distinction: the button hands the browser the whole
estate as JSON and commission backup copies the SQLite file in place, and the
page said nothing about which was which. It says so now, in the disclosure.
“Retry a failed external write” had no route. The table above names POST /conflicts/:bindingId, which resolves a divergence — a different act. The
actual remedy core composes is commission sync --project x --push, and
nothing serves it. It stays a command, and the reason is not that it was hard:
an outbound write has to be attributable to whoever made it, and a queue
drained by a button on a page anybody with the URL can press is a write nobody
can put a name to an hour later. That is the same decision startServer’s
background loop already makes — it pulls and never pushes.
“Fix a sync problem” did become a control, but not through
POST /projects/:slug/connection. Three of core’s four fix sentences are
honestly not this surface’s — replacing a credential happens in the environment
the server runs in, pushing writes to somebody else’s system, and a
disagreement is settled by deciding rather than by re-reading. The fourth is
commission sync --project x, a pull, and that is now POST /projects/:slug/sync sharing one observeFor with the background loop.
The identity chooser needed a filter nobody predicted. This estate declares
33 actors and 32 of them are agents named claude:task-019, because that is how
parallel work gets distinct identities. A chooser listing all of them is 33 rows
of which one is ever the answer. It offers declared HUMANS, plus the way back to
whoever started the server.
One thing outside the count. pr, test and rollback are column values,
and the gate said them in the one place the product asks a person for something.
The phrase table existed in inbox.ts and the gate could not reach it. It now
lives in kinds.ts: label is the phrase, ref is the key, and the refusal
composes its command from the second while the checklist says the first.
What the surface renders now
Counted from the source rather than from the plan, because the plan was wrong three times above.
<Value> — a literal with a copy button | 5 |
<AsCommand> — a receipt, closed, beside the control that did it | 12 |
<Command> — a command somebody is genuinely meant to run | 10 |
| new controls that did not exist at all | 3 |
The three: ticking a criterion (Tick), satisfying a gate requirement inline,
and Check it now on the sync panel.
<Command> is the largest number and that is not a failure of the milestone.
Most of them are on the operations page, where restore and upgrade are honestly
commands and each step of the procedure is one; the rest are a refusal’s own
prompt — which is the one command that must stay visible, because a refusal’s
command is the thing that has NOT happened yet.
.gate-cmd — the class all twenty-seven used — is deleted rather than
deprecated, and tests/acts-in-the-browser.test.ts fails if it comes back.
This page is docs/design/terminal-in-the-browser.md in the Commission
repository, rendered in place — the site keeps no copy of it. The repository is private, so there is no edit link to follow.