# Studio Mori: complete documentation (generated from app/docs content) # Managed vs your own keys > Mori runs two ways: on API keys you bring, unlimited and free, or on managed frontier models we provision for subscribers. This page explains exactly how each works, including what happens when you hit a cap. ## Two ways to power Mori Every copy of Mori is the complete app, every tool, every surface, no feature gates. The only question is which models she thinks with, and there are two answers. - **Your own keys**: plug in an API key from any supported provider. Unlimited from our side; you pay your provider directly for exactly what you use. - **Managed**: Mori thinks with frontier models running on capacity we provision. No keys, no provider accounts, no per-token meter. One flat monthly price, and usage flows in sessions. The free tier gives you both doors: unlimited use with your own keys, plus a fast-frontier managed allowance to try things without setting anything up. Subscriptions are how you make managed your main mode. ## Your own keys: the free path Bring a key from any of the nine built-in providers and everything works: computer use, browser, coding, terminal, voice, memory, MCP. Keys are stored locally and never leave your machine. Mori talks directly to your provider, nothing routes through us. We put no limits on this path. Your provider bills you for the tokens you use, and that is the whole cost. If you are comfortable holding a key, the free tier is the full product, forever. ## Managed: what a subscription adds A subscription removes the key entirely. Mori runs on frontier models we provision and tune per tool, vision-strong models for computer use, fast ones for chat and quick steps. You never touch a provider dashboard or watch a per-token bill. What a tier buys is **capability and room to work**: which model classes Mori reaches for, and how much she can do before a session refills. Higher tiers reach stronger model classes and carry more usage, nothing else about the app changes. ## How usage flows: 5-hour sessions Managed usage flows in **5-hour sessions that refill on their own**. Your first message opens a session; five hours later it resets, whether you used a little or a lot. There is nothing to top up and nothing to meter, heavier tiers simply fit more work into each session. - Free and Plus also carry honest daily limits, sized so steady personal use rarely meets them. - Tiers scale relatively: Pro carries roughly 2x the usage of Plus, Studio roughly 2x Pro, and Max is the most room we offer. ## What each tier reaches - **Free, $0**: fast frontier models on a daily managed allowance, and unlimited use with your own keys. - **Plus, $25/mo**: fast frontier models with far more room, plus deeper-reasoning bursts when a step needs the extra depth. - **Pro, $50/mo**: advanced models as the everyday standard, on every task. ~2x the usage of Plus. - **Studio, $100/mo**: everything in Pro, plus our most powerful models in windows for the hardest work. ~2x the usage of Pro. - **Max, $200/mo**: everything first: new models on day one, top models through the day, our highest usage. - **Custom**: for teams that need more than Max; write to `support@studiomori.ai`. ## Hitting a cap Caps pause; they never lock. When a session or daily limit fills up in managed mode: - Managed usage **pauses until the session resets**: a few hours at most. - Or you **switch to your own keys instantly**, mid-conversation, and keep working with no interruption. - The app itself never stops. Every tool stays available the entire time. There is no overage billing and no surprise invoice. A cap can only ever cost you a short wait or a key switch, never money. ## Which should you pick? If you already hold provider keys and like paying per token, the free tier is the complete product. If you want Mori to just work, no keys, no meters, frontier models tuned per task, pick the subscription whose model class matches your work: Plus for everyday speed, Pro for advanced depth as standard, Studio for top-model windows, Max for everything first. The full tier-by-tier comparison lives at `/pricing`. # How Mori works > The full path of a message: routing, the tool loop, local dispatch, and on-disk verification, running on your machine with your keys. ## The big picture Mori is an Electron app split into two halves. The renderer holds the brain: `chat.ts` owns every turn, routing, provider calls, the tool loop, memory retrieval, and post-turn verification. The main process holds the hands: a single dispatcher, `executeActionCore`, is the only code that touches your machine, files, shell, apps, documents, email, Discord. Every tool the model calls crosses exactly one IPC boundary (`execute-action`) into that dispatcher, which means every side effect passes through one auditable choke point. Mori is local-first and bring-your-own-keys. Model requests go directly from the app to the provider you configured: Anthropic, OpenAI, OpenRouter, or any OpenAI-compatible provider (Groq, DeepSeek, xAI, Together, Mistral, Perplexity). Provider resolution is strict: if your selected provider has no key, Mori falls back to another provider you have entered a key for, and if none exists it tells you to add one. It never silently reroutes agent work elsewhere. ## Routing: the first decision A composer slash layer runs before anything else. Text that parses as a known command, `/usage`, `/model`, `/compress`, `/clear`, `/autonomy`, and the rest of the local registry, executes on the spot and never reaches a provider; only a leading `/` followed by a recognized name is intercepted, so ordinary prose starting with a slash still routes normally. Everything that is not a command becomes a turn, and the turn is classified by intent and sent down one of four paths. - **Browser tasks** (shopping, webmail, "go to this site and do X") take a DOM-driven browser agent. If the Mori Chrome extension is connected, the run happens inside your real Chrome, your logins, your sessions, no extra key needed. Otherwise a fast DOM agent reads elements and clicks by selector, driven by Claude via your Anthropic key. No screenshots in the hot loop. - **Explicit computer control** ("use the computer", "click around") takes the vision computer-use loop. Screenshots stream to the UI so you watch what Mori sees; autonomy defaults to `ask`, so each risky step raises a confirmation. - **Clearly actionable tasks** go to the agent runtime: classify, plan, execute, verify. The runtime never returns fake success, if verification fails, the reply says so. Low-confidence tasks fall through rather than guessing. - **Everything else** takes the LLM path. Pure conversation streams token-by-token on a cheaper fast model (smart routing; disable with `smartRouting: false`). Anything that might need a tool, a memory request, a follow-up to a recent build, is forced onto the tool loop with your chosen strong model, because the streaming path carries no tools and must never pretend to act. ## What the model actually sees The prompt is built for byte-stable caching. The system prompt carries only session-stable content. Mori's core instructions, your host OS, a memory snapshot frozen at session start, and your persona settings, and is marked with a cache breakpoint, so repeated turns hit cached input at roughly a tenth of the cost. Everything that changes per turn rides in a `` block appended to a copy of the latest user message, never persisted into saved history: the current date and time, memories relevant to this message (scored by token rarity plus recency), playbooks from similar past tasks, failure lessons, active goals, and any task left unfinished by a previous session. Conversation history uses a verbatim window of the last 20 messages. Older turns do not vanish: a background pass maintains a running summary (under 200 words, persisted per conversation) that is prepended to the turn context, so long sessions keep their goals, decisions, and file paths. The refresh runs after your reply lands, never in parallel with the turn's own request. ## The tool loop Mori runs two equivalent tool loops, one speaking the Anthropic Messages format, one speaking OpenAI-style chat completions, so every provider gets the same behavior. Each step, the model returns tool calls; Mori executes them, appends the results, and asks the model to continue, until it answers in plain text. - Step budgets come from the active thinking stage: Spark caps at 15 tool steps, Flow at 50, Forge at 90, Trance at 200. Prefix a single message with a stage name to override one turn without changing the session. - Read-only tools (reads, searches, HTTP, OCR) and up to three `run_subtask` subagents dispatch in parallel; anything stateful, terminal commands, file writes, the browser, stays strictly sequential. - Truncated or invalid tool-call JSON never executes. The model gets an error telling it to re-issue the call in smaller pieces, instead of a half-written file silently "succeeding". - Messages you send mid-task fold into the very next loop step as steering notes, no competing second turn. Pause and Stop are honored between every tool call, and Stop also kills any shell children the agent spawned. - Every provider call carries a 150-second timeout and one retry on 429/5xx, so a hung provider cannot freeze a task. - The whole loop is session-scoped. Steering notes, Pause/Stop, the usage meter, and post-turn verification all key off a session id, so each Command Deck pane runs its own independent loop, stopping one pane never touches another, and two panes can be mid-task at the same time. ``` > forge: rebuild the pricing page and verify every link [Chat] Per-turn stage override → Forge [Chat] Provider: Anthropic | Model: claude-sonnet-4-6 [Chat/Anthropic] Tool calls (step 1): read_file, list_files [Chat/Anthropic] 2 read-only tools dispatched in parallel [Chat/Anthropic] Tool calls (step 2): edit_file ``` ## The right rail Long work does not scroll your chat away. It lives in a right rail beside the conversation, in four panes. - **Plan.** Work with three or more real steps shows live numbered phase boxes. Each box reads pending, in progress, or done, and the counts are honest: five phases means five phases, with no decorative steps. - **Activity.** Every tool call as it happens. What was read, what was written, what failed. - **Files.** The files this task touched, with their diffs. - **Preview.** A built site or document renders next to the work that produced it. The rail answers for real state, not for the transcript. Ask a question mid-task and Mori replies from the live plan, the files she just wrote, and the exact text of the last error. Give her a new requirement mid-task and it becomes a plan box: you watch it join the queue, and you watch it complete. ## Thinking that types When the model reasons, the reasoning types into view word by word. It holds on screen until the last word lands, and only then does the reply follow. The pace is capped for reading, but the words are the model's real reasoning stream, not an animation over a spinner. Deeper thinking stages think longer, so you see more of it. ## Deep research runs on a card Deep research does not disappear behind a progress bar. The run shows up as an inline workflow card in the chat: the waves it runs, what it is reading, and the sources as they land. The finished answer arrives with the trail that produced it, and the card stays in the transcript as part of the conversation. ## Dispatch and the safety gate Every tool call lands in `executeActionCore` in the main process. Before anything runs, arguments are normalized, common alias names are mapped onto canonical keys, `~` expands to your home directory, and bare relative paths resolve against the Desktop, then the call passes through the safety gate. - **Hard blocks, no override:** fork bombs, filesystem formats, raw disk writes, and recursive deletes of protected roots (`/`, `~`, `/System`, `/Users`, ...) are refused outright. If you truly intend one, Mori tells you to run it yourself. - **Approval-gated:** destructive-but-legitimate commands, `sudo`, recursive or forced `rm`, `git reset --hard`, `git clean -f`, `killall` all raise a native confirmation dialog, and deleting any file always asks first. This is on by default. - **Purchases stop at the pay button.** Browser checkout runs hold the order at the final step and surface a confirm gate in the UI; the order is only placed after you explicitly approve. Full Auto mode removes clarifying questions, not these gates. Sending, purchasing, deleting, and publishing still require confirmation. Past the gate, the dispatcher routes to platform-native handlers and the action switch: file operations, real deliverables (DOCX, PDF, XLSX, PPTX, zip archives), `send_email` with smart attachment resolution, web search, Discord and Telegram delivery, and app control. Everything executes locally and returns a plain-text result the model reads on its next step. ## Context hygiene: the sanitizer Every message array passes through a sanitizer immediately before every provider request, the last line of defense against a malformed transcript turning into an API 400 that eats the whole turn. On a healthy conversation it is a no-op. When something is off, a stopped turn left a tool call unanswered, an empty block slipped in, a steering note landed between a call and its result, it repairs the array in place and logs each repair. - Anthropic format: no empty messages, the first message is a user message, roles strictly alternate, every `tool_use` id is answered by a `tool_result` in the next user message (missing ones are synthesized as cancellation errors), and orphan results are stripped. - OpenAI format: every `tool_calls` entry is answered by a `role: tool` message before any other message, orphan tool messages are dropped, and null content is normalized. ## Proof, not claims The model's final text is not treated as ground truth. After the reply is produced, Mori re-checks this turn's successful mutations against reality: created files must actually exist on disk, created folders must list, deleted files must be gone, and a started dev server must answer a network probe. Up to 12 checks run with 3-second timeouts, and a verdict line is appended to the reply, or a warning naming exactly what could not be confirmed. A separate honesty check polices send claims on the tool-loop route: if the reply says something was sent or emailed but no send-capable tool succeeded this turn, Mori appends an explicit correction rather than letting a fabricated "done" stand. Replies produced without any tool calls are screened the same way, success claims like "your tweet has been posted" are blocked and replaced with an honest answer. Once the reply returns, background jobs finish the turn without blocking you: the task ledger closes (a completed task saves a reusable playbook, a failed one records a lesson), the exchange is mined for durable facts worth remembering, active goals are judged for progress, and the context summary refreshes for next time. ``` ✓ Verified: 3/3 changes confirmed on disk. ⚠ Verification: 1/2 confirmed: could not verify: index.html. ``` # Thinking stages > Four gears, Spark to Trance, that change how deeply Mori thinks, how long it can act, and how hard it verifies. ## What a stage is A thinking stage is not a label on the same behavior. Each stage reconfigures real machinery for the turn: the reasoning effort sent to the model, the cap on how many tool steps the agent loop may take, how many times a failed tool call is retried, how many deep-research waves are run, and the wall-time estimate Mori shows you up front. Deeper stages genuinely think longer, act longer, and verify more before claiming done. Stage selection happens entirely on your machine. Mori is local-first and bring-your-own-keys: the stage shapes the request body that goes straight from your desktop to your provider. And a stage never changes what Mori is allowed to do, risky actions are approval-gated at every stage, including the longest Trance run. ## The four stages - **Spark**: instant. Quick answers and edits, no ceremony. Effort `low`, up to 15 tool steps, 0 retries, 0 research waves. ETA: seconds. - **Flow**: the default. Normal agentic work. Effort `high`, up to 50 tool steps, 1 retry, 1 research wave. ETA: under 2 minutes. - **Forge**: deep build. Plans first, verifies everything. Effort `xhigh`, up to 90 tool steps, 2 retries, 2 research waves. ETA: 2 to 10 minutes. - **Trance**: maximum. Long autonomous think-build-prove runs. Effort `max`, up to 200 tool steps, 3 retries, 3 research waves. ETA: 10 to 45 minutes. The ETA is shown before the run starts, so you know what you signed up for. If a stage name Mori doesn't recognize ever reaches the resolver, it falls back to Flow rather than failing. ## What each knob does - **Effort**: the depth of the model's reasoning and how much it accomplishes per turn. On Anthropic models this is the `output_config.effort` parameter; on OpenRouter it becomes a thinking budget (see below). - **Tool-step cap** (`depthCap`): the maximum number of tool-loop iterations in one turn. Spark stops at 15 steps; Trance can chain up to 200 before yielding. - **Retries**: how many times a failed tool call is retried before the agent gives up on that step. Spark fails fast; Trance retries three times. - **Research waves**: how many waves of deep research a research task runs. Spark skips research entirely; each deeper stage adds a wave. Use Spark for lookups and one-line edits, Flow for everyday tasks, Forge when you want a plan and verification of every change, and Trance when you want Mori to keep working, and keep proving its work, until the job is genuinely finished. ## Selecting a stage The composer has a stage selector; pick one and it becomes the session's stage for every subsequent turn. The order in the menu is Spark, Flow, Forge, Trance, and each entry carries a one-line blurb of what it does. Flow is the default, you only touch the selector when a task is trivially small or unusually large. ## Per-turn prefix override You can deepen (or lighten) a single turn without changing the session stage by prefixing your message with a stage name and a colon. The prefix is case-insensitive, tolerates surrounding whitespace, and is stripped before the message reaches the model. It applies to that turn only, the next message returns to the session stage. ``` trance: port the sync engine to the new schema and prove every test passes spark: what does this error mean? ``` ## How effort reaches Anthropic models On the Anthropic path, `outputConfigFor` builds the `output_config` fragment of the request body from the stage's effort. The effort parameter is not universal, so Mori gates it per model and never emits an illegal value: - Models that reject effort entirely (Haiku, Sonnet 4.5, and 3.5/3.7-era models) get no `output_config` at all, sending one would be a 400. - Models that accept `low` through `max` but not `xhigh` (Sonnet 4.6, Opus 4.5/4.6) have Forge's `xhigh` downgraded to `high`. - Models with the full range (Fable 5, Opus 4.7+, Sonnet 5) receive the stage's effort unmodified. ``` // Forge on a full-range model { "output_config": { "effort": "xhigh" } } // Forge on Opus 4.6 → downgraded { "output_config": { "effort": "high" } } // Any stage on Haiku → fragment omitted entirely ``` ## How effort reaches OpenRouter models On OpenAI-compatible endpoints, `reasoningFor` produces the equivalent `reasoning.effort` fragment, but only when the base URL is openrouter.ai and the model family is known to accept it (Claude, GPT-5, o1/o3/o4, Gemini 2.5, DeepSeek-R1, Grok). Fast tiers: Haiku, mini, flash, get no reasoning fragment at all, keeping them instant. The stage's five-level effort maps onto OpenRouter's three: Spark (`low`) omits reasoning entirely, Flow (`high`) sends `medium`, and Forge/Trance (`xhigh`/`max`) send `high`. OpenRouter converts the value into a thinking budget as a fraction of max tokens, which is how Forge and Trance genuinely think deeper on that path too. Because Mori is bring-your-own-keys, this all runs against your own OpenRouter account. ``` // Trance on an eligible OpenRouter model { "reasoning": { "effort": "high" } } // Spark, or any fast tier → fragment omitted ``` ## Watching a stage think Every stage shows its reasoning as it happens. The thinking types into view word by word and holds until the last word lands, then the reply follows. The pace is capped for reading, but the words are the model's real reasoning stream, not an animation over a spinner. Deeper stages think longer, so you watch more of it. Beside the thinking, the right rail fills in the plan phase by phase, so a long Forge or Trance run is never a black box. ## Stages and safety Stages scale persistence, not permission. A Trance run can take hundreds of tool steps over 45 minutes, but every risky action inside it, shell commands, file writes outside the workspace, anything destructive, still stops at the same approval gate as a Spark turn. Depth buys you thoroughness; it never buys the agent autonomy you didn't grant. # Slash commands > A `/` in the composer opens Mori's command layer: quick local actions that run the instant you send them, right where you type, and never cost a model turn. ## How the command layer works Type `/` at the start of the composer and an autocomplete menu opens with every command available on this surface. Keep typing to filter (`/co` narrows to `/compress`), arrow up and down to move, Enter or Tab to pick. A recognized command runs the moment you send it, it performs a local action (opens a popover, compresses the thread, flips a setting) and **never goes to the model**, so it is instant and costs nothing. Anything that isn't a real command falls straight through as an ordinary message: `/models are great` is prose and gets sent to Mori. Only a known command name, followed by a space or the end of the line, is intercepted, so a command can never fire by accident from the middle of a sentence. ## The commands The core set on the desktop, roughly in the order you reach for them: - **`/new`**: start a fresh chat. - **`/clear`**: clear the current conversation. - **`/compress`**: fold older turns into a summary to free up the context window. - **`/usage`**: open the context popover: tokens in use, estimated next-turn cost, and what's filling the window, with one-click Compress. - **`/model`**: open the model picker for this chat. - **`/stage `**: set the thinking depth; with no argument, opens the stage menu. - **`/skills`**: browse your skills and commands. - **`/learn `**: turn a folder, a page, a topic, or "what we just did" into a new skill, saved for your approval. Covered below. - **`/sessions `**: search the full text of every past conversation, word for word, results with snippets, instantly, no model turn. New in 1.7.0; covered on the Memory page. - **`/export [md|html] [--redact]`**: save this conversation to your Downloads folder as clean Markdown or a self-contained styled HTML page. Live credentials are always scrubbed; `--redact` additionally strips injection-shaped content. New in 1.7.0. - **`/retry`**: re-run your last message. New in 1.7.0. - **`/branch`**: fork the conversation into a new chat from this point; the original stays untouched, and the fork starts already-summarized. New in 1.7.0. - **`/voice `**: toggle spoken replies; no argument flips it. - **`/autonomy `**: set how much Mori can do without asking. - **`/undo`**: remove the last exchange from the thread. - **`/help`**: reopen the command menu and list everything. ## /learn: teaching a skill on the spot `/learn` is how you teach Mori a new procedure mid-conversation. It takes whatever you give it and hands the model a single authoring task: research the target, then write a skill and save it for your approval. - **`/learn`** with nothing. Mori reflects on what you **just did together** this conversation and distills the winning procedure (not a transcript) into a skill. - **`/learn `**: Mori maps the folder and reads the key files so the skill reflects how that project really works. - **`/learn `**: Mori actually reads the page before writing, rather than guessing from the link. - **`/learn `**: Mori researches the topic first, then writes it up. Whatever the source, the result lands **pending on the Skills page**: Mori never activates a skill she just wrote. And `/learn` waits its turn: if a task is already running, she tells you to finish it first, then run it. ## /usage and /compress: staying inside the window These two are the context-transparency pair. `/usage` opens a popover that breaks your context window down by category, system, conversation, skills, memory, with an estimated cost for the next turn and a one-click Compress right there. `/compress` does that compression without opening anything: it summarizes the older turns in place so a long session keeps going without losing the thread. Both are local. Neither spends a model turn to tell you where you stand, the whole point is to see and manage context without paying for the privilege. ## One registry, every surface The command layer is a single declarative registry shared across every surface Mori runs on, the desktop composer and the chat bots (Discord, Telegram): but each command is only offered where it makes sense, and each surface's autocomplete lists only what it actually supports. - **In the bots:** `/clear`, `/model`, `/status`, `/task`, and `/help`. - **Desktop-only:** everything else, `/new`, `/compress`, `/usage`, `/stage`, `/skills`, `/learn`, `/sessions`, `/export`, `/retry`, `/branch`, `/undo`, `/voice`, `/autonomy`, and the rest. Because the registry never touches the screen or the wire, the same `/` grammar behaves consistently wherever you type it. ## One honest gap As of 1.7.0 the registry carries no vapor: `/retry` and `/branch`, which used to reply "coming soon", now do their full job. The one remaining pointer is `/theme`: it currently directs you to **Settings → Appearance** to switch looks rather than switching them inline. Everything else in the lists above works today, exactly as described. # Context & usage > Mori shows you exactly what's in her context window and roughly what the next turn costs, then lets you compress it in one click. Signed-in users also get an always-on plan meter that never lets managed usage surprise you. ## You can see the window fill Every chat carries a live **context gauge** in the composer bar. It reads the fraction of the model's context window in use right now, as a percentage and a token count, `48% full · 96k`, for example, and its color warms from accent to amber to red as the window fills. This isn't a decoration. It's measured from exactly what will be sent on your next turn, refreshed as the conversation grows. Click the gauge, or type `/usage`: to open the full breakdown. ## The /usage breakdown The popover splits the window into the seven things that actually consume it, each as a colored segment on one bar and a labeled line with its token count: - **System prompt**: the stable core instructions that make Mori herself. - **Tools**: the JSON schemas for every tool she's currently allowed to call. - **Skills**: the how-to procedures relevant to this turn. - **Memory**: the durable facts she carries about you and your work. - **Turn context**: the per-turn clock, running summary, and active goals. - **Conversation**: the verbatim recent messages. - **Images**: screenshots and pictures in context, which cost far more than their text length implies. The denominator is the **model's own context window**: 200k for Claude, more for some others, so the same conversation reads as a different percentage on a bigger-window model. The breakdown is assembled from the real sources on every open; if any one source is briefly unavailable, its bucket just reads zero rather than blocking the popover. ## Next-turn cost estimate The popover footer shows an estimated dollar cost for your **next turn**: the current context as input, plus a typical reply as output, priced by model family. Be clear about what this is: an **estimate, not a bill.** Token counts are a fast characters-over-four approximation, and the model's own tokenizer will differ slightly; the per-token prices are sensible per-family figures, not a live rate card. It's there to give you an honest sense of scale and to catch a runaway context before it gets expensive, not to reconcile an invoice. The popover says as much in its own footnote. Treat the number as a ballpark, and lean on your provider's dashboard for exact spend. ## Compress in one click When the window gets heavy, hit **Compress now** in the `/usage` popover, or type `/compress`. Mori keeps the opening framing and your most recent turns **verbatim**, folds everything in the middle into one dense summary, and keeps that summary in play so every later turn inherits it. You get the room back without losing the thread. A toast reports what happened, how many older turns were folded and roughly how many tokens were freed. Compression needs a bit of history to work on; on a short conversation there's simply nothing in the middle to compress yet, and Mori tells you so instead of pretending. ## Auto-compaction You rarely have to think about any of this. Once a conversation crosses roughly **70% of the window**, Mori compacts the middle **on her own**: the same fold-the-middle, keep-the-edges move as the manual button, run automatically and only once per new stretch of turns. The effect is that long sessions stay fast and coherent instead of slamming into a wall. The manual `/compress` and the `/usage` button are there for when you want to reclaim space early or deliberately; the automatic pass handles the rest quietly in the background. ## The always-on plan meter Signed-in users get a second meter beside the context gauge: a small **plan chip** showing your tier and a bar. Every signed-in user sees it, including bring-your-own-key users, because every tier still has managed features with their own caps. Signed out, or if the status can't be fetched, the chip simply hides. Managed usage flows in three windows at once, a rolling **Session**, a **Today** total, and a longer **Period**. The bar deliberately shows the **most-constrained** of the three: the window that will pause you first. It warms to amber and then red as that window fills. **Hover** the chip and it spells out all three with their reset times; **click** it to open the full set of meters. It refreshes on its own every few minutes, so it stays live even while you're not looking. Hitting a cap never locks the app. Managed usage pauses gracefully until that window resets on its own, and if you have your own API keys, they keep working instantly, right through the pause. # Tool catalog & deferral > Mori carries 154 native tools plus everything you connect over MCP, but by default the model sees a sharp core set and pulls the rest from a searchable catalog on demand. Same permissions, same audit log, a fraction of the tokens. ## Why the schema goes on a diet Every tool a model can call rides along as a schema in every request, name, description, and full parameter definitions. With 154 native tools plus connected MCP servers, that is more than 20,000 tokens of schema per request, paid again on every turn of every task, before a single word of conversation. Cost is only half the problem: a 154-item menu also diffuses tool choice, attention is a budget too. So by default Mori defers most of the catalog. The visible schema carries a core set of 65 tools, files, terminal and dev servers, web and research, element-first computer control, the everyday assistant cluster (music, calendar, reminders, clipboard), memory and continuity, delegation and deliverables, plus three bridge tools that reach everything else. That is 68 schemas instead of 154-plus: a ~56% cut, on the order of 9,000 tokens saved on every request, and a shorter menu that sharpens the choice among the tools that remain. ## The three bridges - `tool_search`: searches the entire permitted catalog, native and MCP alike, by capability ("calendar event", "ocr image", "zip folder"). Matching is BM25-style over each tool's name, description, and parameter names, with rarer terms weighted higher. Results come back as names plus descriptions, ready for the next step. - `tool_describe`: returns the complete parameter schema (JSON Schema) of one named tool, so arguments are written against the real contract rather than a guess. - `tool_call`: runs any catalog tool by name with JSON arguments. It is a pass-through, not a side door, see the next section. The catalog behind the bridges is rebuilt from the live tool definitions on every request. It includes MCP tools the moment a server connects, and it can never go stale or drift from what actually runs. ``` tool_search { query: "zip folder" } → Tools matching "zip folder": • create_zip: … (params: …) tool_describe { name: "create_zip" } → create_zip: full parameter schema (JSON Schema) tool_call { name: "create_zip", args_json: "{ … }" } → executes create_zip through the same permission checks and approval gates as a direct call, and the activity log shows "create_zip", not "tool_call". ``` ## Same permissions, same log `tool_call` grants nothing a direct call would not. It recurses into the exact same dispatcher as every direct tool call, so the entire enforcement stack fires unchanged: permission checks against your settings, approval and confirmation gates, diff cards for file edits, and the activity log. The log records the real tool's name, the audit trail shows what actually ran, never a wall of `tool_call` entries. A few edges are closed deliberately: the bridges cannot call themselves, a tool your settings do not permit is refused at the bridge exactly as it would be anywhere else, and arguments that arrive as invalid JSON are bounced back with instructions to re-check the schema, nothing executes on a half-parsed call. ## Pinning, and turning deferral off Two controls put you in charge of the diet. - **Pinning.** Pin a tool on the Tools page and it is un-deferred: it always rides in the visible schema, sorted ahead of everything else (position is attention), and the turn context names your pinned tools as preferred. Pin what you reach for constantly; the bridges cover the rest. - **The switch.** A settings toggle turns deferral off entirely, sending every schema on every request, useful for debugging, or for models that handle large tool menus well. Deferral is on by default. # Proof, not promises > Mori re-checks her own work against your disk, your ports, and her action log, and tells you exactly what she could and could not confirm. ## Why replies end with a verification line A language model's final text is not ground truth. A model can write "done" whether or not the file landed on disk, and Mori is built on the assumption that it sometimes will. So she treats her own reply as a claim to be checked, not a fact to be believed. After any turn that changed something, files written, folders created, dev servers started, a post-turn verifier re-inspects each successful mutation in the turn's action log against reality and appends a single verdict line to the reply. The checks are plain code running locally on your machine: re-reads of your own disk, a probe of your own ports. The model behind your API key never grades its own homework, and nothing about the check leaves your machine. Turns that changed nothing checkable, pure conversation, read-only work, get no line at all. Silence means there was nothing to verify, not that verification was skipped. ## The verdict line The verifier walks the turn's successful actions and builds one concrete check per claim, capped at 12 checks and 3 seconds per network probe so verification can never balloon a turn: - **File-producing tools** (`create_file`, `write_file`, `update_file`, `edit_file`, `append_file`, `create_zip`, `create_document`, `create_spreadsheet`, `create_slides`): the file is re-read from disk. A file too large to re-read still counts as existing. - **`create_folder`**: the folder is re-listed. - **`delete_file`**: the check is inverted: the file must now be gone. - **`start_dev_server`**: the URL from the tool result gets a real HTTP probe; anything answering on the port passes. Two outcomes: - `✓ Verified: N/N changes confirmed on disk.`: every mutation this turn was independently re-confirmed after the reply was written. - `⚠ Verification: K/N confirmed, could not verify: …`: up to four failing items are named (filename, folder, or URL). The line does not guess at causes; it tells you exactly which artifacts failed the re-check so you can look or ask Mori to retry. The verdict is metadata, not content. It is stripped before text-to-speech reads a reply aloud, before bot-bridge history is stored, and before the memory miner or goal judge see the reply, so Mori's own "✓ Verified" can never be recycled as evidence for anything. ## The honesty guard Verification covers things that leave a trace on disk. Sends are the opposite problem: an email that was never sent leaves no trace at all. Two guards in the chat pipeline cover that gap. - **No-tools validator.** If a reply arrives from a turn in which no tools ran but the text matches a hallucinated-success pattern ("message has been sent", "successfully deployed", "I've posted your tweet"), the reply is blocked outright and replaced with a plain admission that nothing was executed. - **Post-turn send check.** On the tool-loop route, a reply that claims something was sent, emailed, or delivered, and references an address or channel, and does not negate the claim itself, is cross-checked against the action log. If no send-capable tool (`send_*`, `post_*`, `reply_*`, phone tools) succeeded this turn, Mori appends: `⚠ Honesty check: nothing was actually sent this turn, no send tool ran. That claim above is wrong; tell me to try again and I will really send it.` The send check is scoped to the tool loop deliberately: browser and computer-use runs act through the DOM and the screen, not the action log, so judging them by log entries would falsely retract real sends. This is the mirror image of Mori's approval gates. Risky outbound actions, a bulk email, for instance, refuse to run until your latest message is an explicit yes. Approval controls what Mori *may* do; the honesty guard catches her claiming to have done what she *didn't*. ## Verify-on-stop "It should work now" is not evidence, so a turn is not allowed to end on it. When a turn's action log shows successful edits to code files, real code, judged by extension; prose and config edits do not count, but no passing proof after the last edit (a terminal command exiting 0, a clean script run, a dev server started), the reply does not stand as written. The pipeline sends Mori one synthetic message, explicitly marked as a system check rather than something you said: verify now, run the build, typecheck, test, or open the preview, and report the real result, or state the concrete blocker if verification is genuinely impossible. Restating completion without evidence is not an accepted answer. The nudge fires at most once per turn, its outcome is appended to the reply, and it can never break the reply itself, if anything in the check path fails, the original answer is delivered untouched. The point is the incentive: the cheapest way to satisfy the check is to actually run the verification, so "done" and "proven" converge. ## Truncation guards A model reply that hits its output-token cap can end mid-JSON, and a truncated tool call is dangerous precisely because it can look fine: a string cut right after a closing brace still parses. So Mori refuses to execute on truncation rather than trusting a parse. - When a reply is cut at the token cap while tool calls are pending, the entire batch is refused. Each call returns "NOT EXECUTED, your output hit the token cap, so these arguments may be incomplete", with instructions to re-issue the work in smaller pieces, and a follow-up note tells the model not to assume the last action completed. - The guard is enforced on both provider paths, `stop_reason: max_tokens` on Anthropic, `finish_reason: length` on OpenAI-compatible APIs, so switching providers never reopens the hole. - Arguments that arrive as invalid JSON never execute with empty defaults. The old failure mode, a truncated `create_file` call running with no arguments and a half-written file "succeeding" silently, is structurally gone: nothing runs, and the error tells the model how to redo it. On a plain text reply with no tool calls pending, hitting the cap appends an explicit note, say "continue" and Mori picks up exactly where she left off, instead of ending mid-sentence as if finished. ## Diff cards: see the exact change For every file mutation (`create_file`, `write_file`, `update_file`, `edit_file`), Mori reads the current on-disk bytes *before* the write executes. After a successful write she computes a real before/after line diff, a dependency-free LCS, entirely local, and attaches it to that step. In chat, finished work collapses into a single dropdown: "Worked: N steps · 12 searched · 8 read". Expand it and each edited file gets a collapsible card with +/- lines and added/removed counts, alongside grouped chips for the other tools. So "I updated index.html" is never just a sentence; the exact lines that changed are one click away, and they were computed from disk state, not from what the model said it wrote. The diff is bounded so it stays honest without ever hurting the work: previews cap at 400 lines, long unchanged runs collapse to an ellipsis with two lines of context on each side, and files over 6,000 lines fall back to a coarse "N added, M removed (preview omitted)" summary rather than a frozen panel. Diff computation is cosmetic by design, if it fails, the tool result is untouched. ## run_self_test: diagnostics on the real machine When you want proof that Mori's own toolbox works, ask her to "test yourself" or "run diagnostics", or use Settings → Diagnostics. The `run_self_test` tool exercises each capability subsystem on the actual machine, no mocks: - **Terminal/shell**: runs a real command and checks the echo round-trip. - **File read/write**: writes a temp file, reads it back, compares bytes, deletes it. - **Keyboard and mouse**: probes the platform's real input layer (AppleScript and Quartz on macOS; SendKeys and user32 on Windows). - **App detection**: checks against a process guaranteed to be running (Finder on macOS, explorer on Windows), so a failure means the probe is broken, not the target. - **Active window and screen capture**: queries the frontmost window and captures a real thumbnail from the display. - **Dispatch-parity probes**: `git_status`, `run_python`, and `list_dev_servers` are invoked through the real tool-dispatch path, not called directly. This proves the dispatch layer routes each action to a live handler on *this* platform, catching the bug class where an action silently returns "Unknown action" on one OS. "Reached, but git/python isn't installed" still passes parity, and the detail line says so. Each check reports pass/fail with a one-line detail, so a failure points at the subsystem, not at a vague "something's wrong". ``` Self-test on macOS, 10/10 passed: ✓ Terminal / shell: mori-ok ✓ File read/write: wrote + read back ok ✓ Keyboard (AppleScript): osascript available ✓ Mouse (Quartz): Quartz available ✓ App detection (Finder): RUNNING ✓ Active window: Ghostty, mori ✓ Screen capture: 2 display(s) ✓ Dispatch parity: git_status: On branch main… ✓ Dispatch parity: run_python: mori-py-ok ✓ Dispatch parity: list_dev_servers: No dev servers running. ``` ## Reading the signals - **No line at the end**: the turn changed nothing checkable. Nothing was claimed, so nothing needed proving. - **`✓ Verified: N/N changes confirmed on disk.`**: every artifact Mori claims to have produced this turn was independently re-found on your machine after the fact. - **`⚠ Verification: K/N confirmed, could not verify: …`**: treat the named items as not done. Ask Mori to retry them, or check the paths yourself; the label is the filename, folder, or server URL that failed its re-check. - **`⚠ Honesty check: nothing was actually sent this turn…`**: the send claim above it is retracted in-line. No send tool ran; tell her to try again and the real tool will run, through the same approval gate as always. - **A ❌ row in the self-test**: that subsystem is broken on this machine, and the detail text says how it failed. All of this runs locally, against your own disk and your own ports, with the models you brought your own keys for. Approval gates decide what Mori is allowed to do before it happens; verification confirms what actually happened after. Between the two, you are never asked to take her word for it. # Coding & builds > Mori writes real projects on your machine, runs them until they pass, and shows you the receipts. ## The build cycle When you ask Mori to build something, she follows a fixed sequence rather than improvising: map the codebase first (`list_files`, `search_files`, `read_file`: she never edits a file she hasn't read), then create the project folder, write every file with its full path, install dependencies if the project has any, and **run it**: `run_terminal_command` for a build or test, `start_dev_server` for a web app. She reads the command output; if it shows errors, she fixes the files and runs again until the build exits clean or the server is up. The turn ends with the preview opened so you see the result, not a description of it. Two rules are hard-coded into her coding context: she never reports done without a successful run or preview in the same turn ("it should work" is treated as a failure state), and she never claims a file exists unless the file tool actually returned success. Everything runs locally, the code, the shell, the servers all live on your machine, and the only network call in the loop is to the model provider using your own API key. Projects default to your Desktop unless you give a path. She won't open VS Code on her own; the UI gives you a button to open the folder, and she only calls the editor if you explicitly ask. ## Writing and editing files New code goes into files, never pasted whole into chat. For changes to existing files Mori uses `edit_file`: a surgical exact old-text to new-text replacement, not a whole-file rewrite. The edit fails loudly if the target text isn't found, and if it appears more than once she must either add surrounding context to make it unique or set `replace_all` explicitly, so an ambiguous edit can never silently land in the wrong place. - **The 250-line rule**: any file longer than about 250 lines is written as a first section via `create_file` followed by one append call per subsequent section. A large file emitted in a single call gets cut at the model's output-token cap; chunking makes truncation structurally impossible. - **Verified writes**: every create/write is checked on disk immediately: the byte length written is compared against the file's actual size, and the tool result says "verified on disk" only when they match. - **Checkpoints**: before Mori overwrites, edits, or deletes any file, its prior bytes are snapshotted into a per-file history (last 10 snapshots per file, files up to 10 MB). `rollback_file` restores the most recent state, `list_checkpoints` shows the history, and a rollback snapshots the current state first so it can itself be undone. ## The hardened edit engine Under `edit_file` sits a dedicated edit engine built for the ways model-generated edits actually fail. - **Exact match, enforced uniqueness.** The old text must match exactly once. Two or more matches reject the edit with instructions to add surrounding context, silently editing the first of three hits is how files get corrupted, so it cannot happen. - **Fuzzy fallback, byte-exact output.** When the exact pass finds nothing, a second pass folds the unicode look-alikes models emit when they retype code instead of copying it, smart quotes, em-dashes and other dash variants, non-breaking and exotic spaces, trailing whitespace, and requires a unique match in the folded text. The hit is then mapped back to whole-line boundaries of the original file, so untouched lines stay byte-for-byte identical: the folded copy is never written to disk. - **Anti-cascade resolution.** Inside the engine, every edit in a batch resolves against the original content, never against the running result of earlier edits, and overlapping spans are rejected before a single byte moves. One edit can neither shift nor accidentally create the match for another. - **BOM and CRLF round-trip.** A byte-order mark is stripped before matching and restored on write; line endings are normalized for matching and restored by majority, so a CRLF file comes back CRLF. The file keeps its identity through the edit. A successful edit reports the first changed line plus a real unified-style diff of what changed, computed from the on-disk before and after. ## The terminal `run_terminal_command` executes through your login shell (zsh or bash on macOS/Linux), so PATH resolves the way it does in your own terminal, homebrew binaries, `code`, node version managers all work. Every result comes back in a fixed format: the command, the real exit code, and output truncated at 4,000 characters. The default timeout is 300 seconds, adjustable per call from 5 up to 600 via `timeout_seconds`: builds and installs routinely take minutes, and the timeout exists so a hung process can't stall a task forever. A killed process is never reported as a normal failure: the output carries an explicit note that the command timed out and was killed, so a slow `npm install` doesn't read as a build error. Anything meant to run indefinitely belongs in `start_dev_server` instead. The terminal is approval-gated in two tiers. Catastrophic commands, fork bombs, disk formats, raw disk writes, recursive deletes of protected roots, are blocked outright; Mori refuses and tells you to run them yourself if you truly mean it. Destructive-but-legitimate commands (`sudo`, `git reset --hard`, `git clean -f`, `killall`/`pkill`, recursive or forced `rm`, `npm uninstall`) trigger a native confirmation dialog before anything executes, on by default. The Stop button genuinely kills in-flight shell children: SIGTERM first, SIGKILL two seconds later if needed. ``` Command: npm run build Exit code: 0 vite v5.2.0 building for production... ✓ 214 modules transformed. dist/index.html 0.46 kB # a timed-out command reports honestly: [TIMED OUT after 300s, the command was killed; this is NOT a real exit code. Long-running servers belong in start_dev_server; long builds can pass timeout_seconds.] ``` ## Nothing is silently truncated The same discipline governs everything that flows back to the model. - **Command output spills, it does not drop.** Terminal output over 4,000 characters keeps its tail in the reply, errors live at the end of build output, and the full output is saved to a temp file whose path is in the result, so the model can `read_file` or `search_files` the rest instead of losing it. - **File reads continue, they do not dead-end.** Large files come back in whole-line chunks, never cut mid-line, with an explicit continuation offset: "Showing lines 1-312 of 4890. Call `read_file` again with `offset=313` to continue." There is no bare "[truncated]" for the model to misread as the end of the file. Between the two, no long output or large file ever vanishes behind an ellipsis: the model always knows exactly what it has seen, what it has not, and how to get the rest. ## Dev servers Web projects run under `start_dev_server`, which spawns the process for real and leaves it up, it is registered for emergency stop but never killed by a timeout. Mori waits a few seconds for the server to boot, captures the first output, and reports the pid plus the detected local URL (e.g. `http://localhost:5173`). If the process exits immediately, she gets the exit code and the tail of its output instead of a fake "started" message, and tries a different command. - `list_dev_servers`: every running server with pid, command, working directory, and uptime. - `stop_dev_server`: stops by pid, or by a substring match against the command or directory; with no argument it stops everything. Servers persist across turns, so an iterating build keeps one server up rather than restarting it each time. ## Workspaces: isolated branches for risky work For big or risky jobs inside a git repo, Mori can work in a **workspace**: a real git worktree on its own branch, physically isolated in a `.mori-worktrees/` directory so your checked-out tree is never touched mid-experiment. - `create_workspace`: makes a `mori/-` branch and its worktree directory, and switches Mori into it: shell, git, and relative file operations now default there. - `list_workspaces` / `enter_workspace` / `exit_workspace`: see, resume, or leave workspaces; the list marks the active one and reconciles any whose directory has vanished. - `merge_workspace`: commits pending work in the worktree, then merges the branch back into its base with a real merge commit; pass `delete_after` to clean up the worktree in the same step. Merge conflicts are reported with the repo path so they can be resolved, never papered over. - `remove_workspace`: discards the worktree, but refuses if there are uncommitted changes unless you force it, and the branch itself is kept either way. ## Preview Every build ends with the preview open. For a served project that is the dev-server URL; for static projects Mori resolves the best entry point automatically, `index.html` at the project root, then the common build outputs (`dist/`, `build/`, `public/`, `out/`), then any loose `.html` file in the folder root, which covers single-file games. The result renders as a `file://` page in the app's preview drawer, so "done" is something you can look at, not take on faith. Paths written with `~` are expanded before resolution, so a project logged as `~/Desktop/my-game` previews correctly. The preview lives in the right rail beside the chat, next to the plan and the activity feed, so the result sits beside the work that produced it. ## Deploys through your Vercel A deploy is real or it does not happen. Mori ships through your own Vercel account: the project lands in your dashboard, on your domains, under your control. She reports the live URL when the deploy finishes. If the account is not connected, she says so in her first reply, builds everything she can, and stops at the wall with the exact ask. She never invents a placeholder deploy, and she never hands you something that looks finished but works nowhere. ## Proof: step cards, diffs, and the Verified line Every assistant message that did work carries a collapsed activity record, a single "Worked: N steps" header with a summary of the top actions ("12 searched · 8 read · 3 wrote") and an error count if anything failed. Expanding it shows the full detail: one chip per tool call with its status, repeated identical calls grouped with a multiplier, and a **diff card** for every file edit, the filename, `+added`/`−removed` counts, and an expandable before/after view with the actual changed lines. The record is saved on the message, so the audit trail persists in your chat history. On top of the UI record, a post-turn verifier re-checks Mori's claims against reality after any turn that changed things. Each successful file creation is probed on disk, each created folder is listed, each deleted file is confirmed gone, and each started dev server has its port probed, up to 12 checks per turn so verification never balloons a reply. The verdict is appended to the message itself. ``` ✓ Verified: 6/6 changes confirmed on disk. # and when something cannot be confirmed, it says so: ⚠ Verification: 5/6 confirmed: could not verify: game.js. ``` # Computer use & the browser > Mori has three ways to act on the web and your screen. A router picks the right one per request, and every purchase stops at a hard gate you control. ## Three engines, one router Before any model call, Mori classifies your message and routes web and screen work to one of three engines. All three run locally: the desktop app talks directly to the provider with your own key, and nothing is proxied through Studio Mori servers. - **Mori Chrome**: a Chrome extension that runs tasks in your real Chrome, with your actual logged-in sessions. Preferred whenever it is connected. - **The DOM browser agent**: Mori's built-in operator browser, driven by the page's real element structure rather than screenshots. Used for web tasks when Mori Chrome is not connected. - **The computer-use loop**: a screenshot-driven vision loop that operates the entire macOS screen with mouse and keyboard. Used only when you explicitly ask Mori to operate the machine. Both routes require agent mode to be on (it is by default). Anything that is not a web or screen task falls through to the normal tool runtime. ## How a request is routed Routing is deterministic, not model-guessed. `isBrowserIntent` matches shopping language (buy, order, add to cart, checkout), major store names (Amazon, eBay, Walmart, Target, Etsy and others), webmail work that names a mail site ("check my gmail", inbox triage, drafting or replying in webmail), and go-to-a-site-and-act patterns ("go to X and search / fill / book / sign in / post"). `isComputerUseIntent` fires only on explicit operate-the-machine phrasing, "use the computer", "control my screen", "take over the screen", "click around". Two exclusions keep tasks from being hijacked into the wrong engine. Build-a-deliverable requests ("make a PDF and email it") stay in the main runtime, which can create files and attach them, the browser agent cannot. And email addresses are stripped before matching, so "send an email to sam@gmail.com" goes to the `send_email` tool rather than triggering a Gmail browser run; the DOM agent only gets email jobs that explicitly name the webmail site. Inside the main runtime, desktop-app work follows a fixed preference order: direct tools first (open a file, run a command, call the tool that does the job with no UI at all), then **element control** (`screen_elements` + `click_element`, below), and screenshots only as a last resort. The habit is enforced in code: a turn that opens with a screenshot as its very first tool call is rejected and re-asked with instructions to use direct tools or element control instead. ``` "order AA batteries on amazon" → browser engine (shopping) "check my gmail for anything urgent" → browser engine (webmail triage) "go to news.ycombinator.com and find the top post" → browser engine "use the computer to clean up my desktop" → computer-use loop "build a report as a PDF and email it to Sam" → normal runtime (not the browser) ``` ## Chrome first: the Mori Chrome extension When a web task arrives, Mori checks whether the Mori Chrome extension is online. If it is, the task runs in your real Chrome, your logins, your sessions, your autofill, instead of the operator browser. The desktop and the extension join an account-scoped realtime channel (`mori-chrome-`), so signing into the same account on both is the only pairing step; presence detection recognizes them automatically, with no pairing code. The desktop broadcasts the task, the extension streams step updates back, and the run times out after 3 minutes if the extension stops responding. This path needs no Anthropic key on the desktop side, the extension does the work. It is also exposed to the main runtime as the `use_mori_chrome` tool, offered to the model only while the extension is connected, so Mori can hand logged-in web work to your real browser mid-task. If the extension is not connected and no Anthropic key is set, Mori tells you plainly which of the two to add rather than failing silently. ## The DOM browser agent Without Mori Chrome, web tasks run in Mori's built-in operator browser through a fast DOM loop. Each turn, Claude receives the current URL, page title, an indexed list of every interactive element with its exact text, and the page text, no screenshots in the hot loop. It acts by referencing elements by index through a single `browser` tool with actions `goto`, `click`, `type`, `submit`, `press`, `select`, `scroll`, and `done`. This is roughly 10 to 50x faster than a vision loop, and the spend guard is stronger because Mori knows the exact label of every button before it is clicked. The loop caps at 45 steps, streams a screenshot to the UI every few actions so you can watch it work, and honors the Stop button between every action. It requires an Anthropic API key (default model `claude-sonnet-4-6`), called directly from your machine. ## Element control: computer use without vision When Mori drives a desktop app through her normal tool runtime, the preferred instrument is a pair of tools that read the operating system's accessibility tree instead of taking screenshots. `screen_elements` asks the OS: System Events over the AXUIElement tree on macOS, UI Automation on Windows, for the frontmost window's interactive elements, and returns them as a numbered text list: buttons, text fields, links, menus, checkboxes and sliders, each with its role and name, up to 120 per listing. `click_element` then acts on one by `id` (or by exact or partial `name`), resolving the cached entry to its center coordinates and clicking it, pass `double` for the double-click that opens files and folders. Text in, text out, and that has three concrete consequences: - **Any model can operate apps.** No screenshot ever enters the loop, so element control works on every provider you configure, including text-only models with no vision capability at all. The screenshot-driven computer-use loop below requires an Anthropic key; element control does not. - **A fraction of the tokens.** A numbered element list costs on the order of a tenth of the tokens of a screenshot round-trip, so app-driving turns stay fast and cheap. - **Exact labels, not guessed pixels.** Mori knows the precise name of what she is about to click before she clicks it. The list is a snapshot, and it goes stale the moment the screen changes, so every tool result reminds the model to re-run `screen_elements` after each navigation, and `click_element` refuses to act when no current list exists. When an app exposes no accessibility tree at all (some Electron apps), the tool says so explicitly and Mori falls back to `take_screenshot` plus coordinate clicks, screenshots are the fallback, never the default. On macOS this path uses the same Accessibility permission as the rest of computer use, and Mori sets the `AXManualAccessibility` attribute on the target process, which coaxes many Electron apps into exposing their tree. ``` screen_elements → Interactive elements of "System Settings" (23): 1. [Button] General 2. [Button] Appearance 3. [TextField] Search … Act with click_element(id) or click_element(name). The list goes STALE when the screen changes, re-run screen_elements after navigation. click_element { id: "2" } → clicked: [2] Button "Appearance". If the screen changed, call screen_elements again before the next click. ``` ## The computer-use loop For explicit operate-the-machine requests, Mori runs a vision loop: screenshot, Claude picks one action (click, double-click, right-click, type, key combo, scroll, move, wait), Mori executes it on the real OS, screenshots again, repeats, up to 70 steps. Before starting, it prechecks macOS permissions: if Screen Recording is not granted it stops and tells you exactly which toggle to flip, and if Accessibility is missing it opens that settings pane and registers the app for you, since without it synthetic clicks are silently discarded by macOS. Autonomy defaults to **ask**: every click, keystroke, and typed string is described and requires your approval before it executes, and a declined action is reported back to the model rather than performed. You can switch to auto for trusted runs, but the money rules below hold in both modes. This engine requires an Anthropic API key. ## The hard purchase gate Mori will drive shopping all the way to the final review screen and no further. The rule is layered so no single failure can break it. - **Prompt layer.** Both engines carry non-negotiable instructions that survive full-auto: never enter card, CVV, banking, or login credentials; never click the final place-order / pay / submit-payment button; never submit or execute a trade or move money; never solve a CAPTCHA, stop and hand back instead. - **Code layer, DOM agent.** Before any click, the target element's exact label is tested against a pay-button pattern ("Place your order", "Pay now", "Submit payment", "Confirm purchase" and variants). A match never clicks. Mori raises a confirmation card with the order summary, the exact button label, and a screenshot, then stops. The same check runs when the model declares it is done while a pay button is on screen. "Buy Now" is deliberately not treated as spending: on Amazon it only opens the single-item review screen, where the real place-order button is gated. - **Code layer, computer use.** If the model narrates a purchase or trade intent alongside a click or keystroke, Mori refuses to execute the action and hands the screen back to you with the order sitting on the review page. Completing the purchase requires your explicit approval of the exact order in chat. Only then does Mori re-read the review page, find the place-order button by its exact label, click it once, and verify the confirmation page before reporting success. Payment details always come from your own saved sessions and autofill. Mori never types a card number, and it never executes trades. ``` Mori: The order's ready at the final review screen. I won't place it myself, tap "Place the order" below (or reply "place my order") and I'll click "Place your order". Tap Cancel to leave it. ``` ## What needs a key Mori is bring-your-own-keys, and the three engines have different requirements. - **Mori Chrome**: no Anthropic key needed on the desktop. Connect the extension and sign into the same account on both sides. - **DOM browser agent**: Anthropic key required, set in Providers (or picked up from the environment). Requests go straight from your machine to the Anthropic API, or to a custom base URL if you have configured one. - **Computer use**: Anthropic key required, plus macOS Screen Recording and Accessibility permissions for the app. If a web task arrives with neither the extension connected nor a key configured, Mori says so and names both options, it never falls back to a degraded engine without telling you. # Terminal sessions > Named shells that stay alive across turns. The same session Mori drives is the one you can type into. Local, Docker, or SSH, with output kept bounded so a runaway command can't blow memory or context. ## What a terminal session is A terminal session is a long-lived shell that Mori keeps open across turns, keyed by a name you choose. `cd` into a directory, export an env var, start a build, all of it persists in that session until you close it. Reuse the same name and you pick up exactly where you left off; open a new name and you get a clean shell. Five tools drive them: `terminal_open` starts (or reattaches to) a named session, `terminal_run` executes a command and waits for it to finish, `terminal_read` reads recent output without running anything, `terminal_list` enumerates every open session, and `terminal_close` reaps one. This is a real, stateful shell, not a one-shot `run_terminal_command` that forgets everything the moment it returns. Named sessions shipped in 1.3.0 SOVEREIGN. ``` > terminal_open { name: "build", backend: "local" } Opened terminal session "build" [local] at /Users/you/site. Run commands in it with terminal_run. > terminal_run { name: "build", command: "cd apps/web && npm ci" } [build] exit 0 … > terminal_run { name: "build", command: "npm run build" } # still in apps/web [build] exit 0 … ``` ## Shared control: one shell, two drivers The sessions Mori opens are the same sessions you see in the Mori Terminal panel. It is genuinely one shell with two drivers: the agent runs commands through the `terminal_*` tools, and you type into the same tab, same working directory, same environment, same running processes. - The terminal panel is tabbed, one tab per named session, with a backend badge (local / docker / ssh) and a live spinner while a command is running. - The panel polls the shared session list and the active tab's output a few times a second, so commands Mori runs, and their output, show up live as you watch. - `terminal_read` is the mirror image: it lets Mori catch up on output from a command you started by hand, so you can hand work back and forth in the same shell. Everything the panel does routes through the exact same tool dispatch the agent uses, so there is no second, hidden code path, what you can do and what Mori can do are the same surface. ## The CLI auto-demo When a build produces a command line tool, the turn does not end with a claim. Mori opens a terminal session and runs the tool in front of you: a short demo with real commands and real output, in the same panel you can type into. Done means you watched it answer. If the demo fails, that failure is the result, and she keeps fixing instead of reporting success. ## Three backends: local, Docker, SSH You choose where a session's shell actually runs when you open it. A name is pinned to its backend for the life of the session, reopening the same name on a different backend is refused rather than silently swapping shells underneath you. - **local** (the default): your own login shell: `zsh` or `bash` on macOS and Linux, PowerShell on Windows. It inherits the environment Mori is running in, so your `PATH` and tools are already there. Commands land on your real machine. - **docker**: a hardened, throwaway container. Good for running untrusted code, a clean build, or anything you'd rather not run against your home directory. Needs Docker installed. - **ssh**: a persistent shell on a remote host. Needs key-based auth to that host. All three look identical to the model once open: the same commands, the same bounded output, the same exit codes. ## The Docker backend is hardened by default When you open a `docker` session, Mori starts a fresh container locked down before any command runs, you don't have to remember the flags. The container is started lazily on open and reaped with `docker rm -f` the moment you close the session, so nothing lingers. - **`--cap-drop ALL`** strips every Linux capability, and **`--security-opt no-new-privileges`** stops a process inside from escalating. - **`--pids-limit 256`** caps process count (a fork bomb hits a wall) and **`--memory 2g`** caps memory. - Your workspace is bind-mounted read-write at **`/work`**, and the shell starts there, so builds can touch your project files, but the container's own root filesystem is throwaway. - The default image is `node:20-bookworm-slim`; pass `docker_image` to pick another. It degrades gracefully: if the `docker` binary isn't on your `PATH` or the container fails to start, `terminal_open` returns a clear error and tells you to install Docker or use the local backend, it never crashes the app. ``` docker run -d \ --cap-drop ALL \ --security-opt no-new-privileges \ --pids-limit 256 \ --memory 2g \ -v :/work -w /work \ node:20-bookworm-slim sleep infinity ``` ## The SSH backend An `ssh` session opens a persistent remote shell, multiplexed through an SSH ControlMaster socket so reconnecting to the same host is fast. It is built to fail fast rather than hang: - **`BatchMode=yes`** means Mori never blocks on an interactive password prompt. If key auth to the host isn't set up, the connection fails immediately with a clear error instead of freezing a task waiting for a password nobody will type. - A `ServerAliveInterval` keeps the connection healthy, and the shared control-master is torn down when you close the session so no dangling connection is left behind. - Like Docker, it degrades gracefully, a missing `ssh` binary or an unreachable host returns an error, never a crash. Give it the destination as `[user@]host` when you open the session. ## Output is always bounded A shell that stays open forever could flood the model's context with the output of one chatty command. Every session is built to prevent that. - **Sentinel completion.** After each command Mori writes a unique marker line and reads output until that marker appears, so it knows exactly when a command finished and what exit code it returned, with no TTY and no guessing. The exit code comes back on every `terminal_run`. - **Head + tail, middle elided.** A single command's output is captured as a bounded head plus a rolling tail; if it runs long, the middle is dropped and replaced with a `…N lines elided…` marker. The result is flagged as bounded so you know it was trimmed. - **Capped scrollback.** Each session keeps a rolling window of its most recent output for `terminal_read` and the UI; older lines age out rather than accumulate forever. - **Timeouts.** A command waits up to 120 seconds by default (600 max). If it hasn't finished, `terminal_run` returns what it has so far and tells Mori the command may still be running, `terminal_read` catches up later, or `terminal_close` ends it. For servers that never exit, `start_dev_server` is the right tool instead. Closing a session reaps its shell, and for Docker or SSH, the container or the connection too. Every open session is also reaped when you quit the app. ## Commands still pass the safety gate A persistent shell does not mean an ungoverned one. Every command sent through `terminal_run` passes the same command chokepoint as any other shell command Mori runs: an un-bypassable hardline floor refuses catastrophic patterns outright, and dangerous-but-legitimate commands are classified and gated according to your current autonomy level. That means a `terminal_run` of `rm -rf ~` is blocked no matter what, and a `sudo` or a `git reset --hard` prompts for approval under the default Guided level. How much runs without stopping to ask you is governed by the Autonomy ladder, see **Autonomy** for Guided / Trusted / Commander and the hardline floor. # Skills > How-to procedures Mori actually follows. Written by you, shipped built-in, taught with `/learn`, or authored by Mori herself after she nails something hard. Now on the open SKILL.md standard: versioned, with rollback and an approval gate. ## What a skill is A skill is a durable procedure, a short markdown document that tells Mori exactly how to do one kind of task well. Unlike memory (facts) or playbooks (auto-saved task sketches), a skill is a first-class, human-readable how-to guide with a name and a one-line trigger description: "Use when deploying the Vyridian site", "Use when building any website or game". Each skill is one `.md` file with a small YAML frontmatter block, stored locally per profile and per workspace. You can open, edit, version, or delete any of them from the Skills page, nothing is hidden inside a database. ``` --- name: Deploy Vyridian description: Use when deploying the Vyridian site to Vercel. version: 1.2.0 source: user --- 1. run_terminal_command git push 2. verify the deploy URL responds 3. open_preview the production site ``` ## The SKILL.md standard Skills use the open, **agentskills.io-compatible SKILL.md** shape: YAML frontmatter with `name`, `description`, and `version` (plus optional `author` and `license`), followed by a markdown body. A private `metadata.mori` block carries Mori-specific fields, provenance and the approval gate, without polluting the portable part of the file. This format is interoperable in both directions. A skill authored for another agentskills.io tool drops straight in, and skills you pull in through **Import from other agents** arrive in exactly this shape. Older Mori skills written before the standard, a flat frontmatter block with no version, **auto-migrate in place** the first time they load, filled in at version `1.0.0`; the rewrite happens once and never loops. ## How skills reach the brain On every turn, Mori ranks your enabled skills against the message you just sent. The top matches (up to two) get their **full procedure injected** into her thinking for that turn, within a fixed budget; every other enabled skill appears as a one-line index entry so she always knows what exists and can open any of them mid-task with the `skill_view` tool. - Injection is relevance-ranked and budgeted, skills never crowd out the conversation. - Only **enabled and approved** skills are injectable. Disabled skills, and any skill still `pending` or `rejected`, are absent from both the injection and the index. - A skill that names concrete tools (`run_terminal_command`, `create_document`) steers her to the exact right calls, not a paraphrase of them. ## Versioning, history, and rollback Every skill carries a semver version, and every edit is tracked. When you change a skill's body or description in the inline editor, Mori **bumps the patch version** (`1.0.0` → `1.0.1`) and snapshots the previous version into a private history trailer that keeps the last five. The Save button shows you the version you're about to create. The history is a safety net, not a burden. A **Compare** dropdown lets you preview any past version side by side with the current one, old on the left, current on the right, and **Roll back** restores that version as a new bumped version of its own. Nothing is ever lost: rolling back snapshots the current body first, so you can always roll forward again. The whole editor lives inline on the Skills page; there is no separate versioning tool to learn. ## Mori writes her own skills This is the self-improvement loop. When Mori completes something non-trivial, a multi-step win, a recovery from a dead end, a correction from you, she is instructed to capture the WINNING procedure with the `skill_manage` tool. Her writes do not go live by themselves. Agent-authored skills land in a **pending queue**, shown on the Skills page under "Awaiting your approval" with the full procedure visible and a clear "Mori wrote this" provenance. You approve or reject with one click. You can also prompt her directly with **`/learn`**: run it with no argument to distill what you just did together, or point it at a folder, a URL, or a topic and she'll research the target before writing. Either way the draft lands pending, she never activates a skill she just authored. ## The approval gate A skill is only usable when it is **enabled AND approved**. Two kinds of skill arrive un-approved and wait for your sign-off: - **Mori-authored drafts**: written by the self-improvement loop or by `/learn`, sitting in the pending file store until you approve them. - **Imported skills**: pulled in from another agent, carrying `approval: pending` in their frontmatter, live on disk but inert until approved. Both show up together under "Awaiting your approval" with an approve / reject pair. Approving a Mori-authored draft moves the file into the live set; approving an imported skill flips its frontmatter in place. Until then, Mori never follows it, the gate is the same whether the skill came from her or from somewhere else. ``` metadata: mori: approval: pending # not injectable until you approve source: import:hermes # where it came from ``` ## Mid-run lessons Mori learns from failure while a task is still running, not just afterward. When the **same tool fails twice in a row**, or the **verifier rejects a step**, she distills a one-line lesson from what went wrong and folds it into her very next step, so the correction applies inside the same run, before she repeats the mistake. The same lesson is also written to the durable lessons store, so it rides into future matching tasks too. The distillation is a deterministic template built straight from the error, cheap, always available, and never dependent on an extra model call mid-step, so learning can't itself stall the task. ## The built-in set Every fresh install (and every new workspace) seeds eight battle-tested skills. The four work disciplines: - **Web project build**: the scaffold → write → install → RUN → fix → preview discipline, including the chunked-write rule for big files. - **Deep research method**: search angles, full-source reads, second-sourcing surprising claims, answer-first delivery. - **Polished deliverable**: when and how to produce real files (docx/pdf/xlsx/slides) instead of chat dumps. - **Debugging protocol**: reproduce first, read before editing, one surgical change at a time, prove the fix. The four business skills, **Ad campaign craft**, **Business outreach**, **Social content engine**, and **Market & competitor scan**: each stage their output through the approval queue; nothing publishes or sends on its own. Seeding is additive. On startup, any built-in missing from your library is topped up, which is how new built-ins reach existing installs, but a file that already exists is never touched, so your edits to a seed skill always win. To retire a built-in for good, disable it rather than deleting it: seeding restores missing files, never existing ones. ## Skills vs playbooks vs lessons vs memory Four layers, each earning its keep: - **Skills**: deliberate procedures, human-readable, versioned, followed on matching tasks. The strongest signal. - **Playbooks**: lightweight approach sketches Mori auto-saves after completing tasks; cheap, automatic, numerous. - **Lessons**: one-line "never do that again" notes recorded from failures. They now apply **mid-run** as well as on future tasks. - **Memory**: durable facts about you and your projects. All four are ranked per turn and injected only when relevant. A playbook that keeps proving itself is a candidate for promotion: tell Mori to "turn that playbook into a skill", or reach for `/learn`. ## Managing skills The Skills page (Systems → Skills) is the whole catalog: - Source badges, `builtin`, `user`, `agent`: plus a version badge, so you always know where a procedure came from and which revision you're looking at. - Click any skill to expand its full body, edit it inline (each save bumps the version), or compare and roll back an earlier revision. - **New skill** opens a three-field editor: name, the "Use when …" line, and the procedure. - The **Awaiting your approval** lane collects Mori-authored drafts and imported skills together, each with approve / reject. Skills are scoped to the active workspace, your "Client A" workspace can carry client-specific deploy procedures that never leak into "Personal". # Import from other agents > Switched to Mori from another agent? Pull your skills and memory straight in from Hermes, Claude Code, or OpenClaw. Read-only, injection-scanned, and gated behind your approval. ## What import does Import lifts the two things worth keeping out of a competing agent, your **skills** and your **memory**: and brings them into Mori. It reads a known agent's home directory on your Mac, parses the skills and memory notes it finds, and files them into Mori's own stores. Three agents are recognized today: **Hermes** (`~/.hermes`), **Claude Code** (`~/.claude`), and **OpenClaw** (`~/.openclaw`). Nothing is executed. Imported content is data. Mori reads it, she never runs it, and the whole path is pure filesystem: no shell, no network, and it never throws an error back into the app. The most an import can do is add a pending skill and some scanned memory lines, both of which you review before they matter. ## Where to find it Import lives on the Settings page under **Import from other agents**. Open it and Mori scans your home directory once, in the background, for each recognized agent. Any it finds shows a row with the detected path and a count, how many skills and memory lines are sitting there to pull, plus an **Import** button. Agents that aren't installed show as "Not detected" and have no button. Detection is read-only: it counts what's there without moving anything. Nothing is imported until you click Import for a specific agent, and each agent is a separate, explicit click. ## What gets pulled For each agent, import looks only in that agent's standard locations, one level deep, it never crawls arbitrarily far into a foreign tree. - **Skills**: one skill per `name.md` file, or per `name/SKILL.md` folder (the open agentskills.io layout; `skill.md` and `README.md` are accepted too). Frontmatter and the first heading are used to fill the skill's name and description. - **Memory**: the agent's memory and notes files (`MEMORY.md`, `USER.md`, `notes.md`, a `memory/` folder), split into individual fact lines. For Claude Code, per-project `projects/*/memory` and each project's `CLAUDE.md` are read too. Claude Code's `commands/` directory is read as skills alongside `skills/`. Chat history (session databases) is counted so you know it's there, but importing it lands in a later update, it is never parsed today. ## Imported skills wait for your approval A skill you import does **not** go live. Every imported skill is written into Mori's pending store with `approval: pending` in its frontmatter, and Mori will not follow it on any task until you approve it. You review the full procedure on the Skills page under **Awaiting your approval**, then approve or reject each one. Imported skills are slug-prefixed by their origin (`hermes-…`, `claude-…`, `openclaw-…`) so they can never collide with or silently overwrite a skill you already have, and each carries a source tag recording exactly where it came from. This is the same pending gate agent-authored skills pass through, imports simply enter it from outside. ``` --- name: Deploy the staging site description: Use when pushing the staging branch to Vercel. version: 1.0 source: agent metadata: mori: approval: pending source: import:claude --- 1. run_terminal_command git push origin staging 2. verify the preview URL responds 3. report the deploy URL ``` ## Memory is scanned before it's stored Every memory line passes through the **same injection and credential scan** as any other memory write before it can be stored. A line that reads like a prompt-injection attempt, a fake `system:` or `assistant:` role prefix, an "ignore previous instructions" phrase, role or control tags, or that contains something shaped like a live secret (an API key, an AWS key, a private-key block) is refused and counted, never stored. Memory is data Mori remembers, never instructions she follows, so nothing that tries to smuggle instructions in through it survives the door. Clean lines are stored tagged `[imported]`, de-duped against what Mori already remembers, and marked evictable rather than pinned, so a big import can't crowd out the facts you added by hand. The import report tells you exactly how many were added and how many the scan rejected. ## Provider names, never keys Import peeks at the agent's config files for one reason only: to learn which model providers you already use, so the report can say "providers detected: anthropic, openai" and help you get set up faster. It reads **names only**, matched against a fixed whitelist of known provider names. A key value can never come through, even if the config is shaped in an unexpected way, because the scanner only ever emits tokens that are in the whitelist, and an API key is never one of them. Your keys stay exactly where they are; import never reads, copies, or moves them. ## The report After you click Import, Mori shows a short receipt for that agent: - how many skills were imported and that they're pending your approval in **Skills**, - how many memory lines were added, and how many the safety scan rejected, - how many sessions were found and deferred, - any provider names detected in the config. Detect is read-only, it counts what's there (often hundreds of memory lines from a long-lived Claude Code install) without moving a single byte. Everything import does is auditable this way: you see the counts before you commit, and the receipt after. # Memory > Mori keeps a persistent, self-maintaining memory of who you are. One plain markdown file per fact, stored locally, curated by a nightly pass, and walled off from anyone who is not you. ## One file per fact As of 1.7.0, Mori's long-term memory is a directory of markdown files, one file per fact, living in the app's data folder (per profile and workspace). Each file carries frontmatter, `name` (a kebab-case slug, the filename), `description` (one line, capped at 200 characters, this is the recall key, written to be findable later), `type` (`user`, `feedback`, `project`, or `reference`), created and updated timestamps, `links` to related memories, and an optional `pinned` flag, followed by the fact itself as the body. Feedback-type memories get structured **Why:** and **How to apply:** sections so a correction is stored as something actionable, not a bare one-liner. Memories can reference each other with `[[wiki-style links]]` in the body; link targets are parsed out automatically and recorded in the frontmatter. Because everything is plain files on disk, you can read, grep, and edit your own memory with any text editor, and the new Memory page in the app gives you a browser for it. If you were on an earlier version, the old flat `memory.json` store is migrated automatically the first time 1.7.0 touches memory: every entry becomes a file (pinned entries stay pinned, your profile paragraph becomes a `user`-type memory), the original is kept untouched as `memory.json.bak`, and a sentinel file makes the migration strictly one-time, a re-run can never clobber newer content. Signed-in sync moves per-file with last-write-wins stamps, and deletions travel as tombstones, so removing a memory on one machine actually removes it everywhere. ## The index: a tiny map that is always loaded Alongside the fact files sits `INDEX.md`: an auto-maintained table of contents, one line per memory (`name, description`), rebuilt on every write and hard-capped at about 1,800 characters. This index rides in the system prompt on every single turn, so Mori always knows *what* she knows, even when a fact's full body isn't loaded. When the index outgrows its budget, the oldest non-pinned lines drop from the index only, the files stay on disk and remain openable by name. The index is frozen once per session: it never changes byte-for-byte between turns, which keeps the prompt prefix stable and prompt caching effective. The frozen snapshot from before, your synthesized profile paragraph plus pinned memories, capped at 3,200 characters, still rides alongside it. The index text also carries the write discipline as standing instructions: update-not-duplicate, and delete-or-fix when you correct her. ## Recall: ranked by description, injected in full Each turn, a background prefetch (capped at 800ms so it can never stall your message) ranks every memory's description against what you just said, token overlap weighted by rarity, the same mechanism skills use. The top 4 ranked memories are injected as **full bodies**, and pinned memories always ride regardless of relevance, all within a 6,000-character budget. When an injected memory `[[links]]` to another, the linked memory's description comes along as a one-line pointer, never its full body, so a chain of links can't blow the budget. Anything the ranking didn't surface is still one call away: the `memory_view` tool opens any memory file in full by name, and the always-present index tells her which names exist. The injection block itself instructs her to weave in at most one remembered detail when it genuinely fits, never to recite or inventory what she knows. ## Update, don't duplicate, and forget for real Writes go through one tool, `memory_write`, with three actions, `add`, `update`, `delete`: and the discipline is enforced in code, not vibes: - **A near-duplicate add becomes an update.** Before any `add`, the store ranks the incoming description against every existing one; a match over the similarity threshold forces an update of that file instead of creating a twin. Rephrasing the same fact five ways yields one file, freshly stamped, not five. - **Delete is first-class.** Tell Mori a fact is wrong and the file is removed, by exact name or by a unique substring. If a substring matches more than one memory, she refuses and lists the candidates rather than sweep-deleting on a vague match. - **Deletes leave tombstones.** A deletion is recorded as evidence (up to 200 kept), which propagates through sync to your other machines and to the brain's knowledge graph, the nightly pass removes the matching brain page so a deleted fact can't be resurrected from the graph later. - **Every write is scanned.** Incoming text passes the same injection-and-credential scan as before; role-fakes, "ignore previous instructions", and live secrets are refused at the write, so nothing stored now can be replayed as a command later. The older `remember` and `forget` tools still exist and now converge on the file store, a legacy `remember` becomes a well-formed `memory_write` add with the dedupe check applied. ``` You: actually the demo moved to Friday Mori → memory_write(action: "add", description: "Finn's investor demo is Friday (Jul 24, 2026)", body: "The demo moved to Friday (Jul 24, 2026).") ← merged into existing memory "investor-demo" (update-not-duplicate). ``` ## What she captures automatically After every real conversation turn, a cheap background pass on your provider's fast tier (or local Ollama when present) mines the exchange for zero to three durable facts: identity, preferences, projects and their status, tools and stack, people and pets, dates that matter, things you said you would do. Each fact is written as one short third-person sentence that leads with the names and nouns future-you would search for, the sentence doubles as the recall key. Explicitly excluded: one-off task mechanics, moods, pleasantries, and anything the repo, git, or filesystem already records. Relative dates are the classic way memories rot, "the demo is Friday" is useless three weeks later. The capture model is instructed to convert relative dates to absolute ones, and a deterministic regex pass backs it up: surviving mentions of "tomorrow", "tonight", "today", "next week", and bare weekday names are annotated with the actual date, "Friday (2026-07-24)", so the anchor never rots even if the model forgets. Capture knows when not to run: rapid-fire chat is throttled to one pass per 20 seconds, scheduled runs and subagent turns are never mined, and stranger conversations never touch your memory. Everything is fire-and-forget, capture never delays a reply, and failures are silent. There is one more capture path worth knowing about: **the pre-compaction flush**. When a long conversation is about to compact, the ~70% auto-compaction or your own `/compress`: the exact turns about to be folded away get one silent pass first: any durable facts not yet in memory are written through `memory_write` before the detail is destroyed. The flush is best-effort, it gets four seconds and never blocks compaction, but a fact it catches would otherwise have been gone. ## Pinning: memories that cannot expire Pinned memories are never auto-pruned, never dropped from the index, and always injected into every prompt regardless of relevance. Three ways to pin: - **Say so.** An explicit command, remember, don't forget, keep in mind, stores the fact as user-sourced, which pins it. Re-remembering a fact she captured automatically upgrades it to pinned. - **The Save to memory button.** Every assistant reply in chat carries a small "Save to memory" button. One tap pins up to 700 characters of that reply, stamped with the date, into permanent memory, she carries it into every future chat on every surface. Use it for gold: a workload template she nailed, a decision that must not drift. - **The Memory page.** Edit any memory file directly and set it pinned there. The inverse still holds: contradict a stored fact and she is instructed to believe you and delete or fix the stale file rather than argue with her own notes. ## Dream consolidation, and you can audit it Once a day, while the app is idle and on AC power, Mori runs a consolidation pass over everything she has accumulated. As of 1.7.0 this pass actively *fixes* rather than just reporting: - **Near-duplicate memory files merge**: descriptions overlapping past the similarity bar fold the older body into the newer file and delete the twin. - **Relative dates get absolutized** in memory bodies, referenced to each file's own write time. - **The dual store reconciles**: every memory delete tombstones the matching page in the brain's knowledge graph, and found contradictions are fixed newest-wins, not just listed. - **Playbooks and lessons dedupe**: near-duplicate approaches merge, superseded lessons drop. These stores had never been consolidated before 1.7.0. Every step is individually fenced: a failing step skips, and the sweep can never break the app. The whole pass writes a nightly report, one page per night, dated, that the Memory page's Dream log tab renders, so "what did she change overnight" is a thing you can actually read, with honest counts and noted gaps rather than invented totals. ## The Memory page Settings used to have a small memory viewer; 1.7.0 replaces it with a full Memory page, in the sidebar. Three tabs: - **Memories**: every memory file, searchable, with the exact `INDEX.md` recall table the model reads shown collapsed at the top. Open any memory to read or edit its description, type, body, and pinned flag, or delete it outright. Edits and deletes go through the same store as Mori's own writes, tombstones and all. - **Entities**: the entity registry (next section): every person, file, build, app, URL, and past chat she has learned to recognize, with how often each has been used. - **Dream log**: the nightly consolidation reports, newest first. Everything on this page is plain files and JSON on your disk. Nothing here requires an account. ## The entity resolver: slight terms find the real thing "Email the guy from the demo." "Open that site we built last week." "Find my resume." Before 1.7.0, vague references like these either got a lucky guess or a wrong one. Now they go through a universal entity registry, a persistent store of things Mori has encountered, across seven kinds: `build`, `file`, `app`, `person`, `url`, `chat`, `decision`. It fills itself from existing choke points, no new surveillance: projects she builds, files she creates or you attach, people extracted during memory capture, pages she navigates, past conversations, installed apps. The registry caps at 400 entries with usage-weighted eviction, entities you actually use stick around, and anything seen in the last 24 hours is never evicted, because "the thing we just made" is exactly what needs resolving. Resolution runs a ladder, deterministic first: exact name or alias match, then normalized containment and rarity-weighted token scoring, then, only if confidence is still low, a semantic pass through the brain when it's available, and for files a bounded recursive scan of Desktop, Downloads, and Documents (three levels deep, hard 400ms budget) so "my resume" finds `Documents/job/Resume2026.pdf` even though no one ever told her about it. Two things make this land in conversation. First, a fast pre-turn pass detects vague-reference shapes in your message and injects a **RESOLVED REFERENCES** block, candidates, their real paths/addresses/URLs, and confidence, before she starts answering. Second, the `resolve_entity` tool lets her resolve mid-task. The contract in both is explicit: below-confidence means **ask one short question, never guess**. She will say "did you mean the ChainCats site or the Mewterns one?" instead of confidently opening the wrong thing. ## Total recall: every past conversation, word for word Session memory used to mean dense one-paragraph summaries, good for "what were we doing yesterday", useless for "what was that npm flag you mentioned three weeks ago", because the flag never survived summarization. As of 1.7.0, every conversation's **full transcript** is indexed for search on your machine: a dependency-free full-text index over the raw text, with your existing history backfilled once (including, if you're signed in, history synced from your account, pulled gently in the background). Search is AND-first, results containing every query word outrank partial matches, scored by term frequency and rarity with a proximity bonus, so an exact phrase beats the same words scattered across an hour of chat. Every hit comes back with a snippet of the surrounding text. Reach it three ways: ask naturally ("what did we decide about the pricing page?"), the upgraded `search_sessions` tool, or type `/sessions ` for instant local search with no model turn. The summary-based session log still exists and still powers "pick up where we left off" openers, full-text search reaches where summaries can't. Any conversation can also leave the app: `/export` (or the `export_session` tool) writes the transcript to your Downloads folder as clean Markdown or a self-contained styled HTML page. Live credentials are **always** scrubbed on export: API-key shapes, GitHub and AWS tokens, bearer tokens and JWTs, private-key blocks, and anything labeled like a secret are replaced with `[REDACTED]` whether you ask or not, because an export is a file that leaves the machine. `--redact` additionally strips injection-shaped content using the same scanners that guard memory writes. Strangers' bot chats are never indexed and never exportable from your history. ## Long conversations Within a conversation, a recent window of messages is always sent verbatim. Older turns do not vanish: a background pass maintains a running summary of everything that has scrolled out of that window, under 200 words, dense, preserving goals, decisions, names, file paths, unresolved threads, and your preferences while dropping chit-chat. The summary is persisted per conversation and prepended to the turn context on every send. The summary refreshes incrementally as messages fall out of the verbatim window, anchored to message IDs so sliding histories cannot break the bookkeeping. The send path never waits on summarization, the summary simply lags the conversation by roughly one turn. And since 1.7.0, compaction is no longer lossy for facts: the pre-compaction flush writes durable facts to memory before older turns are folded away. The practical effect: an hours-long session keeps its goals and file paths coherent, and what mattered in it survives into every future session. ## Strangers on Discord and Telegram When Mori runs as a bot, you, the owner, are verified by your immutable platform account ID, not a display name, so impersonation is impossible. Everyone else is a stranger, and strangers face a hard wall. Before any stranger turn, memory suppression flips on: memory refreshes return nothing and the frozen memory snapshot is blanked, so not one byte of your saved memory, playbooks, lessons, or task history can enter the prompt where it could be socially engineered out of the model. Strangers also get no tools, no files, no screen, no terminal, no machine access; their answers come from the model alone. The wall runs both ways: nothing a stranger says is ever captured into your memory, their conversations are excluded from summarization, the pre-compaction flush checks for stranger sessions and skips them, and their chats never enter the full-text session index. Strangers do get their own memory, just never yours. Each non-owner gets a per-user history keyed to their platform account ID, kept to a 36-message window, so Mori greets returning server members by name and picks up where the last chat left off. Your own bot chats work the same way per channel: a 36-message live window backed by a stable conversation ID, restored in full after an app restart. And approval gating survives the distance, irreversible or costly actions from a bot chat still require your explicit yes, with typed approval IDs that expire in two minutes and fail closed on anything but a clear yes. # Automation & schedules > A real clock, in the main process: cron expressions, one-shots, and intervals that keep time even with the window closed. Plus silent heartbeat checks and a localhost webhook to poke her from outside. ## The clock lives in the main process Before 1.7.0, scheduled tasks ran on a timer inside the renderer, close the window and the clock died with it. Now the scheduler is a main-process service: it loads its jobs from a plain JSON file in the app's data folder, drives a 30-second tick, and hands due jobs to the app's normal unattended run path. As long as the app process is alive, window closed, minimized, whatever, schedules fire on time. If the app isn't running at all, nothing fires; the catch-up policy below handles what happens when it comes back. Jobs persist atomically (written to a temp file and renamed, so a crash can never leave a half-written schedule file), and there is a cap of 100 live jobs. Spent one-shots are kept as disabled receipts rather than deleted, and they don't count against the cap. ## Three ways to say when A schedule is one string, in one of three forms: - **A 5-field cron expression**: `minute hour day-of-month month day-of-week`, numeric only. `30 8 * * 1-5` is weekdays at 8:30am, local time. Supported per field: `*`, `*/n`, `a`, `a-b`, `a-b/n`, and comma lists of those. Not supported, and rejected with an honest error instead of silently misparsing: month and day names (`JAN`, `MON`), `@macros` (`@daily`, `@reboot`), `L`/`W`/`#`, seconds fields, and timezones. Everything runs on the machine's local clock. - **`at:`**: a one-shot. Fires once, then the job flips to disabled (kept as a record). A timestamp in the past is allowed: an "at now" one-shot fires on the next tick. - **`every:`**: a simple interval, whole minutes, minimum 1. The standard cron day rule applies: when both day-of-month and day-of-week are restricted, a date matches if either does. An expression that never matches a real date within five years, say, `0 0 31 2 *`: is rejected at save time, not silently scheduled forever. ## Missed runs: catch up once, never replay The machine sleeps, the app quits, three hours pass over an `every:30` job. On wake, that job fires **once**, not six times, the next run is always recomputed from now after a fire, never from the missed slots. This is the difference between "catch me up" and a queue of stale replays hammering you the moment the lid opens. A job can also opt out of catch-up entirely with `catchUp: false`, for work where a stale fire is worthless, a missed run is simply skipped and the job reschedules. Heartbeats (below) use this: a health check from two hours ago tells you nothing. The lateness threshold is 90 seconds, three ticks, so ordinary jitter never trips it and real sleep or quit gaps always do. ## What unattended runs are allowed to do Every scheduled run, cron, one-shot, interval, heartbeat, webhook-triggered, carries a safety prefix ahead of its prompt, verbatim and non-negotiable: the run is told it is executing unattended, that you are not there to approve anything, and that it must not send messages or emails, make purchases, or take any irreversible action. Gather, summarize, draft, prepare, then report. This is the same rule the older `schedule_task` runs carried, and it applies identically to jobs fired through the webhook. If you want Mori to actually send the weekly summary email, the scheduled run drafts it and the send happens when you're present to say yes. ## Heartbeats: silence is the success signal A heartbeat is a cron preset, not a new subsystem: an `every:30` job with `catchUp: false` and a quiet contract. The job reads a checklist file, `heartbeat.md` in the app's data folder (`~/Library/Application Support/Studio Mori/heartbeat.md` on macOS, `%APPDATA%\Studio Mori\heartbeat.md` on Windows): and runs each check with read-only tools. You write the checklist in plain language: is the dev server up, did the overnight build finish, is disk space okay. The contract: if the file doesn't exist, or every check passes, the run replies with exactly `HEARTBEAT_OK`: and that reply triggers **total silence**. No notification, no saved transcript, nothing. Only when something genuinely needs your attention do you hear about it, as one short paragraph describing the problem. An all-green heartbeat leaves no trace beyond a single audit line. ``` # heartbeat.md - is anything listening on port 4181? - does ~/Desktop/mori/firstapp build output from last night contain the word "error"? - is free disk space above 20GB? ``` ## Asking for a schedule in chat You don't write cron strings yourself unless you want to. Tell Mori "check my dev server every half hour" or "every weekday at 8:30 summarize my kanban board" and she calls the `schedule_cron` tool with the prompt and schedule filled in, the tool takes a 5-field cron expression, `at:`, or `every:`, plus a heartbeat flag for quiet jobs. Invalid schedules come back with the parser's specific complaint ("month value out of range", "@macros are not supported"), so a bad request fails loudly at creation, not silently at 3am. Jobs created under the old renderer scheduler are migrated to the main-process service once, automatically, with nothing firing twice and nothing dropped. ## Waking Mori from outside: the localhost webhook The MCP server's listener (the same one that exposes Mori's tools to other agents, see MCP, both directions) gains one endpoint: `POST /hooks/wake` on `127.0.0.1`. It is **off by default**, separately from the MCP server itself, and flipping it on requires editing `hooksEnabled` in the server's config, no restart needed, the flag is re-read per request. A call must carry the server's bearer token and either a `jobId`: fire that existing cron job now, or a `prompt`, which becomes an immediate one-shot run. Either way the run goes through the exact same unattended fire path as any schedule, safety prefix included, so a webhook poke can't do anything a scheduled run couldn't. Guardrails: the listener binds to localhost only (nothing off-machine can reach it), requests are rate-limited to 10 per minute, and a bad token gets a flat 403. What this is for: a git post-push hook that has Mori review the diff, a CI script that pokes her when a build finishes, a Shortcuts automation on your Mac. What it is not: a public API. There are no tunnels and no cloud relay, if you want remote triggering, that's what the bots and Mori Link are for. ``` curl -X POST http://127.0.0.1:4519/hooks/wake \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{"prompt": "the deploy just finished, check the\n site loads and summarize anything broken"}' ``` ## Honest limits - **The app must be running.** The main process survives window close, but not quit. A machine that was off all weekend fires each missed job once on launch (unless the job opted out). - **Local time only.** No timezone field, no DST-aware expressions, the machine's clock is the clock. - **Minute resolution.** The tick is 30 seconds and cron has no seconds field; don't schedule anything that needs sub-minute precision. - **Numeric cron only.** `MON`, `@daily`, `L`, `W`, `#` are rejected with an error that says exactly what is supported. - **Unattended means restricted.** No sends, no purchases, no irreversible actions, by design, on every path, with no way to opt out of the prefix. # Workspaces > Fully isolated brains under one account. Memory, chats, skills, board, goals, and audit all separate. Built for people who run more than one life. ## What a workspace is A workspace is a complete, separate instance of Mori's mind. Switching workspace switches **everything at once**: memory, conversations, skills, playbooks, lessons, goals, the task board, schedules, and the audit log. Nothing crosses over. Use them the way an agency uses folders: `Client A`, `Client B`, `Personal`, `Trading`. What Mori learns about one client, names, credentials context, project details, custom skills, stays in that client's workspace, provably: each workspace's data lives in its own set of files on your disk. ``` ~/Library/Application Support/Studio Mori/ memories.json ← Default workspace memories--ws-client-a.json ← Client A skills/ ← Default skills skills--ws-client-a/ ← Client A skills board--ws-client-a.json ← Client A task board ``` ## Switching and creating The switcher lives at the bottom of the sidebar. Pick a workspace from the dropdown and the app remounts instantly, no restart, no sign-out. Click **+**, type a name, hit enter; the new workspace starts clean with the four built-in skills seeded. Your last-used workspace is remembered across restarts, and the Home screen always shows which one you're in. ## What stays shared Deliberately, a few things are account-level, not workspace-level: - **API keys and provider settings**: one set of keys powers every workspace. - **Your Supabase account and cloud sync**: sync binds to the account; workspaces are local separation. - **Bot connections** (Discord/Telegram/phone): the bridges run once for the app. Everything the model *knows*, memory, skills, sessions, tasks, is per-workspace. ## Teams and businesses This is the shape that lets a business run Mori seriously: one operator, many isolated contexts. Each engagement gets a brain that accumulates its own skills and memory, a board whose cards never mix with another client's, and an audit trail scoped to that engagement, reviewable, exportable, deletable as a unit. Combined with the Board's human-approval gate (only you can stage a card into Ready), a workspace is a safe blast radius: even Full Auto decisions stay inside the workspace they were made in. # Goals > Durable objectives with checkable success criteria: persisted locally, injected into every turn, and scored by a background judge that fails closed. ## What a goal is A goal is a long-lived objective that spans many tasks and sessions, "finish the product launch", "keep the docs accurate", not a one-off request. Every goal carries two required parts: an **objective** stated concretely, and **success criteria** that describe how Mori will know it is done. A goal without checkable criteria cannot be created; `create_goal` rejects the call if either field is missing. Goals are plain records in a local JSON file under the app's user-data directory (per profile, capped at the most recent 200). Nothing about a goal leaves your machine except the model calls you already make with your own API keys. Restarting the app changes nothing: goals load from disk on every turn. Each record tracks `status` (`active`, `paused`, `complete`, `blocked`, `failed`), `progress` (0 to 100), an iteration counter against a hard cap, the current blocker, the single next action, the latest judge verdict, and completion evidence. ## The three tools Mori manages goals through three tools, and you can edit the same store from the Goals UI. Both paths write through the same upsert, so neither clobbers the other. - `create_goal(objective, success_criteria, max_iterations?)`: creates an active goal at 0% with a default iteration cap of 25. Returns the new id (`g_` prefix). - `update_goal(id, ...)`: records progress after meaningful work: progress % (clamped 0 to 100), status, blocker, next action, or completion evidence. The `id` argument also accepts a unique fragment of the objective text. - `list_goals()`: one line per goal: id, status, progress, blocker, next action. Read-only, always permitted in agent mode, and safe to run in parallel with other read tools. Two behaviors worth knowing: setting `evidence` when progress is already 100 auto-flips the status to `complete`, and every `update_goal` call increments the iteration counter, the same counter the judge uses. ## Example A typical lifecycle, as the agent runs it. Evidence is expected at completion, file paths, URLs, verified results, and the tool descriptions instruct the model to never mark complete without it. ``` create_goal({ objective: "Migrate the docs site to the new domain", success_criteria: "All pages load at docs.example.com with zero 404s", max_iterations: "12" }) → Created goal g_lxo3f2: "Migrate the docs site...". Track it with update_goal. update_goal({ id: "g_lxo3f2", progress: "60", next_action: "Update DNS CNAME" }) → Updated goal g_lxo3f2: 60% · active · next: Update DNS CNAME update_goal({ id: "g_lxo3f2", progress: "100", evidence: "Crawled 214 pages at docs.example.com, 0 errors (crawl.log)" }) → Updated goal g_lxo3f2: 100% · complete ``` ## Goal awareness on every turn Goals are not write-only bookkeeping. At the start of every turn Mori reloads the store, then injects your active and blocked goals into the turn context that rides along with your newest message, up to six, each with its id, objective, progress, current blocker, and next action. The injected block carries a standing instruction: make progress on the relevant goal, call `update_goal` when you do, and mark complete only with evidence. The context is attached to a copy of the message, never to stored history, so your conversation log stays clean. ## The judge: every turn is scored After each real turn, a background evaluator scores your active goals against what just happened. It is one cheap call on the fast variant of your configured provider, your own key, small token budget, covering up to three active goals at a time. For each goal it returns a progress score, a verdict (`continue`, `complete`, or `blocked`), a one-line next action, and a one-sentence reason, which lands on the goal as `latestEval`. The judge is deliberately conservative and safe to run unattended: - **Progress is monotonic**: the judge can raise progress but a verdict can never lower it below what the goal already recorded. - **Complete requires evidence**: the prompt instructs it to say `complete` only when the success criteria are demonstrably met by evidence in the turn, and completion stamps an evidence note on the goal. - **Single-flight and merge-safe**: overlapping judges skip rather than clobber each other, and results merge onto the latest stored state, not the pre-turn snapshot. If something completed or blocked the goal mid-judge, the verdict is discarded. - **Never breaks a reply**: judge failures are swallowed silently, and the judge never runs for guest (stranger) chats. The judge is a scorer, not an actor. It updates goal state; it never triggers tools, and any actual work toward a goal still happens inside normal turns under the normal approval gates. ## Iteration caps fail closed Every goal has a hard iteration cap (default 25, settable at creation, minimum 1). Both write paths count against it: each `update_goal` call and each judged turn increments the counter. When an active goal reaches its cap, the system fails closed rather than looping forever. The judge flips the goal to `blocked` and appends an explicit blocker, `iteration cap (N) reached`: and the `update_goal` result warns that the goal needs your review before continuing. A capped goal is never silently continued; blocked goals stay visible in the turn context with their blocker, so Mori will surface the situation instead of grinding on it. You unblock by reviewing the goal and setting it back to `active`. ## Goals vs board tasks Goals and the task board solve different problems. Use a **goal** for an objective that outlives any single session and needs a definition of done. Use a **board task** for a discrete unit of work you want tracked or queued on the Kanban, tasks have lanes (`inbox` through `archived`), priorities, and results, and can link back to a goal via `goal_id`, so the natural pattern is one goal broken into several linked cards. The board carries a safety invariant that goals do not need: the agent can only file cards into `inbox`, `planned`, or `waiting_user`. Only you can stage a card into `ready`: the lane the dispatcher auto-runs, by dragging it on the Board. If the model asks for `ready`, the card is re-filed as `planned` instead. Goal bookkeeping itself is metadata-only and low-risk, but it still runs inside the permission system: the goal tools sit in the file-writing category, are disabled entirely at the `safe` permission level, and any risky action taken in service of a goal goes through the same per-action approval gates as everything else. # 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. # Command Deck > Run up to four live chats side by side, each with its own conversation, model, and composer. Broadcast one prompt to all of them and promote the winner. ## Single, 1+1, and 2×2 The Command Deck lets you split the chat surface into more than one live session. There are three layouts, and you switch between them from a small control in the chat header: - **Single**: one chat, exactly the classic experience. - **1 + 1**: two panes side by side. - **2 × 2**: four panes in a grid. Single isn't a special case bolted on, it *is* the primary session, byte-for-byte the single chat you've always had. Growing to a split or a quad adds panes around it; collapsing back leaves the primary chat untouched. Command Deck shipped in 1.5.0. ``` Single 1 + 1 2 × 2 ┌────┐ ┌──┬──┐ ┌──┬──┐ │ │ │ │ │ │ 1│ 2│ │ │ │ 1│ 2│ ├──┼──┤ │ │ │ │ │ │ 3│ 4│ └────┘ └──┴──┘ └──┴──┘ ``` ## Every pane is its own session A pane isn't a view of one shared conversation, each one is a fully independent chat with its own state: - **Its own conversation.** Each pane's header carries a picker to start a fresh chat or swap in any saved conversation, searchable once you have a few. - **Its own model.** A pane can override the model from its composer's model chip; leave it alone and it follows your global model. The header shows which model that pane is on. - **Its own composer, stage, and status.** Each pane types, thinks, and streams on its own, with its own run spinner and thinking-stage tag. Click a pane to focus it, or jump with **Cmd/Ctrl + 1 to 4**: the number in each pane's corner is its shortcut. The focused pane owns the overlays and conversation openers, and gets a highlighted border so you always know where your keystrokes are going. ## Truly concurrent The panes don't take turns. The whole chat engine was made session-scoped so they can run at the same time without stepping on each other. Everything that used to be a single "which run am I" global, the abort controller, the task state, the steering queue, the per-turn scratch, the failure lessons, the compaction keys, is now held per-session. Two panes sending at once each drive their own turn to completion; neither one's Stop button, streaming tokens, or usage accounting bleeds into the other. That re-entrancy is the whole reason the Deck can exist, and it's why the Single preset stays exactly the chat it always was: it's just the primary session running alone. ## Broadcast: ask everyone at once Above a multi-pane deck sits the Broadcast bar. Type a prompt there once, hit send, and it fans out to every pane at the same moment, and because each pane runs on its own model, you get the same question answered several ways in parallel. Before you send, a cost hint sums the estimated spend across every pane at its own model, so you see up front what firing one prompt at four models will cost. You're the aggregator: the answers land side by side for you to read and compare. Nothing is merged or auto-judged, the comparison is yours to make. ## Promote the winner: nothing is deleted Once you've compared the answers, promote the one you want. The Promote control on a pane collapses the deck back to Single with that pane as the primary session, you carry the winning conversation forward and drop back into a normal single chat. The other panes' conversations are **archived, never deleted.** Promote flags the losing broadcast targets as archived in both the local and signed-in stores; they drop out of your active list but stay fully recoverable. A bake-off never costs you the runners-up, you keep every branch you explored. ``` Broadcast → "which of these three headlines is strongest, and why?" pane 1 pane 2 pane 3 (model A) (model B) (model C) Compare → read all three answers side by side Promote ▲ → keep pane 2 → collapses to Single panes 1 & 3 archived (recoverable, not deleted) ``` ## When to reach for it The Deck earns its place whenever one chat isn't enough: - **Model bake-offs.** Put a different model in each pane, broadcast one prompt, and compare how they handle it, then promote the best answer and keep going. - **Parallel workstreams.** Build in one pane while you research in another, or keep a long-running task in pane 1 and hold a normal conversation in pane 2. Because sessions are isolated, one pane grinding away never blocks the others. - **Branching a decision.** Fork the same question three ways, explore each, and keep the branch that pans out without losing the rest. And when you don't need any of it, Single is right there, the classic single chat, unchanged. # Theme Studio > Reskin the entire app in one click. Pick one of six presets, then tune any color, corner, or type size. It applies live everywhere, and imported themes are validated as data, never run as code. ## One click, the whole app Theme Studio is a runtime skin for Mori. Under the hood the app's colors are CSS custom properties on every surface; Theme Studio writes new values for them straight onto the app, so a single change cascades to the sidebar, chat, workbench, and every panel at once, no restart, no reload, nothing to rebuild. Open it from **Systems → Theme Studio**. Your choice is saved locally and re-applied before the interface even paints on the next launch, so there's no flash of the old look while the app boots. - Presets and edits apply **instantly** as you click and drag. - Everything persists across restarts on this machine. - Nothing here touches your account or syncs, a theme is a local preference. ## The six presets Six curated looks ship built in. The preset gallery shows a live mini-preview of each, its real sidebar, panel, and swatch colors, so you can see a theme before you commit to it. - **Mori**: the signature icy-blue look. Calm, neutral, pro. The default. - **Midnight**: deep black-blue. Maximum contrast, minimum glare. - **Paper**: clean light mode. Ink on paper, gentle on the eyes. - **Terminal**: green-on-black CRT. Phosphor glow, monospace everything, tightened corners. - **Sakura**: soft pink on warm dark. Petals at dusk. - **Slate**: neutral grey. No hue, no distraction, all signal. **Terminal** is a full skin, not just a color swap: it switches the entire interface font to monospace, tightens the corner radius, and turns on a subtle CRT flourish, the app keys that off the active theme so the look stays coherent. ## The token editor Every preset is a starting point you can edit. The editor exposes each color as its own swatch, click the chip for a color picker, or type an exact value into the hex field beside it. Tokens are grouped so the picker stays legible: - **Surfaces**: background, secondary, tertiary, input, hover, and elevated fills. - **Text**: primary, secondary, and muted. - **Borders**: border, focus border, and subtle border. - **Accent**: the accent, its hover shade, and a secondary accent. - **Semantic**: danger, danger-hover, success, and warning. Beyond color there are three shape-and-type knobs: a **corner-radius** scale, a **font-size** scale, and a **Sans / Mono** switch for the interface font. The accent glows used around focused and active elements are **derived automatically from your Accent color**, so a single accent change re-tints the whole app consistently, you never tune those by hand. The moment you edit any token, the theme forks into a **Custom** theme, still tagged with the preset you started from (so the Terminal flourish and labels stay coherent). A **Reset** button snaps you back to the clean preset whenever you want the original. ## Export and import A finished theme is portable. From the header you can **Copy JSON** to the clipboard or **Export** it as a `.json` file to share, back up, or move to another machine. **Import** opens a drawer where you paste a theme's JSON and hit Apply, it loads instantly as your active Custom theme. The export is a plain, human-readable object: the base preset it came from, every color token, and the radius, font-size, and font-family values. ``` { "$schema": "mori-theme/v1", "presetId": "terminal", "colors": { "bg-primary": "#010401", "text-primary": "#b6ffc4", "accent": "#35ff6b" }, "radiusScale": 0.55, "fontScale": 1, "fontSans": "'JetBrains Mono', 'Cascadia Code', monospace" } ``` ## Imported themes are data, not code A theme file comes from outside the app, so Theme Studio treats every imported value as **untrusted data and parses it strictly**: it can never smuggle anything executable into the interface. - Only **recognized token keys** survive the import; anything else is dropped. - A color is accepted only if it's a plain hex or a numeric `rgb`/`rgba`/`hsl`/`hsla` value. Anything containing `url(...)`, a CSS expression, `javascript:`, or braces is **rejected outright**: an imported theme can never trigger a resource load. - Font values must be plain family names and separators; the size and radius scales are clamped to safe ranges. - A file that carries **no recognized tokens at all** is refused with a clear reason, so pasting the wrong JSON fails loudly instead of silently breaking your look. The result: you can trade themes freely without trading trust. The worst a bad file can do is get rejected. # Every surface: Discord, Telegram, phone, voice > Mori lives on your desktop. Discord, Telegram, and the Mori Link phone app are remote controls for the same brain, with the same memory, the same tools, and the same guardrails. ## One brain, many surfaces Every remote surface runs through a single bot engine with a thin transport layer per platform: Discord, Telegram, and the Mori Link phone app all deliver messages into the same handler and get replies, images, files, and audio back through their own channel. There is no separate cloud agent, the message travels to the Mori app running on your machine, and Mori acts on your actual computer: your files, your browser, your terminal, your screen. Surfaces can run on their own model and key. If you set a bot provider, model, or API key in Settings, remote chats use those; otherwise they inherit the desktop's active provider. Keys are yours in both cases. Mori is bring-your-own-keys everywhere, and nothing routes through a Studio Mori server for inference. Reliability is treated as a safety property: every outbound send retries three times with backoff before failing, and if a task errors, Mori tells you what happened and offers a retry. A surface never goes silent. ## Owner recognition and locking Mori distinguishes exactly two kinds of people: the verified owner, and everyone else. On Discord and Telegram, verification is anchored to the platform's immutable account ID, not a display name. You set your username once in the app; the first time that user messages the bot, Mori captures their account ID and locks it as the owner ID. From then on the ID is the sole source of truth, so someone copying your display name or handle gets nowhere. The phone app skips this entirely: Mori Link is signed into your own Studio Mori account and must complete an explicit pairing handshake, so every message from a paired phone is treated as the owner by construction. Only the owner can run tasks, touch the machine, or approve anything. The split is enforced in the engine before a single tool is available, it is not a prompt-level suggestion. ## What the owner can do remotely As the verified owner, a DM to Mori is equivalent to sitting at the desktop chat. Mori has her full toolset: open and search the web, read and write files, run terminal commands, launch apps, drive the browser, and build projects. Specific remote behaviors: - **Build screenshots.** When Mori finishes building something (a site, a page, an app), she auto-opens it in a preview and sends a screenshot of each section, scrolling through so reveal animations actually fire, with the file path in the caption. "Show me what you built" reopens and re-sends the most recent build. - **On-demand screenshots.** Screenshots are sent only when you explicitly ask. Mori parses the target: a named window ("screenshot the terminal"), the full desktop (only when you say "full screen" or similar), or, if it's ambiguous, she lists visible windows and asks which one. - **Real file delivery.** On Discord, "send me the zip" attaches the actual file, resolved by name or by "the one you just made", not a screenshot of it. - **Voice notes.** If you ask to hear it ("say it out loud", "send a voice note"), the reply is synthesized and sent as audio. Voice is never unsolicited, and can be disabled in Settings. - **Full Auto toggle.** `/auto on` from any surface runs tasks end-to-end without check-in questions; `/auto off` restores normal check-ins. Irreversible actions still require confirmation either way. Each chat keeps its own memory: the last 36 messages are held per channel and persisted to your account under a deterministic conversation ID, so after an app restart Mori restores the full thread and stays consistent with what she already did. ## Everyone else: stranger mode Anyone who addresses Mori in a server, by mention, reply, or "hey mori", gets a conversation. What they never get is your machine. For non-owners: - **No tools, period.** Answers come from the model API only. No screenshots, no files, no terminal, no browser, no machine actions, the engine strips the toolset before the call, it doesn't rely on the model declining. - **No owner data.** Mori never reveals your identity, files, screen, conversations, memory, or settings, and treats requests to "ignore your rules" as untrusted data to politely decline. - **Their own memory, isolated from yours.** Each non-owner gets a persistent per-user history keyed to their immutable account ID, so returning members are remembered by name across restarts. Their chats are also explicitly barred from writing into your global memory, a stranger cannot plant facts that Mori later treats as yours. ## Approvals: typed, expiring, fail-closed Any risky step mid-task, a spend, an irreversible action, a purchase, pauses the task and asks you in chat. Approvals are typed objects, not loose pattern-matching on whatever you say next: - Every request carries a unique ID and is quoted back to you with its terms. - It expires in **2 minutes**. No reply means no. - Only a clear yes ("yes", "confirm", "go ahead", "approve", …) approves. An explicit no denies. Anything else is treated as a no, and Mori tells you so. - Every decision, approved, denied, unrecognized-treated-as-deny, expired, is written to the durable audit log with the approval ID and the surface it came from. Purchases get the fullest version of this flow: Mori sends the cart summary with a screenshot, states the payment label, and only places the order after an explicit yes. She never enters credentials or clicks a final purchase button on her own. ``` you › order that usb-c hub mori › Anker 555 hub: $34.99, arrives Thu Ready to place: Visa ····4242. Reply "yes" to place the order. (approval apr_lx2k9f3a, expires in 2 minutes; anything but a clear yes is a no) you › hmm maybe mori › Didn't catch a clear yes: treating that as a NO for safety (approval apr_lx2k9f3a). ``` ## Stopping and steering a running task A bare **"stop"** (or "cancel", "abort", "halt", "never mind") from any surface halts the running task immediately, through the same machinery as the desktop Stop button. Mori confirms the halt, logs the cancellation to the audit log, and offers to pick the task back up. Messages that arrive while a task is running don't bounce, they fold into the live task as steering notes ("make the header darker" mid-build lands in the build). If a note can't be folded in, Mori says she's still working rather than dropping it. And a wedged task can never lock a chat forever: task locks expire after 10 minutes, after which new messages are taken normally. Long jobs, builds, shopping runs, deploys, get a short "on it" acknowledgment only if they're genuinely still running after a few seconds, so quick questions never get a spurious ack. ## Mori Link: pairing your phone The phone app talks to your desktop over a realtime channel scoped to your signed-in account, but being on the account is not enough to issue commands. Pairing is an explicit handshake: the phone displays a 6-digit code, you type it into the desktop's "Link a phone" screen, the phone validates the claim, and the desktop stores that phone's unique ID. From then on, commands are executed only if they carry the paired phone's ID, anything else on the channel is dropped before it reaches the engine. Either side can unpair at any time, which clears the stored ID immediately. While a task runs, the desktop streams live status lines to the phone, what Mori is doing and which step she's on, throttled so long tasks don't flood the app. Screenshots and build previews are downscaled and compressed on the desktop to fit the realtime payload cap, so results land on your phone as they happen. The desktop also announces its presence on the channel (machine name and online state), so the app can show whether your computer is reachable before you send anything. # MCP, plug anything in, plug Mori in anywhere > Mori speaks the Model Context Protocol in both directions: any MCP server's tools become hers, and she exposes a curated, token-guarded tool surface for other agents. ## Two directions, one protocol MCP support in Mori is symmetrical. As a **client**, Mori connects to any Model Context Protocol server, filesystem, GitHub, Slack, Postgres, web search, and merges its tools into her own dispatch, so they work identically on every model provider you bring keys for. As a **server**, Mori exposes a small, curated tool surface on localhost so other agents (Claude Code, IDEs, scripts) can read your machine through her, see her Kanban board, and file tasks for you to triage. Both directions follow the same rules as everything else in Mori: local-first (configs and tokens live on your machine, never a cloud account), bring-your-own-keys, and approval-gated for anything risky. Everything is managed from Settings → Connections. ## Connecting a server to Mori In Settings → Connections, give the server a short name and paste its config as JSON. Mori uses the exact `claude_desktop_config.json` shape, so any published server config works verbatim, no translation. Local servers use `command`/`args` (plus optional `env` and `cwd`); remote servers use `url` (plus optional `headers`). A `"disabled": true` flag parks a server without deleting it. On disk, the config is a single file, `mcp.json` in Mori's user-data directory, with the standard `mcpServers` wrapper. You can edit it directly and hit **Reload all** in the card, or add and remove servers one at a time from the UI; each edit reconnects only the server it touches. The card shows a live status dot per server (connecting, ready, error, disabled), the tool count, the discovered tool names, and the exact error message if a connection failed. ``` // Settings → Connections → Add a server // Name: filesystem { "command": "npx", "args": ["-y", "@modelcontextprotocol/server-filesystem", "/Users/you/Documents"] } // The same server in mcp.json (userData): paste-compatible with Claude Desktop: { "mcpServers": { "filesystem": { "command": "npx", "args": ["-y", "@modelcontextprotocol/server-filesystem", "/Users/you/Documents"] } } } ``` ## Transports: stdio, Streamable HTTP, SSE fallback A config with `command` starts a **stdio** server as a child process. Mori resolves your login-shell `PATH` once at startup, because GUI apps launched from Finder do not inherit it, `npx`, `uvx`, nvm-managed node, and Homebrew binaries all work without configuration. Stdio servers receive a minimal environment: the resolved `PATH`, a safe baseline (`HOME`, `USER`, `LANG`, and similar), and whatever you put in the server's own `env` block. They never see Mori's full process environment, so your API keys cannot leak into a third-party server. A config with `url` connects over **Streamable HTTP** first. If that connect fails, legacy SSE-only servers reject the Streamable POST with a 404 or 405. Mori automatically retries the same URL with the **legacy SSE transport**. You do not declare the transport; the fallback is handled at connect time. Custom `headers` (for example an `Authorization` bearer) are sent on either transport. ``` // Remote server over Streamable HTTP (SSE fallback is automatic): { "url": "https://example.com/mcp", "headers": { "Authorization": "Bearer YOUR_KEY" } } ``` ## How foreign tools behave inside Mori Discovered tools are namespaced `mcp____` and merged into Mori's tool dispatch, which means they flow into both her OpenAI-shaped and Anthropic-shaped schemas automatically, one config, every provider. Tool descriptions are prefixed with the server name so the model always knows where a capability comes from. MCP tools sit inside Mori's permission system, not beside it: - Agent mode must be on for any MCP tool to be callable. - At the read-only **Safe** permission level, MCP tools are blocked entirely. - Destructive actions go through Mori's typed approval ladder like her native tools, nothing risky runs on a nod. - Tools are only executed in parallel when the server marks them `readOnlyHint` in its annotations; anything unannotated runs sequentially. Results are normalized into Mori's tool-result convention: text blocks are joined, `resource` blocks are inlined with their URI, and an `image` block is converted into a real vision block so the model can actually see it. ## Reliability: budgets, pagination, hot-reload All configured servers connect in parallel after the window opens, best-effort, a dead or slow server never blocks the others or Mori's startup. Each connection gets a **15-second budget**; each tool call gets **60 seconds**. Timeouts and failures come back to the model as plain `error:` strings rather than crashes, so Mori can route around a flaky server mid-task. - **Pagination**: tool discovery follows the spec's `nextCursor` loop, so servers with large tool catalogs are listed completely (hard cap of 2,000 tools per server, so a broken cursor cannot spin forever). - **Hot-reload**: when a server announces `notifications/tools/list_changed`, Mori re-fetches its tool list on the spot, no reconnect, no restart. - **Manual control**: **Reload all** tears down every client and reconnects from the current config; removing a server closes its client and drops its tools from dispatch immediately. ## Mori as a server: the curated surface Flip on **Mori as a server** in Settings → Connections and Mori serves MCP over Streamable HTTP at `http://127.0.0.1:4519/mcp`. It is fail-closed by design: **off by default**, bound to localhost only, and every request must carry a bearer token that is generated once and shown (masked, with copy buttons) in the card. Requests are stateless, one fresh server-transport pair per POST, nothing shared between clients. Version 1 exposes exactly seven tools, each mapped onto the same execution core Mori's own brain uses: - `mori_status`: app version, platform, agent availability - `mori_read_file`: read a text file (read-only) - `mori_list_files`: list a folder (read-only) - `mori_search_files`: find files by name under a folder (read-only) - `mori_board`: read the full Kanban board, every lane and card - `mori_create_task`: add a card to the board - `mori_goals`: read the persistent goals list There is deliberately no terminal, no file write, no send, and no computer control on this surface. The only write, `mori_create_task`, lands cards in the board's **Inbox** lane, an external agent can ask for work, but nothing executes until you move the card through triage yourself. ## Connecting Claude Code to Mori Turn the server on, copy the token from the Settings card, and register Mori as an HTTP MCP server. From then on, Claude Code sessions can check what Mori is doing, read files through her, inspect her board and goals, and queue tasks into her Inbox. ``` # One-time setup: paste the token from Settings → Connections → Mori as a server claude mcp add --transport http mori http://127.0.0.1:4519/mcp \ --header "Authorization: Bearer " # Then, inside Claude Code: # mori_status -> {"app":"Studio Mori","version":"…","platform":"darwin","tools":7} # mori_create_task -> lands in Mori's Inbox for you to triage ``` ## Files and defaults at a glance - **Client config**: `mcp.json` in Mori's user-data directory, `claude_desktop_config.json` shape (`mcpServers` key; `servers` also accepted). - **Server config**: `mcp-server.json` in the same directory, `enabled` flag plus the generated token. Regenerate by deleting the file while the server is off. - **Defaults**: client has no servers configured; Mori-as-server is disabled; port `4519`; endpoint accepts `POST /mcp` only and answers `401` without the exact bearer token. - **Scope**: both files stay on your machine. Nothing about your MCP setup is synced or sent anywhere. # Autonomy ladder > Guided, Trusted, Commander: how much Mori may run without stopping to ask you, set by you and only you, above a hard safety floor that no level can cross. ## Three levels The autonomy ladder decides how far Mori can go on shell commands before it has to stop and get your yes. There are three rungs: - **Guided** (the default): asks before anything that changes things. Every dangerous command raises a confirmation dialog. - **Trusted**: runs routine work on its own, and auto-runs the specific dangerous commands you've whitelisted, while still confirming anything you haven't. - **Commander**: full autonomy for a time-boxed window: dangerous commands run without a prompt until the grant expires. Every one of them is written to an append-only ledger first. This is a different, newer control from the **permission levels** (safe / standard / full) covered in Security & approvals. Permission levels decide *which categories of tool exist at all* for a session; the autonomy ladder decides *how much confirmation* command execution needs. Both are real and both apply at once. Shipped in 1.3.0 SOVEREIGN. ``` Guided · asks before anything that changes things Trusted · runs routine work; auto-runs your whitelisted dangerous commands Commander · full autonomy for a time-boxed window (every action ledgered) ``` ## How a command is classified Every command that reaches a shell, whether through `run_terminal_command` or a `terminal_run` in a named session, passes one chokepoint that sorts it into three tiers. Before matching, the command is de-obfuscated so a disguise can't slip past: quotes used to split tokens are stripped, line-continuations collapsed, unresolved `${VAR}` expansions blanked, and a `base64 -d | sh` payload is decoded and inspected for what it actually runs. - **ok**: ordinary commands. They run. - **dangerous**: legitimate but destructive: `sudo`, any recursive or forced `rm`, `git push --force`, `git reset --hard`, `git clean -f`, `chmod 777`, `killall` / `pkill`, `npm uninstall` / `npm prune`. These are what the ladder gates. - **hardline**: the un-bypassable set (below). Refused at every level. How a dangerous command is treated depends on your level: Guided always confirms it; Trusted auto-runs it only if it matches one of your allow-globs; an active Commander grant runs it and records it. ## The hardline floor never moves Some commands are never worth the risk, so they are refused outright, at Guided, at Trusted, and even under an active Commander grant. There is no setting, glob, or mode that turns them on. The hardline patterns are: a recursive delete of `/` or `~` (or `$HOME`), formatting or raw-overwriting a disk (`mkfs`, `dd of=/dev/…`, `diskutil erasedisk`), a fork bomb, a power-state change (`shutdown`, `reboot`, `halt`), killing Mori or its updater process, and piping a download straight into a shell (`curl … | sh`). If you genuinely intend one of these, Mori tells you to run it yourself in a real terminal. This floor is the thing the ladder is built on top of. Commander removes the *prompts* on dangerous commands; it does not, and cannot, lower this line. ``` > terminal_run { name: "cleanup", command: "rm -rf ~" } Blocked (hardline safety): "rm -rf ~" matches an un-bypassable danger pattern (recursive delete of / or ~, disk format/overwrite, fork bomb, shutdown/reboot, killing Mori/its updater, or curl-piped-to-shell). This is never run, not even in commander mode. Run it yourself in a real terminal if you truly intend it. ``` ## Commander is a deliberate, expiring grant You cannot drift into Commander. Reaching it takes a double opt-in: you explicitly choose Commander, then confirm in a dialog and pick how long the grant lasts. The main process refuses to elevate without that explicit confirmation, so a stray click, or anything Mori reads, can never flip it on. - **Time-boxed.** The confirm dialog offers 15, 30, or 60 minute grants (a grant is capped at 24 hours). When it expires, autonomy drops back on its own. - **A live countdown.** While a grant is active, a countdown chip shows exactly how much time is left, with an X to **revoke** it instantly. Revoking ends the grant and returns you to Guided. - **You can steer it any time.** The autonomy chip in the composer cycles the level; the `/autonomy guided | trusted | commander` slash command does the same. Choosing Commander either way still routes through the same confirm-and-grant dialog. Crucially, the level and the grant are set *only* from the settings UI. Tool output, web pages, memory, and message content are always treated as data, none of them can change your autonomy, no matter what they say. ## The Commander ledger Everything that runs under a Commander grant, and every hardline attempt that gets blocked, is written to an append-only ledger. It is a **write-ahead** record: the line is written *before* the command executes, so the trail survives even if the command crashes the process. Each entry captures the timestamp, the action, the exact command, the backend it ran on, the autonomy level, the grant's expiry, the decision (`run` or `blocked`), and the reason. The file lives in the app's user-data folder as `mori-commander-ledger.jsonl`, and there is a read-only viewer for it in the app. The ledger is **data, and only data**. Its path is refused at the same dispatch chokepoint every command passes through, so no tool, no `write_file`, `edit_file`, or `delete_file`: can touch it. Mori can never doctor its own audit trail. ``` {"ts":1752_,"action":"terminal_run","command":"rm -rf node_modules","backend":"local","autonomyLevel":"commander","grantExpiresAt":1752_,"decision":"run","reason":"commander grant active (dangerous)"} {"ts":1752_,"action":"run_terminal_command","command":"rm -rf ~","backend":"local","autonomyLevel":"commander","grantExpiresAt":1752_,"decision":"blocked","reason":"hardline pattern, un-bypassable"} ``` ## What never changes, at any level The ladder governs how much confirmation *command execution* needs. It has no reach into the other safety gates, so those hold at every level, including under a live Commander grant: - **The hardline floor holds.** The catastrophic patterns above are refused at Guided, Trusted, and Commander alike. - **Purchases still stop at the pay button.** The purchase gate lives in the browser and computer-use loops, not in command execution, a Commander grant simply doesn't touch it. Mori holds every checkout at the final step and places the order only after you approve it. - **Credentials are never entered.** Mori does not type card numbers, passwords, or the final spend tap. Those stay with you, always. - **Only you change the level.** Autonomy state moves only through the settings UI. Nothing Mori reads, a web page, a file, a message, a tool result, can raise its own permissions. See **Security & approvals** for the permission-level system, typed remote approvals, checkpoints, and the durable audit log that sits underneath all of it. # Security & approvals > Mori runs on your machine with your keys, and every risky action stops for your yes. ## The security model Mori is local-first. The agent, its tools, its memory, and its audit trail all live on your machine; the only outbound traffic is the model calls made with API keys you supply. There is no Mori cloud in the execution path, so there is no server-side actor that can run a tool on your behalf. Every action passes through layered gates before it touches anything real: - **Permission levels** decide which tool categories exist at all for the current session. - **Per-call permission checks** run on every single tool invocation, not just at startup. - **Typed approvals** stop irreversible actions, purchases, remote confirmations, until you explicitly say yes. - **Checkpoints** snapshot files before Mori overwrites or deletes them. - **Post-turn verification** re-checks Mori's claims against reality before you read them. - **A durable audit log** records every tool call, every approval, and every denial. ## Permission levels Tool access is governed by `isToolPermitted`, which runs before every tool call. If agent mode is off, no tool is permitted, full stop. With agent mode on, the `permissionLevel` setting selects one of three tiers: - **safe**: browser tools and read-only tools only. MCP tools are blocked entirely at this level. - **standard**: the default. Most categories are allowed, with per-category kill switches: `allowFileWriting`, `allowTerminalCommands`, `allowBrowserControl`, `allowAppLaunching`. Raw keyboard simulation (`type_text`, `press_keys`, `hotkey`) is off unless you opt in with `allowKeyboardControl`: app-targeted typing is allowed, but blind keystrokes into whatever has focus are not. - **full**: everything the category toggles would otherwise gate. Read-only tools (`read_file`, `list_files`, `git_status`, `search_history`, and the rest of the verification set) are always permitted when agent mode is on. Mori can always check reality, even when it can't change it. A denied call doesn't fail silently: the denial is written to the action log with the exact reason, e.g. `Permission denied: run_command is not allowed under current permission level (safe)`. ## Typed approvals on remote surfaces When you drive Mori from Discord, Telegram, or the phone app, confirmations are not a loose "reply yes" convention. Every approval request is a typed object with its own ID and a hard expiry: - Each request gets a unique approval ID and a **two-minute TTL**. The prompt tells you both: "approval apr_xxx, expires in 2 minutes; anything but a clear yes is a no." - Only an unambiguous yes approves. An unrecognized reply is treated as a **deny**, and Mori tells you so. Silence past the TTL expires the request, also a deny. The system fails closed in every branch. - Every decision, approved, denied, unrecognized-treated-as-deny, expired, is written to the durable audit log with the approval ID, the request text, and which platform answered. An explicit "stop" or "cancel" from you on any surface halts the running task through the same machinery as the desktop Stop button, and that cancellation is logged too. ## Purchases: Mori never clicks the pay button Spending money is gated by button detection, not model judgment. In the browser agent, when a click or submit targets an element whose label matches the pay-button pattern (place order, pay now, buy now, and so on), the click is intercepted before it happens. Mori screenshots the review screen, raises `onPendingPurchase` with a summary, the exact button label, and the image, and ends its turn. The same gate fires if the model declares itself done while a pay button is present on the page. The order is placed only when you explicitly approve, the confirm button in the app, or a clear yes to a typed approval on a remote surface. That approval calls `confirmPlaceOrder`, which re-reads the review page, re-locates the pay button by its exact text, clicks it once, waits, and screenshots the result so you see the confirmation page. If the button is gone, it refuses and tells you to reopen checkout rather than guessing. The computer-use loop has the same rule enforced twice: a hard stop that refuses to execute any click or keystroke the model narrates as a final purchase or trade, and, in the default `ask` autonomy mode, a per-action confirmation gate on every click, keystroke, and key press. Card details, credentials, and the final spend tap are yours, always. ## Post-turn verification and the honesty check Mori's reply text is not treated as ground truth. After any turn that changed things, the post-turn verifier re-checks each successful mutation against reality: files it claims to have created are probed on disk, folders are listed, deletions are confirmed absent, and dev servers it says it started are probed over HTTP. The result is appended to the reply as a verdict line, `✓ Verified: 3/3 changes confirmed on disk`: or a warning naming exactly what could not be verified. Verification is capped and time-boxed so it can never balloon a turn, and a pure-chat turn gets no verdict line at all. A separate honesty check targets the highest-stakes claim: "I sent it." If the reply asserts an email or message went out but no send-capable tool actually succeeded that turn, Mori appends a correction stating that nothing was sent and the claim above is wrong. A fabricated "done" does not stand. ## Checkpoints before every write Before Mori overwrites or deletes a file, the prior bytes are copied into a per-file checkpoint history under the app's data directory. Snapshots are capped at ten per file and skip anything over 10 MB, so the history never bloats, and the snapshot step can never block or fail a write. Three tools ride on this: `list_checkpoints` shows what's saved for a path, `rollback_file` restores the most recent snapshot, and `search_history` finds prior states. When Mori deletes a file it says so in the result: "a checkpoint was saved, rollback_file can restore it." Destructive file operations are undoable by design. ## The MCP server fails closed Mori can act as an MCP server so other agents: Claude Code, IDEs, scripts, can use it as a tool. That surface is deliberately minimal: - **Disabled by default.** You turn it on in Settings. - **Loopback only.** It binds 127.0.0.1:4519 and nothing else, never a network interface. - **Bearer token on every request.** The token is generated once, shown in Settings, and any request without it gets a 401. - **Reads only.** The v1 surface is status, file reads, file listing, file search, and board/goal reads. No terminal, no file writes, no sends, no computer control. - **Task creation doesn't execute.** `mori_create_task` lands a card in your board's INBOX, a human triages it before any worker touches it. Each POST is handled by a fresh stateless server-transport pair, so nothing leaks between clients. ``` curl -X POST http://127.0.0.1:4519/mcp \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' ``` ## Stranger isolation and the audit log On Discord and Telegram, ownership is bound to your immutable platform account ID, captured once, never matched by display name, so impersonation by nickname is impossible. The phone app is owner-by-construction: it is signed into your account. Anyone who is not the verified owner gets a hard-isolated experience. Strangers receive plain conversation from the model's own knowledge only: no tools, no screen, no files, no terminal, and no machine actions of any kind. Your saved memory is stripped from the prompt entirely for those chats (`setSuppressMemory`), so it cannot be socially engineered out of the model, and verification output from your concurrent tasks is never leaked into a stranger's reply. Their messages are treated as untrusted data, instructions to ignore rules, act on the machine, or reveal owner information are declined. Underneath all of it sits the audit log: every completed tool call is appended to a per-profile `audit.jsonl` with timestamp, tool, argument summary, result, and status, alongside every approval decision. Auditing is best-effort by contract, it never blocks or breaks a task, but the record is durable, local, and yours to review or clear. # Tool reference (154 tools) ### open_app (app) Open any desktop application by friendly name. Supports: Discord, Chrome, Edge, Firefox, Brave, VS Code, Notepad, Calculator, Paint, Explorer, PowerShell, CMD, Terminal, Spotify, VLC, Slack, Telegram, Teams, Zoom, Steam, Word, Excel, OBS, Task Manager, and more. The backend handles Windows path resolution automatically. ### focus_app (app) Bring a running application window to the foreground ### close_app (app) Force close a running application by process name ### open_vscode (app) Open Visual Studio Code ### open_new_vscode_window (app) Open a new VS Code window ### open_terminal (app) Open Windows Terminal or Command Prompt ### open_powershell (app) Open PowerShell ### open_cmd (app) Open Command Prompt ### open_url (browser) Open a URL in the default browser ### search_google (browser) Search Google for a query and open results in browser ### open_browser (browser) Open a specific or default browser ### use_mori_chrome (browser) Run a web task in the user's REAL Chrome via the Mori Chrome extension, their actual logged-in sessions (Gmail, GitHub, banking-free sites, etc.). Prefer this for web tasks that benefit from the user's real browser/login. Only offered when Mori Chrome is connected. Pass the full task as 'task'; returns Mori Chrome's result. ### open_site_by_name (browser) Open a well-known website by friendly name. Supports: Google, YouTube, GitHub, Reddit, Twitter/X, Discord, Amazon, Netflix, Twitch, LinkedIn, Facebook, Instagram, TikTok, Wikipedia, Gmail, Notion, Figma, Spotify, WhatsApp, Telegram, Slack, StackOverflow, npm, ChatGPT, Claude, Hacker News, Vercel, Codepen, Replit, and more. For unknown sites, opens https://www.{name}.com. ### list_files (file) List files and folders in a directory ### read_file (file) Read the contents of a file. Large files are returned in line-numbered chunks, pass offset to continue where the previous read stopped. ### create_folder (file) Create a directory (and parent directories if needed) ### create_file (file) Create a new file with optional content ### write_file (file) Write content to a file (creates or overwrites) ### update_file (file) Update an existing file with new content ### edit_file (file) Make a PRECISE, surgical edit to an existing file by replacing an exact snippet. STRONGLY PREFER this over write_file when changing a file that already exists, it edits in place instead of rewriting the whole file, which is faster and avoids losing or breaking other code. Read the file first, then pass old_string copied EXACTLY (including indentation/whitespace). old_string must be unique in the file unless replace_all is true. ### search_files (file) Search a directory tree for a string or regex and return matching file:line results. Use this to find where code lives before editing it. ### send_discord_message (interaction) Send a Discord message to a person or channel (works while the Discord bot is running). Use when the owner asks you to message / DM / tell / reply-to someone on Discord. Resolve the recipient by @mention, username, user ID, or channel ID. ### send_discord_file (interaction) DELIVER a FILE (zip, pdf, doc, image, spreadsheet, anything) to the owner over Discord, the reliable way to actually hand over something you built or found when the conversation is on Discord. Defaults to the CURRENT Discord chat, so you usually just pass the file. The file ref resolves like email attachments: a full path, a bare filename ('site.zip'), or a phrase like 'the zip you just made' / 'the file you just created'. Optionally add a short caption. Prefer this over email when the owner is on Discord, and use it instead of only saying a file is 'saved on disk'. Returns a confirmation; only claim it was sent when it confirms. ### run_self_test (interaction) Run Mori's built-in DIAGNOSTICS, actually exercises the terminal, file access, keyboard, mouse, app-detection, active-window, and screen-capture subsystems on THIS machine and reports which pass/fail. Use when the user asks you to 'test yourself', 'run diagnostics', 'check what works', 'are your tools working', or when troubleshooting why actions keep failing (especially on Windows). ### send_bulk_email (browser) Send the SAME email to a LIST of recipients (up to 25 per run) through the user's signed-in Gmail, sequential, throttled, with a per-recipient sent/failed ledger returned at the end. MANDATORY FLOW: (1) draft it, (2) show the user the FULL recipient list + subject + body and ask for approval, (3) ONLY after they explicitly say yes, call this tool. It will refuse if the user's latest message isn't an approval. Never call it unattended or on a schedule. ### deep_research (browser) Do THOROUGH, autonomous research on a question. Mori breaks it into angles, sends parallel researcher sub-agents to search the live web and READ the best sources, then synthesizes a clear answer WITH a Sources list. Use for anything that needs real, cited findings across multiple sources (market/competitor research, 'find the best X and why', deep how-does-Y-work). For a single quick fact use web_search instead. ### run_subtask (interaction) Delegate an INDEPENDENT chunk of a big job to an isolated SUBAGENT with its own fresh context, use for parallelizable or self-contained pieces (research one of several topics, build one module of a larger app, draft one document of a set) so your own context stays focused. The subagent has your full tools, works autonomously without asking questions, and returns a compact result (with file paths for anything it created). Call up to THREE run_subtask calls in a single response to run them IN PARALLEL. Pass everything it needs in 'goal' + 'context', it cannot see this conversation. It cannot spawn further subtasks or edit memory. Optionally give it a specialist 'role': researcher (gathers facts, read-only), coder (implements and runs code), critic (reviews and finds problems, read-only), designer (shapes look/feel/UX). ### run_script (coding) Write JavaScript that orchestrates MANY steps in ONE turn, the fast path for anything multi-step over files/data/APIs. Inside, an async `mori` object bridges to the machine: await mori.readFile({path}), mori.writeFile({path,content}), mori.listDir({path}), mori.searchFiles({path,query,file_pattern}), mori.httpRequest({url,method,headers,body}), mori.runTerminal({command}), mori.readPdf({path}), mori.ocrImage({path}), mori.queryCsv({path,sql}). Loop, branch, transform, aggregate, normal JS. ONLY what you console.log() comes back to you (intermediate results stay out of your context, so this is far faster than many separate tool calls). Limits: 5-min/50-call cap, no secrets in env, and NO sends/purchases/memory here, those stay as their own tools so approvals apply. Use for: 'rename every file matching X', 'pull these 10 URLs and summarize', 'crunch this CSV then write a report'. Use normal tools for single actions. ### read_clipboard (interaction) Read the current text on the system clipboard, whatever the user last copied. Use when they say 'the thing I copied', 'paste it here', 'what's on my clipboard', or hand you content by copying it. ### write_clipboard (interaction) Put text ON the system clipboard so the user can paste it anywhere with Cmd/Ctrl+V, great for handing them a link, a snippet, a password-free config, or anything they'll paste into another app. Verified after writing. ### configure_app (interaction) Change SAFE, cosmetic/UX app settings for the user by chat. You can set ONLY these: theme (say "dark", "light", or a preset name: Mori, Midnight, Paper, Terminal, Sakura, Slate), thinking stage (spark|flow|forge|trance), spoken replies on/off, ElevenLabs voice id, sidebar density (simple|standard|pro), chat font size (sm|md|lg), scheduled-run notification level (info|warning|alert), and the active model id for the CURRENT provider. Pass only the fields you want to change; omit the rest. Changes apply and persist immediately. This can NOT touch autonomy/permissions, provider API keys, spend caps, bot tokens/ownership, or the account, those are refused, and you should tell the user to change them in Settings themselves. ### http_request (browser) Call a URL or API directly and get the raw response back: JSON APIs, RSS/XML feeds, raw file contents, webhooks, status endpoints. Use this (not web_search) when you have an exact URL or API endpoint and want its actual payload. Supports method, headers, and body. 15s timeout, response capped at 200KB, local/private addresses blocked. ### mouse_drag (interaction) Drag the mouse from one screen point to another (press, move, release): for drag-and-drop, sliders, moving items between panes, drawing. Coordinates are absolute screen pixels. ### draft_social_post (file) Draft a social-media post for the user's BRAND/BUSINESS account and add it to their approval queue. Use when they ask you to write/schedule a post, run their socials, or plan content. It does NOT publish, everything waits for the user's one-click approval in Settings → Business (nothing goes out autonomously). Only for real, disclosed brand accounts via official channels: NEVER fake personas, bought engagement, or manipulation. ### list_business_drafts (file) List the social posts currently in the approval queue (pending or approved). Use to review what you have drafted for the user before they approve. ### business_status (file) Report which of the user's business accounts are connected (X, Meta, LinkedIn, Stripe, Resend, Shopify) and how many drafts await approval. Use before business tasks so you know what's actually wired up. ### system_control (app) Control the machine itself: set_volume (0-100, pass 'value'), mute, unmute, dark_mode_on, dark_mode_off, sleep_displays, lock_screen, wifi_on, wifi_off. Use when the user says 'turn it down', 'dark mode', 'lock my screen', etc. ### music_control (app) Control music playback: play, pause, playpause, next, previous, or now_playing (returns the current track). Works with Spotify and Apple Music, auto-picks whichever is running, or pass 'app'. ### get_calendar_events (app) Read the user's upcoming calendar events (macOS Calendar): for 'what's on my calendar', daily briefs, scheduling around their day. Returns events in the next N days. ### create_calendar_event (app) Add an event to the user's calendar (macOS Calendar). Give a title and an ISO start time; duration defaults to 60 minutes. ### create_reminder (app) Add a reminder to the user's Reminders app (macOS): for 'remind me to call mom tomorrow at 5'. Optional ISO due time; otherwise it lands undated in the default list. ### read_any (file) Open ANY file and understand it, the smart, universal reader. Hand it a path and it auto-detects the type and does the right thing: code in any language (reads the text), images (SEES them with vision), PDFs and Word/PowerPoint/Excel (extracts the text), CSVs (previews + points you at query_csv), zips/archives (lists what's inside), audio/video (metadata), a folder (lists it), or raw binary (identifies it). This is your DEFAULT when the user gives you a file or you don't know the type. For a specific known type you can still use read_file / read_pdf / read_image / query_csv directly. ### read_image (file) LOOK at an image file with your own eyes, the actual picture is attached to the conversation so you can see layouts, photos, charts, UI, colors, anything visual. Use for 'what's in this image', design reviews, checking a screenshot you took (take_screenshot returns a path, read_image it to verify your own work). For pulling text out of an image, ocr_image is cheaper. ### git_status (coding) Show the git status of a repo (branch + changed/staged/untracked files). Use before committing or to see what changed. ### git_diff (coding) Show the git diff of unstaged (or staged) changes. Use to review edits before committing. ### git_commit (coding) Stage all changes (unless add_all is "false") and commit with a message. Use after finishing a unit of work. ### git_log (coding) Show recent commits (one line each). ### git_branch (coding) List all git branches (local + remote). ### git_checkout (coding) Switch to a branch, or create one with create="true". ### git_init (coding) Initialize a new git repository in a folder. ### create_workspace (coding) Make an isolated git-worktree WORKSPACE for a big or risky code job, its own branch + directory, so changes never clobber the main checkout (and parallel subagents don't collide). Afterward, shell/git/relative-file tools default to it. Use before a large refactor or an experiment. ### list_workspaces (coding) List the isolated git-worktree workspaces and which one is active. ### enter_workspace (coding) Switch into a workspace (by id, name, or branch): shell/git/relative-file tools then default to its directory. ### exit_workspace (coding) Leave the current workspace, tools go back to their normal working directories. ### merge_workspace (coding) Merge a workspace's branch back into its base (commits pending work first, then a no-ff merge). Set delete_after='true' to remove the worktree once merged. ### remove_workspace (coding) Remove a workspace's worktree directory (refuses if it has uncommitted changes unless force='true'). The branch is kept. ### web_extract (browser) Fetch a specific web page and return its clean readable TEXT (article body, stripped of scripts/markup). Use when you have a URL and want to READ it, deeper than a web_search snippet. For raw JSON/APIs use http_request instead. ### download_file (file) Download a file from a URL to disk. Defaults to the Downloads folder if no path is given. ### run_python (coding) Run a Python 3 snippet and get its output, for data work, quick math, scripts, using Python libraries. (JavaScript orchestration → run_script; shell commands → run_terminal_command.) ### append_to_file (file) Append text to the end of a file (creating it if needed). Use for logs, notes, or adding to an existing file without rewriting it. ### get_file_info (file) Get a file or folder's size, type, and modified/created times, without reading its contents. ### unzip_file (file) Extract a .zip archive to a folder. ### rollback_file (file) Undo Mori's last change to a file, restores it to the state right before the most recent write/edit/delete. Mori auto-checkpoints every file before changing it, so this is a real undo. The current state is itself checkpointed first (so a rollback can be undone). ### list_checkpoints (file) List the saved checkpoints (auto-backups) for a file, newest first, so you know what rollback_file would restore. ### search_history (file) Search ALL of your past conversations with this user for something discussed before, a decision, a fact, a project detail, 'what did we say about X last week'. Returns matching snippets with the chat title + date. Use this to recall context beyond your live memory. Zero cost to your context window. ### list_processes (terminal) List running processes (pid, CPU, memory). Optionally filter by name. ### kill_process (terminal) Terminate a process by PID. Use to stop a runaway or stuck process (get the PID from list_processes). ### read_pdf (file) Extract the TEXT from a PDF file so you can read, summarize, or answer questions about it. Use for any .pdf the user mentions, reports, papers, invoices, contracts. If the PDF is scanned images (no text layer), it says so, use ocr_image on a page screenshot instead. ### ocr_image (file) Read the TEXT inside an image (png/jpg/screenshot) using the OS's built-in OCR, for screenshots, photos of documents, scanned pages, error dialogs. Give it a file path; get the recognized text back. ### query_csv (file) Run a real SQL SELECT over a CSV file, filter, aggregate, group, sort actual data instead of eyeballing it. The table is '?', e.g. "SELECT city, COUNT(*) AS n FROM ? GROUP BY city ORDER BY n DESC LIMIT 10". Numeric columns are auto-typed so SUM/AVG work. Use for any data question about a .csv. ### create_document (file) Generate a polished, ready-to-send DOCUMENT (Word .docx or .pdf): reports, letters, briefs, proposals, memos. Write the FULL body yourself as rich Markdown (use # / ## headings, **bold**, bullet/numbered lists, > quotes, and | tables |). Saves a real file the user can open. Prefer this over write_file when the user wants an actual document/report/deliverable. ### create_spreadsheet (file) Generate a real SPREADSHEET (Excel .xlsx or .csv): tables, budgets, trackers, data exports. Saves a file the user can open in Excel/Numbers/Sheets. ### create_slides (file) Generate a PowerPoint DECK (.pptx): pitch decks, presentations, reports as slides. Saves a file the user can open in PowerPoint/Keynote. ### remember (file) Save a durable fact or preference about the user/project to long-term memory so you recall it in FUTURE sessions (their name, stack, preferences, key project details). Use for lasting facts, not transient task steps. ### skill_view (file) Open one of your skills (how-to procedures) in full. Your turn context lists the skills you have, call this when a listed skill looks relevant but its body wasn't injected. ### skill_manage (file) Create, update, or delete one of your skills, durable how-to procedures you follow in future sessions. AUTHOR A SKILL when you complete something non-trivial that took 5+ tool calls, recover from a dead end, or the user corrects your approach: capture the WINNING procedure, not the story. Writes may go to a pending queue for the user's approval. ### search_sessions (file) Search your summaries of PAST conversations with this user ('what did we do last week', 'that site we built'). Complements memory: memory = facts, sessions = what happened when. ### forget (file) Remove saved memories matching the given text (stale or wrong facts). ### save_playbook (file) Record HOW you solved a non-trivial, multi-step task, a reusable recipe. Different from remember (which is for facts): this saves the working APPROACH so you handle similar requests faster and more consistently next time. Call it after you finish something involved (a build, a multi-step automation, a tricky workflow). ### schedule_task (file) Schedule a RECURRING or future task that you'll run on your own and report back, e.g. 'every morning at 8am brief me on my calendar', 'every Monday summarize my unread email', 'in 2 hours remind me to stretch'. These run UNATTENDED, so use them for prep/reporting (briefs, summaries, drafts, reminders): they will NOT send messages, make purchases, or do irreversible actions without you present. Give a clear prompt of what to do each run. ### cancel_schedule (file) Cancel a scheduled task by its label or id. ### list_schedules (file) List the tasks you have scheduled to run on their own. ### create_goal (file) Create a DURABLE GOAL, a long-lived objective that spans many tasks and sessions (e.g. 'finish the product launch', 'keep the docs accurate'). Goals persist across restarts and devices. Use for multi-session objectives, NOT for one-off tasks. ### update_goal (file) Update a goal after meaningful work: progress %, status (active|paused|complete|blocked|failed), blocker, next action, or completion evidence. Always record evidence when marking complete. ### list_goals (file) List all durable goals with status, progress, blockers and next actions. ### create_task (file) Add a task card to Mori's task board (the durable Kanban, survives restarts, visible on every device). Use when work should be tracked/queued rather than done immediately, or to break a goal into pieces. ### update_task (file) Move/update a board task: change lane, record the latest action, set a blocker or result. Use lane=completed with a result when done; lane=waiting_user when you need the user. ### list_tasks (file) List Mori's task board (optionally one lane) with ids, lanes, priorities, blockers. ### start_dev_server (terminal) Start a project's dev server (e.g. vite/next/expo) and leave it RUNNING in the background. Returns the pid and the local URL (e.g. http://localhost:5173) once it prints one. Use open_preview or http_request against that URL to check it. Stop it later with stop_dev_server. ### list_dev_servers (file) List the dev servers you currently have running (pid, command, uptime). ### stop_dev_server (terminal) Stop a running dev server by pid, or by a fragment of its command/folder. Omit to stop all. ### list_memories (file) List everything you currently remember about the user/project. ### move_file (file) Move or rename a file ### copy_file (file) Copy a file to a new location ### delete_file (file) Delete a file or directory (use with caution) ### open_project_in_vscode (coding) Open a folder/project in VS Code (opens the folder AND reveals its main file) ### open_preview (coding) Show the user what you built in the Mori Preview window. Pass a project folder or an .html file path to render it live, OR a localhost URL (e.g. http://localhost:3000) to show a running dev server. ALWAYS call this right after building a web project so the user instantly sees the result without opening anything. ### create_project_folder (coding) Create a new named project folder ### scaffold_simple_html_project (coding) Create a simple HTML project with index.html, style.css, and script.js ### scaffold_simple_js_project (coding) Create a simple Node.js project with package.json and src/index.js ### run_terminal_command (terminal) Execute a shell command and return the output. Use for npm, git, build tools, tests. Default timeout 300s (max 600 via timeout_seconds). For servers/watchers that never exit, use start_dev_server instead, never run them here. ### terminal_open (terminal) Open a NAMED, persistent shell session that survives across turns (cd, env vars, and running processes all persist). Reuse the same name to keep working in it. Backend 'local' = the user's own shell; 'docker' = a hardened throwaway container (needs Docker); 'ssh' = a remote host (needs key auth). The user's MoriTerminal UI shares these exact sessions with you. ### terminal_run (terminal) Run a command in a named session (opened via terminal_open) and wait for it to finish. Returns the exit code and bounded output (head+tail; the middle of very long output is elided). State persists between calls in the same session. For servers that never exit, prefer start_dev_server. ### terminal_read (terminal) Read the most recent output lines from a named session without running anything, useful to check on a long-running command or catch up on a session the user has been typing into. ### terminal_list (terminal) List all open terminal sessions as JSON: name, backend, current working directory, whether a command is running (busy), and when it was created. ### terminal_close (terminal) Close a named terminal session and reap its shell (and, for docker/ssh, the container/connection). Always close sessions you no longer need. ### install_npm_dependencies (terminal) Run npm install in a project directory ### take_screenshot (interaction) FALLBACK ONLY: Capture a screenshot of the current screen. Do NOT use this as a first action. Use direct tools (open_app, open_url, create_file, etc.) first. Only use screenshots when direct tools have failed and you need to see the screen state. ### inspect_screen (interaction) FALLBACK ONLY: Capture and inspect the current screen state. Do NOT use this as a first action. Use direct tools first. Only use when you need to observe the screen after direct tools have failed. ### get_active_window (interaction) Get info about the currently active/foreground window (title, process name, PID). ### screen_elements (interaction) PREFERRED way to drive apps: list the interactive elements (buttons, fields, links, menus…) of the frontmost window as a numbered list, read from the OS accessibility tree. Works with ANY model, no screenshot or vision needed. Then act with click_element. Re-run after every navigation: ids go stale when the screen changes. ### click_element (interaction) Click an element from the last screen_elements list, by id (preferred) or by (partial) name. Resolves to the element center and clicks it. Call screen_elements first; re-list after the screen changes. ### mouse_move (interaction) Move the mouse cursor to screen coordinates (x, y). ### mouse_click (interaction) Move mouse to (x, y) and perform a left click. Use for clicking buttons, links, input fields, etc. ### mouse_double_click (interaction) Move mouse to (x, y) and perform a double left click. ### mouse_right_click (interaction) Move mouse to (x, y) and perform a right click (context menu). ### scroll (interaction) Scroll the mouse wheel. Positive = scroll down, negative = scroll up. Amount is in "clicks" (1-10 typical). ### wait (interaction) Wait/pause for a specified number of milliseconds before the next action. ### hotkey (interaction) Press a keyboard hotkey/shortcut using SendKeys format (e.g. "^s" for Ctrl+S, "%{F4}" for Alt+F4, "{ENTER}", "^a" for Ctrl+A). ### show_notification (interaction) Show a native desktop notification ### type_text (interaction) Type text using keyboard simulation ### press_keys (interaction) Press keyboard key combinations (e.g. {ENTER}, ^c for Ctrl+C) ### focus_app_window (app) Find a running application by process name and bring its window to the foreground. Returns NOT_RUNNING if the app is not open. ### is_app_running (app) Check if an application is currently running. Returns RUNNING:count or NOT_RUNNING. ### send_keys_to_app (interaction) Focus a running application and send keyboard keys to it. Use SendKeys format (e.g. "^k" for Ctrl+K, "{ENTER}", "^a"). ### type_into_app (interaction) Focus a running application and type/paste text into it. Better than type_text for long text or special characters because it uses clipboard paste. ### focus_default_browser (browser) Focus/bring to foreground the user's real default browser window (Chrome, Edge, Firefox, Brave). Use after opening a URL to interact with the browser. ### browser_search_site (browser) Search a specific website by opening the search results URL directly in the default browser. Supports: Amazon, GitHub, Google, YouTube, Reddit, X/Twitter, Wikipedia, npm, LinkedIn, and more. Uses direct search URLs, no UI interaction needed. ### browser_compose (browser) Open a compose/create view on a website in the default browser. Supports: X/Twitter (new tweet), Gmail (new email). After opening, use focus_default_browser + type_text to type content. ### send_email (browser) Send a real email through the user's signed-in Gmail in Chrome, the reliable ONE-STEP way to email someone, INCLUDING with a file ATTACHMENT (PDF, image, doc). Use this whenever asked to email/send something to an address. ATTACHMENTS: whenever the user references a file to send, pass that reference as attachmentPath, you do NOT need a full path. It accepts a full path, JUST a filename ('Nighttime_Routine.pdf' or even 'the nighttime routine pdf'), or a phrase like 'the pdf you just made' / 'the file you just created'. Mori resolves it by searching Desktop/Downloads/Documents and by remembering the last file it created this session, so right after you build a PDF you can send it without restating the path. Returns JSON {ok, status, message}: status 'sent' = really sent (say so confidently); 'not_signed_in' = the user must sign into Gmail in Chrome (tell them, offer to resend); 'attach_failed' = the file couldn't be found or the attachment couldn't be confirmed, report exactly what it says, do NOT claim it sent. Only claim it sent when status is 'sent'. ### create_zip (file) Zip a folder or file into a .zip archive, use this to PACKAGE a site/app/project you built before emailing or uploading it. Pass the folder or file as source: a full path, a name, or 'the site/app you just made'. Returns the .zip path, and the zip is remembered, so right after, you can send_email it (attachmentPath: 'the zip you just made') or upload it. Typical flow: build the site (files) → create_zip the folder → send_email the zip. ### upload_to_drive (browser) Upload a file to the user's signed-in Google Drive (My Drive) through the browser. Use this when asked to upload/put/save/back-up a file to Google Drive. Pass the file as a full path, a bare filename, or 'the zip/file you just made'. Mori resolves it. Returns JSON {ok, status, message}: 'sent' = upload confirmed; 'not_signed_in' = tell the user to sign into Drive in Chrome; 'attach_failed' = file not found / couldn't drive the uploader. Only claim it uploaded when status is 'sent'. Common flow: build a site → create_zip → upload_to_drive the zip. ### web_search (browser) Search the web and READ the results back as text, the way to ANSWER questions with current/live info you don't reliably know: crypto/stock prices, 'today/now/latest/current' values, exchange rates, weather, sports scores, breaking news, or any fact you're unsure of. It returns the page's answer box + snippets so you can read the REAL number/fact and report it. ALWAYS use this for live or uncertain data instead of answering from memory (a remembered price/stat is a fabrication). Unlike search_google (which only opens a tab), this gives you the text to read. ### operator_search (browser) Open a real controllable browser and search a shopping site, then return the top listings (title, price, rating). Use this for "find the best X", "compare prices", "shop for Y". Supported sites: amazon, ebay, walmart, bestbuy, target, etsy, google. After this, COMPARE the results and present the best options to the user. ### operator_read_results (browser) Re-read the product listings on the current operator-browser page (after it loads more, or after navigating). Returns numbered results. ### operator_add_to_cart (browser) Add a product to the cart in the operator browser. PREFER passing query = the product's name/title (it matches the right item even if numbering shifted); index is a fallback. Opens the product and clicks Add to Cart. After this you MUST STOP, never proceed to checkout or payment; the user completes the purchase themselves. ### operator_goto (browser) Navigate the operator browser to a specific URL. ### operator_screenshot (browser) Capture what the operator browser currently shows (for you to inspect the page). ### operator_status (browser) Get the operator browser current URL and page title. ### operator_close (browser) Close the operator browser when finished. ### vscode_create_project (coding) Create a new project with HTML/CSS/JS files and open it in VS Code. A complete workflow shortcut, creates files, opens VS Code, opens the main file. ### vscode_open_and_write (coding) Create a file with content and open it in VS Code immediately. ### chrome_read_page (browser) Read the ACTUAL page in the user's real Chrome (their logins, their sessions) via the Mori Chrome extension: full structured extraction, title, headings, every interactive element (indexed), links, page text. Pass url to navigate first, omit to read what's open. Use when the page context or a login matters; plain lookups should use web_search/web_extract instead. TELL the user before using Chrome. The first Chrome action each turn asks the user for an OK. ### chrome_act (browser) Act inside the user's real Chrome via the extension: click / type / select / press / scroll on an element by its [index] from the last chrome_read_page. Returns the action outcome + a fresh page read. Only works on sites the user has approved in the side panel; never touches payment or credentials. ### chrome_screenshot (browser) Screenshot the visible tab of the user's real Chrome via the extension (downscaled). Use to SEE a page when the structured read isn't enough (canvas apps, visual checks). ### scaffold_web_app (coding) START HERE for any website/web-app build: scaffolds a designed Vite + React + Tailwind v4 project, design tokens, shadcn-style components, Lucide icons, Inter, scroll motion, and a reference layout. Then build the real content inside that system (README-DESIGN.md is the contract). Never hand-roll blank HTML/CSS for a web project. ### github_search (browser) Search GitHub repositories by keywords (sorted by stars). REAL research: find libraries, reference implementations, prior art. Follow up with github_read to read the winning repo. ### github_read (browser) Read a GitHub repo directly: without "path" returns metadata + README + the full file tree; with "path" returns that file's contents (via the CDN, effectively unlimited). The real way to study how a library works. ### package_lookup (browser) Look up packages on npm / PyPI / crates.io: version, description, weekly downloads, deps, install command. Exact name gives full metadata; keywords search (npm/crates). ### stack_answers (browser) Search StackOverflow and return the top-voted FULL answer for a technical question or error message, ground truth for debugging, not search snippets. ### hn_search (browser) Search Hacker News discussions, prior art, library sentiment, "has anyone hit this" discourse. Returns stories with points/comments; web_extract a link to read further. ### tool_search (other) Your tool schema shows only the core tools: MANY more exist (git suite, calendar, PDFs/OCR/CSV, music/system control, browser operator, zip, drive, workspaces, MCP servers…). Search the full catalog by capability, e.g. "calendar event", "ocr image", "zip folder". Returns matching tool names + descriptions. Then call tool_describe for the schema and tool_call to run it. ### tool_describe (other) Get the full parameter schema of one tool from the catalog (found via tool_search) before calling it with tool_call. ### tool_call (other) Run a catalog tool that is not in your visible schema (found via tool_search / tool_describe). Same permissions and approval gates as calling it directly. # Changelog > What actually shipped, newest first. Every line here comes from the app's own release notes. No roadmap, no maybes. ## 1.17.0: WATCH HER WORK You can watch the work now, from the first thought to the proof. - **The right rail.** Plan, activity, files, and preview sit beside the chat instead of scrolling it away. The conversation and the work stay visible at once. - **Live phase boxes.** Plans render as numbered boxes with honest counts, each one pending, in progress, or done as she moves. - **Thinking that types.** Reasoning streams in word by word and holds until the last word lands. The pace is capped for reading, and the words are real. - **The workflow card.** Deep research runs on an inline card in the chat: waves, sources, and the trail that produced the answer. - **The composer queue.** Send while she works and the message queues, then folds into the run in order. - **The CLI auto-demo.** A build that ships a command line tool ends with a live demo run in a real terminal. Shipped for macOS and Windows. ## 1.16.0: EVERY CHAT IS ITS OWN The deepest change under the hood since Mori shipped: each conversation now runs on its own engine. - **Chats can't bleed into each other anymore.** Another chat's plan, steps, or receipts can no longer show up in the wrong conversation, each one carries only its own work. - **Stop any task from the background panel.** Hover a running task, hit Stop, and exactly that one stops while everything else keeps building. The tray Stop now honestly stops everything, in every chat. - **The status pill answers for the whole app**: whichever chat is actually working is what you see, and one chat finishing no longer blanks a busy machine. - **Ollama models on tap.** Ask for local AI and Mori pulls the model you need (with your OK, they're big), then verifies it's really there before continuing. ## 1.15.0: SHE SETS UP THE MACHINE The first slice of Mori Infinite: any task, gated by your credit, never by the app or a missing runtime. - **A build never dies on "command not found."** Mori checks what your machine has, node, npm, python, git, ollama, and installs what a task needs through your own package manager (Homebrew on Mac, winget on Windows), asking you first. No package manager? She hands you the official download instead of piping remote scripts into a shell, that line does not get crossed. - **Credits are checked before the build starts.** An empty OpenRouter balance gets a clear "top up first" before any work begins, never again a half-finished build dying at a 402 five minutes in. A low balance gets a heads-up and keeps going. - Plus everything in 1.14.2, for anyone catching up. ## 1.14.2: SHE KNOWS WHAT SHE'S CLICKING A safety hardening and the two most-reported paper cuts. - **Stricter stop-at-pay.** Mori never clicks the final pay button, that was already the rule. Now she also refuses to click anything she can't identify: if the page changed since she read it, she reads it again instead of clicking blind. And with several chats using your Chrome at once, each task keeps its own approval and its own view of the page, one chat's "yes" no longer covers another's. - **Mori's mark on the Mori panel.** The panel header and its composer button wear her icon instead of the same sparkle the Skills button wears. - **Small windows behave.** Opening a side panel in a small window used to crush the chat into an unusable sliver. The panel now measures the space it actually has and always leaves the chat room to breathe. ## 1.14.1: STOP MEANS STOP Two fixes on top of 1.14.0, both reported after install. - **Stop actually stops.** It only ever worked if it caught a network request in mid-air, which is why it looked random: pressed between steps, inside a running command, during a sub-agent, or while Mori was driving your browser, it did nothing at all. The stop signal was being erased one line after it was set, by the very call meant to enforce it. It now holds until the work has genuinely wound down, on every one of those paths. - **Mori's face is back on the pill.** The always-on status pill and the app's own window icon were falling back to a plain dot, so the pill read as an older, different one. The logo was being packaged into the wrong folder, a bug that is invisible when running from source and only appears in an installed build. Fixed on Mac and Windows, with a check that now compares the packaging config against the code that reads it. ## 1.14.0: SHE TELLS YOU FIRST Mori answers before she builds, says what she understood and how she'll do it, and names the point where she'll need you, before she gets there, not after. - **Answer first.** A question gets an answer, not a build. Ask "should I move this to a framework?" and she reads the actual files, then gives you a recommendation and the one trade-off that matters. Ask a question *and* give an order in the same message and she does both, in that order. A plain build order still goes straight to work, it just tells you the plan first. - **She says what she is about to do.** Every task now opens with her reading of the ask, the approach and why, and anything she decided for you, before the first tool call, not buried in the summary at the end. This applies to everything, not just building. It stays in the chat too, so stepping away to another conversation mid-build and coming back no longer leaves you looking at tool activity with no idea what she set out to do. - **She tells you where she will need you.** A job needing a Supabase project, Stripe keys, a deploy under your hosting account or an App Store submission is flagged in the *first* reply, with what she needs and why. She builds everything she can, gets right up to the wall, and stops there with the exact ask, instead of inventing a placeholder key and handing you something that looks finished and works nowhere. - **Visible plans.** Work with three or more real steps shows a live plan with done / in-progress / remaining, updated as she goes. - **The Mori panel got honest.** Ask it mid-build and it answers from live state instead of last turn's: the files it just wrote, the actual error text of anything that failed, and where it really is. Typing an instruction into it now reaches the build, before, an instruction containing a word like "progress" or "status" was silently answered as a question and thrown away. **Fixes.** A terminal line ending in an erase sequence no longer swallows the whole message. A failed review whose finding began with "None" no longer renders as nothing at all. Every CLI tool failure is now recognised as a failure, so the repeat-failure guard actually fires. An ordinary commit can no longer be mistaken for passing test evidence. A message queued in one chat can no longer be delivered into another. A piece of work handed to a sub-agent can no longer land in a previous project's folder: ask for a site at one path right after building one at a similar path, and the files go where you asked. Deep research stays attached to the chat that started it. A dead shell no longer bricks its terminal session, and terminal output can no longer grow without limit. And when the provider says you can afford a smaller reply, Mori retries at that size instead of failing the turn and telling you to top up. ## 1.7.0: THE CLAUDE-GRADE MIND Memory grew up, vague references resolve, and every past conversation is searchable word for word. - **Memory rewrite.** Every fact is now its own markdown file with a findable one-line description, a tiny always-loaded index, enforced update-not-duplicate, first-class delete with tombstones, and `[[links]]` between memories. "The demo is Friday" is stored with the actual date. One-time migration from the old store, with a `.bak` backup. - **The entity resolver.** "The guy from the demo", "that site", "my resume", a universal registry maps slight terms to the real person, file, URL, build, app, or past chat, and asks one short question instead of guessing. - **Total recall.** Full-text search over every past conversation's raw transcript, `/sessions ` or just ask. Export any chat as Markdown or a styled HTML page with automatic secret-scrubbing (`/export`, `--redact` for more). - **Real schedules.** A main-process cron service: 5-field cron expressions, `at:` one-shots, `every:N` intervals, running with the window closed, missed runs fired once on wake, never replayed. Heartbeat checklists that stay silent unless something's wrong, and an off-by-default localhost webhook to poke her from scripts. - **The Brain works out of the box.** No Ollama? A built-in local embedder keeps the knowledge graph alive at $0, honestly labeled as lexical-grade, sticky per brain, never silently switched. - **Nothing durable is lost.** Facts are flushed to memory right before a long conversation compacts, and bot replies are recorded before sending, recovered exactly once after a crash. - **New Memory page.** Browse, edit, and delete everything Mori knows; inspect the entity registry; read the nightly dream-consolidation reports. - **`/retry` and `/branch` are real**, and cold start measured at 0.7s. The deep dives live in Memory and Automation & schedules. ## 1.6.2: MEMORY FIX, POLISHED Small release, real fixes. - **"Open the game you built" actually opens it**: a fresh chat asking to open/run/launch a past project reliably enters the tool path and opens the real thing. - **Cleaner recent-builds list.** Saving a loose file to Desktop or Downloads no longer mistakes the whole folder for a project. - **"Open Terminal / Notes / Music"** launches the real app again, even with a same-named folder around, and project paths with parentheses are handled correctly. ## 1.6.1: MEMORY 1:1 + HONEST USAGE Cross-chat build memory and a plan meter that tells the truth. - **Open what you built, from any chat.** A durable index of every project Mori builds, so a brand-new conversation can "open the game you just made" and land on the exact folder. - **The plan meter is honest.** On your own API key it says so ("on your own key") instead of showing a stuck 0%, with one tap to switch to your Mori plan. Keyless subscribers default to their plan automatically. - **All 154 tools verified wired end-to-end**: nothing declared that can't actually run, and dead code removed with no behavior lost. ## 1.6.0: THE BRAIN + TRUE AGENT A real memory Brain, terminal sovereignty, and the autonomy ladder. - **The Brain.** A private, on-device knowledge graph that remembers people, projects, and files across every conversation and answers "what were we working on?" with cited sources and honest gaps. Runs fully local on Ollama, with its own Brain page. - **Never loses the thread**: the active task always comes first, so an old project can't hijack a session just because it looks similar. - **Named terminal sessions** that stay alive across turns, local, Docker, or SSH, and you can type into the same shell Mori is driving. - **Autonomy ladder** (Guided / Trusted / Commander) with time-boxed grants and a Stop that really stops; computer use shows a floating pill with a Stop button even when the app is minimized. - **Mori for Traders**: market research, backtests, and paper trading, never real money, plus local Ollama models for internal chores. See The brain, Terminal sessions, and the Autonomy ladder. ## 1.5.0: COMMAND DECK Mori stopped being one conversation. - **Multi-pane chat.** Run two, three, or four live sessions side by side, `Single`, `1+1`, or `2×2`: each with its own conversation, model, and composer. `Cmd+1`-`4` jumps between panes. - **Broadcast.** Type once and send to every pane at once, compare the answers side by side, then **Promote** the winner. The losing panes are archived in both stores, never deleted. - **Truly concurrent.** Panes run at the same time without stepping on each other. - **Settings by chat.** Tell Mori "switch to the Terminal theme and Forge stage" and it happens through `configure_app`: but sensitive settings (autonomy, API keys, billing) can never be changed by chat, only in Settings. - **Session-scoped engine.** Under the hood the whole chat engine now keys off a session id, so steering, Stop, the usage meter, and verification are all per-pane. The deep dive lives in Command Deck. ## 1.4.0: SOVEREIGN II The reskin, the migration path, and skills that version themselves. - **Theme Studio.** Six shipped looks, including a full Terminal skin, a live token editor, and import/export of your own theme. - **Import from other agents.** One click pulls your skills and memories in from Hermes, Claude Code, or OpenClaw. Everything lands pending your approval; nothing is trusted on arrival. - **Skills 3.0.** The open `SKILL.md` standard, semantic-version history with rollback, an in-place editor, and an approve/reject flow that covers both Mori's own drafts and imported skills. - **`/learn`.** Teach Mori a new skill from a folder, a link, or "what we just did." - **Mid-run learning.** A failing step becomes a lesson the very next step can use. - **Kanban, for real.** Rename, add, and reorder columns, set WIP limits, and switch between Board and List views, while workers still only pick up what you stage as Ready. - **Always-on plan meter.** Every signed-in user watches their usage fill up right in the composer, with reset times on hover. See Theme Studio, Import from other agents, Skills, and Context & usage. ## 1.3.0: SOVEREIGN Persistent shells, a real autonomy model, and the slash layer. - **Named terminal sessions.** Mori keeps real shells alive across turns, `local`, `docker`, or `ssh`: and you can type into the same session she is driving. - **Autonomy ladder.** Guided / Trusted / Commander, with time-boxed grants, a live countdown, and an append-only action ledger. The hard safety floor can never be bypassed, on any rung. - **Context transparency.** Click the usage chip for a full token breakdown, a next-turn cost estimate, and one-click Compress. - **Slash commands.** Type `/` in the composer, `/usage`, `/compress`, `/clear`, `/model`, `/autonomy`, and more. - **Artifact cards.** Files Mori makes for you now show as rich cards with Open, Reveal, and Preview. - **Better bot replies.** Discord DMs get real file attachments, and long replies chunk cleanly without breaking code blocks. - **What Mori knows about you.** View and edit your profile right in Settings. - **Ollama support.** Run local models, with an automatic one-hop fallback if your primary provider fails. See Terminal sessions, the Autonomy ladder, Slash commands, and Context & usage.