Files
nucleic/docs/WORKTREE_MANAGER.md
T
abkslmandClaude Opus 4.8 d49ad92c69 Store worktrees in-project at .nucleic/worktrees
Move the default session-worktree base from a repo sibling
(<repoParent>/.nucleic-worktrees/<repoSlug>) to inside the project at
<repoRoot>/.nucleic/worktrees. On first worktree creation, add .nucleic/
to the repo's local .git/info/exclude so the user's primary checkout
never reports it as untracked and the tracked .gitignore is untouched.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-06-13 20:24:11 -07:00

9.8 KiB
Raw Blame History

Nucleic — Git / Worktree Manager (v0)

The Conductor-defining feature: each session runs in an isolated git worktree + branch so many agents work the same repo in parallel without colliding. This component owns worktree lifecycle, diffing, and integration (merge / rebase / squash / PR) back to the main line.

Status: design draft. Git commands are spelled out so the plumbing is reviewable; they're the adapter detail, the Swift surface (§7) is the stable contract.


1. Invariants

  1. One session ↔ one worktree ↔ one branch. Never shared, never reused across sessions.
  2. The main working copy is never touched. All agent activity happens in a worktree; the user's primary checkout stays clean and usable.
  3. Structural git ops are serialized per repo. worktree add/remove, branch create, and integration take a per-repo lock. Working-tree edits inside a worktree are not locked — they're isolated by design (each worktree has its own index) and run fully in parallel.
  4. Survivable. A crash may leave worktrees/branches behind; startup reconciliation makes the DB and git worktree list agree again. No silent data loss.
  5. Agent work may be uncommitted. We treat the base commit as the diff anchor and capture working-tree changes whether or not the agent committed (§4).

2. Layout & naming

<repoRoot>/                       ← user's primary checkout (untouched)
<repoRoot>/.git/worktrees/<wt>/   ← git's per-worktree metadata (automatic)

<repoRoot>/.nucleic/worktrees/<sessionSlug>/   ← the agent's isolated checkout
  • worktreeBase default: inside the project under .nucleic/worktrees/, on the same volume (fast checkout, no cross-device copies). .nucleic/ is added to the repo's local .git/info/exclude on first worktree creation so the user's primary checkout never reports it as untracked and the tracked .gitignore is never modified. Default <repoRoot>/.nucleic/worktrees/; user-configurable per project via the worktreeBase override (which keeps the <override>/<repoSlug>/ layout for a shared external dir).
  • Branch: nucleic/<sessionSlug> where sessionSlug derives from the session title, lowercased/kebabbed, deduped with a short suffix on collision.
  • Worktree dir name = sessionSlug. Both branch and dir collisions are checked under the per-repo lock before creation.

3. Lifecycle state machine

              create()                 setup ok
   (none) ───────────────▶ provisioning ─────────▶ ready ──▶ active (agent running)
                               │ fail                              │
                               ▼                                   ▼ finalize()
                            failed                              finalizing
                                                                   │
                              integrate() / discard()              ▼
   cleaned ◀── removeWorktree ◀── merged | discarded ◀── integrated | conflicted
  • provisioning → ready: create worktree+branch, run setup script. Any failure rolls back (remove the half-made worktree/branch) and lands in failed.
  • active: the AgentBackend runs with cwd = worktree.path (this is the WorktreePath in BACKEND_PROTOCOL RunSpec).
  • conflicted: integration hit conflicts; user resolves in the worktree or aborts.

4. Status & diff (live UI source)

