Skip to content

family.py init exits 0 and pushes a holder its own validate-family.py refuses for --id null, --created-by ' ', --created-on 20261011 or --name 'Contoso Group', and puts --tracking-branch true on a branch its manifest reads back as True #307

Closed

Description

@brettheap

Lane: openreposhape-5 (openRepoShape-5)

Found by lane 5's writer of #258 and #261 (fix/scaffold-manifest-values at ca21b57), checking which other writers of the constructs that fix guards it does not reach. The --name row is lane 5's writer of #260 (fix/render-single-pass at 552763d). Pre-existing: the runs below are on main ede15ce (git 2.43.0, Python 3.12.3, Linux), into local bare remotes (--local-remote-dir). gh was stubbed to fail and family.py init never called it.

What is wrong. scripts/family.py init renders templates/family-root/family.yaml from the table _template_values builds (scripts/family.py:599-622). The holder it creates carries its own YAML reader and its own scripts/validate-family.py, and init asks neither of them anything:

  • --id goes through checked_value and PROJECT_ID_RE (:536-537), --org and --tracking-branch through checked_value, --created-by through "not empty" (args.created_by or ..., :554), and --created-on and --name through nothing (:616, :609). Only --family is classified against the naming policy (:542).
  • The template writes id, org, created_on, visibility and tracking_branch bare (templates/family-root/family.yaml:33, :35, :41, :46, :51) and name and created_by double-quoted (:34, :40).

So a value the holder's validator refuses is written and pushed anyway, and a value YAML reads as another type is misread. In every row below --dry-run exits 0 and so does the real run.

value what init checks what the holder's own validate-family.py says
--created-by ' ' not empty family-created-by: blank after strip()
--created-on 20261011, 2026-W41-6, tomorrow nothing family-created-on: DATE_RE is ^\d{4}-\d{2}-\d{2}$ (and 20261011, written bare, reads back as a number)
--id null, true, false PROJECT_ID_RE, which admits all three family-id: written bare, read back as None, True, False
--org null checked_value family-org: read back as None
--name 'Contoso Group' nothing (--name is documented as "display name", :1706) naming-unclassified: the validator classifies name as a family holder, one CamelCase token (ContosoGroup passes)
--tracking-branch true, 007, null checked_value no finding at all; read back as True, 7, None

The last row is silent. The holder is pushed on the branch as typed (true, 007, null), validate-family.py has no rule about tracking_branch, and the next tool to read the field gets the misread value: update-shape.py apply --branch on that holder prints a gh pr create whose --base is True, 7 or main (tracking_branch_of, update-shape.py:1607, is str(... or "main")). --tracking-branch 2026 works only by accident, as #258 says of the scaffold: str(2026) gives the same text back.

SHAPE=<openRepoShape checkout>   # a clean checkout of main
export GIT_AUTHOR_NAME=T GIT_AUTHOR_EMAIL=t@example.invalid GIT_COMMITTER_NAME=T GIT_COMMITTER_EMAIL=t@example.invalid
UP=$(mktemp -d)/openRepoShape   # an upstream one commit ahead of the holders' shape, for update-shape.py below
git clone -q "$SHAPE" "$UP" && printf '\n# a fix\n' >> "$UP/templates/family-root/scripts/validate-family.py"
git -C "$UP" -c user.name=T -c user.email=t@example.invalid commit -qam 'Fix the family validator'
# the first two rows are controls: a plain branch and a one-token name
for flag in "--tracking-branch=main" "--name=ContosoGroup" \
            "--created-by= " "--created-on=20261011" "--created-on=2026-W41-6" "--created-on=tomorrow" \
            "--id=null" "--id=true" "--id=false" "--org=null" "--name=Contoso Group" \
            "--tracking-branch=true" "--tracking-branch=007" "--tracking-branch=null" "--tracking-branch=2026"; do
  TMP=$(mktemp -d)
  init() { python3 "$SHAPE/scripts/family.py" init --org testorg --family Contoso --created-by 'T H' --created-on 2026-09-02 \
               --local-remote-dir "$TMP/remotes" --work-dir "$TMP/work" "$flag" "$@" > /dev/null 2>&1; }
  init --dry-run; printf "== init '%s': --dry-run exit %s, " "$flag" $?
  init; printf 'real run exit %s, the holder pushed on %s\n' $? "$(git -C "$TMP/remotes/Contoso.git" for-each-ref --format='%(refname:short)' refs/heads)"
  (cd "$TMP/work/Contoso" && python3 scripts/validate-family.py > "$TMP/val" 2>&1; echo "   validate-family.py exit $?: $(grep -m1 FINDING "$TMP/val")")
  python3 - "$SHAPE" "$TMP/work/Contoso/family.yaml" <<'EOF'
