Sync a fork from upstream
A fork of this template (roxy, protoTrader, gina, …) pulls fixes + features down from upstream/main with git merge. Two avoidable footguns bite that flow on almost every fork — both fixed by the rules below. Bake them in once and every sync after is near-trivial.
The one rule that matters most: sync with a real merge commit, never a squash. Squashing breaks the fork's merge base — the "behind" count stays permanently inflated and every later sync re-conflicts on code already integrated.
Setup (once per fork)
Two remotes — origin (your fork) and upstream (this template):
git remote add upstream https://github.com/protoLabsAI/protoAgent.gitAnd switch the changelog to fork-owned (see CHANGELOG):
# in the fork's .gitattributes
CHANGELOG.md merge=ours
# + per clone / in CI (the driver isn't carried by .gitattributes alone):
git config merge.ours.driver trueThe sync
# 1. Fetch BOTH — the local clone goes stale; a stale local main makes a
# divergent merge you can't push.
git fetch upstream && git fetch origin
# 2. See the real delta (should be a short list of genuinely-new commits).
git log --oneline origin/main..upstream/main
# 3. Branch off origin/main (NOT local main) and merge upstream.
git checkout -b chore/sync-upstream-$(date +%Y%m%d) origin/main
git merge upstream/main # resolve: identity=ours, code=theirs (see hotspots)
# 4. Run tests, open a PR, and merge it as a MERGE COMMIT:
gh pr merge --merge # NOT --squashFork feature PRs can still squash — they don't touch the upstream base. Only the upstream-sync PR must be a merge commit.
Why merge, not squash
Squash collapses upstream's N commits into one new SHA on your fork. Git never sees upstream's commit SHAs in your ancestry, so the merge base never advances → permanent "behind" inflation + recurring re-conflicts on already-integrated code. A real merge commit makes upstream/main an ancestor, so the base tracks and the next sync shows only genuinely-new commits.
Real example (protoTrader, after two squash-syncs):
| Squash-synced | Re-done as a merge commit | |
|---|---|---|
| Merge base | original fork point (pre-v0.15.0) | real upstream HEAD |
| Behind count | 57 (mostly phantom) | 3 (all genuinely new) |
If your fork is already in the squash-broken state, fix it once: do the next sync as a true merge commit (resolving the now-phantom conflicts in upstream's favor) and merge with --merge. The base re-anchors and the inflation clears.
CHANGELOG: stop the duplicates
The template ships .gitattributes with CHANGELOG.md merge=union — correct for the template's internal feature-branch flow (distinct new entries coexist), but wrong for an upstream→fork sync: the two changelogs share long history your fork has curated, so union splices upstream's whole changelog back in (recurring duplicate ## [X.Y.Z] sections to hand-dedupe).
Forks own their changelog narrative — switch it to merge=ours (above) so a sync keeps your changelog and ignores upstream's. Don't import upstream's ## [X.Y.Z] version headers; if you want a specific upstream entry, copy it into your own [Unreleased] by hand.
Conflict hotspots
Thanks to the operator-fork contract, a clean fork's conflict surface is now tiny — fork identity & behavior are config/plugin-driven, so the files you edit (and therefore conflict on) should be almost none:
pyproject.tomlversion line — the one expected trivial conflict; keep your fork's version.CHANGELOG.md— resolved automatically bymerge=ours(above).- Config / persona / plugins (
config/,plugins/,SOUL.md) — fork-owned (ours); these are adds, not edits, so they rarely conflict.
If you're resolving a conflict in a core .py (e.g. server/a2a.py, server/chat.py), that usually means you edited a core file instead of using a seam — the card is config-driven (a2a.skills/a2a.description), the thread-id is a plugin resolver, the SSRF allowlist is config. File the missing seam rather than re-porting the edit every sync.
Running the fork's tests
A fork usually shares deps with the template; the template venv works:
PYTHONPATH=~/dev/<fork> ~/dev/protoAgent/.venv/bin/python -m pytest -qRelated
- The operator-fork contract — the seams that keep the conflict surface tiny
- Build an operator fork (Roxy)
- Customize & deploy