All notable changes to Heimdall are recorded here.
The format follows Keep a Changelog, and the project follows Semantic Versioning.
- Ghostty over SSH now shows the splash image, not the ASCII fallback. The image
detection recognised Ghostty only by
TERM_PROGRAM=ghostty, which SSH and tmux strip — so a dashboard run over SSH from Ghostty fell back to ASCII even though the terminal can render images. It now also treatsTERM=xterm-ghostty(always forwarded) as Kitty-graphics-capable.
- Himinbjörg — one focus + scroll primitive for the TUI (
app/internal/tui/pane). APane(scroll, selection-that-follows-the-cursor,/filter, sort) and aGroup(Tab/Shift-Tab focus with a focus ring, two-level page+pane scroll, pointer-targeted mouse) replace the three divergent scroll implementations the TUI carried. Capability interfaces (Filterable/Sortable) are opt-in, so static panels get neither. Panes render only (ADR-0022). See ADR-0023. - The full-screen top view (
t) runs on the pane primitive. The whole view is the first focus —↑/↓and the mouse wheel scroll the entire screen, so it is always scrollable at any resolution — andTabsteps the focus ring through each panel and wraps back to the whole-view stop. The mouse wheel/click targets the panel under the pointer (it did nothing in the top view before). The wide two-column panel grid is kept. Dashboard-modal adoption is still in progress onfeature/himinbjorg-focus-scroll. - The command picker (
c) filters by name, and its result filters by line./narrows the command list as you type; after a command runs,/in the result view filters the output lines (case-insensitive substring).escclears either filter, the highlighted row stays in view — the same idiom as the log-view search. - The process view (
p) filters by command./narrows the process table by command name; it stacks with thessort, so you can filter to a process family and sort it by CPU or memory.escclears the filter.
- Moved the P/E core-grouping out of the dashboard into the
domaincore. The standardization boundary is now explicit (ADR-0022): adapters translate platform specifics into neutral metrics, the framework-freedomaincore turns them into display-ready structures (domain.CoreGroups,domain.CoreTypeSummary), and the TUI and CLI render only. The dashboard no longer buckets cores or reads a metric'sGauge— which is what let a wire-encoding detail blank the grid before. - The CLI now reports core groups.
heimdall-cli host <id>gains acore_groupsfield (standardized P/E/LP with real core ids and utilisation), so an agent reads structured groups instead of parsing the12P + 4Estring. Absent on uniform CPUs.
- Bump
golang.org/x/net0.51.0 → 0.55.0 for a medium-severity HTML-parser DoS (Dependabot #1 / PR #6).golang.org/x/sysandgolang.org/x/termcame along.
- New ADR-0022 and
contributor guide (
docs/guides/17-standardization-and-adapters.md); the standardization rule is now in CLAUDE.md, AGENTS.md, the Copilot instructions, a Cursor rule, and the architect agent across all harnesses.
- Top view showed the plain per-core grid instead of the P/E grouping. The
grouping keyed off
cpu.topology's Gauge (the distinct-core-type count), but per-core metrics ride the protoper_coreoneof, so the Gauge is dropped on the wire and the dashboard received it as 0 — every host fell back to the unlabelled grid. It now derives the count from the PerCore slice, which does cross the wire. Worked locally (--once) and in unit tests because the Gauge is set in-process; only the wire round-trip lost it. Thecpu clusters:power line was unaffected.
- CPU power came back blank on every Linux/AMD host with a GPU. The daemon's
in-process-first gate treated
power.totalas proof it already had CPU power, butpower.totalis synthesised frompower.gpuon any GPU-equipped host — so the daemon short-circuited and never consulted the root helper for RAPLpower.cpu. The gate now checks for an actual CPU rail (power.cpu/power.pkg), so a non-root Linux daemon with a running helper reads RAPL again. Regression from the v2.5.0 power standardization; Apple (SMC/IOReport in-process) and Windows (Scaphandre in-process) were never affected. ARM hosts with no RAPL (GB10) stay an honest dash.
- Top view: per-core grid labelled by core type. On hybrid silicon the
per-core bars are grouped under
P-cores (N):/E-cores (N):headers with their real core ids — you can now see which cores are pinned. Sources: new unprivilegedcpu.topologymetric — sysctlhw.perflevel*on Apple Silicon (E-cluster owns the low ids, verified live on an M3 Max),/sys/devices/cpu_core|cpu_atomon Intel hybrid Linux, andGetLogicalProcessorInformationExEfficiencyClass on Windows. Uniform CPUs (AMD, non-hybrid Intel, ARM) and older daemons keep the plain grid — new metric names only, nothing existing changed shape. NARROW tier collapses to one aggregate per type. - Per-cluster CPU power on Apple Pro/Max. The SMC P/E rails that already
fed
power.cpuare now surfaced aspower.cpu.pcluster/power.cpu.ecluster, and the top view's POWER panel showscpu clusters: P … W · E … Wunder the CPU figure. Hosts without cluster rails (base dies, Linux RAPL, Windows) simply don't emit them — a rail that didn't read is never a fabricated 0. (story 0028, issue #5)
- Guard the Apple SMC CPU-power read against unmapped chips. The SMC cluster
keys are verified on an M3 Max; on a different Apple die the same keys could
mean something else.
power.cpufrom SMC is now rejected if the sum is implausible (NaN/Inf, negative, or > 200 W), so such a chip falls back to Unavailable rather than showing a garbage number. IOReport stays the first source (covers every base die), so this only affects the Pro/Max SMC fallback — no change on Linux (RAPL) or Windows (Scaphandre).
- Apple Pro/Max
power.cpunow covers the whole CPU complex, not just the P-cores. The SMC CPU sum wasPC02+PC42(the two P-clusters only); it now also includes the E-core / cluster rails that scale with CPU load (PC03+PC43) — the full CPU consumption, ~18–20 W under a 16-core load on an M3 Max (was ~18 W P-cores-only). GPU (PC1x/PC2x) and memory (PC32) keys stay excluded. The rail is now labelledCPU complex (SMC).
- Apple Silicon Pro/Max CPU power, via raw SMC keys. On Pro/Max/Ultra dies
the IOReport energy model reports
0for CPU (verified —powermetricsdoes too), sopower.cpuused to read Unavailable there. But the per-domain power is still in the SMC under the per-cluster keys (PC02+PC42on an M3 Max — the same source Stats reads). Heimdall now sums those and reports a realpower.cputaggedP-cores (SMC)— ~11 W under load, verified live on an M3 Max. IOReport stays the first source, so base dies (e.g. M4) are unchanged; the SMC keys fill Pro/Max, and only a chip whose keys aren't mapped yet falls back tono per-domain CPU power (IOReport + SMC). The SMC reader was generalized to read any AppleSMC float key.
- Apple GPU power is reported at idle instead of dropped. The collector
skipped a zero-valued GPU rail, so a base M4 — whose GPU idles at a few mW —
looked like it "had no GPU", while a Pro/Max (with a small non-zero idle GPU)
showed one. The GPU energy channel is always valid on Apple Silicon, so
power.gpuis now reported even at ~0 W. The CPU asymmetry is real and unchanged: base M-series expose per-domain CPU power, Pro/Max/Ultra do not (verified —powermetricsalso reportsCPU Power: 0 mW), sopower.cpustays Unavailable-with-reason there. Documented in the macOS guide.
- macOS: running the helper no longer blanks power/GPU — the real fix. v2.4.1
only bounded the helper call, which still left too little of the 1 s budget for
the fallback, so a slow
powermetricshelper on an M-series Pro/Max kept timing out the whole privileged adapter. The adapter now reads in-process first and consults the helper only when the daemon can't read a CPU power rail itself (power.cpu/power.total/power.pkg) — i.e. an unprivileged Linux daemon that needs the root helper for RAPL. On macOS the daemon already has power from SMC/IOReport, so the helper is never on the critical path: running it on Apple Silicon is finally harmless, not destructive. (GPU power alone doesn't count as "has power" — a Linux box with an NVIDIA GPU still needs the helper for RAPL.)
- A slow privileged helper no longer blanks every power/GPU metric. The
helper socket call and the in-process fallback shared the adapter's 1 s
deadline, so a slow helper — notably macOS
powermetrics, which can take ~1 s on an M-series Pro/Max — consumed the whole budget and the fallback never ran: the privileged adapter timed out and reported power/GPU/VRAM/NPU as errored. The helper call is now bounded to a slice of the deadline, leaving time for the in-process read, so the helper stays additive under latency. Matters most on Apple Silicon, where the daemon reads SMC/IOReport unprivileged and the helper is optional — running it must never make things worse.
- Multi-GPU NVIDIA hosts aggregate across cards.
nvidia-smiemits one row per GPU; the collector now reads them all —power.gpusums (sopower.totalis right),gpu.util/gpu.clock/gpu.mem.utilaverage,gpu.temp/gpu.fantake the hottest, andgpu.vrampools used/total with a(N GPUs)note. Single-GPU hosts are unchanged. - Intel NPU utilisation. On Intel NPUs (the
intel_vpudriver, e.g. Core Ultra / Arrow Lake)npu.utilis now a real reading, derived from the driver's cumulativenpu_busy_time_uscounter (idle reads a legitimate 0%). AMD XDNA and Apple ANE still expose no counter and stay Unavailable-with-reason. - AMD
gpu.fanfrom amdgpu sysfs. Read thepwm1duty cycle (0–255) from the amdgpu hwmon and report it as a fan-speed percentage, so a discrete Radeon shows fan speed withoutamd-smi. Absent on APUs with no dedicated GPU fan. power.npuexplains its dash — reported Unavailable-with-reason (no NPU power counter) where no source exists, mirroringnpu.util.
- Windows CPU power now comes from Scaphandre, not LibreHardwareMonitor.
Scaphandre installs the signed Hubblo RAPL driver and runs as a Windows service
exposing a Prometheus endpoint — a far cleaner install than a GUI monitor.
Heimdall scrapes it (
scaph_socket_power_microwatts, summed) and reportspower.cpu(Intel + AMD, pure Go, no cgo). Point it at a non-default endpoint withHEIMDALL_SCAPHANDRE_URL. Without Scaphandre,power.cpureadsno RAPL on Windows — run Scaphandre for CPU power. See guide 16.
- Windows CPU power via LibreHardwareMonitor. Windows exposes no RAPL to user
space (reading the CPU energy MSRs needs a ring-0 driver Heimdall doesn't ship).
When LibreHardwareMonitor is running, Heimdall now reads the CPU-package
power sensor from its WMI provider (
root/LibreHardwareMonitor) and reports it aspower.cpu— Intel and AMD, no cgo. Absent,power.cpureadsno RAPL on Windows — run LibreHardwareMonitor for CPU power.
- Apple Pro/Max:
power.cpuexplains the dash. On Pro/Max dies the CPU/ANE power channels read 0 in both IOReport and powermetrics (only the SMC whole-system total is real; base M-series expose the per-domain figure). Instead of a silent blank,power.cpunow reads Unavailable withPro/Max: no per-domain CPU power.power.totalstill carries the real whole-machine draw. - Host detail view now shows why a metric is dashed — the Unavailable reason (e.g. the CPU-power notes above) renders next to the dash, clamped to width so it can't wrap or break the layout.
- Standardized power on three metrics everywhere:
power.cpu,power.gpu,power.total— the same CPU / GPU / total split btop, mactop, and top show.power.pkgis retired; the CPU rail is alwayspower.cpu:- Linux:
power.cpu= the RAPL package (whole CPU socket — what btop calls "CPU"); the unreliable RAPLcoresubdomain is dropped. - macOS:
power.cpu= IOReport CPU;power.total= SMCPSTR(whole-system) — the SMC figure is no longer mislabelledpower.pkg. power.total=cpu + gpu + npuon every non-Apple host (Apple already reports a whole-system total). This fixes AMD APUs (Strix Halo) under-counting: RAPL package and amdgpu are separate rails and are now summed (matching btop's CPU + GPU).
- Linux:
- Both the top view and the host detail view now show CPU, GPU, and total power.
power.cpuis reported Unavailable-with-reason where no source exists: GB10 (no RAPL on Grace/ARM) and Windows (RAPL inaccessible).
power.total— a source-aware whole-machine power figure, now headlined by the top-view POWER panel. The headline used to bepower.pkg(CPU package only), so a workstation pulling 506 W on the GPU read as ~32 W.power.totalsums the right rails per platform without double-counting: Apple =power.pkg(SMCPSTRis already whole-system); NVIDIA / GB10 =pkg + gpu + npu(the GPU is a separate rail); AMD APU / integrated =pkg(the iGPU is already inside the package). rtx-pro now reads ~538 W, GB10 ~50 W.power.pkgon a no-RAPL SoC now says why. GB10 (Grace/ARM) exposes no RAPL, INA, or any CPU/module power sensor, sopower.pkgreads Unavailable withno RAPL power sensor (SoC/ARM)instead of a silent blank; therepower.totalis the GPU rail alone.
- POWER panel layout: a
totalheadline, thencpu / gpu / npu, thenCPU pkg— making the CPU-cores (cpu) vs CPU-package (pkg) distinction explicit (cpuis the cores, a subset of the wholepkgsocket).
heimdall-clinow exposes OK-metric detail strings. Info metrics (host.version,host.gpu,host.os,host.cpu,host.kernel,host.arch) carry their human value in the metric detail with a zero gauge, so the CLI previously showed them as0and dropped the string entirely. A newdetailsobject (metric name → detail text) surfaces them, plus notes likegpu.vram's43/122 GB shared.metricsstays value-only, so existing scripts are unaffected. Nowheimdall-cli host <id> | jq -r '.details["host.version"]'works.
- Top view: the GB10
gpu.vramdetail wrapped and broke the panel box. The(shared)detail was longer than a discrete card's, so lipgloss wrapped the line and mangled the GPU panel. The detail is now clipped to the space left on the line (never wraps) and shortened to42/122 GB shared(was42.6 / 121.6 GB (shared)). heimdall-clihid unavailable metrics. It only emitted OK metrics asmetrics(name → value), so a metric that is deliberately Unavailable — Applegpu.vram,npu.util, anything needing the helper — vanished from CLI output with no reason. A newunavailableobject (name →{status, detail}) now carries them.metricsis unchanged, so existing scripts keep working.
npu.utilis Unavailable-with-reason everywhere, not a bare dash. NPUs (Apple ANE, Intel AI Boost, AMD XDNA) expose no utilisation counter; hosts without an NPU source now reportnpu.utilUnavailable withno NPU utilisation counter(the AMD path keeps its own specific reason).
scripts/release.shnow builds macOS with CGO (so IOReport/SMC power is compiled in) and refuses to emit a CGO-free darwin binary when run off a Mac —make releaseon a Mac is no longer a footgun. CI is unaffected.- The Makefile stamps
-X main.versionfromgit describe, somake build-tuibinaries report a real version instead ofdev.
power.cpuon Linux from the RAPLcoresubdomain. The Linux privileged path only read the RAPL package (power.pkg), so the top view's CPU power column was blank while the GPU showed 60W+ and the package ~17W — which read as "where's the CPU, and why is the package below the GPU?". It now also reads thecore(pp0) subdomain aspower.cpu(cores only). The two CPU rails are labelled —power.pkg= "CPU package",power.cpu= "CPU cores" — to make clear they are the CPU socket only; a discrete GPU is a separate rail (power.gpu), which is whypower.pkgcan sit well belowpower.gpu. Absent on CPUs with nocoresubdomain.power.npustays unavailable — Intel/AMD NPUs expose no power counter (same posture as Apple ANE / AMD XDNA).
- GPU VRAM on unified-memory NVIDIA (GB10 Grace-Blackwell).
nvidia-smireportsmemory.used/memory.totalas[N/A]on GB10 because there is no discrete VRAM — so hosts likepromaxgb10showed GPU util but nogpu.vram. When the aggregate counter is[N/A],gpu.vramis now derived from the sum of per-process GPU memory (nvidia-smi --query-compute-apps=used_memory) over the system RAM total, e.g.gpu.vram 34% 41.6 / 121.6 GB (shared). Discrete NVIDIA cards (with a real aggregate counter) are unchanged.
- A broken
nvidia-smisilently blanked GPU instead of saying why. Whennvidia-smiis installed but exits non-zero — most often aDriver/library version mismatchafter the NVIDIA driver is upgraded without a reboot — the collector dropped everygpu.*key, so the host looked like Heimdall was failing.gpu.utilandgpu.vramare now reported Unavailable carrying thenvidia-smireason (e.g.nvidia-smi: Failed to initialize NVML: Driver/library version mismatch), so the fix (reboot the host) is obvious. The underlying blank on such hosts is a host driver state, not a Heimdall bug. - Apple Silicon
gpu.vramnow explains the dash. Unified memory has no discrete VRAM, sogpu.vramwas simply absent on Macs. It is now reported Unavailable withunified memory (no discrete VRAM)rather than silently omitted.
- Running the helper could blank out power/GPU it was meant to provide. The
privileged adapter trusted the helper's reply on any successful socket call —
even one with no
okmetrics — so a reachable-but-empty helper shadowed the daemon's own in-process reading. On Apple Silicon, where IOReport/SMC are unprivileged, bootstrapping the helper madepower.*andgpu.*disappear on a Mac that worked fine without it. The adapter now uses the helper only when it returns anokmetric, otherwise falls back to in-process collection — so the helper is additive, never subtractive. Also covers Windows hosts where the helper would otherwise shadow the daemon'snvidia-smiGPU read.
- Windows process table showed every process at 0% CPU and 0% memory. The
collector used
tasklist, which has no CPU column, and the parser also dropped its memory column — sopand the top view listed real processes with fake zeros. Windows now readsWin32_PerfFormattedData_PerfProc_Processfor real instantaneousPercentProcessorTimeand working-set memory (as a percent of total RAM), falling back totasklist(pid + name only) when that query isn't available. macOS/Linux (ps) are unchanged.
- First-run wizard stored
process-intervalas0sso the top view (t) process table was empty out of the box — the daemon never pushed a table. The wizard now prompts for it and suggests2s, storing what you accept; the runtime/flag default stays0s(off) for headless services, so the ADR 0017 opt-in posture is unchanged. Existing daemons started with--process-intervalare unaffected. Set0in the wizard to keep it off. - systemd
ExecStartpaths in the privileged-metrics guide pointed at/usr/bin; the manual install dir is/usr/local/bin. Fixed, with a note that AUR installs use/usr/bin.
- AMD GPU metrics —
gpu.util,gpu.vram,gpu.temp, andpower.gpunow work on AMD hardware (discrete Radeon and the Strix Halo iGPU, e.g. an HP ZBook Ultra G1a). GPU collection wasnvidia-smi-only, so AMD hosts showed a blank accelerator panel. The daemon now prefersamd-smi(ROCm) and falls back to the in-treeamdgpusysfs nodes (gpu_busy_percent,mem_info_vram_*, hwmonpower1_average/temp1_input) — both unprivileged, so it works out of the box with no extra install.npu.utilis advertised asunavailableon XDNA until theamdxdnadriver exposes a utilisation counter. - Richer GPU panel —
gpu.clock(graphics clock),gpu.mem.util(memory- controller utilisation), andgpu.fan(fan speed) on both NVIDIA (nvidia-smi) and AMD (amd-smi/ amdgpu hwmonfreq1_input). The top-view GPU panel now shows clock / mem / fan; anything the chip doesn't expose renders—rather than a fabricated value. - Per-platform privileged-metrics guides — split the one big guide into
macOS,
Linux + NVIDIA,
Linux + AMD, and
Windows (with
amd-smiinstall steps and Windows service setup). The original stays as the cross-platform overview.
- Full-screen
topview (Hliðskjálf) — presston a focused host for a mactop/btop-style single-host dashboard: per-core CPU bars, braille sparklines for CPU/memory/power, GPU/NPU, network & disk, and a process table. It refreshes live and works on macOS, Linux, and Windows. Responsive across four width tiers down to an iPhone-portrait "key numbers" column, so it stays readable in Termius on a phone.esc/qexits. mem.swap,cpu.load, andcpu.freqcollectors — swap usage, 1/5/15m load average, and CPU clock. Each degrades to Unavailable (rendered—) where the platform can't supply it (e.g. load average on Windows, clock on Apple Silicon) rather than reporting a fabricated 0.
t/pkeys — the process table now opens onp("processes");tis the new full-screen top view.- ANE → NPU — accelerator power is now the vendor-neutral
power.npu. The legacypower.anekey is accepted and normalised on ingest, so a mixed fleet with older daemons keeps working.
- macOS power read 0 W on Apple Silicon Pro/Max (e.g. M3 Max). Those chips
report 0 for the IOReport per-domain CPU/ANE energy counters and only a
sub-watt GPU figure, so the package total collapsed to ~0 W — while base
M-series Macs (which do populate the counters) were fine. The helper now reads
the SMC
PSTR("System Total Power") rail — the same unprivileged source mactop/btop use — as the package figure on macOS. Source precedence is now SMC → powermetrics → IOReport sum, so a phantom sub-watt IOReport total can no longer shadow a real reading. Linux (RAPL/hwmon) and Windows (WMI) paths are unchanged; the SMC reader is darwin/cgo-only with a no-op stub elsewhere.
2.1.0 - 2026-06-29
Quality-of-life follow-up to v2.0.0: ephemeral runs that never touch your config, and broader agent/CLI onboarding. No wire or behavior changes to the transport. Full notes: docs/releases/v2.1.0.md.
--no-save/--ephemeralon every binary — run with the given flags but leave the config file untouched, so a one-off (e.g.heimdall-hub --listen :19090 --no-save) never sticks as the new default.- Agent harness snippets for Copilot and other harnesses in the
heimdall-cliguide — a.github/copilot-instructions.mdblock, plus a portable CLI wrapper and an OpenAI-style tool schema for Hermes / OpenAI-compatible / custom harnesses (alongside the existing Claude Code AGENT/SKILL/COMMAND files).
- Run-as-a-service guide — the Privileged Metrics guide now has a complete
systemd setup (helper as root, daemon as your user) using a shared
heimdallgroup and a/run/heimdallsocket, matching the v20660helper socket. The fleet guide cross-links it. - Real agent-session transcripts in the
heimdall-cliguide — three uneditedclaude -psessions (free-form, the/fleetcommand, a targeted risk question) showing what fleet Q&A looks like in practice, plus a "Scriptable & agent-friendly" bullet in the README.
2.0.0 - 2026-06-29
The everything socket release. On-demand interaction across the fleet with no inbound port on any host. Full notes: docs/releases/v2.0.0.md.
The release that brings on-demand interaction back to the fleet without giving any daemon an inbound port. Daemons stay outbound-only; the hub remains the sole listener and mediates every directive over the daemon's existing stream.
- Hub-mediated socket transport (ADR 0018). The daemon's outbound metric
stream is reused as a bidirectional control channel, so the hub can push
directives down to a daemon that never listens:
- Demand-driven push (Phase 1). The hub opens a log/process window on a host only while a dashboard or CLI is subscribed, and closes it on the last unsubscribe — daemons push observability data on demand instead of always-on.
- On-demand commands (Phase 2). A dashboard or the CLI asks the hub to run an allow-listed, read-only command on a host; the hub routes it down the host's stream, the daemon runs it as its unprivileged user, and the result returns correlated by request id. Nothing arbitrary is ever executed.
- Helper-delegated privileged commands (Phase 2b). Commands that need root
(e.g.
dmesg,journal.tail) are delegated by the daemon to the local privileged helper over a unix socket; the helper enforces its own allow-list and never trusts the daemon. The daemon itself stays least- privilege.
heimdall-cli— a first-class, agent-friendly binary. Machine-readable JSON forfleet,hosts,host,top,logs, andrun, built for scripts, CI/CD, and AI harnesses.--hub autodiscovers the hub via zeroconf and, when more than one hub is present, reports them and instructs the operator to pick one with--hub <name>. Shipped with a programmatic/agent guide (bash parsing, a CI/CD GitHub Action that waits for a host to come online, Datadog log piping, and copy-paste AGENT/SKILL/COMMAND files).- On-demand command modal in the dashboard (
c). From a host's detail view, presscto run an allow-listed command and read its result inline. The affordance appears only for hosts that advertise the_cmdcapability — the same capability-gated model as logs (_logs) and top (_proc). - Log search and top sorting (ADR 0019). Inside the log modal,
/filters the buffered and live lines to case-insensitive substring matches, keeping timestamps and scoped to the modal. In the top modal, the process table sorts CPU-descending by default;sopens a sort picker, and the choice re-sorts live and persists to the dashboard config as the new default. - Zeroconf multi-hub discovery (Ratatoskr). When more than one hub is advertised, the dashboard shows a picker and the CLI reports the candidates; with a single hub, both connect transparently.
- Manpages for every binary (roff
.1+ plain text), generated from each binary's--help. - Real-time online → offline on disconnect. The hub now acts on a daemon's
stream ending: it flips the host Offline immediately and pushes a
disconnectedsnapshot to subscribers, so the dashboard reflects the change at once instead of waiting out the freshness window. This covers any detectable socket end — the daemon's cleanCloseSendon SIGTERM and an abrupt process death (the OS closes the fd either way). The timeout path is retained as the fallback for disconnects the hub can't observe (SIGKILL with a frozen network, power loss, partition). Additive wire fieldSnapshot.disconnected = 13; old subscribers ignore it and fall back to the timeout. - Socket-hygiene verification. A
socket-hygiene.featureacceptance suite proves the model against the running processes' real sockets (viass): a daemon listens on nothing (no inbound surface), the hub is the sole listener, the daemon holds exactly one outbound connection — to the hub — and an on-demand command opens no new socket (it rides the existing stream).scripts/ verify-sockets.shruns the same audit on a live host. - Documentation screenshot tooling (
make screenshots). The--snapshotpath now honoursCOLUMNS/LINES, so the generator captures the dashboard at a matrix of sizes/views/themes headlessly — including the wide vs. narrow grid that shows the responsive column drop — as ANSI (always), styled HTML (aha), and animated GIFs of the modal flows (vhstapes). - Capability gating. Daemons opt in to what they expose —
--log-sourceadvertises_logs,--process-intervaladvertises_proc, and--allow-commandsadvertises_cmd. Reserved_-prefixed labels are filtered from user tags and grouping. A host shows an affordance only for what it advertises.
- The helper protocol is now request-based (
collect|exec) with backward compatibility: a silent old client falls back tocollectafter a short read deadline.
StreamControlgainsObservabilityWindowandControlRequestdirectives;Snapshotgains additiveprocesses,processes_at,log_lines, andcommand_resultfields;FederationService.RunCommandreturns aCommandAck. All additive — old daemons/hubs ignore the new fields, so there is no lockstep upgrade.
1.6.0 - 2026-06-29
The Heimdallr's sight release — host logs and a live process view, inside the dashboard, with no inbound port on any daemon.
- In-dashboard logs (
l) and top (t). From a host's detail view, presslto pick a log source and stream it live (scrollable), ortfor a scrollable, refreshing process table.escis the universal back button, unwinding one level at a time. The affordances appear only for hosts that expose the capability. Explorable in--demo. - Push-based, hub-mediated observability (ADR 0017). The daemon — a pure
outbound producer — tails
--log-sourcefiles and collects a process table on--process-interval, pushing both to the hub on its existing stream. The hub buffers them per host and serves them to dashboards. Nothing connects to a daemon; the dashboard talks only to the hub. Cross-platform process collection (pson Linux/macOS,taskliston Windows). - Wire: additive
Snapshot.{processes, processes_at, log_lines}and aProcessRowmessage. Old daemons/hubs ignore them — no lockstep upgrade.
- BREAKING — the daemon no longer acts as a server. Daemons are outbound-only
and must not listen (only hubs do), so the direct daemon-served control plane is
removed:
- daemon flags
--control-listen,--control-token,--control-tls-cert,--control-tls-key(use--process-interval/--log-sourceto push); - dashboard flags
--control,--run,--tail(usel/tin the TUI). --log-sourceis kept, but now configures what the daemon pushes rather than a served stream. On-demand command execution returns with the v2 socket model (feature/sockets); see ADR 0017 §3.9.
- daemon flags
1.5.2 - 2026-06-29
- Dashboard grid clipped on narrow terminals. The fleet grid forced content to ≥ 88 columns and rendered fixed-width columns, so on a narrow screen (portrait tablet/phone over SSH) rows were clipped at the right edge and the chrome borders fell off-screen. The grid is responsive now: it renders at the actual terminal width, drops metric columns right-to-left as it narrows (power → gpu → temp → disk → mem, keeping host, state, and CPU longest), condenses the state badge to a glyph when very narrow, and falls back to a glyph-only footer. No rendered line exceeds the terminal width. Columns are a registry (mirroring the grouping dimensions), so adding one is registering a column, not editing a width conditional.
1.5.1 - 2026-06-28
- Dashboard overflowed short terminals. The fleet grid rendered every row
with no height clamp, so on a screen shorter than the fleet (SSH from a tablet
or phone) the frame overran the terminal — the header scrolled off and
ungrouped filtering looked inert because the visible rows never moved. The grid
now windows host rows to the terminal height, keeps the selected host in view,
and shows an
↑/↓ N moreindicator for hidden rows. Filtering is visibly effective now regardless of grouping or screen size.
- Term-scoped fleet filter.
/parses space-separated terms. A bare term (ba) matches any field — host name, tag value, hub, OS, or state — so it surfaces both hosts namedbar/bazand hosts taggedenv=bar. A scoped term narrows to one field:host=ba,env=fo,hub=home,os=linux,state=offline, orgroup=<value>(the active grouping dimension). Multiple terms narrow conjunctively; an empty filter shows everything. Searchable fields are a strategy registry mirroring the grouping dimensions, so adding a scope is registering a matcher, not editing a conditional.
1.5.0 - 2026-06-27
- Arch Linux packages on the AUR. The release workflow now publishes each
binary as a prebuilt (
-bin) AUR package, so Arch users install withparu -S heimdall-dashboard-bin(andheimdall-hub-bin,heimdall-daemon-bin,heimdall-helper-bin). PKGBUILDs are generated per release from the published checksums (packaging/aur/gen-pkgbuild.sh); the publish step is gated behind theENABLE_AURrepo variable. Packages coverx86_64andaarch64, and the daemon declares the helper as an optional dependency.
1.4.0 - 2026-06-27
- Mímir durable sink + hub state recovery. Point the hub at any
Prometheus-compatible TSDB with
--tsdb <url>: it persists the fleet via Prometheus remote-write and restores its last-known state from the TSDB on restart — repainting the dashboard (including offline hosts, with their real last-seen age) before daemons reconnect, instead of starting blank. Restore is best-effort (scalar values + labels + last-seen; info strings and alert state reconverge live). Heimdall still embeds no database; the sink is off by default.
- Factored the metric→series mapping into
observe.SeriesOf, shared by the OpenMetrics export and the durable sink.
1.3.1 - 2026-06-27
- Dashboard group by OS showed every host as
(unknown)on real data. The dashboard only receives OS as thehost.osmetric (not inContext, which never crosses the wire), so grouping now reads the metric's OS family and falls back toContext.OS(how--democarries it).
1.3.0 - 2026-06-27
- Yggdrasil — interactive dashboard grouping. Group the fleet by origin hub,
OS, or any tag with
g, and filter/search by host name or tag with/. Works live and in--demo, with per-group section headers. - Gjallarhorn — alerts in the dashboard. Firing alerts now surface as a per-row
⚠badge (the host turns red) and a fleet alert count in the header — not just webhook + log. - Windows privileged metrics. The helper reports CPU/zone temperature via WMI
(
MSAcpi_ThermalZoneTemperature);power.pkgstays unavailable on Windows (RAPL needs a kernel driver). Build-tagged, parser unit-tested. - Richer demo mode. The simulated fleet now carries tags, spans three origin
hubs (
home,remote-work-station,central), and runs one host hot enough to fire an alert — so grouping, filtering, and the badge are all explorable with no setup.
- The hub now sends dashboards registry-derived enriched snapshots (merged
labels + alert state) on every update, so live frames match the initial state —
the origin
hublabel and inherited tags no longer vanish between updates. Snapshotgains an additivealertsfield (wire-compatible; old peers ignore it).
1.2.2 - 2026-06-27
- The dashboard can auto-discover its hub over mDNS:
heimdall-dashboard --hub auto(or--discover, with--discover-seedfor overlay networks) — the same Ratatoskr discovery the daemon uses. Discovery only resolves the address; the enrollment token and TLS still gate the connection.
1.2.1 - 2026-06-27
<binary> updatenow elevates when the install directory needs root —sudoon Linux/macOS, a UAC prompt on Windows — instead of failing with a permission error in a system path like/usr/local/bin. Affects all four binaries.
- New guides: Metrics export (Mímir) — scrape Heimdall from Prometheus/Grafana — and Alerting (Gjallarhorn).
- Fleet, privileged-metrics, and federation guides updated for discovery (Ratatoskr), tags (Realms), and Linux power/thermal.
- README: clearer "what & why", v1.2.0 capabilities, a Contributing section, and a changelog link.
1.2.0 - 2026-06-27
The Watch Over All Realms release — discovery, tags, alerting, export, and cross-platform power/thermal. Feature codenames are Norse; see the glossary for what they mean and how to say them.
- Ratatoskr — zeroconf discovery. The hub advertises over mDNS
(
--discoverable) and a daemon finds it with--hub autoor--discover(--discover-seedcovers overlay networks with no multicast). Discovery only resolves the address — the enrollment token and TLS still gate trust, and an explicit--hubalways wins. - Realms — host & hub tags.
heimdall-daemon --tags env=prod,role=dbandheimdall-hub --tags region=apac. Tags ride the stream and inherit across the Bifröst relay; a host's own tag wins over an inherited hub tag. - Mímir — Prometheus/OpenMetrics export + history.
heimdall-hub --metrics-listen :9091serves/metrics(the whole fleet, labeled by host, origin hub, and tags) and/history, backed by a bounded in-memory ring (lost on restart, by design). - Gjallarhorn — alerting.
heimdall-hub --alert-rules rules.jsonevaluates threshold rules (e.g.cpu.util > 90 for 5m) with for-duration hysteresis so spikes don't flap, scoped by tag, and POSTs to--alert-webhookon fire and clear. - Yggdrasil — topology grouping. Each host's Bifröst origin hub is surfaced
as a
hublabel and the hub stores enrolled OS/arch context, so the fleet is groupable by origin hub, OS, and tags in Prometheus/Grafana. - Cross-platform helper parity. The privileged helper reads CPU package
power from RAPL and temperatures from hwmon on Linux, so non-Mac hosts get
power.pkg/temp.pkglike Apple Silicon.
Snapshotgains an additivelabelsfield so tags inherit across the relay (wire-compatible — old peers ignore it).
- ADRs 0009–0012 (Ratatoskr, Realms & Yggdrasil, Mímir, Gjallarhorn), v1.2.0 Gherkin stories 0012–0017, and a codename pronunciation glossary.
- Interactive in-dashboard grouping/filtering (the TUI side of Yggdrasil) is a follow-up; the grouping data is available via labels and export today.
1.1.1 - 2026-06-27
- Add the missing
scripts/gen-dev-certs.shthatmake dev-certsand the TLS acceptance test call. It writes a self-signedhub.crt/hub.key(SANs forlocalhostand127.0.0.1) usable as both the hub server cert and the client CA bundle. - Make the acceptance suite hermetic: each scenario gets its own
HEIMDALL_CONFIG_DIRand each simulated host its own config dir. Scenarios were leaking saved daemon settings (a stalecontrol-listen) into each other, so co-located daemons collided on a port and only one host came online.
- Replace the
switch m.Unitin the daemon print formatter with a per-unit strategy table (no behaviour change). - Bump GitHub Actions off the deprecated Node 20 runtime: checkout v7, setup-go v6, setup-node v6, upload-artifact v7, download-artifact v8, gh-release v3.
1.1.0 - 2026-06-27
First release with a consolidated changelog. Rolls up the v1.0.x line and fixes a print-mode rendering bug in the host inventory metrics.
--printand--jsonmodes rendered host inventory info metrics (host.os,host.cpu,host.gpu,host.kernel,host.arch,host.version) as=0. These carry their value as a string inDetailwith no gauge, and the formatter fell through to the numeric default. Print mode now shows the string (e.g.host.os=darwin 27.0); JSON output carries adetailfield.
- Document the layered config system and first-run wizard.
- Document
--purge-after,--install-location, and disk I/O metrics. - Add
--versionand--purge-afterto the configuration reference.
1.0.7 - 2026-06-27
- Hub now shuts down on SIGTERM whether or not a dashboard is connected.
1.0.6 - 2026-06-27
- Hub, dashboard, and helper resolve settings through the layered options system.
- Daemon resolves settings through the options system with a first-run wizard.
- Layered config resolution (flags > env > file > defaults) with a first-run wizard.
- Departed hosts are purged to bound memory under churn.
- Credit Maple in the README.
1.0.5 - 2026-06-27
- Per-binary installers; system bin as the default install location;
--install-location. - Self-update via
<binary> update.
1.0.4 - 2026-06-27
- Per-NIC network breakdown.
--versionon every binary.- Dashboard shows used/total absolutes for memory, disk, and VRAM.
- Status bar reports the true connection state instead of an optimistic guess.
- Add the dashboard demo gif to the README.
1.0.3 - 2026-06-27
- Host inventory (OS, CPU, GPU, kernel, arch, version) in the detail view.
- Split the adapters package into one adapter per file.
- macOS release binaries build with CGO so IOReport-backed metrics work.
1.0.2 - 2026-06-27
- Disk read/write throughput.
- Dashboard surfaces GPU power, temperature, and VRAM.
- Detail sparklines are capped to their column width.
- Drop maintainer release and brand notes from the README.
1.0.1 - 2026-06-27
- Hosts age from the hub snapshot timestamp, not dashboard wall-clock time.
- Release workflow publishes on the release event with idempotent asset upload.
1.0.0 - 2026-06-27
Initial release. A fleet metrics daemon, an aggregating hub, and a terminal dashboard, streaming over mTLS gRPC.
- Daemon: collects host metrics (CPU, memory, disk, network, temperature, power,
GPU) and streams them to a hub, or prints them locally (
--print,--once,--json). - Hub: aggregates snapshots from many daemons and fans out to dashboards.
- Dashboard: terminal UI with a fleet grid and per-host detail drilldown.
- Optional privileged helper for metrics that need elevated access.
- Demo mode with a synthetic fleet.
- Install script, release script, and release workflow.
- Modality start guides and reference docs.