Commit Graph
34 Commits
Author SHA1 Message Date
Miguel Palhas 4df8fdb7dc feat(maestro): attach files in structured views 2026-09-07 10:20:08 +01:00
Miguel Palhas f765d415d6 fix(maestro): reject structured PTY access 2026-09-05 10:20:41 +01:00
Miguel Palhas 8f5d96a95b feat(maestro): derive structured activity 2026-09-05 04:08:43 +01:00
Miguel Palhas 6fa7dc63e4 fix: serialize structured prompt dispatch 2026-09-05 03:11:16 +01:00
Miguel Palhas d9815d86c6 feat(daemon): resolve ACP approvals 2026-09-05 02:32:01 +01:00
Miguel Palhas bbae09bc48 fix(daemon): reject dead structured sessions 2026-09-05 02:13:55 +01:00
Miguel Palhas 3c638b08de fix: clarify structured prompt failures 2026-09-05 02:05:06 +01:00
Miguel Palhas be23a2b539 fix: harden structured replay 2026-09-05 01:56:07 +01:00
Miguel Palhas b98f5cb3e5 feat(maestro): serve structured session API 2026-09-05 01:46:21 +01:00
Miguel Palhas f02168ffd5 feat(maestro): add SessionView to the protocol and sidecar
Adds `SessionView { Terminal, Structured }` to maestro-protocol, a
defaulted `view` field on `Session` and `CreateSessionRequest`, and
sidecar round-trip tests for both a legacy payload with no `view` key
and a `Structured` session. Nothing reads the field yet; every other
struct literal across the workspace picks up `Default::default()`
(Terminal) to keep compiling.

