Commit Graph
118 Commits
Author SHA1 Message Date
naps62-yoloandClaude Opus 4.8 599f3ed178 fix(maestro): fence registry deregister with a per-connection token (NAP-258)
ci / rust (push) Successful in 7m41s
`Registry::deregister` removed the catalog entry unconditionally. If a daemon's
old WS handler hit heartbeat timeout *after* the daemon had reconnected on a
new socket, the stale handler's deregister wiped the freshly-installed live
entry and broadcast DaemonRemoved + SessionRemoved for all its sessions.

`register` now mints a per-connection fencing token (uuid) stored on the
DaemonEntry and returned to the handler, which holds it for the connection's
life. `deregister(daemon_id, token)` only tears down the entry when the stored
token matches, so a superseded handler's late deregister is a no-op.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 21:02:54 +00:00
naps62-yoloandClaude Opus 4.8 3517e4e145 fix(maestro): recover() re-advertises sessions under current daemon_id (NAP-264)
ci / rust (push) Successful in 6m46s
`SessionManager::recover()` re-inserted each sidecar's `Session` verbatim,
keeping the persisted `daemon_id`. If the operator renamed the daemon between
starts, recovered sessions were re-advertised under the old id and became
unroutable. Overwrite `daemon_id` with the daemon's current id on recover,
warning when the sidecar's id differs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 20:51:18 +00:00
naps62-yolo 421e36c59f Merge branch 'linear/nap-269-registry-ws-drops-on-any-unknown-daemonoutbound-variant'
ci / rust (push) Successful in 3m58s
2026-05-31 19:30:44 +00:00
naps62-yoloandClaude Opus 4.8 8b4cdfd650 fix(maestro): tolerate unknown WS frame types instead of dropping the connection (NAP-269)
ci / rust (push) Successful in 14m9s
`dispatch_text` (registry) and `handle_inbound` (daemon) treated any
frame that didn't deserialize into a known `DaemonOutbound`/`DaemonInbound`
variant as fatal: the registry returned `false` (breaking the handler
loop and deregistering the daemon, wiping its catalog), and the daemon
propagated the serde error out of the read loop. A newer peer emitting a
not-yet-merged variant would therefore get stuck in a tight
disconnect/reconnect loop.

- Add a `#[serde(other)] Unknown` catch-all to both enums so unknown
  `type` tags decode instead of erroring.
- Log-and-skip the `Unknown` variant on both sides.
- Also log-and-skip genuinely malformed frames rather than tearing down
  the connection, so one bad frame can't wipe a daemon's catalog.

Tested: unknown/malformed frames keep the connection alive (registry
handler), and unknown `type` decodes to `Unknown` (protocol round-trip).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 19:30:36 +00:00
naps62-yolo 4a20bc168f Merge remote-tracking branch 'origin/main' into linear/nap-244-cli-rewire-maestro-attachlistspawnkill-through-home-daemon
ci / rust (push) Successful in 14m12s
2026-05-31 19:27:23 +00:00
naps62-yoloandClaude Opus 4.8 643da30f63 test(maestro): e2e for CLI broker path + registry compat fallback (NAP-244)
ci / rust (push) Successful in 10m42s
Drives the real `maestro` binary: one test points `MAESTRO_HOME_DAEMON_URL` at
a live daemon with no registry configured (proves the catalog read comes from
the home daemon), the other points it at a dead port and asserts the CLI warns
and falls back to the registry while still listing the session.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 19:25:28 +00:00
naps62-yoloandClaude Opus 4.8 0df1d61f07 feat(maestro): rewire CLI through home-daemon broker with registry fallback (NAP-244)
`maestro list/spawn/kill/attach` now target the home daemon's local API
(`[cli].home_daemon_url`, default http://127.0.0.1:7070) instead of the
registry directly. The daemon brokers transport selection and `allow_inbound`
enforcement.

- list reads the daemon's catalog mirror (own ∪ peer sessions)
- spawn/kill hit the daemon's local session endpoints
- attach opens the daemon's `WS /api/attach/:id` broker (shared pump loop
  via the new `run_attach_url` / `broker_attach_url`)

Compat fallback to the legacy registry path (HTTP + direct WS) with a stderr
warning when the home daemon is unreachable. Cross-daemon spawn (501) and kill
(404) — not yet brokered locally — also fall back so they keep working.

Adds `[cli].home_daemon_url` config + `--home-daemon-url`/`MAESTRO_HOME_DAEMON_URL`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 19:23:17 +00:00
naps62-yoloandClaude Opus 4.8 3b60f67332 fix(maestro): give CLI reqwest clients connect + request timeouts (NAP-262)
ci / rust (push) Successful in 9m49s
The `RegistryClient` and the bare `reqwest::Client::new()` calls in
`policy.rs` built clients with no timeouts, so `maestro list` / `maestro
policy {show,reload}` would hang forever against a registry/daemon that
is TCP-alive but not making forward progress (SIGSTOP, swap thrash,
deadlock behind slow disk).

