Commit Graph
673 Commits
Author SHA1 Message Date
naps62-yoloandClaude Opus 4.7 4b9acc88de feat(maestro): daemon config defaults, env overrides, maestrod configure
ci / rust (push) Successful in 1m43s
Closes NAP-224. Daemon onboarding now mirrors the registry's NAP-221 /
NAP-222 ergonomics: a fresh host needs three values, not eight.

Config (`crates/maestro-daemon/src/config.rs`):
- Split into `DaemonConfigFile` (on-disk, every field `Option<>`) and
  `DaemonConfig` (resolved, all fields present).
- `load_config(path, EnvOverrides)` resolves env > file > default and
  returns clear `Missing { field, env, path }` errors for the three
  fields that genuinely require a human: `registry_url`, `daemon_url`,
  `token`.
- Defaults: `bind_addr = 127.0.0.1:7070`; `state_dir` /
  `worktree_root` from XDG; `hostname` from `gethostname(2)`;
  `daemon_id` defaults to `hostname`.
- Treats a missing config file as empty so an env-only deploy works.
- Atomic file save (tmp + rename) for `configure` to share.

Env vars (mirrors NAP-222):
- `MAESTRO_TOKEN`
- `MAESTROD_REGISTRY_URL`, `MAESTROD_BIND_ADDR`,
  `MAESTROD_DAEMON_URL`, `MAESTROD_DAEMON_ID`
- `MAESTROD_CONFIG` for the file path

`maestrod configure` (`configure.rs` + clap in `main.rs`):
- Interactive bootstrap when stdin is a TTY: prompts for the three
  required fields, defaults the rest.
- Non-interactive mode (`--non-interactive` or non-TTY stdin) takes
  values from flags only; errors out listing the first missing one.
- Refuses to overwrite an existing config without `--force`.
- Writes via the same atomic-save path used by config-file edits.

`maestrod` grew a clap CLI: `run` (default) and `configure` subcommands,
both honour `--config` / `$MAESTROD_CONFIG`.

Verified end-to-end:
- `maestrod configure --non-interactive --registry-url ... --daemon-url
  ... --token ...` writes a valid file and exits 0.
- `--non-interactive` without all required flags errors with the
  missing field name.
- Existing file + no `--force` is rejected and the file is untouched.
- Workspace `cargo test`: 115 passed (was 97; +18 new daemon tests
  across config + configure).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 10:15:06 +00:00
naps62-yoloandClaude Opus 4.7 2a902b713e feat(maestro): build web bundle into Docker image
ci / rust (push) Successful in 1m13s
NAP-218. Add a node:20 stage to the registry Dockerfile that runs
`pnpm install && pnpm build` against `web/`, then copies the
resulting `dist/` into the Rust builder so rust-embed picks it up.
Single-binary deploy still — no separate static-asset volume.

`.dockerignore` keeps `web/node_modules` and `web/dist` out of the
build context (the node stage produces them fresh) while letting the
sources through.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 09:57:26 +00:00
naps62-yoloandClaude Opus 4.7 077e9e958e feat(maestro): embed and serve web bundle from registry
NAP-218. The registry's `GET /` now serves the built web bundle from
`web/dist/` (embedded into the binary at compile time via rust-embed)
instead of the NAP-215 placeholder, and `GET /assets/*` serves the
hashed JS/CSS chunks Vite produces.

When `web/dist` is empty (fresh checkout without `pnpm build`), a
small fallback page tells the operator to run the JS build. That
keeps `cargo run -p maestro-registry` usable without a node
toolchain installed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 09:57:26 +00:00
naps62-yoloandClaude Opus 4.7 e68c9b6d73 feat(maestro): web client (Vite + TS + xterm.js)
NAP-218. Browser UI for the prototype: catalog tree derived from
`metadata.label`, live updates over `WS /api/events`, search across
session name+label, spawn modal (with optional git worktree), and an
attach view that opens the daemon's per-session WS for xterm.js.

Framework choice: vanilla TypeScript. The interactivity surface is
small enough that pulling in a framework wouldn't pay back; xterm.js
does the heavy lifting. Easy to swap later without protocol impact.

