MoriMori
Mori

The Board & workers

A durable Kanban board that Mori reads and writes — where dragging a card into Ready is the approval that lets a worker run it.

What the Board is

The Board is Mori's durable task queue, rendered as a Kanban. Every card is a real task persisted locally in board.json under the app's user-data folder — nothing lives in the cloud, and the store is capped at the most recent 500 cards. Cards arrive from four places: you add one directly on the Board, Mori's brain files one with create_task, a goal spawns steps, or an outside agent submits one through MCP intake.

The Board and the brain share one store. Moves you make by dragging are visible to the model through list_tasks, and cards the model files appear on the Board within seconds (the page polls every 4 s; the Refresh button forces it). Cards carry a title, optional detail, a priority (low | normal | high | urgent), an optional goal link, the assigned worker, the latest action line, any blocker, and the final result.

Clicking a card's circle icon completes it in one step; clicking the circle on a Done or Archived card reopens it into Ready. Archived cards are hidden unless you toggle the Archived filter.

Lanes

Ten lanes, each with a precise meaning:

  • Triage (inbox) — raw ideas, not yet scoped. The default landing lane for everything the model or an outside agent files.
  • Planned (planned) — scoped, waiting on dependencies or on you.
  • Ready (ready) — approved by you and awaiting a worker. Human-only entry (see below).
  • In Progress (running) — claimed by a worker, currently executing.
  • Waiting · Mori (waiting_mori) — blocked on something Mori still has to do.
  • Waiting · You (waiting_user) — blocked on your input.
  • Review (review) — the worker finished; the result is on the card awaiting your sign-off.
  • Done (completed), Failed (failed), Archived (archived) — terminal lanes; Archived is hidden by default.

An upsert with an unknown lane name is coerced to inbox, so malformed writes can never invent a lane or skip triage.

Columns, WIP limits, and views

The ten lanes are fixed in the backend — they are what the worker, the model's tools, and the Ready-as-consent gesture depend on. The columns you see are a renderer-side view layer over them, so you can shape the board without ever touching those semantics. Rename a column, add one, reorder them, or delete one; the layout persists locally (mori_board_columns_v1 in localStorage, the same local-view pattern as workspaces). A board with no saved config starts as the ten default columns mapping 1:1 to the lanes, so existing boards migrate cleanly.

Every column carries a role (backlog | ready | working | review | done | custom) and an anchor lane. The role is what the dispatcher and safety model read; the lane is the semantic lane a card lands in when it enters the column. That indirection is why renaming or reordering is safe — and the consent invariant is enforced at the trust boundary where the config loads: a ready-role column must stay anchored 1:1 to the ready lane, and any stored column that would let a ready role point at a model-writable lane (or a non-ready column claim the ready lane) is coerced back before it can turn an innocent drag into worker consent. Remove a role's column entirely and the dispatcher degrades safely, falling back to the canonical lane for that role.

WIP limits are an optional per-column cap. A drop that would push a capped column past its limit is rejected — the column shakes and a toast reads "Working" is at its WIP limit (3). Finish a card first. Lower a limit below what a column already holds and the existing cards stay put; only new drops are blocked. The cap binds the dispatcher too: if the Working column is at its limit, the background worker defers pickup rather than exceeding it, so autonomous runs can never overrun a cap you set.

A Board / List toggle switches between the Kanban columns and a flat list of every card — the same tasks, sorted for scanning when the board grows long.

Ready is a human gesture

Dragging a card into Ready is the approval that authorizes execution — so the model is structurally barred from performing it. The UI drag path goes through a separate IPC channel (board-upsert) that only the Board page calls; the model's tools go through create_task and update_task, and both enforce the invariant in the main process:

  • create_task accepts only inbox, planned, or waiting_user as a target lane. Any other value — including ready — is silently re-filed to inbox.
  • update_task refuses to move a card into Ready from any other lane. The card is re-filed to Planned instead and the tool result tells the model exactly why.

This is what the model sees if it tries:

> update_task { id: "t_ly3k9x", lane: "ready" }
Moved task t_ly3k9x to planned — only the human can stage a card
into Ready (drag it on the Board).

> create_task { title: "Deploy the site", lane: "ready" }
Added task t_ly3ka2 to inbox: "Deploy the site".

The dispatcher, Nudge, and Auto-run

