The one that loses data: the project picker lists linked worktrees as
pickable rows, so `base_repo` was easily a worktree rather than the main
checkout, and the new placement then nested the tree inside it. Git hides
a registered worktree's contents from the outer `git status`, so the
outer tree read clean and removing it deleted the inner one's uncommitted
work — while that session could still be running. Placement now resolves
to the main checkout via `--git-common-dir`, and removal refuses outright
when `worktree list` reports anything registered underneath.
Also:
- The `worktree_root` override dropped the old UUID without replacing it,
so two repos spawning the same branch name targeted one directory and
the second failed. The repo basename now sits between root and slug.
- Project staleness read `.git/HEAD`'s mtime, which only moves on
checkout. A repo committed to daily from one long-lived branch reported
its last branch switch and got dimmed. Prefer `logs/HEAD`.
- The projects cache TTL (5s) was shorter than its own background refresh
(60s), so nearly every request rescanned all of $HOME under the scanner
lock — and draft panes re-request on unrelated re-renders. Now 180s.
- onSuccess resolved the leaf against the active tab only, so switching
tabs mid-POST orphaned the session. Searches every tab.
- toggle defaults on for any target the project scan reports (repo or
worktree); only the free-text escape hatch pays for /api/gitroot
- branch mode control switches between creating a new branch (named
after the session name, as before) and picking an existing one from
GET /api/branches; branches held by another worktree stay visible but
disabled, labelled with that worktree's path
- emits create_branch: false on the wire for existing-mode picks
- branch-list load follows the form's ready/destroyed discipline so a
late response cannot corrupt a rebuilt draft
Ten findings from reviewing the accumulated blitz/spawn-v2 diff. The two
that could wedge a repo:
- The cleanup guard refused any worktree whose commits were on no other
ref, which is every ordinary unpushed branch. Paths are deterministic
now, so the leftover tree blocked the next spawn and each branch became
a one-shot. `worktree remove` keeps refs/heads/<branch>, so committed
work is never at risk: guard on an unclean working tree alone.
- A worktree whose directory had vanished failed the git probe and was
reported as unsaved work, stranding the registration forever. Prune it
instead, and prune before `worktree add` tests the target path.
Also:
- The CLI hardcoded create_branch: true, so the daemon's existing-branch
path was unreachable. Added --existing-branch.
- Pastes were forwarded without their DECSET 2004 markers, so a remote
shell ran a multi-line paste line by line.
- spawnForm emitted before its seed was restored and after destroy(), so
a rebuilt draft blanked its cwd and a dead form clobbered the live one.
- showRawCwd and the probe's worktree untick never reached the leaf.
- A late autocomplete fetch reopened the popup under a blurred input.
- The "remove worktree when killed" checkbox promised something the
daemon no longer honours; removed.
Escape closes the popup, refocuses the input and stops propagation so it
can't also discard a surrounding draft pane. Tab commits the highlighted
entry and lets focus move to the next field. Blur / clicking outside
dismisses, and a dismissed popup stays closed until the operator types
again.
forward_to_session dropped Paste(_) on the floor, so pastes into a
session vanished once terminal.rs enabled bracketed paste. Split the
arm: Paste forwards the payload verbatim (the remote readline
reassembles it), only Mouse events have no wire encoding.
Action::OpenSpawnModal becomes NewDraftPane: pressing s turns the empty
pane into an inline draft form (or opens a new tab when the pane shows a
session). The form is a pane-level widget with its own field focus; a
successful submit replaces the draft with the new session in place,
matching the web client's draft panes. SpawnModal and OverlayKind::Spawn
are removed; kill-confirm and help overlays stay.
Six findings from reviewing the accumulated blitz/spawn-v2 diff:
- loadRecents restored the seeded cwd but never emitted it, so the leaf's
draft kept cwd:"" and the next render reseeded the form blank.
- stripDrafts did not recurse, so every half-filled draft in a split tab
was written to localStorage; the old test passed only because hydrate
discards draft on the way back in.
- showRawCwd and selectPreset grabbed focus from async init paths, so a
draft pane stole the caret seconds after mounting.
- .mv-draft inherited no layout once the form left .modal-body.
- onSuccess mounted the new session onto a leaf that may have been closed
while the POST was in flight, stranding it in no pane.
The umbrella `maestro` binary had zero coverage of its feature-gate
combinations (cli / daemon / registry / tui). Add:
- Unit tests in `main.rs` for `is_cli_subcommand` classification (CLI vs
server subcommands, plus the always-false non-`cli` stub) and the
`-v` verbosity -> tracing-directive mapping.
- A `verbosity_directive` helper, extracted from `cli_filter` purely to
make the verbosity mapping testable without touching the process-global
`RUST_LOG` (behavior unchanged).
- An integration test (`tests/cli.rs`) driving the real built binary via
`CARGO_BIN_EXE_maestro`, asserting the `--help`/`--version`/subcommand
dispatch surface. Per-subcommand checks are `#[cfg]`-gated so the suite
is meaningful under every feature subset.
- A CI feature-gate matrix step: build + test each of cli/daemon/registry/
tui in isolation, and assert the no-feature build is rejected.
All four subsets and the default build pass fmt, clippy, and tests.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The subtree search returned early whenever it found *any* group member,
so an in-tree stage that was busy or sleeping on a timer masked a
reparented member genuinely parked on the tty.
Checking every member instead would have meant a /proc scan per session
per tick whenever nothing was parked — the common case. ProcGroups
snapshots /proc once per activity pass and indexes by process group, so
the cost is one walk however many sessions the daemon owns, and the
subtree walk disappears: the index is a superset of it.
Also fixes the background-helper test, which had stopped discriminating.
It polled until awaiting_input was true and caught a moment before the
helper spawned, so it passed against a deepest-descendant walk too. It now
waits for the helper to exist first. Both regression tests were re-checked
by patching each old implementation back in.
Reported in review on #51.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A member of the foreground group can sit outside the pane's subtree: a
stage that forks a terminal-reading child and exits leaves that child
reparented to init while it keeps the foreground pgid. The subtree search
finds nothing and the session reads as idle.
An empty subtree result now falls back to scanning /proc for the group.
The scan only runs when the cheap path found nothing, so the common case
still costs a handful of reads.
Tested at the level it can honestly be tested: scan_for_pgid against this
process's own group, and against a group that cannot exist. Reproducing
the reparenting scenario end to end is not practical — setpgid across
sessions is EPERM, and from a shell the window closes as soon as the shell
reclaims the terminal.
Reported in review on #51.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>