Git Manager¶
Git operations manager for branch management and worktrees.
GitManager -- wraps git CLI commands for the orchestrator's workspace management.
All operations have both synchronous and async variants. The async methods
(prefixed with a) use asyncio.create_subprocess_exec() so they do not
block the event loop — critical for the orchestrator and Discord bot which
share a single-threaded asyncio event loop. Synchronous methods are preserved
for backward compatibility and non-async callers.
Key workflows
- Clone repos:
create_checkoutclones a project's repository. - Prepare task branches:
prepare_for_taskfetches latest, creates a fresh branch off the default branch (handling both normal repos and worktrees). - Commit agent work:
commit_allstages everything and commits if there are changes. - Push and PR:
push_branchpushes to origin;create_prandcheck_pr_mergeddelegate to theghCLI for GitHub PR operations.
Design strengths (see specs/git/git.md §10 for the full list):
- Fresh starting point: prepare_for_task always fetches remote state
before creating a task branch, so agents start from recent code.
- Worktree-aware: Detects worktrees and avoids default-branch checkout
conflicts automatically.
- Retry-resilient: Existing branches are reused on task retry, never
fail with "branch already exists".
- Graceful degradation: Operations that may legitimately fail (no remote,
no upstream) are caught and suppressed rather than propagated.
- Atomic commits: commit_all uses add-then-check-staged to avoid
race conditions between status checks and staging.
Resolved gaps
- G1 (resolved):
merge_branchnow fetches and hard-resetsorigin/<default_branch>before merging, and_merge_and_pushresets local main on push failure to avoid diverged state. - G2 (resolved):
recover_workspaceresets the local default branch toorigin/<default_branch>after any failed merge-and-push, ensuring the workspace is clean for the next task. - G4 (resolved):
prepare_for_tasknow uses hard-reset on the normal path and rebases existing branches on retry.switch_to_branchalso rebases ontoorigin/<default_branch>after switching.
Resolved gaps (continued):
- G3 (resolved): sync_and_merge now attempts rebase-before-merge
when a direct merge fails with conflicts. The task branch is rebased
onto origin/<default_branch> and the merge retried. If the rebase
itself conflicts, the original merge_conflict error is returned.
Resolved gaps (continued):
- G5 (resolved): push_branch now accepts a force_with_lease
keyword argument. When True, uses --force-with-lease for
idempotent retries of PR branches. The orchestrator passes this flag
when pushing task branches for PR creation.
Resolved gaps (continued):
- G6 (resolved): mid_chain_sync pushes intermediate subtask work
to the remote and rebases the chain branch onto origin/<default_branch>
between subtask completions. The orchestrator calls this after each
non-final subtask when auto_task.rebase_between_subtasks is enabled,
reducing drift and providing crash safety for long chains.
See specs/git/git.md for the full behavioral specification.
Classes¶
GitManager ¶
Functions¶
has_remote ¶
Check if the given remote exists in the repository.
checkout_branch ¶
list_branches ¶
Return a list of local branch names. Current branch is prefixed with '*'.
Source code in src/git/manager.py
pull_latest_main ¶
Fetch from origin and hard-reset the default branch to match remote.
Encapsulates the fetch + hard-reset pattern so callers can ensure their
local default branch exactly matches origin/<default_branch>, even
if previous merge commits or failed operations left it diverged.
This is safer than git pull because pull can fail when the local
branch has diverged (e.g. from un-pushed merge commits left by
_merge_and_push). A hard reset unconditionally moves the branch
pointer to match the remote.
Must be called while the default branch is checked out (for normal repos) or used in worktree-aware callers that skip checkout.
Source code in src/git/manager.py
prepare_for_task ¶
Fetch latest and create a task branch off the default branch.
Two code paths depending on whether the checkout is a worktree:
- Normal repo: checkout default branch, hard-reset to
origin/<default_branch>, then create the task branch. The hard
reset ensures we always match remote even if a previous
_merge_and_push left local main diverged.
- Worktree: Can't checkout the default branch (it's already checked
out in the main working tree), so we create the task branch directly
from origin/<default_branch> in a single step.
In both cases, if the branch already exists (e.g. task retried after a
restart), we switch to it and rebase onto origin/<default_branch>
so the agent starts with the latest upstream changes.
Source code in src/git/manager.py
switch_to_branch ¶
switch_to_branch(checkout_path: str, branch_name: str, default_branch: str = 'main', rebase: bool = False) -> None
Switch to an existing branch, pulling latest and optionally rebasing.
Used for subtask branch reuse: when a plan generates multiple subtasks that should share a branch, this lets the second task pick up where the first left off rather than creating a new branch.
When rebase is True, the branch is rebased onto
origin/<default_branch> after switching so subtask chains stay
closer to main and reduce the chance of merge conflicts when the work
is eventually merged back. Controlled by the
auto_task.rebase_between_subtasks config option.
If the branch doesn't exist locally or on the remote (e.g. LINK repos with no remote), creates it as a new local branch.
Source code in src/git/manager.py
mid_chain_sync ¶
Push intermediate subtask work and rebase onto latest main.
Called between subtask completions in a chained plan to:
- Push current commits to remote — saves intermediate work so it survives agent crashes and is visible to other clones.
- Rebase the branch onto
origin/<default_branch>— keeps the subtask chain close to main and reduces the chance of large merge conflicts when the final subtask merges the accumulated work. - Force-push the rebased branch — updates the remote ref to match the rewritten (rebased) history.
This resolves Gap G6 for long subtask chains where drift from
main would otherwise accumulate across multiple sequential
subtask executions.
Returns True if the full sync (push + rebase + force-push)
succeeded. Returns False if the rebase conflicted — the branch
is left in its original pre-rebase state and the initial push may
still have saved the intermediate work to the remote.
All failures are non-fatal: callers should catch exceptions and continue — the next subtask can still work on the branch as-is.
Source code in src/git/manager.py
pull_branch ¶
Pull (fetch + merge) a branch from the origin remote.
If branch_name is None, the current branch is used. Returns the
name of the branch that was pulled.
Source code in src/git/manager.py
push_branch ¶
Push a local branch to the origin remote.
When force_with_lease is True, uses --force-with-lease so the
push is safe for retries: if the branch was already pushed in a
previous attempt, a second push with amended/additional commits will
succeed as long as no other user pushed to the same branch in the
meantime. This resolves Gap G5 for PR branch pushes.
Plain push (default) is used for the sync_and_merge flow where
only the default branch is pushed and force-push is never appropriate.
Source code in src/git/manager.py
rebase_onto ¶
Rebase branch onto target. Returns True on success, False on conflict.
Switches to branch_name, then rebases it onto
origin/<target_branch>. If the rebase encounters conflicts it is
aborted and the method returns False — the branch is left in its
original pre-rebase state.
Used by :meth:sync_and_merge for its rebase-before-merge conflict
resolution (Gap G3), and available as a public API for callers that
need to rebase an arbitrary branch onto any target.
Source code in src/git/manager.py
merge_branch ¶
Merge branch into default. Returns True if successful, False if conflict.
Checks out the default branch, fetches from origin, and hard-resets
to origin/<default_branch> before merging. This ensures the
local default branch matches the remote even when other agents have
pushed since the last fetch (resolves Gap G1).
.. note:: For rebase-before-merge conflict resolution, use
:meth:sync_and_merge which attempts a rebase of the task branch
onto origin/<default_branch> when the direct merge fails.
Source code in src/git/manager.py
sync_and_merge ¶
sync_and_merge(checkout_path: str, branch_name: str, default_branch: str = 'main', max_retries: int = 1) -> tuple[bool, str]
Pull latest main, merge branch, push. Returns (success, error_msg).
Encapsulates the full sync-merge-push flow as a single higher-level operation. Callers (e.g. the orchestrator) no longer need to coordinate fetch / checkout / reset / merge / push individually.
Steps
- Fetch latest remote state.
- Checkout the default branch and hard-reset to
origin/<default_branch>. - Attempt the merge; on conflict, try rebasing the task branch
onto
origin/<default_branch>and retry the merge once. If the rebase itself conflicts or the retry merge still fails, returnmerge_conflict. - Push with up to max_retries retries. On push failure (e.g. another agent pushed in the meantime), pull --rebase and retry. If all retries are exhausted, return a push failure message.
Source code in src/git/manager.py
recover_workspace ¶
Reset workspace to a clean state after a failed merge-and-push.
Checks out the default branch and hard-resets it to
origin/<default_branch> so the workspace is ready for the
next task. This undoes any local merge commit left behind by a
failed push.
Best-effort: callers should wrap in try/except if they cannot tolerate failures here (e.g. the workspace is in a broken git state that even checkout cannot recover from).
Source code in src/git/manager.py
delete_branch ¶
Delete a branch locally and optionally on the remote.
Source code in src/git/manager.py
create_worktree ¶
Create a git worktree for agent isolation on linked repos.
Source code in src/git/manager.py
remove_worktree ¶
Remove a git worktree.
Source code in src/git/manager.py
init_repo ¶
Initialize a new git repo with an empty initial commit.
get_diff ¶
Return the full diff against base branch.
commit_all ¶
Stage all changes and commit. Returns True if a commit was made, False if nothing to commit.
Uses add-all-then-check-staged pattern: git add -A stages
everything (including untracked files the agent created), then
git diff --cached --quiet checks whether anything is actually
staged. This avoids the race condition of checking status before
staging.
Plan files (.claude/plan.md, plan.md, .claude/plans/)
are automatically unstaged to prevent them from being committed to
target repos.
Source code in src/git/manager.py
create_pr ¶
Create a GitHub PR using the gh CLI. Returns the PR URL.
Delegates to gh pr create rather than the GitHub API directly,
so the user's existing gh authentication is reused.
Source code in src/git/manager.py
check_pr_merged ¶
Check if a PR has been merged via the gh CLI.
Returns True (merged), False (still open), None (closed without merge). The orchestrator polls this for AWAITING_APPROVAL tasks to detect when a human merges the PR and the task can be marked COMPLETED.
Source code in src/git/manager.py
get_status ¶
Return the output of git status for the given repository path.
get_current_branch ¶
has_non_plan_changes ¶
has_non_plan_changes(checkout_path: str, default_branch: str = 'main', min_files: int = 3, min_lines: int = 50) -> bool
Check if the branch has substantial code changes beyond plan files.
Compares the current HEAD against the merge-base with the default branch, excluding plan file paths from the diff. Returns True if the diff exceeds the given thresholds (files changed or lines changed), indicating the plan was likely already implemented.
Returns False (conservative) on any git error so callers fall through to normal task-generation behaviour.
Source code in src/git/manager.py
get_default_branch ¶
Detect the default branch for the repository.
Tries multiple strategies to determine the default branch: 1. Query the remote HEAD symbolic ref (most reliable) 2. Check for common default branch names (main, master, develop) 3. Fall back to the current branch
Returns the detected default branch name, or "main" as a last resort.
Source code in src/git/manager.py
get_recent_commits ¶
Return recent commit log (one-line format).
check_gh_auth ¶
Check if the gh CLI is authenticated.
Returns True if gh auth status exits successfully, False
otherwise. Used to pre-validate before attempting repo creation so
callers can surface a helpful error message.
Source code in src/git/manager.py
create_github_repo ¶
create_github_repo(name: str, *, private: bool = True, org: str | None = None, description: str = '') -> str
Create a GitHub repository via the gh CLI.
Returns the HTTPS URL of the newly created repository.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
Repository name (e.g. |
required |
private
|
bool
|
Create a private repo (default |
True
|
org
|
str | None
|
GitHub organization. |
None
|
description
|
str
|
Optional repo description. |
''
|
Raises:
| Type | Description |
|---|---|
GitError
|
If |
Source code in src/git/manager.py
acommit_all
async
¶
Async version of :meth:commit_all.
Source code in src/git/manager.py
make_branch_name
staticmethod
¶
Build a branch name in <task-id>/<slug> format.
Examples: brave-fox/add-retry-logic, calm-river/fix-auth-bug.
The task ID prefix makes branches easy to trace back to their task,
and the slug suffix provides human-readable context.