git worktree add -b fails outright when the branch already exists, even
if the target directory is free. A random fast-forward handle commonly
collides with a branch an earlier, already-removed worktree left behind.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The hook installer only ever wrote ~/.claude/settings.json, so a codex
session reported no activity and never posted its first prompt — leaving
it on the daemon's generated handle with no haiku name.
Adds a Target enum (claude-code, codex) over the hook file's location and
event set. One script and one marker serve both; codex swaps Notification
for PermissionRequest and has no StopFailure, so it never reports Error.
Both agents spell the prompt event UserPromptSubmit and put the text in
the payload's `prompt`, so autoname needs no change.
Codex will not run a hook it has not been told to trust; `maestro hooks
install` says so, and the approval stays the operator's.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Every attached client pushed its own dimensions straight to
`tmux resize-window`, so two clients of different sizes resized the
shared window past each other on every report — 198 resizes in 90
seconds on a four-session daemon. Each flip repaints a full-screen agent
at a new height and orphans the rows it drew at the old one, leaving a
stale input box the operator types into and reads as a stuck pane.
The window now goes to the smallest attached client per axis, tmux's own
rule for multiple clients, and only when that size actually changes.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The TUI read its leader from `[tui].leader`; the web used `DEFAULT_LEADER`,
a module constant. Rebinding one left the other on `ctrl-a`, so the two
escape hatches hanging off the leader (`leader ctrl-k`, `leader leader`)
moved on one client and not the other.
`GET /api/info` now carries `leader`. The web binds that when it arrives and
rebuilds the leader-owning `CommandSet`, so the `?` overlay and the command
bar render the configured chord; an absent or empty field — an older daemon,
or an unreachable `/api/info` — leaves it on `ctrl-a`.
Chose the existing `[tui].leader` served as-is over a new shared key: one
knob, no precedence rules between two spellings, and no migration for
operators who already set it. The name is a wart — it is the leader for every
client now, not the TUI's — and `docs/COMMAND-BAR.md` and `docs/INSTALL.md`
say so.
Add Session::last_focused_at, shipped on the wire like every other
field. Both POST /api/sessions/:id/focus (focused: true) and opening
/api/attach/:id stamp it and emit SessionUpdated, so a client that
only attaches sorts the same as one that reports focus explicitly.
Losing focus does not clear it, and a never-focused session has None,
which is what lets the command bar's empty-query ordering
(docs/COMMAND-BAR.md) sort it after every session that has focused.
The daemon's auto-install skipped on presence, so a hook installed
before #131 never gained UserPromptSubmit and no session was ever named
after its prompt.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Keystrokes and PTY echo are tiny frames. With Nagle on, each can sit up to
40ms waiting for an ACK before the kernel sends it — invisible over
loopback, plainly felt from another device on the LAN.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The browser reached the daemon through maestro-web, a second binary that
served the SPA and reverse-proxied /api/* — two TCP connections and a
WebSocket frame re-encode on every keystroke and every byte of PTY output.
On one machine that hop buys nothing.
The daemon now embeds the bundle and serves it off its own listener, behind
a default-on `web` feature. `--no-default-features` builds without pnpm or
node; MAESTRO_WEB_DIST still takes a prebuilt bundle.
The router's fallback answers an unmatched /api/* with 404 rather than the
SPA shell, so a client expecting JSON gets a status it can read instead of
HTML.
Removes maestro-web and the [web] config section. An existing [web] block
is ignored, not rejected. The installer deletes a leftover
maestro-web.service on upgrade.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Skipping the operator's MCP servers, settings and hooks takes a measured
naming run from 12s to 7s, so the timeout is a margin rather than a race.
Refs #131
A session's rail glows AwaitingInput even after the operator has already
looked at it. Clients now report pane focus (POST /:id/focus); the daemon
clears the glow to Idle after 5s of continuous focus and keeps it cleared
until the session does new work, so a fresh Active cycle is required to
glow again.
POST /api/sessions/:id/prompt claims a session's one naming attempt and runs
claude -p --model haiku in the background; the answer is reduced to a short
slug and replaces the generated handle, settling the session at Haiku. A
chosen name, a rename mid-run, a missing binary or a timeout all leave the
handle alone. The prompt goes to the child on stdin and is never logged.
Refs #131
Adds a UserPromptSubmit entry that forwards the hook payload verbatim to
/api/sessions/:id/prompt, gated by a haiku-autoname feature that the daemon,
the CLI and the umbrella binary all forward. With it off no hook points at
the route.
Refs #131
The spawn form no longer prefills a name, so create() fills one in: a
requested name is marked User, an absent one gets a generated handle marked
Generated. Rename always settles to User.
Refs #131
The status bar gains the TUI's half of the same contract the web client got:
a coloured dot plus the release the daemon is running, pinned right so a narrow
terminal truncates the key hints rather than the release.
`DaemonLink` mirrors web/src/daemonLink.ts — the events loop reports its
connects and disconnects, a probe task polls `GET /api/info` while the daemon
is unreachable, and past the grace window a modal covers the frame and locks
the keyboard (quit excepted) until it answers. A daemon that comes back on a
new commit cannot be reloaded into a running TUI binary, so the status bar says
`updated: <hash> (restart tui)` instead.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`GET /api/info` now carries `build` (commit hash, commit date, dirty flag)
and `instance`, a fresh uuid per daemon process. Together they let a client
name the release it is talking to and, after losing its socket, tell an
upgrade from a plain restart from a network blip.
The stamp comes from `maestro-daemon/build.rs`, which reads the git checkout
it compiles in. The Nix build has no `.git` in the crane-filtered source, so
`flake.nix` passes the flake's own revision and last-modified date through
`MAESTRO_BUILD_*` instead. Neither available leaves the fields empty.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`s` (OpenSpawnModal, was NewDraftPane) puts the form up as a centered
overlay instead of taking over the pane. The attached session stays
attached behind it and comes back untouched on Escape; the modal owns
every key while it is up, so `x` and `j` typed into a field no longer
reach the keymap.
Tab::pane is a session id again and PaneRef is gone. The form lives on
App next to the rename buffer, so its loads no longer address a tab
index that could not identify them.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adds POST /api/sessions/:id/input and a CLI command on top of it, so a
script (or another agent) can hand an instruction to a running session
without attaching to it.
The text goes in as one write and the Enter follows 150ms later: agents
that run a full-screen prompt coalesce a burst of input into a paste, and
a newline inside that burst becomes a line break instead of sending the
message. `--no-enter` skips the Enter and leaves the text in the prompt.
The session argument takes an ID, a unique ID prefix, or a session name;
names win over prefixes. A session whose pane has exited is refused (409)
rather than silently accepted — unlike a stray keystroke on an attach
socket, a send that goes nowhere is worth hearing about.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Session handles are one word from a single 218-entry list — instruments,
forms, notation and theory, tempo and dynamics — replacing the
adjective-noun pair. A batch is shuffled, so it repeats only once the
wordlist runs out.
Project boxes show the parent directory instead of the full path: the
last segment is already the name line above it. The last command is gone
from the box, and picking a project no longer pre-selects it — the
directory and what runs in it are independent choices. Both changes land
in the TUI picker too, which mirrors the web form's rules.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A full-screen app enables mouse reporting once, at startup. A client
attaching later never saw those sequences, so the web terminal read the
pane as a plain alternate screen with mouseTrackingMode "none" and its
touch and wheel gestures had nowhere to go — scrolling did nothing in a
browser opened after the session started.
Read the pane's mouse flags in the same display-message call and emit
the matching DECSETs ahead of the screen content. tmux expands an
unknown format name to the empty string, so the parser only trusts the
mouse tail when every field is present.
Closes#128
Line 2 of a session card spelled out its blocked work ("1 uncommitted
file") and its command ("claude"). Both now render as marks: the
breakdown as diff notation, and the command's leading program as its
tool's icon in the web client.
The probe carries the line delta of the tracked changes — staged and
unstaged together, against HEAD — so `git add` never makes the counter
drop. Untracked files stay a file count; counting their lines means
reading every one off disk. A tracked change with no line delta falls
back to `~N` rather than claiming `+0 -0`.
The TUI keeps its command as text: a terminal cannot be assumed to have
a font carrying tool glyphs.
capture-pane trims trailing spaces, so a line's background stopped at its
last glyph and the client's own background showed through the rest as
stripes. -N keeps the padding, and with it the colour.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A new attach replayed the tail of the raw byte buffer. That tail starts
mid-stream, long after a full-screen program set the modes, colours and
cursor position that give those bytes meaning, so a browser refresh
painted garbage over every agent TUI until something forced a repaint —
which is why opening the same session in the TUI (a resize) fixed it.
Ask tmux for the pane's current screen instead, and fall back to the byte
buffer when there is no pane left to capture, as for a dead session.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Removes the retained-worktree store end to end: the daemon's state-dir
store, GET/DELETE /api/worktrees, `maestro worktrees list|reclaim`, the
TUI overlay behind `w`, and the web `#/worktrees` view and header link.
The kill guard stays. A forced kill and a dirty natural exit now leave
the worktree on disk untouched instead of parking it in a store, so
removing one is an ordinary `git worktree remove`. A pristine tree is
still removed on exit. DELETE /api/sessions/:id always answers 204.
The header's debug button also goes; Ctrl+Shift+D and `?hud` still
toggle the HUD.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The daemon trims and caps the name, persists it to the sidecar under the
sessions lock, and broadcasts SessionUpdated. A blank name clears
metadata.name so the card falls back to the command, matching the rule the
spawn form already uses.
Refs #125
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The no-retained-store test asserts a negative — the tree survives — so
waiting on that negative proves nothing: settle could still remove the
tree one instant after the assertion passes. mark_session_dead now flips
a per-record settled flag once the exit rule has been applied or refused,
exposed as SessionManager::is_settled; the test waits on it before
asserting.
Refs #115
The pipeline leader exits as soon as both stages are forked, which can
beat the surviving stage opening the tty and parking on read — the one-
shot assertion then ran against a group with no parked member yet.
Poll for the answer, accepting it only while the foreground group is
still leaderless so a shell that regained the tty cannot satisfy it.
Fixes#113
Two races. First, the watcher updates the snapshot and emits
SessionUpdated as two steps, so the drain could run between them: the
legitimate breakdown emission then arrived inside the quiet window.
Subscribe before the watcher starts and consume through the breakdown
event so the window begins after the last real change.
Second, the assertion treated any SessionUpdated as a failed
suppression, but the activity pass may legitimately announce an
Active→Idle transition in that window (ACTIVE_WINDOW is 3s), carrying
today's blocked_work unchanged. Only a changed indicator counts as a
re-emit now.
Fixes#113
Dead status is observable a moment before the settle step removes the
worktree, so the assertions raced the removal under parallel load. Wait
for the tree to be gone and the store to stay empty, same as the
retained-store wait cfebfd4 added to the guarded sibling.
Fixes#113
Dead status can be observed before the settle step's store write
lands, so the test's store read raced the write under parallel load.
Wait for the store to have the entry before asserting on it, same as
the settle-state wait a few lines below.
Fixes#111
The activity pass now sets activity_since only when the decided
ActivityState differs from the stored one; foreground/cwd-only diffs and
steady-state ticks leave the clock alone, so the sidebar duration counts
time in the current state instead of reading zero.
Daemon restarts still reset the clock to the restart time (stated in
recover()); the daemon cannot know how long a recovered session had
already been idle.
Every tmux-spawned session now gets its UUID under MAESTRO_SESSION_ID and,
when the runner has wired its base URL from [daemon].bind_addr, that URL
under MAESTRO_URL (#103). The daemon-owned values override anything the
spawn request carried. Adopted sessions stay without them by design.
The sidebar nested a worktree session under the worktree, so a repo grew
a <repo>/worktrees/<branch> folder holding one session of the same name,
and the repo stopped being where you look for the work.
The daemon recorded whatever base_repo the request named, which is the
worktree itself when spawning from inside one; it now records the
resolved main checkout. The web tree groups by that instead of the cwd.
The TUI sidebar is a flat list with no directory grouping, so it needs
no mirror.
Closes#99
run_blocked_work_pass rides the liveness tick but probes on a slower
cadence (DEFAULT_BLOCKED_WORK_INTERVAL, 15s): each probe is three git
subprocesses per session. Sessions without a worktree are filtered out
before any probe, so their indicator costs nothing. A failed probe keeps
the previous value; settled exits clear it.
Session.blocked_work is Option<InTheWay>, refreshed by the daemon's
watcher; None for worktree-less sessions and until first probed.
Defaults on the wire so older payloads stay forward-compatible.
Three in session_manager.rs arrived with #82 after #87 branched; four in
maestro-e2e were fenced off because #88 owns that crate. Removing the
field without them left the workspace uncompilable.