Skip to content
commission

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.

ANot a command5
BA control already exists beside it7
CA control is buildable — the endpoint already exists9
DOperations on the database file6

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 URL
  • Connect.tsx:118 — an issued token
  • Connect.tsx:230 — an MCP client configuration block
  • Operations.tsx:51 — the database path
  • Operations.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.

wherethe actthe route that already exists
Task.tsx:361tick a criterionPOST /tasks/:id/criteria/:n/check
domain.tsx:493satisfy a gate requirement…/criteria/:n/check, POST /tasks/:id/evidence
Record.tsx:156dispute a closure — reopen itPOST /tasks/:id/transition
Notifications.tsx:450change delivery preferencesPATCH /notifications/preferences
Notifications.tsx:289retry a failed external writePOST /conflicts/:bindingId
domain.tsx:1633fix a sync problemPOST /projects/:slug/connection
domain.tsx:974an act gated behind a typed confirmationPOST /tasks/:id/transition
Compose.tsx:1049use the field a derived fact points atPATCH /tasks/:id
Task.tsx:673the 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 /backup exists and returns the whole estate as a JSON document with a content-disposition attachment header — a browser downloads it on a click. So the operations page renders commission backup as 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:

  1. tick a criterion, and satisfy a gate requirement (Task.tsx, domain.tsx)
  2. the identity chooser — it gates whether a person can use any of the rest
  3. the backup link
  4. dispute, notification preferences, sync fix, retry a failed write
  5. copy buttons on the five values
  6. 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 button5
<AsCommand> — a receipt, closed, beside the control that did it12
<Command> — a command somebody is genuinely meant to run10
new controls that did not exist at all3

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.