You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
bug(git-sync): the auto-sync heartbeat pushes without fetch or rebase — on a shared branch it fails non-fast-forward forever after the first foreign push #3011
_run_auto_sync_once is git add -A → git status --porcelain → git commit → git push origin HEAD. No fetch, no rebase, no lease, no branch check. The moment anyone else pushes to the branch the agent is on, every subsequent heartbeat fails non-fast-forward, the agent's own commits pile up locally, and nothing on the agent side ever reconciles. The path only works cleanly in working-branch mode on a branch nobody else writes — which is exactly the mode the create paths do not expose (see the working-branch mode reachable issue on the private tracker).
Scope: Trinity 1.0 · project:tandem (operator ruling 2026-09-24: the repository is the agent; the container is a cache of it). Precondition 2 of 3 for defaulting auto-sync on (with the toggle fix and #2107).
Mechanism (verified at origin/devcec2a64b, 2026-09-24)
docker/base-image/agent_server/routers/git.py:747–800 — the cycle. The docstring is explicit: "Intentionally minimal — heavy conflict handling stays in the operator-initiated sync_to_github endpoint. Auto-sync is a heartbeat, not a rescue." That was the right call when auto-sync was opt-in on a branch the agent owned. It is not a heartbeat any more once it is the durability mechanism for agents whose humans also push.
The failure is not silent — a failed push writes sync-state.json (git.py:102–160, Sync health observability (S1) #389 S1a), the backend poller (services/sync_health_service.py, 60 s) increments consecutive_failures, and a sync_failing operator-queue item fires at three (ALERT_THRESHOLD = 3). But the alert is the only outcome: the divergence itself is never repaired, and the local commits stay on the container disk.
The rescue path exists only on the operator endpoint: sync_to_github with the pull-first strategy (fetch + rebase, conflict handling). The heartbeat never calls it.
Seam history (closed):#389 (heartbeat), #2742 (sync-state atomic writes), #1595/#1596 (worker thread, repack), #1808 (freeze enforced on three failures).
Acceptance Criteria
Before pushing, the heartbeat runs git fetch origin <branch> and, if behind > 0, rebases the local commits onto origin/<branch> (git rebase --autostash); a clean rebase then pushes.
On rebase conflict: abort the rebase, leave the working tree exactly as it was, record last_error_summary = "diverged: rebase conflict on <branch>" in sync-state.json, and let the existing sync_failing path raise the operator-queue item with that reason. Never resolve a conflict automatically, never overwrite the remote, never reset.
Shared-branch refusal: if the branch the agent is on is the repo's default branch and the agent is in source mode (pull-only by contract), the heartbeat refuses to push and records refused: source-mode on <branch> — rather than pushing agent commits straight to main of a repo that deploys on push. Fork-to-own agents (they own their fork's main) are not refused.
After a successful rebase the push is lease-protected against the ref that was fetched (the lease form of push, never the bare forced form); a plain push otherwise.
sync-state.json gains last_successful_push_at and behind_after_fetch so the sync-health row can persist them (consumed by the divergence-age issue on the private tracker).
Tests: foreign commit on the branch → next cycle rebases and pushes; conflicting foreign commit → abort, state recorded, tree untouched; source-mode agent on main → refused, no push; the operator sync_to_github path unchanged.
Journey Impact: existing — agent work reaches the repo on the heartbeat (docs/memory/feature-flows/github-sync.md); the flow now states what happens when the branch moved underneath.
Siblings — the git-sync set, filed 2026-09-24 (operator rulings relayed by corbin)
Platform half of the repository is the agent; the container is a cache of it (Trinity 1.0 · project:tandem · epic abilityai/trinity-enterprise#497). Fleet-convention half: corbin's canon/protocols/agent-git-sync.md.
abilityai/trinity-enterprise#706 — f. divergence-age semantics (red >24h ahead or behind; freeze keys on it for work agents); dirty count + last successful push persisted
abilityai/trinity-enterprise#707 — g. sync health surfaces: fleet health sync block, agent card, sync-audit over MCP
abilityai/trinity-enterprise#708 — h. narrow the .claude/settings.json ignore; hooks-vs-heartbeat owner
Re-prioritised to P1 for 1.0 the same day: #2105 · #2107. Linked, unchanged: #1703 · #2938 · abilityai/trinity-enterprise#142 · abilityai/trinity-enterprise#230.
Summary
_run_auto_sync_onceisgit add -A→git status --porcelain→git commit→git push origin HEAD. Nofetch, norebase, no lease, no branch check. The moment anyone else pushes to the branch the agent is on, every subsequent heartbeat fails non-fast-forward, the agent's own commits pile up locally, and nothing on the agent side ever reconciles. The path only works cleanly in working-branch mode on a branch nobody else writes — which is exactly the mode the create paths do not expose (see the working-branch mode reachable issue on the private tracker).Scope: Trinity 1.0 ·
project:tandem(operator ruling 2026-09-24: the repository is the agent; the container is a cache of it). Precondition 2 of 3 for defaulting auto-sync on (with the toggle fix and #2107).Mechanism (verified at
origin/devcec2a64b, 2026-09-24)docker/base-image/agent_server/routers/git.py:747–800— the cycle. The docstring is explicit: "Intentionally minimal — heavy conflict handling stays in the operator-initiatedsync_to_githubendpoint. Auto-sync is a heartbeat, not a rescue." That was the right call when auto-sync was opt-in on a branch the agent owned. It is not a heartbeat any more once it is the durability mechanism for agents whose humans also push.sync-state.json(git.py:102–160, Sync health observability (S1) #389 S1a), the backend poller (services/sync_health_service.py, 60 s) incrementsconsecutive_failures, and async_failingoperator-queue item fires at three (ALERT_THRESHOLD = 3). But the alert is the only outcome: the divergence itself is never repaired, and the local commits stay on the container disk.sync_to_githubwith the pull-first strategy (fetch + rebase, conflict handling). The heartbeat never calls it.add -Astages every untracked file the fleet-wide.gitignoredoes not exclude (services/git_service/gitignore.py), so a rebase-on-reject retry must also be safe against a commit that just swept in something the agent should not have committed — the staging policy stays the agent's.gitignore(root cause refactor: agent git repo root is $HOME — 22 gitignore patterns exist only to compensate, and every new home-writing feature repeats the dance #1703).Seam history (closed): #389 (heartbeat), #2742 (sync-state atomic writes), #1595/#1596 (worker thread, repack), #1808 (freeze enforced on three failures).
Acceptance Criteria
git fetch origin <branch>and, ifbehind > 0, rebases the local commits ontoorigin/<branch>(git rebase --autostash); a clean rebase then pushes.last_error_summary = "diverged: rebase conflict on <branch>"insync-state.json, and let the existingsync_failingpath raise the operator-queue item with that reason. Never resolve a conflict automatically, never overwrite the remote, never reset.refused: source-mode on <branch>— rather than pushing agent commits straight tomainof a repo that deploys on push. Fork-to-own agents (they own their fork'smain) are not refused.sync-state.jsongainslast_successful_push_atandbehind_after_fetchso the sync-health row can persist them (consumed by the divergence-age issue on the private tracker).main→ refused, no push; the operatorsync_to_githubpath unchanged.Technical Notes
ahead_working/behind_workingagainstorigin/<current_branch>in/api/git/status(git.py:1031–1053), so the fetch is the only new network call per cycle; keep it under the existing_REPO_LOCKandrun_registereddiscipline (bug: git auto-gc can never complete inside agent containers — unbounded, silent workspace .git bloat (273GB observed; 97%-garbage repos) #1595).ahead_workingreports ahead-of-mainfor any non-trinity/*branch, and fleet audit reads it as unpushed #2105 (the ahead count the audit reads is wrong on non-trinity/*branches — fix or this issue'sbehind_after_fetchbecomes the only trustworthy number), bug: git-sync write access is never validated — a read-only credential fails silently forever (ls-remoteand the RESTpermissions.pushfield both lie) #2107 (write access never validated), bug: agent workspaces with git submodules are second-class in the platform's own git paths —create_agentskips them,git_pullfails on a moved pointer #2366 (submodule workspaces are second-class in the platform git paths — the engineer's workspace stays excluded from any default).Journey Impact: existing — agent work reaches the repo on the heartbeat (
docs/memory/feature-flows/github-sync.md); the flow now states what happens when the branch moved underneath.Siblings — the git-sync set, filed 2026-09-24 (operator rulings relayed by corbin)
Platform half of the repository is the agent; the container is a cache of it (Trinity 1.0 ·
project:tandem· epic abilityai/trinity-enterprise#497). Fleet-convention half: corbin'scanon/protocols/agent-git-sync.md.ls-remoteand the RESTpermissions.pushfield both lie) #2107)syncblock, agent card, sync-audit over MCP.claude/settings.jsonignore; hooks-vs-heartbeat ownerRe-prioritised to P1 for 1.0 the same day: #2105 · #2107. Linked, unchanged: #1703 · #2938 · abilityai/trinity-enterprise#142 · abilityai/trinity-enterprise#230.