Repository navigation
adopt-project.py: every failure once a leg repository exists flushes stdout, then prints REFUSED <step>., what failed and one notice of what the run left, so a failed git filter-repo no longer exits 2 with an empty stderr (#171, #188, #177) - #445
Merged
Merged
Conversation
…leg cannot be built, so a `git filter-repo` that fails after both leg remotes exist no longer exits 2 with an empty stderr (#171) `_build_the_legs` caught `CommandFailed` from the leg extraction and returned None on the premise that `run` had already said which command failed and why. `run` only raises, so the human saw the two `bare .../Northwind-*.git` lines, exit 2 and nothing on stderr, with both leg remotes already created and the corrected re-run waiting at `leg-remote-exists`. It now prints `exc.loudly(...)`, naming the verb (extracting or seeding), the leg, the exact command and its output, the way the file's other callers do. A refused push is the one failure `_extract_leg` and `_seed_leg` already print themselves, with the ruleset hint, before they re-raise it; it is not printed a second time. tests/test_adopt_filter_repo_failure.py: a stubbed extraction and a stubbed seeding that raise `CommandFailed` (no filter-repo needed), a real seeded leg pushed to a remote that does not exist (one REFUSED block, the hint intact), and, where filter-repo is installed, the issue's `.gitmodules` with a bare `path` key through `execute` itself. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… leg repositories it leaves behind and stdout flushed first, and a refused push is recognised by a flag the leg builders set rather than by its argv (#171) The review of 5cd06c7 asked for four changes, decided before this round. S1. After the REFUSED block, `_build_the_legs` prints one line naming `urls["spec"]` and `urls["code"]`: they may now exist, one leg may already be pushed, nothing was rolled back, and a re-run of the corrected plan meets `leg-remote-exists` unless they are deleted or fresh ones are used. stdout is flushed first, so a log that merges the two streams keeps the order things ran in: piped, the two `bare ...` lines were block-buffered and came out AFTER the failure they explain. On the issue's reproduction, without the flush REFUSED is line 0 of the merged log and the `bare` lines are 19 and 20; with it the `bare` lines are 3 and 4 and REFUSED is 5. N1. The `args_list[:2] == ["git", "push"]` guard is gone. `_extract_leg` and `_seed_leg` set `exc.reported = True` on the push failure they already print, with the ruleset hint, and re-raise; `_build_the_legs` prints only when the flag is absent. A push is no longer special-cased by its argv, so a future step the builders print themselves needs one line, not a second guard. N2. The end-to-end test appends the bare `path` section to the `.gitmodules` that `git submodule add` wrote, instead of hand-writing the url: a Windows path in a hand-written one needs git's own escaping for the backslash. N3. The seeding case raises `git commit -q -F -`, the command a seeding leg actually dies on, not a `git filter-repo`; the parametrisations carry readable ids. N4. The `_build_the_legs` docstring no longer mentions `gh` (the leg builders only run `git`) and the test module's first docstring line fits in 79 columns. tests/test_adopt_filter_repo_failure.py: the notice (both URLs, `rolled back`, `leg-remote-exists`, `--local-remote-dir`, one line) for both leg kinds and through `execute`; the flush, as events recorded on a stand-in for each stream; and the refused push for BOTH builders, real `git` against a remote that does not exist (the extraction skips only `git filter-repo`): one REFUSED, one `NOTHING has been rolled back`, no `leg-remote-exists`. Each fails with its change reverted; N2, N3 and N4 are about how the tests are written and have no behaviour to revert on Linux. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…or both kinds of remote, `leg-remote-exists` under --local-remote-dir and a `gh repo create` that finds the name taken against GitHub (#171) 41ab8ca said a re-run of the corrected plan "meets `leg-remote-exists`". That refusal is `_create_leg_remotes`' local branch only. Against GitHub the same function runs `gh repo create <org>/<name>` for the spec leg first; `run` raises when `gh` exits non-zero, nothing in `_create_leg_remotes` or `cmd_execute` catches it, and `main` prints it as `REFUSED a git or gh command failed` and exits 2, before the code leg is touched. The notice now reads: "Re-running the corrected plan is refused until they are deleted or fresh ones are used (a new --local-remote-dir, or new leg repositories): under --local-remote-dir it meets `leg-remote-exists`, and against GitHub `gh repo create` finds the name already taken." One sentence for both, because `_build_the_legs` is not told which kind it is building for. The `_build_the_legs` docstring says the same, naming `_create_leg_remotes`. tests/test_adopt_filter_repo_failure.py: the notice test also asserts `gh repo create` and `already taken`, and fails against 41ab8ca's text. Two new tests pin the facts the sentence states from the code that makes them true: an existing bare leg is a `leg-remote-exists` Refusal, and under a GitHub remote the first call is `gh repo create` for the spec leg and its CommandFailed is not swallowed. Nothing runs `gh` and nothing touches GitHub. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…hes stdout, names its step and says what it left, and a refused leg push prints a seed push to the leg's own URL, so a refused split push no longer says only `REFUSED pushing the split branch` ahead of the `bare` lines, and the hint's push no longer fails with `'origin' does not appear to be a git repository` (#188, #177) #188. Reproduced on main f25d805 with local bare remotes and a global core.hooksPath pre-receive hook. With the split push refused, stderr was only `REFUSED pushing the split branch`; in a merged `> log 2>&1` the REFUSED line was line 0 and the two `bare` lines were lines 13-14; and the re-run met `REFUSED leg-remote-exists` with nothing having said why. A pre-commit hook refusing the split commit fell through to `main`'s generic `REFUSED a git or gh command failed`, the same way. `_commit_the_split` now wraps the clone, the mount, the commit and the push. A failure names its step (`cloning the source for the split`, `mounting the legs`, `committing the split`, `pushing the split branch`), flushes stdout first, and prints the exact command and its output. Then it prints the notice `_build_the_legs` prints (#171): both leg repositories exist, both are pushed, and why a re-run is refused. A refused split push also says that a split branch an earlier run left behind refuses every re-run until it is deleted, because fresh legs alone would loop there. The exit is still 2. The opening of the pull request and the setting of the topic flush stdout before they report. `main`'s two handlers flush too, for what no step reports itself, such as a `git_out` Refusal once the legs exist. One wording. `_nothing_rolled_back(urls, pushed)` replaces #171's inline notice, so a leg that cannot be built and a split that cannot be made share one sentence. It says how far the run got (neither leg, the spec leg, or both legs are pushed) rather than "one leg may already be pushed". It says "Re-running the adoption" rather than "Re-running the corrected plan", because the cause is not always the plan. `_flush_stdout` swallows OSError, so a closed stdout cannot replace the report with a BrokenPipeError. #177. A refused leg push printed the scaffold's RULESET_HINT. Its exit (2), `git -C <work> push -u origin main:seed/scaffold`, exits 128 from the leg's work clone. `git filter-repo` removes `origin`, and a seeded leg is a fresh `git init` that never had one. Its exit (1) also said "re-run this scaffold", and its "What already exists is listed above" came before the `bare` lines in a merged log. The adoption now has its own ADOPT_RULESET_HINT. It pushes HEAD to the leg's own URL as `seed/adopt`, bases the pull request on the plan's tracking branch, and says "re-run this adoption, once the leg repositories named below are deleted or replaced". The notice follows it, now also after a refused push. Both leg builders flush stdout before they report. RULESET_HINT, scaffold-project.py and family.py are untouched. Tests: tests/test_adopt_late_failure.py. - Each split step failing (clone for real, mount stubbed, commit for real with nothing staged, push to a missing remote), and a split push refused as not a fast-forward. - The notice's three states. - stdout flushed before stderr, for the split, a leg push, the pull request, the topic, and `main`. - A closed stdout. - The printed seed push RUN from a seeded leg against a bare remote whose hook refuses only `main`. - End to end with git filter-repo and a merged stream: lane 2's push-split repro, and the printed push RUN from an extracted leg. tests/conftest.py: `run_script(..., merge_stderr=True)` gives one pipe for both streams. tests/test_adopt_filter_repo_failure.py: a refused push now gets the hint and then the notice, once each. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings in #185 (scaffold and seeded-leg force-staging), #186 and #193. Textually clean. #185 makes `_seed_leg` pass what `copy_tree` returns to `git_init_commit`, so a test stub of `copy_tree` must now return the paths it wrote; that is the next commit. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…y wrote, so the seeded-leg push tests no longer die with `TypeError: 'NoneType' object is not iterable` in `stage_written` once #175 is merged (#188, #177) Since #185 (#175), `_seed_leg` hands what `copy_tree` returns to `git_init_commit`, whose `stage_written` calls `list(written)`. The stubs returned None, so after merging origin/main 22c9fdd three tests failed in `stage_written`, before the push they are about: `test_stdout_is_flushed_before_a_late_failure_reaches_stderr[leg-push]`, `test_the_printed_seed_push_runs_from_a_seeded_leg` and #171's `test_a_refused_push_is_reported_once_with_its_ruleset_hint[seeded-leg-push]`. Each stub now returns `["README.md"]`, as `copy_tree` would. PR #184's branch carries the same stub. It will need this line too when it merges a main that has #185. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e-notice Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ted with its step and a notice that names the work directory, a failed verification names the split branch and the command that deletes it, and `execute` refuses that branch before creating any leg, so following the printed way out ends in `adoption verified` instead of creating more leg repositories each time (#171, #188, #177) Lane openRepoShape-2 reviewed PR #184 at f416e13 and this branch at 68d4d63. These are the findings, and how each was fixed: - #184 P1-1: the filter-repo e2e test no longer depends on a .gitmodules that #166's `check` now refuses. A stub `git-filter-repo` placed first on PATH exits 1, a failure `check` cannot foresee. The stub is a POSIX shell script, so the test is skipped on Windows (WINDOWS_SKIP). - #184 P2-1: the notice and the `leg-remote-exists` remediation both name the work directory: re-run with a fresh --work-dir, or with none. Two e2e tests follow the printed way out word for word to `adoption verified`, one after a failed leg build and one after a refused split push. - #184 P3-1 and #188 N3: CommandFailed, Refusal, OSError and UnicodeError (`LATE_FAILURES`) are each reported with a stdout flush, the step name and the notice, then exit 2. Anything else, such as Ctrl-C, gets the notice as STOPPED and is re-raised. The substitution table is now built before the leg repositories exist. Each leg builder reads its commit count and tree digest before it pushes. - #188 N1: a failed verification flushes stdout before printing its findings. Its exit names the leg repositories, the work directory and the split branch, with the shell-quoted command that deletes the branch. `execute` refuses with `adopt-branch-exists` before any leg repository exists. That check is skipped while a leg repository from an earlier run is still on disk under --local-remote-dir, so `leg-remote-exists` is still reported first. - #188 N2 and #177: `ADOPT_RULESET_HINT` names the one exit an adoption has. The seed-branch exit is removed, because it cannot finish an adoption. - #188 N4: if the code leg's repository cannot be created after the spec leg's was, the failure gets the notice naming the spec repository. `main` then exits 2 without printing it again, including when the failure is an OSError (a `gh` that cannot be run). - #188 N5: the only command still printed, the branch deletion, is quoted for sh. The tests parse printed commands with shlex.split, using paths that contain spaces. RULESET_HINT, scaffold-project.py and scripts/ are unchanged. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…-failure-notice One conflict, in adopt-project.py, on the `from shape_materialize import` block. This branch had dropped `RULESET_HINT` from it (c77cef0: an adoption prints its own `ADOPT_RULESET_HINT`, and nothing in adopt-project.py uses the scaffold's any more). Main had added `assembly_root_paths` and re-flowed the lines. The resolution keeps both changes: `RULESET_HINT` is not imported, and `assembly_root_paths` is. Nothing in the merged adopt-project.py names `RULESET_HINT` outside a comment. Everything else merged cleanly. `_leg_findings(plan, tree)` and `_entry_findings` are as #198 left them, with `tree` passed. The only unused import is `COMMIT_RE`, which is unused on main as well. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e-notice origin/main moved from 20e13f0 to 8df21e6 (#201: the leg-path helpers move into scripts/shape_materialize.py) while this branch was being finished. This merge brings it in. It merged with no conflict. #201 removed `SAFE_ARG_RE` from the `repo_shape` import, and this branch added `REMEDIATION` there; both changes are kept. The `shape_materialize` import is unchanged from the previous merge: `assembly_root_paths` is imported and `RULESET_HINT` is not. `_leg_findings(plan, tree)` is unchanged. The only unused imports are `COMMIT_RE` and `leg_path_problem`, and main has the same two. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e-notice It merged with no conflict. #200 passes `--force` to `git submodule add` and adds `_refuse_an_unmountable_path` before each mount. This branch does not touch `_mount_the_legs`: its `_commit_the_split` already runs it as the step `mounting the legs`. `_leg_findings(plan, tree)` is unchanged. One behaviour of #200 meets this branch's. The guard's two refusals, `adopt-mount-occupied` and `adopt-mount-unaddable`, come after both legs are pushed. So `_commit_the_split` reports them as it reports every late failure: `REFUSED mounting the legs.`, #200's own remediation, then the notice. `execute` exits 2, as it does on main. Three of #200's tests in tests/test_adopt_leg_path_collisions.py run `cmd_execute` in-process and expect the refusal to escape it. On this merge they fail with `DID NOT RAISE Refusal`. The next commit brings them in line with this branch's report. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…he three #200 added no longer fail on this branch with `DID NOT RAISE Refusal` (#188, #174) #200 (#174) added three tests to tests/test_adopt_leg_path_collisions.py. Each switches off #172's root-kept half and runs `cmd_execute` in-process until the mount guard refuses, after both legs are pushed. They expected the `Refusal` to escape `cmd_execute`, as it does on main, where `main` prints it and exits 2. On this branch every failure once a leg repository exists is reported where it happens (#188). `_commit_the_split` prints `REFUSED mounting the legs.`, the refusal's code and detail, its own remediation, and then the notice of what the run left. `cmd_execute` then returns 2. The exit code and the guard's words are the same as on main. After the merge of a5b09c3 the three tests failed with `DID NOT RAISE Refusal`. `refused_at_the_mount` now records the refusal the guard raises, by wrapping `_refuse_an_unmountable_path`. It asserts that `cmd_execute` returned 2, that stderr carries that refusal word for word under the step, and that the notice is the last line. The three tests' own assertions are unchanged. No code changed. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e-notice One file conflicted, adopt-project.py, in four hunks, all in or at `_verify`. Every one keeps both sides: - `cmd_execute`'s call to `_verify`: main passes the plan's two mount paths, `spec_path` and `code_path`, and this branch passes `urls` and `branch` by keyword. The call now passes both, in that order. - `_verify`'s signature: main added `spec_path` and `code_path` as positional parameters before `seeded`; this branch added `urls` and `branch` as keyword-only. The signature has both: `(..., split_commit, spec_path, code_path, seeded=(), *, urls, branch)`. - `_verify`'s docstring: main's #165 paragraph (only the two mounts are passed over) and this branch's #188 N1 paragraph (a failed verification is the last late failure) are both kept, #165's first, since it says what is checked and #188's says what a failure prints. - `_verify`'s footer: main added `findings += root_findings` and "unless a finding above names another exit", for its findings whose exit is to report the split. This branch flushes stdout first, prints the findings in one write, and follows them with the notice and the split branch's deletion. The footer keeps all of it: the root findings join the list, the flush and the notice follow, and the sentence now reads "fix the plan and re-run, unless a finding above names another exit". The merged diff against main differs from this branch's diff against a5b09c3 in those hunks alone. tests/test_adopt_submodules.py and the new tests/test_adopt_verify.py merged without conflict. The merge breaks main's new direct `_verify` tests: they call it without `urls` and `branch`, which this branch makes required, and one reads the footer's old last sentence. The next commit fixes them. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… `_verify` names in its way out, and read its exit sentence as merged, so the 14 that call it directly no longer fail on this branch with `TypeError: _verify() missing 2 required keyword-only arguments` (#188, #165) #165 (via #173) added tests/test_adopt_verify.py, whose `verify` fixture calls `_verify` positionally with the plan's two mount paths. This branch makes `urls` and `branch` keyword-only and required, because a failed verification names the legs, the work directory and the split branch with the command that deletes it (#188 N1). On the merge, e52f6be, 14 of its 16 tests failed with that TypeError; the two that run `execute` passed, since `cmd_execute` passes both. - The fixture passes `urls`, one per repository as `execute` would have them against GitHub (the assembly's is the source itself), and `branch=SPLIT_BRANCH`. Nothing is pushed to them: `_verify` only names them. - `test_a_spec_gitlink_not_where_the_plan_spells_the_mount_is_two_findings` read main's last sentence, "re-run into a fresh --local-remote-dir or fresh leg repositories, unless a finding above names another exit.", at the end of stderr. As merged, that exit is "fix the plan and re-run, unless a finding above names another exit.", and the notice and the branch deletion follow it. The test now reads that sentence where it stands, right after the count. No code changed. All 16 pass. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… a leg or the split before it creates any leg repository, and the notice says to fix what failed first and names the work directory only once this run built in it, so following `adopt-branch-exists`'s way out no longer makes a new pair of leg repositories and dies at the clone (#188) The review of #188 at 8d4fef4 found one way out that looped (F1, a BLOCKER). `adopt-branch-exists` says "delete the branch, then run `execute` again", with no work-directory clause. It fires only after an earlier run pushed its split, so that run had built both legs and the split in its `--work-dir`. Followed as written, the re-run made both leg repositories (against GitHub, two real ones), then died at `git clone` with `destination path '<work>/Northwind-spec' already exists`. The verification's own FINDING exit, "re-run into fresh legs", led there. F1, the review's better fix. `_refuse_an_occupied_work_dir` refuses, before `_confirm` and before any leg repository exists, a work directory that holds anything at `<work>/<spec leg>`, `<work>/<code leg>` or `<work>/<assembly>` other than an empty directory (`adopt-work-dir- occupied`). That is what `git clone` refuses, and what a seeded leg's first commit would take in. Its remediation names a `--work-dir` that is new or empty, or none. It is the first refusal of what an earlier run left, ahead of `adopt-branch-exists` and so of `leg-remote-exists`. That way their own "run `execute` again" meets a free work directory, and `adopt-branch-exists`'s way out now works as written. A default run is never refused: `_work_root` makes a new, empty directory with `mkdtemp`. A new path or an empty one holds nothing either. The other ways out, made to work as written: - `SHAPE_FILE_EXIT` said "re-run into fresh legs", which left out the work directory and the branch. It now says "re-run as the end of this report says". It is printed only in a failed verification's findings, and that footer names the legs, the work directory and the branch. - #200's MOUNT_REMEDIATION already names an empty `--work-dir`, or none. Its text is unchanged. Its test now also sees that a re-run which keeps the work directory is refused before it makes a leg repository. - The notice (`_nothing_rolled_back`), below. F2. The notice never said to fix what failed, so a cause that was still there (a hook, a ruleset, a symlink loop) made a new pair of legs on every attempt. It now reads "Once what failed above is fixed, re-run the adoption with them deleted or with fresh ones". For a run that was STOPPED, by Ctrl-C say, it reads "Re-run the adoption with ...", since nothing failed. N2. The notice said "`git clone` refuses a directory this run has already cloned into" even when nothing was cloned (the code leg's repository failing, #188 N4). Its work-directory sentence is now read off the disk. It is printed only when this run built something there, and names what: "Use a fresh --work-dir as well, or none: <work> holds Northwind-spec, Northwind-code and Northwind from this run, and `execute` refuses a --work-dir that holds them." Tests (tests/test_adopt_late_failure.py, tests/test_adopt_filter_repo_ failure.py, tests/test_adopt_leg_path_collisions.py): - New: a work directory holding a leg's clone, the split's, or a file at a leg's path is refused before anything exists and ahead of the branch left beside it, with the work directory left unchanged. A default, new or empty work directory reaches the confirmation. - The failed-verification e2e now follows the review's chain. A re-run into fresh legs that keeps the work directory is refused for it, with nothing made. Its way out meets `adopt-branch-exists`, with nothing made. That way out as written (the printed deletion, then the same command again) ends in `adoption verified`. - The notice's tests read the fix-first opening, the STOPPED opening, a work-directory sentence that names exactly what was built, and none when nothing was (N4). Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…it push cannot fast-forward, and says so in sentences true of it, so a branch of that name at the source commit or behind it is pushed over and verifies, as on main, instead of being refused with a false reason (#188) N1 of the review of #188 at 8d4fef4. `_refuse_a_split_branch_left_behind` refused ANY branch named `adopt_branch:` on the assembly, saying "a new split commit is never a fast-forward of the old one, so this run would create and push both legs and then be refused at the split push". For a branch at the source commit, which is what `git branch adopt/three-repo- shape` leaves and what the unit tests' fixture made, that is false: the split commit's one parent is the source commit, the push fast-forwards the branch, and main a5b09c3 verified over it (the review's case F). - `_in_the_history_of` asks `git merge-base --is-ancestor <tip> <source commit>` in the source. Exit 0, a tip at or behind the source commit, is not refused. A tip the source does not hold (exit 128) is not behind its commit either, so it is refused. A tip that is not a hex object name never reaches `git`. - The refusal names the tip: "... at <tip>: neither the source commit <commit> nor a commit behind it". Its remediation now gives the reason in terms that hold for every branch it still refuses: git moves a branch to the split commit without force only from the source commit or behind it, and this tool never forces a push. - `adopt_branch:` is a plan value. A person may point it at a branch of the source's own, and the old remediation told them to delete it. Now "If the branch is not an earlier run's ... give `adopt_branch:` a name no branch has", and the deletion is offered only for an earlier run's. - "nothing was created" becomes "no leg repository was created and nothing was pushed", as `_refuse_what_check_finds` says. `_work_root` has made the work directory by then, and an `org/repo` source has been cloned into it. Tests (tests/test_adopt_late_failure.py): - `leave_a_split_branch` now leaves what an earlier run leaves: a commit on top of the source commit, not a branch at it. So the refusal tests test the real case, and the precedence tests stay meaningful. - New: a branch at the source commit or two commits behind it is not refused. End to end (the review's case F), such a branch is pushed over, its new tip's parent is the source commit, and the run says `adoption verified`. - The refusal's detail is read whole. - `adoption verified` is named once (VERIFIED), as a third use would repeat it. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… every other late failure, with its step, the notice and the split branch, so a `git` failure inside `_verify` no longer reaches `main` as `REFUSED git-failed` with the validators' remediation about pins and no word of the two pushed legs (#188) N3 of the review of #188 at 8d4fef4. "Every failure once a leg repository exists" did not hold inside `_verify`. A `git_out` refusal there, from `source.tree()`, `_tree_of` or the shape pin's read, reached `main`'s generic handler. It printed `REFUSED git-failed: ...` and `Remediation: run git submodule update --init --recursive, then python3 scripts/validate-pins.py ...`, which `_failed_loudly` drops everywhere else. No notice and no branch line followed, although both legs and the split branch existed (the review's case K; the same on main). - `_verify`'s accounting moves, unchanged, into `_accounted_for`, which prints the table and returns the findings in the same order. - `_verify` runs it as every late step runs: `LATE_FAILURES` are reported as `REFUSED verifying the split. <what failed>`, then the notice and the split branch with its deletion, and `execute` exits 2, as it did. Anything else (Ctrl-C) gets `STOPPED verifying the split by ...`, the same notice and branch, and is re-raised. - `_left_by_the_split` says the notice and the branch once, for the findings' footer and for this. So the claim holds as the body states it: every step the run takes once a leg repository exists reports its own failure. The steps after the split push, the pull request and the topic, give their by-hand exits. Tests (tests/test_adopt_late_failure.py): a `_tree_of` that refuses as `git_out` does gives exit 2 with stdout flushed first, the step, no pins remediation, the notice (both legs pushed, fix first, the split's clone named) and the printed deletion. An interrupt gives STOPPED, a notice with no fix-first clause, and the deletion. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e plan, delete the split branch and re-run into fresh legs and a fresh `--work-dir` or none, so an assistant no longer relays "re-run into fresh legs", which `execute` now refuses before it makes anything (#188) F3 of the review of #188 at 8d4fef4. Step 7 said the exit of a failed verification was "to fix the plan and re-run into fresh legs". On this branch that re-run is refused, before any leg exists, by `adopt-branch-exists` (the split branch the failed run pushed is still there), and by `adopt-work-dir-occupied` (5dbda31) when it keeps the work directory. The tool's own exit has three parts: fix the plan, delete the split branch with the command it prints, and re-run into fresh legs and a fresh `--work-dir`, or none. Step 7 now names them. "The source is untouched -- nothing is deleted, `main` is where it was, the split is on a branch" becomes "the source lost nothing". The branch it now says to delete is on the source, and tests/test_repo_hygiene.py caps AGENTS.md at 464 lines, which it already is. So the step keeps its five lines. `scripts/render-cli-reference.py --check`: `[ok] docs/cli.md is what the generator produces`. No `--help` text changed. README.md and docs/ never named this exit. Test (tests/test_adopt_late_failure.py): step 7 is read out of AGENTS.md and must name the three parts. With the old step it fails. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…imes or more is named once, so `'No such file or directory'`, `'work_root'`, `'branch'` and `'utf-8'` no longer trip Sonar's S1192 there (#188) N6 of the review of #188 at 8d4fef4 found two literals that 8d4fef4 used three times: 'No such file or directory' in tests/test_adopt_late_failure.py and 'work_root' in tests/test_adopt_filter_repo_failure.py. This round's tests added two more: 'branch', the git subcommand that makes the split's branch at a commit, and 'utf-8', for the spec leg's built tree. - `NO_SUCH_FILE`, `WORK_ROOT` and `UTF8` are named once each. - `branch_at(repo, commit)` makes the split's branch, as the three tests that need one at a given commit do. Checked with an AST count of literals of five or more characters, against main 54aa69a. In every file this branch changes, no literal is newly at three or more. adopt-project.py's 'assembly' was already at 29 on main. It is at 34 here, one of them `BUILT_IN`'s key for the split. No test changes what it asserts; the two files give 56 passed. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… for a branch of the source's own, and an interrupted split are each read by a test, so reverting any of the three changes fails one (#188) Measuring the revert table for 5dbda31 and 837ccf9 found three changes no test read: - `SHAPE_FILE_EXIT`, the exit inside every `adopt-shape-file-missing` finding, now ends "re-run as the end of this report says" instead of "re-run into fresh legs" (5dbda31, F1). The failed-verification e2e reads it in the finding's own line. - `adopt-branch-exists` tells a plan whose `adopt_branch:` names a branch of the source's own to rename it, not delete it (837ccf9, N1). The refusal test reads that sentence. - `_commit_the_split` gives a STOPPED run (Ctrl-C) the notice with no "Once what failed above is fixed" (5dbda31, F2). Only the leg's interrupt was tested. A new test interrupts the mount and reads the notice: both legs pushed, nothing to fix first, the interrupt re-raised. No code changed. tests/test_adopt_late_failure.py: 46 passed. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e-notice It merged with no conflict. Main brings six commits since 54aa69a: - e7d1720, update-shape.py (#208 via #224); - 404760f, scripts/repo_shape.py's double-quoted flow items (#213 via #244), with tests/test_yaml_flow_quotes.py; - 4835fb4, adopt-project.py's plan writer escapes (#199 via #247), with tests/test_adopt_plan_escapes.py; - b538015, scaffold-project.py's leg paths against the shape's own paths (#197 via #234); - 150c41d, update-shape.py's `apply --add` (#202 via #233); - 39ac59c, scaffold-project.py's leg mount `--force` (#229 via #235). Two of them touch adopt-project.py, and no hunk of either meets this branch's: - 4835fb4 changes the plan writer (`y`, `_PLAIN_RE`), adds `_shown` to the plan and check reports, and writes the `--paths-from-file` listing in `_extract_leg` through `write_paths_listing`. This branch changes other lines of `_extract_leg`: it reads the tree digest before the push and pushes through `_push_the_leg`. The signature is the same on both sides, so main's new direct `_extract_leg` test calls it as written. - b538015 moves `_overlap`, `_first_and_more`, `MOVE_THE_LEG` and `CASE_ASIDE` into scripts/shape_materialize.py, and imports the first three back under the same names. This branch adds no use of any of them. A `symtable` check finds no name that the merged adopt-project.py, scaffold-project.py or scripts/shape_materialize.py reads and nothing binds. `git diff -U0` of this merge against 39ac59c equals the branch's against 54aa69a, apart from the hunk headers. On the merged tree, tests/test_adopt_plan_escapes.py, tests/test_yaml_flow_quotes.py, tests/test_scaffold_leg_path_collisions.py and tests/test_adopt_leg_path_collisions.py give 275 passed. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… the end-to-end test reads that the way out it points at ends the report, so tests/test_adopt_ignored_shape_files.py no longer fails 7 tests on this branch (#188) 5dbda31 reworded `SHAPE_FILE_EXIT`. "re-run into fresh legs" became "re-run as the end of this report says", because the re-run alone left out the work directory and the split branch (#188's review, F1). This file's `PLAN_ANSWER` still held the old words, so 7 of its tests failed. 6 of the 7 run in-process, so they failed on all three operating systems (the re-verification's R1). `PLAN_ANSWER` now holds the new words, and its comment says where the re-run is given. The end-to-end test also reads what those words promise: after the finding, the footer says to fix the plan and re-run, and the report ends with the split branch's deletion. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ing to fix first, so a defect, which fails the same way on every re-run, is told to fix what failed first and no longer leads to one more pair of leg repositories per attempt (#188) Four steps run once a leg repository exists: making the code leg's repository, building a leg, making the split and verifying it. What they do not anticipate they re-raise, after the notice. Every such exception got Ctrl-C's notice, "Re-run the adoption with ...", with nothing to fix first. That fits Ctrl-C, which failed at nothing. A defect, such as a `TypeError`, fails the same way on every re-run, so following that notice as written made one more pair of leg repositories per attempt. That is #188's review's F2 again (the re-verification's R2). `_interrupted` decides which is which, and only a `KeyboardInterrupt` counts. The keyword is renamed from `stopped` to `interrupted`, because a defect stops a run too. A defect's notice now opens "Once what failed above is fixed, re-run", as a refusal's does. It is still printed as `STOPPED <step> by <Type>.` and re-raised with its traceback. An interrupt gets the same notice it got before. The new test reads both kinds at each of the four points. `"_tree_of"` is named once, as `TREE_OF`, so that its third use does not trip S1192. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…der such a ruleset, the bypass is the fix the notice below it asks for, so the notice's "Once what failed above is fixed" no longer reads as an invitation to change the ruleset the hint forbids (#188, #177) On a refused leg push, `ADOPT_RULESET_HINT` says the refusal is BY DESIGN and must not be worked around. It names one exit: an operator holding the bypass right re-runs the adoption. The notice printed under it opens, since #188's review's F2, "Once what failed above is fixed, re-run the adoption ...". Read alone, "fixed" invites a change to the ruleset, which the hint forbids. Read with the hint, it means the bypass holder's re-run. Each text pointed at the other ("what the notice below names", "what failed above") and neither said this (the re-verification's R4). The hint now ends: "Under such a ruleset, that bypass is the fix the notice below asks for, never a change to the ruleset." As the hint's last sentence, it sits directly above the notice it explains. The notice itself is unchanged, because it is printed for every late failure, and a ruleset has nothing to do with most of them. Every line of the hint that a test reads is unchanged too. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rce's own clone, which an `org/repo` source makes in `<work-dir>/source-readonly/<repo>`, so a failure before any leg is built still names the work directory that a re-run given it would be refused in (#188) `execute --source org/repo --work-dir W` clones the source into `W/source-readonly/<repo>` before anything else. A re-run given the same W is refused there, because `gh repo clone` will not clone into that directory again (`source-unclonable`). The notice's work-directory sentence read only the two legs and the split off the disk (5dbda31, N2 of #188's review). So a run that failed before it built either named no work directory at all. The re-verification met this case: the code leg's repository failing after the spec leg's was made (#188 N4; the re-verification's R3). 8d4fef4 had named the directory, but did so unconditionally. `Source.open` now records whether it cloned the source. `_cloned_here` counts that clone, while it holds something, first in the sentence's list. Only a clone this run made is counted, because a local path is read in place. The five places that build the notice hand it the source, and `_create_leg_remotes` takes the source as a keyword for this. The clone is not one of `BUILT_IN`: `_refuse_an_occupied_work_dir` runs after the clone is made, and must not refuse a run for its own clone. The tests cover the reviewer's case through `cmd_execute`, with `gh repo clone` replaced by a local clone, and a cloned source at each of the four points that print the notice. R2's four drivers now take the source those points are handed. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…as well as a defect, at each of the four points, so dropping the source from either handler of a leg or the split fails a test (#188) 03b381a hands the source to every place the notice is built. Its four-point test raised only a defect. A leg and the split report a defect in their `except BaseException` handler, so their `except LATE_FAILURES` handlers could lose the source and no test would fail. The test now also raises a refusal: a `PermissionError`, which is one of `LATE_FAILURES`. Whatever a point re-raises, the test lets through. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e-notice It merged with no conflict. Main brings nine commits since 39ac59c: - 06b5204, six tools escape a name their stdout cannot encode (#214 via #246), with tests/test_stdout_encoding.py; - 60d08a1, `shape_materialize.run()` and 26 other git and gh calls read the child's output as UTF-8 (#217 via #230), with tests/test_git_output_decoding.py; - 2727b08, update-shape.py's `gh pr create --base` (#209 via #218), with tests/test_update_shape_pr_base.py; - 4b2add8, scripts/repo_shape.py's flow mappings (#252 via #257), with tests/test_yaml_flow_mappings.py; - aaa40eb, adopt-project.py refuses a `.gitmodules` the history's gitlinks make unreadable (#195 via #226), with tests/test_adopt_history_gitlinks.py; - d89fffa, family.py `bump` to the current pin (#210 via #219); - eb6df75, shape-doctor.py's family.yaml beside a project.yaml (#211 via #220); - c3f4987, scripts/repo_shape.py's block-scalar records (#248 via #265), with tests/test_yaml_block_scalar_keys.py; - 5751460, adopt-project.py spells every path a FINDING or a refusal names (#212 via #223), with tests/test_adopt_finding_spelling.py. Three files changed on both sides: adopt-project.py, tests/test_adopt_ignored_shape_files.py and tests/test_adopt_verify.py. In each, `git diff -U0` of this merge against 5a8b649 equals main's diff against 39ac59c, and this merge against 5751460 equals the branch's diff against 39ac59c, apart from the hunk headers. Neither side's lines are changed. How main's changes meet this branch's failure handling: - Every definition the late failures go through is byte-identical to 5a8b649's: LATE_FAILURES, the notice (`_nothing_rolled_back`), `_failed_loudly`, `_report_late`, the three builders' handlers, `_verify` and `cmd_execute` among them. Three definitions differ, each by main's hunk alone. - `main()` gains `escape_what_stdout_cannot_encode()` as its first statement (06b5204). The branch writes every REFUSED block and notice to stderr, which Python opens with `backslashreplace` whatever the locale. - `run()` names its codec (60d08a1). Output that is not UTF-8 now reaches the branch's handlers as `CommandFailed`, not as `UnicodeDecodeError`. LATE_FAILURES keeps `UnicodeError`, because `git_out` in scripts/repo_shape.py still decodes strictly. - `Source.registered_submodules` reads the history (aaa40eb). Its one caller in `execute` is `_refuse_what_check_finds`, before `_create_leg_remotes`, so the new refusal comes before any leg repository exists and needs no notice. - 5751460 spells the paths of the verification's findings. The branch's footer and notice follow those findings unchanged. No top-level name is bound twice in adopt-project.py, scaffold-project.py, scripts/repo_shape.py or scripts/shape_materialize.py, and a `symtable` check finds no name they read that nothing binds. On the merged tree, ten test files give 439 passed and 1 skipped (Windows' own stdout default): the two shared ones, main's four new ones that read adopt-project.py (finding_spelling, history_gitlinks, stdout_encoding, git_output_decoding), and test_adopt_late_failure, test_adopt_filter_repo_failure, test_adopt_execute_refusals and test_adopt_plan_escapes. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
It merged with no conflict. Main brings five commits since 5751460, and none of them changes a file this branch changes, scripts/repo_shape.py or scripts/shape_materialize.py: - 23e16ae, scaffold-project.py's `--referent-chain` links (with tests/test_scaffold_referent_chain_links.py); - 60588d5, family.py's case-variant members and validate-family.py; - 018ce0d, bump-leg.py rewrites only the workflow files git tracks; - 67a398a, tests/test_scaffold_pin_and_reuse.py's descendant tests; - d94d155, shape-doctor.py's manifest that does not load. shape-doctor.py loads adopt-project.py for its `placement` row (`walk`, `y`, `emit`, and `PathPolicy` from scripts/path_classify.py), and tests/test_shape_doctor.py feeds placement plans through its `Plan` and `_leg_findings`. Each of those is byte-identical to main's on this branch. tests/test_shape_doctor.py and tests/test_scaffold_pin_and_reuse.py are in this branch's targeted set. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…and `adopt-branch-exists` spell every path they name as a finding does, so a work directory, a leg repository or a source named with a line break or an ESC no longer splits or overwrites the line they print on (#188, #212) Main's 5751460 (#212) made `check` and `execute` spell each path a FINDING or a refusal names with `_spelled`: between backticks, with a backslash escape for a backslash, a backtick and any character `str.isprintable` refuses. This branch's own messages still printed their paths raw: - the notice (`_nothing_rolled_back`): the leg repositories, the work directory and what it holds; - the split line `_left_by_the_split` adds: the assembly and the branch; - `adopt-work-dir-occupied`: the work directory and what it holds; - `adopt-branch-exists`: the assembly and the branch. A leg repository under `--local-remote-dir`, the work directory and a local source are paths a person chose. One holding a line feed, U+0085 or U+2028 split the notice's one line, and a carriage return or an ESC overwrote it. Each is now `_spelled`, as a finding spells a path, so a path reads the same in a finding, in the notice and in this branch's two refusals. Main's own `leg-remote-exists` still names its repository raw, as 5751460 left it. The branch deletion under the split line is not spelled. It is a command to paste, and `sh` reads a quoted path (`shlex.quote`), not a spelled one. Tests: - tests/conftest.py gains `spelled()`, the expectation for a printable path: backticks, with a backslash or a backtick escaped. - The assertions in test_adopt_late_failure.py and test_adopt_filter_repo_failure.py that read these paths expect them spelled. `NAMED` reads the legs between the backticks, so the end-to-end test still deletes the two repositories the notice names, word for word. - Three new tests, each over the five characters test_adopt_finding_spelling.py asks: the notice with its split line, `adopt-work-dir-occupied`, and `adopt-branch-exists`. Each is one printable line holding the spelled path, and the deletion stays the command `sh` reads. They use POSIX paths that are never made, so they run on every platform. - `'--delete'` is named once, as `DELETE_FLAG` (S1192). Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
11 of 15 tasks
Contributor
Author
|
(lane openRepoShape-3, 2026-10-12T19:56:17Z) A v1.0.0 scope notice. Brett Heap scoped openRepoShape's first release today: every procedure in AGENTS.md works end to end for ordinary repositories on Linux, macOS and Windows. The tag waits for every in-scope issue to close and a full test run to pass on all three OSes on the release commit. This PR closes #171 (P2, parent #457), #177 (P1, parent #457), #188 (P2, parent #457): in scope, in the |
brettheap
pushed a commit
that referenced
this pull request
Oct 12, 2026
… into fix/adopt-record-submodule-placement Brings in decff87 (shape-doctor.py: a root's own contracts/shape-pin.yaml that does not read as a pin), dba5fb0 (bump-leg.py), 245d775 (validate-pins.py) and 75052cb (#188: every failure once a leg repository exists flushes stdout, then prints `REFUSED <step>.`, what failed and one notice of what the run left), on top of dd8d0ea, which 136a37f had merged. Merged by sha, no rebase. Two conflicts, each kept on both sides: - adopt-project.py, `_commit_the_split`. #188 puts every step after the legs exist inside its late-failure reporting: a `step` name, one `try`, and `_report_late` with `_nothing_rolled_back`'s notice. This branch appends the `submodules:` record to the split's `project.yaml` there. The merge takes #188's function whole and writes the record inside its `try`, under the `committing the split` step, where the materializer is handed what to append. The record's lines are this branch's, rewrapped for the deeper indent, and nothing else in the function differs from main's. A failure while the record is built or after it is appended is then reported as #188 reports any failure in that step. - tests/test_repo_hygiene.py, `test_agents_md_is_short_enough_to_be_read`. #188 moved the AGENTS.md cap from 481 to 482 for its one line in step 7, with a paragraph saying why; this branch had moved it from 481 to 489 for its eight. AGENTS.md merged cleanly at 490 lines, main's 482 plus this branch's lines exactly. The merge keeps #304's and #188's paragraphs word for word, then this branch's, renumbered "2026-10-12: 482 -> 490", and asserts `<= 490`. Step 7, as #188 words it, and this branch's paragraph after it read true together. One more change, which #189's review planned for whichever of #188 and #189 landed second: tests/test_adopt_late_failure.py hands `_commit_the_split` a plain `{}` as its plan, which has no `entries` for the record to be built from, so its commit and push cases stopped at the record with an AttributeError (reported by #188's own handler). Those tests now pass `SplitPlan()`, a dict with `entries = ()`. The sources they adopt keep no submodule, so no record is written and every assertion stands as it was. All 86 of its tests pass. Checked against `git merge-tree --write-tree`, base dd8d0ea: no name bound twice by the merge; each module's names are the base's plus what each side added; every definition is the changing side's text, apart from the three named above; and AGENTS.md differs from main by exactly this branch's lines, and from this branch by exactly main's. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap
pushed a commit
that referenced
this pull request
Oct 12, 2026
…landed: `_commit_the_split` keeps #445's reported steps and reads `follow_ups:` through `_plan_follow_ups` Main moved from dd8d0ea to 580d7df: decff87 (#267), dba5fb0 (#403), 245d775 (#446), 75052cb (#445), ec8e09a (#406) and 580d7df (#416). Only #445 touches adopt-project.py, and it conflicted in one hunk of `_commit_the_split`. #445 wraps that function's body in a `try` that names each step (cloning the source for the split, mounting the legs, committing the split, pushing the split branch), so a failure once both legs are pushed flushes stdout, prints `REFUSED <step>.`, what failed and one notice of what the run left. This branch reads `follow_ups:` there through `_plan_follow_ups`, the one read of it (#293). The resolution keeps #445's `try`, steps and reports whole, and carries this branch's read into its "committing the split" step: git had kept main's `[str(f) for f in plan.get("follow_ups", [])]` inside the `try`. The two contracts meet without overlap. `plan-bad-follow-ups` and `election-date-malformed` are findings of `_entry_findings`, which `execute` refuses through `_refuse_what_check_finds`. That runs before #445's `_refuse_an_occupied_work_dir`, `_refuse_a_split_branch_left_behind` and `_create_leg_remotes`, so these are refusals before any leg exists: exit 2, `REFUSED <code>:` and the remediation that says no leg repository was created and nothing was pushed, as #445's own refusals before the legs say, and no notice. #445's report covers the failures once a leg exists, which these values no longer reach. The guard in `_manifest_values` is unchanged; #445 does not touch it. #445's comment over `_template_values` in `cmd_execute` named "an `elected_on` that is not a date" as a refusal the table can raise. With #294's finding, `_refuse_what_check_finds` refuses such a date first, so the comment now says the date no longer reaches the table. #445's test of a table refusal stubs `_template_values` and is unchanged. tests/test_adopt_execute_refusals.py: the two end-to-end runs through `main` (#293, #294) now also assert that the refusal says no leg was created and that #445's notice ("NOTHING has been rolled back") does not follow it. Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap
pushed a commit
that referenced
this pull request
Oct 12, 2026
…eport of a failure after the legs carries the materializer's refusals, and its two early refusals follow this branch's `core.symlinks` probe origin/main moved dd8d0ea -> 75052cb (four commits), merged by sha: - decff87 (#237 via #267), shape-doctor.py and update-shape.py; - dba5fb0 (#383 via #403), scripts/bump-leg.py and docs/handbook.html; - 245d775 (#412 via #446), the template's scripts/validate-pins.py; - 75052cb (#171, #188 and #177 via #445), adopt-project.py and AGENTS.md: every failure once a leg repository exists is reported at its step as `REFUSED <step>.`, what failed, and `_nothing_rolled_back`'s notice, exit 2; `execute` refuses `adopt-work-dir-occupied` and `adopt-branch-exists` before it creates anything. tests/test_adopt_ignored_shape_files.py and every other file merge with no conflict. adopt-project.py conflicts in five hunks, each resolved by keeping both sides: 1. The materializer's import list: this branch's names (BOOTSTRAP, SYMLINK_HOPS, TOO_MANY_SYMLINKS, template_files), without RULESET_HINT, which #188 replaced by its own ADOPT_RULESET_HINT and nothing here uses. 2-4. `Source`: this branch's `work_root`, where a climb above the root leads (#183), and #188's `cloned`, read by `_cloned_here`. Both are keyword-defaulted, so the four-argument constructions in the tests and #188's `cloned=True` read as before, and `open` hands both on. 5. `_commit_the_split`: main's steps inside #188's report, with this branch's `spell=_spelled`, and its comment, on the materializer call. How #188 meets this branch's code: - `execute`, all before `_create_leg_remotes`: `_refuse_what_check_finds` (with `plan-root-symlink`), this branch's `core.symlinks` probe (`adopt-symlinks-off`), `_refuse_unconsented_seeding`, then #188's `adopt-work-dir-occupied` and `adopt-branch-exists`, then the confirmation and `_template_values`. A plan's own faults come first, then whether `git` here can hold what the plan keeps, then what an earlier run left, whose way out is "run `execute` again". The probe's throwaway repository (`symlinks-probe-*` in the work directory) is removed before `_refuse_an_occupied_work_dir` reads the work directory, and it is none of the three directories that function asks about. - The materializer's refusals (`materialize-outside`, `materialize-symlink-loop`, `materialize-blocked`) are raised inside the step "committing the split", which #188 wraps. They are `Refusal`s, so `_failed_loudly` prints `REFUSED committing the split. <code>: <detail>` on one line, still spelled by `_spelled`, then their remediation, then the notice that both legs are pushed; `execute` exits 2, as it did. - The two end-to-end tests of that path expected the old one-line `REFUSED <code>:`. The next commit makes them expect #188's report. Checked on the merged tree: - AST (fin183g/names.py, base dd8d0ea): no top-level name is bound twice in adopt-project.py, scaffold-project.py or scripts/repo_shape.py, and none is loaded but bound nowhere; scripts/shape_materialize.py binds `YamlError` and `parse_yaml` twice, as main does since 3ef059c. Main changed the signatures of `Source.__init__` (`cloned=False`), `_create_leg_remotes` (`work_root`, `source=None`) and `_verify` (`urls`, `branch`); every call of the last two is main's, and every construction of `Source` passes four arguments or `cloned=` by keyword. - Both sides intact (fin183g/sides_intact.py): the only lines that differ from either side's own change are those of the five hunks. - A rehearsal of this merge (tree 59eca4e, the same as this one), with the next commit's test change, passed tests/test_adopt_root_symlinks.py, tests/test_adopt_late_failure.py, tests/test_adopt_filter_repo_failure.py and tests/test_adopt_ignored_shape_files.py: `231 passed`. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap
pushed a commit
that referenced
this pull request
Oct 12, 2026
Main 75052cb, merged by sha: #188 with #171 and #177 (PR #445), on top of decff87 (#237), dba5fb0 (#383) and 245d775 (#412). Only 75052cb touches this branch's files, in adopt-project.py. It changes neither the reader (scripts/repo_shape.py), scripts/shape_materialize.py nor a YAML template, so the gate asks the same reader of the same lines. One conflict, in adopt-project.py, resolved keeping both sides: - The `from shape_materialize import (...)` list. This branch added PLACEHOLDER_RE, TEMPLATED and render, which `_written` and `_as_they_stand` use. Main dropped RULESET_HINT: 75052cb prints the adoption's own ADOPT_RULESET_HINT instead (#177), and nothing else uses the scaffold's. The list is main's, without RULESET_HINT, plus the three, each name once. The tree is otherwise `git merge-tree`'s. No function is changed on both sides. The top-level names are exactly both sides' (this branch's additions, main's 25, RULESET_HINT and `misread_placeholders` gone), and none is bound twice in adopt-project.py, scaffold-project.py or scripts/repo_shape.py. scripts/shape_materialize.py binds YamlError and parse_yaml twice, as main does. How #188 meets the gate. `_refuse_what_check_finds` is unchanged in place: it still runs before #188's `_refuse_an_occupied_work_dir` and `_refuse_a_split_branch_left_behind`, which run before the template table and the leg repositories. So this branch's `plan-manifest-misread`, and #261's `plan-manifest-invalid`, still refuse before anything exists, whatever the work directory or the source's branches hold. Probed: a plan carrying `id: "true"` or `visibility: "true"`, with a --work-dir that already holds the assembly's directory, or with a split branch in the source that the push cannot fast-forward, is refused under the gate's code, exit 2, with no leg repository made and the source's branches as they were. With a valid plan, the same two states are refused as `adopt-work-dir-occupied` and `adopt-branch-exists`. #188's late-failure reporting covers the steps once a leg repository exists, where this gate never refuses. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap
pushed a commit
that referenced
this pull request
Oct 12, 2026
…king-branch Conflict in adopt-project.py, resolved to main's file byte for byte. #445 gave adoption its own ADOPT_RULESET_HINT, which already names the tracking branch (`{repo} has no {tracking} ...`) and prints no command, so this branch's two adopt-leg edits to RULESET_HINT calls are dropped. The scaffold and family call edits merged cleanly and are unchanged. RULESET_HINT's comment no longer names the adopt legs as callers. tests/test_ruleset_hint_tracking_branch.py drops the adopt push-line cases and pins instead that ADOPT_RULESET_HINT holds no `main` and that both adopt legs print it for the run's tracking branch. Lane: openreposhape-3 (openRepoShape-3) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap
pushed a commit
that referenced
this pull request
Oct 12, 2026
…n left under `execute`'s own `adopt-branch-exists`, and `execute` refuses only a branch in git's way of it, after #445's refusals #445 (75052cb) refuses in `execute` the split branch an earlier run left, as `adopt-branch-exists`, before any leg exists, and only where the push cannot fast-forward it. Its tests pin its order: an occupied work directory first, then that branch, and a live leg repository under `--local-remote-dir` keeps `leg-remote-exists`. This branch's refusal of the same branch came first, in `_refuse_what_check_finds`, and preempted all three: 8 tests of `tests/test_adopt_late_failure.py` failed on 973d0fe. - `check` keeps the finding main lacks. For the split branch itself where the split is pushed it is now `adopt-branch-exists`, the code `execute` refuses it under and `_left_by_the_split` tells the re-run, asked by #445's rule (`_in_the_history_of`), so a branch at the source commit or behind it, which `execute` pushes over and verifies, is no finding. - A branch in git's way of the split branch (`adopt`, `adopt/three-repo-shape/old`), which main still meets only after both legs exist (measured on ec4a7a0: at `git filter-repo`, or at the split push), stays `plan-split-branch-taken` in `check` and in `execute`. - `execute` asks for it in `_refuse_a_branch_in_the_split_branchs_way`, right after `_refuse_a_split_branch_left_behind`, skipped like it while a leg repository is still there under `--local-remote-dir`. `_refuse_what_check_finds` is main's again. - Dropped: `execute`'s own refusal of the split branch itself, and `adopt-split-target-unreadable`, which #445's `git ls-remote` of the same repository now always meets first. Unchanged: #445's functions and `cmd_execute`'s order before the new call are byte-identical to main's; `tests/test_adopt_late_failure.py` and `tests/test_adopt_e2e.py` are main's, unedited. `execute` over a split branch whose commit #226's history scan names is refused for the scan's finding first, as on main; `check` names the branch first. Lane: openreposhape-3 (openRepoShape-3) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 12, 2026
brettheap
added a commit
that referenced
this pull request
Oct 13, 2026
…where it named `main`, as the base of its pull request and as the local ref the scaffold and the family holder pushed, so a `--tracking-branch trunk` run's refused push no longer advises `push -u origin main:seed/scaffold` (a branch its work clone does not have) and `gh pr create --base main` (a base its repository does not have) (#324) (#373) `RULESET_HINT` is what a person reads when an organisation's ruleset has just refused a direct push: seed a branch, open a pull request. It hard-coded `main` as the local branch of `git -C <work> push -u origin main:seed/scaffold` and as `gh pr create --base main`. A run made with `--tracking-branch trunk` makes its work clones with `git init -b trunk` and pushes `trunk`, so the first command died on `error: src refspec main does not match any` and the second named a base the repository does not have. Two fields name a branch, and they are not one value: - `{tracking}` is the remote's branch, the run's `--tracking-branch`, and stands in `--base` only. Every caller passes it. - `{local}` is the ref in `{work}` that the refused push carried, and stands in the push command only. The scaffold's two pushes (`_seed_legs`, `_build_assembly_root`) and the family holder's (`_create_holder_worktree`) push the tracking branch by name from a clone made on it, and pass `args.tracking_branch` for both. - No caller lacked a tracking branch to pass. Each value has been through `checked_value` by then (it admits only `[A-Za-z0-9._/@+~-]` and no leading `-`) and, since #355, `git check-ref-format --branch`, which must echo it as written, so it is printed as it stands, like `{repo}`. An adoption does not print `RULESET_HINT`. Since #445, both adopt legs print `adopt-project.py`'s own `ADOPT_RULESET_HINT`, which already names the run's tracking branch (`<repo> has no <tracking> for that pull request to target`) and prints no command. `adopt-project.py` is left as `main` has it, byte for byte; the test pins #324's rule for it instead. `tests/test_ruleset_hint_tracking_branch.py` runs every caller against local bare remotes whose `pre-receive` hook refuses everything but `seed/*` (a ruleset in miniature, installed through `GIT_TEMPLATE_DIR`), once with `--tracking-branch trunk` and once with `main`: - the scaffold's spec leg, its assembly root and the family holder: it pins the printed block against a literal copy of the hint as it was, asserts that the ref the hint names as local IS the commit the refused push carried, as the remote's hook recorded it, then runs the printed push and sees `seed/scaffold` arrive at that commit; - an adoption's extracted and seeded leg, from a source on `master`: each prints `ADOPT_RULESET_HINT` for that leg's repository and the run's tracking branch, with no `main` for `trunk` and no `master`, and the constant itself holds no `main`; - it scans every shipped `RULESET_HINT.format(` and `ADOPT_RULESET_HINT.format(` for a call that leaves a field out, which would raise `KeyError` inside the failure path that exists to explain a refusal. Unchanged: for the default `main`, the scaffold's and the family holder's hint is byte-identical. The three callers' `main` runs pin it against the literal copy and pass on `main` and on this change alike. What the hint advises is the same, and so are its two exits (seed by pull request, or ask an operator with the bypass right; never `--admin`, per AGENTS.md); only the branches it names move. Left out: - `gh pr create --base <tracking>` cannot run in any of the three places, for `main` as for any other branch: the repository the push was refused at is empty (`gh repo create` is called without a README), so the pull request has no base to land on. That is older than this change and is not touched here. Lane: openreposhape-3 (openRepoShape-3) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap
added a commit
that referenced
this pull request
Oct 13, 2026
…t as `execute`'s `adopt-branch-exists`, and a branch in git's way of it as `plan-split-branch-taken`, which `execute` now refuses before any leg exists, so a source branch `adopt` no longer has both legs made before `git filter-repo` fails (#320) (#382) `execute` pushes its one split commit to `refs/heads/<adopt_branch>` of the repository the split goes to: the source itself under `--local-remote-dir`, the assembly repository on GitHub otherwise. It extracts each leg from a clone of the source with that branch checked out, where `git filter-repo` makes each source branch a branch. Two kinds of branch stop it, and `check` passed both as `plan ok`. Measured on main ec4a7a0 with git 2.43 and `git filter-repo` 2.47.0: - the split branch an earlier run left, at a commit the push cannot fast-forward: `execute` refuses it as `adopt-branch-exists` before any leg exists (#188 N1, #445), and pushes over one at the source commit or behind it, and verifies; - a branch in git's way of the split branch, whose name is a directory of it or lies under it (`adopt`, `adopt/three-repo-shape/old`): a source holding one made both legs (two bare repositories, or two `gh repo create`s) and exited 2 at `git filter-repo`; a plan seeding both legs from such a source, and a GitHub assembly repository holding `adopt`, made both legs and exited 2 at the split push. Now: - `check` reports the split branch itself where the split is pushed as `adopt-branch-exists`, the code `execute` refuses it under and the one `_left_by_the_split` tells the re-run, by that refusal's own rule, so a branch the push fast-forwards is no finding. One situation keeps one code: `check` and `execute` agree about it as they do about every other finding (#168). - A branch in git's way is `plan-split-branch-taken`. `check` reports it for the source, about both runs and said as the extraction's failure (about the push alone when the plan seeds both legs), and for the assembly repository as the source's `origin` last fetched it. - `execute` refuses it in `_refuse_a_branch_in_the_split_branchs_way`, right after `_refuse_a_split_branch_left_behind`: it lists the repository it pushes to with `git ls-remote --heads`, and without `--local-remote-dir` the source's own branches for the extraction. Like #445's refusal it asks nothing while a leg repository is still there under `--local-remote-dir`, which is `leg-remote-exists`'s. - `check` prints one remediation once after these findings, and names them before the entries' findings. Unchanged: every function but `cmd_check` and `cmd_execute` is byte-identical to main's (compared function by function), among them #445's `_refuse_an_occupied_work_dir`, `_refuse_a_split_branch_left_behind`, `_in_the_history_of` and `_left_by_the_split`, and `_refuse_what_check_finds`; so is `cmd_execute` before the one new call. `tests/test_adopt_late_failure.py` and `tests/test_adopt_e2e.py` are main's, unedited. `plan` is untouched. A plan with no branch in the way checks and executes as before. Left out: a case-only twin of the split branch is not compared. Over a split branch whose commit #226's history scan names, `execute` still refuses the scan's finding first, as on main (`_refuse_what_check_finds` runs before `_refuse_a_split_branch_left_behind`); `check` names the branch first, and its remediation says that finding stands until the branch is deleted. A branch the repository hides from `git ls-remote` (`uploadpack.hideRefs`, `transfer.hideRefs`) is not seen, here or by #445's refusal: the push of the split then fails loudly, after the legs. Lane: openreposhape-3 (openRepoShape-3) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap
pushed a commit
that referenced
this pull request
Oct 13, 2026
…pin-file: #409's four files take main's blobs, which are #409's final head's, `scaffold-project.py` keeps the pin code this branch moved out and gains main's own changes, and #445's `try` in `_commit_the_split` carries this branch's `neutral_pins=` argument beside #394's `append` #409 squash-landed as 9736424 (its final head b70a07d). This branch held #409 at 6494714, and after it #409 only merged main five times, none with a hand edit (`git show --remerge-diff` empty for bac7dee, 480c348, efb7322, 5bdcae3 and b70a07d). `git diff b70a07d 9736424 -- <file>` is empty for each file #409 touched: scaffold-project.py, scripts/repo_shape.py, templates/assembly-root/scripts/validate-pins.py and tests/test_pin_source_env_collisions.py. Conflicts, four files, one hunk each: - scaffold-project.py. #409's squash re-adds `PIN_COLLISION_REMEDIATION`, `PIN_VARIABLE_REMEDIATION`, `_refuse_colliding_pins`, `_colliding_pin_variables` and `_pin_collision_remediation`, which this branch moved to scripts/shape_materialize.py (#273). The hunk takes this branch's side, empty, and #409's auto-merged import of `PIN_SOURCE_ENV_PREFIX, pin_source_env_collisions` (and its two comment lines), which nothing in the file uses any more, is dropped. Not the whole file `--ours`: main changed scaffold-project.py beyond #409 since this branch's last merge (6c2a371's `--tracking-branch` check, a73b4e9's `RULESET_HINT`), and those lines stay. The result equals the file `git merge-tree` gives for this branch with main before the squash (518010b), blob fbe7215, and its diff from this branch's tested file has the patch-id of main's own 3ef059c..518010b change to the file. - templates/assembly-root/scripts/validate-pins.py. Main's side, which is #409's final blob (5f83eb7) with main's #359 workflow reader. This branch had no change of its own to the file beyond #409 at 6494714. - adopt-project.py, `_commit_the_split`. #445 wraps the function in its late-failure `try` with `step` names, and #394 builds an `append` with the plan's submodule record. This branch's only edits there were a comment and `neutral_pins=_recorded_pins(plan)` on the materializer call. Resolved with main's side, and the comment and the argument on main's call inside the `try`: `append=append, neutral_pins=_recorded_pins(plan)`. - docs/cli.md, generated: regenerated with scripts/render-cli-reference.py over the resolved tree (`--check` ok). It differs from main's only in `adopt-project.py plan`'s `--pin`, `--pin-owner` and `--pin-source` help. scripts/repo_shape.py and tests/test_pin_source_env_collisions.py auto-merged to main's blobs (82346be, 5220787). pyflakes reports the same four warnings on the merged adopt-project.py, scaffold-project.py and scripts/shape_materialize.py as before the merge. #445's tests/test_adopt_late_failure.py hands `_commit_the_split` a `SplitPlan`, a `dict` that adds `entries`, as its plan, which `_read_pins` cannot read yet; the next commit makes it read one. Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap
pushed a commit
that referenced
this pull request
Oct 13, 2026
…mmit_the_split` takes the `SplitPlan` mapping #445's tests hand it as the plan and no longer stops at "committing the split" with an AttributeError, and a loaded plan reads exactly as before (#273) #445 (75052cb) tests `_commit_the_split` directly. In tests/test_adopt_late_failure.py, `split_arguments` passes `plan=SplitPlan()`, a `dict` subclass that adds `entries` (#394). Those are all the function read before this branch: `plan.get("follow_ups", [])` and `plan.entries`. Since #273 it also writes each declared pin, `neutral_pins=_recorded_pins(plan)`, and `_read_pins` read `plan.data.get("pins")`. On the merge commit before this one, three of #445's tests failed (3 failed, 83 passed in that file), each with "STOPPED committing the split by AttributeError: 'SplitPlan' object has no attribute 'data'": - test_a_split_that_fails_names_its_step_and_both_pushed_legs[commit] - test_a_split_that_fails_names_its_step_and_both_pushed_legs[push] - test_a_split_the_materializer_cannot_write_is_refused_with_the_notice `Plan.get(key, default=None)` returns `default` when the value is None, and the value otherwise. With no default it returns `plan.data.get("pins")` exactly, so a loaded plan's `pins:` reads as before, absent and `null` alike, and the `rows is None` branch below is unchanged. A mapping has the same `get`, so the test's plan reads too. This was the only `plan.data` in the file. Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap
added a commit
that referenced
this pull request
Oct 13, 2026
… exists, a plan `follow_ups:` that is not a list of text and an `elected_on:` that `election_date` does not read, with or without a `reference:`, so `follow_ups: 5` is no longer a traceback and `elected_on: someday` is no longer refused under a flag neither command takes (#293, #294) (#356) `check` did not ask about a hand-edited plan's `follow_ups:` or `elected_on:`. `follow_ups: 5` ended `check` and `execute` in a `TypeError` traceback, and `follow_ups: abc` passed both, `execute` writing the follow-ups `a`, `b` and `c`. A date `election_date` refuses was refused only on its way to the manifest's reference: with no `reference:`, `check` stopped at exit 2 on `REFUSED election-date-malformed` naming `--elected-on`, a flag neither command takes, and said to re-run with it. Before #314 that refusal came after both legs were created, and a plan that kept its `reference:` adopted with the word in its manifest. `_plan_follow_ups` is now the one read of `follow_ups:` for `_plan_values`, `_print_what_will_happen` and `_commit_the_split`, where it is read inside #445's `committing the split` step. `_follow_ups_findings` reports `plan-bad-follow-ups` for a value that is not a list, saying what it holds, and once for each item that is not text. An empty list and a bare key stay valid. `_elected_on_findings` asks `election_date`, the rule `plan` asks of `--elected-on`, about `str` of the plan's value, with or without a `reference:`, so `plan`, `check` and `execute` cannot disagree about one date. A date it refuses is a FINDING `election-date-malformed` in the plan key's own words, and the remediation says to edit `elected_on:` in the plan file. Only an absent or null `elected_on:` is "no date": `""`, `false`, `0`, `[]` and `{}` are findings. Any other refusal of `election_date` is raised as it is. Both findings come last in `_entry_findings`, through `_plan_field_findings`, the date first as `plan` writes the keys, so `execute` refuses them through `_refuse_what_check_finds`, before #445's checks of what an earlier run left and before any leg exists. The refusal says no leg repository was created, and no notice of what a run left follows it. `_manifest_values` returns None for a plan whose `elected_on:` is such a finding, so #314's validator is not asked about a manifest whose reference that date chooses: the date is one finding, in this rule's words, and not `plan-manifest-invalid` beside it or a `REFUSED` raised from `default_reference` before the list is printed. It refuses the plan's `org:` and reads its `id:` before that guard, so a bad date beside a bad organisation or id does not make `check` and `execute` name different problems first. #314's rows for `"2026-W41-6"` and `"20261011"` in `tests/test_adopt_manifest_values.py` now expect `election-date-malformed`. `tests/test_adopt_execute_refusals.py` gains 54 tests: `check` and `execute` for each bad value, controls, a stricter `election_date` stood in by monkeypatch, both findings in one plan, a bad date beside a bad `org:` or `id:`, and an end-to-end run per issue that refuses with no leg and no notice, then adopts from the corrected plan in the same work directory. Closes #293 Closes #294 Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap
added a commit
that referenced
this pull request
Oct 13, 2026
… writes its pin file as the scaffold does, so an adopted root passes `validate-pins.py` (#273); a `--pin-source` that cannot answer is `pin-source-unreadable` (#339) (#508) **Brought up to date after #409 squash-landed** by merging origin/main (9736424, #409's squash) as f9ed46f. Its `scaffold-project.py` conflict was resolved to this branch's side once `git diff b70a07d 9736424 -- scaffold-project.py` (#409's final head against its squash) came out empty. So the file is the one tested on this branch plus main's own later changes to it, and holds nothing of #409's; details below. This branch carries #275's adopt half. It was built on `fix/pin-source-env-collisions`, PR #409 (#275's scaffold half and its fix round), merged at 6494714 as 583f647, over the earlier 5690235 (34dbd86). After 6494714, #409 only merged main, five times, up to its final head b70a07d, and none of those merges has a hand edit (`git show --remerge-diff` is empty for each). Adopt's refusal of two products that share a source variable goes through #409's function, so this branch lands after #409. It was never rebased. `git diff b70a07d 9736424` is empty for each file #409 touched: `scaffold-project.py`, `scripts/repo_shape.py`, `templates/assembly-root/scripts/validate-pins.py` and `tests/test_pin_source_env_collisions.py`. What f9ed46f resolves, four files with one hunk each: - **`scaffold-project.py`.** - The conflict hunk is main's block from `PIN_COLLISION_REMEDIATION` through `_pin_collision_remediation`, which #409's squash edits: it adds `PIN_VARIABLE_REMEDIATION`, `_colliding_pin_variables` and `_pin_collision_remediation` and changes `_refuse_colliding_pins`. This branch moved all of that to `scripts/shape_materialize.py`. The hunk takes this branch's side. (The merge commit's message says "re-adds"; this is the exact account.) - #409's auto-merged import of `PIN_SOURCE_ENV_PREFIX, pin_source_env_collisions` is dropped, because nothing in the file uses either name any more. - It is not `git checkout --ours` of the whole file. Main changed the file beyond #409 since this branch last merged it: #355's `--tracking-branch` check (6c2a371) and a73b4e9's `RULESET_HINT`. Those lines stay. - The result is the file `git merge-tree` gives for this branch with main before the squash (518010b). Its diff from the file tested here has the same patch-id as main's own 3ef059c..518010b change to it. - **`templates/assembly-root/scripts/validate-pins.py`.** Main's side, which is #409's final blob, with main's #359 workflow reader in it. This branch had no change of its own to the file beyond #409. - **`adopt-project.py`, `_commit_the_split`.** - Main: #445 wraps the function in its late-failure `try`, and #394 builds an `append` with the plan's submodule record. - This branch: its only edits there were a comment and `neutral_pins=_recorded_pins(plan)` on the materializer call. - Both now sit on main's call inside the `try`: `append=append, neutral_pins=_recorded_pins(plan)`. - **`docs/cli.md`.** It is regenerated with `scripts/render-cli-reference.py`, and `--check` is ok. It differs from main's only in the `--pin`, `--pin-owner` and `--pin-source` help of `adopt-project.py plan`. `scripts/repo_shape.py`, `templates/assembly-root/scripts/validate-pins.py` and `tests/test_pin_source_env_collisions.py` are main's blobs, which are #409's: this branch adds nothing to them. Nothing of #409 is lost. Its scaffold behaviour is `shape_materialize.refuse_colliding_pins` and `pin_variable_collisions`, keyed by #409's `pin_product_key`. **73d65c6, after the merge.** - **The change.** `_read_pins` reads `plan.get("pins")` where it read `plan.data.get("pins")`. - **Why.** #445's `tests/test_adopt_late_failure.py` hands `_commit_the_split` a `SplitPlan` as its plan, a `dict` that adds `entries`. On f9ed46f, three of its tests stopped at "committing the split" with `AttributeError: 'SplitPlan' object has no attribute 'data'` (3 failed, 83 passed in that file). - **Nothing else changes.** `Plan.get` with no default returns `plan.data.get(key)` exactly, so a loaded plan reads as before. `adopt-project.py plan --pin` took a NAME only (`--pin openGlass`) and refused a trailing `@<commit>`. `plan`, `check` and `execute` each exited 0, and `execute` printed `adoption verified`. The manifest then listed the name in `neutral_product_pins:` with no `contracts/<product>-pin.yaml` beside it, and on its first run the adopted root's own `validate-pins.py` refused it as `neutral-pin-missing`. #273's table, reproduced on c7c02fa with the issue's own commands (local bare remotes, and a stub `gh` that exits 97): | `--pin` | c7c02fa | this branch | |---|---|---| | `openGlass` | `execute` exit 0, `- openGlass`; clone's `validate-pins.py` exit 2 `neutral-pin-missing` | `plan` exit 2 `pin-malformed`, with the scaffold's remediation (`--pin openGlass@<40 hex>`); no plan is written | | `opensoft/openGlass` | exit 0, `- opensoft/openGlass`; exit 2 `neutral-pin-missing` for `contracts/opensoft/openglass-pin.yaml` | `plan` exit 2 `pin-malformed` | | `spec` | exit 0; `validate-pins.py` exit 0, reading the leg's `contracts/spec-pin.yaml` as the neutral pin | `spec@<sha>` is `adopt-pin-collides` at `plan` | | `shape` | exit 0; `contracts/shape-pin.yaml` read as the neutral pin | `shape@<sha>` is `adopt-pin-collides` at `plan` | | `openGlass@<sha>`, `opensoft/openGlass@<sha>` | `plan` exit 2 `adopt-pin-malformed` | `plan`, `check` and `execute` exit 0, and `gh` is called 0 times. `contracts/openglass-pin.yaml` is written and the manifest lists `- openGlass`. In the clone, `validate-manifest.py` exits 0, and `validate-pins.py` exits 0 with `openGlass: pin commit … digest recomputes` | Brett ruled for decision 1's first option ("go with your recommendation on #273"): adopt takes the scaffold's pin and writes the scaffold's pin file. That reverses the NAME-only `--pin` from #6, and the comment that described it is replaced with the new contract. - **One implementation.** The scaffold's pin code moved into `scripts/shape_materialize.py`, next to `neutral_pin_path` and the materializer that writes the file. What moved: - `PIN_ARG_RE`, `parse_pin`, `pin_digest_from_source` and `pin_digest_from_gh`; - `declared_pin_values`, `declared_pin_products` and `PIN_COLLISION_REMEDIATION` (in #309's wording); - from #275's branch, `PIN_VARIABLE_REMEDIATION` and the variable check; - `_refuse_colliding_pins`, split into the pure `pin_collisions` and `pin_variable_collisions`, and `refuse_colliding_pins(refusal=...)`, which raises one refusal for both. The code did not move verbatim. `_refuse_colliding_pins` became the three functions above; `pin_digest_from_source` was rewritten for #339 (below); and fix round 1 changed three of them (below): `pin_variable_collisions` keys each pin by #275's `pin_product_key`, `pin_digest_from_source` spells a path that is not printable, and `pin_digest_from_gh` refuses a missing `gh`. The scaffold's behaviour is otherwise unchanged. `scaffold-project.py` imports what it calls under its old names. Lane 5's review ran 34 scaffold cases (every pin refusal, the `gh` stub's four modes, dry and real successes) on 34dbd86 and on this branch at 7a94b6b. rc, stdout, stderr, the `gh` log and the files made were identical, apart from the shape's own commit and digest and #339's row. Its pin tests and #275's pass as they are. `tests/test_git_output_decoding.py`'s exemption for `pin_digest_from_gh` follows the function to its new file. - **`plan`.** `--pin`, `--pin-owner` and `--pin-source` mean what they mean for the scaffold. `plan` reads the digest from `--pin-source` or `gh api`, and records product, repository, commit, digest and `digest_source` in the plan, so `check` and `execute` read no network for a pin. `plan` refuses: - a name with no `@<commit>`, as `pin-malformed`. That includes an empty `--pin ''`, which adopt used to drop and now refuses, as the scaffold always has; - `spec`, `code` and `shape`, in any case, as `adopt-pin-collides` (#273 decision 4), in the scaffold's sentence and remediation (#241, #309); - one product given twice, the same way, whether it is a repeat or two names that share one file (decision 5); - two products that one `SHAPE_PIN_SOURCE_<PRODUCT>` names, such as `open-glass` and `open_glass`, the same way, through #275's check; - a leg path over a pin's file, as `adopt-leg-path-collides`; - a `--pin-source` path that `check` would refuse in `digest_source:` (fix round 1), as `adopt-unwritable-value`. It is asked only when a `--pin` records the path; - with no `gh` on PATH and no `--pin-source`, `gh-not-found` (fix round 1). - **`check` and `execute`.** These are new `check` findings, and `execute` refuses each one before any leg exists. They come from `_leg_findings`, the list `check` and `_refuse_what_check_finds` share: - `plan-pin-unrecorded`: a name alone (an old plan's `pins: [openGlass]`, or a hand edit), or an entry missing its commit or digest; - `plan-pin-malformed`; - `plan-pin-collides`, which covers two pins at one file, a pin at a file the shape writes, and two products that share one source variable; - `plan-root-holds-pin`: a source path kept in the root at the pin's file. The materializer would write the shape's pin beside it under `shape/`, and `validate-pins.py` would read the source's file instead. How each finding ends: - `plan-pin-unrecorded` and `plan-pin-malformed` end with the `plan --pin <product>@<40 hex commit>` that records the pin whole. - `plan-pin-collides` ends with re-running `plan` with each product once, or, for two products that share a source variable, with one product of each such group. - `plan-root-holds-pin` ends with the kept path's own remedy: send that entry to a leg, `drop` it, or rename it in the source and re-run `plan`. A leg moved over a pin's file in the plan is `plan-leg-path-collides`. - **Also part of #386: its `pins:` row.** A hand-edited `pins:` that is not a list (a name such as `pins: openGlass`, a number, a mapping, a quoted string) is now `plan-pin-malformed` in `check` (`` `pins:` is 'openGlass', not a list of pins ``), and `execute` refuses it before any leg exists. - Where: `_read_pins` in `adopt-project.py`; the test row is `plan-pin-malformed-not-a-list` in `tests/test_adopt_pins.py`. - Before: on main, `check` said `plan ok` for the name, the mapping and the string, and gave a traceback for the number (reproduced by lane 5's review on cb7063f). #386 records that `execute` then wrote the name as eight one-letter pins. - Not fixed here: #386's other keys, `allow_empty_legs:`, `paths:`, `legs:` and `source:`, so #386 stays open. Lane 2's #435 takes all of #386, `pins:` included, and its `pins:` rule keeps only text items where this branch writes mappings; whichever lands second reconciles them (#435 (comment), and "Where the meaning meets" below). - **`execute`.** It writes each pin through `materialize_assembly_root(neutral_pins=...)`. The manifest lists the product with the owner stripped (decision 3), so the entry and the file agree. - **Docs.** `--help` is updated and `docs/cli.md` regenerated (`render-cli-reference.py --check` ok). `pin_digest_from_source` caught only `Refusal`. A directory that is no clone is git's own failure, so it was always `pin-source-unreadable`, exit 2. A path `subprocess.run` cannot enter never reaches git: it is a `FileNotFoundError`, `NotADirectoryError` or `PermissionError` out of `cwd=`, and nothing caught it. `scaffold-project.py --pin ... --pin-source <that>` ended `--dry-run` and the real run alike in a traceback, exit 1. Since the move above, `adopt-project.py plan --pin` reads its digest through the same function and ended the same way. Both reach it through `declared_pin_values`, and `check` and `execute` read no source, so that call is the only reader. The issue's table, run on ab623dd for the scaffold and on d806e8f for `adopt-project.py plan` (adopt has no `--pin-source` on ab623dd), then on this head. `gh` is a stub that logs and exits 97, with `GH_TOKEN` bogus and an empty `GH_CONFIG_DIR`; it was called 0 times in every run: | `--pin-source` | before | this branch | |---|---|---| | a directory that is not a clone | `pin-source-unreadable`, exit 2 | unchanged | | a path that does not exist | `FileNotFoundError` traceback, exit 1 | `pin-source-unreadable`, exit 2: `<path> could not answer for opensoft/openGlass @ <40 hex>: no such directory` | | a file | `NotADirectoryError`, exit 1 | exit 2, `... it is a file, not a directory` | | under a parent this user cannot enter | `PermissionError`, exit 1 | exit 2, `... Permission denied` | | a symlink loop (not in the issue) | `RuntimeError` out of `Path.resolve()` on 3.12, exit 1 | exit 2, `... Too many levels of symbolic links` | The real run and `--dry-run` give the same stdout and stderr, and nothing exists afterwards (no remote directory, no work directory; for adopt, no plan file). What changed, in `scripts/shape_materialize.py` only: - `pin_digest_from_source` catches `OSError` as well as `Refusal` and raises the existing `pin-source-unreadable` with its remediation unchanged. The message names the path and the reason. - `stat()` judges the source inside the guard, as #312 does in `validate-pins.py`, and the sentences are that file's: a missing path, or one beneath a file, is "no such directory"; a file is "it is a file, not a directory"; any other `OSError`, from `stat()` or from git's `cwd=`, is the system's own reason (`strerror`). It is not `exists()`/`is_dir()`, which before 3.14 swallow only a missing path. A source `stat()` can read but `cwd=` cannot enter, a directory with no search bit, fails in `tree_digest` and the same guard answers it. - `declared_pin_values` resolves the path through a guarded `_resolved_pin_source`. `Path.resolve()` raises a `RuntimeError` on a symlink loop before 3.13 (an `OSError` since), before the digest was asked for. As typed, `stat()` meets the loop and says so. - A source that answers is read exactly as before: a relative path resolves against the working directory, a symlink to a clone is followed, `digest_source` reads `local-clone (<path as typed>)`, and the digest is `tree_digest`'s. `tests/test_pin_source_unreadable.py` (46 tests in the #339 commit; fix round 1 adds 17 for S1 and moves one, below) runs each case through `scaffold-project.py --dry-run`, its real run and `adopt-project.py plan`: missing, a file, beneath a file, relative and missing, a parent and a source with no search bit (skipped on Windows and as root, by a mark, so before any fixture builds a directory), a 300-character name, a symlink loop (skipped on Windows) and a directory that is no clone. Each asserts exit 2, the refusal naming the path and the reason, its remediation, no traceback and nothing created. It also holds `--dry-run` and the real run to the same stdout and stderr, checks the path is printed escaped under an ASCII stderr and as a surrogate when it is not UTF-8, and checks that a relative source and a symlink to a clone read as before. The session `gh` guard fails any test whose run called `gh`. origin/main ab623dd reached this branch through implQ's branch: 34dbd86 is a30091f merged with ab623dd. One file conflicted there, `templates/assembly-root/scripts/validate-pins.py`, in seven hunks, because #312 (8cdca5e) replaced `keyed_sources` and `default_source` with `PinSources` and #275 added `env_shared` and `shared`. #312's blob is the same at its PR head, at the squash and at ab623dd, so it landed the file implQ resolved against. The resolution keeps #312's side in all seven and adds #275's on top: | # | hunk | resolution | |---|---|---| | 1 | `resolve_neutral_pin_source` signature | #312's `sources: PinSources` and 3-tuple return, plus #275's `env_shared: bool = False` | | 2 | its docstring tail | #312's `Returns (path, how, typed)` text, plus #275's "Rule 2 is skipped when `env_shared`" paragraph | | 3 | `_recompute_neutral_pin_digest` signature | #312's `sources`, plus #275's `shared: frozenset[str]` after `report` | | 4 | the `resolve_neutral_pin_source(...)` call inside it | #312's 3-tuple unpack, plus `env_shared = product.casefold() in shared` and the extra argument | | 5 | `_check_one_neutral_product_pin` signature | #312's `sources`, plus #275's `shared` | | 6 | its call of `_recompute_neutral_pin_digest` | `sources, report, shared` | | 7 | `_check_neutral_product_pins` docstring and body | #312's `PinSources(...)` and `_note_unused_pin_sources`, plus #275's paragraph, `_report_pin_source_collisions(products, report)` and `shared = frozenset(pin_source_env_shared(products))` | This branch's own merge of 34dbd86 (5690235) had no conflict, and neither had its merge of #409's fix round, 6494714 (583f647). That fix round made `neutral-pin-source-collides` a note and keyed "one product" by `repo_shape.pin_product_key`. At 421e4e5, `templates/assembly-root/scripts/validate-pins.py`, `scripts/repo_shape.py` and `tests/test_pin_source_env_collisions.py` were byte-identical to the tree `git merge-tree` gives for 6494714 with origin/main 3ef059c (blobs 5604007, 091966b, 5220787), so this branch added nothing to those three files beyond #409. The 34dbd86 merge brought in #335, #312, #317, #270 (f795cf7), #344 and #349 with no conflict. Earlier merges of origin/main (87373a6, for #309's reworded constant, and e43fa95) are unchanged. Fix round 1 merged origin/main three more times: - **cd1fc79 (2bcfa69).** It brought #245, #351, #350, #345, #372, #371, #354, #380, #377 and the rest since ab623dd, with no conflict. - **45e01f4 (4098b61).** It brought #396's `tests/test_agents_md_codes.py`. That test finds `scaffold-pin-collides` as a string constant of `scripts/shape_materialize.py`, which `SHIPPED` lists. - **3ef059c (421e4e5), #314.** One conflict, one hunk in `adopt-project.py`. This branch's `_refuse_an_unrecordable_pin_source` and #314's section "The project must be able to hold what `execute` writes into it (#261)" were added at the same place; both are kept. Where the two meet, lane 5's earlier recipe applies: - `_flag_plan` builds each pin as the `PIN_FIELDS` mapping `plan` writes, not `list(pins)` of products. So the plan `plan` asks the validator about is the plan it writes. - #314's `test_plan_asks_what_check_asks_of_the_same_keys` plans `--pin openGlass@<commit> --pin-source <stand-in>` where it planned the name alone, which `plan` now refuses. Then f9ed46f, after #409 squash-landed, and 73d65c6 after it (top of this description). On the merged tree exactly one test failed: `tests/test_adopt_template_values.py::test_check_finds_and_execute_refuses_a_hand_edited_value[pins-surrogate]`. It plans, then hand-edits the plan to `pins: ["openX\ud800"]`, a name-only pin with a lone surrogate. Before this branch the refusal's headline was `REFUSED plan-unwritable-value`. A name-only pin in a plan is now `plan-pin-unrecorded` (#273), so `check` has a second, independent finding for that very plan, and the headline is the first finding's: ``` REFUSED plan-pin-unrecorded: 2 finding(s) ... FINDING plan-pin-unrecorded: `pins:` entry 1, 'openX\ud800', is a name with no commit and no digest ... FINDING plan-unwritable-value: a `pins:` item is 'openX\ud800', which `execute` writes into the assembly root's `project.yaml`, and it holds a lone surrogate ... ``` Nothing about #270's contract changed. The lone surrogate is still found as `plan-unwritable-value`, listed under the refusal, and `execute` still refuses before any leg exists. `check` builds `_leg_findings`, where `_pin_findings` sits, before it calls `_value_findings`, so the pin finding comes first. d806e8f changes one assertion for that row, `pins` only: the headline is `REFUSED plan-pin-unrecorded: ` where it was `PLAN_REFUSED`. The other 15 rows and the finding assertions, including the `plan-unwritable-value` line, are untouched. Lane 5's review of 7a94b6b found 3 should-fix and 5 nits. Each is answered here: - **#409's `pin_product_key` (review N1).** The merge of #409's fix round could not carry it, because this branch had moved the function that used it out of `scaffold-project.py`. `pin_variable_collisions` now keys each pin by that function, where it casefolded. `openß` and `openss` are two pin files and one `SHAPE_PIN_SOURCE_OPENSS`, and a casefold named each of the two pins twice. No `--pin` or recorded `pins:` entry `PIN_ARG_RE` admits can spell either, so the test asks the function directly. `refuse_colliding_pins`' docstring now says, in #409's words, that the root's `validate-pins.py` only notes such a pair. - **S1, a `--pin-source` path that is not printable.** `pin_digest_from_source` names the path as typed unless it is not printable (`str.isprintable`), and then as Python's `repr` spells it (`shown_source`). That covers the refusal's head and git's own reason, which names the directory it ran in. Printed raw, an ESC reached the terminal, and a line break or a carriage return began a line of the path's choosing, a forged `REFUSED` line among them, from the scaffold and from adopt. The traceback #339 replaced had shown a `repr`. A printable path is named byte for byte as before. - **S2, no `gh` on PATH.** `pin_digest_from_gh` refuses as `gh-not-found`, exit 2, as `repo_shape.tree_digest_from_gh` names it, and offers `--pin-source`. It was a `FileNotFoundError` traceback, exit 1, from the scaffold and, new with this branch, from `adopt-project.py plan --pin`. - **S3, a hand-edited `commit:` or `tree_sha256:`.** The lane's decision is to accept it and document it (option (c)); no behaviour changed. A pair that disagrees passes `check` and `execute` (`adoption verified`), because neither reads a network for a pin, by design. The adopted root's own `validate-pins.py` then exits 1 with `neutral-pin-digest-mismatch` wherever it can read the product. `check` now says so once, after the pins it will write: "each pin as `plan` read it, not read again here: the root's own `validate-pins.py` recomputes its digest wherever it can read the product". Options (a), `execute` recomputes before it creates the legs, and (b), `check` recomputes from a clone still on the machine, are filed as #449. - **N2, a `--pin-source` that `plan` cannot record.** `plan` asks `check`'s question (`one_printable_line`) of the path it records in each pin's `digest_source:`, before any digest is read. It refuses one that fails as `adopt-unwritable-value`, in #270's words (`unwritable_reason`), or as "an invisible formatting character" where those name nothing. Before, it was planned, exit 0, and printed raw in the report. `check` then refused it as `plan-pin-malformed` and said to re-run the same `plan`. The report's `pin` line and `check`'s `write` line now spell the digest's source with `_shown`. The scaffold half is pre-existing (a line break in the path is written into the pin file; a byte that is not UTF-8 dies after all three repositories exist) and is filed as #450. - **N3, six hunks no test guarded.** Each now has a test that fails when it alone is reverted: - the two name guards (a `pins:` name holding an ESC and a forged line); - the lower-casing of the commit and the digest (an upper-cased hand edit); - the `digest_source` printability check; - the not-text check (`commit: 1234567` unquoted); - the gitlink filter (a submodule kept where the pin is written is `plan-submodule-root` alone). - **N4.** This body. - **N5, #339's third decision.** A `--pin-source` that nothing reads is still silent, now in adopt too. This PR does not decide it. #451, filed, lays out refuse, note and leave, so closing #339 loses nothing. Lane 5's second review, of 421e4e5, found no blocking item, 2 should-fix items and 3 nits. The two should-fix items are this body: the landing after #409 (above) and #386 (above). The code needed no change for them. f63d1ed adds two tests and no code: - **n1. `plan` asks nothing of a `--pin-source` no `--pin` records.** With the `args.pin and` guard dropped, such a path holding an ESC or a zero-width joiner was refused in a sentence naming no file ("`execute` writes into , and ..."), and every test still passed. `test_a_pin_source_no_pin_records_is_not_asked_about` holds the plan to exit 0 and `pins: []`, with the path named nowhere in the plan or the report. The ESC row is POSIX-only by the file's `POSIX_NAMES` mark; the zero-width joiner row runs everywhere. It holds #451's answer today, silence, so a decision there changes this test on purpose. - **n2. A printable `--pin-source` is printed as it is.** With `_spelled` in place of `_shown` in `plan`'s `pin` line, every test still passed. `test_a_printable_pin_source_is_printed_as_it_is` holds `plan`'s `pin` line, and `check`'s `write` line for the plan `plan` wrote, to their exact text, ending `(local-clone (<path>))` with no backtick. - **n3** needs a code change and is left out (below). **Fix round 2, on f63d1ed.** Each run had a `gh` stub first on PATH (it logs and exits 97), `GH_TOKEN` bogus, an empty `GH_CONFIG_DIR`, `git-filter-repo` 2.47.0 next on PATH, and the identity blanked with `GIT_CONFIG_COUNT`; the session `gh` guard came first inside each run. - **The pin set: 812 passed, 2 skipped.** It covers adopt pins, adopt plan, #339's file, #275's tests, the pin and neutral-pin validators, git output decoding, adopt template values, CLI reference, scaffold pin and reuse, scaffold pin collisions, #314's two files and #396's file. The skips are `test_adopt_plan.py:403` (no MedxEHR clone) and `test_adopt_template_values.py:223` (this host reads `git config user.name` from the repository, not from `GIT_CONFIG_GLOBAL`). That is fix round 1's 809 passed, plus the three new rows. - **Shape doctor and scaffold leg-path collisions: 369 passed, 3 skipped** (the PowerShell line exists only on a Windows host). - **Mutation probes**, each one line changed in a throwaway worktree, over `tests/test_adopt_pins.py` and `tests/test_pin_source_unreadable.py` less the five end-to-end adoptions: - the `args.pin and` guard dropped: 2 failed, both n1 rows; - `plan`'s `pin` line `_spelled`: 1 failed, the n2 test; - `check`'s `write` line `_spelled`: 2 failed, the n2 test and `test_an_upper_cased_hand_edit_is_written_lower_cased`; - `plan`'s `pin` line raw: 1 failed, the no-break-space test that already guarded that hunk. **On the merged tree, 73d65c6** (the merge f9ed46f and the `_read_pins` commit), with the same harness: - **The pin set** above, with #445's `test_adopt_late_failure.py`, `test_adopt_git_reads.py` and `test_adopt_filter_repo_failure.py` and #387's `test_adopt_empty_symlinks.py` and `test_adopt_root_manifest.py`: **988 passed, 2 skipped**. The skips are the two above. - **Shape doctor, scaffold leg-path collisions and the adopt suites:** **967 passed, 3 skipped** (the PowerShell line). The adopt suites are e2e, execute refusals, verify, root shape paths (#387's too), leg-path collisions, clone template, ignored shape files and submodules. - **On the merge alone, f9ed46f** (the same tree as before the `_read_pins` commit): `tests/test_adopt_late_failure.py` gave 3 failed, 83 passed, the three `AttributeError` rows above. - `render-cli-reference.py --check` is ok on the merged tree. pyflakes reports the same four warnings on `adopt-project.py`, `scaffold-project.py` and `scripts/shape_materialize.py` as before the merge. **Fix round 1, on 421e4e5.** Each run had `git-filter-repo` 2.47.0 on PATH and the identity blanked with `GIT_CONFIG_COUNT`. A `gh` stub that logs and exits 97 was first on PATH, with `GH_TOKEN` bogus and an empty `GH_CONFIG_DIR`, and the session `gh` guard came first inside each run. - **The pin set:** 809 passed, 2 skipped (the same files and skips as above). - **Doctor and scaffold:** 782 passed, 3 skipped (the PowerShell line exists only on a Windows host). It covers shape doctor, scaffold leg-path collisions, scaffold e2e, referent chain links, naming policy, repo hygiene and #351's free-text values. - **The adopt suites:** 882 passed. It covers e2e, execute refusals, verify, spec-only, leg paths, root shape paths, plan escapes, leg-path collisions, clone template and submodules. - **Before the #314 merge,** on 74ce4d9's tree: the first two sets gave 577 passed, 2 skipped and 782 passed, 3 skipped. On 2bcfa69's tree (the merges alone, before the fix) they gave 542 passed, 2 skipped and 782 passed, 3 skipped. After the 45e01f4 merge, `tests/test_agents_md_codes.py`, repo hygiene and CLI reference: 110 passed. - **Lane 5's second review** also ran 421e4e5 merged with main cb7063f and with 53bee5b: the pin set, the manifest-value set, and the doctor set with #357's `tests/test_doctor_validator_rows.py`. All passed, with only the skips above. Proof that fix round 1's tests test something: - **On 2bcfa69's tree,** with this head's two test files laid over it, 31 of the new rows fail: - S1: 17; - N2 and its report spelling: 7; - S2: 3; - the `pin_product_key` port: 2; - `check`'s S3 line: 1; - the not-UTF-8 `adopt plan` row: 1. The N3 rows pass there, as they must: they guard hunks this branch already had. - **Mutation probes.** Each hunk was reverted alone, in a throwaway worktree, and both test files were run: 18 of 18 were caught. - **The port:** the casefold keying (2 fail); the casefold lookup alone (2). - **S1:** the path named raw (13); git's reason named raw (2); every path `repr`'d, printable or not (27). - **S2:** no `except` (3). - **N2:** no `plan` check (11); only #270's question asked (1); the formatting-character reason said otherwise (1); the report's `pin` line raw (1); `check`'s `write` line raw (1). - **S3:** the line dropped (1). - **N3:** the `_pin_fix` name guard (1); the `_read_pins` path guard (1); the lower-casing (1); the `digest_source` check (1); the not-text check (1); the gitlink filter (1). - **The issue rows reproduced.** S1, S2 and N2 were reproduced on 2bcfa69's tree and shown gone on the fix (a probe driving both tools; `gh` was called 0 times). **Before fix round 1.** Runs were on 7a94b6b, with `git-filter-repo` 2.47.0 first on PATH and the repository-local `user.name` blanked with `GIT_CONFIG_COUNT` (a workstation whose standard checkout sets one fails `test_the_scaffold_refuses_with_no_elector` on main itself; CI sets none). 7a94b6b differs from ffd1297 in `tests/test_pin_source_unreadable.py` only, so the three suites that do not read that file ran on ffd1297: - the new file: 46 passed; - adopt pins, adopt plan, git output decoding: 172 passed, 1 skipped (the MedxEHR clone is absent); - scaffold pin and reuse, pin collisions, e2e, referent chain links, neutral pin and pin validators, #275's tests, naming policy, repo hygiene, CLI reference, scaffold leg-path collisions: 571 passed; - shape doctor: 254 passed, 3 skipped (the PowerShell line exists only on a Windows host); - the new file with repo hygiene, git output decoding and CLI reference, once more after the last edit: 176 passed. Earlier on 5690235 and d806e8f, before the #339 commit: the adopt suites (e2e, execute refusals, verify, spec-only, leg paths, root shape paths, plan escapes, leg-path collisions, clone template) 740 passed, and `tests/test_adopt_template_values.py` with #273's other adopt tests 489 passed, 2 skipped. They were not re-run on 7a94b6b: the #339 diff touches `pin_digest_from_source` and the call in `declared_pin_values`, which those suites reach through `test_adopt_pins.py` only. Nothing was run on macOS or Windows. Proof the new tests test something: - **On d806e8f.** 38 of the 46 fail, each on the traceback and its exit 1. The 8 that pass are the unchanged behaviour: the not-a-clone refusal (4 rows) and the valid-source rows (4). - **Mutation probes.** Ten hunks reverted one at a time, each failing at least one new test: the `OSError` catch (16 fail), the catch narrowed to `OSError` only (21), the missing-path clause (9), the clause narrowed to `FileNotFoundError` (3), the `S_ISDIR` judgement (3), the loop guard (4), no resolve at all (3), the old `.resolve()` call (4), the reason as `str(exc)` (12), and the file sentence as the OS's (3). - #273's own 52 tests still fail on c7c02fa (51 fail or error; the one that passes checks the test's own table), and its 18 mutation probes were each caught. **Re-verified after the merge.** An independent re-verification of f9ed46f and 73d65c6 found the code correct: - #409 is carried whole. A 30,000-case fuzz of `--pin` lists through #409's final refusal and this branch's port differed in 0 cases. - `scaffold-project.py` equals the tested file plus main's own changes that are not #409's (same patch-id). - `_commit_the_split` keeps #445's `try` and #394's `append`. - `_read_pins` reads a loaded plan as before. Its findings were all in this description (the #506 bullet and three stale facts), now fixed. Runs on the head and on its merge with main 48585e6: 988 passed, 2 skipped; and 967 passed, 3 skipped. `git merge-tree --write-tree` was run at 01:48Z on 2026-10-13, comparing hunks per file. Each PR head, fetched from `refs/pull/<n>/head` with no `gh`, was merged with 73d65c6 and with origin/main 9736424 alone. Two sets were asked about: - every PR this description listed before that main does not now contain; - every PR numbered above #468 whose head main does not contain: #473, #474, #490, #491, #493, #494, #495, #496, #497, #503, #504 and #505. **Open state, read with `gh` by the coordinator at 02:00Z and refreshed at 02:25Z after a re-verification on main 48585e6.** Every pull request named below is open. #184, #94, #96, #98, #100 and #161 are closed, and #465 landed as 48585e6. `fix/adopt-plan-quote-yaml-keywords` has no pull request. #506 (`fix/scaffold-reuse-seeded-legs`) opened after this list was made, and the re-verification reads it. - **Landed since the last draft of this list:** #343, #346, #355, #358, #373, #378, #382, #387, #394, #397, #401, #404, #409, #419, #452, #456, #465, #466 and #484. #394's `_commit_the_split` conflict and #401's `docs/cli.md` conflict are both resolved in f9ed46f, above. - **Clean, 20:** #266, #347, #356, #374, #376, #384, #395, #453, #467, #468, #473, #474, #490, #491, #493, #494, #495, #497, #503 and #505. #453, #468 and #491 were updated onto main since the first read and now merge clean. #507 (lane 5's #486-#488) shares only the generated `docs/cli.md` with this branch and merges clean with it. - **Conflicting with main alone, and this branch adds no hunk:** - #402: `shape-doctor.py` 5; - #496: `shape-doctor.py` 10 and `tests/test_shape_doctor.py` 1. **Conflicts this branch adds:** - **#435** (3efcc7a, lane 2's `fix/adopt-plan-field-types`, for #386). - It adds 2 hunks in `adopt-project.py`: 5 here, against 3 with main alone. They are `render_plan`'s `pins:` line and `Plan.pins`. - Keep this branch's `_pin_lines` and `_pin_products`. The rest is under "Where the meaning meets". - **#504** (37ef29a, `fix/adopt-root-kept-symlinks`). - It adds 1 hunk in `adopt-project.py`: 2 here, against 1 with main alone. It is the materializer call in `_commit_the_split`. - Keep both arguments and both comments: `append=append, neutral_pins=_recorded_pins(plan), spell=_spelled`. - **#506** (119d8c4, `fix/scaffold-reuse-seeded-legs`). - It adds 1 hunk in `scaffold-project.py`. With main alone, #506 conflicts only in `docs/handbook.html`. - #506 inserts `LEG_TEMPLATES` right after `declared_pin_products`, which this branch moved out, and rewrites `_reusable_remotes` further down. Keeping both sides breaks the file with an unterminated string literal. - Keep only #506's `LEG_TEMPLATES` block. Drop #506's `declared_pin_values`/`declared_pin_products`, which moved here, and this branch's three-line `_reusable_remotes` opener, which #506 rewrites below. Resolved that way, pyflakes is clean and #506's scaffold tests pass (88). - **`fix/adopt-plan-quote-yaml-keywords`** (ed9968f; a branch with no pull request). - It adds 2 hunks in `adopt-project.py`: 3 here, against 1 with main alone. - One is `render_plan`'s `pins:` line. Keep `_pin_lines`, which already sends product, repository and `digest_source` through `y`. - The other is `_refuse_unholdable_values`' signature. Keep `pins: dict`, with that branch's `_validator_judged` and `_named_as_read` above it. - `fix/adopt-late-failure-notice` is no longer on origin. #445, the late-failure notice, is on main and merged here. **Where the meaning meets:** - **#435 and #386.** The coordinator has said this on #435: #435 (comment) - #435's `_plan_key_findings` (`plan-bad-pins`) keeps only text items in `pins:`, and its `PINS_REMEDIATION` tells a person to write `pins: [openGlass]`. - This branch writes each pin as a mapping, and it makes a name alone `plan-pin-unrecorded`. - Read from #435's code, not run: on a tree with both, the plan `plan --pin` writes is `plan-bad-pins` in `check`. - Whichever lands second reconciles them, with three changes: - `_plan_key_findings` takes this branch's mappings, or leaves `pins:` items to `_read_pins`; - a `pins:` that is not a list gives one finding, not both `plan-bad-pins` and `plan-pin-malformed`; - `PINS_REMEDIATION` says the mapping `plan` writes. - Until that is done, `tests/test_adopt_pins.py::test_a_plan_with_a_whole_pin_checks_ok` fails. - **#314.** It landed, and it is merged here (above). - **`fix/adopt-plan-quote-yaml-keywords`.** Its pin tests are written against the name-only `--pin`. They need the `name@commit` form, and their expected `pins: [...]` line becomes `_pin_lines`'s mapping. - **Two unwritable values are refused in two runs (lane 5's second review, n3).** `plan --elected-by 'A<LF>B' --pin openGlass@<sha> --pin-source <a path holding an ESC>` refuses the elector alone, exit 2. Re-run with the elector retyped, it refuses the source; #270's refusal names every unwritable value at once. Reproduced on 421e4e5, with `gh` called 0 times. One refusal would need a code change, not made here: - `_refuse_an_unrecordable_pin_source` would split into a function that returns the sentence; - `_checked_plan_inputs` would ask it before #270's `unwritable_values` raise, and raise one `adopt-unwritable-value` that names both, with both remediations; - a test would add the two-value row. N2's question is `one_printable_line`, not `unwritable_reason`, so it cannot simply join #270's list. - **Docs.** The README, AGENTS.md and `docs/handbook.html` are unchanged, because none of them calls adopt's `--pin` a name. A sentence saying adopt now takes the scaffold's `--pin` is a follow-up. It costs an AGENTS.md line-cap raise, and a README edit forces the handbook's README blob to change. - **A product spelled `null`, `true` or `false`.** The scaffold already accepts `--pin null@<sha>`: it exits 0 and builds a root whose `validate-pins.py` exits 1 `neutral-pin-entry-malformed`. Adopt's `plan` writes `product: null` through `y`, and `check` then finds `plan-pin-malformed` and refuses it before any leg exists. The one fix for both tools is in `parse_pin`, and it would change the scaffold's behaviour. - **Wording shared with the scaffold.** `digest_source` is written bare into the pin file, so a `--pin-source` path holding ` #` is cut short there. `templates/assembly-root/contracts/neutral-product-pin.yaml` and `validate-pins.py`'s `neutral-pin-missing` remediation name only the scaffold as the writer. Two comments in `scripts/repo_shape.py` still name the scaffold's `pin_digest_from_gh`, in a digest-pinned shipped copy. - **Filed as follow-ups:** - #449: S3, a hand-edited `commit:` or `tree_sha256:` that disagrees, with options (a) and (b); - #450: the scaffold half of N2, a `--pin-source` path no file can hold; - #451: #339's third decision (N5), a `--pin-source` that nothing reads. - **`validate-pins.py` prints a `--pin-source` path raw** in `neutral-pin-source-unreadable`, the twin of S1. It is a shipped template that #409 and other open PRs edit, so it is left for its own change. No issue is filed for it yet. - **Windows and macOS** were not run here; CI ran both (below). Two assertions that depend on what Windows answers for a path (a name past the filesystem's limit, a path beneath a file) check only the refusal and its remediation there. Fix round 1's rows that need a control character or a byte that is not UTF-8 in a path, or a symlink to `git`, are skipped on Windows by a mark, and so is fix round 2's ESC row. The rows for a no-break space and a zero-width joiner run everywhere, and so does the in-process `gh-not-found` test. Reviewed independently by lane 5 at 7a94b6b (3 should-fix, 5 nits: fix round 1) and at 421e4e5 (0 blockers, 2 should-fix, 3 nits: fix round 2). After #409's squash, the re-verification of f9ed46f and 73d65c6 found the code correct; its 1 should-fix and 3 nits were in this description and are fixed. GitHub's update-branch then merged main twice (33bae3f, 2170526), with no hand edit. CI's tests, tests-macos and tests-windows and SonarCloud all passed at 2170526 (main cac6bb2 merged in). On that head merged with main 57c85e9 (#490, #474 and #468, none of which touches this branch's files), the pin and adopt set with `tests/test_update_shape_landing.py` gave 1112 passed, 2 skipped, and the doctor, adopt end-to-end, unfinished-merge and bootstrap set gave 1056 passed, 3 skipped (the doctor's PowerShell line, which exists only on a Windows host). Closes #273 Closes #339 Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap
pushed a commit
that referenced
this pull request
Oct 13, 2026
#435 was stacked on #356, which squash-landed on main as 965420b. Its final head 2fdfbed is a server-side merge of main into #356's 5982bbe, so it brings #356's last fix (the `org:` refusal and `id:` read ahead of the election-date guard), #445's REFUSED contract and #401's help text. This is step 1 of bringing a stacked branch onto main after its parent's squash: `git merge-tree 2fdfbed 3efcc7a` was rc 0, and this merge is clean. Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap
pushed a commit
that referenced
this pull request
Oct 13, 2026
… list of mappings, so #386's gate asks it only whether it is a list, in #273's own finding (#386) Main since #356's squash 965420b changed `adopt-project.py` three times: 70d0d1b (#384, `_spelled` in three findings), 56713e2 (#493, the tracking branch from the source's default branch) and aa4e1f5 (#508, lane 5's #273 and #339). aa4e1f5 makes `plan --pin` take `product@<40 hex>` and write each pin as a MAPPING of `product:`, `repository:`, `commit:`, `tree_sha256:` and `digest_source:`, and its `_read_pins` judges each `pins:` entry beside the entries: `plan-pin-unrecorded` for a name alone, `plan-pin-malformed` for anything else `plan` never writes there, and for a `pins:` that is no list. Two hunks conflicted, both in `adopt-project.py`, both the ones aa4e1f5's message names: `render_plan`'s `pins:` line takes main's `_pin_lines`, which writes the product through `y` and the commit and digest quoted, so this branch's `y(pin)` is superseded; and `Plan.pins` takes main's `_pin_products`, which reads a list only. The rest is the reconciliation aa4e1f5 and lane 5's comment on #435 asked of whichever landed second. As this branch stood, its item check took only text, so every plan `plan --pin` now writes was `plan-bad-pins` in its own `check`: - `_plan_key_findings` no longer judges `pins:` entries: they are `_read_pins`' alone, in its words and codes. - A `pins:` that is no list is still asked first, beside the other four keys and before the work directory, because the naming policy reads the root's products from it. Its finding is `_read_pins`' own `plan-pin-malformed`, so it is one finding under one code, and `plan-bad-pins` is gone. - `PINS_REMEDIATION` names the mapping `plan` writes. - `PLAN_KEY_REFUSAL` begins "no leg repository was created and nothing was pushed", the words every refusal before a leg says (#445; conftest's `refused_before_any_leg` asserts them, and #273's not-a-list row now meets this refusal). `tests/test_adopt_execute_refusals.py` says the same: the `pins:` key rows expect `plan-pin-malformed` and gain a single mapping with no list around it; the entry rows move to a test that the gate is silent and `_read_pins` names them; the controls and the corrected end-to-end plan are whole pins; and the round trip runs `plan --pin <name>@<commit> --pin-source` for 2026, 007, 1.5 and openGlass, and holds `check` to `plan ok`. `git diff --stat cc8f2da` of the result lists only `adopt-project.py` and `tests/test_adopt_execute_refusals.py`. Before this commit: `tests/test_adopt_execute_refusals.py` 150 passed and `tests/test_adopt_pins.py` 70 passed. Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap
added a commit
that referenced
this pull request
Oct 13, 2026
… leg that is empty or whose one commit is the scaffold's own seed, and the ruleset hint offers that re-run, by an operator holding the bypass right, as its one exit, so a project a ruleset refused part-way is finished by one re-run, not refused `scaffold-remote-exists` for its own legs or sent to a pull request with no base (#462) (#506) A run whose push an organisation's ruleset refused left its repositories behind: all three empty when the first push, the spec leg's, was refused, and a leg at its one seed commit for each leg pushed before a later push was. `RULESET_HINT` offered two exits and neither finished. The pull request had no base, because the refused push was the first into a repository `gh repo create` had made without a README. The re-run was refused `scaffold-remote-exists` for the scaffold's own legs, with the flag or without it, and after an operator had seeded a leg by hand. Brett Heap ruled on 2026-10-12: "Offer only exits that work", "Re-run reuses empty legs" and "Accept the scaffold's own seed". - `--reuse-empty-repo` reuses each leg as it reuses the root: when it exists with zero commits, asked as before of `remote_is_empty` or `local_remote_is_empty`. - It also reuses a leg whose ONLY commit has exactly the tree this run would seed it with. `only_commit` requires one object name across every ref `git ls-remote` lists, fetches that commit alone (`--depth=1`) into a scratch bare repository, and requires that it has no parent. `seed_tree` builds the leg's template again in a scratch directory and takes `git write-tree`. The commit's message, author and date are not compared; the leg templates take no date, login or commit. - A leg reused at its seed is not given a new commit: `_take_held_seed` fetches that commit into the work clone and resets to it, so the pin and the gitlink name the commit the remote already has, and the push that follows changes nothing or creates the tracking branch at it (a seed pushed to `seed/scaffold` alone). - Any other leg with commits is still `scaffold-remote-exists`; its remedy says it holds commits other than the one seed this scaffold would push, and that there is no `--force`. An empty or seeded leg met without the flag is told to re-run with `--reuse-empty-repo`. - A reused leg keeps its GitHub visibility, and a run whose `--visibility` disagrees is warned, as a reused root already is. - `RULESET_HINT`, the scaffold's, and the new `FAMILY_RULESET_HINT`, `family.py init`'s, each offer ONE exit: an operator holding the bypass right re-runs the same command with `--reuse-empty-repo` and a new `--work-dir` (for the family, `--into` or `--work-dir`), pushing nothing first, since a repository pushed into by hand is no longer empty. The seed branch and the pull request are named only as what cannot finish the run. Both hints take the two fields `{repo}` and `{tracking}`. - AGENTS.md's ruleset rule, README's paragraph on a partway scaffold, the handbook's matching sections, both `--help` texts and docs/cli.md say the same. README and AGENTS.md keep their line counts. Unchanged: the root is reused only when empty, and a root with commits still names `adopt-project.py`; a leg that does not exist is created as before; nothing deletes, rewrites or force-pushes a remote; `family.py init --reuse-empty-repo` reuses an empty holder as before. adopt-project.py is byte-identical to the base: #445 gave adoption its own `ADOPT_RULESET_HINT`, whose one exit is a re-run once the legs its notice names are deleted, and tests/test_ruleset_hint_tracking_branch.py now pins that text beside the other two and takes each tool's exit through to a finished run. Left out: a family holder that holds its own seed is still refused, because the ruling covers the scaffold's legs; and adoption's hint still calls the pull request "the scaffold's other exit", which this change retires. Lane: openreposhape-3 (openRepoShape-3) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap
added a commit
that referenced
this pull request
Oct 14, 2026
… remedy prints its `REFUSED` line alone, the assembly root's `validate-pins.py` still adds the lockstep remedy to its own, and `add` refuses a `.gitmodules` git cannot read as `gitmodules-unreadable` with the holder gate's remedy, so a family holder, a shape pin or an adoption plan is no longer told to run `validate-pins.py` and move a leg pin (#370) (#494) `repo_shape.Refusal` defaulted its remediation to `REMEDIATION`, the assembly root's lockstep remedy: run `git submodule update`, then `validate-pins.py`, and move a leg's gitlink, `contracts/<leg>-pin.yaml` and its workflow `@<sha>` refs in one commit. Every refusal built without a remedy of its own printed it, in every tool and every kind of root, including those `git_out`, `load_yaml` and `find_repo_root` raise. - `Refusal`'s remediation defaults to none, and `str()` of a refusal with none is its `REFUSED` line alone. `repo_shape` cannot know which tool or root it refuses for, so it assumes no remedy rather than one tool's. - `REMEDIATION` keeps its name and its bytes. `validate-pins.py`, the assembly root's gate and the one place it is right, prints it under each refusal it makes that names no other, its own and those `repo_shape` raises for it. It imports only that name, which every `repo_shape.py` a root was ever given defines, and beside one that still defaults to it it prints it once. - `family.py add`, and `add --dry-run`, refuse a `.gitmodules` git cannot read as `gitmodules-unreadable`, in the words and under the code the holder's `validate-family.py` uses for the same file (#300), where it was `git-failed`. With no `.gitmodules` in the working tree git reads the index's copy or HEAD's, and the remedy then says which and how to put it back. - `shape-doctor.py`'s `refusal_line` quotes a refusal with no remedy whole, and a row a remedy-less refusal ends has no NEXT, where it carried the lockstep sentence. - `adopt-project.py`'s report of a failure once a leg repository exists left the lockstep remedy off by comparing against it (#445); it leaves an absent one off too, so no empty line follows its `REFUSED` line. Measured on a scaffolded root, a family holder and three members in local bare remotes, main against this head, 36 runs. On 22 the lockstep trailer is gone and the `REFUSED` line stands alone: the holder's `validate-family.py`, `bootstrap.py` and `family.py siblings`, `family.py` `add`, `bump` and `init`, `update-shape.py check` on a shape pin with no `files:` rows, `validate-manifest.py`, the root's `bootstrap.py`, `bump-leg.py`, `validate-repository-naming.py` and `adopt-project.py check` on a plan. On `add` and `add --dry-run` it is the two lines the holder's gate prints. Unchanged: every remedy a caller passes, and `validate-pins.py`'s output, byte for byte on the six refusals measured (`pin-tag-only`, `yaml-unparsable`, `shape-pin-missing`, `pin-submodule-uninitialized`, `not-a-git-repo`, `manifest-unreadable`); the holder gate's `gitmodules-unreadable`, `adopt-project.py`'s placement-plan refusal and the doctor's `manifest` row, also measured. No refusal code changes but `add`'s, from `git-failed` to `gitmodules-unreadable`, which AGENTS.md does not name. `scripts/repo_shape.py` and `templates/assembly-root/scripts/validate-pins.py` are pinned copies, so `update-shape.py check` reports both as `upstream-changed` in every assembly root, and `repo_shape.py` in every holder; `validate-family.py` is not changed. Left out: per-site remedies for the refusals that now print their line alone, and narrowing `validate-pins.py`'s lockstep remedy on its refusals that are not about a leg (`shape-pin-missing`, `manifest-missing`, `neutral-pin-*`). Lane: openreposhape-3 (openRepoShape-3) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



Lane: openreposhape-1 (openRepoShape-1)
Claim: #188 (comment)
Claim: #177 (comment)
Claim: #171 (comment)
Described at head 92c72c1, which contains main 92e29d9. The pull request's head is 9f19afe:
gh pr update-branchmerged main cb7063f into 92c72c1 (2534d85), then main dd8d0ea (9f19afe), both cleanly.What
The change is in
adopt-project.py, plus one sentence of AGENTS.md. Everything else is tests.Every failure once a leg repository exists is reported where it happens (#171, #188).
runonly raises. On main,_build_the_legsreturns None in silence, so a failedgit filter-repoends with exit 2 and an empty stderr, with both leg repositories made (#171). Everything after the legs falls through to a bareREFUSED <step>, or tomain's generic handler, printed ahead of thebarelines in a merged log (#188). Now every step that runs once a leg repository exists reports its own failure the same way:REFUSED <step>.and what failed: for a command, the exact command, where it ran, its exit code and its output;The steps are:
creating the code leg repository, once the spec leg's exists;extractingorseeding the <role> leg;pushing the <role> leg, orpushing the seeded <role> leg;cloning the source for the split;mounting the legs;committing the split;pushing the split branch;verifying the split.A closed stdout cannot swallow the report: the flush ignores the
BrokenPipeError.Not only a failed command (#184 P3-1, #188 N3). Three more kinds of failure are reported the same way, and exit 2:
Refusalraised on the way:git_out's, the materializer's, or adopt-project.py: the leg mount passes--forcetogit submodule add, so a source whose.gitignoreis*+!.gitignore, or namesspecorcode, adopts instead of dying at the mount after both leg remotes were pushed (#174) #200's mount guard's;OSError: a read-only work directory, a symlink loop the materializer walks into, orgitgone from PATH;UnicodeError:git_out(scripts/repo_shape.py) still decodes git's stdout strictly.On main, each of these escapes, either as a refusal with no notice or as a traceback with exit 1. A
Refusalkeeps its own remediation, unless that is the validators' generic remediation about pins. Anything else, a defect or Ctrl-C, getsSTOPPED <step> by <Type>.and the notice, and is then re-raised with its traceback.One notice,
_nothing_rolled_back. It:REFUSED <step>with no notice of what already exists and no stdout flush, so the combined log shows REFUSED before thebare …lines and the re-run meetsleg-remote-existsunexplained #188's review, F2). A re-run while the cause is still there fails the same way, one more pair of leg repositories behind it. Only an interrupt (Ctrl-C), which failed at nothing, is told "Re-run" alone (re-verification R2). A defect fails the same way on every re-run, so it is told to fix it first, as a refusal is;leg-remote-existsunder--local-remote-dir, and against GitHub agh repo createthat finds the name taken;<work>holdsNorthwind-spec,Northwind-codeandNorthwindfrom this run, andexecuterefuses a --work-dir that holds them." (adopt-project.py:executeprints the command and its stderr when a leg cannot be built, so agit filter-repothat fails after both leg remotes exist no longer exits 2 with an empty stderr (#171) #184 P2-1, review N2). That counts the source's own clone too, which anorg/reposource makes in<work>/source-readonly/<repo>(re-verification R3). A failure before anything was built or cloned leaves the sentence out.When the code leg's repository cannot be created after the spec leg's was, the notice names the spec repository alone, "and nothing is pushed to it".
mainthen exits 2 without printing the failure a second time. That holds for aghthat cannot be run at all, too (#188 N4).executerefuses, before it creates anything, what an earlier run left behind. These are new refusals, in this order:adopt-work-dir-occupied(review F1). A--work-dirthat holds anything but an empty directory at<work>/<spec leg>,<work>/<code leg>or<work>/<assembly>is refused. On main, such a run made both legs and then died atgit cloneinto a directory an earlier run had built in. Remediation: "no leg repository was created and nothing was pushed. Pass a --work-dir that is new or empty, or none: without one, each run makes a new one." A run without--work-dir, or with a new or empty one, is never refused, and neither is a run for its own source clone. The detail ends with the reason of what builds in each occupied directory: "git clonerefuses a directory that is not empty" for an extracted leg and the split, and "a seeded leg is written and committed in its own directory, and only in one that is new or empty" for a seeded leg, which is never cloned (9be2b05 and 92c72c1, rv188f's N-1 and N-2). On main, a person's own files and history left in a seeded leg's directory went into the new leg and were pushed.adopt-branch-exists(adopt-project.py: a failure AFTER both legs are pushed (the split-branch push, the mount, the split commit) prints onlyREFUSED <step>with no notice of what already exists and no stdout flush, so the combined log shows REFUSED before thebare …lines and the re-run meetsleg-remote-existsunexplained #188 N1). The split branch already on the assembly at a commit the split push cannot fast-forward is refused, with the command that deletes it, quoted forsh(adopt-project.py: a failure AFTER both legs are pushed (the split-branch push, the mount, the split commit) prints onlyREFUSED <step>with no notice of what already exists and no stdout flush, so the combined log shows REFUSED before thebare …lines and the re-run meetsleg-remote-existsunexplained #188 N5). A branch at the source commit or behind it is pushed over as on main (review N1). The remediation also covers a plan whoseadopt_branch:names a branch of the source's own. Under--local-remote-dir, a leg repository still on disk keepsleg-remote-existsfirst.Every path these messages name is spelled as a finding spells it (#212). Main's 5751460 has
checkandexecutename a path in a FINDING or a refusal with_spelled: between backticks, with a backslash escape for a backslash, a backtick and any characterstr.isprintablerefuses. This pull request's own messages do the same (e507c42):adopt-work-dir-occupied: the work directory and what it holds;adopt-branch-exists: the assembly and the branch.A leg repository under
--local-remote-dir, the work directory and a local source are paths a person chose. One whose name holds a line feed, U+0085 or U+2028 split the notice's one line, and a carriage return or an ESC overwrote it. Now a path reads the same in a finding, in the notice and in the two refusals. The branch deletion is not spelled: it is a command to paste, so it stays quoted forsh(shlex.quote). Two texts that this pull request prints but does not write keep main's raw spelling, as 5751460 left them: the REFUSED block's command and its(in <dir>)line (CommandFailed.loudly, scripts/shape_materialize.py), andleg-remote-exists'<bare> already exists.A failed verification (#188 N1) flushes stdout before its findings, so they come after every line the run printed. Its exit is still to fix the plan and re-run, unless a finding names another exit. It is followed by the notice and by the split branch this run pushed, with the command that deletes it. It still exits 1. The shape-file finding's own exit now points there: "send that path to a leg or
dropit, and re-run as the end of this report says". A verification that cannot be made at all (review N3), such as agitthat will not list a tree, getsREFUSED verifying the split., the notice and the branch, and exits 2.#177 and #188 N2: a refused leg push prints the adoption's own hint. Both leg builders print
ADOPT_RULESET_HINT, not the scaffold'sRULESET_HINT. The scaffold's hint does not fit an adoption:git -C <work> push -u origin main:seed/scaffold, fails in a clonegit filter-reporewrote, withfatal: 'origin' does not appear to be a git repository.The adoption's hint names ONE exit: an operator holding the bypass right re-runs the adoption, once what the notice below names is cleared. It says why the seed exit cannot finish an adoption: the leg is a repository this run created, and its first push is the one refused, so it has no
mainfor a pull request to target. It prints no command. It ends "Under such a ruleset, that bypass is the fix the notice below asks for, never a change to the ruleset." (re-verification R4). The notice follows it. #177 asked for a test that runs the printed push. No push is printed now, so the tests assert that no command line is printed and that the refused leg remote holds no ref, as the hint says. The one command this pull request adds to the output, the branch deletion, is run by two tests exactly as pasted.AGENTS.md step 7 (review F3) is main's exit-keyed text (#304, a983e6a), and its exit 1 way out says what the tool prints: "so fix the plan, delete the split branch, and re-run into fresh legs and a fresh
--work-dir, or none." That makes AGENTS.md 482 lines, one more than main's 481, and c2f3118 raises the cap in tests/test_repo_hygiene.py to 482 with a dated reason. A test reads each half of the step.Ordering, so the notice is exact. The substitution table is built before the leg repositories are created, so its refusals leave nothing behind (an
elected_onthat is not a date, for example). Each leg builder reads its commit count and tree digest before it pushes. A read that fails after the push therefore cannot make the notice call a pushed leg unpushed.After the split is pushed, a
ghthat cannot be run at all is handled like one that refused, after a stdout flush. The topic then gets its by-hand commands at once. The pull request gets main's by-hand command after the verification table, and only for a split that passed; one that failed or could not be verified gets "No pull request was opened, and none should be" instead (a983e6a).With #200 (#174), the mount guard's refusals,
adopt-mount-occupiedandadopt-mount-unaddable, come after both legs are pushed, so they are late failures too. The run printsREFUSED mounting the legs., #200's own remediation, and the notice. The exit is 2, as on main.Exit codes. A refusal exits 2, a failed verification 1 and a success 0, as on main. What changes is the failures that escaped as a traceback with exit 1 once a leg repository exists, an
OSErroror aghthat cannot be run among them. They are now reported, andexecuteexits 2. As on main, a failed verification's 1 outranks a missing topic. A run whose topic will not set and whose verification then cannot be made exits 2 with the topic'sIS pushedcommands and, after them, the notice; its way out is the notice's re-run, though AGENTS.md step 7 sends an exit 2 that saysIS pushedto the printed command (rv188f's N-3, two failures in one run).With main's own changes since the last round (5751460, d94d155, b30595c, 6d8d88b, 71b58a1, 346079d, a1b3a4b, f795cf7, 161c6c9, 1f90a0f and 92e29d9 are merged; Why, cases 9 to 14):
backslashreplace, so they print in full, the work-directory sentence and the branch line included. What the merge adds is that a stdout line before them no longer raises. At 5a8b649, before this merge,print(f" bare {bare}")raisedUnicodeEncodeErroron such a name, after the spec leg's repository was made, andexecuteexited 1 with a traceback and no notice.run()decode as UTF-8. Output that is not ASCII now reaches the same handlers asCommandFailed. A refused leg push then takes its own route:REFUSED pushing the spec leg., the hint, and the notice. At 5a8b649 underLC_ALL=C PYTHONUTF8=0it wasREFUSED extracting the spec leg. UnicodeDecodeError: ..., without the hint. 60d08a1 also gave the Free-plan probe'sgh apia codec. That probe was the one call after both legs exist that could still escape unreported, with aUnicodeDecodeError..gitmodulesthat the history's gitlinks make unreadable, and c2ff2b0 (adopt-project.pycheckaccepts a.gitmodulesvalue git's submodule reader rejects (update = bogus,update =,update = !cmd,shallow = maybe,fetchRecurseSubmodules = bogus), soplan okis followed byexecutedying ingit filter-repoafter both leg remotes exist, whether the tip or only the history holds the gitlink #215) a.gitmodulesvalue git's submodule reader refuses (update = bogus,shallow = maybe). Both are_entry_findings, raised by_refuse_what_check_findsbefore_create_leg_remotes, so nothing exists yet and no notice is due. Main's owntest_execute_refuses_it_before_any_leg_exists, in tests/test_adopt_history_gitlinks.py and tests/test_adopt_gitmodules_values.py, runs this branch'scmd_executeand asserts that the remotes directory is never made. Before this branch merged them, each source made both legs and died ingit filter-repo'sgit fast-export, and this branch's notice reported it (cases 11 and 12). A value thatcheckreads differently from the installed git still takes that path, notice included.checkandexecuteprint a source path raw in a FINDING, so a\r,\n,\x85, U+2028 or ESC in a name overwrites, splits or escapes the line on a terminal, whereplan's questions spell the same path with_spelled#212) spells the paths a finding or a refusal names. This branch's notice and two refusals now spell theirs the same way (e507c42).checkaccepts plan pathsexecuterefuses asunsafe-value(a space or a non-ASCII character), soplan okis followed by a refusal and #166's split instructions can name entriesexecutewill refuse #196) judges an entry's path by one test, and 71b58a1 (adopt-project.pycheckpasses a plan that keeps the source's ownshape/in the root, andplace()then writes the shape's collision copy onto it after both legs are pushed: a realshape/README.mdis overwritten whileexecuteprints "nothing was overwritten" and endsadopt-lost, and ashape/Makefile/directory or a file namedshapeis a traceback #262, adopt-project.pycheckpasses a plan that keeps a plain file at a directory the shape writes into (.github,contracts,scripts), andexecutedies in aNotADirectoryErrororFileExistsErrortraceback after both legs are pushed #263) finds a kept root path where the shape writes a directory or itsshape/copy. Their findings are_entry_findingsas well, refused before any leg exists. 71b58a1 also gives the materializer a refusal of its own,materialize-copy-blocked, for a run that reaches it withoutcheck's finding. That one is raised inside_commit_the_split, after both legs are pushed, so this branch reports it like any late failure:REFUSED committing the split. materialize-copy-blocked: …, its own remediation, and the notice, with exit 2. On main it escapescmd_execute, and main's test expected that; 9d8aadd makes the test read the report instead, as 8d4fef4 did for adopt-project.py: the leg mount passes--forcetogit submodule add, so a source whose.gitignoreis*+!.gitignore, or namesspecorcode, adopts instead of dying at the mount after both leg remotes were pushed (#174) #200's mount guard. 346079d (adopt-project.pyplan'scross_leg_referencespasses a spec directory's name togit grep -Fwith no-eand the code entries as glob pathspecs, so a spec directory named-notesor a code entry named:cachesilently drops the follow-up, a code entry named*names files that are not in the code leg, a name outside ASCII is printed in git's quoted spelling, and a spec directory named-O<cmd>is run as git grep's pager when a tracked file holds the planned commit's hash #216) has_stage_the_splitadd withgit --literal-pathspecs, also after both legs are pushed, where any failure is reported.executeclones the source with the operator'sinit.templateDirapplied, so a template holdingmodules/specandmodules/codemakes the leg mounts'--force(#200) reactivate those planted stores: both gitlinks pushed on the split branch are a stranger's commit, the VERIFICATION still saysadoption verifiedbecause it checks a mount is present and not which commit, and the recursive clone of the result exits 128 #243) clones the assembly root with no git template (--template=), and adds a verification finding,adopt-mount-commit, for a mount that is not the commit pushed to its leg. The clone is inside this branch'stry:in_commit_the_split; that was the merge's conflict, resolved keeping both. So a clone that fails is still reported undercloning the source for the split. The new finding is a verification finding, so the footer, the notice and the branch line follow it. Agit rev-parsethat fails while it reads the pushed commits isREFUSED verifying the split.with the notice. 90d6077 gives main's new_verifytests the urls and the branch, as 97abf4c did for 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.--elected-by,--referenceor the--sourcedirectory passesplanandcheck, andexecutepushes both legs before it dies withUnicodeEncodeErrorinwrite_lf#255) asks inplan,checkandexecutewhether a file can hold each valueexecutewrites. Its refusals all come before_create_leg_remotes. The case it fixes was a late failure here, reported with the notice; merged, it is refused before any leg exists, as on main (Why, case 13)..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) resolves a relative.gitmodulesurl a leg keeps from the source's remote and from the leg's. Its finding ischeck's, refused before any leg exists. ab623dd (tests: 149 tests in four files run the operator's realgh(407 calls togh --versionandgh auth status, andgh api userbesides with a login) through the doctor'smachinerow and the installed command's machine block, so a suite whose conftest says "NO NETWORK AND NO GITHUB" reaches the host on a logged-in machine, and the oneghstub the doctor tests have is called by that row without the test seeing it #296) fails a test whoseghcalls its guard refuses; the 54-file set passes under it.adopt-project.py executeexit is to fix the plan and re-run into fresh legs, but in GitHub modeexecuteexits 2 with the split pushed whengh pr createis refused, and exits 2 afteradoption verifiedwhen the topic will not set, so following step 7 abandons a pushed adoption whose own exit is a command to run by hand #304) verifies before it exits on a refusedgh pr create, and prints the command to open the pull request by hand after the table, only for a split that passed; NEXT is said once, by_next_steps. The pull request step keeps this branch's reach (aghthat cannot be run is reported like one that refused) and its flush; the by-hand command is main's, printed once, after the table. A split this branch cannot verify after a refused pull request getsREFUSED verifying the split., the notice and "No pull request was opened, and none should be" (Why, case 14). Step 7 keeps main's exit-keyed text, with this branch's exit 1 way out. 92e29d9 (adopt-project.pyplanwritesleg: specwith no question for a source path thatexecutewill refuse (-notes.md), so the answercheckorexecutethen rejects is oneplanchose for the person, androotordropis offered for that entry nowhere #319) changes onlyplan.--pinand--help, with AGENTS.md and README), b30595c and 34ecbc2 (repo_shape.py's quoted scalars, andrender()in scripts/shape_materialize.py), b421e6b (the validators' PYTHONIOENCODING), be2790e, 8a14ac9 and c1b6732 (update-shape.py), 86da808 (bump-leg.py), 6d8d88b, c0ed615, c7c02fa and 6f4ce47 (family.py), 7591455 (tests/conftest.py'sgit()identity), a1b3a4b (a test), 8cdca5e (validate-pins.py), db19187, 0f67f7b and 3ae0109 (family.py and validate-family.py), a2cb7d3 and 1f90a0f (bump-leg.py and update-shape.py, and pin-rewrite helpers in scripts/shape_materialize.py that adopt-project.py does not call), d82a83e (scaffold-project.py), 8325d05 (shape-doctor.py) and 28bdc84 (single-repository.yaml) change nothing the late failures go through. be2790e's update-shape.py notice labels its commandNEXTbecause it finishes the job; this notice's deletion does not, so it is not labelled.In this round's eleven merges, the definitions the late failures go through change only by main's own hunks:
main()(06b5204),run()(60d08a1),_extract_leg's path listing and_mount_the_legs'git rm(6f45bf9), the materializer (71b58a1),_stage_the_split'sgit add(346079d), the split clone's--template=and the verification's mount-commit finding (e43fa95),Source.openand a refusal after_repository_urls(f795cf7), and the pull request step,_verify's NEXT and the end ofcmd_execute(a983e6a). e507c42 changes how four of them spell a path, and 9be2b05 the reasonadopt-work-dir-occupiedgives.Unchanged.
scaffold-project.py,scripts/(so the scaffold'sRULESET_HINTand its callers), the setup scripts, templates,shape-doctor.py,update-shape.py, docs and README.md. AGENTS.md changes in step 7 only.git diff 92e29d9 bbdd313 --statlists AGENTS.md, adopt-project.py, tests/conftest.py and eight test files, and nothing else.A successful adoption is byte-identical to main's. A branch at the source tip is pushed over and verifies on both trees (case 6).
Commits.
executeexits 2 with nothing on stderr whengit filter-repofails, after both leg remotes exist:_build_the_legsswallowsCommandFailedon the premise thatrunalready reported it, andrunonly raises #171, from PR adopt-project.py:executeprints the command and its stderr when a leg cannot be built, so agit filter-repothat fails after both leg remotes exist no longer exits 2 with an empty stderr (#171) #184.REFUSED <step>with no notice of what already exists and no stdout flush, so the combined log shows REFUSED before thebare …lines and the re-run meetsleg-remote-existsunexplained #188 and RULESET_HINT advisespush -u origin ...from an extracted leg whoseoriginfilter-repo removed, and says "listed above" of lines a combined log prints after it #177, first round.copy_treetest stubs return what they wrote, ascopy_treehas since scaffold-project.py (andgit_init_commitin shape_materialize.py, scripts/family.py) stages the first commit withgit add -A -- ., so a globalcore.excludesFilesilently drops.gitattributes,.github/,.gitignore,contracts/*andscripts/*from a freshly scaffolded repository: the same class as #167, on the scaffold side #175.executeprints the command and its stderr when a leg cannot be built, so agit filter-repothat fails after both leg remotes exist no longer exits 2 with an empty stderr (#171) #184 P1-1, P2-1 and P3-1, and adopt-project.py: a failure AFTER both legs are pushed (the split-branch push, the mount, the split commit) prints onlyREFUSED <step>with no notice of what already exists and no stdout flush, so the combined log shows REFUSED before thebare …lines and the re-run meetsleg-remote-existsunexplained #188 N1-N5.--forcetogit submodule add, so a source whose.gitignoreis*+!.gitignore, or namesspecorcode, adopts instead of dying at the mount after both leg remotes were pushed (#174) #200's three mount-guard tests read the reported refusal._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_verifytests pass the urls and the branch_verifynow names.adopt-work-dir-occupied, the fix-first notice, and the work-directory sentence read off the disk (review F1, F2, N2).adopt-branch-existsonly for a branch the push cannot fast-forward (review N1).checkandexecuteprint a source path raw in a FINDING, so a\r,\n,\x85, U+2028 or ESC in a name overwrites, splits or escapes the line on a terminal, whereplan's questions spell the same path with_spelled#212).executereports.executeclones the source with the operator'sinit.templateDirapplied, so a template holdingmodules/specandmodules/codemakes the leg mounts'--force(#200) reactivate those planted stores: both gitlinks pushed on the split branch are a stranger's commit, the VERIFICATION still saysadoption verifiedbecause it checks a mount is present and not which commit, and the recursive clone of the result exits 128 #243_verifytests pass the urls and the split branch.adopt-work-dir-occupiedgives each directory the reason of what builds in it (the confirmation check's N-1).cmd_executenow says it (both for the merge with a983e6a).adopt-work-dir-occupied's seeded-leg reason states the rule, not what a commit would take in (rv188f's N-2).Main was merged nineteen times, never rebased:
.gitmodulesto one leg and a submodule it registers to another passescheckandexecute, saysadoption verified, and the assembly's recursive clone then fails:planshould ask,checkshould find,executeshould refuse #166 via adopt-project.py: a plan that sends a source's.gitmodulesto one leg and a submodule it registers to another, or keeps either in the root, is now acheckfinding and anexecuterefusal, andplanasks the one-leg question on.gitmodulesitself (#166) #194.--spec-path contracts, so the adoption fails after both legs are pushed (main: at the first bootstrap) #172 and 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 via adopt-project.py: a leg's mount path must not be, hold or lie inside a path the assembly root holds, so--spec-path contractsno longer checks ok and dies staging the split after both legs are pushed, and--code-path project.yamlno longer saysadoption verifiedover an assembly with no manifest (#172) #198. One conflict, on thefrom shape_materialize importblock; both sides kept.--spec-path spec/, writes a straysubmodule.spec/.urlbeside a registration that points at the work directory, and the new repository's recursive clone fails once the work directory is gone: #169's bug on the scaffold side #179 via scaffold-project.py: a leg's mount path must be canonical or it is refused before anything is created, so--spec-path spec/no longer scaffolds a project whose recursive clone fails once the work directory is gone (#179) #201._mount_the_legsrunsgit submodule addwithout--force, so a source.gitignorethat hides a mount path (*+!.gitignore, orspec/code) dies at the mount AFTER both leg remotes are pushed, and nothing incheckwarns #174 via adopt-project.py: the leg mount passes--forcetogit submodule add, so a source whose.gitignoreis*+!.gitignore, or namesspecorcode, adopts instead of dying at the mount after both leg remotes were pushed (#174) #200. A clean merge, which broke the three tests 8d4fef4 fixes._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 via adopt-project.py: verification passes over the two leg mounts and no other gitlink, so the source's own submodule kept in the root isadopt-root-submodule, any other root gitlinkadopt-unexpected-gitlink, one in a leg AND the rootadopt-duplicated, and a missing mountadopt-mount-missing(#165) #173. Four conflict hunks at_verify, both sides kept; 97abf4c fixes main's new tests._build_assembly_rootrunsgit submodule addwithout--force, so a leg path an ignore rule matches (--code-path __pycache__or.DS_Storeunder the shape's own.gitignore, or the defaultspecunder an operator's global excludes) passes--dry-run, then dies at the mount AFTER all three repositories exist and both legs are pushed #229 via 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. Clean.checkandexecuteprint a source path raw in a FINDING, so a\r,\n,\x85, U+2028 or ESC in a name overwrites, splits or escapes the line on a terminal, whereplan's questions spell the same path with_spelled#212 via adopt-project.py:checkandexecutespell every path a FINDING or a refusal names asplan's questions spell it, so a source name holding a carriage return, a line feed, U+0085, U+2028 or an ESC no longer overwrites, splits or escapes the line it is printed on (#212) #223. Clean. In the three files both sides changed, the merge's diff against each parent equals the other side's own diff.checkaccepts a.gitmodulesvalue git's submodule reader rejects (update = bogus,update =,update = !cmd,shallow = maybe,fetchRecurseSubmodules = bogus), soplan okis followed byexecutedying ingit filter-repoafter both leg remotes exist, whether the tip or only the history holds the gitlink #215 via adopt-project.py: a.gitmodulesvalue git's submodule reader refuses (update = bogus,update =,update = !cmd,shallow = maybe,fetchRecurseSubmodules = bogus) is acheckfinding and anexecuterefusal, soexecuteno longer dies ingit fast-exportafter both leg repositories exist, whether the tip or only the history holds the gitlink (#215) #240 among three. One conflict, in tests/conftest.py, where both sides append afterresolve(); both kept, main's block first. In adopt-project.py and tests/conftest.py, the merge's diff against each parent equals the other side's own diff (in tests/conftest.py, but for where git places two added blank lines).apply --branch --push --pr: whengh pr createfails after the commit and the push succeeded, it exits 2 with "REFUSED committing the re-synced shape" and no NEXT line, and a re-run on that branch says "nothing to do" and exits 0, so the tool never opens the pull request #204 via update-shape.py: a push orgh pr createthat fails afterapply --branchcommitted is named as that step, keeps the commit and ends on a NEXT line, and a re-run with--pron the branch opens the missing pull request, so the tool no longer refuses "committing the re-synced shape" and then answers "nothing to do" (#204) #227 and family.pyaddmounts the member before it checks--ator the member'sproject.yaml, so a refused add (a leg, a short--at) leaves a staged gitlink, a.gitmodulesregistration and a cached object store, and the next add of a different member commits the refused leg's registration while validate-family.py and the doctor say ok #203 via family.py:addasks every refusal of the holder and of a throwaway clone of the member before it mounts anything, and unwinds and names whatever fails after the mount, so a refused add (a leg, a short or unknown--at, an empty member) no longer leaves a staged gitlink and.gitmodulessection that the next add commits under its own title (#203) #236. Clean; neither changes a file of this branch's.checkaccepts plan pathsexecuterefuses asunsafe-value(a space or a non-ASCII character), soplan okis followed by a refusal and #166's split instructions can name entriesexecutewill refuse #196 via adopt-project.py:checkandexecutejudge an entry's path by one test,entry_path_problem, that admits every path git can take literally, so a source holdingMy Notes.mdno longer checksplan okand is then refused byexecuteasunsafe-value, and a path git cannot take is acheckfinding refused before any leg exists (#196) #231 and adopt-project.pycheckpasses a plan that keeps the source's ownshape/in the root, andplace()then writes the shape's collision copy onto it after both legs are pushed: a realshape/README.mdis overwritten whileexecuteprints "nothing was overwritten" and endsadopt-lost, and ashape/Makefile/directory or a file namedshapeis a traceback #262/adopt-project.pycheckpasses a plan that keeps a plain file at a directory the shape writes into (.github,contracts,scripts), andexecutedies in aNotADirectoryErrororFileExistsErrortraceback after both legs are pushed #263 via adopt-project.py:checkfinds, andexecuterefuses before any leg exists, a kept root path where the shape writes a directory or itsshape/copy, whether the disk keeps case or not; the materializer never writes over a copy (#262, #263) #313 among them. Clean. Only adopt-project.py changed on both sides, and in it only_extract_legwas changed by both, in lines that do not touch; the merged function has both. A clean merge that broke one test of main's, which 9d8aadd fixes.plan'scross_leg_referencespasses a spec directory's name togit grep -Fwith no-eand the code entries as glob pathspecs, so a spec directory named-notesor a code entry named:cachesilently drops the follow-up, a code entry named*names files that are not in the code leg, a name outside ASCII is printed in git's quoted spelling, and a spec directory named-O<cmd>is run as git grep's pager when a tracked file holds the planned commit's hash #216 via adopt-project.py:plan's follow-upgit greptakes a spec directory as its pattern and each code entry as a literal path, so a spec directory named-notes/or a code entry named:cache/no longer loses its follow-up, a code entry named*/no longer names a spec-leg file, a name outside ASCII is spelled instead of C-quoted, and a spec directory named-O<cmd>/no longer runs a command (#216) #228 and family.py:bumpandremovetake the git pathspec from the manifest row'spath:with no check and no--literal-pathspecs, so a hand-editedpath: members/*makesremove --member Atlasexit 0 with ONE commit that unmounts every member while dropping only Atlas's row, andpath: .makes itgit rm -r -fthe holder's own files and die on aFileNotFoundError, thoughvalidate-family.pyalready has a rule,member-path, for whatpath:must equal #299 via family.py:bumpandremoverefusemember-pathwhen a row'spath:is notmembers/<project>or itsproject:is not a directory name, and hand git every path literally, so a hand-editedpath: members/*orproject: .no longer makesremoveunmount every member in one commit while dropping one row (#299) #315. Clean. In adopt-project.py and tests/test_adopt_ignored_shape_files.py, the merge's diff against each parent equals the other side's own diff.executeclones the source with the operator'sinit.templateDirapplied, so a template holdingmodules/specandmodules/codemakes the leg mounts'--force(#200) reactivate those planted stores: both gitlinks pushed on the split branch are a stranger's commit, the VERIFICATION still saysadoption verifiedbecause it checks a mount is present and not which commit, and the recursive clone of the result exits 128 #243 via e43fa95 among them. Three conflict hunks in adopt-project.py, each resolved keeping both sides:_commit_the_split's clone (main's--template=and its comment, inside this branch'stry:),_verify's docstring (main's paragraph, then this branch's), and the root findings (main's lines in this branch's_accounted_for, with itsmountsparameter). AGENTS.md and tests/conftest.py merged with no conflict. 90d6077 fixes main's new tests.--elected-by,--referenceor the--sourcedirectory passesplanandcheck, andexecutepushes both legs before it dies withUnicodeEncodeErrorinwrite_lf#255 via adopt-project.py: a valueexecutewrites into a file is asked whether a file can hold it, inplan, incheckand inexecutebefore any leg exists, so a byte that is not UTF-8 in--elected-by,--referenceor a local--sourcedirectory no longer pushes both legs beforewrite_lfraises (#255) #270, with 8cdca5e and db19187. Clean. adopt-project.py is the only file both sides changed, and in it the merge's diff against each parent equals the other side's own diff. Both sides changedSource.openandcmd_execute, in different lines; the merge has both..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, with ab623dd and a2cb7d3. One conflict, in tests/conftest.py: both sides append at the end of the file; both kept, main'sghguard first.adopt-project.py executeexit is to fix the plan and re-run into fresh legs, but in GitHub modeexecuteexits 2 with the split pushed whengh pr createis refused, and exits 2 afteradoption verifiedwhen the topic will not set, so following step 7 abandons a pushed adoption whose own exit is a command to run by hand #304 via a983e6a among seven. One conflict in AGENTS.md (step 7: main's text, with this branch's exit 1 way out) and six in adopt-project.py: two are this branch's own definitions either side of_flush_stdout, which both sides added word for word;_open_the_pull_request's docstring and its failure (this branch's reach and flush, main's by-hand command after the table);cmd_execute's verification (both); and_accounted_for's end (both, then main's_next_steps)._verifyloses its NEXT, as on main. c2f3118 fixes two tests.planwritesleg: specwith no question for a source path thatexecutewill refuse (-notes.md), so the answercheckorexecutethen rejects is oneplanchose for the person, androotordropis offered for that entry nowhere #319. Clean.Why: before and after, on main 92e29d9
Each case ran on main 92e29d9 and on this branch, from the same driver.
--local-remote-dir, and GitHub only through a simulator (url.<dir>.insteadOfplus a stubgh).GIT_CONFIG_GLOBALandGIT_ALLOW_PROTOCOL=file.<T>/Thing, and the plan isplan --source <T>/Thing --project Northwind --org testorg --elected-by T --elected-on 2026-09-02 --out <T>/p.yaml.<T>is the run's own directory.Unless a case says otherwise, the command is:
1.
git filter-repofails after both leg repositories exist (#171). A stubgit-filter-repofirst on PATH printsfatal: stub git-filter-repo refusesand exits 1.main, exit 2, stderr 0 bytes:
This branch, exit 2, the same stdout, then on stderr:
Then
rm -r <T>/remotes, the realgit filter-repoback on PATH, and the same command:barelines again, stderr 0 bytes, and both leg repositories made again.2. The split push is refused (#188). A
pre-receivehook, installed throughcore.hooksPath, refusesrefs/heads/adopt/*on the source. The command is run as... > log 2>&1.REFUSED pushing the split branch. THE EXACT COMMAND WAS:…--- end output ---. Only then do the run's own lines come, with thebarelines on 13-14. There is no notice.barelines are on 3-4 and the REFUSED block on 8-17. Line 18 is the notice, "NOTHING has been rolled back:<T>/remotes/Northwind-spec.gitand<T>/remotes/Northwind-code.gitexist, and both legs are pushed. Once what failed above is fixed, …".3. A leg push is refused (#177, #188 N2). The hook refuses
refs/heads/mainon the leg remotes;> log 2>&1.main, exit 2, after the REFUSED block:
The printed push, run as printed:
It exits 128. This branch, exit 2, after the REFUSED block. No
git -Cline is printed:4. The verification fails (#188 N1, review F1). The plan keeps the source's own
contracts/shape-pin.yamlin the root;> log 2>&1.main, exit 1. The FINDING is on line 0 and the footer on line 2, above every line the run printed (the last on 21):
This branch, exit 1. The run's lines are 0-18, and the FINDING is on 19, ending "… send that path to a leg or
dropit, and re-run as the end of this report says. …". Then:Next, main's exit, followed as written: the plan fixed, then
execute --plan <T>/p.yaml --yes --local-remote-dir '<T>/remotes 2' --work-dir '<T>/work 2', with the branch left in place.<T>/remotes 2, then fails withREFUSED pushing the split branch(! [rejected] HEAD -> adopt/three-repo-shape (non-fast-forward)).<T>/remotes 2is not created:The tests follow the rest of the way out as written: the deletion, pasted, then
executeagain. That ends inadoption verified.5. A symlink loop the plan keeps in the root (#188 N3). The source holds
.github -> .github, answeredroot, andcheckexits 0 withplan ok.OSError: [Errno 40] Too many levels of symbolic links: '<T>/work/Northwind/.github/workflows', with both legs pushed.6. A branch named
adopt/three-repo-shapeat the source's own tip (review N1).git -C <T>/Thing branch adopt/three-repo-shape, then the command. Both trees exit 0 withadoption verified: every source path is in exactly one place.7. A defect inside the verification (re-verification R2).
main()is run in-process, with_shape_file_findingsreplaced by a function that raisesTypeError.TypeErrorescapesmain(), and nothing is printed to stderr.TypeErroris re-raised, after:8. An
org/reposource, and the code leg's repository cannot be created (#188 N4, re-verification R3). This runs in simulated GitHub mode, asexecute --plan <T>/adoption-plan.yaml --yes --source rv188org/Northwind --work-dir <T>/W, with the code leg'sgh repo createrefused (HTTP 502).REFUSED a git or gh command failed. THE EXACT COMMAND WAS: gh repo create rv188org/Northwind-code …, and no notice.The same
--work-dir <T>/Wagain givesREFUSED source-unclonable: … destination path '<T>/W/source-readonly/Northwind' already exists, on both trees. This branch's notice followed as written (the spec repository deleted, the cause gone) verifies: with a fresh--work-dir, exit 0, and with none, exit 0.9. With main's 06b5204: a name stdout cannot encode. Every path is under
<T>/日本 run. adopt-project.py's own stdout is cp1252 (strict, and Windows' default surrogateescape), and git-filter-repo keeps UTF-8.bare <T>/日本 run/remotes/Northwind-spec.git) and stderr 0 bytes. This branch exits 2 with the REFUSED block and the whole notice on stderr.<T>/日本 run/workholdsNorthwind-spec,Northwind-codeandNorthwindfrom this run …", the branch line andgit -C '<T>/日本 run/Thing' push '<T>/日本 run/Thing' --delete adopt/three-repo-shape.日本is printable, so_spelledkeeps it, and stderr'sbackslashreplaceescapes it.日本is read back as日本.UnicodeEncodeErroratprint(f" bare {bare}"), with one leg repository made and no notice.10. With main's 60d08a1: a refusal that names a non-ASCII path. The hook refuses the spec leg's
mainwithruleset: Übersicht.md changes must arrive by pull request, and the command runs underLC_ALL=C PYTHONUTF8=0.REFUSED pushing the spec leg., git's output (remote: ruleset: \xdcbersicht.md …), the scaffold's two exits, and no notice.REFUSED extracting the spec leg. UnicodeDecodeError: 'ascii' codec can't decode byte 0xc3 in position 17: ordinal not in range(128)and the notice, with no hint.11. With main's aaa40eb: a
.gitmodulesthe history makes unreadable. This uses main's own fixtures. The history adds and removesvendor/b, the tip.gitmodulesis[submodule "b"]with a barepath, and the plan sends.gitmodulesto code.checkexits 1 withFINDING plan-gitmodules-unreadable: ….executeexits 2 withREFUSED plan-gitmodules-unreadable: 1 finding(s) …and "Remediation: no leg repository was created and nothing was pushed.". Its stdout is 0 bytes and the remotes directory is never made, so there is no notice. The two logs are identical once commit names are masked.checkprintedplan ok, andexecutemade both legs and died ingit filter-repo(error: missing value for 'submodule.b.path'). This branch reported that with the notice.12. With main's c2ff2b0: a
.gitmodulesvalue git's submodule reader refuses. This uses main's own fixtures for #215: a gitlink at the tip andupdate = bogusin.gitmodules, and the plan sends.gitmodulesandvendor/to code.checkexits 1 withFINDING plan-gitmodules-unreadable: git cannot read the source's .gitmodules for its submodules (invalid value forsubmodule.b.update:bogus, which is not one ofcheckout,rebase,merge,none), ….executeexits 2 withREFUSED plan-gitmodules-unreadable: 1 finding(s) …and "Remediation: no leg repository was created and nothing was pushed.". Its stdout is 0 bytes and the remotes directory is never made, so there is no notice. The two logs are identical once commit names are masked.checkprintedplan ok, andexecutemade both legs and died in the extraction:13. With main's f795cf7: a local source under a directory whose name is not UTF-8. The source is
<T>/caf\xe9/Thing, where 0xE9 is a byte that is not UTF-8.planandcheckexit 0 on every tree.executeexits 2, prints nothing on stdout, and makes no remotes directory:It is raised before any leg exists, so there is no notice. The two logs are identical once the source's commit is masked.
executemade and pushed both legs, andwrite_lfraised while the split was committed. Exit 2:UnicodeEncodeErrortraceback fromwrite_lf, and no notice.14. With main's a983e6a:
gh pr createrefused. GitHub mode, built with main's own helpers from tests/test_adopt_github_exits.py: a stubghthat refusespr create,https://github.com/rewritten to bare repositories, a bogus token and an empty config directory. Stdout and stderr are merged.adoption verified, thenThe branch IS pushed. Open the pull request by hand:and the command. Here the REFUSED block comes after the split line, because stdout is flushed first; on main it is the log's first line.No pull request was opened, and none should be: the split above did not pass verification.This branch prints its notice and the split branch'sgit ... --deletebefore that line._tree_ofraisingOSError: [Errno 28] No space left on deviceonce_verifyhas begun): main exits 1 with a traceback, and says nothing of the pushed legs or split. This branch exits 2 withREFUSED verifying the split. OSError: [Errno 28] No space left on device, the notice, the split branch and its deletion, and then the no-pull-request line.Tests
New files.
tests/test_adopt_filter_repo_failure.py, 11 tests, from adopt-project.py:executeprints the command and its stderr when a leg cannot be built, so agit filter-repothat fails after both leg remotes exist no longer exits 2 with an empty stderr (#171) #184, reworked. They cover:leg-remote-existsand the GitHub name-taken re-run, from the code that makes them true;git-filter-repofirst on PATH exits 1, whichcheckcannot foresee (adopt-project.py:executeprints the command and its stderr when a leg cannot be built, so agit filter-repothat fails after both leg remotes exist no longer exits 2 with an empty stderr (#171) #184 P1-1);adoption verified. Both work directories have a space in their names, and the test reads the two leg repositories between the notice's backticks.tests/test_adopt_late_failure.py, 86 tests:OSErroron the split.STOPPED). A defect against an interrupt, at four points (R2).--source testorg/Thing --work-dir Wrun throughcmd_execute, where the code leg's repository fails (R3).mainis left to report. The flushes, and a closed stdout. Theghsteps' by-hand exits.adopt-work-dir-occupiedfor a file, a directory and the split, and never for a default, new or empty work directory.adopt-work-dir-occupied's reason for each builder, throughcmd_executeon a code-only source with--allow-empty-leg spec: an extracted leg, the split, the seeded leg, the seeded leg and the split, and an extracted leg and the split (one reason, given once). A seeded leg's directory holding only an ignored.DS_Store, an empty subdirectory or an empty repository is refused by the same rule (N-2).adopt-branch-exists: its printed command run as pasted, from a source path with a space; its precedence belowleg-remote-exists; asked of the assembly's URL; not refused for a branch at or behind the source commit.adopt-work-dir-occupiedandadopt-branch-existsname is spelled (adopt-project.pycheckandexecuteprint a source path raw in a FINDING, so a\r,\n,\x85, U+2028 or ESC in a name overwrites, splits or escapes the line on a terminal, whereplan's questions spell the same path with_spelled#212): three tests, each over the five characters main's tests/test_adopt_finding_spelling.py asks of a finding (a carriage return, a line feed, U+0085, U+2028 and ESC). Each message is one printable line that holds the spelled path, and the deletion stays the commandshreads. They run in-process on POSIX paths that are never made, so every platform runs them.> log 2>&1: a refused split push and its way out; a refused leg push; a failed verification and its way out as review F1 followed it; and a branch at the source commit that is pushed over and verifies.Changed.
tests/conftest.py:run_script(..., merge_stderr=True), andspelled(), the expectation for a path these messages name.tests/test_adopt_leg_path_collisions.py: adopt-project.py: the leg mount passes--forcetogit submodule add, so a source whose.gitignoreis*+!.gitignore, or namesspecorcode, adopts instead of dying at the mount after both leg remotes were pushed (#174) #200'srefused_at_the_mountexpected the guard'sRefusalto escapecmd_execute. Here it is reported, andcmd_executereturns 2. So the helper records the refusal the guard raised, and asserts the return value 2 and the reported block with the notice last. The next-step test also asserts that a re-run into the same work directory isadopt-work-dir-occupiedand makes no leg.tests/test_adopt_verify.py(main's, 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): its fixture passes theurlsandbranchthat_verifynames in its way out. One assertion reads the merged footer, "… fix the plan and re-run, unless a finding above names another exit."tests/test_adopt_ignored_shape_files.py:PLAN_ANSWERreads the shape-file finding's new exit. The end-to-end test also reads that the way out comes after the finding and ends with the branch's deletion.tests/test_adopt_root_shape_paths.py(main's, 71b58a1):test_with_check_skipped_execute_stops_at_the_copy_and_overwrites_nothingexpected the materializer'smaterialize-copy-blockedto escapecmd_execute. Here it is reported, andcmd_executereturns 2. Sorefused_in_the_materializerrecords the refusal the materializer raised, and asserts the return value 2, the reported block, and the notice last. Main's own assertions are kept: the code, the detail, no split branch, the source's file as it was, and both legs pushed.tests/test_adopt_clone_template.py(main's, e43fa95): itsverifyhelper passes theurlsandbranchthat_verifynames in its way out, so the 12 tests that call it directly run. Every assertion stands as main wrote it.tests/test_repo_hygiene.py(main's): the AGENTS.md cap is 482, with a dated entry: step 7's exit 1 way out is one line longer here (c2f3118).Counts at bbdd313, both modes,
/usr/bin/python3 -m pytest, run in separate clean trees.~/.local/bin, socommand -v git-filter-repofinds nothing and git's exec-path has none): 3318 passed, 167 skipped.The set is 54 files: the 40 test files that load adopt-project.py (by its name,
ADOPTor theadopterfixture), and fourteen of main's test files for what else this branch touches (conftest'srun_scriptand itsghguard, shape-doctor.py, which loads adopt-project.py, and repo_shape.py's reader, which loads every plan).Revert tables. Every new or changed test fails with its change undone. Each row undoes ONE change in a clean tree, with every anchor asserted to occur exactly once.
This round, measured at 918e8f2. Merges 922f2d8 and e162551 change no line of this branch's own diff, so these rows stood at e162551. Each row runs tests/test_adopt_ignored_shape_files.py, tests/test_adopt_late_failure.py and tests/test_adopt_filter_repo_failure.py: 119 tests, which all pass unmutated. The SP rows undo e507c42 (#212), one path at a time. The others re-measure the rows of 5a30b77 (SFE, FOOT), 48e93bb (R2a, R2b), 0d2256e (R4), and 03b381a and 5a8b649 (R3a-R3j). The tests each row fails are in the lane's log; every row but BASE fails at least one.
SHAPE_FILE_EXITback to main's "re-run into fresh legs"LATE_FAILURESis told only to re-run, as before_cloned_herenever counts the cloneSource.opennever records that it cloned_create_leg_remotes' notice not handed the source_build_the_legs' refusal handler not handed the source_build_the_legs' other handler not handed the source_commit_the_split's refusal handler not handed the source_commit_the_split's other handler not handed the source_left_by_the_splitdoes not hand on the sourcecmd_executedoes not hand_create_leg_remotesthe sourceadopt-work-dir-occupied's work directory printed rawadopt-work-dir-occupied's names printed rawadopt-branch-exists' assembly printed rawadopt-branch-exists' branch printed raw9d8aadd, measured at e162551. Each row runs
test_with_check_skipped_execute_stops_at_the_copy_and_overwrites_nothing, which passes unmutated:DID NOT RAISE Refusal_report_lateprints the notice before what failed_failed_loudlydrops a refusal's own remediation90d6077, measured at the merge's tree 9570d24. With main's
verifyhelper as main wrote it, the 12 tests of tests/test_adopt_clone_template.py that call_verifydirectly fail withTypeError: _verify() missing 2 required keyword-only arguments: 'urls' and 'branch'.9be2b05, measured at 9be2b05. Each row runs the occupied-directory tests and the spelling tests of tests/test_adopt_late_failure.py, 17 tests, which all pass unmutated:
git clonealonecmd_executehands the refusal no seeded legc2f3118, measured in trees at the merge f4ca72a with it applied (the cap test, the gh-step test's four cases and both step 7 tests, 7 passing): the cap back to 481 fails the cap test; the
_open_it_by_handcall removed fails the two[...-pull-request]cases. The joined step 7 is held from both sides: main's text alone fails this branch's step 7 test, and this branch's old text alone fails main's.92c72c1, measured in trees at bbdd313 with it applied (the 13 occupied-directory tests, all passing): the old sentence back fails 5, the two seeded reasons and the three new cases;
_holds_somethingmade to skip.DS_Store,.gitand empty directories fails the three new cases.The confirmation check at 918e8f2 (rv188f) added rows of its own. Fourteen targeted rows cover the 28 tests of the two new files that no row above fails: GHSTEP, SPLITOS, LIVELEG, TABLE, GHASK, HOLDS, HOLDS2, SUPPRESS, LEGREPORT, AGENTS, PUSHSEED, REMED, BUILDPRINT and GHFIRST. Every named test fails in every row.
Earlier rounds, as measured then. Counts only; each row undoes one change in a clean tree, and every test it names passes unmutated.
Platforms. CI has not run on this branch yet; it runs on pull requests. The Windows job installs no git-filter-repo, sets
PYTHONUTF8=1, and honoursWINDOWS_SKIP. With git-filter-repo hidden, this branch's tests give:tests/test_adopt_late_failure.py: 82 of 86 run on windows-latest. All the in-process ones run. Among them are the#!/bin/shpre-receive hook intest_a_refused_leg_push_offers_an_adoption_its_one_exit, which Git for Windows runs through its own sh, and the pastedgit -C … --deleteintest_a_split_branch_left_behind_is_refused_before_any_leg_exists. They also include R2's 8, R3's 9, the work-directory refusals (4 refused, 3 never refused), the 2 fast-forward cases, the 15 spelling tests, the 5 occupied-directory reasons and the 3 cases of N-2, and AGENTS.md step 7. The 4 that skip are the git-filter-repo end-to-end tests:test_a_refused_split_push_is_logged_after_the_legs_and_its_way_out_runs;test_a_branch_at_the_source_commit_is_pushed_over_and_verifies;test_a_refused_leg_push_is_logged_after_the_legs_with_one_exit;test_a_failed_verification_is_flushed_and_its_way_out_runs.tests/test_adopt_filter_repo_failure.py: 9 of 11 run.test_a_failed_git_filter_repo_names_itself_on_stderrputs a#!/bin/shstub on PATH, so it carriesWINDOWS_SKIP, liketests/test_free_plan_hint.py's stubgh.test_following_the_notice_after_a_failed_leg_ends_adoption_verifiedneeds git filter-repo.tests/test_adopt_verify.py: the 14 direct_verifytests, whose fixture this branch changed, run. Its 2 end-to-end tests skip.tests/test_adopt_ignored_shape_files.py: the 6 in-process tests that readPLAN_ANSWERrun. Its end-to-end test skips.tests/test_adopt_leg_path_collisions.py: the 3 changed mount-guard tests need git filter-repo and skip.tests/test_adopt_root_shape_paths.py: the one changed test needs git filter-repo and skips.tests/test_adopt_clone_template.py(main's): 14 of 31 run. The 12 tests that call the changedverifyhelper run in-process, and nothing in the file carriesWINDOWS_SKIP. The 17 that skip need git filter-repo.Paths as windows-latest spells them.
_spelledescapes a backslash, which is Windows' path separator. So a test that reads a path raw (str(path) in err, or a path between backticks without the escape) passes on Linux and fails on Windows. On Linux a backslash is an ordinary file-name character, so the tests were run again with one in every temporary path (--basetemp '…/bt-bs/head\win'), with git-filter-repo hidden as on windows-latest:operatorhelper writestemplate.as_posix()into a git config file. On Linuxas_posix()keeps the backslash, which is a file-name character there, and git reads it as an escape:fatal: bad config line 2. On Windowsas_posix()turns the separator into/. All 3 stop at that git call, before theverifyhelper this branch changes.;That run catches the mistake: with
work_dir_namedmade to expect the raw path between backticks, 8 tests pass with a plain temporary path and the same 8 fail with the backslash.No added line feeds text to git's stdin. Expected paths are built from
Pathobjects, and the AGENTS.md test reads in text mode.Not verified.
gh repo create, an organisation ruleset, andgit ls-remoteagainst a GitHub URL. A local path or the simulator stands in for them.Reviews
Lane openRepoShape-2 reviewed PR #184 at f416e13 (comment):
.gitmodulesto one leg and a submodule it registers to another passescheckandexecute, saysadoption verified, and the assembly's recursive clone then fails:planshould ask,checkshould find,executeshould refuse #166 lands: taken in d07e94a. The test now uses the stubgit-filter-repo.--work-dir: taken in d07e94a, and closed for every way out by 5dbda31'sadopt-work-dir-occupied.CommandFailedreported: taken in d07e94a.REFUSED <step>with no notice of what already exists and no stdout flush, so the combined log shows REFUSED before thebare …lines and the re-run meetsleg-remote-existsunexplained #188, this pull request.Lane 2 reviewed this branch at 68d4d63 (comment):
adopt-branch-existsadded.gh repo create: taken in d07e94a. The existing test now asserts the notice.shlex.quote, and the tests parse printed commands withshlex.split, on paths with spaces.The lane's adversarial review at 8d4fef4 (CHANGES NEEDED):
adopt-branch-exists's way out looping on the same--work-dir: 5dbda31 addsadopt-work-dir-occupied, ahead of every other refusal of what an earlier run left, and says "re-run as the end of this report says" in the shape-file finding. cdfb308 adds a test that reads that finding's exit.adopt_branch:sentence read by a test in cdfb308._verifyfailure with no notice: 855886d.leg-remote-exists' precedence.adopt-branch-existscomes before any leg check: left. It is a true refusal that makes nothing, and its remediation now says to review a verified run's pull request instead of re-running. The re-run it leads to meetsgh repo createbefore anything is made.--forcetogit submodule add, so a source whose.gitignoreis*+!.gitignore, or namesspecorcode, adopts instead of dying at the mount after both leg remotes were pushed (#174) #200's own, and the notice is common to every late failure.The re-verification at cdfb308 (CHANGES NEEDED on R1):
adopt-project.py executeexit is to fix the plan and re-run into fresh legs, but in GitHub modeexecuteexits 2 with the split pushed whengh pr createis refused, and exits 2 afteradoption verifiedwhen the topic will not set, so following step 7 abandons a pushed adoption whose own exit is a command to run by hand #304, which main fixed in a983e6a (step 7 keyed to the exit, and a refusedgh pr createverified before the run exits); f4ca72a merges that, with this branch's exit 1 way out joined into step 7.The confirmation check at 918e8f2 (rv188f: PR-READY, no BLOCKER and no SHOULD-FIX):
adopt-work-dir-occupiedgivinggit cloneas the reason for a seeded leg, which is never cloned: 9be2b05.rv188f's round H review (2fc1669, then 918e8f2..bbdd313: CHANGES NEEDED in this body only; its run at bbdd313, 3480 passed and 5 skipped, matched):
Supersedes PR #184
This pull request supersedes PR #184. It carries #184's three commits for #171 (5cd06c7, 41ab8ca, f416e13), then the work for #188 and #177 on top of them.
Lane openRepoShape-2 found that #184 cannot land on its own:
.gitmodulesto one leg and a submodule it registers to another passescheckandexecute, saysadoption verified, and the assembly's recursive clone then fails:planshould ask,checkshould find,executeshould refuse #166 landed (e4decfb), its one end-to-end test fails atcheck(review P1-1).Lane 2 suggested folding #184 into this pull request (comment). PR #184 can be closed in favour of this one.
Closes #171
Closes #188
Closes #177
🤖 Generated with Claude Code