The session's base commit SHA is captured at creation (git rev-parse <base>), stored, and used as the diff anchor for the session's lifetime.

  • Everything-vs-base (tracked): git diff --no-color <baseSHA> compares the working tree to the base commit, so it captures committed, staged, and unstaged tracked changes in one shot — independent of whether the agent committed.
  • Untracked new files: enumerate via git status --porcelain=v2 --untracked-files=all; to render their diffs, use intent-to-add semantics (git add -N-style) in a scratch read so new files show as additions. (We do this without mutating the agent's index where possible.)
  • Diffstat / file list: parsed from git status --porcelain=v2 (rename/copy aware).
  • Ahead/behind vs integration target: git rev-list --left-right --count <target>...HEAD(behind, ahead).
  • Dirty/clean: porcelain output empty ⇒ clean.

Status is recomputed on demand and after each turn completes (cheap; can be debounced). The diff feeds the macOS live-diff pane and the iPhone snapshot diffStat.


5. Setup script

Per-project, optional (e.g. npm install, bundle install):

  • Runs in the new worktree, cwd = worktree.path, with project extraEnv, after branch creation and before the session is marked ready.
  • Stdout/stderr captured and surfaced as synthetic AgentEvent.raw/log entries so the user sees provisioning output in the session view.
  • Timeout + failure policy per project: block (session can't start) or warn (proceed, flag it). Default block so agents don't run against a broken environment.

6. Integration (finish a session)

Two phases: finalize (make the branch represent the work) then integrate (land it).

6.1 Finalize — handle uncommitted work

CommitOption:

  • .auto(message) — stage all (git add -A) and commit a snapshot so the branch is clean. Default; message templated from session title.
  • .manual — require the agent/user to have committed; refuse to integrate a dirty tree.
  • .none — leave as-is (only valid for discard).

6.2 Integrate — land the branch

IntegrationStrategy into a target ref (default = project default branch):

  • .mergegit merge --no-ff nucleic/<slug> (preserves the session as a merge).
  • .rebasegit rebase <target> then fast-forward (linear history).
  • .squashgit merge --squash + single commit (one tidy commit per session).

Performed on the target's worktree/checkout context under the per-repo lock. Result:

enum IntegrationResult {
    case clean(mergedInto: GitRef, commit: String)
    case conflicted([ConflictedPath])   // → state `conflicted`
}

Conflicts: detected from a non-zero merge/rebase exit + git status. We surface the conflicted paths in the UI and offer: (a) resolve in the worktree (optionally hand the conflict back to an agent as a new turn), then continue; or (b) abort (git merge/rebase --abort) and leave the branch intact.

6.3 PR flow (alternative to local integration)

git push -u <remote> nucleic/<slug> then gh pr create --base <target> --head nucleic/<slug> --title … --body …. Returns the PR URL. Requires a configured remote + gh auth; the local branch/worktree can then be cleaned or kept.

6.4 Discard

Confirm if the branch has unmerged commits or a dirty tree, then git worktree remove --force <path> + git branch -D nucleic/<slug>.


7. Swift surface

protocol WorktreeManager: Sendable {
    func create(for s: SessionID, in p: Project, base: GitRef, slug: String) async throws -> Worktree
    func status(of w: Worktree) async throws -> WorktreeStatus
    func diff(of w: Worktree, _ opts: DiffOptions) async throws -> Diff
    func finalize(_ w: Worktree, commit: CommitOption) async throws
    func integrate(_ w: Worktree, into target: GitRef, strategy: IntegrationStrategy) async throws -> IntegrationResult
    func createPR(_ w: Worktree, base: GitRef, title: String, body: String) async throws -> URL
    func discard(_ w: Worktree, force: Bool) async throws
    func reconcile(_ p: Project) async throws -> [Orphan]   // startup cleanup (§8)
}

struct Worktree: Sendable, Codable {
    let sessionID: SessionID
    let path: String
    let branch: String
    let baseSHA: String
    let createdAt: Date
}

struct WorktreeStatus: Sendable, Codable {
    let isDirty: Bool
    let ahead: Int
    let behind: Int
    let diffStat: DiffStat        // files, +adds, removes
    let conflicts: [String]       // non-empty ⇒ state conflicted
}

8. Reconciliation & cleanup (crash recovery)

On app launch, per registered project:

  1. git worktree list --porcelain ⨉ our DB of expected worktrees.
  2. In git, not in DB → orphan; offer cleanup (git worktree remove).
  3. In DB, not in git → stale record; mark the session error/cleaned.
  4. git worktree prune to clear deleted-dir metadata.
  5. Sessions left active from a previous run (process gone) → transition to interrupted; the worktree is intact and the session is resumable (BACKEND_PROTOCOL §6).

Guardrails: configurable max concurrent worktrees per repo, a disk-space preflight before create, and a warning when worktree count or total size crosses a threshold.


9. Considerations / open questions

  1. worktreeBase location — in-project .nucleic/worktrees/ (default, same-volume, git-excluded) vs. app-support (centralized but volume-crossing risk). Exposed as a per-project worktreeBase override.
  2. Auto-commit cadence — only at finalize, or periodic snapshot commits during a run so the live diff has a stable base and work is recoverable? (Leaning: finalize-only, with working-tree diff covering interim state.)
  3. Submodules / Git LFS / huge repos — worktree add can be slow or surprising with submodules and LFS; decide whether to --no-checkout + sparse for big repos, and how setup interacts with LFS smudge.
  4. Shared-ref contention — concurrent commits across many worktrees touch shared refs; confirm the per-repo lock scope is only structural ops (not every commit) to keep parallelism high.
  5. Conflict-to-agent loop — should an integration conflict be offer-able as a new agent turn ("resolve these conflicts") automatically, closing the loop without leaving the app?