Citadel Mission Control Specification (schema 1 views, typed local actions)

July 14, 2026 · View on GitHub

citadel dashboard (also npm run dashboard:web) serves a local web app that renders the project's .planning/ state and telemetry live. It is the visible form of the harness: campaigns, operations, runtime forks, fleet, loops, hooks, costs, and handoffs in one glanceable surface.

This document specifies the schema-1 projection, the narrow Operations Protocol intent surface, and revision-bound Operation Fork selection. Roadmap context lives in ROADMAP.md R1 and R3.

Principles

  1. Files are canonical. The dashboard is a view over .planning/ markdown and JSON plus telemetry JSONL. Deleting the dashboard loses nothing. Accepted operation controls are immutable intent files. Fork selection uses the same canonical fork manifest as the CLI.
  2. One contract for terminal and browser. Browser actions write Operations Protocol intent files into .planning/intents/pending/ that authorized executors may consume. The dashboard never gets a private API into the runtime; if the terminal cannot do it through files, neither can the browser.
  3. Honesty over polish. Unknown state renders as unknown, never green. Every cost figure derived from token math carries an "est." label. Every claim links to its evidence: the diff, the file, the telemetry line.
  4. Local only. Binds to 127.0.0.1. Mutation requests require the exact loopback origin, a per-process nonce, JSON content type, and a bounded body. Remote access is out of scope and gated on the threat model (ROADMAP R4).

Architecture