Centralize construction in `http::build_client()`, which applies a 5s
connect timeout and a 30s request timeout, and route every call site
through it. Covered by a test that points the client at a peer which
accepts the connection but never responds and asserts the request times
out instead of hanging.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 19:22:58 +00:00
naps62-yoloandClaude Opus 4.8 2bfab13b58 fix(maestro): heal daemon→registry catalog desync that left dead sessions listed (NAP-291)
ci / rust (push) Successful in 9m41s
The daemon→registry uplink could leave a session in the registry's catalog
after the daemon had already dropped it from its authoritative map, so
`maestro list` / the web UI kept showing it while DELETE returned 404
("no such session on this daemon"). It only healed on the next reconnect.

Two defects in `registry_client::connect_once`, both fixed here:

A. Subscribe-after-snapshot race: the uplink took `manager.snapshot()` and
   then subscribed to `manager.events()`. A reap/kill firing in that window
   was in neither — the registry kept the dead session. Now subscribe before
   snapshotting, matching `local_api::pump_events` (which documents the same
   ordering rule for the browser/CLI event WS).

B. Lagged events were silently dropped (`continue`). If the 256-deep event
   broadcast overflowed, a dropped `SessionRemoved` was never relayed and the
   registry stayed stale until reconnect. The events arm is now extracted into
   `forward_session_event`, which on `Lagged` re-sends a full `SessionsSnapshot`
   — the registry applies it by replacing the daemon's set, healing immediately.

Adds deterministic unit tests for the lag-resync, the Removed mapping, and the
closed-stream break. Existing integration tests still pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 12:36:19 +00:00
naps62-yolo 39dddb17da feat(maestro-tui): trim chrome + walk sidebar in visual order (NAP-289)
ci / rust (push) Successful in 8m18s
Dogfood-driven layout fixes:

- `j`/`k` now walk the same daemon-then-sessions order the sidebar emits
  (`visible_sidebar_order`), so the highlight follows what the user sees
  instead of jumping by Uuid order across daemons.
- Tab bar collapses from 3 rows of box border to 1 row of plain text +
  1 row of horizontal rule. The rule doubles as the pane's focus indicator
  (cyan top border when `focus = Pane`, default otherwise). Saves ~4 rows
  above the pane before any content shows.
- Sidebar drops the full box border in favour of a single right-edge
  vertical rule (also cyan when focused). The "daemons" header now lives
  inline as a bold first line instead of in the block title.

Visible content area grows by ~2 cols + ~4 rows on the typical layout
without losing focus / panel-boundary affordances.

Workspace clippy + 85 unit tests passing.
2026-05-29 10:10:56 +00:00
naps62-yolo f208aa441d feat(maestro-tui): attach lands in Insert + real help + blinking cursors (NAP-288)
ci / rust (push) Successful in 8m45s
Three dogfood-driven UX fixes:

1. `AppState::attach_to_focused` now sets `mode = Mode::Insert` along with
   `focus = Focus::Pane`, so pressing Enter on a sidebar row drops the user
   straight into typing the remote shell. The leader (default `ctrl-a`)
   returns to Normal — same surface as tmux's prefix key.

2. `Keymap::entries(mode)` exposes the live binding table; `App::dispatch
   (Action::ShowHelp)` (and `App::new`) rebuild `HelpOverlay::lines` from
   it. Users see the actual Normal + Insert bindings + the leader hint,
   not just "? to close".

3. Visible blinking cursor in both surfaces that accept text:
   - `Pane::render` calls `Frame::set_cursor_position` at the vt100 cursor
     position when the pane is focused (and the screen hasn't hidden the
     cursor itself). Terminals do the blink.
   - `SpawnModal::render` places the cursor at the end of whichever text
     field is currently focused (cwd / command / label). Daemon dropdown
     and the Submit button still rely on the REVERSED style.

Workspace clippy + 85 unit tests passing.
2026-05-29 09:19:30 +00:00
naps62-yolo f27adf5aba feat(maestro-tui): status bar + focus highlight + j/k sidebar nav (NAP-287)
ci / rust (push) Successful in 15m39s
First-use UX fix: the TUI rendered the daemon list but felt frozen because
mode and focus were invisible, and the sidebar had no nav keys, so
`Enter`/`AttachHighlighted` had nothing to target.

- `render::draw` now reserves the bottom row for a status bar that always
  shows `[NORMAL]`/`[INSERT]` (cyan/yellow chip), the focused panel
  (sidebar / pane), and the most useful key hints (`j/k: nav  •  enter:
  attach  •  s: spawn  •  ?: help  •  ^Wq: quit`). Status bar is never
  eclipsed by overlays.
- `Sidebar::render_with_focus` and the pane block now draw their border
  in cyan when focused, in the default colour otherwise.
