Skip to content

Latest commit

 

History

History
1273 lines (1114 loc) · 77.1 KB

USER_GUIDE.md

File metadata and controls

1273 lines (1114 loc) · 77.1 KB

Vogt — User Guide

Vogt is a single-tenant work register for software projects: declared work items, observed evidence about them, a ranked backlog that can explain itself, and an audit log that records who changed what and why. It is reachable four ways — a CLI, a web GUI, a REST API and an MCP server — all generated from one operation registry, so anything this guide describes on one surface exists on the others too (§8).

The web GUI, terminals, editor, git panel, scheduled agent tasks and the voice assistant are served by the engine, the Rust front half of the stack (engine/, web/; see ENGINE.md). Running the core on its own — without the engine — still gives you everything in §2.1–2.4, §7 and §8 over CLI, REST and MCP; the engine adds §1, §2.5–2.7, §3 and §4, and the browser experience they live in. Install, configuration and the published Docker image are covered in GETTING_STARTED.md and DEPLOYMENT.md; every environment variable is listed in CONFIG.md.

One honest caveat, stated once and not repeated. Everything below is implemented and covered by tests. The Solid surfaces have jsdom coverage against a fake front door and automated Chromium coverage for representative desktop and phone routes, including Sessions composition, terminal splitting, zoom-event ownership and dirty-editor lifecycle. The device-dependent voice and push passes run per release on real hardware rather than in CI.


1. Getting in

This section is about the PWA, which the engine serves. If you do not have the stack running yet, GETTING_STARTED.md brings it up, and ENGINE.md §3 covers the engine's own configuration.

First run. A brand-new instance — one whose core holds no tokens at all — greets you with a setup wizard instead of the sign-in gate: name yourself, choose a username (suggested from your name) and a password, confirm it, and the wizard creates your login with the admin scope and signs you straight in. Nothing is shown once and nothing needs copying down; the wizard's terminal-and-agent hint says how to mint API tokens for those (vogt token issue). That door closes itself the moment any token exists (GETTING_STARTED.md covers the mechanism and the headless equivalent).

