Citadel dashboard
git clone https://github.com/SethGammon/Citadel
T=$(mktemp -d) && git clone --depth=1 https://github.com/SethGammon/Citadel "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/dashboard" ~/.claude/skills/sethgammon-citadel-dashboard && rm -rf "$T"
skills/dashboard/SKILL.md/dashboard — Harness Observability Dashboard
Identity
/dashboard reads the live state of the harness and presents it in a single, readable snapshot. No wall of JSON. No scrolling through log files. One command, one screen, full picture.
When to Use
- "What's happening?" / "Status?" / "What's going on?"
- "Show activity" / "Show me the dashboard"
- After returning to a project after time away
- When /do routes "status", "dashboard", "what's happening", "what's going on", "show activity"
- Directly:
/dashboard
Inputs
None required. Works with whatever state exists on disk.
Protocol
Step 1: COLLECT STATE
Read the following sources. Each is optional — if a file or directory doesn't exist, treat it as empty. Never crash on missing state.
Campaigns:
- Glob
.planning/campaigns/*.md - For each file, read the first 40 lines to extract:
fieldStatus:
field (truncate to 60 chars)Direction:- Phase progress (search for
orPhase N of M
headings)## Phase - Most recent line starting with
from the Decision Log- [
Cost Data (two sources, prefer real):
Source 1 -- Real token data (primary):
- Run
andnode scripts/session-tokens.js --todaynode scripts/session-tokens.js --all - If the script exists and produces output, use its numbers (they read Claude Code's native session JSONL files for exact token counts and compute cost from API pricing).
Source 2 -- Session costs JSONL (fallback, also provides campaign attribution):
- Read
(if it exists).planning/telemetry/session-costs.jsonl - For each line, parse as JSON
- Cost priority:
>real_cost
>override_costestimated_cost - Group by
. For each group:campaign_slug- Count sessions
- Sum best available cost
- Sum agents spawned and duration minutes
- Compute grand total across all campaigns
Live session:
- Read
for current session burn rate.planning/telemetry/cost-tracker-state.json
Data source indicator:
- If
fields are present in session-costs.jsonl entries, note "(real)"real_cost - If only
, note "(est)" so users know accuracy levelestimated_cost
Fleet Sessions:
- Glob
.planning/fleet/session-*.md - For each file, read the first 30 lines to extract:
fieldstatus:
or wave numberwave:
or agent countagents:
Recent Telemetry:
- Read last 50 lines of
(if it exists).planning/telemetry/hook-timing.jsonl - Read last 50 lines of
(if it exists).planning/telemetry/audit.jsonl - Merge and sort by timestamp (descending). Take the 10 most recent entries.
- For each entry: extract
(orts
),timestamp
(orhook
), and a short description field. Format as relative time.event
Recent Hook Activity (separate from general telemetry):
- Read last 20 lines of
.planning/telemetry/hook-timing.jsonl - For each entry with
, extract:event: "timing"
— which hook fired (e.g.,hook
,post-edit
)circuit-breaker
— execution time in millisecondsduration_ms
— convert to relative timetimestamp
— derive from context: ifoutcome
is present and no matching error entry induration_ms
, outcome ishook-errors.jsonl
; if a block entry exists for the same hook within 1 second, outcome ispassblock
- For entries with
(fromevent: "counter"
), extract metric name as the "event" column with count contextincrement() - This section makes silently-firing hooks visible without digging through raw files
Pending Queues:
- Count lines in
(or 0 if missing).planning/telemetry/doc-sync-queue.jsonl - Count lines in
(or 0 if missing).planning/telemetry/merge-check-queue.jsonl - Count files in
(or 0 if missing).planning/intake/
Hook Value Data (for HOOKS VALUE section):
- Read
(if it exists, last 200 lines).planning/telemetry/hook-errors.jsonl- Count entries where
= "protect-files" (blocked file access)hook - Count entries where
= "external-action-gate" (gated external actions)hook - Count entries where
= "quality-gate" (quality violations)hook
- Count entries where
- Read
(if it exists, last 200 lines).planning/telemetry/hook-timing.jsonl- Count entries where
= "circuit-breaker" andhook
= "trips"metric - Count total entries from today (entries containing today's ISO date prefix)
- Count entries where
- Read
(if it exists, last 200 lines).planning/telemetry/audit.jsonl- Count entries mentioning "circuit-breaker" or "circuit_breaker"
Health:
- Count circuit breaker entries from audit.jsonl (from hook value data above)
- Count total lines in
written today.planning/telemetry/audit.jsonl - Count entries in
array ofhooks
(or.claude/hooks-template.json
if template not present); use 0 if neither exists.claude/hooks.json - Read
→.claude/harness.json
object:trust
,sessions_completed
counterscampaigns_completed- Compute level: novice (sessions < 5), familiar (5-19), trusted (20+ with 2+ campaigns)
- If
is set, use that and note "(override)"trust.override
Step 2: FORMAT RELATIVE TIMESTAMPS
Convert ISO timestamps to human-readable relative time:
- < 60 seconds ago: "just now"
- < 60 minutes ago: "{N} min ago"
- < 24 hours ago: "{N} hr ago"
-
= 24 hours ago: "{N} days ago"
If a timestamp is unparseable, display it as-is without crashing.
Step 3: RENDER DASHBOARD
Output the following format verbatim, substituting real values. Omit sections that are entirely empty only if explicitly noted below. Always show the section header even when the content is "(none active)".
=== Citadel Dashboard === As of: {relative timestamp of most recent event, or "now"} CAMPAIGNS {slug}: Phase {N}/{total} — {direction, max 60 chars, ellipsis if truncated} Last event: {most recent telemetry entry for this campaign, or "no telemetry"} (none active) COSTS This session: ${cost} | {duration} min | ${rate}/min | {messages} msgs | {agents} agents Today: ${today_total} across {today_sessions} sessions All time: ${all_time_total} across {all_time_sessions} sessions ({data_source}) By campaign: {slug}: ${total_cost} across {sessions} sessions ({agents} agents, {minutes} min) _unattached: ${total_cost} across {sessions} sessions (no cost data recorded yet) HOOKS VALUE Circuit breaker: {N} trips (prevented token spirals) Quality gate: {N} violations caught pre-commit Protect-files: {N} blocks (path traversal, secrets) External gate: {N} actions gated Total hook fires today: {N} (raw facts only -- no inflated savings claims) FLEET SESSIONS {slug}: Wave {N} — {agent count} agents — {status} (none active) RECENT ACTIVITY (last 10 events) {relative time} | {hook/event name} | {description} (no telemetry recorded yet) HOOK ACTIVITY (last 10 hook fires) {relative time} | {hook name} | {duration_ms}ms | {outcome: pass/block/warn} (no hook timing recorded yet — set CITADEL_DEBUG=true in settings.json for verbose output) PENDING Doc sync: {N} items queued Merge reviews: {N} items queued Intake items: {N} in .planning/intake/ HEALTH Circuit breaker trips this session: {N} Audit entries today: {N} Hooks installed: {N} Trust level: {novice | familiar | trusted} ({N} sessions, {N} campaigns) QUICK COMMANDS /do continue — resume active campaign /do rollback — restore last checkpoint /telemetry — cost breakdown, hook activity, telemetry settings /triage prs — review open PRs /pr-watch — watch PR CI /learn — extract patterns from last completed campaign
Step 4: FRINGE CASE HANDLING
If .planning/ does not exist: Show the dashboard with all counts as 0 and all lists as "(none active)" or "(no telemetry recorded yet)". Add a note below the dashboard:
NOTE: .planning/ not found. Run /do setup to initialize harness state.
If harness.json is missing or malformed: Show "not configured" for hooks count. Do not crash.
If a campaign file is malformed markdown: Skip that file. Log
(1 campaign file skipped — malformed) in the CAMPAIGNS
section if any were skipped.
If telemetry files are very large: Only read the last 50 lines of each telemetry file. This caps read cost regardless of file size. Note: "Showing last 50 events per log file."
If timestamps are missing from telemetry entries: Use the file's modification time as a fallback. If that's also unavailable, display the entry without a timestamp.
Quality Gates
- Dashboard must render even when all state files are missing
- Never display raw JSON to the user — always parse and format
- Relative timestamps required — never show raw ISO strings in output
- Campaign direction truncated to 60 chars with "..." if longer
- Total output must be skimmable in under 30 seconds
Exit Protocol
/dashboard does not produce a HANDOFF block. It is a read-only observability tool. After displaying the dashboard, wait for the next user command.