Repository navigation
adopt-project.py check passes a plan that keeps the source's own project.yaml in the root, and execute writes the shape's manifest beside it as shape/project.yaml and says adoption verified, so shape-doctor.py calls the adopted assembly NOT A SHAPE ROOT and its first make bootstrap fails #305
Description
(lane openRepoShape-1, 2026-10-12T00:14:28Z)
Found by this lane while settling nit 2 of the review of #189 (fix/adopt-record-submodule-placement). Pre-existing on main b30595c.
What is wrong. A source can hold a project.yaml of its own at the top level. plan asks about it under the rule ambiguous-top-level-yaml, and the question offers root as one of its answers: "or project-level wiring for both (root)?". When it is answered root:
checksaysplan ok.executewrites the shape's manifest beside the source's file asshape/project.yaml, prints "beside shape/project.yaml (the source already has project.yaml; nothing was overwritten)", and endsadoption verified.- The assembly root's
project.yamlis then the source's own file:- shape-doctor.py calls the adopted assembly NOT A SHAPE ROOT, exit 2, and its NEXT runs
adopt-project.py planon it; - the first
make bootstrap, whichexecute'sNEXT:names, fails naming, manifest and pins.
- shape-doctor.py calls the adopted assembly NOT A SHAPE ROOT, exit 2, and its NEXT runs
Answered code, the same source adopts into an assembly the doctor calls COMPLIANT.
SHAPE=<openRepoShape checkout>
export GIT_AUTHOR_NAME=T GIT_AUTHOR_EMAIL=t@example.invalid GIT_COMMITTER_NAME=T GIT_COMMITTER_EMAIL=t@example.invalid
for answer in root code; 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
echo "== project.yaml, $(grep -A3 '^ - path: project.yaml$' "$TMP/plan.yaml" | grep -o 'rule: .*'), answered $answer"
python3 - "$TMP/plan.yaml" "$answer" <<'EOF'
import re, sys
plan, answer = sys.argv[1], sys.argv[2]
text = open(plan).read()
text, n = re.subn(r"(?m)^( - path: project\.yaml\n leg: )\S+$",
rf'\g<1>{answer}\n resolution: "the person\'s answer"', text)
assert n == 1, n
open(plan, "w").write(text)
EOF
python3 "$SHAPE/adopt-project.py" check --plan "$TMP/plan.yaml" | tail -1
python3 "$SHAPE/adopt-project.py" execute --plan "$TMP/plan.yaml" --yes \
--local-remote-dir "$TMP/remotes" --work-dir "$TMP/work" > "$TMP/out" 2>&1
echo "execute exit $?:"; grep -E '^ beside shape/project|^adoption verified' "$TMP/out"
git -c protocol.file.allow=always clone -q --recurse-submodules -b adopt/three-repo-shape "$TMP/Thing" "$TMP/clone"
cd "$TMP/clone" || exit 1
echo "the root's project.yaml begins: $(head -1 project.yaml)"
python3 "$SHAPE/shape-doctor.py" --root "$TMP/clone" > "$TMP/doctor" 2>&1
echo "shape-doctor exit $?: $(grep -E '\(exit [0-9]\)$' "$TMP/doctor")"
grep -E '^ n/a +the way in|^ +NEXT +python3 .*adopt-project' "$TMP/doctor" | sed "s#$SHAPE#<shape>#; s#$TMP#<T>#g"
make bootstrap > "$TMP/bootstrap" 2>&1
echo "make bootstrap exit $?: $(grep -E '^bootstrap (ok|FAILED)' "$TMP/bootstrap")"
) doneObserved on main b30595c (<T> is the temporary directory, <shape> the checkout):
== project.yaml, rule: ambiguous-top-level-yaml, answered root
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
the root's project.yaml begins: name: theirs
shape-doctor exit 2: NOT A SHAPE ROOT (exit 2)
n/a the way in this is a git repository with no shape manifest: it is ADOPTED in place, keeping its name, identity and history
NEXT python3 <shape>/adopt-project.py plan --source <T>/clone --project Thing
make bootstrap exit 2: bootstrap FAILED: naming (exit 2), manifest (exit 1), pins (exit 2)
== project.yaml, rule: ambiguous-top-level-yaml, answered code
plan ok
execute exit 0:
adoption verified: every source path is in exactly one place
the root's project.yaml begins: schema_version: 1
shape-doctor exit 0: COMPLIANT (exit 0)
make bootstrap exit 0: bootstrap ok
Measured beside the script:
- Answered
specordrop, the same source also gives a COMPLIANT assembly. - In the
rootcase:- the root's
scripts/validate-manifest.pyexits 1 with 8 findings, among themmanifest-kind,manifest-shapeandmanifest-legs; - its
scripts/validate-pins.pyexits 2,REFUSED manifest-no-legs; - the split commit lists
shape/project.yaml <- the shape's project.yamlunder "WRITTEN BESIDE, NOT OVER", but its FOLLOW-UPS name onlyshape/README.md.
- the root's
Why it matters.
adoption verifiedis printed over an assembly with no shape manifest, so nothing that reads one works: the doctor, both validators andmake bootstrap. Nothing inexecute's output says so. Its last words areadoption verifiedand theNEXT:to clone and bootstrap, and its one line about the file reads as a success: "nothing was overwritten".- By then both legs are pushed, and so is the split branch. The way back is a corrected plan and a second adoption into fresh legs.
- What the person is told next is wrong too:
- The doctor's NEXT is to adopt the assembly that was just adopted.
- AGENTS.md's "Checking" point 5 explains NOT A SHAPE ROOT as "There is no
project.yamland nofamily.yaml", and that is not so here.
- The plan's own question offers
root, and of the four answers it is the only one whose assembly the doctor does not call COMPLIANT. - Main already closed this outcome on another route. A leg mounted at
project.yaml(--code-path project.yaml) used to end in the sameshape/project.yaml,adoption verifiedand NOT A SHAPE ROOT, and 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 now refuses that mount path before anything exists. The comment atadopt-project.py:1315-1318describes it. A source's ownproject.yamlkept in the root reaches the same end.
Where (main b30595c):
contracts/path-classification.yaml:394-408:ambiguous-top-level-yaml. Its question offersrootfor every top-level YAML file,project.yamlincluded.adopt-project.py:3101-3106:executematerializes the root withcollision_dir=COLLISION_DIR(shape,:171), so a shape path the source already holds is written undershape/, and prints thebesideline.scripts/shape_materialize.py:1277-1298,assembly_root_paths():project.yamlis one of its 18 paths.adopt-project.py:3300-3360,_verify:- It accounts for the source's blobs, and through
_shape_file_findings(:3672-3705) for the filescontracts/shape-pin.yamlnames, eleven in this run. project.yamlis rendered, not pinned, so nothing asks whether the root'sproject.yamlis the shape's manifest.- A source's own
contracts/shape-pin.yamlkept in the root does fail verification, withSHAPE_FILE_EXIT(:3644-3650) as its way out.
- It accounts for the source's blobs, and through
adopt-project.py:1426-1440,leg_path_collisions, whichcheckreaches through_entry_findings(:2255-2279). It holds each leg mount path toassembly_root_paths()(_shape_collisions,:1371) and to the source paths the plan keeps in the root (adopt-project.py: a source entry the plan keeps at the root at a leg mount path (a submodule, directory or file namedspecorcode) passescheckasplan okand dies at the mount after both leg remotes are pushed, so the re-run meetsleg-remote-exists#191). It never holds a root-kept source path toassembly_root_paths().adopt-project.py:3108and:1734-1738: the split's FOLLOW-UPS are the plan'sfollow_ups:.planpredicts a collision only for an entry that isrootwhen it writes the plan, and this one wasleg: nullthen.AGENTS.md:228-229: NOT A SHAPE ROOT read as "noproject.yaml".
What would fix it.
checkreports a plan that keeps the source's ownproject.yamlin the root, andexecuterefuses it before any leg exists, through adopt-project.pyexecuterefuses onlyplan-unresolvedandplan-stale, so a plancheckrejects as uncovered or with a bad leg creates and pushes both legs before it fails verification #168's list. The remediation: send it to a leg ordropit. Whether other root paths the shape renders need the same rule is part of this question._verifyrefuses an assembly whose rootproject.yamlis not the shape's manifest, as it refuses a missing pinned shape file, soadoption verifiedis never printed over one.plan's question for a top-levelproject.yamlsays thatrootis not an answer for that path.- Optionally, the split's FOLLOW-UPS name every collision
executeactually made, not only the onesplanpredicted.
Related. #189: found while settling nit 2 of its review. #172 and PR #198: the same outcome by a leg mount path. #262 and #263: other source paths kept in the root that check passes and the shape then cannot write around. #168: execute refuses what check finds.
Not claimed.
🤖 Generated with Claude Code
Activity
brettheap commentedon Oct 12, 2026
CLAIMED — lane openRepoShape-5, session e62c2fd7-176d-429d-89fb-0653dd08ad5f@Eagle, 2026-10-12T11:04:26Z, for #305
Logged in
lanes/log/openRepoShape-5.mdatdb8e335d547fafaa090d705a1e6790e5d191151c.The three reads (lane-collision-protocol Rule 1):
Merge origin/main (3ef059c, #314) into fix/adopt-check-link-and-manif…
docs/release-notes/v1.0.0.md: brought up to main 437a360, so the note…
Merge main 5a25387 into fix/adopt-plan-quote-yaml-keywords (#181)
adopt-project.py: a symlink with an empty target is left to #303's `p…
docs/release-notes/v1.0.0.md: brought up to main d41cc29, so section …
adopt-project.py: `check` follows each declared pin's file, `contract…
adopt-project.py: `check` follows each symlink the plan keeps where t…
adopt-project.py: `_verify` fails a split whose root `project.yaml` i…