- New `Action::SidebarNext` / `SidebarPrev`, bound to `j`/`k` and
  `down`/`up` in the shipped Normal-mode defaults. Dispatch walks the
  catalog's session list, calling `AppState::move_sidebar_highlight`
  (wraps both ends).

Workspace clippy + 85 unit tests + 3 integration tests green.
2026-05-29 09:01:36 +00:00
naps62-yoloandClaude Opus 4.8 1cafa2c2af fix(maestro): install maestro without the registry feature
ci / rust (push) Successful in 8m13s
The installer's cargo install of the umbrella maestro crate built the
default feature set (cli+daemon+registry+tui), forcing every host to
compile the registry — which a host neither needs nor runs (the registry
is a separate, centrally-deployed service). Install with
--no-default-features --features cli,daemon,tui instead.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 06:42:43 +00:00
naps62-yoloandClaude Opus 4.8 212ebbf120 fix(maestro): stop embedding web bundle in the registry
The registry serves no UI — that is maestro-web's job (connection-
orchestration spec §"UI hosting"). The rust-embed of web/dist was
legacy from NAP-218, before maestro-web was extracted (NAP-241), and it
made the registry (and thus the umbrella maestro binary) fail to compile
whenever web/dist was absent — e.g. a fresh cargo install --git after
the tracked .gitkeep was dropped. Remove web_assets.rs, the rust-embed
dep, and the / + /assets routes; the registry is now JSON/WS API only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 06:40:23 +00:00
naps62-yolo 278c81d2bc feat(maestro-tui): wire --config flag + document tui in INSTALL.md (NAP-283)
ci / rust (push) Failing after 5m13s
- `RunConfig` grows a `config_path: Option<PathBuf>` field, plumbed from the
  umbrella binary's global `--config` flag through to `MaestroConfigFile`
  loading. Explicit `--config <path>` wins over `$MAESTRO_CONFIG` over the
  platform default; a missing file still falls back to shipped defaults.
- `docs/INSTALL.md` gains a "terminal UI" section: that `maestro tui` ships
  in the default feature set, how to opt out (`--no-default-features
  --features cli,daemon`), the `MAESTRO_DAEMON_URL` override, and a worked
  example of `[tui]` / `[tui.keymap.normal]` config pointing at the
  full action vocabulary in the design spec.
- Adds a thin-build line for `--features cli` (CLI-only client) and lists
  `tui` in the default-feature set.
- CLAUDE.md repo-layout block lists the new `maestro-tui` crate.

Closes the NAP-275 v1 epic.
2026-05-28 22:42:30 +00:00
naps62-yolo c599fcfdec test(maestro-tui): integration tests against real daemon subprocess (NAP-282)
ci / rust (push) Failing after 5m1s
`crates/maestro-e2e/tests/tui_integration.rs` adds three integration tests
that spin up a real registry + daemon via the existing maestro-e2e harness
and drive `CatalogStore` against the daemon's `/api/*` surface:

- `catalog_bootstrap_against_real_daemon` — WS connect + initial Snapshot
  carries the self-daemon.
- `post_session_propagates_through_events_ws` — POST /api/sessions →
  catalog WS surfaces SessionAdded within 2 s.
- `delete_session_propagates_removal` — DELETE /api/sessions/:id →
  catalog WS surfaces SessionRemoved within 2 s.

These exercise the real wire protocol (subprocess + tmux + axum + tungstenite)
without going through the full ratatui frontend, complementing the unit-test
TestBackend assertions on widgets.

The full App-driven e2e (input injection feeding the dispatch loop, asserting
vt100 contents) would need `App::run` to be backend-generic; deferring as a
follow-up since the integration layer here already proves the wire protocol
round-trip the NAP-282 acceptance asked for.
2026-05-28 22:37:22 +00:00
naps62-yolo 5a17de4489 feat(maestro-tui): App event loop + lifecycle wiring (NAP-281)
ci / rust (push) Failing after 5m4s
Stitches the pieces from NAP-276..280 into a working app:

- `app.rs` — `App` owns config, catalog, emulators, state, modal, chord
  resolver, region map, last-sent resize cache, and the reqwest client.
  `run()` races crossterm events, catalog change notifies, and a 30 Hz
  heartbeat. `redraw()` builds `RenderInputs`, calls `render::draw`,
  refreshes `RegionMap`, then runs per-session min-size resize propagation
  to the daemon.
- `dispatch(Action)` — Action → state mutation. `AttachHighlighted` spawns
  a real `SessionTask` if none exists for the session. `OpenSpawnModal`
  rebuilds the modal against the current daemon list. `KillHighlighted`
  opens a confirm overlay.
- `handle_overlay_input` — intercepts keystrokes while spawn modal / kill
  confirm / help is open. Spawn `Submit` fires `POST /api/sessions`.
  Kill `y` fires `DELETE /api/sessions/:id` and drops the local handle.
