Skip to content

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

Closed

Description

@brettheap

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, resume and status find the estate, but a v1.1.0 holder's own make siblings stops 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)

Reproduction at both pins

One holder is built from each pin's own family-root files:

  • Makefile, .gitignore, scripts/bootstrap.py, scripts/siblings.py and scripts/repo_shape.py;
  • family.yaml with name: "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:

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

  1. Accept both folder names. is_family_folder should accept a folder whose name equals the basename of family.yaml's repository: (v1.1.0's rule, family.py init --name: the holder's README and AGENTS.md name its folder after the display name, where make siblings needs the repository's name, so a holder laid out as its README says clones nothing openRepoShape#629). It should keep accepting one equal to name:, for holders laid out before family.py init --name: the holder's README and AGENTS.md name its folder after the display name, where make siblings needs 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.
  2. Make the refusal honest. Where <Name>/<Name>/family.yaml exists 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.
  3. Check resume:1177-1185 against the new rule. Better, take the holder's repository from family.yaml's repository: (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 Quill and resume Quill find 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/QuillWorks is still found, as it is today.

Found by the independent review of #233 (5560570028).

🤖 Generated with Claude Code

Activity

  1. brettheap commented on Oct 18, 2026

    @brettheap
    ContributorAuthor

    CLAIMED — lane openRepoTools-1, session df82f553-7803-49c4-8ddf-3f9acdcb8ee0@Eagle, 2026-10-18T01:53:59Z, for #236

    Logged in lanes/log/openRepoTools-1.md at 3e988a25f1f423e89a842d7edfd3b5dd0b15a7ec.

    The three reads (lane-collision-protocol Rule 1):

    1. existing `CLAIMED —` comments on opensoft/openRepoTools#236:
       none
    2. `gh pr list --repo opensoft/openRepoTools --state all --search "#236"`, kept where the row or its body names `#236`:
    233	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	chore/openreposhape-pin-v1.0.1	OPEN
    3. `git ls-remote --heads git@github.com:opensoft/openRepoTools` branches naming `236` as a whole token:
       none
    
  2. brettheap commented on Oct 18, 2026

    @brettheap
    ContributorAuthor

    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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions