Commit Graph
13 Commits
Author SHA1 Message Date
Miguel PalhasandClaude Sonnet 5 49ab7e9c81 fix(maestro): suffix branch name on new-branch worktree collision
ci / rust (push) Failing after 3m26s
ci / web (push) Successful in 43s
ci / flake (push) Successful in 3m54s
git worktree add -b fails outright when the branch already exists, even
if the target directory is free. A random fast-forward handle commonly
collides with a branch an earlier, already-removed worktree left behind.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-04 13:29:49 +01:00
Miguel Palhas 2dcef860a6 fix(maestro): group worktree sessions under their repository
The sidebar nested a worktree session under the worktree, so a repo grew
a <repo>/worktrees/<branch> folder holding one session of the same name,
and the repo stopped being where you look for the work.

The daemon recorded whatever base_repo the request named, which is the
worktree itself when spawning from inside one; it now records the
resolved main checkout. The web tree groups by that instead of the cwd.
The TUI sidebar is a flat list with no directory grouping, so it needs
no mirror.

Closes #99
2026-08-24 17:17:35 +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 898c3cc763 feat(maestro): shared unsaved-work probe for worktrees (#79)
One function, unsaved_work(), returns the structured breakdown —
uncommitted tracked changes, untracked non-ignored files, unpushed
commits — so the kill guard and the natural-exit classifier (#82)
answer from the same place. Ignored files never count: every Rust
checkout has a target/, and counting those would leave no session
killable.

WorktreeManager::remove drops its own dirty-tree guard; git itself
still refuses a dirty tree, and the real guard moves onto the kill.
2026-08-24 10:59:08 +01:00
Miguel Palhas 0457a61a61 refactor(maestro): sidestep worktree path collisions with a numeric suffix
Anything occupying <repo>/worktrees/<branch> — one of ours, one the
operator made, an unrelated directory — is now bypassed by appending
-2, -3, ... to the directory; the branch stays as requested. The same
suffix applies under the [daemon].worktree_root override.

The daemon no longer asks whether an occupant is "ours": delete
registered_for(), the adoption branch in run_add and the
SessionManager::create live-session check, plus WorktreeError::PathExists.
A branch already checked out elsewhere is git's refusal to surface,
already marked by the branch picker's is_head/worktree flags.
2026-08-24 10:38:32 +01:00
Miguel Palhas bbbc24a4aa fix(maestro): fifth-review findings, incl. the guarded-worktree dead end
The cleanup guard plus deterministic paths had created a trap: one
untracked file an agent left behind — a log, a .env, a stray artifact —
made the guard refuse removal, the directory stayed, and every later
spawn of that branch hit PathExists. The branch was burned permanently
and the product offered no way to clear it. A leftover tree registered
for the same branch is now adopted, so the operator lands back in their
own unsaved work. A tree a *live* session holds is still rejected, since
two sessions must not share a checkout.

Also:

- Fast-forward still awaited /api/info, /api/projects and /api/names
  inline, the exact freeze the draft loaders were restructured to avoid.
  It posts to the same channel now.
- Draft updates were routed by tab index, which shifts when a tab closes:
  an in-flight load could be dropped or land on the wrong pane. Drafts
  carry an id and are found by it.
- extend_names left the name it had just displayed at the head of the
  queue, so the next reroll produced it again and the key looked dead.
- The fast-forward fallback discarded info.home_dir, so its draft showed
  unabbreviated paths and seeded "Other directory…" with / — disagreeing
  with the normal draft entry point.
- Web fast-forward mounted via openSession, which replaces the focused
  pane: pressing it while attached evicted that session from the layout.
  It reuses an empty pane or splits, as the TUI does.
- branchEdited was inferred from the seeded branch value, but in "new"
  mode that value is the auto-synced session name — so any form rebuild
  froze the branch against later rerolls.
- resolveBaseRepo ignored the worktree checkbox, rebuilding the fieldset
  expanded under an unticked box and probing gitroot for a worktree the
  operator had turned off.
- short_hash used DefaultHasher, whose algorithm is not stable across
  Rust releases, in a directory name — a toolchain bump would orphan
  every worktree under the override root. Now FNV-1a.
2026-08-24 00:28:50 +01:00
Miguel Palhas e9368bb242 fix(maestro): fourth-review findings across daemon, tui and web
The existing-branch picker could never succeed in either client: both
treated the main checkout's HEAD as available, and since that is usually
`main` it became the default pick — git then refuses the second checkout.
A TUI test had baked the bug in by asserting the selection lands on the
`is_head` entry. Both pickers now treat "checked out anywhere" as held.

Worktree paths were still not injective. Flattening `/` and `.` to `-`
made `feat/x`, `feat.x` and `feat-x` share one directory, and the
override root keyed on the repo basename alone, so `~/a/maestro` and
`~/b/maestro` collided. A branch name is already a valid relative path —
git stores it as refs/heads/feat/example — so it is used verbatim and
nests under `worktrees/feat/`; the override root gains a digest of the
full repo path. Git forbids holding both `feat` and `feat/x`, so the
nesting cannot collide for valid refs.

Also:

- Recovered sessions dropped their worktree association, so after a
  restart a kill cleaned up nothing and the leftover directory blocked
  that branch forever. It is persisted on Session now.
- The nesting guard compared git's resolved paths against an unresolved
  one, so it silently passed on any symlinked repo path — and that guard
  is what stands between a kill and an inner tree's uncommitted work.
- Draft loads ran on the TUI event loop: /api/projects on a cold cache
  walks all of $HOME, freezing keys, frames and attached panes for up to
  the request timeout. They are detached tasks posting to a channel now.
- Fast-forward in the web client created the session and never mounted
  it, so `N` looked like a dead key while the TUI attached.
- The fast-forward fallback seed forced the worktree toggle off, against
  both a normal draft and the TUI.
- The projects background refresh honoured the TTL, so two of every three
  ticks were no-ops and requests paid for the rescan instead.
2026-08-24 00:10:35 +01:00
Miguel Palhas 8b3c5d73b6 fix(maestro): third-review findings — nested worktrees, staleness, placement
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.
2026-08-23 21:36:29 +01:00
Miguel Palhas 8376122dfe fix(maestro): second-review findings across daemon, tui and web
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.
2026-08-23 20:54:21 +01:00
Miguel Palhas d8acc3adde feat(maestro): always clean up worktrees on kill behind a dirty-work guard 2026-08-23 20:17:08 +01:00
Miguel Palhas 018f44727c feat(maestro): place worktrees at <repo>/worktrees/<branch> 2026-08-23 20:09:38 +01:00
Miguel Palhas 85e7749e5a feat(maestro): worktree specs can check out an existing branch 2026-08-23 20:05:26 +01: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