.planning/** + telemetry JSONL + OTLP receiver
        │ (fs.watch, debounced; reuse scripts/local-watch.js pattern)

scripts/dashboard-server.js     Node, stdlib only, no new runtime deps
  ├── normalizers (core/…)      parse-campaign, loops, fleet, telemetry readers
  ├── GET /api/*                normalized JSON snapshots
  ├── POST /api/intents         validated immutable pause/resume/stop/retry intents
  ├── POST /api/fork-selections revision-bound selection only, never landing
  ├── GET /api/events           SSE: push invalidation keys on file change
  └── static /                  single-bundle SPA (no CDN, works offline)
  • Server start must not scan the world: index .planning/ lazily, cache parsed records keyed by mtime, and re-parse only changed files on watch events.
  • The SPA receives SSE invalidation keys (campaigns, loops, cost, ...) and refetches only the affected /api/* snapshot. No polling loops.
  • Reuse existing parsers (core/campaigns/parse-campaign, evidence contracts, loops registry readers). The dashboard adds normalizers, not new interpretations of state.
  • Port: default 4180, --port to override, fail with a clear message if taken.

Data contracts (v0.1 endpoints)

All endpoints return { schema: 1, generated_at, source_files: [...], data }. Shapes are versioned with a top-level schema: 1 so v0.2 can evolve without breaking saved clients.

Every panel payload includes state: { path, status, detail, count, unreadable }. Status is healthy, empty, mid-run, unknown, or unreadable. Missing input is unknown, malformed input is unreadable, and neither may be presented as a green zero.

EndpointSourcedata shape (summary)
/api/overviewall below{ needs_you: Item[], active: {campaigns, fleet_agents, loops}, cost_today, last_verify }
/api/campaigns.planning/campaigns/*.md[{ id, title, status, phase: {n, of, title}, progress, started_at, last_handoff, evidence }]
/api/fleet.planning/fleet/[{ session, agents: [{ name, scope, worktree, status, last_discovery }], wave, merge_queue }]
/api/forks.planning/operation-forks/*/fork.jsonfork revision, branch proof summaries, deterministic comparison, selection, and landing state
/api/loops.planning/loops/*.json, daemon.json[{ id, type, trigger, budget: {kind, total, spent}, verifier, last_run: {at, status}, stop_state }]
/api/hooks/feedtelemetry JSONLlast N hook events: { at, event, decision, reason, target } (blocks first)
/api/handoffs.planning/handoffs/, campaign recordstimeline of { at, campaign, summary, path }
/api/costOTLP receiver + transcript fallbacksee Cost modes
/api/activationactivation report or local activation JSONLredacted totals by stage, status, failure, and acquisition source
/api/controlprocess memorysame-origin process nonce and allowed intent actions

Action contract

POST /api/intents accepts exactly:

operation_id, expected_revision, idempotency_key, actor, reason, capability, action

action and capability are limited to pause, resume, stop, or retry. The endpoint never accepts a command, script, path, prompt, or arbitrary metadata. It passes the request to core/operations/intents.js, which checks the current revision, capability grant, lifecycle transition, project containment, and idempotency decision before writing anything.

Accepted calls atomically create one immutable pending intent. They do not edit an operation, campaign, or runtime file. Duplicate calls with the same key and request return the same result. Stale revisions return conflict; missing capabilities return blocked; unavailable or unsafe state returns unknown.

POST /api/fork-selections accepts exactly:

fork_id, branch_id, expected_revision, idempotency_key, actor, reason

It rejects unknown fields, stale revisions, and branches without complete verified evidence. An accepted request updates the canonical fork manifest with one typed selection. It returns landing_effect: none. The browser cannot prepare or apply a merge. The operator must run citadel fork land plan ID, inspect the fresh target state, and supply its exact confirmation token to the CLI.

Mission Control projects the canonical pending queue back onto operation cards. While an intent is pending, the duplicate controls remain unavailable and the UI states that no lifecycle change has been assumed.

The browser first reads /api/control, which returns a random nonce held only for the lifetime of the server process. A mutation must send that nonce in X-Citadel-Nonce, use the exact http://127.0.0.1:<port> origin, declare JSON content, and stay within the 16 KiB body limit.

needs_you is the product. It aggregates anything waiting on a human: campaign phase gates, fleet merge reviews, loops in needs-human-review or blocked, stale approvals. Sorted by age. The dashboard's home answers "do I need to do anything?" in one glance.

Cost modes

Two user populations, two units. Mode is detected per session and shown explicitly.

API-key mode (unit: estimated USD).

  • Source of truth in schema 1 is local session cost telemetry. An OTLP receiver is a future extension and is not exposed by the read-only server.
  • Current projections aggregate session-costs.jsonl and label campaign totals as estimates.
  • Every dollar figure renders with "est." and a tooltip: estimates are computed locally from token counts and can differ from the bill; the Console is authoritative.

Subscription mode (unit: plan window).

  • Pro/Max users do not pay per token; the current UI calls estimated dollars plan-load indicators rather than charges. Plan-window percentages remain future work.

Fallbacks. When no local cost telemetry is available, render unknown with a one-line instruction to enable it, never zero. Provider-specific token and OTLP adapters remain future work.

Panels (v0.1)

PanelContentEvidence links
Needs You (home)aggregated interrupts, age-sorted, count in tab titleeach item links to its file/diff
Campaignscards: phase progress, status, last handoff, evidence freshnesscampaign md, handoff md
Operationsstate, revision, next executor effect, capability-aware controls, result feedbackoperation control record, immutable pending intent
Fleetagents, scopes, worktrees, wave status, merge queue previewworktree paths, discovery log
Operation Forksside-by-side runtime evidence, honest comparison, selection, and landing boundaryreceipts, public replay, canonical fork state
Loopscontract cards: budget burn-down, verifier history, stop-state badgeloop JSON, review artifact
Costtracked or estimated session and campaign spend, explicitly labeledtelemetry lines
Hook feedrecent decisions, blocks first, friendly reason textrule + target file
Activationlocal redacted funnel plus opt-in shared cohort status, denominators, and decision gatesactivation and cohort report JSON

Multi-project switching, fortress view, and direct runtime mutation remain future work.

Quality bars

Performance budgets (enforced by a perf check in CI against a generated fixture project with 1,000 planning files):

  • Cold start to first render: < 1 s. Complete server RSS: < 64 MB, with < 10 MB attributable overhead above the process baseline. File-change to UI update: < 500 ms. Interaction latency: < 100 ms.
  • No layout-forcing reads in render loops; SSE invalidation, never polling.

Design language. Dark-first with a real light mode. The four tier colors are semantic everywhere (cyan skill, blue marshal, orange archon, purple fleet); color is never decoration. Mono for data, sans for prose, tabular numerals for every figure. Motion only narrates state change: 150-250 ms transitions, transforms and opacity only, prefers-reduced-motion respected, 60 fps or the animation ships disabled.

Copy voice. Calm operator. "Archon finished phase 3. Two files need review." Numbers over adjectives. Every empty state teaches (shows the command that would populate it); every error names the next action.

Keyboard. ? overlay, j/k through the needs-you queue, Enter to open evidence, and Escape to cancel a stop or retry confirmation and restore focus. Native buttons remain tab accessible.

Verification

  • Unit: normalizers against fixture .planning/ trees (healthy, mid-campaign, corrupted, empty). Corrupted files render as "unreadable: " rows and never crash the panel.
  • Contract: every documented projection endpoint returns a schema-1 envelope and explicit source state.
  • Perf: budget script in CI per the table above.
  • Visual: screenshot pass on the fixture project (the make-frame technique from the README work) checked at desktop and 380 px widths, dark and light.
  • Manual exit check (matches ROADMAP R1): a person who has never seen Citadel opens the dashboard on a live project and explains what is happening within 60 seconds.

Current evidence (2026-07-10)

  • node scripts/test-dashboard-web.js: healthy, initialized-empty, absent, mid-run, and corrupt fixtures pass; all ten API projections return schema 1 with explicit source state.
  • node scripts/test-dashboard-perf.js: deterministic 1,000-file Windows focused runs after update-path caching measured 251.9-717.2 ms cold and 110.8-458.4 ms invalidated updates in isolated runs. A deliberately concurrent local stress sample reached 1,276.0 ms, so the CI budget remains an isolated-process contract rather than a host-saturation claim. A bare Node 22 process measured 47.6 MB; complete dashboard runs measured 55.1-55.5 MB with 3.9-4.4 MB attributable fixture overhead. CI now fails above either 64 MB absolute RSS or 10 MB overhead, keeping the product footprint bounded without mistaking runtime variance for growth.
  • node scripts/test-dashboard-visual.js: dark/light desktop and 380 px design-token/layout baselines plus keyboard and reduced-motion contracts pass. This is browserless structural evidence, not a pixel screenshot baseline; pixel captures remain pending a browser runtime.
  • node scripts/test-dashboard-interactions.js: browserless state, risk confirmation, keyboard, feedback, nonce submission, responsive, and reduced-motion semantics.
  • node scripts/test-dashboard-web.js: loopback origin, nonce, JSON content, body bound, idempotency, revision, capability, arbitrary-field, and symlink containment checks.
  • Human comprehension remains external: the 8/10 first-time-user result and the under-60-second explanation check are not yet proven.

Out of scope for v0.1

Direct runtime mutation, arbitrary commands, remote access, hosted operation, fortress view, multi-project aggregation, and public embeds. Mission Control can request an action through an immutable intent, but only a separately authorized executor can perform it.