Bug
ao project add records the repo's currently checked-out branch as the project's permanent defaultBranch, instead of the repo's actual default branch (origin/HEAD, typically main). If a user happens to be mid-feature-work (e.g. on fix/some-branch) when they register a project, AO mistakes that personal/transient branch for the project's base branch — and every worker spawned afterward bases its worktree off that stale feature branch instead of main, forever, with no resync and no warning.
Source: Discovered live while triaging #284/#285 — the agent-orchestrator project's stored config shows "defaultBranch": "fix/pr-attachment", a personal scratch branch, not main.
Analyzed against: 7c9ae53
Confidence: High — traced the exact code path and reproduced the live DB state.
Reproduction
- Check out a non-default branch in a registered repo's working directory (e.g.
git checkout -b fix/something).
- Run
ao project add --path <repo> while that branch is checked out.
project.Config.DefaultBranch is persisted as fix/something.
- Switch back to
main in the working directory — AO's stored config is unaffected.
- Spawn any worker: its worktree bases off
fix/something, not main, with no indication anything is wrong.
Confirmed live: ~/.ao/data/ao.db → projects.config for project agent-orchestrator contains {"defaultBranch":"fix/pr-attachment", ...} — fix/pr-attachment was simply the branch checked out in the main working tree at the moment the project was registered, not the repo's actual default branch (origin/HEAD → main).
Root Cause
backend/internal/service/project/service.go:
- Line 202-206 (in
Add): if row.Config.DefaultBranch == "" { if branch := resolveDefaultBranch(path); ... { row.Config.DefaultBranch = branch } }
- Line 254-260:
resolveDefaultBranch runs git -C path symbolic-ref --short HEAD — i.e. whatever branch happens to be checked out right now, not the repo's actual default branch.
This was introduced by PR #209 (closing #208), which fixed a real problem — repos on master/develop/trunk previously fell back to a hardcoded main and failed every spawn with BRANCH_NOT_FETCHED. The fix's approach (snapshot symbolic-ref --short HEAD once, at project add time) conflates two different things:
- The repo's actual default/trunk branch (what
origin/HEAD resolves to — stable, shared, intended target for new worktrees)
- Whatever branch happens to be checked out locally right now (transient, personal, could be any in-progress feature branch)
For a repo cloned fresh and never checked out elsewhere, these coincide, which is presumably why #209's tests passed. But for any already-active local clone (the common case for someone setting up AO on an existing project), they diverge — and once persisted, Config.DefaultBranch is ""-gated (line 202), so it's a one-time snapshot that never self-corrects even after the user returns to main.
Fix (options, not prescriptive)
- Prefer the repo's actual remote default branch:
git -C path symbolic-ref --short refs/remotes/origin/HEAD (falling back to the current symbolic-ref --short HEAD approach only if no origin/HEAD is set, e.g. no remote configured) — this targets the repo's trunk rather than the registrar's transient checkout.
- Surface the detected
defaultBranch in the ao project add output / project settings UI so a user notices if it's wrong, with an easy ao project set-config --default-branch main escape hatch (which already exists per docs/cli/README.md).
- Consider re-validating/re-resolving on a project's
ao project add re-run or via ao project set-config, rather than only ever setting it once at first registration.
Impact
- Every worker session spawned in an affected project silently bases its work off a stale personal branch instead of
main — diffs, PRs, and merges all target/diverge from the wrong base, with no error or warning anywhere in the flow.
- Affects any project registered while the registrar had a non-default branch checked out — likely common, since
ao project add is typically run against an already-active local clone, not a fresh checkout.
Related
- #208 — the original bug this regressed from (closed)
- #284 — PR auto-attach disconnection (separate but adjacent: both are "session/project branch tracking goes stale and never resyncs" bugs)
Bug
ao project addrecords the repo's currently checked-out branch as the project's permanentdefaultBranch, instead of the repo's actual default branch (origin/HEAD, typicallymain). If a user happens to be mid-feature-work (e.g. onfix/some-branch) when they register a project, AO mistakes that personal/transient branch for the project's base branch — and every worker spawned afterward bases its worktree off that stale feature branch instead ofmain, forever, with no resync and no warning.Source: Discovered live while triaging #284/#285 — the
agent-orchestratorproject's stored config shows"defaultBranch": "fix/pr-attachment", a personal scratch branch, notmain.Analyzed against:
7c9ae53Confidence: High — traced the exact code path and reproduced the live DB state.
Reproduction
git checkout -b fix/something).ao project add --path <repo>while that branch is checked out.project.Config.DefaultBranchis persisted asfix/something.mainin the working directory — AO's stored config is unaffected.fix/something, notmain, with no indication anything is wrong.Confirmed live:
~/.ao/data/ao.db→projects.configfor projectagent-orchestratorcontains{"defaultBranch":"fix/pr-attachment", ...}—fix/pr-attachmentwas simply the branch checked out in the main working tree at the moment the project was registered, not the repo's actual default branch (origin/HEAD→main).Root Cause
backend/internal/service/project/service.go:Add):if row.Config.DefaultBranch == "" { if branch := resolveDefaultBranch(path); ... { row.Config.DefaultBranch = branch } }resolveDefaultBranchrunsgit -C path symbolic-ref --short HEAD— i.e. whatever branch happens to be checked out right now, not the repo's actual default branch.This was introduced by PR #209 (closing #208), which fixed a real problem — repos on
master/develop/trunkpreviously fell back to a hardcodedmainand failed every spawn withBRANCH_NOT_FETCHED. The fix's approach (snapshotsymbolic-ref --short HEADonce, atproject addtime) conflates two different things:origin/HEADresolves to — stable, shared, intended target for new worktrees)For a repo cloned fresh and never checked out elsewhere, these coincide, which is presumably why #209's tests passed. But for any already-active local clone (the common case for someone setting up AO on an existing project), they diverge — and once persisted,
Config.DefaultBranchis""-gated (line 202), so it's a one-time snapshot that never self-corrects even after the user returns tomain.Fix (options, not prescriptive)
git -C path symbolic-ref --short refs/remotes/origin/HEAD(falling back to the currentsymbolic-ref --short HEADapproach only if noorigin/HEADis set, e.g. no remote configured) — this targets the repo's trunk rather than the registrar's transient checkout.defaultBranchin theao project addoutput / project settings UI so a user notices if it's wrong, with an easyao project set-config --default-branch mainescape hatch (which already exists perdocs/cli/README.md).ao project addre-run or viaao project set-config, rather than only ever setting it once at first registration.Impact
main— diffs, PRs, and merges all target/diverge from the wrong base, with no error or warning anywhere in the flow.ao project addis typically run against an already-active local clone, not a fresh checkout.Related