Your first sign-in then lands on Setup (#/setup), which walks the two remaining steps with a visible pass or fail on each: linking your own forge PAT — upstream writes are then attributed to you, and the repositories your credential can see become the import picker — and your first project, imported from the forge or registered from a path already mounted into the instance, followed by the first sweep and the collector coverage it earns. Every step is skippable and nothing is wizard-only: Projects and Settings own the same capabilities afterwards, and each write asks for a reason because it is audited like any other.

  1. Open your instance's address in a browser. The engine serves the GUI at / and proxies the core's API behind it, so work surfaces and terminals share one origin.
  2. Enter your username and password at the sign-in gate.
  3. You are in. Sign in with a token is one disclosure away for an API token or the operator's break-glass token.

Signing in mints a session: a core token bound to your own actor, carrying the scopes on your login, which the browser keeps for you and which expires after the instance's session_ttl_days (30 by default). Everything you do — on the work surfaces, in the terminals, through the assistant — is done with that one credential. The engine holds no token list of its own: it asks the core who your session belongs to and derives what you may do here from your scopes, and every Vogt write made through the GUI is forwarded with your own session, so it is audited to you and never to a shared identity. With work.write or project.write you can open terminals, edit files, run agent tasks and use the assistant; with read alone you can look and subscribe to notifications; admin can do everything, including launching GUI processes and repinning the agent CLIs. A 403 means this credential will never work for this route, as against a 401's try a different one; a 503 means the core could not be asked, which is an outage rather than a wrong password, and the gate says so.

Sign out (in Settings) revokes the session at the core and then clears the browser, so a signed-out device stops working everywhere rather than merely forgetting. Your password is changed by an admin with vogt user passwd, which also ends your other sessions.

API tokens are for agents, scripts and MCP clients. They are issued with vogt token issue (see GETTING_STARTED.md), and the CLI, REST and MCP surfaces — and this gate's token form — use them directly.

Settings is a routed modal. Whether it is opened from the desktop rail, the phone bottom bar's More sheet, or the command palette, closing it returns to the route that invoked it, including filters in the query string; a direct #/settings link falls back to Sessions. Browser Back closes a routed Settings view without adding a loop.

Settings still stores device-local named auth profiles for token sign-ins — an advanced arrangement for someone who holds several API tokens and switches between them. A password login needs none of it.

Settings → Theme carries two pickers. App Theme recolours the whole shell and takes effect immediately: choose System to follow your device's light/dark setting, or pin one of Vogt Dark (the default), Vogt Dim (a softer, warm-grey dark), Vogt Light, or High contrast in a dark or light variant. The choice is remembered in this browser and applied before the first paint, so the app never flashes the wrong colours on load. Below it, the Terminal Theme picker sets the terminal palette; leave it and the terminal follows the app theme, or pick one to pin it. Monaco (the file editor and diff views) and the PWA's browser-chrome colour follow the app theme too.

The dialog scrolls its own body between a sticky section list and a sticky Cancel / Save footer, so the footer stays reachable on a laptop and a phone alike, and a header × closes it from the top. The Save button says what it will do: an app- or terminal-theme change applies immediately; a layout-mode change is marked (requires reload) and the button reads Save & reload; changing the token or base reads Validate, save & reload; a preferences-only change reads Save preferences. You can save a preferences or layout change without touching the token field — a blank-but-unchanged token is a valid "keep the current credential", so a same-origin deployment that needs no token can still change its settings. Destructive actions — signing out, clearing managed browser data, deleting a saved profile, and the server retention purges — each ask for confirmation first rather than acting on one click.

2. The surfaces

Board, Backlog, Inbox, Sessions, Projects and the work item share one working-header order: the surface title, the freshness/evidence or connection truth that qualifies its answer, view controls, then the primary action. That truth is stated the same way everywhere — an age pill reading <mode> — updated Ns ago (Live, Polling, Paused, Stale, or an outage), so how current a surface is looks the same wherever you are. A surface can omit a control or action, but it does not move the truth statement away from the answer it qualifies. On narrow screens and at increased browser zoom those regions wrap in the same reading and keyboard order. Longer implementation detail is available from the header's disclosure where a surface needs it; unavailable, empty, loading, and partial answers remain named in the surface's own words. Where a surface has nothing to show, the empty state offers the next act rather than a dead panel: an empty Board offers Quick create, Clear filters and a link to register a project; an empty Backlog names the collector freshness it would rank from and offers Quick create.

Board and Backlog both carry a Refresh cadence in their header — every 10/20/60 seconds, or off — beside a manual Refresh now; the age pill says which is in force. Projects’ header keeps its Project / Dependencies / Drift inbox / Import views as a segmented control that scrolls rather than wraps, so Import stays reachable at any width, and a view that needs a project first says so on the disabled tab.

Board, Backlog, Inbox, Projects, Audit, work items and Sessions are stable, non-closable places. Sessions contains the terminal/editor panes and machine tools; their existing deep links still select the named pane or tool, and a pasted link restores the view it names. Open panes persist per device.

Inbox puts the server-ordered attention stream directly after its title, freshness statement and compact source filter. Coverage/provenance and batch operations remain immediately below the stream as labelled disclosures; an empty answer opens its coverage automatically so “covered and empty” cannot be mistaken for “not collected.” Entry and batch reason fields appear only after you choose the corresponding write, and a refusal stays beside that composer so the reason can be corrected without losing focus or context.

Beside Source, From filters by who caused an entry: Anyone, External people only (people outside the repository's owning GitHub org — outside collaborators included, bots never), Org members, or Bots and Vogt (bots such as Dependabot, Renovate and GitHub Actions, plus Vogt's own drift, CI and agent entries). Each entry says who it came from where that is known. Under External people only, entries whose author could not be resolved yet are hidden and the view says how many ("N hidden: author unknown"); choose Anyone to see them. State shows active, snoozed, archived or all entries. The bot list is the inbox_bot_logins setting.

The Inbox filter is saved to your account and stays applied until you change or clear it: leaving the Inbox, reloading, or opening Vogt on another device brings back the same Source, From and State. A link that names a filter (#/inbox?source=drift&actor=external) applies it for that visit without saving it. Whenever a filter is applied the view says so in a chip ("Saved filter: external people only") with a Clear filters control. Search text is temporary and is not saved.

The Inbox badge in the Places rail and on the phone tab counts under the saved filter, so it matches the list you land on; its tooltip says when a filter is on ("1 active Inbox entries — filter on: external people only"), and the badge carries a ring. Search text never changes the badge.

CI entries come in four kinds, all clearing themselves when the evidence moves on:

  • <workflow> failed on <ref> — a failed run on a watched ref: a default or prod branch (ci_alert_branches, default main, master, prod) or a release tag (ci_alert_tags, default v*). These are the runs that block no pull request — release, release-mobile, build on main — so they failed silently before. The entry names the failed jobs and links the first job's log. It stays until a later run of the same workflow on the same branch succeeds; tags are one lane per workflow, so a later tag's green run of the same workflow clears an earlier tag's failure. Pull-request runs never raise one; a cancelled or still-running run neither raises nor clears one.
  • CI failing: <check> — a failure on the project's newest revision that no watched-ref alert already covers.
  • CI passed|failed on <branch> — CI settled on the newest revision of a branch a still-open work item is bound to (work.bind_branch, or the branch session.start records). The entry carries the work item. When it settles, Vogt also types a one-line notice ([vogt] CI FAILED on wi-7 @ … (WI-7); failing: ci — <log>) into each live session started for that item, once per conclusion, so an agent waiting on CI wakes instead of polling (ci_watch_notify_sessions turns the nudge off).
  • Deploy failed on lane <lane> — a configured deploy lane's receipt (deploy_lanes) reports the deploy or its smoke test failed; it clears when a later receipt reports success.

On a phone, the source and From filters become labelled pills and each entry keeps its evidence in the stream while moving applicable writes into an Inbox actions bottom sheet. The sheet is keyboard-operable, restores focus to its trigger on cancel, and keeps archive, snooze, restore, adoption, suppression and drift resolution reasons tied to the same server operations as the desktop view.

On desktop, the whole Places rail scrolls as one column when running sessions make it taller than the window. Files therefore remains reachable rather than being squeezed behind a separately scrolling session list; when visible it keeps a 260px minimum containing its heading, search, rows and controls. The rail's footer — Settings, Sign out and the connection indicator — is pinned to the bottom of that scroller, so it stays reachable however long the session list grows. Finished sessions, and sub-agent sessions, drop out of the default list six hours after their last event, and only when nobody has typed in them in that time; a session that is running, waiting, blocked, oversight, or pinned Keep awake stays. The footer then says how many are hidden and offers Show, which lists them again until you hide them. The list itself is ordered oversight first, then most recently active. You can drag the rail's right edge to resize it and double-click that edge (or press Home with it focused) to return it to the default width; Ctrl/Cmd+B hides and shows the rail. The compact Files header keeps New file and Refresh on one row, while New folder and Upload files sit under More file actions. Each row keeps the name first, folders disclose nested rows with a caret, Git-changed files show a quiet status letter such as M, and the labelled row menu retains rename/move, duplicate, download, delete, upload-here, terminal and preset operations where they apply.

Which folders are expanded, the search query and the editor sidebar's collapse state are remembered, so switching to another tab and back leaves the tree as you left it rather than collapsing to the root. A file operation refetches only the affected folder, and the tree reconciles its rows and Git markers when the workspace changes under it — a save, a Git action, a new file — and when the tab regains focus. A session's row menu also offers Open files here and Git here, which jump to that session's working directory in the file tree and the Git tab.

Oversight (#/oversight, a Sessions tool) shows every live and hibernated session in one table, ordered by who needs you. A permission dialog comes first, then an agent blocked on a person, then one waiting at its prompt, then a turn that has gone silent, then the rest. Each row says why it is placed where it is, and shows the agent's latest reply and the last lines of its screen. Click a name to open its terminal. The table refreshes every ten seconds and on any session event. Agents driving sessions read the same table with session_sweep.

An agent session can be hibernated to free the memory it holds. Its processes stop, and it stays in the list with a hollow dot and its last screen. Use Hibernate in its row menu, or let the deployment's idle policy do it. Wake (in the row menu, or the Wake session button on its pane) starts it again by resuming the same conversation in the same place. Opening the pane does not wake it. Keep awake pins a session: the idle policy never hibernates it, and after a restart it wakes by itself. Only agent sessions whose conversation can be resumed get these entries (Claude Code sessions, and any session started to continue a conversation).

A session bound to a work item shows it after its name as a ✦ WI-n chip; clicking the chip opens the item (not the terminal), its tooltip is the item's title, and it turns muted once the item is done. Bind to work item… in the row menu opens the command palette as a work-item picker, and Unbind from WI-n clears it. Both are audited writes through Vogt and never change the item's state. The work item's page says who is on it — Being worked by ses_… — running — and offers Attach a running session… and, per session, Unbind…. Moving an item to done while sessions are still bound warns and leaves them running.

Surface Link What it is
Board #/board Work items in columns. The columns are the workflow's states, read from the server — not written down anywhere in the client.
Backlog & bugs #/backlog The ranked global views, with the explainable why behind each row.
Projects #/projects, #/projects?p=<slug> Per-project brief, CI, contract and compliance, the drift inbox, the dependency graph, and the import form.
Work item #/w/<ref> One item in full: description, state history, comments, relations, labels, evidence, and Start session.
Audit #/audit, #/audit?ref=WI-7 Every write: who, what, and why. Filterable by actor, project, operation and time.
Terminal #/t/<session-id> A PTY, attached over WebSocket with scrollback replayed before live output.
Editor #/e/<path> Monaco over the workspace file API.
Git #/g, #/g/<repo> Choose a registered project, then inspect status, diff, log and branches or stage/unstage/discard/commit/checkout.
Tasks #/tasks Scheduled agent runs: create, edit, pause, resume, run now, inspect.
History #/history Every session, live and exited, in one list — each row badged live or exited and filterable by liveness; archived scrollback is full-text searchable.
Assistant #/assistant The conversational surface — absent entirely unless the operator configured an assistant API provider (§2.5).
GUI #/gui Desktop streaming (Selkies/KasmVNC) — hidden unless the engine has GUI_STREAM_URL set, was built with the feature, and the operator recorded an end-to-end verification with GUI_STREAM_VERIFIED=true. An old/direct link otherwise explains that the surface is unavailable.

2.1 The board

Columns come from workflow.list, so a workflow change reshapes the board without a client release. A drag is a work.transition: the card moves immediately, and if the server refuses the move the card goes back and Vogt's own sentence appears in the column the drop landed in. The client never keeps a state the server refused. On a touch device, where there is nothing to drag, each card carries a Move… control that opens the same reason composer with a target-state select — the workflow's own legal edges for that card.

  • Swimlanes by project or initiative; per-column WIP counts; lanes and columns collapse, and the layout is remembered per device. A lane is a grouping, not a move target: dropping a card into another lane of its own column changes nothing and says so, because changing an item's project or initiative is a different write — open the item to do it.
  • On a phone, a labelled state row shows one workflow state at a time; the selected state is in the URL and every state keeps its exact count.
  • Filters by project, state, type, label, initiative and actor are added through + Filter and shown as individually removable chips. The URL remains the shareable/restorable source of truth, and a combination can be named and recalled as a saved lens (per device; shared server-side lenses are deliberately not built — per-user server state would make the instance multi-tenant, and it is not).
  • Quick-create raises an item without leaving the board.
  • Long cards expand and collapse in place so their full title and body remain readable without leaving the bounded, measured Board. An expanded body is rendered from Markdown, the same safe renderer the item page uses.
  • The board is operable from the keyboard: focus an item, move it between columns, open its detail, quick-create. The full list of board keys lives in the ? shortcut help rather than in a legend over the cards, so the first screen belongs to the work. A card is a button — a single click of its body, Enter, or Space opens it.

Every write asks for a reason, and will not submit without one. That is not a form-validation quirk; it is the product's rule. The reason is what the audit log stores, and a board that armed one reason for a session is how fifty audit rows end up saying "triage". A drop does pre-fill the last reason the server accepted, which is a form with a required field rather than a bypass; quick-create deliberately does not inherit it.

2.2 The backlog and bugs

One tab, two ranked views over the same filter set. The order is Vogt's, not the client's: backlog and bugs rank declared work and observed subjects together, and every row can say why it is where it is.

  • Filters by project, label, initiative, actor, type and state are added through + Filter and shown as individually removable chips, exactly as on the board. The URL carries the set, and a combination can be named and recalled as a saved lens (per device). The state filter is the one exception: the ranked views take no state parameter, so it narrows the page already loaded rather than the estate. Its chip says so — State: open · this page only — and the count reads N of M loaded rows. Selecting rows and then changing the state filter keeps the selection: the rows are still on the loaded page, only hidden from view.
  • A row is as tall as what it says. Rank, ref, trust, age and score stay on the row's first line; the title wraps rather than ending in an ellipsis. Start a session… sits on that collapsed line next to More, so it does not need the detail opened first. On a phone the row leads with its title and keeps a single compact meta line under it — ref · kind · state · score, with why an inline link — and the session act moves into the expanded detail. The row names its project rather than showing the slug, keeps the slug in the tooltip, and links it to that project's Projects page.
  • More opens the rest of the row in place: an observed subject's provenance, the ranking factors behind its score, and the acts that row actually has. Declared rows offer Open and Select; observed subjects offer Adopt and Suppress. Nothing moves to a dialog.
  • A pull request collapses under the work it implements. When a PR names a work item — through a Closes #9 / Fixes owner/repo#9 keyword in its body or title, or a branch named for the issue (gh-9-…, wi-7/…) — Vogt reads that as an implemented_by edge and lists the two as one stream: the item stays on the board and the PR folds under it rather than ranking beside it, so a single piece of work is counted once. The edge is observed, never typed in, and it only informs — unlike depends_on it does not block the item from completing. A PR whose target is not itself on the board still lists on its own, so nothing is silently dropped.
  • Bulk transition and bulk labelling each collect their own typed reason, are applied one audited write at a time, and report each refusal against the item that was refused. The bulk bar is pinned to the top of the list while anything is selected, so it stays in reach after the reader has scrolled.

2.3 A work item

The detail page holds the description, the item's own state history (every entry into a state, oldest first, with what it came from, when, who moved it and why), comments, typed relations rendered as links, labels, and the observed evidence collected against it — each with how fresh it is and how much it is trusted.

The description and each comment are rendered from Markdown — headings, lists, links, inline and fenced code — so an item that came from a forge reads as it does there rather than as literal # and backticks. The renderer is strict: it builds the formatting itself and never runs HTML or a javascript:/data: link out of the text. A Raw toggle beside the description shows the Markdown source when you need to copy or check it.

A claim backed by a still-running session is marked provisional rather than fresh. An observation whose payload does not carry the flag at all reads as unverified, not as blank: a blank says "no opinion" when the honest answer is "nobody checked".

The item is edited from this page, not only read. A Move to control under the state rail changes its state, offering the workflow's legal edges and collecting the typed reason every write needs; the move renders optimistically and rolls back visibly if the server refuses it. An Edit panel changes the title, priority, assignee (a keyboard-operable picker over the actor roster), effort, labels and description, through the same reason-collecting form. Adding a comment is a third such form. The Start a session form is collapsed behind a button so the page leads with the item rather than its machinery.

Start session opens a PTY in the project's registered working tree, with this item's brief — description, why, relations — written to a prompt file the agent is pointed at. The session is linked back: the item shows its live activity badge, and the terminal links back to the item.

Branches the item is worked on appear in their own panel — but only when there is one, so an empty panel never reads as "nothing is being worked on". A branch belongs to an item when its name carries the item's reference: wi-7/…, feature/WI-7-…, or, on a forge-linked project, gh-264-… for issue 264. The convention is a configurable pattern (branch_binding_patterns); the branch a session started from Vogt declares it will use comes from branch_binding_template (default wi-7). You can also declare a branch on an item yourself, without starting a session, with work bind-branch WI-7 --reason …; omit the name to take the default from the template, or pass --branch feature/WI-7-… to name your own. It only records the name — Vogt never creates or renames a branch — and declaring one already bound is a no-op. Each row says where it came from — declared (a session said it would use it), observed (a sweep found it in the checkout), or both — and an observed branch says when it was last active ("active 2h ago") and how far it has diverged from the default branch. A branch on one side only is marked drift: Vogt reports where work is in git and never drives it, so declared and observed are shown side by side rather than merged, and a disagreement between them is surfaced rather than silently reconciled.

The git story reads those same branches and the pull request that implements the item (its observed implemented_by edge) back as one answer to where is this in git? Beside the workflow state sits a derived phase — no branch → branch active → PR open → in review → merged — climbed as the git evidence accumulates. It is a second opinion, never the state itself: a merged phase on an item still in_progress is exactly the disagreement the phase is meant to make visible. A Git story panel shows the pull request with its derived state (draft / open / in review / merged / closed), its review decision and checks rollup, and — like every collected fact on the page — how fresh the observation is ("observed 4m ago from the forge"). The obvious contradictions between the item and its git evidence read as drift: an issue closed with a pull request still open, a pull request merged while the item is still open, or a branch still active in a checkout for an item marked done. Everything here is derived and read-only — Vogt reports where the work is in git, it never writes the phase back onto the item. The phase is derived from the branches and the PR edge alone; the engine's per-task run conclusion is not an input to it.

The git story also feeds the ranking: an item with an open pull request or a recently-committed branch is moving, and why shows it lifting above idle work of the same priority — the branch's contribution decaying as its last commit ages out of the activity window.

The page keeps itself current the way the board does: it subscribes to the change stream, so a transition or a new comment somebody else made — and the session's live-activity badge — arrive here without a manual refresh, and a tab brought back from the background reconciles on return. The age badge reads Live while it is listening. A refresh never lands on top of something you are writing: while an edit, a comment or a session name is part-typed, the page holds the current answer rather than swapping it out from under you.

2.4 Projects and the drift inbox

A project page shows its brief, CI status, contract compliance with the criteria that failed, the dependency graph, the import form, and the drift inbox.

The registry list filters by a name-or-slug search and sorts by name, lifecycle or trust, all client-side — the estate returns every registered project, so narrowing and ordering are presentation, not another query. A project's Work panel is the way into that project's work: its counts are links — open work to the Board scoped to the project, open bugs to the bugs view, each by-state count to that state on the Board, and an Audit link to every write against the project. Each is the same ?project=<slug> (or ?p=<slug>) deep link a pasted URL restores.

The drift inbox shows both sides of each disagreement, with provenance and age, before any action is possible. Accept and reject each collect a typed reason. There is no bulk accept, on purpose: accepting drift is a declared-state write, and making that convenient in bulk is exactly how the reason rule erodes.

A proposal may also be flagged superseded: a sweep newer than the proposal no longer reproduces the condition that raised it. It is still open and still yours to resolve — the flag says only that it is worth reading before the ones without it, and it disappears again if the condition comes back. Nothing closes itself.

The dependency panel distinguishes a project that references nothing from one whose manifests are in a format Vogt does not read (go.mod, pom.xml) and from one no sweep has walked. Where the same source lives in two registered projects — a vendored crate that is also its own repository — both ends say so; Vogt does not compare the copies or claim either has drifted.

2.5 The assistant

Absent unless the operator set an API key — the routes answer 404 and every client hides the surface, so an unprovisioned deployment looks unprovisioned rather than broken.

You type in a composer that grows with your message: Enter sends, Shift+Enter starts a new line, and a pasted block keeps its line breaks. A reply comes back rendered as Markdown — headings, lists, links and fenced code — with a copy control beside it. While a reply is on its way a Stop button replaces Send and cancels the request cleanly; a send that cannot reach the engine is left in place marked unsent, with your text restored and a Retry. Clear empties the whole conversation, and asks before it does.

The assistant talks to any OpenAI-compatible chat endpoint: the engine reads ENGINE_ASSISTANT_API_KEY, ENGINE_ASSISTANT_BASE_URL and ENGINE_ASSISTANT_MODEL, so a hosted provider, a proxy or a local server all work if they speak that API. Voice is separate and also optional: ENGINE_ASSISTANT_STT_* and ENGINE_ASSISTANT_TTS_* name speech-to-text and text-to-speech endpoints, and without them the assistant is text-only. The full table is in ENGINE.md §6.

Voice works out of the box. You do not need a paid speech provider to turn voice on. The supported stack bundles a first-party voice sidecar — a local, CPU-only, OpenAI-compatible Whisper transcriber and Piper voice with a small default model set baked in — and wires the engine to it, so a fresh docker compose -f deploy/stack.compose.yml up -d --wait transcribes the microphone and speaks replies with nothing to configure. The first start takes a little longer while the models load. To run without it, clear COMPOSE_PROFILES in deploy/.env; to use a hosted provider instead, set ENGINE_ASSISTANT_STT_BASE_URLS / ENGINE_ASSISTANT_TTS_BASE_URLS (with a key and the provider's model/voice names) — the engine is bound to neither the sidecar nor any one vendor. See CUSTOMISATION.md.

It can read session output, projects, work, labels, initiatives, actors, workflows, dependencies, evidence coverage, contracts, drift, the Inbox, forge status and audit history. You can ask it to update projects and work, manage relations and labels, create initiatives, publish initiative tracking issues, check or change contract adoption, collect evidence, resolve drift, suppress findings, triage the Inbox, synchronize forge observations, and start or stop sessions. These writes and terminal input wait for on-screen approval.

Credentials, actor creation, host filesystem setup/import/export, repository publication/linking, forge policy changes, tool installation and instance maintenance remain unavailable through assistant tools. Listing actors and assigning work to them is supported. The complete capability matrix lists every operation and its reason. Your token scopes and configured integrations can further limit availability; a missing provider is reported as unavailable.

One pending action at a time, carrying the exact payload, the target, and the reason that will be written to the audit log; it expires after 120 seconds — the card counts the seconds down — and a new message abandons it.

The transcript separates newly received turns by time, keeps a tool trace on one quiet line, and turns sessions returned by successful assistant tools into state-labelled chips and Open session actions. Those links come from the structured tool result, not from names guessed out of prose; older transcript entries with no receipt timestamp simply render without a made-up time. Terminal-input approval stays inline in the conversation and shows the exact text, whether Enter will be appended, and the target session before Deny and Approve on screen can be pressed.

Voice never approves. Speech reaches the assistant only as a user message, and an approval is a tap. The microphone beside the message box is tap to talk: tap it, speak, and the message sends on its own about a second after you stop talking — or tap it again to send straight away. Holding it down instead works as push-to-talk: the message sends when you let go, and a pause while you hold never cuts you off. A tap that hears nothing closes the microphone again after a few seconds without sending anything, so it is never left open. Space or Enter taps and holds it from the keyboard.

Hands-free conversation. The head control beside Spoken replies starts a hands-free conversation: speak, go quiet, and the turn sends on its own; the reply is spoken and the microphone re-opens for the next turn, with no touch between them. Turning it on turns Spoken replies on — the mode needs a voice to answer with — and it is offered only where the device can both listen and speak (the app, or a browser with voice input and speech synthesis), disabled with the reason otherwise. A status line reads Listening… / Sending / Speaking / Paused for approval / Muted / Ended; in a conversation the microphone button mutes and unmutes with a tap (or M on the desktop), and a pending write still waits for an on-screen approval — the announcement says so and never offers a spoken yes. The conversation ends when you turn it off, leave the surface, mute-and-forget it past the idle timeout, or hear nothing for a few turns. By default the microphone is closed while a reply is spoken and re-opens when it finishes; setting vogt.assistant.voice.interrupt_response on lets you talk over a reply — the device stops speaking and listens the moment you start (off by default, because imperfect echo cancellation can cut a reply short by mistake). Turn timing is tunable per device through the vogt.assistant.voice.* browser settings (silence window, idle timeout, barge-in); the defaults suit a phone.

A live call. Where the deployment offers it, the assistant can be a call rather than a take: the microphone stays open, a pause of about three quarters of a second ends your turn, and the reply starts speaking as soon as its first clause exists, while the rest is still being written. Speaking over a reply stops it. The same rule as every other voice path holds, and it is the one that matters most here: a spoken "yes" never approves anything. A change still waits on a button on the screen, and while that button is waiting the call will not even pass what you say to the model — it reminds you to tap.

A call's speed is the transcription backend's speed. The engine transcribes your turn while you are still talking: every pause sends what you said since the last one to the backend, and the words come back as a live caption. By the time the call decides your turn is over, usually only the last phrase was still being transcribed, and often not even that, so a long turn is answered about as fast as a short one. Whisper servers, speaches included, only transcribe finished clips, which is why the turn goes in phrase-sized pieces rather than as a continuous stream. Each piece is sent the words before it, so a sentence split at a pause still reads as one. If a backend transcribes short pieces badly, ENGINE_ASSISTANT_CALL_STT_MODE=whole sends whole turns instead. The engine also sends a language with every clip (ENGINE_ASSISTANT_STT_LANGUAGE, en unless you say otherwise) so the backend does not spend the start of each turn detecting one. Opening a call transcribes a moment of silence, so a backend that unloaded its model while idle has it loaded again before your first turn.

Measured against speaches (faster-whisper base.en, int8, CPU) on a busy 24-core host, with four test turns of 2–12 s spoken 12 times each: the wait for the transcript after a turn ended fell from a median of 418 ms (p90 2.2 s) sending whole turns to 128 ms (p90 0.6 s) sending pieces, and the end of speech to the first audio of the reply from 1.6 s to 1.1 s with an instant model. Whole turns also came back as a looping transcript ("…passed passed passed…") 7 times in 48; pieces did not, because a piece that loops is transcribed again without the words before it.

The model is the rest of it. A piece is hidden only when it is transcribed inside the half second between your pause and the end of your turn. On a 24-core CPU with speaches, measured warm, a two-second turn took 1.4 s on faster-whisper small and 0.46 s on base.en, both at int8. A six-second turn took 1.9 s and 0.67 s. Neither model got a word of the test turns wrong. So for a CPU host:

  • Use Systran/faster-whisper-base.en (ENGINE_ASSISTANT_STT_MODEL).
  • Set speaches' WHISPER__COMPUTE_TYPE=int8 and WHISPER__CPU_THREADS=8.
  • Set WHISPER__TTL=-1, so speaches keeps the model loaded rather than dropping it after five idle minutes. A cold load added more than four seconds to a turn.

A tighter silence window does not shorten a slow decode; a faster model or a GPU does. The knobs are in ENGINE.md §6.

When the deployment offers server-side transcription, Settings shows Transcribe voice on the server. On by default a phone uses its own recognizer, which is fast but has never heard your project names; turning this on sends captured audio to the server transcriber instead, handed those names as a hint, so "check komodo on Node B" is far likelier to come through as the words you said. It takes effect the next time the app launches.

An approved write is audited to your actor, made with the credential that pressed approve — your own session or token, forwarded to the core. There is no shared "assistant" actor to fall back to; the only caller with nothing to write as is the break-glass token on a front door with no stack secret, and it is refused by name.

2.6 Quick chat

Chat (in the rail's Machine group, the Sessions tools, and a phone's More sheet) is for a quick question you don't want a terminal for, such as "is this AWS service available in Sydney?". Each chat is kept: the list on the left is searchable by anything said in it, and Archive only hides a chat. The button is absent when the deployment has chats turned off.

  • New chat picks a model (the deployment lists the ones it offers), and the picker in a chat's header switches models from the next message.
  • The agent can read files in its own scratch folder without asking. Anything else waits for you: reading other files, every web search or page fetch, every MCP tool, commands and edits. You get a card showing exactly what it would do, with Allow and Deny. A card nobody answers is denied after ten minutes. Only a person can answer a card. An agent's answer is refused.
  • Stop ends a reply in progress. A reply that failed (a provider hiccup) shows an error with Retry.
  • Continue in session opens a terminal session that carries on the same conversation, with the full tools a session has. The chat then points at that session and takes no more messages.

A chat's agent holds no credentials but its model key, so MCP tools (Vogt, GitHub, Grafana and so on) do not work in a chat yet. Continue in a session when you need them.

2.7 Terminals

Sessions are owned by the server, so closing a browser does not end them and several devices can watch one session at once. On attach you get a scrollback snapshot and then live output; a reconnecting client can resume from its last cursor and receive only what it missed.

The Sessions place keeps an attention-sorted live roster beside an internal tool bar. Terminal panes stay attached while you visit History, Tasks or another place; inactive non-terminal tools unmount so their network requests, listeners and large editor surfaces do not run in the background.

"Suspended" means the socket was closed. A pane you cannot see — another tab, or the app backgrounded for more than half a minute — keeps its socket open and buffers what arrives, so switching back shows only what you missed and never re-reads the whole history. That only holds for the few most recently used hidden panes; beyond that a pane closes its socket and reads Suspended, and reopening it fetches a fresh, bounded view of the screen. A pane you can still see, such as the other half of a split, is never suspended and keeps drawing live output. Editor text and view position are retained while switching tools, and any dirty editor also activates the browser/PWA exit confirmation until it is saved.

Saving is guarded against clobbering. The editor remembers the version it last read; a save that would overwrite a file changed on disk since then is refused and shown inline as File changed on disk with Overwrite and Reload — Overwrite forces your version past the guard, Reload takes the disk's newer content. The toolbar's Reload button discards local changes and re-reads at any time, and Save is disabled while the file is clean. Ctrl/Cmd+S saves the active editor even when focus is on the tab bar, the file tree or a split header rather than inside Monaco. If an unsaved draft is restored on top of a file the disk has since moved past, the editor says Restored unsaved draft; disk differs so the mismatch is not silent. The toolbar also carries Reveal in Git, which opens the Git tab and selects the file where a repository tracks it.

With no tool open, the Sessions overview is a live list of the running sessions — name, activity dot and state word, and working directory — each a link into its terminal, sorted with whatever wants attention first. When there are no sessions at all it becomes a Start a session button beside the configured presets, rather than an empty panel.

New sessions are named for you and created straight away. The + Session button, Ctrl/Cmd+Shift+T and a fresh-shell split take the working directory's basename and deduplicate it — a second shell opened in …/vogt becomes vogt-2, and a pane split off vogt reads as vogt ▸2. Hold Shift on + Session when you would rather type the name yourself.

Splitting composes sessions, not only new ones. Split right and Split down open a small chooser: start a fresh shell in the current directory, or pick any running session not already on screen — that session moves into the new pane and nothing is spawned. Each pane in a split carries a header dropdown that re-targets it at another session without disturbing the layout; choosing a session already shown in another pane swaps the two. The command palette reaches the same acts (Split right with…, Show … in this pane), and a session's rail menu offers Open beside current. Close pane detaches a pane and leaves its session running and listed — closing never kills; killing a shell is the separate, confirmed Kill pane. Broadcast fans typing, paste and the composer out to every pane at once, and Maximise lifts the active pane to full size while the others keep running, hidden; both persist per tab. On a phone one pane shows at a time, and the chooser is still reachable.

Search the buffer with Ctrl/Cmd+Shift+F or the toolbar's Find button: a small find bar opens with the match count and next/previous controls (Enter and Shift+Enter step through matches, Esc closes it). The toolbar also carries an A− / size / A+ font readout — the buttons stop at the 9–24 pt limits and clicking the number resets to the default — and a color-theme picker; both apply to every pane and persist across reloads.

A dropped connection is visible and recoverable. When the websocket drops mid-session the terminal writes a single [disconnected] marker (once per outage, not once per retry) and shows a Reconnecting (try N, next in Xs) overlay with a Retry now button that reconnects immediately; anything you typed while it was down is queued and its size is shown, then flushed on reconnect. When a shell exits, an Exited (code N) banner offers Restart here (a fresh shell in the same directory) and Remove — removing an exited session skips the kill confirmation, since its scrollback is already archived to History.

Each session carries an activity state — idle, running, waiting-for-input, errored — shown in the Sessions roster so an agent waiting for approval is visible without opening it. The state is a heuristic and should be read as a hint: errored is a real non-zero exit, but waiting-for-input is a pattern match on the last ~512 visible bytes, and a session that goes quiet without printing a recognizable prompt reads as idle.

Open #/g to choose a repository from Vogt's registered project list. The chooser maps each project's declared absolute root into the engine's workspace boundary; projects outside that boundary remain visible but unavailable. It does not crawl the workspace or discover unregistered repositories. Choosing a project writes its workspace-relative path into #/g/<repo>, so reload and browser Back/Forward preserve the selection.

Each status row carries its own + / − to stage or unstage it, and a Stage all beside the Status heading stages every change at once; staging a file flips the diff to the index side rather than leaving you on the emptied worktree view. Commit staged is disabled while nothing is staged, with a hint saying so, and the diff toolbar's Open file opens the selected file in the editor. Success banners clear themselves after a few seconds.

Git status, branches, commit history and the selected diff report failures in their own panels and offer Retry. If a refresh fails after a successful read, the previous answer remains visible but is explicitly marked stale. A clean working tree, a repository with no branches or commits, a selected path that is not a repository, and a missing/unavailable path are therefore separate states rather than different ways to draw an empty panel.

History is the one place that lists every session, live and exited. Currently running sessions from the live registry are unioned into the same list as the archived ones (keyed by id, so a session that is both running and partially archived appears once and reads as live), each row badged Live or Exited, and the Status filter narrows the list to running, exited, or archived-with-no-exit-code rows. A live row's replay pane tails its on-disk terminal log directly, so you can watch a running session here without waiting for it to exit. Its output is not in the archive's full-text index; search reaches it through a bounded scan of the session's recent output instead, and a hit from a running session is marked live. Its archive-only Export and Delete actions appear once it has exited and been archived. History loads archived sessions in pages and says how many are loaded versus the server-reported total when that count is available. If no total is available, a full page says that more may exist until Load more reaches a short page. Metadata filters apply only to the pages already loaded; archived output search runs across the server's full archive (up to the displayed result limit) and is debounced, so typing a needle settles to a single server search rather than one per keystroke. Each row also says what the session was: an oversight session reads oversight, a session bound to a work item leads with its ref, and a session that ran an agent conversation shows the agent and the start of the conversation id — including a shell someone typed claude into, which reports its conversation to the engine by itself. The metadata filter matches templates and conversation ids too, so a lost session can be found by the id. A session that has ended with a conversation that can still be resumed offers Resume, which starts a new session on that conversation with the same role and bound to the same work item; it opens in the directory the conversation ran in. A failed archive, search, detail or replay read stays attached to that panel with Retry. Previously loaded content remains visible but is marked stale; an empty archive is shown only after a successful empty response.

Session templates pre-configure command, cwd and environment, and can be matched to a repository name or path prefix. The presets you create in the Workspace Presets editor are stored in this browser — they are not synced across devices or shared with the engine, and the editor says so. Deleting a preset asks first, and Escape will not discard a half-typed one. Presets the operator ships as defaults live instead in the engine's TOML config:

[[session_templates]]
name = "Django Dev"
command = ["bash"]
cwd = "~/projects/myapp"
env = [["DJANGO_SETTINGS_MODULE", "myapp.settings.dev"], ["DEBUG", "1"]]

A template sets the command, working directory and environment — it does not install a toolchain. Sessions run in the pod's image, which already carries python, rust and node/npm; to add another language, extend the image (CUSTOMISATION — Extending the stack image). A template can also run a session inside your own container image — its command is just docker run -it <image> — so one template per image turns this picker into an image picker; see CUSTOMISATION — per-session containers.

2.8 Scheduled agent tasks

A task is a command, a prompt, a schedule (manual, interval, or UTC daily) and a persistent context file. Running one spawns a real PTY session, so attach, scrollback, history, auth and push all work the way they do everywhere else.

Task edits remain a draft until Save Changes or Create Task succeeds. Selecting another task, starting a new draft, closing the Tasks tab, or leaving the route asks whether to save, discard, or remain; a failed save keeps every field. Refresh is a re-read, not a navigation, so it re-loads the list without disturbing the draft rather than raising that question. The draft also survives a full page reload — it is mirrored to the browser session, not held only in memory. An unavailable first read is shown as an error rather than as an empty task list. If a refresh fails after a successful read, the last task list stays visible, is marked stale, and can be retried in place.

Run Now starts the task immediately. With an unsaved draft on screen it reads Save & Run: it saves first, so the run uses the prompt you are looking at rather than the last saved one. Either way the new run appears as a row on the task — its session name is a link — instead of pulling you off Agent Tasks into the terminal. A run whose session is still alive reads Still running and settles to its real outcome on its own once the session exits.

If an agent prints a line beginning with the task's notify phrase (by default VOGT_NOTIFY:; a task may configure its own, and exactly that phrase is matched), two things happen: a push notification goes out, and the line is recorded on the run as a finding. The push is a delivery; the finding is the record, and it survives a phone that was switched off. Findings are listed under their run row, so the record is readable in the GUI and not only in a notification.

A task may name a Vogt subject — a Vogt project slug, a Vogt work item ref, or both, entered on the task form. A bound run's findings become Vogt observations against that subject, with the freshness and trust every other kind of evidence carries.

Fire a task from Vogt's own state. Beyond the schedule, a task can carry triggers that start a run when something happens in Vogt: a work-item transition into a state you name (optionally filtered to a project, item kind, label, or one item — the run is then bound to the item that moved), a new observation of a kind you name, a drift proposal being raised, or a PR's checks going green or red. Add each on the task form, enable or disable it without losing its filter, and set Max concurrent runs to cap how many runs of the task run at once. The engine subscribes to Vogt's event stream rather than polling it. Two safety rules make triggers boring in the good way: a fire that cannot start — the task is at its cap, or the item it would bind to is gone — is dropped, not retried, so a burst never becomes a storm; and every triggered run records which trigger fired it and which event caused it, shown on the run row, so you (and why) can always see that "this run happened because WI-7 entered ready". An api trigger arms a task to be fired programmatically instead of by a person.

Steer a run without killing it. While a task's run is live, a steer bar on its row sends a line of guidance to the agent, delivered the moment the run next pauses at its prompt — you are not fighting it mid-thought. Tick Interrupt first to cancel what the CLI is currently doing (Ctrl-C) before the guidance lands. The same steering is available to the voice assistant as a tool, and a steer is recorded with who sent it.

Approval gates pause the run for a decision, and fail closed. A task can declare gates: named checkpoints — "Deploy to prod?", with options like Approve and Hold — that a run stops at and holds the terminal until you answer. The question shows on the row and on your phone; each option is a button that sends its own reply into the run. The safety rule is that a gate never approves itself by accident: a gate that is interrupted, times out, or whose session dies resolves to blocked, never approved — an interruption is not a yes. The one exception is a task set to auto-approve, which answers its own gates with their affirmative option and records that it did so (as auto-approve), so a run that approved its own checkpoints is always visible as such. A resolved gate stays on the run as a one-line note — who approved which option, or why it was blocked.

Every run ends with a verdict and a conclusion. When a run finishes its row shows a typed outcome badge — Succeeded, Failed, Partial, Skipped, or Blocked — next to the durable facts of what it did: how long it ran, the exit code, the tip of the branch it worked on (a short sha) and the diff stats for what it changed there (files, + insertions, − deletions), plus its cost when the agent reported one. Blocked is a run that stopped at a gate that failed closed; Skipped is a run that decided there was nothing to do (it prints a VOGT_SKIP: line) rather than a run that did the work — the two are worth telling apart.

Ask for structured findings. A task can carry an output schema — a JSON Schema the run's findings must match. When set, the agent's findings block (a fenced json block, or a file you name) is validated against it, and on a mismatch the agent is re-prompted to fix it, up to a small budget, before the run is recorded as Partial. Leave the schema unset and findings stay free-text, exactly as before.

Daily schedules are read in UTC, and the daily-times field is labelled so — a 09:00 is 09:00 UTC, not the reader's local morning.

3. On a phone

The PWA works in a phone browser as it is. The optional Android shell (mobile/) is a Capacitor wrapper whose WebView loads your instance's PWA, so UI changes ship without a new APK; only native plumbing needs one. On first launch it shows a Connect to Vogt screen and asks for your server's URL — the shell carries no baked-in address — then loads that front door; enter an http:// server only on a trusted private network (a VPN), since it sends your token without TLS. Return to the chooser at any time with the Android Back button to point the app at a different server.

  • Push arrives for the things worth an interruption and nothing else by default: a session entering waiting-for-input or errored, new drift, and the agent-task notify hook. idle_stall and agent_task_started exist and default off. Quiet hours digest instead of sending. Browser web-push needs only the engine's VAPID keypair, generated into its state directory on first run; push to the Android shell additionally needs ENGINE_FCM_SERVICE_ACCOUNT_FILE (a path to the document; the inline ENGINE_FCM_SERVICE_ACCOUNT_JSON is also accepted, but puts a private key in the environment and is warned about), and without it only browser subscriptions are delivered.
  • A session waiting for input becomes a card, above the roster, showing the prompt it is actually waiting on. Send y + Enter and Send Ctrl-C send those keystrokes to that session's terminal — they are terminal input, not Vogt approvals, and the card says so. If the prompt cannot be read, or the session has exited, the card says that instead and offers nothing to press. Below the cards, the running sessions that are not waiting still list as rows, so an idle or busy session is one tap from its terminal.
  • A terminal and the Assistant own their phone headers. On the overview, Sessions shows its kicker, title, live/stale connection truth and scrolling tool strip. Opening a terminal replaces that chrome with a compact session bar: Back, activity/name/path, Find and the overflow that retains terminal theme, font, broadcast, split and close actions. Assistant uses its own compact watch status and actions. Other machine tools retain the folded Sessions header, so no route stacks two headers over its work.
  • Open terminals form an identity-stable pager. Swipe horizontally on the session bar, dots or terminal (a vertical gesture still scrolls), tap a state-coloured dot, or focus the bar and press Left/Right. The pager follows the full attention order, including exited sessions; if live activity reorders that list, the terminal you selected stays selected. Back always returns to the Sessions overview, and the bottom place bar stays out of the terminal screen.
  • The editor gives the code the width. The Files sidebar is an overlay drawer that starts collapsed, so the editor gets the whole screen rather than a sliver beside a fixed column; > slides the drawer in over the editor and tapping the shaded area behind it (or <) closes it again. The split divider resizes on a touch drag, and a Git diff renders inline — one column, changes marked in place — instead of the side-by-side view that only fits a desktop's width.
  • The bottom bar reaches four places — Sessions, Inbox, Board, Backlog — and a fifth More slot opens a sheet with the rest (Projects, Audit, Git, History, Tasks, and GUI stream and Assistant where those are enabled) plus Settings and Sign out. Every place and both account actions are two taps from any surface. The bar lifts above the soft keyboard rather than hiding under it.
  • Modifier row above the soft keyboard: the first tier is Esc, Tab, sticky Ctrl and Alt, ^C, Up, Down and ⋯. The final key reveals the remaining arrows, Home/End/PgUp/PgDn, / | ~, Enter, typing, selection, copy and paste controls. Tap Ctrl then a letter on the soft keyboard to send Ctrl+letter (tap Ctrl then r for ^R); tap Alt then a letter for the Esc-prefixed sequence. An armed Ctrl/Alt clears itself after about five seconds, or the moment you use it.
  • Use the terminal's labelled A− / A+ controls for terminal-only font size; the size is remembered per device. Browser pinch/Ctrl+wheel zoom remains the normal whole-app zoom gesture.
  • Copy/paste: long-press to select — a floating Copy chip appears over the selection; tap it to copy. To paste, use the modifier row's Paste button (iOS Safari does not raise the desktop long-press-to-paste menu).
  • Every control is a thumb's size. On a phone, or on any touch screen, no button, tab, tick box or link in the navigation is smaller than 44 by 44 pixels, and rows of them keep space between the targets rather than only inside them.
  • Nothing you type into is under 16px. Below that, mobile browsers zoom the page when the field takes focus and the layout moves out from under you mid-sentence; the floor is set once, for every field in the product.
  • The board renders as a list rather than columns below the narrow breakpoint.
  • Each surface arrives with its work on the screen. Sessions leads with the session wanting attention, Inbox with the first attention row, Board with the selected state's cards, Backlog with the first ranked row. What a surface keeps for reference rather than for steering — a refresh cadence, the saved lenses — is one control away: View controls in the header, and + Filter for the lens row.
  • Tapping a notification lands on the terminal that raised it.

4. Keyboard reference

Shortcut Action
Ctrl/Cmd+K Command palette
Ctrl/Cmd+B Show or hide the Places rail (desktop)
Ctrl/Cmd+Shift+T New terminal session
Ctrl/Cmd+Shift+W Close active tab
Ctrl/Cmd+Alt+←/→ Cycle tabs
Ctrl/Cmd+Shift+C / V / A Copy / paste / select all in a terminal
Ctrl/Cmd+Shift+F Find in the terminal buffer
Middle-click Paste (Linux convention)
Right-click Copy if there is a selection, else paste
Ctrl/Cmd+S Save file (editor)
Ctrl/Cmd+F / H / G Find / replace / go to line
Alt+↑/↓ Move line
Ctrl/Cmd+D Add cursor at next match
j / k Inbox: focus the next / previous entry
e / s / r Inbox: archive / snooze / resolve the focused entry
? Open shortcut help outside text fields, editors and terminals
Board: ←/→, ↑/↓ Move focus across columns and within one (a card focused)
Board: Shift+←/→ Propose a move (the same reason prompt as a drop)
Board: Enter / Space / click Open the focused card
Board: n Quick-create a work item

Desktop Places and the phone bottom bar share live workload counts. A numeric zero means the corresponding canonical read completed and found none; an ellipsis is still loading, a dashed retained value is refreshing, and an em dash means that provider is unavailable. Sessions additionally calls out the number waiting for input. Desktop session rows are keyboard links: focus a row and press Enter or Space to open it, then Tab to the labelled bookmark, duplicate and close controls.

The command palette reaches every surface the Places rail and phone bar do, and every read surface by fuzzy name — projects and work items included, so myprj finds my-project. Alongside the views, it navigates to Open Inbox, Open Sessions, Open History, Open Git, Open Tasks and — when the assistant is configured — Open Assistant, and it offers Sign out; the keyboard is never a poorer map than the chrome. Matches are ranked, not merely filtered: a name match always outranks one that only appears in a row's description, so typing a session's name surfaces that session first, and sessions rank ahead of work items. A command bound to a keyboard shortcut shows it on its row, and the commands you have run most recently appear under Recent at the top of the empty palette. New File opens a form with separate workspace destination and filename fields; Open File… opens a searchable workspace-file chooser whose results you can walk with the arrow keys and open with Enter, without leaving the search box. Cancelling either returns focus to the palette invoker, and creation writes only after Create file succeeds — and refuses, rather than silently overwriting, if a file already lives at that path. Other mutating verbs whose collector is a place — quick-create, the drift inbox, the import form — open that view and do nothing else; per-item verbs (transition, comment, start session) are reached through the item's own entry, which opens the page carrying all three forms: the Move to control changes its state, the comment form adds a comment, and Start a session — collapsed behind a button — opens one. Each collects the typed reason its write needs.

Opening the palette always focuses a blank query and selects its first result. Arrow keys change the announced selection, Enter activates it, Tab stays inside the modal, and Escape closes from anywhere in it and returns focus to the button or shortcut that opened it. Pointer and touch activate the same command rows. Shortcut help states where each binding applies; in particular ? does not take a literal question mark away from an input, editor or terminal.

Opening the palette renders its static and already-cached commands immediately. Tasks, work items, and projects load independently, so one unavailable provider does not block navigation commands. Prefix a query with # to discover workspace project actions from manifests; that bounded scan is deferred until it is requested and reused on later openings. Prefix with /, >, or @ for file, archived-output, or current-file symbol search. Typing ? on its own lists these prefix modes as a legend, so the vocabulary is discoverable once the placeholder text has scrolled away. Closing or changing a special search cancels its obsolete request. Choose Refresh Command Palette Data when a write elsewhere makes a cached command list out of date.

Prefix a palette query with > to search archived terminal output. Choosing a match opens a shareable History URL carrying the query, archived session and selected excerpt; reload restores the same marked context, and Browser Back returns to the route from which the palette was opened. The Search History command opens History with its archived-output search field focused.

5. When something is missing rather than broken

Vogt is two processes, and each keeps working when the other does not. This is a designed property, not a degradation to be surprised by, and the surfaces say which case they are in rather than rendering empty data as truth.

What is down What still works What you see
vogt-core Terminals, files, git, history, the assistant over sessions Vogt surfaces report the outage by name; they do not render an empty backlog
the engine Every Vogt view, and the CLI/REST/MCP surfaces Session controls disable with the reason stated
the assistant Everything else The tab is absent, not broken

A lost event stream is indicated and reconciles on reconnect. A stale view never presents itself as current — everything that aggregates says how old its answer is.

6. Common problems

Copy/paste does not work. The clipboard API needs HTTPS and a permission grant. Right-click always works as a fallback; on mobile, use the paste modal.

A session will not start. Check that you are signed in, and that your login or token carries work.write or project.write — read alone can list sessions and read a scrollback and cannot start or attach to one, because attaching writes to a PTY.

A Vogt surface says the core is unavailable. That is a real outage of a real thing, not an unconfigured feature. /readyz's vogt_core check is the place to look, and it is deliberately non-fatal: restarting the container would not revive the core and would kill every live PTY.

The editor will not save. Check that the token carries filesystem-write, then check file permissions in the workspace.

A write is refused with a reason you did not expect. Read it — Vogt refuses with the rule named. An illegal workflow transition, a missing reason, and a scope the token does not hold all say which they are.

7. Frequently asked

The public demo

A deployment showing the persistent Demo data strip is the public demo, not a connected Vogt instance. Its Board, Backlog, Inbox, Projects, Audit, files, Git, tasks, Assistant and sessions are deterministic fictional data. Writes last only in that browser tab. Reset demo restores the canonical tour; Sign out shows the normal sign-in surface and a reload restores the disposable demo identity.

The strip links directly to three terminal compositions: Build + tests is a two-pane row, Agent review is a nested three-pane view, and Incident view is a three-pane broadcast example. They use the real attach/replay ordering, but the output is scripted. Input matches a small canned command list and is never executed. The GUI stream is likewise a same-origin static illustration, not a remote desktop. The source link in the strip names the exact commit whose PWA asset hashes the demo advertises.

Mobile app opens a separate showcase page with the real responsive PWA in a phone-sized frame. Its Sessions attention cards, terminal pager and structured Assistant approval are the same components loaded by the Android Capacitor WebView; only native push delivery and microphone plumbing cannot be demonstrated by the browser frame.

Can I use it offline? No. Installed PWAs show an explicit offline fallback page rather than pretending to support disconnected use — a queued offline write could not carry an honest freshness answer, so none is offered.

Where are my files? On the server, under the workspace root — the same tree Vogt's project registry records paths in. That agreement is checked and reported at /readyz.

What happens if I close the browser? Sessions keep running. Reconnect from anywhere and the scrollback is replayed.

Can several people share one instance? It is single-tenant by design: scopes are instance-wide, and there is no per-user server state. Each person should have their own.

How do I back up? vogt backup takes the core's SQLite and the engine's state directory — session history, push subscriptions, the VAPID keypair — as one act, plus a manifest recording what was backed up from where. Your own work is in git, in the workspace. DEPLOYMENT.md covers restore.

How do I update? The PWA updates itself; hard-refresh if it seems stuck. The server side moves when the operator pulls a new image (or rebuilds from source) and restarts — publishing an image and deploying it are separate acts on purpose, and DEPLOYMENT.md describes the upgrade order.

8. Driving it from a terminal or an agent

Everything in this guide is also a CLI verb, a REST route and an MCP tool, generated from one operation registry — that is what the parity tests assert.

$ uv run vogt backlog
$ uv run vogt why --ref WI-1
$ uv run vogt contract adopt --project vogt --reason "we keep these conventions"
$ uv run vogt contract check --project vogt --reason "before release"
$ uv run vogt project scaffold --project vogt --reason "close what the check found"

The contract is something a project opts into. A project you register is not measured against it and is never called non-compliant: it reports not_applicable until somebody runs contract adopt, and it is refused nothing either way. Where a check does find a gap, it says what would close it — project scaffold writes the missing skeleton without overwriting anything already there, and where the content is a decision (which licence, what belongs in design/) it says that instead of choosing for you. A criterion your project cannot meet at all — a Cargo workspace has no root src/ — is declared with contract inapplicable and a reason, and stops being counted as a failure while staying on the report.

The same operations are REST routes under /api/ — send a core token, an API token or your own session, as Authorization: Bearer …; GET /openapi.json lists them. For an agent, vogt-mcp speaks MCP over stdio against the same data directory (VOGT_DATA_DIR), and vogt-mcp-remote bridges stdio to a remote instance's /mcp using VOGT_URL and VOGT_TOKEN_FILE. An agent's tool list is exactly what its token may do — ungranted tools are absent rather than present-and-refusing — and every write it makes requires a reason and lands an audit row carrying it.

The only optional integration on the core side is GitHub collection and write-back, enabled by VOGT_GITHUB_TOKEN_FILE; absent means "GitHub was not collected", never "no GitHub subjects". Vogt does not require or contact any other external service. CONFIG.md has the variables.

9. Publishing an initiative to the forge

An initiative is a cross-project epic that lives in Vogt. That keeps it simple, but it also makes it invisible to anyone who only has the forge open. initiative publish closes that gap without giving up the simplicity: for each forge-linked project the initiative spans, it creates — or, on a later run, re-adopts — one tracking issue that carries a checkbox task list of the initiative's member work items.

$ uv run vogt initiative publish --slug platform-epic --reason "make the epic visible"

To correct an initiative later, or to close it, use initiative update. The slug stays as it was, because the tracking issues are labelled with it:

$ uv run vogt initiative update --slug platform-epic --body "Narrowed scope" --reason "scope changed"
$ uv run vogt initiative update --slug platform-epic --state closed --reason "shipped"

The tracking issue is labelled initiative:<slug> and its body looks like this, one line per member — - [ ] #<issue> <title>, checked when the member is done or won't-do:

<!-- vogt:initiative:start -->
<!-- vogt:initiative:platform-epic -->

The platform epic.

### Work items

- [x] #4 Land the provider seam
- [ ] #5 Wire the collectors
<!-- vogt:initiative:end -->

Four rules make this safe to run again and again:

  • It only ever touches the managed region. Anything you write in the issue outside the two <!-- vogt:initiative:… --> markers is yours and is preserved on every re-render — Vogt rewrites the block between the markers and copies the rest of the body through untouched. Adding or removing a member (with work update --initiative) re-renders the list in place.
  • It adopts, it does not duplicate. The hidden marker in the body is how a second publish recognises the issue it already opened. You can even paste the two markers into an existing issue to hand it to Vogt.
  • It never closes the tracking issue. Closing is somebody's call. When the initiative itself is closed, publishing proposes closing each tracking issue as a drift proposal you resolve by hand — Vogt does not write the close.
  • A tick upstream is drift, not a fight. The tracking issue is observed like any other issue, so if a person ticks a box on the forge while the member is still open in Vogt (or the other way round), the next sweep surfaces the disagreement as initiative_checkbox_drift in the drift inbox — neither side is silently overwritten. The next re-render would restore the box; accepting or rejecting the proposal is how you decide which register is right.

A cross-project initiative gets one tracking issue per repo, each linking the others. Publishing writes to the forge, so a project needs forge writeback set to full and a linked repository first (forge link); a repo the initiative touches that is not linked is reported as skipped, with the reason, rather than dropped silently.