Observatory

A read-only local dashboard over guarded run artifacts: runs, routing, phases, cost and stale-run detection.

Overview

The observatory is a read-only local dashboard over cco run artifacts. It reads <home>/runs/<run_id>/ directories and never writes to them. Everything it shows comes from files the runner already records: task_outcome.json, execution_envelope.json, phase_events.jsonl and rejected.patch.

What it shows

  • Runs - one row per run directory, newest first. Columns include the REPO (the basename of the repository the run targeted), the executor:model identity, state, diagnostic code, cost, start time, duration and a task excerpt. Sort by time, duration or cost, and search by text.
  • Executor:model identity - each run shows the executor and model that produced it. Vendor tiers appear under their own label, for example minimax:MiniMax-M3 for the MiniMax command executor, next to claude:sonnet or claude:opus.
  • Routing ladder - for runs routed through --task-class, the detail panel lists every tier attempt from the envelope's routing.attempts. Earlier attempts are costed once in the aggregates.
  • Phase timeline - one segment per phase from phase_events.jsonl. A segment lasts until the next event; the last ends at the task outcome, or at "now" while the run is still going.
  • Aggregates and sparklines - tiles for runs, kept rate, rollback rate, in-progress count, total cost and escalations. Sparklines show cost per run and the rolling kept rate. The API also returns cost per executor:model tier (cost_by_tier).
  • Verify output and rejected patch - for finished runs, the tail of the verify output and the start of rejected.patch when one exists.

Run states, aborted and stale runs

A run with a task_outcome.json shows its recorded state: ready, degraded or blocked. If the outcome file is unreadable the run shows as blocked with the code unreadable_outcome.

A run with no outcome file is classified from its phase events:

StateMeaning
in_progressNo outcome yet and the last event is recent. Shows the current phase and a running duration.
abortedNo outcome, but the last phase is already terminal (decision or rollback). The runner was killed after deciding, so the run can never finish.
staleNo outcome and no phase event for more than 30 minutes. Treated as abandoned instead of blinking as in progress forever.

Kept and rollback rates are computed over finished runs only, so in-progress and stale runs do not distort them. The page polls the API every 3 seconds; if refreshes fail for more than 10 seconds the indicator turns stale and a "connection lost - retrying" banner appears.

Starting it

bash
cco observatory
# then open http://127.0.0.1:4173

# one dashboard over several project homes
cco observatory \
  --home ~/.codified-orchestrator \
  --homes ci=/srv/cco-ci-home,sandbox=~/tmp/cco-sandbox-home

Stop the server with Ctrl-C. If the port is already in use, cco observatory prints the bind error and exits with code 1.

FlagDefaultMeaning
--home PATH$CODIFIED_ORCHESTRATOR_HOME, else ~/.codified-orchestratorOrchestrator home that holds runs/. Registered as the default home.
--homes SPECemptyExtra homes as name=path,name2=path2, for multi-project dashboards. Entries whose directory does not exist are skipped silently.
--port N4173TCP port to listen on.
--host ADDR127.0.0.1Interface to bind. Any other value prints a warning to stderr; see the security model below.
--legacyoffServe the old static dashboard module instead of the runner view.

The page has a home switcher filled from /api/homes, so each project's runs stay separate.

JSON API

The API accepts only GET; other methods get 501. Unknown paths, unknown homes, malformed run ids and missing runs all return 404 {"error": "not found"}.

EndpointReturns
GET /api/homesRegistered home names and the active default. Paths are never exposed.
GET /api/runs[?home=NAME]All runs of a home plus aggregates (runs, kept_rate, rolled_back_rate, in_progress, total_cost_usd, cost_by_tier, escalation_count, and up to 100 series points).
GET /api/runs/<run_id>[?home=NAME]One run's row plus verify_output_tail, rejected_patch_excerpt, routing_attempts and the phase timeline.
bash
curl -s http://127.0.0.1:4173/api/homes
# {"homes": ["default", "ci"], "active_default": "default"}

curl -s 'http://127.0.0.1:4173/api/runs?home=ci'
curl -s 'http://127.0.0.1:4173/api/runs/20261029T100000Z-ready-aaaa1111?home=ci'

A run_id must match ^[A-Za-z0-9_][A-Za-z0-9._-]{0,127}$ and resolve directly under <home>/runs/. Traversal attempts, encoded separators and symlinks pointing outside runs/ return 404.

Security model

  • Read-only. The runner view only answers GET. It never writes, deletes or executes anything, and each artifact read is capped at 1 MiB.
  • Localhost by default. The server binds to 127.0.0.1. Nothing is reachable from other machines unless you pass a different --host, and then a warning is printed to stderr.
  • Security headers. Every response carries X-Content-Type-Options: nosniff; API responses also carry Cache-Control: no-store. Handler failures return a generic 500; the traceback goes to the server's stderr only.
  • Homes are names, not paths. Clients choose among homes the operator registered; a request cannot point the server at an arbitrary directory.
  • Artifacts are sensitive. Outcomes, verify output and rejected.patch can contain prompts, source code and command output. There is no authentication and no TLS, so binding to 0.0.0.0 exposes all of it. For remote access prefer an SSH tunnel: ssh -L 4173:127.0.0.1:4173 host.
  • --legacy is broader. Its /browse route previews files under the checkout and your home directory. Use it only on localhost.