Gitea #133.
2026-09-04 19:41:29 +01:00
Miguel Palhas ba02b11f13 feat(maestro): serve the configured leader to the web client
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.
2026-09-03 18:33:01 +01:00
Miguel Palhas 9d9aef119d feat(maestro): stamp last-focused time on the session (#145)
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.
2026-09-03 14:05:36 +01:00
Miguel PalhasandClaude Opus 5 eaf60d9dac feat(maestro): stamp the daemon with its build commit
`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>
2026-09-02 15:40:41 +01:00
Miguel PalhasandClaude Opus 5 c9cfdb5668 feat(maestro): one-word handles, quieter project boxes
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>
2026-08-31 22:40:49 +01:00
Miguel PalhasandClaude Opus 5 d39fbdbbfc feat(maestro): drop retained worktrees and header debug toggle
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>
2026-08-31 09:00:52 +01:00
Miguel PalhasandClaude Opus 5 345650172b feat(maestro): rename a session over PATCH /api/sessions/:id
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>
2026-08-30 11:26:12 +01:00
Miguel Palhas 5a594b2b7f feat(maestro): serve POST /api/sessions/:id/activity 2026-08-24 21:38:42 +01:00
Miguel Palhas 31702e03e3 refactor(maestro): drop cleanup_on_kill from the wire and the CLI
Nothing has honoured WorktreeSpec.cleanup_on_kill since #56, and after
#79 and #81 survival is decided by what is in a worktree, not by what
the spawn asked for. A field that lies is worse than a missing one.

Removes the field from the protocol, the --cleanup-on-kill flag from
the CLI, and the tests asserting its default.
2026-08-24 12:08:41 +01:00
Miguel Palhas b96f18c7cb feat(maestro): forced kill ends the session and retains the worktree
--force overrides the kill refusal and nothing else (#81). The guarded
worktree is recorded in the retained store with reason force-killed —
never removed — and the outcome names what was kept so a client can say
where the work went. A tree whose guard is clear never needed the
override; force removes it as usual.

The store write happens before teardown, so a failed retention leaves
the session running and every artifact intact. DELETE /api/sessions/:id
takes ?force=true and answers 200 with KillOutcome; the unforced kill
keeps its 204.
2026-08-24 11:29:45 +01:00
Miguel Palhas 62581d9f85 merge: refuse a kill when the worktree holds unsaved work
Collapses the two probes #79 and #80 grew in parallel into one:
worktree_probe::in_the_way is the single predicate behind the kill
refusal, the retained list, and the natural-exit classifier to come.
Keeps #79's in-worktree resolution (detached HEAD, missing directory)
and #80's InTheWay payload, and drops the duplicate UnsavedWork type.

Closes #79
2026-08-24 11:09:24 +01:00
Miguel Palhas a575b8bef1 feat(maestro): refuse a kill when the worktree holds unsaved work (#79)
The guard moves off the worktree and onto the kill. DELETE
/api/sessions/:id now runs the shared unsaved_work probe before tearing
anything down and answers 409 kill_guarded with the breakdown on the
wire, so a client can say "3 uncommitted files, 2 unpushed commits".
The session keeps running and the tree is untouched.

WorktreeManager::remove is unconditional now — git itself still refuses
a dirty tree as a last line of defense.
2026-08-24 11:03:43 +01:00
Miguel Palhas 4921f62796 feat(maestro): GET /api/worktrees and DELETE reclaim endpoints 2026-08-24 10:59:14 +01:00
Miguel Palhas a970ba89c2 feat(maestro): GET /api/names serves name batches (#53) 2026-08-23 21:13:46 +01:00
Miguel Palhas 4a814d5b17 feat(maestro): project discovery scanner and GET /api/projects (#54) 2026-08-23 20:48:59 +01:00
Miguel PalhasandClaude Opus 5 db44650a17 refactor(maestro): collapse the daemon's double event fan-out
ci / rust (pull_request) Successful in 3m18s
ci / rust (push) Successful in 2m5s
A background task, a second broadcast channel and a whole module existed
to remap SessionEvent onto CatalogEvent one variant at a time.
SessionManager now emits CatalogEvent directly.

Deletes local_events.rs, the publisher task, the LocalEventBus channel,
enum SessionEvent, LocalApiState::events, and the duplicate capacity
constant — events_capacity was declared twice for one logical stream.

Lag semantics improve. There were two lag points: the publisher falling
behind the manager, which broadcast one Lagged to every client, and each
WS subscriber falling behind the bus. Only the per-subscriber one is left,
which pump_events already handled — a slow client now makes only itself
resync.

Adds the first daemon-level tests for WS /api/events: the Snapshot-then-
deltas contract, and that a late subscriber gets a Snapshot of current
state. Both were described only in a comment.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 13:37:15 +01:00
Miguel Palhas d006f4a7bd refactor(maestro): drop registry, p2p, and auth from daemon core
Maestro is now single-host: one daemon owns the machine's sessions and
clients talk to it directly. Removes the registry crate, the daemon's
registry client/broker/catalog mirror, JWT attach auth, cookie auth, and
the perimeter (T2) transport config.

The daemon API is now unauthenticated on its bind address, same trust
model as tmux. Session and CreateSessionRequest lose daemon_id;
RegistryEvent becomes CatalogEvent with no daemon variants; attach moves
to WS /api/attach/:id with no token.
2026-08-22 10:00:24 +01:00
naps62-yoloandClaude Opus 4.8 cc8412ca59 feat(maestro): spawn protocol types + relay + daemon HTTP for recents/gitroot/presets (#26)
Expose the daemon-side spawn data (#24 recents, #25 gitroot, #22 presets) to
the web, mirroring the NAP-329 ListDir broker plumbing end to end so it works
across federated hosts.

Wire (maestro-protocol): `RecentDir`/`RecentDirs`/`GitRoot` payloads;
`DaemonInbound::RecentDirs`/`GitRoot` requests + `ForwardRecentDirs`/
`ForwardGitRoot` relay pairs + their `*Result` replies on both enums. All
forward-compatible via the existing `RpcResult` + `#[serde(other)] Unknown`.

Relay (broker + registry + registry_client): home daemon → registry → owning
daemon → back, with the same fail-fast-while-disconnected + RAII pending-entry
cleanup discipline as ListDir. The owning daemon answers RecentDirs from its
spawn history and GitRoot by resolving the path against its home dir
(shared `dirlist::resolve`) then walking up via `gitroot::find_root`.

Daemon HTTP (maestro-daemon):
- GET /api/daemons/:id/recent-dirs → RecentDirs
- GET /api/daemons/:id/gitroot?path= → GitRoot
- Presets ride GET /api/web/info (the documented DaemonInfo sibling) so the
  form needs no extra round-trip; resolved once at startup from [spawn].presets
  (a bad preset list now refuses startup).

Tests: protocol round-trip + forward-compat for the new variants; broker
forward round-trips + fail-fast; HTTP handler tests (self recents/gitroot,
presets advertisement, peer 503 without a registry link).

Closes #26

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 21:09:18 +00:00
naps62-yolo 498cb471b7 feat(maestro): daemon+registry ListDir RPC + broker relay + HTTP dirs endpoint (NAP-329)
Also fixes two pre-existing test call sites that NAP-328 left passing the
old arity to register()/state_for() after adding home_dir.
2026-06-15 21:18:09 +00:00
naps62-yoloandClaude Opus 4.8 6d7cd0b4c8 feat(maestro): spawn form defaults command to claude + prefills cwd to daemon home dir (NAP-328)
Two web spawn QoL improvements plus the wire support they need:

- Command field now prefills `claude` instead of an empty box.
- Working directory prefills the selected daemon's home dir, updating on
  daemon change unless the user has typed their own value.

The daemon resolves its `$HOME` via `dirs::home_dir()` at startup and
advertises it end-to-end: a new `home_dir` field on `DaemonOutbound::Hello`
flows through the registry's `register` into `DaemonInfo` (served on
`/api/daemons` and `/api/events`). The field is `#[serde(default)]`, so older
daemons that omit it deserialize to an empty string — backward compatible, and
the UI simply skips prefill when it's empty.

Hierarchical `/`-separated labels in the web sidebar already worked
(web/src/catalog.ts); no change there.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 18:07:53 +00:00
naps62-yoloandClaude Opus 4.8 1e4b65c73c feat(maestro): daemon brokers cross-daemon create/kill instead of 501/404 (NAP-327)
The home daemon's `POST /api/sessions` (peer `daemon_id`) and
`DELETE /api/sessions/:id` (peer-owned) now broker the op to the owning daemon
over the registry control link rather than returning 501/404. This fixes
cross-daemon spawn/kill for *all* local clients — notably maestro-web, which
talks only to the local daemon and had no registry fallback.

- broker: `BrokerHandle::{forward_create,forward_kill}` + completion handlers,
  mirroring `resolve_attach` (fail-fast while disconnected, RAII pending
  cleanup, RESOLVE_TIMEOUT).
- registry_client: route `Forward{Create,Kill}Result` back to the broker.
- local_api: create forwards on peer `daemon_id`; kill looks up the owner in
  the catalog mirror and forwards (only a truly-unknown session is 404).
  `broker_error_response` maps BrokerError → HTTP (503/504/502/404/...).
- cli: thread the broker into `LocalApiState`.

Tests: broker round-trip / fail-fast / registry-err propagation; updated the
local_api integration test (peer create now 503s without a registry link
instead of 501, proving it brokers).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 16:24:48 +00:00
naps62-yoloandClaude Opus 4.7 a671e15114 feat(maestro): daemon WS /api/events for live catalog updates (NAP-248)
ci / rust (push) Failing after 7m28s
New module `local_events::LocalEventBus` (broadcast<RegistryEvent>) with
two publishers:

* `spawn_session_publisher` — translates SessionManager's own session
  events into the matching `RegistryEvent` variants (with daemon_id
  filled in for the SessionRemoved case).
* `CatalogMirror::apply` — re-publishes peer events after applying them
  to the local mirror, so subscribers see the post-apply state.

New `WS /api/events` route on the daemon: subscribes to the bus, sends
an initial `Snapshot` built from manager.snapshot() ∪ catalog state,
then forwards live deltas. `Lagged` follows the registry's convention
(client resyncs via REST).

Revert the REST-seed hack in web/src/events.ts — the WS endpoint now
exists, so `CatalogStore::start()` can go back to just `connect()`.

E2E: subscribe to a daemon's events WS, expect Snapshot then a
SessionAdded after POSTing /api/sessions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 22:01:08 +00:00
naps62-yoloandClaude Opus 4.7 cebb2d74b1 feat(maestro): daemon broker T2 + pair-JWT verifier + allow_inbound (NAP-247)
* New PairAttachVerifier (crates/maestro-daemon/src/jwt.rs): checks
  signature, aud == self, exp > now, and sub ∈ allow_inbound. Default
  deny — empty allow_inbound rejects every cross-daemon attach.
* /sessions/:id/stream tries the legacy verifier first, falls back to
  the pair verifier on mismatch. Compat with existing direct-attach.
* New BrokerHandle (crates/maestro-daemon/src/broker.rs): shared between
  the registry-client task and HTTP handlers; calls ResolveAttach over
  the control link and correlates the reply via a pending map.
* RegistryClient now owns a BrokerHandle, drains its outbound mpsc
  alongside session events / heartbeats, and routes
  ResolveAttachResult back into the pending map. First heartbeat fires
  immediately so the registry has the daemon's perimeter info before
  the first ResolveAttach.
* /api/attach/🆔 self-owned → local PTY; peer-owned (per catalog
  mirror) → ResolveAttach + T2 dial + bidirectional frame proxy.
  Returns structured errors for missing local perimeter, peer on a
  different perimeter, etc. T1/T3 stay stubbed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 20:32:29 +00:00
naps62-yoloandClaude Opus 4.7 2bf6f95506 feat(maestro): daemon CatalogMirror + own∪peer HTTP responses (NAP-246)
New module crates/maestro-daemon/src/catalog_mirror.rs: in-memory mirror of
the global catalog (peer daemons + their sessions), updated from the
`DaemonInbound::CatalogEvent` frames the registry now fans out.

`local_api` consumes it: `GET /api/sessions` returns the union of the
daemon's own sessions and the mirrored peers; `GET /api/daemons` returns
self + connected peers. The web UI sees the same catalog from any home
daemon.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 19:18:25 +00:00
naps62-yoloandClaude Opus 4.7 b0be488547 feat(maestro): daemon local-view HTTP API + broker WS self-short-circuit (NAP-243)
ci / rust (push) Failing after 10m36s
New module crates/maestro-daemon/src/local_api.rs serving:

* GET /api/sessions     — own sessions
* GET /api/daemons      — self only (v1; peer mirror is NAP-246)
* GET /api/web/info     — daemon identity (browser header data)
* POST /api/sessions    — create on self; 501 for cross-daemon (NAP-247)
* DELETE /api/sessions/:id — kill on self; 404 for unknown
* WS /api/attach/:id    — delegate to existing PTY subscribe when self-owned,
                          404 otherwise (cross-daemon broker is NAP-247)

The new routes share bind_addr with the legacy /sessions/:id/stream WS
endpoint, which keeps working unchanged. The shared serve loop is exposed
as attach::serve_local so the broker WS can reuse the PTY pump.

Trust model documented at the top of the module: no JWT, no cookie on
the new routes — same trust as tmux being one-user-wide. Operators who
bind a perimeter address opt into one-segment-wide trust.

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