Skip to content

adopt-project.py: check asks the split's own clone of the source in the work directory, so a clone this git will not make is adopt-git-unasked before any leg, and _resolved and _answers make their repositories as check's other throwaways do (#1083) - #1119

Merged
brettheap merged 12 commits into
mainfrom
fix/1083-check-asks-the-split-clone
Oct 23, 2026

Conversation

@brettheap

@brettheap brettheap commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

What

adopt-project.py check asks this machine's git what the split will make of
the source in throwaway repositories in the work directory (#910). Two
questions were still asked otherwise, and on git 2.43 both stopped short of
the answer execute meets.

Item Change
(a) the split's own clone check now makes the split's own git clone of the source first, in a throwaway directory in the work directory: git -c protocol.file.allow=always -c core.hooksPath=<empty> clone -q --no-checkout --shared <source> <throwaway>/repository (_split_clone_asked). The protocol setting is FILE_PROTOCOL, the one execute gives each of its clones, and the operator's template is copied in, as _extract_leg's clone copies it. Its environment is _split_clone_env(): _inherited_env(), plus the -c settings a parent git hands this process (GIT_CONFIG_PARAMETERS), as execute's clone takes them. Left out are the checkout (the rehearsal and _paths_git_refuses ask it), this machine's hooks, and every variable that names another repository, its objects, an index or a work tree, which the clone would write into. Where git will not make the clone, that is one adopt-git-unasked finding in git's words, the questions after it are not asked, and execute refuses it before it makes either leg. Where git makes it, nothing changes.
(b) _resolved and _answers Their repositories are now made the throwaway way, through _throwaway and _throwaway_made. That means the work directory, no hook (-c core.hooksPath=<empty> on every command), no fsmonitor program or daemon (-c core.fsmonitor=false, as the rehearsal's and #619's commands have it; r1119b's SHOULD-A), --template=<empty>, -b throwaway and the source's object format, all under _inherited_env(), read before the throwaway is made. Before, it was TMPDIR and a plain git init with the caller's whole environment. A git step that fails in one is adopt-git-unasked in git's words, through the new _throwaway_git, never "a git or gh command failed". _submodule_findings asks no url question once a throwaway in the work directory could not be made or asked. Where a url question itself cannot be asked, the url findings asked before it are kept, and that _Unasked becomes the one finding for the throwaways. _resolved takes source as a positional fourth parameter and _answers takes source=None, so the existing rows that patch or call them keep working.
(c) docstring tests/test_adopt_reencoded_files.py::test_a_link_the_checkout_writes_is_never_written_through said the throwaway is in TMPDIR. It now says the work directory; both are as deep below tmp_path, so the row still asks the same thing. (The issue's adopt-project.py:466 names _pin_row_problem at 74c1770; this docstring is the one PR #1031's body named.)
(d) main's ref-probe row tests/test_adopt_root_symlinks.py::test_check_says_where_git_would_not_make_the_ref_probe (#1091) set GIT_DEFAULT_HASH=bogus to fail the ref probe's git init. Merged with this branch, that variable stops the split's own clone first, so check said adopt-git-unasked and exited 1 where the row wants 0 (r1119b's MUST-2). The row now puts a stub git first on PATH that fails only a git init of a refs-probe-* directory, in git's words (fatal: unknown hash algorithm 'bogus', exit 128), and hands every other command to the real git. It is skipped on Windows, as the other stub rows are. It passes on main's tool and on this head.

Covers #1032, which lane 4 released into this issue: item (b) is that work.

The clone's cost. With --shared the clone copies no object of a source
that is not shallow: it borrows the source's. git ignores --shared for a
shallow source, and says nothing, so that clone is a full copy of the
source's objects. r1119 measured a shallow copy of a 100k-file source with a
115 MiB pack: 7.3 s and 10.2 s, and 117 MiB. That is a cost, not a fault:
each of execute's own clones of such a source is a full copy too. For a
source that is not shallow, r1119 measured 0.06 s and 0.1 MiB (openRepoShape)
and 0.8-1.6 s and 1.2 MiB (100k files, 20,001 refs).

Such a clone also runs the uploadpack.packObjectsHook that this machine's
global or system config names, to pack what it copies, as each of execute's
clones of that source runs it (r1119b's NIT-C). Measured on git 2.43 with a
shallow source and a counting hook:

  • in the global config: 1 run in check, and 1 in a clone made as _extract_leg makes it;
  • in the source's own config: 0 and 0;
  • handed down by a parent git -c: 0 and 0.

Measured

check --work-dir and execute --local-remote-dir against a fresh source, on
git 2.43.0, with a stub gh. Before is main 2a119f28 (its adopt-project.py
is unchanged through 16f9a0e8), after is this branch.

Case Before: check / execute After: check / execute
init.defaultBranch = a..b in the global config exit 0 plan ok / exit 2 REFUSED extracting the spec leg, fatal: invalid branch name: init.defaultBranch = a..b, after Northwind-spec.git and Northwind-code.git exist; nothing rolled back exit 1 FINDING adopt-git-unasked, "the split's git clone of the source exited 128 there: fatal: invalid branch name ..." / exit 2 REFUSED adopt-git-unasked; remotes/ absent
the same setting handed down by a parent git, GIT_CONFIG_PARAMETERS="'init.defaultbranch'='a..b'" the same as the row above (and so on bdcf9952, this branch's first head) the same as the row above
the same setting in GIT_CONFIG_COUNT the same as the first row the same as the first row
GIT_DEFAULT_HASH=bogus exit 0 plan ok / exit 2 "a git or gh command failed", fatal: unknown hash algorithm 'bogus', with Northwind-spec.git left behind exit 1 FINDING adopt-git-unasked ("unknown hash algorithm 'bogus'") / exit 2 REFUSED adopt-git-unasked; remotes/ absent
a kept submodule, init.defaultBranch = a..b exit 2 "a git or gh command failed" at git init -q <TMPDIR>/openreposhape-url-* / the same exit 1 FINDING adopt-git-unasked / exit 2 REFUSED adopt-git-unasked; remotes/ absent
a kept submodule, GIT_DEFAULT_HASH=bogus exit 2 "a git or gh command failed" at the same git init / the same exit 1 FINDING adopt-git-unasked / exit 2 REFUSED adopt-git-unasked; remotes/ absent
a kept submodule, counting hook named by core.hooksPath in the global config 2 post-index-change runs (_resolved's update-index) 0 runs; the rest of check's output is byte for byte the same
a kept submodule, counting core.fsmonitor program in the global config or in GIT_CONFIG_COUNT 8 runs, 4 in each of _resolved's two repositories: in TMPDIR over main 038dafba's tool, and in the work directory at 9b998936 0 runs; the same findings
a source with no submodule, the same hook 0 runs 0 runs
GIT_OBJECT_DIRECTORY naming the source's own objects plan ok; the source's objects/info/ stays empty the same. The clone in the whole environment (mutant M6) wrote objects/info/alternates into the source.

On git 2.55, which CI runs, git clone takes init.defaultBranch = a..b (CI
run 39404718064 on #1031). There the clone question is no finding and every
other question is asked. The rows below follow this machine's git, or a stub,
and lean on no version.

Also measured, on git 2.43, for the clone's flags and environment:

  • with protocol.file.allow = never in the config, the clone exits 128 (transport 'file' not allowed) without FILE_PROTOCOL and 0 with it, as execute's does;
  • a shallow source, and a source that is itself a --shared clone, clone with --no-checkout --shared (exit 0);
  • a parent's -c core.hooksPath=<counting hooks> runs no hook in the clone, because git reads the clone's own -c core.hooksPath after the parent's and takes a key's last value. Without the clone's own setting, the parent's ran reference-transaction 4 times;
  • an attr.tree=bogus among a parent's settings, which is why _inherited_env() leaves them out of the questions a checkout answers, still lets the clone through (exit 0);
  • GIT_ALTERNATE_OBJECT_DIRECTORIES and GIT_SHALLOW_FILE, which _inherited_env() keeps, write nothing outside the clone. A non-empty file in GIT_SHALLOW_FILE stops the clone, as it stops execute's.

Tests

  • tests/test_adopt_check_throwaway_repos.py: 70 rows at 195d9936, and 90 on the merge head e0a98f13, which holds main's 20 rows of adopt-project.py: refused_paths' update-index -z --index-info is untested with core.ignorecase and case twins (a and A) on a case-insensitive disk #1033 as well (see "The merge of main" below). 2a119f2 had 29, and one of them is split in two (below).
    • 42 new rows, the split's two among them:
      • (a) 7;
      • (b) 8, _throwaway_git's failure among them;
      • (c) 2: NIT-R2-2's missing work directory, and the case twins;
      • r1119's review, 18 rows:
        • each form a setting can reach the clone in: a global config file, GIT_CONFIG_COUNT and GIT_CONFIG_PARAMETERS. There are real-git rows under init.defaultBranch = a..b for check and execute, and stub rows for both. The stub git refuses a git clone whose config sets a test key, so they hold on every version of git;
        • _split_clone_env()'s own row: what it takes and what it leaves out;
        • git-config-parameters for the two hook rows;
        • GIT_OBJECT_DIRECTORY naming the source's objects (SHOULD-1);
        • GIT_DIR naming the source, whose own config rewrites the on-disk url. check says what it says without GIT_DIR, and the source's config is unchanged;
        • an environment read that raises leaves no throwaway, for _resolved, _answers and the clone (NIT-3).
      • r1119b's review, 7 rows:
        • a counting core.fsmonitor program runs in none of check's throwaways, from the global config or GIT_CONFIG_COUNT, with a control for each that runs it for a git add (SHOULD-A, 4 rows);
        • --work-dir inside the source, whose config rewrites the on-disk url to a directory that holds nothing or to a network. check says what it says with the work directory outside, prints no NOTE of a url it did not ask, and leaves the source's config as it was (NIT-A, 2 rows);
        • the same rewrite handed down by a parent git in GIT_CONFIG_PARAMETERS, which _answers leaves out, as its docstring says (NIT-B). Its control shows git expands the url so under it.
    • Changed assertions:
      • the work-directory row expects the clone's directory there too;
      • the unmade-work-directory NOTE names the submodule urls (NIT-2), in two rows;
      • _split_clone_env()'s row appends its two GIT_CONFIG_COUNT entries to those the environment holds, and asserts that the last entry kept is its own and that no attr.tree is kept. Built from {}, it failed tests/test_no_auto_maintenance.py::test_no_test_sets_a_git_config_count_of_its_own (r1119b's MUST-1).
    • url_plan and the GIT_DIR row now build their source and plan through two shared helpers, and ask what they asked.
    • Two docstrings changed with no assertion.
    • SonarCloud's new-code duplication read 3.8% at bba168bd (45 of 1,197 new lines; the gate is 3%), from two pairs of repeated test bodies in this file, 10 and 14 lines each. 195d9936 is test-only and factors each pair into one module-level helper in the same file, which takes the function that builds the environment:
      • check_asks_the_clone_a_default_branch, for the check rows under init.defaultBranch = a..b: test_a_default_branch_is_asked_of_the_splits_own_clone (global config) and test_a_parent_gits_default_branch_is_asked_of_the_splits_own_clone (its two parent-git forms);
      • execute_refuses_a_default_branch_its_clone_will_not_take, for the execute rows: test_execute_refuses_a_default_branch_its_clone_will_not_take and test_execute_refuses_a_parent_gits_default_branch_before_any_leg (its two parent-git forms).
      • No test id is renamed, added or removed, and no row asserts or sets up anything it did not.
      • Counted by hand over the lines this PR adds to the file, a run of 10 or more identical lines (leading space ignored) repeats in 54 new lines at bba168bd (46 with blank lines left out), and in none at 195d9936.
    • The 2a119f2 row test_a_default_branch_git_will_not_take_stops_no_question is split in two:
      • test_a_default_branch_is_asked_of_the_splits_own_clone follows this git's own clone;
      • test_a_default_branch_git_will_not_take_stops_no_throwaway_init asks the rehearsal alone, so the -b of every throwaway git init stays pinned on git 2.43.
  • tests/test_adopt_root_symlinks.py: main's ref-probe row, item (d).
  • On 195d9936: 70 passed, and the ref-probe row passes.
  • Over 195d9936, the duplication commit: test_adopt_check_throwaway_repos.py gave 70 passed, 0 skipped, the same 70 ids with the same outcomes as bba168bd (70 passed, 0 skipped), each run in a clean clone. test_path_guard_hygiene.py and test_repo_hygiene.py together: 149 passed.
  • Over d0bc40ce's tool, this round's base (9b998936 merged with main 038dafba): 2 failed, 69 passed, of the 70 rows and the ref-probe row. The 2 are the fsmonitor rows, 8 runs each.
  • Over main 038dafba's tool: 33 failed, 38 passed.
    • 30 of the 33 are the 30 that failed over 16f9a0e8's tool in the last round, row for row. Each fails for its own reason: plan ok where the finding is due, DID NOT RAISE Refusal from execute, url repositories made in TMPDIR, two hook runs, an uncaught _Unasked, or a function or parameter that main does not have.
    • The other 3 are this round's: the two fsmonitor rows (8 runs each, in TMPDIR), and the parent-rewrite row (main's _answers ran in the whole environment, read the rewrite, and found the url answering nothing).
    • This round's two controls pass there. So do the two inside-the-source rows, which pin the mutant R8 rather than a change of this PR, and the ref-probe row.
  • The rows that need git to refuse a clone or a git init use a stub git, first on PATH, and are skipped on Windows. One stub refuses every git clone; another refuses one whose config sets a test key; the ref-probe row's fails only the probe's git init.
  • The real-git execute rows under init.defaultBranch = a..b are skipped where this git's clone takes the name, as 2.55's does.
  • Case twins under core.ignorecase. No case-insensitive disk is at hand. The row is still sound: _paths_git_refuses hands paths to update-index --index-info and reads ls-files, which read no file of the disk, only the setting. So it sets core.ignorecase=true on a disk that holds both twins.
    • Its control shows the setting is live: git add of the twins under it holds at most one (git 2.43 says will not add file alias).
    • --index-info holds both, so neither twin is named.

Mutants

First round, at bdcf9952: 16 mutants of the behaviour changes, each run over the new file (and M15 over the url files too). 13 were killed and 3 survived:

  • M1-M5, M7-M13 and M16 were killed, by the rows the table of that round named. They were not re-run on this head.
  • M6 and M14 survived. They were killed in the second round.
  • M15, git submodule init without the hook setting, survives: 73 rows of the url files pass. It is equivalent, as git submodule init runs no hook.

r1119's round, at 9b998936: 11 mutants, each run over the file with -x, and the tool restored and sha-checked after each. 11 killed.

Id Mutant Killed by
M6 the clone in the whole environment the GIT_OBJECT_DIRECTORY row
M6b _split_clone_env() is the whole environment _split_clone_env()'s row
N11 the clone's environment leaves out GIT_OBJECT_DIRECTORY alone _split_clone_env()'s row
N1 the clone takes no GIT_CONFIG_PARAMETERS, as at bdcf9952 the real-git git-config-parameters row (the stub rows too, measured over bdcf9952)
N8 the clone without its own -c core.hooksPath each of the three hook rows, git-config-parameters among them
M14 _answers in the whole environment the GIT_DIR row: the source's url rewrite gave plan-submodule-url-answers-nothing
M14r _resolved in the whole environment the GIT_DIR row. Measured, it wrote [remote "origin"] and [submodule "u0"] into the source's own config, and check said plan ok
N4 _resolved reads its environment after making its throwaway the environment-read row, resolved
N5 _answers does the same the environment-read row, answers
N6 the clone does the same, outside its try the environment-read row, split-clone
N7 the unmade NOTE names no submodule url the NOTE row

r1119b's round, on this head's tool before its last docstring edit (cea25530, 0b326952): 18 mutants, each run with -x over the rows named, and the tool restored and sha-checked after each. 17 killed; S4 survives as equivalent. A5, A6, A6b, A15, A2b, R8 and A18 are r1119b's own, re-anchored on this head.

Id Mutant Killed by
A5, A6, A6b _split_clone_env() hands back GIT_ATTR_SOURCE, GIT_INDEX_FILE or GIT_WORK_TREE _split_clone_env()'s row
A15 GIT_CONFIG_PARAMETERS always set, empty where the parent sets none _split_clone_env()'s row
A2b the clone's environment drops the GIT_CONFIG_COUNT form _split_clone_env()'s row
KA1 _inherited_env() keeps attr.tree _split_clone_env()'s row
KA2 the clone alone takes the command scope back as it stands, attr.tree and all _split_clone_env()'s row
MR1 the ref probe's CommandFailed stops check the ref-probe row
MR2 the ref probe's failure is taken with no NOTE the ref-probe row
MR3 the probe's directory named otherwise, so the stub misses it and git makes it the ref-probe row: no NOTE
S1 no core.fsmonitor=false, as at 9b998936 the fsmonitor row
S2 core.fsmonitor=false for the throwaway's git init alone the fsmonitor row
S3 _resolution_of's git submodule init without hooks_off the fsmonitor row
R8 _answers' two ls-remotes run in the throwaway's own directory the inside-the-source row, a-directory-holding-nothing; and first over the whole file with -x
R8a only its ls-remote --get-url there the inside-the-source row, a-network: the NOTE of a url on a network not asked. It survived the first form of the row, and the a-network case was added for it
R8b only its other ls-remote there the inside-the-source row, a-directory-holding-nothing
A18 _answers takes _split_clone_env(), a parent's -c too the parent-rewrite row; and first over the whole file with -x
S4 _answers' ls-remotes without hooks_off survives. It is equivalent: ls-remote reads no index and writes no ref, so it runs no hook and no fsmonitor program.

Across the three rounds, 43 distinct mutants: 41 killed, and M15 and S4 survive as equivalent. Of r1119b's own list on 9b998936, R8 and A18 survived there and are killed now. R2 (no --shared, which changes only the cost), R3 (the clone after _paths_git_refuses, whose finding wins either way) and A17 (_resolved takes a parent's -c, under which the throwaway's own hook setting still wins) were judged no defect, and stay so.

Gate

On bba168bd, which merges main 038dafba (#1091, #1111), with a clean tree: 18 batches, 3,413 passed, 0 failed, 8 skipped. The head since is 195d9936, which changes one test file and nothing the tool reads; the rows it re-ran are named under Tests.

The environment was a stub gh, GH_TOKEN bogus, an empty GH_CONFIG_DIR, GIT_CONFIG_GLOBAL=/dev/null, GIT_SSH_COMMAND=false, LANES_LANE unset and EMAIL exported. Each batch had its own basetemp with retention failed, each pytest call ran under 530 s, and at most 4 ran at once.

  • test_adopt_check_throwaway_repos.py, test_adopt_relative_submodule_url.py and test_adopt_reencoded_files.py: 157.
  • test_adopt_nested_attributes.py and test_adopt_empty_symlinks.py: 132.
  • test_adopt_root_symlinks.py, in 2 shards: 205 passed, 3 skipped.
  • test_adopt_symlink_target_bytes.py: 130.
  • test_adopt_execute_refusals.py: 181.
  • test_adopt_entry_paths.py, test_adopt_literal_names.py, test_adopt_project_is_the_source.py, test_adopt_split_branch.py and test_windows_paths.py: 209.
  • test_adopt_submodules.py: 167.
  • test_adopt_history_gitlinks.py and test_adopt_submodule_url_answers.py: 67.
  • test_adopt_manifest_values.py and test_adopt_leg_path_collisions.py: 177.
  • test_adopt_plan_yaml_quoting.py: 540.
  • test_scaffold_leg_path_collisions.py, test_repo_hygiene.py, test_agents_md_codes.py, test_readme_windows_quoting_sentence.py, test_cli_reference.py, test_adopt_work_dir_refusal.py, test_adopt_clone_claimed.py, test_no_auto_maintenance.py and main's test_path_guard_hygiene.py: 520 passed, 2 skipped.
  • test_adopt_c_locale_bytes.py, test_adopt_check_plan_name.py, test_adopt_pins.py and test_pin_annotated_tag.py: 243.
  • test_adopt_shape_pin_and_work_dir.py, which names the work-dir NOTE, test_visibility_field_words.py, test_scaffold_reuse_seeded_legs.py and test_adopt_root_shape_paths.py, which the merge changed: 240.
  • test_shape_doctor.py, in 4 shards of 112: 445 passed, 3 skipped.

Before this round:

The merge of main

e0a98f13 merges main ef619c74 (#1158, which adds #1033's case-twin rows to tests/test_adopt_check_throwaway_repos.py) into this branch by a merge commit, on top of 195d9936. The merge base is main 038dafba. The push fast-forwarded the branch, and nothing was rebased or forced.

  • One conflict, in that file only: the module docstring, where both sides added a paragraph after "finding, with a NOTE." It was settled by keeping both paragraphs as their sides wrote them, adopt-project.py check: follow-ups of PR #1031 (#910): on git 2.43 an init.defaultBranch git will not take passes check and execute stops after both legs exist; _resolved and _answers still use TMPDIR, hooks and a plain git init #1083's first and adopt-project.py: refused_paths' update-index -z --index-info is untested with core.ignorecase and case twins (a and A) on a case-insensitive disk #1033's second, with a blank line between. Every other hunk of the file merged without a conflict, and no other file conflicted.
  • Both sides' rows, helpers and imports are kept, as written. No assertion was changed and no name is defined twice (an AST check of the file's top-level names: 165 names, 0 repeated).
  • Test ids of that file, collected with --collect-only -q in a clean clone at each commit: 29 at 038dafba, 49 at main ef619c74, 70 at 195d9936, 90 at the merge.
    • Main adds 20 ids and removes none. This PR adds 42 and removes 1: test_a_default_branch_git_will_not_take_stops_no_question, which it splits in two (see Tests).
    • The merge is therefore the union of the two sides (91 distinct ids) less that one id, which main left as it was and this PR replaced. Against that expected set the difference is empty.
  • Run on the merge, in a clean clone at e0a98f13, each in the probe environment (a stub gh, GH_TOKEN bogus, an empty GH_CONFIG_DIR, GIT_CONFIG_GLOBAL=/dev/null, GIT_SSH_COMMAND=false, EMAIL exported, LANES_LANE unset):
    • tests/test_adopt_check_throwaway_repos.py, one call: 89 passed, 1 skipped. The skip is main's test_on_a_disk_that_ignores_case_each_twin_is_held_or_named, which needs a disk that ignores case, as its docstring says.
    • tests/test_path_guard_hygiene.py, tests/test_no_auto_maintenance.py and tests/test_adopt_single_repository_record.py, one call: 146 passed, 2 skipped. The two are rows of test_no_auto_maintenance.py that skip when the session began with GIT_CONFIG_GLOBAL set, as the probe environment sets it.
    • These are the only runs on the merge. The gate above ran on bba168bd. The merge brings in main's changes to scaffold-project.py, scripts/family.py, the v1.2.0 release notes and tests, and leaves adopt-project.py as it was at 195d9936; it imports none of those changed files.

Known limits

It was left out of v1.2.0 by Brett Heap's ruling of 2026-10-21T10:32:04Z. Lane 3 has since put it on milestone v1.2.1, and it lands under Brett Heap's v1.2.1 merge word.

Closes #1083
Closes #1032

Lane: openreposhape-1 (openRepoShape-1)

🤖 Generated with Claude Code

Summary by Sourcery

Align adopt-project.py check with the repositories and environment that execution will use, surfacing clone and throwaway Git failures before any split work begins.

New Features:

  • Make check rehearse the split’s own source clone before evaluating paths or submodule URLs, reporting clone failures as adopt-git-unasked before any leg is created.
  • Run submodule URL resolution and answer checks in isolated work-directory throwaway repositories matching the split’s clone environment.

Bug Fixes:

  • Prevent checks from inheriting repository, object, index, work-tree, hook, or fsmonitor state that could modify the source or produce misleading results.
  • Ensure failed throwaway operations preserve earlier findings and report Git’s failure as a single unasked finding instead of a generic command failure.
  • Keep parent Git configuration parameters relevant to the split’s clone while avoiding unsafe repository-related environment variables.

Enhancements:

  • Disable hooks and filesystem monitors consistently across throwaway repositories and align clone protocol, template, object-format, and branch behavior with execution.
  • Update work-directory and unasked-question handling so URL checks are skipped when required throwaways cannot be created or queried.

Documentation:

  • Update user-facing and test documentation to describe the split clone and work-directory throwaway behavior.

Tests:

  • Add comprehensive coverage for clone failures, inherited Git configuration, repository isolation, hooks and fsmonitor suppression, URL-question ordering, work-directory behavior, and environment-read failures.
  • Adapt the ref-probe test to isolate ref-probe failures from the new initial clone check.

Brett Heap and others added 3 commits October 21, 2026 12:58
…` of the source in the work directory, and `_resolved` and `_answers` make their repositories as #910's throwaways are made (#1083)

Work in progress for #1083, stopped at a usage checkpoint.

Built: `_split_clone_asked` (asked first in `_entry_findings`; a failure is
`adopt-git-unasked`, which `execute` refuses before any leg); `_resolved`
and `_answers` made with `_throwaway`/`_throwaway_made` in the work
directory, no hooks, `--template=<empty>`, `-b`, the source's format,
under `_inherited_env()`; `_throwaway_git`; `_submodule_findings` skips the
url questions once one throwaway could not be asked. 16 rows added or
changed in tests/test_adopt_check_throwaway_repos.py (45 rows: all pass on
this head; 14 fail and 31 pass over 2a119f2's adopt-project.py).

Left: the TMPDIR docstring in tests/test_adopt_reencoded_files.py, the
gate, and the PR body.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e_checkout_writes_is_never_written_through` says the throwaway repository is made in the work directory, as #910 makes it, and no longer in `TMPDIR` (#1083)

The last item of #1083's (c): the docstring still said the throwaway
repository the link is asked in is in `TMPDIR`. Since #910 it is made in
the work directory, which is as deep below `tmp_path` as the `TMPDIR` the
row sets, so the row asks the same question and only its words change.
(The issue's `adopt-project.py:466` names `_pin_row_problem` at 74c1770;
this docstring is the one PR #1031's body named.)

Gate on the merged tree (main 6da757c): see the pull request body.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sourcery-ai

sourcery-ai Bot commented Oct 21, 2026

Copy link
Copy Markdown

Reviewer's Guide

check now rehearses the exact source clone that execute will make and standardizes submodule URL questions on the same work-directory throwaway repositories, reporting Git setup or query failures as actionable adopt-git-unasked findings before any split legs are created; the accompanying tests cover ordering, environment/configuration parity, hooks, cleanup, compatibility, and failure handling.

Sequence diagram for check rehearsing the source clone

sequenceDiagram
    participant Check
    participant WorkDir
    participant Git
    participant Execute
    participant Legs

    Check->>WorkDir: _throwaway()
    Check->>Git: clone --no-checkout --shared with FILE_PROTOCOL
    alt clone succeeds
        Check->>Git: _paths_git_refuses()
        Check->>Git: _submodule_findings()
        Check-->>Execute: findings and plan
        Execute->>Legs: create and extract split legs
    else clone fails
        Git-->>Check: exit status and stderr
        Check-->>Execute: adopt-git-unasked
        Execute-->>Legs: refuse before creation
    end
Loading

Sequence diagram for submodule URL questions in throwaway repositories

sequenceDiagram
    participant Check
    participant Throwaway
    participant Git
    participant Findings

    Check->>Throwaway: _resolved(source)
    Throwaway->>Git: _throwaway_git(configure origin and .gitmodules)
    Throwaway->>Git: _throwaway_git(hash-object and update-index)
    Throwaway->>Git: git submodule init
    Git-->>Throwaway: resolved URL or failure
    Check->>Throwaway: _answers(urls, source)
    Throwaway->>Git: git ls-remote --get-url
    Throwaway->>Git: git ls-remote for local URLs
    alt throwaway creation or Git query fails
        Git-->>Findings: _Unasked as adopt-git-unasked
        Findings-->>Check: stop later URL questions
    else questions succeed
        Git-->>Findings: URL findings and notes
    end
Loading

File-Level Changes

Change Details Files
Make check rehearse the split's source clone before running other Git questions, matching execute's clone behavior and refusal timing.
  • Add a work-directory throwaway git clone --no-checkout --shared using FILE_PROTOCOL, an empty hooks path, the source template, and inherited environment.
  • Convert clone failures into a single adopt-git-unasked finding so later checks are skipped and execute can refuse before creating legs.
  • Add tests for clone failures, protocol handling, environment-sensitive Git settings, work-directory cleanup, and execute refusal behavior.
adopt-project.py
tests/test_adopt_check_throwaway_repos.py
Run submodule URL resolution and local URL-answer checks in standardized throwaway repositories instead of ad hoc temporary repositories.
  • Pass the source through _resolved, _answers, and related finding functions so throwaways use the source format, work directory, fixed branch, empty template, inherited environment, and disabled hooks.
  • Add _throwaway_git to preserve Git's failure text as adopt-git-unasked findings rather than generic command failures.
  • Stop or suppress later URL questions when a throwaway cannot be made or queried, while retaining findings obtained before the failure.
  • Preserve compatibility for existing callers by making new source parameters optional.
adopt-project.py
tests/test_adopt_check_throwaway_repos.py
Expand regression coverage and update documentation for the new throwaway and clone semantics.
  • Verify throwaways are created and removed in the work directory, including previously absent or unwritable work directories.
  • Verify hooks do not run, source object formats and Git settings are respected, URL questions are ordered correctly, and case-twin behavior remains stable.
  • Update docstrings and implementation comments to describe the split clone and work-directory URL repositories.
tests/test_adopt_check_throwaway_repos.py
tests/test_adopt_reencoded_files.py
adopt-project.py

Assessment against linked issues

Issue Objective Addressed Explanation
#1083 Make check rehearse the split’s own source clone in the work directory, so a clone failure is reported as adopt-git-unasked before either leg is created. ✅
#1083 Move _resolved and _answers throwaway repositories from TMPDIR to the work directory and create them with the standard throwaway settings, reporting Git failures as adopt-git-unasked and avoiding hooks. ✅
#1083 Close the smaller documentation and regression-test gaps, including correcting the identified adopt-project.py docstring and covering a missing work directory and case twins under core.ignorecase. ❌ The PR adds tests for the missing-work-directory and case-twin cases, and updates several docstrings, but the diff does not change the specifically identified adopt-project.py:466 docstring. It therefore does not fully address this objective.

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@brettheap

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-1, 2026-10-21T14:36:09Z) r1119, lane 1's independent reviewer (not the writer), reviewed #1119 at bdcf9952: READY on condition, and PARTIAL because the usage window forced a checkpoint. No MUST. The condition is SHOULD-1, a missing test that pins the clone to _inherited_env(). Measured, that is a safety property: with the whole environment, check writes objects/info/alternates into the source under GIT_OBJECT_DIRECTORY. SHOULD-2 is the GIT_CONFIG_PARAMETERS gap. The mutant runs and the gate are left to a fresh reviewer. The review follows in full.


r1119 review of PR #1119 at bdcf9952 (PARTIAL: stopped at the coordinator's checkpoint)

Verdict: READY on condition (PARTIAL). No MUST found. Every behaviour claim I measured holds. The condition is SHOULD-1, a missing test for a safety property. Mutant runs over the tests and the merged-tree gate were not finished; they are listed at the end.

Scratch state: all three copies of adopt-project.py (worktree, $S/base, $S/m6/tree) hash to b985928ed29eb465216f6fa0b308e39c1f23a196aea9ddf81f585a6a0e2d120d, and git status --short in the worktree is empty. The gate's processes were killed by PID and none is left. Nothing was written to GitHub, committed or pushed.

Findings

SHOULD-1: no test pins that the clone runs under _inherited_env(), and the body explains M6 wrongly (adopt-project.py:11184-11189)

  • The body calls M6 a fidelity limit. Measured, it is a safety property: the whole environment makes check write into the source repository.
  • I ran check with GIT_OBJECT_DIRECTORY=<source>/.git/objects:
    • head: exit 0, plan ok, and the source's objects/info/ stays empty.
    • M6 copy (env=None on the clone): exit 0, plan ok, and the source gains objects/info/alternates: …/god-m6/Thing/.git/objects. That is a write into the source, which check promises never to make.
  • Measured directly too: a plain GIT_OBJECT_DIRECTORY=<foreign> git clone --no-checkout --shared writes <foreign>/info/alternates and <foreign>/pack.
  • Fix:
    • Add a row that runs check with GIT_OBJECT_DIRECTORY naming the source's own object directory, and asserts the source's objects/info is unchanged. It kills M6.
    • Reword the body's Known limits and the M6 row of the Mutants table.

SHOULD-2: the known limit is real, and it is the failure #1083 exists to remove (adopt-project.py:11188)

  • With GIT_CONFIG_PARAMETERS="'init.defaultbranch'='a..b'":
    • head check: exit 0, plan ok;
    • execute: exit 2, REFUSED extracting the spec leg, fatal: invalid branch name: init.defaultBranch = a..b;
    • remotes: ['Northwind-code.git', 'Northwind-spec.git'], and "NOTHING has been rolled back".
  • A narrow fix is safe. A parent -c reaches the clone (measured: exit 128, invalid branch name), and the clone's own -c core.hooksPath comes after it, so it still wins.
  • Fix: give the clone _inherited_env() plus GIT_CONFIG_PARAMETERS when it is set, never the whole environment (see SHOULD-1). Attributes are irrelevant without a checkout, so the attr.tree reason for leaving it out does not apply here. This can be a follow-up issue if the lane prefers, but the body should then name that issue.

NIT-1: "--shared, so no object is copied" is false for a shallow source (docstring at adopt-project.py:11175-11176, and the body's "the object copy (--shared)")

  • git silently ignores --shared when the source is shallow.
  • Measured with a shallow copy of a 100k-file, 115 MiB-pack source: the clone took 7.3 s and 10.2 s, used 117.1 MiB, and made no alternates file.
  • Fix: say so in the docstring and the body. It is a cost, not a fault: execute's own clones copy as much.

NIT-2: the work-dir-unwritable change hides a plan finding for one round, and the NOTE does not say so (adopt-project.py:11050-11055)

  • With a read-only --work-dir and a kept relative-url submodule:
    • base: adopt-work-dir-unwritable and plan-submodule-relative-url;
    • head: adopt-work-dir-unwritable only.
  • The relative-url finding needs no work directory, so the user sees it one check later. That is acceptable under one cause, one finding.
  • But the body's "Its NOTE still says what went unasked" is only partly true. The NOTE says "It asked nothing about the source's paths and files there" and names no submodule url.
  • Fix: add "or its submodules' urls" to the NOTE, or reword the body.

NIT-3 (from reading only, unmeasured): in _resolved (:7708-7709) and _answers (:7918-7919), env = _inherited_env() runs after _throwaway(...) but outside the try. If it raised, the throwaway would be left behind. By reading, GIT_CONFIG_COUNT='²' would do it: '²'.isdigit() is true but int('²') raises. Fix: compute env before _throwaway.

Checked and found sound

Body-claim table

# Claim Measured
1 init.defaultBranch = a..b (global config): before plan ok / execute exit 2 at the spec leg with both legs made; after adopt-git-unasked / REFUSED adopt-git-unasked, remotes/ absent true. The head finding ends "the split's git clone of the source exited 128 there: fatal: invalid branch name: init.defaultBranch = a..b"
2 GIT_DEFAULT_HASH=bogus: before plan ok / "a git or gh command failed" with Northwind-spec.git left; after FINDING / REFUSED, remotes/ absent true. Base left ['Northwind-spec.git']
3 kept submodule with a..b: before exit 2 at git init -q <TMPDIR>/openreposhape-url-*; after FINDING / REFUSED true. Base said git init -q …/tmpx/openreposhape-url-vnouwe9o. On git 2.43 the head finding is the clone's, not _resolved's
4 kept submodule with GIT_DEFAULT_HASH=bogus true
5 counting hook in the global config, kept submodule: 2 runs before, 0 after; the rest of the output byte for byte the same true. Base ['post-index-change', 'post-index-change'], head []. Head's output is the same with and without the hook, and base and head differ only in the plan's path
6 no submodule, same hook: 0 / 0 true
7 protocol.file.allow = never: clone exits 128 without FILE_PROTOCOL, 0 with it true (transport 'file' not allowed)
8 a shallow source and a --shared-clone source clone with exit 0 true, but see NIT-1
9 45 rows, 45 passed on head true (45 passed in 93 s)
10 14 failed / 31 passed over the base tool true over both 16f9a0e's and 6da757c's adopt-project.py, the same 14 rows
11 2a119f2's adopt-project.py is unchanged through 16f9a0e true (empty diffstat)
12 Known limit: M6/M14, the _inherited_env() gap the gap is true (SHOULD-2). Calling M6 only a fidelity limit is false (SHOULD-1)
13 Behaviour change: an unwritable work dir asks no url question true. "Its NOTE still says what went unasked" is partly false (NIT-2)
14 Mutants: 13 of 16 killed unverified: no mutant was run over the tests
15 Gate: 3,050 passed, 5 skipped unverified: stopped before the first batch ended
16 M15 is equivalent unverified

Cost of the new clone, against base (base makes none)

Source Clone time Disk Whole check, base vs head
openRepoShape (516 commits, 179 refs) 0.06 s 0.1 MiB not timed
100k files, 115 MiB pack, 20,001 refs 0.8 s and 1.6 s 1.2 MiB 55.7–61.2 s vs 59.8–82.2 s (+4–8 s typical; 82.2 s is one noisy run)
the same, shallow 7.3 s and 10.2 s 117 MiB, a full copy 59.9 s vs 65.2 s

The load average was about 90 throughout, so the whole-check times are noisy.

Mutants: 0 run over the tests

  • I designed 16 mutants. None of them was run over the tests before the checkpoint.
  • M6 was only probed by hand (SHOULD-1); I did not run it over the test file.

Merge with main

Not finished (for a fresh reviewer)

  1. Mutant runs. Run them in $S/base, with a basetemp separate from the gate's. Run each over tests/test_adopt_check_throwaway_repos.py; the url mutants also over test_adopt_relative_submodule_url.py and test_adopt_submodule_url_answers.py, and R21 also over test_adopt_c_locale_bytes.py. Use -x, and restore and sha-check after each. R6, R7, R8, R14, R15 and R21 need count=1 replacements: patch _resolution_of, the ls-remote calls and the second _resolved(…) call by their own lines.
    • R1: no removal in _split_clone_asked.
    • R2: no --shared.
    • R3: the clone asked after _paths_git_refuses.
    • R4: _submodule_findings drops moved/notes on an _Unasked.
    • R5: _entry_findings ignores the returned unasked.
    • R6: _throwaway_git without .strip().
    • R7: _resolution_of(scratch…).
    • R8: _answers' ls-remote with cwd=scratch, which I expect survives.
    • R11 and R17: _Unasked(scratch…).
    • R12: a clone failure as unmade=True.
    • R14: the second _resolved without source.
    • R15: no ON_THIS_DISK_ONLY.
    • R21: base without _path_argument.
    • R26 and R27: no removal in _resolved / _answers.
    • Also run M6 itself over the tests, which I expect survives.
  2. The gate on $S/merged, using $S/gate.sh: the PR's file; test_shape_doctor.py in 4 shards ($S/doctor-shard{0..3}.txt, 112 ids each); test_scaffold_leg_path_collisions.py; and test_adopt_c_locale_bytes.py, test_adopt_clone_claimed.py, test_adopt_relative_submodule_url.py, test_adopt_submodule_url_answers.py, test_adopt_shape_pin_and_work_dir.py, test_adopt_work_dir_refusal.py, test_adopt_check_plan_name.py and test_adopt_reencoded_files.py.
  3. Safety probes not run:
    • core.fsmonitor (global, and in the source's config);
    • a smudge or process filter;
    • submodule.recurse=true;
    • an init.templateDir holding hooks and a config;
    • a partial-clone or promisor source, against git 2.43's CVE-2024-32004 class. That class is already reachable through the reads of the source check makes, and execute clones the source anyway.
  4. Cleanup not measured: on KeyboardInterrupt (by reading, the try/finally covers it), and two concurrent check runs in one work directory (by reading, mkdtemp names are unique).

Scratch: /workspace/projects/openRepoShape-worktrees/_lane1-scratch/r1119/ holds probe.py, cost.py, bigsource.py, timecheck.py, gate.sh and probes/.

Lane: openreposhape-1 (openRepoShape-1)

… parent git hands it, as execute's does, and no variable naming another repository; _resolved and _answers read their environment before their throwaway (#1083)

r1119's fix round on #1119.

SHOULD-2: `_split_clone_asked` makes the clone in `_split_clone_env()`:
`_inherited_env()` plus `GIT_CONFIG_PARAMETERS` where it is set. Measured on
bdcf995 with git 2.43, `GIT_CONFIG_PARAMETERS="'init.defaultbranch'='a..b'"`
gave `plan ok` from `check`, and `execute` made both legs and stopped
extracting the spec leg. Now `check` says `adopt-git-unasked` and `execute`
refuses before any leg, as for the setting in a config file or in
`GIT_CONFIG_COUNT`. A parent's `-c core.hooksPath` runs no hook: the clone's
own `-c` comes after it.

SHOULD-1: a row runs `check` under `GIT_OBJECT_DIRECTORY` naming the source's
own objects and holds the source's object directory unchanged. It kills M6:
in the whole environment the clone wrote `objects/info/alternates` into the
source. A row under `GIT_DIR` naming the source, whose own config rewrites
the on-disk url, kills M14 (`_answers` in the whole environment) and holds
the source's config unchanged.

NIT-1: the docstring says git ignores `--shared` for a shallow source, so
that clone is a full copy, a cost `execute`'s own clones pay too.
NIT-2: the unmade-work-directory NOTE says it asked nothing about the urls of
the submodules the plan keeps either.
NIT-3: `_resolved`, `_answers` and the clone read their environment before
they make their throwaway, so a raise there leaves none behind.

tests/test_adopt_check_throwaway_repos.py: 63 rows (45 before), 18 new.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@brettheap brettheap changed the title adopt-project.py: check asks the split's own git clone of the source in the work directory, so a clone this git will not make is adopt-git-unasked before any leg, and _resolved and _answers make their repositories as check's other throwaways do (#1083) adopt-project.py: check asks the split's own clone of the source in the work directory, so a clone this git will not make is adopt-git-unasked before any leg, and _resolved and _answers make their repositories as check's other throwaways do (#1083) Oct 21, 2026
@brettheap

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-1, 2026-10-21T16:52:08Z) r1119b, lane 1's independent reviewer (not the writer), re-checked #1119 at 9b998936: NOT READY, with two MUSTs, each with a proven fix. All five round-1 findings are resolved, and the safety probes found nothing written outside the throwaways.

A fix round follows. The review follows in full.


Verdict: NOT READY. Two MUSTs. Both have small fixes, and I proved each fix on a scratch copy. All five round-1 findings are resolved. Every check the brief listed was run; this report is complete, not PARTIAL.

Nothing was written to GitHub, committed or pushed. My worktree /workspace/projects/openRepoShape-worktrees/l1-r1119 is still clean at 9b998936. Every scratch clone I mutated hashes back to 4a0f5881… with a clean status, and none of my processes is still running.

Merge with main (4ed7c7bf)

  • git merge-tree --write-tree 9b998936 origin/main is textually clean (exit 0, tree 4bcc0222).
  • I built a scratch merge commit 91c0a834, checked out detached at /workspace/projects/openRepoShape-worktrees/_lane1-scratch/r1119/r2/merged.
  • Gate on the merged tree: 1,061 passed, 2 failed, 8 skipped.
Batch Result
test_adopt_check_throwaway_repos.py 63 passed
test_no_auto_maintenance.py 1 failed, 57 passed, 2 skipped (MUST-1)
test_scaffold_leg_path_collisions.py 104 passed
test_shape_doctor.py, 4 shards of 112 445 passed, 3 skipped
c_locale_bytes, clone_claimed, relative_submodule_url, shape_pin_and_work_dir, submodule_url_answers, work_dir_refusal 188 passed
test_adopt_root_symlinks.py (it names adopt-work-dir-unwritable) 1 failed, 204 passed, 3 skipped (MUST-2)

Round-1 findings

  • SHOULD-1: resolved.
    • M6 is killed by test_the_clone_writes_nothing_into_an_object_directory_around_it.
    • M14 and M14r are both killed by test_a_repository_around_check_rewrites_no_url_it_asks.
    • Each was my own run on head. The body now calls this a safety property.
  • SHOULD-2: resolved, and sound.
  • NIT-1: resolved. The docstring (:11222-11227) and the body say it. Measured: a -q --shared clone of a shallow source prints nothing and makes no alternates.
  • NIT-2: resolved. Head's NOTE now ends "nor about the urls of the submodules the plan keeps"; bdcf9952's did not.
  • NIT-3: resolved. The environment is read first at :7710, :7919 and :11231. Undoing each one (N4r, N5a, N6c) is killed by its environment-read row.

Safety probes

Trees: base 823624d9 (before the PR), head, main 4ed7c7bf, merged. "Runs" counts programs the probe's own logging scripts recorded during check.

# Probe Result Writes outside the throwaway
1 core.fsmonitor program, global config 8 runs on every tree, from _resolved's update-index. On head they run in the work dir's openreposhape-url-* (SHOULD-A) none
2 core.fsmonitor in the source's config 0 none
3 smudge and clean filter, global runs only in #910/#654's -attributes-/-reencoded- throwaways, which rehearse a checkout by design; none in the clone, -url- or -ask-. The counts (28/30/24/30) differ only in clean inside -reencoded-, code this PR does not touch; I did not re-run to prove it is timing none
4 process filter, global 2 runs on every tree, same throwaways as row 3 none
5 filter in the source's config 0 none
6 submodule.recurse=true 0, findings unchanged none
7 init.templateDir with hooks and a config (fsmonitor, hooksPath) base and main 10 runs; head and merged 0 (an improvement) none
8 template config naming remote.origin.uploadpack 0 none
9 partial clone, history blobs missing 0 none
10 partial clone, HEAD blobs missing (re-made after plan) the source's own uploadpack program runs 5 times, and a promisor pack is written into the source, identically on all four trees. It comes from check's reads of the source, not from this PR (NIT-D) the source's objects/pack (all trees)
11 shallow source, global uploadpack.packObjectsHook head and merged 1 run, base and main 0 (NIT-C) none
12 shallow source, hook and fsmonitor in the source's config 0 none
13 GIT_ALTERNATE_OBJECT_DIRECTORIES and GIT_SHALLOW_FILE naming a victim repository; GIT_NAMESPACE; GIT_REPLACE_REF_BASE plan ok none
14 a parent -c of hooksPath, fsmonitor, core.worktree=<victim>, commit-graph, reflogs, auto gc and maintenance, sharedRepository base 10 runs; bdcf9952, head and merged 0 none
15 SIGINT, to the process group and to Python alone, paused in the clone and in _resolved work dir empty in 8 of 8 (head and merged). Exit -2 with a traceback, which predates this PR none
16 two concurrent check runs, one work dir they overlapped (2 throwaways at once); both match a solo run; work dir empty after none

New findings

MUST-1: head fails a gate test (tests/test_adopt_check_throwaway_repos.py:1243)

  • test_the_clone_takes_a_parent_gits_settings_and_no_other_repository calls appended_to_command_scope({}, …).
  • That fails tests/test_no_auto_maintenance.py::test_no_test_sets_a_git_config_count_of_its_own. It failed in lane 1's G5 on head and in my run on the merged tree.
  • Fix: replace that block with:
    for name, value in appended_to_command_scope(
            os.environ, [("attr.tree", "HEAD"), ("test.heldone", "one")]).items():
        monkeypatch.setenv(name, value)
    env = adopter._split_clone_env()
    assert env["GIT_CONFIG_PARAMETERS"] == parameters
    assert [env.get(name) for name in named] == [None] * len(named)
    scoped = [(env[f"GIT_CONFIG_KEY_{n}"], env[f"GIT_CONFIG_VALUE_{n}"])
              for n in range(int(env["GIT_CONFIG_COUNT"]))]
    assert scoped[-1] == ("test.heldone", "one")
    assert "attr.tree" not in [key.lower() for key, _ in scoped]
  • Proven: with it, the row and the scan both pass.
  • It still tests what it claims: the row still fails under A5 (GIT_ATTR_SOURCE handed back), A15 (GIT_CONFIG_PARAMETERS always set) and a mutant that keeps attr.tree.

MUST-2: the merge with main fails a test that main added (main's tests/test_adopt_root_symlinks.py:2070)

SHOULD-A: a core.fsmonitor program still runs in _resolved's throwaways (adopt-project.py:11118)

NIT-A: R8 survives because no row puts --work-dir inside a repository (:7929; a row beside :1283)

  • With a work dir inside the source and the source's config rewriting the url:
    • head gives [plan-submodule-relative-url];
    • R8 adds a false plan-submodule-url-answers-nothing.
  • Fix: add that row.

NIT-B: A18 survives (docstring at :7911-7913)

  • The docstring says _answers leaves out GIT_CONFIG_PARAMETERS; no row pins it.
  • Fix: add a git-config-parameters url-rewrite variant of the :1283 row.

NIT-C: a shallow source makes the clone run uploadpack.packObjectsHook from protected config (probe 11)

  • execute's clone runs it too, and turning it off would cost fidelity.
  • Fix: one sentence in _split_clone_asked's docstring or under Known limits.

NIT-D (predates this PR; for a follow-up issue): probe 10. check's own reads of a partial source fetch from its promisor, running the source's uploadpack program and writing into the source, the same on every tree.

NIT-E: the body's Gate section is stale. It says 8 batches and 0 failed. It should now carry lane 1's G5 failure and MUST-2's merged-tree result.

Mutants: 31 killed of 36 run

  • r1119's list, 14 of 17 killed. Run: R1–R8, R11, R12, R14, R15, R17, R17b, R21, R26, R27. R9, R10, R13, R16, R18–R20 and R22–R25 are described nowhere in round 1, so I did not run them.
    • R2 (no --shared) survives: only cost changes.
    • R3 (clone after _paths_git_refuses) survives: nearly equivalent, since the clone's finding wins either way.
    • R8 survives, and is a real gap: NIT-A.
  • SHOULD-1's three, 3 of 3 killed: M6, M14, M14r.
  • My own, 14 of 16 killed: A1, A2b, A5, A6, A6b, A11, A12, A15, A22, A23, A24, N4r, N5a, N6c.
    • A17 (_resolved takes the parent's -c) survives. That is not a defect: the throwaway's own hooks setting still wins.
    • A18 survives: NIT-B.
    • An over-broad A2 was replaced by A2b and is not counted.

Body claims

Claim Measured
Closes #1083 is the only closing keyword; #1032 is "Covers", #1136 "filed as" true (regex scan; GitHub lists only #1083 as closed)
Known limits names #1136 true (#1136 is open and holds all three items)
Title within the 250 cap true, 248
63 rows; 35 new = 7+8+2+18 true
Over bdcf9952's tool: 9 failed / 54 passed, and the 9 named true, exactly those 9
Over 16f9a0e8's tool: 30 failed / 33 passed; 15 of r1119's 18 rows fail, 3 pass true
GIT_CONFIG_PARAMETERS row: before plan ok, after adopt-git-unasked true for check. execute was not run by hand; its rows pass on head and fail over bdcf9952's tool
Without its own -c, the parent's hooks ran reference-transaction 4 times true (4)
attr.tree=bogus still clones true
GIT_ALTERNATE_OBJECT_DIRECTORIES and GIT_SHALLOW_FILE write nothing true (I did not try a non-empty shallow file)
(b) "no hook" partly false: the fsmonitor program runs (SHOULD-A)
11 killed this round; 24 of 25 across both rounds the arithmetic holds. M6, M14, M14r and N4–N6 reproduced; the first round's 13 were not re-run
Gate: 1,192 passed, 0 failed the sum is true but stale: G5 has 1 failure (NIT-E)

Probe scripts and logs are in /workspace/projects/openRepoShape-worktrees/_lane1-scratch/r1119/r2/ (probe2.py, probe_s2.py, probe_intr.py, probes/). The mutant definitions and results are in mutants.py and mut/summary.txt there, and the gate logs in gate/.

Lane: openreposhape-1 (openRepoShape-1)

Brett Heap and others added 4 commits October 21, 2026 21:03
….fsmonitor=false`, and the rows r1119b named pass on main: the clone-env row appends to the environment's count, and the ref-probe row fails only the probe's `git init`

r1119b's second review of #1119, after the merge of main 038dafb (#1091, #1111):

- MUST-1: `test_the_clone_takes_a_parent_gits_settings_and_no_other_repository`
  built its `GIT_CONFIG_COUNT` from `{}`, which
  `test_no_test_sets_a_git_config_count_of_its_own` refuses. It now appends
  to `os.environ`, and asserts the last entry kept and no `attr.tree`.
- MUST-2: main's `test_check_says_where_git_would_not_make_the_ref_probe`
  set `GIT_DEFAULT_HASH=bogus`, which now stops the split's own clone
  first (`adopt-git-unasked`, exit 1). A stub `git` first on PATH fails
  only `git init` of a `refs-probe-*` directory, with git's words; skipped
  on Windows.
- SHOULD-A: `_throwaway_made` returns `-c core.fsmonitor=false` beside
  `core.hooksPath`, as the rehearsal and `_stored_as_written` give theirs.
  A counting fsmonitor program ran 8 times in `_resolved`'s two
  repositories in one `check`; now 0, in the global config and in
  `GIT_CONFIG_COUNT`. New rows: that one and its control.
- NIT-A: a row with `--work-dir` inside the source, whose config rewrites
  the url (kills R8).
- NIT-B: a row with that rewrite handed down by a parent git in
  `GIT_CONFIG_PARAMETERS`, which `_answers` leaves out (kills A18).
- NIT-C: `_split_clone_asked`'s docstring says a shallow source makes the
  clone run the `uploadpack.packObjectsHook` of protected config, as
  `execute`'s clones do.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…inside the source also rewrites the url to a network, so a `--get-url` asked around the throwaway, which names it in a NOTE as not asked, fails it

f1119c's mutant R8a, which moved only `_answers`' `git ls-remote
--get-url` out of its throwaway repository into the throwaway's own
directory, survived the row that kills r1119b's R8: the expansion there
only decides between the disk and a network, and the url itself was
still asked in the throwaway. A source config that rewrites the on-disk
url to `https://f1119c.invalid/Thing` makes R8a print the NOTE of a url
on a network it did not ask; on this head `check` prints none, inside
the source or outside it. Now killed by the row's `a-network` case.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…r system config as where the `uploadpack.packObjectsHook` a shallow source's clone runs comes from, since a parent's `-c` did not reach it

cea2553's NIT-C sentence said "protected config ... or a `-c`". f1119c
measured it on git 2.43, with a shallow source and a counting hook:

- in the global config, 1 run in `check` and 1 in a clone made as
  `_extract_leg` makes it;
- in the source's own config, 0 and 0;
- in `GIT_CONFIG_PARAMETERS`, as a parent `git -c` hands it down, 0 and 0.

So the `-c` is dropped. The commit message of cea2553 says "of
protected config" for the same sentence; this is the measured form.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@brettheap

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-1, 2026-10-21T22:23:41Z) r1119c, lane 1's independent re-check of round 3 at bba168bd: READY, with no MUST or SHOULD. It is not the writer f1119c.

The full re-check

Verdict: READY.

I found no MUST and no SHOULD. Every item r1119b raised is resolved. Nothing was committed, pushed or written to GitHub. My worktree /workspace/projects/openRepoShape-worktrees/l1-r1119 is still at bba168bd with a clean status. The mutant clone hashes back to b7df715b… (the sha of the head's adopt-project.py) after every mutant. None of my processes is still running.

Checks

1. Main. origin/main is still 038dafba, and git merge-base --is-ancestor origin/main HEAD says it is an ancestor of the head. I needed no merge-tree run.

2. MUST-1: resolved.

  • tests/test_no_auto_maintenance.py passes whole: 58 passed, 2 skipped. It includes test_no_test_sets_a_git_config_count_of_its_own. The 2 skips are "the session began with GIT_CONFIG_GLOBAL set", which is the probe environment.
  • The row at tests/test_adopt_check_throwaway_repos.py:1313 (it was :1243) now calls appended_to_command_scope(os.environ, [("attr.tree","HEAD"), ("test.heldone","one")]). It asserts that the last scoped entry is test.heldone and that attr.tree is absent.
  • I ran seven mutants and all were killed by that row: A5, A6, A6b, A15, A2b, KA1 and KA2.
  • The failure text shows the row now reads the session's own entries: KA1 and KA2 fail with assert 'attr.tree' not in ['gc.auto', 'maintenance.auto', 'attr.tree', 'test.heldone'].

3. MUST-2: resolved.

  • Head's ref-probe row passes on head and on main's tool (tests/test_adopt_root_symlinks.py::test_check_says_where_git_would_not_make_the_ref_probe).
  • @WINDOWS_SKIP sits directly above the def.
  • The stub (refusing_the_ref_probe, tests/test_adopt_root_symlinks.py:2070-2090) fails only an init where some argument matches */refs-probe-*. It prints UNKNOWN_HASH and exits 128. Everything else runs exec "$REAL_GIT". REAL_GIT is shutil.which("git") taken before PATH is changed, which gave /usr/bin/git here.
  • The docstring is true. I ran main's old row (GIT_DEFAULT_HASH=bogus) against head's tool and it failed with assert 1 == 0 and FINDING adopt-git-unasked.
  • MR1, MR2 and MR3 were all killed by that row. MR1 gave REFUSED … git init -q …/refs-probe-… exit 128. MR2 and MR3 gave no NOTE.

4. SHOULD-A: resolved.

  • _throwaway_made (adopt-project.py:11541) now returns ["-c", "core.hooksPath=<empty>", "-c", "core.fsmonitor=false"]. All five of its callers get both settings: _resolved, _answers, _AttributesProbe, _stored_as_written and _SplitRehearsal.
  • test_no_fsmonitor_of_this_machines_runs_where_the_urls_are_asked, both global-config and git-config-count, passes on head, and so do both controls.
  • The same row fails 2 of 2 over d0bc40ce's tool and over main's, with 8 counted runs in each of the 2 × 2 cases. The logs held 8 lines each.
  • S1, S2 and S3 were all killed. S4 survives, and it is equivalent: ls-remote reads no index.
  • The duplicate -c core.fsmonitor=false in _stored_as_written (adopt-project.py:12250) and _SplitRehearsal.git (:12533) is harmless. Measured: git -c core.fsmonitor=false -c core.hooksPath=/nonexistent -c core.fsmonitor=false config --type=bool --get core.fsmonitor printed false, exit 0.

5. NIT-A, NIT-B, NIT-C: resolved.

  • R8 was killed by the a-directory-holding-nothing case. R8b was killed by the same case. R8a was killed only by a-network, so that case is needed.
  • A18 was killed by test_a_parent_gits_url_rewrite_is_not_read_where_a_url_is_asked.
  • I re-ran probe_nitc.py from my scratch on head. The result matches logs/nitc2.log exactly: the global hook gives 1 run in check and 1 in _extract_leg's clone; the source's config gives 0 and 0; a parent's -c gives 0 and 0.
  • I ran the same probe on main's tool (038dafba) as a differential. The global hook gave 0 runs in check and 1 in the clone. So the one run on head comes from this PR's split clone.
  • The _split_clone_asked docstring (adopt-project.py:11650-11657) says the global or system config. I measured the global config, not the system one; that part rests on reading git, but the claim is correct. The docstring is true.

6. Gate on head. I ran at most 3 pytest calls at once, each under 5 minutes, in the probe environment. Result: 931 passed, 0 failed, 8 skipped.

Batch Result
tests/test_adopt_check_throwaway_repos.py 70 passed
tests/test_no_auto_maintenance.py + tests/test_path_guard_hygiene.py 107 passed, 2 skipped
tests/test_adopt_root_symlinks.py, whole 205 passed, 3 skipped
tests/test_shape_doctor.py, 4 shards (111+1, 111+1, 112, 111+1) 445 passed, 3 skipped
tests/test_scaffold_leg_path_collisions.py 104 passed
  • Base vs head. Head's test file over main's tool gives 33 failed, 37 passed. Adding the ref-probe row, which passes there, makes 33 of 71 failed, as claimed. Over d0bc40ce's tool it gives 2 failed, 69 passed: the two fsmonitor rows. Both match the body and report.
  • Row counts. 2a119f2 has 29 rows and head has 70, as the body says.
  • f1119c's batch sum. Its 18 batches add up to 3,413 passed and 8 skipped, which matches the body and report. I did not re-run all 18.

7. Body.

Findings

No MUST and no SHOULD. The NITs are optional and none blocks landing.

  • NIT-1, squash message. The cea25530 commit message still says "of protected config". The bba168bd docstring corrects it to "global or system config; a parent's -c did not reach it". Use the corrected wording in the squash message.
  • NIT-2, test file (tests/test_adopt_check_throwaway_repos.py, REWRITTEN_TO, the a-network param). The host https://f1119c.invalid/ names an agent in a committed test. Optional fix: use a neutral name such as example.invalid.
  • NIT-3, test file (test_no_fsmonitor_of_this_machines_runs_where_the_urls_are_asked and its control). They are not WINDOWS_SKIP. The existing hook rows aren't skipped either, and CI is Linux-only. Whether Git for Windows runs a #!/bin/sh fsmonitor program is unverified.
  • NIT-4, body, "Measured" section (line 38). It still says "Before is main 2a119f28", while main's adopt-project.py has since changed by 546 lines. The two newest rows name 038dafba where it matters, so this is a label only.

Scratch and logs are in /workspace/projects/openRepoShape-worktrees/_lane1-scratch/r1119/r3/. The files to look at are gate/summary.txt, mut/summary.txt, mut/mutlogs/, logs/bvh-main.log, logs/bvh-d0bc.log and body.md.

Lane: openreposhape-1 (openRepoShape-1)

…nd execute a default branch git will not take share one helper each, so SonarCloud's new-code duplication on #1119 falls under its 3% gate (#1083)

The parent-git rows and the global-config rows repeated one body each; each body is now a module-level helper that takes the function building the environment, and no row asserts or sets up anything else than before. All test ids keep their names.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…s/test_adopt_check_throwaway_repos.py

Lane: openreposhape-1 (openRepoShape-1)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@brettheap

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-1, 2026-10-23T18:08:24Z) Lane 1's delta check of #1119 since its READY head bba168bd: two commits, neither changes what a test asserts; READY carries to e0a98f13 once CI is green there.

Lane: openreposhape-1 (openRepoShape-1)

brettheap added a commit that referenced this pull request Oct 23, 2026
…eps at a pin's file reached through a kept link, and holds that `check` renames the path the walk reaches, not the file kept in it, so rv1091's R6 no longer survives (#1116) (#1140)

One row that holds the remedy `check` gives where a kept link leads a pin's walk to a directory, so that rv1091's mutant R6 (NIT 4 of its review of #1091, issuecomment-6316443939) is killed. This is a test-only change: `tests/test_adopt_root_symlinks.py` gains one row, and no production file, refusal code or sentence changes. It is a follow-up to #1091 (#904), so Brett Heap's ruling of 2026-10-21T10:32:04Z left it out of v1.2.0, and it is on the v1.2.1 milestone.

- #1091 landed as `4ed7c7bf` and what it ships is right; only this row was missing.
- `_kept_pin_finding` words a kept pin's finding through a kept link with `_through_the_kept_link(link, reached, entry)`. Its remedy says to send the link to a leg or `drop` it, "or rename `<reached>` in the source and re-run `plan`", where `reached` is the file the root walk reaches.
- R6 hands that "rename" the kept row's own path (`path`) in place of `reached` (line 778: `link, reached, entry` becomes `link, path, entry`).
- No row put a directory at a pin's reached file (rv1091's NIT 4), so in each of them `path` and `reached` are the same string and R6 passed all of them. With R6 applied alone, the two adopt-root files give `1 failed, 326 passed, 3 skipped`, and the one failure is the new row, so main's 326 other rows do not notice it.

`test_a_directory_at_a_pins_file_through_a_link_names_what_it_reaches`, at `tests/test_adopt_root_symlinks.py:1173`, 30 added lines, in the file's style and on its helpers (`planned`, `declare`, `check`, `held_pins`, `symlink_findings`, `refused_by`, `execute_args`, `no_leg_was_created`, `ticked`):

- the plan keeps `contracts -> governance`, keeps `governance/`, and puts a directory at `governance/openglass-pin.yaml` holding `notes.md`; a pin is declared on `openGlass`;
- `check` exits 1, with no symlink finding;
- its one `plan-root-holds-pin` line names the file `governance/openglass-pin.yaml/notes.md` kept under `governance/`, inside `contracts/openglass-pin.yaml` through the symlink `contracts` to `governance`, and its remedy renames `governance/openglass-pin.yaml`, the path the walk reaches, not the file kept in it;
- `execute` refuses with `plan-root-holds-pin` and the same line before any leg.

No assert joins conditions with `and` or `or`, the added lines are at most 76 wide (the file's widest is 80) and ASCII. The docstring says that before #1091 `check` passed this plan, measured on main `823624d9`.

R6 alone, in a scratch tree at this head: `adopt-project.py` sha256 `0c1c1f3d7abf41136a61d7d4651f13fef5d3642a3162c024e921429b88bd7c1f` before, `9a921c6a9261234a68ef930e478421dca2d7135ad8674b357f2f55f46f773068` with R6, and restored to the first (checked), tree clean.

| Where | Result |
|---|---|
| the row, with R6 | `1 failed, 208 deselected`: `assert held_pins(checked) == [line]` at `:1196`, the finding reading "rename `governance/openglass-pin.yaml/notes.md`" |
| both adopt-root files, with R6, git 2.43 | `1 failed, 326 passed, 3 skipped`; the failed id is the new row alone |
| the row, on `823624d9`'s `adopt-project.py` (before #1091) | `1 failed, 208 deselected`: `assert 0 == 1`, `check` said `plan ok` |

All through `run9.sh` (stub `gh`, bogus token, `GIT_SSH_COMMAND=false`, no global git config), at head `2b6db57a3`:

| Git | Files | Result | END |
|---|---|---|---|
| 2.43.0 | `tests/test_adopt_root_symlinks.py` and `tests/test_adopt_root_shape_paths.py` | 327 passed, 3 skipped (the reftable rows at 1956, 2011 and 2562) | 2026-10-21T16:21:00Z rc=0 |
| 2.43.0 | `tests/test_repo_hygiene.py`, `tests/test_agents_md_codes.py` and `tests/test_cli_reference.py` | 170 passed | 2026-10-21T16:23:02Z rc=0 |
| 2.47.1 (`git version 2.47.1`) | `tests/test_adopt_root_symlinks.py` | 209 passed, no skips | 2026-10-21T16:31:33Z rc=0 |

The 2.43 count is 327 with the row, which is the 326 other rows plus it. `tests/test_agents_md_codes.py` passes, as no refusal code changed.

- `origin/main` is `4ed7c7bf`, unmoved when fetched at the start, mid-way and before the push. `git merge-tree --write-tree origin/main HEAD` is clean, tree `d675b07f29edb9a061c3bc542dc2a76334d1e1a0`, the head's own tree: the branch differs from main by this one file, 30 insertions.
- None of the 22 open PRs touched `tests/test_adopt_root_symlinks.py` when this was written.
- **NIT round 1, 2026-10-22:**
  - origin/main `038dafba9dbedd9405b316cc749a04dffe02bb36` (#1111) is merged in as `e7623ed98`, cleanly, with tree `8a5cf7b1`, as rv1140 predicted.
  - The head `94712c3bc076142c16e48af70c218e6e958bd005` merges with main at its own tree, `f8faecc8cbcefbb87453f28647baf7a636ee1825`.
  - Of the 28 open PRs, #1119 (head `bba168bd`) now edits `tests/test_adopt_root_symlinks.py`. Its merge-tree with this head is clean, tree `81a0397437484f52b52789a86a2b5da43f712dae`.
- As a check on the nearest neighbour, #1118 (`c60066cbe`, which edits `tests/test_adopt_root_shape_paths.py` and was cut at `6da757cb`) conflicts in that file with `origin/main` alone, and with this branch the same: that is #1118's, not this row's.

This round is one merge of main and one new commit, after rv1140's review: READY at `2b6db57a`, with NITs (#1140 issuecomment-6341111415).

- **The row now keeps two files** under `governance/openglass-pin.yaml/`: `README.md` and `notes.md`.
  - Git's order puts `README.md` first, so the one `plan-root-holds-pin` line names `governance/openglass-pin.yaml/README.md`. The sections above describe the first commit's one-file row.
  - This kills rv1140's mutant M3, which changes `rows[0]` to `rows[-1]` at `adopt-project.py:770`. M3 survived the one-file row.
  - The docstring names the sort order.
- **Mutants, the row alone,** each applied alone and restored with its sha256 checked:
  - M3: killed (it survived before);
  - R6: still killed;
  - rv1140's M1, M2 and M4 to M7: all killed.
- **Runs at `94712c3bc`:**
  - git 2.43.0, `tests/test_adopt_root_symlinks.py` and `tests/test_path_guard_hygiene.py`: **255 passed, 3 skipped**. The skips are the reftable rows.
  - git 2.47.1, the same files: **258 passed**.

Review: lane 5's independent re-check, by a reviewer that wrote none of #1140: READY at `94712c3b`, the head that lands (issuecomment-6344746584), with no MUST, SHOULD or NIT. It checked the delta from `2b6db57a`, which rv1140 passed (issuecomment-6341111415). CI (Brett Heap's ruling 27): H0 is the head 94712c3 itself, which passed tests, tests-git-bash and SonarCloud; main has touched none of its files since its base 038dafb; and the local gate passed on its predicted squash tree 19f483e over main dea964a: the adopt_root_symlinks, repo_hygiene, agents_md_codes, cli_reference sets, 377 passed, 3 skipped. Landed on Brett Heap's word of 2026-10-23T13:10:26Z, relayed to every lane by lane 3 (#473 issuecomment-6365697123): "merge v1.2.1 PRs once reviewed and green".

Closes #1116

Lane: openreposhape-5 (openRepoShape-5)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

@brettheap
brettheap marked this pull request as ready for review October 23, 2026 19:44
@sourcery-ai

sourcery-ai Bot commented Oct 23, 2026

Copy link
Copy Markdown

Sorry @brettheap, you've used your own review budget of 250,000 diff characters for the last 7 days.

You can request another review in 17 hours and 2 minutes by commenting @sourcery-ai review. Upgrade to get a review now.

@brettheap
brettheap merged commit b7edd88 into main Oct 23, 2026
5 checks passed
@brettheap
brettheap deleted the fix/1083-check-asks-the-split-clone branch October 23, 2026 19:45
brettheap pushed a commit that referenced this pull request Oct 23, 2026
…o-fetch

Step 1 of the three-merge restack after #1119 squash-landed on main as b7edd88.

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 23, 2026
Step 2 of the three-merge restack. git diff 9b85644 b7edd88 is empty for
#1119's files, so any conflict there takes ours, the branch's version.

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 23, 2026
Step 3 of the three-merge restack after #1119's squash b7edd88.

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 23, 2026
…fig, step 1 of 3 to restack #1169 onto main after #1119's squash

One conflict, in the module docstring of tests/test_adopt_check_throwaway_repos.py: #1119 added its #1033 paragraph and #1136 added its own at the same place. Both stay, #1033 first.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
brettheap pushed a commit that referenced this pull request Oct 23, 2026
… step 2 of 3 to restack #1169 onto main after #1119's squash

Two conflicts, adopt-project.py and tests/test_adopt_check_throwaway_repos.py, both #1119's files; both took this branch's side with git checkout --ours. git diff 9b85644 b7edd88 is empty over the whole tree, so the merge changes no file: its tree is the tree of the commit before it.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
brettheap pushed a commit that referenced this pull request Oct 23, 2026
…ep 3 of 3 to restack #1169 onto main after #1119's squash b7edd88

No conflict.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
brettheap pushed a commit that referenced this pull request Oct 23, 2026
#1180 restacked on main after #1119's squash b7edd88 through three merges
(c3f8a2b, ef73f75, b18d2e8), then a0e8cde fixed r1180d's NIT-1 to NIT-3.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Oct 23, 2026
…py` where GNU make at the root reads a kept `GNUmakefile` or `makefile`, and its NOTE names that file and what it takes for `make bootstrap` to run there (#1047) (#1121)

GNU make reads the first it finds of `GNUmakefile`, `makefile` and `Makefile`, and only that one. Where the source keeps `GNUmakefile` or `makefile` in the root, `make bootstrap` at the root of the clone reads the source's file, not the shape's `Makefile`, and exits 2 with "No rule to make target 'bootstrap'". `execute`'s NEXT printed it there anyway.

This PR also covers the third box of #1048 (see #1048): the `_next_steps` NOTE that said the shape's targets "arrive only once the follow-up merges the two", which is untrue where GNU make reads a kept `GNUmakefile` or `makefile`. #1105 covers that issue's other two boxes, and leaves this one to this PR.

## Measured

GNU Make 4.3 on Linux, a disk that keeps case. One directory per set of files, each file with a `which` target:

| files | `make which` reads | `make -n bootstrap` |
|---|---|---|
| `GNUmakefile` | `GNUmakefile` | exit 2, no rule |
| `makefile` | `makefile` | exit 2 |
| `Makefile` (with a `bootstrap` rule) | `Makefile` | exit 0 |
| `GNUmakefile` + `makefile` | `GNUmakefile` | exit 2 |
| `GNUmakefile` + `Makefile` | `GNUmakefile` | exit 2 |
| `makefile` + `Makefile` | `makefile` | exit 2 |
| all three | `GNUmakefile` | exit 2 |

A `GNUmakefile` that holds `include Makefile` runs the shape's rule (exit 0), and so does `make -f Makefile bootstrap`.

What NEXT printed on main, from a real `plan`, `check` and `execute --local-remote-dir`, with the printed command run in a recursive clone of the split ("S" is the source's own `scripts/bootstrap.py`, kept with `scripts/`):

| the root keeps | NEXT on main | in the clone |
|---|---|---|
| none | `make bootstrap` | exit 0 |
| `Makefile` | `python3 scripts/bootstrap.py`, #480's NOTE | exit 0 |
| `makefile`, `GNUmakefile`, or both | `make bootstrap` | **exit 2, no rule** |
| `Makefile` + `makefile`, `Makefile` + `GNUmakefile`, all three | `python3 scripts/bootstrap.py`, and a NOTE saying the targets arrive once the two `Makefile`s are merged | the command exits 0; `make bootstrap` exits 2, and still does after `cat shape/Makefile >> Makefile` |
| S + `GNUmakefile`, S + `makefile`, S + `Makefile` + `GNUmakefile` | no command; the NOTE says to merge, "and then run `make bootstrap`" | `make bootstrap` exits 2 |

## What changed

`adopt-project.py`, `_next_steps`:
- It takes `surviving=`, the paths the plan's entries send to the root (`paths_for["root"]`, passed by `cmd_execute`).
- It asks them for a kept `GNUmakefile` or `makefile` in GNU make's order, with `_root_keeps`, as `_makefile_follow_up` asks them. So NEXT names the same file as the plan's `CONTRACTS_DIR` follow-up.
- Where it finds one, the command is `python3 scripts/bootstrap.py`. The NOTE names the file in the follow-up's words, and says `make bootstrap` and the shape's other make targets run there only once that file has them, or includes a Makefile that does:
  - the shape's `Makefile` is the root's: "NOTE the shape's Makefile is the root's `Makefile`, but GNU make at the root reads the source's `GNUmakefile`, the first of `GNUmakefile`, `makefile` and `Makefile` it finds, so `make bootstrap` and the shape's other make targets run there only once `GNUmakefile` has them, or includes a Makefile that does; `python3 scripts/bootstrap.py` is what the shape's `make bootstrap` runs."
  - the source also keeps its own `Makefile`: "NOTE the root keeps its own Makefile and the shape's is shape/Makefile, and GNU make at the root reads ..." (the same clause, and the same ending).
  - the source's own script went aside too: the merges first, as before, "and then run `python3 scripts/bootstrap.py`; GNU make at the root reads ...".
- A root that keeps neither name gets the block it got before, byte for byte. That includes `MAKEFILE`, which GNU make never reads where case is kept.

## Tests

`tests/test_adopt_next_make_reads_first.py`, new, 40 tests (38, and the 2 of the fix round below):
- 13 rows word for word: none, `Makefile`, `makefile`, `GNUmakefile`, `makefile`+`GNUmakefile`, `Makefile`+`makefile`, `Makefile`+`GNUmakefile`, all three, S, S+`Makefile`, S+`GNUmakefile`, S+`makefile`, S+`Makefile`+`GNUmakefile`.
- 3 rows that keep `make bootstrap`: `MAKEFILE`; longer names (`makefile.am`, `Makefile.in`, `GNUmakefile.bak`); and the names below the root.
- 13 rows in which NEXT names the same file as `_makefile_follow_up`, for the same root paths.
- 9 end to end rows, one for each row that keeps `GNUmakefile` or `makefile`. Each is a real `execute`, and its clone is made by pasting NEXT's own clone line. In that clone:
  - NEXT's lines are the row's;
  - `make which` prints the file the NOTE names;
  - `make -n bootstrap` has no rule;
  - the printed command exits 0 and ends `bootstrap ok`. For S rows, it does so once the shape's script is merged, as the NOTE says;
  - where a `Makefile` is kept, the follow-up's merge alone still leaves no rule;
  - an `include Makefile` in the file GNU make reads makes `make -n bootstrap` run the shape's recipe.

On main's `adopt-project.py`, the first 38 all fail. The 9 end to end rows fail on what NEXT prints. The unit rows that guard the unchanged cases fail there only for want of the `surviving=` keyword. M1 below shows the same rows passing and every row that keeps `GNUmakefile` or `makefile` failing. On this head, all 40 pass.

Mutants of the change, each restored and checked by sha256. Each run is the new file plus the five `next` tests of `test_adopt_e2e.py` (43 tests). Every mutant is killed:

| mutant | failed |
|---|---|
| M1 `first` never found | 27 |
| M2 `makefile` asked before `GNUmakefile` | 6 |
| M3 the command ignores `first` | 6 |
| M4 the branch for a `Makefile` kept beside `first` dropped | 6 |
| M5 the branch for a script gone aside ignores `first` | 9 |
| M6 the lookup folds case | 7, two of them existing `test_adopt_e2e.py` rows |
| M7 `cmd_execute` passes no paths | 9, the end to end rows |

Gate on this head, named files only, one basetemp per run. Each run had `EMAIL` set, `GIT_CONFIG_GLOBAL` an empty file, `GIT_CONFIG_NOSYSTEM=1`, a stub `gh` first on PATH, `GH_TOKEN=bogus` and an empty `GH_CONFIG_DIR`:
- the new file, `test_adopt_make_reads_first`, `test_adopt_e2e`: 98 passed, with the new file at 38 tests (at `c04bb710`; the fix round's gate is below).
- `test_adopt_github_exits`, `test_adopt_printed_sentences`, `test_repo_hygiene`, `test_readme_windows_quoting_sentence`, `test_cli_reference`, `test_agents_md_codes`: 291 passed.
- `test_scaffold_leg_path_collisions`: 104 passed.
- `test_adopt_contracts_dir`, `test_adopt_verify`, `test_adopt_pins`, `test_adopt_root_manifest`, `test_adopt_ignored_shape_files`, `test_adopt_nested_attributes`, `test_adopt_shape_pin_and_work_dir`: 378 passed.
- `test_shape_doctor.py` in 4 shards: 112 + 112 + 112 + 109 = 445 passed, and 3 skipped (the PowerShell line, which is printed only on a Windows host).

## Fix round 1 (RV1121's SHOULD-1)

RV1121 found the code right and one test missing: nothing held `cmd_execute` to handing `_next_steps(surviving=)` only the root's paths. Its mutant X4, which passes `paths_for["root"] + paths_for["code"] + paths_for["drop"]`, kept all 43 tests green. The case is a real one: the `root-project-makefile` rule sends a kept `GNUmakefile` to the root, and its reason says that moving a Makefile that only builds the code to the code leg "is a one-line edit of the plan" (`contracts/path-classification.yaml`, lines 236-237).

Two end to end rows, `test_next_keeps_make_bootstrap_where_the_plan_sends_the_makefile_elsewhere[code]` and `[drop]`. Each keeps `GNUmakefile` in the source, edits the plan to send it to `leg: code` (then `leg: drop`), runs a real `execute`, and pastes NEXT's own clone line. Each asserts that NEXT's lines after the clone are `    make bootstrap` and nothing else, that the clone's root has no `GNUmakefile`, and that `make -n bootstrap` there exits 0 and runs `python3 scripts/bootstrap.py`.

The `execute`, paste and clone steps of the nine existing end to end rows moved into one helper, `executed_and_cloned`, which the two new rows share, so no block of the test is written twice. Those rows assert what they asserted. The file now holds 40 tests (38 and 2).

- **X4, on this head's `adopt-project.py` with the one line changed:** the new file and the five `next` rows of `test_adopt_e2e.py` (45 selected): 2 failed, 43 passed. Both new rows fail on NEXT's lines, which are `python3 scripts/bootstrap.py` and the NOTE "the shape's Makefile is the root's `Makefile`, but GNU make at the root reads the source's `GNUmakefile`, ...", naming a file the root does not have. The file was restored and its sha256 read `60bad59a...` again.
- **On main's `adopt-project.py`:** the two rows pass, since main printed `make bootstrap` there. They hold the unchanged case against the new lookup, and X4 is the mutant that tells them apart.
- **On this head `1c5173758`**, same environment as above: the new file 40 passed, `test_adopt_e2e.py` 27 passed, `test_repo_hygiene.py` 100 passed.
- **Merge:** `git merge-tree --write-tree` of this head with main at `823624d93`: clean (tree `73ddafb2`).

RV1121's four NITs are not in this PR; the lane files them.

## Merge

- `git merge-tree --write-tree` of this head with main at `6da757cbe`, where the branch starts: clean (tree `21955cc7`). Main has since moved to `7cd66902a`, which changes `scaffold-project.py`, its test and the docs, and none of this branch's files. The merge with it is clean too (tree `ec4e6c8e`).
- With #1105's head `5a4de7f76`: clean (tree `3394d19d`). #1105's `adopt-project.py` change is in `_predict_collisions`, apart from `_next_steps`. On that merge, built in a `git clone --shared` checkout, `test_adopt_next_make_reads_first`, #1105's `test_adopt_collision_make_reads` and `test_pinned_copy_text` gave 152 passed.
- With the other open pull requests that change `adopt-project.py` (#1120 `c55e55e3`, #1119 `bdcf9952`, #1118 `c60066cb`, #1106 `504a1dd6`, #1091 `160de118`): each clean.

## Not in this PR

- Once #1105 lands, the lookup "which of `GNUmakefile` and `makefile` the root keeps first" is written three times: in `_makefile_follow_up`, `_predict_collisions` and `_next_steps`. Each is the same two lines over `GNU_MAKE_READS` and `_root_keeps`. They are kept apart here so this PR and #1105 touch disjoint lines; one helper could serve all three after both land.
- `templates/assembly-root/AGENTS-shape.md` line 19 ("`git clone --recurse-submodules` this repository and run `make bootstrap`") has the same gap in an adopted root that keeps `GNUmakefile` or `makefile`. It is pinned, and #1105 changes that file, so it is left.
- On a disk that ignores case (#1049), a kept `makefile` is reported as a `Makefile` collision, and NEXT's NOTE says "the root keeps its own Makefile". The new clause is true there too, since GNU make reads that file and merging into it brings the targets. The spelling is #1049's.
- A kept directory named `GNUmakefile/` or `makefile/` is named as the source's file. GNU make stops on it with "Is a directory". `_makefile_follow_up` does the same.

Closes #1047

Lane: openreposhape-4 (openRepoShape-4)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Oct 24, 2026
…` and refuse a partial-clone source that lacks an object its branches, tags or HEAD reach as `source-partial-clone`, so neither fetches into the source any more (#1145) (#1180)

`check` promises never to write into the source, and a partial clone (`git clone --filter`) broke that through git itself. A read that needs an object the clone lacks fetches it from the promisor remote. That runs the `upload-pack` the source's config names, and writes a promisor pack into the source's `objects/`.

- `check` and `execute` now run git with `GIT_NO_LAZY_FETCH=1`. They refuse a partial-clone source that lacks an object its branches, tags or HEAD reach as `source-partial-clone`. The refusal names how many objects it lacks and the cure for each promisor remote, `git fetch --refetch --no-filter <remote>`. The walk is `git rev-list --objects --missing=print --branches --tags HEAD --`.
- Measured with git 2.43, on a `--filter=blob:none --no-checkout` clone:
  - On base, `check` ran `upload-pack` 26 times and wrote 26 promisor packs into the source. `execute` failed with `Blob not found` after both leg repositories were made.
  - On head, both exit 2 with `source-partial-clone`, run nothing, leave `objects/` unchanged and make no leg.
  - A full clone and a cured partial clone adopt as before.
- `tests/test_adopt_partial_clone_source.py` holds the rows, and `docs/cli.md` gains the help text.

Review: lane 1's independent reviewers found `2c8082a2` NOT READY for one SHOULD (#1180 issuecomment-6351633105), and `6cd96b8b` NOT READY for one SHOULD (issuecomment-6356200375). They found `b316f415` provisionally READY (issuecomment-6364551003), and `a4ec51d2` READY with 4 NITs (issuecomment-6372009946). After #1119 landed, the branch was restacked onto main by three merges, and its changed lines equal the reviewed delta. Lane 1's delta check of the NIT round `a0e8cded` found the tool's AST unchanged apart from docstrings, and the two new rows kill the surviving mutants M15 and M17 (issuecomment-6374457465).

Tests, under a stub `gh` with a bogus token: at `a0e8cded`, lane 1's gate gave 650 passed and 3 skipped over the PR's test file, the throwaway and single-repository-record files, the doc trio, the path-guard and no-auto-maintenance scans, `tests/test_scaffold_leg_path_collisions.py` and `tests/test_doctor_partial_clone.py`. The doctor's 4 shards passed on #1190's head `359a749c`, whose tree is this PR's plus #1190's change.

It lands by fast refresh (Brett Heap, 2026-10-15T22:48:35Z). Its H0 `a0e8cded` is green on Linux and SonarCloud. The branch was never rebased. After H0, the only commit is update-branch's clean merge of main, giving 58a2175, which is green on SonarCloud and Git Bash. Lane 1's gate passed on its tree: 2 pytest calls over `tests/test_adopt_partial_clone_source.py`, `tests/test_path_guard_hygiene.py`, `tests/test_no_auto_maintenance.py`, `tests/test_scaffold_leg_path_collisions.py`: 321 passed, 2 skipped.

Closes #1145

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Nov 8, 2026
… parent's `-c` settings, `attr.tree` too, and the source's objects, `GIT_ATTR_SOURCE` is refused by name before any question, and each throwaway reads its environment first (#1136) (#1169)

`check`'s throwaways from #910 (the rehearsal, `_stored_as_written`, `_AttributesProbe`) were made with `_inherited_env()`. That leaves out a parent git's `-c` settings (`GIT_CONFIG_PARAMETERS`), `GIT_ATTR_SOURCE` and any `attr.tree` in `GIT_CONFIG_COUNT`. `_stored_as_written` held none of the source's objects, and the rehearsal chose the files it asks about with `check-attr --cached`, which reads no `attr.tree`. So `check` answered as a git with none of them would. `execute` then stopped after both legs existed, or refused a plan its own git would carry out.

- The rehearsal and `_stored_as_written` now take a parent's `-c` settings, `attr.tree` included, and the source's objects. Each throwaway reads its environment before it is made.
- `GIT_ATTR_SOURCE` is refused by name before any question is asked.
- The rehearsal's attribute reads run without `--cached` (ruling X1), so a parent's `attr.tree` reaches them as it reaches `execute`'s git.
- Measured with git 2.43, against base `bba168bd`:
  - A parent's `-c core.protectHFS=true`, or a `required` clean filter that fails, used to pass `check` and stop `execute` with both legs made. Now `check` refuses it, and `execute` refuses it before any leg.
  - A parent's `attr.tree` naming the empty tree used to refuse a plan `execute`'s own git carries out. That plan is now `plan ok` and adopts.
- Known limits: under a failing filter that two places' rules name, a misnamed second finding comes beside the right first one. `update-index -q --refresh`'s exit is not read, and the trace is on #1192. Under `attr.tree`, the copies finding asks for lines in the root `.gitattributes`, which git does not read there.

Review: lane 1's independent reviewers reviewed each round. r1169c's review of `76c9b767` stopped part-way with no MUST, and r1169d finished it NOT READY for two SHOULDs (#1169 issuecomment-6374351753). The fix round `d9224ecf` adds r1169d's own row (l), which kills mutant K3 (3 of 3), and corrects the body. r1169e found `d9224ecf` READY, with no MUST or SHOULD (issuecomment-6375188826). After #1119 landed, the branch was restacked onto main by three merges, and its changed lines equal the reviewed delta (sha256 `5c59dd53`).

Tests, under a stub `gh` with a bogus token: at `d9224ecf`, lane 1's gate gave 1,253 passed and 9 skipped over 7 calls. They covered the PR's five test files, the single-repository-record file, the no-auto-maintenance and path-guard scans, `tests/test_scaffold_leg_path_collisions.py`, `tests/test_adopt_root_symlinks.py`, and `tests/test_shape_doctor.py` in 4 shards.

It lands by fast refresh (Brett Heap, 2026-10-15T22:48:35Z). Its H0 `d9224ecf` is green on Linux and SonarCloud. The branch was never rebased. After H0, the only commit is update-branch's clean merge of main, giving 989295b, which is green on SonarCloud and Git Bash. Lane 1's gate passed on its tree: 7 pytest calls over `tests/test_adopt_check_throwaway_repos.py`, `tests/test_adopt_single_repository_record.py`, `tests/test_adopt_nested_attributes.py`, `tests/test_adopt_reencoded_files.py`, `tests/test_command_scopes_built_at_the_test.py`, `tests/test_no_auto_maintenance.py`, `tests/test_path_guard_hygiene.py`, `tests/test_scaffold_leg_path_collisions.py`, `tests/test_adopt_root_symlinks.py` and `tests/test_shape_doctor.py` in 4 shards: 1,253 passed, 9 skipped.

Closes #1136

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Nov 8, 2026
…s message spells a path byte that is not UTF-8 as `_shown` does, an escape, and no longer says it names it with a `?` in its place, which #900 replaced (#1018) (#1104)

`env_commit`'s docstring in `scripts/shape_materialize.py` says what the split commit's message does with a `root` or `drop` path that holds a byte that is not UTF-8, as the code does since #900. It said the message names such a path "with a `?` in place of that byte, a path that does not exist". That was true before #900, and it is not true now. A reader of the docstring is told about a `caf?.md` the tool no longer writes.

Found by lane 1's writer on #900 and filed as #1018. Lane 5 took it outside v1.2.0, under Brett Heap's ruling of 2026-10-21T10:32:04Z.

Measured on main `2a119f281` and again on `16f9a0e81` (main after #1087), with a small script that calls: `adopt-project.py`'s own `_split_message` and `_shown`, then `env_commit` and a real `git commit`, read back with `cat -A`.

- The issue's claim held: the docstring (about line 2103 when filed, line 2268 on `2a119f281`, line 2349 on this branch) still said the message names the byte with a `?`.
- The code does not. `_shown("caf\udce9.md")` is `` `caf\udce9.md` ``, and the split message for a `root` and a `drop` entry named that holds no lone surrogate. Through `env_commit` and `git cat-file commit HEAD`, `cat -A`:

  ```
  DROPPED (in no leg and no longer here):$
    `caf\udce9.md`$
  STAYS IN THE ASSEMBLY ROOT:$
    README.md$
    `caf\udce9.md`$
  ```
- What stays true of `env_commit` itself: a message that holds the lone surrogate, handed to it directly, is still written as `?`. `cat -A` reads `Name: caf?.md$`. So the `errors="replace"` paragraph is kept, and what changes is who it describes.
- #1087 (main `16f9a0e81`) changed `scripts/shape_materialize.py` by 128 lines, none of them in `env_commit`, `COMMIT_COMMAND` or the message, and changed nothing in `adopt-project.py` (0 diff lines). The rebase onto it was clean and the docstring needed no other change.

One file, `env_commit`'s docstring, in two paragraphs: the `errors="replace"` paragraph (round 1, 11 lines added and 7 removed) and the sentence above it (round 2, below; 3 added and 2 removed). Together 14 added and 9 removed against main `6da757cb`. The `errors="replace"` paragraph now says:

- encoding, a character UTF-8 cannot carry is written as `?`, and nothing raises after both legs are pushed, as before;
- `adopt-project.py` refuses a lone surrogate in a path it extracts (`entry_path_problem`), and a `root` or `drop` entry that holds one is listed in the split message but never raw: the message names each path as `plan`'s report does (`_shown`, #900), so the byte 0xE9 reads as an escape with no surrogate in it, `` `caf\\udce9.md` ``, the path the source holds;
- written raw it would be a `?`, `caf?.md`, a path that does not exist, which is what `env_commit` still writes for a lone surrogate it is handed.

The backslash is doubled in the source, as the neighbouring docstrings write `\\udcc3`. A single `\u` in a non-raw docstring would be an escape that makes a lone surrogate. Measured: the docstring holds no character from U+D800 to U+DFFF and holds the six characters `\udce9`. The longest docstring line is 78 characters.

**The code is unchanged.** A script compares the AST of `scripts/shape_materialize.py` with every docstring blanked: the branch's file is identical to `origin/main`'s after the rebase onto `16f9a0e81` (round 1), and the merge of this head into `origin/main` `6da757cb` is identical to `6da757cb`'s file (round 2).

**No pin moves.** `scripts/shape_materialize.py` is not a pinned copy. `COPIED_VERBATIM`, `COPIED_FROM_SHAPE`, `FAMILY_COPIED_VERBATIM` and `FAMILY_COPIED_FROM_SHAPE` do not list it, and `templates/assembly-root/scripts/validate-pins.py` says of it "which no project carries". So no `upstream-changed` row, no digest, no assembly copy and no `tests/test_pinned_copy_text.py` entry changes.

**No other text pins these words.** Searched in `tests/`, `README.md`, `docs/`, `docs/handbook.html`, `AGENTS.md` and `templates/`: no test reads `env_commit`'s docstring (`__doc__`, `getdoc`), and none of them holds "in place of that byte" or "a path that does not exist" for this. The other places that name the `?` are about this function's behaviour or about history, and are still true: `_split_message`'s docstring says `caf\\udce9.md` "was `caf?.md`" (before #900), `_refused_by_policy` already says the message "lists it as `_shown` does (#900)", and `tests/test_adopt_spelled_names.py` quotes the message as it was on `c7a3ff16`. The `?` in `argv_text`, `trailer_line` and the commit-trailer text is about a `--trailer`, not the split message.

No rows and no mutants: the change is a docstring, the code is the same AST, and no test pins the docstring. So there is no row to fail on main and nothing to mutate.

All with `$Q/v110/run9.sh`, `EMAIL=service@opensoft.dev`, `GIT_SSH_COMMAND=false`, `GIT_CONFIG_GLOBAL=/dev/null`, the stub `gh`, a fake token, git 2.43.0 (`/usr/bin/git`).

Two shards, run before the rebase, at the `2a119f281` base with this one-file change in the tree. Together they are every `tests/*.py` that names `shape_materialize` or `env_commit` (86 files, including `tests/test_adopt_spelled_names.py` and `tests/test_adopt_entry_paths.py`, which run `env_commit` and the #900 message) and `tests/test_agents_md_codes.py`:

| run | files | result |
|---|---|---|
| shard A | 43 | 2873 passed, 4 skipped in 1707.51s, rc=0 |
| shard B | 44 | 4611 passed, 31 skipped in 2462.01s, rc=0 |

After the rebase onto `16f9a0e81`, on the committed head `e32b2703d733f6c60c1b2cba2d62d164d93d7b9a` (`dirty=0`): the trio, `tests/test_repo_hygiene.py`, `tests/test_agents_md_codes.py` and `tests/test_cli_reference.py`, **170 passed in 55.67s**, rc=0. The docstring-only change needed no new shards: the shards' counts are runs at the `2a119f281` base, and #1087 did not touch `env_commit`.

Round 2, on the committed head `de89aa81b1533e32f1007f991aeae05cd915b36d` (`dirty=0`): the same trio, **170 passed in 61.77s**, rc=0. The round 2 commit is one docstring sentence, so it needed no new shards either.

git 2.47.1: no run. The change adds no test file of its own, so there is none to run on it.

`git merge-tree --write-tree HEAD <other>` with HEAD at `de89aa81b1533e32f1007f991aeae05cd915b36d`, after a re-fetch of `origin main` with `-c gc.auto=0 -c maintenance.auto=false` (round 2, about 14:03Z on 2026-10-21):

| against | result |
|---|---|
| `origin/main` `6da757cbe9a4eb66d60c34f38dcc2b332d175e8b` | clean, rc 0, tree `15c11b6c959fc4c7540105d5d62e3f961c8e27fd` |
| #1105 `fix/1048-makefile-follow-up-gnu-read-order` `5a4de7f763b5a3c32adce9614a3a539508416eb4`, an open PR that touches `scripts/shape_materialize.py` | clean, rc 0, tree `db05aead67052643942e35a9689d9dd4a0873fda` |
| #1118 `fix/1089-named-link-skip-tests` `c60066cbeec18b8b6c926e254ed4bee6b9a486c2`, the other open PR that touches `scripts/shape_materialize.py` | clean, rc 0, tree `a40df3b63c32c2a3d8f4c40587b094940c8d6567` |

Read with `gh pr list --state open --json number,headRefName,headRefOid,files`: 16 open PRs, three of them (this one, #1105 and #1118) touch `scripts/shape_materialize.py`, the only file this change touches. The other 13 (#1120, #1119, #1117, #1111, #1109, #1107, #1106, #1102, #1101, #1098, #1097, #1091, #1088) touch no file this change touches.

The same command with HEAD at the reviewed head `e32b2703` gives rv1104's trees: `de31d857` against `6da757cb` and `195ffe19` against #1105. The round 1 merge against #1090 is moot: #1090 has landed in `6da757cb`.

Held as a draft until the v1.2.0 tag; it is now on the v1.2.1 milestone.

- **NIT 1, `scripts/shape_materialize.py` lines 2340-2343, a line round 1 did not change.** The paragraph above the reworded one said the adoption's split message "names each entry path as the source spells it". Since #900 that is true of a printable path alone: `_split_message` lists any other through `_shown`, as the escape the next paragraph describes. It now says "as the source spells it (a printable one; see below)". One new commit on top of `e32b2703`, `de89aa81b1533e32f1007f991aeae05cd915b36d`, 3 lines added and 2 removed, docstring only, pushed as a fast-forward (no rebase, the PR is open).
  - Measured: the AST with docstrings blanked is identical to `e32b2703`'s and, merged into `6da757cb`, to `6da757cb`'s. The docstring holds no lone surrogate, still holds the six characters `\udce9`, and its longest line is 78 characters.
  - Searched `tests/`, `README.md`, `docs/`, the handbook mirror, `AGENTS.md`, `templates/` and the scripts for "names each entry path", "as the source spells" and the new words: only `tests/test_adopt_entry_paths.py` line 233 matches, a test's docstring about `execute` handing each path on as the source spells it, which is another sentence and pins nothing here.
- **NIT 2, this body.** It named main as `16f9a0e81` and #1090 as the one open PR that touches the file. #1090 has landed. Main is `6da757cb`, and the merge-trees above are redone against it, against #1105 and against #1118, a draft that opened since and also touches the file.

Review: rv1104-1107, lane 5's independent opus reviewer, which wrote none of #1104: READY at `de89aa81`, H0 of this landing (issuecomment-6318111413), with no finding; NIT 1 and NIT 2 of the round before are fixed. CI (Brett Heap's ruling 27 and his fast-refresh ruling of 2026-10-15T22:48:35Z): H0 de89aa8 passed tests, tests-git-bash and SonarCloud; the head 92285d4 is a clean update-branch merge of main b66d8f0, with tests-git-bash and SonarCloud green; and the local gate passed on its predicted squash tree 6bd2128 over main b66d8f0: the repo_hygiene, agents_md_codes, cli_reference sets, 171 passed. Landed on Brett Heap's word of 2026-10-23T13:10:26Z, relayed to every lane by lane 3 (#473 issuecomment-6365697123): "merge v1.2.1 PRs once reviewed and green".

Closes #1018

Lane: openreposhape-5 (openRepoShape-5)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Nov 8, 2026
…eeps a `GNUmakefile` or a `makefile`, which GNU make reads first, the Makefile collision follow-up no longer says the shape's make targets arrive with the merge, and the pinned file says `make` reads the first of the three and the block reaches it only once that file holds it or includes a Makefile that does (#1048) (#1105)

Two sentences said a kept `Makefile` is what holds the shape's block back. GNU make reads the first of `GNUmakefile`, `makefile` and `Makefile` it finds, and only that one, so where the root also keeps `GNUmakefile` or `makefile`, `make` reads that file, and reaches the merged `Makefile` only where that file includes it.

**Measured** (a real `execute`, then `make -n` in the clone, for each kept name; r1046's NIT-B, repeated; the include rows are RV1105's SHOULD-1):

| the source keeps | file `make` reads | after `cat shape/Makefile >> Makefile`, `make -n bootstrap` |
|---|---|---|
| `Makefile` | `Makefile` | exit 0 |
| `Makefile` + `makefile` | `makefile` | exit 2, "No rule to make target 'bootstrap'" |
| `Makefile` + `GNUmakefile` | `GNUmakefile` | exit 2, same |
| all three | `GNUmakefile` | exit 2, same |
| `Makefile` + a `makefile` or `GNUmakefile` that ends in `include Makefile` | that file | exit 0, runs `python3 scripts/bootstrap.py` (exit 2 before the merge) |
| `makefile` or `GNUmakefile` alone | that file | no merge to make: the shape's `Makefile` is the root's, and `make contracts-dir` has no rule until that file holds the block or includes `Makefile` |

**What changed**
- `scripts/shape_materialize.py`, `collision_follow_up`: takes `reads_first`, the file GNU make reads before the shape's `Makefile`. Where it is set, the Makefile's follow-up says GNU make reads that file, the first of the three it finds, and that the shape's make targets reach `make` there only once that file has them, or includes a Makefile that does; until then `python3 scripts/bootstrap.py` is what the shape's `make bootstrap` runs. Where `scripts/bootstrap.py` went aside too, it says neither command runs the shape's bootstrap until the script is merged, and then `python3 scripts/bootstrap.py` runs it. Where the root keeps only the source's `Makefile`, the sentence is what it was, byte for byte. `GNU_MAKE_READS` joins `SHAPE_MAKE_TARGETS`.
- `adopt-project.py`, `_predict_collisions`: reads the file from the plan's root paths as `_makefile_follow_up` does, so the `CONTRACTS_DIR` line and the collision line of one plan name the same file.
- `templates/assembly-root/AGENTS-shape.md`: says `make contracts-dir` prints the block wherever `make` reads the shape's Makefile, names the three files in the order the code reads them, and says that where a `GNUmakefile` or `makefile` is kept, beside a `Makefile` or instead of one, `make` reads it, and not the shape's Makefile unless that file includes it. It is one verbatim copy: the digest in every project's `contracts/shape-pin.yaml` is rendered from its bytes at adopt and scaffold time, and no digest or `tree_sha256` of it is stored in this repository, so there is no pin to move here; each project re-pins through `update-shape.py` as for any template change.
- `tests/test_pinned_copy_text.py`: `ADOPTED_SENTENCES` holds the new text.
- `tests/test_adopt_collision_make_reads.py` (new, 31 rows): the follow-up per kept set, the two lines of one plan naming one file, no collision line where no `Makefile` is kept, the two name tuples equal, the pinned file naming every file the code reads first, the script-aside tail where `make` reads another file, and, in a real adoption's clone, `make -n` held to both sentences for each kept name and for a kept `GNUmakefile` or `makefile` that ends in `include Makefile`.

**Fix round 1** (RV1105's review, `b28f40de0` on top of `5a4de7f76`)
- **SHOULD-1:** the follow-up said the targets reach `make` "only once `X` holds them, or includes a Makefile that does, and not with that merge alone". The last clause is untrue where `X` already ends in `include Makefile`: the merge alone brings them. The clause is gone, and the docstring paragraph, the `SHAPE_MAKE_TARGETS` comment and the `ADOPTED_SENTENCES` comment now say the merge alone brings the targets only where that file includes `Makefile`.
- **NIT-1 (and RV1121's NIT-3):** the follow-up now uses the words of #1121's NOTE: "has them", and "is what the shape's `make bootstrap` runs". `_next_steps` is not touched.
- **NIT-2:** the script-aside tail, where `reads_first` is set, said "neither `make bootstrap` nor `python3 scripts/bootstrap.py` runs the shape's bootstrap before both merges are made", which reads as if `make bootstrap` runs it after them. It now says neither runs it until the script is merged, and then `python3 scripts/bootstrap.py` runs it. I did not keep "before both merges": where `X` is given the targets itself, as the sentence before invites, `make bootstrap` runs the shape's bootstrap once the script is merged, with no `Makefile` merge at all.
- **NIT-3:** the template's "and never the shape's Makefile" is now ", and not the shape's Makefile unless that file includes it". `ADOPTED_SENTENCES` changes with it. No other copy or regex in `tests/` pins that sentence (`git grep` for "never the shape" and "reads the first of them").
- **NIT-4:** the `collision_follow_up` docstring no longer says `execute` reports through it. It says `plan` predicts the follow-ups and stores them in `follow_ups:`, the split commit carries those, `execute` names its collisions by path only (its `beside` lines and the "WRITTEN BESIDE" list), and `Materialized.follow_ups()` has no caller. The method stays.
- NITs 5 to 8 are left to the lane.

**Rows of this round:** `tests/test_adopt_collision_make_reads.py` and `tests/test_pinned_copy_text.py`, 118 rows. With this round's test files on `5a4de7f76`'s code, 17 fail and 101 pass; on `b28f40de0` all 118 pass. The 17 fail for the right reason:

| rows | why they fail on `5a4de7f76` |
|---|---|
| 4 collision rows (`Makefile+makefile`, `Makefile+GNUmakefile`, all three, `Makefile+GNUmakefile+scripts`) | the line holds "not with that merge alone" |
| 2 new script-tail rows (`GNUmakefile`, `makefile`) | the line holds "before both merges are made" |
| 2 new include rows in a real clone (`GNUmakefile`, `makefile`) | `make -n bootstrap` exits 2 before the merge and 0 after it alone (both asserted first, and passing), then the split message holds "not with that merge alone" |
| 7 clone rows, the pinned-file row and `test_a_scaffolded_root_is_told_its_makefile_exports_none` | the pinned file still says "and never the shape's Makefile" |

**Mutants of this round**, each run against the 118 rows on `b28f40de0`, each file restored and its sha256 checked:

| id | mutant | rows failing | result |
|---|---|---|---|
| MA | ", and not with that merge alone" put back | 6 | killed |
| MB | the script-aside tail put back ("before both merges are made") | 3 | killed |
| MC | the template's "and never the shape's Makefile" put back | 9 | killed |
| MD1 | include rows: `not in message` inverted to `in message` | 2 | killed |
| MD2 | include rows: after the merge, `returncode == 0` inverted | 2 | killed |
| ME | "holds them" put back | 6 | killed |
| MF | "the shape's `bootstrap` target runs" put back | 5 | killed |

**Gate** on `b28f40de0`:

| files | passed | failed | skipped |
|---|---|---|---|
| `test_adopt_collision_make_reads`, `test_pinned_copy_text`, `test_adopt_make_reads_first`, `test_adopt_contracts_dir`, `test_adopt_printed_sentences`, `test_adopt_e2e`, `test_repo_hygiene`, `test_scaffold_leg_path_collisions` | 470 | 0 | 0 |
| `test_shape_doctor`, 8 shards (62 + 56 + 54 + 56 + 60 + 57 + 58 + 45 collected) | 445 | 0 | 3 (PowerShell rows, Windows host only) |
| **total** | **915** | **0** | **3** |

**Merge-tree:** `b28f40de0` against main `4ed7c7bf5` is clean, and so is it against each open PR that touches these files: #1104, #1106, #1118, #1119, #1120, #1121 (`1c5173758`), #1122, #1123 and #1125.

**Round 0** (`5a4de7f76`, main `6da757cbe` merged in): on main's code (plus the constant alone), 12 of the then 27 rows failed and 15 passed; the 30 test files that run the changed code or read the template gave 2109 passed, 0 failed and 4 skipped (3 PowerShell rows in `test_shape_doctor.py`, 1 MedxEHR row in `test_adopt_plan.py` that needs a clone `SHAPE_MEDXEHR_CLONE` names); nine mutants each failed 1 to 11 of the then 114 rows.

**Not here**
- The third box of #1048, the `_next_steps` NOTE in `adopt-project.py`, is covered by the change #1047 makes to it: see #1047 and #1121.
- `templates/assembly-root/AGENTS-shape.md` line 19 and the five-commands table say `make bootstrap` without this caveat. They say nothing about a kept file, so they are outside #1048's two sentences.

Closes #1048

Lane: openreposhape-4 (openRepoShape-4)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment