Module: Aidp::Worktree

Defined in:
lib/aidp/worktree.rb

Overview

Manages git worktree operations for parallel workstreams. Each workstream gets an isolated git worktree with its own branch, allowing multiple agents to work concurrently without conflicts.

Defined Under Namespace

Classes: Error, NotInGitRepo, WorktreeExists, WorktreeNotFound

Class Method Summary collapse

Class Method Details

.create(slug:, project_dir: Dir.pwd, branch: nil, base_branch: nil, task: nil) ⇒ Hash

Create a new git worktree for a workstream

Parameters:

  • slug (String)

    Short identifier for this workstream (e.g., "iss-123-fix-login")

  • project_dir (String) (defaults to: Dir.pwd)

    Project root directory

  • branch (String, nil) (defaults to: nil)

    Branch name (defaults to "aidp/#slug")

  • base_branch (String) (defaults to: nil)

    Branch to create from (defaults to current branch)

Returns:

  • (Hash)

    Worktree info: { path:, branch:, slug: }



26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
# File 'lib/aidp/worktree.rb', line 26

def create(slug:, project_dir: Dir.pwd, branch: nil, base_branch: nil, task: nil)
  ensure_git_repo!(project_dir)

  branch ||= "aidp/#{slug}"
  worktree_path = worktree_path_for(slug, project_dir)

  if Dir.exist?(worktree_path)
    # Directory exists but not in registry - try to recover it
    recovered = recover_existing_worktree(slug, worktree_path, branch, project_dir, task)
    return recovered if recovered

    raise WorktreeExists, "Worktree already exists at #{worktree_path}"
  end

  branch_exists = branch_exists?(project_dir, branch)
  run_worktree_add!(
    project_dir: project_dir,
    branch: branch,
    branch_exists: branch_exists,
    worktree_path: worktree_path,
    base_branch: base_branch
  )

  # Initialize .aidp directory in the worktree
  ensure_aidp_dir(worktree_path)

  # Register the worktree
  register_worktree(slug, worktree_path, branch, project_dir)

  # Initialize per-workstream state (task, counters, events)
  Aidp::WorkstreamState.init(slug: slug, project_dir: project_dir, task: task)

  {
    slug: slug,
    path: worktree_path,
    branch: branch
  }
end

.exists?(slug:, project_dir: Dir.pwd) ⇒ Boolean

Check if a worktree exists

Parameters:

  • slug (String)

    Workstream identifier

  • project_dir (String) (defaults to: Dir.pwd)

    Project root directory

Returns:

  • (Boolean)


144
145
146
# File 'lib/aidp/worktree.rb', line 144

def exists?(slug:, project_dir: Dir.pwd)
  !info(slug: slug, project_dir: project_dir).nil?
end

.find_by_branch(branch:, project_dir: Dir.pwd, create_if_not_found: false, base_branch: nil) ⇒ Hash?

Find a worktree by branch name

Parameters:

  • branch (String)

    Branch name to search for

  • project_dir (String) (defaults to: Dir.pwd)

    Project root directory

  • create_if_not_found (Boolean) (defaults to: false)

    Whether to create a worktree if not found

  • base_branch (String, nil) (defaults to: nil)

    Base branch to use if creating a new worktree

Returns:

  • (Hash, nil)

    Worktree info or nil if not found and create_if_not_found is false



155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
# File 'lib/aidp/worktree.rb', line 155

def find_by_branch(branch:, project_dir: Dir.pwd, create_if_not_found: false, base_branch: nil)
  # First, try exact match in the registry
  registry = load_registry(project_dir)
  slug, data = registry.find { |_slug, info| info["branch"] == branch }

  # Check if the existing worktree is still active
  if data
    active = Dir.exist?(data["path"])
    Aidp.log_debug("worktree", active ? "found_existing" : "found_inactive", branch: branch, slug: slug)
    return {
      slug: slug,
      path: data["path"],
      branch: data["branch"],
      created_at: data["created_at"],
      active: active
    }
  end

  # If not found and create_if_not_found is true, create a new worktree
  if create_if_not_found
    # Generate a slug from the branch name, replacing problematic characters
    safe_slug = branch.downcase.gsub(/[^a-z0-9\-_]/, "-")

    Aidp.log_debug("worktree", "creating_for_branch", branch: branch, slug: safe_slug)
    return create(
      slug: safe_slug,
      project_dir: project_dir,
      branch: branch,
      base_branch: base_branch
    )
  end

  # If no existing worktree and not set to create, return nil
  nil
end

.info(slug:, project_dir: Dir.pwd) ⇒ Hash?

Get info for a specific worktree

Parameters:

  • slug (String)

    Workstream identifier

  • project_dir (String) (defaults to: Dir.pwd)

    Project root directory

Returns:

  • (Hash, nil)

    Worktree info or nil if not found



125
126
127
128
129
130
131
132
133
134
135
136
137
# File 'lib/aidp/worktree.rb', line 125

def info(slug:, project_dir: Dir.pwd)
  registry = load_registry(project_dir)
  data = registry[slug]
  return nil unless data

  {
    slug: slug,
    path: data["path"],
    branch: data["branch"],
    created_at: data["created_at"],
    active: Dir.exist?(data["path"])
  }
end

.list(project_dir: Dir.pwd) ⇒ Array<Hash>

List all active worktrees for this project

Parameters:

  • project_dir (String) (defaults to: Dir.pwd)

    Project root directory

Returns:

  • (Array<Hash>)

    Array of worktree info hashes



69
70
71
72
73
74
75
76
77
78
79
80
# File 'lib/aidp/worktree.rb', line 69

def list(project_dir: Dir.pwd)
  registry = load_registry(project_dir)
  registry.map do |slug, info|
    {
      slug: slug,
      path: info["path"],
      branch: info["branch"],
      created_at: info["created_at"],
      active: Dir.exist?(info["path"])
    }
  end
end

.remove(slug:, project_dir: Dir.pwd, delete_branch: false) ⇒ Object

Remove a worktree and optionally its branch

Parameters:

  • slug (String)

    Workstream identifier

  • project_dir (String) (defaults to: Dir.pwd)

    Project root directory

  • delete_branch (Boolean) (defaults to: false)

    Whether to delete the git branch

Raises:



87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
# File 'lib/aidp/worktree.rb', line 87

def remove(slug:, project_dir: Dir.pwd, delete_branch: false)
  registry = load_registry(project_dir)
  info = registry[slug]

  raise WorktreeNotFound, "Worktree '#{slug}' not found" unless info

  worktree_path = info["path"]
  branch = info["branch"]

  # Remove the git worktree
  if Dir.exist?(worktree_path)
    Dir.chdir(project_dir) do
      system("git", "worktree", "remove", worktree_path, "--force", out: File::NULL, err: File::NULL)
    end
  end

  # Remove the branch if requested
  if delete_branch
    Dir.chdir(project_dir) do
      system("git", "branch", "-D", branch, out: File::NULL, err: File::NULL)
    end
  end

  # Mark state removed (if exists) then unregister
  if Aidp::WorkstreamState.read(slug: slug, project_dir: project_dir)
    Aidp::WorkstreamState.mark_removed(slug: slug, project_dir: project_dir)
  end
  # Unregister the worktree
  unregister_worktree(slug, project_dir)

  true
end