Changelog

All notable changes to Irrlicht, following Keep a Changelog conventions.

v0.6.1 (2026-08-26)

A session that fails now reads red — error is the fourth session state, and it reaches the menu bar, the dashboard and the wire

Highlights

A failed session turns red instead of green

A macOS session row and three web dashboard rows, each showing a red error state with the provider's own message

Until now a session whose provider call failed, whose credentials were rejected, or whose process died mid-turn kept reading ready — green, finished, nothing to see. Irrlicht now carries a fourth state, error, and shows the agent’s own failure text under the session: the HTTP status, the provider’s message, and where the retry ladder stands.

Why it matters: the one session you most need to look at was the one Irrlicht told you to ignore. A red row and a verbatim provider message tell you whether to wait for the retry or go fix a credential. (#1796, #1798, #1799, #1800, #1801, #1802)

Irrlicht now reports its own faults

The macOS panel with a red banner reading "Irrlicht has a problem — the Irrlicht daemon is not responding"

A fault in Irrlicht’s own machinery has no session to attach to, so it used to be visible only inside a --diagnose tarball. Two diagnoses the daemon already computed — hook entries that went missing, and a hook channel that is installed but receiving nothing — now surface as a banner in the macOS panel and the web dashboard.

Why it matters: a silently degraded daemon and a genuinely quiet machine used to look identical. The banner says plainly when Irrlicht cannot vouch for what it is showing. (#1801, #1802, #1372, #1368)

Added

  • The error domain state and its detail payload (#1798, #1809) — phase (retrying / terminal / unknown), class, message, HTTP status and the three retry counters, every number optional because the recorded payloads disagree about which ones exist.
  • claude-code and GitHub Copilot report their API and session errors (#1799, #1811) — the retry ladder and the give-up message for claude-code, both recorded session.error shapes for Copilot, driven entirely off recordings already in the corpus.
  • gemini-cli, opencode, codex, pi and aider report errors too (#1800, #1813) — five real defects, one per adapter; pi’s errored turn used to stick in working for the rest of the session’s life.
  • Process death is detected as an error, not as a clean finish (#1800, #1813) — an agent that dies mid-turn is kept and turned red instead of being deleted or left waiting forever on an open tool call.
  • error on the wire (#1801, #1812) — errorCount on /state and the diagnostics bundle, an error bucket in the subagent summary, and by_state derived from the canonical vocabulary instead of a three-key literal typed in three places.
  • The macOS Activity Matrix carries the error series (#1821, #1831) — the chart, tooltip, accessibility text and CSV export all gained the fourth column; the other three segments were being normalized against a total that excluded it.
  • Four onboarding-factory error scenarios (#1803, #1819) — provider-overloaded-retry, provider-overloaded-terminal, auth-credentials-rejected and agent-process-crash-midturn, so a declaration replaces a hand-written cell in 40 of 44 adapter/scenario pairs.
  • Model alias sync: three new frontend model IDs from codeburn (orcarouter/fusion, orcarouter/fusion-flash, orcarouter/fusion-mini), and claude-4-sonnet-thinking repointed to the generation its slug actually names — so those sessions stop pricing as the wrong model.

Fixed

  • An errored session stays red across a daemon restart (#1815, #1833) — a release, a reboot or a rebuild used to reset it to green, or delete the row outright. Five separate reapers could take an errored row; a static tripwire now fails any new one that does not consult the exemption.
  • Upgrading to this release heals a session that failed under v0.6.0 (#1843) — the ledger schema version is bumped, so the transcript is re-scanned and the failure re-derived instead of the session sitting green over bytes the daemon will never re-read.
  • The history strip no longer paints an unknown state green — or overwrites it on disk (#1807, #1832) — an unencodable state used to be coerced to ready and written back to history.json every 60 ticks, destroying the original value.
  • An unrecognized session state is tolerated rather than deleted or painted green (#1797, #1806).
  • claude-code’s terminal API error carries its real HTTP status and class (#1818, #1834) — 2.1.245 writes both as structured fields; the adapter read neither, hardcoding a generic bucket and a nil status.
  • Replay reconstructs a mid-turn process death instead of replaying it green (#1817, #1835) and the crash fixture was re-recorded so it stops asserting the old behaviour (#1836, #1842).
  • A SIGTERM landing right after the daemon publishes its address file no longer leaves a stale file behind (#1808, #1830) — the root cause of an intermittent Linux CI failure.
  • Recording drivers observe teardown instead of asserting it (#1825, #1827) — every exit_clean run used to leak a live agent process and its tmux session; nine leaked claude processes in one morning showed up as real sessions in the menu bar.

Changed / Docs

  • The “three states only” claim is retired repo-wide, with a tripwire (#1804, #1820) — a static check now flags any line naming three-or-more-but-not-all of the canonical states, so the fifth state does not have to be found by review one defect at a time.
  • tools/mutate.sh names the confirmed cause of a byte-parity refusal instead of guessing between two (#1816, #1838).
  • Three verification steps that could silently not run now fail loudly (#1823, #1839) — plus full go vet (and therefore copylocks) as two new preflight gates.
  • ir:exec spells out the checkpoint-and-restore idiom (#1824, #1840) — four agents had each independently discarded uncommitted work restoring a mutation against ambient HEAD.
  • ir:exec ticks the parent epic’s checklist on close, and ir:triage derives milestones from version.json rather than from the latest published tag, which could not express a patch release inside the current cycle (#1841).

Known limitation

  • The history strip stays three-state: its wire format is 2 bits per bucket and all four codes are taken, so an errored bucket renders blank rather than red. Tracked in #1805.

Technical appendix

  • The state. StateError plus CanonicalStates() as the single declaration of the vocabulary; IsCanonicalState and the filesystem repository’s unrecognized-state warning both derive from it instead of carrying a hand-typed three-way format string. (#1798, #1809)
  • The detail type. session.SessionError carries Phase, Class, Message and four optional numeric fields. Terminal-ness is the adapter’s verdict and never Attempt == MaxAttempts — across all 16 recorded api_error events the counter runs 1..6 of 10 and never reaches the ceiling. RetryIn serializes as retry_in_ms, a fractional number, rather than as bare unlabelled nanoseconds. (#1798, #1809)
  • The pipe. The fold lives in applyMetadata, the one function both of the tailer’s mutually exclusive routing paths call, so a Skip=true api_error and an ordinary session.error are both seen. Surfaced from surfaceSporadicMetrics above computeMetrics’ early return, since a transcript of only skipped error events has an empty message history. Added to newMergedMetrics’ allowlist and deliberately not to any carry-forward helper, which would make the error unclearable. (#1798, #1809)
  • The rule. SignalSessionError sits at TierTranscript with its own policy row and ceiling, immediately above agent_done, because a terminal failure leaves a transcript tail that reads as a finished turn. SignalProcessDeath gets its own SignalKind and its own row above the transcript-tier waiting rules — a dead process leaves an open tool call open forever, so those rules keep firing and keep painting a dead session waiting; placement, not the tier value, was the hard part. (#1798, #1800, #1809, #1813)
  • A Stop hook ends a turn, it does not succeed it. Both producers emit their failure inside a turn that then ends normally — claude-code writes system/turn_duration on the next line, Copilot writes assistant.turn_end about 6ms before session.error. So the clearing rule is one shared predicate, SessionError.ClearedByTurnBoundary, applied identically by the hook arm and the transcript arm; and a SessionError still standing when classifyAgentDone runs routes to StateError. ErrorPhaseUnknown clears like terminal, not like retrying: an absence of information about a further attempt is not evidence of recovery. (#1799, #1811)
  • isApiErrorMessage is matched on the flag’s value, never its presence. claude-code writes the field on assistant messages whether or not anything failed — 4 of the 6 occurrences in replaydata are false, all user interrupts. The committed ESC fixture is the defect test and goes red against a presence-matching implementation. (#1799, #1811)
  • IsError is never a session error. That channel means a tool result failed and must not turn a session red; gemini-cli was the one site in the tree conflating the two. (#1798, #1800, #1813)
  • The is-active partition, settled. Seven sites re-derived “is this session active” inline while asking two genuinely different questions. SessionState.HasWorkInFlight (“is more work coming?”, where an errored-but-retrying session counts) is now distinct from concurrencyActive (“does this occupy a slot?”, where it never does). Only hasActiveChildren changed behaviour, and it was the site whose answer was wrong: a child mid-retry answered “not active” and released its parent to green while the subagent sat red. The web client’s two inline copies collapsed into hasWorkInFlight, with a test that reads the Go source and asserts both rules name the same states. (#1801, #1812)
  • Colour is defined three times, and the light value is not a copy. The dark #FF3B30 measures 3.01:1 against its own wash on white; #CC0C00 is the same hue at L=40%, 4.69:1, the same band as --ready. Figures come from platforms/web/snapshots/contrast.mjs and are re-asserted in the suite rather than typed into a comment once. The macOS and web reds are pinned to each other by a test that extracts IrrHex.error from Tokens.swift. (#1801, #1802, #1810, #1812)
  • The error line is keyed off the payload, not off state === 'error' — the daemon clears the payload on recovery while the state has already moved on — and every numeric field is checked for null rather than falsiness, so a transcript that said nothing never renders “attempt 0 of 0”. (#1801, #1812)
  • The banner is role="alert", deliberately unlike the unapplied-grants banner’s role="status". One reports a standing condition while the sessions around it are still correct; this one says the dashboard cannot vouch for what it is showing. TurnsSinceReceipt was removed from the fault payload because it climbed on every turn boundary, changing the banner key and re-announcing to a screen reader roughly every 30 seconds forever. Known gap, stated in the struct doc: irrlichtrelay rebuilds the payload from its own cache and carries no daemon health, so a relay-connected dashboard does not see these. (#1801, #1802, #1810, #1812)
  • macOS decoded the error from a key the daemon does not emit. SessionError read a top-level error; the daemon writes metrics.session_error. Every error would have rendered the bare “The session failed” fallback, silently, forever, while looking like working code. SessionState.error is now a passthrough to metrics.sessionError, and testDaemonGoldenPayloadDecodes decodes the daemon’s own committed golden payload rather than hand-written JSON. (#1802, #1810)
  • Restart survival needed four mechanisms plus a fifth found by /simplify. LedgerState.SessionError persists the sticky failure; retainedErrorAcrossRestart exempts an errored row from three startup deleters; seedRetainRestoredError re-registers a retention entry so the row also survives the periodic liveness sweep, which the startup exemptions cannot reach; seedRestoreErrorVerdict re-places the process-death hold. The fifth deleter — sweepStaleSnapshot’s pid == 0 branch, firing after the 30-minute ready TTL, well inside the 12 hours an errored row is promised — was found by execution, and the fix is structural: PIDManager.retainsError is the one predicate every reaper consults, and TestEveryReaperConsultsTheErrorExemption walks the AST and fails any deleting function that neither consults it nor sits on a documented exempt list. Both the deletion relation and the delegation relation are derived from the source to a fixed point, not typed. (#1815, #1833)
  • Retention was behind the consent gate. The seeding ran only for rows whose adapter’s observe permission was granted — which on a fresh IRRLICHT_HOME is no adapter at all, so the user most likely to hit it was the one who had not finished onboarding. Replaced by a seed-level pass over every persisted row, using the same predicate as the startup exemption. (#1815, #1833)
  • The ledger schema bump, and the rule that decides one. Additive is not the discriminator — versions 4 and 5 were both additive omitempty fields and both bumped. The test, now written on the constant, is whether the current parser would read bytes an existing ledger has already consumed differently than the parser that wrote them. TestLedgerState_FieldSetChangeRequiresASchemaDecision pins the field set and the literal version, so the question is asked rather than skipped. (#1843)
  • history.json’s coercion. statePriority()’s default arm mapped every state without a 2-bit code onto statePriorityReady, so the bucket painted green and save() rewrote the file with the coerced value. Unencodable states now carry a negative in-memory sentinel encoding to the existing no-data wire code, and the verbatim string is kept so snapshot() writes back exactly what was read. The wire format is untouched: still 60 buckets × 2 bits = 15 bytes = 20 base64 characters. (#1807, #1832)
  • Replay. case timelineProcessExit hard-codes ready but is unreachable for a mid-turn death, so the crash recording carried no process event at all. Routing that branch through the predicate was measured and rejected: it left the crash fixture untouched and made other goldens fabricate a working→error their own daemon never produced. The observation gets its own lifecycle.KindProcessDiedMidTurn, gated on a test-and-set of the verdict registry because the two exit paths race. Both hold-release edges are mirrored; without the teardown one, a recording carrying death-then-teardown pinned the next lifetime to error. (#1817, #1835)
  • The re-record. A recording is frozen, so only a new one clears a golden. The new claude-code 2-25 recording was captured on a daemon built at/after the fix, coexisting on 127.0.0.1:7842 under its own /tmp IRRLICHT_HOME; its golden now reads 2 recorded transitions, 2 replayed, 2 ordered matches, no mismatches and no missing kinds. Census moved by exactly one figure: recordings +1. (#1836, #1842)
  • Driver teardown. Claude Code answers one Ctrl-D with “press Ctrl-D again” and lets the confirmation expire, so step_exit_clean’s single press plus a one-second sleep observed nothing. Death is now an observation at four levels: a strict require_tmux_session_gone, an unconditional trap … EXIT per driver, a post-driver assertion in run-cell.sh built on the gone/survived/could-not-look contract, and a Go tripwire grading four invariants over every driver enumerated from disk. A guard was added against the false success the fix would otherwise create: a trap writing the exit reason unconditionally would turn “never formed a verdict” into ok. (#1825, #1827)
  • Verification that cannot run must not look like verification that found nothing. /simplify’s four review angles must be confirmed considered; the preflight chunk list was missing swift and bash, so an agent following only the skill skipped the whole macOS gate on a platforms/macos/ change; and go test runs only vet’s small high-confidence subset, which excludes copylocks. Each is a check this change adds, so each carries a committed mutation fixture that breaks the thing it protects and confirms the check goes red. (#1823, #1839)
  • mutate.sh’s NUL refusal. The byte-parity guard reads the whole file as one awk record; macOS’s system awk represents that record as a NUL-terminated C string, so length() stops at the first embedded NUL — confirmed against platforms/web/irrlicht.js (117644 bytes, one NUL at offset 67966; the guard measured exactly 67966) and ruled out as a buffer ceiling. The guard now reads the stopping byte via dd/od, never through awk, and names the confirmed cause with its offset. (#1816, #1838)

v0.6.0 (2026-08-24)

The hooks epic closes: every adapter that can observe an approval prompt or a turn boundary now does, live — kiro-cli, gemini-cli, mistral-vibe, opencode, pi, hermes, antigravity and copilot’s http tier all shipped this cycle, on top of months of safety hardening to the channel underneath them

Epic #1355 set out to make hooks the tier-1 state channel wherever an agent has one, and to make adding a new adapter a declaration rather than a rewrite. Both halves are done. The safety half — permission-gated installs, mandatory transcript-path confinement, per-adapter version floors, a liveness watchdog, wall-clock ceilings on hook-asserted holds, entry re-verification, registry-driven uninstall, consent folded into the decode path so a receiver cannot forget it — shipped first and gated everything after it. The rollout half then closed adapter by adapter: eight more CLIs now push state instead of irrlicht guessing it from transcript prose, across four different delivery shapes (JSON config splice, comment-preserving TOML, an installed plugin/extension, and a shared shell-store). aider was audited and deliberately left unchanged — its notifier signal doesn’t fire on ~30% of real turns and never in headless mode, so it can’t replace the existing heuristic; that’s a recorded negative result, not a gap. gemini-cli is scheduled for deprecation in v0.7 as no longer independently verifiable, so its hooks are the tier it ships with, not the start of further investment.

Added

  • kiro-cli now uses hook-based state detectionpostToolUse and stop events, beacon delivery, a flat entry shape (no matcher-group wrapper) — the cheapest real test of whether the hook machinery generalizes past the first three adapters. (#1716, #1732)
  • gemini-cli now uses hook-based state detection (Notification/AfterTool/AfterAgent, beacon delivery) — the strongest pilot on the evidence, since upstream ships its own Claude-Code-to-Gemini hook migration command. (#1717, #1724)
  • mistral-vibe now uses hook-based turn-done detection (post_agent_turn) — the first non-JSON hook format, needing a new comment-preserving TOML writer (hooktoml) so installing a hook doesn’t destroy the user’s config layout. (#1718, #1733, #1734)
  • opencode now observes turn state through a plugin irrlicht installs, since opencode removed its config-file hook path upstream between versions — a shipped-artifact route rather than a config write. (#1719, #1763)
  • pi now observes turn and tool events (turn_start/turn_end, agent_settled, tool_execution_*) through its own first-class extension system — the epic’s earlier “permanent tier-5, no approval prompt exists” verdict had stopped one layer short of this. (#1721, #1758)
  • hermes now observes an approval state its own shared SQLite store cannot express, through its own shell hooks. (#1722, #1765)
  • antigravity now observes turn end through a hook merged into a config file irrlicht does not own — the shape most different from every prior adapter’s config-splice or installed-artifact route. (#1723, #1775)
  • GitHub Copilot CLI now has a hook-authoritative live-state tier via its native http hooks, on top of the transcript-based detection v0.5.10 shipped. (#1378, #1436)
  • kiro-cli’s copied default agent now refreshes on CLI version change — the grant-time snapshot no longer silently drifts from kiro_default across upgrades. (#1736, #1749)
  • irrlichd gained a hook-post beacon that reads the daemon’s port from its own address file at fire time instead of a port baked in at install — the delivery mechanism command-only agents (gemini-cli, mistral-vibe) need, and structurally immune to the #1178 stale-port class of bug. (#1373, #1412)
  • Installed hook entries are now re-verified and repaired behind consent, closing the gap where an agent’s own settings UI could silently delete or mangle what irrlicht installed. (#1372, #1422)
  • A permission that is granted but not actually in force is now visible without opening Settings — a status strip (macOS) and banner (dashboard) reading “N permissions are granted but not applied”, with every cause named. (#1385)
  • Click-to-focus works for sessions running in a herdr pane. (#1350)
  • of status --summary and of coverage --hooks give a one-line onboarding-maturity read per adapter instead of hand-inspecting replaydata/. (#1374)
  • A “core-12” scenario tier, alpha maturity level, and derived-unobservable cell state for the onboarding matrix. (#1414)
  • tools/mutate.sh, a shared mutation-testing harness — three separate issues had each independently reinvented the same anchor-count-and-replace shape before it existed as one committed tool. (#1750)
  • Model alias sync: 4 new frontend model IDs synced from codeburn (codex-auto-review, mimo-v2.5-pro, mimo-v2.5, GLM-5.3/glm-5.3) — sessions on those models price correctly instead of at $0.

Fixed — hook-channel safety hardening

  • Hook-asserted persistent holds now have a wall-clock ceiling, so a missed release can no longer pin a session at waiting forever. (#1360, #1376)
  • A per-adapter hook-liveness watchdog now detects “granted, installed, but nothing arriving” instead of the daemon staying silently blind. (#1368, #1413)
  • An unrecognized hook event is now counted and reported instead of an Info log nobody reads. (#1364, #1403)
  • Every hook adapter now declares a minimum upstream CLI version, refusing to install hooks a too-old CLI can’t parse safely. (#1365, #1393)
  • Every hook receiver now confines a caller-supplied transcript_path, made structurally unskippable via DecodeConfined plus an architecture rule rather than a convention someone could forget. (#1361, #1380, #1465)
  • Consent is now folded into the decode chokepoint itself, so a hook receiver cannot dispatch without naming the permission that gates it. (#1488)
  • AssertPermissionGated is now key-aware — a receiver gated on the wrong permission no longer passes the same contract test a correctly-gated one does. (#1475, #1489)
  • A failed consent-effect apply is now surfaced and retried instead of silently recorded as “granted” while nothing actually happened. (#1362, #1379)
  • A grant-all dev/test daemon can no longer rewrite your real agent configs outside its own IRRLICHT_HOME. (#1449, #1460)
  • hookjson installs now preserve comments, key order, and && in command strings — a read-modify-write previously reordered a hand-organized config and HTML-escaped shell operators on first install. (#1371, #1381)
  • --uninstall-hooks, the recording rig, and install/uninstall now drive their adapter lists off the agent registry instead of a hardcoded two-adapter list guaranteed to miss adapter #3. (#1357, #1375)
  • site/install.sh --uninstall and irrlichd --uninstall-task-eta now actually remove hook entries and CLAUDE.md blocks and signal a running daemon immediately, instead of leaving stale entries that silently reappear on next start. (#1416, #1421, #1425, #1437, #1458)
  • claudecode’s disclosed hook-consent count is now derived from installedHookEvents rather than hand-typed, so it cannot drift from what’s actually installed again. (#1356, #1370, #1409)
  • A mistyped irrlichd flag (e.g. --recrod) now fails loudly instead of silently starting a daemon with recording off. (#1417)
  • A codex hook-receiver bug and three separate hook-recording blockers are fixed, unblocking real hook-bearing recordings for that adapter. (#1443)

Fixed — other

  • The macOS .popover now reparents as a child window of its host panel, fixing a class of focus/ordering bugs. (#1745)
  • Ghostty tab selection now matches by working directory instead of guessing the frontmost tab. (#1470)
  • Session detection no longer births a session working on metrics that observed nothing. (#1464)
  • Every SessionDetector and PermissionService construction now routes through a single allocator, closing two classes of construction-order bugs. (#1444, #1459)
  • Consent is reloaded from the store when another process changes it, instead of a stale in-memory read winning. (#1429)
  • Mistral-vibe’s hook install no longer fails against a config the CLI itself wrote — hooktoml now models multi-line TOML arrays. (#1753, #1755)

Changed / Docs / Distribution

  • AGENTS.md split into a lean index plus topic-scoped docs/*.md files, keeping the force-injected root file under its own 400-line budget. (#1744)
  • gemini-cli is scheduled for deprecation in v0.7 — the adapter can no longer be independently verified on this account tier. (#1725)
  • CI badge publishing (ARS score, star-history chart) moved to gist-backed jobs instead of pushes main’s branch-protection rule can never accept. (#1654, #1602)
  • Dependencies: golang.org/x/tools, modernc.org/sqlite (twice), actions/upload-artifact bumped; no open Critical/High Dependabot or CodeQL alerts at release time.
  • A long tail of CI-gate, replay-fixture, and macOS Swift-test reliability hardening landed this cycle without a user-visible change: bounded git/tmux/herdr shellouts, deterministic snapshot fixtures (locale, timezone, scale, @AppStorage), hardened workflow badge/gist publishing, and the mutation/registry-tripwire test infrastructure (tools/mutate.sh, real-config-fixture registry, adapter-registry-derived completeness checks) that the adapter rollout above leaned on. See docs/ci-gates.md, docs/testing-contracts.md, and docs/swift-testing.md for the standing write-ups.

Technical appendix

  • core/pkg/atomicfile replaces 11 independent hand-rolled atomicWriteFile/AtomicWriteFile copies (9 adapters plus 2 exported package functions) with one implementation, migrated to the strictest behavior found across all 11 (os.CreateTemp over a fixed temp name, 0o600/0o700 modes). (#1761)
  • A shared single-file-artifact installer (core/adapters/inbound/agents/hookartifact) replaces pi’s and opencode’s ~250-line near-duplicate installers, carrying both adapters’ existing mutation tests forward as locks, including opencode’s foreign-file-clobber-on-uninstall protection. (#1764)
  • Six adapters’ hand-written hook-version-gate tests were deleted where the shared contracttesting.AssertHookVersionGate contract strictly dominates them, proven per-adapter by a floor-weakening mutation rather than by inspection. (#1762)
  • A registry tripwire now requires every hooks-installing adapter to declare a real captured config fixture, closing the blind spot where every adapter graded its own install against a config its own tests wrote — the exact gap that let mistral-vibe’s #1753 regression through undetected. (#1756)
  • hookcov now attributes hook_received events by cross-referencing sibling events for the same session, fixing miscounted coverage for adapters whose turn-done hook name collides with another adapter’s canonical wire name. (#1768)
  • The recording rig gained a promote-time hook-free check, a spawn-path unapplied_grants poll (closing a race where the daemon’s socket accepts connections before its permission-apply pass completes), and corrected coexist-mode adapter docs. (#1754)
  • The rig’s agent-home Source-variant tripwire is now existence-checked via an AST scan of core/domain/agent, instead of fataling only when some future adapter happens to exercise an unhandled variant. (#1767)
  • kiro-cli’s own SQLite store is now declared as an Advisory write — a third permission-declaration category for paths an adapter’s Apply writes to that irrlicht doesn’t own and can’t safely back up/restore. (#1748)

v0.5.10 (2026-08-09)

Two more agents monitored — GitHub Copilot CLI and Hermes — herdr joins tmux and kitty as a controllable terminal, and live state now comes from the agent’s own hooks instead of prose heuristics

Three things grew this cycle. The adapter roster went from nine to eleven: GitHub Copilot CLI and Hermes Agent are both monitored now, and neither was a copy of an existing adapter — Copilot writes explicit turn boundaries into a uniformly-enveloped events.jsonl, and Hermes writes no transcript files at all, keeping every session in one shared SQLite store. herdr joins tmux and kitty as a terminal backend you can type into, interrupt, and read back from the daemon, headless and over the relay. And the state machine stopped guessing: Claude Code’s Stop and Notification/idle_prompt hooks and Codex’s PermissionRequest/Stop hooks are now authoritative live signals, so “an approval prompt is on screen” and “the turn is over” are observed facts rather than inferences from the tail of an assistant message.

Underneath that, the signal machinery got a shared spine — explicit tiers, one ordered policy table, a clock, and an arming predicate — so a signal now declares how far it can be trusted in one place instead of each classifier rule restating it. Four separate fswatcher fixes close the class of bug where a session was simply never observed and nothing said so: a dropped kqueue creation event is now caught by a periodic reconcile sweep, a watch registration that fails mid-scan is reported instead of swallowed, and the reporting is capped so an FD-exhaustion storm doesn’t rotate its own first lines out of the log.

The rest is correctness and hygiene: a slow-but-executing edit is no longer mislabelled as a stalled prompt, a timed-out lsof probe no longer permanently kills a live background process, the macOS menu-bar panel stopped burning ~10% CPU while closed, and the master “Enable notifications” toggle actually silences everything including the spoken alerts. Security-wise, 4 CodeQL alerts and 89 SonarQube findings were cleared, golang.org/x/net was bumped twice, and every ecosystem in the repo now gets scheduled dependency updates rather than waiting for an alert.

Added

  • GitHub Copilot CLI is now a monitored agent — the tenth adapter. Sessions are discovered from ~/.copilot/session-state/<session-id>/events.jsonl (relocatable via $COPILOT_HOME), with the session id taken from the containing directory rather than the filename, which is constant. Copilot writes explicit turn boundaries, so working/ready ride on real records instead of a quiet-tail heuristic; permission prompts route to waiting from the transcript itself, with no hook installation required; and tokens and cost are live per assistant message rather than flat-zero until the agent exits. In-process subagents (task/explore) are counted on the parent, which is where Copilot records them — it creates no child session directories. Verified against CLI 1.0.77–1.0.78. (#1256, #1332)
  • Hermes Agent is now a monitored agent — the eleventh adapter, and the first one onboarded whose sessions have no files of their own: every session lives in one shared SQLite store at $HERMES_HOME/state.db. A row appears while the session is still live, and finish_reason is an explicit turn boundary, so no idle-flush heuristic is needed. Verified against Hermes Agent v0.19.0 on macOS. (#1325, #1327)
  • herdr is now a supported terminal backend, alongside tmux and kitty. A session running in a herdr pane can be typed into, interrupted and read back from the daemon — headless, with no macOS automation prompt, and reachable over the relay — by scripting herdr pane send-text / send-keys / read against the pane recorded in $HERDR_PANE_ID. Verified end to end against herdr 0.8.0 driving a real Claude Code TUI. (#1348, #1349)
  • Codex has a hook-authoritative live-state tier. The daemon installs PermissionRequest, PostToolUse and Stop hooks into ~/.codex/hooks.json, so an approval overlay flips the session to waiting the moment it appears rather than when prose matching happens to notice it, and turn-end arrives as a push instead of an inference. (#1171, #1174)
  • Claude Code’s Stop hook is now the authoritative ready signal, replacing the transcript-tail IsAgentDone heuristic and its codex carve-out at true turn end. (#1161, #1170)
  • Claude Code’s Notification/idle_prompt hook now routes to waiting — the false-negative the prose heuristic structurally cannot reach: a turn that ends idle with no trailing question or cue, which used to show ready. (#1173, #1175)
  • Signals now declare an explicit tier and reconcile through one shared mechanism, so a hook-fed fact and a transcript-derived guess are no longer weighed by ad-hoc precedence scattered across classifier rules. (#1288, #1295)
  • A waiting session’s headline says what the question is about. Every pending question is now shaped <3–5 word topic>: <question> — “DB migration: drops users.legacy_id — run it or keep the column?” instead of a context-free “Should I proceed?”. (#1142, #1146, #1186, #1193)
  • A Tips & Tricks page on the docs site, collecting the easy-to-miss ways to get more out of Irrlicht — starting with keeping the menu bar always visible, which was previously buried inside macOS Setup. (#1317)
  • .claude/skills/**/*.md is linted as its own preflight and CI gate. Skill files decide how agents triage, plan, implement and review, and until now nothing mechanical read them. (#1209, #1280, #1290)
  • /ir:triage emits an effective complexity level 1–10 plus the run plan /ir:exec should spend, moving the budget decision to where the reading has already happened. (#1204)
  • A red-first test gate and an evidence bar for dismissals. A test asserting a defect must be seen failing before the fix exists, and any claim whose function is “you don’t need to look here” now carries the same evidence bar as the claim it supports. (#1126, #1201)
  • /ir:code-review is a repo-owned, agent-invocable reviewer. ir:exec’s review gate pointed at the built-in /code-review, which is disable-model-invocation and unreachable — so every run silently degraded into an improvised pass. (#1205, #1217)

Fixed

  • A session is no longer invisible for the daemon’s whole lifetime because macOS dropped a file-creation event. fsnotify’s kqueue backend discovers new files by diffing a directory listing, so a creation can be missed outright; a periodic reconcile sweep now re-walks the tree and reports what fsnotify didn’t. (#1248, #1286)
  • A watch registration that fails during the tree scan — FD exhaustion, a permissions change, Linux’s inotify limit — is now reported instead of discarded. The watcher previously signalled “ready” over a subtree it was not watching, and every session under it was simply never observed, with nothing logged. (#1255, #1279)
  • Watch-failure reporting is capped and skips vanished directories, so an EMFILE storm reports the first failures individually plus one summary total rather than tens of thousands of lines that rotate the informative ones out of the log. (#1289)
  • A timed-out lsof probe is no longer read as “this background process is dead”. The caller responds to a dead verdict by purging the process and persisting that to the ledger, so one slow probe could permanently drop a still-running background process and flip a live session to ready with no way back — and lsof is slowest exactly when the machine is busiest. (#1307)
  • A slow-but-executing edit is no longer flagged as stalled. The threshold was the 5s poll cadence, which sits inside the observed duration tail for legitimately-running edits, so a 14–16s edit flickered workingwaitingworking. It is now a named 30s threshold set past that tail; a genuinely held prompt still trips it. (#1130, #1132)
  • The macOS menu-bar panel no longer burns ~10% CPU while closed. orderOut() left the SwiftUI session list window-attached, so the working-dot animation and every coalesced WebSocket flush kept re-laying-out the full list off screen. (#690, #1134)
  • The macOS master “Enable notifications” toggle is authoritative. It was wired only to the Settings UI, so turning it off collapsed the section but left the per-event keys true behind it — banners kept posting and the Speak-aloud voice kept talking. (#1183, #1202)
  • A waiting cue or question sitting before the trailing 200 runes of a long final message now flips the session to waiting. The prose heuristics only ever saw the display-truncated tail. Fixed for the shared path and then for Codex, which never set the flag at all. (#1150, #1155, #1159, #1163)
  • A turn that ends with an irrlicht-question marker routes to waiting. The marker fed only the display headline, so the headline showed a pending question while the state contradicted it. (#1138, #1145)
  • irrlicht-focus resolves the daemon’s port instead of hardcoding :7837, so focusing a session monitored by a dev daemon on an alternate port no longer sends the request to production. (#1215, #1241)
  • The agent hooks the daemon installs now POST to the port the daemon actually binds. A daemon on an alternate port installed hooks that delivered to whatever owned :7837, so its own hook-authoritative tier silently received nothing; existing :7837 entries are recognized as ours and repointed in place rather than orphaned beside a duplicate. (#1178, #1207)
  • The task ETA converges as a task progresses instead of staying anchored to one unrepresentative round. Measured over 398 transcripts / 116 episodes: MAE 241.6m → 195.5m, median absolute error 25.9m → 21.7m, jitter 2.4m → 59s. (#977, #1211)
  • Replay pairs each recording with its sibling events.jsonl sidecar. The resolver only probed a legacy filename, so os.Stat always failed and all ~380 recordings replayed transcript-only — the sidecar path had no fixture coverage at all, which is how the hook→signal drift in #1320 could sit undetected and why “no goldens moved” was vacuous evidence on the PR that fixed it. (#1341)
  • The hook-name → SignalKind mapping is one shared table. It was written twice, and the two had drifted: the daemon held a permission signal on PreToolUse, the replay harness fell through and returned. The paths agreed by luck. (#1320, #1323)
  • npm test self-heals in a fresh worktree. node_modules/ is gitignored, so a new git worktree add died with vitest: command not found — which reads as “no web tests here” rather than “deps missing”. A pretest script now runs npm ci --ignore-scripts only when dependencies are absent, never rewriting the lockfile. (#1229)
  • The nightly star-history chart builds again. GitHub now blocks the REST stargazers listing for personal access tokens; the fetch is ported to the GraphQL stargazers connection. (#1187)
  • 11 SonarQube security findings resolved, including a BLOCKER path injection where Codex’s hook receiver passed transcript_path straight from an HTTP body to os.Open. The path is now confined to a .jsonl rollout resolving inside $CODEX_HOME/sessions, with symlinks resolved before the containment check. (#1196)
  • The 4 open CodeQL code-scanning alerts are closed — three workflows without a permissions: block, and one go/path-injection sink that now carries a local guard CodeQL recognizes. (#1259, #1260)
  • 78 of the 80 open SonarQube findings cleared across the shell, Go and web analyzers, plus the earlier shell-rule sweep in tools/lib/. (#1250, #1262)
  • golang.org/x/net bumped to v0.56.0 (GO-2026-5942 / CVE-2026-46600, reachable in called code and therefore failing every pre-push hook for a diff touching core/), and the go directive floor raised to 1.25.12 so Go’s toolchain switching clears 38 stdlib advisories per module automatically. (#1247, #1252, #1253, #1296)
  • tools/security-scan.sh gained --changed scoping, so a pure-Go push no longer pays for an npm audit of both web trees and gets rejected by a pre-existing advisory it could not have caused — the failure mode that forces --no-verify, which disables every other gate too. (#1213, #1231, #1242)
  • A network-failed npm audit is no longer reported as a fabricated advisory. npm exits non-zero both when it finds advisories and when the audit cannot run; the report’s shape now decides which. (#1140)
  • The pre-push preflight scopes every gate to the packages and web trees the push actually touches, so a typical push finishes in seconds instead of blowing past an automated caller’s command timeout and being killed after the commit had already landed. (#1151, #1154)
  • The security scanner derives its module list from go.work instead of a hardcoded copy, so a seventh module cannot be added and silently never scanned. (#1293)
  • The roundtrip testbed image installs @anthropic-ai/claude-code with --ignore-scripts and then runs only the one named install step it actually needs, instead of letting every package in the tree run install hooks. (#1274)
  • A session running inside a herdr pane no longer takes on a stranger’s terminal identity. herdr’s server owns each pane’s pty, outlives any attached client and is reparented to init, so every terminal variable a pane inherits — $TERM_PROGRAM, $TMUX, $TMUX_PANE, $KITTY_*, $VSCODE_PID — describes whatever environment the server happened to start in. Irrlicht captured those verbatim, so a click-to-focus jumped to an unrelated application, and a server started from inside tmux made the backchannel resolve to tmux and type into a foreign pane in a different window. (#1348)
  • Every newly discovered session is now classified against its own metrics, not just child sessions. A top-level session was birthed flat ready and only re-classified when it had a ParentSessionID, which is invisible for an agent whose transcript file is created empty and filled in later — but very visible for one that writes the file only when the first prompt is sent, so the open turn is already on disk at discovery. Such a session reported idle while the agent was demonstrably generating. Found while onboarding GitHub Copilot; the fix is adapter-agnostic. (#1256)
  • MergeMetrics now carries TranscriptPermissionPending, so a transcript-tier permission prompt actually routes the session to waiting on the live path. The merge is an explicit field allowlist and the field was missing from it, so the flag was computed correctly, survived replay through a separate converter, and was then dropped on the only path that matters. (#1256)
  • exit_plan_mode and ask_user are now recognized as user-blocking tools. Both hold a persisted tool call open while the user decides, so without an entry the session sat in working while it was really waiting. (#1256)
  • Open subagents are now counted from the parser that actually read the transcript. agents.SubagentCounters built one parser at wiring time and closed over it, while the metrics adapter builds a separate parser per transcript path — so any adapter keeping its count in parser state reported zero forever. (#1256)
  • nanoid bumped to 3.3.18 in both web trees, clearing GHSA-2v37-7h3g-55p8 (high, custom generators can loop indefinitely when size is zero). Dev-only — it reaches the trees as a transitive dependency of the vitest toolchain and ships in no artifact — but it failed this release’s own security gate, and the lockfiles were bumped with a newer npm than the local default so the libc fields on the @rolldown/binding-linux-* entries survived the write.
  • Model alias map re-synced against codeburn upstream: 4 new aliases added (quickdesk-auto, k2d6-agent, k3, k3-agent).

Changed / Docs / Distribution

  • The release playbook tags the exact squash-merge commit of the release PR rather than HEAD, and asserts the tagged tree’s version.json before pushing — v0.5.9 nearly shipped its tag on a star-history bump. (#1135, #1139)
  • The Claude Code and Codex hook installers share one hookjson installer and one waiting-cue rule, instead of two adapters maintaining a near-verbatim copy of the same logic. (#1179, #1206)
  • “The installed hook endpoint follows the daemon’s bind address” is now a shared contract assertion (contracttesting.AssertHookEndpointFollowsBindAddr) that each hook-installing adapter wires with one call, so a future adapter cannot quietly ship a hardcoded port. (#1216, #1240)
  • signalPolicies gained a clock and an arming predicate, PreCompact and the stalled-edit-tool overlay moved onto the shared SignalHolds mechanism, and the policy map plus order slice folded into one ordered table — so a policy that is added but not ordered is now unrepresentable rather than test-caught. (#1306, #1318, #1324)
  • The onboarding factory has one owner for the record-daemon lifecycle, and its guards prove freshness rather than presence — seventeen findings from the Copilot and Hermes runs shared the shape “confirms something exists where it means it came from this run”, three of which composed into a fully-green path to committing another run’s data. (#1214, #1239, #1333, #1337)
  • ir:exec gained a commit step, a stale-playbook check, a duplicate-work guard, base re-fetch before opening the PR, WIP markers while a run is still pushing, needs-info surfacing in every mode, and review depth scaled to the diff. (#1153, #1160, #1162, #1220, #1221, #1225, #1227, #1277, #1278, #1287, #1328)
  • Both web trees moved to Node 24 + jsdom 30, and dependency updates are now scheduled weekly for every ecosystem in the repo (Go modules, both npm trees, GitHub Actions) rather than arriving only when a security alert fires. (#1225, #1253, #1304)
  • AGENTS.md gained a Process Rules heading and improvement-suggestion guidance. (#1136)
  • /ir:release follows /ir:refresh-aliases by path rather than by name, so a skill added or renamed after a session started is still reachable. (#1249, #1254)

Technical appendix

  • GitHub Copilot CLI adapter (#1256, #1332). Copilot writes an append-only, uniformly-enveloped events.jsonl per session under $COPILOT_HOME/session-state/<session-id>/, so a plain JSONLineParser covers it and no sidecar read is needed; the session id comes from the directory via the existing FilesUnderRoot.SessionIDFromPath seam. All three states are transcript-derivable, which is what removed the planned hook tier from scope. Reaching waiting required one shared addition: the classifier’s permission rule read a hook-fed field no parser can set and that is never populated under replay, so this adds the transcript-tier twin (ParsedEvent.PermissionRequest/ResolvedMergeMetrics). Three adapter-agnostic bugs fell out of the onboarding: MergeMetrics’s explicit field allowlist was missing TranscriptPermissionPending, so the flag was computed correctly, survived replay through a separate converter, and was dropped on the only path that matters — parser tests, classifier tests and every replay golden were green against completely broken production behaviour; every newly discovered session is now classified against its own metrics rather than only child sessions, which is what birthed a top-level session flat ready while the agent was demonstrably generating; and agents.SubagentCounters built one parser at wiring time and closed over it while the metrics adapter builds a parser per transcript path, so any adapter keeping its count in parser state reported zero forever (Claude Code was immune by accident — its counter ignores the receiver, and its unit test passed for the same reason). exit_plan_mode and ask_user are now recognized as user-blocking tools.
  • Hermes Agent adapter (#1325, #1327). Hermes persists no transcript files — every session lives in one shared SQLite store at $HERMES_HOME/state.db — so this generalizes the daemon wiring that had hard-coded opencode as the only ProcessOwnedStore adapter, and registers the onboarding-factory column with a working interactive driver. Everything was verified against v0.19.0 on macOS rather than derived from docs: a sessions row is INSERTed ~2s after launch with ended_at NULL and message_count incrementing, so the store is a live discovery source rather than a post-hoc archive; finish_reason is an explicit turn boundary (stop ends the turn, tool_calls means work continues); and sessions.cwd is populated only by Hermes’ tui_gateway, so an interactive hermes chat reports source cli. The store is opened through core/pkg/sqlitero, per the convention that an adapter reading an agent-owned SQLite store never calls sql.Open directly.
  • herdr terminal backend (#1348, #1349). herdr’s server owns each pane’s pty, is long-lived, detached (PPID 1) and outlives any attached client — so $TERM_PROGRAM, $TMUX, $TMUX_PANE, $KITTY_* and $VSCODE_PID inside a pane all describe the environment the server started in, frozen at that moment and handed to every pane it will ever spawn. Capturing them verbatim sent click-to-focus to an unrelated application and — verified against a server started from inside tmux — made resolveBackend pick tmux and type into a foreign pane in a different window, which is worse than reporting the session uncontrollable. Launcher capture now takes a herdr pane’s own address ($HERDR_PANE_ID plus $HERDR_SOCKET_PATH, herdr’s documented equivalent of tmux -S <socket> and the only key that distinguishes one running server from another) to the exclusion of everything else. Submitting input goes through pane run rather than a trailing carriage return, because a CR carried inside send-text stops submitting once the text wraps in the agent’s composer — so a realistic prompt was typed and left unsent.
  • Hook-authoritative live state for Claude Code and Codex (#1161, #1170, #1173, #1175, #1171, #1174). Claude Code’s Stop hook registers with no matcher (Claude Code rejects one) and is wired through the detector as a consume-once overlay carrying last_assistant_message, the display-truncated text, and a full-text waiting-cue verdict computed via the same WaitingScanWindow the parser uses. Notification/idle_prompt ships as a live-only IdlePromptPending overlay consumed by a new classifier rule that overrides the ready verdict, held while the finished turn stays idle and cleared the moment a new turn begins so it never pins waiting into the next turn — the shape the #1141 spike argued for after showing the hook is late-but-authoritative. Codex’s tier mirrors it against ~/.codex/hooks.json (a dedicated file, never config.toml, so a malformed write cannot corrupt the user’s main config): PermissionRequest → live waiting, PostToolUse clears the pending overlay once an approved tool runs, Stopready.
  • Signal tiers, policy table, clock and arming predicate (#1288, #1295, #1306, #1318, #1324). SignalHolds.Hold records arrival time and takes the clock from the caller (time.Now in the daemon, virtual time in the replay harness) so staleness stays testable and replay-deterministic; stale widened to func(HoldContext) bool carrying metrics, held-since and now, which is what let PreCompact (#657) stop being hand-rolled — its rule is half wall-clock, and the old func(*SessionMetrics) bool could ask “do the metrics contradict me” but never “have I been held too long”. signalPolicy then gained a symmetric ripe func(holdContext) bool (nil = ripe on arrival) evaluated between stale and apply, so arm-then-fire signals became expressible and markStalledEditTool (#488) — the last hand-rolled per-session overlay map on SessionDetector — became an ordinary policy row. stale is evaluated before ripe, so a hold that is both over and not yet ripe is dropped rather than left waiting for a due date that can never arrive. The follow-up fold merged the policy map and order slice into one ordered table, added session.TierOf so a signal’s tier is declared once instead of restated by every rule that reads it, and stamped provenance off a structural signal rather than comparing reason prose.
  • fswatcher reconcile sweep (#1248, #1286). fsnotify’s macOS kqueue backend discovers new files by diffing a directory listing rather than receiving a per-file notification, so a creation can be missed outright — 660 instrumented runs delivered the linked new-session event every time (worst case 9.9ms), which is why neither a longer deadline nor a sleep would have helped. Watch now runs a periodic sweep that re-walks the tree and reports what fsnotify didn’t: a transcript with no emission recorded becomes an EventNewSession, one whose size moved becomes an EventActivity. Every emission goes through a new emit() that records the reported size, so in the healthy case the sweep finds nothing to say and stays silent — notifications remain the fast path.
  • fswatcher registration reporting (#1255, #1279, #1289). addExistingDirs discarded the result of every per-subdirectory watcher.Add, then called signalReady() unconditionally, so Ready() reported the tree armed over a subtree it was not watching. Both tree scans now route through one addWatch helper that reports a rejected registration via the Logger, per Key Conventions’ logged-not-propagated rule, and stays deliberately non-fatal — one unreadable directory must not strand the whole watcher, which is why the error was discarded in the first place. The review pass then capped the logging: every failure mode this targets (EMFILE, fs.inotify.max_user_watches) is process-global, so once one trips mid-scan every remaining Add fails and one line per directory becomes tens of thousands per boot, rotating out the lines that name where the budget ran out. A directory removed between being enumerated and being armed returns ENOENT and is not a blind spot — nothing is left under it to observe — so it is neither counted nor reported.
  • lsof timeout (#1307). anyLiveOutputWriter ran lsof under a 2s timeout and discarded the error, so a timeout was indistinguishable from “no live writer”. The doc comment called that “the safe (don’t pin forever) degradation”, but the caller responds to a dead verdict with PurgeDeadBackgroundProcs, which purges from the tailer and persists through saveLedger — so the staleness does not self-correct. The correlation runs the wrong way too: lsof walks every process on the machine, so it is slowest exactly when the machine is busiest, which is when long-running background work is most likely.
  • Stalled-edit threshold (#1130, #1132). markStalledEditTool set OpenToolStalled once a permission-gated edit tool had been open for staleWorkingRefreshInterval (5s) and rule 1b routed that straight to waiting, but 5s is a poll cadence, not a “a human is looking at a prompt” threshold. Edit tools are usually near-instant (median ~0.1s, mean ~1.4s) with a real long tail — legitimately-executing, prompt-free edits of 14–16s have been observed — so the gate sat inside that tail. A named stalledEditToolThreshold (30s) sits past it; the #488 hook-unavailable path still trips on a genuinely held prompt.
  • macOS hidden-panel rendering (#690, #1134). With the panel closed, orderOut() kept the NSHostingView window-attached, so the whole SwiftUI session list continued animating and laying out off screen: the working-dot repeatForever breathe re-rendered at frame rate and every coalesced WebSocket flush (10/s under any working session) re-laid-out the full non-virtualized list. MenuBarController now attaches the hosting controller only while the panel is shown, and SessionManager’s coalescing window is visibility-tiered — 0.1s visible (unchanged), 2s hidden — so the consumers that matter with the panel closed (menu-bar quota bars, context-pressure alerts, the debug-state file) stay fresh without riding the full render path.
  • Master notification toggle (#1183, #1202). The gate moved to SessionManager.sendNotification, the funnel where both the UNNotificationRequest (banner + sound) and the separate SoundPlayer.speak call sit — one guard silences every channel and no future caller can route around it, which matters most for the TTS path since it bypasses UNNotificationContent entirely. “Key absent” falls back to the per-event OR: an install predating the toggle (#940) that hasn’t opened Settings since has no stored value, and silencing those would be a worse regression than the bug.
  • Waiting cues past the display tail (#1150, #1155, #1159, #1163). ExtractQuestionSnippet/ExtractWaitingCue only saw LastAssistantText, tail-truncated to 200 runes for display. The claudecode parser now scans a bounded window of the full assistant text and carries the verdict as a per-pass PendingWaitingCue, threaded ParsedEvent → tailer state (+ledger, restart-safe) → SessionMetricsTailerToDomain → domain metrics, OR’d into IsWaitingForUserInput. Display truncation is unchanged, so the sidebar/tooltip contract is preserved. Codex then mirrored it in parseCodexMessage with no domain or tailer changes, since the plumbing was already adapter-agnostic.
  • Question marker → waiting (#1138, #1145). The parsed irrlicht-question marker fed only the display headline while the classifier relied solely on IsWaitingForUserInput(), whose prose heuristics see only the truncated tail — so on a long final message the headline showed a pending question while the state contradicted it (observed on real session 71f27332). SessionMetrics.PendingQuestionMarker is set in the single tailer→domain Convert seam and only from a real TaskQuestion marker, never the away_summary recap or the LastAssistantText fallback, both of which also populate QuestionHeadline.
  • Topic-prefixed question headlines (#1142, #1146, #1186, #1193). Two production paths converge on one output shape. The marker path bumps the irrlicht-question instruction to v3 asking for <topic>: <question> directly, routed through a new CompactQuestionVerbatim kind so a multi-sentence marker keeps its topic prefix instead of losing it to sentence selection. The regex path derives a 3–5 word topic from the session’s first user prompt (slash-command-stripped, whitespace-collapsed, word-boundary-cut, rune-capped) and joins it to the extracted question, re-capped through the verbatim kind so topic and question share one budget and the question tail is trimmed, never the topic. The ~70 char cap relaxed to a soft ~90 — clarity over brevity — which survives to the UI because daemon-side compaction caps at 200 runes and the em-dash/semicolon/colon separators are not sentence terminators. Marker key and sentinels are untouched, so patchManagedBlock upgrades installed v1 blocks in place on the next daemon start.
  • Hook endpoint resolution (#1178, #1207, #1215, #1241, #1216, #1240). core/pkg/daemonaddr is the single source of truth for the bind address and the loopback URL same-machine clients should use. Resolution is a pure function of the environment on purpose: the installers run from permission Apply closures inside adapter packages constructed before the TCP listener exists, so there is no resolved address to thread down to them. irrlicht-focus needed a second source — ClientURL prefers an explicit IRRLICHT_BIND_ADDR, else the address the running daemon published to <IRRLICHT_HOME>/irrlichd.addr, else the default — because a client cannot assume it inherited the daemon’s environment: the macOS app plants the variable only in the daemon child’s environment and does not launch irrlicht-focus at all. The three obligations (an install writes the resolved port; an entry left by a daemon on another port is rewritten in place with no duplicate matcher group; uninstall is not port-scoped) were encoded twice verbatim across two adapters’ hookport_test.go, so they moved into contracttesting.AssertHookEndpointFollowsBindAddr — the shape core/internal/contracttesting exists for, with AssertPermissionGated as precedent.
  • ETA convergence (#977, #1211, #1223, #1228). ForecastTaskCompletion splits into ObservedRoundRate (the measurement plus the observation count backing it) and BlendRoundRate (Bayesian shrinkage toward the corpus prior, weighted by that count), so the first measured round nudges the estimate and the fifth largely replaces the prior; #753’s zero-round bootstrap is untouched, so a number still appears at the very first marker. Re-measured with tools/eta-research over 398 transcripts / 116 episodes against the shipped prior-bootstrap: MAE 241.6m → 195.5m, median absolute error 25.9m → 21.7m, median relative 100% → 96%, jitter 2.4m → 59s. Separately, the age/stale/title preamble that each ETA-chip branch rebuilt by hand (3 branches per frontend, 6 sites, each with a bare 180 literal) collapsed into one etaChipHeader helper per frontend, with the threshold named in both and cross-referenced to the daemon’s session.TaskEstimateGraceAge; the macOS snapshot baselines are byte-identical.
  • Replay sidecar pairing (#1320, #1341). resolveInputPaths only probed the legacy <transcript-stem>.events.jsonl spelling, but every sidecar in replaydata/ is a plain events.jsonl beside its transcript — so os.Stat always failed, useSidecar stayed false catalog-wide, and both fixture drivers replayed all ~380 recordings transcript-only. applyHookEvent was therefore never reached over a real recording, which is how the hook→signal mapping could drift without moving a single golden, and why “no goldens moved” was vacuous evidence on the PR that introduced the drift. Probing the sibling name turns sidecar mode on catalog-wide.
  • Shared hook-name → SignalKind table (#1320, #1323). The mapping was a switch in SessionDetector.HandlePermissionHook and again in the replay harness’s applyHookEvent, and the two had drifted: the daemon holds a permission signal on PermissionRequest and PreToolUse, the harness recognized only the former and fell through default: return. replaydata/ carries two PreToolUse events, each followed within ~100ms by a PermissionRequest producing the same hold — so the paths agreed by luck. The table did not land next to the hook-name constants in the claudecode adapter as proposed, because core/architecture_test.go bars application/services from importing adapters/inbound.
  • Security sweep (#1196, #1247, #1250, #1252, #1253, #1259, #1260, #1262, #1274, #1293, #1296). The BLOCKER was Codex’s hook receiver passing transcript_path from an HTTP body straight to os.Open, letting any local process able to reach the hook port steer the daemon into reading a file outside the ~/.codex/sessions tree the transcripts permission scopes consent to; confineToSessionsDir now requires a .jsonl rollout resolving inside $CODEX_HOME/sessions, resolves symlinks before the containment check so a link planted inside the tree cannot point out of it, and rebuilds the path from the trusted root so no caller-controlled string reaches the open. CodeQL’s four alerts were three workflows missing a permissions: block plus one go/path-injection whose existing filepath.Rel + HasPrefix confinement is not a shape the query recognizes as a sanitizer, so it got a sink-local guard as defense-in-depth. On dependencies: nothing in this repo ever moved one except a GitHub security alert, which is alert-driven and one-shot — postcss was patched in one web tree and left stale in the other (#1225), and x/net sat on v0.55.0 carrying GO-2026-5942 since its one security bump. Both surfaced by security-scan.sh rejecting an unrelated push, which is the worst possible time to find them and the reason a chronically-red gate trains people to reach for --no-verify. Dependabot now covers every ecosystem weekly, security-scan.sh derives its module list from go work edit -json so it cannot disagree with what the toolchain builds (an empty parse hard-fails rather than looping over nothing and reporting a pass), and the go directive floor moved to 1.25.12 — a floor rather than a pinned toolchain line, because pinning would force one exact toolchain download on every contributor and CI job to fix one machine’s staleness.
  • Skill-file linting (#1209, #1280, #1290). tools/skill-lint.sh runs five checks over every .md under .claude/skills/ plus any other tracked SKILL.md. Unresolved conflict markers and leftover {{TOKEN}} / REPEAT: / OPTIONAL: scaffolding are hard failures; the internal-reference, list-count and frontmatter checks warn until their noise floor is known (--strict promotes them). The checks read a prose view with fenced blocks, inline code spans and frontmatter blanked, because markdown that documents a marker is not markdown that has one. That blanking is also where the review pass found three silent-pass paths, each reproduced before fixing: an unclosed fence blanked every following line so one missing backtick defeated both ERROR checks and the file reported exit 0 — fence balance is now decided first and an odd count is itself an ERROR; an unterminated frontmatter bound its closing --- to the first thematic break in the body and swallowed everything between; and frontmatter was blanked for the check that reads description:, the field the loader matches on. The rule the fixes encode: when a file cannot be parsed with confidence, degrade toward more checking, never less.

v0.5.9 (2026-07-15)

A correctness release: sessions no longer wedge at “working” after a background subagent, and agents whose session directories are relocated by an env var or config key are finally visible instead of silently absent.

No new marquee surface this cycle. This one closes a set of gaps where Irrlicht either got a session’s state wrong and stayed wrong, or never saw the session at all.

The headline is a regression introduced in v0.5.8: the pendingBackgroundAgentCount guard from #1037 could never release, because Claude Code serializes the count with omitempty — a zero is written as absence, never as an explicit 0. So the release condition the guard waited for never occurred. Any parent session that spawned a single background subagent could never reach ready again for the life of that tailer. v0.5.8 traded a false-ready for a permanent false-working; this release makes the guard actually release.

The rest is discovery and accounting. Three agents honored environment variables and config keys that relocate their session root, and Irrlicht read none of them — so setting VIBE_HOME, KIRO_HOME, or Vibe’s [session_logging].save_dir made every session from that agent silently invisible, with no error and no log. All three are now read. Alongside that, the transcript tailer learned to notice a file rewritten in place rather than only one that shrank, which is the difference between resuming mid-record on top of stale state and re-reading correctly.

Added

  • The History Activity chart is now behind a beta toggle (#1080, #1075) — in Advanced Settings, default off, on both the web dashboard and the macOS app. The matrix is reconstructed from opt-in recordings, and a never-recorded bucket renders identically to an idle one, so for anyone not recording it was a grid of blanks reading as “idle”. Turning the toggle off while Activity is on screen backs out to the default chart rather than stranding the view.

Fixed

  • A parent session no longer stays stuck at working forever (#1098, #1076) — once a Claude Code session had spawned a background subagent. Absence of pendingBackgroundAgentCount on an end-of-turn turn_duration line now normalizes to zero, and the count survives a daemon restart via the ledger instead of going inert.
  • Mistral Vibe sessions are found when $VIBE_HOME is set (#1108, #1095) — upstream honors the variable; the adapter hardcoded ~/.vibe and found nothing.
  • Mistral Vibe sessions are found when [session_logging].save_dir is set (#1123, #1115) — Vibe writes that key into every installation’s default config.toml on first run as a resolved absolute path, so it is an inviting edit rather than an obscure knob.
  • Kiro CLI sessions are found when KIRO_HOME is set (#1074) — Kiro CLI v2.3.0 added the variable; the adapter was authored after the sweep that wired the equivalent fix into codex, claudecode, and pi, and was missed.
  • Mistral Vibe tokens and cost no longer flatline at zero after an in-place rewind (#1078, #1063) — the parser’s token high-water mark and todo reconciler now reset when the tailer detects a rotation, instead of computing a negative delta against the rewound session’s smaller counters and clamping to zero for the rest of the session. Vibe’s auto-compact threshold also now reads the effective per-model value rather than the global default.
  • The tailer detects a transcript rewritten in place, not just one that shrank (#1120, #1104) — a rewrite whose new size was greater than or equal to the last read offset previously took the incremental path, seeking to a stale offset inside a file whose earlier bytes had changed. A fingerprint of the bytes ending at the resume point now catches this, and it is persisted so a rewrite landing while the daemon is down is caught on the next boot.
  • Open tool calls are cleared when a transcript rotates (#1118, #1088) — so a call left open in the pre-rotation content can no longer keep a session pinned.
  • Mistral Vibe forces the waiting state when it asks a structured question (#1109, #1087) — its tool is named ask_user_question, which the shared user-blocking matcher did not recognize, so Vibe fell through to the trailing-question-mark text heuristic instead. Its write_file tool is also now recognized as a permission-gated edit.
  • History control rows that ask to be hidden are actually hidden (#1101, #1079) — an author display: flex on .history-ctl-row outranked the user-agent [hidden] rule, so all three per-chart toggles were silently inert, and two rows shipped the hidden attribute in the markup, so the default page load mis-rendered too.

Changed / Docs / Testing

  • monitoring-surface.md documents all 10 agent adapters; it previously covered 4 (#1092, #1090).
  • The structural claims in monitoring-surface.md are now enforced as census tests, so the doc cannot drift from the adapters it describes without a test failing (#1107).
  • Five stale source comments were corrected and their remote annotations dropped (#1111, #1097).
  • The four duplicated copies of the sessionsDir() env-override helper were extracted into one (#1094, #1085).
  • /ir:triage gained an Observability axis with a cost-tiered instrument plan — it asks whether a reported behavior can be reproduced and observed with what exists today, which is the gap that blocks an agent before verifiability does (#1093, #1091).
  • The 2026-07-15 agent-release sweep was recorded, and false claims it surfaced across the source and docs were corrected (#1072, #1077).
  • The Kiro CLI rewind fork lifecycle was re-recorded, and the Vibe rewind driver verdict reconciled against replay (#1105, #1124).
  • The Vibe checkpoint-rewind daemon premise is scoped to the TUI path in replaydata, matching the ACP-only reachability the rotation fix depends on (#1110, #1089).
  • Model alias map: re-synced against codeburn’s BUILTIN_ALIASES with no changes this cycle.

Technical appendix

  • pendingBackgroundAgentCount guard release (#1098, #1076): #1037 (v0.5.8) added the guard; its release condition was observing pendingBackgroundAgentCount: 0, which never occurs. Across 1,698 observed turn_duration lines the value 0 appears zero times, while 113 appear 176 times and the field is omitted 1,522 times — it is value-driven, not version-driven. The fix normalizes absent → 0 in claudecode/parser.go, scoped to turn_duration only. That scoping is load-bearing: stop_hook_summary shares the same turn_done branch but never carries the field (0 of 88 observed lines), so absence there means “no information”, not “zero” — conflating them would clobber a live non-zero count on every stop-hook event and re-open #1036. Two tests exist purely to lock the scoping. Part 2 persists the count in LedgerState alongside BackgroundProcs (#445) and LastEventType (#649), because a restart resumes at LastOffset and processes zero lines, which left the guard inert on every restart. No schema bump.
  • In-place rewrite detection (#1120, #1104): TranscriptTailer.TailAndProcess had one rotation check, fileSize < t.lastOffset. The fix hashes the up-to-512 bytes ending at lastOffset after each pass and re-hashes that window before trusting the offset on the next. The soundness argument is one line: an append never rewrites bytes below lastOffset, so a matching window means a genuine append and a mismatch means the consumed prefix was rewritten — routed through the same reset-and-replay-from-byte-0 path a shrink already used. A newline-boundary probe was rejected as insufficient (the prefix is corrupt whether or not the stale offset lands on a boundary); inode identity was rejected as the wrong signal (an in-place rewrite keeps the inode, and reading it needs build-tagged files on a platform-neutral package). Failure modes are asymmetric by design: a pre-#1104 ledger carrying an offset but no fingerprint, and an unreadable window, both fall back to historical resume-at-offset behavior — a false positive costs a full re-read, a false negative is what main did. Because resetAccumulatorsForRotation is the only caller of ResetForRotation(), #1063/#1078’s rotationResetter opt-in inherited this blind spot exactly — those fixed what happens on a detected rotation, not detection itself.
  • openToolCalls on rotation (#1118, #1088): #1088 was a verification task over four narrow relocation/prune edges, none known broken. Three proved not real and are now pinned by lock tests plus documented evidence: delete-then-create ordering during relocation; subagent relocation detection (unreachable); prune vs. cumulative totals. The fourth was real: resetAccumulatorsForRotation cleared every other per-file accumulator but not openToolCalls. Re-reading from byte 0 re-inserts every surviving tool_use by id and the map is id-keyed, so that much is idempotent — what it cannot fix is a call left open in the pre-rotation content, which no surviving line re-inserts or closes. The headline relocation finding from parent issue #1065 was already solved in #877 and is untouched.
  • Vibe rotation parser reset (#1078, #1063): Vibe’s parser tracks a token high-water mark (lastPromptTokens/lastCompletionTokens) derived from meta.json’s session-cumulative counters and emits each turn’s delta. Previously unreachable because Vibe’s /rewind always forked a new session directory — 2.19.1 made it reachable via the ACP path only: _overwrite_messages_sync (tempfile + os.replace) is gated on inplace=True, which the TUI does not pass (defaults to inplace=False → forks) but ACP does, and ACP shares the session directory. Without a parser-side reset the stale high-water mark computes a negative delta, both values clamp to zero, and emitContribution’s early return skips updating the mark — so every subsequent turn_done repeats the clamp. The fix adds an optional rotationResetter interface (ResetForRotation()) the tailer type-asserts for, a no-op for the six other adapters sharing the tailer. Vibe’s implementation resets the token high-water mark and the TodoReconciler — the latter because its synthetic per-todo task IDs are assigned in lockstep with the tailer’s taskSeq, which is already reset to 0 on rotation, so leaving nextID/idByKey stale would desync the two ID spaces. Deliberately not reset: path (the file does not change across an in-place rewrite) and sidecarCache (self-invalidates via its own mtime/size check).
  • Vibe $VIBE_HOME (#1108, #1095): settled against the installed mistral-vibe 2.19.1 package two independent ways — source read of vibe/core/paths/_vibe_home.py and executing that same installed code. Upstream honors the variable; the adapter’s contradicting in-repo claim was wrong and the changelog was right.
  • Vibe save_dir (#1123, #1115): vibe/core/config/models.py:58-63save_dir defaults to SESSION_LOG_DIR ($VIBE_HOME/logs/session) and, when set, replaces the root rather than layering under it. vibe/core/session/session_logger.py:56-76 writes <save_dir>/<prefix>_<ts>_<id>/, holding the messages.jsonl + meta.json pair the adapter watches. #1115 asked for a worth-fixing judgement first: cli/cli.py:133-141 (bootstrap_config_files) writes a default config.toml on first run with save_dir already spelled out as a resolved absolute path, so the key sits in every installation’s config. That same fact makes the fix low-risk — in the common case the configured value equals what Irrlicht already computed, so the read is a no-op. Without it, the fswatcher blocks in waitForRoot on a path that never appears, indistinguishable from “no vibe sessions exist”.
  • Kiro KIRO_HOME (#1074): follows 1165dcdf (“honor agent-CLI env vars for relocated session dirs”, #349/#362), which wired the same fix into codex (CODEX_HOME), claudecode (CLAUDE_CONFIG_DIR), and pi (PI_CODING_AGENT_SESSION_DIR); kirocli was authored later (#590) and missed. sessionsDir() now reads KIRO_HOME, rejects non-absolute values with a log (same as siblings), and joins sessions/cli onto an absolute override; the default stays home-relative .kiro/sessions/cli. The false comment claiming “Kiro documents no env var that relocates the session root” — already false when written — is deleted. kirocli had no adapter tests at all; a table test modeled on codex/adapter_test.go is new. Caveat carried forward from the PR: the sessions/cli append is not docs-confirmed — kiro.dev’s settings reference says KIRO_HOME overrides the ~/.kiro directory “used for … sessions” without stating whether sessions live directly under it or under a subdirectory. The append was implemented by analogy with CODEX_HOME (identical shape) rather than guessing a flat layout.
  • Vibe tool-name matchers (#1109, #1087): two shared matchers used exact equality and did not know Vibe’s snake_case names. ask_user_question is Vibe’s structured question tool (live in 2.19.1 tools_available), but the user-blocking set was {AskUserQuestion, ExitPlanMode, question} — the question entry matches a tool literally named question, not a substring — so Vibe never fired rule 1 (NeedsUserAttention). Fixed in both copies: core/pkg/tailer/tailer_config.go and core/domain/session/metrics.go. The duplication is kept deliberately — isUserBlockingToolName’s doc comment records that it is kept local to the tailer to avoid a domain-package import, and that rationale is intact (no production file in core/pkg/tailer imports domain/session; only its tests do). Both sites now carry an explicit KEEP-IN-SYNC cross-reference and twin tests pin both sets. Separately, write_file was added to isPermissionGatedEditTool: upstream’s v2.14.0 search_replaceedit rename had silently opted edit into the OpenToolStalled heuristic with no change on our side, while write_file never matched write. Because that matcher is shared, every adapter under core/adapters/inbound/agents/ was re-checked against the doc comment’s “no adapter names a long-running tool with one of these spellings” claim.
  • History [hidden] (#1101, #1079): .history-ctl-row sets an author display: flex, which outranks the UA [hidden] { display: none } rule, so every el.hidden = true flipped the property and left the row on screen — syncHistoryRangeRow, syncGranularityRow, and syncDoraProjectRow were all silently inert. #history-dora-row and #history-granularity-row ship the hidden attribute in index.html, so the default page load mis-rendered too; #history-chart had the same shape with an author display: block. Fixed with two scoped hatches mirroring the existing .history-matrix-scroll[hidden] precedent — the one element that already worked because someone hit this exact bug there and patched it locally while the siblings were missed. #history-chart[hidden] needs the id to win the cascade (1,1,0 beats #history-chart’s 1,0,0). Deliberately not a global [hidden] { display: none !important } reset: that would change every element currently relying on the buggy behavior and wants its own sweep of .hidden = call sites first. The whole platforms/web/ tree was swept — DOM hidden toggling happens exclusively in historyTab.js (12 call sites), everything else hides via classList; of the 12, the 4 fixed here were the only unhatched ones. This is a real visible change: rows users currently see start disappearing, per each helper’s stated intent.
  • Activity chart gate (#1080, #1075): a plain local display preference, enableActivityChart, default off, on both frontends. Unlike the task-eta (#558) and backchannel (#724) activations it sits beside in Advanced Settings, nothing is installed — no ActivationClient, no daemon reconcile, no .onAppear mirror; the daemon still serves chart=state unconditionally. Web: SETTINGS_DEFAULTS plus an Advanced Settings row with the existing beta-badge, split deliberately into two halves — syncActivityChartVisibility() hides button[data-chart=state] and rides applySettings() alongside the other reflect-onto-DOM toggles, while leaveActivityChartIfSelected() hangs off the per-key effect path next to SOURCE_SETTING_KEYS (a first draft had both in applySettings(), which meant editing relayUrl fired a history fetch). The button ships hidden in index.html so a default-off gate never flashes it. macOS: @AppStorage("enableActivityChart") plus LeadingToggle(beta: true), with HistoryTab.visible(activityEnabled:) filtering the picker per the existing visibleCharts precedent. Both platforms reset the selection when the gate is turned off while Activity is live (web → cost, macOS → .usage); on macOS this is reachable in normal use since Settings is a separate window.
  • /ir:triage Observability axis (#1093, #1091): Verifiability names the measurement; Observability asks whether the instrument exists — ✓ Verifiability with ✗ Observability is the case the axis is built for. One deliberate deviation from #1091 as filed: a cost gate rather than a hard fail to needs-info. Low (instrument exists, just run it) and Medium (extend an existing surface — new lifecycle.Kind, metric field, snapshot scene) leave the verdict unchanged and land the plan in the brief as the ticket’s first step; only High (the rig outweighs the fix) routes to needs-info plus a child issue. The skill’s standing “never propose solutions” contract gets an explicit carve-out, stated in both the opening paragraph and “What to leave out” so the two cannot drift: triage may prescribe the instrument, never the fix.

v0.5.8 (2026-07-15)

Mistral Vibe becomes a fully supported agent, the Activity Matrix lands on macOS, and a long sweep of session-lifecycle fixes closes the gaps where a session could silently stall in the wrong state.

Highlights

Mistral Vibe is now a supported agent

The Irrlicht dashboard listing three sessions in one project — a Codex session on gpt-5.6-terra, a Claude Code session on opus-4-8, and a Mistral Vibe session on mistral-medium-3.5, each with its own context bar and running cost

Irrlicht now watches Mistral Vibe sessions the same way it watches Claude Code, Codex, and the rest: working/waiting/ready state, the model chip, the context bar, running cost, task summaries, and backchannel control all work out of the box. No configuration — start vibe in a project and it shows up.

Why it matters: if you run Vibe alongside your other agents, it no longer sits invisible in a terminal you have to remember to check. (#921)

The Activity Matrix comes to the macOS app

The Activity Matrix in the macOS History panel — a grid of projects down the left and time buckets across the top, each cell a stacked working/waiting/ready mini bar, with Export CSV and Export JSON buttons

The Activity Matrix — a projects × time grid showing when your agents were working, waiting on you, or idle — shipped web-only in v0.5.7. It is now a top-level History tab in the macOS app too, zoomable across nine granularities from one minute to one year, with CSV and JSON export.

Why it matters: the menu bar app was the one place you could not see your own activity history, which is where most people actually live. (#1038, #1028, #1047, #1046)

Added

  • macOS: a “How is this calculated?” link on the CO2 chart (#1035, #1029) — the same link to the CO2 Methodology page the web dashboard already had, and it stays visible in the empty-data state.
  • A waiting-only summary mode replaces expand-all (#993, #985) — a strict two-state toggle instead of a noisy expand-all that flattened every session’s summary into a wall of text; sessions pop open and closed automatically as they enter and leave the waiting state.

Fixed

  • Codex subagent rollouts now link to their parent session (#1055, #1050) — instead of appearing as unrelated top-level sessions.
  • Codex accounts reporting only a single 7-day quota window (#1052) — no longer get a phantom 5-hour bucket invented alongside it, and the macOS and web copy no longer promises a fixed 5h/7d pair.
  • A parent session no longer flips to ready while a background subagent is still running (#1037, #1036) — Claude Code’s own pendingBackgroundAgentCount is now an independent hold condition alongside the file-based check it used to race against.
  • Non-Claude adapters no longer fall back to Claude’s model config (#1022, #1019) — mistral-vibe, aider, antigravity, gemini-cli, kiro-cli, and opencode read the operator’s ~/.claude/settings.json, which could surface an unrelated Claude Code session’s model on a Vibe session.
  • The waiting pill no longer truncates mid-question (#1004, #979) — the hard 70-character cut is gone, a scored sentence covers turns that end without a literal question, and the separate purple “intent” pill is removed on both platforms.
  • A session’s first turn is no longer swallowed (#1010, #1000, #999) — when the daemon discovers it late, or when it is a child session, the missing ready→working step is synthesized instead of collapsing the turn.
  • Backchannel state is re-keyed across presession reconciliation (#1007, #1001) — a session discovered before its process is identified keeps working control instead of silently losing it.
  • Cold-start session discovery is now bounded (#1011) — the filesystem watcher arms a root watch and scans newest-first instead of walking an unbounded history.
  • A tracked pre-session is not reaped until its PID is confirmed dead (#994).
  • History’s concurrency charts resolve each project once per scan (#1058, #1049) — and cache it, instead of invoking git per request.
  • A session whose only recorded transition is still active is counted in the concurrency series (#990, #983) — instead of vanishing from the chart.
  • Idle Mistral Vibe sessions retry cwd/project resolution (#1031) — instead of staying unattributed.
  • Homebrew: the cask uses the symbol form depends_on macos: :ventura (#1053) — silencing the deprecation warning Homebrew printed on every brew upgrade.

Changed / Docs / Distribution

  • Two macOS snapshot references drifted by a toolchain antialiasing change were regenerated (#1044).
  • ir:test-mac stamps the dev version string into the replace-mode Info.plist (#1006) — so a dev build is identifiable in the GUI.
  • onboarding-factory: teardown polls, daemon.log classification, and a known-seams document (#1025) — recording which gaps are deliberate.
  • onboarding-factory: shared TUI-dialog-dismiss poll extracted into _lib/drive (#1014, #1008) — and mistral-vibe’s tool-permission step waits on a condition instead of a fixed sleep.
  • CI: the star-history chart commit routes through a PR (#1015, #1013) — and its stargazer fetch uses a PAT instead of GITHUB_TOKEN.
  • /ir:exec docs and self-assign precondition (#1023, #991, #1030, #1027) — documents [mode] <N> invocation and auto mode, requires a feature to be implemented and verified across every frontend it touches, and makes Phase 4’s self-assign a verified precondition rather than a best-effort step.

Technical appendix

  • Mistral Vibe adapter (#921): a full agent column — Go adapter under core/adapters/inbound/agents/vibe/ plus the complete onboarding-factory matrix column (44 scenario cells, driver, recordings). Source is FilesUnderRoot on ~/.vibe/logs/session with SessionIDFromPath, since the transcript filename is the constant messages.jsonl and the session id is therefore the parent <session-id> directory. Verified against a real ~/.vibe transcript rather than the AI-generated handover doc, which was wrong on two points now corrected: tool calls use the OpenAI nested function.name shape (not a flat name), and vibe is a Python console-script whose comm is the interpreter — so an ExactName{"vibe"} process match would never fire and it needs a CommandPattern on the command line instead. cwd, model, context window, and token counts come from the sibling meta.json sidecar (config.active_model, config.auto_compact_threshold, stats.context_tokens), memoized by (mtime, size), since the JSONL itself carries no timestamp, cwd, model, or usage. SplitsQueuedFollowUpTurns opts into queued-turn boundary detection (#988) because vibe’s message_queue.py drains a mid-turn follow-up synchronously the instant the prior turn’s agent_running() clears — a genuinely distinct turn with no observable ready gap.
  • Activity Matrix on macOS (#1038, #1028): the daemon’s chart=state API was already correct and fully tested from v0.5.7; nothing on the macOS side ever consumed it, and the v0.5.7 changelog did not disclose the gap. Adds HistoryStateResponse (a sibling to HistoryYieldResponse/HistoryDoraResponse, matching the wire shape 1:1) and HistoryGranularity (the nine-step 1m..1y zoom that replaces range/start/end entirely for this chart, mirroring the daemon’s own chart=="state" special case). Activity is a top-level tab rather than nested under Metrics because it has no Range/Group at all. HistoryActivityContentView pins the project-name column beside a horizontally-scrollable grid — SwiftUI has no built-in two-axis sticky-header grid, so only the row-label column is pinned in this pass, a deliberate v1 scope reduction. Cells scale against the busiest cell in the whole grid, not per-row. Tooltips go through the app’s own .tooltip() rather than SwiftUI’s .help(), which does not render inside the NSPanel.
  • Activity Matrix cleanup (#1047, #1046): live QA of the new macOS tab surfaced four bugs in chart=state, two of them shared with its chart=agents sibling. A session with no recorded CWD was labeled “unknown” unconditionally inside the concurrency tracker, unlike every other chart, which only surfaces “unknown” past a 10%-of-window-total share — the substitution moved out to resolveUnknownConcurrencyProject/resolveUnknownStateProject. buildStateResponse never capped project rows, so every project with any historical activity appeared, including years-old one-off worktree sessions; capped to the busiest 8 (historyStateProjectLimit). concurrencyProject() did a raw filepath.Base(cwd) with no git-root resolution, so a session run inside .claude/worktrees/<N>-<slug>/ was keyed as its own project instead of folding into the real repo; now wired through git.Adapter.GetProjectName via a concurrencyProjectResolver interface, memoized per scan. HistoryActivityContentView was the one view in HistoryView.swift not constraining its width with .frame(maxWidth: .infinity) and overflowed the fixed 380pt panel.
  • pendingBackgroundAgentCount hold (#1037, #1036): the file-based hasActiveChildren check loses a short race when a child’s transcript finishes and is reclassified to ready (and cleaned up) moments before Claude Code delivers the task-notification that would give the parent a reason to keep working. Claude Code’s own turn_duration system event already reports pendingBackgroundAgentCount; it is now folded into holdParentForActiveChildren and reevaluateParent as an independent OR condition.
  • Non-Claude model fallback (#1022, #1019): getDefaultModelFromConfig’s switch only special-cased “pi” and “codex”, so every other adapter fell into a catch-all default that read the operator’s ~/.claude/settings.json. A mistral-vibe session whose meta.json sidecar had not been written yet (the window right after a /clear rotation) would surface an unrelated claude-code session’s model name instead of staying empty.
  • Waiting pill (#1004, #979): the summary fell back to the raw first prompt cut at a period, and the question box truncated an already-correctly-extracted question at a hard 70-rune cut, sometimes chopping off the actual question. The daemon’s safety bound rises to ~200 runes, a shared heuristic sentence-scorer covers the case where no literal question exists, and the pill passively upgrades from Claude Code’s own away_summary recap once it arrives. The separate purple “intent” pill is deleted on both platforms — the surviving orange/waiting pill is the only textual element and shows only while waiting, so a finished session shows no textual pill by design.
  • Waiting-only summary mode (#993, #985): a strict two-state toggle — collapsed (unchanged) and waiting (summary/question block shown only for sessions whose state === 'waiting'), computed live off session state so a session transitioning in or out of waiting pops open or closed automatically instead of relying on a stale snapshot. Web keeps its per-row manual chevron as a separate row-scoped concern layered on top of the global mode; macOS has no per-row toggle, so the mode alone drives summaryBlock visibility.
  • Backchannel re-keying (#1007, #1001): both BackchannelEngine’s edge state and the terminal-observer/session state were keyed on an identity that changes when a presession reconciles into a real session, so control silently detached at reconciliation; both are now re-keyed across the transition.
  • Codex rollout linkage (#1055, #1050): derives Codex thread identity and subagent parent linkage from rollout session metadata, and defers zero-byte child create events until their header is readable, preventing a child from flickering as a top-level session before its parent link is known.
  • Concurrency project caching (#1058, #1049): raw-CWD project resolution now persists on the daemon-lifetime concurrency tracker, with a cache-miss guard so concurrent history requests invoke git once per CWD rather than once per request.
  • Model alias map: re-synced against codeburn’s BUILTIN_ALIASES with no changes — the only upstream addition is a gpt-4.1 self-alias that is a no-op for us and stays deliberately omitted.

v0.5.7 (2026-07-13)

History gains three new lenses — agent activity over time, DORA metrics, and CO2 equivalents — alongside a major flaky-test cleanup, non-English waiting-cue detection, and a wave of accessibility and mechanical-quality fixes.

Added

  • Activity Matrix chart in History (chart=state) (#981, #986) — a projects × time-bucket grid of working/waiting/ready agent counts, zoomable across nine granularities from 1 minute to 1 year.
  • DORA metrics in History (#951, #959) — Deployment Frequency, Lead Time for Changes, Change Failure Rate, and MTTR as a new Metrics section on both macOS and web, computed on request from the selected project’s git repo.
  • CO2 equivalent reference lines on the CO2 chart (#952, #955) — a web search, a phone charge, a flight, a tree-year, and more, plus a “CO2 Methodology” docs page citing the source for every figure.
  • Non-English question and waiting-cue detection (#939) — sessions ending a turn in German, Spanish, French, or Portuguese now correctly register as waiting on you instead of only ASCII/English cues.
  • Cache-bloat badge shows percentage above baseline (#949) — not just an arrow.
  • macOS: a single “Enable notifications” master toggle (#947) — gates all three per-event notification rows instead of always showing them.
  • onboarding-factory: interrupt and reset_session driver actions (#970, #935, #972) — implemented for antigravity and kiro-cli.

Fixed

  • WCAG-AA contrast failures in summary/question pills (#984, #987) — on macOS and web, in both light and dark mode; some combinations measured as low as 2.03:1 against the 4.5:1 minimum.
  • CO2 chart reference-line labels overlapping each other (#980, #982) — the near-invisible methodology icon is now a clearly visible “How is this calculated?” link.
  • DORA project picker could overflow the History panel (#969, #962) — with a long project name.
  • History tab’s Range/Group controls closed a <fieldset> with a mismatched </div> (#968, #958) — HTML5 silently ignores the mismatch rather than closing, so later controls could end up nested inside the still-open fieldset.
  • Custom backchannel actions (e.g. /compact) never actually submitted (#966, #963) — they typed the command but never appended a submit sequence.
  • Contrast fix for the comparison-badges documentation page (#961, #960) — third attempt at satisfying an automated checker that appears to ignore alpha-compositing.
  • The ready→working “force-bounce” now logs a reason (#953, #957) — instead of silently flipping state with no visible cause in the events log.
  • A session starting a fresh background process right after settling to ready could bounce back to ready (#941, #937) — missing the new process’s liveness hold.
  • A recurring family of -race test flakes, closed out for good (#973, #976, #965, #967, #975) — root-caused to a session-repository aliasing bug (a shared *SessionState pointer handed out by Load()).
  • Swallowed exceptions in the web dashboard are now logged (#938) — instead of silently discarded, so private-mode storage failures and dropped websocket frames are at least visible in the console.

Changed / Docs / Distribution

  • macOS Settings and History panels now share one header/toggle/spacing/width chrome (#947) — the History Filters dropdown is removed in favor of the existing drilldown, and the Menu Bar Icon segmented control now fills its row edge-to-edge.
  • Internal: 13 functions taking 8+ positional parameters now take an options struct (#945).
  • ~50 mechanical SonarQube maintainability findings resolved (#932, #950, #960, #961) — across Go, Swift, JavaScript, CSS, and Dockerfiles, plus 79 already-fixed findings closed via the SonarQube API after being suppressed in code but never marked resolved upstream.

Technical appendix

  • Activity Matrix (#981, #986)ConcurrencyReader.StateSeries reconstructs per-project, per-state bucketed counts from lifecycle recordings; sessionTimeline.activeIntervals is refactored onto a shared stateReconstruction so the merged (agents) and per-state (state) views can never drift apart. chart=state resolves its window from a named ?granularity= step; month/6-month/year use an averaged bucket width rather than true calendar boundaries, a deliberate documented approximation. Found via live smoke-testing: a session still active at the query window’s end was spuriously counted as having gone “ready” — fixed with a regression test. A second, unrelated pre-existing edge case was found in the same session and filed separately as #983.
  • DORA metrics (#951, #959) — supersedes #919/#936’s wrong-shaped standalone-script attempt. core/domain/dora holds pure, unit-tested metric functions; core/adapters/outbound/git gains three read-only methods; new chart=dora follows the existing chart=yield/chart=agents pattern.
  • CO2 equivalents (#952, #955) — up to 3 red dotted reference lines at the height of a relatable everyday CO2e equivalent (17 entries spanning 0.2g to ~100 tonnes), ported natively to macOS SwiftUI Charts. Follow-up (#980, #982) densifies the table with 8 new entries, converts remaining imperial units to metric, and adds a pixel-level minimum-gap backstop.
  • Non-English waiting-cue detection (#939) — question/waiting-cue detection was ASCII/English-only. Five follow-up review rounds tightened the initial patch, dropping ambiguous German/Spanish/Portuguese possessive forms and English-word-collision prefixes, adding NFD-accent folding, and refactoring into named helpers per CodeScene/SonarCloud feedback; full 123-subtest session package suite passes unchanged throughout.
  • Cache-bloat percentage badge (#949, #946) — computes how far the session’s current median cache-creation per turn sits above the project’s p25 baseline.
  • Notifications master toggle + Settings/History chrome unification (#947) — a single toggle gates the three per-event rows with a one-time migration default; shared PanelHeader, IrrSpacing tokens, and EqualWidthSegmentedControl (an NSViewRepresentable bridge) so the Menu Bar Icon segmented control fills its row edge-to-edge. Removes History’s Filters dropdown entirely since drilldown already covers narrowing to one project/branch/model.
  • onboarding-factory driver actions (#970, #935, #972) — ports the interrupt step (Escape) into antigravity and kiro-cli, live-verified against real authenticated sessions. kiro-cli’s reset_session live-verifies (2.6.0) that /chat new rotates the session id in-process; antigravity’s agy has no reset command at all, documented as a genuine capability gap rather than a stub TODO.
  • WCAG-AA contrast (#984, #987) — pill text was a fixed hex on a 12%-alpha wash of itself; macOS adds Color.adaptive(light:dark:) and per-appearance tokens tuned against the measured wash colors; web adds light-mode overrides for the state-color tokens and their -dim companions.
  • DORA project picker overflow (#969, #962) — the picker used .fixedSize() with no upper bound.
  • History fieldset close tags (#968, #958) — a mismatched </div> against a “special” HTML5 element is ignored rather than closing it.
  • Backchannel Custom actions (#966, #963) — routed through SendCommand, which already owns the per-backend submit logic, instead of SendInput’s bare typing with no submit sequence.
  • Ready→working force-bounce logging (#953, #957)forceReadyToWorkingIfActive never called d.log.LogInfo like every sibling transition function.
  • Background-liveness probe ordering (#941, #937) — hoisted the force-bounce to run before the liveness probe, kept gated in lockstep with the NoSubstantiveActivity skip (#329).
  • mockRepo/memRepo aliasing race (#973, #976, #965, #967, #975) — root cause of a recurring whack-a-mole of “-race flakes” (#606, #942/#944, #956/#964); routed Load/Save/ListAll through a new deepCopySessionState helper closing the aliasing class at every call site, plus new regression-guard tests.
  • Swallowed exceptions logged (#938) — 21 empty/near-empty catch blocks now console.debug the error; extracted a shared collapsedSet.js factory to resolve a SonarCloud duplication-gate regression the fix triggered.
  • Options-struct refactor (#945) — 13 functions exceeding SonarQube’s 7-parameter limit now take a doc-commented Deps/Options struct; 61+ call sites updated, pure signature refactor.
  • SonarQube backlog (#901, #932, #950, #960, #961) — mechanical fixes, a cognitive-complexity extraction (resolveHistoryQuery out of handleGetHistory, complexity 20→under 15), a <dialog>-element conversion for Settings/Permissions modals, and 79 already-fixed findings closed via the SonarQube API.

v0.5.6 (2026-07-09)

Subscription quota lands in the menu bar icon; a full security & code-health sweep closes every open CodeQL/SonarQube finding.

Highlights

Subscription quota in the menu bar status icon

Settings panel showing the new menu bar icon style picker (Lights / Usage / Combined), a quota provider picker, and a quota shape picker (Bars / Circle)

The menu bar status icon can now render your 5-hour/weekly subscription-quota windows directly — as stacked bars or a compact ring — instead of only inside the popover. Pick Lights (today’s dots, unchanged default), Usage (quota only), or Combined (both side by side), and choose which provider’s quota to show if you have more than one subscription.

Why it matters: a glance at the menu bar now tells you how close you are to a quota reset, without opening the popover first. (#917, #909)

Added

  • /ir:release gained an automated security-scan gate (#890) — Dependabot, CodeQL, govulncheck, gosec, and npm audit all run before every release build, closing a gap where CodeQL’s default setup had silently lapsed with zero local scanning to catch it.
  • Model alias map synced with codeburn — added Xiaomi MiMo and Kwaipilot KAT-Coder aliases (currently price at $0 pending a LiteLLM entry for either model).

Fixed

  • Sessions with a still-active background child no longer flip to ready mid-turn (#889, #893, #897, #899) — covers both a parent whose turn ends on a waiting cue and a parent that already looked idle when a new Workflow-tool subagent appears.
  • A session’s transcript moving into or out of a git worktree is no longer mistaken for the session ending (#877, #878).
  • Long-running background-research subagents no longer falsely time out and get reaped (#881, #882) — the inter-write quiet window is raised to 90s to cover slow Web Search/Fetch-heavy gaps.
  • The CO2 estimate next to cost no longer wraps to a second line for kilogram-scale sessions (#920, #922).
  • install.sh no longer kills unrelated irrlichd daemons on the machine when restarting its own, and the release smoke test’s daemon-liveness check no longer flakes (#871, #876).
  • The README’s CodeScene badge was reading the hotspots-only score instead of the overall project score (#918).

Security

  • Closed every open CodeQL code-scanning alert and the remaining SonarQube security/reliability findings from the project-wide #901 sweep (#894, #896, #907, #908, #910, #912, #913) — path-injection guards across the daemon’s HTTP-facing file handlers, an SSRF check on the relay’s outbound WebSocket URL, prototype-pollution hardening in the dashboard, and path-traversal / innerHTML-injection hardening in the onboarding-factory viewer.

Changed / Docs / Distribution

  • README star-history chart is now self-hosted instead of depending on star-history.com’s shared, rate-limited API token (#903, #904); the SonarCloud quality-gate badge was replaced with the steadier security/reliability/maintainability rating badges (#902).
  • /ir:codescene-report replaced by /ir:sonarqube-report (#884) — concrete file:line findings with fix guidance instead of an aggregate hotspot score.
  • /ir:doc-review now fixes findings directly (#875) — instead of filing a GitHub issue for most of them; filing is the fallback only when a fix is genuinely ambiguous.

Technical appendix

  • Quota menu bar icon (#917, closes #909) — new @AppStorage-backed MenuBarStyle/QuotaVisualStyle/MenuBarQuotaProvider settings (default .lights preserves today’s icon); QuotaMenuBarRenderer renders stacked 5h/7d bars or a compact ring, sharing SessionListView’s pace-percent and color-ramp logic so the icon can’t disagree with the popover. A same-PR follow-up fixed real defects an independent second review caught: settings changes not triggering a repaint, a stale-snapshot mismatch with the popover, an absolute-only color ramp that could diverge from the popover’s pace-aware one, and hardcoded colors unreadable in light menu-bar appearance.
  • CO2 unit line-wrap fix (#922, fixes #920)formattedCO2 used two decimal digits for the kg branch while g/mg branches use 0-1, producing strings like “16.15kg” that overflow the fixed-width cost/CO2 column; matched the g branch’s precision and added lineLimit(1) as defense-in-depth.
  • CodeScene badge score source (#918) — the refresh workflow read current_score (hotspots-only) instead of code_health_weighted_average_current (the actual project score).
  • Cognitive-complexity sweep, part of #901 (#916) — refactored all 119 open go:S3776/javascript:S3776 findings down to Sonar’s threshold across 74 files via pure structural extraction, zero intended behavior change.
  • Flaky relay control test fixed (#915)TestRelayRoutesControlToDaemon now reads the client’s initial snapshot frame (which can only arrive post-registration) before dialing, establishing a real happens-before instead of relying on timing.
  • swift:S1075 false-positive suppression (#914, part of #901) — all 70 flagged lines are local filesystem/binary paths, fixed loopback-API routes, SwiftUI preview literals, or opaque test fixtures.
  • Remaining CodeQL alerts closed (#913) — a post-#910 scan found 4 more alerts, 3 new dataflow paths to already-guarded sinks through a different taint source, each closed with a direct taint-visible guard; the 4th (relay-URL request forgery) is left as documented, accepted risk since a user-configurable relay endpoint is incompatible with CodeQL’s fixed-allowlist sanitizer guidance.
  • All open CodeQL alerts resolved, part of #901 (#910, closes #907) — 37 alerts: 1 critical SSRF (relay WebSocket URL validation before DialContext), 35 high path-injection, 1 medium prototype pollution (irrlicht.js agent registry now Object.create(null)).
  • Remaining SonarQube security/reliability findings, part of #901 (#912) — an empty go.sum for a zero-dependency module, NOSONAR annotations on two already-documented intentional trade-offs, and a Dockerfile CMD rewritten to exec form.
  • swift:S100 naming-convention sweep (#911, part of #901) — mechanical rename of 98 underscore-named XCTest functions to camelCase across 12 test files, with matching snapshot reference images renamed alongside.
  • SonarQube findings phase 1, part of #901 (#908) — sanitized dashboard_url before logging, tightened a Keychain item to ThisDeviceOnly, charCodeAtcodePointAt on atob() output, safer float parsing. 17 additional findings confirmed false-positive/wontfix by direct code inspection.
  • README badge swaps (#902, #904, fixes #903) — self-hosted star-history SVG decouples the README from star-history.com’s shared, rate-limited token pool; SonarCloud’s quality-gate badge replaced with the security/reliability/maintainability rating badges.
  • Turn-done waiting hold, part of #901 sweep (#899, fixes #897) — a parent whose turn ends on a waiting cue collapsed straight to waiting even with a genuinely-running background Agent-tool child, because that branch never checked hasActiveChildren the way the ready-transition branch already did.
  • javascript:S6819 accessibility fix, part of #901 (#900, closes #898) — replaced a nested role="button" div/span pair in the design-system’s GroupHeader.jsx reference copy with two real sibling <button> elements.
  • SonarQube VULNERABILITY/BUG/CODE_SMELL cleanup, part of #901 (#896, fixes #895) — ~700 findings addressed across shell/JS/Go/Swift/CSS: absolute-path resolution for 16 shelled-out binaries, non-root Docker users, ARIA labels and contrast fixes, locale-safe sort comparators. One real bug fixed as a byproduct: state_machine.go’s TotalDurationMs() was missing its lock entirely.
  • Ready-parent hold for new Workflow-tool children (#893, fixes #889)holdParentWorkingForNewChild forces the parent back to working the instant a new child is discovered, surviving the periodic stale-session refresh instead of being silently undone.
  • Onboarding-factory archive-name and innerHTML hardening (#894, fixes #892) — a SafeArchiveName type is now the only path to archiveFilePath; manifestBox/driftNote innerHTML templates replaced with DOM construction.
  • ir:release security-scan gate (#890, fixes #885)tools/security-scan.sh checks open Dependabot/CodeQL alerts, govulncheck, gosec, and npm audit; wired into /ir:release Step 5.5 and tools/preflight.sh --only security. Re-enabled CodeQL default setup for Go and JavaScript/TypeScript.
  • dashboard_url iframe validation (#888, closes #886)resolveDashboardIframeUrl rejects anything but a same-origin http(s) URL instead of assigning server-provided input directly to iframe.src.
  • /ir:sonarqube-report replaces /ir:codescene-report (#884) — SonarQube Cloud’s issues API returns concrete file:line findings with rule keys and fix guidance, unlike CodeScene’s aggregate-only hotspot scores.
  • SubagentQuietWindow raised to 90s (#882, fixes #881) — 30s falsely tripped on a genuine 61-second inter-write gap from a WebSearch/WebFetch-heavy background subagent, causing the parent to surface ready for 67 seconds while real work continued.
  • Worktree-relocation transcript tracking (#877, #878)onRemoved now detects a transcript relocated into a sibling project-dir slug (e.g. entering/closing a git worktree) and re-points tracking at the surviving file instead of demoting the session to ready.
  • viewer.js decomposition (#873, #880) — extracted renderPlayback’s (660 lines, CodeScene’s worst score in the repo) timeline geometry, DOM painting, and replay-transport concerns into three sibling modules; DOM output byte-identical, locked by 41 new characterization tests.
  • irrlicht.js row-rendering decomposition (#872, #879) — split the ~270-line, ~37-branch updateSessionRow into per-column renderRow* helpers; pure mechanical extraction, DOM output unchanged.
  • Release-tooling fixes found while shipping v0.5.5 (#876, fixes #871)install.sh’s daemon kill is now scoped to whatever is bound to port 7837; the smoke test’s daemon-liveness check gained a retry loop; seed-demo-sessions now auto-creates its scenarios’ referenced cwd paths.
  • Agent landscape refresh (#874) — refreshed GitHub stars/metadata for tracked agents; marked Gemini CLI, Antigravity, and Kiro as irrlicht-supported; archived Void and Roo Code.
  • ir:doc-review fixes-by-default (#875) — every determinable finding is now fixed in place, with filing/closing a GitHub issue as the fallback only for genuinely ambiguous findings.
  • Model alias sync — added mimo-v2-flashxiaomi/mimo-v2-flash and kat-coder-pro-v1kwaipilot/kat-coder-pro from codeburn’s BUILTIN_ALIASES; neither canonical is in LiteLLM’s pricing table yet.
  • tools/security-scan.sh’s Dependabot/CodeQL gate was silently non-functional — missing an explicit --method GET, gh api defaulted every alert query to POST (since -f params force POST) and 404’d, misreported as an auth/scope failure. Running it for real surfaced CodeQL alert #54 (go/request-forgery, relay URL), already documented as accepted risk in #913 but never dismissed — dismissed now.

v0.5.5 (2026-07-04)

CO2 estimates next to cost, a self-explaining cache-regression badge, and reconnect reliability fixes.

Highlights

Estimated CO2 footprint alongside cost

A session row's cost column reading 7.1g CO2e instead of a dollar amount, with two more rows showing 9.3g CO2e and 2.0g CO2e

Click a session’s cost figure and it now cycles to an estimated CO2e footprint, with a hover tooltip disclosing how confident the number is — provider-disclosed coefficients where a provider publishes them (Gemini, Mistral), a cross-model average otherwise.

Why it matters: cost isn’t the only budget that matters. Now you can see a session’s environmental footprint with the same glance you’d check its price — no separate tool, no export. (#829, #831)

Cache-regression badge explains itself

A red badge under a session row reading claude-code 2.1.143 +14K cache tokens vs 2.1.98

The badge that flags a session whose prompt-cache reuse has regressed now always shows which agent version caused it (or a compact “cache ↑” fallback), with the full plain-language explanation on hover — instead of a bare icon you had to guess at.

Why it matters: when caching quietly breaks and a session starts costing more per turn, you immediately know why and which update triggered it, not just that something changed. (#813, #826, #827, #842)

Fixed

  • History → Usage tab: grouping by Model no longer buckets everything under “unknown” (#792, #793) — a wrong source field meant every session landed in one aggregate bucket regardless of which model was actually used.
  • History’s tab switcher moved into the header, controls collapsed to one row (#785, #788) — the Usage/Yield/Quota picker now lives in the header instead of a static label, and Range/Chart/Group/Forecast controls sit on one scrollable row instead of three stacked ones.
  • Menu bar app recovers when the daemon it’s connected to restarts (#843, #845) — local-daemon traffic now rides its own recyclable connection with a visible “daemon unreachable — retrying” state, instead of retrying forever against a wedged one.
  • Relay connections recover from the same wedged-restart failure mode (#846, #848) — mirrors the local-daemon fix for a restarted standalone irrlichtrelay.
  • Menu bar session count no longer double-counts relay-echoed sessions (#828, #830) — a local daemon that also publishes to a relay it subscribes to could overcount a project’s sessions in the menu bar dots/badge.
  • “Expand All Task Summaries” toggle now persists across restarts (#799, #800).
  • Antigravity sessions bound to a non-interactive background poller no longer show as phantom menu-bar circles (#784, #791) — a known third-party helper process is now recognized and excluded at admission time.
  • FocusMonitor’s remaining crash path on the macOS 26 SDK closed (#782, #790).
  • Subagents whose worktree/transcript disappeared mid-run are reaped (#850) — instead of lingering as phantom sessions.
  • Daemon route-registration startup race fixed (#794, #795, #798).

Changed / Docs / Distribution

  • Dropped a fragile SwiftPM resource-bundle dependency (#844, #847) — removes a crash mode where a dev build’s .app could outlive the worktree it was built from.
  • Doc-review sweep gained a --fix mode, wired into the release process itself, and the README’s CodeScene score badge now auto-refreshes on every push to main (#834, #835, #837).

Technical appendix

  • Estimated CO2 footprint (#829, #831)capacity.EstimateCO2Grams: a tiered CO2e estimation formula using provider-disclosed coefficients for Gemini and Mistral, falling back to a cross-model average (Epoch AI) for everything else, including Claude and GPT. Wired through the tailer’s existing cumulative-token computation into SessionMetrics.EstimatedCO2Grams + CO2Tier, mirroring EstimatedCostUSD’s plumbing. Ships as a click-to-cycle web dashboard surface first; CLI/macOS parity and historical/aggregate tracking are deferred follow-ups.
  • Cache-regression badge explanation (#813, #826, #827, #842) — an always-visible badge on both macOS and web showing the version attribution (or a compact cache ↑ fallback), composed once daemon-side into cache_bloat_explanation instead of each client re-deriving the copy independently.
  • History Group=Model fix (#792, #793)RecordSnapshot/RecordBaseline stamped each cost row’s Model field from SessionState.Model, which is never assigned in production code, so every row landed in the aggregate “unknown” bucket. Falls back to Metrics.ModelName, mirroring the session-list handler’s existing pattern.
  • History header/controls redesign (#785, #788) — moves the Usage/Yield/Quota tab picker into the header, flattens the controls onto one horizontally-scrollable row, and gives the popover a hard fixed height independent of width.
  • Local-daemon reconnect recovery (#843, #845) — fixes a reset-every-attempt backoff bug plus a URLSession.shared wedge against a restarted daemon, via a dedicated recyclable URLSession and a surfaced “daemon unreachable — retrying” state.
  • Relay reconnect recovery (#846, #848) — the same wedged-URLSession.shared fix, this time for a restarted standalone irrlichtrelay.
  • Relay-echo dedup in the flat sessions array (#828, #830) — mirrors the existing apiGroups name-based collapse into rebuildSessionsFromMap(), which only deduped by id and missed the echoed session’s drifted id.
  • Antigravity non-interactive host rejection (#784, #791) — a known third-party helper (CodexBar) keeps an Antigravity CLI process running in the background to poll quota; a synchronous ancestry check at PID-discovery time (PIDManager.AllowsSession) now excludes processes whose ancestry doesn’t resolve to a known terminal/IDE.
  • FocusMonitor KVC crash path (#782, #790)isFocusActive’s remaining value(forKey:) reads now use the same responds(to:)-guarded selector dispatch as the earlier singleton-lookup fix.
  • Subagent reaping on deleted transcript (#850) — adds isDeletedTranscript (confirmed-gone via os.IsNotExist) so a subagent whose parent PID was reused by an unrelated process is reaped instead of surviving forever.
  • Daemon route-registration startup race (#794, #795, #798) — plus a flaky CI test and preflight tooling fix bundled in the same PR.
  • SwiftPM resource-bundle removal (#844, #847)Bundle.module’s only use (AppIcon.icns) was already covered by a direct bundle copy, so the fragile SwiftPM mechanism is removed entirely.
  • ir:doc-review --fix mode (#834, #837) — auto-corrects doc claims directly contradicted by a code-derived fact; wired into /ir:release Step 4b so drift from any earlier release gets caught, not just this one’s diff. This is what caught README.md/site/index.html’s stale “no hooks” claim, corrected in this release.
  • CodeScene badge automation (#835, #802) — a scheduled workflow refreshes the README’s CodeScene badge on every push to main; a manual fetcher backs the /ir:codescene-report skill.
  • Hexagonal layering enforcement + ARS regression gate (#796, #803)core/architecture_test.go statically enforces domain/ports import direction; tools/ars-gate.sh flags an Agent Readiness Score regression as an advisory PR check.
  • Shared contract test for consent-gated adapter call sites (#797, #817)contracttesting.AssertPermissionGated behaviorally verifies a permission’s Apply/Remove wiring at runtime.
  • ir:test-mac componentized restart (#833, #838) — a TARGET=daemon|macos|full axis restarts just the changed component; MODE=replace now installs directly into /Applications/Irrlicht.app instead of a same-bundle-id parallel /tmp copy.
  • CodeScene hotspot cleanup sprint — eight no-behavior-change refactors splitting flagged hotspots: tailer.TailAndProcess/processParsedEvent (#822), domain/session.go by concern (#808, #816), irrlicht.js into modules (#805, #820), SessionListView.swift into per-concern views (#818), PIDManager’s liveness sweep/dedup deletion (#810, #819), irrlichd main() into named startup phases (#821), SessionManager god-object into extensions (#815), and session_detector_test.go by scenario group (#814, #824).
  • Dependency bumpgolang.org/x/net 0.52.0 → 0.55.0 in core (#801).
  • Misc test/tooling hygieneSessionRowSnapshotTests made hermetic against a live daemon (#841); pre-push hook no longer inherits a stray GIT_DIR/GIT_WORK_TREE (#812).
  • Docs — worktree shortcuts + Task Management tidy-up and a git-stash note in AGENTS.md (#789, #825), Karpathy Guidelines section removed (#787), ir:test-mac doc defaults to replace mode (#786), CodeScene/ARS README badges given their own line plus a workflow badge (#836, #823).

v0.5.4 (2026-06-29)

A full History view with spend analytics, a backchannel that acts on your agents, and plain-language session summaries.

Highlights

Spend & history analytics

Irrlicht's History view: a cumulative cost chart with a linear projection and a per-project breakdown with Export CSV / JSON buttons

A new History view turns every recorded session into cost analytics — a cumulative spend chart with a linear projection, breakdowns and drill-downs by project, branch/worktree, provider and model, a concurrent-agents timeline, and a productive-vs-reverted “yield ratio” per project. Facet any axis, cross-filter by token type, and export the whole thing to CSV or JSON. It ships in both the macOS app and the web dashboard.

Why it matters: you can finally see where your agent spend actually goes — which project, which model, and how much of it landed versus got reverted — without exporting anything by hand. (#752, #761, #771, #773, #778, #772)

Backchannel — act on your agents

The Backchannel Rules editor showing an Auto-compact rule: when Context (%) is at least 85 for any agent, Send the Compact command

Backchannel turns Irrlicht from a read-only monitor into something that can talk back: control discovered agents locally or remotely, across agents and terminal backends. Define event→action rules (e.g. when context crosses 85%, send the compact command), pick from agent-translated command presets, or write your own Custom command — and trigger them by a backchannel token.

Why it matters: routine babysitting — compacting a full context, answering a prompt, nudging a stuck session — can now happen automatically or with one click, from any machine. (#731, #733, #769)

Plain-language session summaries

A session row leading with a purple intent block 'Add OAuth login to the web dashboard' and an amber waiting block 'Should I run the migration?'

Session rows now lead with a human-readable headline instead of raw transcript text: a purple block summarizing what the agent is working on, and an amber block with the exact question it’s waiting on. Summaries are generated from lightweight markers with a pluggable compaction step.

Why it matters: a glance at the menu bar tells you what each agent is doing and which one actually needs you — no expanding rows to read the last message. (#765, #770, #743)

Added

  • ETA accuracy improvements, surfaced sooner (#768) — a replay-based estimator informs the task-completion estimate earlier in a turn.
  • Built-in diagnostics bundle (#742) — a /debug/bundle endpoint and an irrlichd --diagnose flag collect a redacted snapshot for bug reports.
  • Cache-creation regression detection (#749) — sessions that stop reusing prompt cache are flagged and attributed to the upstream agent version that changed.
  • Generic GUI-host fallback for click-to-focus (#741) — focusing a session works for more terminal/IDE hosts out of the box.
  • Agent-legible HTML snapshot artifacts for the session list, plus an enriched opt-in lifecycle trace that makes ghost sessions reconstructable (#764, #760).

Fixed

  • Detached background agents are now badged (#747) — instead of showing as phantom rows.
  • Sidebar no longer double-lists relay-echoed local projects (#748).
  • Ghost sessions age out / get reaped (#740, #734) — antigravity PID==0 ghosts age out, and sessions bound to a claude --bg-spare infra PID are reaped.
  • Settings panel no longer janks (#730) — the ~2fps / high-CPU redraw storm is gone.
  • Text-to-speech crash fixed on macOS (#781).
  • Launch crash fixed on the macOS 26 / Xcode 26 SDK (#782) — FocusMonitor resolves the Focus singleton via a guarded selector instead of KVC, which now raises NSUnknownKeyException.
  • Price non-Anthropic-frontend sessions correctly (#726) — synced the model-alias map with codeburn and added new aliases (OpenAI Codex gpt-5.5, Hermes lowercase glm-5.2).

Changed / Docs / Distribution

  • History and Settings panels decluttered on macOS (#781).
  • New /ir:exec skill (#758, #767) — an end-to-end issue loop (worktree → visual HTML plan → implement → PR → /review → /simplify); it now marks the issue in progress before implementing.
  • Antigravity 1.8 + 5.1 scenarios flipped to observed (#775, #776, #777) — by capturing and serving the conversations/<id>.db store in replay, with the context-window replay test pinned hermetic.
  • Snapshot coverage added for the unobservable session-row states (#762).
  • Dependency bumpform-data 4.0.5 → 4.0.6 in platforms/web (#739).

Technical appendix

  • History view — web (#752, #771, #778) and macOS (#761, #773) — a cost-analytics surface built entirely from on-disk recordings: Phase 1 lands the cumulative cost chart with a linear projection and Export CSV/JSON; Phase 2 adds branch/worktree and provider/model attribution with drill-downs; Phase 3 adds a concurrent-agents timeline computed from recording overlap; #778 adds faceted cross-filtering with token-type grouping. The macOS side reaches cost-analytics parity with the web dashboard.
  • Yield ratio per project (#772) — a productive-vs-reverted spend metric per project (spend that survived in the tree vs. spend on changes later reverted), surfaced in the History view.
  • Cache-creation regression detection (#749) — detects sessions whose prompt-cache reuse drops off and attributes the regression to the upstream agent version in play, so an agent update that breaks caching is visible rather than silently doubling cost.
  • Backchannel control (#731) — control discovered agents locally and remotely, across agents and terminal backends, reusing Focus’s Launcher targeting; gated behind a control permission so nothing is exercised while pending or denied.
  • Backchannel read-back (#733) — adds the terminal backend (tmux/kitty/iTerm) as a complementary observation source alongside the transcript tailer.
  • Backchannel command presets (#769) — agent-translated command presets plus a Custom option; #781 adds a backchannel token trigger.
  • Concise session summaries (#765, #770, #743) — session rows render a purple user-intent block and an amber waiting-question block from lightweight summary markers; #770 adds concise intent + waiting headlines with a pluggable compaction step; #765 also fixes a global collapse bug.
  • ETA estimator (#768) — replay-based research into the task-completion estimator improves accuracy and surfaces the ETA earlier in a turn.
  • Diagnostics bundle (#742) — a GET /debug/bundle endpoint and an irrlichd --diagnose flag assemble a redacted diagnostics snapshot for bug reports.
  • Click-to-focus GUI-host fallback (#741) — a generic outermost-top-level-.app host-detection fallback so click-to-focus resolves the right GUI host for more terminal/IDE embeddings.
  • Ghost-session lifecycle (#740, #734) — antigravity PID==0 ghost sessions age out via the no-substantive-activity gate; sessions bound to a long-lived claude --bg-spare infra PID are reaped via the infra-reaper self-healing sweep.
  • claudecode detached background agents (#747) — detached background agents that land in the claude daemon run pool are badged rather than rendered as phantom active rows.
  • Relay sidebar dedupe (#748) — the macOS sidebar de-duplicates local projects echoed back by the relay.
  • Settings panel jank (#730) — eliminated the ~2fps / high-CPU redraw storm in the Settings panel.
  • FocusMonitor SDK launch crash (#782)resolveFocusStatusCenter() resolved INFocusStatusCenter.default via KVC value(forKey:), which under the macOS 26 / Xcode 26 SDK raises NSUnknownKeyException (uncatchable from Swift) instead of returning nil — SIGABRTing every Developer-ID launch (DevID-gated, so ad-hoc/CI builds never hit it; only the release smoke test does). Now resolved with a responds(to:)-guarded selector perform.
  • Agent-legible observability (#760, #762, #764) — #760 enriches an opt-in daemon lifecycle trace (with replay --ghosts) so ghost sessions are reconstructable; #762 snapshots the unobservable session-row states; #764 emits agent-legible HTML snapshot artifacts for the session list.
  • Model-alias sync (#726 + this release) — re-synced core/pkg/capacity/aliases.go against codeburn’s BUILTIN_ALIASES; this release adds openai-codex:gpt-5.5gpt-5.5 (in LiteLLM) and the lowercase glm-5.2glm-5p1 Hermes spelling.
  • Antigravity replay observability (#775, #776, #777) — captures and serves antigravity’s sibling conversations/<id>.db store in replay (#775), flips the 1.8 + 5.1 cells to observed via golden-summary surfacing and a re-record (#776), and pins the LiteLLM cache so the replay context-window test is hermetic on cold CI (#777).
  • /ir:exec skill (#758, #767) — an end-to-end issue execution loop (worktree → visual HTML plan → implement → PR → /review → /simplify); #767 marks the issue in progress before implementing.
  • Dependenciesform-data 4.0.5 → 4.0.6 in platforms/web (#739).

v0.5.3 (2026-06-21)

Google Antigravity support, publish your sessions to a remote relay, and a clutch of daemon + adapter fixes.

Added

  • Google Antigravity adapter (#715) — one adapter covers both the agy CLI and the Antigravity IDE. Discovery is transcript-first, so PID-less IDE sessions are first-class.
  • Publish to relay (#718, #721, #722, #723) — a macOS toggle that streams this daemon’s sessions out to a standalone relay so you can watch them from another machine; pushing outbound means the daemon needs no inbound reachability (works behind NAT). The forwarder hot-reloads, so toggling Publish on/off or editing the URL/token applies to the running daemon with no relaunch and no interruption to session monitoring — whether the app spawned the daemon or adopted an already-running one.
  • Relay multi-tenant workspace isolation (#709, #713) — one relay can serve several daemons’ sessions kept apart per workspace.

Fixed

  • Antigravity context bar now renders (#719, #720) — tokens and the canonical model are read from the sibling conversation store rather than the transcript.
  • Claude Code sessions stay waiting across a daemon restart (#705, #706) — instead of flipping back to working.
  • Brand flame gradient legible on light backgrounds (#708).
  • Unified assistant-text truncation (#710) — every adapter truncates long assistant text the same way, via one tailer rule.

Changed / Docs

  • Exhaustive Source → watcher dispatch in buildAgentWatchers (#714) — a new source can’t silently land without a watcher.
  • Dashboard: extracted a collapsed-groups store (#712) and shared todo-snapshot reconciler (#711) — internal refactors.
  • Docs — onboarding-factory skill gains an overnight push-through mode (#717); the task-ETA figure was removed from the landing page (#704).

Technical appendix

  • Relay publish hot-reload (#722, #723) — a new relay.PublishController (core/adapters/outbound/relay/controller.go) owns the forwarder lifecycle: Apply(enabled, url, token) starts, stops, or reconfigures a single forwarder (cancel the ctx + start a fresh relay.NewForwarder), idempotent when the effective config is unchanged, mutex-guarded so concurrent PUTs serialize, blank URL counts as “off”. Always constructed and seeded once at startup from IRRLICHT_RELAY_URL / IRRLICHT_RELAY_TOKEN so headless/standalone daemons are unchanged. A loopback-only PUT /api/v1/relay/publish accepts {enabled,url,token}Apply → returns the resulting status (same shape as GET). On macOS, DaemonManager.publishSettingsDidChange() PUTs the config via a new PublishClient instead of relaunching, re-syncing on spawn and on adopt; the relay token moves from a spawn-time env var to the loopback PUT body (same 127.0.0.1 trust boundary as every other daemon endpoint). buildDaemonEnv strips inherited IRRLICHT_RELAY_URL / IRRLICHT_RELAY_TOKEN so an app-spawned daemon never self-seeds from a stale value, and the PUT retries so a single dropped request can’t strand the daemon.
  • Publish to relay (#718, #721) — the original macOS toggle (UserDefaults URL + Keychain token) with a PublishStatusMonitor poll surfacing the live link state (connecting / connected / auth_failed / disconnected) as a status dot.
  • Relay multi-tenant workspace isolation (#709, #713) — the relay partitions each daemon’s sessions per workspace so one relay hosts several daemons without cross-talk.
  • Antigravity adapter (#715) — one adapter onboards both the agy CLI and the Antigravity IDE; transcript-first discovery so PID=0 sessions are first-class, a multi-root source over the brain stores, and a path-based session id (constant transcript.jsonl).
  • Antigravity context bar (#719, #720) — tokens and the canonical model live in a sibling SQLite protobuf (conversations/<conv>.db, gen_metadata), not the transcript; read on turn_done.
  • Claude Code waiting-across-restart (#705, #706) — the waiting state is preserved across a daemon restart rather than recomputed back to working.
  • Adapter assistant-text truncation (#710) — collapsed into one shared tailer rule.
  • Daemon watcher dispatch (#714)buildAgentWatchers switched to an exhaustive Source dispatch so an unmapped source is an explicit gap, not a silent no-op.

v0.5.2 (2026-06-19)

Gemini CLI joins the lineup, the menu bar gets ~3× lighter under load, and context-pressure alerts become configurable.

Highlights

Gemini CLI is now a supported agent

Gemini CLI shown as a newly supported agent alongside Claude Code, Codex, Pi, Aider, OpenCode and Kiro CLI

Irrlicht now watches Gemini CLI sessions and reports them in the same working / waiting / ready vocabulary as every other agent. It reads Gemini’s JSONL transcripts under ~/.gemini/tmp, follows nested subagent chats, and tracks per-turn token deltas — no SDK, no config. The adapter ships at the alpha maturity stage with 44 recorded scenarios behind it.

Why it matters: if quota pushes you from Claude Code or Codex over to Gemini CLI, your monitoring follows you instead of going dark. (#668, #659, #679, #680, #681)

~3× less CPU when you’re running a lot of agents

Before/after CPU bar chart: ~100% of a core before, ~31% after, with 40 agents at 200 WebSocket pushes per second

With dozens of agents all ticking metrics at once, the macOS app used to redraw the whole session list on every single WebSocket message and saturate a CPU core. WebSocket-driven refreshes are now coalesced into one redraw per ~100ms window, and the per-row 1-second timers collapse into a single shared clock. State changes still flash through immediately, so the menu bar stays as responsive as ever.

Why it matters: running a fleet of agents no longer spins up your fan or drains your battery — measured ~100% → ~31% of a core under 40 agents. (#693, #690)

Context-pressure alerts you can tune

The Context pressure alert setting with a spoken-voice option and an Alert at 80% threshold field

The context-fill alert that warns you before a session runs out of room now has a threshold you set yourself — fire it at 80%, 90%, or wherever you like — instead of a fixed cutoff. The new alert lives in a tidied-up settings panel where less-common options collapse under an Advanced Settings group and in-progress features carry a Beta badge.

Why it matters: people who want an early heads-up and people who only want a last-second warning can each set the threshold that fits how they work. (#692, #689, #702, #694)

Added

  • Gemini CLI adapter (#668) — transcript-, process- and PID-aware monitoring for Gemini CLI sessions, at the alpha stage.
  • /ir:doc-review skill (#698, #691) — an objective, binary-criteria documentation audit that files one agent-ready GitHub issue per doc surface; supports a --report-only dry run.
  • Configurable context-fill alert threshold (#692, #689) — set the context-pressure alert as a percentage (default 80%) or an absolute token count; the former 95% critical tier folds into the single configurable threshold.
  • Settings: Advanced Settings group + Beta badges (#702, #694) — less-common controls collapse under a disclosure group, and in-progress features are marked Beta.
  • Full docs for every environment variable, hook, and focus/CLI endpoint (#699, #700, #701).

Fixed

  • Dead sessions now age out correctly (#684, #667) — a no-op refresh no longer bumps updated_at, so a crashed or abandoned agent stops looking alive.
  • Manual /compact shows the full lifecycle (#658, #656, #657) — working for the whole compaction window, back to ready when it finishes, via a Claude Code PreCompact hook and treating the manual compact_boundary as a turn boundary; existing installs pick up the hook on the next daemon restart, and an interrupted compaction is bounded by a timeout.
  • Installer downloads and verifies before removing the existing install (#654) — a failed download can’t leave you with no app.
  • gemini-cli: ESC-cancel and aborted-turn notices settle the turn (#659, #679, #680, #681) — plus a batch of consolidated detector edge cases.

Changed / Distribution

  • Gemini CLI moves from planned to alpha in the compatibility grids.
  • Release DMGs are codesigned before notarization (#652) — Gatekeeper’s primary-signature check now passes on the DMG file itself.
  • Dependency bumps (#686, #687, #688) — vite 8.0.14 → 8.0.16, form-data 4.0.5 → 4.0.6.

Technical appendix

  • gemini-cli adapter (#668) — new transcript + process + PID adapter at core/adapters/inbound/agents/geminicli/. Watches JSONL session files under ~/.gemini/tmp named session-<ts>-<first8hex> (session id is the filename stem, not the header UUID), with nested subagent chats at chats/<parent-uuid>/<child-uuid>.jsonl. Process discovery matches the bin/gemini command line (the OS process is node); cwd is read from the transcript body, forcing an EnrichNewSession pass off m.LastCWD. No explicit end-of-turn marker — a text-only assistant message maps to turn_done, benign info notices are ignored, and ESC-cancel / aborted-turn notices settle the turn (#659, #665). 44 scenarios recorded; consolidated detector fixes in #679 cover #660–#664, #676.
  • WebSocket refresh coalescing (#693, #690)session_updated pushes are batched into a single flushUIRefresh per ~100ms window that patches all dirty sessions in one map pass + one recompose (patchApiGroups(sessions:)) rather than O(K·N) per-message recomposes. Per-row 1Hz duration TimelineViews collapse into one shared DurationClock observed only by leaf labels. Context-pressure alerts ride rebuildSessionsFromMap and are deferred by at most one window; state-transition notifications still fire synchronously. Per-message debug print()s gated behind IRRLICHT_DEBUG. Added tools/wsload plus a deterministic coalescing regression test. Measured 40 sessions / 200 pushes/sec, release: ~100% → ~31% of a core.
  • Configurable context-fill alert threshold (#692, #689) — new ContextPressureThreshold value type (value + unit) as the single source of truth with a pure isExceeded(by:); SessionManager seeds the 80%/percent default, drives checkContextPressureAlerts off the configured value (fires once per session, re-arms on change), and adapts the notification title/body to the unit. The session-row badge reads the threshold via @AppStorage and collapses to one tier — the former 95% critical tier is removed. A token-count mode fires even when the model’s context window is unknown.
  • Settings reorganization (#702, #694) — less-common controls collapse under an Advanced Settings disclosure group; in-progress features carry a Beta badge.
  • Detector no-op refresh (#684, #667) — a refresh that produces no state change no longer advances updated_at, so the idle-sweep age-out timer isn’t reset on every poll and dead sessions transition out as designed.
  • claudecode manual /compact lifecycle (#658, #656, #657) — a PreCompact hook forces working during compaction and the manual compact_boundary releases to ready after; EnsureHooksInstalled adds the hook on daemon restart, and an interrupted compaction is bounded by a timeout.
  • Docs (#698, #691, #699, #700, #701) — the /ir:doc-review skill audits every doc surface against binary criteria and files one issue per surface; this release’s docs also gained full coverage of environment variables, hooks, focus endpoints, and the CLI tools.
  • Release tooling (#652, #653, #655)tools/build-release.sh codesigns the DMG between hdiutil create and notarytool submit so the stapled ticket covers the signed bytes; release-skill docs record the DMG-signature ordering and port-safe smoke/canary guidance.
  • Model aliases — codeburn BUILTIN_ALIASES sync ran at release time: no new entries needed (the upstream gpt-4.1 self-alias is a documented no-op); all eight LOCAL_OVERRIDE entries unchanged.

v0.5.1 (2026-06-07)

Ghost sessions and stuck states fixed: /compact strandings, Claude Code 2.1.168’s background daemon, and restart amnesia.

No new features this time — five fixes that together remove every known way a session row could lie to you: sessions stuck “working” after /compact, permanent ghost proc-… rows minted by Claude Code 2.1.168’s new background-daemon processes, a “?” agent icon from a startup race, and idle sessions resurrected as “working” by a daemon restart.

Fixed

  • Running /compact in an idle session no longer strands it in “working” (#641, #642) — the synthetic compact-summary transcript event is no longer mistaken for a real user turn.
  • Claude Code 2.1.168’s background-daemon processes no longer mint permanent ghost rows (#644, #648) — claude daemon run, --bg-pty-host PTY hosts, and --bg-spare spares are excluded by their command line, and ghosts persisted by earlier versions are retired automatically on the first scan after upgrading.
  • Ghost pre-session rows whose real session is bound to a sibling process are swept continuously (#645, #646) — not only at daemon startup, with a 90-second grace period so a freshly opened second agent in the same directory still gets its row.
  • Sessions created during a daemon-startup race no longer show “?” instead of the agent icon (#643, #647) — the adapter identity is backfilled on the next activity event, which also unblocks PID discovery and ghost cleanup for that session.
  • A daemon restart can no longer resurrect an idle session as permanently “working” (#649, #650) — the last event type is persisted in the metrics ledger (schema v4), older ledgers get a one-time full re-scan that also heals sessions stranded by the pre-fix parser, and dead background processes are purged from the ledger instead of resurrecting on every restart.

Docs

  • Corrected the documented permissions.json location (#640) — it lives in the daemon data dir, not Application Support.

Technical appendix

  • claudecode parser (#642)handleUserEvent now skips user events carrying isCompactSummary / isVisibleInTranscriptOnly (core/adapters/inbound/agents/claudecode/parser.go), so a manual /compact — which never starts a turn — can’t trip classifier rule 4 into working. Regression-covered by compaction_test.go against the recorded #641 transcript shape.
  • processlifecycle argv exclusion (#644, #648) — new ProcessObserver.ArgvOf(pid) port primitive (KERN_PROCARGS2 parse on darwin sharing the preamble decoder via procargs2ArgvOffset; /proc/<pid>/cmdline on linux; stub elsewhere) and a per-adapter agent.Process.ExcludeArgv predicate consulted by the scanner. claudecode declares IsInfraArgv: positional matches on daemon run / --bg-pty-host / --bg-spare argv elements — never substring scans, so prompts merely mentioning those tokens stay matched, and a nil argv is never excluded. Verdicts are cached per PID (argv is immutable for a process’s lifetime), nil reads are retried next poll, and the first excluded verdict emits a one-shot retirement removal that deletes pre-sessions persisted by pre-filter daemons.
  • detector sweep (#645, #646)sweepSupersededPreSessions now also runs on the CheckPIDLiveness tick instead of seed-only. findSupersedingSession stays the single matching predicate, extended to return the match kind: PID matches retire immediately; the CWD fallback only retires pre-sessions older than 90s whose superseding session has a live, distinct PID — guarding the #113 two-agents-one-cwd regression. Deletions route through deletedSessions so the scanner can’t flap-remint.
  • detector adapter backfill (#643, #647)processActivityLocked backfills state.Adapter from the watcher identity under WithSessionStateLock (#606 discipline), healing sessions created through the no-identity debounce/refresh fallback; since the PID-discovery retry passes state.Adapter, discovery and pre-session cleanup unblock on the same pass.
  • tailer ledger schema v4 (#649, #650)LastEventType is persisted in LedgerState and restored in SetLedgerState; LedgerSchemaVersion bumped to 4 with the load side aliasing the canonical const so write and validate can’t drift. Older-schema ledgers are discarded on load → one-time full re-scan under the current parser, which heals sessions already persisted as working over silent transcripts. The background-process liveness probe’s dead verdict now calls PurgeDeadBackgroundProcs, dropping phantom background_procs ledger entries that previously resurrected as background_process_count=1 on every restart.
  • claudecode permissions disclosure (#648) — the transcripts (observe) permission’s user-facing Touches/Detail text now discloses the process scanning it has always gated: reading the working directory and command line of running claude processes.
  • Model aliases — codeburn BUILTIN_ALIASES sync ran at release time: no new entries needed (the upstream gpt-4.1 self-alias is a documented no-op); all eight LOCAL_OVERRIDE entries unchanged.

v0.5.0 (2026-06-06)

Consent-first permissions, agent-authored task ETAs, and Kiro CLI joins the watch list.

Highlights

Nothing is read or modified until you grant it

Permission wizard listing Claude Code and Codex with per-permission toggles, what each permission touches, and an Apply button

Every read and modification irrlicht performs — transcript tailing, hook installs, statusline wraps, database polling — is now a declared per-agent permission behind explicit consent. When irrlicht detects a coding agent it hasn’t asked about, a wizard appears on whichever surface you’re looking at (macOS app or web dashboard), shows exactly what each permission touches, and exercises nothing until you hit Apply. Revoking actively undoes: hooks uninstall, watchers stop.

Why it matters: you can see — and veto — every way irrlicht touches your system, per agent, before it happens.

Heads-up on upgrade: existing installs see the wizard once after updating, and monitoring is paused until you answer it. Notifications are now opt-in by default too. (#570, #571, #579, #583)

Know when your agent will be done

Dashboard session list where a working session shows a ~9m-left task ETA chip beside its cost, with task progress 3/5 underneath

Working sessions now carry a task-completion ETA chip — “~9m left” — in the web dashboard and the macOS menu bar. The agent authors its own progress estimate and emits it as a hidden in-band marker; irrlicht parses it read-only, projects a completion time from the measured pace, and degrades honestly: a range while the rate is barely measurable, dimming when the last report goes stale, and a tasks-derived fallback when no marker arrives at all.

Why it matters: “is it nearly done or should I grab lunch?” is now answered at a glance, for every working session, without asking the agent. (#558, #567, #605, #621, #626)

Kiro CLI joins the watch list

A Kiro CLI session row showing a live context bar, task progress 1/2, and the model name

Irrlicht now watches AWS Kiro CLI sessions: live working/waiting/ready state, project + branch resolution, PID binding, task progress synthesized from Kiro’s todo lists, and model + context-window metrics read from Kiro’s session sidecar.

Why it matters: if Kiro is one of your agents, it now shows up beside Claude Code, Codex, and the rest — same row anatomy, same states, no special casing. (#280, #590, #603, #613)

Added

  • irrlicht-ls reaches dashboard parity and ships in the default install (#554, #580, #609) — hierarchical subagent display, project group headers, color-coded context utilization, cost and adapter columns, task-progress detail lines, --format json, and --id/--state/--project/--adapter filters; the PKG now symlinks it into /usr/local/bin.
  • Menu-bar attention icon for pending permission items (#607) — the macOS icon signals when an agent is waiting on a permission decision.
  • Gas Town: unified rig/role/cost display in the session list (#559, #560).

Fixed

  • Gas Town polling no longer spikes CPU (#557, #575) — event-driven polling replaces the hot loop.
  • Stale subagent badges (#601, #600, #637) — waiting-path cleanup, observable deletions, and push Seq-gap detection with immediate re-hydration keep parent badges truthful.
  • Sessions survive daemon restarts mid-run (#576, #584) — stale transcripts owned by a live process are rescued at backfill instead of being dropped.
  • Web: rows of a single collapsed group render (#564, #566).
  • macOS login item re-registers on every launch (#562, #563) — a self-heal for installs where the login item silently vanished.
  • macOS: premium voices whose names carry a quality suffix match again (#569).
  • First launch after install no longer races Gatekeeper (#553) — the installer pre-warms assessment so the app is ready before the daemon wait expires.
  • Ready-state icon stays inside its layout box (#596, #598).
  • Workflow-tool subagents link to their parent session (#565, #635) — and workflow run journals are no longer mistaken for transcripts.
  • Kiro: permission-gated edit-tool prompts classify case-insensitively (#588, #612).
  • Claude Code: task IDs are taken from TaskCreate results (#620) — and pruned task lists clear instead of lingering.

Changed

  • The eyed-flame mark lands everywhere (#587, #594) — app icon, menu bar, favicon, landing page, and docs all swap to the new mark; shape-only, the state-color system is unchanged.
  • Notifications are opt-in by default (#579) — part of the consent-first rollout; kitty click-to-focus asks before using remote control.

Docs

  • macOS setup guide on the site (#574).

Technical appendix

  • Consent-first permission architecture (#570, #571, #579, #583) — new core/domain/permission package: State/Kind/Set with absent-key-means-pending as the upgrade path. Adapters declare Permissions with Apply/Remove effect closures — claude-code: transcripts/hooks/statusline/instructions; codex + pi: transcripts; aider: history; opencode: database; gastown: state; launcher: env; kitty: remote-control. PermissionService exercises grants, undoes revokes, persists permissions.json, arbitrates wizard answers across surfaces (first answer wins), and runs a detection poller (pgrep / GT_ROOT stat probe, no session reads) only while something is pending in ask mode. GET /api/v1/permissions + POST /api/v1/permissions/answer; hook and statusline receivers drop payloads while ungranted. IRRLICHT_PERMISSION_MODE=grant-all auto-grants for demo/record/test daemons, strictly in-memory. --uninstall-hooks records hooks=denied so a restart cannot silently reinstall. The task-eta CLAUDE.md managed block became the instructions permission (#577, #583).
  • Task-completion ETA pipeline (#558, #567, #605, #621, #626, #619, #638, #620) — the agent emits a hidden irrlicht-eta marker in-band; the claudecode adapter scans full text blocks tolerantly (key drift accepted, absurd values rejected, latest valid wins). TaskEstimate mirrors through tailer → domain → metrics adapter; ForecastTaskCompletion projects from measured rate anchored at session start. Chip render rules: hidden unless working with reported progress, range below half completion, stale dimming past 3 min (web taskEtaPresentation, mirrored in SessionListView.swift). Hardening: tasks-derived fallback + hook marker carrier + 0/N chip (#605), pinned-high range between markers (#621), subagent-aware aggregation with marker > tasks > subagents precedence (#626), mandatory per-phase Bash-description carrier v3/v4 (#619, #638), authoritative TaskCreate IDs (#620).
  • Kiro CLI adapter (#280, #590, #603, #613, #612)FilesUnderRoot/JSONLineParser adapter watching ~/.kiro/sessions/cli/<uuid>.jsonl (verified against kiro-cli 2.5.1). No explicit end-of-turn marker: a text-only AssistantMessage maps to turn_done. The transcript carries no cwd — fallback to the <uuid>.json metadata sidecar via token-walk; PID discovery via pgrep -x kiro-cli. Sidecar MetricsReader surfaces model + context window live (#603); tasks synthesized from todo_list create/complete (#613); permission-gated edit-tool classification is case-insensitive (#612). Onboarded as a matrix column with an interactive tmux driver.
  • Session-detection internals — backfill rescues stale transcripts owned by a live process (#584); BuildAgentGroups no longer mutates its input (#573); PID assignment synchronized with the detector loop (#618) and pidmanager sweep paths brought under assignMu (#634); dead ContentChars pipeline removed, kiro tool-status semantics pinned (#595); Workflow-tool subagents link to their parent and run journals are excluded from discovery (#635).
  • Web client — single collapsed group renders rows (#566); push Seq-gap detection with immediate re-hydrate (#637); stale subagent state fixes: waiting-path cleanup, summary ordering, observable deletions, history leak (#601).
  • Gas Town — event-driven polling kills the CPU spike (#575); unified rig/role/cost block (#560); gt fetch timeouts logged instead of silent fallback (#633).
  • macOS app — attention icon for pending permission items (#607); ready-state icon clamp (#598); premium-voice quality-suffix matching (#569); login-item reconcile on launch (#563); swift test clean on main (#585); GroupView snapshots pinned to dark aqua (#610).
  • CLI + installirrlicht-ls dashboard parity, file-only, no daemon required (#580); ships in the bundle with a PKG postinstall symlink into /usr/local/bin (#609, #608); curl installer pre-warms Gatekeeper (#553).
  • Tooling / CI / tests — gofmt sweep + CI gofmt gate (#629, #632, #623); de-flakes: FSWatcher timeout (#630), ParentBadgeCleared polling (#624, #631), gastown replay settle window (#611), deterministic cursor-GC aging (#614), child-ready polling (#578, #582). Onboarding-factory (internal): recordings as single source of truth (#556), paginated coverage-matrix viewer (#591), kiro-cli and claudecode column recordings (#627, #636). Release-skill docs record build-release.sh as authoritative (#552).
  • Model aliases — codeburn BUILTIN_ALIASES sync ran at release time: no new entries; all eight LOCAL_OVERRIDE entries unchanged upstream.

v0.4.8 (2026-05-30)

Watch your agents from any machine — the irrlichtrelay ships, alongside a Linux daemon and per-provider quota tracking.

Highlights

Watch every machine from one place

Three irrlichd daemons pushing over authenticated wss to a standalone irrlichtrelay, which fans out to the macOS app and web dashboard

irrlichtrelay is a new standalone binary. Point each machine’s daemon at it and your macOS app and web dashboard show every session from every host in one aggregated list — your laptop, a Linux box, a VM — without being at any of them. The link is secured end-to-end: TLS/wss, bearer-token auth (with a token-minting CLI), an origin allowlist, and per-IP / per-connection caps.

Why it matters: you no longer have to sit at the machine running an agent to see what it’s doing — one relay, every host, from anywhere, over an authenticated connection. (#483, #547, #544, #548, #519)

Added

  • Linux daemon (#482, #478) — irrlichd now builds and runs on Linux (amd64 + arm64), daemon-only, behind a portable ProcessObserver observation layer; install via the same curl … | sh one-liner.
  • Per-provider windowed usage spend + subscription empty-state (#441, #386) — usage and quota are tracked per provider, with a clean empty-state before the first window of data lands.
  • OpenCode inherits OpenAI rate limits (#424) — OpenCode sessions pick up the OpenAI rate-limit window via the JWT account_id, so quota burndown is accurate for OpenCode→OpenAI users.
  • Background Bash processes hold a session working (#450, #452) — a claudecode session with a live backgrounded process stays working until it finishes, instead of falsely settling to ready.
  • Editable relay URL + token fields in macOS Settings (#550) — the relay endpoint and bearer token are now editable text fields in the app’s Settings.

Fixed

  • Surface claudecode tool-use permission prompts as waiting (#490) — a session paused on a permission prompt now flips to waiting instead of looking busy.
  • Codex: settle interrupted turns (#464) — a turn_aborted is treated as turn-end, so an interrupted Codex turn settles instead of hanging in working.
  • Price Warp / Cursor / Antigravity sessions correctly — added 22 new model aliases synced from codeburn (Warp auto-routers and codex display strings, Cursor dash-form reasoning tiers, Antigravity Gemini 3.5 Flash, and human-readable display-name forms), so these frontends price at real dollars instead of $0. Closes the alias-sync gap deferred in v0.4.7.
  • claudecode: strip trailing period from background-process output path (#501).

Changed / Distribution

  • Web dashboard split into three files (#418) — index.html + irrlicht.css + irrlicht.js, with Vitest unit tests; the daemon serves them from disk, no codegen.
  • Linux replay Dockerfile relocated to tools/ (#499).

Technical appendix

  • Relay (irrlichtrelay) — v0 round-trip daemon → relay → macOS + web (#483); v1·A secure exposure — TLS/wss, bearer-token auth + token-minting CLI, origin allowlist (#547); v1·B hardening — websocket read-limit + per-IP / per-connection caps (#544); v1 epic C–G — compound session keying across hosts, origin glyph, deploy artifacts, fade-on-disconnect, coding-factory demo (#548); macOS auto-connect on relay URL, connection-status dot, restored ⓘ + scroll (#519); editable relay URL + token Settings fields (#550); live cross-host round-trip testbed under examples/ (#486).
  • Cross-platform observation layer — new ports.ProcessObserver seam with build-tagged process_{darwin,linux,other}.go; the adapters are unchanged (internal seam, not DI), and a Linux daemon falls out — Windows is now “add one file” (#482, #478).
  • Quota / pricing — per-provider windowed usage spend + subscription empty-state (#441, #386); OpenCode→OpenAI rate-limit inheritance via JWT account_id (#424); 22 new frontend aliases in core/pkg/capacity/aliases.go synced from codeburn’s BUILTIN_ALIASES. Five Warp codex aliases resolve to gpt-5.3-codex, which LiteLLM does not yet price — they log on miss and are flagged for a follow-up sync rather than blocking the release.
  • Session detection — claudecode tool-use permission prompts surface as waiting (#490); backgrounded-Bash tracking keeps the session working and recognizes TaskOutput / <task-notification> completion in SDK-harnessed claude as well as BashOutput / KillShell in bare claude (#450, #452, #501); codex turn_aborted treated as turn-end (#464).
  • Onboarding factory (internal fixture tooling — no runtime impact) — eight-phase rewrite of the scenario × adapter coverage matrix: tools/agent-onboardingtools/onboarding-factory, with the of CLI as the sole writer of everything under replaydata/ (#522–#530). Schema cutover to per-scenario shards — one catalog (scenarios.json) plus per-cell metadata.json in id-prefixed folders, every recording moved under recordings/<name>/ (#510, #511, #514, #524). New 4-verb ir:onboarding-factory skill retiring ir:onboard-agent; of validate as a CI integrity gate. Per-adapter column recordings landed across claudecode, codex, opencode, aider, and pi (#515, #517, #518, #504, #489, #477, and others).
  • Tests / CI — per-worktree state isolation + headless daemon startup smoke test (#448); hermetic replay so byte-identity goldens reproduce (#447, #451); fswatcher flake fixes (#485, #487); three-dot diff in the replaydata deletion guard (#466); web dashboard Vitest suite (#418).

v0.4.7 (2026-05-22)

macOS distribution levels up: Sparkle auto-updates and a notarized DMG, plus OpenCode task progress reaches dashboard parity.

Added

  • Sparkle 2.x auto-update integration (#413) — the app now checks for updates on launch and offers a one-click upgrade; a manual “Check for Updates…” button lives in both the popover and the Settings panel. Signed appcast served from irrlicht.io/appcast.xml. First Sparkle-enabled release is v0.4.7; users on v0.4.6 must do one manual upgrade before auto-updates begin.
  • Web dashboard ports the macOS overlay’s provider quota chip (#417, closes #387) — popover and dashboard now show identical 5h/7d subscription bars (or cumulative usage spend) for the same /api/v1/sessions response. Settings modal gains a Provider-quota section with the same auto/subscription/usage controls as the macOS app.
  • OpenCode todowrite snapshots surface as task-progress dots (#410, closes #277) — parity with Claude Code’s TaskCreate/TaskUpdate pipeline. Content-keyed deltas survive OpenCode’s lack of stable todo IDs.

Fixed

  • OpenCode working→ready latency (#412, closes #278) — sessions flip from working to ready ~2.5 s faster.
  • Claude Code wrapped command preserves user statusLine output (#404).
  • Curl installer survives GitHub API rate limits (#401) — falls back to the redirect-based latest-release URL when the API returns 403.

Changed / Distribution

  • Developer-ID signed + Apple-notarized DMG (#406, #409, closes #233) — first launch through the curl installer or Homebrew cask no longer trips Gatekeeper; the quarantine-strip workarounds in site/install.sh and the cask postflight are gone.
  • com.apple.security.get-task-allow removed from production entitlements (#407, #415) — Apple notarization rejects binaries with the debug entitlement set to true. Build-time + canary-install guards both assert it is absent on the shipping artifact.

Docs

  • Kitty terminal-host docs (#402) — clarify that font/config changes require a full app restart, not just a reload.

Removed

  • tools/coverage-viewer and tools/find-flicker-sessions.sh deleted (#411, #414) — superseded by the unified agent-onboarding viewer at tools/agent-onboarding.

Internal

  • /ir:onboard-agent pipeline lands (#328, #408) — drives every adapter through a shared agent-agnostic scenario catalogue keyed by capabilities; records lifecycle fixtures and surfaces material drift vs. committed recordings. First-class opencode driver added in this release.

Deferred

  • Model alias map sync from codeburn skipped — 13 upstream additions remain unsynced; several target canonicals (e.g. gpt-5.3-codex) are not yet in LiteLLM’s pricing table, so adding them now would still resolve to zero-cost capacity. Will land in a follow-up once LiteLLM catches up.

Technical appendix

  • Sparkle 2.x integration (#413) — Sparkle 2.9.2 added via SwiftPM with a thin UpdateManager wrapping SPUStandardUpdaterController. EdDSA public key nKRcUPAmK6syLFEvp9O30FFvjhTIfGxYVv/6y8zpZI0= baked into both the tracked Info.plist and the tools/build-release.sh heredoc. tools/build-release.sh copies Sparkle.framework into Contents/Frameworks/, adds the @executable_path/../Frameworks rpath, and signs the nested helpers (Downloader.xpc, Installer.xpc, Updater.app, Autoupdate, framework binary) deepest-first before the outer bundle. New site/appcast.xml ships with one signed entry for v0.4.6 so the feed serves a valid response immediately. After installing v0.4.7, drag the app to /Applications/ — Sparkle refuses to self-update from a Gatekeeper-translocated ~/Downloads/ path.
  • Notarized + DevID signed DMG (#406, #409)tools/build-release.sh signs with the Developer ID cert + hardened runtime + entitlements when DEVELOPER_ID is set; notarizes and staples when NOTARYTOOL_KEYCHAIN_PROFILE is also set. The build exits 1 if DEVELOPER_ID is set without NOTARYTOOL_KEYCHAIN_PROFILE. FocusMonitor.swift detects the DevID signature at runtime and loads INFocusStatusCenter via NSClassFromString rather than statically linking Intents.framework.
  • get-task-allow removed from prod entitlements (#407, #415)Irrlicht-dev.entitlements still carries it for local Xcode debug. The verification uses value-aware XPath rather than key-grep so an explicit <false/> doesn’t false-fail; the canary install check re-runs the assertion against the actually-shipping bundle.
  • OpenCode todowrite (#410)opencode.Parser is stateful: each snapshot translates into the minimal TaskCreate/TaskUpdate delta sequence the tailer expects, content-keyed because OpenCode’s todos carry no stable IDs. A ~15-line accumulator in opencode/metrics.go populates metrics.Tasks on the SQLite metrics path. New TestComputeMetrics_TodowriteTasks drives querySessionMetrics with a synthetic SQLite DB.
  • OpenCode latency (#412) — eliminates a ~2.5 s tail where the SQLite metrics path waited on a debounce timer that no longer applied once the session went idle. New core/application/services/debounce_test.go pins the timing invariant.
  • /ir:onboard-agent pipeline (#328, #408) — unifies fixture refresh + adapter bootstrap + new-agent onboarding into one skill. Drives the real CLI through a shared agent-agnostic scenario catalogue keyed by requires: [capability]; adapters declare Capabilities and matrix cells fall out automatically. New drive-opencode-interactive.sh driver: each send step is an opencode run --session <id> subprocess; post-run SQLite export to the parts JSONL the parser expects.
  • Installer rate-limit hardening (#401)site/install.sh falls back to the /releases/latest redirect URL when the GitHub API returns 403.

v0.4.6 (2026-05-17)

Public roadmap page lands, with screenshots and concept tiles for every release row

Highlights

Public roadmap at /docs/roadmap.html

Roadmap page top — Horizon and v1.0 entries on a dashed future spine

A chronological newest-at-top timeline shows every shipped release with big milestones, italic notes for narrower changes, and a compact ALSO SHIPPED roll-call of every other issue/PR. Future versions (v0.5 → v1.0 plus horizon) are bold guesses against the Platform Rollout wiki, with concept tiles for the in-flight relay-v0 work, the planned VS Code panel, iOS / iPadOS, Android + Apple watch, and the v1.0 second-desktop + hosted relay launch.

Why it matters: where the project is going is now discoverable without spelunking GitHub issues. Linked from every docs sidebar, the landing footer, and the CHANGELOG.md preamble. Future releases will migrate items from above the today line down across it as they ship. (#395)

Added

  • Moonshot Kimi family in the alias mapkimi-auto, kimi-code, kimi-for-coding resolve to moonshot.kimi-k2-thinking in LiteLLM (codeburn sync). Sessions on Kimi-routed frontends now price at non-zero.

Changed

  • Brand: gradient flame refresh propagates across the remaining surfaces (#394) — picks up the docs sidebar dot, design-system preview tiles, and the few last places where the old wisp survived the v0.4.5 rollout.
  • Working-state icon: heartbeat halo → breathing solid dot (#393) — the SMIL halo animation was distracting; the solid dot with a gentle opacity breathe reads cleaner at 14px in the menu bar.
  • Web dashboard: subagent dot-matrix row dropped to match the macOS overlay (#397) — the 89/96-style dot strip was a web-only detail that no longer matched the unified row anatomy shipped in v0.4.5.

Fixed

  • Claude Code task list: prune entries absent from task_reminder snapshots (#396) — the in-memory task list is reconciled against the authoritative snapshot so dropped tasks no longer linger as in_progress indefinitely.

Docs / Tooling

  • Release-flow friction removed from CLAUDE.md (#392).
  • /ir:release release-notes template + Step 4a-roadmap mechanics — three-layer release notes (Headline → ≤ 3 Highlights with screenshots → Also → Technical appendix) at .claude/skills/ir:release/release-notes-template.md. Step 4a-roadmap codifies the roadmap-update protocol (Python recipe for ALSO SHIPPED, row-shape templates, today-line bump, pill rotation for minor releases).

Technical appendix

  • Public roadmap page (#395) — site/docs/roadmap.html is a single file with all CSS scoped inline (no docs.css edits). Pure-CSS git-branch via pseudo-elements; ALSO SHIPPED list extracted via git log <prev-tag>..<this-tag> with hex-color and natural-language noise filters. Linked from every docs/*.html sidebar (new "Project" section), the landing-page footer, and the CHANGELOG.md preamble.
  • Roadmap concept tiles — six future-section composites generated via Python + PIL + rsvg-convert using the new gradient flame brand. Tiles under assets/roadmap/v<ver>/; CSS .release-figure renders future tiles at 0.78 opacity and past at 0.92.
  • Release-notes template + Step 4a-roadmap expansion — three-layer template with worked example from v0.4.5. Step 2 / Step 4a in the skill updated to consume it; Step 4a-img enforces the highlight-image asset checklist; Step 4a-roadmap codifies a 5-substep protocol (extract refs, insert row, bump today, pill rotation, verify).
  • Brand refresh continuation (#394) — propagates the v0.4.5 single-path gradient flame to surfaces the original PR missed: docs sidebar .dot swapped for an inline gradient flame SVG, design-system preview tiles updated, AppIcon iconset regenerated via the reproducible tools/build-app-icon.sh builder.
  • Working-state breathing dot (#393) — svgIcons.working swaps the SMIL heartbeat halo for a solid circle with a CSS opacity breathe (no animation under prefers-reduced-motion). macOS SessionStateIcon updated to match.
  • Web subagent dot-matrix row removed (#397) — the 89/96 progress-dot strip below the session row no longer renders; macOS overlay never had it, this brings web in line.
  • Claude Code task-list pruning (#396) — the tailer treats task_reminder attachments as authoritative; any local in_progress missing from the snapshot is demoted to completed. Mirrors the v0.3.12 phantom-in_progress fix (#289) but for the absent-task case.
  • Model aliases — codeburn sync — adds kimi-auto, kimi-code, kimi-for-coding to core/pkg/capacity/aliases.go, all LOCAL_OVERRIDE to moonshot.kimi-k2-thinking since LiteLLM only ships the dotted-prefix key. Five changed entries in the codeburn diff are intentional LOCAL_OVERRIDEs — not updated.

v0.4.5 (2026-05-16)

Added

  • Pro / Max subscription burn-rate forecast in the macOS overlay (#309, #379) — Two ingestion paths cover the major subscription combos: Codex CLI's token_count events carry rate_limits in-band (parser extension), and Claude Code's statusline JSON is captured via a new statusLine.command that POSTs to /api/v1/hooks/claudecode/statusline. The statusline chain is wrapped in bash -c '...' so the bash-only tee >(...) process substitution survives Claude Code's POSIX-sh invocation; v1 wraps installed before the fix are migrated on the next daemon start without losing the user's original statusline command. Snapshots flow into SessionMetrics; a 5-sample rolling history feeds a linear-projection forecast; the menu-bar header shows a provider chip (Anthropic / OpenAI inferred from plan_type or adapter) with stacked 5h / 7d horizontal progress bars carrying percent + reset time inline, coloured via the existing pressure-level ramp. Version moves out of the header and into a small footer in Settings so the header slot is always reserved for quota data
  • Brand: new single-path gradient flame across design system + macOS app (#388) — Replaces the old "outer + inner-highlight + tear ellipse" wisp (viewBox 32) with a single-path silhouette designed at viewBox 1254, plus per-state gradient treatments (purple / orange / green) and a flat mono + black/white variant set. Six source SVGs land under assets/irrlicht_flame_*.svg; the design-system preview gains a "state colors" tile row and refreshed palette swatches per hue; OffFlameImage.swift is rescaled to viewBox 1254 with the single path and the unused core gradient dropped; AppIcon.icns is regenerated via a new reproducible builder tools/build-app-icon.sh (rsvg-convert with qlmanage fallback) that produces a byte-identical .icns on re-runs. The landing-page navbar swaps its 8×8 .sparkle dot for an inline 22×22 gradient flame SVG using the new mark, keeping the breathe animation
  • Unify per-row state icons across web and macOS (#382) — Replaces the dashed circle (web) and SF Symbol hammer/hourglass (macOS) with a shared visual vocabulary: working = heartbeat halo (purple), waiting = two-bar pause (orange), ready = unchanged checkmark. Web swaps svgIcons.working for an inline SMIL heartbeat-halo SVG, drops the now-unused .working spin rules, and hides the animated halo under prefers-reduced-motion. macOS adds a new SessionStateIcon SwiftUI view that renders the halo natively (repeatForever, honors accessibilityReduceMotion) and the pause bars as two RoundedRectangles. Snapshot fixtures impacted by the icon swap are re-recorded. Closes #380
  • /ir:triage assigns release milestones on ready-for-agent (#375) — Triage was applying labels but leaving milestone empty, so every ready-for-agent issue still needed a manual maintainer pass to land in a release bucket. The skill now picks a milestone from priority and creates one if the bucket doesn't exist: Priority-HighACTIVE (in-progress release), Priority-MediumNEXT, Priority-LowFUTURE. The three buckets are computed once per run from the latest published release tag (gh release view --json tagName), so the skill survives a version bump without an edit. Skipped on needs-info / wontfix and on issues already carrying a maintainer-set milestone. Bundled into the existing gh issue edit call. The brief template gains a **Milestone:** line

Fixed

  • Detect imperative waiting cues without a literal ? (#381, #383) — IsWaitingForUserInput now classifies turns ending with an imperative ask ("let me know if it's right", "verify locally and reply with the diff", "awaiting your go-ahead before I merge") as waiting instead of falling through to ready. Adds ExtractWaitingCue alongside the existing ExtractQuestionSnippet — both are OR'd in the public predicate, leaving snippet semantics unchanged. The cue regex set comes from issue #381's coverage matrix: five buckets (direct asks, approval framings, action gates, curated imperatives, trailing soft asks), all running against the trailing 1–2 sentences so earlier paragraph content can't trigger false positives. ExtractWaitingCue walks the tail newest-first so the more recent sentence is returned when both the last and second-to-last sentence match. State-classifier Rule 2a now reports turn ended with question or cue → waiting. Adds the agent-imperative-pending replay scenario and refreshes five existing claudecode goldens
  • Web: migrate sessions when project_name lands after the first push (#377) — The web dashboard's applySessionUpdate updated agent fields in place but never reconciled group membership when project_name changed. So if the first session_created WS push arrived before git-metadata enrichment had filled in project_name, the agent was placed in the "unknown" bucket and stayed there permanently. The macOS app dodges this because patchApiGroups schedules a full re-hydration on a missed session id; the web view had no equivalent path. Fix: on a session_updated for an existing top-level entry, detect when the new project_name points at a different group than where the agent currently lives, splice it out, find-or-create the target group, and clean up the source group if it goes empty. Children inherit their parent's group and have their sessionIndex entries re-anchored via indexChildren
  • Pricing: normalize frontend-rewritten model names — 63-entry alias map synced from codeburn (#371, #376) — Sessions running through frontends that rewrite the model name before the LLM call (Cursor's claude-4.6-opus-fast-mode, OMP's anthropic--claude-4.6-opus, Antigravity's gemini-3.1-pro-high) priced at $0 because CapacityManager.GetModelCapacity did an exact LiteLLM-key lookup with no normalization. Adds core/pkg/capacity/aliases.go (a 63-entry alias map ported from codeburn's BUILTIN_ALIASES) and resolves it inside GetModelCapacity before the existing lookup. Exact-match only; no prefix/fuzzy logic. Five aliases whose codeburn canonicals are missing from LiteLLM are marked with // LOCAL_OVERRIDE: <reason> and re-pointed to LiteLLM-present keys. Adds the /ir:refresh-aliases skill (standalone-PR + release-inline modes) that diffs against codeburn upstream, wired into /ir:release as Step 1.5 (fail-soft). Verified end-to-end via replay against claudecode/06-cost-calculation-07f5cca9: byte-identical $86.83 cost vs the canonical baseline

Docs

  • List VS Code extension as a planned platform (#370) — Adds "VS Code extension" between CLI (alpha) and Linux (planned) in both the landing-page Platforms column (site/index.html) and the README Platforms table; the README row links to the tracking issue (#350). Groups editor/UI surfaces (menu bar, web, CLI, VS Code) before OS-target planned items (Linux, Windows, iOS/iPadOS)

v0.4.4 (2026-05-16)

Added

  • Web dashboard reaches overlay parity (beta) (#354) — The dashboard served at http://127.0.0.1:7837 now reaches visual and functional parity with the macOS menu-bar overlay, in service of the upcoming VS Code panel (#350) and future relay-served clients. Row anatomy matches SessionListView.swift: state · num · subagent badge · branch · ctx-bar (with token overlay inside) · cost · model · adapter icon, with strict flex-wrap: nowrap, branch + bar growing into available slack, and a 22 px row height that survives the Context ↔ history mode toggle without shifting rows. Stacked below the row are the waiting question, task progress dots, the right-anchored 89/96-style subagent dot-matrix, and the pressure alert. The header carries the version chip, aggregated state icons (≤ 3 inline, else "N sessions"), a view-mode cycle (Context / 1 Min / 10 Min / 60 Min), a theme toggle (☀/☾), a settings cog (⚙), and a 3-state connection indicator (watching / reconnecting / disconnected) with a banner when the daemon connection drops. A new Settings modal mirrors SettingsView.swift minus the bits the web can't do — show-cost, debug mode (toggles row-id/row-elapsed/row-created chips), and three notification toggles (ready / waiting / context pressure) that fire new Notification(...) only when the dashboard isn't the focused tab. Themes pick icon_svg_light vs icon_svg_dark per resolved theme so the Codex icon is now visible on both. Group headers cycle day/week/month/year on click, persist their collapse state in localStorage, and Gas Town groups get a ⛽ glyph. A responsive cascade keeps the context bar — the load-bearing signal — visible at any width: cost drops below 420 px, model below 360, adapter icon below 320. On the daemon side this added exactly one route: GET /api/v1/version returning {"version": "..."}. The previous timeline-heatmap and Raw-JSON tab — both debug surfaces the overlay never had — are removed. Marked beta in README.md and site/docs/quickstart.html

Fixed

  • Codex split-event token_count priced at $0 (#361) — Codex sessions reported $0 cost because the parser emitted PerTurnContribution{Model: ""} on every token_count event — Codex splits the model name onto turn_context and the usage cursor onto token_count, so the tailer bucketed deltas under cumByModel[""], which has no pricing. In applyContribution, contributions with no model now fall back to t.metrics.ModelName (already maintained by applyModelMetadata from turn_context) so pricing and the UI's model display read the same field by construction. LedgerState persists ModelName so the fallback still works on the first token_count after a daemon restart. Six Codex replay goldens regenerated with the corrected pricing
  • Adapters honor agent-CLI env vars for relocated session dirs (#349) — Three coding-agent adapters now respect their upstream env-var convention for relocating session transcripts: Pi via PI_CODING_AGENT_SESSION_DIR (the absolute session dir itself), Claude Code via CLAUDE_CONFIG_DIR with /projects appended (transcripts only — the PID-metadata directory at ~/.claude/sessions/ remains hardcoded because that hunk is unverified), and Codex via CODEX_HOME with /sessions appended. fswatcher.New treats an absolute dir as-is and falls back to $HOME-relative for relative paths. Non-absolute env values (relative paths, unexpanded ~, trailing slashes) are filepath.Clean-ed and rejected with a log.Printf warning. Env vars are read once at Agent() construction, so a daemon restart is required after changing them. Default-install behavior is unchanged. Aider and OpenCode are intentionally untouched

Docs

  • Subtle mascot illustrations + sentence-level correctness audit (#365) — Adds 9 thematically-matched flame-mascot figures across site/docs/*.html, designed to read on both light and dark themes (640 px WebP @ q88, ~908 KB total, ~6% of the 14.4 MB PNG source). Floats reflow to centered blocks at ≤ 720 px. Three parallel review passes then cross-checked every factual claim in the docs against the current Go source and fixed 17 stale or wrong statements: context-pressure thresholds corrected to 0–60% / 60–80% / 80–90% / 90%+ to match core/pkg/tailer/tailer_metrics.go; menu-bar icon called a "sparkle" corrected to "flame"; six references to the deleted ./validate.sh script replaced with go test ./core/... -race -count=1 + tools/replay-fixtures.sh; processscanner/processlifecycle/ rename propagated; HTTP/WS diagram updated to /api/v1/sessions(/stream); adapter field now lists all 5 wired adapters; never-implemented --format json / --id <prefix> CLI flag docs removed; log filename corrected to events.log; lsof command updated to include the -Fn flag actually used

Distribution

  • Five release-skill guardrails against v0.4.3's shipping defect (#360) — v0.4.3 shipped a binary that crashed on every end-user install (#357 root-caused; #356 + #358 fixed). Five further guardrails prevent recurrence. (1) Drop NSFocusStatusUsageDescription from the Info.plist template — with Intents.framework statically linked, TCC preflights kTCCServiceListenEvent at process startup and SIGABRTs ad-hoc-signed builds regardless of the usage description. (2) Rewrite the coupling-rule prose with a per-key include/exclude table explaining the three-way relationship between AMFI-gated entitlements, TCC-gated NS*UsageDescription keys, and the linked frameworks themselves (the structural lever). (3) Add a framework-link audit between Swift build and signing: otool -L against a FORBIDDEN_FRAMEWORKS list aborts the release on any match. (4) Strengthen smoke-test failure semantics with explicit "DO NOT SHIP" framing, a tccutil reset prereq to clear poisoned cache, and a four-step debugging checklist. (5) Replace Step 9 verify with a real end-to-end install canary that runs curl ... | sh against the just-published release and validates via pgrep — the installer's "Launching... ✓" lies when AMFI kills the app immediately

v0.4.3 (2026-05-15)

Note: Assets re-cut later the same day. The original v0.4.3 binary statically linked Intents.framework via FocusMonitor.swift's direct INFocusStatusCenter calls, which made macOS TCC preflight kTCCServiceListenEvent at process startup and AMFI/TCC SIGABRT the ad-hoc-signed binary with launchd POSIX 153 on every end-user install. The re-cut moves FocusMonitor to dynamic dispatch (NSClassFromString + SecCodeCopySigningInformation gate) so Intents.framework is never loaded on ad-hoc builds. DND-aware notification silencing is paused until Developer-ID signing lands (#233); restoration tracked in #357. Release-skill regression fix that prevents recurrence: #356.

Added

  • macOS: autostart Irrlicht.app at login, on by default (#343) — On first launch the app registers itself as a login item via SMAppService.mainApp so the menu-bar overlay is up before you open your first terminal. A new toggle in Preferences flips the setting, and the choice is persisted to UserDefaults; a one-time gate (didApplyDefaultLoginItem) ensures the default never re-enables itself after you turn it off. The XPC call to launchd runs on a detached userInitiated task so the toggle animation stays smooth on slower Macs. The unsigned-then-signed-build dev edge case is called out inline in applyDefaultIfNeeded so future maintainers know why the gate exists

Fixed

  • macOS: focus VS Code windows on other Spaces (#344, #348) — When VS Code (or Cursor / Windsurf) was fullscreen on a different macOS Space, clicking a session row in the overlay was a silent no-op. AX's kAXWindowsAttribute omits cross-Space fullscreen windows for Electron hosts, so the title-matching activator never saw the target window. The fix enumerates the app's Window menu instead — that list is always complete — and AX-presses the title-matching item so macOS performs the Space switch and window raise atomically. Window-menu titles are recognized across the major macOS-supported locales (en/de/fr/es/it/pt/nl/sv/da/no/fi/pl/cs/ru/tr/ja/zh-Hans/zh-Hant/ko) so the path works for non-English users too. Hardening from /simplify review: drop the second-to-last-menu positional fallback (could trigger a destructive action in non-Cocoa-standard apps), unwrap menuBarRef before CFGetTypeID, and collapse the imperative lookup into first(where:) + compactMap
  • daemon: session disappears when a second Claude is opened in the same VS Code window (#345, #347) — Opening a second Claude Code session inside the same VS Code window briefly leaves the new process in the parent CWD before it cds into its worktree. The scanner minted a proc-<NEW> pre-session for that CWD; the claudecode adapter's CWD-based PID discovery — with no transcript yet, so the metadata-based filter is bypassed — then returned the neighbor process's PID, and HandlePIDAssigned's same-PID cleanup deleted the legitimate neighbor's session row. The row reappeared later via the activity-driven recovery path, which presented as a confusing flicker. Fix: pre-session IDs already encode the PID by construction (fmt.Sprintf("proc-%d", pid) in processlifecycle/scanner.go), so the daemon now short-circuits adapter-level discovery for them and calls HandlePIDAssigned directly with the parsed PID. The short-circuit sits above the ProcessWatcher == nil / discoverFn == nil guards so it's robust against future adapters that have a process matcher but no PIDForSession. Real sessions (UUID IDs with a transcript path) continue through the adapter unchanged. E2E regression test uses sync/atomic.Int32 for the discovery-call counter so a future regression races visibly under -race

Docs

  • Landscape page refresh against live GitHub data (#346) — site/landscape/index.html and site/landscape/compare/index.html regenerated from a fresh gh api sweep of 38 tracked agents (May 15, 2026 snapshot). Aider and OpenCode flip from planned to live in the landscape table to match their existing adapters under core/adapters/inbound/agents/. Two repo renames propagated: Pi badlogic/pi-monoearendil-works/pi (the v0.74 move) and Warp warpdotdev/Warpwarpdotdev/warp. Two plausibility-rule trips noted with explicit reasoning: Warp jumped 26.5k→58.5k stars after open-sourcing its codebase; Ruflo grew 33.1k→51.3k on viral promotion. The ir:agent-releases skill's tracked-releases.md adds 22 new versions across Claude Code (v2.1.120–v2.1.142), Codex (v0.125–v0.130 stables), Pi (v0.71–v0.74), and Gas Town (v1.0.1, v1.1.0)

v0.4.2 (2026-05-15)

Changed

  • macOS app: drop legacy file-polling, retire vaporware IRRLICHT_DISABLED env var (#337) — IRRLICHT_DISABLED was never wired into any code path, and IRRLICHT_USE_FILES gated a fallback path in SessionManager that the WebSocket transport has fully replaced; both are removed. ~165 lines of dead Swift in SessionManager.swift go with them (file watcher, debounce/periodic timers, loadExistingSessions, createInstancesDirectoryIfNeeded); init unconditionally uses WebSocket. Equivalent orphan reaping still happens daemon-side via PIDManager's syscall.Kill(pid, 0) sweep — no safety net was lost. The macOS app no longer reads from the daemon-owned instancesPath; that ordering dependency is now documented inline

Docs

  • configuration: document four real env vars with concrete recipes (#337) — site/docs/configuration.html drops the USE_FILES / DISABLED rows and adds rows for the four env vars that exist in code but were undocumented: IRRLICHT_UI_DIR, IRRLICHT_BIND_ADDR, IRRLICHT_MDNS, IRRLICHT_DEBUG. A "When to use these" section walks three real recipes — LAN phone access (with both shell-env and launchctl setenv flows so it works whether the daemon is shell-launched or auto-spawned by the macOS app), "Dashboard UI not found" recovery (showing the full four-place auto-detect order so the override slots in clearly), and a debug state dump. The original IRRLICHT_DEBUG=1 open -a Irrlicht example was broken on macOS — LaunchServices spawns GUI apps without inheriting shell env — and is replaced with three working alternatives (direct binary invocation, open --env, launchctl setenv)
  • architecture, SECURITY: drop vaporware kill-switch bullet, cross-link network-exposure docs (#337) — site/docs/architecture.html no longer mentions the IRRLICHT_DISABLED kill switch (which never existed). SECURITY.md cross-links the network-exposure paragraphs to configuration.html and to the planned hub-mode design

Distribution

  • Release skill: enforce long-line paragraphs in release notes / PR body (#335) — GitHub renders release-body markdown with the GFM "breaks" extension, so every soft line break inside a paragraph or bullet becomes <br>. The v0.4.0 and v0.4.1 release bodies were hand-wrapped at ~75 cols and shipped as a stack of short ragged lines on the release page (both since fixed via gh release edit --notes-file). Step 2 of /ir:release now carries an explicit line-wrap rule explaining the difference between GFM-with-breaks (release notes, PR body, issue body) and standard CommonMark (CHANGELOG.md); Step 8 switches the example from --notes to --notes-file pointing at a tempfile, so the body is reviewable, re-runnable, and the long lines survive shell escaping
  • assets: version reference screenshots (#336) — assets/session_limits.png and assets/straeter_light.png are now versioned alongside the other reference shots used in README drafts and social posts

v0.4.1 (2026-05-14)

Fixed

  • kitty: click-to-focus lands on the right window and tab (#326) — Three failures in the kitty click-to-focus path, fixed end-to-end. (1) When kitty is launched from a shell whose env contains TERM_PROGRAM=vscode (e.g. a VS Code integrated terminal), kitty inherits that value because kitty itself does not set TERM_PROGRAM (upstream kitty #4793); the daemon captured the inherited value and the click was routed to VS Code's activator. ReadLauncherEnv now overrides TermProgram to "kitty" whenever KITTY_WINDOW_ID is set and process ancestry confirms kitty.app is a parent. (2) With multiple kitty processes running, AppActivator.activate(bundleID:) always picked one — typically the oldest — and a post-kitten focus-window re-activate fired async, outside the menu-bar click context, racing macOS yield-focus rules and producing the "raises then drops back" symptom. The daemon now whitelists KITTY_PID; a new Launcher.KittyPID field is plumbed through to the Swift app; KittyActivator calls NSRunningApplication(processIdentifier:).activate(options: []) synchronously inside the click handler. (3) Apple-signed agents like pi (and /bin/zsh) hide their env from sysctl, so KITTY_* env vars never reached their sessions — every click hit bundle fallback. Three new darwin-only helpers in osutil_darwin.go derive these fields without reading the agent's env: kittyAncestryPID, kittyListenOnFor, kittyWindowIDForPID. backfillLauncher was extended so pre-existing sessions get all four kitty fields refreshed on daemon restart

Security

  • kitty: uid-check on /tmp/kitty-{PID} socket probe/tmp is world-writable, so kittyListenOnFor could be tricked into trusting a pre-planted Unix socket at the canonical path and sending kitten @ ls to a hostile listener. kittyListenOnFor now stats the candidate socket and skips any whose owner uid doesn't match os.Getuid() — kitty binds with its own credentials, so a foreign-owned socket at the canonical path is either stale or hostile. Test coverage in osutil_darwin_test.go exercises the current-uid, foreign-uid (root-gated), non-socket, missing-file, and zero-PID branches

Changed

  • kitty: cache ancestry walk in ReadLauncherEnv — The kitty-via-vscode pi worst-case path was walking the parent-process chain up to three times (each walk shells ps up to maxAncestry times). A new resolveHostFromAncestry returning (termProgram, hostPID) is memoized inside ReadLauncherEnv via a closure, so the chain is walked at most once. resolveTermProgramFromAncestry and kittyAncestryPID become thin wrappers; call-site behavior is unchanged

Docs

  • api-reference, contributing: catch v0.4.0 sweep gaps (#332) — site/docs/api-reference.html still referenced agentCfgs in the GET /api/v1/agents blurb; renamed to allAgents to match cmd/irrlichd/main.go. site/docs/contributing.html adapter-PR checklist still said "Implements the AgentWatcher interface"; replaced with the current Agent() / agent.Agent / allAgents contract

Distribution

  • Release skill hardening (#332) — Step 6 checksum recipe now includes irrlichd-darwin-universal.tar.gz (the curl --daemon-only path verifies it; omitting it shipped a release where the standalone daemon installer failed the integrity check). Step 7b drops --delete-branch from gh pr merge --squash so the release branch remains addressable post-squash. Step 4b trigger table gains rows for adapter-package edits and main.go slice/wiring renames so future Phase-A-style shape changes can't slip past doc sweeps

v0.4.0 (2026-05-14)

Added

  • macOS: per-event notification sound picker (#253) — Preferences gains a separate row per notification event (ready / waiting / context-pressure) with its own enable toggle, sound picker (Ping / Chime / Funk / Whoosh / Sosumi / None / Speak aloud / Custom), and preview button. Custom audio (aiff/wav/mp3/m4a/caf) is imported into ~/Library/Sounds/, transcoding mp3/m4a to LPCM-in-CAF via AVAudioFile so UNNotificationSound will play it. "Speak aloud" routes title+body through AVSpeechSynthesizer, pinned to en-US, and exposes three voice variants (Default / Zoe-Premium / Jamie-Premium); if a premium voice isn't installed, the row renders an inline "Install … in System Settings" button that deep-links to Accessibility → Spoken Content. Defaults: all three events enabled; Ready=Funk, Waiting=Ping, Context=Sosumi. Existing preferences preserved on upgrade

Fixed

  • Claude Code: don't bounce ready→working on post-turn away_summary (#329) — Claude Code writes a system/away_summary recap ~3 minutes after a turn ends. The parser correctly marked it Skip=true, but the fswatcher's mtime trigger still ran the full classification pipeline, and the force-bounce in processActivity saw the stale LastEventType from the prior turn_done and flipped the ready session back to working indefinitely. The tailer now surfaces a NoSubstantiveActivity signal when a pass consumed new content but produced no state-relevant change; the detector short-circuits the force-bounce / re-classify path on that signal while still refreshing LastEvent / EventCount / UpdatedAt and broadcasting so the UI's "last activity" stays current
  • Claude Code: detect AskUserQuestion / ExitPlanMode via PreToolUse hook (#307) — Claude Code can lag flushing the assistant tool_use block to JSONL for minutes after rendering an AskUserQuestion / ExitPlanMode overlay; the transcript-driven detector never saw the open tool call, so the session sat in working while the user stared at the prompt. A PreToolUse hook scoped to AskUserQuestion|ExitPlanMode fires synchronously when the model emits the tool_use and flips permissionPending; the existing PostToolUse matcher is widened to include both tools so the same edge clears the flag when the user answers. Legacy installs are migrated in place by upgradeStaleHookMatchers
  • Codex: treat <proposed_plan> as user-blocking like ExitPlanMode (#322) — Codex's Plan Mode ends a turn with a <proposed_plan>…</proposed_plan> block — semantically identical to Claude Code's ExitPlanMode. The block arrives as plain assistant text, so the classifier never saw an open user-blocking tool and fell through to ready, leaving the dashboard green while the agent was actually blocked. When an assistant message contains a fully-closed <proposed_plan> block, the codex parser now synthesizes a virtual ExitPlanMode tool-use — same user-blocking path as Claude Code; the existing ClearToolNames hook on user messages closes it when the user replies
  • Daemon: reject zombie sessions with missing cwd (#321) — A daemon restart within 2 minutes of claude --resume against a session whose worktree had been deleted re-admitted the session as a ghost: the transcript-mtime check treated the refreshed mtime as live, PID discovery failed, and the steady-state sweep only cleaned it up ~75s later. Admission now checks cwd existence alongside the stale-transcript guard at both onNewSession and seedAlivePIDs

Changed

  • #159 Phase A — Agent declaration replaces agents.Config — The legacy agents.Config struct + its five per-adapter Config() constructors and four map helpers are removed. Each adapter now exports a single Agent() constructor returning a sealed-sum declaration: Agent = Identity × Process × Source, where Process is ExactName | CommandPattern, Source is FilesUnderRoot | FilesUnderCWD | ProcessOwnedStore, and FileParser is JSONLineParser | RawLineParser. The daemon consumes []agent.Agent directly, with per-projection helpers in adapters/inbound/agents/maps.go. Variant-dispatched watcher wiring in cmd/irrlichd/wiring.go replaces the per-adapter loop. Phase A also lands an M0 contract-test layer (SessionState on-disk, 7 PushMessage shapes, GET /api/v1/agents) to guard the public surface
  • #159 Phase A — Watcher port replaces AgentWatcher; identity carried on the merge pipeline — The inbound watcher port gains an Identity() method and WithIdentity() builder; each per-watcher drain goroutine in SessionDetector.Run() captures identity once and wraps every event with it, so agent.Event.Adapter is removed. The old AgentWatcher interface is deleted. NewSessionDetector panics at construction when any watcher's Identity() is the zero value. metrics.New takes a single Registry struct instead of four positional maps

Docs

  • /ir:release skill — adapt to PR-required main + fix tap-publish race (#306) — main is now protected by a "Changes must be made through a pull request" repo rule. The release flow now stages on a short-lived release/v$NEW_VERSION branch, opens a PR with the drafted release notes, squash-merges, hard-resets local main to origin/main, and tags the merged commit. Step 6.5 patches the in-repo cask template via sed and leaves the sibling tap untouched until Step 8.5

v0.3.13 (2026-05-11)

Fixed

  • OpenCode: suppress ghost sessions when no opencode process is live — the OpenCode watcher's startup scan emitted EventNewSession for every non-archived row whose time_updated fell within maxAge, regardless of whether opencode was actually running. On every daemon restart, every historical session in the DB became a "live" row in the menu bar with no path that ever removed them. Now gates emission on a live opencode process owning the session's CWD via processlifecycle.LiveCWDs(processName). Sessions in the DB with no live process are tracked (cursor seeded so historical activity isn't back-filled if the process later starts) but not surfaced; a new emitted flag enables EventNewSession to fire on the dormant→live transition
  • OpenCode: clean up carryover ghost state on startup — users upgrading from v0.3.12 had ghost session JSON files (PID=0, DB-backed TranscriptPath) that neither syscall.Kill nor isStaleTranscript caught. A new branch in isStartupZombie deletes PID=0 sessions whose adapter has a registered process name iff no live process of that name owns the session's CWD. Only deletes when the lookup returns a definitive non-nil result

Changed

  • OpenCode: GC stale cursors, drop dead initialArchiveCleanupWindowgcExpiredCursors drops cursor entries whose lastObserved predates maxAge so the cursor map can't grow without bound for users who accumulate many sessions but rarely run the CLI. Tracked separately from cur.lastTS so a session whose time_updated bumps without new parts isn't wiped prematurely
  • OpenCode: consolidate DB-backed predicate, cache liveCWDs lookup — DRYs the "is this path DB-backed?" check and caches liveCWDs per adapter inside CleanupZombies so M ghost candidates sharing an adapter pay one pgrep fork, not M
  • Release: keep homebrew tap from silently lagging (#299) — the tap was stranded at v0.3.8 across four releases because Step 8.5 no-op'd silently when IRRLICHT_TAP_DIR was unset. update-cask.sh now auto-discovers a sibling ../homebrew-irrlicht clone before bailing and hard-fails on --push without a tap dir; the skill's Step 8.5 verifies the published cask version after publish and prints a loud WARNING on mismatch

Docs

  • README: supported platforms table (#301, #302, #303) — adds a Platforms table with CLI access references and links the macOS access cell to the releases page
  • Landing: mark OpenCode alpha; teach release skill the landing-page grid — the parallel grid on site/index.html was missed in v0.3.12 because Step 4b only enumerated site/docs/*.html. Extends the dynamic enumeration to scan site/*.html and adds explicit trigger-table rows for adapter maturity-stage changes and new platforms
  • README — note codeburn alongside other quota & cost trackers in the positioning section

v0.3.12 (2026-05-09)

Added

  • OpenCode adapter (#255) — first agent on the new SQLite-backed monitoring path. OpenCode stores all session data in a single WAL-mode database rather than JSONL files; the adapter ships an fsnotify WAL watcher polling session/part tables, a parser mapping step-finish / text / tool rows to normalized events, a MetricsProvider that bypasses the JSONL tailer for cost + token snapshots, CWD-based PID discovery, parent-child session linking via parent_id from the DB, and EventRemoved emission on session.time_archived. Closes #100
  • ir:triage skill (#283) — strictly diagnostic GitHub-issue triage skill that scores each issue against a 6-axis readiness rubric (Scope / Specification / Verifiability / Context / Independence / Reversibility) and lands it at ready-for-agent or needs-info with a one-line justification per label decision. Bulk sweep skips already-triaged issues; explicit /ir:triage #N always re-triages and edits the prior comment in place

Fixed

  • History bar right-anchors when states overflow bucketCount (#286) — HistoryBarView's anchor math only worked when states.count <= bucketCount. After #249 lowered bucketCount from 150 → 60, the 150-state test fixture started rendering an all-green bar because offset collapsed to 0 and the loop drew the oldest states inside the canvas while the newest tail was clipped past the right edge. Now takes states.suffix(bucketCount) and recomputes offset against the visible slice
  • Claude Code: reconcile phantom in_progress from task_reminder snapshots (#289) — Claude Code occasionally emits a TaskUpdate against a stale taskId and never sends a follow-up completed, so the UI hung at n / total forever. The tailer now treats the task_reminder attachment as authoritative — any local in_progress whose ID is missing from the snapshot is demoted to completed, and snapshot status wins on any present-with-divergent-status case. Closes #282
  • macOS: sync apiGroups on local session delete + reset (#287) — local deletes/resets only updated sessions (menu bar) and sessionMap and skipped apiGroups (list view), so a deleted session lingered in the list until rehydration and a reset row stayed working in the list while the menu bar already showed ready. Mirrors the WS handler and adds SessionState.withState(_:) so all 10 optional fields (children, role, subagents, adapter, launcher, …) survive the round-trip

Changed

  • Co-locate adapter display name + icons with Go adapters (#284) — adds DisplayName + IconSVGLight/IconSVGDark to agents.Config and a new GET /api/v1/agents endpoint serving them. Adapter is now the single source of truth for its own branding; adding a new adapter is a Go-only change. The macOS app and web dashboard look up name and icon from the registry — the five Swift <adapter>SVG functions and two switch statements in SessionState.swift are gone. Web dashboard renders adapter SVGs via <img src="data:image/svg+xml;base64,..."> so the browser image-loading sandbox blocks scripts even if the daemon binary is tampered with. AgentRegistry is @MainActor-isolated for Swift 6 strict-concurrency cleanliness. Closes #260

Docs

  • Adapter interfaces documented with exact Go signatures (#292) — site/docs/adapters.html gains an "Adapter Interfaces" section with the actual agents.Config, tailer.TranscriptParser (plus the optional RawLineParser, IdleFlusher, PendingContributor, ParserStateProvider hooks), agent.PIDDiscoverFunc, and agent.Event / inbound.AgentWatcher types, with file paths so readers can jump from doc to source
  • Release skill sweeps every docs page on each release (#293) — /ir:release Step 4b now enumerates site/docs/*.html and top-level READMEs dynamically (rather than from a hardcoded list) and walks each against the release diff so new pages cannot be silently missed

CI

  • Coverage workflow surfaces badge update failures (#281) — validates GIST_SECRET / COVERAGE_GIST_ID up front, captures the gist PATCH response so a non-2xx fails the job with the actual error body instead of dying silently inside curl -sf … > /dev/null, and adds connect/max-time + retry settings so transient 5xx and stalled handshakes don't hang the step

Tests

  • Replay: refresh stale opencode baseline-hello golden (#285) — golden was committed in #255 with a populated source_transcript field that the test zeros before comparison; regenerated via UPDATE_REPLAY_GOLDENS=1 to bring opencode in line with the other 4 adapters

v0.3.11 (2026-05-02)

Fixed

  • Serve stale LiteLLM cache instead of zeroing all costs (#275) — when the model-pricing cache was older than 24h, every cost calculation silently fell to zero (and omitempty dropped estimated_cost_usd from output entirely). Stale pricing is now served to non-daemon callers (replay tool, CLI, tests); IsCacheStale keeps its job of driving the daemon's background refresh
  • Aider: keep turn open across multiple > Tokens: lines (#274) — under --yes-always, aider auto-accepts file-add prompts and re-prompts the model within one user turn. The parser now treats > Tokens: as end-of-one-model-call (emitting assistant_message) and synthesizes the turn_done via an idle-flush hook, so sessions don't flip to ready mid-turn
  • Aider: emit turn_done on LLM-layer error (#273) — when aider prints a > litellm.BadRequestError: … blockquote without a > Tokens: line, the session no longer hangs in working forever
  • Tailer: drop bufio.Scanner cap so JSONL lines >2 MB don't wedge sessions (#271) — large transcript lines used to silently stop being processed once they exceeded the default 64 KB scanner buffer
  • Close 13 Code Scanning alerts (#266)
  • macOS: use brand off-flame for idle/empty state (#248)

Changed

  • Performance: shrink mobile payload, unblock render path (#272) — image optimization and CSS deferral for the marketing site; meaningful drop in mobile LCP/CLS
  • Daemon serves dashboard from disk, drops //go:embed (#267) — runtime walk-up search for platforms/web/index.html so the dashboard can be hot-edited in dev and shipped as a separate file in production bundles
  • Session history streams over WebSocket; bit-pack 60-bucket rings (#249) — replaces polling with live updates and a more compact wire format
  • Centralize per-adapter transcript extension (#251)

Distribution

  • onboard-agent covers claudecode/codex multi-turn + interrupted-turn (#269) — fixture matrix gains coverage for two real-world replay scenarios

Docs

  • Maturity-stage rubric and adapter onboarding section (#264)

Tests

  • Replay: zero source_transcript so goldens are worktree-portable (#250)

v0.3.10 (2026-04-27)

Fixed

  • Sweep zombie sessions on startup (#242) — Claude Code sessions no longer linger in the menu bar / UI after the underlying claude process has exited. The daemon now reaps stale entries on launch
  • Prune deleted sessions from apiGroups synchronously (#244) — when a session is removed, the overlay now updates immediately instead of showing a stale row until the debounced rehydrate lands. Walks agents, child subagents, and nested groups; drops project groups that become fully empty (gas-town excepted, since it renders even with no rigs)

Changed

  • Daemon: drop recycled-PID predicate from CleanupZombies — simpler, more reliable startup-cleanup path. Groundwork for #242

Tests

  • e2e regression test for the startup zombie sweep (#242)
  • 5 new SessionManagerApiGroupsTests cases covering top-level / child / parent removal, gas-town empty-rigs survival, and unknown-id no-op (#244)

v0.3.9 (2026-04-27)

Added

  • Aider adapter — first agent shipped through the new /ir:onboard-agent discovery flow. Includes parser, tmux-driven interactive driver, scenario fixtures, and a pinned trailing-? waiting-state contract. Aider sessions show alongside Claude Code, Codex, Pi, and Gas Town with the same three-state vocabulary
  • /ir:exec skill — issue-driven plan generation. Reads a GitHub issue and produces a structured implementation plan to start a fresh worktree
  • coverage-viewer dev webview (#222) — local web UI for the agent × scenario fixture matrix; shows which lifecycle events each adapter has recorded
  • tui capability + category taxonomy — adapters can declare tui as a discoverable capability so the canonical scenario matrix can target TUI-style agents
  • IRRLICHT_DEMO_MODE=1 — daemon flag that disables ProcessWatcher and per-adapter AgentWatchers so tools/seed-demo-sessions can stage screenshot scenarios without live processes leaking into the dropdown
  • Process discovery: CommandLineMatch + TranscriptFilename probes — wrapper-launched agents (e.g. invoked via pgrep -f) and per-CWD agents that write their transcript next to the project are now detected without a kqueue race
  • Transcript activity emission for CWD-resident transcripts — processlifecycle now emits transcript_activity events for agents whose transcript lives next to the working directory

Fixed

  • Tailer survives SendMessage tool across turn_done (#81) — Claude Code emits a turn_done between the assistant message and a follow-up SendMessage tool call; the tailer used to drop the second half. Sessions stay coherent across that boundary now
  • Mid-paragraph question detection + snippet trim (#236) — the waiting-state classifier used to require a question at the end of the assistant message. It now picks up questions mid-paragraph and trims the surfaced snippet for the menu bar block
  • Skip rhetorical Q&A pairs in question detection"Did X happen? Yes."-style self-answered questions no longer flip a session to waiting
  • Menu-bar button stays highlighted while panel is open (#224) — the NSPanel migration in 0.3.8 lost the button-pressed appearance; restored with explicit highlight-on-show / unhighlight-on-close
  • Tooltips restored after NSPanel migration (#218) — switched from SwiftUI .help() (silently dropped inside NSPanel) to an NSView-bridged tooltip modifier
  • History bars align with Context layout; modes renamed to "Min" with tooltips (#210)
  • Cost display drops cents at ≥$100 (#214, #215) — $132.41 was line-breaking the row; now renders $132
  • Stale session ledger files GC'd (#185) — the per-session ledger directory used to grow without bound; now cleaned alongside session expiry
  • Claude Code hook errors silenced when daemon is down (#221) — hooks no longer print noisy connection errors when irrlichd isn't running
  • Replay byte-identity test excludes bare events.jsonl — the bare events file is regenerated and shouldn't be part of the byte-identity check
  • Coverage-viewer rejects path-traversal in API + uses aider.Parser after stub removal

Changed

  • /ir:onboard-agent overhaul (#199, #200) — moved from a hardcoded scenario list to a features.json + replaydata layout with 3-subagent discovery, reasoned merge, and cross-agent feature widening. Adds Codex + Pi drivers and scenario columns; gastown gets its own orchestrator scenario axis. Onboarding aider through this flow validated the design end-to-end
  • Canonical scenario × adapter fixture matrix (#228, #231) — covers the 7 actionable scenario × adapter cells plus agent-question-pending for claudecode/codex/pi. Adds drive-pi-interactive.sh and two pi script-based fixtures
  • Dev scripts consolidated under tools/ — standalone tooling (HTTP viewers, fixture generators, homebrew-tap helper) now lives in top-level tools/ rather than core/cmd/
  • tools/homebrew-tap/update-cask.sh simplified — single source of truth for cask updates; the in-repo template and external tap repo are bumped from one script
  • Aider parser: single regex match per line; documented interface contract
  • e2e tests for processlifecycle crash, concurrent sessions, fswatcher (#205) — extracts IsCanonicalState and assertWatchersExited helpers

Distribution

  • Homebrew cask via own tap (#187) — brew tap ingo-eichhorst/irrlicht && brew install --cask irrlicht now resolves to the latest release. The cask is auto-bumped on each release via tools/homebrew-tap/update-cask.sh

Site

  • Landing page rewrite — restructured around a "first 30 seconds" pain → state → install flow, with stage-tag legend, install stats strip, and expanded "why" section. New menu-bar explainer screenshot and dark-forest backdrop
  • README restaged for first-30-seconds skim, adapters tagged by stage, explainer image promoted to hero banner
  • Design system reference added under tools/irrlicht-design-system/

v0.3.8 (2026-04-24)

Added

  • Menu bar rewrite: NSStatusItem + NSPanel — replaces SwiftUI's MenuBarExtra(.window) so content changes and panel resize land in the same runloop tick. Eliminates the one-frame background flash on group collapse/expand, and keeps the panel top pinned to the status item while height grows downward. Panel opens rightward with 10pt continuous-curve rounded corners; screen-edge clamp + notch fallback cover narrow right-edge displays
  • SessionListView column rebalance — panel 350 → 380pt; context bar 80 → 100pt; cost column 40 → 36pt. Branch column shrinks to 44pt when a subagent badge is present so the context bar's x-position is constant across rows. Two-pass FlowLayout aligns the 7pt task-progress circles with the taller "done/total" label

Fixed

  • Settings overlay background no longer transparent — the NSStatusItem + NSPanel rewrite uses a transparent panel so the rounded-corner clip works, which meant every SwiftUI branch had to paint its own background. SessionListView did, but the Settings pane didn't — the desktop wallpaper bled through. SettingsView now paints the same windowBackgroundColor, and a new SettingsViewTests pixel-opacity assertion samples the four corners + center to catch any future regression
  • "…" overflow indicator beyond 5 menu-bar groups — when more than five project groups are active, the menu bar icon shows a trailing "…" so you know the list is truncated rather than silently dropping the extras
  • Replay harness mirrors daemon parent-hold, permission-pending, and orphan promotion — sidecar-driven replay was skipping three pieces of daemon logic (parent-child hold when subagents are active, permission-pending overlay from PermissionRequest/PostToolUse hooks, and stale-sweep promotion of children whose transcripts go quiet). Extended-check now passes on the subagent and permission-hook fixtures without regenerating their sidecars

Changed

  • Core ARS composite 8.0 → 8.2 — large internal refactor: session_detector.go split into _activity/_helpers/_lifecycle/_subagent files; cmd/replay/main.go split into lifecycle/metrics/replay_sidecar/replay_transcript/extended_check/types/fixtures_test; cmd/irrlichd/main.go request handlers factored into handlers.go. Behavior unchanged
  • Unified agent registration via agents.Config — adding a new agent adapter is now one Config() constructor + one line in main.go's agentCfgs slice. PIDDiscoverFunc moved to domain/agent/ so Config can reference it without violating hexagonal layering; metrics adapter inverted so outbound no longer imports inbound
  • Shared constants between daemon and replayHookPermissionRequest/PostToolUse/PostToolUseFailure in the claudecode adapter, exported services.SubagentQuietWindow, and services.ForceReadyToWorkingReason — so hook names, the 30s stale-sweep window, and the force-ready reason string can't drift between the live classifier and the replay

Developer tooling

  • /ir:onboard-agent skill — produces a canonical scenario × adapter fixture matrix. Scenarios are defined once, agent-agnostically, with a requires: [capability] list; adapters declare Capabilities, and matrix cells fall out automatically. Unifies refresh, bootstrap, and new-agent-onboarding workflows
  • /ir:agent-landscape hardened against hallucinations — every agent in the landscape report is verified against the GitHub API before publishing; the skill refuses to emit entries it can't resolve

Site

  • Landing page: Terminals & IDEs column dropped — the click-to-focus host list grew past what fit cleanly in the features grid; replaced with a single "works with your terminal" sentence

v0.3.7 (2026-04-24)

Added

  • Agent history bar with 1s/10s/60s granularity — server-side pre-aggregates per-session state buckets (working/waiting/ready) under /api/v1/sessions/history. A single cycling mode button in the menubar switches between context display and the three history granularities
  • History persistence across daemon restarts — buffers saved to ~/.local/share/irrlicht/history.json every 60s and on shutdown, so the timeline survives a restart instead of resetting to empty
  • Waiting-state question block in the session row — when a session goes to waiting, the menubar row now shows the last assistant question (or the AskUserQuestion text) in an orange block beneath the row
  • Claude Code task list progressTaskCreate / TaskUpdate tool calls surface as a progress dot strip on the session card with a live "N/M" count
  • Click-to-focus across 17 terminal/IDE hosts — extending v0.3.6's launcher work to Zed, Rio, Tabby, WaveTerm, Alacritty, Nova, cmux, Kitty (socket-based) and the JetBrains family
  • Web UI timeline seeded from persisted daemon history — on page load the dashboard pulls /api/v1/sessions/history?granularity=1 and paints the last 60s immediately instead of starting empty

Fixed

  • Accurate per-model cost estimation across all adapters and restarts — the cost tracker now handles usage maps consistently for Claude Code, Codex, and the pi adapter, and survives daemon restarts without double-counting
  • Offline-at-startup: LiteLLM capacity fetch retries with backoff — the capacity table no longer stays empty when the laptop is offline at daemon boot
  • History timeline ticks flow right→left — new ticks land in the rightmost bucket and older ones shift left as time passes, in both the Swift HistoryBarView and the web dashboard canvas
  • Waiting-state detection survives long assistant messagesExtractAssistantText keeps the tail (with leading ellipsis) so a trailing question-mark still trips the waiting classifier; AskUserQuestion tool calls with no text block fall back to questions[0].question
  • Menubar popover: dynamic height + collapse state survives session refreshes — single ScrollView with .fixedSize(vertical:) lets the popover size to content up to 560pt; collapse state lifted onto SessionManager.collapsedGroupNames
  • Tasks: state resets on transcript rotation, stable across schema bump — in-memory task list cleared on file rotation; ledger schema bumped to v2 to force re-scan
  • Menubar tooltips work inside MenuBarExtra panels — switched from .help() to an NSView-bridged .tooltip(...) modifier
  • Launcher: ProcessRunner calls dispatched off the main thread — focus/open operations no longer stall the UI
  • Launcher: fullscreen Space handling — correctly raises windows on a fullscreen Space
  • Swift: Task model renamed to SessionTask so it stops shadowing Swift's built-in concurrency Task

Changed

  • Parsers split per-adapter — format-specific transcript parsers (Claude Code AskUserQuestion, TaskCreate/Update) live under the agent adapter packages instead of the shared tailer
  • Cost tracker usage-map extraction and ledger hot-path simplified; history granularity parsing cleaned up; $0 cost toggle stays visible so the timeframe cycle button remains reachable
  • Task parsing switch flattened; status constants lifted to one place

Tests

  • History persistence round-trip (save/load/missing-file/corrupt-file/version-mismatch) on HistoryTracker
  • AskUserQuestion text fallback (short message) and long-text tail storage on the Claude Code parser
  • Integration test for GET /api/v1/sessions/history (response shape + bad-granularity 400)
  • SessionMetrics.formattedCost two-decimal regression
  • New SessionRowView snapshot suite (waiting-question block + ContextBar token-count label); NSHostingView.appearance pinned to .darkAqua so snapshots are deterministic

v0.3.6 (2026-04-19)

Added

  • Jump to launching terminal or IDE on session click (#170) — Clicking a session row or delivered desktop notification brings the originating iTerm2 tab, Terminal.app window, or AXTitle-matched editor to the front. Host resolution walks the session's ppid chain; iTerm2 sessions are matched by UUID, Terminal tabs by tty, and generic apps by scoring window titles against the session CWD's deepest ancestor segment. The daemon captures the launcher env ($TERM_PROGRAM, $ITERM_SESSION_ID, tty) on first PID assignment so the lookup works even after the agent has been running for hours

Fixed

  • claude-code: gate PID negative filter on metadata mtime (#169) — After /clear, Claude leaves ~/.claude/sessions/<pid>.json pointing at the old session for up to two minutes. The detector no longer holds onto the dead session; once the new transcript's mtime exceeds the stale metadata, the PID is reassigned and the old session is cleaned up immediately
  • Web UI: render sessions on initial load (#167) — Initial-load handler now reads the bare-array response from GET /api/v1/sessions (previously expected a groups/orchestrator wrapper, so the dashboard stayed empty until the 30s rehydrator or a WebSocket delta arrived). Also removed three stale references to the removed /api/v1/orchestrators/gastown endpoint
  • macOS: restore project-group reorder chevrons (#172) — The up/down chevrons in top-level project group headers came back; ordering is derived from apiGroups directly so the UI state stays in sync with persisted order
  • CLI: irrlicht-ls runs from any subdirectory (#175) — go run now invoked from the repo root via --workspace

Changed

  • macOS Launcher split into per-host activators behind a HostActivator protocol — iTerm, Terminal.app, and an AXTitleMatchActivator for generic apps each live in their own file. Window raising uses the Accessibility API to raise a specific window rather than the frontmost one

Distribution / Dev

  • Persistent self-signed "Irrlicht Dev" identityir:test-mac now signs dev builds with a stable designated requirement so Accessibility and Automation TCC grants survive rebuilds. Run scripts/dev-sign-setup.sh once to install it

v0.3.5 (2026-04-19)

Added

  • Per-group cost display with switchable time frames (#83, #162) — Project group headers now surface day / week / month / year cost totals via a timeframe toggle instead of a single hard-coded window
  • curl | sh installer at irrlicht.io/install.sh — One-line install pulls the latest release zip, verifies the sha256, and registers the app with LaunchServices. Rerunning removes any previous install cleanly
  • Raw tab in the web UI — Inspect the /api/v1/sessions JSON payload live

Changed

  • Capacity: LiteLLM is now the single source of truth (#165) — The hand-maintained core/pkg/capacity/model-capacity.json table is removed; lookups go through a process-wide singleton that hot-reloads the LiteLLM cache when it's refreshed, so the daemon no longer needs a restart to see a new model. Fixes the 200K/1M flip on Claude Opus 4.7 and enables 1M context for Sonnet 4.6

Fixed

  • macOS: only notify on transitions from working (#161) — State-transition notifications fire on working → waiting and working → ready only. A waiting → ready transition no longer fires a redundant "ready" notification
  • Detector: synthetic waiting for collapsed user-blocking tools (#150, #160) — Sessions that ended on a user-blocking tool whose start was collapsed out of the transcript no longer linger as working
  • Replay: split sidecar timelines at process_exited boundaries (#144, #163) — /continue sessions with multiple process lifetimes no longer report spurious extra transitions
  • Detector: unstick subagents via parent task-notification (#134, #156)
  • Installer: preserve provenance and register with LaunchServices (#158) — First launch is no longer quarantined
  • Security: lock down irrlichd network exposure (#94, #155) — Binds to localhost only and rejects cross-origin WebSocket upgrades
  • Site: curl | sh install command wraps on narrow screens; hero spacing cleaned up
  • Tests: three stale SessionManagerTests updated (#166) — Match the current SessionState decoder and abbreviated RelativeDateTimeFormatter behavior

Distribution / CI

  • ARS badge workflow pinned to v0.0.9; GOPROXY=direct no longer required
  • ir:release skill guards against stale Swift binaries and missing SwiftPM resource bundles — the two root causes of the broken v0.3.4 bundle

v0.3.4 (2026-04-14)

Added

  • Gas Town full role support with recursive group nesting (#154) — First-class role registry surfaces all roles (mayor, deacon, witness, refinery, polecat, scribe, …) instead of treating them as ad-hoc strings, and codebase/rig groups can nest recursively for richer worker hierarchies
  • Desktop notifications on macOS state transitions (#147) — System notifications fire when sessions transition into waiting or ready, with a Settings toggle to opt out
  • Permission-pending state via Claude Code hooks (#108) — The daemon consumes PreToolUse / Notification hooks to detect modal permission prompts directly, instead of inferring them from transcript heuristics
  • Landscape page — 3-month growth trend lines, head-to-head comparison page, and alternative agent metrics

Fixed

  • Push: broadcast buffer increased (#152) — Bursty session updates no longer drop waiting transitions before clients can drain them
  • Tailer: preserve user-blocking tools across turn_done sweep (#148) — In-flight permission prompts (Bash, AskUserQuestion, …) are no longer cleared when the assistant briefly stops streaming
  • Daemon: prevent fswatcher event drops (#143) — Switched to a non-blocking event pump so user-blocking tool starts are never missed
  • macOS dev workflow builds a real .app bundle (#149) — Bundle identifier migrated to io.irrlicht.app so dev and release builds no longer share state
  • macOS: corrected dev fallback path for the bundled irrlichd binary — The menu bar app finds the daemon when running outside an installed bundle
  • Skill: ir:test-mac builds from the active worktree — When invoked from a worktree, builds from there instead of the main checkout

Changed

  • Unified replay tool (#141) — replay-session and replay-lifecycle consolidated into a single tool with subcommands, removing duplicated transcript-loading code

Docs

  • README problem section tightened to lead with concrete user pain rather than the architecture
  • CHANGELOG.md added at the repo root and wired into the ir:release skill so every release updates it

v0.3.3 (2026-04-11)

Fixed — Subagent lifecycle reliability

Several edge cases in parent/child session tracking caused subagents to linger in working after their parent's turn ended, or to disappear entirely when transcripts were still being written.

  • Subagent count unified at the adapter level (#132) — In-process and file-based counts are now reconciled in the adapter so the daemon sees a single authoritative value, eliminating drift between the two sources
  • Subagent quiet window bumped to 30s — API response gaps on long tool calls were tripping the previous window and prematurely marking subagents idle. 30s survives the observed gap distribution
  • Fast-forward orphaned subagents when parent turn is done — When the parent session finishes its turn but a child was left in an intermediate state, the child is now fast-forwarded to ready instead of hanging in working
  • Don't fast-forward subagents whose transcripts are still being written — Guards the fast-forward path so in-flight children aren't killed mid-stream
  • Re-evaluate parent when liveness sweep deletes last child — When the liveness sweep removes the final child session, the parent is immediately re-classified so its state reflects the new reality
  • FS watcher recursively watches newly-created subtrees — New project/session directories created after daemon startup are now observed without a restart

Added

  • Cost display toggle (#130) — The macOS menu bar cost readout can now be toggled in Settings and is off by default for users who prefer a lighter status line
  • Full session lifecycle recording & replay (#107, #138) — ir:test-mac runs can now record presession events, session events, and tail output into a sidecar file. The replay harness replays any recording byte-identically against the production tailer + classifier, and fails loudly on sidecar drift
  • Curated subagent fixtures — Real-world subagent transcripts (including an 11-background-agent fixture) are bundled with their lifecycle events so regressions in parent/child tracking are caught offline
  • Tailer FIFO replaced with id-keyed map (#117) — Open-tool tracking now uses a stable map keyed by tool use id instead of a FIFO, removing ordering assumptions that broke with parallel tool calls

Docs

  • README rewritten around real user pain and the competitive landscape of agent monitoring tools
  • GitHub-recognized community health files added: CODE_OF_CONDUCT.md, CONTRIBUTING.md, SECURITY.md, issue and PR templates

v0.3.2 (2026-04-07)

Fixed — Claude Code session state flicker (#102)

Four distinct bugs were causing long-running Claude Code sessions to bounce between working, waiting, and ready. All are fixed in this release.

  • Stale-tool timer disabled for Claude Code — The 15s heuristic tripped on every multi-second Bash invocation (builds, tests, find, network calls). Permission-pending modals and long-running Bash are indistinguishable in the JSONL stream, so the false-positive rate swamped the signal. Brings Claude Code in line with the Pi adapter's existing policy.
  • Tailer open-tool tracking collapsed to a single source of truth — Parallel integer counters and a name slice could desync on orphan tool_result events (from --continue resumes or compact replays), leaving metrics in an impossible has_open=false / open_tool_names=[Bash] state. Now derived solely from the name slice — orphans are idempotent no-ops.
  • ESC interrupts distinguished from benign tool errors — The classifier's cancellation rule fired on any tool_result.is_error=true (grep misses, failed builds, find on protected dirs), incorrectly flipping the session to ready. New LastWasUserInterrupt signal fires only on the literal [Request interrupted by user text marker.
  • stop_reason allow-list — Only null was treated as intermediate streaming; max_tokens and pause_turn passed through as terminal and tripped IsAgentDone() mid-turn. Flipped to an allow-list of end_turn/stop_sequence/refusal/tool_use — unknown future values default to "assume streaming".

Fixed

  • OpenCode agent registry corrected to anomalyco/opencode and marked as planned
  • Empty-state text updated from "Claude Code" to the generic "coding agent"

Added

  • Offline replay harness (core/cmd/replay) — Takes any Claude Code, Codex, or Pi transcript and feeds it through the production tailer + classifier using virtual time. A 500-hour session replays in under a second. Every transition is logged with reason, metric snapshot, and trigger cause
  • Regression fixtures under testdata/replay/<adapter>/ — Four Claude Code, one Codex, one Pi — all post-fix flicker-clean
  • Local session scanner (scripts/find-flicker-sessions.sh) — Ranks every transcript under ~/.claude/projects, ~/.codex/sessions, and ~/.pi/agent/sessions by flicker count, for harvesting new regressions
  • Adapter registry (core/adapters/inbound/agents/registry.go) — Single source of truth for adapter name → parser and adapter name → StatePolicy lookups, consumed by the replay harness

Docs

  • README updated with cost tracking, subagent visibility, and corrected state detection behavior
  • State machine docs: cancellation section rewritten to reflect the new LastWasUserInterrupt signal
  • API reference: session metrics schema updated (last_tool_result_was_error removed, last_was_user_interrupt added)

v0.3.1 (2026-04-06)

Added

  • Dynamic model capacity from LiteLLM — Context window sizes and pricing are now fetched from the LiteLLM API at daemon startup, removing hardcoded fallback assumptions
  • Token usage in debug mode — Debug mode now shows token usage metrics for all models, with percentage kept next to the context bar
  • Reorderable project groups — Project groups in the macOS popup and menu bar can now be reordered by the user

Changed

  • Removed client-side session expiry setting — Session expiry is now handled entirely by the daemon, simplifying the macOS app settings
  • Adapter-driven stale-tool waiting — Stale-tool timeout is now configured per adapter policy rather than globally

Fixed

  • Codex gpt-5.3 context window mapping corrected to 256K
  • Large appended transcript lines no longer skip events
  • Wrapped Codex transcript schema now parsed correctly
  • Pi compaction events treated as activity for working-state detection
  • Subagent tool tracking and debug row display preserved correctly
  • False ready state during tool calls and subagent work prevented
  • Session state flicker between tool calls — First streaming chunk of each new assistant message (new message ID, stop_reason=null) was misclassified as final, triggering false working→ready→working transitions on every tool call cycle
  • Stale-tool timeout increased from 5s to 15s to reduce false waiting transitions during long-running builds

Site

  • Agent landscape page with 63 tracked agents, 3-month trends, and deny list

v0.3.0 (2026-04-05)

Added

  • Permission-pending detection — Sessions with non-blocking tool calls awaiting user approval now transition to waiting state after a 5-second stale-tool timeout, correctly reflecting that the agent is blocked on permission
  • Last question display — When an agent asks a question and enters waiting state, the question text is captured and available via the API for display in the menu bar
  • Session state dots — Colored dots in the macOS menu bar and popover headers provide at-a-glance status for each session
  • Gas Town educational UI — Role hierarchy visualization and active tool display for Gas Town orchestrator sessions
  • Web dashboard redesign — Compact rows, DOM reconciliation for flicker-free updates, and timeline heatmap
  • Adapter-specific PID lifecycle — Codex and Pi sessions now have tailored PID discovery and process lifecycle handling alongside Claude Code
  • ir:agent-releases skill — Tracks upstream coding agent releases and reports changes that impact irrlicht monitoring

Changed

  • Per-adapter transcript parsers — Each agent adapter (Claude Code, Codex, Pi) now has its own transcript parser instead of a single shared parser, enabling format-specific handling and cleaner architecture
  • Shared CWD extraction logic extracted, dead code removed

Fixed

  • PID assignment race condition — Serialized PID assignment with state transitions to prevent concurrent sessions from claiming the same process
  • Orphan session cleanup after /clear — Sessions left behind when a user runs /clear in Claude Code are now properly cleaned up
  • Stuck working state from local commands — Local command events (shell escapes) no longer prevent sessions from transitioning out of working state
  • Daemon performance with many sessions — Resolved performance degradation when monitoring many concurrent sessions
  • Git root resolution for deleted worktree directories
  • Project name detection for non-git directories on activity refresh
  • Expanded click target for Settings and Quit buttons
  • URL-encoded dynamic badge URLs
  • Pi nested message parsing for role/stopReason/content fields
  • Pi PID discovery via command-line pattern matching
  • Include “assistant” in IsAgentDone fallback for turn detection

Site

  • Refined hero section — removed glow, shortened copy, cleaned up labels
  • Added counter.dev analytics to all pages

v0.2.5 (2026-04-05)

Added

  • Pi coding agent adapter — Monitor Pi sessions alongside Claude Code and Codex. Watches ~/.pi/agent/sessions/, parses JSONL v3 transcripts, detects state from stopReason, extracts tokens/cost/model, and supports Pi subagent linking via parentSession
  • Pi icon in macOS menu bar — Greek letter pi (π) icon distinguishes Pi sessions from other agents

v0.2.4 (2026-04-05)

Added

  • Will-o’-the-wisp app icon — Custom purple wisp flame icon for the macOS app bundle, replacing the default icon
  • Will-o’-the-wisp favicon — SVG and PNG favicons added to landing page and all documentation pages
  • SEO metadata — Open Graph, Twitter Card, and canonical URL tags across all site pages
  • CLAUDE.md development guide — Project conventions, build commands, and architecture overview for AI-assisted development

Fixed

  • Ready sessions no longer deleted while process is alive — Sessions in ready state are preserved as long as their Claude Code process is still running, preventing premature cleanup during idle periods

Distribution

  • ir:release skill — Automated release pipeline with DMG, PKG, universal binary builds, changelog updates, and GitHub release creation
  • DMG background asset for branded installer experience

v0.2.3 (2026-04-04)

Added

  • Subagent session lifecycle — Background and foreground subagents are detected as child sessions linked to their parent, with real-time state tracking and automatic cleanup when finished
  • Purple badge — Parent sessions display a live count of active subagents in a purple circle badge in the menu bar
  • Hierarchical dashboard APIGET /api/v1/sessions returns a DashboardResponse with Orchestrator → Group → Agent → Children hierarchy
  • PID discovery retry with backoff — Retries at 500ms, 1s, 2s intervals with CWD-based fallback when lsof fails
  • macOS app improvements — Debug mode (IRRLICHT_DEBUG=1), dev daemon support, clean shutdown, version display in UI, WebSocket keepalive with auto-reconnect
  • irrlicht-ls enhancements--format json output, --id prefix filter, hierarchical display with indented child sessions and agent count badges

Changed

  • Modular SessionDetector — Refactored into focused collaborators: StateClassifier (pure state transitions), MetadataEnricher (git/metrics), PIDManager (process lifecycle)
  • Unified processlifecycle package — Process scanner and process watcher merged into a single adapter
  • Cascade delete removes all child sessions when a parent session is deleted
  • Stale child sessions (transcript >2min old) cleaned up automatically in periodic sweep

Fixed

  • No false “waiting” state during tool execution — Only user-blocking tools (AskUserQuestion, ExitPlanMode) trigger waiting; Agent, Bash, Read etc. correctly show as working
  • Multi-instance session assignment — Running two Claude Code sessions in the same repo no longer causes PID conflicts; disambiguator ensures unique assignment
  • Subagent badge no longer persists after agent finishes
  • WebSocket connection state replaced isWatching flag with proper ConnectionState enum

v0.2.2 (2026-04-03)

Added

  • Embedded daemon in app bundle — Irrlicht.app bundles both SwiftUI UI and Go daemon; no separate services needed
  • DaemonManager — Auto-spawns, monitors, and restarts embedded daemon with exponential backoff
  • Session tooltips on hover in menu bar popover
  • DMG and PKG distribution artifacts

Fixed

  • Delete sessions immediately on process exit
  • Skip orphan transcript files on startup
  • Delete old session when /clear reuses same PID
  • Filter daemon self-PID from lsof
  • Ready-session TTL cleanup (30min default)

v0.2.0 (2026-04-03)

Added

  • OpenAI Codex adapter with recursive directory watching for sessions/YYYY/MM/DD/ structure
  • Per-model pricing data and EstimateCostUSD for cost tracking
  • Cost display in menu bar UI per session and per project group
  • Token breakdown tracking (input, output, cache read, cache creation)
  • Codex transcript event parsing (message, response_item, function_call, turn_context)
  • Codex model detection from ~/.codex/config.toml
  • Content character counting for token estimation when explicit counts unavailable
  • GPT-5.4 model capacity entry
  • Dark mode adaptive Codex SVG icon
  • Project name coloring by max context utilization (green/yellow/orange/red)
  • macOS .pkg installer bundling daemon + app + LaunchAgent

Changed

  • Filesystem watcher now recursively watches all subdirectories (supports deep nesting)
  • Git adapter resolves main repo root via --git-common-dir (worktree-aware)
  • Git adapter strips worktree- prefix from branch names
  • GetCWDFromTranscript now tail-reads last 32KB for latest CWD (supports mid-session worktree switches)
  • CWD/branch/project refreshed on every activity (not just on first detection)
  • Context utilization tests updated for 1M context windows
  • Token extraction refactored with shared extractUsage helper
  • Model config updated to v2.1.0 with pricing data for all models
  • build-release.sh now builds both daemon and Swift app, creates .pkg installer

Fixed

  • ESC cancellation detection using is_error flag on tool results
  • Parent session state adjustment based on sub-agent activity
  • Permission prompt detection as waiting state
  • Filesystem watcher race condition on directory creation

v0.1.0 (2026-03-20)

Added

  • Initial release
  • Claude Code session monitoring via filesystem watching
  • Three-state model: working, waiting, ready
  • macOS SwiftUI menu bar application
  • Go daemon (irrlichd) with HTTP API and WebSocket streaming
  • Context utilization tracking with pressure levels
  • Process exit detection via kqueue
  • Pre-session detection via process scanning
  • Git branch and project name resolution
  • mDNS/Bonjour service advertisement
  • Embedded web dashboard
  • irrlicht-ls CLI listing tool
  • Structured JSON logging with rotation
  • Gas Town orchestrator integration
  • Subagent detection and parent-child relationships

Format

This changelog follows the Keep a Changelog conventions. Versions use Semantic Versioning.

Types of changes:

  • Added — new features
  • Changed — changes in existing functionality
  • Deprecated — soon-to-be removed features
  • Removed — now removed features
  • Fixed — bug fixes
  • Security — vulnerability fixes