# PRD: Branchless Worktree Architecture **Author:** Lex Christopherson **Date:** 2026-03-15 **ADR:** [ADR-001-branchless-worktree-architecture.md](./ADR-001-branchless-worktree-architecture.md) **Priority:** Critical — blocks reliable auto-mode operation --- ## Problem Statement SF's auto-mode is unreliable. Users experience: 1. **Infinite loop detection failures** — the agent writes planning artifacts on slice branches that become invisible after branch switching, causing `verifyExpectedArtifact()` to fail repeatedly. Auto-mode burns budget retrying the same unit 3-6 times before hard-stopping. This is the #1 user complaint. 2. **State corruption across branches** — `.sf/` planning artifacts (roadmaps, plans, decisions) are gitignored but branch-specific. Multiple branches sharing a single `.sf/` directory clobber each other's state. Users see wrong milestones marked complete, wrong roadmaps loaded, and auto-mode starting from the wrong phase. 3. **Excessive complexity** — 770+ lines of merge, conflict resolution, branch switching, and self-healing code exist solely to manage slice branches inside worktrees. This code has required 15+ bug fixes across versions and remains the primary source of auto-mode failures. These problems are architectural. They cannot be fixed by patching individual symptoms. ## Vision Auto-mode uses git worktrees for isolation and sequential commits for history. No branch switching. No merge conflicts within a worktree. Planning artifacts are tracked in git and travel with the branch. The git layer is so simple it can't break. ## Success Criteria | Criterion | Measurement | |-----------|-------------| | Zero loop detection failures from branch visibility | No `verifyExpectedArtifact()` failures caused by branch mismatch in 50 consecutive auto-mode runs | | Zero `.sf/` state corruption | Manual worktrees created via `git worktree add` have correct `.sf/` state without any SF-specific initialization | | Code deletion | Net removal of ≥500 lines of merge/conflict/branch-switching code | | Test simplification | Removal or simplification of ≥6 merge-specific test files | | Backwards compatibility | Existing projects with `sf/M001/S01` slice branches continue to work (read-only; new work uses new model) | | No new git primitives | The implementation uses only: worktrees, commits, squash-merge. No new branch types, merge strategies, or conflict resolution. | ## Non-Goals - Parallel slice execution within a single worktree (if needed later, use separate worktrees) - Changing how milestones relate to `main` (squash-merge stays) - Modifying the dispatch unit types or state machine (except removing `fix-merge`) - Changing the worktree-manager.ts manual worktree API (`/worktree` command) ## Current Architecture ### Branch Model (M003, v2.13.0) ``` main └─ milestone/M001 (worktree at .sf/worktrees/M001/) ├─ sf/M001/S01 (slice branch — code + .sf/ artifacts) │ └── merge --no-ff → milestone/M001 ├─ sf/M001/S02 │ └── merge --no-ff → milestone/M001 └── squash merge → main ``` ### Data Flow ``` Agent writes file → on slice branch → handleAgentEnd → auto-commit on slice branch → switch to milestone branch → verifyExpectedArtifact → FILE NOT FOUND (it's on slice branch) → loop counter++ → retry → same result → HARD STOP ``` ### Code Involved | File | Lines | Purpose | |------|-------|---------| | `auto-worktree.ts` | 512 | Worktree lifecycle + slice→milestone merge | | `git-service.ts` | 915 | Branch creation, switching, merge with conflict resolution | | `git-self-heal.ts` | 198 | Merge failure recovery | | `auto.ts` | ~150 lines | Merge dispatch guards, fix-merge routing, branch-mode vs worktree-mode branching | | `worktree.ts` | ~40 lines | Slice branch delegates | | 11 test files | ~2000 lines | Merge/branch/worktree test coverage | ### `.sf/` Tracking (Current — Contradictory) - `.gitignore` line 52: `.sf/` — ignores everything - `smartStage()` lines 338-349: force-adds `SF_DURABLE_PATHS` — tracks milestones/, DECISIONS.md, PROJECT.md, REQUIREMENTS.md, QUEUE.md - Result: `.sf/milestones/` is partially tracked on some branches, fully ignored on others. The code fights the config. ## Proposed Architecture ### Branch Model ``` main └─ milestone/M001 (worktree at .sf/worktrees/M001/) │ commit: feat(M001): context + roadmap commit: feat(M001/S01): research commit: feat(M001/S01): plan commit: feat(M001/S01/T01): implement auth service commit: feat(M001/S01/T02): implement auth tests commit: feat(M001/S01): summary + UAT commit: docs(M001): reassess roadmap after S01 commit: feat(M001/S02): research commit: feat(M001/S02): plan commit: ... commit: feat(M001): milestone complete │ └── squash merge → main ``` One branch. Sequential commits. No merges within the worktree. ### Data Flow ``` Agent writes file → on milestone branch → handleAgentEnd → auto-commit on milestone branch → verifyExpectedArtifact → FILE FOUND (same branch) → persist completion → next dispatch ``` ### `.sf/` Tracking (Proposed — Coherent) **Tracked (travels with branch):** ``` .sf/milestones/**/*.md (except CONTINUE markers) .sf/milestones/**/*.json (META.json integration records) .sf/PROJECT.md .sf/DECISIONS.md .sf/REQUIREMENTS.md .sf/QUEUE.md ``` **Gitignored (ephemeral):** ``` .sf/auto.lock .sf/completed-units.json .sf/STATE.md .sf/metrics.json .sf/sf.db .sf/activity/ .sf/runtime/ .sf/worktrees/ .sf/DISCUSSION-MANIFEST.json .sf/milestones/**/*-CONTINUE.md .sf/milestones/**/continue.md ``` ### Why This Works | Problem | How It's Solved | |---------|----------------| | Artifact invisibility after branch switch | No branch switching. Artifacts commit on the one branch. | | `.sf/` state clobbering | Artifacts tracked in git. Each branch carries its own `.sf/`. `git worktree add` and `git checkout` give correct state. | | Merge conflict complexity | No merges within a worktree. Only merge is milestone→main (squash). | | Manual worktree initialization | Tracked artifacts are checked out with the branch. No SF-specific bootstrap needed. | | Dual isolation mode maintenance | Single mode: worktree. Branch-mode (`git.isolation: "branch"`) deprecated. | ## Implementation Plan ### Phase 1: `.gitignore` + Tracking Fix **Goal:** Planning artifacts are tracked in git. `.gitignore` reflects reality. 1. Update `.gitignore`: - Remove blanket `.sf/` ignore - Add explicit runtime-only ignores (see proposed list above) 2. Force-add existing planning artifacts on current branch: ``` git add --force .sf/milestones/ .sf/PROJECT.md .sf/DECISIONS.md .sf/REQUIREMENTS.md .sf/QUEUE.md ``` 3. Ensure runtime files are NOT tracked: ``` git rm --cached -r .sf/runtime/ .sf/activity/ .sf/STATE.md .sf/metrics.json .sf/completed-units.json .sf/auto.lock ``` 4. Update README suggested `.gitignore` section 5. Remove `smartStage()` force-add of `SF_DURABLE_PATHS` — no longer needed since `.gitignore` doesn't block them **Verification:** `git status` shows planning artifacts tracked, runtime files untracked. `git worktree add` on a new worktree has correct `.sf/milestones/` state. ### Phase 2: Remove Slice Branch Creation + Switching **Goal:** No code creates, switches to, or references slice branches for new work. 1. Remove `ensureSliceBranch()` from `git-service.ts` (lines 485-544) 2. Remove `switchToMain()` from `git-service.ts` (lines 549-563) 3. Remove `getSliceBranchName()` from `worktree.ts` (lines 94-98) 4. Remove `isOnSliceBranch()` and `getActiveSliceBranch()` from `worktree.ts` 5. Update `auto.ts` dispatch paths — remove branch creation before `execute-task` 6. Update `handleAgentEnd` — remove branch-switching logic post-dispatch **Verification:** Auto-mode runs a full slice (research → plan → execute → complete) without creating any branches. All commits land on `milestone/`. ### Phase 3: Remove Slice Merge Code **Goal:** All slice→milestone and slice→main merge code is deleted. 1. Remove `mergeSliceToMilestone()` from `auto-worktree.ts` (lines 253-350) 2. Remove `mergeSliceToMain()` from `git-service.ts` (lines 705-893) 3. Remove merge dispatch guards from `auto.ts` (lines 1635-1679) 4. Remove `fix-merge` dispatch unit type from `auto.ts` 5. Remove `buildPromptForFixMerge()` from `auto.ts` 6. Remove `withMergeHeal()` from `git-self-heal.ts` (lines 99-136) 7. Remove `abortAndReset()` from `git-self-heal.ts` (lines 37-84) — or simplify to crash-recovery-only 8. Remove `shouldUseWorktreeIsolation()` preference resolution — worktree is the only mode 9. Remove `getMergeToMainMode()` — milestone merge is the only mode 10. Deprecate `git.isolation: "branch"` and `git.merge_to_main: "slice"` preferences **Verification:** `git grep mergeSliceToMilestone` returns zero results. `git grep mergeSliceToMain` returns zero results. `git grep fix-merge` returns zero results (outside of changelog/docs). ### Phase 4: Simplify `mergeMilestoneToMain()` **Goal:** Milestone→main merge is clean and minimal. The function becomes: 1. Auto-commit any dirty state in worktree 2. `process.chdir(originalBasePath)` — back to main repo 3. `git checkout main` 4. `git merge --squash milestone/` 5. Build commit message with milestone summary + slice manifest 6. `git commit` 7. Optional: `git push` 8. `removeWorktree()` + `git branch -D milestone/` No conflict categorization. No runtime file stripping (runtime files are gitignored, not in the merge). No `.sf/` special handling. If squash-merge conflicts (parallel milestone edge case): stop auto-mode with clear error, user resolves manually or SF dispatches a one-time resolution session. **Verification:** Complete a full milestone in auto-mode. `main` receives one squash commit with all code and planning artifacts. ### Phase 5: Test Cleanup **Goal:** Test suite reflects the simplified architecture. 1. Delete or rewrite: - `auto-worktree-merge.test.ts` — tests slice→milestone merge (deleted) - `auto-worktree-milestone-merge.test.ts` — rewrite for simplified milestone→main - `worktree-e2e.test.ts` — rewrite for branchless flow - `worktree-integration.test.ts` — rewrite for branchless flow - Merge-related test cases in `git-service.test.ts` 2. Add new tests: - Branchless worktree lifecycle: create → commit → commit → squash-merge → cleanup - `.sf/` tracking: planning artifacts tracked, runtime files ignored - Manual worktree: `git worktree add` has correct `.sf/` state - Crash recovery: dirty state on milestone branch, restart, auto-commit, continue 3. Remove merge-specific doctor checks or simplify: - `corrupt_merge_state` — keep (still relevant for milestone→main) - `orphaned_auto_worktree` — keep - `stale_milestone_branch` — keep - `tracked_runtime_files` — keep **Verification:** `npm run test` passes. No test references `mergeSliceToMilestone`, `mergeSliceToMain`, or `ensureSliceBranch`. ### Phase 6: Migration + Backwards Compatibility **Goal:** Existing projects with slice branches continue to work. 1. State derivation (`deriveState()`) continues to read `sf/M001/S01` branch naming for legacy detection 2. On first run after upgrade: - Detect existing slice branches - Notify user: "SF no longer creates slice branches. Existing branches are preserved but new work commits directly to the milestone branch." - No forced migration — legacy branches are read-only context 3. Doctor check: `legacy_slice_branches` — informational, not auto-fix 4. Update `shouldUseWorktreeIsolation()` preference handling: - `git.isolation: "worktree"` → default behavior (only option) - `git.isolation: "branch"` → warning, treated as worktree - Remove preference UI for isolation mode **Verification:** Open a project with existing `sf/M001/S01` branches. SF reads state correctly, new work commits on milestone branch without slice branches. ## Stress Test Results Validated by three independent models: ### Gemini 2.5 Pro — 6 Attack Vectors | Attack | Severity | Mitigation | |--------|----------|------------| | Parallel milestone code conflict at squash-merge | Medium | `git rebase main` before squash. Rare in single-user. | | SQLite desync after `git reset --hard` | Low | DB rebuilt from tracked markdown on startup (M001/S02 importers). | | Ghost lock after SIGKILL | Low | Existing heartbeat lock detection handles this. | | Squash merge loses bisect granularity | Low | Commit messages tag slices. Branch preservable if needed. | | Disk space with multiple worktrees | Low | Single active milestone at a time. Immediate cleanup. | | Plan-action atomicity gap (crash between write and commit) | Low | `handleAgentEnd` auto-commits. Sequential model simplifies recovery. | ### GPT-5.4 (Codex) — Codebase-Informed Analysis - Confirmed `smartStage()` force-add already implements tracked-artifact intent - Confirmed `resolveMainWorktreeRoot` (PR #487) contradicts this architecture - Confirmed `.sf/milestones/` partially tracked on `main` despite `.gitignore` - Verdict: **Model is sound. Removes only accidental complexity.** ### GPT-5.4 (Codex) — Dissenting Opinion Codex agreed on tracked artifacts and worktree-per-milestone, but pushed back on removing slice branches, calling it "a redesign, not a simplification." Specific concerns: | Concern | Rebuttal | |---------|----------| | Crash recovery for orphaned slice branches disappears | The failure mode (orphaned branch needing merge) is caused by slice branches. Removing branches removes the failure. Sequential commits on one branch need no orphan recovery. | | Concurrent edits to shared root docs (DECISIONS.md) from two terminals | Standard content conflict at squash-merge time. Not caused by or solved by slice branches. | | Continuous integration via slice→milestone merges | In sequential single-user work, there's nothing to integrate against within the worktree. Pre-flight rebase before squash-merge is more direct. | | Need a replacement slice-boundary primitive | Accepted: conventional commit tags (`feat(M001/S01):`) + optional git tags (`sf/M001/S01-complete`) serve as boundaries. | Codex's analysis confirms the tracked-artifact approach but recommends treating branchless as a deliberate redesign with explicit replacement primitives, not a casual deletion. ### Edge Case: Two Milestones Touching Same Source Files Scenario: M001 and M002 both modify `src/auth.ts`. M001 squash-merges first. Resolution: Before M002 squash-merges, rebase onto updated `main`: ``` cd .sf/worktrees/M002 git fetch origin main git rebase main # Resolve any conflicts (code-only, never .sf/) # Then squash-merge ``` This is standard git workflow. SF can automate the rebase step as a pre-merge check. ### Edge Case: Agent Crash Mid-Commit Scenario: Power loss during `git commit` on the milestone branch. Resolution: Git's internal journaling protects the object store. On restart: - If commit completed: state is consistent - If commit didn't complete: working directory has uncommitted changes, `handleAgentEnd` auto-commits on next dispatch - No branch to be "stuck between" — single branch means no split-brain state ### Edge Case: User Edits Main While Worktree Active Scenario: User makes manual commits on `main` while M001 worktree is active. Resolution: Worktree is on `milestone/M001` branch, independent of `main`. Manual `main` commits don't affect the worktree. At squash-merge time, `git merge --squash` handles the divergence normally. If there's a conflict, it's resolved once. ## Metrics ### Before (Current) | Metric | Value | |--------|-------| | Merge/conflict/branch code | 770+ lines across 4 files | | Merge-related test files | 11 files | | Branch types | 4 (main, milestone/*, sf/*/*, worktree/*) | | Merge strategies | 3 (--no-ff, --squash, conflict resolution) | | Dispatch unit types with merge logic | 2 (complete-slice, fix-merge) | | Isolation modes | 2 (branch, worktree) | | Doctor git checks | 4 | ### After (Proposed) | Metric | Value | |--------|-------| | Merge/conflict/branch code | ~50 lines (simplified `mergeMilestoneToMain` only) | | Merge-related test files | 3-4 files (rewritten) | | Branch types | 2 (main, milestone/*) | | Merge strategies | 1 (--squash) | | Dispatch unit types with merge logic | 0 | | Isolation modes | 1 (worktree) | | Doctor git checks | 3-4 (simplified) | ### Net Impact - **~720 lines deleted** (net, after simplified replacements) - **~7 test files deleted or consolidated** - **2 branch types eliminated** - **2 merge strategies eliminated** - **1 dispatch unit type eliminated** (fix-merge) - **1 isolation mode eliminated** (branch) - **0 merge conflicts possible within a worktree** ## Dependencies - **M001 (Memory Database):** The SQLite database (`sf.db`) must remain gitignored. The M001/S02 importer layer rebuilds it from tracked markdown. This PRD's `.gitignore` update explicitly ignores `sf.db`. - **PR #487:** Must be closed. The `resolveMainWorktreeRoot` approach (sharing `.sf/` across worktrees) contradicts tracked-artifact architecture. ## Open Questions 1. **Squash vs `--no-ff` for milestone→main merge?** Squash gives clean history on `main` but loses bisect granularity. `--no-ff` preserves granular commits but clutters `main`. Current proposal: squash (matching existing behavior), with option to preserve milestone branch for debugging. 2. **Should `worktrees/` move outside `.sf/`?** Having worktrees inside `.sf/` creates a nesting-doll pattern (worktree contains `.sf/` which is inside `.sf/worktrees/`). Relocating to `.sf-worktrees/` or `~/.sf/worktrees//` is cleaner but changes the filesystem layout. Recommendation: defer, address separately if it causes issues. 3. **Pre-flight rebase automation?** Before milestone→main squash-merge, should SF automatically `git rebase main`? Gemini recommends yes. Risk: rebase can fail with conflicts, adding a code path. Recommendation: implement as a doctor check ("milestone branch is behind main by N commits") with manual resolution, automate later if needed.