A dispatcher ticks in the background every 30 seconds. On each tick it checks two gates before doing anything: the Auto-run toggle must be explicitly on, and the live chat brain must be idle — a worker never competes with a conversation you are having. If both pass, it picks one Ready card — highest priority first (urgent > high > normal > low), oldest first within a priority — and claims it into In Progress. One card at a time; a synchronous mutex guarantees the timer and a manual nudge can never claim the same card twice.

Auto-run is off by default and opt-in per install: cards never start running just because the app updated or restarted. With Auto-run off, the Board is purely a planning surface until you press Nudge dispatcher, which runs a single tick immediately. A nudge is a human click, so it counts as approval and proceeds even while Auto-run is off — but it still only claims cards already sitting in Ready, and still yields to an active chat.

How a worker executes a card

A claimed card runs as an isolated subagent — the same mechanism as run_subtask, with the same sandbox, tool permission gates, and step limit. It calls your configured provider with your own API key; there is no Studio Mori backend in the loop. The card's title and detail become the goal, and the worker is told its final message becomes the result a human will review.

The worker's toolset is your permitted toolset minus a hard denylist: it cannot spawn further subagents, run deep research, edit Mori's memory or playbooks, or send email or Discord messages. Cards assigned a read-only role profile (researcher, critic) additionally lose every file-writing and terminal tool. Anything that remains permission-gated in your settings stays gated inside the worker.

While it runs, you stay in control. Before landing the result, the dispatcher re-reads the card from disk: if you deleted it mid-run it stays deleted, and if you dragged it to another lane your move wins and the worker's outcome is discarded rather than clobbering the board.

Retries, failure, and Review

The dispatcher inspects the worker's output for failure signatures (provider errors, a stop, the step limit). On failure the card goes back to Ready with the error recorded as its blocker and attempt N failed — will retry as its latest action. After two retries — three attempts total — it lands in Failed with gave up after retries. If the worker process itself crashes, the card moves straight to Failed with a worker crashed: … blocker. Nothing silently retries forever, and every failure leaves its reason on the card.

On success the card lands in Review, never directly in Done. The worker's summary (up to 4,000 characters) is stored on the card as its result, the blocker is cleared, and the latest action reads finished — awaiting your review. You close the loop yourself: drag to Done if the work holds up, back to Ready to re-run it, or to Planned to rescope. The same approval philosophy applies at both ends of a run — you stage the work in, and you sign the work off.

Filing cards from other agents (MCP intake)

Mori can act as an MCP server so other agents — Claude Code, an IDE, a script — file work onto the Board. It is fail-closed: disabled by default, binds to 127.0.0.1 only, and every request must carry the bearer token generated once and shown in Settings. The v1 surface is deliberately narrow — read-only file and status tools plus mori_board (read the whole board) and mori_create_task. No terminal, no file writes, no sends, no computer control.

mori_create_task can only land cards in Triage (inbox). It never executes anything itself, and because outside agents cannot touch Ready either, intake work always passes through your triage and your drag before any worker runs it.

curl -s http://127.0.0.1:4519/mcp \
  -H "Authorization: Bearer $MORI_MCP_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call",
       "params":{"name":"mori_create_task",
                 "arguments":{"title":"Audit pricing page copy",
                              "detail":"Compare against the brand voice doc"}}}'

# → Added task t_ly3kb7 to inbox: "Audit pricing page copy".

Scheduled runs: triage and rehydration

The Board's cousin is the scheduler — background runs created with schedule_task ("check my inbox every morning", one-off reminders) that fire on their own clock, and only while the chat brain is idle, so a scheduled run never competes with a conversation you are having.

Every run grades its own findings. After it completes, one cheap structured call on your provider's fast tier assigns a level: info (routine, nothing notable), warning (needs attention soon), or alert (needs attention now). A run that failed outright is never graded — it is always an alert. And the grader fails open: if the grading call itself breaks, the run falls back to info rather than being dropped, so under the default threshold a broken judge can never silently eat a result.

Each schedule carries its own notify threshold. A result below the bar stays silent — no notification for "checked, nothing new" — but nothing is lost: the outcome is recorded on the schedule and the full transcript is saved regardless. At or above the bar, a desktop notification delivers the single most important finding in one sentence, and every run lands in the audit log with its level either way.

Every run is also saved as a complete conversation, titled with the schedule's label and timestamp. Clicking the notification opens that conversation as a live chat with full context — what the run saw, what it concluded — so autonomous work is auditable after the fact and continuable in place: reply in the thread and the work picks up where the run left off. If the machine slept through occurrences, the schedule rolls forward to the next future slot instead of firing a backlog.