Commit Graph
26 Commits
Author SHA1 Message Date
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 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 Palhas 1b289a0450 feat(maestro): daemon auto-installs claude-code hook on first start 2026-08-24 22:25:54 +01:00
Miguel Palhas 8370ddf066 feat(maestro): hold agent-reported activity above inference for a TTL 2026-08-24 21:36:56 +01:00
Miguel Palhas 44b9745e8c feat(maestro): read-side in-the-way probe and guard-free reclaim 2026-08-24 10:53:38 +01:00
Miguel Palhas 132b606830 feat(maestro): retained-worktree store in state_dir (#78)
Per-entry JSON files under <state_dir>/retained-worktrees/, written like
sidecar records: atomic tmp+rename, missing/malformed files tolerated on
read. Entries carry path, base repo, branch, reason (force-killed,
exited-with-unsaved-work), timestamp; they leave on reclaim or when
prune_missing notices the directory was removed by hand.
2026-08-24 10:35:26 +01:00
Miguel Palhas 2a84e07f7d feat(maestro): GET /api/branches — local branches and worktree occupancy (#55) 2026-08-23 21:14:16 +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 PalhasandClaude Opus 5 63898cef25 feat(maestro): put a Panes seam behind Tmux
ci / rust (pull_request) Successful in 1m45s
ci / rust (push) Successful in 1m38s
SessionManager held a concrete Tmux, so its decisions — spawn rollback
ordering, liveness diffing, activity change-detection — could only be
reached by forking a real tmux server. Inner now holds an Arc<dyn Panes>;
Tmux is the production adapter, FakePanes the one tests drive.

Arc<dyn Panes> rather than a type parameter: a generic would spread
through LocalApiState, attach and cli, and one vtable hop is free next to
a fork+exec.

Adds tests/session_decisions.rs — six cases, no subprocess, 0.24s.
Replaces the chmod-0o500 rollback test, which detected root and returned
early, so on CI it asserted nothing; the failure is now forced by a
regular file sitting where the sidecar directory's parent must be, which
no uid can work around. It also asserts the rollback *order*, not just
the end state.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 12:44:13 +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 fd80ae152a feat(maestro): infer session activity state + foreground (Active/Idle/AwaitingInput) (#18)
ci / rust (push) Failing after 2m15s
Add an orthogonal activity dimension to sessions, inferred entirely from
structured tmux/`/proc` state — no PTY byte-stream parsing, no work on the
attach hot path.

Protocol: `ActivityState` enum (Active/Idle/AwaitingInput) + three additive,
serde-defaulted `Session` fields (`foreground`, `activity`, `last_activity_at`)
so the snapshot/diff flow stays forward-compatible with older daemons.

Detection (piggybacks the existing ~2s liveness tick, no new timer):
- foreground command via `tmux list-panes -F '#{pane_pid} #{pane_current_command}
  #{pane_tty}'`;
- quiescence from a per-`PtyBridge` last-read `Instant`, bumped in `run_reader`;
- AwaitingInput via best-effort `/proc` walk to the deepest descendant: state
  `S` + a non-timer `wchan` + an stdio fd on the pane tty. (`/proc/pid/syscall`
  is ptrace-gated and unreadable for own sessions, so `wchan` distinguishes a
  blocking read from a plain `sleep`.)
- combine: recent output → Active; else input-blocked → AwaitingInput; else
  Idle. Emit `SessionUpdated` only on (activity, foreground) change — no diff
  storms at steady state.

`maestro list` gains an ACTIVITY column and shows the live foreground. Pure
combine/parse logic is unit-tested; an e2e asserts `cat` → AwaitingInput and
`sleep` → Idle with correct foreground against real tmux.

Closes #18
Closes #8

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 14:51:08 +00:00
naps62-yoloandClaude Opus 4.8 b5fabff8e4 feat(maestro): durable per-host spawn history + recent-dirs query (#24)
Add a `spawn_history` daemon module backing the spawn form's "recent dirs"
picker. Sidecars are deleted on kill, so they can't be the source — use an
append-only JSONL log at `<state_dir>/spawn-history.jsonl`, one record per
successful spawn (cwd, command, name, worktree_branch, at).

- Write: `SessionManager::create` appends after a successful spawn,
  best-effort — a log-write failure only warns, never fails the spawn.
- Read: `recent_dirs()` folds newest-first, dedups by cwd, drops entries past
  the recency cap, caps the result count.
- Prune: opportunistic `compact()` from `append` once the file passes a size
  threshold; atomic tmp+rename rewrite.
- Config: `[daemon].spawn_history_days` (default 30, `0` disables). History is
  wired into the manager at daemon startup.

Time-dependent logic is factored into `*_as_of(now)` helpers for
deterministic unit tests. Adds module unit tests (append/dedup/recency
prune/compaction/disabled/malformed/cap), a config resolution test, and
real-tmux integration tests proving create→append→read end to end.

Closes #24

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 20:54:21 +00:00
naps62-yoloandClaude Opus 4.8 099dfc17b0 feat(maestro): gitroot walk-up probe for worktree base_repo inference (#25)
ci / rust (push) Successful in 5m56s
Add a pure `gitroot::find_root` resolver that walks up from a path to the
nearest ancestor holding a `.git` entry (directory or linked-worktree file)
and returns that repo toplevel, else None. Mirrors the `dirlist` module
shape: filesystem-only, no `git` subprocess, unit-tested in isolation.

Drives the spawn form's worktree section (#21): infer `base_repo` from the
chosen working dir so a worktree spawn needs only a branch. HTTP/wire
exposure lands in #26.

Closes #25

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 20:37:17 +00:00
naps62-yoloandClaude Opus 4.8 b6833ede43 refactor(maestro): drop allow_inbound policy gate, keep JWT session binding (NAP-339)
The peer (T2) attach path had a per-pair access-control layer
(`[daemon.policy].allow_inbound`): a target-side allow-list of daemon ids
permitted to broker-attach, enforced inside JWT verification, hot-reloadable via
SIGHUP / `POST /api/policy/reload`, with a `maestro policy` CLI. On a flat-trust
perimeter (the operator's VPN/firewall is the boundary for which hosts connect)
that granularity is unused, and deny-by-default made it config friction —
cross-daemon attach silently failed until every peer was listed.

Remove the allow-list; keep the registry-minted pair-attach JWT and ALL its
binding checks (iss=registry, aud=target, sid=session, exp). The firewall gates
host->port reachability; it does not stop a reachable host from attaching to a
session it wasn't handed or replaying a token elsewhere — that's the JWT's job
and it stays. Only the `sub in allow_inbound` check is dropped.

Removed: DaemonPolicySection / [daemon.policy].allow_inbound (config + daemon),
policy.rs (SIGHUP listener + GET/POST /api/policy), the `maestro policy` CLI,
PairAttachVerifier's allow-list field/reload/snapshot + JwtError::NotAllowed,
and the e2e broker_rejects_when_target_doesnt_allow_inbound test. The NAP-265
pre-upgrade-502 path stays covered by broker_attach_returns_502_on_perimeter_mismatch.

Also dropped the unit's ExecReload=kill -HUP: with no SIGHUP handler left,
default SIGHUP disposition is terminate, so wiring `systemctl reload` to it would
kill the daemon. Re-introducing a per-pair gate later is a verify-time change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 07:07:55 +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-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-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 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
naps62-yolo 90127e0ed3 feat(maestro): wire maestro-daemon to unified config (NAP-233)
Daemon now loads `[shared].registry_url`, `[shared].token`, and the
`[daemon]` section from the unified file via maestro-config. The
registry-WS endpoint is derived from `[shared].registry_url` (scheme
swap + `/daemon`), so operators configure a single base URL instead of
duplicating the daemon-specific endpoint.

Env-var renames (all per the new `MAESTRO_<SECTION>_<FIELD>` convention):
- MAESTROD_REGISTRY_URL → MAESTRO_SHARED_REGISTRY_URL (and the value is
  now an http(s) base, not the ws daemon endpoint)
- MAESTROD_DAEMON_URL   → MAESTRO_DAEMON_URL
- MAESTROD_DAEMON_ID    → MAESTRO_DAEMON_ID
- MAESTROD_BIND_ADDR    → MAESTRO_DAEMON_BIND_ADDR
- MAESTROD_CONFIG       → MAESTRO_CONFIG
- MAESTRO_TOKEN         → MAESTRO_SHARED_TOKEN

`maestrod configure` now writes the unified file with read-modify-write
so any existing `[cli]` / `[registry]` sections on the same host are
preserved. The interactive prompts reflect the new shape: registry URL
is asked for as a base URL, and the token prompt names the new env var.

Missing-file behaviour is hard cutover: if the config doesn't exist and
not every required field comes from env, the daemon errors with a
message pointing at `maestrod configure` and explicitly noting that the
legacy paths (~/.config/maestrod/config.toml) are no longer read.

e2e harness updated to match: `Daemon::spawn` takes the registry's HTTP
base, and the spawned subprocess sees the renamed env vars.
2026-05-26 09:31:15 +00:00
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-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 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