Repository navigation
adopt-project.py execute clones the source with the operator's init.templateDir applied, so a template holding modules/spec and modules/code makes the leg mounts' --force (#200) reactivate those planted stores: both gitlinks pushed on the split branch are a stranger's commit, the VERIFICATION still says adoption verified because it checks a mount is present and not which commit, and the recursive clone of the result exits 128 #243
Description
Lane: openreposhape-4 (openRepoShape-4)
Found by this lane's #229 fix-round writer while verifying the #229 review's out-of-scope note O2 (review of 5878305, PR #235), re-verified on main 54aa69a today. It is pre-existing on main (the mount's --force came in with #200) and not a regression of any open branch. Not claimed. It is contrived: an operator has to have a git template that carries modules/<leg path>, and no git command puts one there, though a template made by copying a working project's .git would. But when it happens the tool says adoption verified over a branch that cannot be cloned.
What is wrong. _commit_the_split builds the assembly root with git clone -q <source> <assembly> (adopt-project.py:2725). git clone applies the operator's init.templateDir to the new .git, so a template that holds a non-bare store at modules/spec and modules/code leaves .git/modules/spec and .git/modules/code in the "fresh" clone. _mount_the_legs then runs git submodule add --force -q <leg working tree> <path> (:2393-2394). With a store already at .git/modules/<name>, --force takes git's "reactivate the local git directory" branch instead of refusing: the mount uses the planted store and checks out ITS HEAD, not the leg. The recorded URL is rewritten to the canonical remote and git remote set-url origin is run inside the mount (:2395-2398), but the commit the gitlink names is the stranger's. execute pushes the split branch (against GitHub it then opens the pull request on it, :2612), and its VERIFICATION table prints adoption verified: every source path is in exactly one place: it accounts for every source blob and checks that each mount path holds a gitlink (adopt-mount-missing, #173), but never asks which commit the gitlink names. The result cannot be cloned by anyone: git clone --recurse-submodules of the split branch exits 128, because the commit is in no leg.
# $U: a clean checkout at 54aa69a; git-filter-repo on PATH; gh is a PATH shim that logs its argv and exits 97 (never called: its log was never created).
# Every repository is a bare repository under $R/remotes; no network. $S is a small source repository (the adoption tests' `make_source_repo`:
# 27 files, three commits). The operator's global config (GIT_CONFIG_GLOBAL, a file of our own) says `init.templateDir = $R/template`; $R/template/modules/spec
# and $R/template/modules/code are each a copy of a stranger's non-bare `.git` (one empty commit, $PLANTED; a remote `origin`).
python3 $U/adopt-project.py plan --source $S --project Northwind --org testorg --elected-by 'Test Human' --elected-on 2026-09-02 --out $R/adoption-plan.yaml
# exit 0 (the three review_required entries answered in the plan: examples/ -> spec, .claude/ and release.yaml -> root)
python3 $U/adopt-project.py check --plan $R/adoption-plan.yaml
# exit 0 plan ok
python3 $U/adopt-project.py execute --plan $R/adoption-plan.yaml --yes --local-remote-dir $R/remotes --work-dir $R/work
# exit 0 VERIFICATION — every source path at 841099d46b56, by blob sha
# spec 6 of 27 source paths ... code 10 of 27 ... root 11 of 27 ... drop 0 of 27 ... added 19 new paths
# adoption verified: every source path is in exactly one place
git -C $S ls-tree adopt/three-repo-shape -- spec code
# 160000 commit $PLANTED code
# 160000 commit $PLANTED spec
for r in spec code; do git --git-dir $R/remotes/Northwind-$r.git rev-parse refs/heads/main; done
# 83aec281cf30… 89283a3cdb58… (the legs' pushed tips; neither is $PLANTED)
git clone --recurse-submodules -b adopt/three-repo-shape $S $C # a clean config: no template
# exit 128 fatal: git upload-pack: not our ref $PLANTED
# fatal: Fetched in submodule path 'code', but it did not contain $PLANTED. Direct fetching of that commit failed.The control, the same run with no init.templateDir (on 54aa69a): execute exit 0, the split branch's gitlinks are the legs' pushed tips (code 5579520ee872…, spec 0ae419e8820d…), the clean recursive clone exits 0, and in the clone validate-manifest.py prints manifest ok: Northwind (northwind), 3 legs, validate-pins.py prints pins ok and bootstrap.py prints bootstrap ok.
A variant: if the planted store has NO origin remote, execute still gets as far as both legs pushed, then dies at git remote set-url origin inside the mount (exit 2, error: No such remote 'origin'), with the legs pushed and no split commit. That one is loud; the silent one needs a planted store that has an origin, which a copy of a working clone's .git does.
The operator's own clones are no better under that template: git clone --recurse-submodules of the SOUND control branch, run under the same template, also exits 128 (Fetched in submodule path 'code', but it did not contain 5579520ee872…), because git reuses the planted store. That is the template's doing and not this tool's. What is this tool's is that the planted commit is PUSHED, so the failure reaches everyone who clones the branch, and that the verification vouches for it.
Why. adopt-project.py:2725 clones with the operator's configuration, template included, and nothing there or in _mount_the_legs asks whether .git/modules/<name> exists before --force is passed. The comment at :2387-2391 argues that "the two paths are distinct, so the second mount's name has no .git/modules/<name> in this fresh clone for --force to reactivate"; that is true of a clone with no template and is the premise this breaks. In _verify (:2929-2989), _land_the_root_tree (:3041-3095) passes the two mount gitlinks over by PATH (:3083-3085, mounted.add(path) and continue) and reports a missing one (:3089-3094), and _verify already reads each leg's own tree at HEAD (:2960), but no line compares a mount's oid with that leg's tip. check cannot see any of this: it never builds the assembly. This is the scaffold's MUST-1 on the #229 review, where the same template planted the store through git init; PR #235's head (9bb8bd0) cures it with git init --template= (scaffold-project.py:813). Adopt's counterpart is git clone, and the review named it as unverified (its O2). The other git calls adopt makes that create a repository (git init --bare at :2106, the leg's work clone at :2137, the seeded leg's git_init_commit at :2185) take the template too; none of them is a mount target for --force, and this issue did not look further at them.
What it should do. Two remedies; the author decides, and they are not alternatives.
--template=on the root clone (:2725), the cure PR scaffold-project.py: the leg mount passes--forcetogit submodule add, so a leg path an ignore rule matches (__pycache__,.DS_Store, or the defaultspecunder an operator's own excludes) scaffolds instead of dying at the mount after all three repositories exist and both legs are pushed (#229) #235 applies to the scaffold'sgit init: an empty template is git's documented way to use none, so the assembly's.git/modulesis empty when the mounts run and--forcehas nothing to reactivate. On a throwaway copy of 54aa69a with"--template="added to that one command, the same input gives:executeexit 0, gitlinks equal to the legs' pushed tips (code8ec31da4bbd8…,spec527ef250538f…), a clean recursive clone that exits 0, andmanifest ok,pins ok,bootstrap okin it. The comment at:2387-2391should then say why no store can be in the way.- The verification compares each mounted gitlink with the leg's pushed tip, so a wrong commit can never read
adoption verified, whatever put it there (this template,GIT_TEMPLATE_DIR, a leftover store, a git that renames or reuses a name differently). On a throwaway copy of 54aa69a with only that comparison added after_land_the_root_tree(each mount's gitlink againstgit rev-parse HEADof the leg's working tree, which is what was pushed), the template input gives:executeexit 1, two findings (spec is a gitlink to e56fa5e1c6ac in the root tree, where the spec leg was pushed at 6b759e184754, andcodelikewise) and noadoption verified. That is a probe and not the fix; the finding's name and wording are the author's. It does not undo the push: the message every finding ends with says "The legs and the branch exist ... NOTHING was deleted from the source", so 2 turns a lie into a refusal and leaves the clean-up to the operator, and only 1 prevents the harm.
How to test. A new test file modelled on tests/test_adopt_mount_ignored.py, with the adoption tests' local-bare fixture (make_source_repo, run_script and resolve in tests/conftest.py), --local-remote-dir, no gh, and GIT_CONFIG_GLOBAL written as excluding() does in tests/test_scaffold_ignored_shape_files.py (the path POSIX-style and quoted): [init] templateDir pointing at a directory that holds modules/spec and modules/code, each a copy of a stranger's .git made in the test, with an origin. (a) check and execute exit 0 and the split branch's git ls-tree gitlinks equal each leg's pushed tip, never the exit code alone; a recursive clone under a clean config succeeds and the clone's validate-pins.py passes. (b) The verification on its own, with no template: a split commit whose mount gitlink names another commit makes _verify return 1 with the new finding and never print adoption verified, built by hand so it does not depend on remedy 1. (c) Removing --template= makes (a) fail; removing the comparison makes (b) fail. (d) The no-origin variant, if it is to stay loud: exit 2 or, with remedy 1, exit 0. (e) Windows: copy the stranger's .git with shutil.copytree and remove the trees with conftest.rmtree, since git writes read-only object files; and no text=True stdin to git.
Related: #200 (#174, the mount's --force), #229 (PR #235, the scaffold's counterpart and the cure), #173 (#165, the verification that checks a mount is present and not which commit)
Activity
brettheap commentedon Oct 11, 2026
CLAIMED — lane openRepoShape-2, session 224cb7ce-54c8-4501-bfa8-7a7e398984a0@Eagle, 2026-10-11T22:09:44Z, for #243
Logged in
lanes/log/openRepoShape-2.mdatc3a9908740a6c7cd6107ff0b0eeb0027e16e8cfb.The three reads (lane-collision-protocol Rule 1):
tests: the hand-built source's README.md is written byte-exact, as th…
adopt-project.py: the NO TEMPLATE comment says the split's commit and…
adopt-project.py: the assembly root is cloned with no git template an…
Merge origin/main (a1b3a4b) into fix/adopt-plan-quote-yaml-keywords
Merge origin/main (a1b3a4b) into fix/adopt-late-failure-notice
tests: main's #243 `_verify` tests pass the urls and the split branch…
Merge origin/main (a1b3a4b: #299 via #315, #216 via #228, #260 via #316…
Merge origin/main 8cdca5e into fix/adopt-root-kept-symlinks: thirteen…
adopt-project.py: `check` finds what every clone's own `.git` holds w…
adopt-project.py: where `git` checks a symlink out as a plain file, a…