import sys; sys.path.insert(0, sys.argv[1] + "/scripts")
from repo_shape import load_yaml
m = load_yaml(sys.argv[2])
print("   family.yaml reads back:", {k: m[k] for k in ("id", "org", "name", "created_by", "created_on", "tracking_branch")})
EOF
  case "$flag" in --tracking-branch=*)
    python3 "$SHAPE/update-shape.py" apply --root "$TMP/work/Contoso" --yes --upstream "$UP" --at "$(git -C "$UP" rev-parse HEAD)" \
        --branch resync | grep -o -E -- '--repo [^ ]+ --base [^ ]+ --head [^ ]+' | sed 's/^/   update-shape.py apply --branch prints: gh pr create /';;
  esac
done

Observed on main ede15ce:

== init '--tracking-branch=main': --dry-run exit 0, real run exit 0, the holder pushed on main
   validate-family.py exit 0: 
   family.yaml reads back: {'id': 'contoso', 'org': 'testorg', 'name': 'Contoso', 'created_by': 'T H', 'created_on': '2026-09-02', 'tracking_branch': 'main'}
   update-shape.py apply --branch prints: gh pr create --repo testorg/Contoso --base main --head resync
== init '--name=ContosoGroup': --dry-run exit 0, real run exit 0, the holder pushed on main
   validate-family.py exit 0: 
   family.yaml reads back: {'id': 'contoso', 'org': 'testorg', 'name': 'ContosoGroup', 'created_by': 'T H', 'created_on': '2026-09-02', 'tracking_branch': 'main'}
== init '--created-by= ': --dry-run exit 0, real run exit 0, the holder pushed on main
   validate-family.py exit 1: FINDING family-created-by: created_by is empty. Creating a family is a human's act and the manifest records whose.
   family.yaml reads back: {'id': 'contoso', 'org': 'testorg', 'name': 'Contoso', 'created_by': ' ', 'created_on': '2026-09-02', 'tracking_branch': 'main'}
== init '--created-on=20261011': --dry-run exit 0, real run exit 0, the holder pushed on main
   validate-family.py exit 1: FINDING family-created-on: created_on is 20261011, expected an ISO date YYYY-MM-DD
   family.yaml reads back: {'id': 'contoso', 'org': 'testorg', 'name': 'Contoso', 'created_by': 'T H', 'created_on': 20261011, 'tracking_branch': 'main'}
== init '--created-on=2026-W41-6': --dry-run exit 0, real run exit 0, the holder pushed on main
   validate-family.py exit 1: FINDING family-created-on: created_on is '2026-W41-6', expected an ISO date YYYY-MM-DD
   family.yaml reads back: {'id': 'contoso', 'org': 'testorg', 'name': 'Contoso', 'created_by': 'T H', 'created_on': '2026-W41-6', 'tracking_branch': 'main'}
== init '--created-on=tomorrow': --dry-run exit 0, real run exit 0, the holder pushed on main
   validate-family.py exit 1: FINDING family-created-on: created_on is 'tomorrow', expected an ISO date YYYY-MM-DD
   family.yaml reads back: {'id': 'contoso', 'org': 'testorg', 'name': 'Contoso', 'created_by': 'T H', 'created_on': 'tomorrow', 'tracking_branch': 'main'}
== init '--id=null': --dry-run exit 0, real run exit 0, the holder pushed on main
   validate-family.py exit 1: FINDING family-id: id is None; it must match ^[a-z0-9][a-z0-9-]*$
   family.yaml reads back: {'id': None, 'org': 'testorg', 'name': 'Contoso', 'created_by': 'T H', 'created_on': '2026-09-02', 'tracking_branch': 'main'}
== init '--id=true': --dry-run exit 0, real run exit 0, the holder pushed on main
   validate-family.py exit 1: FINDING family-id: id is True; it must match ^[a-z0-9][a-z0-9-]*$
   family.yaml reads back: {'id': True, 'org': 'testorg', 'name': 'Contoso', 'created_by': 'T H', 'created_on': '2026-09-02', 'tracking_branch': 'main'}
== init '--id=false': --dry-run exit 0, real run exit 0, the holder pushed on main
   validate-family.py exit 1: FINDING family-id: id is False; it must match ^[a-z0-9][a-z0-9-]*$
   family.yaml reads back: {'id': False, 'org': 'testorg', 'name': 'Contoso', 'created_by': 'T H', 'created_on': '2026-09-02', 'tracking_branch': 'main'}
== init '--org=null': --dry-run exit 0, real run exit 0, the holder pushed on main
   validate-family.py exit 1: FINDING family-org: org is None
   family.yaml reads back: {'id': 'contoso', 'org': None, 'name': 'Contoso', 'created_by': 'T H', 'created_on': '2026-09-02', 'tracking_branch': 'main'}
== init '--name=Contoso Group': --dry-run exit 0, real run exit 0, the holder pushed on main
   validate-family.py exit 1: FINDING naming-unclassified: 'Contoso Group' matches no family in the naming policy. A family holder is one CamelCase token with no hyphen, underscore, dot or space.
   family.yaml reads back: {'id': 'contoso', 'org': 'testorg', 'name': 'Contoso Group', 'created_by': 'T H', 'created_on': '2026-09-02', 'tracking_branch': 'main'}
