Agentwt

One Rust binary that judges every worktree's integration state and merge order and writes one JSON document per repo.

It replaces cmd/worktrees/print_agent_worktrees_status.sh, cmd/worktrees/_lib/verdict.sh, the merge, cherry-pick, sync and remove scripts, and smine's Go status parser; the smine config server renders its output. Evidence for each choice: the fleet research notes in git history (docs/fleet-research.md).

Rust terms, once

TermMeaning
CrateA package: a library crate is imported with use, a binary crate run. worktrunk ships both from one repo; we use only its binary.
Cargo featuresCompile-time switches; gix's tree merge sits behind features = ["merge"].
serdeDerives JSON (de)serialization on our structs.

Dependencies and why

CrateRoleDecision
gix ≥ 0.87 (merge feature)In-process merge engine: merge_base, merge_commits, merge_trees, virtual merge bases, rename tracking, typed conflicts, TreatAsUnresolved::git().The engine; jj-lib chose it too.
serde, serde_jsonJSON in (worktrunk, registries, rollouts) and out.Standard.
worktrunkNone as a crate.Library API breaks most releases, all git via subprocess, no gix. Run wt as a pinned binary, parse its JSON.
git2 / libgit2Not used.Recursive strategy, index-based output, openssl baggage; gix covers it.
go-gitn/aNo merge-tree, cherry, patch-id or worktree list.

Modules

inventory reads worktrunk, gitstate judges each branch with gix, sessions joins the session registries, order computes the conflict matrix and train, ops replaces the scripts.

inventory: worktrunk JSON

wt -C <repo> list --format=json --branches
RuleDetail
ParseOwned structs; assert schema == 2; a test validates against worktrunk.dev/schema/list-v2.json. Pin the wt version.
Absent vs nullAbsent integration = determined not integrated; null = undetermined or timeout. Model as Option<Option<T>> or a three-state enum; never collapse.
Kept fieldspath, head, changes booleans (dirty = any of staged/modified/renamed/deleted/conflicted), locked/prunable/detached/operation, default_branch.{ahead, behind, integration, merge_conflicts}, upstream. Ignored: branch_mismatch, marker.
Prefix filterOn branch (claude/, claude-routines/), applied here; detached rows (the Codex pool) kept on purpose.
Per-candidate re-targetingPer non-agent candidate branch, wt list with env GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=worktrunk.default-branch GIT_CONFIG_VALUE_0=<candidate> (verified: no config write). Yields contained-in and squashed-into across candidates without a cherry loop.
Accepted side effectsThe first run writes worktrunk.default-branch into the repo config and creates .git/wt/cache/.
RetryOnce on a non-zero exit: git worktree list can fail while the Desktop pool is mid-removal.

gitstate: gix

Opens the main repo once; bare access suffices, no working copy.

AnswerHow
FROMThe branch reflog. A bare SHA origin resolves to the candidate branches containing it (an ancestor check per candidate); a routine chain resolves through the previous routine to its root. unknown only when nothing contains the SHA.
Ahead/behind vs FROMTwo rev-walks; worktrunk's numbers (vs the default branch) stay separate.
UnpickedPatch-id per commit = hash of its diff against its first parent (gix diff, whitespace-normalized); unpicked when it appears in no candidate's merge-base..candidate range. Replaces git cherry.
Applied verdictCherry-pick simulation: merge_trees(base = commit^, ours = FROM tip, theirs = commit); clean and result tree == FROM tree → applied. No probe worktree.
Merged-intoEach candidate's first-parent history searched for a merge commit whose non-first parent is the branch tip. Cheap; kept.
DroppedResolved and picked-resolved (range-diff interdiff, subject twins): no library equivalent; the order module answers the same question forward-looking.
Untracked filtergix status as the git status equivalent, excluding .idea/, .claude/, .claude-worktree, .DS_Store; the remove gate uses the same list.

sessions: registries

SourceRule
Claude~/.claude/sessions/*.json, liveness by pid. Joined to a worktree on cwd by longest prefix (the file holds no branch). Carries status, waitingFor, name, sessionId, entrypoint, hostSessionId.
CodexRollout session_meta: cwd, originator, git.commit_hash; joined on path or head sha to the detached pool rows. The chat_processes.json schema is unconfirmed: optional.
Multi-profileEvery CLAUDE_CONFIG_DIR sibling; the account from oauthAccount.accountUuid.

order: matrix and train

#StageRule
1InputsDefault tip M, agent tips B1..Bn, plus a dirty snapshot per worktree (the working tree written as a tree object into a throwaway object store), so live edits count: the gap in clash.
2PrefilterTouched-path sets from merge_base(M, Bi)..Bi; disjoint pairs commute.
3Pairwisemerge_commits(Bi, Bj) with TreatAsUnresolved::git(), fail_on_conflict for the cheap probe, rename tracking on; store clean or the unresolved paths. Filter with is_unresolved; never count every entry (clash's defect).
4TrainT0 = M; per candidate order, merge_commits(T(k-1), Bk). Clean: write the merged tree and a synthetic commit into the throwaway store, continue. Conflict: record (k, paths), continue from T(k-1). Objects go to a temp object dir with the repo objects as alternate; nothing lands in .git/objects.
5OrderingConflict-graph components merge independently. Within one: greedy fewest-conflicts-with-remaining, brute force for components ≤ 6, verified with the train. Per branch: conflicts_with, train position, first blocking pair, residual conflicts.
6CostN=12 → ≤ 66 pair merges, in-process, sub-second.

ops: replacing the scripts

OpRule
mergegit merge --no-edit <branch> semantics into the main checkout's HEAD: refuse dirty, abort on conflict. gix has no porcelain merge with working-copy update: shell to git for this one command, or implement merge_commits + checkout. Never wt merge.
cherry-pickSame rule.
syncMerge base into the branch, never rebase.
removeOwn safety predicate (contained somewhere, clean tree under the untracked filter), then git worktree remove. Not wt remove: it refuses locked worktrees and uses default-target containment.
Destructive flags

--force and --delete-branch destroy work as named, so the Go callers archive first: the Repos page salvages a checked-out worktree before a forced removal and writes the branch diff before a deletion, both into one trash stamp, and refuses the removal if the archive fails.

Output contract

One JSON document per repo, rendered by the config server; nothing else parses git.

FieldHolds
repoThe repository.
checkoutbranch, dirty
rows[]worktree, branch, from, counts (vs FROM and vs default), verdicts, merged_into, contained_in[] (with a reason per candidate), unsafe_reasons[], session{}, order{conflicts_with[], train_position, blocking}

Three-state fields serialize as absent / null / value, mirroring worktrunk, so the UI keeps one rule.

Open items