Local-first observability for AI agent ecosystems. Built for Kiro CLI agents, adaptable to anything that can record a run - cron jobs, launchd services, Claude Code hooks.
Watch your agents live: what ran, what failed and why, what's queued, when everything happens - plus a multi-terminal console to chat with several agents at once and pipe them into each other.
Running AI agents locally sounds simple - until you need scheduling, concurrency control, crash recovery and observability. Without infrastructure, agents are fragile scripts that silently fail, leak processes and produce inconsistent results.
This dashboard is the observability half of that missing layer: not a wrapper around an LLM, but DevOps for AI agents - the gap between "I have a prompt" and "I have reliable automation".
"I went from 'run a script and hope' to a self-managing fleet of agents with full observability." - the itch this project scratches
- 🏠 Local-first - your data never leaves your machine. No cloud dependency, no account, no telemetry.
- ⚡ Real-time - a WebSocket event channel updates every open page within ~1 second.
- 🔍 Failure diagnosis - one click explains a failed run: exit-code meaning, the run's own log segment, and known-pattern detection (expired auth, missing deps, timeouts, rate limits...). Every run logged, every failure categorized.
- 🖥️ Multi-terminal console - up to 6 simultaneous PTY sessions that survive page refreshes, with a broadcast bar to ask all agents the same thing.
- 🔗 Pipe mode - multi-agent orchestration made visible: chain agents so the output of one becomes the prompt of the next, with an animated flow view, live streaming and visible hand-offs.
- 📅 A real scheduler view - if your agents run on plain cron (launchd, crontab), the Supervisor gives them what cron never had: plain-English schedules, next-run countdowns, failure pinning, one-click runs, inline cron editing and enable/disable.
- 📦 Battle-tested at scale - born from an ecosystem of 20+ autonomous agents running on a single laptop with zero manual maintenance: the dashboard is how that fleet stays observable.
- macOS - fully supported.
- Linux - fully supported (validated end-to-end on Ubuntu 24.04 / EC2).
- Windows - via WSL2 (the terminals and pipe mode use Unix PTYs). Note: WSL2 shuts a distro down when its last process exits, so run the server in a session you keep open (a terminal window,
tmux, or a systemd unit withsystemd=truein/etc/wsl.conf).
| Agents - searchable grid with health scores, trends and one-click run/terminal | ![]() |
Board - work-items kanban (running / pending / failed / done) with retry, cancel and detail panel; Pending cards show priority while execution uses priority then FIFO and the view stays newest-first. Discarded backlog items move to the configurable Downloads/ToDelete/backlog-deleted/, never an internal permanent trash |
![]() |
| Health - day×hour activity heatmap (failure-tinted) + worst-first score cards | ![]() |
| Supervisor - clickable KPI filters, "up next" countdowns, cron in plain English | ![]() |
| Console - multi-terminal grid, agent chat sessions that survive refreshes, broadcast bar | ![]() |
| Pipe mode - chain agents with an animated flow view and live-streamed output (~1s latency) | ![]() |
| A2A - delivery queue, recent handoffs, active discoveries, consumer watermarks, transport drift and worker health, all read through the ecosystem's canonical readers | Built-in /a2a page |
| Pizza Bot Inbox - long-running threads, unread results and actions waiting for your decision, backed by a loopback DeepAgents/LangGraph sidecar | See docs/pizza.md |
| Logs - live tail with SSE, error highlighting, deep-linkable files | ![]() |
Per-agent observability: 30-day P50/P95/P99 duration percentiles and success-rate charts on every agent page.
Requirements: Python 3.12+, uv, Node 20+, sqlite3 CLI (only needed by bin/record-run).
Debian/Ubuntu one-liner (including WSL2)
sudo apt-get update && sudo apt-get install -y git sqlite3 curl python3-venv
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash - && sudo apt-get install -y nodejs
curl -LsSf https://astral.sh/uv/install.sh | shNote: install python3-venv (unversioned) - pinning a version like python3.12-venv breaks on newer Ubuntu releases that ship a different Python.
git clone https://github.com/brunokktro/agent-dashboard.git
cd agent-dashboard
# frontend (build once - FastAPI serves the result)
cd frontend && npm install && npm run build && cd ..
# backend
cd backend && uv sync
DASHBOARD_AGENTS_DIR=~/.kiro/agents uv run uvicorn dashboard.main:app --port 7780Open http://localhost:7780. Fresh install with no data? The Overview shows you exactly how to record your first run.
The universal adapter wraps any command and records it (job id, duration, status, exit code, log):
bin/record-run nightly-backup ./backup.sh
bin/record-run lint-check npm run lintWorks from cron, launchd, Makefiles, CI, Claude Code hooks. Recorded runs appear everywhere: Overview, Health, heatmap, diagnosis.
If you use KiroCrew, this dashboard is also packaged as a Crew App - the app.json at the repo root is the manifest. Installed as an app, it shows up in the Crew sidebar with everything working (terminals and live streams included), the backend is spawned and health-checked by the gateway on an automatic port, and no Node or manual build is needed - the built UI ships in the repo.
In the App Store: agent-dashboard is listed in the official catalog, kirodotdev/KiroCrewApps (added in #46), pinned to the
releasebranch. Install it from the App Store tab: no clone, no Node and no uv on the target machine. The gateway installs the Python requirements into the app's own data dir on first start, and enabling the app registers theagent-dashboard-troubleshootingskill, so KiroCrew's diagnose chat knows the install paths, the backend log and the known failures.
Where the built bundles live. main never tracks build output - it is a source branch. The App Store installs from the release branch, which is main plus one generated commit carrying the built frontend/dist and ui/dist, so an install needs no Node on the target machine. The root package.json is an empty marker that keeps the installer's build step a no-op (see DECISIONS #14). That branch is produced, never hand-edited:
bin/make-release # build, sanitize, publish release, tag source, verify blob hashes
bin/make-release --dry-run # build and scan the exact tree without publishing
bin/make-release --dry-run --output /path/to/tree # preserve that validated tree for an install testBefore any GitHub write, make-release mounts main plus both fresh bundles, validates the
required entry points, runs bin/scan-release positive controls, then scans both the mounted tree
and a tar archive of it. Site-specific customer names can be supplied without committing them via
RELEASE_DENY_TERMS_FILE=/path/to/terms.
Release process, so every version stays traceable:
- Bump the version in
app.json,backend/pyproject.tomlandbackend/src/dashboard/__init__.py(a test fails if they drift). bin/release-notes --save- writesdocs/releases/<version>.md: the commits behind the release, grouped by type. That is the internal record.- Curate the user-facing entry in
CHANGELOG.mdfrom it. bin/make-release- publishes thereleasebranch and creates thev<version>git tag on the source commit. An existing tag is never moved: a published version is immutable.
Until then (or for development), install from a local clone:
# third-party apps must be explicitly allowed once, then restart the gateway
# ~/.kiro/crew/config.json -> "agent": { "apps_allow_third_party": true }
kirocrew app install /path/to/agent-dashboard
kirocrew app enable agent-dashboard
kirocrew restartOpen the Crew dashboard and click Agent Dashboard in the sidebar. Per-app settings (hide agents you do not operate, site-specific failure hints) live in the host's data/config.json - see Configuration.
agents/dashboard-support is an agent that installs and troubleshoots this project: a .md spec plus a kiro-cli .json config, with the command-level playbooks under agents/dashboard-support-data/references/. Link both files into your ecosystem and it shows up in the dashboard like any other agent, chat and terminal included:
ln -s "$PWD/agents/dashboard-support.md" "${DASHBOARD_AGENTS_DIR:-$HOME/.kiro/agents}/"
ln -s "$PWD/agents/dashboard-support.json" "${DASHBOARD_AGENTS_DIR:-$HOME/.kiro/agents}/"You just cloned the repo and something does not work. The agent detects the actual state of the machine (OS incl. WSL2, Python 3.12+, Node 20+, uv, sqlite3, whether frontend/dist was built, whether DASHBOARD_AGENTS_DIR exists and has a runs.db, whether the port is free), reports it as a table, and gives you the exact commands in dependency order - runtimes, then the frontend build, then uv sync, then the server.
It knows the traps that are not obvious from the outside: dist/ is gitignored so there is no UI until you build; WSL2 shuts the distro down when its last process exits; bin/record-run needs the sqlite3 CLI; and an empty dashboard on a fresh install is correct, because the dashboard consumes artifacts it does not create.
It closes by validating for real - the server up, an HTTP check on / that must return HTML and not just any 200, and a throwaway record-run that has to appear in the API. An exit code from an intermediate step is never accepted as success.
The detection phase is also a standalone script - run it yourself and paste the output when asking for help anywhere:
bin/collect-diagnosticsIt reports platform, runtime versions, build state, ecosystem state, port and server health in one block, treats a missing binary as data rather than a failure, and prints no secrets (environment variables come out as SET/UNSET only).
Hand the agent a Slack user id and it runs the same diagnosis conversationally, asynchronously, in the other person's language (pt-BR, en-US, es - guessed from their profile locale, then mirrored from their reply). It states in the first message that it will wait at most 5 minutes, then waits with a bound instead of hanging.
Which Slack client does the talking is deliberately unspecified: the spec lists the four capabilities it needs (open a DM, post a message, read history since a point, download a file) and you map them to any Slack MCP server configured in your environment. Ids and handles are runtime inputs - the files only ever carry placeholders like <SLACK_USER_ID>.
The wait window is a standalone script, usable on its own:
bin/await-reply --timeout 300 --interval 15 -- <read-new-messages-command> "<CONVERSATION_ID>"It polls your fetch command until it prints something, and separates the three outcomes that matter:
| Exit | Meaning |
|---|---|
0 |
reply received - payload on stdout |
3 |
window closed with no reply - send the closing message and stop |
4 |
every poll failed - state unknown, do not report it as "no reply" |
That last one exists because a failed read reported as silence is a lie that looks like a fact.
Everything is environment-driven - no hardcoded paths.
| Env var | Default | Description |
|---|---|---|
DASHBOARD_AGENTS_DIR |
~/.kiro/agents |
Root of the observed ecosystem |
DASHBOARD_PORT |
7780 |
HTTP port |
DASHBOARD_SUPERVISOR_SERVICE |
(empty) | launchd/systemd service name for supervisor status |
DASHBOARD_BIG_LOG_MB |
50 |
Large-log alert threshold |
DASHBOARD_STUCK_AFTER_MINUTES |
30 |
Stuck queue item threshold |
DASHBOARD_EXCLUDE_AGENTS |
[] |
Glob patterns of agent names to hide from every view, JSON list (e.g. '["vendor-*","*-heartbeat"]') - useful for vendor-installed agents you do not operate |
DASHBOARD_INCLUDE_AGENTS |
[] |
Allowlist: when set, ONLY agents matching these globs are shown. Exclusions still win |
DASHBOARD_UPSTREAM_REPO |
brunokktro/agent-dashboard |
owner/name checked by the header's update button; empty disables the check |
DASHBOARD_EXTRA_HINTS |
[] |
Site-specific failure hints for the run diagnosis, JSON list of [regex, hint] pairs matched against the failing run's log (e.g. '[["corp-sso","SSO expired - re-authenticate"]]') - keeps internal tool names out of the code |
DASHBOARD_REDIRECTS |
{} |
Paths this dashboard no longer serves, mapped to where they now live: {"/old-page": "http://localhost:7781/old-page"}. Answers 307 so a bookmark never dead-ends. |
DASHBOARD_PIZZA_URL |
http://127.0.0.1:7782 |
Loopback-only Pizza Bot sidecar shown in the /pizza tab. Non-loopback values are rejected. |
Running under an app host (e.g. as a KiroCrew app) the backend gets a minimal environment, so DASHBOARD_* vars cannot reach it. There, the host's per-app settings file is the configuration channel: $KIROCREW_HOME/apps/agent-dashboard/data/config.json (editable via the host's PUT /api/apps/agent-dashboard/config), accepting exclude_agents, extra_hints, big_log_mb, stuck_after_minutes, job_agent_overrides and agent_deps with the same shapes as the env vars. Recognized keys override the environment; the file is optional.
Fastest path: bin/init-ecosystem scaffolds them for you (and can install the service unit). It is non-destructive - an existing file is reported and skipped, never overwritten:
bin/init-ecosystem # report what is missing; writes nothing
bin/init-ecosystem --runners # scaffold run-agent.sh + run-scheduled.sh (+ empty schedule.json)
bin/init-ecosystem --service # launchd (macOS) or systemd --user (Linux) unit, so it survives a reboot
bin/init-ecosystem --all # both
bin/init-ecosystem --runners --force # refresh the templates, moving yours to <file>.bak-<timestamp>Works the same on a fresh clone and on an install you already have running - if you cloned before this script existed, git pull and run it. Re-running is safe: existing files are reported as KEPT and never silently overwritten; --force backs yours up first, so nothing is ever lost.
The scaffolded runners assume kiro-cli and wrap each run with bin/record-run, which is what makes it appear in the dashboard - edit the one RUN= line for your own setup.
A fresh install observes an empty ecosystem and therefore looks dead. bin/install-starters gives it a heartbeat and a routine, with work that is actually useful:
bin/install-starters # show what would be installed
bin/install-starters --scripts # heartbeat (every 15 min) + log-hygiene (weekly) - no LLM needed
bin/install-starters --agents # failure-triage + dashboard-support (need kiro-cli)
bin/install-starters --allheartbeat verifies the server serves the app (200 and HTML) and that the API reads your ecosystem, which also means the Overview, heatmap and health score have real data from the first quarter hour. log-hygiene compresses logs past the large-log threshold and never deletes. failure-triage diagnoses the last 24h of failures through the dashboard's own /diagnose endpoint. Schedule entries are merged, so a job id you already have is never rewritten.
Details and how to write your own: starters/README.md. Anything tied to a system only you can reach (internal account tooling, a private API) belongs in your own ecosystem rather than a public repo - the starters ship the shape, you keep the specifics.
A shared agents directory (~/.kiro/agents is the default) usually holds agents installed by other tools too. Two ways to narrow it, and they compose:
| Approach | How | When |
|---|---|---|
| Separate ecosystem | DASHBOARD_AGENTS_DIR=~/my-agents |
full isolation - its own runs.db, queue and schedule |
| Filter a shared dir | DASHBOARD_INCLUDE_AGENTS='["my-*","ops-*"]' (allowlist) and/or DASHBOARD_EXCLUDE_AGENTS='["Vendor*"]' (denylist) |
keep one ecosystem, hide what you do not operate |
An allowlist usually beats chasing a growing denylist. Exclusions win over inclusions, so you can allow a broad pattern and still carve out exceptions.
The header has a version button: click it and the dashboard compares your version against the upstream repo's app.json. It only calls out when you click - there are no background network requests - and a check that fails says so instead of claiming you are up to date. Point it at your own fork with DASHBOARD_UPSTREAM_REPO=owner/name, or set it empty to disable the check entirely.
The rest of this section is the contract, if you would rather write them yourself.
The dashboard observes your ecosystem; it does not ship an agent runtime. The two "run" buttons delegate to scripts that live in YOUR ecosystem, at $DASHBOARD_AGENTS_DIR/scripts/:
| Script | Called by | Contract |
|---|---|---|
run-agent.sh <agent-name> run --no-interactive |
the Run button on an agent | executes the agent once; stdout/stderr are captured to logs/adhoc-<agent>-<stamp>.log. <agent-name> is the filename stem (the dashboard identity); when the agent's .json config carries a different internal name, it arrives as the AGENT_CLI_NAME env var - use it for kiro-cli chat --agent, since kiro-cli resolves by the internal name |
run-scheduled.sh <job-id> <script> <timeout-sec> |
the Run button on a scheduled job | executes the job the same way the scheduler would (and should record the run into runs.db, e.g. via bin/record-run) |
If a script is absent, the Run buttons render disabled, with a tooltip naming the file you need - the API also answers 503 with the same detail for anyone calling it directly. Everything else (Overview, Health, Logs, Queue, diagnosis) works without them. A minimal run-agent.sh for kiro-cli users:
#!/usr/bin/env bash
# $DASHBOARD_AGENTS_DIR/scripts/run-agent.sh
exec kiro-cli chat --agent "${AGENT_CLI_NAME:-$1}" --no-interactiveWrap it with bin/record-run "$1" ... if you also want the run to land in runs.db.
The dashboard reads, under DASHBOARD_AGENTS_DIR:
| Artifact | Shape |
|---|---|
runs.db |
SQLite table runs(job_id, started_at, duration_sec, status, exit_code, log_path) - auto-created if missing |
queue/{pending,running,done,failed}/*.json |
Work items: {id, agent, input, priority, created, status} |
scripts/schedule.json |
{"jobs": [{id, script, cron, timeout_sec, enabled}]} |
locks/*.lock |
PID files marking running agents |
*.md |
Agent specs with YAML frontmatter (name, description) |
*.json |
kiro-cli agent configs (enables chat/terminal for that agent) |
flowchart LR
subgraph Browser["🖥️ Your browser"]
UI["React 19 · Tailwind · shadcn/ui<br/>TanStack Query · xterm.js · Recharts"]
end
subgraph Server["⚙️ FastAPI backend (localhost)"]
API["REST API<br/>/api/*"]
WS["WebSocket<br/>events · terminals"]
SSE["SSE<br/>live log tail"]
DS["Typed datastore adapter"]
PIPE["Pipe runner<br/>PTY-streamed agent chains"]
end
subgraph Ecosystem["📁 Your agent ecosystem (files)"]
DB[("runs.db<br/>SQLite")]
Q["queue/*/·json"]
SCHED["schedule.json"]
LOCKS["locks/*.lock"]
SPECS["agents *.md / *.json"]
LOGS["logs/*.log"]
end
UI <--> API & WS & SSE
API --> DS
WS --> DS
PIPE -->|"kiro-cli chat"| AGENTS["🤖 your agents"]
DS -->|read| DB & Q & SCHED & LOCKS & SPECS
SSE -->|follow| LOGS
DS -.->|"only writes:<br/>queue moves · acks"| Q
cd backend && uv run pytest && uv run ruff check src tests- Real artifacts, no mocks - fixtures build a miniature ecosystem in a temp dir: a real SQLite
runs.db, real queue JSON files, real markdown specs. Mocks mask type errors and schema drift; real files catch them. - Property-based invariants (Hypothesis) - e.g. the health score must stay within 0-100 for ANY run history the generator can invent.
- Regression-bug-first - every bug found in real use becomes a failing test before the fix (e.g. queue files whose name differs from the item id, YAML folded descriptions, sort-by-insertion-order assumptions).
- Fixtures must mirror real-world variance - the one bug that escaped had a fixture sharing the same assumption as the code. Lesson encoded in the suite.
The Help tab inside the app is a full user guide: sidebar navigation, per-feature walkthroughs with screenshots, an onboarding section for Kiro CLI / Claude Code users, the data contracts, and an exit-code cheat-sheet for the failure diagnosis.
All screenshots in this README (and in the in-app Help) come from a generic, deterministic demo ecosystem - never from anyone's real agents. demo/seed-demo.py rebuilds it from scratch:
python3 demo/seed-demo.py # writes to demo/demo-agents/ (gitignored)
python3 demo/seed-demo.py /tmp/demo # or anywhere elseIt creates 12 fictional agents with specs, configs, a runs.db with ~2,300 runs derived from each agent's real cron cadence walking backwards from now, schedule, queue items and logs. Deterministic (fixed seed) and self-checking: it asserts that no run lands in the future - a future-stamped run once rendered -5358s ago in a published screenshot, and the assert keeps that bug from coming back.
To capture against it, point the server at the demo tree and use a viewport of 1400x860:
cd backend
DASHBOARD_AGENTS_DIR=../demo/demo-agents uv run uvicorn dashboard.main:app --port 7781Screenshots land in docs/img/ AND frontend/public/help/ (copy first, then npm run build - the Help tab serves from dist/).
- Failure diagnosis isolates the failing run's own log window (by timestamp) and matches known patterns - expired auth, missing dependencies, PATH issues, rate limits, timeouts - into "probable cause" hints.
- Terminal sessions are durable: each PTY lives server-side keyed by session id; a page refresh reattaches and replays recent output. Detached idle shells expire after 15 minutes; a foreground command or agent remains alive for up to 8 hours.
- Pipe jobs are durable too: persisted to disk, they survive backend restarts and are reported honestly as
lostif their process died with the server. - Directory is the source of truth for queue state - the JSON
statusfield is display-only. - Everything real-time falls back gracefully: if the WebSocket drops, 10s polling keeps the UI honest.
- Kiro CLI - the agentic CLI this dashboard was built around
- KiroCrew - multi-agent crews; complements this dashboard (we observe, they orchestrate)
- AI-assisted log enrichment on the diagnosis panel
- Chat companions for script-based agents
- Pluggable datastore adapters (other run recorders)
This repository has two lines of history, on purpose:
| Branch | What it is | Pushed to GitHub? |
|---|---|---|
main |
the local build: integrations and paths specific to one ecosystem | never |
public |
what is published here | yes, as main on GitHub |
A local pre-push hook refuses every push that does not come from public, so git push,
git push --all and git push origin main all fail by design.
To publish a change:
git switch public # or use a worktree, to keep the local build running
git cherry-pick <commit> # only commits that are safe to share
git show --stat HEAD # review: no local paths, names or internal data
git push origin public:mainAnything that exists only on main never passes through public. The hook lives in
.git/hooks/pre-push; it is not versioned, so a fresh clone does not have it.







