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]>
9.8 KiB
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
- One session ↔ one worktree ↔ one branch. Never shared, never reused across sessions.
- The main working copy is never touched. All agent activity happens in a worktree; the user's primary checkout stays clean and usable.
- 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. - Survivable. A crash may leave worktrees/branches behind; startup reconciliation makes
the DB and
git worktree listagree again. No silent data loss. - 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
worktreeBasedefault: 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/excludeon first worktree creation so the user's primary checkout never reports it as untracked and the tracked.gitignoreis never modified. Default<repoRoot>/.nucleic/worktrees/; user-configurable per project via theworktreeBaseoverride (which keeps the<override>/<repoSlug>/layout for a shared external dir).- Branch:
nucleic/<sessionSlug>wheresessionSlugderives 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
AgentBackendruns withcwd = worktree.path(this is theWorktreePathin BACKEND_PROTOCOLRunSpec). - 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 projectextraEnv, after branch creation and before the session is markedready. - 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) orwarn(proceed, flag it). Defaultblockso 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 fordiscard).
6.2 Integrate — land the branch
IntegrationStrategy into a target ref (default = project default branch):
.merge—git merge --no-ff nucleic/<slug>(preserves the session as a merge)..rebase—git rebase <target>then fast-forward (linear history)..squash—git 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:
git worktree list --porcelain⨉ our DB of expected worktrees.- In git, not in DB → orphan; offer cleanup (
git worktree remove). - In DB, not in git → stale record; mark the session
error/cleaned. git worktree pruneto clear deleted-dir metadata.- Sessions left
activefrom a previous run (process gone) → transition tointerrupted; 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
worktreeBaselocation — in-project.nucleic/worktrees/(default, same-volume, git-excluded) vs. app-support (centralized but volume-crossing risk). Exposed as a per-projectworktreeBaseoverride.- 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.)
- 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. - 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.
- 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?