Loading...
Loading...
Read-only observability dashboard plugin for Hermes Agent — journeys, crossings, guideposts, and reports.
npx skill4agent add aradotso/trending-skills hermes-labyrinth-observabilitySkill by ara.so — Daily 2026 Skills collection.
mkdir -p ~/.hermes/plugins
git clone https://github.com/stainlu/hermes-labyrinth.git ~/.hermes/plugins/hermes-labyrinthhermes dashboardcurl http://127.0.0.1:9119/api/dashboard/plugins/rescanmkdir -p ~/.hermes/dashboard-themes
cp ~/.hermes/plugins/hermes-labyrinth/theme/hermes-labyrinth.yaml ~/.hermes/dashboard-themes/| 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 |
http://127.0.0.1:9119/api/plugins/hermes-labyrinth/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}.mdcurl http://127.0.0.1:9119/api/plugins/hermes-labyrinth/journeys | jq .JOURNEY_ID="your-journey-id"
curl "http://127.0.0.1:9119/api/plugins/hermes-labyrinth/journeys/${JOURNEY_ID}/crossings" | jq .JOURNEY_ID="your-journey-id"
curl "http://127.0.0.1:9119/api/plugins/hermes-labyrinth/reports/${JOURNEY_ID}.md" > report.mdJOURNEY_ID="your-journey-id"
curl "http://127.0.0.1:9119/api/plugins/hermes-labyrinth/reports/${JOURNEY_ID}.json" > report.jsoncurl http://127.0.0.1:9119/api/plugins/hermes-labyrinth/healthdashboard/plugin_api.pyimport 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")
# Usage
journeys = get_journeys()
for j in journeys:
print(j["id"], j.get("status"), j.get("started_at"))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")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]: # last 10 journeys
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}")dashboard/dist/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();
}
// Example: log all failed crossings in the most recent journey
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();src/parts/*.jssrc/labyrinth.cssdashboard/dist/index.html# Build dashboard/dist and index.html
npm run build
# Run reproducibility and parse checks
npm run check
# Run browser smoke tests (headless Chrome)
npm run smoke
# Smoke-test the deployed GitHub Pages demo
npm run smoke:live
# Run all tests (build checks, fixture tests, smoke)
npm testnpm testdashboard/distindex.html.
├── 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.jsonHermes 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: Labyrinthimport 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?")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())
# Filter to only cron-triggered journeys
cron_journeys = [j for j in journeys if j.get("origin") == "cron"]
# Filter to only failed journeys
failed_journeys = [j for j in journeys if j.get("status") in ("failure", "error")]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}")ls ~/.hermes/plugins/hermes-labyrinth/dashboard/manifest.jsonhermes dashboardcurl http://127.0.0.1:9119/api/dashboard/plugins/rescanmanifest.jsoncurl http://127.0.0.1:9119/api/plugins/hermes-labyrinth/healthhermes dashboard9119npm run build
# Then verify reproducibility
npm run checkPATHnpm run smokenpm run smoke:livescripts/test-plugin-api.py