fix(git-sync): auto-sync heartbeat fetches and rebases before push; refuses on a shared source-mode branch (#3011) - #3016
Conversation
…efuses on a shared source-mode branch (#3011) The heartbeat was add -A -> commit -> push origin HEAD with no fetch, so the first foreign push to the agent's branch failed every later cycle non-fast-forward, forever, with the agent's commits piling up locally. - fetch origin <branch>; if behind, rebase --autostash onto it and push with --force-with-lease pinned to the fetched sha (a racing push is rejected, never overwritten); a plain push otherwise; a branch not yet on the remote is created by the push - rebase conflict (or timeout): rebase --abort, repo left as it was, record 'diverged: rebase conflict on <branch>' for the existing sync_failing path; never resolve, overwrite or reset - source-mode agent on the default branch (origin/HEAD, else main/master) refuses before committing: 'refused: source-mode on <branch>'; fork-to-own agents (GIT_UPSTREAM_REPO or an upstream remote) exempt - sync-state.json gains behind_after_fetch and last_successful_push_at Fixes #3011 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
/review ReportBranch:
Execution coverage
Fix mutation: with Critical FindingsNone. Informational Findings[I1] Test gap: the "operator path unchanged" test is a source-text assertion (Confidence: 8) [I2] Behaviour change: an operator-enabled auto-sync on a non-fork source-mode agent now fails every cycle (Confidence: 7) [I3] Error handling: a failed ahead/behind count after a successful fetch is recorded as Low confidence (appendix)
Clean Categories
Summary
🤖 Generated with Claude Code |
Live verification against the local dev instanceSetup: I built the base image from this branch as
Caveats:
Cleanup: agent deleted; its workspace volume, git-config row, sync-state row, queue item and the 🤖 Generated with Claude Code |
…efusal helpers beside the settings guard, guard stays after staging The only git.py conflict was two independent helper blocks added at the same spot (#3016's _is_missing_remote_ref/_is_shared_source_branch/_rebase_onto_remote and this PR's _guard_container_only_settings/_has_staged_changes); both kept. _run_auto_sync_once auto-merged: dev's refusal-before-commit and fetch/rebase/lease-push are intact, with the guard between `git add -A` and the staged-only commit check. Docs keep both sides (git-sync-health 1b then 1c; agent-lifecycle carries #3010/#3011 text plus the ent#708 note). registry.json rebuilt from the index stages. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nity-enterprise#705) Create-time kind: agent (default) | deployment, on POST /api/agents and MCP create_agent. An explicit source_mode always wins. An agent gets a working branch + auto_sync_enabled + freeze-on-sync- failure, but only when the Abilityai#2107 push probe says its token can push to that repo. A deployment, an ephemeral ghost, a tokenless create, a refused or unverifiable probe all stay pull-only, with the reason on the create response's git_mode (never on the /ws broadcast). A template someone else owns therefore never receives an agent's branches (the ent#162 class); Cornelius, built from a shared public upstream, is pinned pull-only. Fork-to-own gets the trio. Existing agents are not flipped. Operator runbook for migrating a live agent: docs/migrations/AGENT_WORKING_BRANCH_DEFAULT_2026-09.md. Stacked on Abilityai#3016, Abilityai#3017, Abilityai#3018 (+Abilityai#3015): merges only after them. Related to Abilityai/trinity-enterprise#705 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Summary
_run_auto_sync_once) wasadd -A → commit → push origin HEADwith no fetch, so the first foreign push to the agent's branch failed every later cycle non-fast-forward, forever. It now fetchesorigin/<branch>, and when behind, rebases--autostashand pushes with--force-with-lease=refs/heads/<branch>:<fetched sha>(a push racing it is rejected, never overwritten; never bare--force). Not behind → the plain push as before; branch absent on the remote → the push creates it.git rebase --abort: repo left exactly as it was, remote untouched, nothing reset or auto-resolved; recordsdiverged: rebase conflict on <branch>, which the existing three-strikesync_failingpath raises.GIT_SOURCE_MODE=trueon the repo's default branch (origin/HEAD, elsemain/master) refuses before committing —refused: source-mode on <branch>. Fork-to-own agents are exempt (GIT_UPSTREAM_REPO, or theupstreamremote on the volume, since that env isn't re-derived on recreate).sync-state.jsongainsbehind_after_fetchandlast_successful_push_at(inputs for trinity-enterprise#706).sync_to_githubpath unchanged.Changes
docker/base-image/agent_server/routers/git.py—_is_shared_source_branch,_rebase_onto_remote,_is_missing_remote_ref; reconcile step in_run_auto_sync_once; two sync-state fieldstests/unit/test_3011_autosync_rebase.py— 16 tests on real repos with a foreign clone pushing underneath (8 red on the pre-fix cycle)docs/memory/feature-flows/git-sync-health.md(§1b),docs/memory/architecture/agent-lifecycle.md,tests/registry.jsonTest Plan
cd tests && pytest unit/test_3011_autosync_rebase.py unit/test_agent_server_auto_sync.py unit/test_1595_git_maintenance.py unit/test_2036_claude_settings_leak.py— 79 passedtests/lint_sys_modules.pycleandiverged; source-mode on main → refusedFixes #3011
🤖 Generated with Claude Code