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
| Term | Meaning |
| Crate | A package: a library crate is imported with use, a binary crate run. worktrunk ships both from one repo; we use only its binary. |
| Cargo features | Compile-time switches; gix's tree merge sits behind features = ["merge"]. |
| serde | Derives JSON (de)serialization on our structs. |
Dependencies and why
| Crate | Role | Decision |
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_json | JSON in (worktrunk, registries, rollouts) and out. | Standard. |
| worktrunk | None as a crate. | Library API breaks most releases, all git via subprocess, no gix. Run wt as a pinned binary, parse its JSON. |
| git2 / libgit2 | Not used. | Recursive strategy, index-based output, openssl baggage; gix covers it. |
| go-git | n/a | No 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
| Rule | Detail |
| Parse | Owned structs; assert schema == 2; a test validates against worktrunk.dev/schema/list-v2.json. Pin the wt version. |
| Absent vs null | Absent integration = determined not integrated; null = undetermined or timeout. Model as Option<Option<T>> or a three-state enum; never collapse. |
| Kept fields | path, 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 filter | On branch (claude/, claude-routines/), applied here; detached rows (the Codex pool) kept on purpose. |
| Per-candidate re-targeting | Per 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 effects | The first run writes worktrunk.default-branch into the repo config and creates .git/wt/cache/. |
| Retry | Once 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.
| Answer | How |
| FROM | The 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 FROM | Two rev-walks; worktrunk's numbers (vs the default branch) stay separate. |
| Unpicked | Patch-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 verdict | Cherry-pick simulation: merge_trees(base = commit^, ours = FROM tip, theirs = commit); clean and result tree == FROM tree → applied. No probe worktree. |
| Merged-into | Each candidate's first-parent history searched for a merge commit whose non-first parent is the branch tip. Cheap; kept. |
| Dropped | Resolved and picked-resolved (range-diff interdiff, subject twins): no library equivalent; the order module answers the same question forward-looking. |
| Untracked filter | gix status as the git status equivalent, excluding .idea/, .claude/, .claude-worktree, .DS_Store; the remove gate uses the same list. |
sessions: registries
| Source | Rule |
| 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. |
| Codex | Rollout 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-profile | Every CLAUDE_CONFIG_DIR sibling; the account from oauthAccount.accountUuid. |
order: matrix and train
| # | Stage | Rule |
| 1 | Inputs | Default 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. |
| 2 | Prefilter | Touched-path sets from merge_base(M, Bi)..Bi; disjoint pairs commute. |
| 3 | Pairwise | merge_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). |
| 4 | Train | T0 = 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. |
| 5 | Ordering | Conflict-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. |
| 6 | Cost | N=12 → ≤ 66 pair merges, in-process, sub-second. |
ops: replacing the scripts
| Op | Rule |
| merge | git 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-pick | Same rule. |
| sync | Merge base into the branch, never rebase. |
| remove | Own 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.
| Field | Holds |
| repo | The repository. |
| checkout | branch, 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
- The Codex
chat_processes.json schema; whether Codex hooks fire in Codex Desktop.
- gix parity is the maintainers' own baseline claim. Run the discrimination plan (git history:
docs/investigation-baseline.md §8) once: current table vs the new binary on claude-configs, then train order vs a real merge in a scratch clone.
- Whether to keep FROM once the default-branch numbers and the train exist; today FROM is unknown for 13 of 17 rows.