Repository navigation
park, resume and status find a family folder by family.yaml's name:, but openRepoShape v1.1.0 names it after the holder's repository, so a family whose name differs has no layout that works #236
Description
What happens
park, resume and status find a family estate with is_family_folder, in the shared estate resolver they carry byte for byte:
| command | lines |
|---|---|
park |
259-268 |
resume |
242-251 |
status |
380-389 |
A directory <dir> is a family folder only when <dir>/<dir>/family.yaml has kind: family-manifest AND its name: equals the folder's name. The comparison is [ "$YAML_VALUE" = "$name" ], at park:267, resume:250 and status:388.
openRepoShape v1.1.0 names that folder, and the holder inside it, after the holder's REPOSITORY. When a family's display name: differs from its repository name, no layout works for both:
- Named after
name:.park,resumeandstatusfind the estate, but a v1.1.0 holder's ownmake siblingsstops before cloning anything. - Named after the repository. The holder works, but none of the three commands finds the estate.
Line numbers here are at main 2732d59. These files are byte-identical at #233's head b52394d.
openRepoShape's side, at 190133d (v1.1.0)
- The folder is the repository's name (family.py init
--name: the holder's README and AGENTS.md name its folder after the display name, wheremake siblingsneeds the repository's name, so a holder laid out as its README says clones nothing openRepoShape#629 → siblings.py and the family-root README:make siblings,parkandresumeprint the family's name and every member row spelled and never end in a traceback onname: "a\ud83db"or a dash underPYTHONIOENCODING=ascii, and the holder's README names its folder after the repository, so--family Quill --name QuillWorkslays out as its README says (#454, #629) openRepoShape#642).templates/family-root/scripts/siblings.py:18: "WHICH NAME<Family>IS (family.py init--name: the holder's README and AGENTS.md name its folder after the display name, wheremake siblingsneeds the repository's name, so a holder laid out as its README says clones nothing openRepoShape#629). It is the holder's REPOSITORY name ... NEVERname:, the display name (--name), which is only a title".templates/family-root/README.md:61-67: "Both are named after the REPOSITORY,{{FAMILY}}(the last part of{{FAMILY_REPOSITORY}}, and whatgit clonecalls the holder's directory)".
make siblingschecks that before it clones (make siblingsandfamily.py siblings: clone each member on its tracking branch, warn before cloning outside a<Family>folder, and run the holder's pinned siblings.py openRepoShape#543 → siblings.py and family.py:make siblingsasks about the family folder before it clones, clones each member on its tracking branch, andfamily.py siblingsruns the holder's own copy (#543) openRepoShape#578).siblings.py:38: "THE PARENT-FOLDER CHECK, BEFORE ANYTHING IS CLONED (make siblingsandfamily.py siblings: clone each member on its tracking branch, warn before cloning outside a<Family>folder, and run the holder's pinned siblings.py openRepoShape#543)".siblings.py:1330:siblings: stopped before cloning: …, then exit 1. make reports it as 2.
- At
7f84ca4it cloned first and only warned afterwards, printing the move to<repository>/<repository>. - How a family gets two names.
family.py init --family Quill --name QuillWorkswritesname: "QuillWorks"andrepository: testorg/Quill.
Reproduction at both pins
One holder is built from each pin's own family-root files:
Makefile,.gitignore,scripts/bootstrap.py,scripts/siblings.pyandscripts/repo_shape.py;family.yamlwithname: "QuillWorks",repository: testorg/Quill;- one member,
Alpha, reached through a local bare remote.
It is cloned into each layout. Every LANES_* and TMUX* variable is unset, and nothing reaches the network.
python3 exp_family_folder.py <openRepoShape clone> <openRepoTools checkout> <empty work dir>
# then, in each layout's $HOME: bash <checkout>/park <Name> --dry-run; bash <checkout>/status <Name>
| layout | command | at 7f84ca4 |
at 190133d (v1.1.0) |
|---|---|---|---|
QuillWorks/QuillWorks (folder = name:, the only layout the three commands find) |
make siblings in the holder |
exit 0. Alpha is cloned, then the holder warns and prints mv ~/projects/QuillWorks/QuillWorks ~/projects/Quill/Quill |
exit 2: siblings: stopped before cloning: the holder is not in a folder named Quill. Nothing was cloned, fetched or moved. Alpha is NOT cloned |
| ″ | status QuillWorks |
exit 0 | exit 1, one finding: Alpha "not here: no working clone beside the holder (resume QuillWorks places it)". But resume reaches that clone through the same make siblings |
| ″ | park QuillWorks --dry-run |
finds the holder and dispatches to the cloned Alpha (it exits 2 here only because this fixture's member has no Makefile) |
exit 2: FINDING Alpha: skipped: no working clone at ~/projects/QuillWorks/Alpha; run \make siblings` first.` |
Quill/Quill (folder = repository, v1.1.0's rule) |
park Quill --dry-run |
exit 2: REFUSED: no estate named 'Quill' under your projects directory. An estate is a FAMILY folder (Quill/Quill/family.yaml) or a standalone project root (Quill/project.yaml). That file exists |
exit 2, the same refusal |
| ″ | status Quill |
exit 2, the same refusal | exit 2, the same refusal |
| ″ | make siblings in the holder |
exit 0, Alpha cloned |
exit 0, Alpha cloned |
Read across, the table says three things:
- This PR did not cause it.
park,resumeandstatusrun each estate's OWN holder files, never this repository'supstream/openRepoShapemount. So the conflict 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 mismatch is older. At
7f84ca4it was already there, but only as a warning: the members were still cloned. - v1.1.0-shaped holders make it a hard stop.
make siblingsandfamily.py siblings: clone each member on its tracking branch, warn before cloning outside a<Family>folder, and run the holder's pinned siblings.py openRepoShape#543 and family.py init--name: the holder's README and AGENTS.md name its folder after the display name, wheremake siblingsneeds the repository's name, so a holder laid out as its README says clones nothing openRepoShape#629 turn it into a stop before cloning. A v1.1.0 family whosename:differs from its repository cannot be both found by these commands and populated by its holder.
The suite cannot see it: FAMILY_YAML (tests/test_park_resume_commands.py:111-120) always writes repository: {org}/{name}, with name: equal to the repository's basename.
A related reading: resume derives a family holder's repository as ROOT_REPOSITORY="$MANIFEST_ORG/$MANIFEST_DIRNAME" (resume:1185), with MANIFEST_DIRNAME taken from the recorded family folder (resume:1177). That is right only when the folder IS the repository's name, so it already assumes v1.1.0's rule, while is_family_folder assumes the other one.
exp_family_folder.py (from the review)
#!/usr/bin/env python3
"""Reproduce: a family whose display `name:` differs from its repository name.
Usage: exp_family_folder.py <openRepoShape clone> <openRepoTools checkout> <work dir>
For each pin (7f84ca4, 190133d) it builds a holder from that pin's own
family-root files, with family.yaml `name: "QuillWorks"`, `repository:
testorg/Quill` (what `family.py init --family Quill --name QuillWorks`
writes), and one member reached through a local bare remote. Then:
layout N (folder = name:, what openRepoTools' is_family_folder requires):
QuillWorks/QuillWorks -> run the holder's own `make siblings`
layout R (folder = repository, what openRepoShape v1.1.0 #629 prescribes):
Quill/Quill -> run openRepoTools `park Quill --dry-run` and
`status Quill`, and the holder's `make siblings`
Nothing here touches the network: every url is a local bare repository.
"""
import os
import shutil
import subprocess
import sys
from pathlib import Path
ORS, ORT, WORK = (Path(p).resolve() for p in sys.argv[1:4])
PINS = {"old": "7f84ca42ca86a8902928345109d2bf6bad87bd91",
"new": "190133d5e704e380c430f87ca9c4e31746de0012"}
FILE_OK = {"GIT_CONFIG_COUNT": "1", "GIT_CONFIG_KEY_0": "protocol.file.allow",
"GIT_CONFIG_VALUE_0": "always"}
def env_for(home: Path) -> dict:
env = {k: v for k, v in os.environ.items()
if not k.startswith("LANES_") and not k.startswith("TMUX")
and k not in ("PROJECTS_DIR", "MAKEFLAGS", "MAKELEVEL")}
env.update(HOME=str(home), PYTHON=sys.executable,
GIT_AUTHOR_NAME="t", GIT_AUTHOR_EMAIL="t@t.invalid",
GIT_COMMITTER_NAME="t", GIT_COMMITTER_EMAIL="t@t.invalid",
**FILE_OK)
return env
def git(*args, cwd, env=None):
p = subprocess.run(["git", *args], cwd=str(cwd), capture_output=True,
text=True, env=env)
if p.returncode:
raise SystemExit(f"git {args} failed in {cwd}:\n{p.stderr}")
return p.stdout
def show(rev: str, rel: str) -> bytes:
return subprocess.run(["git", "show", f"{rev}:{rel}"], cwd=str(ORS),
capture_output=True, check=True).stdout
def build(base: Path, rev: str, home: Path) -> Path:
env = env_for(home)
member = base / "seed" / "Alpha"
(member / "scripts").mkdir(parents=True)
(member / "project.yaml").write_text(
'schema_version: 1\nkind: project-manifest\nid: alpha\nname: "Alpha"\n'
'tracking_branch: main\nlegs:\n - role: assembly\n'
' repository: testorg/Alpha\n path: "."\n', encoding="utf-8")
(member / "scripts" / "bootstrap.py").write_text(
'print("BOOTSTRAP-Alpha ran")\n', encoding="utf-8")
git("init", "-q", "-b", "main", ".", cwd=member, env=env)
git("add", "-A", cwd=member, env=env)
git("commit", "-qm", "seed", cwd=member, env=env)
bare = base / "remotes" / "Alpha.git"
bare.parent.mkdir(parents=True)
git("clone", "-q", "--bare", str(member), str(bare), cwd=base, env=env)
holder = base / "seed" / "Quill"
(holder / "scripts").mkdir(parents=True)
for rel, dest in (("templates/family-root/Makefile", "Makefile"),
("templates/family-root/.gitignore", ".gitignore"),
("templates/family-root/scripts/bootstrap.py", "scripts/bootstrap.py"),
("templates/family-root/scripts/siblings.py", "scripts/siblings.py"),
("scripts/repo_shape.py", "scripts/repo_shape.py")):
(holder / dest).write_bytes(show(rev, rel))
(holder / "family.yaml").write_text(
'schema_version: 1\nkind: family-manifest\nid: quill\n'
'name: "QuillWorks"\norg: testorg\nrepository: testorg/Quill\n'
'tracking_branch: main\nmembers_dir: members\nmembers:\n'
' - project: Alpha\n id: alpha\n repository: testorg/Alpha\n'
' path: members/Alpha\n', encoding="utf-8")
git("init", "-q", "-b", "main", ".", cwd=holder, env=env)
git("add", "-A", cwd=holder, env=env)
git("commit", "-qm", "seed", cwd=holder, env=env)
git("submodule", "add", "-q", str(bare), "members/Alpha", cwd=holder, env=env)
git("commit", "-qam", "the member, pinned", cwd=holder, env=env)
hbare = base / "remotes" / "Quill.git"
git("clone", "-q", "--bare", str(holder), str(hbare), cwd=base, env=env)
return hbare
def layout(hbare: Path, home: Path, folder: str) -> Path:
projects = home / "projects"
(projects / folder).mkdir(parents=True)
target = projects / folder / folder
git("clone", "-q", "--recurse-submodules", str(hbare), str(target),
cwd=projects, env=env_for(home))
return target
def run(argv, cwd, home):
p = subprocess.run(argv, cwd=str(cwd), capture_output=True, text=True,
env=env_for(home), input="")
return p.returncode, p.stdout, p.stderr
def tail(text: str, n: int = 6) -> str:
lines = [l for l in text.splitlines() if l.strip()]
return "\n ".join(lines[-n:])
for label, rev in PINS.items():
base = WORK / label
if base.exists():
shutil.rmtree(base)
base.mkdir(parents=True)
print(f"===== pin {label} ({rev[:7]}) =====")
hbare = build(base, rev, base / "home-build")
home_n = base / "home-n"
holder_n = layout(hbare, home_n, "QuillWorks")
rc, out, err = run(["make", "siblings"], holder_n, home_n)
placed = (home_n / "projects" / "QuillWorks" / "Alpha" / ".git").exists()
print(f" layout N QuillWorks/QuillWorks: `make siblings` exit {rc}; "
f"member Alpha placed beside the holder: {placed}")
print(f" stderr tail: {tail(err, 3)}")
home_r = base / "home-r"
holder_r = layout(hbare, home_r, "Quill")
for cmd in (["bash", str(ORT / "park"), "Quill", "--dry-run"],
["bash", str(ORT / "status"), "Quill"]):
rc, out, err = run(cmd, home_r, home_r)
print(f" layout R Quill/Quill: `{Path(cmd[1]).name} {' '.join(cmd[2:])}` "
f"exit {rc}: {tail(err or out, 2)}")
rc, out, err = run(["make", "siblings"], holder_r, home_r)
placed = (home_r / "projects" / "Quill" / "Alpha" / ".git").exists()
print(f" layout R Quill/Quill: `make siblings` exit {rc}; member Alpha "
f"placed: {placed}")Suggested change
- Accept both folder names.
is_family_foldershould accept a folder whose name equals the basename offamily.yaml'srepository:(v1.1.0's rule, family.py init--name: the holder's README and AGENTS.md name its folder after the display name, wheremake siblingsneeds the repository's name, so a holder laid out as its README says clones nothing openRepoShape#629). It should keep accepting one equal toname:, for holders laid out before family.py init--name: the holder's README and AGENTS.md name its folder after the display name, wheremake siblingsneeds the repository's name, so a holder laid out as its README says clones nothing openRepoShape#629. 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. - Make the refusal honest. Where
<Name>/<Name>/family.yamlexists but names another family, refuse by saying so. "An estate is a FAMILY folder (Quill/Quill/family.yaml)" is printed today while that file exists. - Check
resume:1177-1185against the new rule. Better, take the holder's repository fromfamily.yaml'srepository:(or the record) rather than from the folder name.
The test that would catch it
In tests/test_park_resume_commands.py, add a family fixture whose name: differs from its repository: name: "QuillWorks", repository: testorg/Quill, laid out at Quill/Quill. Then assert:
park Quill --dry-run,status Quillandresume Quillfind it. Red today: exit 2, "no estate named 'Quill'".- The holder's real
make siblings, from the pinned v1.1.0 files, clones the member beside it. - The name-keyed layout
QuillWorks/QuillWorksis still found, as it is today.
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:59Z, for #236
Logged in
lanes/log/openRepoTools-1.mdat3e988a25f1f423e89a842d7edfd3b5dd0b15a7ec.The three reads (lane-collision-protocol Rule 1):
resume names the repository of the holder it resolved, not the one th…
park, resume and status find a family folder named for its repository…
brettheap commentedon Oct 18, 2026
RELEASED — lane openRepoTools-1, session df82f553-7803-49c4-8ddf-3f9acdcb8ee0@Eagle, 2026-10-18T08:34:31Z, for #236 — fixed by PR #240 (landed a41adb6); the issue is closed