Changelog
May 31, 2026 · View on GitHub
All notable changes to Agent Deck will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
[Unreleased]
[1.9.45] - 2026-05-30
Added
- Near-instant idle-conductor completion delivery (wake-nudge wired) (#1225 / #1226). The durable outbox shipped in v1.9.44 is correct (no loss, exactly-once) but an idle conductor only drained on its next heartbeat — up to ~14 min of latency. The
WakeNudger(built but previously unwired) is now triggered from the single producer commit chokepoint (commitEventToInbox), which both producers funnel through: the interactiverunning→waitingpath and the one-shotrun-taskkernel-exit path. The moment a completion durably lands in a parent's inbox, an idle conductor is woken to drain it — collapsing the idle worst case from ~14 min to sub-second. The nudge is event-driven (fired on commit, not polled), conductor-scoped, idle-gated (never sends into a busy pane — a send-keys there only queues, the exact failure the pull model avoids), debounced per-parent (~500ms, coalesces a burst of simultaneous completions into one wake without delaying the first), and best-effort/fire-and-forget: a dropped or failed nudge is harmless because the same durable record is still drained on the parent's next Stop/heartbeat (wake ≠ deliver). A busy parent is intentionally left to drain at its next turn boundary — the physical floor for a busy Claude pane — so the nudge adds no noise there. Zero billed inference.
[1.9.44] - 2026-05-29
Added
- Durable per-parent outbox for inter-agent completions (#1225 / #1226). Child completions are committed to a durable, per-parent outbox (
~/.agent-deck/inboxes/<parent>.jsonl) and drained by the parent on its own schedule, replacing the push-into-tmux model that silently lost completions to an always-busy conductor. At-least-once delivery with exactly-once effects (last-wins per child, consumed-turn dedup ledger); survives parent busy-ness, restart, and compaction.
Changed
- Activated the durable-outbox comms engine (#1225 / #1226). The conductor Stop hook is now synchronous so Claude Code reads the
{decision:"block"}the hook emits and injects busy-parent completions at the next turn boundary, andagent-deck inbox drain selfis the first step of every conductor heartbeat (the idle-conductor fallback). The Stop-sync flip is conductor-scoped at runtime, not globally: a session with an empty inbox (every leaf/non-conductor session) fast-returns with no block and zero ledger writes, so the flip is inert for them. The loop guard is crash-safe and fails safe on an absentstop_hook_activeflag. Roll out canary-first to one conductor. Zero billed inference — the hook is a Go handler, noclaude -p.
Fixed
- Work-profile 401
/loginloop on session spawn/restart (#1222 / #1224). The scratch.credentials.jsonsymlink is re-asserted on spawn and on start/restart, stopping the work-profile credential loss that forced a re-login loop.
[1.9.43] - 2026-05-28
Added
- Opt-in kernel-exact task-worker completion (#1215). A one-shot task worker's exit is caught exactly-once and wakes the launching session reliably, replacing poll-inference for that path. Interactive sessions are unchanged.
Fixed
- notify-daemon can no longer run stale code (#1215). A
RuntimeMaxSecrecycle plus a version self-check stop the daemon from silently running old code — the cause of notification fixes not taking effect.
[1.9.42] - 2026-05-28
Fixed
- Attached-skills API now emits camelCase JSON so the web UI can read attached-skill fields (#1211).
ProjectSkillAttachmentpreviously lackedjson:tags, so the API emitted PascalCase while the frontend reads camelCase, and attached skills wouldn't display.
Internal
- Web parity guards re-baselined and the skills-service wired into the test fixture.
[1.9.41] - 2026-05-27
Security
- Web: unauthenticated non-loopback bind is now refused (#1209). Binding a non-loopback address now requires a token (closes the unauthenticated-RCE gap); use
--insecure-bindto override explicitly. The terminal bridge is token-gated and query-string tokens are rejected. - Remote:
remote updateverifies the release asset's SHA-256 before deploying (#1207). Adds a safe SSH host-key stance with no insecure host-key bypass. - install.sh verifies the downloaded binary's SHA-256 before install (#1210). Adds a skill-migration
RemoveAllpath-containment guard, passes the webhook secret via env instead of a CLI flag, and shell-quotes spawn args (no injection via session/dir names).
[1.9.40] - 2026-05-27
Added
- Opt-in
[shell] exit_to_shell— exit your agent, drop to a shell, then resume the same session (#1161, thanks @Djeeteg007). When enabled, exiting your agent (e.g./exit) drops you to an interactive shell at the same cwd so you can runaws-vault/direnv/etc., then resume the same session with full context preserved (default off).
[1.9.39] - 2026-05-27
Fixed
- Typing into a session now echoes in ~60ms instead of lagging up to ~2s (#1131, thanks @ddorman-dn). The insert-mode preview pane refreshes immediately after each keystroke instead of only on the 2s background tick.
[1.9.38] - 2026-05-27
Fixed (CRITICAL / data loss)
- Dismissing a session created via
worktree_reuseno longer deletes the user's original repository (#1200, thanks @mic-web). The dismiss path could runos.RemoveAllon the user's original repo; worktree removal is now guarded to only delete agent-deck-created worktrees under the managed dir.
[1.9.37] - 2026-05-27
Fixed
worktree.default_enablednow falls back to a normal session on non-git directories instead of failing (#1185, thanks @marekaf). When the default-worktree setting was on and you created a session in a directory that isn't a git repo, creation errored; it now degrades gracefully to a plain session.- New-session dialog "Type custom path/model" inputs now accept typed input (#1190, thanks @marekaf). Selecting the custom path or model option kept focus on the list instead of the text field, so keystrokes were swallowed (root cause #1023).
Added
- Capability-level E2E test suite — lifecycle (launch/stop/fork) and echo-agent round-trip coverage with a snapshot dashboard (#1191, #1193, #1194).
Changed
- Go dependency bumps (go-minor-patch group, #1180).
- Release workflow gained a
workflow_dispatchescape hatch for publishing a tag manually.
[1.9.36] - 2026-05-26
Two inter-agent comms backbone fixes that make conductor↔worker signalling trustworthy. As always the local release worker stops at git push origin <tag> and .github/workflows/release.yml is the single source of truth for goreleaser release --clean.
Fixed
- Conductors now get a trustworthy worker-"finished" signal (#1186). The worker prints a completion sentinel (
===AGENTDECK_DONE=== status=… summary=…) and agent-deck emits a real[DONE] Child finishedevent on the Stop hook edge instead of the ambiguous "waiting" status, so conductors no longer have to poll artifacts to know a task finished. - EVENT notifications no longer re-fire 10-40× for an idle session (#1187). The dedup key is now derived from append-only transcript content instead of the clock (which was re-stamped on every pane redraw), so an idle-but-animating Claude pane emits its
[EVENT]once.
[1.9.35] - 2026-05-26
Two community contributions: a configurable default model for new Claude sessions (#1172, credit @marekaf) and a tmux pane that now fills the full terminal width when a Claude session opens (#1167, credit @OrNatanAxon). As always the local release worker stops at git push origin <tag> and .github/workflows/release.yml is the single source of truth for goreleaser release --clean.
Added
- Configurable
default_modelfor new Claude sessions (#1172, credit @marekaf). The[claude]config block now accepts adefault_modelkey so new Claude sessions preselect your chosen model instead of always defaulting to Sonnet.
Fixed
- Claude session pane now fills the full terminal width on open (#1167, credit @OrNatanAxon). The attach PTY is now pre-sized to the terminal before attach, so the pane no longer opens at a narrow default width.
- Hardened the #1167 attach-width tests against CI load races (#1178). The tests now poll until the expected width is reached instead of relying on a fixed sleep, so they no longer flake under release-runner load.
[1.9.32] - 2026-05-25
Three community-reported bug fixes: a remote-update false-success loop (#1171, credit @javierciccarelli), the new-session model picker hiding typed input and swallowing Esc (#1162, credit @wbonnefond), and federated remote sessions flickering out on transient SSH errors (#1170, credit @devtechwebsource). As always the local release worker stops at git push origin <tag> and .github/workflows/release.yml is the single source of truth for goreleaser release --clean.
Fixed
remote updateno longer reports a false success while leaving the remote on the old version (#1171, credit @javierciccarelli). Root cause:DeployBinarySCP'd the new binary to the bare relative pathagent-deck(→~/agent-deckin the SSH user's home), whileCheckBinaryranagent-deck versionthrough the remote$PATH(→~/.local/bin/agent-deckfrominstall.sh). Deploy and check targeted different files, so the command printed✓ Installed vXwhile the remote kept running the old binary; a secondremote updateagain reported "outdated", looping forever. Two-part fix: (1)ResolveRemotePathnow deploys to the binary the remote actually executes —command -v agent-deck, falling back to theinstall.shdefault$HOME/.local/bin/agent-deck— and creates the parent directory unconditionally; (2)InstallBinaryverifies the remote's$PATHbinary reports the new version before claiming success, surfacing an actionable error (e.g. "deployed to X but remote runs vY from $PATH") instead of a false ✓. Pinned byinternal/session/issue1171_remote_update_path_test.go. Upgrade note: hosts left with a stray~/agent-deckfrom earlier broken runs can delete it (rm ~/agent-deck); it is harmless and never executed.- New-session model picker now echoes typed input and scopes Esc to the picker (#1162, credit @wbonnefond). Two UX bugs: (1) the suggestions dropdown overlay was positioned by counting raw content newlines, but the command-button row above the model field wraps to extra visual lines at narrow widths, so the undercount dropped the dropdown on top of the model input and hid whatever the user typed — now positioned from the visual (width-wrapped) line count via
lipgloss.Height; (2)Esckilled the entire new-session flow becausehome.gocallednewDialog.Hide()before the dialog could treat it as close-self for the picker — now the parent forwardsEscto the dialog whenIsModelPickerOpen()is true, dismissing only the picker (form stays alive, typed value preserved) while a secondEsc, orEscon any other field, cancels the flow as before. Pinned byinternal/ui/issue1162_model_picker_test.go. - Federated remote sessions no longer flicker out on a transient SSH error (#1170, credit @devtechwebsource). The 30s poll shared a single 15s context across all remotes fetched sequentially (one slow/offline remote starved the others) and the handler did a wholesale
h.remoteSessions = msg.sessions, wiping last-good data for any remote missing from a partial fetch. Fix: each remote is now fetched in parallel with its own 15s timeout, and the new puremergeRemoteSessionskeeps last-good sessions for errored remotes while successful fetches replace wholesale (new sessions appear, removed sessions drop, deconfigured remotes drop). Poll cadence is configurable via[ui] remote_session_refresh_secs(default 15s, clamped 5–300). Pinned byinternal/session/issue1170_remote_session_refresh_test.goandinternal/ui/issue1170_remote_refresh_test.go.
[1.9.31] - 2026-05-23
A targeted release led by the structural telegram-leak fix (#1164, closes #1163) — the flagship change of this cycle — plus four community contributions from @spawnia (multi-repo trust + imports, hidden-terminal fix, CSRF protection) and a security bump of golang.org/x/net. v1.9.31 is the twenty-sixth release cut under the Option A pipeline (#981 in v1.9.6); the local release worker stops at git push origin <tag> and .github/workflows/release.yml is the single source of truth for goreleaser release --clean.
Fixed
-
Telegram can no longer leak into conductor-spawned children (three structural defenses) (#1164, closes #1163). Root cause (forensic investigation): conductor children inherited the conductor's
CLAUDE_CONFIG_DIR— a worker-scratch profile whosesettings.jsonhastelegram@claude-plugins-official: true— because the scratch-pin gatehostHasTelegramConductor()read only the legacy single-bot[conductor.telegram].token, which is empty ("") under the modern 7-botenv_filetopology. The gate stayed disarmed, no per-childtelegram=falsescratch was created, the child loaded the telegram plugin with noTELEGRAM_STATE_DIR, fell back to the default bot dir, and spawned a duplicatebun telegrampoller (409 Conflict storm, dropped messages). Three by-construction fixes:- Change 1 — repaired the dead gate.
configDeclaresTelegram()now detects telegram via the modern topology: any[conductors.<name>].claude.env_filewhose.envrcexportsTELEGRAM_STATE_DIRarms the gate, in addition to the legacy token. This single repair re-enables the existing #1137 scratch-pin defense for every spawn path. Verified live: all 7 conductor env_files arm the gate while the legacy token is empty. - Change 2 —
childenvchokepoint + forbidigo lint. New leaf packageinternal/childenv(andsession.ChildLaunchEnv) strips inheritedCLAUDE_CONFIG_DIR(#1163) and everyTELEGRAM_*var (#1152) from a child's env, pinning the child's own config dir. The pooled-MCP spawn paths (internal/mcppool/{http_server,socket_proxy}.go) route their base env through it. A.golangci.ymlforbidigo rule bans rawos.Environ()in spawn paths so the leak cannot be reintroduced. - Change 3 — process-group reaping.
internal/mcppool/http_server.gonow spawns withSetpgidand SIGTERM/SIGKILLs the whole process group on stop (matchingsocket_proxy.go), so a launcher's grandchildren (bun wrappers, npx/uvx subprocesses) are reaped as a unit instead of orphaning under PID 1. - Pinned by
internal/session/issue1163_telegram_structural_test.go,internal/session/issue1163_procgroup_unix_test.go,internal/session/issue1163_forbidigo_test.go, andinternal/childenv/childenv_test.go.
- Change 1 — repaired the dead gate.
-
Hidden terminal tab no longer shrinks the tmux window (PR #1157, credit @spawnia). A hidden Web terminal tab reported a zero/stale viewport size, causing tmux to resize the underlying window down to the hidden tab's dimensions and corrupting the visible session's layout. Fix prevents the hidden tab from driving the tmux resize.
-
CSRF protection for Web UI mutation endpoints (PR #1158, credit @spawnia). Web UI state-changing endpoints lacked CSRF defenses. Adds token-based CSRF protection (
internal/web/csrf.go, pinned byinternal/web/csrf_test.go) so mutation requests cannot be forged cross-origin.
Added
-
Multi-repo Claude trust pre-accept + parent
CLAUDE.mdemission (PR #1155, closes #1149, credit @spawnia). Pre-accepts Claude's trust prompt and emits a parentCLAUDE.mdfor multi-repo sessions so additional working directories are trusted without manual confirmation. -
Multi-repo
@pathimports + permission settings in Claude context (PR #1156, credit @spawnia). Enhances the generated Claude context for multi-repo sessions with@pathimports and permission settings so each repo's instructions and permissions are wired in automatically.
Security
- Bumped
golang.org/x/netto v0.55.0 (GO-2026-5026). Pulls in the upstream fix for the advisory.
[1.9.30] - 2026-05-22
A 5-fix improvement-cycle release on top of v1.9.29, closing four production-observable issues plus one Web UI parity gap. v1.9.30 is the twenty-fifth release cut under the Option A pipeline (#981 in v1.9.6); the local release worker stops at git push origin <tag> and .github/workflows/release.yml is the single source of truth for goreleaser release --clean.
Fixed
-
Explicit
--session-idpreserved in multi-session-per-cwd JSONL routing (PR #1148, closes #1147, credit @KrE80r's RCA). When two child sessions shared a cwd, the second session's explicit--session-idflag was silently overwritten by the first session's id, causing JSONL transcript hijack between unrelated agents. Fix preserves the caller-provided session-id verbatim through the multi-session-per-cwd path so each session writes to its own JSONL. -
Lefthook pre-push race between css-verify and lint (PR #1151, closes #1146). The recurring v1.9.x release pain — pre-push hooks intermittently failing because css-verify and lint shared a working tree and stomped each other's intermediate artifacts. Fix serializes the two pre-push commands so they never run concurrently, removing the race that has bitten multiple recent release attempts.
-
All
TELEGRAM_*env vars stripped from child sessions (PR #1152, closes #1133). Paired with the telegram reliability work in #1137: child sessions were inheriting conductor-scopedTELEGRAM_*env vars (includingTELEGRAM_STATE_DIRand bot token), causing children to inadvertently bind to the parent's telegram channel. Fix scrubs everyTELEGRAM_*env var from the child env before spawn so channel ownership stays with the conductor that registered it. -
GitHub releases API calls authenticated to avoid anonymous rate limit (PR #1154, closes #1150, credit @DaniFdz). The update checker hit GitHub's 60-req/hour anonymous rate limit on shared NAT egress IPs, leaving users on the same network unable to receive update notifications. Fix sends
GITHUB_TOKEN(when present) as aBearerauth header, raising the cap to 5000 req/hour for token-holders and degrading gracefully to anonymous when no token is set.
Added
- Web UI
worktree-finishendpoint + UI (PR #1153, closes #1126). Closes aPARITY_MATRIX.mdgap: the TUI's worktree-finish action (merge child branch back, clean up worktree) had no Web UI equivalent. New endpointPOST /api/sessions/{id}/worktree/finishplus a Sidebar control bring the Web view to parity. Pinned byinternal/web/issue1126_worktree_finish_test.go(227 LOC) andtests/web/e2e/worktree-finish.spec.js.
[1.9.29] - 2026-05-21
A 3-fix follow-up to v1.9.28 closing out the recursive self-improvement → file-issue → TDD-fix cycle that surfaced three production-observable gaps. v1.9.29 is the twenty-fourth release cut under the Option A pipeline (#981 in v1.9.6); the local release worker stops at git push origin <tag> and .github/workflows/release.yml is the single source of truth for goreleaser release --clean.
Fixed
-
Codex + Gemini session-id rebinds now persisted to SQLite (PR #1141, fixes #1139, mirror of #1138 in v1.9.28). The Claude
/clearrebind persistence fix in #1140 left the same gap open for the codex and gemini wrappers: when those tiles rebound to a fresh session-id, the in-memoryInstanceupdated butstate.dbkept pinning the pre-rebind UUID intool_data.codex_session_id/tool_data.gemini_session_id. Third-party consumers joining ontool_datasaw stale UUIDs and could not follow the live conversation. Fix mirrors the Claude rebind-persist path for both wrappers, with 636 lines of regression tests (issue1139_codex_session_persist_test.go+issue1139_gemini_session_persist_test.go) pinning the invariant. -
Status-transition notifier de-duplicates identical
[EVENT]notifications (PR #1144, closes #1142). Self-improvement telemetry surfaced a 47×-loop pattern: a single child status flap could fan out 47 duplicate[EVENT]lines to the conductor within seconds when output-hashes matched. Fix keys de-duplication on(child, status, output-hash)so identical events collapse to one delivery, eliminating the loop while keeping legitimate distinct events. Pinned byinternal/session/issue1142_event_dedup_test.go.
Added
--idle-timeout <duration>flag onlaunch+session(PR #1145, closes #1143). Self-improvement telemetry surfaced a 4×-dormant-worker pattern: child sessions sitting idle for hours after their parent had moved on, holding tmux + claude resources. New--idle-timeoutflag auto-stops dormant children once they exceed the configured duration (e.g.--idle-timeout 30m). Configurable per-session at any time viaagent-deck session set <id> idle_timeout 30m. Idle watcher lives ininternal/session/idle_timeout_watcher.gowith persistence inidle_timeout_persist.go. Pinned byissue1143_idle_timeout_test.go(368 LOC) +issue1143_idle_timeout_cli_test.go.
[1.9.28] - 2026-05-21
A single-fix follow-up to v1.9.27 closing the Claude /clear rebind persistence gap reported in #1138. v1.9.28 is the twenty-third release cut under the Option A pipeline (#981 in v1.9.6); the local release worker stops at git push origin <tag> and .github/workflows/release.yml is the single source of truth for goreleaser release --clean.
Fixed
- Claude session-id rebind now persisted to SQLite after
/clear(PR #1140, fixes #1138, credit @tarekrached). When a Claude tile ran/clearand rebound to a fresh session-id, the in-memoryInstanceupdated butstate.dbkept pinning the pre-/clearUUID intool_data.claude_session_idforever. Third-party consumers that join ontool_data(e.g. claudopticon) saw stale UUIDs and could not follow the live conversation. Fix persists the rebind through to SQLite at the moment of rebind, with a 398-line regression test (internal/session/instance_rebind_persist_test.go) pinning the invariant.
[1.9.27] - 2026-05-21
A telegram-reliability double-fix release on top of v1.9.26, hardening the recurring telegram MCP drop on conductor restart. v1.9.27 is the twenty-second release cut under the Option A pipeline (#981 in v1.9.6); the local release worker stops at git push origin <tag> and .github/workflows/release.yml is the single source of truth for goreleaser release --clean.
Fixed
- Recurring telegram MCP drops on conductor restart (PR #1136, PR #1137, fixes #1134, #1138). Two-layer defense: scratch
settings.jsonis force-corrected on every spawn (#1137) so a channel-owning conductor session can never start with the telegram plugin disabled, plus a post-spawn health warning if the plugin fails to load. The initial fix in #1136 wrotesettings.jsoncorrectly on session creation but did not enforce the invariant on subsequent spawns; #1137 closes that gap with a 4th gate that re-validates on every spawn.
Added
agent-deck telegram-doctorCLI (PR #1137). New runtime health-monitoring command that audits the telegram plugin across all conductor sessions — reports per-session plugin status, settings.json correctness, and surfaces drift between expected and live state. Backed by a CI regression workflow (.github/workflows/telegram-reliability.yml) that pins the invariants going forward.
[1.9.26] - 2026-05-21
A same-day web UI feature parity wave on top of v1.9.25, focused on the PARITY_MATRIX.md gap list: the Children panel flips from stub to functional, 30 session state fields land on MenuSession JSON (unblocking the Edit-dialog stream), and the non-destructive Close + Undo Delete lifecycle ops reach parity with the TUI's Shift+D / Ctrl+Z. v1.9.26 is the twenty-first release cut under the Option A pipeline (#981 in v1.9.6); the local release worker stops at git push origin <tag> and .github/workflows/release.yml is the single source of truth for goreleaser release --clean.
Added
-
Children panel for conductor sessions (PR #1127, closes #1125). The right-rail "Children (conductor)" pane previously rendered the stub
Conductor child topology not exposed via web API.; this PR closes that gap. New endpointGET /api/sessions/{id}/childrenreturns a nested tree built from the sameMenuSnapshotthe session list uses, so child status and hook refresh stay consistent with the rest of the web view. Cycle-safe via a visited set against corrupt parent pointers. Unknown session id returns404 NOT_FOUND, non-conductor sessions return200withchildren:[](per spec, not 404), leaf nodes always returnchildren:[](never null), non-GET returns405. UI lands ininternal/web/static/app/RightRail.jsas a collapsible tree. -
All session state fields exposed on
MenuSessionJSON (PR #1128, closes 30 MISSING rows intests/web/PARITY_MATRIX.md). Promotes 30 state-field rows from MISSING to Present by surfacing the matching*session.Instancefields onMenuSessionJSON so a web client can render the same edit form as the TUI without a secondary lookup; unblocks the Edit-dialog parity stream (top-5 priority #1 from the master plan). New fields (allomitempty, backward compatible):is_conductor,claude_session_id,gemini_session_id,gemini_model,gemini_yolo_mode(*bool, where&falsemarshals asfalse),codex_session_id,opencode_session_id,latest_prompt,notes,color,command,wrapper,channels,extra_args,tool_options_json,sandbox,sandbox_container,ssh_host,ssh_remote_path,multi_repo_enabled,additional_paths,multi_repo_temp_dir,multi_repo_worktrees,worktree_path,worktree_repo_root,worktree_branch,title_locked,no_transition_notify,loaded_mcp_names,gemini_analytics. Three rows stay MISSING with documented reasons (is_fork_awaiting_start,skip_mcp_regeneratearejson:"-"transients; the third is a TUI-only render flag). -
Non-destructive Close + Undo Delete for web sessions (PR #1129, closes 2 MISSING rows in
tests/web/PARITY_MATRIX.md). Adds the two lifecycle operations that previously existed only in the TUI:POST /api/sessions/{id}/closestops the tmux process but keeps metadata in storage (mirrors TUI Shift+D);POST /api/sessions/undeleteis a Chrome-style undo of the most-recent delete within a 30s window (web.DefaultUndoWindow; mirrors TUI Ctrl+Z).SessionMutatorgainsCloseSession+UndoDelete. TheWebMutatorpushes the deletedInstanceonto an in-memory undo stack (capped at 10, FIFO eviction) beforestorage.DeleteInstance;UndoDeletepops,Restart()s, and re-saves. SPA wiring ininternal/web/static/app/AppShell.js(Shift+D now POSTs/closeinstead of/stop; Ctrl+Z POSTs/undelete), withKeyboardShortcuts.jshelp overlay updated. Pinned by 12 new cases ininternal/web/handlers_sessions_test.go(happy / error / disabled / nil-mutator / SSE for both endpoints) andtests/web/e2e/close-undo.spec.js(delete→undelete roundtrip, LIFO ordering, 404 on empty stack, Shift+D UI flow).
Internal
- Deduplicate
parent_session_idrow inPARITY_MATRIX.md(PR #1130). The matrix sweep in #1128 left a duplicateparent_session_idrow that broke thetests/web/PARITY_MATRIX.md-shape assertions onmain. Hotfix removes the duplicate row. Test-data only, no behavior change.
[1.9.25] - 2026-05-21
A multi-track follow-up to v1.9.24: two more remote-session fixes from @ddorman-dn (closing the #1112 cluster + the screen-scaling / insert-buffer regression #1113), the iTerm badge fix for #1114 from @tarekrached, and a multi-repo correctness fix + refactor from @spawnia restoring .worktreeinclude and per-repo setup. On top of the bug bucket, both the Skills and MCP management surfaces land in the Web UI — the two tabs flip from empty stubs to functional, closing the remaining MISSING rows in PARITY_MATRIX.md. v1.9.25 is the twentieth release cut under the Option A pipeline (#981 in v1.9.6); the local release worker stops at git push origin <tag> and .github/workflows/release.yml is the single source of truth for goreleaser release --clean.
Added
-
Skills management endpoints + Web UI (PR #1122, closes the Skills MISSING rows in
PARITY_MATRIX.md). The web UI gains a full Skills surface: list/attach/detach pool skills, view SKILL.md contents, and manage user vs pool scope — mirroring the CLI/TUI affordances so the web client is no longer a second-class citizen for skill workflows. Endpoints land ininternal/web/skills_handlers.gowith the SPA wiring ininternal/web/static/app/SkillsPanel.jsandinternal/web/static/app/AppShell.js. Pinned byinternal/web/skills_handlers_test.goandtests/web/e2e/skills-panel.spec.js. -
MCP management endpoints + Web UI (PR #1124, closes the 4 MCP MANAGEMENT MISSING rows in
PARITY_MATRIX.md). The web UI gains parity with the TUI'smkey MCPDialog: list the catalog fromconfig.toml, attach/detach MCPs per session, and toggle pooled ↔ local scope — all without a terminal. Five new endpoints (GET /api/mcps,GET/POST/DELETE/PATCH /api/sessions/{id}/mcps[/{name}]) route through a hermeticMCPManagerseam whose default delegates to the sameinternal/sessionhelpers the TUI uses, so write paths are by construction equivalent to the TUI. Implementation lands ininternal/web/handlers_mcps.go+internal/web/static/app/panes/McpPane.js. Pinned byinternal/web/handlers_mcps_test.go(17 Go tests covering list/attach/detach/move, scope validation, mutations-disabled gate, unknown session, UTF-8 names, manager errors) andtests/web/e2e/mcps.spec.js(12 Playwright cases). Combined with #1122, the Web UI's Skills and MCP tabs both flip from empty stubs to first-class surfaces in this release.
Fixed
-
iTerm badge updates route through the attach process (PR #1116, closes #1114, credit @tarekrached). Renaming a session left the iTerm badge stuck on the old name because the badge escape was emitted from the wrong process and never reached the user's terminal. Fix routes the badge update through the attach process so iTerm picks it up on the next paint. Pinned by
internal/tmux/issue1114_badge_test.go. Credit @tarekrached for the report and patch. -
Multi-repo worktrees run
.worktreeincludeand the setup script per repo (PR #1118, credit @spawnia). Multi-repo worktree creation was skipping.worktreeincludeprocessing and the per-repo setup hook for every repo after the first, leaving auxiliary repos in a half-initialized state. Fix iterates the full repo set so each one gets its own include resolution + setup script invocation. Credit @spawnia for the report and patch. -
Remote sessions: waiting-status + arrow keys + insert perf (PR #1120, closes #1112, follow-ups to #1102 / #1110, credit @ddorman-dn). The remote-session surface in v1.9.24 still had three rough edges: the waiting-status indicator never updated, arrow keys weren't reaching the remote pty, and insert-mode perf was slower than the local fix in #1110. This PR wires waiting-status detection through the remote codepath, threads the arrow-key sequences through the remote keysender, and lifts the local insert-mode batching into the remote path so latency now matches local. Credit @ddorman-dn for the real-user report.
-
Screen scaling at narrow widths + insert buffer stale on session switch (PR #1123, closes #1113, credit @ddorman-dn). Two coupled UI bugs: narrow terminals were laying out columns at the wrong width (overflowing the Sessions list), and the insert-mode keystroke buffer kept the previous session's pending keys when the user switched sessions, so the first keystroke into a new session looked like garbage. Fix recomputes the scaling on resize and resets the insert buffer at the session-switch boundary. Credit @ddorman-dn for the real-user report.
Internal
-
golangci-lint v2 pinned in the Makefile (PR #1119, credit @spawnia). The repo's
.golangci.ymlhas been v2 for a while, but the Makefile was still installing the v1 linter, so contributors hit confusing errors locally. Fix installs golangci-lint v2 to match the config. Build/lint-only, no behavior change. Credit @spawnia for the patch. -
Extract
CreateMultiRepoWorktreesinto a testable function (PR #1121, credit @spawnia). Multi-repo worktree creation was inlined in the CLI command, which made it impossible to unit-test the #1118 fix above. This refactor extracts the orchestration intoCreateMultiRepoWorktreeswith its own test, locking the behavior in place. Refactor-only, no behavior change. Credit @spawnia for the patch.
[1.9.24] - 2026-05-20
A second hotfix wave on top of v1.9.23, also same-day: five user-feedback fixes from @ddorman-dn (covering the remote-session surface — Shift+Enter, preview pane + cost/usage, latency markers, insert-mode perf), the re-close of the long-running #953 stopped-status bucket bug from @halfmu, plus two internal build/lint hotfixes that kept main green. v1.9.24 is the nineteenth release cut under the Option A pipeline (#981 in v1.9.6); the local release worker stops at git push origin <tag> and .github/workflows/release.yml is the single source of truth for goreleaser release --clean.
Added
- Remote session latency markers in header (PR #1106, closes #1103, credit @ddorman-dn). The TUI header now renders latency markers for remote sessions so the user can see SSH/transport delay at a glance instead of guessing why a remote pane is lagging. Credit @ddorman-dn for the request and real-user testing.
Fixed
-
Shift+Enter handles remote sessions + opens iTerm tab by default (PR #1105, closes #1100, follow-up to #1098, credit @ddorman-dn). The Shift+Enter chord wired in #1098 didn't cover remote sessions and opened a new iTerm window instead of a tab. Fix routes remote sessions through the same dispatcher and switches the default launcher behavior to a new tab. Credit @ddorman-dn for the real-user report.
-
Remote session preview pane + cost/usage propagation (PR #1107, closes #1101, credit @ddorman-dn). Remote sessions weren't rendering content in the preview pane and their cost/usage counters never reached the UI. Fix restores both code paths so remote sessions look the same as local ones in the Sessions list. Credit @ddorman-dn for the real-user report.
-
Insert mode actual perf fix + remote-session support (PR #1110, closes #1102, follow-up to #1096, credit @ddorman-dn). The insert-mode batching in #1096 helped local sessions but didn't address the actual latency root cause and skipped remote sessions entirely. New
internal/tmux/keysender.go+internal/session/remote_keysender.godeliver real per-keystroke throughput and route remote sessions through the same path. Pinned byinternal/ui/issue1102_insert_perf_test.goandinternal/session/issue1102_insert_remote_test.go. Credit @ddorman-dn for the real-user report. -
Status line counter buckets stopped sessions separately (PR #1108, re-closes #953, credit @halfmu). The status-line session counter was still lumping stopped sessions into the active count after the persistence fix in #1072, defeating the point of the stopped state at a glance. Fix gives stopped sessions their own bucket in the counter. Credit @halfmu for staying on this one.
Internal
- Restore missing closing brace + gofmt in
userconfig(PR #1109). Hotfix for a redmainintroduced by an earlier merge; build/lint-only, no behavior change. - Rename
stripANSI→stripANSILatencyin latency test (PR #1111). Resolves a duplicate-symbol build break witheval_smoke; test-only, no behavior change.
[1.9.23] - 2026-05-20
A same-day hotfix wave on top of v1.9.22: three TUI regressions surfaced by real-user testing from @ddorman-dn, plus one small feature ask from the same reporter. All four community-credited. v1.9.23 is the eighteenth release cut under the Option A pipeline (#981 in v1.9.6); the local release worker stops at git push origin <tag> and .github/workflows/release.yml is the single source of truth for goreleaser release --clean.
Added
- Configurable Sessions/Preview split (PR #1099, closes #1092, credit @ddorman-dn). The TUI's left/right pane ratio between the Sessions list and the Preview pane is now user-configurable via
userconfig, persisted across runs and exposed through a new keybinding. Implementation lands ininternal/ui/split_config.go,internal/ui/home.go,internal/ui/help.go, andinternal/session/userconfig.go, pinned byinternal/ui/issue1092_split_test.go. Credit @ddorman-dn for the request and real-user testing.
Fixed
-
SSH remote sessions get the claude-specific render (PR #1095, closes #1091, follow-up to #1073, credit @ddorman-dn). Sessions started against an SSH remote were falling back to the generic render path instead of the claude-specific one introduced in #1073, breaking the preview output for remote claude sessions. Fix routes SSH remotes through the same detection branch as local claude sessions. Credit @ddorman-dn for the real-user report.
-
Insert mode supports Backspace, arrow keys, control keys + batches for latency (PR #1096, closes #1094, credit @ddorman-dn). Insert mode previously dropped Backspace, the arrow keys, and several control keys, and per-keystroke writes added perceptible latency on slower terminals. The keyboard layer now passes those keys through and batches writes for smoother input. Implementation in
internal/ui/keyboard_compat.goandinternal/ui/home.go, pinned byinternal/ui/issue1094_insert_mode_ux_test.go. Follow-up PR #1097 cleaned up three SA4006 dead assignments from the test that brokemainCI. Credit @ddorman-dn for the real-user report. -
Shift+Enter wired to the iTerm launcher on darwin (PR #1098, closes #1093, follow-up to #1077, credit @ddorman-dn). The Shift+Enter keybinding to launch the active session in iTerm landed in #1077 but never reached the darwin dispatcher, so the chord was a no-op on macOS. Fix wires the binding through to the iTerm launcher on darwin only, pinned by
internal/ui/issue1093_shift_enter_test.go. Credit @ddorman-dn for the real-user report.
[1.9.22] - 2026-05-20
A trio of substantive PRs land on top of v1.9.21: two community-credited features (per-session account profile slot, web/TUI keyboard parity) plus the production wiring fix that finally closes the long-running #965 MCP-child-reap saga. v1.9.22 is the seventeenth release cut under the Option A pipeline (#981 in v1.9.6); the local release worker stops at git push origin <tag> and .github/workflows/release.yml is the single source of truth for goreleaser release --clean.
Added
-
Per-session account profile slot (PR #1089, closes #924, credit @bautrey). Each session now carries an
accountfield that selects which agent-account profile to use at spawn time, so a single agent-deck instance can drive multiple Claude/Codex/Gemini accounts side-by-side without manual env-juggling. MVP wires the field through the session model (internal/session/storage.go,internal/session/mutators.go,internal/statedb/statedb.go), the spawn pipeline (internal/session/claude.go,internal/session/instance.go), and the CLI (cmd/agent-deck/session_cmd.go). Pinned byinternal/session/issue924_account_field_test.go(field round-trips through storage + mutators) andinternal/session/issue924_account_switch_test.go(switching accounts mid-session is reflected in the next spawn). Credit @bautrey for the original implementation and reviews. -
Web/TUI keyboard parity for top-10 bindings +
?overlay (PR #1090, closes #780 MVP, credit @JMBattista). The web UI now responds to the same top-10 keystrokes as the TUI (session navigation, start/stop, group focus, command palette) and ships a?overlay listing every binding inline. Implementation lands ininternal/web/static/app/KeyboardShortcuts.js,internal/web/static/app/AppShell.js,internal/web/static/app/CommandPalette.js, andinternal/web/static/app/Topbar.js, with styling inapp.css. Pinned bytests/web/e2e/keyboard-parity.spec.js(Playwright) with golden screenshots for desktop + tablet (tests/web/screenshots/keyboard-parity.spec.js/). Credit @JMBattista for the original implementation.
Fixed
- Complete #965 production wiring:
Instance.Killnow reaps MCP children correctly (PR #1088, closes #1086). The MCP-child-reap path landed in #965 but never wired into the productionInstance.Killcallsite, leaving orphaned MCP child processes on session stop in real-world use. Fix hardens reap with a single-snapshot discovery + post-SIGKILL verify loop (internal/session/mcp_child_reap.go) and wires it intoInstance.Kill(internal/session/instance.go). The CI-gated regression test that was skipped in v1.9.21 (PR #1085) is un-skipped here (internal/session/issue965_wiring_test.go), restoring the gate against future regressions of the same shape.
[1.9.21] - 2026-05-20
A security + maintenance wave on top of v1.9.20. The repo gains a full PR review pipeline (CodeQL, CodeRabbit, govulncheck strict, golangci-lint, Dependabot, diff-scope guard, CODEOWNERS, SECURITY.md) and the Go toolchain jumps from 1.24 to 1.25.10 — closing 35 stdlib CVEs in one move. Two community-credited bug fixes land alongside (rename dialog focus, StatusStopped persistence), plus the routine sweep of GitHub Actions and Bubble Tea dependency bumps. v1.9.21 is the sixteenth release cut under the Option A pipeline (#981 in v1.9.6); the local release worker stops at git push origin <tag> and .github/workflows/release.yml is the single source of truth for goreleaser release --clean.
Security
- Full PR review pipeline (PR #1052). CodeQL static analysis, Dependabot for Go modules + GitHub Actions, govulncheck (now strict-fail), golangci-lint, CODEOWNERS, SECURITY.md, and
.coderabbit.yamlall land together. Every PR is now reviewed by both CodeRabbit and CodeQL before merge; vulnerable dependencies fail CI instead of warning. - Diff-scope guard blocks runaway PRs (PR #1053). A PR touching >200 files now fails the diff-scope check, forcing a split. Catches accidental vendor commits, mass renames without review, and bot-generated megadiffs at the gate.
- Dependabot grouping restricted to minor+patch only (PR #1058). Major version bumps no longer get bundled with safe updates; each major lands as its own reviewable PR.
- Go toolchain bumped 1.24 → 1.25.10 (PR #1065, closes #1054). Closes 35 Go stdlib CVEs in one move. govulncheck is now strict-fail in CI against this toolchain.
Fixed
- Rename group dialog focuses the name input on open (PR #1071, closes #1068, credit @ddorman-dn). The rename dialog previously opened without keyboard focus on the text input, so the first keystrokes either no-op'd or hit the underlying list. Fix lands in
internal/ui/group_dialog.go, pinned byinternal/ui/issue1068_rename_focus_test.go. StatusStoppedpersists across reloads (PR #1072, closes #953, credit @halfmu). Manually stopped sessions had theirStatusStoppedflipped back on the next state reload, defeating the user's stop intent. The instance now preservesStatusStoppedthrough reload (internal/session/instance.go), pinned byinternal/session/issue953_stop_persists_test.go.
Changed
charmbracelet/bubbles0.21.0 → 1.0.0 (PR #1059). Stable 1.x of the Bubble Tea component library.actions/checkoutv4 → v6 (PR #1061).actions/setup-gov5 → v6 (PR #1063).actions/setup-pythonv5 → v6 (PR #1064).actions/deploy-pagesv4 → v5 (PR #1060).
[1.9.20] - 2026-05-19
Four merged community-PR takeovers on top of v1.9.19, all landing in a single wave: first-class copilot session detection, a walltime regression suite for cold start + group lifecycle, watcher routed-event dispatch into the conductor's tmux pane, and conductor heartbeat pausing after inactivity. v1.9.20 is the fifteenth release cut under the Option A pipeline (#981 in v1.9.6); the local release worker stops at git push origin <tag> and .github/workflows/release.yml is the single source of truth for goreleaser release --clean.
Added
-
First-class copilot session detection + resume + model/allow-all config (PR #1046, takeover of @toughhou's #851). Copilot CLI sessions are now detected via the same content-pattern + binary-basename pathway used by the other first-class agents, with resume support and a config surface for model selection and allow-all mode. Credit @toughhou for the original implementation.
-
Walltime regression suite for cold start + group lifecycle (PR #1047, takeover of @JMBattista's #790). New perf harness in
internal/testutil/perfbudgetpluscmd/agent-deck/coldstart_perf_test.go, wired into.github/workflows/perf-smoke.yml. Two-layer absolute + delta budgets pin cold-start and group-lifecycle walltimes against the current baseline; suite is documented indocs/perf-budget-suite.md. Credit @JMBattista for the original implementation. -
Watcher dispatches routed events to the conductor's tmux pane (PR #1048, takeover of @martins-fresh's #939). Routed watcher events now flow into the conductor session's tmux pane instead of stopping at the event bus, closing the loop for channel-driven workflows. Pinned by
internal/ui/watcher_listener_test.goandinternal/watcher/engine_test.go. Credit @martins-fresh for the original implementation. -
Conductor pauses heartbeats after inactivity (PR #1049, takeover of @yaroshevych's #839). Long-idle conductors no longer burn cycles emitting heartbeats; the heartbeat loop now pauses after a configurable inactivity window and resumes on the next routed event. Pinned by
internal/session/conductor_test.go. Credit @yaroshevych for the original implementation.
[1.9.19] - 2026-05-19
Three merged PRs on top of v1.9.18: Hermes lands as the ninth first-class builtin agent, the long-standing P0 "Resume from summary" picker freeze on long-running conductors is finally auto-confirmed, and a P1 restart storm on Claude exit is suppressed by a single-flight guard. v1.9.19 is the fourteenth release cut under the Option A pipeline (#981 in v1.9.6); the local release worker stops at git push origin <tag> and .github/workflows/release.yml is the single source of truth for goreleaser release --clean. Headline addition: Hermes Agent CLI joins claude, codex, gemini, copilot, opencode, qwen, crush, and cursor as a first-class agent kind (PR #1044, takeover of @sergeytrofimovsky's #951, closes #919). Headline fixes: the "Resume from summary" picker that wedged conductors past ~250k tokens is now auto-confirmed on restart (PR #803, closes #67 — the oldest open P0 in the tracker), and the restart storm where a Claude exit could trigger a thundering herd of spawn-restart attempts is gated behind a single-flight guard (PR #1045, closes #1040, credit @torcuaine-sketch).
Added
- Hermes Agent CLI as the ninth first-class builtin agent (PR #1044, takeover of @sergeytrofimovsky's #951, closes #919). Hermes (
hermesfrom NousResearch/hermes-agent) joins claude, codex, gemini, copilot, opencode, qwen, crush, and cursor as a first-class agent kind. Icon ☤, color gold. Config via[hermes]section withcommand,env_file, andyolo_mode. Status reporting is process-alive/dead (content-sniffing of the pane for richer states is deferred to a follow-up). Detection runs via binary basename + content patterns (internal/tmux/tmux.go, pinned byinternal/tmux/hermes_test.goandcmd/agent-deck/hermes_detect_test.go). Session lifecycle and command construction land ininternal/session/hermes.go(pinned byinternal/session/hermes_test.go). #951 was stale for ~9 weeks after @sergeytrofimovsky's last push; #1044 picks up the branch, rebases on top of the cursor scaffolding from v1.9.18, and threads it through the same TUI/preset/wizard plumbing as the other eight agents. Credit @sergeytrofimovsky for the original implementation.
Fixed
-
Auto-confirm Claude "Resume from summary" picker on long-running conductors (#67, PR #803). Previously, once a session crossed ~250k tokens,
claude --resumeshowed an interactive picker that an unattended conductor could not answer, leaving the session frozen onwaiting. After restart, agent-deck now samples the tmux pane for the picker text and auto-presses Enter to accept the default (Resume from summary). Opt out with[claude].auto_resume_summary = false. Implementation ininternal/session/claude_resume_picker.go, pinned byinternal/session/claude_resume_picker_test.go. Closes the oldest open P0 in the tracker (#67, filed v1.0.x era). -
P1 restart storm: single-flight restart on Claude exit (#1040, PR #1045, credit @torcuaine-sketch). Pre-fix, when a Claude process exited unexpectedly, multiple restart paths (watcher, status poller, UI-driven retry) could fire concurrently and each call into the spawn pipeline, producing a thundering herd of
claude --resumeinvocations that fought over the same tmux pane and CODEX/auth state. Fix adds a spawn guard (internal/session/instance_spawn_guard.go) that serialises restart-on-exit attempts per session: the first claim wins, concurrent claims are coalesced into a wait, and the guard releases only after the spawn settles (success, hard-fail, or timeout). Pinned byinternal/session/issue1040_restart_storm_test.gowith cases for concurrent restart claims, coalesced wakeups, and timeout release. Closes #1040 (credit @torcuaine-sketch for the repro + diagnosis). -
[opencode].env_file,[codex].env_file, and[copilot].env_filesilently ignored (PR #1044).getToolEnvFile()fell through toGetToolDef()for these builtins, which returned nil. Now explicitly handled ininternal/session/env.go.
Changed
- Uniform
[tool].commandand[tool].env_fileoverrides for all builtin agents (PR #1044). All nine builtin agents (claude, gemini, opencode, codex, copilot, hermes, qwen, crush, cursor) now support acommandfield in their config section to override the default binary/invocation (e.g.,[gemini] command = "gemini-nightly --flag") and anenv_filefield to inject env from a per-tool.envrc-style file. Previously only[claude].commandworked and env_file coverage was uneven. Verified end-to-end byscripts/verify-command-override.shand pinned byinternal/session/command_override_test.go.
[1.9.18] - 2026-05-19
Three merged community PRs on top of v1.9.17 — a community-contribution wave that lands one new builtin agent, one config knob, and one watcher data-integrity fix in a single morning. @Juoper contributed both additions and @AndreIntelas contributed the fix (and originally filed the issue). v1.9.18 is the thirteenth release cut under the Option A pipeline (#981 in v1.9.6); the local release worker stops at git push origin <tag> and .github/workflows/release.yml is the single source of truth for goreleaser release --clean. Headline addition: Cursor CLI (cursor agent) lands as the eighth first-class builtin agent alongside claude, codex, gemini, copilot, opencode, qwen, and crush, with session start/restart, optional --continue, TUI presets, settings panel, setup wizard, and tmux detection (#893, credit @Juoper). Second addition: the Codex home directory is now configurable per-profile in user config so users with non-default Codex installs (or multiple Codex profiles) can point agent-deck at the right CODEX_HOME without symlink hacks (#1043, credit @Juoper). Headline fix: firstLine() in the watcher no longer byte-slices through the middle of a multi-byte UTF-8 sequence, which had been poisoning watcher_events.subject rows with invalid bytes and wedging downstream consumers (SQLite UnicodeDecodeError, JSON encoder failures) — a single poisoned row stalled @AndreIntelas's Slack-to-conductor bridge until the underlying row was found and deleted (#1042, closes #1041, credit @AndreIntelas).
Added
-
Cursor CLI (
cursor agent) as the eighth first-class builtin agent (PR #893, credit @Juoper). Cursor's CLI joins claude, codex, gemini, copilot, opencode, qwen, and crush as a first-class agent kind, following the same scaffolding pattern established by the crush takeover (#1028 in v1.9.14): session start/restart builds thecursor agentcommand with optional--continuefor resume (internal/session/instance.go), tmux detection regex matches Cursor's prompt banner (internal/tmux/patterns.go,internal/tmux/tmux.go), TUI presets land Cursor in the New Session dialog (internal/ui/newdialog.go), the Settings panel exposes a Cursor row alongside the other agents (internal/ui/settings_panel.go), the Setup Wizard recognises Cursor as a detectable install (internal/ui/setup_wizard.go), and the Home + Edit Session dialogs render the display label (internal/ui/home.go,internal/ui/edit_session_dialog.go). Pinned bycmd/agent-deck/copilot_detect_test.go,internal/session/instance_test.go,internal/tmux/tmux_test.go,internal/ui/newdialog_test.go,internal/ui/settings_panel_test.go,internal/ui/setup_wizard_test.go, andinternal/ui/styles_test.go. Closes the community ask for Cursor support (PR #893, credit @Juoper — first contribution). -
Configurable Codex home directory in user config (PR #1043, credit @Juoper, 2nd contribution today). Users with non-default Codex installs or multiple Codex profiles previously had to symlink
~/.codexto the active install before launching a Codex session through agent-deck. This PR adds a[codex]block to user config with aconfig_dirkey, plus per-profile codex overrides, that agent-deck now respects when launching Codex: the wrapped command setsCODEX_HOMEto the resolved directory and explicitlyos.MkdirAlls the path so Codex starts cleanly even on a fresh override. Default behaviour is unchanged — no config block, no override, Codex uses its own default. Pinned byinternal/session/instance_test.go(CODEX_HOME injection + dir creation across the default + override + per-profile matrix) andinternal/session/userconfig_test.go(the config parser correctly threads[codex].config_dirand per-profile overrides through to the resolved value). (PR #1043, credit @Juoper.)
Fixed
firstLine()in watcher no longer poisonswatcher_events.subjectwith mid-codepoint UTF-8 cuts (#1041, PR #1042, credit @AndreIntelas). Pre-fix,firstLine()ininternal/watcher/webhook.gosliced its input by byte length to enforce the subject cap, which can cut inside a multi-byte UTF-8 sequence (cyrillic is 2 bytes/codepoint, em-dash is 3, emoji is 4) and write invalid UTF-8 bytes intowatcher_events.subject. The poisoned row then wedged downstream consumers: SQLite readers in @AndreIntelas's Slack-to-conductor bridge crashed onUnicodeDecodeErrorreading the same row on every poll, JSON encoders failed to serialise the row for the watcher API, and the bridge stayed stalled until the offending row was manually found and deleted. Fix slices by byte length as before, then trims back trailing bytes (at most 3 — UTF-8's max sequence length is 4) untilutf8.ValidStringreturns true on the result. ASCII inputs are unchanged. Pinned byinternal/watcher/webhook_test.gowith cases for cyrillic (2-byte), em-dash (3-byte), and emoji (4-byte) cuts at the cap, plus ASCII baseline, exact boundary fit, and a newline-before-cap regression case. Closes #1041 (PR #1042, credit @AndreIntelas).
Known issues
HOMEBREW_TAP_GITHUB_TOKENrepo secret was set ahead of v1.9.17 and the brew tap auto-publish was verified end-to-end against that release; v1.9.18 is the second cut riding that pipeline, so brew users on v1.9.17 canbrew upgrade agent-deckto v1.9.18 once CI completes.
[1.9.17] - 2026-05-18
Maintenance: verify HOMEBREW_TAP_GITHUB_TOKEN repo secret lets CI auto-publish brew formula. Brew users on v1.8.3 can now brew upgrade agent-deck to v1.9.17.
[1.9.16] - 2026-05-18
Five merged PRs on top of v1.9.15 — a community-takeover wave plus the first dedicated onboarding-docs landing in this minor. Three of the five PRs are takeovers of stalled community contributions that had sat behind merge conflicts after the v1.9.x bundle moved past them: @MauriceDHani's #885 (#1034), @oryaacov's #892 (#1035), and @JMBattista's #789 (#1036) — all carry original attribution and follow the @strofimovsky-#840 takeover pattern. The remaining two are docs-only: a five-minute conductor + watcher onboarding pair with architecture diagrams and a new README quickstart (#1037), and a path-selector UX RFC opened for discussion (#1033). v1.9.16 is the eleventh release cut under the Option A pipeline (#981 in v1.9.6); the local release worker stops at git push origin <tag> and .github/workflows/release.yml is the single source of truth for goreleaser release --clean. Headline addition: emacs ctrl+n / ctrl+p line-navigation aliases across every list view and dialog, closing Feedback Hub #600 point 3 from @balazser (#1034, credit @MauriceDHani). Headline CI change: the Lighthouse PR gate is re-enabled with a two-layer absolute + delta budget against the current bundle baseline (#1036, credit @JMBattista, leverages #1018).
Added
-
Emacs
ctrl+n/ctrl+pnavigation across every list view and dialog (Feedback Hub #600 point 3, PR #1034, credit @MauriceDHani). @balazser's Feedback Hub note asked for emacs-style line nav as aliases fordown/jandup/kso the muscle memory carried over from emacs/readline works inside agent-deck without re-learning. @MauriceDHani's original #885 added the key arms acrosshome.go,newdialog.go(form fields + recent-sessions picker + path-suggestions dropdown, withj/kalso added to the recent picker),skill_dialog.go, andwatcher_panel.go(list + detail mode), plus matching test coverage. The takeover preserves Maurice's diff verbatim except for one regression fix surfaced by Maurice's own test cases: thepreviewScrollOffset = 0reset was sitting inside the movement guard (if h.cursor > 0 { h.cursor--; h.previewScrollOffset = 0; ... }), so the clamp cases (ctrl+nat last item,ctrl+pat index 0) failed the nav-resets-preview contract that the rest of the navigation keys (pgup/pgdown/home/end/ctrl+b/ctrl+f) honour unconditionally. Fix hoists the reset above the guard so every key press in these arms resets the preview offset regardless of whether the cursor actually moved. Pinned by the updated tests inhome_test.go,newdialog_test.go,skill_dialog_test.go, andwatcher_panel_test.go. Closes Feedback Hub #600 point 3 (PR #1034, credit @MauriceDHani). -
+/-reorder sessions and groups in the tree (PR #1035, credit @oryaacov). Many terminals (Terminal.app, default iTerm2) silently drop modifier info on arrow keys, so the existingShift+↑/↓accelerators failed to fire for a meaningful slice of users; theK/Jalternates worked but weren't discoverable from the hint bar. @oryaacov's #892 made+and-the primary, terminal-portable, plain-ASCII default for reorder, withctrl+up/ctrl+downwired for terminals that do pass modifiers andK/Jpreserved untouched (the original PR's test plan called it out). The takeover re-applies Yaacov's diff against current main (which had separated the reorder help rows during the v1.9.x merge train) and adds the bottom-right hint bar row "+/- Move" next to "↑↓ Nav". Pinned byinternal/ui/reorder_keys_test.go, which asserts+moves the cursor item up and-moves it down. Takeover of #892 (PR #1035, credit @oryaacov).
CI
- Lighthouse PR gate re-enabled with two-layer absolute + delta budget (PR #1036, credit @JMBattista). The Lighthouse PR gate has been off since the v1.7.42 era because
agent-deck webalways started the TUI alongside the web server and the lhci collect step deadlocked against bubbletea's cancelreader. #1018 (merged v1.9.12) added the--no-tuiflag and the matching flag-parse test on main; @JMBattista's original #789 was approved on substance but stalled on CHANGELOG/workflow conflicts after eight subsequent PRs reshaped main. The takeover applies JM's CI-infra chunks (the production--no-tuiflag and its flag-parse test from #789 are dropped — #1018 already landed them, and re-applying would duplicate code and collide withcmd/agent-deck/issue_perf_no_tui_test.go). What lands:.github/workflows/lighthouse-ci.yml(new, two-layer gate — Layer 1 is absolute thresholds in.lighthouserc.jsonwith hard-fail ontotal-byte-weight/script:size/CLSand soft-warn onFCP/LCP/TBT/Speed Index; Layer 2 istests/lighthouse/compare-deltas.mjs, a delta gate failing any PR that growstotal-byte-weightorscript:sizeby more thanMAX_*_DELTA_PCT(default 5%) vs the base ref); thelighthouse-regression-acknowledgedlabel as a manual override turning the delta gate green while leaving the absolute gate hard, with idempotent label bootstrap that soft-fails on fork PRs whereGITHUB_TOKENis read-only;.lighthouserc.jsonre-baselined against the current bundle (total-byte-weight180 KB → 350 KB,script:size120 KB → 330 KB, LCP 1500 → 1300, Speed Index 1500 → 1100) and rewired toagent-deck web --no-tui --listen 127.0.0.1:19999 --token test;.github/workflows/weekly-regression.ymlswitched from theagent-deck-test-serverstop-gap toagent-deck web --no-tuidirectly (matching the PR-gate path);tests/lighthouse/{README,budget-check.sh,calibrate.sh}updated for the new invocation; and.github/workflows/README.mddocuments the active gate plus the override label. Verified locally thatagent-deck web --no-tuiemits theWeb server: http://...line matching the newstartServerReadyPattern,/healthzreturns 200, and the process stays alive (no cancelreader panic). Two conditional follow-up gates carry forward from JM's PR: a manual-override drill on a follow-up PR (synthetic >5% bundle bump → check fails → label applied → check green → label removed → check fails again) and a Sunday weekly-regression run confirming no false-positive regression issue is filed. Takeover of #789 (PR #1036, credit @JMBattista, leverages #1018).
Docs
-
Conductor + watcher onboarding guides, architecture diagrams, and a README quickstart (PR #1037). Feedback Hub support questions over recent releases repeatedly flagged the same gap — multiple users said the conductor and watcher concepts are hard to explain and hard to set up cold, and the existing reference material assumed too much. This PR frames agent-deck as "orchestrating a fleet of AI agents" and lands two five-minute guides plus visuals:
docs/CONDUCTOR-SETUP.mdwalks zero-to-conductor in five minutes — the @BotFather flow for a dedicated bot, the single-command interactive wizard, the channel topology (one conductor ↔ one channel, never shared), and the six gotchas users hit most (plugin auto-disable for the wrong profile,env_filevs.wrapperfor env injection, thechannelsfield needing to be set explicitly, profile-mismatch silent failures, Slack/Discord variants, and clean-slate teardown viaconductor teardown <name> --remove).docs/WATCHER-SETUP.mdcovers the doorbell pattern, the four built-in adapter types, routing viaclients.json, the external polling-script pattern with its four rules (dedupe locally, forward-lean payloads,--no-waiton the inbound POST, alert on silence), trigger-format conventions, and the common gotchas. Both guides ship with architecture diagrams underdocs/images/(fleet-topology.pngshowing the conductor + watchers + child sessions topology,watcher-doorbell.pngshowing the doorbell sequence-flow).README.mdgains a new "Quickstart: orchestrate a fleet of AI agents" section at the top with a three-step path linking into the new guides, and the Documentation section now opens with an Onboarding table so first-time visitors land on the 5-min guides before the reference material. Considered but not built: anagent-deck conductor initscaffold — the existingconductor setupwizard already does everything an init command would do, so adding it would mean two near-identical commands. Verified by walking the WATCHER-SETUP CLI end-to-end against v1.9.15 and checking that every flag, subcommand, and config block shown in CONDUCTOR-SETUP exists in the currentcmd/agent-deck/conductor_cmd.go+watcher_cmd.go. (PR #1037.) -
Path-selector UX rethink RFC opened for discussion (PR #1033, refs #1020, #983, #896, #885, #1021, Feedback Hub #600 from @balazser and @Showtimes).
docs/internal/path-selector-ux-rfc.mdnames the current path-field state machine (S1 soft-select / S2 edit / S3 popup-active / S4 popup-suppressed-edit) and presents two redesign proposals to collapse it. Three accumulated user reports (#1020 and Feedback Hub #600 from @balazser and @Showtimes) are mapped to specific confusion classes in the current truth table, then resolved differently by each proposal: Model A (Modal — explicit popup state, visually obvious that the popup is the active surface) and Model B (Drawer — popup never auto-shows, gated behindCtrl+Space). The RFC recommends Model A; it is a draft for design discussion and ships no code changes. (PR #1033.)
Known issues
HOMEBREW_TAP_GITHUB_TOKENrepo secret is not yet set, so the brew tap formula update step inrelease.ymlwill fail for v1.9.16 (known, user action — does not block the release tarballs or the GitHub release itself; same as v1.9.6 through v1.9.15).
[1.9.15] - 2026-05-18
Two merged PRs on top of v1.9.14 — one @smorin feature request that closed in 6 hours from issue → ship, and one silent data-loss fix that matches the same InsertSessionAndVerify pattern landed in #993. v1.9.15 is the tenth release cut under the Option A pipeline (#981 in v1.9.6); the local release worker stops at git push origin <tag> and .github/workflows/release.yml is the single source of truth for goreleaser release --clean. Headline addition: agent-deck fork --with-state (and the gitignored variant) now carries the parent worktree's staged / unstaged / untracked changes into the new worktree, so a fork mid-edit no longer forces a stash-and-restore dance (#1030, closes #1029, credit @smorin); launch additionally now returns the new session ID as JSON so callers can chain follow-up commands without re-querying state (#1032). Headline fix: concurrent launch calls previously raced on the SQLite session-insert path and silently dropped N-1 of N parallel launches — under the storm test, 4 of 5 concurrent launches were vanishing without an error to either caller, leaving phantom tmux panes with no DB row (#1032, closes #1031).
Added
-
agent-deck fork --with-statecarries parent WIP into the new worktree (#1029, PR #1030, credit @smorin). Pre-fix,forkcut a clean worktree from the parent branch HEAD, so any uncommitted state in the parent (staged hunks, unstaged edits, untracked new files) was left behind — the user had to either commit-then-fork, stash-and-restore on both sides, or manuallycpthe changed paths. @smorin's #1029 asked for the carry-over to be a first-class flag, and the implementation landed 6 hours later.--with-statecollects the parent's staged + unstaged + untracked set viamaterialize_wip(new internal/git helper) and replays it into the freshly created worktree before returning;--with-state-and-gitignoredextends the same flow to include gitignored files (build artifacts,.envfiles the user wants carried, IDE state). Default fork behaviour is unchanged — no flag, no carry-over. Pinned byinternal/git/issue1029_with_state_test.go(happy-path matrix across the three change categories) andinternal/git/issue1029_edge_test.go(binary files, symlinks, empty staged hunks, rename detection, gitignored-only variant). Closes #1029 (PR #1030, credit @smorin). -
agent-deck launchreturns the new session ID as JSON output (PR #1032). The launch path previously printed human-readable status lines and the session ID had to be re-discovered viasession listfiltering — racy when multiple launches were in flight.launchnow emits a JSON object with the resolved session ID (and the existing human-readable lines for tty callers), so scripted callers can pipeagent-deck launch ... | jq -r .session_idand chain follow-up commands deterministically. Shipped alongside the #1031 fix in the same PR because the race fix exposed the session ID as a first-class return anyway. (PR #1032.)
Fixed
- Concurrent
launchno longer silently drops N-1 of N parallel inserts (#1031, PR #1032). Pre-fix,launch's session-insert path used a plainINSERTwithout a verify-after-write step. Under concurrent launches (the storm test: 5 launches in flight), SQLite's busy-handler returned without surfacing an error to the caller for N-1 of N inserts — the tmux pane spawned, but the DB row never appeared, leaving phantom sessions with no managed state and no caller-visible signal of the loss. Fix lifts theInsertSessionAndVerifypattern from #993 (where the same race was fixed for the bridge-driven add path): insert, then immediately read-back inside the same connection to confirm the row exists, retry-with-backoff on miss, fail loudly with a clear error if the verify still misses after the retry budget. Pinned bycmd/agent-deck/issue1031_launch_race_test.go, which spawns 5 concurrent launches against a shared state DB and asserts that 5 distinct session IDs make it into both the JSON return values and the persisted state — pre-fix this test reproduced the loss every run; post-fix it's been green across 100 storm-test repetitions in the PR's CI matrix. Closes #1031 (PR #1032).
Known issues
HOMEBREW_TAP_GITHUB_TOKENrepo secret is not yet set, so the brew tap formula update step inrelease.ymlwill fail for v1.9.15 (known, user action — does not block the release tarballs or the GitHub release itself; same as v1.9.6 through v1.9.14).
[1.9.14] - 2026-05-17
Four merged PRs across two bugfixes and two additions. v1.9.14 is the ninth release cut under the Option A pipeline (#981 in v1.9.6); the local release worker stops at git push origin <tag> and .github/workflows/release.yml is the single source of truth for goreleaser release --clean. Headline fixes: custom-command sessions no longer false-error on CanRestart when claude_session_id is null, finally closing REQ-7 / #911 after the PR sat behind a 2-day CI queue stall (#989); and ScheduleWakeup upstream 5xx now go through a retry policy with structured observability instead of surfacing as immediate failures (#1026, closes #976). Headline additions: sessions now expose the resolved CLAUDE_CONFIG_DIR to spawned processes as an env hint, so child workers can find the same profile without re-resolving (#1027, closes #925); and Charm's crush lands as the seventh first-class builtin agent (#1028, closes #940).
Fixed
-
Custom-command sessions no longer false-error from null
claude_session_id(#911 / REQ-7, PR #989). Sessions launched via a custom command path never had aclaude_session_idwritten to state (Claude Code only emits one for the standard launch flow), so theCanRestartpredicate's null check fired and the UI showed a spurious "cannot restart — missing session id" error on what was otherwise a perfectly restartable session. Fix treats a nullclaude_session_idas "not yet emitted" rather than "broken state" for the CanRestart path: custom-command sessions are restartable as long as the tmux/process anchor is intact, and the standard-flow null check still gates the resume-by-id code path where the id is genuinely required. Pinned byinternal/session/issue911_custom_command_test.go. The PR itself was ready for two days but sat behind a stalled CI queue; anadeck-unstick-989worker cleared the queue and the merge went through. Closes #911 / REQ-7 (PR #989). -
ScheduleWakeupretry policy + structured observability for upstream 5xx (#976, PR #1026). Pre-fix, a transient 5xx from the upstream wakeup endpoint surfaced as an immediate failure to the caller and a single line in the logs — no retry, no context for triage. Fix adds a bounded exponential-backoff retry on retryable upstream errors (5xx + connection reset) and emits structured fields (attempt,status,next_delay_ms,final) at each step so the upstream-error rate is queryable from logs instead of inferred from caller-side noise. Permanent failures (4xx, auth, malformed) skip the retry and fail fast as before. Closes #976 (PR #1026).
Added
-
Sessions expose resolved
CLAUDE_CONFIG_DIRas an env hint (#925, PR #1027). When a session resolves its effectiveCLAUDE_CONFIG_DIR(from explicit config, profile fallback, or default), the resolved absolute path is now passed to spawned processes as an environment hint so child workers / hooks / MCPs find the same profile without re-running the resolution logic and risking divergence. Resolution rules and precedence are unchanged; only the visibility of the result is new. Pinned byinternal/session/issue925_resolved_account_env_test.go. Closes #925 (PR #1027). -
charmbracelet/crush as a first-class builtin agent (7th) (#940, PR #1028). Launch, attach, kill
crushsessions (Charm's terminal-first AI coding assistant from github.com/charmbracelet/crush). Icon 💘, color magenta. Config via[crush]section withcommand,env_file,yolo_mode. Per-session resume via--session <id>/--continueflows throughCrushOptions(ToolOptionsJSON). Detection wired across the CLI (agent-deck add -c crush .), tmux pane content patterns (charm crush,crush>), and the four UI surfaces (new-session dialog, setup wizard, settings panel, home preset). Adapter mirrors the existing copilot adapter — no shared infrastructure changes, no impact on other agents. Closes #940 (PR #1028).
Known issues
HOMEBREW_TAP_GITHUB_TOKENrepo secret is not yet set, so the brew tap formula update step inrelease.ymlwill fail for v1.9.14 (known, user action — does not block the release tarballs or the GitHub release itself; same as v1.9.6 through v1.9.13).
[1.9.13] - 2026-05-17
First release driven by findings from the Weekly Regression cron, which began producing actionable signal once the host-sensitive split landed in v1.9.12 (#1019). Two merged PRs, both closing the two halves of #1022 (the cron's first real report): a visual-regression fix that restores the <header> landmark in the web shell, and a perf fix that code-splits Chart.js off the initial-paint payload to clear the Lighthouse budget overshoot. v1.9.13 is the eighth release cut under the Option A pipeline (#981 in v1.9.6); the local release worker stops at git push origin <tag> and .github/workflows/release.yml is the single source of truth for goreleaser release --clean.
Fixed
- Web shell restores the
<header>semantic element (#1022 part 1, PR #1024). The Topbar component (introduced in the PR-B redesign port, b923e8bc / #860) rendered its root as<div class="topbar">instead of<header class="topbar">. Several Playwright visual tests in the Weekly Regression suite gate onpage.waitForSelector('header', ...)to detect when the Preact app has mounted; without a<header>landmark in the rendered DOM they timed out before the screenshot step, so the suite reported failures with no diff image to triage. Fix changesTopbar()'s root element back to<header class="topbar">; the CSS grid inapp.csstargets the.topbarclass (not the tag), so layout and styling are unaffected — the change is purely semantic. Pinned byinternal/web/issue1022_header_test.go, which assertsTopbar.jssource contains a<header>opening tag and no longer contains the old<div class="topbar">root. Closes #1022 part 1 (PR #1024).
Performance
chart.umd.min.jsis now code-split and lazy-loads on the Costs route (#1022 part 2, PR #1025). The Weekly Regression Lighthouse run reportedscript.size291 KB vs the 120 KB budget (2.4× over) andtotal-byte-weight399 KB vs 180 KB (2.2× over). Confirmed cause:index.htmleagerly loaded the 206 KBchart.umd.min.jseven though only the Costs route consumes Chart.js. Fix removes the eager<script src="/static/chart.umd.min.js" defer>fromindex.htmland dynamically injects the same asset fromCostDashboard.jsthe first time the Costs tab renders; the loader caches a single Promise so concurrent mounts share one fetch and there's no second-paint race. Initial-paint payload: ~313 KB → ~107 KB (107 KB app JS + 206 KB chart → app JS only; chart deferred to Costs route). Pinned byinternal/web/issue1022_codesplit_test.go, which asserts the servedindex.htmland the entry JS (main.js,App.js) do not referencechart.umd, locking in the savings. Closes #1022 part 2 (PR #1025).
Known issues
HOMEBREW_TAP_GITHUB_TOKENrepo secret is not yet set, so the brew tap formula update step inrelease.ymlwill fail for v1.9.13 (known, user action — does not block the release tarballs or the GitHub release itself; same as v1.9.6 through v1.9.12).
[1.9.12] - 2026-05-17
UX + perf + test infra sweep on top of v1.9.11 — four merged PRs covering two user-facing fixes, one perf-oriented addition, and one test-infra cleanup. v1.9.12 is the seventh release cut under the Option A pipeline (#981 in v1.9.6); the local release worker stops at git push origin <tag> and .github/workflows/release.yml is the single source of truth for goreleaser release --clean. Headline fixes: heartbeat NEED lines now auto-retire after 3 unanswered cycles instead of repeating verbatim for 12–21 hours and burying fresh urgent items (#1017, closes #971); and the New-session dialog's path-suggestions popup no longer swallows Up/Down arrows when focus has just Tab-landed on a pre-filled path, restoring the user's ability to navigate up/down out of the path section (#1021, closes #1020 — credit @JMBattista). Headline addition: the web subcommand gains a --no-tui flag that boots the HTTP server without bubbletea, saving 5 MB / 14% RSS at cold boot on Linux and addressing @arioliveira's "heavy on M4" complaint where bubbletea's macOS working set was traced at >60 MB (#1018). Test infra: four host-sensitive tests are now gated behind a hostsensitive build tag so the default pre-push and CI runs stay green; the Weekly Regression cron opts in with -tags hostsensitive and now runs cleanly (#1019, closes #969).
Fixed
-
Heartbeat NEED lines auto-retire after 3 cycles (#971, PR #1017). The conductor bridge's heartbeat loop emitted
NEED:lines verbatim every cycle, so an unanswered NEED would repeat unchanged for 12–21 hours — once the user tuned out, fresh urgent items got buried under the same repeating line. Fix inconductor/bridge.pyadds a pure helperfilter_need_lines(response, prev_counts, threshold=3)with three-state behaviour: cycles 1..threshold-1 forward the NEED line as-is, cyclethresholdemits a one-shotSTILL BLOCKED (3 cycles, no reply)escalation in place of the verbatim NEED, and cyclesthreshold+1..drop the line silently (auto-retire). Wired intoheartbeat_loopwith per-conductor in-memory state so each conductor's NEED stream is tracked independently; NEED lines that stop appearing get cleared from state, so a recurring problem is re-counted fresh next time it shows up. Pinned byconductor/tests/test_issue971_need_retire.py(9 cases: per-cycle behaviour, multi-line independence, configurable threshold, "fresh NEED still passes" non-regression). Closes #971 (PR #1017). -
Path-suggestions popup no longer swallows Up/Down when focus is Tab-landed on a pre-filled path (#1020, PR #1021, credit @JMBattista). v1.9.x's #983 closed @paskal's #896 sub-bugs 3+4 by auto-activating the path-suggestions popup on the first Up/Down whenever it was visible. That worked for the active-editing flow but @JMBattista reported the side effect: once focus lands on a Tab-pre-filled path, arrows always got swallowed by the popup and the cursor could never move up/down out of the path section. Discriminator was already in the code —
pathSoftSelectedis true the moment focus lands on a path field with a pre-filled value (pathInput blurred, user navigating between fields), and the soft-select handler clears it on the first real keystroke (the boundary between "Tab-landed" and "actively-editing"). Fix ininternal/ui/newdialog.gogates the auto-activate on!d.pathSoftSelected, restoring pre-#983 escape behaviour for #1020 while keeping the post-typing path that #896 sub-bugs 3+4 fix relies on. Explicit popup entry stays available via Space or Right per the existing soft-select handler. The #896 sub-bug 3+4 regression tests previously bypassed the soft-select handler via directpathInput.SetValue, leaving them in a synthetic soft-selected-with-value state that doesn't happen via real keystrokes — each test now mirrors the post-typing state (pathInput.Focus()+pathSoftSelected = false) so they cover the actual user flow they were always meant to describe. Pinned byinternal/ui/issue1020_path_selector_ux_test.go. Closes #1020 (PR #1021).
Added
agent-deck web --no-tuifor headless web mode (PR #1018, addresses @arioliveira "heavy on M4" complaint). Thewebsubcommand previously booted a full bubbletea TUI in the same process as the HTTP server, costing 5–30+ MB of RSS overhead depending on workload (theadeck-test-webuiworker traced ~60 MB steady-state to bubbletea + eager TUI initialization on macOS M4).--no-tuiruns HTTP-only: bubbletea is never constructed (skipstea.NewProgram,p.Run, maintenance worker, kitty-keyboard disable, CSIu reader wrap),main()blocks onserver.Start()in the foreground and returns on server shutdown, and the nested-session / outer-tmux / update-prompt guards are skipped (all TUI-specific and harmless to headless boot). Sessions remain manageable via the web UI;MemoryMenuDatafalls back toSessionDataService(storage-backed) when no in-memory snapshot is published. Default behaviour is unchanged — without--no-tui, the TUI boots as today. Benchmark (Linux, cold boot, empty profile):--no-tuipeak RSS 30.5 MB vs. with-TUI 35.4 MB, saved 5 MB (14%); the Linux number is the floor — savings grow with bubbletea's working set under load (rendered widgets, populated session lists, macOS terminal overhead, the >60 MB observed on M4). Pinned bycmd/agent-deck/issue_perf_no_tui_test.go(two arms:flag_extractionpure unit onextractNoTuiFlag;headless_server_startssubprocess that runs the binary with--no-tuiand a free listen port, asserts HTTP responds within 5s — only passable if bubbletea actually skipped, since bubbletea panics on stdin without a TTY and would kill the process before the server came up). (PR #1018.)
Test infra
- Host-sensitive tests split behind a
hostsensitivebuild tag (#969, PR #1019). Four tests with environment-dependent flakes are now gated behind thehostsensitivebuild tag, so the default pre-push and CI runs stay deterministic while the Weekly Regression cron opts in with-tags hostsensitiveand runs them on hosts known to satisfy their preconditions. Tests moved:TestWatcherEventDedup(internal/statedb/statedb_hostsensitive_test.go, races two goroutines on a shared SQLite handle —-race+ kernel scheduling trippedSQLITE_BUSYnon-deterministically);TestSession_SetAndGetEnvironment(internal/tmux/tmux_hostsensitive_test.go, depends on a live external tmux server and is sensitive to per-session env table being clean of prior state — flaky on hosts with lingering tmux servers);TmuxSurvivesLoginSessionRemovalplus its two test-only helpersstartFakeLoginScope/startAgentDeckTmuxInUserScope(internal/session/session_persistence_hostsensitive_test.go, requiressystemd-run --user, a running user systemd manager tracking MainPID for the spawned scope, and no lingering tmux racing teardown — not the case on most CI runners, inside nested tmux, or withoutloginctl enable-linger); andTestTmuxPTYBridgeResize(in-place build-tag promotion ininternal/web/terminal_bridge_integration_test.go, the inlineCI/GITHUB_ACTIONSenv skip didn't catch every headless environment without real PTY winsize propagation). Opt-in remains available viago test -tags hostsensitive -race ./...on machines that satisfy the preconditions. Closes #969 (PR #1019).
Known issues
HOMEBREW_TAP_GITHUB_TOKENrepo secret is not yet set, so the brew tap formula update step inrelease.ymlwill fail for v1.9.12 (known, user action — does not block the release tarballs or the GitHub release itself; same as v1.9.6 through v1.9.11).
[1.9.11] - 2026-05-16
Small follow-up sweep on top of v1.9.10 — four merged PRs covering one user-facing fix, one infrastructure addition, and two documentation entries. v1.9.11 is the sixth release cut under the Option A pipeline (#981 in v1.9.6); the local release worker stops at git push origin <tag> and .github/workflows/release.yml is the single source of truth for goreleaser release --clean. Headline fix: the NoTransitionNotify mute flag is now honored on inbox / deferred-queue replay, closing the third reported variant of #962 — completing the unification of all three variants behind a single eventDeliverable gate shared between emission and replay (#1014). Headline addition: agent-deck now recognizes the bare-repo-at-root worktree layout alongside the existing nested .bare/ convention, with strict self-discrimination so findNestedBareRepo no longer misidentifies internal git subdirs (hooks/, objects/, refs/) as nested bare repos (#1016, takeover of @keelerm84's #1011, closes #891 follow-up). Documentation: a 514-line docs/internal/state-db-schema.md now documents every table in the SQLite state.db that backs agent-deck profiles (#1015, closes #975); and the worker-prompt template now includes a "Step 0 — Prelude reads" convention so conductor-spawned workers stop hitting Claude Code's "must Read before Edit/Write" tool guard mid-cycle (#1013, closes #968).
Fixed
-
Transition notifier respects
NoTransitionNotifymute flag on inbox / deferred-queue replay (#962 v3, PR #1014). Reported by @seanyoungberg as a third variant of #962: the per-sessionNoTransitionNotifyflag was checked at new event emission (transition_daemon.go:210and:375) but pre-fix was never re-consulted during inbox / deferred-queue replay. Once an event sat in the queue, togglingagent-deck session set-transition-notify <child> offhad no effect on already-queued re-deliveries — the user-observed symptom was 4–5[EVENT]fires per single underlying transition into the conductor pane after the mute toggle. Architecturally this is the same shape as variants 1 (sessions removed between enqueue and drain, PR #992 in v1.9.8) and 2 (target-busy inbox entries never cleaned, PR #1009 in v1.9.10) — the replay path never re-validated against current session state. Fix generalizes the existingchildPresenceresolver into a singleeventDeliverablegate that returns(deliverable, reason)and centralizes the per-session predicate viainstanceAcceptsTransitionEventsshared between emission and replay; future per-session bypass conditions (paused, conductor-stopped, etc.) plug in here only. Pinned byTestTransitionNotifier_MuteFlagRespectedOnReplay_RegressionFor962V3ininternal/session/issue962_v3_mute_replay_test.go: enqueue 5 deferred events for a child → flipNoTransitionNotify=true→ drain with target available → zero dispatches, queue drained. Pre-fix dispatches 4; post-fix dispatches 0. Variant 1 and variant 2 regression tests still green. Closes #962 v3 (PR #1014). -
findNestedBareRepono longer misidentifies internal git subdirs as nested bare repos (PR #1016). It usedIsBareRepo(which callsgit rev-parse --is-bare-repository) on each child of a candidate dir; that subcommand walks up the directory tree via repo discovery and reportstruefor every descendant of a bare repo, includinghooks/,objects/,refs/, etc. Pre-fix, callingfindNestedBareRepoon a bare-at-root dir would return one of those subdirs as "the nested bare repo." The public APIs avoided this because they all checkIsGitRepofirst, butMergeBackdid invoke the helper on potentially-bare paths and only "worked" because git rev-parse rescued by walking up. NewisBareRepoSelfhelper combines--is-bare-repository == truewith--git-dir == .(or its resolved absolute equivalent) to confirm the candidate is itself the bare repo, not just a descendant.findNestedBareRepoand (as a post-takeover follow-up to the review note on #1011)IsBareRepoAtRootboth use it. Original fix by @keelerm84 in #1011; review-note follow-up applied during the takeover. -
MergeBackshort-circuits tomergeBackInBareRepowhen projectRoot is itself a bare repo (PR #1016). Pre-fix it calledfindNestedBareRepo(projectRoot)even when the path was already a bare dir, then patched up empty results with anIsBareRepocheck. It only produced correct behavior becausegit rev-parseinside the misidentified subdir would discover the parent bare repo. With the strict-self fix above, that accidental rescue went away —MergeBacknow short-circuits tomergeBackInBareRepo(projectRoot, …)as soon asIsBareRepo(projectRoot)is true. Original fix by @keelerm84 in #1011.
Added
-
Agent-deck now recognizes the bare-repo-at-root worktree layout (#891 follow-up, PR #1016, takeover of @keelerm84's #1011) alongside the existing nested
.bare/convention. The two are distinguished by basename:.bare⇒ nested (project root is the parent dir), anything else ⇒ at-root (the bare dir itself is the project root and linked worktrees live as direct children alongsideHEAD/objects//refs/). Concretely: a plaingit clone --bare repo.gitcheckout where the user adds worktrees inside the bare dir — e.g.~/code/proj.git/{main,feature-x}/with~/code/proj.git/being the bare repo — is now a first-class layout. Pre-fix, three flows misbehaved against this layout:GetMainWorktreePathreturned the parent of the bare dir (a generic dir holding unrelated projects),GetWorktreeBaseRooterrored out viagit rev-parse --show-toplevel(which has no meaning on a bare repo), andGenerateWorktreePathinsiblingmode would have placed new worktrees at<bare-dir>-<branch>(outside the bare dir entirely). NewIsBareRepoAtRootpredicate drives the branching inGetMainWorktreePath,GetWorktreeBaseRoot, andGenerateWorktreePath— the at-root layout auto-overridessibling/subdirectoryso new worktrees land at<bareRoot>/<branch>, matching whatagent-deck worktree listalready enumerates. Custompath_templateconfig still wins. README's "Bare repositories and worktrees" section now documents both layouts side-by-side. Pinned byinternal/git/bare_at_root_test.go. Original investigation and patch by @keelerm84 in #1011; re-applied after merge-train conflicts and extended with the strict-self discriminator (PR #1016). -
Worker-prompt "Step 0 — Prelude reads" convention (#968, PR #1013). Conductor-spawned workers were hitting Claude Code's "must Read before Edit/Write" tool guard mid-cycle because their
PROMPT.mdjumped straight into edits without first reading the files they were about to touch. Fix is template-level, not per-worker:skills/agent-deck/SKILL.mdgains a new "Worker Prompt Conventions" subsection in Sub-Agent Launch with a "Step 0 — Prelude reads" template skeleton that future prompt-authors copy, andskills/agent-deck/references/goal.mdinjects Step 0 into the autonomous worker contract so goal-driven workers get the rule for free across every cycle re-fire. Closes #968 (PR #1013).
Docs
state.dbschema reference (#975, PR #1015). Newdocs/internal/state-db-schema.md(514 lines) documents every table in the SQLite state.db that backs agent-deck profiles:metadata,instances,groups,instance_heartbeats,recent_sessions,cost_events,watchers,watcher_events. Per-column type, constraints, semantics, examples plus thetool_dataJSON blob shape, migration history, and Go type mappings. Verified against a.schemadump from a freshly-initialised profile. Closes #975 (PR #1015).
Known issues
HOMEBREW_TAP_GITHUB_TOKENrepo secret is not yet set, so the brew tap formula update step inrelease.ymlwill fail for v1.9.11 (known, user action — does not block the release tarballs or the GitHub release itself; same as v1.9.6 through v1.9.10).
[1.9.10] - 2026-05-16
Major sweep release on top of v1.9.9 — eight merged PRs covering five user-facing fixes, two infrastructure additions, and one repo-hygiene change. v1.9.10 is the fifth release cut under the Option A pipeline (#981 in v1.9.6); the local release worker stops at git push origin <tag> and .github/workflows/release.yml is the single source of truth for goreleaser release --clean. Headline fixes: chat history is now preserved across conductor restart so users no longer lose the active conversation when bouncing a conductor (#1010, closes #956); agent-deck launch detaches the tmux server from the SSH login cgroup so sessions survive SSH logout — the long-standing "sessions die when I disconnect" footgun (#1008, closes #958); the transition-notifier now cleans target-busy inbox entries on redelivery and enforces a TTL, closing a second-order variant of #962 that surfaced after v1.9.8's deferred-queue fix (#1009); the feedback dialog's "don't ask again" is now scoped per-release-series instead of forever, so a user who opted out of v1.9.x feedback still gets prompted on v1.10.x (#1004, closes #967); and worker worktrees always root at fresh origin/<default> instead of whatever the local main happens to point to, eliminating a class of "PR worker built on stale main" bugs (#1005, closes #973). Headline additions: agent-deck launch now enforces a concurrency cap to prevent swap thrash when a conductor fan-outs N child sessions in parallel on a memory-constrained host (#1003, closes #964); and the MCP child-reap mechanism shipped in v1.9.9 (#1000) is now wired into the production MCP spawn paths via process-tree discovery, so stopped sessions no longer leak orphan stdio MCP processes (#1006, completes the #1000 mechanism). Repo hygiene: .planning/ is now untracked, following the same per-developer-local-file pattern established for CLAUDE.md in v1.9.9's #1002 (#1007, closes #970).
Fixed
-
Chat history preserved across conductor restart (#956, PR #1010). When a conductor session was restarted via
agent-deck session restart(or via the daemon's auto-restart path), the conductor's chat scrollback was thrown away because the restart path only preserved the session's metadata row and re-launched the agent process — the on-disk Claude Code history file for that profile/session was not bound back to the new agent's stdin context. Users observed a fresh "How can I help?" prompt instead of the prior conversation. Fix ininternal/session/instance.go: the restart path now resolves and re-binds the prior history file before the new agent boots, so the agent re-enters with the same conversation context it had pre-restart. Pinned byinternal/session/issue956_chat_history_restart_test.go. Closes #956 (PR #1010). -
agent-deck launchdetaches tmux server from login cgroup to survive SSH logout (#958, PR #1008). When a user ranagent-deck launch <path>over SSH and then disconnected, systemd'sKillUserProcesses=yes(default on most modern distros) reaped the entire SSH login cgroup — including the freshly-spawned tmux server — even though tmux conventionally daemonizes. Sessions appeared to launch successfully then vanish minutes later. Fix consolidates launch-settings wire-up into a single helper (internal/session/issue958_launch_settings_wiring_test.go) that explicitly detaches the tmux server from the login cgroup at spawn time, sologinctl terminate-sessionno longer cascades into the agent-deck session tree. Closes #958 (PR #1008). -
Transition-notifier cleans target-busy inbox entries on redelivery + enforces TTL (#962 variant, PR #1009). v1.9.8's #992 closed the most common #962 spam class (replayed events for removed sessions), but a second-order variant remained: when a conductor was busy at delivery time, the notifier correctly deferred — but on redelivery the prior "target-busy" inbox entry was not cleaned, accumulating one stale entry per retry until the inbox file grew to thousands of duplicate lines. Fix in
internal/session/inbox.go: redelivery now removes the priortarget-busyentry for the same (child, event) tuple, and the inbox enforces a TTL ontarget-busy-class entries so a permanently-busy conductor doesn't accumulate them indefinitely. Pinned byinternal/session/issue962_target_busy_ttl_test.go. Closes #962 variant (PR #1009). -
Feedback "don't ask again" is now per-release-series instead of forever (#967, PR #1004). The feedback dialog's "don't ask me again" checkbox wrote an unbounded opt-out to
feedback-state.json— a user who clicked it once in v1.9.0 would never be prompted again, even after multiple minor-version upgrades. Fix ininternal/feedback/state.go: the opt-out is now scoped to the current MAJOR.MINOR series (e.g. opting out in v1.9.4 suppresses prompts for the rest of v1.9.x but resumes prompting at v1.10.0). The state file format gains amajor_minorfield; legacy entries (no field) are treated as opted-out only for the version they were recorded against, so existing users aren't suddenly re-prompted on the v1.9.10 upgrade itself. Pinned byinternal/feedback/feedback_test.goadditions. Closes #967 (PR #1004). -
Worker worktrees always root at fresh
origin/<default>(#973, PR #1005). The PR worker's worktree-creation path branched from whatever the localmainpointed at — which on a stale workstation could be hours or days behindorigin/main. Workers thus produced PRs against a stale base, and even after rebase the test harness ran against the worker's stale base for the first run. Fix ininternal/git/git.go: new-branch worktree creation now performs an explicitgit fetch origin <default>and roots the new branch atorigin/<default>rather than at the local symbolic ref. Pinned byinternal/git/issue973_worker_spawn_fresh_main_test.go. Closes #973 (PR #1005).
Added
-
agent-deck launchconcurrency cap to prevent swap thrash (#964, PR #1003). On memory-constrained hosts (≤16 GB), a conductor that fan-outs N child sessions via parallelagent-deck launchcalls could OOM the host — each Claude Code child process is ~1.5–2 GB resident, and N=8 trivially exceeds available RAM, pushing the host into swap thrash that takes the whole machine unresponsive. Fix incmd/agent-deck/launch_throttle.go: introduce a process-wide semaphore that caps in-flight launches; the cap is configurable viaAGENT_DECK_LAUNCH_MAX_CONCURRENCY(default derived from available memory) and waits politely rather than failing the launch. Pinned bycmd/agent-deck/issue964_launch_cap_test.go. Closes #964 (PR #1003). -
MCP child-reap wiring via process-tree discovery — completes the v1.9.9 #1000 mechanism (#1000 follow-up, PR #1006). v1.9.9 shipped the
mcp_child_reap.goprimitive that walks the MCP catalog and signals registered child PIDs, but the production session-stop path had no callers — the reap was unit-tested but never fired in real use. v1.9.10 closes the loop:internal/session/mcp_child_reap.gois now invoked from the session-stop path via process-tree discovery (so even MCP children not in the registry — e.g. spawned by a runaway plugin — are reaped if their parent was the stopping session). Pinned byinternal/session/issue965_wiring_test.go. Completes the #1000 mechanism (PR #1006).
Changed
.planning/is now untracked (#970, PR #1007)..planning/holds per-developer local planning notes that should never be committed — the same pattern v1.9.9's #1002 applied toCLAUDE.md. v1.9.10 extends the pattern:.planning/is removed from version control and added to.gitignore. Existing local copies are preserved; the directory is simply no longer tracked. Closes #970, completes the #1002 pattern (PR #1007).
Known issues
HOMEBREW_TAP_GITHUB_TOKENrepo secret is not yet set, so the brew tap formula update step inrelease.ymlwill fail for v1.9.10 (known, user action — does not block the release tarballs or the GitHub release itself; same as v1.9.6 through v1.9.9).
[1.9.9] - 2026-05-15
Patch release on top of v1.9.8 — five user-facing fixes spanning the launch path, MCP plugin lifecycle, session reaping, send-after-restart timing, and the web waiting-status renderer. v1.9.9 is the fourth release cut under the Option A pipeline (#981 in v1.9.6); the local release worker stops at git push origin <tag> and .github/workflows/release.yml is the single source of truth for goreleaser release --clean. Headline fixes: agent-deck launch no longer propagates TELEGRAM_STATE_DIR into child sessions, eliminating duplicate Telegram pollers that fired on every conductor-spawned child (#998, closes #955); MCP pool now refreshes .mcp.json plugin pins on session upgrade so stale pin entries don't survive a plugin version bump (#999, closes #960); the web event renderer maps waiting status to a waiting badge instead of misclassifying it as an error (#997, closes #963); session stop now reaps lingering MCP child processes so a stopped session no longer leaves orphan stdio servers attached to the daemon — mechanism shipped, full session-cmd wiring follow-up TBD (#1000, closes #965); and session send after a restart waits for slash-command registration before dispatching the first slash payload, fixing the race that silently dropped the message (#1001, closes #966). (Note: REQ-7 [#989] and Node 24 actions [#991] remain pending and are deferred to v1.9.10.)
Fixed
-
agent-deck launchstripsTELEGRAM_STATE_DIRfrom child env to prevent duplicate Telegram pollers (#955, PR #998). When a conductor withTELEGRAM_STATE_DIRexported in its process env spawned a child viaagent-deck launch <path>, the env var leaked into the child session's wrapper. The plugin-telegram MCP server treats a non-emptyTELEGRAM_STATE_DIRas a signal to attach the bot poller — so every conductor-spawned child silently started a secondbun telegramprocess polling the same bot token, producing duplicate inbound messages and double-reactions in the conductor. Fix ininternal/session/env.go: launch-time env construction now explicitly removesTELEGRAM_STATE_DIR(and any other channel-scoped state vars that must not propagate) before the child wrapper command is materialized. The conductor's own poller is unaffected because it runs under the conductor'senv_file-injected env, not the inherited shell env. Pinned bycmd/agent-deck/issue955_telegram_env_strip_test.go. Closes #955 (PR #998). -
MCP pool refreshes
.mcp.jsonplugin pins on session upgrade (#960, PR #999). The.mcp.jsonfile written by the MCP pool included plugin pin entries (enabledPlugins+ per-plugin version pins) that were materialized once at session creation and never refreshed. When a plugin's catalog version advanced (e.g.telegram@claude-plugins-officialpublished a new version), an existing session kept its old pin and continued running the stale plugin until the user manually edited.mcp.jsonor removed and re-created the session. Fix ininternal/session/pin_refresh.go: the MCP catalog write path now diffs the existing.mcp.jsonpins against the current catalog state and rewrites the pin block when a drift is detected, preserving any user-edited fields outside the pin block. Pinned byinternal/session/issue960_pin_refresh_test.go. Closes #960 (PR #999). -
Web event renderer treats
waitingstatus as waiting, not error (#963, PR #997). The SSE event payload ininternal/web/handlers_events.gomapped session statuses to badge classes via a switch that lacked awaitingcase — so a session in thewaitingstatus (the most common attention-needed signal, fired onAskUserQuestionandEnterPlanMode) fell through to thedefaultbranch which rendered as theerrorbadge class. Users reported sessions appearing red on the web dashboard despite no actual error. Fix: add explicitwaiting→status-waitingmapping. Pinned byinternal/web/issue963_waiting_status_test.go. Closes #963 (PR #997). -
Session stop reaps lingering MCP child processes (mechanism shipped) (#965, PR #1000). When a session was stopped, its attached MCP stdio servers (one process per attached MCP) were left running because the stop path only signaled the top-level Claude Code process and relied on the OS to GC the orphans — but stdio MCPs daemonized into their own session and survived parent death, accumulating on the host until the user manually killed them. Fix: new
internal/session/mcp_child_reap.gointroduces a reaping primitive that walks the MCP catalog for the stopping session and sends SIGTERM (then SIGKILL after grace) to each registered child PID. Mechanism is shipped and unit-tested; the session-cmd wiring that calls it from the stop path is a follow-up tracked separately so the reap can be exercised by the daemon before being wired into the user-facing command. Pinned byinternal/session/issue965_mcp_reap_test.go. Closes #965 (PR #1000, wiring follow-up TBD). -
session sendafter restart waits for slash-command registration before dispatching (#966, PR #1001). Immediately after a session restart, Claude Code re-registers its slash commands asynchronously — there's a ~500ms–2s window where the agent is "ready" (responds to the keystroke probe) but slash-command dispatch is not yet wired. Asession sendissued in that window with a slash-prefixed payload (/foo …) was delivered as literal text instead of being recognized as a command, producing a silent miss. Fix incmd/agent-deck/session_cmd.go:waitForAgentReadynow additionally polls for slash-command registration before returning when the payload begins with/, with a bounded extra wait. Non-slash payloads keep the prior ready-only semantics. Pinned bycmd/agent-deck/issue966_slash_after_restart_test.go. Closes #966 (PR #1001).
Known issues
HOMEBREW_TAP_GITHUB_TOKENrepo secret is not yet set, so the brew tap formula update step inrelease.ymlwill fail for v1.9.9 (known, user action — does not block the release tarballs or the GitHub release itself; same as v1.9.6, v1.9.7, and v1.9.8).
[1.9.8] - 2026-05-15
Patch release on top of v1.9.7 — a quality-of-life sweep that closes five user-facing CLI/notify papercuts plus one CLI ergonomics gap. v1.9.8 is the third release cut under the Option A pipeline (#981 in v1.9.6); the local release worker stops at git push origin <tag> and .github/workflows/release.yml is the single source of truth for goreleaser release --clean. Headline fixes: the transition-notifier's deferred retry queue no longer replays transition events for sessions removed via agent-deck rm — a class of bug responsible for all-day stale-event spam on conductor-innotrade (#992, closes #962); agent-deck rm is now parallel-safe at the CLI-subprocess layer (the structural fix landed in #909 in v1.9.1, but until #993 there was no regression coverage at the user-facing surface that pinned xargs -P N agent-deck rm-style invocation) and session remove <bogus> correctly exits non-zero (#993, closes #961); agent-deck launch <path> invoked from inside a conductor now derives the group from the cwd instead of inheriting the parent's conductor group (#994, closes #972); session send --timeout now actually extends the agent-ready wait instead of bailing at a hardcoded 80s (#995, closes #957); and 17+ alias forms across session, group, and launch are now accepted instead of silently rejected (#996, closes #974). (Note: REQ-7 [#989] and Node 24 actions [#991] remain pending CI/web-UI merge respectively and are deferred to v1.9.9.)
Fixed
-
session send --timeoutactually extends the agent-ready wait — was hardcoded to 80s (#957, PR #995). The 80swaitForAgentReadygate incmd/agent-deck/session_cmd.gowas hardcoded (maxAttempts := 400at a 200ms poll) and ignored the--timeoutflag that the caller already passed through. As a result,session send --timeout 5magainst a busy recipient silently failed at ~80s instead of waiting the requested 5 minutes — exactly the symptom in #957. The--timeoutflag still bounded the completion phase (when paired with--wait), but never the agent-ready phase that preceded it, so a slow-to-become-ready agent was unreachable above 80s no matter what value the user passed. Fix: introduce anagentReadyCheckerinterface (GetStatus+CapturePaneFresh) mirroring the existingstatusCheckerpattern used bywaitForCompletion;*tmux.Sessionsatisfies it naturally, and tests can now exercise the timeout loop without a real tmux session.waitForAgentReadynow accepts the caller's timeout; poll interval stays 200ms;maxAttemptsis derived from the requested duration. Zero/negative preserves the historical 80s default for safety. The--timeouthelp text is updated to reflect that it now bounds both the agent-ready phase and (with--wait) the completion phase. Pinned byTestSessionSend_RespectsTimeoutFlag_RegressionFor957incmd/agent-deck/issue957_send_timeout_test.go, which mocks an agent that never reaches "waiting" and asserts the function honors a 1s caller timeout (returns in ~1s, not the legacy 80s) and a 500ms timeout (returns in <2s); the test fails to compile on origin/main (new signature) and passes after the fix. Closes #957 (PR #995). -
agent-deck rmis now parallel-safe at the CLI-subprocess layer andsession remove <bogus>exits non-zero instead of silently lying (#961, PR #993). The structural fix for the parallel-rmload-modify-write race landed in v1.9.1 (#909) — but the regression coverage stopped at the storage layer (internal/session/rm_lifecycle_test.go), so a future refactor could silently re-open the race at the CLI surface without any failing test. The production patternxargs -P N agent-deck rmexercises a fundamentally different shape (distinct OS processes, distinct*sql.DBpools racing for the samestate.db) than the in-process storage test. #961 also reported a smaller-but-paired contract bug:session remove <bogus>silently printed a success line and exited 0 instead of returning aNOT_FOUNDerror. Fix incmd/agent-deck/issue961_rm_safety_test.go: (1)TestAgentDeckRm_ParallelSafe_RegressionFor961seeds 14 sessions, spawns 14 concurrentagent-deck rm <title> --jsonsubprocesses against a sharedHOME/state.db, and asserts every CLI exits 0 and an independentlist --jsonshows zero survivors — RED-proven against a reverted pre-#909handleRemove(≈13/14 survivors despite every CLI reporting success), GREEN against current main. (2)TestSessionRemove_NoOpExitsNonZero_RegressionFor961pins the not-found contract:session remove <bogus>andsession remove <bogus> --forcemust exit non-zero (code 2,NOT_FOUND); the no-op-success-exit-0 path is now blocked. The bugs reported in #961 themselves were already remediated on main; this PR closes the CLI-layer regression-coverage gap. Closes #961 (PR #993). -
Transition-notifier deferred queue no longer replays events for sessions removed via
agent-deck rm(#962, PR #992). The transition-notifier's deferred retry queue (runtime/transition-deferred-queue.json) kept replaying transition events for child sessions that had been removed viaagent-deck rm. The rm path (issue #910, v1.9.x) sweeps the inbox JSONL and the dedup ledger but never touched the queue file, so on every daemon pollDrainRetryQueueWithResolverredispatched stale child events to the conductor for hours — observed all day onconductor-innotradeas a recurring spam class. Fix: add a consumer-side registry-presence filter insideDrainRetryQueueWithResolver(internal/session/transition_notifier.go). Queued entries whosechild_session_idno longer exists in the profile registry are dropped (logged aschild_removed_from_registryinnotifier-missed.log) before the target-availability check fires the send. Defense-in-depth: even if the rm-sweep path misses a queue entry (race, parallel rm, stale file from upgrade), the dispatch loop refuses to fire events for vanished children. The check is wired via a nullable resolver field (n.childPresence) so existing struct-literal test helpers stay backwards compatible — only notifiers built viaNewTransitionNotifierget the live filter. Fail-open on storage errors so a transient DB outage doesn't introduce a silent-loss path strictly worse than the bug being fixed. Pinned byTestTransitionNotifier_SkipsRemovedSessions_RegressionFor962ininternal/session/issue962_no_replay_test.go(enqueue deferred event for a child →DeleteInstancethe child → drain; pre-fix the sender is invoked once, post-fix zero sends and queue cleared). Closes #962 (PR #992). -
agent-deck launch <path>from inside a conductor derives the group from the cwd, not the parent session (#972, PR #994). Bug:agent-deck launch <path>invoked from inside a conductor inherited the parent'sconductorgroup instead of landing in the project group derived from the cwd. Every conductor-spawned child required a follow-upagent-deck group moveto land in the right group — directly contradicting thefeedback_agent_deck_conductor_uses_agent_deck_group.mdmemory rule that each conductor's children must land in its project group, never inconductor. Fix incmd/agent-deck/launch_cmd.go: extendresolveGroupSelectionwith acwdDerivedparameter and a fixed priority order — explicit-g/--group> cwd-derived project group > parent-session group. The cwd-derived value is computed from the resolved project path via the new exportedsession.GroupPathForProjectwrapper around the existingextractGroupPathheuristic, solaunchandNewInstanceshare a single source of truth.handleAddkeeps its existing semantics (passes""for the cwd-derived slot) because its path resolution happens after group resolution. Pinned byTestLaunch_DerivesGroupFromCwdNotParent_RegressionFor972incmd/agent-deck/issue972_launch_group_test.go(4 sub-cases: cwd-derived group wins over parent; explicit-gstill wins over both; parent is fallback when no cwd-derived group; empty-empty returns empty for the caller's default). Closes #972 (PR #994).
Changed
- 17+ CLI verb/flag alias forms across
session,group, andlaunchnow resolve to the canonical handlers (#974, PR #996). Three CLI ergonomic rejections reported in #974 that broke muscle memory and made shell scripts brittle: (1)session update <id> --no-parent— verbupdatewas not in the dispatch switch incmd/agent-deck/session_cmd.go, so the entire subcommand returnedunknown verb. Fix: addupdatedispatch plusresolveSessionUpdateAliasthat maps--no-parenttounset-parent <id>and--parent <pid>(or--parent=<pid>) toset-parent <id> <pid>; bareupdatefalls through to the existingsethandler. (2)group remove <name>— onlydeleteandrmwere aliased ingroup_cmd.go. Fix: extractgroupVerbCanonicaland addremovealongside the existing aliases. (3)launch <path> ... -parent <pid>— the value was silently swallowed becausereorderArgsForFlagParsingincli_utils.gomatched flag tokens by literal string (-p,--parent) and never matched-parent. Go'sflagpackage itself treats-fooand--fooas the same flag, so the reorder pass was the only thing rejecting it. Fix: look flags up by name (dashes stripped) so every long form works with either single or double dash. Side effect: the previously missing--tmux-socketentry is now covered too. Pinned byTestCLI_VerbAliases_Accepted_RegressionFor974incmd/agent-deck/issue974_verb_aliases_test.go(7 sub-cases covering all three forms plus sanity rows for the existing canonical spellings). Closes #974 (PR #996).
Known issues
HOMEBREW_TAP_GITHUB_TOKENrepo secret is not yet set, so the brew tap formula update step inrelease.ymlwill fail for v1.9.8 (known, user action — does not block the release tarballs or the GitHub release itself; same as v1.9.6 and v1.9.7).
[1.9.7] - 2026-05-15
Patch release on top of v1.9.6 — two community-impacting UX features plus two infrastructure repairs that close long-standing CI flakes. v1.9.7 is the second release cut under the Option A pipeline (#981 in v1.9.6); the local release worker stops at git push origin <tag> and .github/workflows/release.yml is the single source of truth for goreleaser release --clean. Headline fixes: the Weekly Regression Check cron — broken since v1.9.0 and tracked as a "Known issue" in every release between — is finally repaired by switching to the TUI-less agent-deck-test-server binary so the cron no longer needs an interactive PTY (#986, closes #943); and the TotalLastWeek Monday-UTC test flake — v1.9.6's #977 fixed the production bug, but the older TestStore_TotalLastWeek_OnlyLastWeekEvent test was still wall-clock-bound and would still fail on a Monday-UTC tick in CI — is now deterministic via the SetClock injection seam shipped in #977 (#990). Headline features: session list within each group is now sorted by "most actionable" (waiting > attached > running > idle > stopped, ties broken by recency) so the top of each group is always where attention should go (#987, closes #857); and the New Group dialog now pre-fills the path field with the parent of the most-recently-created session in the current view, eliminating the most common Tab-and-paste step (#988, closes #918). (Note: REQ-7 [#989] and Node 24 actions [#991] remain UNSTABLE pending CI green and are deferred to v1.9.8.)
Fixed
-
Weekly Regression Checkcron job no longer fails on every run — switched to TUI-lessagent-deck-test-serverbinary (#943, PR #986). The Weekly Regression Check workflow has been failing since v1.9.0 (tracked as a "Known issue" in every v1.9.x release) because.github/workflows/weekly-regression.ymlinvoked the full TUI-boundagent-deckbinary inside a non-PTY GitHub Actions runner. Bubble Tea'stea.NewProgramrequires a TTY; without one the program initializer returnedErrInputTTYNotFoundand the workflow exited 1 before any of the regression scenarios ran. The TUI-less harness binarycmd/agent-deck-test-serveralready existed (added in v1.8.x for browser-harness E2E coverage) — it exposes the same session/group/state APIs over a localhost HTTP control plane without ever instantiating a Bubble Tea program — but the cron workflow was never updated to use it. Fix:.github/workflows/weekly-regression.ymlnowgo installs./cmd/agent-deck-test-server, exposesAGENT_DECK_TEST_SERVER_PORT, and points the regression scenarios at the HTTP control plane; the TUI binary is no longer invoked in cron paths. Verified by manually triggering the workflow against this branch and observing all 12 regression scenarios pass green for the first time since v1.9.0. Closes #943 — the entry can be removed from the v1.9.7 "Known issues" list (it was carried in v1.9.5 and v1.9.6 explicitly) (PR #986, closes #943). -
TestStore_TotalLastWeek_OnlyLastWeekEventis now deterministic viaSetClock— Monday-UTC flake closed permanently (PR #990). v1.9.6's #977 fixed the productionTotalLastWeekMonday-UTC bug by hoisting boundary computation into Go via the newStore.SetClock(func() time.Time)injection seam — but the olderTestStore_TotalLastWeek_OnlyLastWeekEvent(which long predates #932 and tested a different invariant: that only events within the last-week window are counted) still constructed event timestamps relative to the wall clock and computed its expected window the same way. On a Monday-UTC CI tick the old SQL bug was gone but the test still flipped from green to red because both sides of the assertion shifted independently. Fix: thread theSetClockseam throughinternal/costs/store_test.go'sTestStore_TotalLastWeek_OnlyLastWeekEventso it pins the clock to a deterministic mid-week instant; event-row construction and expected-window computation now both consume the pinned clock. The newTestStore_TotalLastWeek_HandlesMondayBoundaryfrom #977 already pinned the Monday boundary explicitly; this PR extends that discipline to the older test. The "Known issues" entry from v1.9.5 (internal/costs::TestStore_TotalLastWeek_OnlyLastWeekEventMonday-UTC flake) — already removed in v1.9.6 because v1.9.6 was cut on a non-Monday — is now closed structurally, not just calendrically (PR #990).
Added
-
Sessions within each group are now sorted by "most actionable" instead of insertion order (#857, PR #987). Within a group, session order had been creation-order — so the session most needing attention (waiting on user input, e.g. an
AskUserQuestionprompt or anEnterPlanModecheckpoint) could end up at the bottom of a long list, requiring vertical scrolling to find. #857 requested a stable status-driven ordering. Fix: newsortByActionableininternal/session/groups.gosorts each group's sessions by status priority —waiting(highest, attention-needed) >attached>running>idle>stopped— with ties broken by most-recent activity (updated_atDESC). The home view (internal/ui/home.go) calls the sort after every group rebuild, so newly-waitingsessions float to the top of their group automatically. Pinned byinternal/ui/issue857_sort_actionable_test.go(5 sub-cases covering each status pair plus the recency tiebreaker). The grouping dimension itself (which group a session belongs to) is unchanged — only intra-group order is affected. Closes #857 (PR #987). -
New Group dialog pre-fills the path field with the most-recently-used parent directory (#918, PR #988). Creating a new group always opened the path field empty, forcing the user to either type the path or Tab over to a terminal,
pwd, copy, paste back. For users whose groups all live under the same parent (e.g.~/Developer/,~/innotrade/), this was a recurring papercut. Fix:internal/ui/group_dialog.gonow pre-fills the path field with the parent directory of the most-recently-created session visible in the current view — falling back to$HOMEif no sessions exist yet. The user can still Tab into the field and edit / clear, so the default never gets in the way. Pinned byinternal/ui/issue918_default_path_test.go(3 sub-cases: empty-state fallback to$HOME, single-session parent extraction, multi-session most-recent-wins). Closes #918 (PR #988).
Known issues
HOMEBREW_TAP_GITHUB_TOKENrepo secret is not yet set, so the brew tap formula update step inrelease.ymlwill fail for v1.9.7 (known, user action — does not block the release tarballs or the GitHub release itself; same as v1.9.6).
[1.9.6] - 2026-05-15
Patch release on top of v1.9.5 — 7 community-credited bug fixes plus a structural change to the release pipeline (Option A: CI-only publish). v1.9.6 is the first release cut under the new pipeline (#981, closes #980): the local release worker now stops at git push origin <tag> and .github/workflows/release.yml is the single source of truth for goreleaser release --clean. This eliminates the double-publish race that fired on every release from v1.9.0–v1.9.5 (workflow runs returned 422 Validation Failed: already_exists × 5 because the local worker and the tag-push workflow both raced to upload the same five assets). Headline fixes: TotalLastWeek Monday-UTC SQL flake (#977, closes #932 — production bug, not a test bug); session-share export.sh silent exit 5 on real Claude Code sessions (#978, closes #895); worktree merge-back support in the bare-repo parent layout (#979, closes #891); brew-upgrade silent-success lie in agent-deck update (#982, closes #954); two residual sub-bugs from @paskal's #896 popup-keyboard report (#983); spawn-path unification with one-INFO-per-override audit seam plus sidecar cleanup on session-id clear (#984, closes #922 + #923); and the duplicate bun telegram poller race when enabledPlugins.telegram=true collides with conductor --channels plugin:telegram@... (#985, closes #941). Credits: @Victor Salvador Gasparini (#895), @Clindbergh (#891), @alexandergharibian (#954), @paskal (#896 residual), @bautrey (#922 + #923).
Fixed
-
TotalLastWeekreturns the correct week window even when the host clock ticks Monday 00:00 UTC (#932, PR #977). SQLite'sdate('now','weekday 1')is a no-op when "now" is already Monday — it returns today rather than advancing to next Monday — so on a Monday-UTC tickinternal/costs/store.go'sTotalLastWeekquery computed the window as[two-Mondays-ago, last-Monday)instead of the intended[last-Monday, this-Monday). This is a real production bug (the costs panel showed the week-before-last's totals every Monday morning UTC, off by one week), not just a test flake — but the surface symptom users saw first wasTestStore_TotalLastWeek_OnlyLastWeekEventfailing whenever the CI runner happened to wake across the Monday 00:00 UTC tick, which is what the v1.9.5 "Known issues" entry tracked. Fix: compute both boundaries in Go using an injectableStore.SetClock(func() time.Time)and pass them as bound parameters to the SQL query, so the SQL no longer touchesdate('now',...)for boundary math. NewTestStore_TotalLastWeek_HandlesMondayBoundarypins the clock to a Monday UTC instant and asserts the window is[last Mon, this Mon); the test fails deterministically against the pre-#977 SQL and passes with the fix (PR #977, closes #932). -
session-shareexport.shno longer silently exits 5 on real Claude Code sessions (#895, PR #978). Reporter @Victor Salvador Gasparini hit silent exit 5 on every real Claude Code session export. Two stacked bugs collapsed onto the same exit code. (1)skills/session-share/scripts/utils.sh'sencode_pathhelper only converted/to-, but Claude Code's project-path encoding (under~/.claude/projects/<encoded>/) also converts.to-. Any username or directory containing a.(common on macOS wherefirst.lastaccounts are the norm) causedfind_session_fileto return empty and the script bailed silently at line 81. (2)export.sh:130'sjqfilter assumed.message.contentwas always an array. Real Claude Code JSONL contains records where.message.contentis a plain string — slash-command output, local-command caveats, etc. — andjqerrored with exit 5 on the type mismatch;set -o pipefailpropagated the nonzero status, the assignment's failure trippedset -e, and theEXITtrap'srm -f "$TEMP_FILE"then ate the diagnostic, leaving only the bare exit 5 visible to the user. Fix:encode_pathbecomessed 's|[/.]|-|g'(single pass, both characters); thejqfilters in bothexport.shandsanitize_jsonlinutils.shtype-check.message.contentand only iterate when it's an array, falling through to a string-passthrough branch otherwise. New regression testskills/session-share/tests/test_export_895_regression.shreproduces the original exit 5 with a string-content user record and also pins the path-encoding behaviour; both cases fail at HEAD pre-fix and pass post-fix. Credit @Victor Salvador Gasparini for the repro and the JSONL sample (PR #978, closes #895). -
finishWorktreemerge-back now works in the bare-repo parent layout instead of exiting 128 (#891, PR #979). In the bare-repo layout introduced by #715 (<project>/.bare/+ linked worktrees),GetWorktreeBaseRootreturns the project root — which is the parent of.bare/, not itself a git working tree. The pre-#979finishWorktreeininternal/ui/home.goshelled out togit -C <projectRoot> checkout <target>andgit merge, both of which exit 128 withfatal: not a git repository(ormust be run in a work treeon the bare directory directly). The user saw "merge-back failed" with no actionable diagnostic, and the worktree was left orphaned — exactly what @Clindbergh reported in #891 against the #715 bare layout. Fix: newgit.MergeBack(projectRoot, source, target)helper ininternal/git/mergeback.godetects the layout and branches: regular layout takes the existingcheckout+mergepath; bare layout's fast-forward case usesgit update-refdirectly on the bare dir; the non-FF case spins up a throwaway worktree oftarget, performs the merge there, then prunes the temporary worktree.finishWorktreeininternal/ui/home.gonow delegates togit.MergeBackand the unusedos/execimport is dropped. Pinned byTestWorktree_MergeBack_BareRepo_RegressionFor891ininternal/git/mergeback_bare_repo_test.go, which uses the existing hermeticcreateBareRepoLayoutfixture and assertsmainadvances to the feature SHA after merge-back. Credit @Clindbergh for the #891 + #715 reproduction (PR #979, closes #891). -
agent-deck updatenow detects brew refusing to upgrade and fails loudly instead of lying (#954, PR #982). Reporter @alexandergharibian filed #954 with this exact sequence:agent-deck updateprintedAlready up-to-date. Warning: agent-deck 1.8.3 already installedimmediately followed by✓ Updated to v1.9.4— the second line was a lie. Root cause:brew upgradeexits 0 even when it refuses to upgrade (e.g. when the tap formula is stale, when the cask metadata cache is out of date, or when the installed version literal is already at the requested target despite a higher latest existing). The pre-#982handleUpdateincmd/agent-deck/update_cmd.goonly checked brew's exit status, saw 0, and printed the success line unconditionally. Fix: capture brew's combined stdout+stderr through a smallbrewRunnerinterface, scan the output for thealready installedrefusal marker (and the cask-side equivalent), and surface a clear actionable error pointing at tap-staleness as the root cause. ThebrewRunnerindirection also makes the path mockable — newTestUpdate_DetectsNoVersionBump_FailsLoudly_RegressionFor954replays @alexandergharibian's exact output verbatim and asserts the CLI errors instead of printing the success line; a companionTestUpdate_AcceptsRealUpgrade_NoFalseFailureguards the happy path against false positives from over-aggressive marker matching. Credit @alexandergharibian for the report with full repro output (PR #982, closes #954). -
New-session dialog popup: arrow keys auto-activate the suggestion list and Enter selects the highlighted item (#896, PR #983). @paskal's original #896 enumerated four sub-bugs in the path-suggestions popup. PR #908 (in v1.9.2) closed sub-bugs 1 (Tab on an invalid path) and 4 (Ctrl+W on a path). #983 closes the remaining two: sub-bug 3 — when the popup was visible after typing a prefix, Enter submitted the form with the literally-typed value instead of the highlighted suggestion, because
home.go's Enter handler only intercepted whenIsSuggestionsActive()was true and plain arrow keys on a freshly-rendered popup leftsuggestionsActive=false. Sub-bug 2 — Up/Down on a visible popup advanced dialog focus between fields instead of navigating the popup, forcing the user to fall back toCtrl+N+Spaceto "wake" the popup before they could arrow through it. Shared root cause: the popup had two distinct states — visible (just rendered) and active (consuming nav keys) — gated by separate flags, and onlySpace/Ctrl+Never flipped the active flag. From the user's perspective the popup looked interactive when it wasn't. Fix ininternal/ui/newdialog.go: when the popup is visible on the path field and the user presses the first Down or Up arrow, auto-activate it. The existingsuggestionsActivearrow handler then advances the cursor, andhome.go's existing Enter handler picks the highlighted suggestion correctly. Pinned byTestNewDialog_PopupEnter_SelectsHighlightedSuggestion_RegressionFor896andTestNewDialog_PopupArrows_NavigateReliably_RegressionFor896ininternal/ui/issue896_residual_test.go. Credit @paskal for the original four-bug enumeration that drove both PRs (PR #983, closes #896 residual). -
Spawn paths emit one INFO event per worker-scratch
CLAUDE_CONFIG_DIRoverride, and clearing a session's Claude session id deletes the hook sidecar (#922, #923, PR #984). #922 (spawn unification audit seam): three separate spawn-env builders ininternal/session/instance.go—buildClaudeCommandWithMessage,buildBashExportPrefix,buildClaudeResumeCommand— each silently swapped the resolvedCLAUDE_CONFIG_DIRforWorkerScratchConfigDirwhen the latter was non-empty. Users whose per-group[groups.X.claude].config_dirresolution was overridden by the worker-scratch mechanism had nothing to grep for: the swap was invisible in logs, and the audit reporter could not tell from log output whether a misroute had occurred (a real concern after v1.9.4's #950 hoistedprepareWorkerScratchConfigDirForSpawnto the top ofRestart()). The prep-call unification half of @bautrey's original investigation was already addressed by #950 — the wedge that remained on main was the silent swap. Fix: every swap now routes through a single seam (applyWorkerScratchOverrideininternal/session/worker_scratch.go) that emits one INFO event per swap, carryinginstance_id, the resolved (overridden)config_dir, and the worker-scratch dir; the three call sites share one log shape, so misroutes are debuggable instead of silent. Pinned byTestBuildClaudeCommand_WorkerScratchOverrideEmitsInfoLog,TestBuildClaudeResume_WorkerScratchOverrideEmitsInfoLog, andTestBuildClaudeCommand_NoOverrideNoLogininternal/session/issue922_spawn_unify_test.go. #923 (sidecar lifecycle): clearing a session's Claude session id (viasession set claude-session-id ""or by editing the field empty in the Edit Session dialog) left the hook sidecar file on disk; subsequent operations could find the stale sidecar and treat the session as still hook-bound. Fix ininternal/session/mutators.go: clearing the field now also unlinks the sidecar path; setting it to a non-empty value preserves the sidecar. Pinned byTestSetField_ClearClaudeSessionID_DeletesHookSidecarandTestSetField_NonEmptyClaudeSessionID_KeepsSidecarininternal/session/issue923_sid_clear_test.go. Credit @bautrey for both reports and the original spawn-unification investigation (PR #984, closes #922 + #923). -
Conductor sessions with
--channels plugin:telegram@...no longer spawn duplicatebun telegrampollers whenenabledPlugins.telegram=trueis set globally (#941, PR #985). When a conductor session was launched with--channels plugin:telegram@claude-plugins-officialand the ambient profile'ssettings.jsonalready hadenabledPlugins."telegram@claude-plugins-official"=true, the Claude CLI loaded the plugin twice — once from the globalenabledPluginsflag and once from the--channelsflag. Each load spawned its ownbun telegram startpoller against the conductor'sTELEGRAM_STATE_DIR, and the two pollers raced for the same bot token. Telegram's Bot API returns409 Conflicton competinggetUpdatescalls and starts dropping incoming messages — exactly the symptom reported in #941 (intermittent message loss on the agent-deck and innotrade conductors). TheTelegramValidatoralready surfaced this asGLOBAL_ANTIPATTERN+DOUBLE_LOADwarnings, and v1.7.22 plus the v3 topology memory rule (telegram_channel_conductor_only.md) documented the contract — conductors use--channelsexplicitly;enabledPlugins.telegrammust be false globally — but the contract was enforced only by docs, so operators who left the global flag on silently got two pollers. #985 lifts the rule into the spawn path: new predicateneedsScratchForGlobalChannelConflictininternal/session/worker_scratch.gofires when a Claude session has aplugin:telegram@...channel and the resolved source profile'ssettings.jsonhas the global flag set.NeedsWorkerScratchConfigDirnow ORs the new predicate, so channel-owning conductors with the antipattern automatically get a per-session worker-scratch config dir withenabledPlugins.telegram=false— the global load is suppressed inside that scratch dir while the--channelsload still runs, so exactly onebun telegrampoller runs per conductor. Pinned byTestTelegram_GlobalScope_OneBunPollerOnly_RegressionFor941ininternal/session/issue941_global_antipattern_test.go(PR #985, closes #941).
Changed
- Release pipeline restructured: CI is now the single source of truth for publishing (Option A) (#980, PR #981). Every release from v1.9.0 through v1.9.5 generated five
422 Validation Failed: already_existserrors in the CI workflow logs (e.g. workflow run 25884533972 for v1.9.5) because the local release worker rangoreleaser release --cleanand the tag push triggered.github/workflows/release.ymlto do the same thing — both raced to upload the same five assets (4 platform tarballs +checksums.txt) to the same GitHub release. Whichever lost the race got back 422 on every asset, exited 1, and the release-watcher fired on every release. The fix splits responsibility cleanly: the local release worker now stops atgit push origin <tag>and never invokesgoreleaser release(see the "Release worker template" section ofFLEET-PIPELINE-PLAN.md), and.github/workflows/release.ymlgets aconcurrency.group: release-${{ github.ref }}block as belt-and-suspenders against accidental re-runs for the same tag. To catch.goreleaser.ymldrift at PR review time instead of at tag-push time (when recovery requires deleting a partial GitHub release), this PR also adds.github/workflows/release-snapshot.yml— a path-filtered workflow that runsgoreleaser release --snapshot --skip=publishon every PR touching.goreleaser.yml,cmd/agent-deck/**, or the release workflows themselves. Verified locally: snapshot on clean main exits 0 with 4 platform tarballs +checksums.txt; snapshot with a deliberately brokenmain.gopath exits 1 with the goreleaser build error surfaced inline. v1.9.6 is the first release cut under this pipeline. (PR #981, closes #980).
Known issues
Weekly Regression Checkcron job still failing (issue #943, unchanged from prior releases) — does not block the release pipeline; a separate worker is fixing.HOMEBREW_TAP_GITHUB_TOKENrepo secret is not yet set, so the brew tap formula update step inrelease.ymlwill fail for v1.9.6 (known, user action — does not block the release tarballs or the GitHub release itself).
[1.9.5] - 2026-05-14
Patch release on top of v1.9.4 — two community PRs plus an inline fix closing a silent drift bug. Headline is the real fix for keycap-class emoji rendering (#952, closes #937 v2): the v1.9.3 fix in #948 corrected <base>+U+FE0F pairs via uniseg but missed the keycap class <base>+U+FE0F+U+20E3 (e.g. #️⃣ 0️⃣–9️⃣ *️⃣), which uniseg classifies as 1 cell while every modern terminal paints at 2 — re-introducing per-frame row-offset drift @jennings reported against v1.9.3 (68dba73d). Second fix prevents an empty Claude command from no-op'ing a session restart and leaving the pane dead (#855). Third fix bumps the in-source Version constant in cmd/agent-deck/main.go from 1.8.3 to 1.9.5, closing a 5-release silent drift that the validate-tag CI check kept false-alarming on (investigated by worker #947 against v1.9.4). Thanks to @maxfi and @jennings for the dual repros on #952.
Fixed
-
Keycap emoji sequences (
#️⃣ 0️⃣–9️⃣ *️⃣) no longer cause per-frame row-offset drift in the TUI — reopen of #937 against v1.9.3 (#937 v2, PR #952). PR #948 (v1.9.3) closed #937 by routinginternal/ui/home.go's width and truncation gates throughgithub.com/charmbracelet/x/ansi(uniseg-backed) on the theory that uniseg correctly classifies<codepoint>+U+FE0Fas 2 cells; that holds for @maxfi's four reported emoji (🏷️ 🛠️ ⚙️ 🗂️) but does not hold for keycap sequences such as#️⃣(U+0023 + U+FE0F + U+20E3) — uniseg reports 1 cell while every terminal we tested (Ghostty, Terminal.app, iTerm2, Warp, Termius) renders 2. @jennings reported continued drift against v1.9.3 (commit68dba73d) with exactly that emoji class plus🔁, appearing in pane content, not just session titles. Fix: newcellWidth/cellTruncatehelpers ininternal/ui/cellwidth.gowalk extended grapheme clusters viarivo/unisegand promote any cluster containing U+20E3 (COMBINING ENCLOSING KEYCAP) to width 2; the existing 9 callsites #948 swapped plus 6 more pane-content / final-viewport-clamp callsites it missed (clampViewToViewport,renderSessionItempane-title append,renderPreviewPaneper-line + post-build width enforcement, notes-editor line truncation) now route through these helpers so the cell-count gates and the terminal agree on keycap glyphs. Pinned byinternal/ui/issue937_keycap_test.go(5 sub-cases across width parity, truncation budget, andtruncatePathintegration). Residual:ensureExactWidth/lipgloss.JoinHorizontalmeasure withlipgloss.Width, which shares the upstream uniseg disagreement —clampViewToViewportnow acts as the final cell-correct backstop, so a worst-case keycap-at-right-edge clips visually rather than wrapping; full structural fix requires an upstream change ingithub.com/charmbracelet/x/ansiand/orgithub.com/rivo/unisegand is being filed separately. Credit @maxfi and @jennings for the dual repros (PR #952, closes #937). -
Empty Claude command no longer no-ops a session restart, leaving the pane dead (PR #855). When a session's configured Claude command was empty (string-empty after trim, e.g. from a partially-edited Edit Session dialog or a legacy
state.dbrow written before the launcher gate landed),buildClaudeCommand()ininternal/session/instance.goreturned an empty argv slice; the restart path then forwarded that empty argv totmux respawn-pane, which interprets no-arg respawn as "use the shell" — so the user's pane was rebound to a bare interactive shell with no Claude process, no MCP servers, no channel wiring. From the TUI it looked like a successful restart (status flipped toattached) but the pane was effectively dead: no agent, no/qhandler, no hook fast-path, just a shell prompt. Fix:buildClaudeCommand()now treats an empty base command as "still emit the Claude binary" and falls through to default-arg construction, so the restart respawns Claude with the same default invocation a fresh session would get rather than degrading to a shell. Pinned byinternal/session/empty_command_claude_restart_test.go(TestBuildClaudeCommand_EmptyBaseCommand_StillEmitsClaudeBinary) plusshell_restart_test.goandsessions_disappear_on_restart_test.gocoverage ininternal/session/internal/ui(PR #855). -
In-source
Versionconstant incmd/agent-deck/main.gono longer drifts from the released tag (no external issue — worker #947 investigation).cmd/agent-deck/main.go:39declaresvar Version = "1.8.3"with the comment// overridden at build time via -ldflags "-X main.Version=..."; goreleaser sets the ldflag on every release build, so released binaries always reported the correct version and the drift was invisible in production. The literal had been stuck at1.8.3since the v1.9.0 cut — five consecutive releases (v1.9.0, v1.9.1, v1.9.2, v1.9.3, v1.9.4) shipped with a stale literal that only surfaced when something read it without the ldflag, e.g.go build ./cmd/agent-deckfrom a developer checkout,go runinvocations, or thevalidate-tagCI check on a non-goreleaser build path — the latter false-alarmed on every release tag from v1.9.0 onward and worker #947 traced it to the literal in main.go rather than the tag mechanics. Fix: bump the literal to1.9.5in lockstep with the tag; the ldflag override remains so goreleaser builds are unaffected. Validated bygo build -o /tmp/agent-deck-no-ldflag ./cmd/agent-deck && /tmp/agent-deck-no-ldflag --versionreportingAgent Deck v1.9.5(would have reportedv1.8.3pre-bump). This closes thevalidate-tagfalse-alarm class permanently — future releases that forget the literal bump will be caught by the same check.
Known issues
internal/costs::TestStore_TotalLastWeek_OnlyLastWeekEventstill fails when the local clock is on a Monday in UTC (issue #932, unchanged from v1.9.0–v1.9.4). v1.9.5 was cut on a Thursday — full suite green.Weekly Regression Checkcron job still failing (issue #943, unchanged from prior releases) — does not block the release pipeline.
[1.9.4] - 2026-05-14
Emergency P0 hotfix on top of v1.9.3 — single PR (#950) restoring macOS OAuth onboarding for users on the default Claude profile. v1.9.2's #779 (per-session Claude Code plugin enablement) inadvertently broke a long-standing invariant: worker-scratch CLAUDE_CONFIG_DIR injection was firing for every session, not just those with an explicit config_dir. On macOS this caused the Claude CLI to look up OAuth credentials in a non-keychain path and fail onboarding entirely. @paskal bisected the regression to #779 and shipped the fix within 2 hours of report — thank you.
Fixed
- macOS OAuth onboarding no longer breaks for sessions without an explicit
config_dir(#949, PR #950). v1.9.2's #779 expandedinternal/session/instance.go's env-construction path so that the worker-scratchCLAUDE_CONFIG_DIRoverride was set unconditionally whenever the session had a worker-scratch directory — which is every managed session. Pre-#779, the override fired only when the user had explicitly configured a per-sessionconfig_dir(e.g. for multi-profile setups). On Linux this was mostly harmless; on macOS it diverted the Claude CLI away from the default keychain-backed OAuth credential store, so first-run onboarding silently failed with a generic "auth required" loop and existing OAuth tokens stopped being found. Fix: re-gate the worker-scratchCLAUDE_CONFIG_DIRinjection on a non-emptyconfig_dirfield, restoring the v1.7.68/v1.9.1 invariant. Pinned byinternal/session/issue949_scratch_injection_gate_test.go(PR #950, closes #949). Credit to @paskal for bisect-and-fix within 2 hours of the original report.
Known issues
internal/costs::TestStore_TotalLastWeek_OnlyLastWeekEventstill fails when the local clock is on a Monday in UTC (issue #932, unchanged from v1.9.0–v1.9.3). v1.9.4 was cut on a Thursday — full suite green.
[1.9.3] - 2026-05-13
Hotfix release on top of v1.9.2 — single PR (#948) addressing two TUI rendering regressions reported on the day v1.9.2 shipped. Both issues were visible on first attach: viewport content from the previously-attached session bled into newly-attached panes until a manual resize, and emoji glyphs followed by a Variation Selector-16 (U+FE0F) were drawn at single-cell width, causing overlapping text in the session list and status bar. Thanks to @Kevsosmooth, @maxfi, and @jennings for repro details and pane captures.
Fixed
-
Viewport content no longer bleeds across session-switch / resize until manual repaint (#936, PR #948). After v1.9.2's
home.gorefactor, attaching session B while session A's pane was still in the viewport left A's last-rendered content visible in B's frame until the user manually resized the terminal — the viewport's internal content cache wasn't being invalidated on the attach transition or on terminal resize, so Bubble Tea's diff renderer computed an empty diff against the stale cache and drew nothing for B. Fix: explicitviewport.SetContent("")+ cache-invalidation flag on every attach-state transition (attaching→attachedandattached→detached) and on everytea.WindowSizeMsg, before the next paint cycle. Coverage ininternal/ui/issue936_attach_resize_test.go(PR #948, closes #936). -
Emoji + Variation Selector-16 (U+FE0F) sequences now render at correct two-cell width instead of overlapping (#937, PR #948). Sessions whose names contained emoji presentation sequences (e.g.
⚙️= U+2699 + U+FE0F,❤️= U+2764 + U+FE0F) were measured at one cell because the cell-width function summedrunewidth.RuneWidthper rune — VS16 reports width 0, and the base symbol is a width-1 "text-default" codepoint, so the combined glyph rendered as 1 cell while the terminal actually drew it at 2 cells, shifting every column to the right of it by one and producing overlap in the session list, status bar, and dialog labels. Fix: cell-width walker now detects the<base, U+FE0F>pair and returns 2 for the sequence, matching Unicode UAX #11 emoji presentation semantics and matching what every modern terminal (iTerm2, kitty, WezTerm, GNOME Terminal, Windows Terminal) actually paints. Coverage ininternal/ui/issue937_emoji_vs16_test.go(PR #948, closes #937).
Known issues
internal/costs::TestStore_TotalLastWeek_OnlyLastWeekEventstill fails when the local clock is on a Monday in UTC (issue #932, unchanged from v1.9.0/v1.9.1/v1.9.2). v1.9.3 was cut on a Wednesday — full suite green.
[1.9.2] - 2026-05-13
Patch release on top of v1.9.1 — 17 community PRs merged over the two days since v1.9.1. Headline fix is the two-TUI-against-one-profile crash (#944, closes #927): with allow_multiple=true (the default), running agent-deck simultaneously on PC + phone-over-SSH caused every managed session to oscillate to StatusError within ~20s because each TUI's reconnect sweep killed the other's control pipes. Other bundles: conductor bridge.py robustness for non-UTF-8 output and --wait-vs-session output --json reply parsing (#926, closes #920/#921); cross-profile data migration CLI (session move / conductor move / group move, #929, closes #928); first-class per-session Claude Code plugin enablement mirroring the --mcp / --channel surface (#779); seven smaller TUI/CLI/UX fixes; and the v1.9.0 [Unreleased] hierarchy-keys work (#848) finally cuts.
Fixed
-
Two simultaneous agent-deck TUIs against the same profile no longer oscillate sessions to StatusError (#927, PR #944). With
[instances] allow_multiple=true(the default), running TUI A on the desktop and TUI B via SSH from the phone causedkillStaleControlClientsininternal/tmux/pipemanager.go:443to SIGTERM every control-mode client whoseclient_pid != os.Getpid()— making no distinction between truly orphaned clients from a previously-crashed TUI (#595's motivation) and live sibling TUI's active control pipes. So A's reconnect sweep killed B's pipes, and vice versa, indefinitely; every managed session flipped to error within ~20s. Fixed by newisControlClientOrphan(pid int) boolhelper that reads PPID from/proc/<pid>/staton Linux (zero-fork) orps -p <pid> -o ppid=on macOS / as fallback; ifppid <= 1orkill(ppid, 0) == ESRCH→ orphan, sweep; otherwise reads the parent's exe (/proc/<ppid>/exereadlink, orps -p <ppid> -o comm=) and checks whether it matchesos.Executable()or has "agent-deck" in its basename. Match → live sibling, preserve. No match (init / systemd-user / launchd / random binary) → orphan, sweep. Any metadata-read error returns true (treat as orphan) so #595's cleanup semantics are preserved on hosts where both/procandpsfail (essentially never). Also relevant to #936 (Kevsosmooth's 9-orphan report) (PR #944). -
Conductor
bridge.pyno longer crashes on non-UTF-8 CLI output, and Telegram replies are clean instead of statusline garbage (#920, #921, PR #926). Three converging fixes ininternal/session/conductor_templates.go: (1)subprocess.run(..., errors="replace")survives non-UTF-8 bytes in CLI output — the bridge previously crashed withUnicodeDecodeErrorwhen CLI captures contained ANSI escape sequences with high-bit chars under Python 3.14.4 (earlier Pythons silently mangled bytes rather than crashing). Closes #920. (2)get_session_output()switches from-q(raw pane capture) to--jsonand parses thecontentfield — the bridge was forwarding the cosmetic frame + statusline to Telegram instead of the assistant reply. Closes #921 output path. (3)send_to_conductor()'s--waitbranch now fetches the reply via a separatesession output --jsoncall rather than parsingsession send --wait's stdout —--waitemits only a send-confirmation JSON then prints the response as raw pane capture, so the reply can't be extracted from that stdout cleanly; the two-call pattern resolves this. Closes #921 wait/send path. Validated against multiple Telegram round-trips on macOS 26.x ARM64 + Python 3.14.4 (PR #926). -
Attached session status refreshes immediately on exit instead of taking 2–3s to mark with
X(PR #854). When exiting a session with/q, agent-deck navigates back to the list but the just-closed session took 2–3 seconds to be marked withX. Three coordinated fixes: reconcile the just-attached session status synchronously when returning to the main UI; clear stale hook fast-path state for that session before live tmux polling; and refresh delayed attach-return pane caches as a secondary repaint (PR #854). -
ctrl+win path inputs deletes only the trailing segment, andTabno longer silently jumps off a non-existent path (#896, PR #908). Bubbles' defaultdeleteWordBackwardstops at whitespace; paths usually have none, so on/a/b/cctrl+w cleared everything instead of dropping the trailing segment. New path-aware backward-word delete also stops at/, wired into new-session path input (focusPath), worktree branch input (focusBranch— slashes are common in branch names), the multi-repo path editor, and the edit-paths dialog:/a/b/c→/a/b/,~/x/y→~/x/,feature/foo→feature/. Second fix: Tab on a non-empty path that doesn't resolve to an existing directory now keeps focus on the input (previously focus advanced to the agent selector and the typed value was left dangling). Empty paths and valid directories advance as before. Nine table cases inTestDeleteWordBackwardPathplus end-to-endTestNewDialog_CtrlW_*andTestNewDialog_Tab_*(PR #908). -
Lone ESC press no longer requires a second key to register inside TUI dialogs (PR #917).
csiuReaderwas buffering a lone0x1bbyte waiting for the next byte to decide if it was the start of an escape sequence; on a blocking stdin this meant ESC was never delivered until another key arrived, so the first Esc press appeared to do nothing in every dialog (also affectedyNetc. wherever a lone ESC was the canonical "cancel"). Added apollFnfield (backed byunix.Pollon POSIX, stub on Windows) that checks within 50ms whether more bytes follow: if none arrive, the ESC flushes immediately as a standalone keypress; if bytes do follow, they bundle with the ESC as before, preserving SS3/CSI sequence handling. Same shape as charmbracelet/ultraviolet's timer-based flush; the difference isunix.Polllets us stay zero-goroutine on the read path. Reported on macOS 14.x (PR #917). -
Watcher
[source]settings are now actually read fromwatcher.tomlinstead of silently ignored (PR #938). The watcher engine'sRegisterAdaptercall ininternal/ui/home.gopassed an emptySettingsmap, so adapterSetup()always saw no configuration — making the github adapter unusable (adapter_setup_failed: github adapter requires a webhook secretregardless of what was in~/.agent-deck/watcher/<name>/watcher.toml) and forcing webhook/ntfy/slack adapters to silently fall back to defaults. NewloadWatcherSourceSettings()helper reads the[source]table from eachwatcher.tomlbeforeRegisterAdapter, restoring the documented behaviour described inAdapterConfig.Settings's comment (adapter.go) and in thewatcher-creatorskill's TOML template. Errors (file missing, parse error, no[source]section) yield an empty map so adapters fall back to defaults — matching pre-patch behaviour when no config is present. Verified by the github-adapter reproduction in the PR (port 18461 was not listening pre-patch; binds on TUI start and accepts signed webhooks post-patch with HTTP 202) (PR #938).
Added
-
Per-session Claude Code plugin enablement, mirroring the
--mcp/--channelsurface (PR #779). First-class management of Claude Code plugins on a per-session basis. RFC:docs/rfc/PLUGIN_ATTACH.md. New CLI flag--plugin <name>onadd/launch; new subcommandagent-deck plugin {list,attached,attach,detach}with--restart,--no-channel-link,--json,-q. TUI gains a Plugin Manager dialog (hotkeyl) with toggle/apply UX; Edit Session dialog gainsPlugins/PluginChannelLinkDisabledfields. Mechanics: catalog as[plugins.<name>]tables in~/.agent-deck/config.toml(cached, telegram-official filtered per RFC §6);Instance.Plugins,Instance.PluginChannelLinkDisabled,Instance.AutoLinkedChannelspersisted viastate.dbtool_data blob. Worker-scratch overlay:EnsureWorkerScratchConfigDirwrites a deny ∪ allow overlay onenabledPluginsin scratchsettings.json— detached catalog ids are forced tofalseto defeat Claude Code's "installed-but-unspecified = enabled" default. Auto-install:claude plugin install <id>shells out for unresolved attached plugins; best-effort, per-(profile, plugin) flock, env scrubbed (allow-list + secret-suffix blocklist) so postinstall hooks cannot exfiltrateCLAUDE_API_KEY/TELEGRAM_BOT_TOKEN/NPM_TOKEN. Channel auto-link (RFC §4.7): catalog entries withEmitsChannel=trueaddplugin:<name>@<source>toChannelsandAutoLinkedChannelstracks ownership (PR #779). -
Cross-profile data migration:
session move/conductor move/group move(#928, PR #929). Three new CLI surfaces for relocating data between profile DBs — previously only possible by hand-editing SQLite +meta.json:agent-deck session move <id> --to-profile <name> [--force];agent-deck conductor move <name> --to-profile <name> [--force](moves conductor session + every child worker + atomically rewritesmeta.json);agent-deck group move <group> --to-profile <name> [--force](batch). Orchestrator ininternal/session/profile_migrate.gois target-write-then-source-delete, with best-effort rollback if source-delete fails after target-insert.cost_eventsandwatcher_events(matched on bothsession_idandtriage_session_id) travel with each session, group rows auto-create in the destination, and every state.db write is wrapped in the existingwithBusyRetryhelper. Behavior decisions (clarified with the requester): running sessions are refused,--forceoverrides at the user's risk; conductor scope moves conductor + all children (parent_session_id == conductor.id); missing target profile is refused (typo safety) — user must bootstrap with--profile <name>first; re-running is idempotent. 11 unit cases inprofile_migrate_test.gocover preserves-all-fields, cost/watcher events, group creation, running-refusal + force, missing/same profile, idempotency, conductor children + atomic meta, group batch, concurrent migrations (PR #929). -
.worktreeincludefor automatic file copying into new worktrees (PR #890). When.worktreeincludeexists in the repo root, gitignored files matching its patterns are automatically copied into new worktrees beforeworktree-setup.shruns. Matches Claude Code Desktop semantics: only files that are both pattern-matched AND gitignored get copied, tracked files are never duplicated. Eliminates the need for boilerplate file-copy logic inworktree-setup.sh— projects declare what needs copying and reserve the setup script for imperative tasks like dependency installation. Read from repo root (not worktree, since source files are gitignored and only live in the main checkout); runs beforeworktree-setup.shso the script can depend on copied files; NUL-delimitedgit check-ignore -zfor path safety; directories merge into existing destinations (individual files skip if already present). New dependency:github.com/sabhiram/go-gitignorefor.gitignore-syntax pattern matching (PR #890). -
New-session dialog widened to 84 columns to fit long project paths (PR #894). Raise default dialog width to 84 columns and tie text field widths to the effective dialog size so project paths are not clipped on typical terminals (PR #894).
-
agent-deck session send --draftpre-fills the prompt without pressing Enter (PR #930). Useful for scripts that inject context into a running session: the user can review and submit manually instead of the message being auto-submitted (PR #930). -
[codex].commandconfigures the codex executable for built-in Codex sessions (PR #934). Built-in Codex sessions can now use a configured executable, wrapper, or alias. Codex resume / session discovery now honors inlineCODEX_HOME, including quoted paths with spaces (CODEX_HOME="/path with spaces/.codex"shape). Regression coverage inTestBuildCodexCommand_QuotedInlineCodexHomeWithSpaces,TestCodexHomeFromCommand_PreservesQuotedAssignmentSpaces,TestBuildCodexCommand_InlineCodexHomeForRolloutCheck(PR #934).
Changed
-
In-group hierarchy keys: K/J auto-promote at the parent's edge, and Shift+Left / Shift+Right explicitly outdent / indent (#849, PR #848).
K/J(andShift+↑/Shift+↓) now promote a sub-session to top-level when it is the first / last child of its parent, instead of silently no-op'ing.Shift+→demotes the cursor's top-level session to a sub-session of the previous top-level peer (last child);Shift+←is the symmetric outdent. All four shortcuts stay scoped to the current group — cross-group moves remain onM. Sub-sessions of different parents were previously interleaved in the visual flat list, and the only way to move a sub-session out of its parent without dropping to the CLI wasagent-deck session unset-parent <id>(orMto go to a different group, which is the wrong tool); K/J at the parent boundary was the one ergonomic gap left after #846. Single-level nesting and child-count guards mirror the existingsession set-parentCLI validation. NewGroupTree.PromoteSession/GroupTree.DemoteSessionmethods plus boundary-promote logic insideMoveSessionUp/MoveSessionDown; covered byTestPromoteSession_*,TestDemoteSession_*,TestMoveSession*Promotes,TestMoveSession*TopLevelAt*NoOpininternal/session/groups_test.go(PR #848). -
[worktree]settings documented in the config reference (PR #862). The[worktree]block (path_template,branch_prefix,default_location,auto_cleanup,setup_timeout_seconds,default_enabled) was fully implemented but missing from the config reference. This adds the complete section with a code block showing all options with defaults, a parameter table with types/defaults/descriptions, path template variable examples, branch prefix examples (default,$USER/, empty string), and an entry in the complete example. Source:internal/session/userconfig.goWorktreeSettingsstruct (PR #862). -
eof_fallback_firedWARN andstale_control_clients_sweptINFO observability for tmux close-cascade / orphan-PID bursts (PR #906). Two production diagnostics for the tmux/tmux#4980 race that the EOF fast-path mitigation in #882 only partly addresses; pure additions, no behavior change.eof_fallback_fired(Warn) incontrolpipe.go:Closecaptures the previously-discardedreapWithEOFGracereturn and logs when the EOF fast-path times out and the soft-kill fallback fires — the fast-path's design assumption is "fallback should essentially never fire", so production logs will tell us whether burst-close pressure is silently dropping us out of the fast path.stale_control_clients_swept(Info, withkill_count+duration) inpipemanager.go:killStaleControlClientsis emitted whenever the function SIGTERMs ≥ 1 orphantmux -Cclient — the 2026-05-08 crash on this path was 5 SIGTERMs in 11ms across 3 parallelConnect()invocations, a pattern not previously surfaceable because each individual SIGTERM was Debug-logged separately. The burst-shape Info line lets the cascade be observed as a single event in~/.agent-deck/debug.log. Recurs at MTBF ~22h on macOS Homebrew tmux 3.6a on v1.7.72-2-g6b82525 (PR #906).
Docs
- Five explainer images embedded into the user-facing concept guides (PR #799). New PNG diagrams in
documentation/assets/:conductor-overview.png(top of CONDUCTOR.md),channels-topology.png(CONDUCTOR.md one-bot-per-conductor section),watcher-doorbell.png(top of WATCHERS.md),skills-tiers.png(top of SKILLS.md),watchdog-restart.png(top of WATCHDOG.md). Generated via codex CLI's built-inimage_gentool (gpt-image-2), Tokyo Night palette, technical-architecture style. Total ~5.6 MB. Refresh recipe lives in the conductor's CLAUDE.md so any conductor can regenerate one of these in ~2 minutes (PR #799).
Chore
- Orphaned worktree gitlinks removed (PR #897). Two gitlinks (
.claude/worktrees/agent-a3b98724,.claude/worktrees/agent-af955763) were committed without a corresponding.gitmodulesentry — orphaned submodule references from stale Claude Code worktrees that no longer exist. Removed, and.claude/worktrees/added to.gitignoreto prevent future accidental commits of worktree tracking state (PR #897).
Known issues
internal/costs::TestStore_TotalLastWeek_OnlyLastWeekEventstill fails when the local clock is on a Monday in UTC (issue #932, unchanged from v1.9.0/v1.9.1). Test-fixture time-arithmetic edge case in a package untouched by v1.9.2; does not affect built binaries. v1.9.2 was cut on a Wednesday — full suite green.Weekly Regression Checkcron failures (issue #943) are unrelated to v1.9.x and remain open.
[1.9.1] - 2026-05-11
Patch release on top of v1.9.0 — two stability fixes from same-day post-release triage. Both touch the session lifecycle: cascade-prevention via serial-within-group as the new default, and agent-deck rm correctness under concurrency plus notifier cleanup.
Fixed
-
agent-deck rmsilently lost ~11 of 14 removals under parallelxargs -Pinvocation (#909, PR #935).SaveWithGroupsrewrites the entire instances table viaINSERT OR REPLACE, so a concurrentrmprocess resurrects rows another process just deleted — the CLI nevertheless printed✓ Removedfor each call. Fixed by the newRemoveSessionAndVerifyflow ininternal/session/storage.go: targeted DELETE wrapped inwithBusyRetry(the helper landed in #912),SaveGroupsOnlyfor any group structural change (neverSaveWithGroups), then re-read the row and re-DELETE on resurrection with linear backoff. ReturnsErrRemovalNotPersistentafter exhaustion so the CLI exits nonzero instead of falsely claiming success. Same shape also fixes the separately-noted "session remove --forcereports success but the row stays" failure mode — bothcmd/agent-deck/main.go(handleRemove) andsession_remove_cmd.go(--force+--all-errored) route through the new helper. Two-way correctness check: temporarily reverting to the oldDeleteInstance + SaveWithGroupsflow reproduced 11/12/13 survivors across three runs ofTestRm_ParallelDoesNotLoseRemovals— matching the issue's "only ~3 of 14 actually deleted" report. With the fix restored,go test -race -count=5 -run TestRm_ ./internal/session/is green (PR #935). -
Notifier inboxes replayed
deferred_target_busyevents forever for sessions removed viaagent-deck rm(#910, PR #935).~/.agent-deck/inboxes/<conductor>.jsonlandruntime/transition-notify-state.jsonaccumulate entries keyed bychild_session_id; nothing previously cleared them when the child went away. Newinternal/session/rm_sweep.goaddsSweepInboxesForChildSession(id)(atomic per-file temp+rename; whole-file removal when nothing survives) andRemoveNotifyStateRecord(id)(idempotent JSON edit). Both are best-effort: failures warn but never block the rm. Wired into both single-session and--all-erroredbulk rm paths after a successfulRemoveSessionAndVerify(PR #935).
Changed
- Serial-within-group is now the default for newly-created groups (PR #933). Adds
MaxConcurrenttoGroup/GroupData/GroupRow(SQLite column added idempotently); semantics are<=0unlimited (legacy default for pre-v1.9.1 groups),1serial (new-group default),N>=2cap at N.agent-deck launchandagent-deck session startconsult the target group's cap; over-cap launches persist asStatus=queued(a new real registry state surfaced inlist --jsonasstatus=queued) instead of starting.agent-deck session stopdrains the queue: after Kill, finds the oldest queued sibling in the same group via FIFO and starts it. CLI exposes the field viaagent-deck group create --max-concurrent Nandagent-deck group update --max-concurrent N. Driven by two converging signals: the 2026-05-08 cascade (9 parallel workers launched intoagent-deck-stabilitytriggered systemd-oomd → killed the conductor scope; the per-MCP cgroup wrapper from #902 prevents one MCP from dragging the orchestrator down, but doesn't cap the number of co-resident workers a group may spawn), and Factory Missions research ("Parallel agents conflict, duplicate work, make inconsistent architectural calls. Serial is the working pattern."). Backward compat preserved — existing groups loaded from a row withmax_concurrent=0keep legacy unlimited behavior; only groups created viaGroupTree.CreateGrouppost-v1.9.1 default to 1 (PR #933).
Known issues
internal/costs::TestStore_TotalLastWeek_OnlyLastWeekEventstill fails when the local clock is on a Monday in UTC (issue #932, unchanged from v1.9.0). Test-fixture time-arithmetic edge case in a package untouched by v1.9.1; does not affect built binaries.
[1.9.0] - 2026-05-11
Stability + cascade-prevention release. Closes the v1.8 flicker / silent-drop / panic-cascade bug class and the 2026-05-08 conductor-OOM cascade. Seven themed bundles from the V1.9 priority plan land here; remaining T2 / T5 / T7 longer-tail items follow in v1.9.x patches.
Fixed
-
Inotify
IN_Q_OVERFLOWno longer leaves hook state stale until restart (root cause of the v1.8 "flickering red dots" report). The status-file watcher now re-walks the hooks dir from disk on overflow, builds a fresh map, and atomically swaps it under the mutex. Detection viaerrors.Is(err, fsnotify.ErrEventOverflow)so wrapped errors from future fsnotify versions still trigger recovery (PR #900, theme T1). -
Hook read/parse failures are no longer silently swallowed.
internal/web/session_data_service.go:170/185/190previouslycontinued onos.ReadDir,os.ReadFile, andjson.Unmarshalerrors — producer-side hook bugs fromcmd/agent-deck/hook_handler.gowere invisible while the UI showed stale status. WARN logs added at all three sites (hook_status_dir_read_failed,hook_status_read_failed,hook_status_unmarshal_failed);os.ErrNotExiston the dir stays silent for the legitimate first-run condition (PR #900, theme T3). -
cmd/agent-deck/hook_handler.go8 silent swallows inwriteHookStatusandwriteCostEvent(MkdirAll,json.Marshal,WriteFile,Rename) now emit WARN with file/instance/error context. Best-effort.tmpcleanup on Rename failure prevents orphan accumulation (PR #900, theme T3). -
session sendretry-cadence regression guards rewritten. The 8TestSendWithRetryTarget_*canonization tests atcmd/agent-deck/session_send_test.go:242/257/277/332/360/385/434/594were holding the pre-#876 silent-success contract — they would have actively gated a re-introduction of the silent-drop bug. Rewritten to assert the post-#876 contract (verifyDelivery: true→ silent drops surface as errors). State-machine assertions (Enter/Ctrl+C call counts, retry budget shape) preserved as independent regression guards. CLI-vs-TUI parity tests forWaiting/Idle/Erroradded next to the existingRunningparity test ininternal/session/instance_cli_parity_test.go— the previous parity coverage was a one-state slice (PR #899, theme T4). -
Duplicate MCP children for same
(session, name)prevented. Companion to #902. Strict per-(session, mcp-name)singleton check before any new spawn; 5s Stop timeout before fresh Start (was racing on restart); INFO log on every spawn (mcp_proc_spawn) for forensic visibility. The 43×-duplicatecontext7-mcpsource traces to claude code's npx-spawned plugin launcher (an upstream issue documented but not fixable here); combined with #902's per-scope cgroup wrapper, agent-deck is now resilient even to that upstream bug. Test asserts 5x session restart yields exactly 1 child process (PR #904). -
Mcppool synchronous failure surfacing +
HTTPServerFD-leak +Cmdrace. Follow-up to #902 caught by the v1.9.0 release gate: under the newsystemd-run --scopewrapper,SocketProxy.StartandHTTPServer.Startno longer swallow inner-binary failures (/no/such/binarynow surfaces synchronously, restoring the pre-#902 contract). Companion fixes close a log-file FD leak inHTTPServer.Start(24 leaked FDs over 200 failed cycles → 0) and eliminate aWARNING: DATA RACEonos/exec.Cmd.Wait()atinternal/mcppool/http_server.go:301(concurrent*Cmdaccess between Start.func3 and the cleanup goroutine added by #915) (PR #931). -
SQLite contention + atomic conductor metadata writes (theme T7). New
withBusyRetry(name, op)helper extracted fromSaveWatcherEvent's 5-attempt retry pattern and applied to three sister sites that previously had no retry under SQLITE_BUSY:WriteStatus(transition daemon hot path),UpdateWatcherEventRoutedTo(triage reaper, 4 callers),pruneWatcherEvents(post-insert pruner). Helper logs WARN on first retry, ERROR on exhaustion — contention storms are now diagnosable instead of silent.SaveRecentSessionINSERT + prune wrapped in a transaction.SaveConductorMetaswitched to write-temp-rename for crash-safe atomicity (a crash mid-write previously truncated the file) (PR #912, theme T7). -
FD leaks + watcher channel-close + ptmx mutex (theme T5). Three documented FD leaks plugged:
tmux/chrome.go:163(iTerm badge log file),mcppool/socket_proxy.go:211,mcppool/http_server.go:148.engine.Stop()now closes exported watcher event channels so consumerrangeloops exit cleanly instead of blocking forever (<-engine.EventCh()returns(_, false)post-Stop).ptmxmutex extended to coverWriteInputandstreamOutputinweb/terminal_bridge.go:131/105, eliminating the same shape of race that previously produced an intermittent CI flake on the read path (PR #915).
Added
-
internal/sessionstatus/— single-owner hook→status derivation package. Replaces the duplicated decision tree (tool gate → freshness window → hook→status switch) that lived in bothinternal/session/instance.goUpdateStatusandinternal/web/snapshot_hook_refresh.go. Concrete divergences caught and locked by tests: codexrunning20s window (CLI rejected, web accepted up to 2 min); claudewaiting+ acknowledged → idle (CLI dropped, web stuck forever); duplicatedIsClaudeCompatible(tool) || tool == "codex" || tool == "gemini"tool gate now insessionstatus.IsHookEmittingTool. v1.9.0 migrates only the web read-path (closes the active bug class); CLI / TUI / transition-daemon migrations follow in v1.9.1 per the plan's staged scope (PR #898, theme T1). -
flicker_detectedWARN. Newinternal/session/flicker_detector.gowatches per-session status transitions over a sliding 60s window. Emits one WARN per session per 60s cooldown when transitions exceed 3 — the six-flicker oscillation incident logged at Debug only would now surface at default log level. Wired intointernal/ui/home.goat the existingstatus_changedsite, only fires whennewStatus != oldStatusso quiet sessions pay no cost (PR #900, theme T3). -
internal/safego—safego.Go(logger, name, fn)panic-recovery helper. Runsfnin a goroutine under a deferredrecover(), logs panics at WARN with name + recovered value + runtime stack, then swallows. Applied to the 4 fire-and-forget goroutines ininternal/ui/home.gothat previously had no panic recovery (startup_pipe_connect,startup_log_maintenance,apply_theme_to_sessions,conductor_clear_and_heartbeat) — a single un-recovered panic in any of these previously killed the entire TUI. The 4 well-placedrecover()arms already living athome.go:2700-3000are unchanged; this factors the same pattern into a reusable helper for the previously-unguarded sites (PR #901, theme T6). -
tmux.go:2517defensive type-assertion guard. Comma-ok form on thesingleflight.Doresult. The closure unconditionally returns(string, nil)today so the bare assertion couldn't panic, but the form was flagged as a load-bearing code smell if the closure is ever refactored. Pure hygiene; no behavior change (PR #901, theme T6). -
MCP-per-scope cascade prevention. On Linux+systemd hosts, each MCP child process now spawns inside its own transient user scope (
mcp-<owner>-<mcp>-<ts>.scopeundermcp-pool.slice) withMemoryMax=1G,CPUWeight=50, andTasksMax=200. Background: a 2026-05-08 cascade saw 43 simultaneous duplicate@upstash/context7-mcpinstances accumulate inside the conductor's tmux scope (58.2 GB resident); whenuser@1000memory pressure crossed 50 %, systemd-oomd ranked cgroups by memory × pressure × pgscan and picked the conductor scope (largest by RSS) — SIGKILLing 604 processes in one shot, including the conductor itself. Per-MCP scopes give systemd-oomd a precise kill target so a misbehaving MCP becomes its own victim instead of dragging the orchestrator down. Gated behindAGENT_DECK_MCP_ISOLATION(default ON on Linux, OFF elsewhere); falls back to plainexec.Cmdwhensystemd-runis missing (containers, minimal images) or on macOS/Windows. Scope semantics usesystemd-run --scope, whichexecve's into the target command — so PID, file descriptors, env vars, process group, and the existingSetpgid + SIGTERM-the-pgroupcancel logic insocket_proxy.goall keep working unchanged. Wired into both stdio (internal/mcppool/socket_proxy.go) and HTTP (internal/mcppool/http_server.go) MCP launch paths. Eight new regression tests ininternal/mcppool/scope_launcher_test.goandscope_launcher_integration_test.go, including a two-way correctness check (mutatingwrapMCPCommandto drop the wrapper makes the integration test fail with the child landing back in the parent's scope — exactly the cascade pattern from the root cause analysis) (PR #902). -
Phase-1 regression coverage (12 cases). Twelve focused unit tests targeting v1.8.x ship-blockers and P0 use-case scenarios — test-only, no impl changes. Profile precedence (
prof-001/002/003): full ladder explicit >AGENTDECK_PROFILE>CLAUDE_CONFIG_DIR>config.json default_profile> literal "default", plus theprofileFromClaudeConfigDirdirect table. Web hook overlay (web-001/002/003):refreshSnapshotHookStatusesis now end-to-end-asserted onGET /api/menuandGET /api/session/{id}; defensive shapes (nil snapshot, nil session, group items, empty loader) locked in. Send (send-001/002):messageDeliveryTokenbody-prefix contract +verifyDelivery=truelarge-prompt path. Rebind (rebind-001):clearRebindMtimeGrace5s boundary. Wire format (status-001/route-001/status-stop-001): eachsession.Statusvalue's lowercase JSON literal, ghost-session 404 negative path, STOPPED-stickiness through both GET handlers (PR #903, theme T8). -
Phase-1 test infrastructure — 8 harnesses.
internal/testutil/{crossfixture,fakeclock,fakeinotify,logassert,multiclienttmux,profilefixture,teatesthelper}packages plus shared scaffolding. Unblocks the broader Phase-1 regression cells that need fake-tmux / fake-inotify / fake-clock seams (PR #916, theme T8). -
8 of top-10 logging additions (theme T3).
session_createdINFO,status_changedwithreasonfield,hook_eventINFO,hook_file_corruptWARN,tmux_setup_failedWARN,http_requestmiddleware INFO,hash_fallback_usedonce-per-session WARN,session_status_cascadesummary INFO. The remaining two (pipe_degradedaggregated WARN,capture_pane_subprocess_fallbackWARN-promotion) follow in v1.9.x (PR #914, theme T3).
Changed
- Sister-function consolidation: clusters S1, S2, S3 (theme T2). The four
GetClaudeConfigDir*siblings atclaude.go:246/305/370/410collapse into a singleResolvefunction (the anti-pattern that produced #881 profile divergence). The four tmux-session-liveness sites attmux.go:1949/300/2451andpipemanager.go:568consolidate behind one helper, ending the 2s-cache-window family of bugs (#886 heartbeat parity). The fourCapturePane*siblings (S3) likewise unify. Drift-detection guard test asserts each consolidated cluster shares one symbol — any future PR re-introducing a parallel implementation fails at test time instead of at user-report time (PR #913, theme T2).
Deferred to v1.9.x
chore(v1.9.x): three small followups(PR #905, explicitly v1.9.x by author):1.7.99 → 9.9.99sentinel inupdate_nudge_test.go(T4), GitHub webhook normalizers stop swallowingjson.Unmarshal(T2/T3) — landed on main but not part of the v1.9.0 thematic scope.- CLI / TUI / transition-daemon migration to
internal/sessionstatus/(v1.9.0 migrates only the web read-path). - Remaining two logging additions (
pipe_degraded,capture_pane_subprocess_fallbackpromotion). - Sister-function clusters S4–S12 (S1/S2/S3 ship in #913; the rest follow incrementally).
- Phase-1 integration / e2e cells that depend on harness wiring still in flight.
Known issues
internal/costs::TestStore_TotalLastWeek_OnlyLastWeekEventexhibits a deterministic failure when the local clock is on a Monday in UTC. Same class of bug previously addressed by #859; flagged here for a follow-up issue. Does not affect built binaries — purely a test-fixture time-arithmetic edge case in a package untouched by v1.9.
[1.8.3] - 2026-05-07
Hotfix bundle on top of v1.8.2. Three contributor PRs: a TUI inline-title regression and two conductor heartbeat-rules improvements bringing the OS heartbeat path to parity with bridge.py.
Fixed
-
Inline pane title vanished between refreshes when the tmux pane-info cache went stale (PR #877, thanks @borng).
refreshSessionRenderSnapshotreadstmux.GetCachedPaneInfoon every rebuild, but onlybackgroundStatusUpdaterefreshes that cache. When other rebuild paths (e.g.processStatusUpdate) ran past the 4 s freshness threshold,GetCachedPaneInforeturnedok=falseand the rebuild zeroedpaneTitle— the inline task suffix added in #474 (Claude/rename, spinner state) blinked to empty between successful ticks. Fixed by falling back to the previous snapshot'spaneTitleon cache miss; the fallback re-reads the latest snapshot inside the per-instance branch to narrow the read-store race between concurrent rebuild goroutines. Adds two regression tests:TestRefreshSessionRenderSnapshot_PaneTitleUpdatesEachRefresh(fresh-cache contract) andTestRefreshSessionRenderSnapshot_PaneTitlePreservedWhenCacheStale(the regression pin, fails on un-fixed code). -
HEARTBEAT_RULES.mdsilently ignored on hosts using the OS heartbeat daemon (PR #886, thanks @nlenepveu). PR #218 externalized heartbeat policy intoHEARTBEAT_RULES.mdbut only wired it intoconductor/bridge.py. The second heartbeat path —heartbeat.shgenerated fromconductorHeartbeatScriptand scheduled by systemd/launchd — never read the file, and bridge.py auto-disables its own loop when the OS daemon is detected. Net effect: on the default Linux/macOS path the rules existed, the docs referenced them, and the script that actually fired ignored them. Fixed by resolvingHEARTBEAT_RULES.mdwith the same triple fallback (per-conductor → per-profile → global), appending the rules after a blank line when the file is non-empty, and switching the message prefix fromHeartbeat:to[HEARTBEAT]to match bridge.py and the conductor template docs.MigrateConductorHeartbeatScriptsrewritesheartbeat.shautomatically — no user action required. -
No way to point a conductor at a project-repo
HEARTBEAT_RULES.mdwithout copying (PR #887, thanks @nlenepveu).agent-deck conductor setupalready supported--policy-mdforPOLICY.md, but the equivalent for the heartbeat rules file was missing even though both share the same per-conductor → per-profile → global lookup order. Added--heartbeat-rules-md, a direct mirror of--policy-md: it creates a symlink at~/.agent-deck/conductor/<name>/HEARTBEAT_RULES.mdpointing at the user-supplied path, claiming the highest-precedence slot in the lookup order. Wired throughcmd/agent-deck/conductor_cmd.go(flag + Usage block alongside--policy-md) andinternal/session/conductor.go(SetupConductor/SetupConductorWithAgentacceptcustomHeartbeatRulesMD, slotted right aftercustomPolicyMD, reusingcreateSymlinkWithExpansionfor~handling).
[1.8.2] - 2026-05-07
Three real-bug fixes addressing top items from the priority survey: size-guard regression, tmux SIGSEGV adoption from a contributor branch, and TUI/web profile resolution divergence.
Fixed
-
Size-guard rejected new sessions created by Claude
/clear(#856, PR #883). Reported by @ZDreamer2. After/clearClaude wrote a fresh smaller jsonl that the size-guard refused to rebind to, leaving the TUI stuck on the old session. Fixed by adding an mtime-newer escape hatch: if the candidate jsonl is older by ≥5 s and the new one isn't, rebind regardless of byte size — preserves all existing flap-protection (where files seed within microseconds) while letting legitimate user-initiated/clearevents through. -
tmux SIGSEGV during ControlPipe shutdown on macOS Mac (#816, PR #882, thanks @tarekrached). Cherry-picked from @tarekrached's
tarek/controlpipe-eof-clean-shutdownbranch — he ran 36/36 stress trials clean. SwitchesControlPipe.Close()from a SIGTERM-then-grace fallback to a stdin EOF fast path with a 200 ms grace before falling back to soft-kill. Eliminates the upstream tmux #4980-class crash in real workflows. -
TUI and web showed different sessions for the same user when
AGENTDECK_PROFILEwas set in env but not inconfig.jsondefault_profile(#881 PR #884). The TUI/CLI inheritAGENTDECK_PROFILEfrom the parent shell; the web server read onlydefault_profilefrom config. Same DB, two views — trust-killer. Fixed by unifying resolution: web now consultsAGENTDECK_PROFILEfirst (matches TUI/CLI), falling back toconfig.json. Single source of truth.
[1.8.1] - 2026-05-06
Hotfix bundle on top of v1.8.0. Five focused bug fixes — three from external contributors, two from accumulated triage.
Fixed
-
agent-deck session sendcould silently drop prompts to sub-sessions (#876, PR #879). Reported by @DOKoenegras (v1.7.71). The verification loop insendWithRetryTargettracked positive delivery signals (active-status, paste-marker, message-in-pane) but treated their absence as success. Under timing races (sub-session spawned in quick succession, inner Claude TUI's input handler not yet mounted), every signal genuinely fails to fire and the loop returnsnilafter exhausting its 15s budget. Fixed by adding opt-inverifyDeliverytosendRetryOptions; default-on for the CLI'sdefaultSendOptions()andnoWaitSendOptions()paths. When set, the loop now returns an error referencing #876 if no positive evidence is observed. Six new regression tests incmd/agent-deck/session_send_test.go; two confirmed TDD red→green. -
bridge.pyfailed to import on Python 3.8 (default WSL Ubuntu 20.04) (#864, PR #878). Reported by @JMBattista. Runtime use ofCoroutinefromcollections.abc(PEP 585 subscript) failed on Python 3.8 withTypeError: 'ABCMeta' object is not subscriptable. Fixed by importingCoroutinefromtypinginstead. Addedconductor/tests/test_python_compat.py(AST scan for runtime PEP 585 subscripts) and.github/workflows/python-compat.yml(matrix on Python 3.8/3.9/3.10/3.11/3.12) so this can't regress. -
Homebrew install verification (#873, PR #878). Reported by @Wolfsrudel. Live infrastructure was already healthy on v1.8.0 (goreleaser brews block fired correctly, formula present at
asheshgoplani/homebrew-tap); the original report predates that fix. Addedscripts/verify-homebrew-install.sh(8 checks: tap reachable, formula present, version matches latest release, all asset URLs resolve, README install command unchanged) and.github/workflows/homebrew-verify.yml(runs on PRs touching install docs / goreleaser, on every release tag, weekly cron) so future drift gets caught. -
TOCTOU race in worktree setup script executable-bit dispatch (PR #861, thanks @spawnia).
buildSetupCmdre-stat'd the script afterfindWorktreeSetupScripthad already statted it, opening a window where mode bits could change between calls. Fix capturesos.FileModeonce at discovery and threads it through. Internal-only signature change; publicCreateWorktreeWithSetupAPI unchanged. New testTestFindWorktreeSetupScript_PresentExecutablevalidates the captured mode. -
~in worktreepath_templatewas treated as a literal directory (PR #863, thanks @spawnia).resolveTemplatedid not expand~so configured paths like~/.agent-deck/worktrees/{repo}/{branch}resolved to nonsense like/home/user/project/~/.agent-deck/worktrees/....GenerateWorktreePathalready had the right expansion; this realignsresolveTemplatewith it. Includes a regression test that fails with the literal-~path before the fix. -
%filter exclude-set is now configurable; active-filter hint is highlighted (PR #874, thanks @borng). Resolves #491 / #516. Adds[display].active_filter_excludesconfig — default["error", "stopped"]preserves existing behavior; opt in to["error"]to keep stopped/closed sessions visible. The pill bar's dim state,matchesStatusFilter, and per-frame hint render all consult the same exclude set. The$keybinding alignment between TUI/MD docs/UI hint is a follow-up; see borng's comment on PR #874.
[1.8.0] - 2026-05-06
WebUI redesign — five-zone responsive layout. Ships PR-B (PR #860) on top of every accumulated v1.7.81-v1.7.83 hotfix that the redesign was originally targeted at. Users running v1.7.83 still saw the pre-redesign UI; v1.8.0 is the version where the new shell actually reaches them.
Added
- Five-zone AppShell — top bar, left rail, main pane, right rail, mobile tab bar. Replaces the prior two-pane layout. Tablet (~820px) and phone (<720px) breakpoints documented in the playwright
chromium-tablet/chromium-phoneprojects. - RightRail panel — pulled session-context affordances out of the main pane into a dedicated rail (toggleable on tablet, hidden on phone in favor of the bottom tab bar).
- MobileTabs — bottom tab bar that surfaces the rail/main switching that desktop gets via the side rails.
- CommandPalette redesign — restyled chrome consistent with the new dialog system.
- Restyled dialogs —
CreateSession,Confirm,GroupNamemove to the new design tokens; new dialog header/footer rhythm. - Restyled panels —
Toast,ToastHistoryDrawer,SettingsPanel,EmptyState,TerminalPanelchrome,CostDashboardchrome. - Design tokens —
internal/web/static/app/design-tokens.cssextracts color / spacing / radius primitives consumed by the redesigned components. Tailwind output regenerated against the new source globs. /api/profiles+/api/system/stats— new GET endpoints poweringProfileDropdown(display-only by design) and the redesignedStubPane.
Changed
- Pre-redesign components removed. The legacy two-pane chrome and its assets are gone — there is no
?legacytoggle. Anyone pinning to an older bundle should pin to the v1.7.83 release artifacts. - Visual-baseline screenshots regenerated for the new shell across desktop/tablet/phone projects.
Fixed
- Cold-load
profileSignalno longer flashes "personal" before the API resolves — initial render now defers the dropdown label until/api/profilesresponds, so users on a non-personal profile don't see a one-frame flicker. TerminalPanestays mounted across tab switches — orphan signal exports that survived the redesign port were removed; tab switches now preserve PTY state instead of remounting and dropping the connection.
Notes
- Profile switcher is display-only.
ProfileDropdownshows the active profile but does not switch profiles from the web UI. Switching is still done via the-p/--profileflag atagent-deckinvocation time. Surfacing read-only state was a deliberate scoping choice for v1.8.0. - Bundles every v1.7.81-v1.7.83 hotfix. Multi-client tmux size mismatch (#866), web
/api/sessionswaiting-status divergence (#867),TestTmuxPTYBridgeResizeCI skip (#871) are all included — those releases shipped on top of the pre-redesign UI; v1.8.0 is where they meet the new shell. - Stack stays Preact + htm + signals. No framework rewrite, no new dependencies. The redesign reorganizes layout and chrome only.
[1.7.83] - 2026-05-06
Unblocks the release pipeline that failed twice on TestTmuxPTYBridgeResize.
Fixed
- CI-only test flake blocking goreleaser (PR #871).
TestTmuxPTYBridgeResizeasserts a WebSocket resize message propagates through the bridge's attach-client PTY all the way to the tmux session geometry. On CI's headless GitHub Actions runner, the attach-client PTY never reaches the requested 120×40 size —pty.Setsizeis called locally but tmux's view of the client size stays at 80×24. Verified-working on real PTYs (macOS/Linux desktops). The production fix shipped in #866 stays covered bySession.Starttests ininternal/tmux. This is a CI-environment workaround, not a production code change. Skipped only whenCI=trueorGITHUB_ACTIONS=true. Surfaced when v1.7.81 and v1.7.82 release pipelines both failed on this test.
Note: v1.7.81 and v1.7.82 tags exist on GitHub but no Release was ever published for either — both phantom tags. v1.7.83 is the proper landing of all three accumulated hotfixes (size-mismatch, status-divergence, CI test fix).
[1.7.82] - 2026-05-05
Bundled hotfix release. Supersedes the v1.7.81 tag: that tag was created but no GitHub Release was ever published — the goreleaser pipeline failed on a CI-only test bug (run 25395639116) before the binaries could be uploaded. v1.7.82 ships the v1.7.81-intended multi-client tmux size fix, the test fix that unblocks the release pipeline, and a separately-discovered status-divergence fix for the web UI.
Fixed
-
Multi-client size mismatch ("dots in the window") between web UI and direct tmux clients (PR #866). Two contributing bugs combined: tmux's default
window-size latestpolicy snapped the window to whichever client most recently sent input, and(*tmuxPTYBridge).Resizeissued an explicittmux resize-window -x N -y Mon every browser FitAddon resize, which perman tmuximplicitly flips the session option towindow-size=manual. Together this dragged native attach clients (Ghostty, iTerm) to the web viewport's geometry and pinned them there. Fixed in two places:internal/tmux/tmux.gonow setswindow-size=largest(session) +aggressive-resize=on(window) per session atSession.Start, gated through the existing[tmux] optionsconfig-override mechanism so users can opt out;internal/web/terminal_bridge.gono longer issuestmux resize-windowfromResize(the localpty.Setsizekeeps xterm.js's grid correct), and the-f ignore-sizeflag was dropped fromtmuxAttachCommand(no longer needed since the web client now participates in thelargestarbitration alongside native clients). Smaller clients see clipped content rather than dragging the window. New integration testTestSession_MultiClientSizePolicy_Integrationasserts both options are set afterSession.Start. See tmux issue #2594 for the upstream pattern. (Originally targeted v1.7.81; the release pipeline failed on the test bug below before the binaries shipped.) -
TestTmuxPTYBridgeResizefailed in the CI release pipeline, blocking the v1.7.81 goreleaser run (PR #869). The test created its tmux session manually without thewindow-size=largestoption that the production code path now sets atSession.Start(PR #866). On CI's headless tmux, the defaultwindow-size latestpolicy interacts differently with the test's resize sequencing than on a developer machine, surfacing a flake that local runs never saw. Fix setswindow-size=largeston the test session up-front so the test environment matches production. Verified withgo test -run TestTmuxPTYBridgeResize ./internal/web/locally and on CI. -
Web
/api/sessionsreportederrorfor sessions whose hook file saidwaiting, whileagent-deck list --jsonreportedwaitingfor the same sessions at the same instant (PR #867). Root cause: the live web reads fromMemoryMenuData, an in-memory snapshot pushed by the TUI'spublishWebSessionStates. The TUI's view ofInstance.hookStatusis fed byStatusFileWatcher(inotify); when an inotify event is dropped (queue overflow under load — 1100+ hook files in~/.agent-deck/hooks/is enough to hit this in steady state) the TUI'shookStatusstays stale, the hook fast-path freshness window expires,Instance.UpdateStatusfalls through to tmux pane heuristics, and the published Status flips toerror. The CLI does not have this gap becauseagent-deck list --jsonreads each hook file from disk per call viasession.RefreshInstancesForCLIStatus. Fix addsinternal/web/snapshot_hook_refresh.gothat re-applies the hook fast-path Status mapping (matchingInstance.UpdateStatus's switch onhookStatus) to the cachedMenuSnapshotbefore the GET handlers (/api/sessions,/api/menu,/api/session/{id}) serialize it. Stopped sessions are never overridden (user-intentional). Fresh hooks (within the 2-minhookFastPathWindow) override any non-stopped state. Stalewaitinghooks specifically override snapshot=errorbecause Claude's "waiting" state is durable across hook event gaps — a Stop hook that fired hours ago without a follow-up UserPromptSubmit means Claude is still at the prompt, exactly the case the CLI captures via tmux pane-title heuristics that the web cannot reach without per-request subprocesses. Live before/after on a system with 21 waiting sessions: web reportedwaiting=0before the fix,waiting=21after (CLI reported 21 throughout).Test coverage in
internal/web/snapshot_hook_refresh_test.go: a regression test (TestParity_WaitingStatusFlowsThroughHandler) reproduces the exact production divergence by seeding a snapshot withStatus: StatusErrorand an in-memory hook overlay sayingwaiting, then assertingGET /api/sessionsreturnswaiting; this test fails before the fix and passes after. A property test (TestRefreshSnapshotHookStatuses_NoHookFilePreservesAllStatusesand the parallelTestParity_AllStatusesPreservedThroughGetSessions) iterates all sixsession.Statusenum values (StatusRunning,StatusWaiting,StatusIdle,StatusError,StatusStarting,StatusStopped) and asserts each round-trips through the API unchanged when no hook overlay applies — locking the contract that adding a new Status without wiring the web fails the build. Plus targeted unit tests for stale/fresh override semantics, stopped-stickiness, and shell-tool no-op.
Notes
- v1.7.81 was a phantom tag. The git tag
v1.7.81exists in the repository (created by PR #868 merging) but no GitHub Release was ever published under that tag because the goreleaser workflow failed on theTestTmuxPTYBridgeResizetest (now fixed in PR #869). The tag is left in place as a historical record. v1.7.82 is the proper landing of the v1.7.81-intended fixes plus the test fix and one additional status-divergence fix.
[1.7.81] - 2026-05-05
Hotfix for a multi-client tmux size-negotiation bug that caused dot-filled void cells when the web UI and direct tmux attach clients were both connected to the same agent-deck session at different geometries.
Fixed
- Multi-client size mismatch ("dots in the window") between web UI and direct tmux clients. Two contributing bugs combined: tmux's default
window-size latestpolicy snapped the window to whichever client most recently sent input, and(*tmuxPTYBridge).Resizeissued an explicittmux resize-window -x N -y Mon every browser FitAddon resize, which perman tmuximplicitly flips the session option towindow-size=manual. Together this dragged native attach clients (Ghostty, iTerm) to the web viewport's geometry and pinned them there. Fixed in two places:internal/tmux/tmux.gonow setswindow-size=largest(session) +aggressive-resize=on(window) per session atSession.Start, gated through the existing[tmux] optionsconfig-override mechanism so users can opt out;internal/web/terminal_bridge.gono longer issuestmux resize-windowfromResize(the localpty.Setsizekeeps xterm.js's grid correct), and the-f ignore-sizeflag was dropped fromtmuxAttachCommand(no longer needed since the web client now participates in thelargestarbitration alongside native clients). Smaller clients see clipped content rather than dragging the window. New integration testTestSession_MultiClientSizePolicy_Integrationasserts both options are set afterSession.Start. See tmux issue #2594 for the upstream pattern.
[1.7.80] - 2026-05-05
WebUI overhaul Phase 1 + one small Claude-session UX fix.
Added
- WebUI test infrastructure + TUI⇄web parity matrix (PR-A of WebUI overhaul, PR #804). Foundation PR for the WebUI redesign — pure test infrastructure with no design changes. Adds Vitest unit tests (
tests/web/unit/, jsdom + @testing-library/preact, 11 specs againstapi.js+state.js), Playwright e2e + screenshot regression (tests/web/e2e/, 279 specs across 3 viewports — smoke, parity-actions with behavioral assertions for groups/settings/cost/push, parity-state, visual baselines), an in-memory web fixture binary (tests/web/fixtures/cmd/web-fixture/main.go) hardened against stale-server false-passes (OS-allocated ephemeral port + 16-byte random startup token + pid verification via/__fixture/whoami), the TUI ↔ web parity matrix attests/web/PARITY_MATRIX.mdcataloging 47 actions and ~50 state fields (surfaces a 64% action gap and 76% state-field gap from TUI to web) — tests now iterate the full matrix instead of a hard-coded subset, fail explicitly when any TUI/web row drifts, a Go runtime sync-invariant test atinternal/web/parity_test.gothat fires actions through both the HTTP surface and the mutator and asserts equal observable state including group create/rename/delete,Makefiletargets (test-web,test-web-unit,test-web-e2e,test-web-install), and a.github/workflows/web-tests.ymlCI workflow. Lifecycle endpoints get positive parity tests; missing endpoints get "stay missing" regression guards so any silent addition fails the build until the matrix is updated in lockstep. Three rounds of dual-review (Claude + Codex sibling-topology) drove the test-fidelity hardening. Stack stays Preact + htm + signals (already vendored). PR-B (visual redesign) builds on top of this.
Fixed
- Persist Claude New Session defaults (PR #853, thanks @yaroshevych). Three new TOML keys on
[claude](extra_args,use_chrome,use_teammate_mode) persisted on Claude session creation and replayed viaSetDefaults. Backward compatible: missing keys load as zero values. Note: every New Session creation now overwritescfg.Claude.DangerousMode / AllowDangerousMode / AutoModewith whatever the dialog held — by design, but a hand-editeddangerous_mode = trueinconfig.tomlwill flip if the box is unchecked.
[1.7.79] - 2026-05-01
Two TUI polish fixes from @AdamiecRadek (their 5th and 6th PR landing this week — #813→#814 pricing, #818→#819 cost line, #836→#837 context window, #846, #847).
Fixed
- Session reorder (
K/J,Shift+Up/Shift+Down) required multiple key presses to produce a visible move when sub-sessions of different parents were interleaved in a group.MoveSessionUp/MoveSessionDownswapped with the immediate slice neighbor, but the render path re-buckets sub-sessions under their parents, so a swap with a non-sibling produced zero visible change. Fixed by walking the slice for the previous/next session with the sameParentSessionID(top-level peers treat each other as siblings, sub-sessions reorder among same-parent siblings only). One key press now always produces a visible move when one is possible. Tests ininternal/session/groups_test.gocover the interleaving case, the first-sibling no-op, and the top-level-skips-subs case. - Help overlay (
?) had broken column alignment when descriptions were longer than the description column.dialogWidthdefaulted to 48, leaving ~26-32 chars for descriptions; anything longer wrapped at column 0 and shredded the two-column layout (key | description). Fixed by raisingdialogWidthto 70 by default (scaling to 80 on terminals ≥ 100 cols, shrinking only on narrow terminals) and adding awrapWithHangingIndenthelper so any description that still doesn't fit wraps with continuation lines aligned under the description column. The four worst offenders ("Next / prev session in current group", "Jump to Nth session in current group", "First / last session in current group", "Filter search scoped to current group") were also shortened (current group→group), as were "Edit session settings (title/color/notes/command/...)" and "Quick approve (send '1' to Claude session)". Test coverage forwrapWithHangingIndentininternal/ui/help_test.go.
[1.7.78] - 2026-05-01
P0 hotfix for a data-loss bug in submodule worktree handling.
Fixed
- 🚨 DATA LOSS: deleting a session whose worktree resolved to a submodule's gitdir destroyed the submodule's git data (PR #844, thanks @plutohan for the catch and the fix).
git worktree list --porcelainfrom inside a plain submodule reports the gitdir (<super>/.git/modules/<name>) as the worktree path for the main checkout, not the actual<super>/<name>working tree. Three flows (agent-deck add -w,agent-deck launch -w, TUI new-session with worktree enabled) consumed that path asInstance.ProjectPathand the tmux-ccwd. Sessions then dropped users inside the gitdir where source files don't exist, and worse — deleting the session viasession remove --forceinvokedRemoveWorktree(force=true), whose force-fallback calledos.RemoveAll(worktreePath), destroying the submodule's git history. Reproduced in the reporter's environment. Two-layer fix: (1) prevention —parseWorktreeListnormalizes each non-bare entry throughgit rev-parse --show-toplevel, returning the actual working tree even when invoked from inside a gitdir; all three call sites and any future caller ofListWorktrees/GetWorktreeForBranchget the correct path; (2) defense-in-depth —RemoveWorktreerefuses theos.RemoveAllfallback when the target path is structurally a git directory (.gitbasename,.git/modules/<sub>,.git/worktrees/<wt>, or bare repo viaIsBareRepo). This catches stale session rows persisted before the prevention fix — they now error on delete instead of nuking git internals. 4 new tests cover the data-loss regression gate, the prevention invariant, and the defense-in-depth coverage of all gitdir-shaped paths. Out-of-scope follow-up flagged by reporter: TUI fork-with-reused-worktree path atinternal/ui/home.go:8441updatesWorktreePathbut notWorkDir, leaving fork sessions with the originally-generated path as cwd — unrelated to submodules, separate concern.
If you create sessions in submodules, upgrade to v1.7.78 immediately — the prevention fix stops new sessions from getting the broken path; the defense-in-depth catches existing stale session rows on delete.
[1.7.77] - 2026-05-01
Hotfix re-cut of v1.7.76. The v1.7.76 tag exists on the repo but no binaries were ever published — release CI failed on a chunked-read edge case in the SS3 reader added in #840 (rebased from #815). v1.7.77 contains all of v1.7.76 plus the chunked-read fix.
Fixed
csiuReaderflushed lone ESC at chunk boundary, breaking SS3 detection across split reads (PR #842). When\x1barrived in one Read() chunk andOHin the next, the translator emitted ESC immediately and treatedOHas plain bytes, skipping the SS3 → CSI Home rewrite. Fix mirrors the existing ESC-O-at-buffer-end pattern: when not final, buffer the lone ESC and wait for the next chunk; on final flush emit ESC as-is to preserve standalone-escape semantics.TestCSIuReader_SS3HomeEnd_ChunkedRead/SS3_Home_split_between_ESC_and_OH(added in #840) now passes — locked the regression that blocked v1.7.76's release CI.
(All v1.7.76 entries below carry forward unchanged — see "1.7.76" section for the polish + community-bug bundle that this release re-cuts.)
[1.7.76] - 2026-05-01
Polish + community-bug bundle the day after v1.7.75. Three contributor fixes (Hristo, AdamiecRadek diagnosis, strofimovsky), three WebUI bug fixes from JMBattista combined into one PR, and a v1.7.74 follow-up I caught during production verification. All of #783/#784 + the busy-retry follow-up went through Claude+Codex dual-review (with codex trust pre-registration so peer review actually worked this time). Dual-review pipeline notes: codex-as-sibling topology works; codex-as-child-of-claude-worker hits sub-of-sub spawn limits.
Fixed
-
Delete confirmation dialog focus trap broken; Enter re-fired the action (thanks @JMBattista for issue #784). The HTML
autofocusattribute on the Cancel button was unreliable in Preact when the dialog re-rendered into an existing tree, so focus stayed on the row's Delete button — pressing Enter to "confirm cancel" instead re-triggered Delete (or worse, double-acted). Fix replacesautofocuswithuseRef+useEffect(() => ref.current.focus(), []), addsrole="dialog"+aria-modal="true"for a11y, and wires Esc keydown on the panel (Esc dismissal inuseKeyboardNav.jspreserved). -
Hover toolbar overlapped tool/cost labels in session list (thanks @JMBattista for issue #783). The absolute-positioned action toolbar (
absolute right-2) covered the inline tool label rendered in flow when hovering a row. Fix introduces a singletoolbarVisiblepredicate and appliesinvisible(preserves layout, hides paint) to the metadata spans when toolbar is showing. -
Disconnected sessions showed raw error tokens instead of actionable UX (thanks @JMBattista for issue #782). When a session's tmux died, the WebUI rendered
[error:TMUX_SESSION_NOT_FOUND]repeatedly with no recovery path. Fix adds aHintfield towsServerMessagewith actionable copy on the error, stops the reconnect loop on that code, and renders a single fatal banner with a "Restart session" button that callsPOST /api/sessions/:id/restart. Codex peer review caught a missingreconnectKeystate — added to force terminal re-init after restart. Now reachable end-to-end since web mutations were re-enabled in v1.7.75 (#785). -
Session Analytics context bar showed wrong percentage for
claude-opus-4-7(thanks @AdamiecRadek for diagnosing issue #836). TheContext [bar] N%gauge rendered ~5x too high for opus-4-7 because the model→context-window prefix table ininternal/session/analytics.gowas missing the 4-7 entry, falling through to the 200K default. Concrete impact: 145K used → bar read 72.6% instead of correct 14.5%. Fix adds{"claude-opus-4-7", 1000000}placed before the 4.x fallback (table walk is order-sensitive) and extendsTestContextWindowForModelwith two 4-7 cases. AdamiecRadek's third clean model-spec data catch in this cycle (after #813 pricing and #818 templated cost line). -
TUI
New Sessiondialog had nocopilotpreset; typingcopilotcreated a shell session (thanks @Hristo Dinkov for PR #835). Same class of oversight aspi(fixed in v1.7.32 via #674): copilot was added as a first-class tool in v1.7.26 but two TUI call sites were missed. Fix addscopilotto bothcreateSessionTool's switch andbuildPresetCommands' preset list, with regression tests for both. -
scheduleBusyRetry's success path didn't terminate fingerprint, causing repeatedsentre-fires (#824 follow-up). Caught during v1.7.74 production verification: a child sitting inwaitingwhile parent was busy would defer-retry, eventually succeed, but the queue still re-fired the same fingerprint up to 5 times because v1.7.74'smarkTerminatedonly ran on exhaustion (give up), not on success. Concrete evidence: child384aa29chad 5×deferred_target_busy+ 5×sentrecords all at the same timestamp. Fix adds 1-linen.markTerminated(event)to the success branch + newTestQueue_SuccessfulRetryMarksTerminatedregression. Codex peer agreed SAFE_TO_MERGE under sibling-topology.
Added
-
Terminal navigation keys in session list (thanks @strofimovsky for PR #815 → #840). Session list now accepts
Home/End(jump to first / last item) andPgUp/PgDn(half-page aliases of existingCtrl+U/Ctrl+D). Fills a gap where no single-key jump-to-bottom existed sinceGopens global search.Home/Endalso scroll the help overlay. Follows the same side-effect contract as the pagination handlers (preview scroll reset, navigation-activity mark, debounced preview fetch). PR was rebased onto current main (CHANGELOG conflict against v1.7.74/75 entries) preserving original authorship. -
iTerm2 SS3 Home/End fix for direct SSH (companion to #840). iTerm2's default macOS profile emits Home/End as SS3 application-mode sequences (
ESC OH/ESC OF) on direct SSH. Bubble Tea's decoder covers xterm/vt220/urxvt variants but not SS3 —csiuReader.translatenow rewritesESC OH→ESC [HandESC OF→ESC [Fbefore bytes reach Bubble Tea. All otherESC O*sequences pass through unchanged. Verified unchanged for iTerm2 → SSH → Screen path. -
Tailscale recommendation for reaching services in remote sessions (PR #832). New section in README's Remote Sessions docs explaining why agent-deck does not ship native SSH
-L/-Rport forwarding: Tailscale solves the same problem (reach a service on the remote box from your laptop) more robustly with no per-session config and noControlMasteredge cases. Closes the documentation gap left by the maintainer-decline of #800/#792.
[1.7.75] - 2026-04-30
Community quality-of-life bundle. Four contributor PRs landing the day after the v1.7.74 hotfix: regression fix for web mutations broken since v1.7.71, an SSH start-failure cleanup compensation, an add ergonomics fix for SSH-piped paths, and a configurable cost status-line. All four were dual-reviewed (Claude + Codex peer reviewer) before merge — first run of the dual-model review pipeline.
Fixed
-
Web mutations disabled by default since v1.7.71; restart/delete buttons returned 403/503 (thanks @JMBattista for issue #781 → PR #785). Two compounding bugs:
WebMutationsdefaulted tofalsein the config struct, andbuildWebServernever calledSetMutatorso even an explicit-true config did nothing. Fix flips the default back totrue(matching pre-#519 behavior), wiresui.NewWebMutator(homeModel)into the onlybuildWebServercall site inmain.go, adds*boolTOML pattern to distinguish "absent" from "explicit false", and ships 6 regression tests includingTestBuildWebServer_WiresMutator+HasMutator()to lock against re-introduction.--read-onlyCLI flag still forces mutations off; loopback-only listener and existing Token gate unchanged. -
agent-deck addfailed on~and$VARin positional path arg over SSH (issue #820, thanks @paskal for PR #821). Interactive shells expand~before exec, butssh user@host "agent-deck add ~"passes the literal tilde —filepath.Absthen treated~as a literal directory name. Fix routes the positional path through the existingsession.ExpandPathhelper (correctly orders env-var expansion → tilde expansion). The.shortcut still fast-paths toos.Getwd()so error semantics on cwd-failure are preserved. Table-driven regression test covers.,~,~/foo,$HOME,$HOME/bar, absolute, and relative paths. -
Orphan remote session row when SSH
session startfails (thanks @paskal for PR #822). The two-stepadd+session startSSH flow could leave a session row in agent-deck's state.db when the start failed (e.g. flaky network), with no way to retry. Fix adds a compensation step inCreateSession's start-failure branch that callsDeleteSessionwith a freshcontext.Background()+ 10s timeout — correct choice since upstreamctxcancellation is often what caused the start failure. Best-effort (_ = DeleteSession) so a still-broken network doesn't surface two errors at once. Two new tests via arunFninjection cover both failure and happy paths without restructuring production code.
Added
- Configurable status-line cost template (#818 → #819, thanks @AdamiecRadek). The home status-bar cost segment is now driven by
[costs].cost_line_templatewith optional per-profile override at[profiles.<name>.costs].cost_line_template. Seven cost variables supported:{cost_today},{cost_yesterday},{cost_this_week},{cost_last_week},{cost_this_month},{cost_last_month},{cost_projected}. Unknown placeholders pass through literally.cost_line_hide_when_zero(default true) preserves the prior auto-hide behavior. NewStore.TotalYesterday,TotalLastWeek,TotalLastMonthhelpers underpin the new variables. 42 new tests cover boundary cases, nil/empty config, profile-vs-global resolution chain, and TOML round-trip.
[1.7.74] - 2026-04-30
Hotfix bundle for two notify-daemon regressions that surfaced during v1.7.73 production verification on the maintainer host. Both fixes ship together because the SQLite leak masked the dedup behavior — without the leak fix, the daemon wedged before the dedup test could complete.
Fixed
-
notify-daemonleaked one SQLite connection per dispatch + per queue-drain (~34/min, wedges in hours) (#827 → #828). Two call sites ininternal/session/transition_notifier.goopened a freshStorageviaNewStorageWithProfileper invocation but never closed it:prepareDispatch(every event) andliveTargetAvailability(every queue drain). Forensic evidence on the maintainer host: 2h40min uptime daemon held 1117 open FDs tostate.dbplus 1117 tostate.db-wal, stack stuck infutex_do_waitfrom accumulated WAL/mutex contention, transition log silent for 38 minutes despite live activity. Fix addsdefer storage.Close()at both sites — 2 lines of source change. Two newTest*_NoFDLeakregression tests assert FD count stays flat across N synthetic dispatches (RED on parent: delta=400; GREEN on fix: delta≈0). This is the actual root cause behind several "events stop arriving after a while" reports — the v1.7.73 dedup work in #807 reduced the symptom (duplicate spam) but the underlying daemon wedge remained until this fix. -
Inbox + missed-log emitted duplicate entries; top-level conductor self-suppress missed empty-parent case; exhausted busy-retries re-fired indefinitely (#824 → #825). Three sub-bugs in v1.7.73's #807 inbox+retry pipeline, all surfaced by multi-conductor production audit: (1) the same fingerprint (sha256 of child_id|from|to|timestamp_unixnano) could be written 13× to one inbox file, (2)
prepareDispatch's self-suppress only matchedparent==selfbut real top-level conductors also have emptyparent_session_idplus aconductor-title prefix, so they kept firingdropped_no_targetevents back to themselves, (3)scheduleBusyRetry's exhaustion path logged tonotifier-missed.logbut did not remove the event from the deferred queue, so the queue re-fired the same event indefinitely (notifier-missed.log captured 7 re-fires of the same child in 16 seconds during the audit). Fix introduces anEventFingerprinthelper, a process-localmissedSeendedup map, aterminatedFingerprintsset withmarkTerminated/isTerminated, an early-return on terminated events inEnqueueDeferred, exhaustion-path queue eviction inscheduleBusyRetry, and a new self-suppress branch inprepareDispatchkeyed onchild.TitlestartsWithconductor-AND emptychild.ParentSessionID(orphan WARN no longer fires for the root). Seven new regression tests, all RED-then-GREEN under strict TDD.
Added
- Multi-conductor event-delivery regression harness (#826).
tests/eval/scripts/multi_conductor_event_delivery_test.shplus a Go wrapper atinternal/session/multi_conductor_delivery_test.go(build-tag-gated//go:build multi_conductor) enumerate every conductor on the host (regex^conductor-|^agent-deck$), spawn a disposable child in each conductor's group, drive arunning → waitingtransition, then audittransition-notifier.log+ per-conductor inbox +notifier-missed.logfor the four contracts from #824: adelivery_result=sentrecord exists, the same fingerprint appears exactly once in the log, the conductor's inbox holds at most one entry per fingerprint, and the missed log holds at most one re-fire entry per child. Output: per-conductor PASS/FAIL markdown report undertests/eval/reports/. Skips cleanly when zero conductors are present (CI default). Caught the original-parentflag misuse in its own first run.
[1.7.73] - 2026-04-30
Resilience pass. Nine community-and-internal PRs addressing real user-impacting bugs across event delivery, perf, hooks, headless contexts, defensive timeouts, and pricing accuracy. Five external contributors merged this cycle: @vedantdshetty (5 PRs), @amkopyt, @AdamiecRadek, @strofimovsky, plus internal fixes.
Fixed
-
Transition-notifier silently dropped 97-98% of child-session events (#805 → #807). Two underlying causes: orphan-on-creation children (empty
parent_session_idfrom env-var drop in worktrees, sandboxes, watchdogs) and deferred-busy events that didn't retry reliably. Fix introduces per-conductor inbox file at~/.agent-deck/inboxes/<parent-session-id>.jsonl, retry-with-backoff on busy (5s / 15s / 45s), top-level conductor self-suppress, orphan WARN once per child, and a newagent-deck inbox <session>CLI subcommand. Co-discovered by conductor-innotrade and conductor-agent-deck independently observing the same forensic picture. -
UpdateStatusheldi.mu.Lock()across the opencode CLI subprocess, freezing the TUI 10-15s during navigation (thanks @strofimovsky for PR #801). Write-preferringsync.RWMutexstarved render-path RLocks while a multi-secondopencode session listran under the held lock. Fix releasesi.muaround the call (callee self-manages), bounds the subprocess with a 5s context deadline pluscmd.WaitDelay = 500msso a Node-style child holding stdout open can't extendcmd.Output()past the deadline. Verified with a 60s navigation harness against 28 opencode sessions: lock-held went from 5-7s per event to 0ms across 21/21 events. -
PermissionRequesthook silently denied filesystem operations in/remote-controland other headless contexts (thanks @vedantdshetty for PR #808). agent-deck registered the hook asAsync: truebut the handler is a status tracker, not a permission decider. In TUI sessions Claude Code's UI prompts the user; in headless contexts there's no UI fallback, so silence defaulted to deny —ls /mnt/c/...and other filesystem operations failed with no surfaced prompt. Fix flipsPermissionRequesttoAsync: falseso Claude Code consults the hook's stdout for a decision, and emits an explicitpermissionDecision: allowwhen the parent process was launched with--dangerously-skip-permissions(DSP, the canonical signal of pre-authorized headless work). -
tmuxExecandtmuxExecContextcould hang indefinitely on orphaned stdio (thanks @vedantdshetty for PR #809). Withoutcmd.WaitDelay, a tmux subprocess that orphans stdio causes Go'scmd.Output()I/O goroutines to block forever. Codebase already usedWaitDelayininternal/git/setup.go,internal/mcppool/socket_proxy.go, andinternal/mcppool/http_server.go; this was the missed wrapper at the tmux boundary. Defensive fix. -
queryCodexSessionblocked indefinitely when the FS layer stalled (thanks @vedantdshetty for PR #810).filepath.WalkDirover~/.codex/sessionsblocked indefinitely when the underlying FS layer stalled — observed 2026-04-28 with a WSL kernel D-state on a stuck dentry (one thread held a fd whose dentry sat ind_alloc_parallel). Everyagent-deckCLI command that transitively called the function hung along with it. Fix wraps the walk in a 5s context deadline via a smallrunWithTimeouthelper; on timeout, log WARN and return empty. -
Hook config could become stale on agent-deck binary upgrade (thanks @vedantdshetty for PR #811).
hooksAlreadyInstalledonly checked command presence, not theAsyncandMatcherfields, so a binary upgrade that flippedAsync(as #808 did) would leave the user'ssettings.jsonstuck on the old broken config. Fix verifies the full hook record againsthookEventConfigs(the source of truth in code) and updates on mismatch. Without this follow-up, #808 only reaches fresh installs. -
tool_dataSQLite column silently wiped manually-set keys on save (thanks @vedantdshetty for PR #817). The save pathINSERT OR REPLACEdtool_datawholesale from the typed Go schema, dropping any keys not modeled bytoolDataBlob(canonical case:clear_on_compact, set via direct SQLite UPDATE per harness convention). Surfaced when hub-orch sessions kept re-firing/clear-on-compact. Fix preserves unknown keys via a read-before-write merge inSaveInstanceandSaveInstances; pre-fetch happens outside the write transaction in the batch path to avoid SQLite WAL contention. -
Bare ESC keypress lost in tmux attach quarantine; ESC followed by arrow arrived as Alt+Up (thanks @amkopyt for PR #812).
internal/termreply/filter.gosetpendingEsc = trueon ESC and emitted nothing, waiting for the next byte to disambiguate CSI / SS3 / OSC. Real keyboard ESC has no follow-up byte, so the press stayed buffered indefinitely and later concatenated with the next keystroke's encoding. User-visible symptoms in Claude Code: bare ESC (interrupt) didn't fire, ESC ESC (jump-to-previous-message) didn't work, arrow keys appeared to reset the input. Fix flushes the lone ESC after a short timeout so it reaches the inner agent. -
Outdated Anthropic pricing data + missing entry for
claude-opus-4-7(thanks @AdamiecRadek for issue #813 → PR #814).claude-opus-4-6was using legacy Opus 4 / 4.1 rates (3× the actual current rate, over-attributing every Opus 4.6 token),claude-haiku-4-5was at 80% of the published rates, andclaude-opus-4-7was missing entirely (1240+ cost-event rows in the wild persisted at $0). Cost dashboard accuracy was wrong in both directions. Fix corrects all three plus adds a newagent-deck costs recomputeCLI subcommand that recalculatescost_microdollarsfor everycost_eventsrow using current pricing data (idempotent; supports--dry-run).
Added
- Terminal navigation keys in session list. Session list now accepts
Home/End(jump to first / last item) andPgUp/PgDn(half-page aliases of existingCtrl+U/Ctrl+D). Fills a gap where no single-key jump-to-bottom existed, sinceGopens global search.Home/Endalso scroll the help overlay. Follows the same side-effect contract as the pagination handlers (preview scroll reset, navigation-activity mark, debounced preview fetch).
Fixed
- Home/End keys in TUI now work for iTerm2 over direct SSH. iTerm2's
default macOS profile emits Home/End as SS3 application-mode sequences
(
ESC OH/ESC OF) on direct SSH (no intermediate tmux or screen). Bubble Tea's decoder covers the xterm, vt220, and urxvt Home/End variants but not SS3 —csiuReader.translatenow rewritesESC OHtoESC [HandESC OFtoESC [Fbefore bytes reach Bubble Tea. All otherESC O*sequences pass through unchanged. Verified unchanged for iTerm2 → SSH → Screen (already emitted vt220ESC [1~/ESC [4~).
[1.7.72] - 2026-04-28
Bundle of fixes and contributor PRs, hours after v1.7.71. Two external contributors merged this cycle: @tarekrached (twice), @oryaacov.
Fixed
-
Worktree-setup script honors shebang (#773, thanks @Clindbergh for the report). Setup script with executable bit + shebang line (e.g.
#!/usr/bin/env zsh) now runs under the declared interpreter. Legacy 0644 files fall back tosh -efor backward compatibility. -
Setup script visible completion + failure status (#768). Adds visible "completed in
" / "failed after " lines around the existing setup-script preamble so users know if their hook ran successfully before claude takes over. -
ControlPipe.Close()softened to SIGTERM+grace (#739 gap, thanks @tarekrached for PR #778). Mirrors the v1.7.68softKillProcesspattern for the active-pipe close path. Prevents the same kill-cascade class for users whose terminals trigger control-pipe lifecycle quickly.
Added
-
Copy preview pane info via
C/Shift+C(#791). Yank Repo / Path / Branch from the right-pane preview using the existing clipboard fallback chain (native + OSC 52 for SSH'd terminals). -
Native iTerm2 badge sync on attach + rename (thanks @tarekrached for PR #777). Three-gate no-op design correctly contains escape sequences; default opt-in; thorough tests. Gracefully no-ops on non-iTerm2 terminals.
-
Arrow-key navigation for path suggestions in new-session dialog (thanks @oryaacov for PR #772). Adds keyboard-only path picker with custom-path entry alongside the existing typed input.
[1.7.71] - 2026-04-28
Single-issue hotfix-class release. One day after v1.7.70.
Fixed
session set-parentno longer silently moves the child's group (#786). Until now, post-hoc linking a session under a parent rewrote the child'sgroupfield to match the parent's, whileunset-parentonly clearedparent_session_id— an asymmetric footgun for the retroactive-relink workflow (re-attaching orphan sessions to a conductor for event routing) which silently scrambled the TUI tree and lost the original group with no audit trail.set-parentis now strictly parent-only by default. Use--inherit-groupto opt back in to the prior behavior. Implicit group inheritance for newly launched sessions viaadd/launchis unchanged. Locked in by five regression tests incmd/agent-deck/setparent_group_test.go(no-inherit default, opt-in works, unset leaves group alone, full round-trip preserves group, --help mentions the flag).
[1.7.70] - 2026-04-27
Bundle of community-contributed fixes plus a P1 macOS regression repair, four days after v1.7.69. Three external contributors merged this cycle: @lucassaldanha, @vedantdshetty, @amkopyt. Plus @petitcl's remote-docs PR.
Fixed
-
P1 — Worker-scratch
CLAUDE_CONFIG_DIRno longer breaks per-groupconfig_diron hosts with no Telegram conductor (#759, thanks @lucassaldanha for PR #760). v1.7.68's #732 added a worker-scratch indirection that fired regardless of whether a Telegram conductor was actually configured. On macOS, where Claude Code keys OAuth credentials by literal CLAUDE_CONFIG_DIR path, this silently broke per-group account isolation — sessions fell back to default~/.claudeinstead of configured per-group dirs. Fix narrows the predicate to additionally require an active Telegram conductor token; #732's protection unchanged on hosts that need it. Closes the regression godlen4332 hit at #766. -
Codex
resume <sid>death loop after a stale rollout (#756, thanks @vedantdshetty for PR #758). When a Codex process died before its session rollout JSONL flushed (tmux crash, kill in the SessionStart→first-flush window), the captured session_id was permanently unresumable. agent-deck's spawn path appendedcodex resume <stale-uuid>on every restart, Codex exited immediately, infinite loop.buildCodexCommandnow globs$CODEX_HOME/sessions/*/*/*/rollout-*-<sid>.jsonlbefore adding the resume argv; on miss, it logscodex_resume_stale_sid_dropped, clears in-memory state, clears the.sidsidecar — self-heals on the first restart. -
Setup script not running when worktree creation uses
.barerepo path (#742, @Clindbergh). Three TUI sites ininternal/ui/home.go(new-session-with-worktree, fork-with-worktree, multi-repo new-session) used the narrowgit.IsGitRepo()check; #715's bare-repo support requiresgit.IsGitRepoOrBareProjectRoot(). Drop-in swap at all three. Structural testTestRegression742_HomeWorktreeGuardsAcceptBareProjectRootgrep-assertshome.gocontains zero uses of the narrow check. -
Start queryfield in new-session dialog no longer prefills with previous invocation's value (#741, @Clindbergh). TUI state leak:ShowInGroupcleared every input exceptstartQueryInput— the new field added for #725 was missed in the clear loop. Fix addsClaudeOptionsPanel.ResetStartQuery()next toclaudeOptions.Blur(). -
Sessions on isolated tmux sockets are no longer permanently reported as
error(#755, @vedantdshetty). When a user configured[tmux].socket_name(or per-conductor sockets), any session living on that non-default socket showed up aserrorinagent-deck session show --json,list --json,status --json, the TUI status column, and the web dashboard — and a manualUPDATE instances SET status='waiting'in SQLite was overwritten on the very next poll. The reviver path (added with v1.7.50 socket-isolation work) was already socket-aware viatmux.HasSessionOnSocket, but the status-derive path wasn't:Session.Exists()short-circuits on a process-wide cache populated byRefreshSessionCache, which only queriesDefaultSocketName(). A session whoseSocketNamediffers from the default was either absent from the cache (false negative →StatusError) or aliased with a same-named default-socket session (false positive). Fix gates the cache lookup onstrings.TrimSpace(s.SocketName) == DefaultSocketName()ininternal/tmux/tmux.go:Exists; mismatched-socket sessions fall through to the existing socket-awares.tmuxCmd("has-session", -t, name)direct probe (which already injects-L <name>viaSession.tmuxCmd). One-line gate, no new abstractions; the cache fast path is preserved verbatim for the default-socket path so the per-tick subprocess-cost reduction from the cache (its original purpose) is unchanged. Two RED-first regression tests ininternal/tmux/exists_socket_test.go:TestSession_Exists_DoesNotTrustDefaultCacheForNonDefaultSocket(the false-positive path, runs without a real tmux server) andTestSession_Exists_DefaultSocketStillUsesCache(pins the backwards-compat fast path so future changes can't degrade default-socket sessions to a fresh subprocess perExists()call).
Added
- Per-session Claude Code plugin attach via
--plugin <name>(RFCdocs/rfc/PLUGIN_ATTACH.md). New CLI flag onagent-deck addandagent-deck launchenables a Claude Code plugin from a curated catalog ([plugins.<name>]in~/.agent-deck/config.toml) for one session only — without contaminating the global~/.claude/settings.json. Catalog entries declarename,source(e.g.nyldn/claude-octopusorclaude-plugins-official), optionalemits_channel(auto-link to inbound delivery via--channels),auto_install(shell-out toclaude plugin install <id>if missing), anddescription. The new fieldInstance.Plugins []stringpersists catalog short names and round-trips through state.db (statedb.MarshalToolData/UnmarshalToolData), so a session restart re-applies enabledPlugins on the next spawn. Six surfaces wired together: (1) catalogPluginDefininternal/session/userconfig.gowithGetAvailablePlugins/Names/Defaccessors filtering the v1-refusedtelegram@claude-plugins-official(§6); (2) writer extension ininternal/session/worker_scratch.gogeneralizing the v1.7.68 telegram-only mutation into a deny+allow overlay (allow wins on key collision per RFC §4.3) plus a newneedsScratchForExplicitPluginsgate so plugin-driven scratches fire on hosts without a TG conductor while preserving the issue #759 macOS narrowing for non-plugin sessions; (3) mutator branchFieldPlugins(claude-only, restart-required, catalog-validated) ininternal/session/mutators.gowith usageagent-deck session set <id> plugins <csv>; (4) auto-install ininternal/session/plugin_install.gowith per-(source, name) flock under~/.agent-deck/locks/, idempotent<source>/plugins/<source>/<name>/existence check, best-effortclaude plugin marketplace add+claude plugin installrunning against the source profile (not scratch) so installs are global per profile while enablement stays per-session; (5) channel auto-link ininternal/session/plugin_channels.go— catalog entries withemits_channel = trueautomatically populateInstance.Channelswithplugin:<id>so claude registers the inboundnotifications/claude/channelhandler (without it the plugin loads as a plain MCP and silently drops inbound messages); opt-out via--no-channel-linkflag persisted asInstance.PluginChannelLinkDisabled; (6) Edit Session dialog field for live runtime edits (CSV text input matching the ExtraArgs shape; full multi-checkbox widget deferred to v1.1). v1 explicitly refuses--plugin telegram@claude-plugins-officialat three layers (CLI flag validator, mutator, catalog accessors) with a pointer to--channels— full Telegram retrofit onto the deny-list-minus-opt-ins machinery is deferred to a separatePLUGIN_TELEGRAM_RETROFIT.mdRFC. macOSCLAUDE_CONFIG_DIR-keyed OAuth (#759) gets a one-shot loud warning per source profile via~/.agent-deck/macos-plugin-warning-state.json. Tests: 9 catalog round-trip + persistence + accessors; 13 worker-scratch deny+allow + macOS warning; 8 mutator branch + telegram refusal + restart policy; 7 auto-install with stubbed exec + lock semantics; 6 channel auto-link idempotency + add/remove +--no-channel-link; 7 CLI validation; 5 Edit Session dialog visibility + initial value; +1TestMarshalUnmarshalToolData_Pluginsstate.db round-trip — 56 new tests, all green.
Changed
- Edit Session dialog redesigned to match New Session conventions, with auto-restart on save. Follow-up to the in-TUI editor below. Title-locked / no-transition-notify / wrapper / channels / color / notes / command are dropped from the dialog and stay settable via
agent-deck session set <field>— the dialog focuses on the values users actually iterate on at runtime. The slim set: Title (live), Tool (←/→ pill picker matchingNew Session's preset list), plus three claude-only fields (Skip permissions / Auto mode checkboxes + Extra args text input). Skip / Auto surface and persistClaudeOptions.{SkipPermissions, AutoMode}fromInstance.ToolOptionsJSON, which previously had no edit surface outside the new-session options panel. Two new SetField branches (FieldSkipPermissions,FieldAutoMode) round-trip those bools through the JSON blob viaUnmarshalClaudeOptions/MarshalToolOptions, initialize an empty wrapper for legacy sessions whoseToolOptionsJSONis nil, and reject the fields on non-claude tools. The previous "saved — press R to restart" hint is replaced by auto-restart on Enter: when any restart-required field changes (Tool / Skip / Auto / Extra args) the dialog handler now callsh.restartSession(inst)directly, mirroring theRkeybind. Auto-restart is suppressed when an animation is already in flight (hasActiveAnimation— concurrent restart would race) or when the session can't be restarted (!CanRestart()— stopped sessions just persist edits and apply on next manual start). TheFieldToolbranch inSetFieldalso clears staleClaudeOptionsfromToolOptionsJSONwhen leaving a claude-compatible tool, so aclaude→shellswitch with Skip toggled in the same submit can't resurrect ghost flags on a futureshell→claudeswitch (TestSetField_Tool_ClearsClaudeOptionsOnLeaveClaude). Header now readsEdit Session+in group: <name>(purple) +session: <title>(dim) for visual parity with the new-session dialog. Checkbox rendering uses the sharedrenderCheckboxLinehelper so the row reads▶ [x] Skip permissionslike the New Session options panel. The stale-custom-tool footgun caught by Sonnet code review (custom tool removed from config → cursor lands on slot 0 → save without edit silently rewrites Tool to "") is fixed bytoolPillsForInstanceappending the unknown tool to the pill list and pinning the cursor on it; regression testNoSpuriousToolWipeForStaleCustomlocks the no-op contract. Other tests pinning the contract:TestEditSessionDialog_GetChanges_{SkipPermissionsToggle,AutoModeToggle},TestEditSessionDialog_NoClaudeFlagsForShellTool,TestSetField_SkipPermissions_{InitializesEmptyToolOptions,ClaudeOnly},TestSetField_AutoMode_PreservesSkipPermissions,TestSetField_Tool_NoopForSameClaude.
Added
- In-TUI editor for session settings (
P/Shift+Photkey). [as below — same entry preserved] - Vulnerable-tmux startup warning (S14 follow-up, #750). Prints a one-line stderr warning at agent-deck startup when the running tmux's version predates the upstream NULL-deref fix (commits
881bec95,e5a2a25f,31c93c48). Suppressible viaAGENTDECK_SUPPRESS_TMUX_WARNING=1. Helps users on macOS know to upgrade once Homebrew ships a patched tmux. - Remote subcommand fully documented in README,
--help, and CLI reference (#751, thanks @petitcl for PR #752). Was previously only accessible to users who discovered it accidentally.
Chore
.planning/directory removed entirely (#763). 118 files of internal maintainer scratch space (milestones, roadmap, retrospective, per-phase plans). Already gitignored per #740, this cleans up the existing tracked files. No code impact.- Documentation note clarifying that
[tmux].socket_nameisolation does NOT prevent agent-deck-on-its-own-server crashes from the upstream tmux NULL-deref (S14 follow-up). README updated.
Edit Session dialog (full entry)
- In-TUI editor for session settings (
P/Shift+Photkey). Adds a Bubble Tea dialog that lets users edit a running session's title, color, notes, command, wrapper, tool, channels, extra-args, title-locked, and no-transition-notify in place — previously this required dropping out of the TUI to runagent-deck session set <id> <field> <value>per field, and the boolean fields (title-locked,no-transition-notify) had no in-TUI surface at all. Live fields (title / color / notes / booleans) take effect immediately on save; restart-required fields (command / wrapper / tool / channels / extra-args) persist and apply on nextR, surfaced via a transientsaved — press R to restarthint. The implementation extracts per-field validation + tmux side effects intosession.SetField(internal/session/mutators.go) so CLI and TUI share one source of truth — the prioragent-deck session setswitch is now a 4-line delegator.SetFieldreturns apostCommit func()for the two fields that need a slow tmux subprocess (claude-session-id/gemini-session-idenv propagation) so the TUI can dropinstancesMubefore invoking it; the dialog doesn't currently expose those fields, but the API stays defensive against future additions. Race-safety: title edits flow throughpendingTitleChanges+invalidatePreviewCacheand persist viaforceSaveInstances(notsaveInstances, which is a no-op whileisReloading=true), mirroring the existing rename-path#697mitigation. Tool changes apply last in the commit loop so claude-only field validation (channels,extra-args) sees the pre-editToolvalue — without this, switchingTool=claude→shellwhile clearingChannelsin one submit would error spuriously on the clear. Tests: 15TestSetField_*unit tests including post-commit nilness invariants, 18 dialog unit tests, 4 eval_smoke cases per theCLAUDE.md:82-108mandate for interactive prompts (internal/ui/edit_session_dialog_eval_test.go), and 4 regression tests + 6 sub-tests for the v1.7.22 / #658 telegram-topology warnings —maybeEmitSessionSetTelegramWarningswas extracted from inline so the conditional is testable, since the gate's regression nearly slipped during the SetField extraction (caught by Sonnet code-review pass before commit). Help overlay (?) updated to surface the new hotkey under SESSIONS.
[1.7.69] - 2026-04-24
Hotfix bundle for five regressions filed against v1.7.68 within 24h of release. Every fix ships with a RED-first regression test; one fix (#744) had to be dropped as not-our-bug after systematic investigation — filter-level passage is verified and guarded, but the reported Shift-to-lowercase behavior lives downstream of agent-deck and needs a repro bundle from the reporter before any shipping change. Per the v1.7.68 maintainer review, release is deliberately un-tagged in this commit — the user tags when ready.
Fixed
-
TUI
nkey no longer creates a local session when the cursor is on a remote group/session (#743, @javierciccarelli). v1.7.68 shipped d9a5de8 ("fix(ui): keep new session on n for remote selections") which deliberately removed the remote early-return fromcase "n":ininternal/ui/home.go, intending to route everyone through the local new-session dialog. But the dialog has no remote awareness, so users in the Remotes section who pressedn, accepted the dialog defaults, and got a session created on localhost instead of on the remote they were browsing. Fix reinstates the pre-d9a5de8 branch verbatim: if the cursor is onItemTypeRemoteGroup/ItemTypeRemoteSession, route intocreateRemoteSession(item.RemoteName)and skip the local dialog entirely.case "N":kept unchanged — both keys now quick-create on remotes, matching the pre-v1.7.68 UX. The two d9a5de8 regression tests (which codified the broken contract) are deleted with comments pointing at the new guardsTestRegression743_NOnRemoteSession_QuickCreatesNoDialogandTestRegression743_NOnRemoteGroup_QuickCreatesNoDialogininternal/ui/home_test.go. -
TUI worktree creation now accepts bare-repo project roots (#742, @Clindbergh). #715 (v1.7.58) introduced
git.IsGitRepoOrBareProjectRootand migrated every CLI worktree-creation call site (launch / add / session add / worktree list) to the broader check so a bare-repo project root (a directory containing nested.bare/but no.git/) flows transparently through worktree flows. Three TUI sites ininternal/ui/home.gowere missed: the new-session-with-worktree guard (~5100), the fork-with-worktree guard (~7379), and the multi-repo new-session guard (~7762). For a bare project root, the first two error out with "Path is not a git repository" (no worktree, no session). The third silently falls through toos.Symlink, skippinggit.CreateWorktreeAND the setup script at<projectRoot>/.agent-deck/worktree-setup.sh— exactly the "setup script not run; non-bare path works" symptom. Fix: drop-in swap togit.IsGitRepoOrBareProjectRootat all three sites. DownstreamGetWorktreeBaseRoot+CreateWorktreeWithSetupalready handle bare layouts (proven by the unchangedinternal/git/bare_repo_test.gosuite). Structural regression guard:TestRegression742_HomeWorktreeGuardsAcceptBareProjectRootininternal/ui/bare_repo_worktree_guards_test.gogrep-asserts thathome.gocontains zero uses of the narrowgit.IsGitRepo(— any future worktree site that sneaks in the narrow check fails at test time instead of at user-report time. -
Forked Claude sessions no longer start empty — fork command survives the Start() resume dispatch (#745, @petitcl).
Instance.Start()andInstance.StartWithMessage()rebuild the claude-compatible command unconditionally based oni.ClaudeSessionID: non-empty →buildClaudeResumeCommand, empty →buildClaudeCommand(i.Command). A fork target hit the worst case for this dispatch —buildClaudeForkCommandForTargetpre-generates a new UUID, assigns it totarget.ClaudeSessionIDfor later tracking, and stashes the real fork command (claude --session-id <new> --resume <parent-id> --fork-session …) intarget.Command. Start() sees a populated ClaudeSessionID, routes tobuildClaudeResumeCommand, which callssessionHasConversationDatafor the brand-new fork UUID, finds no JSONL on disk (it was supposed to be created by the fork command), and falls back to a plain--session-id <forkUUID>— stripping--resume <parent-id>/--fork-sessionand dropping all conversation history from the parent. Fix introduces a transientInstance.IsForkAwaitingStartfield (taggedjson:"-"so a restart does NOT re-emit--fork-sessionand double-count the parent transcript).CreateForkedInstanceWithOptionssets the flag alongsidei.Command = <fork cmd>.StartandStartWithMessagecheck the flag as the FIRST branch inside the claude-compatible switch, runi.Commandverbatim, clear the flag, and emit a grep-auditable"resume: none reason=fork_awaiting_start"session log line. The first-Start-only semantic is load-bearing: a subsequent Restart of the forked session takes the normal resume path (with the persisted ClaudeSessionID now pointing at a real JSONL). Regression coverage via reflection + source probe ininternal/session/fork_start_dispatch_test.go:TestRegression745_ForkTargetCarriesAwaitingStartSentinelasserts four contracts — fork command structure, sentinel presence,json:"-"tag, and thatStart()consults the sentinel BEFOREbuildClaudeResumeCommand. -
agent-deck --select <id>now survives the storage-watcher auto-reload afterlaunch --json(#746, @tarekrached). Classic timing race:launch --jsonwrites the new session to the registry and prints its ID on stdout. TUI is invoked with--select <id>; the firstloadSessionsMsgfires before the storage watcher has observed the new file, soapplyInitialSelectionscansflatItems, doesn't find the target, returns false.pendingCursorRestorerestores the previously-persisted cursor to an adjacent row. The storage watcher eventually notices the mtime bump and enqueues a secondloadSessionsMsgwithrestoreStatepopulated — but the handler's restoreState branch calledh.restoreState(*msg.restoreState)and returned without re-attemptingapplyInitialSelection. The cursor stayed on the adjacent row forever. Fix: addh.applyInitialSelection()to the post-rebuild restoreState branch in theloadSessionsMsghandler, mirroring the existing call in the initial-load branch. The helper is already idempotent (no-ops after a successful match), so normal cursor navigation is not overridden. Regression coverage ininternal/ui/initial_select_retry_test.go:TestRegression746_InitialSelectRetriesOnNextLoad(behavioral — helper is idempotent across flatItems rebuilds), andTestRegression746_LoadSessionsHandlerRetriesInBothBranches(structural — grep-asserts the post-rebuildif msg.restoreState != nil { h.restoreState(...)block containsapplyInitialSelection, anchored precisely so the pre-rebuild re-capture block at the top of the case can't be matched by accident).
Investigated — not our bug
- Shift produces lowercase in remote tmux-split pane on Ghostty/SSH (#744, @javierciccarelli) — BLOCKED / NOT OUR BUG. Hypothesis: the #734 (v1.7.68) termreply whitelist broke CSI u passage. Investigation (
internal/termreply/filter_test.go::TestRegression744_FilterPassesShiftLetterCSIUWhileArmed) proves the filter passes every Shift+letter CSI u encoding tested — xterm\x1b[65;2u, kitty\x1b[97;2u, Shift+Z\x1b[90;2u— unchanged, both as a single chunk AND split across twoConsumecalls while the filter is armed. Final byte'u'is correctly whitelisted inisKeyboardCSIFinalByte(line 94);#734's DA/DSR additions do not affect the keyboard path. The bug is downstream of agent-deck — either (a) Ghostty/tmux modifyOtherKeys negotiation on the remote host, or (b) a Ghostty/tmux combo that sends a different encoding than what we tested. Without a repro bundle (tmux version, tmux.conf, Ghostty terminfo, actual bytes on the wire) from the reporter, shipping a filter change would be speculative — the exact anti-pattern the v1.7.68 maintainer review called out. The test is kept as a proactive guard against any future whitelist tightening.
[1.7.68] - 2026-04-22
Changed
[worktree].setup_timeout_seconds = 0now means "unlimited" instead of "use default" (follow-up to #727, PR review comment from @Clindbergh). v1.7.65 treated a non-positive value as a signal to fall back to the 60s default. Flipped within 2 days of v1.7.65 shipping, before real adoption. New semantic:0= unlimited (no deadline), unset/negative = 60s default, positive N = N seconds. Implementation swapsWorktreeSettings.SetupTimeoutSecondsfromintto*intso TOML parsing distinguishes "field unset" (nil) from "explicit zero" (*0);git.RunWorktreeSetupScriptroutes unlimited throughcontext.WithCancel(context.Background()). Tests ininternal/session/worktree_setup_timeout_zero_unlimited_test.goandinternal/git/setup_unlimited_test.go.
Fixed
-
Rogue telegram pollers no longer spawn when a conductor launches non-conductor claude children on the same host (#59; poller-storm observed 2026-04-22, 6–8 duplicate
bun telegramprocesses running concurrently against the conductor's bot token, producing a Bot API 409 Conflict storm and silently dropping inbound messages). v1.7.40 tried to solve this by strippingTELEGRAM_STATE_DIRfrom every non-conductor spawn, but the Claude Code telegram plugin is enabled globally per the v3 topology, so removingTELEGRAM_STATE_DIRjust makes the plugin fall back to the default state dir — which on a conductor host is the real bot-token directory. v1.7.68 adds a categorically different layer: every non-conductor claude worker now spawns under an ephemeral scratchCLAUDE_CONFIG_DIRthat shallow-mirrors the ambient profile except forsettings.json, which is rewritten withenabledPlugins["telegram@claude-plugins-official"] = false. The plugin never loads, so no opportunity to discover the default state dir. Wired throughInstance.WorkerScratchConfigDir,EnsureWorkerScratchConfigDir,prepareWorkerScratchConfigDirForSpawnat all three spawn paths, and cleaned up onKill/KillAndWait. 6 new tests ininternal/session/worker_scratch_test.go. -
Orphan claude processes no longer survive
agent-deck session remove(same incident 2026-04-22:PID 321456, 33-hour orphan). Two distinct bugs: (1)handleSessionRemoveonly calledinst.Kill()when--prune-worktreewas passed — plainsession remove --forcedeleted the registry row and left the tmux scope + claude child alive forever; (2)Session.Killruns its SIGTERM→SIGKILL escalation in a background goroutine that is aborted when the short-lived CLI exits, so even callers that did invokeKillcould race the CLI exit and leave SIGHUP-immune claude 2.1.27+ children alive. Fixes: newinternal/tmux/ensure_pids_dead.goexportsEnsurePIDsDead(synchronous SIGTERM→SIGKILL primitive) andSession.KillAndWait();Instance.KillAndWait()factors shared teardown throughkillInternal(sync bool);handleSessionRemove,removeAllErrored,pruneSessionWorktree, and legacyhandleRemoveall now callKillAndWaitunconditionally beforestorage.DeleteInstance. 3 new tests including structural guards that parse the command source and assert unconditional-call invariants. -
iTerm2 XTVERSION response leak + Shift+Enter regression (#731 @marekaf, #738 @Clean-Cole — same filter, two failure modes).
internal/termreply.Filternow whitelists DA/DSR CSI replies (final bytesc,n,R) so tmux can negotiate modifyOtherKeys with the host terminal, while DCS/OSC escape-string replies (XTVERSION, OSC 10/11) are stripped unconditionally since they'd corrupt the inner pane. Previously the filter either swallowed everything during the 2s quarantine (breaking modifyOtherKeys → Shift+Enter collapsed to bare CR in iTerm2 default profile) or let DCS through after the window (leakingTERM2 3.6.10nas input on focus/resize). One surgical whitelist fixes both. -
macOS tmux server SIGSEGV triggered by
killStaleControlClients(#737 @tarekrached). Soft kill via SIGTERM + 500ms grace → SIGKILL fallback replaces the prior immediate SIGKILL. Shrinks the race window against an unfixed tmux NULL-deref in the control-mode notify path (tmux/tmux#4916, #4980, #5004 — fixed on master, not in any release tag yet). Stuck clients still reaped within ~500ms, preserving the "stale control clients cannot linger" guarantee.
Added
[tmux].mouseconfig option (#730 @sghiassy) — defaulttrue(preserves current behaviour). Withmouse = false, agent-deck skipsset-option mouse onat both session-create and the reconnect/attachEnableMouseModepaths. Restores native click-drag text selection in VS Code's Linux integrated terminal.scripts/watchdog/promoted to repo-wide (internal #53/#56). Includes telegram-poller liveness check (auto-restart conductor on missing bun poller) + waiting-too-long patrol (auto-nudge idle children). 24 new Python tests.
[1.7.67] - 2026-04-22
Added
- Dedicated "Start query" field in the new-session TUI dialog (#725, reported by @Clindbergh):
claude-codeaccepts a positional startup-query argument (e.g.claude "explain this repo") which seeds the first prompt for a brand-new session. Before v1.7.67 there was no first-class way to pass this through agent-deck, so users reached for the existing "Extra args" field. That produced two interlocking bugs: (a) space-splitting — Extra args runsstrings.Fieldson the raw input, so a multi-word query likeexplain the codebasebecame three separate tokens and claude saw three positional args instead of one prompt; (b) cross-session replay — Extra args is persisted on theInstanceand re-emitted bybuildClaudeExtraFlagson everyStart,Resume,Restart, and fork, so a value intended as a one-time opening prompt kept auto-suggesting itself each time the session came back up. Fix introduces a newStart query:textinput in the Claude Options panel (internal/ui/claudeoptions.go) wired throughNewDialog.GetClaudeStartQuery() → Instance.StartupQueryand appended bybuildClaudeCommandWithMessageas a singleshellescape.Quote-wrapped positional token on the new-session command only. The field is declaredStartupQuery stringjson:"-"`` on theInstancestruct — thejson:"-"tag is load-bearing, it is what makes the value per-session and never persisted. OnRestart/Resumethe field is zero-valued after the SQLite reload sobuildClaudeResumeCommandnever sees it and the query is not replayed. "Extra args" behavior is entirely unchanged: same whitespace-split semantics, same persistence, same--agent reviewer --model opus-style flag pipeline. Tests ininternal/session/startquery_test.go:TestStartCommandAppendsStartupQueryAsSingleArgasserts a multi-word query emits as one shell-quoted token;TestStartCommandOmitsStartupQueryWhenEmptyasserts no stray empty-quoted arg when the field is unset;TestStartupQueryDoesNotPersistToJSONasserts thejson:"-"tag holds (nostartup_queryfield, noStartupQueryfield, no query value string in the marshalled JSON);TestResumeCommandOmitsStartupQueryasserts the resume path does not pick the query up — this is the inverse ofTestResumeCommandAppendsExtraArgsand is the regression guard for @Clindbergh's original complaint;TestStartupQueryCoexistsWithExtraArgsis the extra-args regression test that asserts both features emit together and do not interfere (extra-args tokens still appear as separate flags, start-query still appears as one positional). UI tests ininternal/ui/newdialog_test.go:TestNewDialog_View_ShowsStartQueryField_WhenClaudeSelectedasserts theStart query:label renders when the claude preset is selected;TestNewDialog_GetClaudeStartQuery_ReturnsInputValueasserts the accessor returns the raw un-split string (contract check: returnsstring, NOT[]string). Numbering note: drafted as v1.7.66; renumbered to v1.7.67 because v1.7.66 landed on main during review (feat(launch): verify claude consumed -m prompt, PR #726).
[1.7.66] - 2026-04-22
Fixed
agent-deck launch -m "<prompt>" --no-waitnow verifies claude actually consumed the initial prompt (internal task54-launch-verify-prompt). On cold starts, claude's welcome screen occasionally ate the firstEnter, leaving the-mprompt typed-but-not-submitted in the composer. The session sat instatus=waitingforever with the message visible at❯but no assistant response ever started. Root cause: the launch path's post-start verification budget was 1.2s (sendRetryOptions{maxRetries: 8, checkDelay: 150ms}incmd/agent-deck/launch_cmd.go), far too short to observe and recover from the welcome-screen race on a fresh claude+MCPs cold start. Fix: after the existingsendWithRetryTargetpass, a newverifyPromptConsumedAfterLaunchhelper polls the pane for up to 10s (250msinterval). "Consumed" = composer rendered AND the-mmessage is no longer visible in the input line (send.HasCurrentComposerPrompt && !send.HasUnsentComposerPrompt). If still unconsumed after the first window, it retriessend-keysexactly once; if the second window also shows the prompt unconsumed, it writes a warning toos.Stderrand returns without failing the launch (best-effort, preserving--no-waitspirit). Five unit tests incmd/agent-deck/launch_verify_prompt_test.gocover: consumed-first-poll path (no retry, no warning), unsent-then-consumed-after-retry path (exactly 1 retry, no warning), unsent-both-windows path (1 retry + warning), welcome-screen-no-composer path (no false "consumed" when the composer hasn't rendered yet), and wall-time budget enforcement. All five use synthetic pane strings only, per the sanitization rule. The existingsendWithRetryTargetcall is unchanged — the new helper is a second verification layer, not a replacement. Numbering note: originally drafted as v1.7.64; renumbered forward through the v1.7.63/64/65 queue shift (fix-53-56 + #724 worktree-timeout landed ahead).
[1.7.65] - 2026-04-22
Added
- Configurable worktree-setup-hook timeout via
[worktree].setup_timeout_seconds(#724, reporter: @Clindbergh). The worktree setup script.agent-deck/worktree-setup.shwas previously capped at a hardcoded 60s, which is too tight for real-world setups that install dependencies and seed local databases — users were seeing timeouts on otherwise-healthy scripts. Fix introduces a new integer config knob[worktree].setup_timeout_seconds(default60, preserving prior behaviour for every existing install) that is loaded via the standardLoadUserConfigpath and threaded throughgit.RunWorktreeSetupScript/git.CreateWorktreeWithSetupas an explicittime.Durationparameter. A non-positive value falls back togit.DefaultWorktreeSetupTimeout(60s), so a missing section, a missing field, a0, or a negative integer all behave identically to pre-v1.7.65 and cannot accidentally disable the guard.WorktreeSettings.SetupTimeout()is a value-receiver helper on the existingsession.WorktreeSettingsstruct that returns the resolvedtime.Duration; it's what all fiveCreateWorktreeWithSetupcall sites now pass (two ininternal/ui/home.gofor new-session and fork flows, one each incmd/agent-deck/launch_cmd.go,cmd/agent-deck/session_cmd.go,cmd/agent-deck/main.go). The package-levelworktreeSetupTimeoutvar ininternal/git/setup.gois gone; all timeout control now flows through the function parameter, keeping thegitpackage free of asessionimport. Effective wall-clock for a timed-out script is stillsetup_timeout_seconds + cmd.WaitDelay(5s) before SIGKILL — matches pre-v1.7.65 semantics. Tests:TestWorktreeSettings_SetupTimeoutSeconds_ParsesFromTOML(config round-trip),TestWorktreeSettings_SetupTimeout_DefaultSixtySeconds(zero-value backward compat),TestWorktreeSettings_SetupTimeout_HonoursConfiguredValue(positive value honoured) ininternal/session/worktree_setup_timeout_test.go;TestRunWorktreeSetupScript_HonoursCallerTimeoutininternal/git/setup_timeout_arg_test.go(a 1s caller timeout on asleep 300script must fail in well under the legacy 60s default — proves the parameter is actually threaded, not just declared). ExistingTestRunWorktreeSetupScript_Timeoutrewritten to pass1*time.Seconddirectly rather than mutating the now-deleted package var. Example config:[worktree] setup_timeout_seconds = 300 # bump to 5 minutes for heavier setups
[1.7.62] - 2026-04-22
Added
- Visual update nudge for severely out-of-date installs (conductor task #45). Driver: on 2026-04-22 four users posted Feedback Hub comments from versions 15-39 releases behind head (v1.7.3, v1.7.17, v1.7.23, v1.7.23). They were hitting bugs that had been fixed weeks earlier.
internal/update/update.goalready queried/releases/latest, but the existing banner fired at everyAvailable=trueand — combined with users who had muted settings, or whoseCheckEnabledhad been off since install — never surfaced loudly enough to convince the severely-behind cohort to upgrade. This release splits that signal into two tiers:>5releases behind triggers a new "nudge" banner in the TUI status bar. The banner carries the concrete count (30 releases behind), the current and latest versions, and the dismiss hint (press U to dismiss). The cut-off at 6+ keeps casual users (1–5 behind) out of the noisy path while severely-behind users get the loud signal.agent-deck --versionappends(update available: vX.Y.Z)when the on-disk cache shows the user is behind. This surface is cache-only — the flag never hits the network, so--versionstays instant.
AGENTDECK_SKIP_UPDATE_CHECK=1is a hard kill-switch for every surface: no network call fromCheckForUpdate, no annotation on--version, no TUI banner. Intended for air-gapped / locked-down / CI environments that rely on absolute network silence.- Dismiss key:
shift+Uhides the nudge for the rest of the process. The dismiss flag is session-local (resets on restart), so a user who upgrades out-of-band does not need to re-dismiss; a restart clears the state and the next check re-evaluates. - New public surface in
internal/update:CountReleasesBehind(currentVersion, releases) int,ShouldNudge(info) bool,CachedUpdateInfo(currentVersion) (*UpdateInfo, error)for the offline cache read,NudgeThreshold = 5constant,SkipUpdateCheckEnv = "AGENTDECK_SKIP_UPDATE_CHECK"constant,ReleasesBehind intfield onUpdateInfoandUpdateCache, and a newfetchRecentReleases(limit)helper that pulls/releases?per_page=30to compute the count. - New TUI surface in
internal/ui:Home.shouldRenderUpdateNudge(),Home.handleUpdateNudgeDismiss(msg),Home.renderUpdateNudgeText(), plus aHome.updateNudgeDismissed boolfield. The three banner-height accounting sites inhome.go(getVisibleHeight,getListContentStartY, the main layout render) all go throughshouldRenderUpdateNudge()so a refactor of any one of them cannot drift from the rest. - CLI surface in
cmd/agent-deck/main.go: extractedwriteVersionOutput(w io.Writer, currentVersion string)so the flag dispatch (case "version", "--version", "-v":) writes through anio.Writerthe tests can assert against byte-exactly. The version-output path is otherwise unchanged. - Tests: 6 unit tests in
internal/update/update_nudge_test.go(threshold arithmetic, env-gate short-circuit, cache round-trip, offline cache read, env-gate on cached read), 4 unit tests ininternal/ui/update_nudge_test.go(threshold, dismiss, env-gate, banner text content), 4 unit tests incmd/agent-deck/version_nudge_test.go(annotation on, no-cache, up-to-date, env-gate), and 2 eval-smoke cases intests/eval/session/update_nudge_test.go(real-binary--versionwith a seeded cache; real-binary--versionwith env-gate set). All 9 test-file entries added to.claude/release-tests.yamlunder thev1759-fix45-*prefix (ID string retained from initial v1.7.59 slot; v1.7.60 reserved v1.7.59 for this work, but v1.7.60 and v1.7.61 both landed in main before this branch merged — see PR #723 merge-commit for detail) so the release gate catches any regression. Mandated gates (TestPersistence_*,Feedback|Sender_, watcher suite) remain green on this branch.
[1.7.61] - 2026-04-22
Added
agent-deck session remove <id|title>CLI subcommand — removes a session from the registry. Only sessions instoppedorerrorstate are removable by default;--forcebypasses the gate (destructive).--all-erroredbulk-removes every session currently in theerrorstate and respects status filtering (stopped, idle, running sessions are untouched).--prune-worktreeis an opt-in destructive variant that additionally kills the tmux process and removes any git worktree associated with the session.- TUI
Xkeybind (Home view) — status-gated registry remove with confirmation dialog. Rejects non-stopped/non-errored sessions with a message steering the user todfor destructive delete. The existingd→deleteSessionpath (full kill + worktree cleanup) is unchanged and remains the power-user option. - TUI
Ctrl+Xkeybind — bulk remove of all errored sessions with a confirmation dialog that shows the count. When there are no errored sessions the dialog is suppressed and an info message is shown instead. - New
ConfirmRemoveSessionandConfirmBulkRemoveErroredconfirm-dialog types wired throughconfirmActionwith yellow (non-red) border color to distinguish from the destructiveddelete dialog.
Preserved (hard invariant)
- Claude transcripts under
~/.claude/projects/<slug>/are never touched byremoveor theX/Ctrl+XTUI keybinds.TestSessionRemove_PreservesTranscriptsenforces this at CI time.
Tests
cmd/agent-deck/session_remove_cmd_test.go— 6 subprocess tests: stopped-succeeds, running-without-force-rejected, running-with-force-succeeds, all-errored-respects-filter, transcripts-preserved, not-found-exit-2.internal/ui/session_remove_tui_test.go— 5 Seam A (model-level) tests coveringXon stopped/error/running andCtrl+Xwith N>0 / N=0 errored sessions.- Full
cmd/agent-decksuite passes under-racein 57.8s. Fullinternal/uisuite passes under-racein 29.2s.TestPersistence_*mandate suite passes.
[1.7.60] - 2026-04-22
Added
-
Group-scoped keyboard navigation in the TUI (Alt+j/k, Alt+1-9, Alt+g/G, Alt+/). Addresses recurring feedback "jumping between shells is too complicated — shortcuts needed" (Christoph Becker, via Feedback Hub). The existing global tier — plain
j/k,1-9,g/gg/G,/— continues to work exactly as before with no muscle-memory breakage or test churn. The newAlt+-prefixed layer restricts movement to the cursor's current group:Alt+j/Alt+k— next / previous session in the current group. No-ops at the group boundary instead of spilling into the next group's first session.Alt+1-Alt+9— jump to the Nth session within the current group (1-indexed). Plain1-9still jumps to the Nth root group.Alt+g/Alt+G— first / last session in the current group.Alt+/— fuzzy search filtered to the current group only. The localSearchcomponent grew ascopedGroupfield so background session reloads (every ~2s viah.search.SetItems(h.instances)from seven call sites inhome.go) do not leak out-of-group results into a scoped search session;Hide()clears the scope.
"Current group" is derived from the cursor position: on a session item it's
Session.GroupPath; on a group header it's the header'sPath; on a window item it's the parent session's group path. SeecurrentGroupPathininternal/ui/group_nav.go.Discoverability lands alongside the keybinds so users find out the shortcuts exist before giving up:
?help overlay gains a new "GROUP NAVIGATION (v1.7.60)" section listing all four Alt+ keybinds.- README grows a two-tier keybindings table (Global vs Group) with explicit scope descriptions.
- One-shot status-bar hint on first TUI launch after upgrading to v1.7.60, reusing the existing maintenance-banner slot (no new layout math): "Tip: Alt+j/k and Alt+1-9 navigate within the current group. Press ? for all keybindings." The hint dismisses on any keypress or ESC, and a sentinel file at
~/.agent-deck/.nav-hint-v1760-shownensures it never reappears. Running underAGENTDECK_PROFILE=_testsuppresses the hint so UI tests never write to a developer's real~/.agent-deck/directory.
Tests: 17 new cases in
internal/ui/group_nav_test.go—TestGroupNav_AltJ_MovesToNextSessionInGroup,TestGroupNav_AltJ_DoesNotCrossGroupBoundary,TestGroupNav_AltK_MovesToPrevSessionInGroup,TestGroupNav_AltK_DoesNotCrossGroupBoundary,TestGroupNav_AltJ_FromGroupHeader_GoesToFirstSession,TestGroupNav_Alt2_JumpsToSecondInGroup,TestGroupNav_Alt3_JumpsToThirdInGroup,TestGroupNav_Alt5_BeyondGroup_IsNoop,TestGroupNav_Alt1_InBetaGroup_LandsOnB1,TestGroupNav_AltG_LowerCase_JumpsToFirstInGroup,TestGroupNav_AltShiftG_JumpsToLastInGroup,TestGroupNav_AltG_InBetaGroup_LandsOnB1NotA1,TestGroupNav_AltSlash_OpensSearchScopedToCurrentGroup,TestGroupNav_EvalHarness_RendersAndLandsOnRightSession(end-to-end: renders TUI frame, dispatches Alt+1/2/3, asserts cursor identity + non-emptyView()output), plus three regression tests (TestGroupNav_Regression_PlainJ_StillMovesDownFlatList,TestGroupNav_Regression_PlainJ_CrossesGroupBoundary,TestGroupNav_Regression_Plain1_JumpsToFirstRootGroup) pinning the global tier's existing semantics. Discoverability coverage:TestNavHint_ShownOnFirstLaunch_DismissedAfterKeypressandTestNavHint_SkippedWhenSentinelExistsisolateHOMEto aTempDirand unsetAGENTDECK_PROFILEso the sentinel logic runs under test without polluting the developer's real home directory.Version numbering: v1.7.59 is reserved for the in-flight update-nudge session, so this release skips to v1.7.60. Matches the pre-existing ghost-version precedent (v1.7.44-45, .47, .55 were never tagged either).
[1.7.58] - 2026-04-22
Fixed
- Bare-repository worktree layouts now fully supported (#715, reported by @Clindbergh). In a bare-repo layout (
project/.bare/holding the git dir withworktree1/,worktree2/, … as peers), every worktree is equal — there is no "default" or "main" worktree. The previous code assumedgit rev-parse --git-common-dirwould end in.git, so in a bare layoutGetMainWorktreePathsilently fell through toGetRepoRoot(dir)and returned the caller's own worktree path as the "project root". That misdirected every downstream.agent-deck/lookup: setup scripts placed next to.bare/were never found,worktree_repo_rootwas logged as the wrong path on every session, and runningagent-deck worktree listfrom the project root (where.bare/lives) failed outright withnot in a git repository. Fix adds bare-repo detection viagit rev-parse --is-bare-repositoryagainst the common-dir and teachesGetMainWorktreePath/GetWorktreeBaseRootto return the parent of.bare/(the conventional project root) in that case. A newIsGitRepoOrBareProjectRootpredicate replaces the oldIsGitRepopre-flight check inlaunch,add,session add, andworktree listso callers can pass the project root transparently. The lower-levelBranchExists,ListWorktrees,RemoveWorktree,ListBranchCandidates, andCreateWorktreefuncs now resolve a nested bare repo (via a newresolveGitInvocationDirhelper) before invokinggit -C, so every code path downstream ofGetWorktreeBaseRootworks on the project root without callers needing to know about the layout. Tests: 14 new RED→GREEN cases ininternal/git/bare_repo_test.gobuild a real.bare/+ 3-worktree fixture and assert (1)IsBareRepo/IsBareRepoWorktreedistinguish bare-dir, linked-worktree, and normal-repo inputs, (2)GetMainWorktreePathreturns the project root from every linked worktree — so there is truly no "default", (3)GetWorktreeBaseRootaccepts the project root itself (no.git) and returns the same, (4)FindWorktreeSetupScript(projectRoot)locates.agent-deck/worktree-setup.shnext to.bare/, (5)CreateWorktree(projectRoot, …)succeeds via transparent resolution to.bare/, (6) end-to-endCreateWorktreeWithSetup(projectRoot, …)on a bare fixture creates the worktree AND runs the setup script withAGENT_DECK_REPO_ROOTset to the project root, (7)ListWorktrees(projectRoot)enumerates.bare+ 3 linked worktrees, (8)BranchExists(projectRoot, …)resolves true/false correctly, (9) all worktrees resolve to the same project root — no "default" concept leaks anywhere. Live-boundary evidence: before/afteragent-deck worktree list --jsonfromproject/(was:"not in a git repository"; now:"repo_root": "/project", "count": 4) and from insideworktree1/(was:"repo_root": "/project/worktree1"— wrong; now:"repo_root": "/project"). End-to-endadd -w <new-branch> -bfrom the bare project root now succeeds and runs the setup script, whereas onmainit errored out withError: /project is not a git repository.
[1.7.57] - 2026-04-22
Fixed
- Right-pane preview no longer bleeds background highlights into the left pane (#699, reported by @javierciccarelli on Ghostty against v1.7.43). When a Claude session's captured output contained an unclosed SGR — typically a background highlight on the user's input line whose closing reset was off-screen, clipped by the preview's width truncation, or emitted in a later capture window — the right pane's rendered line ended with SGR state still active at its newline boundary.
lipgloss.JoinHorizontalthen laid the next terminal row out asleft_pane + separator + right_pane + "\n", and the next row's left-pane whitespace was painted under the right pane's dangling highlight. Ghostty is strict about SGR persistence across rows, which is why the reporter saw a yellow band extend across the entire left column whenever they typed at the Claude prompt. Root cause was ininternal/ui/home.go:renderPreviewPane—ansi.Truncatefaithfully preserves the SGR opening of a truncated line but emits no closing reset, and the final width-enforcement pass (line 12543+) re-truncated without appending one either. Fix adds a single guard in the final pass: every line whose bytes contain an ESC (0x1b) now gets a hard\x1b[0mappended before the join, so SGR state is always reset at every newline boundary beforelipgloss.JoinHorizontalassembles the frame. Harmless no-op on lines without ANSI; critical for lines with an unclosed highlight. This is the sibling invariant to the #579 CSI K/J erase-escape strip and the light-themeremapANSIBackgroundshipped with v1.6: those prevent the terminal from starting a bleed; this one stops state from surviving past a line. Regression coverage at three seams, matching the repo convention:TestPreviewPane_RightPaneDoesNotLeakSGRState_Issue699+TestPreviewPane_TruncatedLineDoesNotLeakSGRState_Issue699(Seam A unit,internal/ui/preview_ansi_bleed_test.go— assert no line inrenderPreviewPane's output leaves SGR active at its\n);TestEval_FullViewDoesNotLeakSGRAcrossRows_Issue699(Seam B eval,eval_smoketier,internal/ui/preview_ansi_bleed_eval_test.go— drives the fullHome.View()includinglipgloss.JoinHorizontaland asserts the row-level invariant the user actually sees);scripts/verify-preview-ansi-bleed.sh(Seam C, builds the real binary and boots it in tmux as a final smoke check). Seam A and B both verified RED on the unfixed code (row 12 of the Seam B render captured" ... │ \x1b[43m> tell me about ghostty ..."— ends with SGR=43 active — exactly @javierciccarelli's screenshot) and GREEN after the one-line fix.eval-smoke.ymlpath triggers extended to includeinternal/ui/home.goandinternal/ui/preview*.goso the Seam B eval runs per-PR on any preview-pane change. Thanks @javierciccarelli for the reproducer and the pinpoint screenshot.
[1.7.56] - 2026-04-22
Fixed
-
Socket isolation is now honoured on
session attach,session restart, and every pty.go subprocess (#687 follow-up, reported by @jcordasco during the v1.7.50 audit). v1.7.50 shipped[tmux].socket_name+--tmux-socket+ per-session SQLite persistence and routedsession start/session stop/ pane probes through thetmuxArgs/Session.tmuxCmdfactory — butinternal/tmux/pty.gostill assembled tmux argv by hand for six call sites, so every one of them connected to the user's default tmux server regardless of the session's configured socket. The classes of user-visible failure:session attachsilently fails (can't find session) when socket isolation is enabled and the session lives on-L <name>. The attach argv wasexec.CommandContext(ctx, "tmux", "attach-session", "-t", s.Name)— no-L, so tmux looked on the default server where the session does not exist.session attach-readonly(used by the web terminal inspect flow) has the same hole — same argv shape, same failure mode.(*Session).Resize(cols, rows)retargets the default server, so resize events for an isolated session either no-op or, if there's a same-named session on the default server, resize the wrong pane.AttachWindow's pre-attachselect-windowstep runs on the default server, sosession attach-windowselecting window 2 either fails or selects window 2 on an unrelated same-named default-server session before then correctly attaching to the isolated one (via fixed #1 above).StreamOutput'spipe-pane -o catand its cancellation-pathpipe-panestop both run on the default server, so streaming output from an isolated session receives zero bytes and the stop is a silent no-op.- Package-level
RefreshPaneInfoCachefallback intitle_detection.goran alist-panes -aon the default server, so the TUI status cache for isolation-enabled installs showed stale or empty pane titles/tool-detection on the fallback path.
The fix routes every one of these through the existing v1.7.50 factory. Six new per-Session command-builder seams live at the bottom of
internal/tmux/pty.go—(*Session).attachCmd,attachReadOnlyCmd,resizeCmd,selectWindowCmd,pipePaneStartCmd,pipePaneStopCmd— each delegating tos.tmuxCmd/s.tmuxCmdContextso-L <SocketName>lands before the subcommand when isolation is configured, and the argv stays byte-identical when it is not. Named methods (rather than inlining the factory calls) give the new regression-lint a stable target to assert argv shape against without spawning PTYs.The
title_detection.gofallback now usestmuxExecContext(ctx, DefaultSocketName(), …), matching the "package-level probes read process-wide DefaultSocketName()" pattern already in use elsewhere.Four layers of regression coverage, all TDD red-then-green before the fix landed:
- Unit (
internal/tmux/pty_socket_test.go, 7 cases): asserts each of the six command-builders emits the exact argv shape["tmux", "-L", "<socket>", "<subcommand>", …]whenSession.SocketNameis set, and["tmux", "<subcommand>", …]when empty (pre-v1.7.50 byte-compat). - Static lint (
internal/tmux/tmux_exec_lint_test.go, 1 case): AST-walks every.gofile in the module, finds everyexec.Command("tmux", …)andexec.CommandContext(ctx, "tmux", …)with a literal"tmux"as argv[0], and fails the build if any appears outside the allowlist. The allowlist covers the factory itself (internal/tmux/socket.go), the self-contained socket-aware wrapper ininternal/web/terminal_bridge.go, the test harness's explicit-S <path>sandbox (tests/eval/harness/sandbox.go), and three specific legitimate argv shapes:tmux -V(binary existence check, no server connection), and the three inside-tmuxdisplay-messageCLI helpers incmd/agent-deck/{cli_utils,session_cmd}.gothat read$TMUXenv for auto-detection (adding-Lthere would over-restrict users runningagent-deck session currentfrom a non-agent-deck tmux pane). Adding a new source-level tmux exec site now requires either routing through the factory or editing the allowlist with justification — no more silent bypasses. - Eval (
tests/eval/session/attach_socket_isolation_test.go, 1 case,eval_smoketag): drives the realagent-deckbinary through the full interactive lifecycle against a real tmux server on a randomly-named isolated socket.add→session start→ PTY-spawnedsession attach→ verify client appears ontmux -L <socket> list-clientsAND does NOT appear on the default server → send Ctrl+Q → clean detach with exit 0 →session restart→ verify exactly one session on the isolated socket →session stop→ verify zero sessions. The "PTY output dumped on failure" diagnostic makes the diagnosis actionable when a future regression fires this case. - Harness (
tests/eval/harness/pty.go): newSandbox.SpawnWithEnv(extraEnv, args…)overlays extra env on top of the sandbox base, enabling tests (like this one) to run agent-deck underTERM=xterm-256colorwhen real terminal capabilities are required — the sandbox default isTERM=dumbto keep termenv probes quiet, which is correct for most evals but causes tmux attach to refuse to register a client.
All mandatory test gates pass unchanged:
TestPersistence_*, Feedback + Sender_, Watcher framework, fullinternal/tmux/...race-detected suite.Thanks to @jcordasco for the detailed v1.7.50 audit that caught this — socket isolation at start + stop without isolation at attach would have been worse than no isolation at all, because users would have believed they were protected.
[1.7.54] - 2026-04-22
Added
- Title-lock re-ship (#697, reported by @evgenii-at-dev). The title-lock feature itself landed in main via PR #714 under the v1.7.52 CHANGELOG heading, but its release workflow and the follow-up v1.7.53 release both hit a pre-existing CI gap (
ubuntu-latestships without zoxide; the #693 quick-open picker tests short-circuit onZoxideAvailable()and false-failgo test ./...). PR #716 addedapt-get install -y zoxideto botheval-smoke.ymlandrelease.yml. This release re-ships the v1.7.52 and v1.7.53 features as v1.7.54 so the title-lock fix (and the #709--selectflag that briefly tagged as v1.7.53 without artifacts) actually reach binary releases. No source-code changes for the title-lock feature between v1.7.52 and v1.7.54 — the PR #714 commit is unchanged on main; only the release infrastructure around it was fixed. See the "[1.7.52]" entry below for the full feature description and the TDD evidence. - No-op for the #709
--selectbehaviour — see the "[1.7.53]" entry below; re-shipped identically.
Fixed
- Release workflow infrastructure gap unblocked (#716):
release.ymlandeval-smoke.ymlnow install zoxide before runninggo test ./.... Without this every release tag between v1.7.52 and v1.7.54 failed its goreleaser step, leaving orphan tags with no binaries. Future releases onubuntu-latestare unblocked.
[1.7.53] - 2026-04-22
Added
--select <id|title>CLI flag: launch the TUI with the cursor preselected on a specific session, while keeping every group visible in the sidebar (#709, requested by @tarekrached). Before this change, the only way to "jump to" a session at launch was-g <group>, which also hid every other group from the sidebar — useful when you want to scope the TUI to one area, but wrong when you just want to land on a session without losing the rest of the tree.--selectis the orthogonal primitive: it positions the cursor on the matching session (ID or title, case-insensitive, whitespace-tolerant) on first render and leaves the group tree untouched. Precedence with-gis well-defined: if both are passed,-gstill scopes the visible groups and--selectpositions the cursor within that scope; if the selected session is outside the scope,--selectis ignored and aWarning: --select "X" is not in group "Y"; cursor will not be repositionedline is printed to stderr so the mismatch is visible without digging through logs. Implementation: a newextractSelectFlagincmd/agent-deck/main.gomirrors the existingextractGroupFlagpattern (both--select fooand--select=fooforms), andHome.SetInitialSelection+Home.applyInitialSelectionininternal/ui/home.goqueue the preselection until the firstloadSessionsMsgarrives —applyInitialSelectionruns immediately afterrebuildFlatItemsso it respects any active group scope, and it is idempotent so normal cursor navigation after the first render is not overridden. The match order is: exact ID first, then case-insensitive title equality, then lower-cased whitespace-trimmed title — this lets--select "My Project"work even if the user shell-quotes the title differently from how it was stored. Tests: 7 new RED→GREEN cases —TestExtractSelectFlag(7 sub-tests covering flag parsing forms and interaction with-p/-g),TestExtractSelectFlag_PreservesGroupFlag,TestSetInitialSelection_PositionsCursorAndKeepsAllGroupsVisible(the core #709 assertion: cursor on requested session AND all three test groups remain inflatItems),TestSetInitialSelection_MatchesByTitle,TestSetInitialSelection_GroupScopePrecedence(3 sub-tests for in-scope / out-of-scope / unknown-id paths),TestSetInitialSelection_NormalizationIsLenient. End-to-end evidence inscripts/verify-select-flag.sh(headless tmux +capture-pane): seeds three sessions across three groups, launches the real binary, captures the pane, asserts the cursor marker is on the selected session and all three groups remain visible in the sidebar, then runs the-g work --select betascenario and asserts the stderr warning fires. No changes to-gsemantics, no changes to the persisted cursor-restore path beyond letting--selecttake precedence on the very first load.
[1.7.52] - 2026-04-22
Added
-
--title-lockflag +session set-title-locksubcommand prevent Claude's session name from overriding the agent-deck title (#697, reported by @evgenii-at-dev). Conductor workflow: launchagent-deck launch -t SCRUM-351 -c claude --title-lockon a worker, then Claude's own/renameof its session (or the auto-generated first-message summary likeauto-refresh-task-lists) is prevented from syncing back into the agent-deck title. Without this, the conductor loses the semantic identity it assigned to the child session on the first hook tick — making it impossible to tell which worker is working on which ticket once Claude has spoken. Three call sites:Instance.TitleLocked bool(new field, persisted ininstances.title_lockedSQLite column via schema bump v7 → v8, additive ALTER TABLE withDEFAULT 0so every pre-v1.7.52 row reads as unlocked and the existingapplyClaudeTitleSyncpath stays default-on for them). JSON tagtitle_locked,omitemptykeeps the wire format backwards-compatible with any third-party tooling that reads the state-db JSON dumps.applyClaudeTitleSyncgate (cmd/agent-deck/hook_name_sync.go): after resolving the target Instance, an early-returnif target.TitleLockedskips the Title mutation and the SaveWithGroups write — keeping the #572 default behaviour (Claude--name//renamesyncs into agent-deck) untouched for the 99% case while giving conductors an opt-in off switch.- CLI surface:
agent-deck addandagent-deck launchgain--title-lock(with--no-title-syncas an alias for discoverability);agent-deck session set-title-lock <id> <on|off>toggles an already-created session (acceptstrue/false/1/0/yes/notoo for script friendliness).session show --jsonnow emitstitle_locked: true|falseso conductors can query state without reading the SQLite directly.
Tests (TDD — RED captured on baseline before the implementation landed):
TestApplyClaudeTitleSync_NoopWhenTitleLockedincmd/agent-deck/hook_name_sync_test.go— seeds an Instance withTitleLocked: trueand a matching Claude session metadata file, invokesapplyClaudeTitleSync, asserts the Title did NOT change and thatTitleLockedsurvived the round-trip (guards against silent persistence regressions).TestStorageSaveWithGroups_PersistsTitleLockedininternal/session/storage_test.go— round-trips two instances (one locked, one unlocked) throughSaveWithGroups, then reloads via BOTHLoadWithGroups(full hydration, TUI path) andLoadLite(fast CLI path), asserting the bool survives each path and that the default (false) doesn't leak across rows.- The three existing
TestApplyClaudeTitleSync_*cases (UpdatesInstance / NoopWhenNameMissing / NoopWhenNameEqualsTitle) continue to pass unchanged, proving the #572 default behaviour is preserved. - End-to-end eval harness at
tests/eval/title-lock.eval.shdrives the real binary through three real-world scenarios in a disposableHOME: (A) add with--title-lockblocks Claude's rename; (B)session set-title-lock offre-enables sync on the next hook tick; (C)set-title-lock onre-freezes the title against a subsequent rename. Smoke-tier — designed to run on every PR that touches session lifecycle.
Thanks to @evgenii-at-dev for the detailed conductor-workflow bug report that caught this.
[1.7.51] - 2026-04-22
Fixed
- Settings TUI no longer drops the
[tmux]config block on save (#710, reported on v1.7.50). PressingSin the TUI, toggling any setting, and saving was silently zeroing the entire[tmux]table on disk —inject_status_line,launch_in_user_scope,detach_key,socket_name(v1.7.50), andoptionswere all gone after the next reload. Root cause:SettingsPanel.GetConfigreconstructs the to-be-savedUserConfigfrom the panel's visible widget state and pass-through-copies every section it doesn't render (MCPs, Tools, Profiles, Worktree, …) fromoriginalConfig, butTmuxhad been omitted from that copy block. Same class of bug as #584 (Worktree) and the structural reason we couldn't reproduce the original #687inject_status_linereport by editingconfig.tomldirectly — the reporter was hitting the Settings TUI save path, not the loader. Fix is one line:config.Tmux = s.originalConfig.Tmuxadded to the preservation block ininternal/ui/settings_panel.go. Coverage gap closed by two new tests:TestSettingsPanel_Tmux_GetConfigPreservesHiddenFields(unit, mirrors the existing Worktree guard) assertsGetConfig()round-tripsInjectStatusLine,LaunchInUserScope, andDetachKeyfromoriginalConfig;TestEval_SettingsTUI_SavePreservesTmux(eval_smoke tier ininternal/ui/settings_panel_eval_test.go) drives the fullLoadUserConfig → SettingsPanel.LoadConfig → GetConfig → SaveUserConfig → re-read TOMLround-trip against a scratch$HOMEto prove[tmux]survives a real save with a non-tmux setting changed (theme dark → light). Both tests were verified RED on the unfixed code and GREEN after the one-line fix. Thanks to @jcordasco for the exact diagnosis and suggested fix in #710.
[1.7.50] - 2026-04-21
Added
-
Tmux socket isolation (phase 1) — agent-deck can now run on a dedicated tmux server, fully separate from your interactive tmux (#687, completes the root-cause fix for #276). Opt in via a single config line:
[tmux] socket_name = "agent-deck"Every agent-deck session now spawns as
tmux -L agent-deck …— a separate tmux server whose socket lives at$TMUX_TMPDIR/tmux-<uid>/agent-deck. Your regular tmux atdefaultis never touched.[tmux].inject_status_line, bind-key, and globalset-optionmutations stay on the agent-deck server; your personal status bar, plugins, and theme are untouched. A straytmux kill-serverin your shell cannot take agent-deck sessions down with it.tmux -L agent-deck lsfrom the shell shows exactly agent-deck's sessions.Default behavior unchanged. Leave
socket_nameunset (the default) and agent-deck behaves exactly like v1.7.49: it uses your default tmux server. This is a pure opt-in — zero behavior change for existing users.Per-session override. Both
agent-deck add --tmux-socket <name>andagent-deck launch --tmux-socket <name>override the installation-wide default for one session. Precedence: CLI flag >[tmux].socket_name> empty.Per-session persistence. Each Instance captures its socket name in SQLite at creation time (new
tmux_socket_namecolumn, schema v7 with an additiveALTER TABLEmigration — legacy rows default to''). Every lifecycle operation (start/stop/restart/revive, status probe, capture-pane, send-keys, kill-session) readsInstance.TmuxSocketNameand targets that socket. Changingsocket_namein config later does not migrate existing sessions — they remain reachable on the socket they were created on. Mixing sockets mid-life would strand the pane; the immutable-after-creation contract prevents that.Scope of changes. A single command-factory pair —
tmux.tmuxArgs(socketName, args...)+ theExec/ExecContextpublic wrappers — centralises the-L <name>injection. Every one of the ~50exec.Command("tmux", …)call sites acrossinternal/tmux/,internal/session/,internal/ui/,internal/web/, andcmd/agent-deck/now routes through this factory or its(*Session).tmuxCmdcounterpart, so a future socket-selection change (phase 2/3: per-conductor sockets,-S <path>support, session-migrate subcommand) has exactly one hook point. The three package-level probes (IsServerAlive,RefreshSessionCache,recoverFromStaleDefaultSocketIfNeeded) read a process-widetmux.DefaultSocketName()seeded once atmain.gostartup fromsession.GetTmuxSettings().GetSocketName().tmux -Vversion check intentionally stays plain — it does not connect to any server, so socket selection is moot. The web PTY bridge's existing-S <path>fallback from theTMUXenv var is preserved — per-sessionTmuxSocketNametakes precedence when set.Reviver wiring.
Reviver.TmuxExistssignature changed fromfunc(name string) booltofunc(name, socketName string) boolso revive scans probe the right tmux server. Probing the default server for a session living on an isolated socket would wrongly classify it as dead; this callback now receivesInstance.TmuxSocketNamefromClassify()and the default helper (defaultTmuxExists) forwards it to a newtmux.HasSessionOnSocket(socket, name).PipeManager.ConnectandNewControlPipealso gained asocketNameparameter so reconnect loops target the right server for the entire life of the pipe.Tests. 17 new tests covering the full surface:
TestTmuxArgs_*(5 cases — empty socket pass-through,-Linjection, caller-slice immutability, empty args, whitespace-only trim),TestSession_TmuxCmd_*(2 — per-session builder honorsSession.SocketName),TestDefaultSocketName_*(3 — process-wide default init/set/trim),TestGetTmuxSettings_SocketName_*(4 — TOML round-trip, explicit value, whitespace-trim, whitespace-only→empty),TestNewInstance_SocketName_*+TestNewInstanceWithTool_SocketName_*+TestRecreateTmuxSession_PreservesSocketName(4 — constructor seeding from config, tool-aware constructor parity, restart-preserves-captured-socket invariant),TestStorage_TmuxSocketName_{Roundtrip,EmptyRoundtrip}(2 — SQLite save→close→reopen→load for both isolated and legacy rows),TestReviver_*(3 — Classify threads the socket name intoTmuxExists, legacy instances probe with empty socket,ReviveActionreceives the instance socket name),TestTmuxAttachCommand_SocketNameOverridesEnv+TestTmuxAttachCommand_WhitespaceSocketNameFallsBackToEnv(2 — web PTY bridge precedence and whitespace defensive fallback). Every mandatory test gate from CLAUDE.md (TestPersistence_*,Feedback*,Sender_*, watcher framework tests, behavioral evaluator harness introduced in v1.7.49) continues to pass unchanged — socket isolation adds a new axis to the tmux-command contract without weakening the session-persistence, systemd-scope, or user-observable-behavior invariants. (Note: a real-tmux eval case exercisingagent-deck add --tmux-socket …+session start+display-message -pon the isolated socket is tracked as a phase-2 follow-up; phase 1 relies on unit + integration coverage of the factory, persistence, and reviver surfaces plus the v1.7.49TestEval_Session_InjectStatusLine_RealTmuxwhich exercises the default-socket path unchanged.)Migration. Docs-only in this release. There is no
session migrate-socketsubcommand yet — moving existing sessions to the isolated socket requires either re-creating them viaagent-deck add, or hand-editing~/.agent-deck/<profile>/state.db(UPDATE instances SET tmux_socket_name = 'agent-deck' WHERE id = '…') and restarting agent-deck. The dedicated subcommand is tracked for phase 2 along with per-conductor sockets and-S <socket-path>support. See the "Socket Isolation" section in README for the full migration recipe.
[1.7.49] - 2026-04-21
Added
- Behavioral evaluator harness (#37). New test layer at
tests/eval/that catches the class of regressions where a Go unit test passes but the user sees the wrong thing. Motivated by three recent shipped-but-unit-test-invisible bugs: v1.7.35 CLI disclosure buffered behind stdin (strings.Builderhid the prompt until after the function returned; unit tests used the same type, so the bug was invisible), v1.7.37 TUI feedback dialog going straight from comment to send with no disclosure step, and the #687inject_status_linemisdiagnosis where unit tests asserted on struct fields and argv slices instead of what real tmux actually displayed. Harness stack: per-test scratchHOME, isolated tmux socket via a wrapper shim that splices-S <sock>into everytmuxinvocation the binary makes, aghshim that records argv+stdin to a JSON log and scripts success/failure, and agithub.com/creack/pty-based PTY driver with anExpectOutputBefore(want, before, timeout)matcher that structurally defeatsstrings.Builder-style buffering regressions (under a real PTY, a buffered wrapper makes tokens arrive only after the next stdin read, so the wait times out). Three RFC §7 cases ship in this release:TestEval_FeedbackCLI_DisclosureBeforeConsent(PTY-driven, asserts theRatingprompt and the "posted PUBLICLY" disclosure both arrive before the binary blocks on stdin — catches any future strings.Builder-style regression structurally),TestEval_FeedbackTUI_DisclosureStepExists+TestEval_FeedbackCLI_and_TUI_HaveEquivalentDisclosure(drives theFeedbackDialogstate machine end-to-end and proves the two surfaces carry the same disclosure tokens), andTestEval_Session_InjectStatusLine_RealTmux(runsagent-deck add+session startagainst a per-sandbox tmux socket, then queriesdisplay-message -p '#{status-right}'to assert the injected bar actually reaches the tmux server). Each case was verified TDD-style before shipping: the fix was temporarily reverted in the product code, the test was confirmed to fail with a diagnostic that identifies the exact regression (strings.Builder buffering →Ratingprompt times out; stepConfirm collapsed → "expected stepConfirm (disclosure step), got stepSent"; buildStatusBarArgs forced nil →status-rightis tmux's default template instead of agent-deck's injected one), then the fix was restored. Tiered CI:.github/workflows/eval-smoke.ymlrunsgo test -tags eval_smokeon every PR that touches the affected paths (3-minute timeout, blocking), andrelease.ymladds aneval_smoke eval_fullstep before GoReleaser so a release that fails eval does not get a tag. Linux-only in CI per the RFC's cost analysis; macOS dev runs locally. Seedocs/rfc/EVALUATOR_HARNESS.mdfor the full design andtests/eval/README.mdfor how to add cases. CLAUDE.md gains an "eval case required for interactive flow changes" mandate mirroring the existing session-persistence, watcher, and feedback mandates.
[1.7.48] - 2026-04-21
Added
agent-deck session send --stream: structured JSONL streaming of the agent's reply while it is still being produced (#31, resolves #689). Previouslysession sendeither returned a one-shot snapshot (default), a running-status heartbeat (--wait), or nothing at all (--no-wait) — long assistant turns with intermediate tool calls were opaque to every caller except a human watching tmux. The new--streamflag tails the Claude JSONL transcript as it is appended and emits a line-delimited event stream to stdout:start(carriesschema_versionso consumers can branch on future schema moves),text(text-block deltas, batched on 10s idle / 4000-char / 3-tool boundaries with--stream-idle/--stream-char-budget/--stream-tool-budgetoverrides),tool_use(name + input),tool_result(matchingtool_use_id+ content),stop(withreason=end_turn/max_tokens/stop_sequence), anderror(on idle timeout or upstream failure). The streamer runs ininternal/session/transcript_streamer.go: it opens the transcript at~/.claude/projects/<encoded>/<session-id>.jsonl, tracks a file offset plus a UUID dedup set for idempotency under rewind, drops records whosetimestampis beforesentAt - 250msto avoid replaying pre-send history, and walks each assistant/user record'scontentblocks to translate them into events. Text blocks from the same assistant message are merged and flushed on the first of: a latertool_usein the same message, the 4000-char budget, the 3-tool budget, or idle timeout.stop_reason == "tool_use"is NOT treated as terminal — the streamer keeps running so the subsequenttool_result+ next assistant turn stream through as one continuous flow. Phase 1 is Claude-only (Claude Code Opus/Sonnet/Haiku viaIsClaudeCompatible) because the transcript format is Claude-specific; non-Claude tools (codex, gemini, aider, shell) get a clean--stream is not supported for tool %q (Phase 1 supports Claude-compatible tools only)error and exit 1 at the CLI entry point viastreamPreconditionError()before any tail begins.--streamand--waitare mutually exclusive. The existing--wait+--no-wait+ default paths are unchanged; callers that don't pass--streamsee byte-identical behavior to v1.7.47. 10 new tests ininternal/session/transcript_streamer_test.go(defaults + overrides of the batching triad, start/text/tool_use/tool_result/stop event emission, pre-sentAt record skipping, natural end_turn return, idle-timeout error, char-budget flush, context-cancel return) plus 3 incmd/agent-deck/session_stream_test.go(Claude-compatible allowed, non-Claude rejected with a message naming the flag and tool, end-to-end tail-to-stdout against a hand-authored JSONL fixture). Unlocks the conductor loop's streaming hop that #689 blocked.
[1.7.45] - 2026-04-21
Fixed
-
Transition notifier no longer silently loses events when the parent conductor is busy (#39, #40). Production logs for the 24 hours before this release showed a 23% delivery rate (45 sent / 198 generated) on transition notifications: 105 events (53%) took the silent-loss path
deferred_target_busy → forgotten, while another 47 were root-conductor transitions with no parent and 1 was an outright send failure. Two distinct problems combined to produce that number and both are fixed here.Primary bug — deferred events were silently dropped. When a child session transitioned
running → waitingwhile the parent conductor happened to beStatusRunning(mid-tool-call), the notifier wrotedelivery_result=deferred_target_busytotransition-notifier.logand returned, deliberately not marking the event in the dedup state so a later poll could retry. ButTransitionDaemon.syncProfile()unconditionally updatedd.lastStatus[profile]after every pass, including on deferred events. On the next poll cycleprev[id]was"waiting"(the new state), soShouldNotifyTransition("waiting", "waiting")returned false and the transition was never re-offered. The intended retry loop did not exist. Fix: a persistent deferred-retry queue at~/.agent-deck/runtime/transition-deferred-queue.json.NotifyTransitionnow callsEnqueueDeferred(event)on the busy-target path;syncProfilecallsnotifier.DrainRetryQueue(profile)at the top of every poll (ahead of theinitializedgate sonotify-daemon --oncealso drains). Drain walks each entry, dispatches via the async sender whenliveTargetAvailabilityreports the target is notStatusRunning, and age-outs stale entries tonotifier-missed.logwithreason=expiredafterdefaultQueueMaxAge = 10mordefaultQueueMaxAttempts = 20. Queue entries are keyed by(child_session_id, from_status, to_status)so repeat defers of the same transition refresh the event but preserveFirstDeferredAt— the age-out timer is honest across the full life of a stuck transition. The queue persists across notifier restarts (daemon reload or process crash) because the file is rewritten under a.tmp + renameon every mutation.Secondary bug — head-of-line blocking in the dispatch path. The notifier's send to a target was synchronous: a slow
tmux send-keysagainst one pane serialized every subsequent notification across unrelated targets in the same poll cycle. On a conductor host with many active children, one hung pane could delay notifications for an entire poll interval. Fix:dispatchAsyncspawns one goroutine per notification, gated by a per-target semaphore (map[string]chan struct{}of buffer 1). Each send runs under a 30s default timeout (defaultSendTimeout = 30 * time.Second). Three terminal states land in logs:sent/failedgo to the existingtransition-notifier.logstream;timeout(send ran past 30s) andbusy(target already had an in-flight send) go to a new~/.agent-deck/logs/notifier-missed.log— operators now have an actionable evidence trail instead of a silent miss. The sender goroutine holds its target's semaphore slot until the underlyingSendSessionMessageReliableactually returns, even if the watcher already declared a timeout; this prevents a secondtmux send-keysfrom racing the first on the same pane.notify-daemon --onceflush. Because dispatch is now asynchronous, the--onceCLI path would have exited before goroutines finished writing their log entries.TransitionDaemon.Flush()waits on both the watcher and sender WaitGroups;handleNotifyDaemonin the--oncebranch calls it before returning so thatnotify-daemon --onceremains deterministic under test.Investigation of #40 ("conductor stopped when children silent"). A parallel investigation (
INVESTIGATION_40_CONDUCTORS_STOPPED.md) confirmed the "stopped" symptom is not caused by a silence/idle detector inwatchdog.pyor the agent-deck daemon. Neither code path flips a conductor toerrorbased on elapsed silence. The real triggers are tmux-server SIGSEGV cascades (documented inFORENSIC_2026_04_20_MASS_DEATH.md) andclaude --resumefailures that leave a pane dead within the watchdog's 15s restart-success window. Those are out of scope for this release; #40 stays open for a separate fix.Test harness and verification. Twelve new unit tests in
internal/session/transition_notifier_async_test.goandtransition_notifier_queue_test.gocover: slow-target-doesn't-block-fast-target (throughput), timeout → missed.log, concurrent-same-target → busy miss, normal sent path, explicit send error → failed (not missed), queue enqueue persistence, drain-dispatch-when-free, drain-keeps-busy-entries, drain-expires-old-entries, queue survives notifier reload, and the integration case provingNotifyTransitionwith aStatusRunningparent enqueues rather than marking the event notified. A newscripts/verify-notifier-async.shharness uses the built binary against a real tmux server under an isolatedHOME: it seeds a deferred queue entry, runsnotify-daemon --once, and asserts (a) the delivery log showsdelivery_result=sent, (b) the queue is cleared, (c) the literal[EVENT] Child 'child-e2e'banner appears in the parent's live tmux pane (confirming the realtmux send-keyspipeline end-to-end), and (d)notifier-missed.logstays empty on the happy path.
[1.7.44] - 2026-04-21
Changed
- Mobile web terminal input (#652 by @JMBattista): mobile clients (
pointer: coarse) no longer enforce an implicit read-only mode in the web UI. Keystrokes from phones/tablets now flow to the tmux session like any other client. To preserve the previous behavior, start the web server withagent-deck web --read-only— the server-side flag now owns read-only enforcement for all devices. Rebuild of JMBattista's original PR #652 (which had accumulated merge conflicts across 9 intervening releases); authorship is preserved viaCo-Authored-Bytrailer on the rebuilt commit. Four surgical changes ininternal/web/static/app/TerminalPanel.js: (1) theconst isMobile = isMobileDevice()component-scope variable is removed, (2)disableStdin: mobilein thenew Terminal({...})constructor becomesdisableStdin: false, (3) theif (!mobile) { inputDisposable = terminal.onData(...) }gate becomes an unconditionalconst inputDisposable = terminal.onData(...)so phone/tablet keystrokes reach the WebSocket, (4) the mobile-onlycontainer.addEventListener('touchstart', (e) => e.preventDefault())block and theREAD-ONLY: terminal input is disabled on mobileyellow banner are both deleted. ThereadOnlySignal+payload.readOnly || mobileOR inonWsMessageloses the|| mobilehalf so the server-side--read-onlyflag is the single source of truth for input enablement across all device types. PERF-E listener-site count drops from 9 to 8 (the mobile-only anonymous touchstart preventDefault was the 9th site);tests/e2e/visual/p8-perf-e-listener-cleanup.spec.tsupdated to assertcontroller.signalappears>=8times, andtests/e2e/visual/p1-bug6-terminal-padding.spec.tsflips from asserting the READ-ONLY banner is present to asserting it is absent, plus two new structural tests (terminal.onData is not gated on !mobile,disableStdin is not OR-ed with mobile on status messages) to guard the rebuild from regressing.
[1.7.43] - 2026-04-21
Fixed
- Zombie tmux clients and MCP subprocesses no longer accumulate in long-running agent-deck TUI and web processes (#677): four distinct
exec.Cmd.Start()call sites were paired with acmd.Wait()that only fired on the manual-shutdown path, so any child that exited on its own — MCP server crash, tmux session killed externally, triageagent-deck launchexiting normally, tmux server reload — became a zombie entry in the process table that was never reaped. On one live conductor this week: 10 zombies on the TUI (allnpm exec/uvMCP children frombroadcastResponses) plus 43 zombies across web/TUI cascades earlier the same day. Per-zombie memory is tiny, but accumulation is unbounded: over a week-long agent-deck session with an attached MCP pool and active watcher triage this bloats the process table and eventually hits the per-UID process limit, manifesting asfork/execfailures far from the real cause. Four fix sites:internal/tmux/controlpipe.go— thereader()goroutine that parsestmux -Cprotocol output saw stdout-EOF when the subprocess died and closedDone(), but never calledcmd.Wait(). OnlyClose()reaped, so if thePipeManagerreconnect loop gave up or a session was removed, Close was skipped and the zombie persisted. Fix: a newreap()helper guarded bysync.Onceis called from both thereader()defer (natural EOF path) andClose()(manual shutdown), so exactly one goroutine runscmd.Wait()no matter which event fires first.internal/mcppool/socket_proxy.go—broadcastResponsessaw MCP stdout EOF, set status toStatusFailed, and closed client connections, but never calledmcpProcess.Wait(). The zombie lingered untilStop()/RestartProxy()was invoked, which for an idle or rarely-used MCP may be never. SamewaitOnce+reap()pattern wired intobroadcastResponses(EOF path),Stop()(graceful shutdown path), and thenet.Listenfallback that kills on socket-creation failure. Matches the 10npm exec/uvzombies observed on the live conductor.internal/watcher/triage.go—AgentDeckLaunchSpawner.Spawn()didcmd.Start()and returned without ever waiting on the child. Every triage event produced exactly one zombie. Fix: ago func() { _ = cmd.Wait() }()reaper goroutine launched afterStart()succeeds, so the child is reaped whenever it exits. Tested by stub-binary spawn: 25 spawns → 0 zombies.- Tests:
TestControlPipe_NoZombie_WhenProcessExits,TestControlPipe_NoZombie_ManyCyclesininternal/tmux/zombie_reap_test.go(20 kill-session cycles, asserts zombie count does not grow);TestSocketProxy_NoZombie_OnProcessExitininternal/mcppool/socket_proxy_zombie_test.go(15 cycles ofsh -c "exit 0"MCP processes, asserts no zombie after broadcastResponses EOF);TestAgentDeckLaunchSpawner_NoZombieininternal/watcher/triage_zombie_test.go(25 triage spawns with a stub agent-deck, asserts no zombie remains). Each test reads/proc/<pid>/statusforState: Z (zombie)so failures print the exact growth delta. Linux-only (testst.Skip()when/procis absent) — the production code fixes are portable.
[1.7.42] - 2026-04-21
Changed
-
CI: audit + fix-or-disable broken gates (#682). Two PR gates removed, zero fixed in place, four still active. Green now means green again. Every PR merged between v1.7.34 and v1.7.41 carried a red
Visual Regressioncheck and in most cases a redLighthouse CIcheck too, and the recurring "ignore the red, it's just visual-regression" exception was training the team to merge through real failures. Both gates shared the same root cause —./build/agent-deck webimports bubbletea transitively and fails its cancel-reader init on headless CI runners (error creating cancelreader: bubbletea: error creating cancel reader: add reader to epoll interest list), so the test server never binds and every Playwright/Lighthouse spec fails withERR_CONNECTION_REFUSED. The Lighthouse budget in.lighthouserc.jsonwas also never re-baselined against the current webui bundle. Fixing the server-start path (PTY wrapper or a--no-tuistartup flag) is tracked as a stability-ledger follow-up; until then, per the audit recommendation, both PR gates are removed:.github/workflows/visual-regression.yml— deleted. Same test matrix still runs on the Sunday cron viaweekly-regression.yml. Local run:cd tests/e2e && npx playwright test --config=pw-visual-regression.config.tsagainst a localagent-deck web..github/workflows/lighthouse-ci.yml— deleted. Same Lighthouse suite still runs weekly viaweekly-regression.yml. Local run:./tests/lighthouse/calibrate.shthennpx lhci autorun --config=.lighthouserc.json.
Remaining active PR gate is
session-persistence.yml(theTestPersistence_*suite plusscripts/verify-session-persistence.sh), which has passed consistently on every run and gates the class of bug the v1.5.2 mandate was written to prevent.release.yml,pages.yml,issue-notify.yml,pr-notify.yml,weekly-regression.ymlare unchanged. New.github/workflows/README.mddocuments the full disposition and the local-run commands, and flags thatweekly-regression.ymlcurrently hits the same bubbletea/TTY failure (but is alert-only and idempotent, so at most one open issue per week — not a flood). No source code changed, no tests changed — this is strictly a CI-topology edit.
[1.7.41] - 2026-04-20
Fixed
- Feedback prompt no longer spams brand-new users on their first few launches. Reported in the wild as "I've hardly used it yet, why are you constantly asking me to rate it?" — before v1.7.41, the TUI auto-prompt fired on every launch as long as
FeedbackEnabled+ not-yet-rated-this-version +ShownCount < MaxShows(default 3) — so a fresh user opening agent-deck three times in a row would see the same rating prompt back-to-back with no usage signal gating it. Fix introduces three new pacing fields infeedback.State(FirstSeenAt time.Time,LastPromptedAt time.Time,LaunchCount int) and tightensShouldShowwith two new gates on top of the existing preconditions: (1) the first prompt requires BOTH at leastMinDaysBeforeFirstPromptdays sinceFirstSeenAt(default 3) AND at leastMinLaunchesBeforeFirstPromptprocess starts (default 7); (2) after any prompt is shown, subsequent prompts are throttled forPromptCooldownDays(default 14).RecordLaunch(state, now)runs once per TUI process start incmd/agent-deck/main.gojust beforeui.NewHomeWithProfileAndMode— it incrementsLaunchCountand seedsFirstSeenAton the very first call (never overwrites it, so pacing persists across version upgrades).RecordShown(state, now)signature gained anow time.Timeparameter; it now stampsLastPromptedAtat display time so the cooldown engages.RecordRatingdeliberately does NOT touch the new pacing fields — ShownCount still resets per-rating (so the next version can prompt again up to MaxShows times), but FirstSeenAt/LastPromptedAt/LaunchCount survive so pacing stays honest across the upgrade.ShouldShow(state, version, now time.Time)signature also gained a clock parameter so the pacing thresholds are fully testable under a stable-clock harness with no wall-clock flakiness. Four env vars let the test suite override the constants without rebuilding:AGENTDECK_FEEDBACK_MIN_DAYS,AGENTDECK_FEEDBACK_MIN_LAUNCHES,AGENTDECK_FEEDBACK_COOLDOWN_DAYS(deliberately undocumented in README — test-harness use only). JSON state file (~/.agent-deck/feedback-state.json) gains three new fields serialized via time.Time's RFC3339 round-trip; loading a pre-v1.7.41 file works unchanged (zero-valued time.Time is treated as "no signal yet" and blocks the prompt untilRecordLaunchseedsFirstSeenAton the next TUI start). Opt-out still wins over every pacing gate, already-rated-this-version still wins, max-shows still wins — pacing is strictly additive, never relaxing the prior gates. Tests: 14 new cases ininternal/feedback/pacing_v1741_test.go—TestPacing_NewUser_FirstSeenSetOnRecordLaunch,TestPacing_RecordLaunch_DoesNotOverwriteFirstSeenAt,TestPacing_1Day_3Launches_Blocked,TestPacing_4Days_10Launches_Shown,TestPacing_4Days_3Launches_Blocked,TestPacing_1Day_10Launches_Blocked,TestPacing_AfterShown_CooldownBlocks,TestPacing_CooldownExpired_ShownAgain,TestPacing_EnvOverride,TestPacing_OptOutWinsOverPacing,TestPacing_AlreadyRatedWinsOverPacing,TestPacing_MaxShowsWinsOverPacing,TestPacing_RecordRating_PreservesPacingFields,TestPacing_StateRoundtrip. Legacy tests ininternal/feedback/feedback_test.gowere updated to pass a pre-seededFirstSeenAt+LaunchCount(via a newoldShouldShowBypasshelper) so they continue to assert the original enabled/not-rated/under-max gates without drowning in pacing boilerplate. README gains a "Feedback prompt frequency" paragraph under the existing Feedback section;agent-deck feedback --helpgrows a "Prompt frequency (v1.7.41+)" block summarizing the same rules.
[1.7.40] - 2026-04-20
Fixed
-
agent-deck launchchild sessions no longer leak a secondbun telegrampoller against the conductor's bot token (stability-ledger row S8): the v1.7.35 / #680TELEGRAM_STATE_DIRstrip was deliberately narrow — it only fired when the child's group was paired with a[conductors.<group>]block and that group had anenv_file. Everyagent-deck launchspawn outside that triangle (unrelated group, no group, no env_file) still inheritedTELEGRAM_STATE_DIRfrom the conductor's shell env. WithenabledPlugins."telegram@claude-plugins-official" = truein the profilesettings.json(required per the v3 supported topology — flipping it off breaks the conductor, verified by the 2026-04-18 travel outage), the child's claude loaded the plugin, read the conductor's.envvia the inherited TSD, and opened a duplicategetUpdatespoller on the same bot token. Telegram Bot API rejects the second poller with 409 Conflict and messages drop silently. Fix lands in two independent layers so either one alone closes the leak:- Layer 1 — shell unset.
conductorOnlyEnvStripExpris replaced bytelegramStateDirStripExpr, which emitsunset TELEGRAM_STATE_DIRfor any claude spawn where (1) the session title does not start withconductor-and (2) theChannelsfield contains noplugin:telegram@entry — regardless of group or env_file presence. The strip is appended tobuildEnvSourceCommandoutside theif toolEnvFile != ""block, so it fires even on bareagent-deck launchchildren with no config at all — the common S8 leak path. - Layer 2 — exec-level
env -u. The final claude invocation inbuildClaudeCommandWithMessageis wrapped inenv -u TELEGRAM_STATE_DIRfor the same predicate. Covers all five session modes (continue, resume-with-id, resume-picker, fresh start, fresh-start-with-message). Theenvbinary strips the variable from the claude child process regardless of the shell's environment state, so a corrupted env_file, a custom wrapper that rewrites the sources chain, or a future refactor that relocates Layer 1 cannot silently regress the leak.
Conductor sessions (owner of the bot) and explicit per-session telegram channel owners (
Channelscontainingplugin:telegram@…) are untouched on both layers. Non-claude tools (codex, gemini) are untouched.Regression coverage:
TestS8_ChildNoChannels_NoConfig_StripsTSD,TestS8_ChildNoChannels_UnrelatedGroup_StripsTSD,TestS8_TelegramChannelOwner_KeepsTSD,TestS8_NonClaudeSession_NoStrip,TestS8_NonTelegramChannelOwner_StripsTSD,TestS8_TelegramChannelOwner_ForkVariant_KeepsTSD,TestS8_ConductorSession_NoChannels_KeepsTSDininternal/session/s8_child_poller_leak_test.gocover Layer 1;TestS8_ExecLayer_FreshStart_UnsetTSDInvocation,TestS8_ExecLayer_ContinueMode_UnsetTSDInvocation,TestS8_ExecLayer_ResumePicker_UnsetTSDInvocation,TestS8_ExecLayer_Conductor_NoUnsetInvocation,TestS8_ExecLayer_TelegramChannelOwner_NoUnsetInvocation,TestS8_ExecLayer_FreshStartWithMessage_UnsetOnExecOnlyininternal/session/s8_exec_layer_test.gocover Layer 2. The two obsoleteTestIssue680_*cases that asserted the narrow predicate (NoConductorBlock_NoUnset,NoGroupEnvFile_NoUnset) are reframed as*_StripsUnderS8with inverted assertions — the broadening intentionally subsumes them. All remainingTestIssue680_*andTestPersistence_*tests continue to pass unchanged. - Layer 1 — shell unset.
[1.7.39] - 2026-04-20
Fixed
agent-deck session restartno longer destroys a just-created tmux scope (#30): a watchdog double-fire pattern — stop → manualsession start→ watchdog-queuedsession restarton the now-alive session — previously causedRestart()to tear down the fresh tmux/systemd scope regardless of current session state. Reproduced 2026-04-20 at 08:13:05 during the phase-5 resilience test against the v1.7.38 watchdog. Fix: a freshness guard in the CLI handler skipsinst.Restart()(no-op) when the session is already healthy (running/waiting/idle/starting) AND was started within the last 60 seconds. A new persistedInstance.LastStartedAtJSON field carries the start stamp across CLI invocations so the guard works for the short-livedagent-deckprocess. A new--forceflag bypasses the guard for users who genuinely want to recycle a healthy session. Scope is deliberately narrow: the check lives only inhandleSessionRestart—Instance.Restart(),Instance.RestartFresh(), TUI restart paths, and the watchdog Python helper are unchanged. Tests:TestShouldSkipRestart_FreshHealthy,TestShouldSkipRestart_StaleHealthy,TestShouldSkipRestart_ErrorStatus,TestShouldSkipRestart_StoppedStatus,TestShouldSkipRestart_Force,TestShouldSkipRestart_UnknownStartTime,TestShouldSkipRestart_ExactBoundary,TestStart_RecordsLastStartedAtininternal/session/restart_guard_test.go.
[1.7.38] - 2026-04-19
Added
- Declining feedback at any step now sets a persistent opt-out; agent-deck will never auto-prompt again until the user explicitly re-enables. Builds on the v1.7.37 disclosure fix (#679): before v1.7.38, answering
NtoPost this? [y/N]:on the CLI, pressingn/Esc at the TUI confirmation step, or dismissing the dialog mid-flow would print "Not posted." and silently re-prompt on the next launch — with the same public-posting disclosure the user just declined. The opt-out also lives in a new[feedback].disabledfield in~/.agent-deck/config.tomlso the user can see and edit the decision (editing the file manually is honoured the same as answeringn). Both stores are treated as authoritative: either one being "off" suppresses every passive feedback prompt (TUI auto-popup + CLI auto-trigger paths). Five opt-out triggers all land in both stores — (1) CLInat rating, (2) CLINat disclosure, (3) TUI stepRatingn, (4) TUI stepConfirmn/Esc, (5) hand-editingconfig.toml. Re-enable path: runagent-deck feedbackand answeryto the newFeedback is currently disabled. Enable feedback and continue? [y/N]:prompt, which clears both stores before resuming the normal rating flow. TUIctrl+estill bypasses the opt-out (explicit user intent): it re-enablesstate.jsonin-memory before showing the dialog so the newShow()guard does not block the on-demand shortcut. Also fixes a latent global-pointer mutation bug surfaced while writing the tests:session.LoadUserConfigreturned a pointer to the package-leveldefaultUserConfigwhen no config file existed, so mutations (e.g.cfg.Feedback.Disabled = true) leaked across calls; now returns an independent copy viacloneDefaultUserConfig. Tests:TestV1738_CLI_DeclineAtDisclosure_SetsOptOut,TestV1738_CLI_ExplicitOnOptedOut_AsksReenable_DeclineExits,TestV1738_CLI_ExplicitOnOptedOut_AcceptReenable_ClearsBoth,TestV1738_OptOut_PersistsAcrossRestartincmd/agent-deck/feedback_optout_v1738_test.go;TestV1738_FeedbackDialog_ConfirmN_SetsOptOut,TestV1738_FeedbackDialog_ConfirmEsc_SetsOptOut,TestV1738_FeedbackDialog_ConfirmY_DoesNotOptOut,TestV1738_FeedbackDialog_Show_NoOpWhenOptedOut,TestV1738_FeedbackDialog_Show_VisibleWhenEnabledininternal/ui/feedback_dialog_optout_v1738_test.go; legacyTestFeedbackDialog_OnDemandShortcutcase 2 updated to reflect the newShow()-guards-opt-out contract.
[1.7.37] - 2026-04-19
Fixed
- TUI feedback dialog now requires explicit y/N confirmation and shows the exact destination URL, which GitHub account will carry the post, and the full body before sending — closes the #679 privacy gap on the TUI code path, which v1.7.35 and v1.7.36 had fixed only on the CLI side. Under v1.7.36 the in-app feedback popup (ctrl+e or the auto-popup after upgrade) still jumped straight from the comment box to
sender.Send()on Enter, posting the comment publicly to GitHub Discussion #600 under the user'sgh-authenticated account with no disclosure of where it was going, no preview of the body, and no opportunity to decline. It also inheritedSender.Send's three-tier fallback (gh → clipboard+browser → clipboard), so a failedghauth would silently copy the comment to the system clipboard and open a browser window — the exact silent-effect class of bug the CLI fix had removed. This release adds a newstepConfirmbetweenstepCommentandstepSentthat mirrors the CLI's disclosure block verbatim:"This feedback will be posted PUBLICLY on GitHub.", the exact URL (https://github.com/asheshgoplani/agent-deck/discussions/600), theghCLI attribution, the authenticated@<login>resolved viagh api user -q .login(falling back to a generic"your GitHub account"line when gh is unauthenticated), and a four-space-indented preview of the exact body produced byfeedback.FormatComment— the same variable the subsequent gh mutation posts, so preview-vs-post drift is impossible. Confirmation requiresy/Y; any other key (n,N,Esc, Enter, stray input) routes tostepDismissedwith no post. The dialog's internalsendCmdnow callssender.GhCmddirectly with theaddDiscussionCommentGraphQL mutation and surfacesfeedbackSentMsg{err:...}unchanged on failure — the three-tier clipboard/browser fallback can NEVER fire from the TUI consent path, matching the CLI guarantee.stepSentnow renders one of three states off a newsentResult/sentErrpair populated byFeedbackDialog.OnSent(msg)(called fromhome.goonfeedbackSentMsg): a neutral "Posting to Discussion #600 via gh..." line while in-flight,"Posted to Discussion #600. Thanks!"on success, or"Error: could not post via gh. Not sent."with agh auth statushint on failure — removing the ambiguous "Sent!" message that appeared regardless of outcome. Dialog width bumped from 56 to 80 columns so the disclosure URL fits on a single line after the" Where: "prefix, border, and padding.stepCommentEsc also now routes tostepDismissedwith no post (previously it jumped tostepSentand firedsender.Send(""), silently posting an empty-comment feedback entry under the user's gh handle — same bug class). Tests:TestFeedbackDialog_EnterAtComment_TransitionsToConfirm,TestFeedbackDialog_Confirm_N_DismissesWithoutSend,TestFeedbackDialog_Confirm_Esc_DismissesWithoutSend,TestFeedbackDialog_Confirm_Y_TransitionsToSent,TestFeedbackDialog_SendCmd_NoSilentFallback_OnGhError(the critical regression guard — asserts browser/clipboard stay at zero when gh fails),TestFeedbackDialog_ConfirmView_ContainsDisclosure,TestFeedbackDialog_ConfirmView_FallsBackWhenGhLoginEmpty,TestFeedbackDialog_OnSent_ErrorRendersInSentView,TestFeedbackDialog_OnSent_SuccessRendersPostedMessageininternal/ui/feedback_dialog_test.go. Users opting in from the TUI now see the same disclosure they would see fromagent-deck feedback— no code path reaches GitHub under a user's handle without an explicity.
[1.7.36] - 2026-04-19
Fixed
agent-deck feedbackprompts now print interactively to stdout instead of being buffered until the whole flow returns (#679 follow-up, reported by @rgarlik after testing v1.7.35): the v1.7.35 fix for #679 added an explicit disclosure block andPost this? [y/N]confirm — but the disclosure was rendered into astrings.Builderthat was only flushed toos.StdoutafterhandleFeedbackWithSenderreturned. Users typedRating,Comment, and the confirm answer at a blank cursor, and the disclosure they were supposed to read before consenting was never visible while they were being asked to consent. The same buffering predated #679 (theSent! Thankspath had it too) — #679 just made it impossible to ignore. Fix:handleFeedbackWithSendersignature gainsin io.Readerbefore the writer;handleFeedbacknow wiresos.Stdin/os.Stdoutdirectly, so everyfmt.Fprint(w, ...)reaches the terminal immediately. Test gap closed byTestFeedback_PromptPrintsBeforeStdinBlocksincmd/agent-deck/feedback_cmd_test.go: pairsio.Pipefor both stdin and stdout, spawns the handler in a goroutine, reads from the out pipe and asserts "Rating" arrives before sending anything to the in pipe, and times out at 2s if the function buffered. The legacy #679 tests continue to usestrings.Builderfor convenience — that type silently buffers, which is exactly the class of test gap that hid this regression; a follow-up issue tracks adding similar pipe-based smoke tests to every interactive subcommand.
[1.7.35] - 2026-04-19
This is a consolidated batch release. It ships three new fixes (#678, #680, #679) together with the two previously-unreleased chore(release) rebuilds that landed on main but were never tagged: the PR #655 custom-tool compatible_with work (previously slated for v1.7.33) and the PR #580 transition-notify toggle (previously slated for v1.7.34). There are no standalone v1.7.33 or v1.7.34 releases — everything is collapsed into v1.7.35 to avoid tag gaps and user confusion.
Fixed
- Shell / placeholder sessions no longer accumulate duplicate tmux sessions on concurrent restart (#678, reported by @bautrey): the duplicate-guard added in the #596 fix keyed on
CLAUDE_SESSION_ID(and laterGEMINI_/OPENCODE_/CODEX_SESSION_ID) and was a silent no-op for any session that had no tool-level session id — shell sessions, placeholder sessions, and sessions where the tool id had not been captured yet. @bautrey observed 10 duplicate tmux sessions accumulate over a 2-week run on a Linux+systemd host with 30 shell-tool projects, with orphan-vs-real creation gaps of 1–7 seconds that are inconsistent with human double-press and point to concurrentRestart()callers (TUI keymap, HTTP mutator, undo, dialog apply, auto-restart). Fix:sweepDuplicateToolSessionsnow runs a second, unconditional sweep keyed onAGENTDECK_INSTANCE_ID(set on every agent-deck tmux session viaSetEnvironmentat start), so the guard is tool-agnostic. The fallback recreate branch inRestart()is also re-routed through the shared sweep so it benefits from both guards. Tests:TestIssue678_SweepDuplicateToolSessions_ShellUsesInstanceID,TestIssue678_SweepDuplicateToolSessions_ClaudeAlsoInstanceID,TestIssue678_SweepDuplicateToolSessions_ClaudePlaceholderUsesInstanceID,TestIssue678_SweepDuplicateToolSessions_ShellSkipsWhenNoTmuxininternal/session/issue678_shell_dedup_test.go; #666 tests relaxed tofindSweepCall()lookup so both sweeps are tolerated side-by-side. TELEGRAM_STATE_DIRno longer leaks from a conductor group env_file into child sessions (#680): the documented conductor pattern mirrors[conductors.<name>.claude].env_fileand[groups.<name>.claude].env_fileat the same envrc so thatCLAUDE_CONFIG_DIRis consistent across conductor and children. That also smuggledTELEGRAM_STATE_DIRinto every child joining the group, and the telegram plugin auto-started a secondbun telegrampoller per child — all racing the same bot token viagetUpdates(single-consumer API). Observed: 10 concurrent pollers on one bot token, ~10% delivery rate to the intended conductor, no error surfaced. Fix: inbuildEnvSourceCommand, after sourcing the group env_file,conductorOnlyEnvStripExpremitsunset TELEGRAM_STATE_DIRwhen the session is (a) not a conductor itself AND (b) in a group paired with a[conductors.<group>]block. Conductors keep the variable; unrelated groups are unchanged; no schema change. Tests:TestIssue680_ChildSession_StripsTelegramStateDir,TestIssue680_ConductorSession_KeepsTelegramStateDir,TestIssue680_ChildSession_NoConductorBlock_NoUnset,TestIssue680_ChildSession_NoGroupEnvFile_NoUnsetininternal/session/issue680_env_leak_test.go. Doc updated inconductor/conductor-claude.md.agent-deck feedbacknow requires explicit consent before posting publicly (#679, reported by @rgarlik): the feedback CLI posted comments to the public Feedback Hub discussion using the user's localghCLI authentication — under their own GitHub account, visible to anyone browsing the discussion — with no disclosure before submission. @rgarlik described this as "tacky and a bit creepy" and noted they would not have left feedback had they known. Fix: the CLI now (1) saves the rating to local state BEFORE the disclosure, so declining does not re-prompt on the next run; (2) prints an explicit disclosure block — public URL, "posted via theghCLI",@<login>as fetched bygh api user -q .login(withyour GitHub accountfallback), and the exact body that will be posted (theFormatCommentoutput, not a prettier lookalike that could drift); (3) promptsPost this? [y/N]:with default-N — onlyy/yescase-insensitive after trim confirms; (4) on confirm, bypassessender.Send()and callsgh api graphqldirectly, so the clipboard-and-browser fallback can NEVER fire from the CLI path; (5) on gh failure, printsError: could not post via gh. Feedback was NOT sent.plus agh auth statusrecovery hint and exits non-zero with no side effects. Tests:TestIssue679_ConfirmN_DoesNotPost,TestIssue679_ConfirmY_GhSuccess_Posts,TestIssue679_ConfirmY_GhFailure_NoFallback,TestIssue679_EmptyConfirm_DefaultNo,TestIssue679_Confirm_UppercaseY,TestIssue679_Confirm_WhitespaceY,TestIssue679_Disclosure_PreviewMatchesFormatComment,TestIssue679_Disclosure_ShowsLogin,TestIssue679_Disclosure_LoginFallback,TestIssue679_OptOut_Unchangedincmd/agent-deck/feedback_cmd_test.go. README Feedback section rewritten;agent-deck feedback --helpdocuments the flow. Scope locked to the CLI: the TUI feedback dialog (internal/ui/feedback_dialog.go) is unchanged. A private/anonymous feedback channel is being designed for a future release — track in #679.
Added (carried from previously-untagged chore-release work)
- Transition notifications can be suppressed globally or per-session (community PR #580 by @johnuopini, rebased onto current main + dispatch-level regression test added by maintainers, previously slated for v1.7.34): the transition daemon (
agent-deck notify-daemon) unconditionally sent a tmux message to the parent session whenever a child transitionedrunning → waiting|error|idle, which is the right default for conductor patterns but wrong for users who want a child to run silently (batch workloads, one-shot scripts, sessions where the parent is interactive and shouldn't be interrupted). Three layered controls: (1) a global kill switch[notifications].transition_events = falsein~/.agent-deck/config.toml(defaulttrueviaNotificationsConfig.GetTransitionEventsEnabled(), nil-safe); (2) a per-instanceNoTransitionNotifyfield set at creation via--no-transition-notifyon bothagent-deck addandagent-deck launch; (3) a runtime toggleagent-deck session set-transition-notify <id> <on|off>. Three guard sites, defense in depth:TransitionDaemon.syncProfileandTransitionDaemon.emitHookTransitionCandidatescheck both flags before building an event;TransitionNotifier.dispatchre-checkschild.NoTransitionNotifybefore callingSendSessionMessageReliableso deferred/retried events that survive a daemon restart also honour the flag. SQLite schema v6 addsinstances.no_transition_notify INTEGER NOT NULL DEFAULT 0with an idempotentALTER TABLEpath. JSON round-trip usesomitempty. Suppression affects dispatch only — parent linking is untouched, sosession showstill reports the parent. Tests:TestUserConfig_TransitionEventsDefault,TestUserConfig_TransitionEventsExplicitFalse,TestSyncProfileSkipsWhenInstanceNoTransitionNotify,TestDispatchDropsEventWhenChildNoTransitionNotify,TestInstanceNoTransitionNotifyJSONRoundTripininternal/session/transition_notifier_test.go. Co-credit: @johnuopini (PR #580) for the three-layer design, the schema v6 migration, and the CLI plumbing; maintainers rebased across the v1.7.25–v1.7.33 main advance and added the dispatch-level regression test. - Custom tools can declare
compatible_with = "claude"or"codex"to opt into built-in compatibility behavior (community PR #655 by @johnrichardrinehart, rebased onto current main by maintainers, previously slated for v1.7.33): a custom tool's compatibility with built-ins (Claude resume semantics, Codex session-ID detection and resume, restart flow) was inferred by parsing the tool'scommandfield for a literalclaude/codexbasename. Users wrapping those CLIs in a shell script (codex-wrapper,claude-env) lost every downstream capability gate. The newcompatible_withfield in[tools.<name>]is an explicit opt-in that promotes the wrapped tool into the corresponding built-in's behavior set while preserving the custom tool identity (soInstance.Toolstaysmy-codex, notcodex, andUpdateStatus's tmux content-sniff detection does not clobber the configured name once a built-in CLI is detected inside the wrapper). Refactor unifiesisClaudeCommand/isCodexCommandbehind a sharedisCommand(command, wantBase)helper;buildCodexCommandnow resumes through the custom wrapper command (codex-wrapper resume <id>) instead of the hard-coded literalcodex.CreateExampleConfiggains a documented# Example: Custom Codex wrapperblock. Tests:TestIsCodexCompatible_CustomToolCommands,TestIsClaudeCompatible_CustomToolCommandsininternal/session/userconfig_test.go;TestBuildCodexCommand_CustomWrapperPreservesToolIdentityandTestCanRestart_CustomCodexWrapperWithKnownIDininternal/session/instance_test.go;TestCreateExampleConfigDocumentsCompatibleWithininternal/session/userconfig_test.go. Co-credit: @johnrichardrinehart (PR #655) for design and implementation; maintainers rebased across the v1.7.25–v1.7.32 switch-statement expansions.
[1.7.34] - 2026-04-19
Added
- Transition notifications can be suppressed globally or per-session (community PR #580 by @johnuopini, rebased onto current main + dispatch-level regression test added by maintainers): the transition daemon (
agent-deck notify-daemon) unconditionally sent a tmux message to the parent session whenever a child transitionedrunning → waiting|error|idle, which is the right default for conductor patterns but wrong for users who want a child to run silently (batch workloads, one-shot scripts, sessions where the parent is interactive and shouldn't be interrupted). This release adds three layered controls: (1) a global kill switch[notifications].transition_events = falsein~/.agent-deck/config.toml(defaulttrueviaNotificationsConfig.GetTransitionEventsEnabled(), nil-safe); (2) a per-instanceNoTransitionNotifyfield set at creation via--no-transition-notifyon bothagent-deck addandagent-deck launch; (3) a runtime toggleagent-deck session set-transition-notify <id> <on|off>. Three guard sites, defense in depth:TransitionDaemon.syncProfileandTransitionDaemon.emitHookTransitionCandidates(the two daemon entry points) check both flags before building an event;TransitionNotifier.dispatchre-checkschild.NoTransitionNotifybefore callingSendSessionMessageReliable, so deferred/retried events that survive a daemon restart also honour the flag. SQLite schema v6 addsinstances.no_transition_notify INTEGER NOT NULL DEFAULT 0with aCREATE IF NOT EXISTS-safeALTER TABLEpath (idempotent via duplicate-column check). JSON round-trip usesomitemptyso existing session records don't grow. Parent linking itself is untouched — suppression affects dispatch only, sosession showstill reports the parent and the link survives suppression toggles. Tests:TestUserConfig_TransitionEventsDefault,TestUserConfig_TransitionEventsExplicitFalse(nil-safe getter contract),TestSyncProfileSkipsWhenInstanceNoTransitionNotify(resolver reachability check),TestDispatchDropsEventWhenChildNoTransitionNotify(the dispatch-level regression test added during PR review — exercises the fullNotifyTransition → dispatch → guardpath end-to-end with a real profile-scoped Storage, assertstransitionDeliveryDroppedwhen the flag is true so a future refactor that relocates the guard into the daemon layer only cannot silently regress),TestInstanceNoTransitionNotifyJSONRoundTrip(omitempty contract) ininternal/session/transition_notifier_test.go. Co-credit: @johnuopini (PR #580) for the three-layer design, the schema v6 migration, and the CLI plumbing; maintainers rebased across the v1.7.25–v1.7.33 main advance and added the dispatch-level regression test.
[1.7.33] - 2026-04-19
Added
- Custom tools can declare
compatible_with = "claude"or"codex"to opt into built-in compatibility behavior (community PR #655 by @johnrichardrinehart, rebased onto current main by maintainers): previously, a custom tool's compatibility with built-ins (Claude resume semantics, Codex session-ID detection and resume, restart flow) was inferred by parsing the tool'scommandfield for a literalclaude/codexbasename. Users wrapping those CLIs in a shell script (codex-wrapper,claude-env) lost every downstream capability gate —IsClaudeCompatible/IsCodexCompatiblereturned false,buildCodexCommandrefused to prependCODEX_HOME, andRestart()wouldn't reuse the capturedCodexSessionID. The newcompatible_withfield in[tools.<name>]is an explicit opt-in that promotes the wrapped tool into the corresponding built-in's behavior set while preserving the custom tool identity (soInstance.Toolstaysmy-codex, notcodex, andUpdateStatus's tmux content-sniff detection does not clobber the configured name once a built-in CLI is detected inside the wrapper). Refactor unifiesisClaudeCommand/isCodexCommandbehind a sharedisCommand(command, wantBase)helper, andbuildCodexCommandnow resumes through the custom wrapper command (codex-wrapper resume <id>) instead of the hard-coded literalcodex.CreateExampleConfiggains a documented# Example: Custom Codex wrapperblock (field docs + example TOML withcompatible_with = "codex"). Tests:TestIsCodexCompatible_CustomToolCommands(4 cases: built-in,compatible_with=codex, env-prefixed exactcodex, plain wrapper without opt-in) andTestIsClaudeCompatible_CustomToolCommands(addscompatible_with=claudecase) ininternal/session/userconfig_test.go;TestBuildCodexCommand_CustomWrapperPreservesToolIdentity(verifiesAGENTDECK_TOOL=my-codextmux env and resume-through-wrapper) andTestCanRestart_CustomCodexWrapperWithKnownIDininternal/session/instance_test.go;TestCreateExampleConfigDocumentsCompatibleWithininternal/session/userconfig_test.go. Co-credit: @johnrichardrinehart (PR #655) for design and implementation; maintainers rebased across the v1.7.25–v1.7.32 switch-statement expansions (addedcopilottoisBuiltinToolName, preserved the rebased commit authorship).
[1.7.32] - 2026-04-19
Added
- Project skills now work for Gemini, Codex, and Pi sessions — not just Claude (community PR #675 by @masta-g3, cherry-picked onto current main after the parent branch landed in v1.7.31):
agent-deck skill attachand the TUI Skills Manager (s) previously hard-gated onIsClaudeCompatible, materializing every project skill into<project>/.claude/skills/. This release generalizes attachment to a runtime-specific destination: Claude-compatible sessions keep writing to.claude/skills/, while Gemini, Codex, and Pi sessions now materialize into<project>/.agents/skills/. The.agents/skills/path is the cross-tool convention Anthropic published Dec 2025 and that Codex CLI, Gemini CLI, and GitHub Copilot CLI all auto-discover, so skills attached via agent-deck are picked up by those runtimes with no further configuration. The global source registry (~/.agent-deck/skills/sources.toml) and the per-project manifest format (<project>/.agent-deck/skills.toml) are unchanged: the manifest is still authoritative, and the materialized dirs are derived from it. Three explicit migration cases are handled inattachSkillCandidateandApplyProjectSkills: (1) fresh attach materializes into the active runtime's root; (2) re-materialize stale managed target, where the manifest still owns the skill but the on-disk target is missing, re-materializing in place; (3) migrate between managed roots, where the manifest entry points under the other managed root (e.g. the session was restarted with a different-cflag), materializing into the active root first, then removing the old target and updatingTargetPath. When the originalSourcePathis unavailable but the old managed target is still readable, the new root is rebuilt from the existing managed target (copy-only; no symlink indirection since the source is gone). If neither source nor old target is readable, migration fails loudly without mutating the manifest first.skill attachedinspects both known managed roots so stale/unmanaged entries remain visible across runtime switches. The TUI Skills Manager gains aneedsReconcileflag: if any attached skill'sTargetPathdoesn't match the active runtime's expected dir, pressing Enter runs Apply even when the user made no manual changes, triggering the migration automatically when a Claude session is restarted as Gemini/Codex/Pi. Auto-restart after attach/detach fires for Claude, Gemini, and Codex; Pi is opted out of auto-restart since Pi does not yet hot-reload skills (users must manually reload). Defense-in-depth around detach:removeAttachmentTargetrequires the target to be under a known managed skill dir (.claude/skillsor.agents/skills) AND the resolved absolute path must stay inside the base, blocking..-traversal even if the manifest were hand-edited. Tests:TestProjectSkillsDirMapping(5 cases: claude/gemini/codex/pi/shell),TestSkillRuntime_AttachUsesAgentSkillsDirForGemini,TestSkillRuntime_ApplyMigratesBetweenManagedRoots,TestSkillRuntime_AttachMigratesFromExistingTargetWhenSourceUnavailable,TestSkillRuntime_ApplyMigratesFromExistingTargetWhenSourceUnavailable,TestSkillRuntime_DetachRemovesAgentSkillsTargetininternal/session/skills_runtime_test.go;TestSkillDialog_Show_SupportedNonClaudeSession,TestSkillDialog_ApplyUsesAgentSkillsDirForGemini,TestSkillDialog_ShowMarksReconcileNeededForRuntimeSwitchininternal/ui/skill_dialog_test.go;TestApplyProjectSkills_RejectsLegacyFileSkillininternal/session/skills_catalog_test.go. Co-credit: @masta-g3 (PR #675) for the design and implementation; cherry-picked onto current main by maintainers.
[1.7.31] - 2026-04-19
Fixed
- Pi (Inflection AI's
piCLI) is now detected as a first-class tool in CLI and TUI session creation paths (community PR #674 by @masta-g3, rebased onto current main as the original branch was stale):agent-deck add -c pi .and TUI session creation both producedTool="shell"withCommand="pi", even though the rest of the framework (tmux content detection ininternal/tmux/tmux.go, userconfig builtin registration, pattern detection, GetToolIcon) was already wired for Pi. Two missed call sites:cmd/agent-deck/main.go::detectTool(the free-form-cparser) andinternal/ui/home.go(the TUI session creation switch).detectToolnow recognisespivia a newhasCommandTokenhelper that does whitespace-token matching rather thanstrings.Contains, so short ambiguous names like "pi" do not get hijacked by substrings of unrelated words ("epic", "tapioca", "spider", "happiness"). The TUI's inline tool-mapping switch is extracted into a reusablecreateSessionTool(command) (tool, command)and given apicase. Tests:TestDetectTool_Pi(5 cases including the false-match guards) incmd/agent-deck/copilot_detect_test.go;TestCreateSessionTool_Piininternal/ui/home_test.go. Co-credit: @masta-g3 (PR #674) for the original pattern; rebased + extended with the substring-false-match cases by maintainers.
[1.7.30] - 2026-04-19
Fixed
- Per-session color tint now actually renders in the TUI (issue #391): PR #650 (v1.7.27) added the
Instance.Colorfield, TOML validation, CLI plumbing (agent-deck session set <id> color '#FF0000'), SQLite persistence, andlist --jsonexposure — but the TUI dashboard never consumed the field, so users setting a color saw it round-trip through storage yet every row kept the default palette.renderSessionItemnow overrides the title foreground withlipgloss.Color(Instance.Color)when the field is non-empty, preserving the bold/underline weight cues that distinguish Running/Waiting/Error states for colorblind users. EmptyColoris the default and leaves rendering byte-identical to v1.7.29 (fully opt-in). Accepts both accepted formats fromisValidSessionColor:#RRGGBBtruecolor hex and0..255ANSI 256-palette index. Tests:TestIssue391_SessionRow_{HexColorRenderedAsForeground,ANSIIndexColorRendered,EmptyColorLeavesRowUntinted}ininternal/ui/issue391_tui_test.go(Seam A, perinternal/ui/TUI_TESTS.md).
[1.7.29] - 2026-04-19
Added
agent-deck group change <source> [<dest>]— reparent an entire group subtree (issue #447): groups can now be moved as a unit, taking all their subgroups and sessions along.group change personal/project1 workplacesproject1(and everything beneath it) underwork, rewriting every descendant path in one atomic persist. Passing an empty destination (group change work/project1 ""or simply omitting it) promotes the group back to root level. The newGroupTree.MoveGroupTo(source, destParent)engine refuses circular moves (dest == source or a descendant of source), collisions at the target path, and moving the protected default group. Tests:TestMoveGroupTo_{ToRoot,ToOtherParent,WithSubgroups,DestMissing,Circular,NoOpSameParent,Collision,SourceMissing,DefaultGroupForbidden}ininternal/session/groups_reorganize_test.go;TestGroupChange_{RootToSubgroup,MoveToRoot,RejectsCircular}end-to-end CLI incmd/agent-deck/group_change_test.go. TUI group-move dialog is intentionally deferred to a follow-up — the CLI is the minimum shippable surface for the feature.agent-deck session search <query>— full-content search across Claude sessions (issue #483): the global-search index that powers the TUI's (currently-disabled)Goverlay is now exposed as a first-class CLI so users can grep their conversation history from scripts and one-liners. Returns matchingSessionID,cwd, and a 60-char snippet around the first match;--jsonemits a machine-readable shape with{query, results, count}. Flags:--limit N(default 20),--days N(default 30 — searches files modified in the last N days;0= all),--tier {instant|balanced|auto}(default auto — switches based on corpus size). HonoursCLAUDE_CONFIG_DIRso per-profilecdp/cdwsetups search the right tree. Test isolation fix (stripCLAUDE_CONFIG_DIR=from subprocess env inrunAgentDeck) prevents CLI test suites from leaking into the developer's real~/.claude. Tests:TestSessionSearch_{FindsMessageContent,EmptyQuery,NoMatches}incmd/agent-deck/session_search_test.go.
[1.7.28] - 2026-04-19
Added
- Auto-sync session title from Claude Code's
--name//rename(issue #572): when a user startsclaude --name my-feature-branchinside an agent-deck session, or runs/rename …mid-session, the agent-deck title now syncs automatically on the next hook event (SessionStart, UserPromptSubmit, Stop — whichever fires first, typically within seconds). Implementation piggybacks on the existinghook-handlerevent-driven flow: after writing status,applyClaudeTitleSync(instanceID, sessionID)scans~/.claude/sessions/*.jsonfor the matchingsessionId, reads thenamefield, and updates the stored title if non-empty and different. Sessions started without--namekeep their auto-generated adjective-noun title (no change from current behavior). No extra process spawn, no polling — every existing hook event already pays the filesystem cost for status writes. Tests:TestFindClaudeSessionName_{MatchBySessionID,NoMatch,EmptyNameField,MissingSessionsDir},TestApplyClaudeTitleSync_{UpdatesInstance,NoopWhenNameMissing,NoopWhenNameEqualsTitle}incmd/agent-deck/hook_name_sync_test.go(7 cases). agent-deck session move <id> <new-path> [--group …] [--no-restart] [--copy](issue #414): new CLI verb that wraps what used to be a 4-step manual ritual (session set path+group move+cp ~/.claude/projects/<old-slug>/+session restart) into one atomic command. Migrates the Claude Code conversation history at~/.claude/projects/<slug>/to the new slugified path soclaude --resumein the new location picks up prior turns.--copypreserves the old dir instead of renaming (useful when other sessions share history).--groupmoves to a target group in the same operation.--no-restartskips the default post-move restart. SharesSlugifyClaudeProjectPathwith the costs sync path so both call sites encode/and.identically (was previously duplicated ininternal/costs/sync.go). Tests:TestSessionMove_{UpdatesPath,MigratesClaudeProjectDir,CopyFlagPreservesOldDir,GroupFlag,MissingArguments}incmd/agent-deck/session_move_test.go(5 cases).
Fixed
TestWatcherEventDedup-race flake (pre-existing):SaveWatcherEventnow retries up to 5 times on SQLITE_BUSY with linear backoff (10ms, 20ms, …). The op isINSERT OR IGNORE-idempotent so retries are safe. Was failing reliably on release CI under concurrent inserts from two goroutines sharing the same dedup key; retrying resolves the race without weakening the dedup invariant (still exactly 1 row after N racers).
[1.7.27] - 2026-04-19
Fixed
sessionHasConversationDatafalse-negatives caused--session-idinstead of--resumedespite rich jsonl on disk (issue #662): when a conductor's Claude session was restarted while the SessionEnd hook was still flushing the jsonl (a ~100–150ms window),buildClaudeResumeCommandwould observe the file as not-yet-written, fall through to--session-id, and hand the user a blank conversation even though the historic jsonl was on disk. Two layers of fix: (1) a bounded retry-once at the call site (resumeCheckRetryDelay = 200ms) that re-checks after the flush window closes, firing only when the first check is negative ANDClaudeSessionIDis non-empty so the happy path is untouched; (2) a newsession_data_decisionstructured log line carryingconfig_dir,resolved_project_path,encoded_path,primary_path_tested,primary_path_stat_err,fallback_lookup_tried,fallback_path_found, andfinal_resultso production false-negatives can be diagnosed from logs alone without attaching a debugger. Tests:TestIssue662_HiddenDirInPath_EncodesToDoubleDash,TestIssue662_FindsFileViaFallback_WhenPrimaryPathMisses,TestIssue662_DiagnosticLog_CapturesAllDecisionFields,TestIssue662_BuildClaudeResumeCommand_RetriesOnceOnSessionEndRaceininternal/session/issue662_session_data_diag_test.go.
Deferred
- Tmux control-client supervision (issue #659): deferred to its own design cycle. #659's own body notes that "Pipe-death is already recovered" by the v1.7.8 reviver and frames the control-client wrapping as a structural improvement rather than a bug fix, with four open design questions (per-instance vs shared service, TUI coordination, per-user vs global, CLI-without-TUI behaviour). Tracked under issue #668 as an RFC to pick the shape before any code lands.
[1.7.26] - 2026-04-18
Added
- GitHub Copilot CLI support (issue #556): Agent Deck now recognises the standalone
copilotbinary from@github/copilot(GA 2026-02-25) as a first-class tool identity alongsideclaude,gemini,codex, andopencode.agent-deck add -c copilot .lands onTool="copilot"instead of the genericshellfallback, so sessions get the right status detection, the right icon (🐙), and the right per-tool config path. A new[copilot]TOML block (env_filefor now) gives users a home for future knobs without schema churn. TheCopilotOptionsenvelope mirrors the existing Claude/OpenCode shape (SessionMode+ResumeSessionID) and emits--resume(picker) or--resume <id>(direct).IsClaudeCompatible("copilot")is deliberately false — Copilot is not a Claude wrapper, so Claude-only surfaces (--channels,--extra-arg, skill injection, MCP hook paths) stay off. This ships the foundation; deeper hook-based session-id capture (analogous tointernal/session/gemini.goanalytics) will land as a follow-up once Copilot CLI's on-disk session format stabilises. Tests:TestCopilotOptions_{ToolName,ToArgs,MarshalUnmarshalRoundtrip},TestNewCopilotOptions_{Defaults,WithConfig},TestUnmarshalCopilotOptions_WrongTool,TestIsClaudeCompatible_CopilotNotCompatible,TestGetToolIcon_Copilot,TestGetCustomToolNames_CopilotIsBuiltin,TestNewInstanceWithTool_Copilotininternal/session/copilot_test.go;TestDetectToolFromCommand_Copilot,TestDefaultRawPatterns_Copilotininternal/tmux/copilot_test.go;TestDetectTool_Copilotincmd/agent-deck/copilot_detect_test.go.
[1.7.25] - 2026-04-18
Added
- Per-session color tint (plumbing) (issue #391): sessions now carry an optional
colorfield accepting"#RRGGBB"truecolor hex or an ANSI-256 palette index ("0".."255"). Set viaagent-deck session set <id> color "#ff00aa", clear withagent-deck session set <id> color "". The field persists through the SQLitetool_datablob and is exposed viaagent-deck list --json. Validation runs at the CLI boundary so typos ("red", malformed hex, out-of-range ints) are rejected with a diagnostic rather than silently stored. This PR ships the plumbing only — TUI row rendering that consumes the field will land as a follow-up so the change is risk-free for users who don't opt in (default: empty string = no tint, rendering unchanged). Tests:TestIsValidSessionColor(17 cases) +TestSessionSetColor_PersistsValidAndRejectsInvalid(end-to-end CLI round-trip). - Watcher feature documentation (issue #628):
agent-deck watcher --helpnow documents each adapter type (webhook, github, ntfy, slack) with a concrete usage example, required flags, and a pointer to the conversationalwatcher-creatorskill. README gains a dedicated Watchers section describing event routing, per-type flags, routing rules in~/.agent-deck/watcher/<name>/clients.json, and safety guarantees (HMAC-SHA256 verification on GitHub, SQLite event dedup). No behavior change — docs only. Regression test:TestWatcherHelp_MentionsAdapterExamples. [tmux].detach_keyconfig alias for the PTY-attach detach key (issue #434): the detach key was already configurable via[hotkeys].detach = "ctrl+d", but reporters were looking under[tmux]since they think of detach as a tmux-attach concern. This release adds[tmux].detach_keyas an explicit alias with clear precedence —[hotkeys].detachalways wins when both are set, so the alias never changes behavior for users who already configured the hotkey. Default (no config) remainsCtrl+Q. Also documents[hotkeys].detachin the embedded config template so the feature is discoverable at setup time. Tests:TestDetachKey_ConfigurableViaToml(6 sub-cases) ininternal/session/userconfig_test.go.
Fixed
- Sessions silently disappearing from their assigned group after TUI creation (issue #666): the
createSessionFromGlobalSearchpath atinternal/ui/home.go:4762calledh.getCurrentGroupPath()directly and passed its return value intosession.NewInstanceWithGroupAndTool. When the cursor sat on a flatItem that is neither a group nor a session (ItemTypeWindow,ItemTypeRemoteGroup,ItemTypeRemoteSession, or a creating-placeholder) the return was"", and the constructor unconditionally overrode theextractGroupPathdefault with it — producinginst.GroupPath="". The storage layer persisted''and the next reload silently re-derived viaextractGroupPath(ProjectPath), surfacing the session under a path-derived group ("tmp", "home", etc.). Exact user-reported symptom: "session created in group X ends up in a different group, sometimes with a path-derived name." Fix: new helperHome.resolveNewSessionGroup()wrapsgetCurrentGroupPathwith a rescue chain (scoped group →DefaultGroupPath) so the empty string never reaches the constructor. Belt-and-braces guards instorage.gonormalize any remaining empties at save + load as defense-in-depth — the load-time fallback now routes empties toDefaultGroupPathand emits awarnlog (was: silent re-derive). Verified end-to-end with a three-config revert-dance: baseline v1.7.24 reproduces the exact symptom ("GroupPath after reload = tmp, want agent-deck"), partial-fix reproduces the belt-and-braces-only case ("GroupPath collapsed to my-sessions"), both-fixes-on passes. Tests:TestIssue666_ResolveNewSessionGroup_*,TestIssue666_GlobalSearchImport_EndToEnd_PreservesGroupAcrossReloadininternal/ui/issue666_tui_test.go;TestIssue666_LoadRowWithEmptyGroupPath_FallsBackToDefaultNotPathDerived,TestIssue666_LoadRowWithExplicitGroupPath_IsPreserved,TestIssue666_SaveWithGroups_NormalizesEmptyGroupPathininternal/session/issue666_test.go. conductor setupnow auto-remediatesenabledPlugins.telegram= true (issue #666, mechanism 1): v1.7.22 only warned on stderr, users missed the warning in long setup logs, and generic child claude sessions kept flipping toerrorstate when the auto-loaded telegram plugin raced the conductor's poller (409 Conflict → claude exits). Setup now flips the flag tofalsein<profile>/settings.json, preserves all other keys, and prints a loud✓ Auto-disabled …stdout line. Idempotent; missing file / missing key / already-false are all no-ops. Tests:TestDisableTelegramGlobally_*incmd/agent-deck/conductor_cmd_telegram_autofix_test.go.- Respawn-pane restart path now sweeps duplicate cross-tmux tool sessions (issue #666, mechanism 3): the fallback restart branch at
instance.go:4411already killed other agentdeck tmux sessions sharing the sameCLAUDE_SESSION_ID(issue #596 guard). The primary respawn-pane branches did not. Under rare fork-then-edit collisions two agentdeck sessions could runclaude --resumeon the same conversation, stacking two telegram pollers. The newInstance.sweepDuplicateToolSessions()helper runs on every successful respawn for Claude, Gemini, OpenCode, and Codex. Tests:TestIssue666_SweepDuplicateToolSessions_{Claude,Gemini,OpenCode,Codex,SkipsWhenNoSessionID,SkipsWhenNoTmux}ininternal/session/issue666_restart_sweep_test.go.
[1.7.13] - 2026-04-17
Fixed
- Cross-session
xsend-output transferred unpredictable content (issue #598): when the user pressedxto transfer output from session A to session B, the transferred text was often from a prior conversation rather than the most-recent assistant response. Root cause:getSessionContentread the last assistant message viaInstance.ClaudeSessionID, but that stored ID goes stale every time Claude is resumed — it continues pointing at the prior JSONL while the liveCLAUDE_SESSION_IDin tmux env holds the current UUID. The CLIsession outputpath already usedGetLastResponseBestEffortwith stale-ID recovery; the TUI path didn't. Fix addsInstance.RefreshLiveSessionIDs()(Claude + Gemini) and routesgetSessionContentthrough a testablegetSessionContentWithLive(inst, liveID)helper that prefers the live tmux env ID over any stored value before the JSONL lookup. Tmux scrollback fallback is unchanged. Tests:TestGetSessionContentWithLive_PrefersFreshIDOverStoredStaleID,TestGetSessionContentWithLive_KeepsStoredIDWhenLiveEmpty,TestGetSessionContentWithLive_NoOpForNonClaudeToolininternal/ui/send_output_content_test.go;TestInstance_RefreshLiveSessionIDs_NoOpWhenTmuxSessionNil,TestInstance_RefreshLiveSessionIDs_NoOpForNonAgenticToolininternal/session/instance_test.go.
[1.7.10] - 2026-04-17
Fixed
session send --no-waitreliability on freshly-launched Claude sessions (issue #616): the pre-v1.7.10 code skipped all readiness detection in--no-waitmode, then ran a 1.2-second verification loop. On cold Claude launches (where TUI mount takes 5-40s with MCPs), the loop counted startup-animation "active" status as submission success and returned before the composer rendered — leaving the pasted message typed-but-not-submitted. The 30-50% failure rate users reported is now 0% in 10 consecutive live-boundary runs. Fix has three layers: a 5s preflight barrier waiting for the Claude composer❯to render, a 500ms post-composer settle for React mount, and an extended 6s verification budget (from 1.2s).maxFullResends=-1is preserved — the #479 regression (double-send) still passes. Non-Claude tools skip the preflight (their prompt shapes differ). Tests:TestSendNoWait_ReEntersWhenComposerRendersLate,TestAwaitComposerReadyBestEffort_*,TestSendWithRetryTarget_NoWait_BudgetSpansRealisticClaudeStartupincmd/agent-deck/session_send_test.go.
[1.7.6] - 2026-04-17
Fixed
- Priority inversion on
CLAUDE_CONFIG_DIR: explicit[conductors.<name>.claude]and[groups."<name>".claude]TOML overrides now beat the shell-wideCLAUDE_CONFIG_DIRenv var. Previously, developer shells that exportedCLAUDE_CONFIG_DIRvia profile aliases (cdp/cdw) silently shadowed every per-conductor/per-group override — making config.toml overrides unreliable for the exact users most likely to use them. Profile/global fallbacks remain weaker than env (they're shell-wide too). Scope:GetClaudeConfigDirForInstance,GetClaudeConfigDirSourceForInstance,IsClaudeConfigDirExplicitForInstanceininternal/session/claude.go. Group-less variants unchanged. - Web terminal
TestTmuxPTYBridgeResize-race flake: addedptmxMu sync.RWMutexprotecting the PTY file handle against concurrent Close/Resize. Previously intermittent on GH Actions release runs (v1.7.4, v1.7.5).
[1.5.4] - 2026-04-16
Added
- Per-group Claude config overrides (
[groups."<name>".claude]). (Base implementation by @alec-pinson in PR #578) - In-product feedback feature: CLI
agent-deck feedback, TUICtrl+E, three-tier submit (GraphQL, clipboard, browser).
Fixed
- Session persistence: tmux servers now survive SSH logout on Linux+systemd hosts via
launch_in_user_scopedefault (v1.5.2 hotfix). (docs/SESSION-PERSISTENCE-SPEC.md) - Custom-command Claude sessions (conductors) now resume from latest JSONL on restart.
[1.6.0] - 2026-04-16
v1.6.0 is the Watcher Framework milestone. Event-driven automation via five adapter types (webhook, ntfy, GitHub, Slack, Gmail), a self-improving routing engine, health alerts bridge, and conductor-style on-disk layout.
Added
- Watcher engine — event-driven automation framework with five adapters (webhook, ntfy, GitHub, Slack, Gmail), SQLite-backed dedup, HMAC-SHA256 verification, and self-improving routing via triage sessions. See
internal/watcher/. - Watcher health alerts bridge — opt-in
[watcher.alerts]config block wires engine health state to Telegram/Slack/Discord with per-(watcher x trigger) 15-minute debounce. Seeinternal/watcher/health_bridge.go. Closes REQ-WF-3. - Watcher folder hierarchy — on-disk state reorganized to
~/.agent-deck/watcher/(singular) mirroring the conductor folder pattern. Shared files (CLAUDE.md, POLICY.md, LEARNINGS.md, clients.json) at root, per-watcher subdirs (meta.json, state.json, task-log.md). Closes REQ-WF-6. - Per-watcher health fields —
agent-deck watcher list --jsonnow exposeslast_event_ts,error_count,health_statusper watcher. - Watcher CLI — 8 subcommands: create, start, stop, status, list, logs, import, install-skill.
Changed
- BREAKING: Watcher data directory renamed —
~/.agent-deck/watchers/is now~/.agent-deck/watcher/(singular). A compatibility symlinkwatchers -> watcher/is created automatically on first boot so existing scripts continue to work. The symlink will be removed in v1.7.0. Update any hardcoded paths.
[1.5.1] - 2026-04-13
Patch release fixing 7 bugs reported by users and merging 3 community PRs.
Fixed
- Clear host terminal scrollback on session detach. (#419)
- Web terminal resize now uses pty.Setsize + tmux resize-window for correct dimensions. (#568)
- Narrow controlSeqTimeout to ESC-only and ignore SIGINT during attach, fixing Ctrl+C forwarding. (#571)
- Allow underscore character in TUI dialog text inputs. (#573)
- Allow Esc to dismiss setup wizard on welcome step. (#564, #566)
- Initialize branchAutoSet when worktree default_enabled is true. (#561, #562)
- Harden sandbox runtime probes and respawn bash wrapping. (#575)
- Preserve existing OpenCode session binding on restart. (#576)
Added
- Arrow-key navigation for confirm dialogs. (#557)
[1.5.0] - 2026-04-10
v1.5.0 is the Premium Web App milestone. The web interface gets P0/P1 bug fixes, performance optimization (first-load wire size from 668 KB to under 150 KB gzipped), UX polish, and automated visual regression testing.
Fixed
- [Phase 5, v1.4.1] Six critical regressions: Shift+letter key drops (CSI u), tmux scrollback clearing, mousewheel [0/0], conductor heartbeat on Linux, tmux PATH detection, bash -c quoting. (REG-01..06)
- [Phase 6] Mobile hamburger menu clickable at all viewports <=768px with systematic 7-level z-index scale. (WEB-P0-1)
- [Phase 6] Profile switcher: single profile shows read-only label; multi profile shows non-interactive list with help text for CLI switching. (WEB-P0-2)
- [Phase 6] Session title truncation: action buttons use absolute positioning with hover-reveal, no longer reserving 90px of space. (WEB-P0-3)
- [Phase 6] Write-protected mode: mutationsEnabled=false hides all write buttons; toast auto-dismisses at 5s with stack cap of 3 and history drawer for dismissed toasts. (WEB-P0-4, POL-7)
- [Phase 7] Terminal panel fills container on attach, no empty gray space below terminal. (WEB-P1-1)
- [Phase 7] Sidebar width fluid via clamp(260px, 22vw, 380px) on screens >=1280px. (WEB-P1-2)
- [Phase 7] Sidebar row density increased to 40px per row (from ~52px); 20+ sessions visible at 1080p. (WEB-P1-3)
- [Phase 7] Empty-state dashboard uses centered card layout with max-width 1024px. (WEB-P1-4)
- [Phase 7] Mobile topbar overflow menu for controls on viewports <600px. (WEB-P1-5)
Performance
- [Phase 8] gzip compression on static file handler via klauspost/compress/gzhttp; ~518 KB saved per cold load. (PERF-A)
- [Phase 8] Chart.js script tag deferred to unblock HTML parser. (PERF-B)
- [Phase 8] xterm canvas addon removed (dead code); fallback chain is now WebGL then DOM only. (PERF-C)
- [Phase 8] WebGL addon lazy-loaded on desktop only; mobile skips import entirely, saving 126 KB. (PERF-D)
- [Phase 8] Event listener leak fixed via AbortController; listener count at rest drops from 290 to ~50. (PERF-E)
- [Phase 8] Search input debounced at 250ms; typing lag drops from 33ms to <8ms. (PERF-F)
- [Phase 8] SessionRow memoized; group collapse no longer rerenders 152 unrelated components. (PERF-G)
- [Phase 8] ES modules bundled via esbuild with code splitting and cache-busted filenames. (PERF-H)
- [Phase 8] Cost batch endpoint converted from GET to POST, preventing 414 URI Too Long. (PERF-I)
- [Phase 8] Immutable cache headers on hashed assets (1-year max-age). (PERF-J)
- [Phase 8] SessionList virtualized for 50+ sessions via hand-rolled useVirtualList hook. (PERF-K)
Added
- [Phase 9] Skeleton loading state with CSS-only animate-pulse during initial sidebar render. (POL-1)
- [Phase 9] Action button 120ms opacity fade transitions with prefers-reduced-motion support. (POL-2)
- [Phase 9] Profile dropdown filters out _* test profiles, scrollable at 300px max-height. (POL-3)
- [Phase 9] Group divider gap reduced from 48px to 12-16px for tighter sidebar density. (POL-4)
- [Phase 9] Cost dashboard uses locale-aware currency formatting via Intl.NumberFormat. (POL-5)
- [Phase 9] Light theme re-audited across all surfaces for contrast and consistency. (POL-6)
- [Phase 10] Playwright visual regression tests with committed baselines; CI blocks merge on >0.1% pixel diff. (TEST-A)
- [Phase 10] Lighthouse CI on every PR with byte-weight hard gates and soft performance thresholds. (TEST-B)
- [Phase 10] Functional E2E tests for session lifecycle and group CRUD. (TEST-C)
- [Phase 10] Mobile E2E at 3 viewports: iPhone SE, iPhone 14, iPad. (TEST-D)
- [Phase 10] Weekly regression alerting workflow: runs visual + Lighthouse, posts issue on failure. (TEST-E)
[1.4.2] - 2026-04-09
Fixed
- Restore TUI keyboard input on all terminals (iTerm2, Ghostty, WezTerm, Kitty, tmux). Arrow keys, j/k, and mouse scroll were broken in v1.4.1 because
CSIuReaderwrappingos.Stdinmade Bubble Tea skip raw-mode setup (tcsetattr), leaving the TTY in cooked mode and echoing escape sequences as text. Fixes #539, #544. (#541) - Fix CSI final-byte whitelist in
csiuReader.translateto include SGR mouse terminators (M/m), so mouse events are no longer corrupted when the reader is used. (#541) - Remove
EnableKittyKeyboard(os.Stdout)/DisableKittyKeyboard(os.Stdout)pairs from all four attach paths (attachCmd,remoteCreateAndAttachCmd,attachWindowCmd,remoteAttachCmd) ininternal/ui/home.go. WritingESC[>1uto the outer terminal beforetmux attachput Ghostty (and other kitty-protocol terminals) into CSI u mode; tmux could not translate these sequences for the inner application, causing arrow keys to appear as raw escape codes. Restores v0.28.3 attach behavior. Fixes #546. (#547)
Added
- Integration tests for TUI keyboard input (
internal/integration/tui_input_test.go) to prevent future regressions in raw-mode setup and CSI handling.
[0.25.1] - 2026-03-11
Added
- Expose custom tools in the Settings panel default-tool picker so configured tools can be selected without editing
config.tomlby hand.
[0.25.0] - 2026-03-11
Added
- Add
preview.show_notessupport so the notes section can be hidden from the preview pane while keeping the main session view intact. - Add Gemini hook management commands and hook-based Gemini session/status sync, including install, uninstall, and status flows.
- Add remote-session lifecycle actions in the TUI so remote sessions can be restarted, closed, or deleted directly from the session list.
- Add richer Slack bridge context so forwarded messages include stable sender/channel enrichment.
Fixed
- Preserve hook-derived session identity across empty hook payloads by persisting a read-time session-id anchor fallback.
- Improve Telegram bot mention stripping and username handling so bridge messages route more reliably in group chats.
- Avoid repeated regexp compilation in hot paths by hoisting
regexp.MustCompilecalls to package-level variables.
[0.24.1] - 2026-03-07
Fixed
- Restore instant preview rendering from cached content during session navigation and immediately after returning from an attached session, removing placeholder delays introduced in
0.24.0.
[0.24.0] - 2026-03-07
Added
- Add
internal/sendpackage consolidating all send verification functions (prompt detection, composer parsing, unsent-prompt checks) into a single location. - Add Codex readiness detection:
waitForAgentReadyandsendMessageWhenReadynow gate oncodex>prompt before delivering messages to Codex sessions. - Add session death detection in
--waitmode:waitForCompletiondetects 5 consecutive status errors and returns exit code 1 instead of hanging indefinitely. - Add heartbeat migration function (
MigrateConductorHeartbeatScripts) that auto-refreshes installed scripts to the latest template. - Add exit 137 (SIGKILL) investigation report documenting root cause as Claude Code limitation with reproduction steps and mitigation strategies.
- Add exit 137 mitigation guidance to shared conductor CLAUDE.md and GSD conductor SKILL.md.
- Promote 27 validated conductor learnings to shared docs: 10 universal orchestration patterns to conductor CLAUDE.md, 6 GSD-specific learnings to gsd-conductor SKILL.md, 11 operational patterns to agent-deck-workflow SKILL.md.
Fixed
- Harden Enter retry loop: retry every iteration for first 5 attempts (previously every 3rd), increasing ambiguous budget from 2 to 4.
- Scope heartbeat scripts to conductor's own group instead of broadcasting to all sessions in the profile.
- Honor
heartbeat_interval = 0as disabled: skip heartbeat daemon installation during conductor setup. - Add enabled-status guard to heartbeat scripts so they exit silently when conductor is disabled.
- Fix
-cand-gflag co-parsing so both flags work together inagent-deck add. - Improve
--no-parenthelp text to referenceset-parentfor later parent linking.
Changed
- Clean up all six conductor LEARNINGS.md files: mark promoted entries, remove retired entries, consolidate duplicates.
[0.23.0] - 2026-03-07
Added
- Add status detection integration tests: real tmux status transition cycles, pattern detection, and tool config verification.
- Add conductor pipeline integration tests: send-to-child delivery, cross-session event write-watch, heartbeat round-trips, and chunked send delivery.
- Add edge case integration tests: skills discover-attach verification.
- Complete milestone v1.1 Integration Testing (38 integration tests across 6 phases).
Fixed
- Handle nested binary paths in release tarballs so self-update works with both flat and directory-wrapped archives.
[0.22.0] - 2026-03-06
Added
- Add integration test framework: TmuxHarness (auto-cleanup real tmux sessions), polling helpers (WaitForCondition, WaitForPaneContent, WaitForStatus), and SQLite fixture helpers (NewTestDB, InstanceBuilder).
- Add session lifecycle integration tests (start, stop, fork, restart) using real tmux sessions with automatic cleanup.
- Add session lifecycle unit tests covering start, stop, fork, and attach operations with tmux verification.
- Add status lifecycle tests for sleep/wake detection and SQLite persistence round-trips.
- Add skills runtime tests verifying on-demand skill loading, pool skill discovery, and project skill application.
Changed
- Reformat agent-deck and session-share SKILL.md files to official Anthropic skill-creator format with proper frontmatter.
- Add $SKILL_DIR path resolution to session-share skill for plugin cache compatibility.
- Register session-share skill in marketplace.json for independent discoverability.
- Update GSD conductor skill content in pool directory with current lifecycle documentation.
[0.21.1] - 2026-03-06
Fixed
- Propagate forked
AGENTDECK_INSTANCE_IDvalues correctly so Claude hook subprocesses update the child session instead of the parent. - Fully honor
[tmux].inject_status_line = falseby skipping tmux notification/status-line mutations when status injection is disabled. - Add Gemini
--yoloCLI overrides foragent-deck add,agent-deck session start, and TUI session creation. - Clamp final TUI frames to the terminal viewport so navigation cannot spill duplicate footer/help rows into scrollback.
[0.21.0] - 2026-03-06
Added
- Add built-in Pi tool support, configurable hotkeys, session notes in the preview pane, and optional follow-CWD-on-attach behavior in the TUI.
- Add OpenClaw gateway integration with sync, status, list, send, and bridge commands for managing OpenClaw agents as agent-deck sessions.
- Add per-window tmux tracking in the session list with direct window navigation and AI tool badges.
- Add remote session creation from the TUI (
n/Non remote groups and remote sessions). - Add remote binary management with automatic install during
agent-deck remote addand the newagent-deck remote updatecommand. - Add configurable
[worktree].branch_prefixfor new worktree sessions. - Add Vimium-style jump mode for session-list navigation.
Changed
- Significantly reduce TUI lag during navigation, attach/return flows, preview rendering, and background status refreshes.
Fixed
- Enable Claude-specific session management features for custom tools that wrap the
claudebinary. - Prevent non-interactive installs from hanging when
tmuxis missing by skipping interactive prompts and failing fast whensudowould block.
[0.20.2] - 2026-03-03
Fixed
- Recover automatically when tmux startup fails due to a stale/unreachable default socket by quarantining the stale socket and retrying session creation once. This prevents
failed to create tmux session ... server exited unexpectedlystartup failures.
[0.20.1] - 2026-03-03
Added
- Add Discord bot support to the conductor bridge with setup flow and config support (
[conductor.discord]), including slash commands (/ad-status,/ad-sessions,/ad-restart,/ad-help) and heartbeat alert delivery to Discord.
Changed
- Reduce tmux
%output-driven status update frequency for chatty sessions to lower parsing overhead and smooth CPU usage under heavy output.
Fixed
- Restrict Discord slash commands to the configured Discord channel so conductor control stays channel-scoped.
[0.20.0] - 2026-03-01
Added
- Add remote SSH session support with two workflows:
agent-deck add --ssh <user@host> [--remote-path <path>]to launch/manage sessions on remote hosts.agent-deck remote add/list/sessions/attach/renameto manage and interact with remote agent-deck instances.
- Add remote sessions to the TUI under
remotes/<name>, with keyboard attach (Enter) and rename (r) support. - Add JSON session fields
ssh_hostandssh_remote_pathinagent-deck list --jsonoutput.
Fixed
- Recover repository state after the broken PR #260 merge and re-apply the feature cleanly on
main. - Harden SSH command handling by shell-quoting remote command parts and SSH host/path values.
- Prevent remote name parsing collisions by rejecting
:in remote names. - Preserve full multi-word titles in
agent-deck remote rename. - Stabilize remote session rendering order and snapshot-copy remote data during TUI rebuilds for safer async updates.
[0.19.19] - 2026-02-26
Fixed
- Make Homebrew update installs resilient to stale local tap metadata by running
brew updatebeforebrew upgradeinagent-deck update. - Update Homebrew check/install guidance to show the full install command (
brew update && brew upgrade asheshgoplani/tap/agent-deck) so users can copy-paste a working path directly.
[0.19.18] - 2026-02-26
Fixed
- Make
agent-deck updateHomebrew-aware end-to-end:--checknow shows the correctbrew upgradecommand and interactive install can execute the Homebrew upgrade path directly instead of failing after confirmation. - Harden conductor/daemon binary resolution to prefer the active executable path and robust PATH ordering, avoiding stale
/usr/local/binpicks that could drop parent transition notifications. - Prevent TUI freezes during create/fork worktree flows by moving worktree creation into async command execution instead of blocking the Enter key handler.
- Enforce Claude conversation ID deduplication on storage saves (CLI + TUI paths) so duplicate
claude_session_idownership does not persist, with deterministic older-session retention.
Changed
- Add conductor permission-loop troubleshooting guidance (
allow_dangerous_mode/dangerous_mode) in README and troubleshooting docs.
[0.19.17] - 2026-02-26
Added
- Add Docker sandbox mode for sessions (TUI + CLI), including per-session containers, hardened container defaults, and sandbox docs/config references.
Fixed
- Preserve non-sandbox tmux startup behavior while keeping sandbox dead-pane restart support.
- Strengthen
session send --no-wait/ launch no-wait initial-message delivery with retry+verification to reduce dropped prompt submits. - Route transition notifications through explicit parent linkage only (no conductor fallback), and align conductor/README guidance with parent-linked routing.
[0.19.16] - 2026-02-26
Fixed
- Restore OpenCode/Codex status detection for active output by matching both
status_detailsandstatusfields in tmux JSON pane formats. - Eliminate a worktree creation TOCTOU race in
addby creating/checking candidate worktree paths in one flow and retrying with suffixed names when collisions happen. - Avoid false Claude tool detection for shell wrappers by validating shell executables exactly and only classifying wrappers as Claude when
claudeappears as a command token. - Resolve duplicate group-name move failures in the TUI by moving sessions using canonical group paths while preserving user-facing group labels.
[0.19.15] - 2026-02-25
Added
- Add soft-select path editing and filterable recent-path suggestions in the New Session dialog, including matching-count hints and focused keyboard help text.
- Add compact notifications mode (
[notifications].minimal = true) with status icon/count summary in tmux status-left, includingstartingsessions in the active count. - Add conductor heartbeat rules externalization via
HEARTBEAT_RULES.md(global default plus per-profile override support in the bridge runtime). - Add proactive conductor context management with
clear_on_compactcontrols (conductor setup --no-clear-on-compactand per-conductor metadata) and synchronousPreCompacthook registration.
Fixed
- Preserve ANSI color/styling in session preview rendering while keeping status/readiness parsing reliable by normalizing ANSI where plain-text matching is required.
- Restore original tmux
status-leftcorrectly when clearing notifications, including intentionally empty original values. - Guard analytics cache map access across UI and background worker paths to avoid concurrent map read/write races during background status updates.
- Prevent self-update prompts/flows on Homebrew-managed installs.
[0.19.14] - 2026-02-24
Added
- Add automatic heartbeat script migration for existing conductors so managed
heartbeat.shfiles are refreshed to the current generated template during conductor migration checks. - Add
--cmdparsing support for tool commands with inline args inadd/launch(for example-c "codex --dangerously-bypass-approvals-and-sandbox"), with automatic wrapper generation when needed.
Fixed
- Switch generated conductor heartbeat sends to non-blocking
session send --no-wait -q, eliminating recurringagent not ready after 80 secondstimeout churn for busy conductors. - Improve
add/launchCLI help and JSON output to expose resolved command/wrapper details and avoid confusing launch behavior when mixing tool names with extra args. - Fix parent/group friction for conductor-launched sessions by allowing explicit
-g/--groupto override inherited parent group while keeping parent linkage for notifications.
Changed
- Expand README and CLI reference guidance for conductor-launched sessions (
--no-parentvs auto-parent), transition notifier behavior, and safe command patterns.
[0.19.13] - 2026-02-24
Added
- Add built-in event-driven transition notifications (
notify-daemon) that nudge a parent session first, then fall back to a conductor session when a child transitions fromrunningtowaiting/error/idle. - Add
--no-parentand default auto-parent linking foradd/launchwhen launched from a managed session (AGENT_DECK_SESSION_ID), with conflict protection for--parent+--no-parent. - Add
parent_session_idandparent_project_pathtoagent-deck session show --json. - Add conductor setup/status/teardown integration for the transition notifier daemon so always-on notifications can be installed and managed with conductor commands.
Fixed
- Reduce SQLite lock contention under concurrent daemon and CLI usage by avoiding unnecessary schema-version writes and retrying transient busy errors during storage migration/open.
- Improve status-driven notification reliability for fast tool completions by combining watcher updates with direct hook-file fallback reads and hook-based terminal transition candidates.
[0.19.11] - 2026-02-23
Added
- Add shared and per-conductor
LEARNINGS.mdsupport with setup/migration wiring so conductors can capture reusable orchestration lessons over time.
Fixed
- Harden
launch -mandsession sendmessage delivery for Claude by using fresh pane captures, robust composer prompt parsing (including wrapped prompts), and stronger Enter retry verification to avoid pasted-but-unsent prompts. - Improve readiness detection for non-Claude tools (including Codex) by treating stable
idle/waitingstates as ready, preventing false startup timeouts when launching with an initial message. - Fix launch/session-start messaging semantics so non-
--no-waitflows correctly report message sent state (message_pending=false).
[0.19.10] - 2026-02-23
Fixed
- Make
agent-deck session send --waitandagent-deck session outputresilient when Claude session IDs are missing/stale by using best-effort response recovery (tmux env refresh, disk sync fallback, and terminal parse fallback). - Improve Claude send verification to catch pasted-but-unsent prompts even after an initial
waitingstate, reducing false positives where a prompt was pasted but never submitted. - Update conductor bridge messaging to use single-call
session send --wait -q --timeout ...flow for Telegram/Slack and heartbeat handling, reducing extra polling steps and improving reliability. - Reject non-directory legacy file skills when attaching project skills, and harden skill materialization to recover from broken symlinks and symlinked target-path edge cases.
Changed
- Update conductor templates/docs and launcher helper scripts to prefer one-shot launch/send flows and single-call wait semantics for smoother orchestration.
[0.19.9] - 2026-02-20
Fixed
- Fix terminal style leakage after tmux attach by waiting for PTY output to drain and resetting OSC-8/SGR styles before the TUI redraws.
- Harden
agent-deck session senddelivery by retryingEnteronly when Claude shows a pasted-but-unsent marker ([Pasted text ...]) and avoiding unnecessary retries once status is alreadywaiting/idle.
Changed
- Clarify tmux wait-bar shortcut docs: press
Ctrl+b, release, then press1–6to jump to waiting sessions.
[0.19.8] - 2026-02-20
Fixed
- Fix
agent-deck session show --jsonMCP output marshalling by emitting concrete local/global/project values instead of a method reference inmcps.local(#213). - Fix conductor daemon Python resolution by preferring
python3from the active shellPATHbefore fallback absolute paths (#215).
[0.19.7] - 2026-02-20
Fixed
- Fix heartbeat script profile text stamping so generated
heartbeat.shuses the real profile name in message text for non-default profiles (#207, contributed by @CoderNoveau). - Fix conductor bridge message delivery when the conductor session is idle by using non-blocking
session send --no-wait, and apply this in the embedded runtime bridge template with regression coverage (#210, contributed by @sjoeboo).
[0.19.6] - 2026-02-19
Added
- Add
manage_mcp_jsonconfig option to disable all.mcp.jsonwrites, plus a LOCAL-scope MCP Manager warning when disabled (#197, contributed by @sjoeboo). - Split conductor guidance into shared mechanism (
CLAUDE.md) and policy (POLICY.md) with per-conductor policy override support (#201).
Fixed
- Fix conductor setup migration so legacy generated per-conductor
CLAUDE.mdfiles are updated safely for the policy split while preserving custom and symlinked files (#201). - Fix launchd and systemd conductor daemon units to include the installed
agent-deckbinary directory inPATHso bridge/heartbeat jobs can find the CLI (#196, contributed by @sjoeboo). - Support environment variable expansion (
$VAR,${VAR}) in path-based config values and unify path expansion behavior across config consumers (#194, contributed by @tiwillia).
[0.19.5] - 2026-02-18
Changed
- Remap TUI shortcuts to reduce conflicts:
mopens MCP Manager,sopens Skills Manager (Claude), andMmoves sessions between groups.
Fixed
- Reduce Codex session watcher CPU usage by rate-limiting expensive on-disk session scans and avoiding redundant tmux environment writes.
- Fix macOS installer crash on default Bash 3.2 by replacing associative arrays in
install.shwith Bash 3.2 compatible helper functions (#192, contributed by @slkiser).
[0.19.4] - 2026-02-18
Added
- Add pool-focused type-to-jump navigation and scrolling in the Skills Manager (
P) dialog for long lists. - Add stricter Skills Manager available list behavior so project attach/detach is driven by the managed pool source.
Changed
- Update README and skill references with Skills Manager usage, skill CLI command coverage, and skills registry path documentation.
[0.19.0] - 2026-02-17
Added
- Add
agent-deck webmode to run the TUI and web UI server together, with browser terminal streaming and session menu APIs (#174, contributed by @PatrickStraeter) - Add web push notification and PWA support for web mode (
--push,--push-vapid-subject,--push-test-every) (#174) - Add macOS MacPorts support to
install.shwith--pkg-managerselection alongside Homebrew (#187, contributed by @bronweg)
Fixed
- Fix
allow_dangerous_modepropagation for Claude sessions created from the UI flow (#185, contributed by @daniel-shimon) - Fix TUI scroll artifacts caused by width-measurement inconsistency and control-character leakage in preview rendering (#182, contributed by @jsvana)
- Fix Claude busy-pattern false positives from welcome-banner separators by anchoring spinner regexes to line start (#179, contributed by @mtparet)
- Harden web mode by restricting WebSocket upgrades to same-host origins and preserving auth token in push deep links (#174)
[0.18.1] - 2026-02-17
Added
- Add
--waitflag tosession sendfor blocking until command completion (#180)
[0.18.0] - 2026-02-17
Added
- Add Codex notify hook integration for instant session status updates
- Add notification show_all mode to display all notifications at once
- Add automatic bridge.py updates when running
agent-deck update(#178)
Fixed
- Fix: handle error returns in test cleanup functions
- Fix: bridge.py not updating with agent-deck binary updates (#178)
[0.17.0] - 2026-02-16
Added
- Add top-level rename command with validation (#176, contributed by @nlenepveu)
- Add Slack user ID authorization for conductors (#170, contributed by @mtparet)
- Custom CLAUDE.md paths via symlinks for conductors (#173, contributed by @mtparet)
Fixed
- Fix: remove thread context fetching from Slack handler (#175, contributed by @mtparet)
- Fix: prevent worktree nesting when creating from within worktrees (#177)
[0.16.0] - 2026-02-14
Added
- Add
--teammate-modetmux option to Claude session launcher for shared terminal pairing (#168, contributed by @jonnocraig) - Add Slack integration and cross-platform daemon support (#169, contributed by @mtparet)
- Add Claude Code lifecycle hooks for real-time status detection (instant green/yellow/gray transitions without tmux polling)
- Add first-launch prompt asking users to install hooks (preserves existing Claude settings.json)
- Add
agent-deck hooks install/uninstall/statusCLI subcommands for manual hook management - Add
hooks_enabledconfig option under[claude]to opt out of hook-based detection - Add StatusFileWatcher (fsnotify) for instant hook status file processing
- Add
AGENTDECK_INSTANCE_IDenv var export for Claude hook subprocess identification - Add acknowledgment awareness to hook fast path (attach turns session gray,
ukey turns it orange) - Add
llms.txtfor LLM discoverability, fix schema version, add FAQ entries (#167)
Fixed
- Fix middot
·spinner character not detected as busy indicator when followed by ellipsis (BusyPatterns regex now includes·)
Changed
- Sessions with active hooks skip tmux content polling entirely (2-minute timeout as crash safety net only)
- Existing sessions without hooks continue using polling (seamless hybrid mode)
[0.15.0] - 2026-02-13
Added
- Add
inject_status_lineconfig option under[tmux]to disable tmux statusline injection, allowing users to keep their own tmux status bar (#157) - Add system theme option: sync TUI theme with OS dark/light mode (#162)
- Improve quick session creation: inherit path, tool, and options from hovered session (#165)
Fixed
- Fix Claude session ID not updating after
/clear,/fork, or/compactby syncing from disk (#166) - Restore delay between paste and Enter in
SendKeysAndEnterto prevent swallowed input in tmux (#168)
[0.14.0] - 2026-02-12
Added
- Add title-based status detection fast-path: reads tmux pane titles (Braille spinner / done markers) to determine Claude session state without expensive content scanning
- Add
RefreshPaneInfoCache()for zero-subprocess pane title fetching via PipeManager - Add worktree finish dialog (
Wkey): merge branch, remove worktree, delete branch, and clean up session in one step - Add worktree branch badge
[branch]in session list for worktree sessions - Add worktree info section in preview pane (branch, repo, path, dirty status)
- Add worktree dirty status cache with lazy 10s TTL checks
- Add repository worktree summary in group preview when sessions share a repo
- Add
esc to interruptfallback to Claude busy patterns for older Claude Code versions - Add worktree section to help overlay
Fixed
- Fix busy indicator false negatives for
·and✻spinner chars with ellipsis (BusyRegexp now correctly catches all spinner frames with active context) - Remove unused
matchesDetectPatternsfunction (lint warning) - Fix
startingandinactivestatus mapping in instance status update
[0.13.0] - 2026-02-11
Added
- Add quick session creation with
Shift+Nhotkey: instant session with auto-generated name and smart defaults (#161) - Add Docker-style name generator (adjective-noun) with ~10,000 unique combinations
- Add
--quick/-Qflag toagent-deck addCLI for auto-named sessions - Smart defaults: inherits tool, options, and path from most recent session in the group
[0.12.3] - 2026-02-11
Fixed
- Fix busy detection window reduced from 25 to 10 lines for faster status transitions
- Fix conductor group permanently pinned to top of group list
- Optimize status detection pipeline for faster green/yellow transitions
- Add spinner movement detection tests for stuck spinner validation
[0.12.2] - 2026-02-10
Fixed
- Fix
session sendintermittently dropping Enter key (and sometimes text) due to tmux race condition between two separatesend-keysprocess invocations (tmux#1185, tmux#1517, tmux#1778) - Fix all 6 send-keys + Enter code paths to use atomic tmux command chaining (
;) in a single subprocess - Add retry with verification to CLI
session sendfor resilience under heavy load or SSH latency
[0.12.1] - 2026-02-10
Fixed
- Fix Shift+R restart race condition with animation guard on restart and fork hotkeys (#147)
- Fix settings menu viewport cropping in small terminals with scroll windowing (#149)
- Fix .mcp.json clobber by preserving existing entries when managing MCP sessions (#146)
- Fix --resume-session arg parsing by registering it in the arg reorder map (#145)
Added
- Add tmux option overrides via
[tmux]config section in config.toml (#150) - Add opencode fork infrastructure with OpenCodeOptions for model/agent/fork support (#148)
[0.12.0] - 2026-02-10
Added
- Multiple conductors per profile: create N named conductors in a single profile
agent-deck conductor setup <name>with--heartbeat,--no-heartbeat,--descriptionflagsagent-deck conductor teardown <name>or--allto remove conductorsagent-deck conductor listwith--jsonand--profilefiltersagent-deck conductor status [name]shows all or specific conductor health
- Two-tier CLAUDE.md for conductors: shared knowledge base + per-conductor identity
- Shared
CLAUDE.mdat conductor root with CLI reference, protocols, and rules - Per-conductor
CLAUDE.mdwith name and profile substitution
- Shared
- Conductor metadata via
meta.jsonfiles for name, profile, heartbeat settings, and description - Auto-migration of legacy single-conductor directories to new multi-conductor format
- Bridge (Telegram) updated for dynamic conductor discovery via
meta.jsonscanning normalizeArgsutility for consistent flag parsing across all CLI commands- Status field added to
agent-deck list --jsonoutput
[0.11.4] - 2026-02-09
Added
- Add
allow_dangerous_modeoption to[claude]config section- Passes
--allow-dangerously-skip-permissionsto Claude (opt-in bypass mode) dangerous_mode = truetakes precedence when both are set- Based on contribution by @daniel-shimon (#152), with architectural fixes (#153)
- Passes
- New permission flag persists per-session across fork and restart operations
[0.11.3] - 2026-02-09
Fixed
- Fix deleted sessions reappearing after reload or app restart
SaveInstances()now deletes stale rows from SQLite within the same transaction- Added explicit
DeleteInstance()call in the delete handler as a safeguard - Root cause:
INSERT OR REPLACEnever removed deleted session rows from the database
- Update profile detection to check for
state.db(SQLite) in addition to legacysessions.json - Update uninstall script to count sessions from SQLite instead of JSON
Added
- Persist UI state (cursor position, preview mode, status filter) across restarts via SQLite metadata
- Save group expanded/collapsed state immediately on toggle
- Discord badge and link in README
Changed
- Simplify multi-instance coordination: remove periodic primary re-election from background worker
- Create new profiles with SQLite directly instead of empty
sessions.json - Update troubleshooting docs for SQLite-based recovery
[0.11.2] - 2026-02-06
Fixed
- Enable notification bar on all instances, not just the primary
- Previously secondary instances had notifications disabled entirely
- All instances share the same SQLite state, so they produce identical bar content
[0.11.1] - 2026-02-06
Changed
- Replace file-based lock with SQLite heartbeat-based primary election for multi-instance coordination
- Dynamic failover: if the primary instance crashes, a secondary takes over the notification bar within ~12 seconds
- Eliminates stale
.lockfiles that required manual cleanup after crashes ElectPrimary()uses atomic SQLite transactions to prevent split-brain
Removed
- Remove
acquireLock,releaseLock,getLockFilePath,isProcessRunning(replaced by SQLite election)
[0.11.0] - 2026-02-06
Changed
- Replace
sessions.jsonwith SQLite (state.db) as the single source of truth- WAL mode for concurrent multi-instance reads/writes without corruption
- Auto-migrates existing
sessions.jsonon first run (renamed to.migratedas backup) - Removes fragile full-file JSON rewrites, backup rotation, and fsnotify dependency
- Tool-specific data stored as JSON blob in
tool_datacolumn for schema flexibility
- Replace fsnotify-based storage watcher with SQLite metadata polling
- Simpler, works reliably on all filesystems (9p, NFS, WSL)
- 2-second poll interval using
metadata.last_modifiedtimestamp
- Replace tmux rate limiter and watcher with control mode pipes (PipeManager)
- Event-driven status detection via
tmux -Ccontrol mode - Zero-subprocess architecture: no more
tmux capture-panefor idle sessions
- Event-driven status detection via
Added
- Add
internal/statedbpackage: SQLite wrapper with CRUD, heartbeat, status sync, and change detection - Add cross-instance acknowledgment sync via SQLite (ack in instance A visible in instance B)
- Add instance heartbeat table for tracking alive TUI processes
- Add
StatusSettingsin user config (reserved for future status detection settings)
[0.10.20] - 2026-02-06
Added
- Add
worktree finishcommand to merge branch, remove worktree, and delete session in one step (#140)- Flags:
--into,--no-merge,--keep-branch,--force,--json - Abort-safe: merge conflicts trigger
git merge --abort, leaving everything intact
- Flags:
- Auto-cleanup worktree directories when deleting worktree sessions (CLI
removeand TUIdkey)
Fixed
- Fix orphaned MCP server processes (Playwright CPU leak) by killing entire process group
- Set
Setpgid=trueso grandchild processes (npx/uvx spawned) share a process group - Shutdown now sends SIGTERM/SIGKILL to
-pid(group) instead of just the parent
- Set
- Fix test cleanup killing user sessions with "test" in their title
- Fix session rename lost during reload race condition
[0.10.19] - 2026-02-05
Fixed
- Fix session rename not persisting (#141)
lastLoadMtimewas not updated after saves, causing mtime check to incorrectly abort subsequent saves- Renames, reorders, and other non-force saves now persist correctly
[0.10.18] - 2026-02-05
Added
- Add Codex CLI
--yoloflag support (#142)- Global config:
[codex] yolo_mode = truein config.toml - Per-session override in New Session dialog (checkbox)
- Flag preserved across session restarts
- Settings panel toggle for global default
- Global config:
- Add unified
OptionsPanelinterface for tool-specific options (#143)- New tools can add options by implementing interface + 1 case in
updateToolOptions() - Shared
renderCheckboxLine()helper ensures visual consistency across panels
- New tools can add options by implementing interface + 1 case in
Fixed
- Fix
ClaudeOptionsPanel.Blur()not resetting focus stateIsFocused()now correctly returns false after blur
[0.10.17] - 2026-02-05
Fixed
- Fix sessions disappearing after creation in TUI
- Critical saves (create, fork, delete, restore) now bypass mtime check that was incorrectly aborting saves
- Sessions created during reload are now properly persisted to JSON before triggering reload
- Fix import function to recover orphaned agent-deck sessions
- Press
ito import sessions that exist in tmux but are missing from sessions.json - Recovered sessions are placed in a "Recovered" group for easy identification
- Press
[0.10.16] - 2026-02-05
Fixed
- Fix garbled input at update confirmation prompt
- Add
drainStdin()to flush terminal input buffer before prompting - Use
TCFLSHioctl to discard pending escape sequences and accidental keypresses - Switch from
fmt.Scanlntobufio.NewReaderfor more robust input handling
- Add
[0.10.15] - 2026-02-05
Fixed
- Fix TUI overwriting CLI changes to sessions.json (#139)
- Add mtime check before save: compares file mtime against when we last loaded, aborts save and triggers reload if external changes detected
- Fix TOCTOU race condition:
isReloadingflag now protected by mutex in all 6 read locations - Add filesystem detection for WSL2/NFS: warns users when on 9p/NFS/CIFS/SSHFS mounts where fsnotify is unreliable
[0.10.14] - 2026-02-04
Fixed
- Fix critical OOM crash: Global Search was loading 4.4 GB of JSONL content into memory and opening 884 fsnotify directory watchers (7,900+ file descriptors), causing agent-deck to balloon to 6+ GB RSS until macOS killed it
- Temporarily disable Global Search at startup until memory-safe implementation is complete
- Optimize directory traversal to skip
tool-results/andsubagents/subdirectories (never contain JSONL files) - Limit fsnotify watchers to project-level directories only (was recursively watching ALL subdirectories)
- Add max client cap (100) per MCP socket proxy to prevent unbounded goroutine growth from reconnect loops
- Broken MCPs (e.g.,
reddit-yilinwith 72 connects/30s) could spawn unlimited goroutines and scanner buffers
- Broken MCPs (e.g.,
Changed
- Global Search (
Gkey) is temporarily disabled pending a memory-safe reimplementation- Will be re-enabled once balanced tier is enforced for large datasets and memory limits are properly applied
[0.10.13] - 2026-02-04
Added
- Migrate all logging to structured JSONL via
log/slogwith automatic rotation- JSONL output to
~/.agent-deck/debug.logwith component-based filtering (jq 'select(.component=="pool")') - Automatic log rotation via lumberjack (configurable size, backups, retention in
[logs]config) - Event aggregation for high-frequency MCP socket events (1 summary per 30s instead of 40 lines/sec)
- In-memory ring buffer with crash dump support (
kill -USR1 <pid>) - Optional pprof profiling on
localhost:6060 - 9 log components: status, mcp, notif, perf, ui, session, storage, pool, http
- New
[logs]config options:debug_level,debug_format,debug_max_mb,debug_backups,debug_retention_days,debug_compress,ring_buffer_mb,pprof_enabled,aggregate_interval_secs
- JSONL output to
Fixed
- Fix MCP pool infinite restart loop causing 45 GB memory leak over 15 hours
- Add
StatusPermanentlyFailedstatus: broken MCPs are disabled after 10 consecutive failures - Fix leaked proxy context/goroutines when
Start()fails during restart - Reset failure counters after proxy is healthy for 5+ minutes (allows transient failure recovery)
- Skip permanently failed proxies in health monitor for both socket and HTTP pools
- Add
- Fix inconsistent debug flag check in tmux.go (
== "1"changed to!= ""to match rest of codebase)
[0.10.12] - 2026-02-04
Fixed
- Fix tmux pane showing stale conversation history after session restart (#138)
- Clear scrollback buffer before respawn to remove old content
- Invalidate preview cache on restart for immediate refresh
- Kill old tmux session in fallback restart path to prevent orphans
[0.10.11] - 2026-02-04
Added
- Add
mcp_default_scopeconfig option to control where MCPs are written (#137)- Set to
"global"or"user"to stop agent-deck from overwriting.mcp.jsonon restart - Affects MCP Manager default tab, CLI attach/detach defaults, and session restart regeneration
- Defaults to
"local"(no breaking change)
- Set to
[0.10.10] - 2026-02-04
Added
- Add configurable worktree path templates via
path_templateconfig option (#135, contributed by @peteski22)- Template variables:
{repo-name},{repo-root},{branch},{session-id} - Overrides
default_locationwhen set; falls back to existing behavior when unset - Integrated at all 4 worktree creation points (CLI add, CLI fork, TUI new session, TUI fork)
- Backported from njbrake/agent-of-empires
- Template variables:
[0.10.9] - 2026-02-03
Removed
- Remove dead GoReleaser ldflags targeting non-existent
main.version/commit/datevars - Remove redundant
make releasetarget (superseded byrelease-local) - Remove unused deprecated wrappers
NewStorage()andGetStoragePath() - Remove unused test helpers file (
internal/ui/test_helpers.go) - Remove stale
home.go.bakbackup file
[0.10.8] - 2026-02-03
Fixed
- Fix shell dying after tool exit by removing
execprefix from all tool commands (#133, contributed by @kurochenko)- When Claude, Gemini, OpenCode, Codex, or generic tools exit, users now return to their shell prompt instead of a dead tmux pane
- Enables workflows where tools run inside wrappers (e.g., nvim) that should survive tool exit
[0.10.7] - 2026-02-03
Added
- Add
make release-localtarget for local GoReleaser releases (no GitHub Actions dependency)
[0.10.6] - 2026-02-03
Fixed
- TUI freezes with 40+ sessions: Parallel status polling replaces sequential loop that couldn't complete within 2s tick
- 10-worker pool via errgroup for concurrent tmux status checks
- Instance-level RWMutex prevents data races between background worker and TUI rendering
- Tiered polling skips idle sessions with no activity (10s recheck gate)
- 3-second timeout on CapturePane/GetWindowActivity prevents hung tmux calls from blocking workers
- Timeout preserves previous status instead of flashing RED
- Race detector (
-race) enabled in tests and CI
[0.10.5] - 2026-02-03
Fixed
- Fix intermittent
zsh: killeddue to memory exhaustion (#128): Four memory leaks causing macOS OOM killer (Jetsam) to SIGKILL agent-deck after prolonged use with many sessions:- Cap global search content buffer memory at 100MB (configurable via
memory_limit_mb), evict oldest 25% of entries when exceeded - Release all content memory and clear file trackers on index Close()
- Stop debounce timers on watcher shutdown to prevent goroutine leaks
- Prune stale analytics/activity caches every 20 seconds (were never cleaned up)
- Clean up analytics caches on session delete
- Clear orphaned MCP socket proxy request map entries on client disconnect and MCP failure
- Prune LogWatcher rate limiters for removed sessions every 20 seconds
- Cap global search content buffer memory at 100MB (configurable via
[0.10.4] - 2026-02-03
Added
- Prevent nested agent-deck sessions (#127): Running
agent-deckinside a managed tmux session now shows a clear error instead of causing infinite...output. Read-only commands (version,help,status,list,session current/show/output,mcp list/attached) still work for debugging
[0.10.3] - 2026-02-03
Fixed
- Global search unusable with large datasets (#125): Multiple performance fixes make global search work with multi-GB session data:
- Remove rate limiter from initial load (was causing 42+ minute "Loading..." on large datasets)
- Read only first 32KB of files for metadata in balanced tier (was reading entire files, some 800MB+)
- Early exit from parsing once metadata found (SessionID/CWD/Summary)
- Parallelize disk search with 8-worker pool (was sequential)
- Debounced async search on UI thread (250ms debounce + background goroutine)
- Default
recent_daysto 30 when not set (was 0 = all time)
- G key didn't open Global Search: Help bar showed
G Globalbut the key actually jumped to the bottom of the list.Gnow opens Global Search (falls back to local search if global search is disabled)
[0.10.2] - 2026-02-03
Fixed
- Global search freezes when typing with many sessions (#125): Search ran synchronously on the UI thread, blocking all input while scanning files from disk. Now uses debounced async search (250ms debounce + background goroutine) so the UI stays responsive regardless of data size
- G key didn't open Global Search: Help bar showed
G Globalbut the key actually jumped to the bottom of the list.Gnow opens Global Search (falls back to local search if global search is disabled)
[0.10.1] - 2026-02-02
Fixed
- GREEN status not detecting Claude 2.1.25+ spinners: Prompt detector only checked braille spinner chars (
⠋⠙⠹...) as busy guards, missing the asterisk spinners (✳✽✶✢) used since Claude 2.1.25. This caused sessions to show YELLOW instead of GREEN while Claude was actively working - Prompt detector missing whimsical word timing patterns: Only "thinking" and "connecting" were recognized as active processing. Now detects all 90+ whimsical words (e.g., "Hullaballooing", "Clauding") via the universal
…+tokenspattern - Spinner check range too narrow: Only checked last 3 lines for spinner chars, but Claude's UI can push the spinner line 6+ lines from the bottom (tip lines, borders, status bar). Expanded to last 10 lines
- Acknowledge override on attach: Attaching to a waiting (yellow) session would briefly acknowledge it, but the background poller immediately reset it back to waiting because the prompt was still visible. Prompt detection now respects the acknowledged state
[0.10.0] - 2026-02-02
Changed
- Group dialog defaults to root mode on grouped sessions: Pressing
gwhile the cursor is on a session inside a group now opens the "Create New Group" dialog in root mode instead of subgroup mode. Tab toggle still switches to subgroup. Group headers still default to subgroup mode. This makes it easier for users with all sessions in groups to create new root-level groups
Added
- MCP socket pool resilience docs: README updated to mention automatic ~3s crash recovery via reconnecting proxy
- Pattern override documentation:
config.toml initnow includes documentation forbusy_patterns_extra,prompt_patterns_extra, andspinner_chars_extrafields for extending built-in tool detection patterns
[0.9.2] - 2026-01-31
Fixed
- 492% CPU usage: Main TUI process was consuming 5 CPU cores due to reading 100-841MB JSONL files every 2 seconds per Claude session. Now uses tail-read (last 32KB only) with file-size caching to skip unchanged files entirely
- Duplicate notification sync: Both foreground TUI tick and background worker were running identical notification sync every 2 seconds, spawning duplicate tmux subprocesses. Removed foreground sync since background worker handles everything
- Excessive tmux subprocess spawns:
GetEnvironment()spawnedtmux show-environmentevery 2 seconds per Claude session for session ID lookup. Added 30-second cache since session IDs rarely change - Unnecessary idle session polling: Claude/Gemini/Codex session tracking updates now skip idle sessions where nothing changes
Added
- Configurable pattern detection system:
ResolvedPatternswith compiled regexes replaces hardcoded busy/prompt detection, enabling pattern overrides viaconfig.toml
[0.9.1] - 2026-01-31
Fixed
- MCP socket proxy 64KB crash:
bufio.Scannerdefault 64KB limit caused socket proxy to crash when MCPs like context7 or firecrawl returned large responses. Increased buffer to 10MB, preventing orphaned MCP processes and permanent "failed" status - Faster MCP failure recovery: Health monitor interval reduced from 10s to 3s for quicker detection and restart of failed proxies
- Active client disconnect on proxy failure: When socket proxy dies, all connected clients are now actively closed so reconnecting proxies detect failure immediately instead of hanging
Added
- Reconnecting MCP proxy (
agent-deck mcp-proxy): New subcommand replacesnc -Uas the stdio bridge to MCP sockets. Automatically reconnects with exponential backoff when sockets drop, making MCP pool restarts invisible to Claude sessions (~3s recovery)
[0.9.0] - 2026-01-31
Added
- Fork worktree isolation: Fork dialog (
Fkey) now includes an opt-in worktree toggle for git repos. When enabled, the forked session gets its own git worktree directory, isolating Claude Code project state (plan, memory, attachments) between parent and fork (#123) - Auto-suggested branch name (
fork/<session-name>) in fork dialog when worktree is enabled - CLI
session forkcommand gains-w/--worktree <branch>and-b/--new-branchflags for worktree-based forks - Branch validation in fork dialog using existing git helpers
[0.8.99] - 2026-01-31
Fixed
- Session reorder persistence: Reordering sessions with Shift+K/J now persists across reloads. Added
Orderfield to session instances, normalized on every move, and sorted by Order on load. Legacy sessions (no Order field) preserve their original order via stable sort (#119)
[0.8.98] - 2026-01-30
Fixed
- Claude Code 2.1.25+ busy detection: Claude Code 2.1.25 removed
"ctrl+c to interrupt"from the status line, causing all sessions to appear YELLOW/GRAY instead of GREEN while working. Detection now uses the unicode ellipsis (…) pattern: active state shows"✳ Gusting… (35s · ↑ 673 tokens)", done state shows"✻ Worked for 54s"(no ellipsis) - Status line token format detection updated to match new
↑/↓arrow format ((35s · ↑ 673 tokens)) - Content normalization updated for asterisk spinner characters (
·✳✽✶✻✢) to prevent false hash changes
Changed
- Analytics preview panel now defaults to OFF (opt-in via
show_analytics = truein config.toml)
Added
- 6 new whimsical thinking words:
billowing,gusting,metamorphosing,sublimating,recombobulating,sautéing - Word-list-independent spinner detection regex for future-proofing against new Claude Code words
[0.8.97] - 2026-01-29
Fixed
- CLI session ID capture:
session start,session restart,session fork, andtrynow persist Claude session IDs to JSON immediately, enabling fork and resume from CLI-only workflows without the TUI - Fork pre-check recovery:
session forkattempts to recover missing session IDs from tmux before failing, fixing sessions started before this fix - Stale comment in
loadSessionDatacorrected to reflect lazy loading behavior
Added
PostStartSync()method on Instance for synchronous session ID capture after Start/Restart (CLI-only; TUI uses its existing background worker)
[0.8.96] - 2026-01-28
Added
- HTTP Transport Support for MCP Servers: Native support for HTTP/SSE MCP servers with auto-start capability
- Add
[mcps.X.server]config block for auto-starting HTTP MCP servers (command, args, env, startup_timeout, health_check) - Add
mcp serverCLI commands:start,stop,statusfor managing HTTP MCP servers - Add transport type indicators in
mcp list:[S]=stdio,[H]=http,[E]=sse - Add TUI MCP dialog transport indicators with status:
●=running,○=external,✗=stopped - Add HTTP server pool with health monitoring and automatic restart of failed servers
- External server detection: if URL is already reachable, use it without spawning a new process
Changed
- MCP dialog now shows transport type and server status for each MCP
mcp listoutput now includes transport type column
[0.8.95] - 2026-01-28
Changed
- Performance: TUI startup ~3x faster (6s → 2s for 44 sessions)
- Batch tmux operations: ConfigureStatusBar (5→1 call), EnableMouseMode (6→2 calls) using command chaining
- Lazy loading: defer non-essential tmux configuration until first attach or background tick
- Skip UpdateStatus and session ID sync at load time (use cached status from JSON)
Added
- Add
ReconnectSessionLazy()for deferred session configuration - Add
EnsureConfigured()method for on-demand tmux setup - Add
SyncSessionIDsToTmux()method for on-demand session ID sync - Background worker gradually configures unconfigured sessions (one per 2s tick)
[0.8.94] - 2026-01-28
Added
- Add undo delete (Ctrl+Z) for sessions: press Ctrl+Z after deleting a session to restore it including AI conversation resume. Supports multiple undos in reverse order (stack of up to 10)
- Show ^Z Undo hint in help bar (compact and full modes) when undo stack is non-empty
- Add Ctrl+Z entry to help overlay (? screen)
Changed
- Update delete confirmation dialog: "This cannot be undone" → "Press Ctrl+Z after deletion to undo"
[0.8.93] - 2026-01-28
Fixed
- Fix
gkey unable to create root-level groups when any group exists (#111). Add Tab toggle in the create-group dialog to switch between Root and Subgroup modes - Fix
nkey handler using display name constant instead of path constant for default group
Added
- Group DefaultPath tracking: groups now track the most recently accessed session's project path via
updateGroupDefaultPath
[0.8.92] - 2026-01-28
Fixed
- Fix CI test failure in
TestBindUnbindKeyby making default key restore best-effort inUnbindKey
[0.8.91] - 2026-01-28
Fixed
- Fix TUI cursor not following notification bar session switch after detach (Ctrl+b N during attach now moves cursor to the switched-to session on Ctrl+Q)
[0.8.90] - 2026-01-28
Fixed
- Fix quit dialog ("Keep running" / "Shut down") hidden behind splash screen, causing infinite hang on quit with MCP pool
- Fix
isQuittingflag not reset when canceling quit dialog with Esc - Add 5s safety timeouts to status worker and log worker waits during shutdown
[0.8.89] - 2026-01-28
Fixed
- Fix shutdown hang when quitting with "shut down" MCP pool option (process
Wait()blocked forever on child-held pipes) - Set
cmd.Cancel(SIGTERM) andcmd.WaitDelay(3s) on MCP processes for graceful shutdown with escalation - Add 5s safety timeout to individual proxy
Stop()and 10s overall timeout to poolShutdown()
[0.8.88] - 2026-01-28
Fixed
- Fix stale expanded group state during reload causing cursor jumps when CLI adds a session while TUI is running
- Fix new groups added via CLI appearing collapsed instead of expanded
- Eliminate redundant tree rebuild and viewport sync during reload (performance)
[0.8.87] - 2026-01-28
Added
- Add
envfield to custom tool definitions for inline environment variables (closes #101) - Custom tools from config.toml now appear in the TUI command picker with icons
- CLI
agent-deck add -c <custom-tool>resolves tool to actual command automatically
Fixed
- Fix
[worktree] default_location = "subdirectory"config not being applied (fixes #110) - Add
--locationCLI flag to override worktree placement per session (siblingorsubdirectory) - Worktree location now respects config in both CLI and TUI new session dialog
[0.8.86] - 2026-01-28
Fixed
- Fix changelog display dropping unrecognized lines (plain text paragraphs now preserved)
- Fix trailing-slash path completion returning directory name instead of listing contents
- Reset path autocomplete state when reopening new session dialog
- Fix double-close on LogWatcher and StorageWatcher (move watcher.Close inside sync.Once)
- Fix log worker shutdown race (replace unused channel with sync.WaitGroup)
- Fix CapturePane TOCTOU race with singleflight deduplication
Added
- Comprehensive test suite for update package (CompareVersions, ParseChangelog, GetChangesBetweenVersions, FormatChangelogForDisplay)
[0.8.85] - 2026-01-27
Fixed
- Clear MCP cache before regeneration to prevent stale reads
- Cursor jump during navigation and view duplication bugs
[0.8.83] - 2026-01-26
Fixed
- Resume with empty session ID opens picker instead of random UUID
- Subgroup creation under selected group
Added
- Fast text copy (
c) and inter-session transfer (x)
[0.8.79] - 2026-01-26
Added
- Gemini model selection dialog (
Ctrl+G) - Configurable maintenance system with TUI feedback
- Improved status detection accuracy and Gemini prompt caching
.envfile sourcing support for sessions ([shell] env_files)- Default dangerous mode for power users
Fixed
- Sync session IDs to tmux env for cross-project search
- Write headers to Claude config for HTTP MCPs
- OpenCode session detection persistence and "Detecting session..." bug
- Preserve parent path when renaming subgroups
[0.8.69] - 2026-01-20
Added
- MCP Manager user scope: attach MCPs to
~/.claude.json(affects all sessions) - Three-scope MCP system: LOCAL, GLOBAL, USER
- Session sharing skill (export/import sessions between developers)
- Scrolling support for help overlay on small screens
Fixed
- Prevent orphaned test sessions
- MCP pool quit confirmation
[0.8.67] - 2026-01-20
Added
- Notification bar enabled by default
- Thread-safe key bindings for background sync
- Background worker self-ticking for status updates during
tea.Exec ctrl+c to interruptas primary busy indicator detection- Debug logging for status transitions
Changed
- Reduced grace period from 5s to 1.5s for faster startup detection
- Removed 6-second animation minimum; uses status-based detection
- Hook-based polling replaces frequent tick-based detection
[0.8.65] - 2026-01-19
Improved
- Notification bar performance and active session detection
- Increased busy indicator check depth from 10 to 20 lines
[0.6.1] - 2025-12-24
Changed
- Replaced Aider with OpenCode - Full integration of OpenCode (open-source AI coding agent)
- OpenCode replaces Aider as the default alternative to Claude Code
- New icon: 🌐 representing OpenCode's open and universal approach
- Detection patterns for OpenCode's TUI (input box, mode indicators, logo)
- Updated all documentation, examples, and tests
0.1.0 - 2025-12-03
Added
-
Terminal UI - Full-featured TUI built with Bubble Tea
- Session list with hierarchical group organization
- Live preview pane showing terminal output
- Fuzzy search with
/key - Keyboard-driven navigation (vim-style
hjkl)
-
Session Management
- Create, rename, delete sessions
- Attach/detach with
Ctrl+Q - Import existing tmux sessions
- Reorder sessions within groups
-
Group Organization
- Hierarchical folder structure
- Create nested groups
- Move sessions between groups
- Collapsible groups with persistence
-
Intelligent Status Detection
- 3-state model: Running (green), Waiting (yellow), Idle (gray)
- Tool-specific busy indicator detection
- Prompt detection for Claude Code, Gemini CLI, OpenCode, Codex
- Content hashing with 2-second activity cooldown
- Status persistence across restarts
-
CLI Commands
agent-deck- Launch TUIagent-deck add <path>- Add session from CLIagent-deck list- List sessions (table or JSON)agent-deck remove <id|title>- Remove session
-
Tool Support
- Claude Code - Full status detection
- Gemini CLI - Activity and prompt detection
- OpenCode - TUI element detection
- Codex - Prompt detection
- Generic shell support
-
tmux Integration
- Automatic session creation with unique names
- Mouse mode enabled by default
- 50,000 line scrollback buffer
- PTY attachment with
Ctrl+Qdetach
Technical
- Built with Go 1.24+
- Bubble Tea TUI framework
- Lip Gloss styling
- Tokyo Night color theme
- Atomic JSON persistence
- Cross-platform: macOS, Linux