Repository navigation
repo_shape.py's Refusal gives every refusal built without a remediation the assembly root's own (run git submodule update --init --recursive, then python3 scripts/validate-pins.py ... contracts/<leg>-pin.yaml), so in a family holder family.py add over an unparsable .gitmodules (git-failed), bump on a family.yaml of the wrong kind, and the holder's own validate-family.py on a short pin.commit each tell the operator to run a file no holder carries and to move a leg pin a holder does not have #370
Description
Lane: openreposhape-4 (openRepoShape-4)
Found by this lane's adversarial review of #300 (PR #317, head 03ddadd; its out-of-scope item 1): "add over an unparsable .gitmodules says REFUSED git-failed … bad config line 5. Its remediation is the assembly-root default ... a file no holder carries. It is identical on main, so it has been there since #242." PR #317 lists it under "Not in this change". Widened here after reading where the text comes from: the same default reaches every refusal in scripts/family.py and templates/family-root/scripts/validate-family.py that is built without a remediation of its own. Re-verified on main c7c02fa and on a scratch merge of #318's head 0f2a0dd (which carries c7c02fa) with #317's head c23e2b0, with local bare remotes, an empty global git config, GIT_ALLOW_PROTOCOL=file and a gh that exits 97 (never called).
What is wrong.
U=<openRepoShape checkout> # a clean checkout at c7c02fa
# Runs from a clean checkout of openRepoShape, in bash, on Linux (GNU coreutils). Nothing leaves the machine: members and
# holder live in local bare repositories under $R, git reads an empty global config, GIT_ALLOW_PROTOCOL=file makes any
# accidental https/ssh clone fail, and a `gh` that exits 97 stands first on PATH.
R=$(mktemp -d); mkdir $R/bin; printf '#!/bin/sh\nexit 97\n' > $R/bin/gh; chmod +x $R/bin/gh; touch $R/gitconfig
export PATH=$R/bin:$PATH GIT_CONFIG_GLOBAL=$R/gitconfig GIT_CONFIG_NOSYSTEM=1 GIT_ALLOW_PROTOCOL=file \
GIT_AUTHOR_NAME=t GIT_AUTHOR_EMAIL=t@t.invalid GIT_COMMITTER_NAME=t GIT_COMMITTER_EMAIL=t@t.invalid
for P in Atlas Nova Zed; do
python3 $U/scaffold-project.py --org testorg --project $P --elected-by 'Test Human' --elected-on 2026-10-12 \
--local-remote-dir $R/remotes --work-dir $R/work >/dev/null
done
python3 $U/scripts/family.py init --org testorg --family Fam --created-by 'Test Human' --created-on 2026-10-12 \
--local-remote-dir $R/fam --work-dir $R/h >/dev/null 2>&1
H=$R/h/Fam; cd $H
add() { python3 $U/scripts/family.py add --family-root $H --member "$1" --local-remote-dir $R/remotes; }
add testorg/Nova >/dev/null
printf 'this line is not git config\n' >> .gitmodules # a .gitmodules git cannot parse
add testorg/Atlas 2>&1 | sed "s#$H#\$H#" | cut -c1-190; echo "add: exit ${PIPESTATUS[0]}"
git checkout -q -- .gitmodules
python3 - <<'PY' # Nova's pin.commit shortened by hand to 12 hex
import re
t = open("family.yaml").read()
open("family.yaml", "w").write(re.sub(r'commit: "([0-9a-f]{12})[0-9a-f]{28}"', r'commit: "\1"', t))
PY
python3 scripts/validate-family.py 2>&1 | cut -c1-190; echo "validate: exit ${PIPESTATUS[0]}"
git checkout -q -- family.yaml
python3 - <<'PY' # family.yaml's kind changed by hand
t = open("family.yaml").read()
open("family.yaml", "w").write(t.replace("kind: family-manifest", "kind: project-manifest"))
PY
python3 $U/scripts/family.py bump --family-root $H --member Nova --to $(git -C members/Nova rev-parse HEAD) 2>&1 \
| sed "s#$H#\$H#" | cut -c1-190; echo "bump: exit ${PIPESTATUS[0]}"Output at c7c02fa (the merge of #318 and #317 prints the same; each remediation line is cut at 190 characters by the block, and continues "... names the leg in ONE commit (see README, 'The lockstep invariant')"):
REFUSED git-failed: `git config -z --file .gitmodules --list` in $H exited 128: fatal: bad config line 4 in file .gitmodules
Remediation: run `git submodule update --init --recursive`, then `python3 scripts/validate-pins.py`. If the PIN itself is stale, advance the gitlink, `contracts/<leg>-pin.yaml` and every wor
add: exit 2
REFUSED member-pin-commit: Nova: pin.commit is '0e8f87dc451f', which is not 40 hex. An abbreviated oid, a branch or a tag is a moving reference.
Remediation: run `git submodule update --init --recursive`, then `python3 scripts/validate-pins.py`. If the PIN itself is stale, advance the gitlink, `contracts/<leg>-pin.yaml` and every wor
validate: exit 2
REFUSED family-wrong-kind: $H/family.yaml: kind is 'project-manifest', expected 'family-manifest'
Remediation: run `git submodule update --init --recursive`, then `python3 scripts/validate-pins.py`. If the PIN itself is stale, advance the gitlink, `contracts/<leg>-pin.yaml` and every wor
bump: exit 2
A holder has no scripts/validate-pins.py, no legs, no contracts/<leg>-pin.yaml and no workflow @<sha> that names a leg. Each of the three remedies is a command that fails, followed by a procedure for a pin the holder does not have. In the first case, after #317 the holder's own validator calls the same file gitmodules-unreadable ("git cannot read it either ... git config -f .gitmodules --list says where it breaks"), so add and the gate now name one fault two ways. On main the holder's validator says family ok over that same file.
The sites, by an AST scan of each file at c7c02fa (identical on the merge but for line numbers), are every Refusal(code, detail) with no third argument:
scripts/family.py:family-unreadableandfamily-wrong-kind(Family.open),member-manifest-unreadableandmember-bad-id(_read_member_project),remote-unreadable(_local_remote_is_empty).validate-family.py:member-pin-commit,member-commit-unresolvable,shape-pin-missing,shape-pin-unreadable,family-manifest-unreadable,family-members-unreadable.- Through
scripts/repo_shape.py, in both files: everygit-failedfromgit_out(17 calls infamily.py; in the validator, the onetree_digestmakes, while its ownrev-parseis re-raised asmember-commit-unresolvable, listed above, and family.py:addrefusesmember-path-registeredwhen a.gitmodulessection under another name already gives the mount path, andvalidate-family.pyfindsgitmodules-path-twice, soaddno longer commits two sections for one path that a recursive clone resolves to the later one, and a holder that already has such a ghost section fails its own gate (#300) #317's.gitmodulesread carries a remediation of its own), and everyyaml-unreadableandyaml-unparsablefromload_yaml, except the non-UTF-8 case, which carries its own.
Why. Refusal.__init__(self, code, detail, remediation=REMEDIATION) (scripts/repo_shape.py:131 at c7c02fa), and REMEDIATION (:123) was written for an assembly root's legs. A holder ships repo_shape.py and validate-family.py, and neither file says which kind of root it serves. #44 and #48 (both closed) fixed the same mismatch in update-shape.py, which named project.yaml and validate-manifest.py on a family root.
What it should do. No refusal a family tool prints names a file or a pin a holder does not have. The decisions are the author's:
- Where the text comes from. Either each listed site in
family.pyandvalidate-family.pycarries its own remediation. Or both files pass a family default (git_outandload_yamlwould then have to take the caller's). OrREMEDIATIONbecomes neutral ("git's line above says what failed; fix it and run the command again"), and the assembly-root text moves to the assembly-root sites that mean it. addover an unparsable.gitmodules: the same code and remedy as the validator'sgitmodules-unreadable, since_submodule_pathsand_check_gitmodules_pathsread the file the same way (family.py:addrefusesmember-path-registeredwhen a.gitmodulessection under another name already gives the mount path, andvalidate-family.pyfindsgitmodules-path-twice, soaddno longer commits two sections for one path that a recursive clone resolves to the later one, and a holder that already has such a ghost section fails its own gate (#300) #317).- Both files are copied into holders, so the validator's text reaches them through
update-shape.py.
How to test. Each repro row as a test that asserts the refusal code and that validate-pins.py and <leg>-pin.yaml are absent from stderr: add and bump in the family tests, the short pin in the validator's. A test can also walk both files' ASTs, as the scan above did, and fail on a Refusal built without a remediation, so a new site cannot slip back to the default.
Verified at main c7c02fa and at the scratch merge of #318's head 0f2a0dd with #317's head c23e2b0 (scripts/family.py sha256 8339041e…, validate-family.py 7ec3eec0…); the finding survives the merge.
Related: #300 / PR #317 (gitmodules-unreadable, and its "Not in this change" list), #242 (the .gitmodules read in add that first met this default), #44 and #48 (the same mismatch in update-shape.py, fixed).
Activity
brettheap commentedon Oct 12, 2026
(lane openRepoShape-2, 2026-10-12T17:10Z)
One more route to the default trailer this issue reports. It was found in this lane's review of #343 at 039530b.
update-shape.py'sread_pinrefuses acontracts/shape-pin.yamlwhosefiles:is0,false,~,{}or[]asupdate-pin-no-files.read_pin's other refusals that are built without a remediation of their own, printsrepo_shape's defaultgit submodule updatetrailer.read_pinrefusesupdate-pin-row, and the pinned validate-pins.py and validate-family.py reportshape-pin-row, when a shape pin'sfiles:is not a list, sofiles: 5,trueor1.5no longer endscheck,apply, shape-doctor.py and the project's own validator inTypeError: ... is not iterable(#290) #343 leaves these falsy values alone on purpose, so they belong here rather than in a new issue.The review is
handoffs/xFactory/attachments/lane-openRepoShape-2/review343-039530b/final-report.mdin opensoft/brett-wip.brettheap commentedon Oct 12, 2026
CLAIMED — lane openRepoShape-3, session 43f4ac63-0116-41de-977d-afc23cdf7fdf@Eagle, 2026-10-12T19:29:44Z, for #370
Logged in
lanes/log/openRepoShape-3.mdatcf3170b184e19759222177d73acdc0d3d5d0c477.The three reads (lane-collision-protocol Rule 1):
brettheap commentedon Oct 12, 2026
(lane openRepoShape-3, 2026-10-12T19:50:30Z) A v1.0.0 scope notice. Brett Heap scoped openRepoShape's first release today: every procedure in AGENTS.md works end to end for ordinary repositories on Linux, macOS and Windows. The tag waits for every in-scope issue to close and a full test run to pass on all three OSes on the release commit. This issue is #370 (P2, parent #459): in scope, in the
v1.0.0milestone. Nothing changes in how it is worked. Known-limit issues stay open, without the milestone, under their area's parent.Merge origin/main (751cb2f, #507) into fix/refusal-default-remediatio…
repo_shape.py, validate-pins.py, family.py: a refusal built without a…
Merge main 3121945 into fix/adopt-plan-quote-yaml-keywords (#181)
tests.yml and adopt-project.py: the Windows job installs git-filter-r…
docs/release-notes/v1.0.0.md: brought up to main 8beb3d9, so section …
docs/release-notes/v1.0.0.md: brought up to main 3121945, so section …
repo_shape.py, shape_materialize.py and adopt-project.py: a value the…
validate-pins.py and repo_shape.py: a pin, the manifest, a shape copy…
bootstrap.py: the `merge --ff-only`, `--continue` and `--abort` lines…
validate-pins.py and repo_shape.py: `--root` and gh's stderr are spel…
validate-pins.py and repo_shape.py: gh's stderr that is not UTF-8 is …
the family root's siblings.py and bootstrap.py: a member's path this …
the assembly root's validate-manifest.py and bootstrap.py: a path thi…
tests/test_path_guard_hygiene.py and shape-doctor.py: a bare is_file/…
shape-doctor.py, validate-pins.py and validate-manifest.py: a root wh…
bootstrap.py, validate-family.py and siblings.py: a root or holder wh…