Hash-based routing (`#/`, `#/attach/<id>`) keeps the registry's
static surface dumb — no SPA fallback needed; concurrent attaches
work by opening the same hash URL in another tab.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 09:57:11 +00:00
naps62-yoloandClaude Opus 4.7 b68e39a9e5 feat(maestro): maestro-cli — list/spawn/attach/kill
ci / rust (push) Successful in 1m7s
Closes NAP-216. Thin client for the registry HTTP API + per-session
daemon WebSocket. Becomes the headless test harness for NAP-217 and
unblocks the demo-by-script path before the web client lands (NAP-218).

Crate layout (`crates/maestro-cli/`):

- `cli.rs` — clap-derive parser. Commands: `list`, `spawn`, `attach`,
  `kill`, `set-registry`. Global `--registry-url` (env
  `MAESTRO_REGISTRY_URL`), `--config`, repeatable `-v`.
- `config.rs` — `CliConfig` persisted at `~/.config/maestro/cli.toml`,
  storing the default registry URL + the session cookie issued by the
  registry's cookie middleware.
- `http.rs` — `RegistryClient` wrapping `reqwest`. Attaches the
  persisted cookie, captures `Set-Cookie` responses (first-contact
  bootstrap and rotation), atomically writes the new value back to
  the config file.
- `sessions.rs` — `list/spawn/kill` + connect-info fetch. `list` prints
  a per-daemon grouped table; `--json` for scripting.
- `attach.rs` — raw-TTY WS proxy. Termios cfmakeraw with an RAII guard
  for restore-on-drop, SIGWINCH → `ClientControl::Resize` text frame,
  configurable detach key (default Ctrl-q; accepts `^X` form), echoes
  `Ping`/`Pong` for keepalive symmetry with the daemon side.

Workspace deps added: clap (derive+env), reqwest (rustls+cookies),
nix (term+signal).

Smoke-tested against the live registry at maestro-registry.n62.casa:
`maestro list` issued + persisted the cookie and printed the empty
catalog cleanly. 19 new unit tests (parser, config round-trip,
table-printing, cookie extraction, attach URL/detach-key parsing).
Full workspace `cargo test`: 97 passed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 09:49:46 +00:00
naps62-yoloandClaude Opus 4.7 8586f46dad feat(maestro): Dockerfile for maestro-registry (Dokploy-ready)
ci / rust (push) Successful in 1m1s
Multi-stage build (rust:1-bookworm builder → debian:bookworm-slim
runtime) for the `maestro-registry` binary. Uses BuildKit cargo cache
mounts so subsequent rebuilds are fast.

Defaults `MAESTRO_REGISTRY_BIND_ADDR=0.0.0.0:8080` so the container
is reachable from a reverse proxy (Traefik, etc.) without any extra
config. Operator supplies `MAESTRO_TOKEN` as a platform secret; with
that set the binary needs no writable volume.

`.dockerignore` keeps `target/`, `worktrees/`, `.git/`, docs, and the
WIP `web/` out of the build context.

Verified: docker build succeeds; `docker run -e MAESTRO_TOKEN=...
-p 8080:8080 maestro-registry` boots and responds 200 on the cookie
auth endpoint.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 20:14:22 +00:00
naps62-yoloandClaude Opus 4.7 44e0a50192 feat(maestro): env-var overrides for registry token + bind_addr
Lets `maestro-registry` run in container deployments (e.g. Dokploy)
without mounting a writable config volume.

- `MAESTRO_TOKEN`: when set, bypasses `registry.toml` entirely (no
  read, no write). The container needs no persistent volume just to
  seed the shared secret — pass it as a platform secret instead.
- `MAESTRO_REGISTRY_BIND_ADDR`: overrides the file's `bind_addr`.
  Lets the container bind `0.0.0.0:$PORT` without rewriting TOML.
- New `load_config(path, env_token, env_bind_addr)` in `token.rs`
  centralises the env > file > default precedence. Takes explicit
  `Option`s so tests don't touch process-global env.
- `TokenError::InvalidBindAddr` carries the raw input + parse error
  so misconfigured deploys log a useful diagnostic.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 20:12:12 +00:00
naps62-yoloandClaude Opus 4.7 3022ee2c62 feat(maestro): add maestro-registry binary entry point
ci / rust (push) Successful in 1m0s
Closes NAP-221. Adds the `maestro-registry` bin so the registry can be
spawned as a child process (required by the NAP-217 E2E harness) and
run as a long-lived service.

- New `crates/maestro-registry/src/main.rs` with a `#[tokio::main]`
  entry point: loads config via `ensure_token` (override path via
  `MAESTRO_REGISTRY_CONFIG`), binds the configured `bind_addr`, serves
  the production router, and shuts down cleanly on Ctrl-C. Mirrors the
  `maestrod` wiring (EnvFilter tracing, ExitCode reporting).
- `[lib]` + `[[bin]]` sections in `crates/maestro-registry/Cargo.toml`
  so the crate exposes both surfaces (same pattern as `maestro-daemon`).
- `RegistryConfig` gains a `bind_addr: SocketAddr` field with
  `serde(default)` to default to `127.0.0.1:8080` per spec §Registry.
  Existing TOML files without the field still load.
- `default_config_path` re-exported from the crate root for the bin.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 17:28:20 +00:00
naps62-yoloandClaude Opus 4.7 18bd385a2a feat(maestro): registry HTTP API, cookie auth, JWT issuance, events WS
ci / rust (push) Successful in 48s
Adds the client-facing surface on the registry (NAP-215). Endpoints:

- GET /api/daemons — list connected daemons (with daemon_url)
- GET /api/sessions — flat session catalog
- POST /api/sessions — forwards CreateSession to chosen daemon, 504 on
  10s daemon timeout
- DELETE /api/sessions/:id — forwards KillSession
- GET /api/sessions/:id/connect-info — returns {daemon_url, jwt} with a
  5min HS256-signed attach JWT
- WS /api/events — snapshot + delta broadcast of catalog and presence
- GET / placeholder served until NAP-218 web bundle lands

Cookie auth middleware mints a long-lived httpOnly JWT cookie on first
request and reissues silently on missing/tampered cookies. Per spec the
perimeter is the operator's network — the cookie is a second factor.

Protocol additions: daemon_url on Hello, DaemonInfo, ConnectInfo,
RegistryEvent. Daemon config + RegistryClient now carry daemon_url.

Registry state now exposes an event broadcast channel (subscribe gives
a snapshot + tokio::sync::broadcast::Receiver) and helper lookups
daemon_for_session / daemon_url.

Tests: 11 reqwest-driven HTTP integration tests cover every endpoint,
the cookie flow, 504 timeout path, worktree forwarding, and events WS
propagation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 17:08:41 +00:00
naps62-yolo 499cffe397 fix(maestro): use AsyncFd for FIFO PTY reader (non-blocking)
ci / rust (push) Successful in 40s
`tokio::fs::File` routes every `.read()` through `spawn_blocking`. On
an idle FIFO opened r+w, that thread blocks in `read()` forever — and
when the test runtime tries to shut down, the blocking-pool thread is
stuck on the syscall and the test binary hangs (which is exactly what
just took out CI run 11).

Switch to `std::fs::File` opened `O_RDWR | O_NONBLOCK` wrapped in
`tokio::io::unix::AsyncFd`. Reads now drive off epoll readiness with
`try_io`; no blocking-pool threads, no hung shutdown.

`Drop` still aborts the task so per-test cleanup is clean.
2026-05-24 16:47:44 +00:00
naps62-yolo 2b84783968 fix(maestro): drop stale wait_for_async import in attach test too
ci / rust (push) Has been cancelled
2026-05-24 16:17:28 +00:00
naps62-yolo fb2a0b1e6a fix(maestro): drop stale wait_for_async import in session_manager test
ci / rust (push) Failing after 1m14s
2026-05-24 16:10:18 +00:00
naps62-yolo 0624ad1257 style(maestro): apply cargo fmt
ci / rust (push) Failing after 1m15s
2026-05-24 15:53:35 +00:00
naps62-yolo f64d9312ee ci(maestro): drop container, install rustup + tmux on default image
ci / rust (push) Failing after 27s
`rust:1.83-bookworm` has no Node, which breaks `actions/checkout@v4`
(it's a JS action). The default gitea runner image has Node, so go back
to that and install rustup + tmux + git + build-essential ourselves.
2026-05-24 15:37:33 +00:00
naps62-yolo 22d079145f ci(maestro): fix runner image + trigger on linear/** branches
ci / rust (push) Failing after 26s
CI has been red since NAP-211 because docker.gitea.com/runner-images:
ubuntu-latest has no `rustup`; the toolchain install step exits 127.

- Use `rust:1.83-bookworm` as the job container so rustup + cargo + a
  stable toolchain are pre-installed
- Install `tmux` + `git` so the daemon integration tests can run
- Add `rustfmt` and `clippy` components
- Also trigger on pushes to `linear/**` so yolo branches see CI before
  they're merged
2026-05-24 15:24:44 +00:00
naps62-yolo ab9a5bf017 feat(maestro): wire daemon to registry + per-session WS stream
Completes the headless daemon stack for NAP-214.

- `config`: daemon TOML config (daemon_id, registry_url, bind_addr,
  token, state_dir, worktree_root) with sensible XDG defaults.
- `jwt`: HS256 verifier; the daemon validates per-attach JWTs locally
  with no callback to the registry. Also exposes `issue()` for tests.
- `pty::PtyBridge`: per-session FIFO + tokio broadcast pump. The FIFO is
  opened r+w so it never sees zero writers — stays alive across
  `pipe-pane` re-issues. Helper `pipe_pane_command()` builds the
  `cat >> <fifo>` shell line.
- `session::SessionManager`: authoritative live-session store. Creates
  worktrees (when requested), spawns tmux, mkfifo + pipe-pane, writes
  sidecar; tears it all down on kill. Emits diff events on a
  broadcast for the registry client. `recover()` adopts surviving
  `maestro-*` tmux sessions on startup and re-pipes them.
- `registry_client::RegistryClient`: persistent outbound WS to the
  registry with exponential backoff. Sends Hello → snapshot, drives a
  select loop that pumps inbound CreateSession/KillSession RPCs into
  the manager (replying with results), emits SessionAdded/Removed/
  Updated diffs from the manager, and heartbeats every 10s.
- `attach`: axum router mounting `WS /sessions/:id/stream?token=<jwt>`.
  Binary frames carry PTY bytes either way (subscribe to broadcast →
  send to client; receive from client → tmux send-keys -l). Text
  frames carry `ClientControl::{Resize,Ping,Pong}`.
- `bin/maestrod`: ties everything together, loads config from
  `MAESTROD_CONFIG` env or `~/.config/maestrod/config.toml`,
  recovers, spawns the registry client, binds the WS server.

Also adds `JwtClaims` to `maestro-protocol` so the daemon and the
future registry JWT issuer (NAP-215) agree on the wire shape.

Integration tests cover session manager round-trip + recovery + PTY
broadcast; attach endpoint JWT (bad/wrong-session/wrong-daemon),
PTY round-trip through real tmux, multi-attach broadcast, ping/pong;
registry client registration + snapshot + diff forwarding +
CreateSession RPC round-trip.

Closes NAP-214.
2026-05-24 15:22:36 +00:00
naps62-yolo c721b5ec62 feat(maestro): add registry daemon-side WS handler and state
ci / rust (push) Failing after 6s
Registry-side primitives for the daemon↔registry conversation. No
client-facing HTTP yet (that's NAP-215).

- `state::Registry`: in-memory store of connected daemons + a session
  catalog keyed by (daemon_id, session_id). Tracks pending CreateSession
  / KillSession RPCs so a single daemon connection can multiplex them.
- `handler::handle_socket`: per-connection loop that authenticates the
  Hello, applies snapshot/diff/heartbeat frames, and resolves matching
  *Result frames. Heartbeat watchdog is a `tokio::time::timeout` around
  the read side — no separate janitor task.
- `server::router_with_timeout`: axum router mounting `WS /daemon`. The
  timeout knob lets tests verify the 30s drop path in milliseconds.
- `token::ensure_token`: first-run bootstrap that generates a 256-bit
  hex token, prints it once to stderr, and persists it to
  `~/.config/maestro/registry.toml`.
- Outbound `create_session` / `kill_session` exposed as the internal
  API NAP-215's HTTP handlers will call.

Integration tests use a real axum server bound to `:0` and drive it via
`tokio-tungstenite`, covering: bad-token reject, snapshot accept, diff
apply (add/update/remove), heartbeat timeout dropping the daemon,
re-registration replacing the catalog, and a full CreateSession RPC
round-trip.

Closes NAP-213.
2026-05-24 13:42:00 +00:00
naps62-yolo 946aa384a0 feat(maestro): add daemon tmux/sidecar/worktree primitives
ci / rust (push) Failing after 3s
Local primitives for `maestrod`, no network code yet:

- `tmux`: subprocess wrapper around new-session, kill-session,
  list-sessions (filtered to maestro-*), send-keys -l, pipe-pane -o,
  resize-window, kill-server.
- `sidecar`: atomic JSON metadata store keyed by session UUID, with
  load_all for daemon restart recovery.
- `worktree`: `git worktree add -b` with collision-proof generated paths
  and `git worktree remove --force` for cleanup_on_kill.

Integration tests use real tmux (isolated via unique socket + temp
TMUX_TMPDIR) and real git (tmpdir repo).

Closes NAP-212.
2026-05-24 13:02:41 +00:00
naps62-yoloandClaude Opus 4.7 777d246aab feat(maestro): add maestro-protocol crate with wire types
ci / rust (push) Failing after 25s
Introduces the maestro-protocol crate (NAP-211): the shared serde
types that the daemon, registry, and clients all serialize across
WebSocket boundaries. Covers:

  - Session, SessionStatus
  - CreateSessionRequest with optional WorktreeSpec
  - RpcResult<T> reply envelope
  - DaemonOutbound (Hello, snapshot, add/remove/update,
    Heartbeat, request results)
  - DaemonInbound (Ack, Error, CreateSession, KillSession)
  - ClientControl text frames (Resize, Ping, Pong, Error)

Schema mirrors the wire-protocol tables in the design spec. Unit
tests round-trip every variant and assert tag/discriminator
stability so future renames break loudly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 12:35:16 +00:00
naps62-yoloandClaude Opus 4.7 de8db03a18 chore(maestro): scaffold cargo workspace and CI
Adds the empty Cargo workspace (resolver = 2, members = crates/*),
a stable rust-toolchain pin with rustfmt + clippy, and a Gitea
Actions pipeline that runs cargo fmt --check, clippy -D warnings,
and cargo test on every push/PR.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 12:35:08 +00:00
naps62-yoloandClaude Opus 4.7 dc963a27f7 docs(maestro): add repo-level CLAUDE.md
Orients future sessions and Linear-driven skills: points at the spec,
the Linear project, the dependency chain (NAP-211 is the starting
point), the target workspace layout, and conventions (TDD, conventional
commits with maestro scope, real tmux+git in integration tests, no
mocking).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 12:05:27 +00:00
naps62-yoloandClaude Opus 4.7 810a4fbc28 docs(maestro): make network layer agnostic in spec
Removes Tailscale assumptions from the design. Operator configures bind
addresses and is responsible for reachability + perimeter (Wireguard,
private LAN, or any other mechanism); maestro does not police the network
or terminate TLS itself.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 11:52:17 +00:00
naps62-yoloandClaude Opus 4.7 7af764c1a3 chore(maestro): initial scaffold and brainstorm spec
Adds .claude/linear.json for Linear-driven skills (Maestro project on
team Naps62) and the brainstorm design doc covering architecture, wire
protocol, session model, auth, and prototype scope boundary.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 11:51:03 +00:00