Skip to content

Latest commit

 

History

History
205 lines (164 loc) · 12.5 KB

File metadata and controls

205 lines (164 loc) · 12.5 KB

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

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.

Endpoint Source data shape (summary)
/api/overview all 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.json fork 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/feed telemetry JSONL last N hook events: { at, event, decision, reason, target } (blocks first)
/api/handoffs .planning/handoffs/, campaign records timeline of { at, campaign, summary, path }
/api/cost OTLP receiver + transcript fallback see Cost modes
/api/activation activation report or local activation JSONL redacted totals by stage, status, failure, and acquisition source
/api/control process memory same-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)

Panel Content Evidence links
Needs You (home) aggregated interrupts, age-sorted, count in tab title each item links to its file/diff
Campaigns cards: phase progress, status, last handoff, evidence freshness campaign md, handoff md
Operations state, revision, next executor effect, capability-aware controls, result feedback operation control record, immutable pending intent
Fleet agents, scopes, worktrees, wave status, merge queue preview worktree paths, discovery log
Operation Forks side-by-side runtime evidence, honest comparison, selection, and landing boundary receipts, public replay, canonical fork state
Loops contract cards: budget burn-down, verifier history, stop-state badge loop JSON, review artifact
Cost tracked or estimated session and campaign spend, explicitly labeled telemetry lines
Hook feed recent decisions, blocks first, friendly reason text rule + target file
Activation local redacted funnel plus opt-in shared cohort status, denominators, and decision gates activation 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.