Repository navigation
adopt-project.py says adoption verified when a submodule the plan keeps in a leg has a url that names no repository (a relative url that resolves alike from the source and the leg, or an absolute one), so the recursive clone its NEXT: line prints fails; nothing asks whether the url answers #418
Description
(lane openRepoShape-1, 2026-10-12T16:42:43Z)
Raised by lane openRepoShape-2's review of the #190 branch at b0c959e: its P3-3, in its comment on #190. #190's body asks for this in the sentence before its sign-off: "_verify ... should not print adoption verified for an assembly whose recursive clone cannot fetch a kept submodule". PR #245 closed #190 without it ("P3-3 is left: main shares it, and it is outside #190's cause"), so this issue keeps it tracked. Pre-existing on main before #245 (b30595c), and still there since #245 landed as 161c6c9 (main 92e29d9).
What is wrong. Nothing in plan, check or execute asks whether a kept submodule's url names a repository. When a submodule the plan keeps in a leg has a url that answers nothing, execute still ends adoption verified: every source path is in exactly one place. The NEXT: it prints, git clone --recurse-submodules and then make bootstrap, then fails at the first step. The url can be:
- relative and alike: it resolves to the same place from the source and from the leg, and nothing is there. adopt-project.py: a relative
.gitmodulesurl the plan keeps in a leg is resolved from the source's remote and from the leg's, and where the two differcheckfinds it andexecuterefuses before any leg exists, so a recursive clone no longer fails afteradoption verified(#190) #245 compares the two answers and takes "alike" as settled. Its NOTE says so: "each names the same repository from there as from the source". - absolute: for example a dependency since renamed or deleted. An absolute url is never resolved at all.
Against GitHub, where check and execute resolve from the same remotes (simulated here with no network: a stub gh, and url.<dir>.insteadOf https://github.com/):
SHAPE=<openRepoShape checkout>
export GIT_AUTHOR_NAME=T GIT_AUTHOR_EMAIL=t@example.invalid GIT_COMMITTER_NAME=T GIT_COMMITTER_EMAIL=t@example.invalid
TMP=$(mktemp -d); mkdir -p "$TMP/gh/testorg" "$TMP/bin"
# GitHub with no network: every https://github.com/ url is a bare repository under $TMP/gh,
# and a stub `gh` makes one for `gh repo create` and answers the rest
printf '[url "%s/gh/"]\n\tinsteadOf = https://github.com/\n' "$TMP" > "$TMP/gitconfig"
export GIT_CONFIG_GLOBAL="$TMP/gitconfig" GIT_ALLOW_PROTOCOL=file PATH="$TMP/bin:$PATH"
cat > "$TMP/bin/gh" <<EOF
#!/bin/sh
case "\$1 \$2" in
"repo create") git init -q --bare -b main "$TMP/gh/\$3.git" && echo "https://github.com/\$3";;
"pr create") echo "https://github.com/testorg/Northwind/pull/1";;
"repo edit") ;;
*) exit 1;;
esac
EOF
chmod +x "$TMP/bin/gh"
git init -q -b main "$TMP/dep" && (cd "$TMP/dep" && echo dep > value.txt && git add -A && git commit -qm dep)
git init -q -b main "$TMP/Northwind" && cd "$TMP/Northwind" || exit 1
mkdir src docs && printf 'print(1)\n' > src/app.py && printf '# Spec\n' > docs/spec.md
printf '# Northwind\n' > README.md && git add -A && git commit -qm init
git submodule add -q "$TMP/dep" vendor/lib
git config -f .gitmodules submodule.vendor/lib.url ../missing && git add .gitmodules
git commit -qm 'vendor/lib from ../missing'
git clone -q --bare . "$TMP/gh/testorg/Northwind.git" && git remote add origin https://github.com/testorg/Northwind.git
git fetch -q origin && git remote set-head origin main
git clone -q --recurse-submodules https://github.com/testorg/Northwind.git "$TMP/source-clone" > /dev/null 2>&1
echo "the source's own recursive clone: exit $?"
python3 "$SHAPE/adopt-project.py" plan --source "$TMP/Northwind" --project Northwind --org testorg \
--elected-by 'T H' --elected-on 2026-09-02 --out "$TMP/plan.yaml" >/dev/null
sed -i 's/^ leg: null$/ leg: code\n resolution: "the vendored library is code"/' "$TMP/plan.yaml" # GNU sed
python3 "$SHAPE/adopt-project.py" check --plan "$TMP/plan.yaml" > "$TMP/check" 2>&1
echo "check exit $?: $(grep -o -m1 -E '^plan ok|^FINDING [a-z-]*' "$TMP/check")"
grep '^NOTE `check` resolved' "$TMP/check"
python3 "$SHAPE/adopt-project.py" execute --plan "$TMP/plan.yaml" --yes --work-dir "$TMP/work" > "$TMP/exec" 2>&1
echo "execute exit $?: $(grep -o -m1 -E '^adoption verified|^REFUSED [a-z-]*' "$TMP/exec")"
git clone -q --recurse-submodules -b adopt/three-repo-shape https://github.com/testorg/Northwind.git "$TMP/assembly" > "$TMP/clone" 2>&1
echo "the assembly's recursive clone: exit $?"; grep -m1 '^fatal: clone of' "$TMP/clone" | sed "s#$TMP#<T>#g"Observed on main 92e29d9 (<T> is the temporary directory; the same on main 1f90a0f, on PR #245's head 8ce5614, and on that head merged with main b30595c):
the source's own recursive clone: exit 1
check exit 0: plan ok
NOTE `check` resolved the relative url of each submodule the plan keeps in the code leg from that leg's GitHub remote, `https://github.com/testorg/Northwind-code.git`, and each names the same repository from there as from the source: `../missing` of `vendor/lib`, to `https://github.com/testorg/missing`. `execute --local-remote-dir` resolves it again from the bare repository it would create instead, and refuses there, before it creates any leg, a url that names another repository.
execute exit 0: adoption verified
the assembly's recursive clone: exit 1
fatal: clone of 'https://github.com/testorg/missing' into submodule path '<T>/assembly/code/vendor/lib' failed
Before #245 (main b30595c), the output is the same without the NOTE.
Under --local-remote-dir, the test path, with a relative and an absolute url:
SHAPE=<openRepoShape checkout>
export GIT_AUTHOR_NAME=T GIT_AUTHOR_EMAIL=t@example.invalid GIT_COMMITTER_NAME=T GIT_COMMITTER_EMAIL=t@example.invalid
SUB="git -c protocol.file.allow=always"
for kind in relative absolute; do
TMP=$(mktemp -d); mkdir "$TMP/remotes"
git init -q -b main "$TMP/dep" && (cd "$TMP/dep" && echo dep > value.txt && git add -A && git commit -qm dep)
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 && git add -A && git commit -qm init
$SUB submodule add -q "$TMP/dep" vendor/lib
# the source's remote sits where the legs will be made, so `../missing` names the same
# place from the source and from either leg: a repository that is not there
git init -q --bare -b main "$TMP/remotes/Thing.git" && git remote add origin "$TMP/remotes/Thing.git"
if [ $kind = relative ]; then url=../missing; else url="$TMP/nowhere.git"; fi
git config -f .gitmodules submodule.vendor/lib.url "$url" && git add .gitmodules
git commit -qm "vendor/lib from $url" && git push -q origin main
echo "== $kind url: $(git config -f .gitmodules submodule.vendor/lib.url | sed "s#$TMP#<T>#")"
$SUB clone -q --recurse-submodules "$TMP/remotes/Thing.git" "$TMP/source-clone" > /dev/null 2>&1
echo "the source's own recursive clone: exit $?"
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
sed -i 's/^ leg: null$/ leg: code\n resolution: "the vendored library is code"/' "$TMP/plan.yaml" # GNU sed
python3 "$SHAPE/adopt-project.py" check --plan "$TMP/plan.yaml" > "$TMP/check" 2>&1
echo "check exit $?: $(grep -o -m1 -E '^plan ok|^FINDING [a-z-]*' "$TMP/check")"
python3 "$SHAPE/adopt-project.py" execute --plan "$TMP/plan.yaml" --yes \
--local-remote-dir "$TMP/remotes" --work-dir "$TMP/work" > "$TMP/exec" 2>&1
echo "execute exit $?: $(grep -o -m1 -E '^adoption verified|^REFUSED [a-z-]*' "$TMP/exec")"
$SUB clone -q --recurse-submodules -b adopt/three-repo-shape "$TMP/Thing" "$TMP/assembly" > "$TMP/clone" 2>&1
echo "the assembly's recursive clone: exit $?"; grep -m2 fatal "$TMP/clone" | sed "s#$TMP#<T>#g"
doneObserved on main 92e29d9 (the same on main 1f90a0f, on 8ce5614, and on it merged with main b30595c):
== relative url: ../missing
the source's own recursive clone: exit 1
check exit 1: FINDING plan-submodule-relative-url
execute exit 0: adoption verified
the assembly's recursive clone: exit 1
fatal: repository '<T>/remotes/missing' does not exist
fatal: clone of '<T>/remotes/missing' into submodule path '<T>/assembly/code/vendor/lib' failed
== absolute url: <T>/nowhere.git
the source's own recursive clone: exit 1
check exit 0: plan ok
execute exit 0: adoption verified
the assembly's recursive clone: exit 1
fatal: repository '<T>/nowhere.git' does not exist
fatal: clone of '<T>/nowhere.git' into submodule path '<T>/assembly/code/vendor/lib' failed
The relative case's check finding here does not catch this defect. check resolves the leg's url from GitHub's remote, for want of a --local-remote-dir (#417). execute --local-remote-dir resolves both sides alike and again ends adoption verified over the failing clone. Before #245 (main b30595c), check said plan ok in this case too, and every other line was the same.
Why it matters.
adoption verifiedis true about what it checks: every blob is in exactly one place. But the person and the agent read it as the go-ahead for the next step it prints, and that step fails.- The source's own recursive clone fails the same way. The adoption reproduces a breakage it could have named first, before two leg repositories were made and the split was pushed.
- Under
--local-remote-dirthe answer is certain. On GitHub, whether a url answers can depend on whose credentials ask, so there it can be stated as a NOTE.
Where (main 92e29d9):
adopt-project.py:4555-4624,_verify: blob accounting only.adoption verifiedis at:4623.cmd_executethen prints_next_steps(:4627-4633), whoseNEXT:holds the recursive clone at:4632, after any verification that passed, unlessgh pr createwas refused (:4308-4314).adopt-project.py:2903-2953,_relative_url_findings: it resolves a relative url from both remotes and passes it when the two answers agree (:2933-2935), without asking whether anything answers. The NOTE is at:2946-2953.adopt-project.py:2799-2815,_kept_relative_urls: only a url thatRELATIVE_URL_RE(:2747) matches is resolved, so an absolute url is never asked about.
What would fix it.
check, andexecutebefore it creates any leg, probe each submodule url the plan keeps in a leg, as the leg will resolve it.git ls-remote <url>needs no clone. A url that answers nothing is a finding under--local-remote-dir, and a NOTE on GitHub, naming the submodule and the url. PR adopt-project.py: a relative.gitmodulesurl the plan keeps in a leg is resolved from the source's remote and from the leg's, and where the two differcheckfinds it andexecuterefuses before any leg exists, so a recursive clone no longer fails afteradoption verified(#190) #245's body suggests the--local-remote-dirhalf, as a NOTE._verify's closing line could also say that the recursive clone was not tried.
Related. #190 and PR #245 (P3-3 there, "left"). #417, on check's missing --local-remote-dir. #215, closed by #240: .gitmodules values git's submodule reader refuses are now a check finding. A url git accepts that names no repository is not one of them.
Not claimed.
🤖 Generated with Claude Code
Activity
brettheap commentedon Oct 14, 2026
CLAIMED — lane openRepoShape-1, session 300f8f46-004a-4e72-8587-4f192be2e108@Eagle, 2026-10-14T22:35:57Z, for #418
Logged in
lanes/log/openRepoShape-1.mdatd046d1392683e6a2e60e96dff174afc69e4eb6c7.The three reads (lane-collision-protocol Rule 1):
adopt-project.py: a kept submodule url that starts with two separator…
Merge origin/main (de428f5) into fix/418-adopt-submodule-url-answers
Merge origin/main (4cf5fb0) into l3-563-docs
docs/release-notes/v1.1.0.md: refreshed at main 4cf5fb0 (49 pull requ…
adopt-project.py: under `LC_ALL=C PYTHONUTF8=0`, `plan`, `check` and …
adopt-project.py: `check` takes `--local-remote-dir` and resolves eac…