Repository navigation
adopt-project.py _verify never asks whether the split's root project.yaml is the shape's manifest, so the one refusal #305 added before any leg exists is all that keeps the source's own file out of the root, and a plan that gets past it ends adoption verified over an assembly shape-doctor.py calls NOT A SHAPE ROOT, where #165's gitlink checks in the same function fail closed #389
Description
Lane: openreposhape-5 (openRepoShape-5)
(lane openRepoShape-5, 2026-10-12T15:18:02Z)
Found by lane 5's review of fix/adopt-check-link-and-manifest at 69c54c9 (the fix for #303 and #305), its N8. That PR leaves out the second bullet of #305's "What would fix it", on purpose, and closes #305; this issue is where the bullet is tracked. Pre-existing on origin/main 161c6c9 (adopt-project.py is the same file at 0f67f7b). Not claimed.
What is wrong. _verify (adopt-project.py:4358-4430) is the check that makes the rest trustworthy: every source blob in exactly one place. It fails closed on a root gitlink that is not a mount (#165) and on a mount at another commit (#243), and says so: it "does not lean on check's refusals to keep it out" (:4376-4382). It has no word about the root's project.yaml. Its only look at the shape's own files is _shape_file_findings (:4781, called at :4408), which reads the files: list of contracts/shape-pin.yaml, and project.yaml is rendered, not pinned, so it is not on that list.
When the source's own project.yaml is kept in the root, materialize_assembly_root(..., collision_dir=COLLISION_DIR) (:4023-4025; COLLISION_DIR = "shape", :182) writes the shape's manifest beside it as shape/project.yaml and prints "beside ... nothing was overwritten" (:4026-4028). _verify counts the source's file as a root path like any other and ends adoption verified (:4426). That is #305's outcome.
The fix for #305 (69c54c9, not merged when this was filed) makes check find that plan, as plan-root-blocks-manifest, and execute refuse it before any leg exists (_refuse_what_check_finds, called at :4066). Through execute as shipped nothing reaches _verify with the file any more. What keeps it out is that one refusal: _verify itself, untouched by that PR, is as blind as on main.
SHAPE=<openRepoShape checkout> # with git-filter-repo on PATH
# Every repository is local (--local-remote-dir), so no GitHub is reached. Keep the real `gh` out of reach anyway:
STUB=$(mktemp -d); printf '#!/bin/sh\nexit 1\n' > "$STUB/gh"; chmod +x "$STUB/gh"
export PATH="$STUB:$PATH" GH_TOKEN=bogus GH_CONFIG_DIR=$(mktemp -d)
export GIT_AUTHOR_NAME=T GIT_AUTHOR_EMAIL=t@example.invalid GIT_COMMITTER_NAME=T GIT_COMMITTER_EMAIL=t@example.invalid
for mode in as-shipped refusal-removed; do (
TMP=$(mktemp -d)
git init -q -b main "$TMP/Thing" && cd "$TMP/Thing" || exit 1
mkdir src docs && printf 'print(1)\n' > src/app.py && printf '# Spec\n' > docs/spec.md
printf '# Thing\n' > README.md && printf 'name: theirs\n' > project.yaml # the source's own project.yaml
git add -A && git commit -qm init
python3 "$SHAPE/adopt-project.py" plan --source "$TMP/Thing" --project Northwind --org testorg \
--elected-by 'T H' --elected-on 2026-09-02 --out "$TMP/plan.yaml" >/dev/null
python3 - "$TMP/plan.yaml" <<'EOF' # answer root, by hand: the one answer #305 refuses
import re, sys
plan = sys.argv[1]
text = open(plan).read()
text, n = re.subn(r"(?m)^( - path: project\.yaml\n leg: )\S+$",
r'\g<1>root\n resolution: "the person\'s answer"', text)
assert n == 1, n
open(plan, "w").write(text)
EOF
echo "== $mode"
python3 "$SHAPE/adopt-project.py" check --plan "$TMP/plan.yaml" 2>&1 | grep -E '^plan ok|^FINDING' | cut -c1-60
if [ "$mode" = as-shipped ]; then
python3 "$SHAPE/adopt-project.py" execute --plan "$TMP/plan.yaml" --yes \
--local-remote-dir "$TMP/remotes" --work-dir "$TMP/work" > "$TMP/out" 2> "$TMP/err"
else # as tests/test_adopt_verify.py:419 does for #165: `check`'s refusal taken away
python3 - "$SHAPE" "$TMP" > "$TMP/out" 2> "$TMP/err" <<'EOF'
import importlib.util, sys
from argparse import Namespace
from pathlib import Path
shape, tmp = Path(sys.argv[1]), Path(sys.argv[2])
spec = importlib.util.spec_from_file_location("adopt", shape / "adopt-project.py")
adopter = importlib.util.module_from_spec(spec)
spec.loader.exec_module(adopter)
adopter._refuse_what_check_finds = lambda *_: None
sys.exit(adopter.cmd_execute(Namespace(
plan=str(tmp / "plan.yaml"), source=None, local_remote_dir=tmp / "remotes",
allow_empty_leg=[], yes=True, work_dir=tmp / "work")))
EOF
fi
echo "execute exit $?:"; grep -E '^ beside shape/project|^adoption verified' "$TMP/out" | cut -c1-100
echo "leg repositories: $(ls "$TMP/remotes" 2>/dev/null | tr '\n' ' ')"
[ -d "$TMP/remotes" ] || exit 0
git -c protocol.file.allow=always clone -q --recurse-submodules -b adopt/three-repo-shape "$TMP/Thing" "$TMP/clone"
echo "the root's project.yaml begins: $(head -1 "$TMP/clone/project.yaml")"
python3 "$SHAPE/shape-doctor.py" --root "$TMP/clone" < /dev/null 2>&1 | grep -E '\(exit [0-9]\)$'
) doneObserved on origin/main 161c6c9 (Linux, git 2.43.0, Python 3.12.3). The two modes print the same there, because check finds nothing on main:
== as-shipped
plan ok
execute exit 0:
beside shape/project.yaml (the source already has project.yaml; nothing was overwritten)
adoption verified: every source path is in exactly one place
leg repositories: Northwind-code.git Northwind-spec.git
the root's project.yaml begins: name: theirs
NOT A SHAPE ROOT (exit 2)
== refusal-removed
(the same lines)
Observed on 69c54c9, the fix for #305:
== as-shipped
FINDING plan-root-blocks-manifest: the plan keeps the file `
execute exit 2:
leg repositories:
== refusal-removed
FINDING plan-root-blocks-manifest: the plan keeps the file `
execute exit 0:
beside shape/project.yaml (the source already has project.yaml; nothing was overwritten)
adoption verified: every source path is in exactly one place
leg repositories: Northwind-code.git Northwind-spec.git
the root's project.yaml begins: name: theirs
NOT A SHAPE ROOT (exit 2)
Why it matters.
- It is the repository's own rule for this function, at
:4376-4382: fail closed, and do not lean oncheck. adopt-project.py_verifyskips every root gitlink and compares a kept root.gitmodulesbyte for byte, so any plan that keeps a source submodule or its.gitmodulesin the root ends with exit 1 andadopt-lost#165's gitlink checks are the precedent, and their test (tests/test_adopt_verify.py:386-427,test_execute_fails_a_source_submodule_the_plan_checks_let_into_the_root) takes_refuse_what_check_findsaway to prove it. A later change toplan-root-blocks-manifest, or to how a kept path is computed, would reopen adopt-project.pycheckpasses a plan that keeps the source's ownproject.yamlin the root, andexecutewrites the shape's manifest beside it asshape/project.yamland saysadoption verified, so shape-doctor.py calls the adopted assembly NOT A SHAPE ROOT and its firstmake bootstrapfails #305 with nothing behind it. - The cost of a miss is adopt-project.py
checkpasses a plan that keeps the source's ownproject.yamlin the root, andexecutewrites the shape's manifest beside it asshape/project.yamland saysadoption verified, so shape-doctor.py calls the adopted assembly NOT A SHAPE ROOT and its firstmake bootstrapfails #305's. By the time_verifyruns both legs are pushed, and so is the split branch.adoption verifiedand theNEXT:to clone and bootstrap hand over an assembly that the doctor calls NOT A SHAPE ROOT (exit 2) and whosemake bootstrapfails. - It is not reachable through
executeas shipped, so this is defence in depth, not a defect a user can hit today.
Where (origin/main 161c6c9):
adopt-project.py:4358-4430,_verify:_land_the_root_tree(:4403, defined at:4489),_mount_commit_findings(:4404),_account_for(:4407) and_shape_file_findings(:4408) are everything it asks of the split.adopt-project.py:4023-4028, the materialization and thebesideline;scripts/shape_materialize.py:1487-1509,materialize_assembly_root, and:1512,assembly_root_paths()(18 paths,project.yamlamong them).adopt-project.py:1386-1389, the comment on the same outcome by a leg mounted atproject.yaml(adopt-project.py accepts a leg mount path that collides with a directory the shape writes, such as--spec-path contracts, so the adoption fails after both legs are pushed (main: at the first bootstrap) #172).
What would fix it. _verify refuses a split whose root project.yaml is not the shape's manifest: a finding of its own (for example adopt-manifest-not-shape), exit 1 like its others, so adoption verified is never printed over one. How to tell is the author's:
- the split tree holds
shape/project.yaml: the manifest could not be written at the root and went beside the source's file. Cheap, and no judgement of content. Onlyproject.yamlis fatal:shape/README.mdand the other copies are the designed follow-up merge, and stay as they are; - or the source's
project.yamlis counted to the root in_account_for; - or the root's bytes are compared with the manifest
executerendered, or the project's ownvalidate-manifest.pyis run on the split. That costs more, and the stand-in in the tests'ROOT_FILES("project.yaml": "kind: project\n",tests/test_adopt_verify.py:61) would have to become a real manifest.
How to test. In tests/test_adopt_verify.py, the way the #165 tests do:
- (a) with the
verifyfixture, which calls_verifyon hand-made commits and needs nogit filter-repo, so Windows runs it: a split whose tree holdsshape/project.yamlbeside the root'sproject.yaml(root_files=) returns 1 with the new finding, and prints noadoption verified; - (b) shaped like
test_execute_fails_a_source_submodule_the_plan_checks_let_into_the_root(:386-427): the source's ownproject.yamlansweredroot,_refuse_what_check_findsmonkeypatched to a no-op,executereturns 1 with the finding and noadoption verified. It needsgit filter-repo, and is skipped without it as everyexecutetest is; - (c) a control:
shape/README.mdbeside a keptREADME.mdstill givesadoption verified.
Reverting the check fails (a) and (b).
Not in this issue.
- The refusal in
checkandexecute, andplanno longer offeringrootforproject.yaml: done by the fix for adopt-project.pycheckpasses a plan that keeps the source's ownproject.yamlin the root, andexecutewrites the shape's manifest beside it asshape/project.yamland saysadoption verified, so shape-doctor.py calls the adopted assembly NOT A SHAPE ROOT and its firstmake bootstrapfails #305. - The other root paths the shape renders (the pins, the validators,
scripts/repo_shape.py, the naming policy,scripts/bootstrap.py,contracts/shape-pin.yaml). Measured on c7c02fa,checkpasses the source's own file at each, and several verify over a broken assembly. They are reached only by hand-editing acontracts/orscripts/answer toroot, and the fix for adopt-project.pycheckpasses a plan that keeps the source's ownproject.yamlin the root, andexecutewrites the shape's manifest beside it asshape/project.yamland saysadoption verified, so shape-doctor.py calls the adopted assembly NOT A SHAPE ROOT and its firstmake bootstrapfails #305 proposes them as a follow-up of their own. This issue is the manifest, as adopt-project.pycheckpasses a plan that keeps the source's ownproject.yamlin the root, andexecutewrites the shape's manifest beside it asshape/project.yamland saysadoption verified, so shape-doctor.py calls the adopted assembly NOT A SHAPE ROOT and its firstmake bootstrapfails #305 was. - adopt-project.py
checkpasses a plan that keeps the source's ownproject.yamlin the root, andexecutewrites the shape's manifest beside it asshape/project.yamland saysadoption verified, so shape-doctor.py calls the adopted assembly NOT A SHAPE ROOT and its firstmake bootstrapfails #305's fourth, optional bullet: the split's FOLLOW-UPS naming every collisionexecuteactually made, not only the onesplanpredicted. - A top-level
Project.yaml(the case twin), whichplanstill asksambiguous-top-level-yaml's question that offersroot;checkrefuses that answer with the case aside. - Changing what
materialize_assembly_rootdoes with a collision.
Related. #305 and the PR that closes it. #165 and #243: the gitlink and mount-commit checks this would sit beside. #172 and PR #198: the same outcome by a leg mount path. #168: execute refuses what check finds.
Activity
docs/release-notes/v1.0.0.md: brought up to main 437a360, so the note…
adopt-project.py: `check` finds, and `execute` refuses before any leg…
docs/release-notes/v1.0.0.md: brought up to main d41cc29, so section …
brettheap commentedon Oct 16, 2026
CLAIMED — lane openRepoShape-1, session 300f8f46-004a-4e72-8587-4f192be2e108@Eagle, 2026-10-16T11:30:05Z, for #389
Logged in
lanes/log/openRepoShape-1.mdatbc511caea4f4d2c78f9b862a54c28e18d8604462.The three reads (lane-collision-protocol Rule 1):
adopt-project.py: a `project.yaml` the plan drops is never the source…