Skip to content

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

Closed

Description

@brettheap

(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:

  • check says plan ok.
  • execute writes the shape's manifest beside the source's file as shape/project.yaml, prints "beside shape/project.yaml (the source already has project.yaml; nothing was overwritten)", and ends adoption verified.
  • The assembly root's project.yaml is 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 plan on it;
    • the first make bootstrap, which execute's NEXT: names, fails naming, manifest and pins.

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")"
) done

Observed 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 spec or drop, the same source also gives a COMPLIANT assembly.
  • In the root case:
    • the root's scripts/validate-manifest.py exits 1 with 8 findings, among them manifest-kind, manifest-shape and manifest-legs;
    • its scripts/validate-pins.py exits 2, REFUSED manifest-no-legs;
    • the split commit lists shape/project.yaml <- the shape's project.yaml under "WRITTEN BESIDE, NOT OVER", but its FOLLOW-UPS name only shape/README.md.

Why it matters.

  • adoption verified is printed over an assembly with no shape manifest, so nothing that reads one works: the doctor, both validators and make bootstrap. Nothing in execute's output says so. Its last words are adoption verified and the NEXT: 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.yaml and no family.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 same shape/project.yaml, adoption verified and 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 at adopt-project.py:1315-1318 describes it. A source's own project.yaml kept in the root reaches the same end.

Where (main b30595c):

  • contracts/path-classification.yaml:394-408: ambiguous-top-level-yaml. Its question offers root for every top-level YAML file, project.yaml included.
  • adopt-project.py:3101-3106: execute materializes the root with collision_dir=COLLISION_DIR (shape, :171), so a shape path the source already holds is written under shape/, and prints the beside line. scripts/shape_materialize.py:1277-1298, assembly_root_paths(): project.yaml is 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 files contracts/shape-pin.yaml names, eleven in this run.
    • project.yaml is rendered, not pinned, so nothing asks whether the root's project.yaml is the shape's manifest.
    • A source's own contracts/shape-pin.yaml kept in the root does fail verification, with SHAPE_FILE_EXIT (:3644-3650) as its way out.
  • adopt-project.py:1426-1440, leg_path_collisions, which check reaches through _entry_findings (:2255-2279). It holds each leg mount path to assembly_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 named spec or code) passes check as plan ok and dies at the mount after both leg remotes are pushed, so the re-run meets leg-remote-exists #191). It never holds a root-kept source path to assembly_root_paths().
  • adopt-project.py:3108 and :1734-1738: the split's FOLLOW-UPS are the plan's follow_ups:. plan predicts a collision only for an entry that is root when it writes the plan, and this one was leg: null then.
  • AGENTS.md:228-229: NOT A SHAPE ROOT read as "no project.yaml".

What would fix it.

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

  1. brettheap commented on Oct 12, 2026

    @brettheap
    ContributorAuthor

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

    Logged in lanes/log/openRepoShape-5.md at db8e335d547fafaa090d705a1e6790e5d191151c.

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

    1. existing `CLAIMED —` comments on opensoft/openRepoShape#305:
       none
    2. `gh pr list --repo opensoft/openRepoShape --state all --search "#305"`, kept where the row or its body names `#305`:
       none
    3. `git ls-remote --heads git@github.com:opensoft/openRepoShape` branches naming `305` as a whole token:
       none
    
  2. added a commit that references this issue on Oct 12, 2026
  3. added this to the v1.0.0 milestone on Oct 12, 2026
  4. added a commit that references this issue 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