- Insert-mode passthrough: characters (with ctrl/alt mapping), arrows
  (CSI sequences), Enter/Tab/Backspace/Esc become binary bytes on the
  active session's WS sink.
- `lib::run` — installs the panic hook, runs a 3×500ms `/api/web/info`
  probe, loads `[tui]` from `MaestroConfigFile`, spawns
  `CatalogStore::run_events_loop`, waits up to 2s for the bootstrap
  Snapshot, enters the terminal guard, and drives `App::run`.

90 tests passing (5 new app dispatch unit tests + the existing widget +
catalog + session_task + emulators + pure-logic suites).
2026-05-28 22:30:45 +00:00
naps62-yolo 50688c39b7 Merge branch 'linear/nap-280-tui-widgets'
ci / rust (push) Failing after 4m14s
2026-05-28 22:24:22 +00:00
naps62-yolo 953d170dae feat(maestro-tui): ratatui widgets + render orchestration (NAP-280)
ci / rust (push) Failing after 4m44s
Adds the full visual layer:

- `state.rs` — `AppState` (tabs/focus/mode/sidebar visibility/overlay).
  v1 keeps each tab to one optional session; the split tree is v1.5.
- `terminal.rs` — `TerminalGuard` (raw mode + alt screen + mouse capture
  with RAII restore) and an idempotent panic hook that restores the
  terminal before re-panicking.
