| name | hermes-labyrinth-observability |
| description | Read-only observability dashboard plugin for Hermes Agent — journeys, crossings, guideposts, and reports. |
| triggers | ["add hermes labyrinth to my agent setup","how do I observe my hermes agent runs","install hermes labyrinth plugin","view agent journey crossings dashboard","export hermes agent report","debug autonomous agent with labyrinth","set up hermes observability plugin","how do I read hermes labyrinth API"] |
Hermes Labyrinth
Skill by ara.so — Daily 2026 Skills collection.
Hermes Labyrinth is a read-only observability dashboard plugin for Hermes Agent. It turns autonomous agent runs into a navigable map of crossings (prompts, tool calls, tool results, failures, model switches, subagents, approvals, memory hits, redactions, context compression, cron runs) with exportable evidence. It is not a chat UI — it is a black-box recorder for agents moving through unknown work.
Install
Plugin Directory Install
mkdir -p ~/.hermes/plugins
git clone https://github.com/stainlu/hermes-labyrinth.git ~/.hermes/plugins/hermes-labyrinth
Start or restart the Hermes dashboard:
hermes dashboard
If the dashboard is already running, rescan plugins without restarting:
curl http://127.0.0.1:9119/api/dashboard/plugins/rescan
Open the dashboard in your browser and select the Labyrinth tab.
Optional Theme
mkdir -p ~/.hermes/dashboard-themes
cp ~/.hermes/plugins/hermes-labyrinth/theme/hermes-labyrinth.yaml ~/.hermes/dashboard-themes/
What Labyrinth Tracks
| View | Contents |
|---|
| Journey index | Recent CLI, dashboard, gateway, cron, and delegated work |
| Labyrinth map | Ordered crossings through a selected agent journey |
| Inspector | Input, output, duration, status, evidence, guideposts per crossing |
| Guideposts | Generated observations backed by local evidence |
| Skill atlas | Bundled, optional, external, and user skill inventory |
| Cron gate | Scheduled autonomy, next runs, last failures, workdirs |
| Model ferry | Model/provider transitions across sessions |
| Reports | Redacted Markdown and JSON exports for one journey |
API Surface
All endpoints are read-only. The plugin API is served by Hermes dashboard at:
http://127.0.0.1:9119/api/plugins/hermes-labyrinth/
Endpoints
GET /api/plugins/hermes-labyrinth/health
GET /api/plugins/hermes-labyrinth/journeys
GET /api/plugins/hermes-labyrinth/journeys/{journey_id}
GET /api/plugins/hermes-labyrinth/journeys/{journey_id}/crossings
GET /api/plugins/hermes-labyrinth/skills
GET /api/plugins/hermes-labyrinth/cron
GET /api/plugins/hermes-labyrinth/guideposts
GET /api/plugins/hermes-labyrinth/reports/{journey_id}.json
GET /api/plugins/hermes-labyrinth/reports/{journey_id}.md
Example: Fetch All Journeys
curl http://127.0.0.1:9119/api/plugins/hermes-labyrinth/journeys | jq .
Example: Fetch Crossings for a Journey
JOURNEY_ID="your-journey-id"
curl "http://127.0.0.1:9119/api/plugins/hermes-labyrinth/journeys/${JOURNEY_ID}/crossings" | jq .
Example: Export a Journey Report as Markdown
JOURNEY_ID="your-journey-id"
curl "http://127.0.0.1:9119/api/plugins/hermes-labyrinth/reports/${JOURNEY_ID}.md" > report.md
Example: Export a Journey Report as JSON
JOURNEY_ID="your-journey-id"
curl "http://127.0.0.1:9119/api/plugins/hermes-labyrinth/reports/${JOURNEY_ID}.json" > report.json
Example: Health Check
curl http://127.0.0.1:9119/api/plugins/hermes-labyrinth/health
Python API Client Examples
The plugin backend lives at dashboard/plugin_api.py. You can also call the HTTP API from any language. Here are Python examples:
import urllib.request
import json
BASE = "http://127.0.0.1:9119/api/plugins/hermes-labyrinth"
def get_journeys():
with urllib.request.urlopen(f"{BASE}/journeys") as r:
return json.loads(r.read())
def get_crossings(journey_id: str):
with urllib.request.urlopen(f"{BASE}/journeys/{journey_id}/crossings") as r:
return json.loads(r.read())
def get_report_json(journey_id: str):
with urllib.request.urlopen(f"{BASE}/reports/{journey_id}.json") as r:
return json.loads(r.read())
def get_report_md(journey_id: str) -> str:
with urllib.request.urlopen(f"{BASE}/reports/{journey_id}.md") as r:
return r.read().decode("utf-8")
journeys = get_journeys()
for j in journeys:
print(j["id"], j.get("status"), j.get("started_at"))
Iterate Crossings and Inspect Tool Calls
import urllib.request
import json
BASE = "http://127.0.0.1:9119/api/plugins/hermes-labyrinth"
def inspect_tool_crossings(journey_id: str):
with urllib.request.urlopen(f"{BASE}/journeys/{journey_id}/crossings") as r:
crossings = json.loads(r.read())
for crossing in crossings:
if crossing.get("type") == "tool_call":
print(f"Tool: {crossing['tool']}")
print(f" Status: {crossing.get('status')}")
print(f" Duration: {crossing.get('duration_ms')}ms")
print(f" Input: {json.dumps(crossing.get('input', {}))[:200]}")
print()
inspect_tool_crossings("your-journey-id")
Download and Save All Reports for Recent Journeys
import urllib.request
import json
import pathlib
BASE = "http://127.0.0.1:9119/api/plugins/hermes-labyrinth"
OUT = pathlib.Path("./labyrinth-reports")
OUT.mkdir(exist_ok=True)
with urllib.request.urlopen(f"{BASE}/journeys") as r:
journeys = json.loads(r.read())
for j in journeys[:10]:
jid = j["id"]
try:
with urllib.request.urlopen(f"{BASE}/reports/{jid}.json") as r:
(OUT / f"{jid}.json").write_bytes(r.read())
with urllib.request.urlopen(f"{BASE}/reports/{jid}.md") as r:
(OUT / f"{jid}.md").write_bytes(r.read())
print(f"Saved reports for {jid}")
except Exception as e:
print(f"Failed {jid}: {e}")
JavaScript / Frontend API Examples
The frontend plugin bundle lives in dashboard/dist/. If you're extending the UI or writing a custom integration:
const BASE = "http://127.0.0.1:9119/api/plugins/hermes-labyrinth";
async function fetchJourneys() {
const res = await fetch(`${BASE}/journeys`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
}
async function fetchCrossings(journeyId) {
const res = await fetch(`${BASE}/journeys/${journeyId}/crossings`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
}
async function fetchReportMarkdown(journeyId) {
const res = await fetch(`${BASE}/reports/${journeyId}.md`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.text();
}
async function logFailures() {
const journeys = await fetchJourneys();
if (!journeys.length) return;
const crossings = await fetchCrossings(journeys[0].id);
const failed = crossings.filter(c => c.status === "failure" || c.status === "error");
console.table(failed.map(c => ({
type: c.type,
tool: c.tool ?? "-",
duration_ms: c.duration_ms,
error: c.error?.slice(0, 120),
})));
}
logFailures();
Build & Development
The frontend is built from src/parts/*.js + src/labyrinth.css into dashboard/dist/. The demo index.html is generated with content-hash query strings.
npm run build
npm run check
npm run smoke
npm run smoke:live
npm test
Full Test Suite
npm test
Runs:
- Reproducible build checks for
dashboard/dist and index.html
- Frontend JavaScript parse checks
- Backend Python parse checks
- API normalization fixture tests (including numeric Hermes timestamps)
- Packed-artifact and dead-control regressions
- Headless Chrome smoke coverage for map modes, route changes, search, dataset switching, and threshold filter
Repository Layout
.
├── dashboard/
│ ├── manifest.json # Hermes dashboard plugin manifest
│ ├── plugin_api.py # Read-only API over local Hermes state
│ └── dist/ # Generated dashboard plugin bundle
├── docs/
│ ├── CONCEPT.md
│ ├── DESIGN_BRIEF.md
│ └── FUNCTIONAL_SPEC.md
├── scripts/
│ ├── build-plugin.mjs # Builds dashboard/dist and index.html
│ ├── smoke-demo.mjs # Browser smoke test for public demo
│ ├── test-plugin-api.py # Fixture tests for API normalization
│ └── verify.mjs # Local verification checks
├── src/
│ ├── demo/ # GitHub Pages demo source
│ ├── parts/ # Ordered frontend source chunks
│ └── labyrinth.css # Frontend CSS source
├── theme/
│ └── hermes-labyrinth.yaml
├── index.html # Generated GitHub Pages demo
└── package.json
Architecture
Hermes local state
├─ state.db sessions/messages
├─ skills directories
└─ cron config
↓
dashboard/plugin_api.py
↓
/api/plugins/hermes-labyrinth/*
↓
src/parts/*.js + src/labyrinth.css
↓ npm run build
dashboard/dist/*
↓
Hermes dashboard tab: Labyrinth
Data Policy (Important)
- Read-only by design: does not start, stop, resume, mutate, or create Hermes sessions.
- Secret redaction: applied to previews and reports.
- Unknown fields: stay unknown — not silently dropped.
- Reports: generated from local Hermes state only.
- Public demo: uses sample/mocked data — not live telemetry.
Common Patterns
Check Plugin Health Before Querying
import urllib.request, json
def is_labyrinth_healthy() -> bool:
try:
with urllib.request.urlopen(
"http://127.0.0.1:9119/api/plugins/hermes-labyrinth/health",
timeout=3
) as r:
data = json.loads(r.read())
return data.get("status") == "ok"
except Exception:
return False
if not is_labyrinth_healthy():
print("Labyrinth plugin not reachable — is `hermes dashboard` running?")
Filter Journeys by Type
import urllib.request, json
BASE = "http://127.0.0.1:9119/api/plugins/hermes-labyrinth"
with urllib.request.urlopen(f"{BASE}/journeys") as r:
journeys = json.loads(r.read())
cron_journeys = [j for j in journeys if j.get("origin") == "cron"]
failed_journeys = [j for j in journeys if j.get("status") in ("failure", "error")]
Summarize Crossing Types in a Journey
from collections import Counter
import urllib.request, json
BASE = "http://127.0.0.1:9119/api/plugins/hermes-labyrinth"
def summarize_journey(journey_id: str):
with urllib.request.urlopen(f"{BASE}/journeys/{journey_id}/crossings") as r:
crossings = json.loads(r.read())
counts = Counter(c.get("type", "unknown") for c in crossings)
total_ms = sum(c.get("duration_ms", 0) for c in crossings)
print(f"Journey {journey_id}: {len(crossings)} crossings, {total_ms}ms total")
for ctype, n in counts.most_common():
print(f" {ctype}: {n}")
Troubleshooting
Plugin tab not appearing in dashboard
- Confirm clone location:
ls ~/.hermes/plugins/hermes-labyrinth/dashboard/manifest.json
- Restart
hermes dashboard or rescan: curl http://127.0.0.1:9119/api/dashboard/plugins/rescan
- Check
manifest.json is valid JSON.
API returns 404
- Verify the plugin loaded:
curl http://127.0.0.1:9119/api/plugins/hermes-labyrinth/health
- Confirm
hermes dashboard is running on port 9119.
Build artifacts out of date
npm run build
npm run check
Smoke tests fail locally
- Requires headless Chrome/Chromium available in
PATH.
- Run
npm run smoke for local, npm run smoke:live for the deployed Pages demo.
Reports contain redacted fields
This is expected. Labyrinth applies secret redaction to all previews and exports by design. Raw values are only accessible inside the Hermes process itself.
Numeric timestamps in API responses
The API normalization layer handles numeric Hermes timestamps automatically (covered by fixture tests in scripts/test-plugin-api.py). If you consume the API directly, expect either ISO 8601 strings or Unix epoch integers in timestamp fields.
Links