Repository navigation
resume's leg guard checks each leg against the root's tracking_branch, not the leg's own branch:, so it lets bootstrap move a leg (exit 0) and refuses a leg on its own branch #235
Description
What happens
resume asks every leg first. root_legs_ok (resume:692-712) is F3 of the review on #83: it refuses a root whose leg sits off its tracking branch, because the root's own make bootstrap would move that leg. But it takes the tracking branch from the ROOT: branch="$(tracking_branch_of "$root")" (resume:694), compared at resume:699. The root's bootstrap.py places each leg on THAT LEG'S OWN branch: when the row declares one, and on tracking_branch only otherwise. So for a leg that declares its own branch:, the guard reasons about the wrong branch, in both directions:
- It lets a move through.
resumeexits 0 after bootstrap has moved the leg's HEAD. That is the move F3 exists to refuse. - It refuses a leg that is where it belongs. A leg on its own declared branch is refused as "not its tracking branch".
status has the same blind spot. It passes the root's $branch to read_leg (status:5547), whose finding at status:894 says "on branch X, not Y (resume refuses a root whose leg sits on a feature branch)".
Line numbers here are at main 2732d59. park, resume, status and tests/test_park_resume_commands.py are byte-identical at #233's head b52394d.
openRepoShape's side
- Where a leg goes.
templates/assembly-root/scripts/bootstrap.pycallscheckout_tracking_branch(root, leg, str(leg.get("branch") or branch))at7f84ca4(line 349) and at190133d(line 1064). A leg's own branch has won since openRepoShape v0 (v0: naming policy, assembly-root templates, scaffold, bootstrap, and the agent procedure openRepoShape#1). - The field is validated. At
190133d,templates/assembly-root/scripts/validate-manifest.py:627(_leg_branch_findings) checks it as "Each leg's ownbranch, OPTIONAL, which wins overtracking_branch". That came with assembly-root bootstrap.py: a lone surrogate or NUL intracking_branch(or a leg'sbranch:) endsmake bootstrapin a subprocess traceback in step (a), andvalidate-manifest.pysaysmanifest ok, so no tool names it openRepoShape#455 → bootstrap.py and validate-manifest.py: a branch holding a NUL or a lone surrogate is a finding and step (a) does not hand it to git, sotracking_branch: "a\ud83db"or a leg'sbranch: "a\x00b"no longer endsmake bootstrapin a subprocess traceback under amanifest ok(#455) openRepoShape#644. - v1.1.0 moves such a leg in more states.
- bootstrap.py and AGENTS-shape.md: a leg on another branch is placed when its tracking branch is at the pin and left when it is behind, and the family holder's copy still promises placement unconditionally openRepoShape#603 → bootstrap.py: a leg on another branch whose tracking branch is behind the pin is fast-forwarded and checked out as one at the pin already is, and the family holder's AGENTS-shape.md and bump-leg.py say bootstrap places a leg where that loses no commit (#603) openRepoShape#615 (
fast_forward_to_pin,bootstrap.py:578): a leg whose branch is BEHIND the pin is fast-forwarded to the pin and checked out, from that branch, from a HEAD detached at the pin, or from ANOTHER branch. - bump-leg.py, then
make bootstrap, leaves the leg detached: bootstrap never fast-forwards a local tracking branch that is behind the new pin, though AGENTS.md, README and bump-leg.py say it re-places it openRepoShape#475 → templates/assembly-root/scripts/bootstrap.py: a tracking branch behind the pin is fast-forwarded to it, and a fresh clone's branch past the pin with no commit of its own is moved back to it, sobump-leg.pythenmake bootstrap, andgit clone --recurse-submodulesthenmake bootstrap, leave the leg on its branch AT the pin, no longer detached undernot moving an existing branch(#475) openRepoShape#490: the fast-forward and the move back to the pin. - At
7f84ca4, a branch whose tip was not the pin was never moved.
- bootstrap.py and AGENTS-shape.md: a leg on another branch is placed when its tracking branch is at the pin and left when it is behind, and the family holder's copy still promises placement unconditionally openRepoShape#603 → bootstrap.py: a leg on another branch whose tracking branch is behind the pin is fast-forwarded and checked out as one at the pin already is, and the family holder's AGENTS-shape.md and bump-leg.py say bootstrap places a leg where that loses no commit (#603) openRepoShape#615 (
Reproduction at both pins
Each run builds the same estate with the suite's own helpers (seed_workspace, standalone_manifest, run, offline from tests/test_park_resume_commands.py) and that pin's own templates/assembly-root/{Makefile,.gitignore,scripts/bootstrap.py} and scripts/repo_shape.py:
- the root
Deltahastracking_branch: main; - the
specleg's row declaresbranch: develop; - the leg's remote has
c1ondevelopandmain, thenc2onmainonly, andc2is the pin; - the superproject is clean;
- every
LANES_*andTMUX*variable is unset.
python3 probe_leg_branch.py <openRepoTools checkout> <openRepoShape clone> <empty work dir>
the leg before resume Delta |
at 7f84ca4 |
at 190133d (v1.1.0) |
|---|---|---|
on main (the ROOT's branch) at the pin; local develop one commit BEHIND the pin |
exit 0, not refused; the leg stays on main. Bootstrap: [spec] spec: PIN 91a86c1689ae != branch develop tip a3b073e15bc1 |
exit 0, not refused; the leg ends on develop. Bootstrap: [spec] spec: branch develop fast-forwarded 9856f90a0779 -> 87c6196d63f5, the pin; origin/develop is 9856f90a0779, which the pin deliberately does NOT follow |
on main at the pin; local develop AT the pin (variant: git branch develop HEAD in the leg) |
exit 0, not refused; the leg ends on develop. Bootstrap: [spec] spec: on develop at 0b9013cf0444 (branch tip == pin) |
exit 0, not refused; the leg ends on develop. Bootstrap: [spec] spec: on develop at d406a159c993 (branch tip == pin) |
on develop, its OWN declared branch, at the pin (variant: git checkout -b develop in the leg) |
exit 1: REFUSED root Delta: leg spec is on 'develop', not its tracking branch 'main'; |
exit 1, the same refusal |
Read across, the table says three things:
- This PR did not cause it.
resume,parkandstatusrun each estate's OWN copies of these files, never this repository'supstream/openRepoShapemount. So the gap does not come with Pin openRepoShape at v1.1.0, 190133d on its main: six of the eight mounted files moved (bootstrap places legs only where no commit is lost; the family Makefile refuses a holder without siblings.py); no test expectation moved #233. - The gap is older than v1.1.0. Rows 2 and 3 already happen with
7f84ca4's bootstrap. - v1.1.0-shaped estates widen it. bootstrap.py: a leg on another branch whose tracking branch is behind the pin is fast-forwarded and checked out as one at the pin already is, and the family holder's AGENTS-shape.md and bump-leg.py say bootstrap places a leg where that loses no commit (#603) openRepoShape#615 turns row 1, a declared branch behind the pin, from "left alone" into a silent move. The more of this estate's roots are built from openRepoShape v1.1.0, the more often
resumewill report success over a leg it has moved.
In every row nothing is lost: the leg's commits stay on their branches. What breaks is the guard's promise, that resume refuses rather than moves a leg's HEAD, and in row 3 a correct checkout is refused.
probe_leg_branch.py (from the review). Row 2's variant replaces line 84 with m.git("branch", "-q", "develop", "HEAD", cwd=leg); row 3's replaces lines 83-84 with m.git("checkout", "-q", "-b", "develop", cwd=leg) and also prints the REFUSED line.
#!/usr/bin/env python3
"""`resume` on a root whose spec leg declares its OWN `branch: develop`
(validate-manifest.py's `_leg_branch_findings`, #455: it "wins over
`tracking_branch` in `bootstrap.py`"), with the leg checked out on `main`,
the ROOT's tracking branch, and local `develop` one commit BEHIND the pin.
`root_legs_ok` compares the leg's HEAD with the ROOT's tracking branch, so
it lets the leg through; then the root's real `make bootstrap` runs. Built
once per pin, with that pin's own bootstrap.py and repo_shape.py.
Usage: probe_leg_branch.py <openRepoTools checkout> <openRepoShape clone> <work dir>
"""
import subprocess
import sys
from pathlib import Path
ORT, ORS, WORK = (Path(p).resolve() for p in sys.argv[1:4])
sys.path.insert(0, str(ORT / "tests"))
import test_park_resume_commands as m # noqa: E402
PINS = {"7f84ca4": "7f84ca42ca86a8902928345109d2bf6bad87bd91",
"190133d": "190133d5e704e380c430f87ca9c4e31746de0012"}
NAME = "Delta"
def show(rev, rel):
return subprocess.run(["git", "show", f"{rev}:{rel}"], cwd=str(ORS),
capture_output=True, check=True).stdout
for label, rev in PINS.items():
base = WORK / label
base.mkdir(parents=True)
# the leg: c1 on develop and main, then c2 on main only (the pin)
leg_seed = base / "seed" / f"{NAME}-spec"
leg_seed.mkdir(parents=True)
(leg_seed / "spec.md").write_text("one\n", encoding="utf-8")
m.git("init", "-q", "-b", "main", ".", cwd=leg_seed)
m.commit_all(leg_seed, "c1")
m.git("branch", "develop", cwd=leg_seed)
(leg_seed / "spec.md").write_text("two\n", encoding="utf-8")
m.commit_all(leg_seed, "c2, the pin")
leg_bare = base / "remotes" / f"{NAME}-spec.git"
leg_bare.parent.mkdir(parents=True)
m.git("clone", "-q", "--bare", str(leg_seed), str(leg_bare), cwd=base)
seed = base / "seed" / NAME
(seed / "scripts").mkdir(parents=True)
(seed / "project.yaml").write_text(
"schema_version: 1\nkind: project-manifest\n"
f"id: {NAME.lower()}\nname: \"{NAME}\"\ntracking_branch: main\nlegs:\n"
f" - role: assembly\n repository: {m.ORG}/{NAME}\n path: \".\"\n"
f" - role: spec\n repository: {m.ORG}/{NAME}-spec\n path: spec\n"
" branch: develop\n", encoding="utf-8")
for rel, dest in (("templates/assembly-root/Makefile", "Makefile"),
("templates/assembly-root/.gitignore", ".gitignore"),
("templates/assembly-root/scripts/bootstrap.py", "scripts/bootstrap.py"),
("scripts/repo_shape.py", "scripts/repo_shape.py")):
(seed / dest).write_bytes(show(rev, rel))
for verb in ("park", "resume"):
m.write_stub(seed, verb, label=NAME)
m.git("init", "-q", "-b", "main", ".", cwd=seed)
m.commit_all(seed, "seed")
m.git("-c", "protocol.file.allow=always", "submodule", "add", "-q",
str(leg_bare), "spec", cwd=seed)
m.commit_all(seed, "the leg, pinned at c2")
bare = base / "remotes" / f"{NAME}.git"
m.git("clone", "-q", "--bare", str(seed), str(bare), cwd=base)
ws = m.seed_workspace(base, "legged",
{f"workspaces/{m.ORG}/{NAME.lower()}.yaml":
m.standalone_manifest(NAME)})
home = base / "home"
(home / "projects").mkdir(parents=True)
(home / ".agents").mkdir()
(home / ".agents" / "workspace.yaml").write_text(
"repository: tester/legged\npath: ~/projects/legged\n", encoding="utf-8")
m.git("clone", "-q", str(ws), str(home / "projects" / "legged"), cwd=home)
root = home / "projects" / NAME
m.git("-c", "protocol.file.allow=always", "clone", "-q",
"--recurse-submodules", str(bare), str(root), cwd=home / "projects")
leg = root / "spec"
m.git("checkout", "-q", "main", cwd=leg) # on the ROOT's branch, at the pin
m.git("branch", "-q", "develop", "origin/develop", cwd=leg) # the leg's own branch, behind
before = m.git("rev-parse", "--abbrev-ref", "HEAD", cwd=leg).stdout.strip()
clean = m.git("status", "--porcelain", cwd=root).stdout == ""
r = m.run(m.RESUME, NAME, home=home, env=m.offline({"dir": base / "remotes"}))
after = m.git("rev-parse", "--abbrev-ref", "HEAD", cwd=leg).stdout.strip()
print(f"[{label}] superproject clean before: {clean}; leg HEAD before: {before}")
print(f"[{label}] resume exit {r.returncode}; refused the root: "
f"{'REFUSED root' in r.stdout}; leg HEAD after: {after}")
for line in r.stdout.splitlines():
if line.strip().startswith("[spec]") or "left" in line and "branch" in line:
print(f"[{label}] bootstrap said: {line.strip()}")Suggested change
-
Read each leg's own branch, in the shared resolver.
legs_of(resume:451-471, inside the shared estate resolver thatpark,resumeandstatuscarry byte for byte,resume:107-472) prints onlypath. A row'sbranch:can follow itspath:, so the reader has to finish the row before it prints. Either:- print
path<TAB>branchper row, with the branch empty when none is declared; or - add a
leg_branch_of <root> <path>reader beside it.
Either way, change all three copies together:
test_the_three_commands_carry_the_same_resolver_byte_for_byte(tests/test_park_resume_commands.py:1370) holds them equal. - print
-
Compare with the leg's branch. In
root_legs_ok, compare each leg's HEAD with${leg_branch:-$root_tracking_branch}, exactly asbootstrap.py:1064chooses, and name that branch in the refusal. Do the same forstatus'sread_leg(status:5547,:894). -
Refresh the comment. The comment above the guard (
resume:679-691) still names only the two moves bootstrap made at7f84ca4. At v1.1.0, step (a) also:- fast-forwards a behind branch from that branch, from a HEAD detached at the pin, or from another branch (bootstrap.py: a leg on another branch whose tracking branch is behind the pin is fast-forwarded and checked out as one at the pin already is, and the family holder's AGENTS-shape.md and bump-leg.py say bootstrap places a leg where that loses no commit (#603) openRepoShape#615, templates/assembly-root/scripts/bootstrap.py: a tracking branch behind the pin is fast-forwarded to it, and a fresh clone's branch past the pin with no commit of its own is moved back to it, so
bump-leg.pythenmake bootstrap, andgit clone --recurse-submodulesthenmake bootstrap, leave the leg on its branch AT the pin, no longer detached undernot moving an existing branch(#475) openRepoShape#490); - moves a held branch back to the pin from a HEAD detached there (templates/assembly-root/scripts/bootstrap.py: a tracking branch behind the pin is fast-forwarded to it, and a fresh clone's branch past the pin with no commit of its own is moved back to it, so
bump-leg.pythenmake bootstrap, andgit clone --recurse-submodulesthenmake bootstrap, leave the leg on its branch AT the pin, no longer detached undernot moving an existing branch(#475) openRepoShape#490); - leaves a leg detached off the pin where it is (bootstrap.py: a leg detached off the pin is left where it is and said, so a commit made there is no longer stranded when its branch is already at the pin, the
not moving an existing branchline names where the checkout is, and bump-leg.py and AGENTS-shape.md say "or says why not" (#533) openRepoShape#556); - refuses a leg with an operation in progress (bootstrap.py and AGENTS-shape.md: a leg with an operation in progress or an ignored file in the way is refused, P9 names the branch it left, and no git is
git-not-runnable, somake bootstraploses nothing (#704) (#694) openRepoShape#745).
- fast-forwards a behind branch from that branch, from a HEAD detached at the pin, or from another branch (bootstrap.py: a leg on another branch whose tracking branch is behind the pin is fast-forwarded and checked out as one at the pin already is, and the family holder's AGENTS-shape.md and bump-leg.py say bootstrap places a leg where that loses no commit (#603) openRepoShape#615, templates/assembly-root/scripts/bootstrap.py: a tracking branch behind the pin is fast-forwarded to it, and a fresh clone's branch past the pin with no commit of its own is moved back to it, so
The test that would catch it
In tests/test_park_resume_commands.py, add a seed like seed_leg_member (:405) whose spec row declares branch: develop, built from the pinned bootstrap.py and repo_shape.py as that seed already is. Then add three cases, one per row of the table:
- (1) on
main,developbehind the pin:resumemust REFUSE the root by name, and the leg must stay onmain. Today, at the v1.1.0 pin, this case is red: exit 0, the leg ondevelop. - (2) on
main,developat the pin: the same assertions. Red at both pins today. - (3) on
developat the pin:resumemust NOT refuse the root, and the verb must run. Red at both pins today: exit 1.
Also add a status case asserting the leg's finding names develop.
Found by the independent review of #233 (5560570028).
🤖 Generated with Claude Code
Activity
brettheap commentedon Oct 18, 2026
CLAIMED — lane openRepoTools-1, session df82f553-7803-49c4-8ddf-3f9acdcb8ee0@Eagle, 2026-10-18T01:53:36Z, for #235
Logged in
lanes/log/openRepoTools-1.mdatcf1ec741d49ca44f3d64bf236dd68dca6bebbfbc.The three reads (lane-collision-protocol Rule 1):
Apply the independent review of #238: test leg_branch_of's parsing, f…
brettheap commentedon Oct 18, 2026
RELEASED — lane openRepoTools-1, session df82f553-7803-49c4-8ddf-3f9acdcb8ee0@Eagle, 2026-10-18T09:32:36Z, for #235 — fixed by PR #238 (landed 14239b9); the issue is closed
park's not-carried report counts a leg's unpushed commits on the bran…