- `sidebar.rs` — daemon → session tree with ●/○/✗ glyphs. Pulls a label
  out of `metadata.label` (the protocol's shape), falls back to the
  command.
- `tabs.rs` — top-row TabBar with active highlight.
- `pane.rs` — `vt100::Screen` → ratatui `Buffer` cell-by-cell, with full
  fg/bg/bold/italic/underline/reverse translation. `PaneStatus` round-trips
  from `SessionState`.
- `overlays.rs` — SpawnModal (5-field form with rotation), KillConfirm,
  HelpOverlay, Toast. All centered popups with `Clear` underlay.
- `render.rs` — `draw(f, RenderInputs)` composes everything; returns a
  `RegionMap` whose `region_at(col, row)` resolves a mouse coordinate to
  a tagged Region (modal eclipses the rest).

22 new unit tests bringing the crate total to 80 passing. Tests use
content-based assertions over `ratatui::backend::TestBackend` output rather
than file-based goldens — less brittle to whitespace shifts while still
covering the visible content.
2026-05-28 22:24:06 +00:00
naps62-yoloandClaude Opus 4.8 a429c69956 chore(maestro): drop obsolete web/dist/.gitkeep handling
ci / rust (push) Failing after 3m2s
build.rs now builds web/dist before include_dir! evaluates, so the
checked-in .gitkeep placeholder is no longer needed for a fresh checkout
to compile (verified by building with web/dist entirely absent). Ignore
dist/ wholesale and update the static_bundle doc comment.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-28 22:21:59 +00:00
naps62-yolo 888a00d177 feat(maestro-tui): SessionTask + SessionEmulators + ResizeTracker (NAP-279)
ci / rust (push) Failing after 5m11s
Per-attached-session task:
- Owns one WS to `WS /api/attach/:id` and a `vt100::Parser`.
- Binary frames → `parser.process(bytes)`. Text frames → `ClientControl::Ping`
  is replied to as Pong; other text frames are ignored in v1.
- `SessionInput::Bytes(b)` becomes a binary frame; `Resize { cols, rows }`
  resizes the parser AND sends `ClientControl::Resize` on the wire.
- WS drop → `Reconnecting` with exponential backoff (1s → 30s, ±25% jitter).
  Parser screen is **not** reset across reconnects, so the user keeps seeing
  the last-known frame instead of a blank.
- `SessionHandle::end()` transitions the task to `Ended` and stops it.

SessionEmulators:
- `(SessionId → SessionHandle)` registry.
- Owns a `ResizeTracker` whose `set_pane_size(sid, pane_id, (cols, rows))`
  and `session_size(sid) -> Option<(cols, rows)>` compute the per-dimension
  minimum across all panes bound to the same session — the value the
  dispatcher feeds to the daemon.

Tests:
- 7 unit tests for `ResizeTracker` covering per-dim min, cross-session
  isolation, and pane/session drop semantics.
- 2 unit tests for `SessionState` transitions.
- 1 axum-WS round-trip integration test asserting `"hello"` lands in the
  vt100 screen within 500ms.
2026-05-28 22:15:49 +00:00
naps62-yolo 0f3770f711 feat(maestro-tui): CatalogStore over WS /api/events (NAP-278)
ci / rust (push) Failing after 5m5s
CatalogStore is the TUI's view of the daemons-and-sessions tree, backed by
`WS /api/events` on the local daemon. The daemon's WS opens with a Snapshot
frame, so that's bootstrap; subsequent frames are deltas. No separate REST
round-trip needed.

Surface:
- `apply_event` — pure, used by tests directly and by the WS loop.
- `snapshot()` — cheap O(d+s) read returning sorted Vec views + monotonic
  version for cheap render staleness checks.
- `wait_for_initial_snapshot` / `changed` — render-loop wakeups.
- `run_events_loop(ws_url)` — long-running task with exponential backoff
  (1s → 30s, ±25% jitter). Reconnect re-sends a fresh Snapshot, which
  reconciles state without server-side replay.

8 unit tests + 1 axum WS round-trip integration test passing.
2026-05-28 22:08:06 +00:00
naps62-yoloandClaude Opus 4.8 0ed2a66176 ci(maestro): install node + pnpm for maestro-web build.rs
Compiling maestro-web now requires the frontend toolchain to build the
embedded SPA bundle.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-28 22:02:11 +00:00
naps62-yoloandClaude Opus 4.8 eeccc1ad5b feat(maestro): preflight pnpm + node at installer web step
The opt-in maestro-web step now checks for the frontend toolchain its
build.rs requires; missing tools prompt abort/skip (interactive) or skip
the refresh (non-interactive) instead of failing cargo with a raw panic.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-28 22:02:11 +00:00
naps62-yoloandClaude Opus 4.8 c355f9bd2e fix(maestro): tie maestro-web recompile to web/dist + harden bundle build
Watch the embedded web/dist output (not just frontend inputs) so a
rebuilt bundle is actually re-embedded; assert pnpm build produced
web/dist/index.html; strengthen the embed test to require a vite-built
hashed asset reference.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-28 22:02:11 +00:00
naps62-yoloandClaude Opus 4.8 092a04857f feat(maestro): build maestro-web SPA bundle in build.rs
include_dir! now embeds a freshly built web/dist on every compile; pnpm +
node are hard build requirements. Fixes the placeholder served by
cargo install --git.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-28 22:02:11 +00:00
naps62-yolo 31e1b73193 feat(maestro-tui): TuiConfig with shipped defaults + override merge (NAP-277)
ci / rust (push) Successful in 15m8s
`TuiConfig::load(&TuiSection)` builds the runtime config:

1. Apply shipped default Normal-mode bindings (cover every v1 action).
2. Insert mode binds the leader → `LeaveInsert` (built from the leader spec
   so rebinding leader moves the Insert escape automatically).
3. Apply user overrides from `[tui.keymap.normal]` / `[tui.keymap.insert]`
   over the defaults, with the same validation path — bad TOML entries
   surface as `BadKeymap { mode, input, action, source }`.

Also adjusts the existing `MaestroConfigFile` round-trip test to include the
new `tui` field.

8 new unit tests passing (49 total for tui + config).
2026-05-28 19:52:41 +00:00
naps62-yolo 231032aa85 feat(maestro-tui): Keymap table with input/action validation (NAP-277)
Two-level `(mode → input → action)` lookup. `set` validates each whitespace-
separated chord token via `InputKey::parse` and the action name via
`Action::parse`, so a bad TOML entry surfaces a precise diagnostic at TUI
startup instead of silently dropping a binding.

`known_bindings(mode)` feeds `ChordState` so a key only holds when it strictly
prefixes a real binding.

8 unit tests passing.
2026-05-28 19:50:41 +00:00
naps62-yolo 67870bb9e8 feat(maestro-tui): InputKey canonicalisation + ChordState (NAP-277)
- `InputKey` — hashable variant covering chars-with-modifiers, specials, and
  mouse events (kind + tagged region). `canonical()`/`parse()` round-trip the
  TOML form (`ctrl-w h`, `mouse:click(pane)`).
- `canonicalise()` — `crossterm::Event` → `InputKey`, with a region-resolver
  callback supplied by the renderer (NAP-280 will wire `RegionMap` to it).
- `ChordState` — small state machine that holds a key when it strictly
  prefixes a known binding, resolves otherwise. Drops stale held keys past
  `timeout`.

11 unit tests passing.
2026-05-28 19:49:59 +00:00
naps62-yolo a4252438cd feat(maestro-tui): Action enum with canonical-string parser (NAP-277)
Closed list of TUI verbs. The string form (`EnterInsert`, `FocusPane(Left)`,
`GotoTab(3)`) is what users write in `[tui.keymap.normal]`; the enum is what
the event loop matches on.

Parser surfaces `Unknown`, `BadArg`, and `Malformed` diagnostics so a bad
keymap entry is rejected at load time with a specific reason. Display round-
trips so the canonical form is stable.

9 unit tests passing.
2026-05-28 19:48:10 +00:00
naps62-yolo ed128ec200 feat(maestro): add [tui] config section + crossterm workspace dep (NAP-277)
Schema-only — adds `TuiSection` with `leader`, `chord_timeout_ms`, and per-mode
keymap override maps; pulls crossterm into the workspace deps so the next
commit's input layer can consume it.

Field set matches `docs/superpowers/specs/2026-05-28-maestro-tui-design.md` §3.9.
2026-05-28 19:47:05 +00:00
naps62-yoloandClaude Opus 4.8 92396a6bfe docs(maestro): implementation plan — build maestro-web SPA bundle in build.rs
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-28 19:27:13 +00:00
naps62-yolo e276fcf43e Merge branch 'linear/nap-276-tui-scaffold'
ci / rust (push) Successful in 16m15s
2026-05-28 19:24:44 +00:00
naps62-yolo c99f23c705 feat(maestro): scaffold maestro-tui crate + maestro tui subcommand (NAP-276)
ci / rust (push) Successful in 15m43s
New `crates/maestro-tui/` library behind a `tui` cargo feature on the umbrella
binary (default-on). Ships a clap-derive `TuiArgs` and a `run()` stub that
returns `NotImplemented` — exercises the full dispatch path so phases 2+ can
fill in the catalog, session liveness, widgets, and event loop without
touching scaffolding.

Acceptance:
- `cargo build -p maestro --features tui` succeeds
- `maestro tui` prints "maestro: not implemented" and exits 1
- `cargo test -p maestro-tui` passes (2 clap-parse tests)
- `cargo fmt --check` + `cargo clippy --all-targets --features tui -- -D warnings` clean

Phase 1 of the Maestro TUI epic (NAP-275); plan in
`docs/superpowers/plans/2026-05-28-maestro-tui.md`.
2026-05-28 19:24:34 +00:00
naps62-yoloandClaude Opus 4.8 63217d7851 docs(maestro): spec — build maestro-web SPA bundle at compile time
build.rs runs pnpm install+build before include_dir! embeds web/dist, so
any install method (cargo install --git, --path, wizard) ships a real
bundle instead of the placeholder. Hard-fail when pnpm/node missing;
wizard prompts abort/skip; CI gains the node+pnpm toolchain.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-28 19:24:18 +00:00
naps62-yolo d0f8997a78 docs(maestro): TUI v1 implementation plan — 25 TDD-cut tasks
Phases: scaffolding, pure-logic units (Action / InputKey / Keymap / TuiConfig),
catalog (REST + events WS), session liveness (per-session vt100+WS task with
reconnect), widgets (sidebar / tab / pane / overlays) with golden-snapshot tests,
top-level event loop, lifecycle wiring (spawn / kill), integration test against
in-process daemon, e2e through two daemons via the maestro-e2e harness.

v1 ships tabs-only; splits and command palette deferred to a follow-up plan.
2026-05-28 18:14:12 +00:00
naps62-yolo 2b96330534 docs(maestro): TUI design — maestro tui, vim-style layout, multi-session liveness
Spec covers the new `crates/maestro-tui/` crate gated on a `tui` feature of
the umbrella binary: ratatui + vt100, sidebar/tab/pane components, configurable
keymap (defaults are defaults), first-class mouse, vim-buffer mental model.

Goals + non-goals; v1 ships tabs-only, v1.5 enables splits on the same layout
tree. Talks only to the local daemon's `/api/*` surface — peer sessions reach
us via the catalog mirror, attach hops are broker-mediated transparently.
2026-05-28 18:00:47 +00:00
naps62-yolo 973b49e3df Merge pull request 'feat(maestro): unify maestro/maestrod/maestro-registry into one binary (NAP-245)' (#3) from linear/nap-245-unify-maestromaestrodmaestro-registry-into-a-single-maestro into main
ci / rust (push) Successful in 2m35s
2026-05-27 16:12:36 +01:00
naps62-yolo 9ce6303293 refactor(maestro): wire --config flag through to maestro registry
ci / rust (push) Failing after 2m13s
ci / rust (pull_request) Successful in 4m54s
`registry::run` now accepts an `Option<PathBuf>` config override so the
umbrella binary's `--config` flag (and its clap-bound `MAESTRO_CONFIG`
env var) reach the registry, mirroring how the daemon already consumed
the override.
2026-05-27 10:45:16 +00:00
naps62-yolo ade0d49096 chore(maestro): retarget installer, e2e harness, and docs at unified maestro binary (NAP-245)
- Installer: one `cargo install maestro` instead of two; ExecStart becomes
  `~/.cargo/bin/maestro daemon`; user unit renamed `maestrod.service` →
  `maestro.service`; configure step is `maestro daemon configure`.
- E2E harness: both `Daemon` and `Registry` subprocess wrappers shell out to
  the unified `maestro` binary with the appropriate subcommand.
- State dir: `~/.local/state/maestrod` → `~/.local/state/maestro`.
- Docs: CLAUDE.md repo layout, design spec repo layout + "Daemon (`maestro daemon`)"
  heading, INSTALL.md (full rewrite of CLI references + new thin-builds section).
2026-05-27 10:43:51 +00:00
naps62-yolo 35482118b1 feat(maestro): unify maestro/maestrod/maestro-registry into single binary (NAP-245)
New `crates/maestro/` umbrella binary hosts the CLI, daemon, and registry
behind Cargo features `cli`, `daemon`, `registry` (default = all three).
Library crates keep their structure; per-binary `main.rs` files are removed
and their dispatch logic is exposed as public functions:

- `maestro_cli::dispatch(command, config, registry_url)`
- `maestro_daemon::cli::dispatch(config, cmd)` (with public `DaemonCmd`/`ConfigureArgs`)
- `maestro_registry::run()`

Subcommand surface:

- `maestro list / spawn / attach / kill / set-registry / policy` (unchanged)
- `maestro daemon [run|configure]` (was `maestrod`)
- `maestro registry` (was `maestro-registry`)

Hard cutover — old binary names are not preserved.

Verified: default-feature build, plus `--no-default-features --features {cli|daemon|registry}`
each build clean with `-D warnings`.
2026-05-27 10:35:41 +00:00
naps62-yolo 8e857e9fe7 Merge branch 'linear/nap-237-daemon-policy-hot-reload-sighup-maestro-policy-reload'
ci / rust (push) Successful in 4m46s
2026-05-27 09:53:28 +00:00
naps62-yoloandClaude Opus 4.7 f69c7a9eee feat(maestro): hot-reload [daemon.policy] via SIGHUP + maestro policy CLI (NAP-237)
ci / rust (push) Failing after 2m51s
`PairAttachVerifier` now stores `allow_inbound` behind `Arc<RwLock<_>>` so
clones share the same set. Two triggers re-read the unified config file
and swap it atomically:

- SIGHUP — `systemctl --user reload maestrod`, or any `kill -HUP`. The
  installer's user unit now sets `ExecReload=/bin/kill -HUP $MAINPID`.
- `POST /api/policy/reload` — used by `maestro policy reload`. The CLI
  reads `[daemon].bind_addr` from the unified config (default 7070) and
  POSTs to `http://127.0.0.1:<port>`. No PID lookup required.

Also adds `maestro policy show [--json]` and `GET /api/policy` for
"did my edit get picked up?" debugging. A broken mid-edit config logs
an error and leaves the previously-effective list in place rather than
locking everyone out.

In-flight attaches keep their existing connection — revocation requires
killing the session, as spec'd.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 09:53:19 +00:00
naps62-yoloandClaude Opus 4.7 7814ca99de fix(maestro): chain pipe-pane into new-session so initial output isn't lost (NAP-253)
ci / rust (push) Failing after 1m45s
`SessionManager::create` used to call `tmux new-session` (which exec's the
user's command immediately), run an async `PtyBridge::start_with_replay`
(mkfifo + open + spawn reader), then issue a *separate* `tmux pipe-pane`.
Any output the child wrote between exec and that third tmux subprocess
landing went into the pane but never into the FIFO. Hit in NAP-252
dogfood: `bash -c 'echo X; exec bash'` sessions showed only the trailing
prompt, never the X.

Two changes:

1. New `Tmux::new_session_piped(id, cwd, cmd, sink)` chains
   `; pipe-pane -t maestro-<id> <sink>` into the same tmux command argv
   as `new-session`. Within a tmux command chain, `pipe-pane` is
   processed immediately after `new-session` returns — before the child
   has finished `execve` — so the FIFO is wired up before the first
   write.
2. `SessionManager::create` now builds the FIFO + reader task **before**
   spawning the session, then calls `new_session_piped` instead of the
   two-step `new_session` + `pipe_pane`.

`Tmux::pipe_pane` and the standalone `Tmux::new_session` stay around —
`recover()` (NAP-228) still needs a standalone pipe-pane re-attach, and
some integration tests want a bare session without a pipe.

Tests:
- Unit: argv assertion that the chained `pipe-pane` follows `new-session`
  when a sink is supplied.
- Integration: `create_captures_initial_output_in_replay_buffer` spawns
  `sh -c 'echo NAP_253_FIRST_LINE_MARKER; exec sleep 30'` and polls
  `subscribe_pty_with_replay` until the marker shows up in the replay
  buffer (or fails after 5s). Fails before the fix, passes after.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 09:49:43 +00:00
naps62-yoloandClaude Opus 4.7 08f5bbc045 test(maestro): fix borrow-after-partial-move in recover regression test
ci / rust (push) Successful in 2m51s
Capture sidecar_dir/fifo_dir/tmux/worktree_root before `drop(fx.manager)`
so the post-drop builder calls don't need to reborrow the partially-moved
fixture. Pure compile fix — same scenario as before.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 07:43:15 +00:00
naps62-yoloandClaude Opus 4.7 d23ee6a8b2 fix(maestro): re-pipe pane on recover so attach sees output (NAP-228)
`Tmux::pipe_pane` used `tmux pipe-pane -o`, which tmux documents as "only
open a new pipe if no previous pipe exists." When `maestrod` restarts and
`SessionManager::recover` re-issues `pipe-pane` against the adopted tmux
session, the previous daemon process's pipe to a now-orphan FIFO is
still active — so `-o` makes the call a no-op. The revived manager's
PtyBridge reads from a fresh FIFO that tmux never feeds, and attach
clients see input land in the pane but no output flow back.

Drop the `-o`. tmux closes any existing pipe before opening the new one,
which is what `pipe_pane` callers always wanted: `create` runs on a
fresh session (nothing to clobber), `recover` actively needs the
clobber.

Test: `recover_re_pipes_pane_so_output_reaches_subscribers` creates a
bash session under one manager, drops it, recovers under a second
manager, sends keys, and asserts the echo bytes reach a fresh
subscriber within 5s. Fails before the fix (stale pipe), passes after.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 07:43:15 +00:00
naps62-yoloandClaude Opus 4.7 1c5b6c615c feat(maestro): default daemon TMUX_TMPDIR to $XDG_RUNTIME_DIR (NAP-234)
ci / rust (push) Failing after 2m13s
Plain `tmux ls` in the user's shell now sees maestro-managed sessions
without the `TMUX_TMPDIR=/tmp` workaround, because most shells already
set `TMUX_TMPDIR=$XDG_RUNTIME_DIR` and the daemon now matches.

Precedence: env `MAESTRO_DAEMON_TMUX_TMPDIR` > `[daemon].tmux_tmpdir` >
`dirs::runtime_dir()` > None (tmux's own `/tmp` default). Set
`tmux_tmpdir = "/tmp"` in config.toml if the daemon must outlive the
user's login without linger enabled.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 07:37:33 +00:00
naps62-yoloandClaude Opus 4.7 b1aa2e6eab fix(maestro): chain default-terminal into new-session instead of pre-set
ci / rust (push) Successful in 2m59s
CI e2e tests failed with "tmux exited with code 1: server exited
unexpectedly" because the startup `start-server \; set-option -g
default-terminal` call left an empty server that tmux's default
`exit-empty on` killed before the first `new-session` could attach.

Move the `set-option -g default-terminal tmux-256color` into the same
tmux command as `new-session` (chained with `;`) so a single tmux
invocation creates the server, sets the option, and spawns the pane.
The pane reads `default-terminal` at creation time, so chaining before
`new-session` still applies to the very first session.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 06:56:29 +00:00
naps62-yoloandClaude Opus 4.7 080f961238 feat(maestro): daemon spawns tmux sessions with 256-color TERM (NAP-252)
ci / rust (push) Failing after 3m7s
`maestrod` running from a stripped systemd env leaves the spawned shell
without `TERM`/`COLORTERM`, and the pane process inherits tmux's
`default-terminal` (which defaults to `screen`, 8 colors). Browser
attaches via xterm.js render ANSI sequences as plain text.

Fix:
- `Tmux::new_session` now passes `-e TERM=xterm-256color -e
  COLORTERM=truecolor` to `tmux new-session`.
- New `Tmux::set_option_global` chains `start-server \; set-option -g`
  so it works before any session exists; the daemon calls it on startup
  to set `default-terminal=tmux-256color`.

Operators wanting 24-bit color should add
`set-option -ga terminal-overrides ",xterm-256color:RGB"` to their
own tmux config — left out of the daemon since that's operator territory.

Tests:
- Unit: `new_session_args` returns the expected argv including the
  `-e TERM=…` / `COLORTERM=…` pair.
- Integration: spawn a session under an isolated tmux server, set
  `default-terminal`, run `printenv` + `tput colors`, assert the pane
  sees `TERM=tmux-256color`, `COLORTERM=truecolor`, and 256 colors.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 06:50:56 +00:00
naps62-yoloandClaude Opus 4.7 3bd793f394 feat(maestro): replay PTY scrollback on attach (NAP-229)
ci / rust (push) Failing after 7m57s
Each PtyBridge keeps a per-session ring buffer (Mutex<VecDeque<u8>>,
default 64 KB, override via `[daemon] pty_replay_bytes`). The reader
task appends each chunk while holding the buffer lock and broadcasts in
the same critical section, so `PtyBridge::subscribe_with_replay` —
which snapshots the buffer and subscribes to the broadcast under that
same lock — produces a snapshot that's a true prefix of subsequent live
bytes. No duplicates, no gaps at the seam.

`SessionManager` exposes the new `subscribe_pty_with_replay`, and
`attach::serve_local` sends the snapshot as a single binary frame
before pumping the live receiver. End result: attaching to an idle
bash session now shows the prompt immediately; reattaching after
running a build sees the last screenful instead of a blank terminal.

Adds `[daemon] pty_replay_bytes` to the unified config (None ⇒
default; Some(0) ⇒ disabled). E2E test exercises the full path: client
A drives output through, disconnects, client B sees the prior output
on connect.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 22:56:23 +00:00