git-workflow — branch-and-pr-loop
Detail page of the git-workflow recipe card.
B1 — the two-step branch-per-change loop
Section titled “B1 — the two-step branch-per-change loop”Step 1 — Author (opens the PR; NEVER merges it):
# sync-before-start (see B4): branch off up-to-date maingit fetch origin --prune --tagsgit switch main && git merge --ff-only origin/maingit switch -c feat/<slug> # slug = kebab summary of the change
# ... implement the whole cutover; run beds freely throughout to VERIFY# (Risk Driven Development: prove high-risk assumptions on a bed first) ...
# on R10 PASS, open the PR (do NOT merge):# write the cutover narrative to CHANGELOG/<placeholder>.md — a PLACEHOLDER CalVer# (any valid YYYY.DDD.HHMM; the evaluator OVERWRITES it with the merge-time VER).git add <only the cutover's files> CHANGELOG/<placeholder>.mdgit commit -m "<conventional commit>" \ -m "Assisted-by: <Harness> <Provider Full Model Name> (<confidence>)"git push origin feat/<slug> # feat push — allowed by the gategh pr create --base main --head feat/<slug> \ # fill the PR template completely (single org source: opencharly/.github/.github/PULL_REQUEST_TEMPLATE.md — no per-repo copy) --title "<subject>" \ --body-file <pr-body.md># STOP. Do NOT merge your own PR. Hand off to a FRESH pr-validator (Step 2).Step 2 — Fresh evaluator (plugins/internals/agents/pr-validator.md, spawned
with NEW context): it independently re-validates the PR vs R0–R10 + the relevant
skills, posts charly/pr-validator on the head SHA, and ONLY on PASS
generates the merge-time CalVer, rewrites the version surfaces on feat/ (the
CHANGELOG rename + any schema bump), re-posts the status on the new head,
gh pr merge --squash --delete-branch, and tags. Its inputs include the PR’s
FULL comment thread, per the pr-validator spec’s comment-intake rule — every
comment is investigated independently and considered in the verdict, never
granted or denied authority merely by existing (see
plugins/internals/agents/pr-validator.md “Comment intake”, never restated
here). The author pastes the evaluator’s verbatim verdict + what it
merged/tagged (paste-proof survives delegation). On FAIL the PR stays OPEN
and is UPDATED IN PLACE → the author R1-RCAs, fixes in the same tree,
APPENDS a fix commit, and pushes it fast-forward (status resets) → the
evaluator re-runs. Never close a PR and open a replacement to carry a fix.
Because the merge is a SQUASH and main is protected linear, main gains exactly
ONE commit per cutover — the author’s change, any review-round fix commits, and the
version stamp, folded together.
Concurrent landings — N open cutovers do NOT serialize beyond git itself
Section titled “Concurrent landings — N open cutovers do NOT serialize beyond git itself”With several cutovers in flight (a multi-cutover program), only two things are inherently ordered: the merge instants (git — seconds each) and any real dependency DAG between cutovers. Everything else runs CONCURRENTLY, and no doc may mandate more serialization than that without a technical reason:
- Implementation — one git worktree per cutover, per-worktree binaries for
verification (
/charly-internals:agents“Per-worktree binaries”, proven), bed gates from multiple branches overlapping under the shared hardware ceiling. - Validation — fresh
pr-validators run CONCURRENTLY across all ready PRs (each is an independent context; nothing couples them before the merge instant). - After each merge — every still-open PR goes
BEHIND(all three repos setstrict: truerequire-up-to-date — KEPT deliberately: one shared Go package with no CI onmainmeans merging a stale-base green PR opens a semantic-conflict blind spot; that is the technical reason, not doctrine). Recover withgh pr update-branch(never force-push), then a risk-proportional DELTA RE-GATE re-posts the per-commit status: compute the overlapgit diff --name-only <old-main>..main∩ the branch’s touched files — EMPTY → rebuild +go test ./...+golangci-lint run+ re-post (minutes); NON-EMPTY → additionally re-run the cutover’s primary beds; a full roster re-run only when the overlap hits the cutover’s risky paths. The ORIGINAL full R10 against the branch’s final code remains mandatory before the FIRST validation — the delta re-gate covers only the mechanical update-branch merge on top of an already-R10’dmain.
Gitlink ANCESTOR bump → gh pr update-branch flags CONFLICTING (recover
locally). When the just-merged delta and a still-open PR both bump the SAME
submodule gitlink and one bump is an ANCESTOR of the other, GitHub’s
gh pr update-branch does NOT auto-resolve it — it conservatively reports the PR
CONFLICTING instead of fast-forwarding the gitlink to the descendant. The
compliant recovery is the update-branch EQUIVALENT done LOCALLY: in the feat
worktree, git merge origin/main (git resolves the gitlink to the descendant
commit automatically), then push the result FAST-FORWARD. A MERGE, never a rebase;
no force-push — the exact constraints gh pr update-branch itself honors. Then
VERIFY the merge resolved the gitlink FORWARD — git ls-tree HEAD <sub> (or
git diff --submodule=short origin/main..HEAD) must show the DESCENDANT commit, never
the ancestor: a recovery that silently re-pins the OLDER submodule bump is the exact
regression this class produces (a sibling merge advances main mid-validation, then a
naive recovery reverts the gitlink to the older pointer). Only after the descendant-wins
check re-post the status and delta-re-gate as above.
Multi-committer main advances — out-of-tree PRs from other committers. The
orchestrator is NOT the sole source of main advances: another committer (a human
maintainer, a parallel session, an outside contributor via the fork+PR path) may
land an unrelated PR on main WHILE this plan’s feat/ branches are in flight. The
discipline above is main-advance-agnostic and applies to ANY merge regardless of
source — treat an out-of-tree merge IDENTICALLY to an internal one:
- Detect proactively, not only reactively. Fetch
origin/main(and each submodule’smain) before opening EACH PR and before each merge — do not rely solely on the orchestrator’s own-merge broadcast. Afeat/branch that goesBEHINDfrom an external merge is recovered exactly as above (gh pr update-branch, delta re-gate, forward-gitlink verify).strict: trueis KEPT precisely for this: a stale-base green PR merged over an external advance opens a semantic-conflict blind spot, so the delta re-gate’s overlap check (git diff --name-only <old-main>..main∩ branch-files) is what makes an out-of-tree merge safe — EMPTY overlap → re-post; NON-EMPTY → re-run the primary beds. A teammate pushing whileBEHINDan external merge updates its branch and reports the overlap for the orchestrator’s delta-re-gate call. - Divergent-lineage submodule bump (not ancestor/descendant). The
descendant-wins rule above covers the common case (the external merge bumped a
submodule to a DESCENDANT of our feat’s pin). If an out-of-tree merge bumps a
submodule to a commit NOT in our feat’s gitlink ancestry (a divergent lineage — a
different branch merged, or a revert), do NOT blindly descendant-wins:
re-resolve the feat to the new
main’s submodule commit, RE-RDD the affected cross-repo composition (the composition-at-latest-versions high-risk unknown — prove it on adisposable: truebed), and only then re-post status. A naive update-branch that re-pins the OLDER or divergent gitlink is the exact regression. - The rebase broadcast covers external advances too. When the orchestrator
detects ANY
mainadvance — internal OR external — it broadcasts the rebase to every in-flight teammate and re-runs the per-merge delta re-gate over the external delta; the orchestrator OWNS external-advance detection (it is the one actor that fetchesorigin/mainacross all lanes).
The cross-repo WIP landing sequence — commit-to-rebase without shipping unproven code
Section titled “The cross-repo WIP landing sequence — commit-to-rebase without shipping unproven code”A multi-repo cutover hits a genuine tension: the WIP must be COMMITTED before a
rebase onto freshly-advanced mains (a submodule-spanning working tree makes
git stash unsafe — a gitlink stash can silently drop a submodule’s in-progress
pointer, R6), yet a runtime-class commit gate demands LIVE proof of the FINAL code,
which does not exist until AFTER the rebase. Resolve it by committing at the tier
that is HONEST at each stage on an UNPUSHED branch, then re-stamping the tier once
the final code is proven — never by shipping anything pre-bed:
- (a) Freeze + exploratory roster. Freeze the WIP and run the EXPLORATORY bed
roster against that frozen state (the
disposable: truebeds whose kinds match the change). - (b) Commit LOCALLY at the then-honest tier — do NOT push. With the live
runner having actually run and its output pasted, the honest tier is
analysed on a live system(the project rulebook “AI Attribution”). Commit at that tier to create the rebase-able base. The commit stays LOCAL. - (c) Rebase onto the current mains. Bring the branch onto the just-advanced
origin/mainof every repo, and REGENERATE every generated file from the merged sources (task cue:gen, codegen) — never hand-merge a generated artifact (generated-artifact drift is an R1 incident). - (d) Re-run the gates, re-freeze.
go test/golangci-lint/ build /charly box validateon the rebased tree, then freeze again. - (e) FINAL roster on the rebased FINAL code. The R10 acceptance roster runs on the rebased, transitional-free code — the state that will actually ship.
- (f) Amend/reword to the EARNED tier, then push + open the PRs. Re-stamp the
commit’s attribution tier to what the FINAL roster earned (
fully tested and validatedon a clean full pass). This amend is legal ONLY because the branch is UNPUSHED — the “amend only before the first push” invariant above. THEN pushfeat/<slug>and open the PRs (B1 step 1; multi-repo order = B2).
Nothing pre-bed ever ships, and every intermediate commit passes the PreToolUse tier gate TRUTHFULLY at the stage it was made — the tier only ever moves UP, to match proof that now exists.
B4 — sync to upstream + prune (per repo: main, sdk, plugins, box/, pkg/)
Section titled “B4 — sync to upstream + prune (per repo: main, sdk, plugins, box/, pkg/)”- Sync-before-start.
git fetch origin --prune --tags; ff localmaintoorigin/main. Never force-reset a diverged localmain— if it cannot fast-forward, STOP + run/charly-internals:root-cause-analyzer. (Localmainnow only ever fast-forwards to what agent-validated PRs merged remotely.) - Switch-to-upstream check. Before opening the PR, confirm
originis the canonical upstream and the PR targets the upstreammain(not a stale fork/branch). On mismatch, STOP and surface it. - Prune merged branches.
feat/is deleted at merge (--delete-branch+delete_branch_on_merge). Sweep leftovers:git branch --merged main→ delete local;git fetch --prunedrops remote-tracking refs deleted upstream. Only ever delete branches confirmed--merged; never-Dan unmerged/abandoned branch without operator confirmation — it may hold unlanded work. - Worktree hygiene.
git worktree listto inventory;git worktree pruneto clear stale admin entries. Remove an agentisolation: worktreeafter its change lands. Before reusing a long-lived worktree, ff its base toorigin/main. A linked superproject worktree shares superproject objects but not submodule objects: initialize only the submodules the cutover needs, and initialize each from the common Git directory’s matchingmodules/<path>reference so its clone records a Git alternate instead of duplicating object packs. Verify the alternate and the exact gitlink before use; never compensate with/tmp, a user cache, or an unrelated recursive submodule checkout.