== init '--tracking-branch=true': --dry-run exit 0, real run exit 0, the holder pushed on true
   validate-family.py exit 0: 
   family.yaml reads back: {'id': 'contoso', 'org': 'testorg', 'name': 'Contoso', 'created_by': 'T H', 'created_on': '2026-09-02', 'tracking_branch': True}
   update-shape.py apply --branch prints: gh pr create --repo testorg/Contoso --base True --head resync
== init '--tracking-branch=007': --dry-run exit 0, real run exit 0, the holder pushed on 007
   validate-family.py exit 0: 
   family.yaml reads back: {'id': 'contoso', 'org': 'testorg', 'name': 'Contoso', 'created_by': 'T H', 'created_on': '2026-09-02', 'tracking_branch': 7}
   update-shape.py apply --branch prints: gh pr create --repo testorg/Contoso --base 7 --head resync
== init '--tracking-branch=null': --dry-run exit 0, real run exit 0, the holder pushed on null
   validate-family.py exit 0: 
   family.yaml reads back: {'id': 'contoso', 'org': 'testorg', 'name': 'Contoso', 'created_by': 'T H', 'created_on': '2026-09-02', 'tracking_branch': None}
   update-shape.py apply --branch prints: gh pr create --repo testorg/Contoso --base main --head resync
== init '--tracking-branch=2026': --dry-run exit 0, real run exit 0, the holder pushed on 2026
   validate-family.py exit 0: 
   family.yaml reads back: {'id': 'contoso', 'org': 'testorg', 'name': 'Contoso', 'created_by': 'T H', 'created_on': '2026-09-02', 'tracking_branch': 2026}
   update-shape.py apply --branch prints: gh pr create --repo testorg/Contoso --base 2026 --head resync

Run on the --created-on 20261011 and the --name 'Contoso Group' holders, shape-doctor.py --root <holder> ends INVALID (family) (exit 1) with the validator's finding as its family row.

Why it matters.

  • Each row of the first five ends init exit 0 over a holder that exists and is pushed, whose gate is red from its first commit. validate-family.py is what the holder's make validate runs first (templates/family-root/Makefile:52) and what its validate workflow runs (.github/workflows/validate.yml:191, and :202 with --no-members).
  • init cannot be run again for that holder: with the commit pushed, the same command with a corrected value ends REFUSED family-remote-exists: <remote> already exists and has commits and There is no --force., with or without --reuse-empty-repo (run). The way back is a hand edit of family.yaml, which AGENTS.md:211-212 tells an agent never to make to turn a row green.
  • Against GitHub, _create_holder_remote runs gh repo create (:737, called at :848) before the manifest is rendered (_create_holder_worktree, :744), so each of these rows would first create a real repository. Read from code; the GitHub path was not run.
  • The --tracking-branch row is silent. Nothing fails: the holder sits on true while its manifest says True, and the pull request base a tool prints is a branch the holder does not have.

Expected. The same principle #261's fix applies to scaffold-project.py and adopt-project.py: refuse, before anything is created and under --dry-run too, what the holder's own programs refuse, by asking those programs and not by copying their rules. Concretely, load the validator the template ships (templates/family-root/scripts/validate-family.py) from that file and run it over the family.yaml the run would write, and read each rendered line with repo_shape.parse_yaml, as #261's manifest_validator, manifest_invalid and manifest_misreads do for the assembly root.

Related. #258: the scaffold's --id and --tracking-branch, the same misread. #259: quoting the template placeholders, which lists family.yaml. #261: the values the validator refuses, in the scaffold and adopt; its fix (fix/scaffold-manifest-values at ca21b57) changes neither scripts/family.py nor templates/family-root/. #260: render's placeholder expansion; its writer found the --name row.

Not claimed.

🤖 Generated with Claude Code

Activity

  1. brettheap commented on Oct 12, 2026

    @brettheap
    ContributorAuthor

    CLAIMED — lane openRepoShape-5, session e62c2fd7-176d-429d-89fb-0653dd08ad5f@Eagle, 2026-10-12T11:02:39Z, for #307

    Logged in lanes/log/openRepoShape-5.md at 8e823c25cf1ac33a815b0ffaacfefaf383206a19.

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

    1. existing `CLAIMED —` comments on opensoft/openRepoShape#307:
       none
    2. `gh pr list --repo opensoft/openRepoShape --state all --search "#307"`, kept where the row or its body names `#307`:
    314	scaffold-project.py and adopt-project.py ask the project's YAML reader and validate-manifest.py about the manifest's values before anything exists, so `--id null`, `--tracking-branch true` and a blank `--elected-by` are refused (#258, #261)	fix/scaffold-manifest-values	OPEN
    316	shape_materialize.py: `render` substitutes in one pass and refuses only a placeholder its table lacks, so `--elected-by '{{SPEC_PATH}}'` is no longer recorded as `spec` and `{{NOPE}}` no longer refuses after the legs are pushed (#260)	fix/render-single-pass	OPEN
    3. `git ls-remote --heads git@github.com:opensoft/openRepoShape` branches naming `307` as a whole token:
       none
    
  2. added this to the v1.0.0 milestone on Oct 12, 2026
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

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions