Skip to content

Orgabot is in private alpha. Request access, or join our Discord.

← All documentation
Using Orgabot

The dashboard

The localhost ops dashboard is the day-to-day operating surface (overview, missions, repositories, issues, inbox, analytics and budgets), plus the gotchas that come with a long-running process.

npm run orgabot -- dashboard [--port N]     # from framework/; default http://127.0.0.1:4317

The dashboard is localhost-only and is the intended day-to-day operating surface. The CLI and Claude Code remain useful fallbacks.

Screenshots of each surface belong here and are not in place yet. They have to be captured from a real dashboard against real state, and an illustration of one would be worse than none. Drop them into site/public/docs/ and reference them from this page; Writing documentation has the mechanics.

What each surface does

SurfaceWhat you do there
Overview / Get StartedThe canonical onboarding checklist. orgabot doctor's "Next step" banner is computed from the same journey.
Since you were goneThe Command Center panel that answers what happened while you were away, most recent first, with the handle to continue from beside each item. Painting it changes nothing; only Mark caught up advances your watermark. Same digest as orgabot catchup - see Catching up.
New Mission terminalLaunch a mission and stay attached to it. See The mission terminal.
MissionsCards for live and finished work, with an AI-derived human title and the worker model and provider that ran it.
RepositoriesOne card per git root, with monorepo sub-projects nested inside it, plus Add repository: the creation wizard that registers a local repo or clones one from GitHub. See Adding a repository.
System → Capability RegistryRead-only visibility into adopted MCP servers, which roles hold grants on them and what those grants actually materialize, attached skills, and the doctor findings with their exact fix commands. See The Capability Registry tab.
IssuesGitHub issue triage through a scoped Issue Curator credential rather than your ambient gh.
IncidentsFailures from every source (Orgabot's own failed missions and held review cycles, GitHub's failed workflow runs and deployments, and Firebase App Hosting's failed rollouts and builds via the Firebase connector), each with a Fix with Orgabot button, plus projects delivering with no verify command.
Inbox / approvalsPending requests routed to the role that must sign off.
Security → Approvals & AccessConnector and repository grants, identities, pending organization approvals, and memory findings. A memory finding is a conflict alert (two knowledge records disagree on the same topic), not a vulnerability. Resolve acknowledges it; it does not merge or delete the records. See Context and memory.
Security → Memory AccessMemory declassification grants: release one knowledge record to one provider, for a bounded time, on a resolved approval. Handling ceilings are read-only here. Same records and same governed mint as orgabot memory declassify. See Context and memory.
DepartmentsA business-facing read model: participants, staffing, active work, blockers, capability-slot completion.
Organization → GovernanceResponsibilities, councils, trust, decisions, and handoffs, from the same projection the CLI reads. Assign or close a responsibility, seat or unseat a role, record a decision, advance a handoff. A trust level is read-only here. See The Governance tab.
Analytics and BudgetsCost accounting off the metrics journal, and spend and quota gauges.
Quota meterA gauge in the mission status bar showing the smallest remaining plan window; click it for each executor's session and weekly detail. See the status-bar quota meter.
Queued counterAn amber N queued count beside the quota meter, for launches waiting in the implement admission queue. It appears only when something is actually queued, and clicking it expands the queue so you can cancel any launch you no longer want.
Organization switcherThe header control that also opens the four-step new-organization wizard.

Starting the stack

One command brings the whole local control plane up and supervises it:

orgabot up                  # dashboard + Restate + Mission service + registration + GitHub webhooks
orgabot up --no-restate     # or --no-github, --no-dashboard
orgabot stack status        # what is up right now, without starting anything

orgabot dashboard on its own also brings Restate up first (issue #1888), so missions launched from it are restart-safe by default; pass --no-durable (or --no-restate) to launch the dashboard alone, without Restate. It never starts the GitHub webhook listener as a separate supervised process the way orgabot up does; that piece is its own auto-connect (issue #997), controlled by --no-github-connect.

A few properties worth knowing:

  • A second `orgabot up` is safe. Anything already answering is reused, never re-bound, so you do not get port-in-use chaos and nothing healthy is reclaimed out from under the work it is doing.
  • Bring-up is ordered. The Mission deployment is registered only after both the runtime and the service answer, so registration cannot race the service's listen.
  • A missing prerequisite is a stated skip, never a failure. Without ngrok, the dashboard and Restate still come up and the GitHub line says why it was skipped. Every component gets a line, including the skipped ones.
  • Companion logs stay out of your console. Restate's INFO stream is extremely chatty; each companion writes to its own file under ~/.orgabot/logs/stack/, and the console gets one line each.
  • The durable service runs the real coding worker, not the mock, unless you set ORGABOT_WORKER yourself.
  • Ctrl-C stops the whole group. Companions are signalled as process groups, because npx and ngrok are launchers whose real work survives a signal sent only to them.
  • The dashboard is never tunneled. github connect exposes only its HMAC-signed webhook route, and starting the stack does not widen that.

orgabot down tells you how to stop it. It deliberately does not hunt for processes to kill: the orgabot up that started the companions owns them, and a command that went looking for Restate-looking processes would cheerfully take down a Restate you are running for something else.

Adding a repository

Repositories → Add repository registers a project without dropping to a terminal. It is the dashboard equivalent of orgabot project add, and both routes run the same detection, so neither can drift from the other.

There are two ways in, and they converge on the same confirmation step:

  • A repo already on this machine. Type the path, or use Browse... to open the native folder chooser. A host with no folder chooser hides the button and leaves the text field working.
  • Clone from GitHub. Type an owner/repo slug or a GitHub URL that reduces to one and press Clone. Cloning uses whatever git credentials the host already has; Orgabot stores no new credential. The wizard polls the clone job and opens detection automatically when it lands, so a clone and a hand-cloned path are indistinguishable from there on.

Then the wizard runs detection and shows you what it found before anything is saved:

  1. Detect posts the path to a preview that runs project autodetection and saves nothing. A path that is not a git root, or a workdir that does not exist, is refused here with the same message the CLI gives.
  2. Confirm shows the detected default branch, package manager, and logo, and lets you override the name, the workdir (a monorepo sub-directory; blank means the repo root), the verify command, and the logo (upload an SVG or PNG, or point at a repo-relative path). Only Register writes to the registry.
  3. Done confirms the project and it appears in the cards below immediately. If the repo carries no ORGABOT.md, the wizard says so and gives you the exact orgabot project init <id> command to add worker context. See Invariants for why that operating reference is repository-controlled context and never authority.

A logo that fails validation rejects the whole registration, so a half-registered project never lands.

Where a clone lands

You do not choose the destination, and that is deliberate. A dashboard-initiated clone lands only under Orgabot's managed repos root, ~/.orgabot/repos/<owner>/<repo>, at a path derived from the slug:

  • The slug is checked against a strict allowlist before any path is built, so .., a separator trick, or anything outside owner/repo never reaches the filesystem.
  • The derived path is then resolved with the same realpath-first containment check the project registry uses: symlink resolution first, containment check second, reject on escape. A symlinked parent cannot smuggle the clone out of the root.
  • A destination that already exists is refused rather than written into. Register it as a local path instead.

If you want a clone somewhere specific, clone it there yourself and use the local-path entry. That keeps the free-form destination out of Orgabot, which is the traversal surface the root boundary exists to close.

The Capability Registry tab

System → Capability Registry answers "what capability exists, who holds it, and is it healthy" without running a CLI verb. It is display only: the tab adopts nothing, grants nothing, and revokes nothing, so it carries no authority of its own and turning it off would remove only visibility. Every repair it suggests is a command you run yourself.

The registry is installation-wide and deduplicates MCP definitions by transport fingerprint. Claude/Codex config entries are source evidence only. Planning, import, manual definition, and bearer/environment custody use the printed CLI commands; this tab does not perform them. OAuth connect, reconnect, and revoke controls live on the organization's Connections card. Only protected-secret handles enter durable records. None of these operations grants a role. Retain the plan and apply receipt, inspect this registry and Doctor, then prove a granted read-only call uses Orgabot's canonical profile. Keep intentional Claude/Codex entries for direct harness use. Delete one only when you no longer want it in that harness, after proving the Orgabot call still works with the entry temporarily set aside. Unknown status is a stop condition, not an all-clear.

Fingerprint dedupe is installation-wide: equivalent transports share one canonical server. Inside one organization, equivalent unresolved observations share one pending connection profile; different organizations and different account/credential profiles remain separate by design. A reconnect or fresh validated enumeration updates the canonical server inventory and its evidence, rather than creating a duplicate server. It never changes role grants.

The whole page is one typed projection served by GET /api/registry, so the tab cannot disagree with enforcement about what a grant means. It refreshes only while it is open, because the read touches the org store, the packs directory, and the doctor.

Per organization it shows:

  • An MCP server card for each adopted local MCP server: its name, category and source, its transport (redacted, exactly as adoption stored it, since adoption stores no secret), and whether its tool inventory has been enumerated and how large it is.
  • A role grant card inside each server card, one per role holding a live grant: the grant's scope, its expiry (or "no expiry"), and the effective tools it materializes. That list is computed by the same deny-by-default clamping the launch path applies, so a grant that materializes nothing is shown as exactly that, with the fail-closed call-out explaining why, rather than looking like a working grant.
  • A skill card per capability pack: whether it is inline knowledge or backed by an installed SKILL.md, which roles it is attached to, and its resolution status. A skill-backed pack whose source cannot be read shows unavailable, which is the same case where brief assembly substitutes its fail-soft "knowledge source unavailable" note.
  • A doctor panel carrying orgabot doctor's findings verbatim, each with the exact fix command to paste.
  • An assurance pill on each role grant card, reading "2 / 2 missions informed". It counts the missions recorded for that role and how many of their briefs actually stated this grant's effective tools. A grant whose tools never reach a brief is permitted but unused, and this is where that shows up: informed means every recorded mission was told, missing means at least one was not, and no missions yet means there is nothing to conclude either way.

Project-scoped doctor findings get their own panel below the organizations. A project id is not an organization slug, so folding them into an org would hide them, or misattribute them on the rare occasion the two strings match.

The plan on a mission

An organization mission's drill-down shows the Plan that launched it, under the definition of done: the Chief of Staff's decomposition, or the single approved step. Each step states its accountable role, its sub-goal, why that role, what counts as done, what it starts after, and its status, and the step that is this mission is marked. The plan is read from the recorded Mission Plan, which owns its state; the page shows it and never edits it. A mission no organization launched has no plan section. Opening Details from a running mission's terminal closes the terminal, so the record is read on its own page.

Capability assurance on a mission

Each organization-linked mission records the capability facts its brief carried, and the mission drill-down shows them under Capability assurance: the accountable role, the MCP tool names the brief stated, and the resolved skill knowledge it carried. An empty list reads as "No MCP tools were stated", so a brief that announced nothing is distinguishable from a mission that recorded nothing.

The block is information, never authority. What a worker may actually call is decided by the materialized allowlist at launch and re-checked by the gate on every command; the brief only tells the worker what it holds, because a grant a worker never learns about goes unused. A plain (non-organization) mission carries no block at all, and its brief is byte-for-byte what it always was.

Two reading rules the tab follows, because an audit surface that reads clean when it is actually blind is worse than one that is obviously broken:

  • A missing list is "unknown", never "none". Only a real empty list reads as none.
  • A failed refresh keeps the last good payload and labels it STALE, next to the timestamp the server generated it.

The Governance tab

Organization → Governance is the dashboard equivalent of the five governed command families: orgabot responsibility, orgabot council, orgabot trust, orgabot decision, and orgabot handoff. It reads one endpoint, GET /api/organizations/:org/governance, whose payload is the same projection orgabot responsibility list and orgabot trust history read, so the browser and the terminal cannot drift into answering "who is accountable" differently.

It shows five things for the selected organization:

  • Responsibilities per subject, with the role currently accountable and the closed assignments behind it. Naming a role accountable grants it nothing: access stays separately granted and separately revocable.
  • Councils, with their charter, decision rights, quorum, seats, and whether the body is currently below quorum. An observer never counts toward quorum, so a council does not read as able to decide because somebody joined to be informed.
  • Trust, as the version chain for each role with the evidence each judgement cites.
  • Decisions, with the alternatives weighed, why each rejected one was rejected, and the observed outcome.
  • Handoffs: what a person has been asked to do and where that ask currently stands.

Four operations are performable from the page, and each one posts to the same governed store the CLI writes through, so the append-only ledger entry is identical whichever surface produced it: assign a responsibility, close one, seat or unseat a role on a council, record a decision, and advance a handoff. Refusals the model already carries come back in the store's own words and are reported rather than worked around - a second accountable role for one subject, a seat that already ended, a rejected alternative with no reason, a handoff already in a terminal state. Nothing is deleted: a responsibility and a seat both end by being closed, because "who was accountable when this happened" is the question the model exists to answer and a removed record answers it wrongly with total confidence.

Two rendering rules here are load-bearing rather than cosmetic:

  • A trust level is advisory, and the tab carries no control that changes one. A level grants, widens, and clamps nothing, and there is deliberately no button beside it - a control that changed a level next to one that changed access is how an advisory number starts reading like an authority. Set a level from the terminal with orgabot trust set, where the change is versioned and audited.
  • A decision nobody has revisited renders "not followed up", never neutral or blank. A decision whose outcome was never observed must not read like one that worked.

Live GitHub state (optional)

npm run orgabot -- github connect
npm run orgabot -- github disconnect

This wires live PR, CI, and issue state into the dashboard through a webhook listener and an ngrok tunnel. Only the HMAC-authenticated POST /webhooks/github route is tunneled; the dashboard UI is never tunneled. The tabs render read models (projections) rebuilt from those webhook events.

Once this is set up, `orgabot dashboard` starts it for you. Starting the dashboard auto-starts the same webhook listener and tunnel whenever GitHub ingestion is already configured: a webhook secret has been persisted (you ran github connect, or the guided GitHub setup, at least once) and at least one GitHub App is connected. An already-running tunnel is reused rather than re-bound, and every reason not to start (no configuration yet, no connected App, ngrok missing, tunnel already up) is printed on the dashboard console as a stated skip; the dashboard itself always comes up. Opt out for one session with orgabot dashboard --no-github-connect, or globally with ORGABOT_AUTO_GITHUB_CONNECT=off. orgabot up is unchanged: the stack supervisor owns the GitHub component there, including --no-github.

An issue card is half GitHub (title, labels, open/closed) and half local mission state (which attempts ran, and what the newest one is doing right now). Only the GitHub half is held behind that rate bound. Mission status on an issue card is always current: every Issues response re-reads local mission state, so a mission that just completed, failed, or was restarted by a retry follow-up shows its real status on the next poll rather than waiting for the GitHub cache to expire.

GitHub is polled at a bounded rate, never once per UI poll. A per-batch cap is not a rate limit: a caller that polls every few seconds starts a fresh batch as soon as the last one finishes, and a large backlog then drains at poll speed. That is how a 5,000/hour GraphQL quota once emptied in about 14 minutes with no missions running.

Live Firebase state (optional)

npm run orgabot -- firebase connect --project <firebase-project-id> --orka-project <project>
npm run orgabot -- firebase check

Watches a Firebase project's App Hosting rollouts and builds, so a failed deploy becomes an incident here instead of something you find later in the Firebase console. Read-only, polled at a bounded rate on the dashboard's own timer, and never as a side effect of loading this page. See Firebase connector.

How the Issues tab loads

The tab is built so that nothing you click waits on GitHub.

  • It paints what it last saw. The open issues Orgabot last observed are kept on disk, so the first time you open Issues after restarting the dashboard the rows are already there, and a GitHub outage shows you the list you had a minute ago with its age and the error, rather than an empty screen. It is an observation, not a claim: a repository Orgabot has never read is reported as unknown, never as "no open issues".
  • The list is preloaded. Starting the dashboard warms the All Projects open-issue list in the background, so the first time you open Issues it is already there. Startup itself is never delayed by it.
  • The tab follows the project explorer. With All Projects selected you see every registered project; select one and you see that project's repositories plus its children's (monorepo sub-projects and sibling repositories nested under it in the explorer), and the sub-heading names the scope it is actually showing. Selecting a parent always means the whole family; click a child to narrow to just that one.
  • Selecting a project paints from cache. That project's rows are sliced out of the All Projects list you already have, and only that project's repositories are then refreshed behind the response. An unrelated slow repository can neither delay it nor appear in it.
  • Issues load a page at a time. The first response is one page; scrolling to the bottom fetches the next one.
  • A webhook delivery updates one row. With live GitHub state connected, an issue that is opened, closed, retitled or relabelled updates just that row within seconds. Every other row stays exactly as it was last read, and no repository is re-listed to learn it.
  • Missed deliveries are repaired in the background. If the tunnel was down, the dashboard was not running, or a repository has no webhook, a bounded pass on the dashboard's own clock re-reads the stalest repositories a few at a time, inside the same rate bound. Nothing you click triggers it.
  • Refresh no longer blocks. The button asks for a fresh read and keeps the rows you are looking at, showing a refreshing state until that read actually lands. It never blanks the list, and it never holds the screen for as long as GitHub takes.

Searching the Issues tab

A search box sits at the top of the tab. It filters the list as you type, on the issue number (with or without the #), title, repository, label, project name, and the mission id of any attempt against it. Several words narrow rather than widen: every term has to match.

  • It composes with everything else. The status pills (All / Open / Closed) and the project explorer's scope still apply on top of the query, and the pill counts describe the rows you are actually about to see.
  • It narrows Ready for Verification too, so searching for one issue does not leave an unrelated verification card sitting above it.
  • It says what it is hiding. A line above the list reads Showing 3 of 41 loaded issues matching "…", so a filtered list can never read as the whole list. Clear search (or the Escape key inside the box) restores it.
  • It searches what has been loaded, and keeps loading. Issues arrive a page at a time. With a query active the remaining pages are pulled in on their own rather than waiting for you to scroll, and until they have all arrived the tab says so instead of reporting "no match".
  • The query is remembered across a reload, which is why the tab always states, in words, that a search is narrowing the list.

The header's Search anything box (press /) covers issues as well, open and recently-closed, alongside missions, pull requests, CI/CD runs, projects and settings. Type an issue number to put it first. Choosing an issue result opens the Issues tab with this search box already narrowed to that issue, rather than dropping you at the top of a long list.

Pressing I opens the Issues tab. Pressing it again while the tab is already showing means show me all the issues: it clears the search box and puts the status filter back on All, which is the quick way back out of a deep link that narrowed the list to one issue. On an already-unfiltered tab it does nothing.

Epics group their children

An epic is not a different kind of issue on GitHub, it is an issue that names others. The Issues tab reads that structure from the issues it already imported and renders a program as one block: the epic's card, and its children indented beneath it.

Which issues are children is decided by exactly the parse the epic run uses for a body: the #N references in an issue Orgabot recognizes as a tracker. A bug that happens to mention two other issues is not an epic. A cross-repository owner/repo#N is not a child: it names work this epic does not drive.

It is the epic body's list. When a repository registers GitHub's native sub-issues, an epic run prefers those, and reading them costs one API call per issue, which a list repainted on every poll does not spend. So on such a repository the group can differ from what a Fix would sequence, and the card labels the list as the epic body's rather than claiming otherwise.

Issue bodies also arrive bounded, at the same length the on-disk observation store already used, so the list you see on a cold start and the list after a live read are the same. A body that reached the bound may have been cut, so its trailing partial reference is dropped (a cut inside #1234 leaves a perfectly well-formed #12 naming an unrelated issue) and the group says the list may be incomplete. An epic declaring more children than an epic run will sequence is capped, and the group says pressing Fix on it would be refused.

  • Children are ordered most-required-first. The order comes from what the children themselves declare (Depends on #N, or a ## Dependencies section), applied only among the epic's own children; anything that declares nothing keeps the order the epic lists it in. If the children declare a cycle, no order satisfies them, so the group keeps the epic's declared order and says so.
  • The group is collapsed by default, with a line naming how many children the epic has and how many are in the current view. An epic with nine slices would otherwise push every other issue off the screen.
  • A search never shows a child alone. Matching a child pulls its parent's card in for context and renders the match inside it, expanded, with the group saying how many other children it is not showing. Matching the epic itself shows the whole program.
  • A child can belong to several epics, and is then shown under each of them.
  • A child that is itself an epic appears twice: nested under its parent, and again as its own group with its own children. Nesting stops there, one level deep, so two trackers that name each other cannot expand forever.
  • A child whose parent is not in the view carries a part of #N link on its own card instead, so it is never shown without its context. That covers a parent on a page not yet loaded; an epic that is CLOSED is read through a separate list from the open issues, so an open child of a closed epic shows no chip.
  • Fix works at either level. Pressing Fix on the epic runs the whole program; pressing Fix on one child runs just that child. You never choose a mode: Fix submits the work item and the intent, and the capability is decided from the item's own normalized type. Here that type is read from the issue Orgabot already holds, so it counts as an epic on the same terms this group does: a tracker label or title and the children it names. A lone epic label on an issue naming no children is not enough to start a fan-out, because anyone with triage permission can add one. On a project an organization owns, an epic launches autonomous epic execution under the routed role, with every child still passing its own verification, review, and merge gates; otherwise it starts an epic run over its children. An ordinary issue takes the ordinary fix path either way.

Talking to Orgabot from the issue itself

Orgabot can answer on the GitHub issue thread. Mention it in a comment and the Issue Curator replies there, so you can ask a question from wherever you already are, with no dashboard. It is off by default: set ORGABOT_ISSUE_CHAT=1 in the environment the webhook listener runs under, and connect GitHub so deliveries reach Orgabot at all.

A reply is Orgabot posting under your GitHub App on a repository anyone may comment on, so what it refuses is most of the feature. Five checks, in this order, and all of them before a model is ever called:

  1. It never answers its own comment. A reply Orgabot posts arrives back as another comment delivery, so answering one would answer forever. Any bot is refused, not only Orgabot: two automations answering each other is the same loop with more steps.
  2. Only a trusted author. The same allowlist and trust-all opt-in that govern whether an issue may launch a mission. A reply spends your model budget and speaks with your App's voice, so it is not a smaller decision than a launch.
  3. Only when addressed. The comment must @-mention one of your configured self logins (ORGABOT_ISSUES_SELF_LOGINS). Without that Orgabot would join every human conversation on every issue.
  4. Only a new comment, not an edit or a deletion, and only on an issue: a pull-request thread belongs to the review loop.
  5. Only at a bounded rate. A busy thread cannot become a stream of public comments.

Past those, the model decides. It is told it can only reply (it cannot change code, start or stop a mission, or close the issue), and that saying nothing is a correct outcome, so a comment that warrants no answer gets none. It is grounded in the issue's own text, the recent thread, and Orgabot's local mission records for that issue; it makes no GitHub call of its own beyond reading the thread.

What it is allowed to read is narrower than the thread. An earlier comment is quoted only if its author is trusted; an untrusted third party's comment is dropped, not fenced, which is the same rule that governs issue text reaching a mission. Only a login you configured as Orgabot's own counts as Orgabot's earlier turn, so a third-party bot echoing a reporter cannot have that text arrive as something Orgabot said. Everything quoted is length-bounded and stripped of the markers that delimit the brief, so a comment cannot end the data region and continue as an instruction. And the thread excerpt is the recent turns, not the oldest, so on a long issue Orgabot answers what is actually being discussed.

Every reply carries a footer naming the Issue Curator, so a human reading the thread can tell an agent's answer from a maintainer's.

A refusal is silent on GitHub. Nothing Orgabot declines to answer produces a comment saying it declined; the reason is logged locally instead. And one comment is never two replies: the delivery is claimed before the model is called and the post itself is an idempotent issue operation keyed on that delivery, so GitHub's at-least-once redelivery cannot double-post.

Who may launch a mission from an issue

An issue body becomes the instruction driving a worker that has git and pull-request authority, so not every issue is allowed to start one on its own. Trusted issue authors, at the top of the Issues tab, is where that is configured.

The rule it configures governs the paths where Orgabot decides for itself: the automatic issue sweep, an epic run launching children you did not individually pick, and whose issue comments are read into a mission instruction. Pressing Fix with Orgabot yourself is your decision and is never governed by it, which is also the way to run one issue whose opener is not on the list.

Two settings, and by default neither is set, which trusts nobody:

  • The allowlist. GitHub logins that may auto-launch a fix-and-ship mission. Type them comma or space separated; a leading @ is fine. A login GitHub could not have issued is refused rather than quietly dropped, so a typo cannot look saved.
  • Trust every author. The deliberate trust-everyone opt-in. On a public repository this lets any reporter's issue text drive a worker, so it asks before turning on and stays behind a warning while it is on.

Both were previously environment-only (ORGABOT_ISSUES_ALLOW_AUTHORS and ORGABOT_ISSUES_TRUST_ALL_AUTHORS=1, also read under their older ORKA_ spellings). The environment still works and nothing about an existing install changes. The panel names which spelling supplied a value, so what you are reading is never an unattributed number.

Set in two places that disagree, Orgabot refuses rather than guessing. It does not prefer the environment (which would make your dashboard edit look saved and do nothing) and it does not prefer the dashboard (which would make your shell profile silently stop working). The panel shows the conflict, names what each side holds, and the setting resolves closed meanwhile: an unresolvable trust configuration must never be able to widen who reaches a worker. The two settings resolve independently, so a conflicting allowlist does not switch off a cleanly-configured opt-in. Either Clear dashboard allowlist (handing the decision back to the environment) or unsetting the variable resolves it; identical values on both sides are not a conflict at all.

When a launch is refused for this reason, the refusal says so by name: its opener @X is not a trusted author. The epic run's report names every child it passed over for it, so a run that delivered nothing tells you why rather than looking empty.

The automatic issue sweep never launches on a stale belief

A GitHub webhook delivery is a snapshot of the issue as it looked at the moment it fired. GitHub redelivers, a sweep can be held for an unrelated reason and pick the same delivery back up later, and a busy install can simply fall behind, so by the time an issues opened delivery is actually acted on, the issue itself may have moved on. The sweep never trusts the delivery alone:

  • It re-reads the issue's live state immediately before launching. If the issue is already closed on the provider, nothing starts: the sweep records that outcome once, so the exact same stale delivery is never re-decided on a later pass. If the live read fails (rate limit, an outage, a permission gap), the sweep holds instead of launching; an unreadable state is never treated as evidence the issue is closed.
  • A stored closing delivery cancels a stored opening one. If the event store also holds a closed, deleted, or transferred delivery for the same issue, the opened delivery is dropped before the live read even happens, whichever of the two arrived first.
  • A delivery older than 24 hours is not "newly opened." Past that age it is unhandled backlog: it stays visible as an ordinary open issue on this tab, where Fix with Orgabot or Decline still apply, but the sweep will not auto-launch it on its own.

Every decision a sweep makes, a launch, a hold, a skip, and this stale-delivery outcome, is appended to a local, durable log (~/.orgabot/issues/decisions/), one file per day, so a burst of unexpected launches (or a burst of silent holds) can be diagnosed after the fact instead of guessed at from whatever happened to still be in memory.

Verifying a delivered fix

Ready for Verification is one of the Issues tab's status filters, beside All and Open, and its pill carries the number waiting. It is derived state, not a queue you maintain: an issue appears there when a merged pull request closes it via a closing keyword, and each card names the issue, its real current state on GitHub, and the pull request credited with the fix. Two buttons settle a card.

The filter only appears while something is actually awaiting a check, and it disappears again once the last card is settled. It is a filter rather than a section stacked on top of the list because a long verification backlog used to push the open issues off the screen.

  • Verify closes a still-open issue with a comment crediting the pull request, or records a human sign-off on one the merge already closed.
  • Not Complete opens a composer for what is still missing. The note is posted to the issue, an issue the merge closed is reopened, and unless you uncheck Fix now a fresh fix-and-ship mission starts right away carrying your note.
  • Discuss opens a conversation with the role that did the work, about its choices and what you need to do next. It is the one action on the card that settles nothing.

Discuss is advisory, and that is the point. Verify and Not Complete are decisions and both are terminal; Discuss is the question you ask before making one. It launches no mission, resumes none, posts no comment, and moves nothing about the issue. If you ask it to do something, it tells you which control does it.

It answers as the organization role that owned the delivering mission, and says plainly when the work was not role-owned rather than borrowing a role that did not do it. Everything it knows is first-hand and local: Orgabot's own mission records, their objectives and summaries, the pull request, the verification evidence, and any rejection note you typed earlier. It makes no GitHub call, so a rate limit cannot hold it up. Where it does not have a fact it says so and names where you can see it, rather than guessing at a diff it was not shown. An absent verification observation is reported as an absence, never as a pass.

The issue text, the worker's own summary and the pull-request prose are written by whoever opened the issue and by a worker, so the conversation treats them as data to report on, never as instructions to follow. The transcript lives in the browser for as long as the card is open and is not stored: a conversation about a delivery is not mission state. It needs the Help Desk assistant provider configured; without one the panel says there is nobody to ask.

Either decision is terminal for that card. The buttons come off the moment the write lands, the confirmation (including the id of any mission just launched) is held on screen for about ten seconds, and the card then fades out of the list. It leaves whether or not the refresh behind it has caught up, which matters most for Not Complete: that request launches a mission before it answers, and the list re-read behind it may serve a cached page first, so a rejected fix used to sit in the verification queue with its buttons still live.

A rejection is scoped to the pull request it rejected, both on screen and in the durable record. Rejecting one attempt does not make the issue permanently unverifiable: when a later pull request closes the same issue, that genuinely new fix comes back for a fresh check.

An issue whose mission is queued

When admission control (token bandwidth, the concurrency cap, host resource pressure, or the host-wide admission budget below) has not started an issue's fix mission yet, the issue card says queued: an amber ⏸ queued status pill, the mission id, and the reason it is waiting. It does not say cancelled. Nothing was discarded, and the mission relaunches under that same id when capacity returns.

The card offers no second Fix with Orgabot while it waits, for the same reason a throttle-queued incident offers none: the throttle queue already owns the relaunch, so a fresh launch would put two missions on one issue. To take the issue back, cancel the queued launch with orgabot throttle cancel <entry-id>, which ends the parked mission and frees the issue for a new fix.

The host-wide admission budget: never start-then-pause

A 2026-09-09 incident drove a 16-core laptop to a 1-minute load of 178: three concurrent review loops plus a full-repo security scan, with nothing bounding how much of that could run at once. The capacity governor (below) is an emergency brake: it fires after the host is already thrashing. This budget is the admission gate in front of it: a fixed, host-wide cap on how many of each kind of concurrent work Orgabot will ever start together, checked before the work begins.

Five kinds share one lease pool (orgabot verify slots):

kindwhat it boundsdefault
fulla full local verification run1
lighta light local verification run2
scanan independent-review security scan (semgrep, etc.)1
workera coding-worker session (the actual agent process doing the work)max(1, floor(cpus / 8))
loopa review-loop or ship-loop driver mid-round2

A launch beyond a kind's budget is queued, never started-then-paused: the worker or loop never spins up in the first place, so it never has to be found and stopped later by the governor. orgabot verify slots shows, per kind, how many leases are active, who holds each one, and the queue behind it in promotion order, the same surface orgabot verify slots has always shown for full/light/scan, now covering worker and loop too. A worker-session launch refused by the budget is reported through the same throttle-queue mechanism as a token-bandwidth or concurrency refusal (see the section above); a review/ship loop refused by the budget shows as queued directly in orgabot verify slots, since a loop is driven inline by the CLI process rather than relaunched.

Two missions on the same project whose instructions name overlapping file paths take the same project write-lease, so the second waits for the first's merge instead of producing a conflicting pull request. The 12 design-system sub-issues under one epic that all touched src/dashboard/ui/ in the same seven seconds is exactly the case this closes. The rule is deliberately cheap and conservative: any overlap in the named paths serializes; disjoint paths on the same project still run concurrently.

The lease is held by the mission, not by the process that launched it: it is taken only once the launch has actually been admitted (a launch refused by any other gate reserves nothing), and it is released as soon as the mission's pull request is observed merged or the mission reaches a terminal state, whichever comes first, whether or not that happened in the process that started it. So a mission launched from the dashboard does not keep the project reserved for as long as the dashboard stays up, and a merged mission that then holds on an unconfirmed acceptance criterion keeps its hold but not the lease: after the merge there is nothing left to serialize.

An epic coordinator (orgabot fix-epic) takes no write-lease at all: it fans out child missions and produces no diff of its own, so a lease under its id would only make its own children wait for a merge that never comes. A child may also overlap a lease its parent still holds from an older run, and the controller releases such a lease and logs that it did. Sibling children that name the same paths still serialize against each other. A mission waiting on a lease is logged as a lease wait naming the holding mission, never as "mission slots full".

Stopping a mission that is queued for a worker slot stops it for good: the queued request is dropped, and the worker never starts when the slot later frees.

orgabot verify preset --set <preset> (laptop, workstation, ci_offloaded) derives full/light from this host's cores and memory; scan/worker/loop keep their own defaults (or whatever you have already tuned) either way; set them directly in the hostVerify block of ~/.orgabot/config.json, the same file every kind above reads from, or via a future orgabot verify slots --set knob per kind.

The issue card and the mission's own detail read this from one derivation, so they cannot disagree about whether a mission is queued. See Queued is its own status.

Declining an issue

Every open issue card carries three answers, not two: Fix with Orgabot, Mark Verified, and Decline.

Decline is the answer for an issue that will not be fixed at all: a duplicate, working as intended, out of scope. It opens a text field for an optional reason (Decline & Close stays enabled with the box empty; you do not have to justify the decision), then closes the issue on GitHub. Anything you type is posted to the issue as a comment alongside the decline, so the record says why.

The decision is also recorded locally, and that record does one specific thing: a declined issue is never auto-fixed, even if it is later reopened. Closing an issue is reversible, and without the record a reopened issue would look like a fresh arrival to the automatic issue sweep, which would start exactly the mission you refused. Clicking Fix with Orgabot on the same issue clears the record, because an explicit launch supersedes the earlier decline.

Like a verification, a decline is terminal for the card: the buttons come off as soon as the write lands, the confirmation holds for about ten seconds, and the card then fades out of the list.

Shipping built-but-unshipped work

A mission can build real, correct work and stop before delivering it: the change sits committed on a local branch with no pull request. The Pull Requests tab lists that work as built · unshipped cards (visible under the "All" filter), each carrying the evidence to judge it: the branch and its base, how many commits it is ahead, how many files changed, when it was built, and the owning mission's status. The ahead/files counts come from cached local git reads; an unmeasured branch shows nothing rather than a made-up zero.

Each card carries a one-click Ship button, and a select checkbox feeds the Ship selected bulk action above the list. Both start the ordinary governed delivery for that exact branch, the same path as orgabot ship: push, draft PR, CI gate, independent review loop, and the normal merge gates. Nothing on this surface is a raw push.

A bulk ship reports per-item results: a row whose pull request already merged, or whose branch is gone, is refused with its own reason (judged from live branch and PR facts, never from the record's say-so), and the refusal never drops the rest of the batch. The CLI analogs are orgabot pending list (the same rows, with the same evidence), orgabot ship <project> --branch <branch>, and orgabot ship --all.

When a dashboard action fails

An action that fails (a mission that will not launch, a rejected write, a request that never reached the server) does not open a dialog you have to click through. A failure is not a question, so it is reported two ways at once:

  • Inline, at the control you used. The message appears next to the button or form that caused it, with that control still on screen so you can correct the input and try again. Org command-center actions report on the organization's own card. If the control is no longer on screen (you changed tab, or the panel closed), the message appears in a small dismissible strip in the bottom-right corner instead of being dropped.
  • As an incident. The same failure is raised on the header Alert Bell and on the Command Center's Incidents and recovery panel, so a message you clicked past is still there when you come back to it. Dismiss it from either place once you have dealt with it.

Questions still use a dialog, because a question needs an answer: confirming a destructive action, or typing a value the action cannot proceed without.

In those dialogs, cancel is the bottom-left button and the action you asked for is the bottom-right one, and both are labelled with the action itself rather than with "Continue" and "Cancel": ending a mission asks you to choose between End mission and Don't end mission. When the question is not phrased as a plain instruction Orgabot can name, the buttons fall back to Continue and Cancel rather than guessing at a label.

Acknowledging an incident, once

The same failure is shown in several places: Security → Incidents, the Command Center's Incidents and recovery and Problems and recovery panels, and the header Alert Bell (and its Needs your attention list). They are one incident, not several, so:

  • It is the same card wherever it appears, with the same Fix with Orgabot and Mark as acknowledged controls. A Command Center preview panel renders that card compressed rather than as a different card: the same title, status, project, and controls, with the id and progress lines left to the full view.
  • One acknowledgment clears all of them, whichever surface you make it on, including the taskbar dismissal for a failed mission. You never have to acknowledge the same incident twice.
  • Un-acknowledging works the same way, so an incident you put back is back everywhere.

Fixing a reported incident

Every incident card carries a Fix with Orgabot button next to Mark as acknowledged. Acknowledging files an incident away; the Fix button acts on it.

Clicking it opens a small composer with an optional note:

  • Leave the note empty and it is a straight autofix.
  • Type a note to guide the fix ("the timeout is flaky, raise it rather than skipping the test"). It is carried into the mission brief as guidance, not as authority.

The launched mission carries the context that incident actually needs, gathered by Orgabot rather than by you:

IncidentWhat the mission gets
Failed workflow runThe failing job's log tail (gh run view --log-failed), the run URL, and the branch it ran on. If the log cannot be read, the brief says so and tells the worker to reproduce the failure locally rather than guess.
Failed deploymentThe environment and deployment log. The mission fixes the cause in the repository and is explicitly told not to re-deploy: deploying is a governed action.
Failed missionThe original request, the recorded failure, and its branch and pull request, so it continues that work instead of starting over.
Review-held missionThe same, plus the instruction to make verification pass and clear every blocking finding on the existing branch.

A fix continues an existing branch only when that branch is safe to continue, so it updates the pull request already in flight instead of opening a rival one. Otherwise it gets a fresh orgabot/… branch and its own pull request, which is what happens in the two commonest cases:

  • CI failed on the default branch. A mission is never run directly on main, so a default-branch failure always gets a fresh branch.
  • The run came from a fork. GitHub reports a fork pull request's CI failure in your repository, and the fork's author chose that branch's name, so a same-named branch of yours is not the same branch. Without positive proof the run's head is your own repository, Orgabot does not continue onto it.

Two things the button will not do:

  • A throttle-queued launch has no Fix button (it is shown disabled with the reason). It is queued, not failed: it relaunches itself under its own mission id when bandwidth frees up, so a fix mission would duplicate it. A resolved incident is disabled for the same reason. A verification-unconfigured incident is disabled too, and carries Set verify command instead: nothing in the code is broken, and only you can say which command is that repository's suite.
  • A fix is an ordinary mission. It is owned by a role wherever an organization maps the project (and holds for you to pick one when it cannot be placed), and it still goes through verification, the independent review, and the merge gate. Nothing here merges or waives anything.

A project delivering with no verify command

A verify command is optional. A project without one still runs missions, and those missions still open pull requests: the verification gate records an honest skip - no command ran, so there is no exit code and nothing is presented as a pass - and the change is not blocked for it.

What used to happen instead was worse in both directions. A mission on such a project stopped at launch with the work not started, and a plan in the New Mission terminal could not be approved at all; meanwhile the pre-ship phase recorded that skip as verified, so an unverified delivery read exactly like a verified one on every surface. Both are gone: the mission runs, and the skip is recorded as a skip.

So that the absence is not silent, it is reported:

  • On the mission terminal, once, at launch: which policy asked for verification, that this project has no command, and where to set one.
  • In the New Mission terminal's plan review, as a line saying nothing on the affected step will be machine-checked, with Open project Verification beside that step. The plan still launches, and auto mode no longer stops for it.
  • As an incident, on Incidents and everywhere else incidents appear. It is keyed by project, not by mission - ten missions against one unconfigured repository are one thing to fix - it reads amber rather than red, because nothing failed, and it carries a Set verify command button that opens the same dialog as Repositories → Set command.

The incident auto-resolves the moment the project has a command; there is nothing to acknowledge afterwards. It is never raised for a project where you cleared the command and recorded a deliberate skip - that is an answered question, not an open gap.

You can set the command from Repositories (Set command, or Edit when one exists), from the incident card, or from the CLI with orgabot project set-verify <project> <command>. The dialog opens pre-filled with the command registration would have detected, tests it before saving, and records the result as the project's verification baseline.

Issue dependencies hold a fix before it starts

An issue that declares prerequisites does not launch a fix-and-ship mission while those prerequisites are still open. Ordering is enforced before a worker starts, because once two workers have each built the same subsystem the duplicate work already exists and one pull request ends unmergeable.

Two sources decide it, in this order:

  1. GitHub's native issue dependencies (the Blocked by links). Authoritative, because unlike body text they cannot be spoofed by whoever filed the issue.
  2. Prose in the body as a fallback, and only for an issue with no native links at all: a ## Dependencies (or Blocked by / Prerequisites / Requires) section listing #N, or an inline Depends on #N / Blocked by #N. A bare #N anywhere else is not a dependency.

Where the two disagree, the native links win. Dependency text selects an ordering and nothing else: it never reaches the instruction, the worker, or any authority decision, so it can only ever restrict a launch, never widen one.

A hold names its blockers and clears itself. Closing the last blocking issue launches the held mission with no operator action: the blocker's close arrives as a webhook and re-runs the sweep, so this needs live GitHub state (orgabot github connect, above) to be wired up. Without the tunnel, run npm run orgabot -- issues to re-evaluate by hand.

Four cases hold with a distinct reason rather than guessing:

ReasonWhat it means
dependencies_openA declared prerequisite is still open. Clears when it closes.
dependency_unreadableA prerequisite's state could not be read (rate limit, permission, deleted). Retried each sweep; an unreadable dependency is never read as "closed".
dependency_cross_repoThe declaration names another repository, whose state is not readable with this repo's credential. Link it natively instead, or override.
dependency_cycleThe issues block each other, so waiting can never clear it. Edit one declaration, or override.

To launch anyway, re-send the dashboard Fix with "overrideDependencies": true, or set ORGABOT_ISSUES_IGNORE_DEPENDENCIES=1 for the sweep. Either way the override is written to the audit trail as an IssueDependencyOverridden event, so overruling the gate is never invisible.

Two gotchas worth knowing

The dashboard does not hot-reload. It is a long-running process. After you pull a change that touches dashboard code, restart it: a git pull alone is not enough. (Conversely, because the CLI runs the TypeScript sources directly through tsx, a rebuild of dist/ is never what you need.)

Actions repaint before the network, not after. Every state-changing control applies its change locally, repaints, sends the request, and rolls back only on refusal. await fetch(...) followed by a poll with nothing painted in between is the shape that made the dashboard feel laggy.

The throughput objective

Insights -> Profiler opens with Throughput objective, above every chart on the page. It answers one question:

Are you completing more work per hour, with the verification and review evidence you configured intact, on a host you can still use?

That is deliberately not "is CPU below 80%" and not "is the machine swapping". Both of those are satisfied perfectly by running fewer missions, which is the opposite of what you want. High utilization is not a failure here.

The badge reads one of four ways:

  • meeting - the rate is what your host and configuration allow, and nothing visible is holding it back.
  • constrained - the rate is sound, but a named queue (implement, verify or CI) is where a large share of a typical mission is spent. Nothing is being traded away; that queue is what stands between this and a higher rate.
  • unsound - throughput was bought with something you did not agree to sell. A mission completed while its verification was OBSERVED to have failed for the commit it delivered, too large a share of deliveries failed after shipping, or peak load went past the point where the machine stays usable.
  • unknown - too little completed work in the window to judge a rate. This is not a problem report.

unsound outranks the rate, always. A host finishing eight missions an hour by skipping its gates is not outperforming one finishing two with them, so the verdict is decided on soundness first and the rate is reported beneath it as a fact rather than as the answer. If you see unsound for missing evidence, orgabot timeline --compliance <mission> lists the gates that were not satisfied.

Turns that do not deliver a diff are not held to a verification gate. A conversational or control turn completes with no verification evidence by design, and it is excluded from the count rather than reported as unverified. A mission whose deliverable shape was never recorded is excluded too: the objective never guesses which gates a mission owed.

An absence is never counted as a failure. A verification that was skipped, never started, or is still awaiting CI has not failed. Nor has a CI check that failed while naming no checks at all, which is unsatisfiable authority rather than a red result. Where the mission recorded the commit it delivered, evidence about an earlier commit is an absence too, which is the ordinary state of a mission that went through a repair round before shipping green.

And an absence is never counted as a pass. This is the same rule and it is the one easier to get wrong, because the mistake looks like good news. If some completions confirmed their gate and others could not answer, the badge says how many could not rather than reporting an all-clear over both. If nothing in the window recorded anything either way, it reports the rate and says plainly that the gates are not answered here. A green that covers unconfirmed work is worse than a red: red invites you to look, green closes the question.

The rate counts missions that finished inside the window, not every mission that overlapped it. An hour that three long-running missions merely passed through is not an hour in which three missions completed.

None of the recommendations will ever tell you to run fewer missions.

Checking why a poll feels slow

http://127.0.0.1:4317/api/perf

The dashboard times each stage of its own /api/state read and keeps a rolling window per stage. GET /api/perf returns p50/p95/max for each stage, the budget that stage is held to, and how effective the read caches are. It is read-only and localhost-only like the rest of the dashboard, and reading it computes nothing.

Use it when a poll feels slow: the stages list is sorted slowest-first, so the first row names what is actually costing the time, and breaches lists the stages whose p95 is over budget. Budgets are reporting thresholds, not gates. Nothing is refused or dropped for being slow, because a dashboard that silently skipped work to stay fast would be wrong instead of slow.

Retained mission history no longer makes a poll progressively slower: parsed mission projections and stored GitHub deliveries are reused until the underlying files actually change, so a poll costs what your active work costs, not what your archive costs. A poll that arrives with no new GitHub delivery does not even look at the delivery files, and the organization store is read once per request instead of once per place that needs it.

The routine poll also carries only the view you are looking at. Missions, the sidebar, the overview, incidents, approvals and pull requests are on every response, because the taskbar, the inbox and the notification bell need them everywhere; the organization, catalog, repositories, CI/CD and activity views travel only when you are on a view that paints them.

Nothing disappears when it is left out. The page keeps the last copy it fetched, so the organization switcher and the mission composer still work wherever you are, and the Profiler names anything it is showing from that copy rather than from the current poll. Any action you take, and switching to a view that is painting carried-over data, fetches the full picture again immediately.

Each mission on that response carries a shortened instruction, not the whole one. Measured on a real install, the instruction was about 80% of a mission record, and every three-second poll was serializing all of it for every mission in your history so that lists could render a one-line headline from it. The lists are unchanged: they only ever showed the first line, clamped.

Opening a mission still shows the complete instruction. The drill-down paints immediately from what the poll already gave it and then reads that one mission's full text from GET /api/missions/:id, which is one read when you open a mission rather than a cost on every poll for missions you are not looking at. If that read fails you keep the shortened text on screen; you never get an empty instruction.

The Profiler's Dashboard latency section shows what this saves, as "Instruction text elided" beside the response size it reduces, and GET /api/perf reports the same number under missionInstructionElidedBytes.

The operational store

~/.orgabot/operational.db is a local SQLite database holding an operational-row store and a durable change outbox. It is the substrate for upcoming work that pushes changes to the browser instead of having the browser re-ask for everything every three seconds.

Your mission lifecycle journal is still the authority for mission state. This database holds a projection plus the change outbox the live taskbar is delivered from, so nothing you see in the dashboard is decided by it.

orgabot opstore            # create or migrate it, and report schema, size, and change head
orgabot opstore verify     # integrity-check it and prove its change feed is readable
orgabot opstore backup ~/orgabot-backup.db

Take the backup with orgabot opstore backup rather than copying the file: it uses SQLite's own consistent-snapshot path, so it is safe while Orgabot is running, whereas a plain cp of the .db can miss the write-ahead-log sidecars. If the file is ever damaged, deleting it is safe and orgabot opstore recreates it.

The live taskbar

The mission taskbar no longer waits for the next poll. The dashboard opens one Server-Sent Events connection to /api/stream, and mission, approval, and input transitions are pushed to it as they commit, so a mission that finishes leaves the taskbar in a fraction of a second rather than on the next three-second tick.

Things worth knowing:

  • The poll is still there, slower. With the stream connected it drops to a 20-second consistency check. It is the repair pass, not the delivery mechanism, and it is what corrects the screen if a notification is ever lost.
  • It falls back silently. No operational store, a browser without EventSource, or repeated connection errors, and the dashboard returns to its original three-second poll. Nothing is lost, it is only slower.
  • Background updates preserve your place. Missions, Issues, organization lists, approvals and the role dock keep unchanged rows and focused controls in place. Insertions and deletions preserve the visible row. If an updated sort order would interrupt reading, an update button lets you choose when to apply it. Opening another page still starts at the top.
  • Reconnecting resumes where you were. Each tab remembers its position and replays what it missed. If it was away long enough that the change history no longer covers it, the server says so and the tab reloads a fresh snapshot rather than quietly skipping what happened in between.
  • It only ever reads. The stream carries ids and statuses out to the browser. It carries nothing in: your actions are still ordinary requests through the same authority checks the CLI uses, and no mission instruction, terminal output, question, or answer travels on it.
  • Dismissing a failure is pushed too. A failed mission stays pinned to the taskbar until you acknowledge it. That dismissal is now streamed, so a second window drops the red tab immediately instead of keeping it until its own consistency check comes round.

GET /api/perf reports it under stream. These numbers answer different questions, so do not add them up:

  • deliveryMs is fresh commit-to-dispatch only: how long a change committed after a live subscriber attached waited to reach the browser. That is the number this transport is responsible for.
  • replayAgeMs is the age of retained history while catching a reconnecting client up. Useful for backlog size; it is not live delivery latency. A multi-thousand-second figure here after a long absence is expected catch-up, not a delivery regression.
  • finalization counts who decided a mission was over: worker means the mission finished and said so, reconciler means its process disappeared without writing anything and a background sweep noticed. A taskbar entry that lingered for minutes is almost always the second kind, which is a mission problem, not a dashboard one. Each mission is counted once here however many windows you have open.

The same /api/perf report also labels request age (snapshot generatedAt to paint), commit-to-visible (a mutation's committedAt to paint, which needs the snapshot mutation correlation or a streamed change), provider freshness (Issues fetchedAt to paint), and transport latency (fresh stream commit to browser receive). Those are separate segments. Live process samples are labeled live_process_sample and are not a controlled baseline; issue #1812 timings remain historical reference only. For a controlled cold/warm local SQLite snapshot baseline plus a synthetic scroll geometry model, run npm run bench:dashboard-baseline from framework/. The model is not a browser reproduction. PostgreSQL mode is not implemented and fails explicitly; this command alone does not satisfy the local/cloud acceptance criteria.

The browser reports the rest of the journey under browser, and those are separate segments rather than one total:

  • taskbarTransition is how long the committed state took to become visible. For a pushed change it is measured from the commit the change carried, not from the age of the last snapshot, so the 20-second consistency poll cannot make a fast delivery look slow.
  • streamVisible is the last leg only: the change arriving in the page to the repaint that showed it. It is the part the browser itself is responsible for.

To turn the whole thing off, set ORGABOT_DISABLE_CHANGE_STREAM=1 before starting the dashboard. The dashboard then behaves exactly as it did before.

Updating one row instead of the page

Learning about a change quickly is only half of it. The page used to redraw a whole area to show one changed thing, so a single mission going from running to failed rebuilt the entire mission bar.

It now keeps what it knows keyed by the id of the thing itself, and updates only what actually changed:

  • One mission changing updates one tab, plus the counts beside it. Every other tab keeps its place, and nothing else on screen is redrawn.
  • A view you are not looking at does no work. An issue being updated while you are on Repositories costs that page nothing at all.
  • What you are doing outranks the update. A row you are typing in, or have text selected in, waits until you are done, while every other row still updates. Before this, one focused field paused the whole area, so a mission could finish and the bar would keep showing it as live until you clicked away.
  • Repeated or out-of-order updates cannot move you backwards. Every change carries its position, and anything already applied is ignored, so a reconnect that replays the last few seconds changes nothing on screen.
  • Actions no longer wait for a full refresh. Acknowledging a failed mission is answered with just that mission's new state, and the page applies it. If a reply ever arrives without one, the page falls back to the refresh it did before, so nothing is left half-updated.

If the connection is ever interrupted long enough that the change history no longer covers you, the page still rebuilds from one full read and picks the updates back up from there, exactly as before.

The live terminal

An open mission terminal is streamed the same way, on its own connection to /api/missions/<id>/session/stream. Lifecycle rows and newly written worker output arrive as they are committed, so the terminal reads like a terminal rather than refreshing every 2.5 seconds.

  • Your place in the session is a real position, not a guess. The terminal resumes from the same cursor the polled read always used: a journal sequence and a byte offset in the mission's log, both of which exist on disk. So a dropped connection, a browser refresh, and a restarted dashboard all resume exactly where you were, with no row repeated and none skipped.
  • Polling is the fallback, and only the fallback. A browser without EventSource, a session the server will not stream, or repeated errors, and the terminal returns to its 2.5-second poll. The two never run at once: the poll stops the moment the stream connects.
  • Status and controls move with the events. A mission that starts waiting on an approval, is interrupted, or finishes updates the terminal's banner and buttons as the transition arrives, not on the next tick. The one exception is deliberate: while you are typing in the terminal, the repaint waits, so a transition can never take the caret out from under you.
  • Output is redacted before it is sent, exactly as before. The stream reuses the server's existing formatting-then-redaction path, which is why a credential split by a colour escape is still caught. Raw output never travels on the dashboard-wide change stream either.
  • A big backlog arrives in chunks. Opening a terminal on a mission with a very long log fills in progressively instead of freezing the page, and a connection that stops keeping up is dropped and resumes from its cursor.

GET /api/perf reports it under stream.sessions, kept separate from the change stream's own counters: a terminal frame is not a state change, and adding the two together would describe neither.

Issues after a restart

Issues keeps what GitHub last told it, on disk. Opening the tab on a freshly started dashboard paints those rows straight away and refreshes them in the background, instead of showing you nothing while gh is asked.

That also means a GitHub outage or a rate limit no longer blanks the screen: you keep the rows from the last successful read, and the tab tells you it is refreshing. What it never does is invent an answer. A repository it has not been able to read is not reported as having no open issues.

Notifications (optional)

With terminal-notifier installed, background missions post clickable notifications when they finish or need you. orgabot doctor reports whether it is present.

Is GitHub ingress working?

Connectors -> Connected reports it as two facts that fail independently, because merging them is what leaves you reading green with nothing arriving:

  • Public endpoint - whether a tunnel or edge is up. A claim about a process.
  • Signed delivery - whether one signed delivery has actually been verified at Orgabot's own receiver, and when. Evidence, never a tick. It reads Deliveries refused since rather than green when the newest refusal is more recent than the newest acceptance, which is what an operator who rotated the webhook secret without re-wiring the App is actually looking at.

Refusals are listed with their reason. A wrong webhook secret shows as signature invalid - the endpoint is reachable and verifies nothing, which is a different problem from one nobody is sending to, with a different repair. If no webhook is configured at all, it says so: Orgabot reconciles GitHub state by polling instead, which is slower and bounded by the API rate limit, and that is a supported mode rather than a fault.

The repair button starts or re-wires the tunnel and the App. It cannot make GitHub send a delivery, so it finishes by saying the endpoint is up and nothing has verified yet, and keeps refreshing until one does.

The same answer is available from the terminal with orgabot github ingress.