Skip to content

the family root's siblings.py: with a token in use, the clone and the fetch that carry it run in bootstrap.py's untraced() environment, so make siblings and family.py siblings write the token to no trace the caller asks git for (#1055) - #1075

Merged
brettheap merged 3 commits into
mainfrom
fix/1055-siblings-untraced
Oct 21, 2026

Conversation

@brettheap

@brettheap brettheap commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Lane: openreposhape-5 (openRepoShape-5)
Claim: #1055 (comment)

Goal

make siblings and family.py siblings now write the token in SHAPE_LEGS_TOKEN or SHAPE_LEGS_APP_TOKEN to no trace the caller asked git for. #996 (#1045) did the same for make bootstrap.

Lane 5 found this in its independent review of #1045 (rv996, #1045 issuecomment-6288783192) and measured it in #1055.

What changed

templates/family-root/scripts/siblings.py gives the credential's -c url.https://x-access-token:<token>@github.com/.insteadOf=... rewrite, in argv, to two gits:

  • the clone in place, for a member with no clone beside the holder;
  • the fetch in place, for a member whose clone is there.

Both ran in the caller's own environment. So every trace2 target the caller named wrote the token in the start event of each, one line per member. scrub kept it out of the output, and only that part held.

Now:

  • rewrite_environment(token), a new function in siblings.py:
    • with a token in use, it returns untraced() from the bootstrap.py beside it. That is every GIT_TRACE* variable taken out, and GIT_TRACE2, GIT_TRACE2_EVENT and GIT_TRACE2_PERF set to 0, which also turns off a target the global or the system config names;
    • with no token, it returns None, so the two gits keep the caller's environment and stay traced where the caller asked, as before.
  • The clone and the fetch each run with env=rewrite_environment(token). Nothing else in the file changes environment.
  • The member's own scripts/bootstrap.py, which place runs after a clone, is given no token in its argv, and no env, so it keeps the caller's environment and its traces. That environment holds SHAPE_LEGS_TOKEN and SHAPE_LEGS_APP_TOKEN as before; the shipped member bootstrap (templates/assembly-root/scripts/bootstrap.py) reads neither. A test row holds this, and so does tests/test_validator_child_encoding.py, which reads that call as handing no env.
  • the family root's bootstrap.py and siblings.py: under GNU make 4.4's named-pipe jobserver, a member's make that says it is 4.4 or later is handed the holder's MAKEFLAGS as it is, so it runs its jobs together again; every other make still gets no jobserver and no -j (#987) #1023's jobserver handling is untouched: dispatch still runs each member's make with member_make_environment(make), and no MAKEFLAGS reading changed.
  • The docstrings now say the promise in full: git_prefix says the token is "never written into .git/config, never persisted, never printed, and never traced". place's ABSENT paragraph in the module docstring says the same, and rewrite_environment says what was measured.

One thing the issue did not ask for. GIT_TRACE_PERFORMANCE writes a git's whole command line, -c included, on git 2.43.0 and 2.47.1 alike. So at main it held the token too, two lines per run, and the rows below set it. untraced() already takes it out: every GIT_TRACE* variable goes.

This follows #1045's untraced() (#996): the clone and the fetch run in the very environment make bootstrap gives every git it hands the token.

Where the helper comes from: imported, not copied

siblings.py imports untraced from the bootstrap.py beside it, next to REWRITE_TARGETS, credential, member_make_environment, member_path and members_of.

The issue left this to the writer, because a holder may keep a bootstrap.py older than its siblings.py (update-shape.py apply --accept-local), and an import of a name that copy lacks would stop make siblings, park and resume with an ImportError. The issue's text was written against main 1a47ed07, where siblings.py still kept its own copy of the MAKEFLAGS readings. Since then #1023 (9e154d87, lane 4's) has decided the question for this pair of files:

untraced takes the same road for the same reasons:

Probes, before (main 00f6d7f7) and after (this change)

Setup:

  • git 2.43.0 (/usr/bin/git) and git 2.47.1, Python 3.12.3;
  • the family of tests/test_family.py (two members, local bare remotes, the file transport allowed through GIT_CONFIG_COUNT), laid out as <tmp>/InkRouter/InkRouter;
  • offline: a stub gh first on PATH, a bogus GH_TOKEN, an empty GH_CONFIG_DIR, GIT_SSH_COMMAND=false;
  • the holder's own scripts/siblings.py was main's (sha256 86bf57e0...) or this change's;
  • the fake token w1055-a-token-no-forge-takes.

Each case was run three ways:

  • make siblings, which clones both members;
  • make siblings again, which fetches both;
  • family.py siblings --family-root <holder> in a fresh layout, which clones both.

Traces were asked for in one of three ways:

  • the seven variables GIT_TRACE, GIT_TRACE2, GIT_TRACE2_EVENT, GIT_TRACE2_PERF, GIT_TRACE_PACKET, GIT_TRACE_CURL and GIT_TRACE_PERFORMANCE, each set to a file under the run's TMPDIR;
  • trace2.eventTarget, normalTarget and perfTarget in a scratch GIT_CONFIG_GLOBAL;
  • the same three in a scratch GIT_CONFIG_SYSTEM.

Each way was run with SHAPE_LEGS_TOKEN, with SHAPE_LEGS_APP_TOKEN, and with no token.

That is 108 runs at main and at the change, 54 for each git. Each run's TMPDIR and the whole family folder (the clones' .git/config included) were searched for the token.

traces asked by main, a token set (both variables, both gits) this change, a token set no token, either
the seven variables trace2-event.json, trace2-normal.txt, trace2-perf.txt, trace-performance.txt: 2 lines each, in the clone, the fetch and family.py alike none no token anywhere; the same files written
the global config cfg-event.json, cfg-normal.txt, cfg-perf.txt: 2 lines each none the same
the system config sys-event.json, sys-normal.txt, sys-perf.txt: 2 lines each none the same

What the table rests on:

  • Every one of the 108 runs (and the 54 runs again on this head's final bytes, sha256 b1e7bb21...) exited 0 and ended siblings ok: 2 member(s) beside the holder in ..., with the token in neither stdout nor stderr.
  • The 2 lines in each file are the start (or performance:) line of the git given the -c, one for each member. Each carries the token twice, once for each spelling the rewrite covers.
  • With every member a local path, GIT_TRACE's file was written and held no token, and GIT_TRACE_CURL wrote nothing, with no http remote. An https member is a different case, measured below.
  • With the change and a token set:
    • every trace file but GIT_TRACE_PACKET's was still written, by the run's other gits and the member's own bootstrap;
    • the packet trace was not written, since only the clone and the fetch write one here.

An https remote, measured offline (rv1075, then the row of Round 2). The rewrite reaches only github.com, and the probes above are offline, with local paths. A member whose .gitmodules url is https://github.com/<org>/<repo>.git is reached by it, and can be tried offline behind a dead proxy, http.proxy=http://127.0.0.1:9, so that nothing leaves the host:

  • rv1075 set one member's url so, with make siblings, a fake SHAPE_LEGS_TOKEN, and the seven variables set to files, on git 2.43.0 and 2.47.1. Main's siblings.py (86bf57e0) held the token in trace.txt, trace2-event.json, trace2-normal.txt, trace2-perf.txt and trace-performance.txt; this head held it in no file and no output. The clone failed cleanly, git clone exited 128, and make siblings exited 2.
  • GIT_TRACE's file holds it too, in the run_command: git remote-https origin https://x-access-token:<token>@github.com/... lines: 3 lines on git 2.43.0, 4 on 2.47.1 (rv1075).
  • Round 2 below holds this in a test row.

Still not measured: a github.com clone or fetch that succeeds, since no run here reaches the network.

Tests

tests/test_family_siblings_untraced.py is new. It uses tests/test_family.py's family and estate, offline from tests/test_family_add_refusals.py, files_holding and CREDENTIALS from tests/test_family_bootstrap_stops_named.py, and NO_CREDENTIAL and needs_make from tests/test_siblings_family_name.py. No assert joins conditions with and. The token is the fake w1055-a-token-no-forge-takes.

The rows. Each entry point is crossed with each way traces are asked for:

  • entry points: make siblings cloning, make siblings again fetching (after a first run with no token and no trace), and family.py siblings cloning;
  • ways: the seven variables, the three targets in a scratch global config (a copy of the session's, which keeps git's automatic maintenance off), and the three targets in a scratch system config.

Three tests (the third is round 2's):

  • test_with_a_token_in_use_the_token_is_in_no_trace: 18 rows (3 entries x 3 ways x 2 variables). Each asserts:
    • exit 0 and siblings ok: 2 member(s) ...;
    • the token in neither stdout nor stderr;
    • the token in no file under the test's directory: TMPDIR, every trace and config file, and the clones and their .git/config;
    • every trace file the way asks for written: 5 for the variables, 3 for each config;
    • the member's own bootstrap traced where a member was cloned: a git traced in a leg of each clone, which only that bootstrap runs. Where nothing was cloned, nothing ran there.
  • test_with_no_token_the_clone_and_the_fetch_are_traced_as_asked: 9 controls (3 entries x 3 ways). Each asserts exit 0, siblings ok and the same trace files written. It also asserts the trace2 event file holds siblings.py's own git at each member, read from the events: git clone -q --no-local ... to each member's slot, or git fetch --quiet origin in each member's clone.

Fails on main. The file was run against main's siblings.py (sha256 86bf57e0..., put in place and then restored, sha256 checked):

  • 18 failed, 9 passed: every row with a token failed, and the 9 controls passed;

  • the variables rows found the token in trace-performance.txt, trace2-event.json, trace2-normal.txt and trace2-perf.txt;

  • the global-config rows found it in cfg-event.json, cfg-normal.txt and cfg-perf.txt;

  • the system-config rows found it in sys-event.json, sys-normal.txt and sys-perf.txt.

  • test_an_https_member_with_a_token_in_use_is_in_no_trace: 3 rows, one for each of CALLERS_CONFIG (round 2 and round 3, below).

Mutants

Each mutant was applied alone to the worktree's file and the new test file run (git 2.43.0). The bytes were restored and checked by sha256 after each:

  • siblings.py sha256 b776c69b...: this head's code; it differs from the head only in two docstring lines;
  • bootstrap.py sha256 50a16751..., main's, unchanged here.

All are killed.

mutant result
M1 the token condition dropped: untraced()'s environment always killed, 9 failed (every control)
M1b the condition inverted killed, 27 failed
M1c the clone untraced always, the fetch as it is killed, 6 failed (the clone controls)
M2 the clone left in the caller's environment killed, 12 failed (every clone row with a token)
M3 the fetch left in the caller's environment killed, 6 failed (every fetch row with a token)
M4a GIT_TRACE2_EVENT not set to 0 (TRACE2_OFF) killed, 12 failed (the global- and system-config rows)
M4b GIT_TRACE2 not set to 0 killed, 12 failed (the same)
M4c GIT_TRACE2_PERF not set to 0 killed, 12 failed (the same)
M5 the GIT_TRACE* removal dropped (env = dict(os.environ)) killed, 6 failed (the variables rows, by GIT_TRACE_PERFORMANCE)
M6 the prefix narrowed to GIT_TRACE2 killed, 6 failed (the same)
M7 the member's own bootstrap run untraced too killed, 12 failed (the clone rows' bootstrap check)

M5 and M6 are killed only because the variables rows set GIT_TRACE_PERFORMANCE. With a local-path remote, of the issue's six variables none but trace2's writes the -c, and trace2's three are set to 0 whatever the prefix does. With an https remote GIT_TRACE writes it too, in the run_command: git remote-https lines; that is what the row of Round 2 holds, and the mutant R4 below, which keeps the caller's GIT_TRACE, passed all 27 earlier rows.

Runs

How every run was made:

  • through lane 5's runner: /usr/bin/python3 -m pytest -q -rs -p no:cacheprovider -o tmp_path_retention_policy=failed, one basetemp per run;
  • LANES_* cleared, a stub gh first on PATH, GH_TOKEN=bogus, an empty GH_CONFIG_DIR;
  • EMAIL=service@opensoft.dev, GIT_SSH_COMMAND=false, GIT_CONFIG_GLOBAL=/dev/null at the start of the session.
git files result
2.43.0 test_family_siblings_untraced.py, on this change before the docstring fix 27 passed
2.43.0 the same file against main's siblings.py 18 failed, 9 passed
2.43.0 gate shard A: 19 of the 41 files below 1623 passed, 4 skipped, in 19:51
2.43.0 gate shard B: the other 22 of the 41 files 2415 passed, 8 skipped, in 15:03
2.47.1 test_family_siblings_untraced.py 27 passed, in 1:14

What the 41 gate files are:

  • the new file;
  • test_family_member_make_jobserver.py;
  • every file grep -l siblings tests/ names;
  • the pinned-copy and upgrade-row files: test_pinned_copy_text.py, test_update_shape.py, test_update_shape_add.py, test_update_shape_kept_helper.py, test_no_auto_maintenance.py, test_template_text_placeholders.py, test_bootstrap_spelling.py;
  • the trio: test_repo_hygiene.py, test_agents_md_codes.py and test_cli_reference.py.

The gate runs (both shards at once, then the 2.47.1 run) were made on this change as it is committed: siblings.py sha256 b1e7bb21... and the new test file sha256 7ba7a416..., at main 00f6d7f7 (this branch's base; no rebase was needed, see Merges). The runner's START line read dirty=2, the two tracked files this commit changes, and a sha256 check after the runs found siblings.py unchanged (neither it nor the test files were modified after the runs started). The first pair of shards was stopped part way and re-run whole; only the re-runs count here.

The 41 files, by shard:

  • shard A (19): test_shape_doctor.py, test_validator_child_encoding.py, test_family_py_unreadable.py, test_pinned_copy_text.py, test_siblings_inputs.py, test_siblings_family_name.py, test_family_member_make_jobserver.py, test_update_shape_add.py, test_adopt_printed_sentences.py, test_family_printed_quoting.py, test_family_makefile_siblings.py, test_template_text_placeholders.py, test_park_resume_targets.py, test_windows_paths.py, test_doctor_gitmodules_not_utf8.py, test_local_remote_dir_urls.py, test_family_siblings_untraced.py, test_setup_family_folder.py, test_bootstrap_refusal_exit.py;
  • shard B (22): test_repo_hygiene.py, test_doctor_legs_members_next.py, test_update_shape.py, test_family.py, test_update_shape_kept_helper.py, test_yaml_reader_spaces.py, test_bootstrap_spelling.py, test_agents_md_codes.py, test_family_siblings_placement.py, test_no_auto_maintenance.py, test_cli_reference.py, test_family_siblings_bootstrap_unreadable.py, test_bootstrap_branch_bytes.py, test_family_init_asks_every_value.py, test_validate_workflow_gitmodules_urls.py, test_adopt_contracts_dir.py, test_doctor_unfinished_merge.py, test_family_members_not_a_list.py, test_family_init_landing.py, test_family_docs_one_spelling.py, test_doctor_members_spelled.py, test_pins_child_kind_and_siblings_root_spelling.py.

The skips are the environment's, each named by its reason:

  • shard A: 3 in test_shape_doctor.py (the PowerShell line is printed, and powershell.exe exists, only on a Windows host) and 1 in test_family_member_make_jobserver.py (make -j2 here writes --jobserver-auth=3,4, not GNU make 4.4's named pipe);
  • shard B: 2 in test_doctor_legs_members_next.py (this git crashes on submodule init, exit 139, as git 2.43 does on a bare branch), 4 in the same file (this git dies on a shallow of -2**31, as git 2.43 does), and 2 in test_no_auto_maintenance.py (the session began with GIT_CONFIG_GLOBAL set, and its file includes that one);
  • the 2.47.1 run of the new file skipped none.

The full suite was not run.

Static checks:

  • flake8 finds in siblings.py only the three lines it finds at main (two E501, one E127, each moved down by the added lines);
  • in the new test file it finds only the F811s every fixture-importing file has;
  • both files are ASCII, apart from the dashes and the CJK example already in siblings.py.

Merges

Fetched with -c gc.auto=0 -c maintenance.auto=false, on 2026-10-20 at about 21:03Z. Round 1, git merge-tree --write-tree of the first head c3cf5154 against (round 2's are in its own section):

The pinned copy

templates/family-root/scripts/siblings.py is in FAMILY_COPIED_VERBATIM, so each holder gets one upstream-changed row for it, as #1045 gave one for bootstrap.py:

No Windows or macOS run was made, since CI is Linux only (#597).

Round 2: rv1075's review, head 041f7831

rv1075, lane 5's independent reviewer, found c3cf5154 READY (#1075 issuecomment-6302090874): no MUST, two SHOULDs and a NIT. One commit on top of it, 041f7831, with no rebase: main has touched none of this PR's files.

The three findings:

The code of siblings.py is unchanged. Its syntax tree with the docstrings taken out is the same at c3cf5154 and at 041f7831 (compared with ast); the file's sha256 moved from b1e7bb21... to 19107ad4... over comments and docstrings only. So round 1's two gate shards (1623 passed, 4 skipped; 2415 passed, 8 skipped) were not re-run; the targeted runs below are.

The new row, test_an_https_member_with_a_token_in_use_is_in_no_trace (the file has 28 rows now). Its setup:

  • the estate of tests/test_family.py, with IRRS's url in the holder's .gitmodules given as https://github.com/InkRouter/IRRS.git, and IRSS still a local path;
  • http.proxy=http://127.0.0.1:9, a dead proxy, in git's command scope after the entries the session holds, and NO_PROXY and no_proxy empty, so nothing leaves the host;
  • a fake SHAPE_LEGS_TOKEN and the seven trace variables set to files;
  • make siblings, cloning.

It asserts:

  • the proxy is set where the row sets it: http.proxy=http://127.0.0.1:9 is in the command scope the run's git reads (command_scope_at, which runs git config --show-origin --list and writes nothing);
  • exit 2 and IRRS: git clone exited 128: on stderr, which is how the head fails (rv1075 measured the same);
  • IRSS was cloned and IRRS was not;
  • the token is in neither stdout nor stderr, and in no file under the test's directory;
  • the five trace files the variables ask for are written, by the run's other gits.

No assert names the proxy in git's message. The git 2.47.1 this lane builds has no git-remote-https (git: 'remote-https' is not a git command, fatal: remote helper 'https' aborted session), so there the clone fails at once without reaching the proxy, still with 128. GIT_TRACE has written the run_command: git remote-https origin https://x-access-token:<token>@github.com/... line before that, so the row finds the leak on both gits. A first local version asserted the proxy's message and failed on 2.47.1 for that reason; the assert was dropped, and the commit amended, before anything was pushed.

It fails on main. The file was run against main's siblings.py (sha256 86bf57e0..., put in place and then restored, sha256 checked each time):

  • git 2.43.0, the whole file: 19 failed, 9 passed, the 18 rows of round 1 and the new row. The new row fails at assert files_holding(tmp_path, TOKEN) == [], with 5 files holding the token: temp/trace-performance.txt, temp/trace.txt, temp/trace2-event.json, temp/trace2-normal.txt and temp/trace2-perf.txt. The round 1 rows find 4 of those, since a local path leaves temp/trace.txt clean;
  • git 2.47.1, the row alone: 1 failed, the same 5 files.

It kills R4. R4 is rv1075's mutant, in rewrite_environment: return {**untraced(), **{k: v for k, v in os.environ.items() if k == "GIT_TRACE"}} if token else None, which keeps the caller's GIT_TRACE for the clone and the fetch. Applied alone, then restored and sha256-checked:

  • git 2.43.0, the whole file: 1 failed, 27 passed: the new row alone, with temp/trace.txt the one file holding the token;
  • git 2.47.1, the row alone: 1 failed, the same one file.

The runs on the pushed head 041f7831 (the runner's START line read dirty=0), through the same runner as round 1:

git files result
2.43.0 the new file, test_update_shape_kept_helper.py, test_family_makefile_siblings.py, test_family_member_make_jobserver.py, test_pinned_copy_text.py, and the trio (test_repo_hygiene.py, test_agents_md_codes.py, test_cli_reference.py) 589 passed, 1 skipped (the jobserver file's named-pipe row, as in round 1), in 5:09
2.47.1 (git version 2.47.1, printed from the exact path first on PATH) the new file 28 passed, in 2:13
2.43.0 the new file against main's siblings.py 19 failed, 9 passed
2.47.1 the new row against main's siblings.py 1 failed
2.43.0 the new file with R4 1 failed, 27 passed
2.47.1 the new row with R4 1 failed
2.43.0 on a predicted merge with main (below): the new file, test_update_shape_kept_helper.py, test_pinned_copy_text.py, test_family_makefile_siblings.py 305 passed, in 2:50

The full suite was not run. flake8 finds in siblings.py the same three lines as at round 1, and in the test file only F811s, from the fixtures it imports. Both files are ASCII, apart from the dashes and the CJK example already in siblings.py.

The merges of round 2. Fetched with -c gc.auto=0 -c maintenance.auto=false at about 23:00Z; git merge-tree --write-tree of 041f7831 against:

Round 3: the landing gate's failure, head 962ae43c

The failure. Lane 5's landing gate (log c1075.log, #1075 issuecomment-6304830183) failed the https row of round 2 at 041f7831: 1 failed, 356 passed. The change in siblings.py was not at fault; the row depended on the caller's git config.

  • The gate ran with neither GIT_CONFIG_GLOBAL=/dev/null nor GIT_SSH_COMMAND=false, which the writers' runs and round 2's runs had.
  • conftest.py's session global file includes the caller's global config (or ~/.gitconfig, where GIT_CONFIG_GLOBAL is unset). The operator's ~/.gitconfig has url.git@github.com:.insteadOf=https://github.com/.
  • Git keeps the first of two insteadOf prefixes of the same length, and the config files are read before the command line, so that rewrite beat the token's -c rewrite of the same prefix. Measured with scratch files and a stand-in token, on git 2.43.0: under a global file with that rewrite, git -c url.https://x-access-token:T@github.com/.insteadOf=https://github.com/ ls-remote --get-url https://github.com/InkRouter/IRRS.git printed git@github.com:InkRouter/IRRS.git.
  • So the row's https://github.com/InkRouter/IRRS.git went over ssh to InkRouter/IRRS, a real private repository. The dead proxy never applied, the clone succeeded, make siblings exited 0 and the placed members were both there, and the row failed assert result.returncode == 2.

The fix is in tests/test_family_siblings_untraced.py only. No other file changed in this round, and siblings.py is byte-identical to 041f7831's (sha256 19107ad4...).

  • hermetic_config gives the https row's git a global file of its own, session_global_config("") (conftest.py's own function: automatic maintenance off, and no include of the caller's file), GIT_CONFIG_NOSYSTEM=1 and GIT_SSH_COMMAND=false.
    • It is the least invasive way: only the one row with an https url sets it, conftest.py and every other row stay as they were, and the other rows' remotes are local paths, which no rewrite of GitHub's url reaches.
    • GIT_CONFIG_GLOBAL set takes git's two user-level files out of the reading, and GIT_CONFIG_NOSYSTEM the system file whatever GIT_CONFIG_SYSTEM says (checked on git 2.43.0 and 2.47.1 with scratch files).
    • GIT_SSH_COMMAND=false is a second wall: a url somehow sent over ssh runs false. No mutant can show it needed, since the runner exports it too.
  • url_after_config is a new assert before the clone: git ls-remote --get-url <url> (it applies the config's insteadOf and asks nothing of the network) must give the url unrewritten under the row's environment. A config that reaches the row now fails by name, before any clone.
  • The row is parametrized by CALLERS_CONFIG: none (round 2's row), and global and system, each a stand-in for a caller's config that would send the url somewhere reachable. as_the_callers_config writes a scratch file of the test's own, url.<file URI of the family's own IRRS remote>.insteadOf=https://github.com/InkRouter/IRRS.git, into this process's GIT_CONFIG_GLOBAL (the session file includes it) or GIT_CONFIG_SYSTEM. It is built from tmp files only: no network, and never the real ~/.gitconfig. The file now has 30 rows.
  • With the fix, in each of the three, the clone fails at the proxy with git's 128, make siblings exits 2, IRSS is placed and IRRS is not, and the token is in no output and in no file under tmp_path.

The variants fail without the fix. Each was applied alone to the worktree's test file (or siblings.py) and run through the runner, with the file's sha256 checked after it was restored:

  • 041f7831's file as pushed, under a caller's global config. Lane 5's runner now honours L5_GIT_CONFIG_GLOBAL=<scratch file> as the caller's global config, with ssh impossible. The scratch file rewrote https://github.com/InkRouter/IRRS.git to the run's own IRRS remote (a file:// url under the run's basetemp). The whole file, git 2.43.0: 1 failed, 27 passed, the https row at assert 0 == 2, the gate's failure reproduced with no network.
  • T2, 041f7831's row plus the variants: the new file with hermetic_config and the url_after_config assert taken out. The https rows, git 2.43.0: 2 failed, 1 passed, [global] and [system] at assert 0 == 2 (the clone went to the local remote and exited 0), and [none] passes. On git 2.47.1 the same.
  • T1, the row's global file dropped and the assert kept: [global] fails at the assert, which sees file:///...IRRS.git for the url; the other two pass.
  • T3, GIT_CONFIG_NOSYSTEM dropped and the assert kept: [system] fails at the assert; the other two pass.

R4 stays killed. rv1075's R4 (rewrite_environment keeps the caller's GIT_TRACE), git 2.43.0, the whole file: 3 failed, 27 passed, the three https rows.

The runs on the pushed head 962ae43c (START read dirty=0), through the same runner as before:

git files result
2.43.0 the new file, test_update_shape_kept_helper.py, and the trio (test_repo_hygiene.py, test_agents_md_codes.py, test_cli_reference.py) 359 passed, in 3:05
2.47.1 (git version 2.47.1, printed from the exact path first on PATH) the new file 30 passed, in 1:51
2.43.0 the new file, with L5_GIT_CONFIG_GLOBAL naming that scratch file 30 passed, in 2:21
2.43.0 041f7831's file, with the same 1 failed, 27 passed
2.43.0 / 2.47.1 T2, the https rows 2 failed, 1 passed / 2 failed, 1 passed
2.43.0 T1, T3, the https rows 1 failed, 2 passed each
2.43.0 R4, the whole file 3 failed, 27 passed

The full suite was not run. flake8 finds in the test file only F811s from the fixtures it imports, and nothing over 79 columns; the file is ASCII.

The merge-tree of round 3. Fetched with -c gc.auto=0 -c maintenance.auto=false at about 10:53Z; git merge-tree --write-tree of 962ae43c against:

Closes #1055

Lane: openreposhape-5 (openRepoShape-5)

🤖 Generated with Claude Code

… fetch that carry it run in bootstrap.py's `untraced()` environment, so `make siblings` and `family.py siblings` write the token to no trace the caller asks git for (#1055)

`make siblings` and `family.py siblings` gave the credential's `-c url.https://x-access-token:<token>@github.com/.insteadOf=...` rewrite, in argv, to two gits that ran in the caller's own environment: the clone and the fetch in `place`. A trace writes a command's arguments, so every trace2 target the caller named (the `GIT_TRACE2*` variables, or `trace2.eventTarget`, `normalTarget` and `perfTarget` in the global or the system config) held the token in the `start` event of each, one line per member. `GIT_TRACE_PERFORMANCE`'s file held it too, on git 2.43.0 and 2.47.1 alike. `scrub` kept it out of the output, and only that part held. Found by lane 5's review of #1045 (rv996) and measured in #1055.

- `rewrite_environment(token)` is new: `untraced()` from the `bootstrap.py` beside the file where a token is in use, and None, the caller's own environment, where none is. The clone and the fetch each run with `env=rewrite_environment(token)`. With no token both stay traced where the caller asked.
- The member's own `scripts/bootstrap.py`, which `place` runs after a clone, is given no token and keeps the caller's environment and its traces. #1023's jobserver handling is untouched.
- `untraced` is imported, as #1023 imports `member_make_environment`, not copied: one answer to which traces a command carrying the token is kept out of, for `make bootstrap` and `make siblings`. A holder that keeps a `bootstrap.py` from before #996 is refused by name, `update-accept-local-missing-name` (#536), before a byte is written, so no `ImportError` reaches `make siblings`. The docstrings say the promise in full: never written into `.git/config`, never persisted, never printed, never traced.
- `tests/test_family_siblings_untraced.py` (27 rows) crosses `make siblings` cloning, `make siblings` fetching and `family.py siblings` with the trace variables, the global config and the system config, for `SHAPE_LEGS_TOKEN` and `SHAPE_LEGS_APP_TOKEN` (18 rows: exit 0, `siblings ok`, the token in no output and in no file under the test's directory, every trace file written, the member's own bootstrap still traced), and with no token (9 rows: still traced as asked). All 18 token rows fail on main's `siblings.py`; the 9 controls pass. Eleven mutants, each alone, are all killed.
- Runs on git 2.43.0: the new file 27 passed; 1623 passed, 4 skipped, and 2415 passed, 8 skipped, over 41 files (the new file, every file that runs `siblings.py`, the pinned-copy and upgrade-row files, the trio). git 2.47.1: the new file 27 passed.

`templates/family-root/scripts/siblings.py` is in `FAMILY_COPIED_VERBATIM`, so each holder gets one `upstream-changed` row for it, as #1045 gave one for `bootstrap.py`.

Closes #1055

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

sourcery-ai Bot commented Oct 20, 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 21 hours and 29 minutes by commenting @sourcery-ai review. Upgrade to get a review now.

@sourcery-ai

sourcery-ai Bot commented Oct 20, 2026

Copy link
Copy Markdown

Reviewer's Guide

This PR hardens make siblings and family.py siblings against credential disclosure by running token-bearing clone and fetch commands in the shared bootstrap.py untraced() environment, while preserving normal caller tracing for tokenless and member-bootstrap operations. New regression tests exercise all supported credential and trace-target combinations, with documented compatibility safeguards for pinned family copies.

Sequence diagram for token-safe sibling clone and fetch

sequenceDiagram
    participant Caller
    participant Siblings as siblings.py
    participant Bootstrap as bootstrap.py
    participant Git
    participant MemberBootstrap as member bootstrap.py

    Caller->>Siblings: make siblings or family.py siblings
    Siblings->>Bootstrap: credential()
    Bootstrap-->>Siblings: token
    Siblings->>Bootstrap: untraced()
    Bootstrap-->>Siblings: trace-disabled environment
    Siblings->>Git: clone or fetch with env
    Git-->>Siblings: result without token in traces
    opt clone completed
        Siblings->>MemberBootstrap: run without token environment
        MemberBootstrap->>Git: member bootstrap operations
        Git-->>MemberBootstrap: caller tracing preserved
    end
Loading

Flow diagram for conditional sibling git tracing

flowchart TD
    A["Resolve credential with credential()"] --> B{Token in use?}
    B -->|Yes| C["rewrite_environment(token)"]
    C --> D["untraced() removes GIT_TRACE* and disables trace2"]
    D --> E[Clone or fetch with protected environment]
    B -->|No| F["rewrite_environment returns None"]
    F --> G[Clone or fetch with caller environment]
    E --> H[Run member bootstrap without token]
    G --> H
Loading

File-Level Changes

Change Details Files
Prevent credentials embedded in Git rewrite arguments from reaching caller-requested Git traces during sibling placement.
  • Added rewrite_environment(token) to reuse bootstrap.py’s shared untraced() environment when a token is present, while preserving the caller environment without a token.
  • Applied the protected environment only to the sibling clone and fetch subprocesses; member bootstrap processes remain caller-traced.
  • Imported untraced from the holder’s pinned bootstrap.py and updated documentation describing the trace and compatibility contract.
templates/family-root/scripts/siblings.py
Added comprehensive regression coverage for credential leakage across trace mechanisms and sibling entry points.
  • Covered clone and fetch paths through make siblings and family.py siblings, both credential variables, environment trace variables, and global/system trace2 targets.
  • Verified successful placement, absence of tokens from output and files, continued tracing of unrelated/member-bootstrap Git commands, and preserved tracing when no token is used.
  • Added tests for the shared-helper import contract and updated the expected imported-name count.
tests/test_family_siblings_untraced.py
tests/test_update_shape_kept_helper.py

Assessment against linked issues

Issue Objective Addressed Explanation
#1055 When a credential token is in use, prevent the git clone and git fetch commands run by siblings.py from exposing it in caller-requested traces, including trace targets configured globally or system-wide. ✅
#1055 When no token is in use, preserve the caller's tracing environment for those commands, and leave the member's own bootstrap traced because it is not given the token. ✅

Possibly linked issues


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-5, 2026-10-20T21:05Z) @codex review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-20T21:08:51.706502Z c3cf515 Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Keep it up!

Reviewed commit: c3cf51544b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@brettheap

Copy link
Copy Markdown
Contributor Author

Independent review of #1075 (#1055) at c3cf5154, for lane openRepoShape-5: READY

The code does what #1055 asks, and every run I made passed. There is no MUST. Two SHOULDs remain: one sentence in the body is false (I reproduced the failure), and one leak path has no test row.

Findings

SHOULD 1: the body says "No ImportError reaches a holder". Two applies in a row make one. The cause is the #536 guard in update-shape.py:4288-4289. It only checks files that --accept-local names in the same run (row.state in (BOTH, LOCALLY_MODIFIED) and row.path in accepted). The probe is in $Q/v118/rv1075/tools/guard.sh and guard2.sh; $Q is /tmp/claude-1000/-workspace-projects-openRepoShape/50cede29-ab82-4ca3-9f67-d98e1a58c8d2/scratchpad.

SHOULD 2: every row uses a local file remote, so a leak through GIT_TRACE on an https member (a real github.com member) has no test.

  • The survivor: my mutant R4 keeps the caller's GIT_TRACE in rewrite_environment's environment, and all 27 rows still pass.
  • The leak is real: with http.proxy=http://127.0.0.1:9 (a dead proxy), GIT_TRACE writes trace: run_command: git remote-https origin https://x-access-token:<token>@github.com/... offline. That is 3 lines on git 2.43.0 and 4 on 2.47.1.
  • Through make siblings: I set one member's .gitmodules url to https://github.com/rv1075-org/IRRS.git, behind the dead proxy.
    • Main's siblings.py (86bf57e0) with a token puts it in trace.txt, trace2-event.json, trace2-normal.txt, trace2-perf.txt and trace-performance.txt.
    • The head puts it in no file and no output. The clone fails cleanly ("git clone exited 128", exit 2).
    • All 4 runs took 17.67 s.
  • The body is wrong on two points:
    • "Not measured: an https remote … these probes are offline": it can be measured offline.
    • "Of the issue's six variables, none but trace2's writes the -c": true only for a file remote.
  • Fix: add one https row behind a dead proxy; it kills R4.

NIT: three places say the member's bootstrap "is given no token": siblings.py:86, siblings.py:645 and the test docstring at line 23. It does inherit SHAPE_LEGS_TOKEN and SHAPE_LEGS_APP_TOKEN in its environment, because siblings.py:908 passes no env. What it is not given is the token in its argv, as the issue words it. Nothing leaks today: the member's templates/assembly-root/scripts/bootstrap.py reads neither variable (grep finds none).

Checked and found true

  • The head: c3cf5154 is one commit on 00f6d7f7, touching the 3 files the brief lists (366 lines added, 14 removed).

    • sha256: siblings.py b1e7bb21, the new test 7ba7a416, bootstrap.py 50a16751, main's siblings.py 86bf57e0. All match the body.
  • What the change does (item 1):

    • The credential's -c reaches only two gits, the clone (:1015) and the fetch (:1048). Both now get env=rewrite_environment(token).
    • The other gits siblings.py runs never get the -c: git config -z -f at :435, and git_text at :687 (rev-parse and remote get-url).
    • The member's bootstrap (:908) and the member's make (:1234, member_make_environment) are unchanged, and no MAKEFLAGS reading changed.
  • The import (item 2): update-shape.py's own readers, run in process, say:

    • siblings.py takes six names from bootstrap;
    • bootstrap.py at 00f6d7f7, at head and in both merges lacks none of them;
    • at 6476c170 it lacks ['untraced'] only.
    • The "six names" docstring is true, and so is "never traced".
  • Base-fail (item 3): the new test file run against main's siblings.py gives 18 failed, 9 passed. The variables rows found the token in 4 files, trace-performance.txt first; the config rows found it in the cfg-* and sys-* files.

  • The rows (item 3):

    • They search for the token in every file under tmp_path (TMPDIR, traces, configs, the clones and their .git/config) and in stdout and stderr.
    • They cover the three trace ways and both token variables.
    • conftest.py always sets GIT_CONFIG_GLOBAL for the session, so the global-config rows cannot hit a KeyError.
  • The writer's mutants, re-run each alone (item 4): all killed, with the same counts as the writer's log.

    mutant result
    M1 9 failed
    M2 12 failed
    M3 6 failed
    M5 6 failed, all through temp/trace-performance.txt alone, so the rows do hold GIT_TRACE_PERFORMANCE
  • My mutants (item 4):

    mutant result
    R1: only SHAPE_LEGS_TOKEN read as the condition killed, 9 failed (every SHAPE_LEGS_APP_TOKEN row)
    R2: the fetch untraced always killed, 3 failed (the fetch controls)
    R3: GIT_TRACE* taken out but trace2 not set to 0 killed, 12 failed (the config rows)
    R4: the caller's GIT_TRACE kept survives (27 passed); SHOULD 2
    R5: the caller's GIT_TRACE_PACKET and GIT_TRACE_CURL kept survives (27 passed); low value, since curl's trace redacts the auth header by default (my 2.43 probe: written, no token in it)
  • Runs through run9.sh (item 5):

    • git 2.43, the 8 files the brief names: 588 passed, 1 skipped (the jobserver test's named-pipe skip), in 4:19; the START line read dirty=0.
    • git 2.47.1, printed as git version 2.47.1 from the exact path: the new file, 27 passed in 1:55.
  • Merges (item 6): all clean, and in both merges siblings.py is identical to the head's.

    merged with result
    origin/main, re-fetched, now 4a6b9afd (validate-pins.py: a --pin-source key is the longest declared pin before an = that leaves a path, as a --link-source key is since #980, so open=y=<path> verifies the pin open=y and open=y= is refused as naming no path (#1053) #1067's files only, none of this PR's) tree 1a60e7bd
    6c861635 tree 225b2a99, as the body says
    validate-family.py, validate-pins.py and both bootstrap.py copies: a git Windows cannot start for a reason other than 193 that names no file (winerror 216, 1260, 11, or 188-202) is git-not-runnable, exit 2, with the put-git-on-PATH remedy, where each ended in a traceback, exit 1 (#1040) #1062 957ba038 (its diff does not touch untraced or TRACE) tree 84d4b54a, as the body says
  • Title, body and static checks (item 7):

    • The title is 241 characters and true. The only closing keyword is Closes #1055, in the body and in the commit message.
    • flake8 finds in siblings.py the same 3 problems as on main (two E501, one E127), and in the test only 10 F811s.
    • No duplicated 8-line block with any other test file, no assert … and, no added line over 79 characters, no non-ASCII added.
    • Cognitive complexity: rewrite_environment is 1; place keeps its structure, only the env= arguments were added.
    • The writer's numbers, counted from their logs: probe.log has 108 runs, all exit 0 with siblings ok and no token in the output; the 36 main runs with a token held it and the other 72 held none. probe-final.log has 54 runs, none holding the token. The gate shards were 19 + 22 = 41 files: A 1623 passed, 4 skipped; B 2415 passed, 8 skipped.

Could not verify

  • A real github.com https clone or fetch that succeeds (FETCH_HEAD and reflog anonymising the url).
  • git 2.55.0 (CI), Windows and macOS.
  • The writer's two full gate shards and the 108-run probe: I counted them from the logs and did not re-run them.
  • SonarCloud's own analysis.

Checklist candidates

  • A guard claim about update-shape.py is probed across two applies in a row, not one.
  • Rows that look for a token in a trace include an https member behind a dead proxy.

Scratch left

  • $Q/v118/rv1075/: logs, tools/, the holder guard/fam/InkRouter, and https-probe/.
  • Basetemps $Q/v110/bt/rv1075-*.
  • The worktrees rv1075-head, rv1075-mut and rv1075-up are removed (--force only for my own untracked probe test), and the refs refs/l5rv/rv1075-pr1075 and refs/l5rv/rv1075-pr1062 are deleted. Nothing of mine is running.

brettheap added a commit that referenced this pull request Oct 20, 2026
…a signal fails the run by name wherever the update met it, read from git's trace, not only where its code is the update's last; with a token in use that stays a limit (#1056) (#1076)

Where a git that git's update ran dies of a signal, `make bootstrap` in a family holder now fails naming it wherever the update met it, not only where the signal's code is the update's last. This is #1056, found by rv996, lane 5's independent review of #1045 (issuecomment-6288783192, checklist candidate 1).

Git keeps only the last code that is not 0 (`update_submodules` in git 2.47.1's `builtin/submodule--helper.c`), and only 128 stops it. So a member's crash or kill before a later gitlink's 128 or 1 left no signal in the update's exit code, and #1045's `ended_on` saw none: the run said the credential advice and `family bootstrap ok`, exit 0, with IRRS's legs never updated.

One commit, on main `00f6d7f7` (which holds #1045 and #1023). It changes two files: the family root's `templates/family-root/scripts/bootstrap.py` and `tests/test_family_bootstrap_stops_named.py`.

### The signal is read from git's own trace (no token in use)

- With no token in use, git's update is already traced into a directory of the run's own (#892). `traced_in` now reads that trace once and returns a `Trace`: `fetched` (#892's work trees, as before) and `died`, read by the new `died_in`.
- **`died_in`** reads the events by the code, never git's words, as #892 reads a fetch:
  - a git writes a `child_start` event (with the argv) and a `child_exit` event (with the code) for each child it starts;
  - `child_of` pairs the two by the writing git's `sid` and the `child_id`;
  - a `child_exit` whose code is `128 + N`, N below `signal.NSIG` (`died_of`, now shared with `ended_on`), for a child whose argv begins `git` (`git_child`), is a git dead of signal N;
  - a hook or another program is not counted. Its code is its own: a `post-checkout` hook that exits 137 is no signal, and `git checkout` exits 1 over it (measured, both gits).
- **The first signal** is the first by the time each event gives, which is the order git met them. Where trace2 gives no time (its brief form), it is the order of the files and their lines.
  - File order alone would name the wrong one. The files go in the order their gits started, so a member's own update comes after git's update's file.
  - Measured on both gits: SIGKILL at IRRS's `code` leg, then SIGTERM at its `spec` leg. That ended IRRS's own update 143, and git's update's `child_exit` 143 for it came first in the files.
- **`signal_met`** chooses what (a) tells:
  - where git's update exited 0, nothing;
  - else the trace's first signal, where there is one;
  - else the update's own exit code, through `ended_on` as #998 reads it (`128 + N`, or `-N` where git itself was killed).
- **Nothing where the update exited 0.** It did all it set out to do then, whatever a git it ran died of on the way. Measured on both gits: a `reference-transaction` hook killed IRSS's `git fetch` after it wrote the branch, git found the commit reached, checked it out and exited 0. The run says every member and leg was fetched, as before.

### What the run says

- Where the signal's code is the update's own, the line is #998's, word for word, so the controls read as before:
  ```
    UPDATE KILLED, and not for want of a credential: git's update ended on signal 9 (SIGKILL, exit 137), which a git it ran died of. Git's own words above say which git and where.
  family bootstrap FAILED: git's update (ended on signal 9)
  ```
- Where the trace gave it and the update ended with another code:
  ```
    UPDATE KILLED, and not for want of a credential: a git that git's update ran died of signal 9 (SIGKILL, exit 137), and git's update went on and ended with exit 128. Git's own words above say which git and where.
  family bootstrap FAILED: git's update (a git it ran died of signal 9)
  ```
- No credential advice in either case (`credential_advice`).

### A signal and B4 both told: the signal first

- Before, `fetch_members` returned one failure line, the signal's or the walk's stop's. Now `told_past` says each that is there, each with its own failure line.
- Where both are told, **the signal is said first**. Why:
  - It is git's order. A death past the holder's records (128) ends git's update, so git met the signal at an earlier gitlink and went on past it.
  - Git's own words, printed just above, say them in that order too.
  - The two cures do not depend on each other.
- Measured: the killed checkout in IRRS, then B4 at IRSS, with the credential. Before, the B4 line alone was said. Now:
  ```
    UPDATE KILLED, and not for want of a credential: a git that git's update ran died of signal 9 (SIGKILL, exit 137), and git's update went on and ended with exit 128. Git's own words above say which git and where.
    UPDATE STOPPED, and not for want of a credential: git stopped at members/IRSS, whose fetch reached its remote and did not bring <commit>, ...
  family bootstrap FAILED: git's update (a git it ran died of signal 9), members/IRSS (git stopped at it)
  ```
- A `NOT CHECKED OUT` row or a whole-holder stop (`unfetchable`, `updated_none`) still comes first and alone, as for #998's signal (mutant S7's order is unchanged).

### With a token in use: the limit, said and pinned

The issue offered two ways. I measured both, and chose to say the limit and pin it with a row.

| measured (killed checkout, then IRSS's pin moved on, the token's fake value) | git 2.43.0 | git 2.47.1 |
|---|---|---|
| a directory trace target with the token's `-c` in argv: events holding the token | 1 (the top git's `start` argv) | 1 (the same) |
| the rewrite given in the environment instead, IRSS's origin an https url through a refused local proxy: events holding the token | 8 (the remote helper's `start` and `child_start` events give the url) | 0 |
| an `af_unix:dgram:` socket target read by a thread, in memory | 189 datagrams, 1 holding the token, the `child_exit` 137 for `git checkout` there; no file | 194, 1, the same; no file |

So:
- **Any file target writes the token**: in argv on both gits, and, moved to the environment, in git 2.43.0's remote helper url.
- **Git's one target that is no file, a Unix socket, works**, but this PR does not take it:
  - this process would have to read it for the whole update, and every git the update starts waits on it once Linux has queued 10 events (`net.unix.max_dgram_qlen` is 10 here), so a stalled reader hangs `make bootstrap`;
  - the socket's path must fit in the 107 bytes Linux takes. This harness's TMPDIR is 151 bytes, and a socket path under this session's scratchpad (114 bytes) was refused (`AF_UNIX path too long`);
  - git for Windows builds without it (read, not run).
  It would be a second trace mechanism, and is better as its own issue if wanted.
- **What the docstrings now say:**
  - `ended_on`: with a token in use the exit code is all there is, and a signal before a later failure is not told.
  - `credential_advice`: the limit and the measurement.
  - `run_update`: points at both.
- **The row:** `test_with_a_credential_in_use_a_signal_is_told_only_from_git_s_code`.
  - `[alone]`: the killed checkout alone is named, from git's own 137.
  - `[then-pin-moved-on]`: the killed checkout, then IRSS's pin moved on with the transport refused, is the credential's skip, the advice and ok.
  - Both: the token is in no output and no file under TMPDIR, with every caller trace on.

### Two docstrings corrected

- `ended_on`: git's update ends with a signal's code only where no gitlink after it fails, since it keeps the last code that is not 0. (It said the update "ends with that code".)
- `credential_advice`: a failed fetch does not always end the update with 128. A clone that fails gives 1 and the update goes on (rv996's NIT 2 on #1045), so the advice is dropped beside a signal even where a credential was also wanting. The run fails on the signal, and a run made again says the advice. (It said an update ending `128 + N` "met none".)
- The module docstring's (a) and (c), `fetch_members`, `run_update`, `fetched_in`, `GIT_DIED`, `UPDATE_KILLED` and `TRACE_ENV` say the same.

The probe is a pytest file kept out of the commit. It builds each state with `tests/test_family_bootstrap_stops_named.py`'s own helpers, runs the family root's `bootstrap.py --skip-members` (offline), and records the exit code, whether the credential advice is said, and whether the fake token is in any output or any file under TMPDIR. It also runs git's own update, traced, on a copy of the same state with no token. It was run at main `00f6d7f7` and at this head, on git 2.43.0 (`/usr/bin/git`) and on git 2.47.1 (a local build, first on PATH). Each cell is the verdict, the signal said, and the exit code:

| state (in IRRS, then in IRSS) | main 2.43.0 | head 2.43.0 | main 2.47.1 | head 2.47.1 |
|---|---|---|---|---|
| killed checkout, then IRSS's pin moved on, no credential | ok + advice (0) | FAILED signal 9 (1) | ok + advice (0) | FAILED signal 9 (1) |
| killed checkout, then IRSS's checkout refused, with the credential | ok + advice (0) | FAILED signal 9 (1) | ok + advice (0) | FAILED signal 9 (1) |
| killed checkout, then B4 at IRSS, with the credential | FAILED + B4 stop (1) | FAILED signal 9 + B4 stop (1) | FAILED + B4 stop (1) | FAILED signal 9 + B4 stop (1) |
| control: killed checkout alone, with the credential | FAILED signal 9 (1) | FAILED signal 9 (1) | FAILED signal 9 (1) | FAILED signal 9 (1) |
| P3, then IRSS's pin moved on, no credential | ok + advice (0) | FAILED signal 11 (1) | ok + advice (0) | ok + advice (0) |
| P3, then B4, with the credential | ok + advice (0) | FAILED signal 11 (1) | ok + advice (0) | ok + advice (0) |
| control: P3 alone, with the credential | FAILED signal 11 (1) | FAILED signal 11 (1) | ok + advice (0) | ok + advice (0) |
| a hook that exits 137 (no signal), then IRSS's pin moved on, no credential | ok + advice (0) | ok + advice (0) | ok + advice (0) | ok + advice (0) |
| a hook that exits 137, alone, with the credential | ok + advice (0) | ok + advice (0) | ok + advice (0) | ok + advice (0) |
| SIGKILL at IRRS, then SIGTERM at IRSS, with the credential | FAILED signal 15 (1) | FAILED signal 9 (1) | FAILED signal 15 (1) | FAILED signal 9 (1) |
| SIGKILL at IRRS's `code` leg, then SIGTERM at its `spec` leg, with the credential | FAILED signal 15 (1) | FAILED signal 9 (1) | FAILED signal 15 (1) | FAILED signal 9 (1) |
| the same two legs, then IRSS's pin moved on, no credential | ok + advice (0) | FAILED signal 9 (1) | ok + advice (0) | FAILED signal 9 (1) |
| token in use: killed checkout, then IRSS's pin moved on | ok + advice (0) | ok + advice (0) | ok + advice (0) | ok + advice (0) |
| token in use: killed checkout alone | FAILED signal 9 (1) | FAILED signal 9 (1) | FAILED signal 9 (1) | FAILED signal 9 (1) |
| token in use: killed checkout, then B4 | FAILED + B4 stop (1) | FAILED + B4 stop (1) | FAILED + B4 stop (1) | FAILED + B4 stop (1) |
| token in use: killed checkout, then IRSS's checkout refused | ok + advice (0) | ok + advice (0) | ok + advice (0) | ok + advice (0) |

Read across the table:
- **Every no-token row of the probe that holds a signal now fails naming it, with no advice** (the rows that said `ok` with the advice, and the one that named SIGTERM): both killed-checkout rows (pin moved on, checkout refused) on both gits, and P3 (then IRSS's pin moved on, then B4) on git 2.43.0. P3 on git 2.47.1 is not a crash there (git refuses the line and exits 128, so nothing is killed), so its rows read as before.
- **Killed checkout, then B4:** the B4 line alone was said at main. Now both are said, the signal first.
- **A hook's own code is no signal:** a `post-checkout` hook that exits 137 reads the same at main and at this head: `ok`, with the advice.
- **The first signal is the first git met:** SIGKILL at IRRS, then SIGTERM at IRSS (and the two legs of IRRS) named SIGTERM at main, from git's update's own last code, and name SIGKILL now.
- **A signal in an update that exits 0 is not said.** A `reference-transaction` hook that kills IRSS's `git fetch` once, after it wrote the ref (git then finds the commit reached, checks it out and exits 0): measured at this head on both gits (main reads no trace, so it says the same), the run says `every member and leg fetched` and `family bootstrap ok`, exit 0. The trace there holds a `git fetch` `child_exit` of 137, and the row `test_a_git_killed_in_an_update_that_exits_0_is_not_said` pins that nothing is said (mutant M7).
- **With a token in use nothing changed**, on purpose (the four `token in use` rows): the signal is told only where its code is git's last.
- **The fake token** was in no output and in no file under TMPDIR in any run of any row (probe lines `token in output: False`, `token in temp files: []`).

All in `tests/test_family_bootstrap_stops_named.py` (#954's and #1045's file), reusing its helpers and its style, with no assert that joins conditions with `and`. The module docstring gains the new rows' list, and the P3 state moved into a `p3()` helper so two rows build it. 11 new rows (37 before, 48 after).

The issue's Verify rows (fail at main):
- `test_p3_before_a_later_failure_fails_naming_the_signal`: P3, then IRSS's pin moved on, no credential, git 2.43.0: exit 1, signal 11 (SIGSEGV) named, no advice, FAILED. Skipped where git does not crash, as #1045's P3 row is.
- `test_a_killed_checkout_before_a_later_failure_fails_naming_it[pin-moved-on-no-credential]` and `[checkout-refused-with-the-credential]`: the killed checkout (a `post-checkout` hook sending SIGKILL to its parent, `kill -s KILL $PPID`), then IRSS's pin moved on with no credential, and then IRSS's checkout refused with the credential. Each exits 1, names signal 9 (SIGKILL), gives no advice. Every POSIX git.

The choice rows (fail at main):
- `test_a_signal_and_b4_after_it_are_both_named_the_signal_first`: the killed checkout, then B4 at IRSS, with the credential: both lines, the signal first, and the failure line names both.
- `test_the_first_signal_is_named[two-members]` and `[two-legs]`: SIGKILL, then SIGTERM, at two members and at two legs of one member. The first by time is named (SIGKILL), where git's update's own last code, and the files' order, name SIGTERM.

The controls and the limit (pass at main, as intended, so they hold the rest as it was):
- `test_a_killed_checkout_alone_is_named_as_before`: #998's line, word for word.
- `test_a_hook_s_own_code_is_no_signal`: a hook that exits 137 is not a signal.
- `test_a_git_killed_in_an_update_that_exits_0_is_not_said`: the `reference-transaction` row above.
- `test_with_a_credential_in_use_a_signal_is_told_only_from_git_s_code[alone]` and `[then-pin-moved-on]`: the token's limit, pinned. Alone, the killed checkout is named from git's own 137. Then IRSS's pin moved on, the run is the credential's skip, the advice and `ok`, as before. Both: the fake token is in no output and in no file under TMPDIR, with every caller trace variable on.

The controls the issue names that already existed are untouched and pass: the signal alone (`test_an_update_ended_on_a_signal_fails_naming_it`, the P3 row), the credential's skip (`test_the_credential_s_skip_is_as_it_was`) and `test_a_code_no_signal_gives_is_read_as_before[exit-128]` and `[exit-255]`.

**Failing at main:** main's `bootstrap.py` bytes (sha256 `50a16751...`) against this branch's 48-row file, git 2.43.0: **6 failed, 42 passed**. The six are exactly the rows under "fail at main" above (the P3 row, the two killed-checkout rows, the both-named row and the two first-signal rows). They pass with the change.

Each mutant applied alone to the change's `bootstrap.py` (a script replaces one exact string and asserts it occurs once), the stops-named file run on git 2.43.0 (`/usr/bin/git`) through the lane's runner, the file restored from the head's bytes and its sha256 checked after each. All on the final 48-row file (M1 to M3 were run once on the first 47 rows and again on the 48, with the same rows failing).

| | mutant | result |
|---|---|---|
| M1 | the trace read dropped (`Trace(fetched_in(events))`) | killed: 6 failed, 42 passed |
| M2 | `128 + N` read as `>= 128` (`GIT_DIED <= code`) | killed: 31 failed, 17 passed |
| M3 | a `child_exit` of a child that is not `git` taken (`git_child` accepts any `child_start`) | killed: 1 failed (the hook row) |
| M4 | the last signal for the first | killed: 2 failed (`two-members`, `two-legs`) |
| M5 | the advice still printed beside a signal | killed: 10 failed |
| M6 | the sort by time dropped (file order) | killed: 1 failed (`two-legs`) |
| M7 | the exit-0 gate dropped (`signal_met` tells a signal where git exited 0) | killed: 1 failed (the `reference-transaction` row) |
| M8 | a child paired by `child_id` alone, without the writing git's `sid` | killed: 1 failed (the hook row) |
| M9 | B4 dropped beside a signal | killed: 1 failed (the both-named row) |
| M10 | B4 said before the signal | killed: 1 failed (the both-named row) |
| M11 | the "went on and ended with exit R" text never said | killed: 6 failed |
| M12 | that text always said | killed: 5 failed |
| M13 | git's update's own exit code asked before the trace | killed: 2 failed |
| M14 | the failure line always "ended on signal N" | killed: 6 failed |
| M15 | the `child_exit` kind check dropped from `died_in` | **survives**: 48 passed |

**M15 is equivalent, and the check stays as a reader's help.** `died_in` only takes an event whose `(sid, child_id)` is one `child_start` named for a git, and only a `child_exit` event carries a `code`. A `child_start` event has none, so `died_of(None)` is None whether or not the kind is asked. No input reaches a different answer.

All through the lane's runner (the real `gh` replaced by a stub, every `LANES_*` variable cleared, `EMAIL=service@opensoft.dev`, `GIT_SSH_COMMAND=false`, `GIT_CONFIG_GLOBAL=/dev/null`), at most two at once, never the full suite. Git 2.43.0 is `/usr/bin/git`; git 2.47.1 is a local build, first on PATH. `bootstrap.py`'s sha256 began `f1c156e29d07e149`, which the runs recorded and the committed file has (the test file's begins `1d558af665c3`).

| run | git | result |
|---|---|---|
| `tests/test_family_bootstrap_stops_named.py` (48 rows) | 2.43.0 | **48 passed** |
| the same | 2.47.1 | **46 passed, 2 skipped** (both P3 rows: that git exits 128 there, not 139) |
| main's `bootstrap.py` bytes, this branch's 48 rows | 2.43.0 | 6 failed, 42 passed (the rows listed under Tests) |
| the probe, after, 17 states | 2.43.0 and 2.47.1 | 17 passed each |
| group A: the stops-named file, `test_family.py`, `test_update_shape_kept_helper.py`, `test_family_holder_remedies_compose.py`, `test_family_gitmodules_values.py`, `test_pinned_copy_text.py`, `test_repo_hygiene.py` | 2.43.0 | **707 passed** |
| group B: `test_family_holder_bootstrap_texts.py`, `test_family_bootstrap_gate_order.py`, `test_bootstrap_without_git.py`, `test_family_unnamed_gitlink.py`, `test_family_config_appends.py`, `test_family_member_make_jobserver.py` (#1023's), `test_family_siblings_bootstrap_unreadable.py`, `test_family_head_lost_gitmodules.py`, `test_family_members_not_a_list.py`, `test_family_makefile_siblings.py`, `test_bootstrap_spelling.py`, `test_template_text_placeholders.py`, `test_no_auto_maintenance.py`, `test_agents_md_codes.py`, `test_cli_reference.py` | 2.43.0 | **1010 passed, 3 skipped** (GNU make's jobserver spelling here, and two rows that need a session without `GIT_CONFIG_GLOBAL`) |
| the 15 mutants | 2.43.0 | see Mutants |

Both groups and every file that names the family root's `bootstrap.py` (`grep -l 'family-root/scripts/bootstrap' tests/*.py` gives two files, both in the groups), `test_pinned_copy_text.py` and the trio (`test_repo_hygiene.py`, `test_agents_md_codes.py`, `test_cli_reference.py`) are in those two groups. No other test pins the functions or lines this changes (a grep of `tests/`, `docs/` and `scripts/` for them finds only a docstring's mention in `test_family_holder_bootstrap_texts.py`, in group B, and an unrelated workflow-validator row). The runs were made at main `00f6d7f7`'s tree plus this change, before main moved (see Merges).

`git merge-tree --write-tree` of this head `73906948` (parent main `00f6d7f7`), after fetching with `gc.auto=0` and `maintenance.auto=false`:

| against | result |
|---|---|
| `origin/main` `4a6b9afde` (3 commits past `00f6d7f7`) | clean (tree `ad8dab73`); none of the 3 touches `bootstrap.py` or the stops-named file |
| #1062 `fix/1040-windows-start-errors` `957ba038` (touches the family root's `bootstrap.py`, in `git_not_started` only) | clean (tree `70c5a715`); the merged `bootstrap.py` parses, and holds `died_in` and #1062's `4551` |
| #1075 `fix/1055-siblings-untraced` `c3cf5154` (lane 5's: `siblings.py`, `tests/test_family_siblings_untraced.py`, `tests/test_update_shape_kept_helper.py`) | clean (tree `6770fa61`); its `siblings.py` imports `untraced` from `bootstrap.py`, which this does not touch |

The other open PRs (#1073, #1072, #1071, #1070, #1069, #1068, #1064, #1031) touch neither file.

`templates/family-root/scripts/bootstrap.py` is in `FAMILY_COPIED_VERBATIM`, so each holder gets one `upstream-changed` row for it, as for #1045. The only import is unchanged (`signal` came with #998), and `siblings.py`'s import list from it is unchanged. No Windows or macOS run was made, since CI is Linux only (#597).

Review: rv1076, lane 5's independent reviewer, not the writers (w1056 and f1056): READY at `73906948` (issuecomment-6302104583) with no MUST or SHOULD; of its three NITs, the body's is fixed, and the other two (a docstring clause on a member's failed clone, and a text no row reaches) go to a follow-up. CI (Brett Heap's ruling 27): H0 is the head 7390694 itself, which passed tests, tests-git-bash and SonarCloud; main has touched none of its files since its base 00f6d7f; and the local gate passed on its predicted squash tree 9dae534 over main bccae28: the family_bootstrap_stops_named, repo_hygiene, agents_md_codes, cli_reference sets, 218 passed. Landed on Brett Heap's word of 2026-10-18T14:45:28Z, relayed to every lane by lane 3 (#473 issuecomment-6251255596): "merge v1.2.0 PRs once reviewed and green".

Closes #1056

Lane: openreposhape-5 (openRepoShape-5)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… remote is GitHub's, behind a dead proxy, finds a clone that keeps the caller's GIT_TRACE, and the member's own bootstrap is said to be given no token in its argv (#1055)

Lane 5's independent review of #1075 (rv1075, issuecomment-6302090874) found the code ready, with two SHOULDs and a NIT. This commit answers the test SHOULD and the NIT; the PR body answers the other SHOULD, whose gap is #1082's.

- SHOULD 2: every row used a local file remote, so a leak through `GIT_TRACE` on an https member, which is what a real github.com member is, had no test. rv1075's mutant R4, which keeps the caller's `GIT_TRACE` in `rewrite_environment`'s environment, passed all 27 rows. With an https member, git writes the clone's rewritten url, token and all, in `GIT_TRACE`'s `run_command: git remote-https` lines, offline, behind a dead proxy. `test_an_https_member_with_a_token_in_use_is_in_no_trace` gives one member of the estate a `https://github.com/<org>/<repo>.git` url and `http.proxy=http://127.0.0.1:9` in git's command scope, sets a fake `SHAPE_LEGS_TOKEN` and the trace variables, and asserts: the proxy is set, `make siblings` exits 2, the clone exited 128, the other member is placed, and the token is in no output and in no file under the test's directory, with every trace the variables ask for still written.
- NIT: `siblings.py`'s module docstring and `rewrite_environment`'s, and the test file's, said the member's own bootstrap "is given no token". It inherits `SHAPE_LEGS_TOKEN` and `SHAPE_LEGS_APP_TOKEN` in its environment, because `run_member_bootstrap` passes no `env`, and the shipped one reads neither. They now say it is given no token in its argv. `rewrite_environment`'s docstring also names the https measurement. The code of `siblings.py` is unchanged: its syntax tree, docstrings aside, is the same as before.

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

Copy link
Copy Markdown
Contributor Author

Re-check of #1075 (#1055) at 041f7831, for lane openRepoShape-5: READY

Both SHOULDs and the NIT from round 1 are fixed, and every run I made passed or failed as the body says. One body number is stale: the #1076 merge tree. That is a NIT.

Findings

NIT: the body's #1076 merge tree comes from the commit before the amend, not from 041f7831.

Checked and found true

  1. The commit.
    • It is one commit on c3cf5154 and changes only siblings.py and the test file.
    • Compared with ast, siblings.py's syntax tree with docstrings taken out is equal at c3cf5154 and 041f7831; with the docstrings it is not. 12 lines changed, all in docstrings, and no comment.
    • sha256: siblings.py 19107ad4, the test file 9b4eaf67.
    • All three NIT places now say the bootstrap is given no token in its argv and inherits the token variables: siblings.py:86, rewrite_environment's docstring, and the test docstring at lines 23-24.
    • "The shipped one reads neither" is true: templates/assembly-root/scripts/bootstrap.py mentions neither variable (checked by grep in round 1; this commit does not change that file).
  2. The new row.
    • Style: no assert joins conditions with and or or, the widest line is 77, there is no non-ASCII, and flake8 finds only F811s (13) in the test file and the same 3 lines as main in siblings.py.

    • The proxy is what git reads:

      • The row asserts http.proxy=http://127.0.0.1:9 is in the command scope git reads, and sets NO_PROXY and no_proxy empty.
      • Before the amend, the row also asserted the proxy's message, and that version passed on git 2.43 (589 passed in the writer's log). git 2.43 has git-remote-https, so that clone did stop at 127.0.0.1 port 9.
      • My own direct probe on 2.43: Failed to connect to 127.0.0.1 port 9 after 0 ms, exit 128.
    • Fails on main and kills R4: each result below has the row failing at assert files_holding(tmp_path, TOKEN) == [], so every assert before it held.

      run 2.43.0 2.47.1
      main's siblings.py (86bf57e0), whole file 19 failed (the 18 token rows and the new row), 9 passed
      main's siblings.py, the row alone 1 failed; token in 5 files, temp/trace-performance.txt first 1 failed, the same 5 files
      R4, whole file 1 failed (the new row), 27 passed
      R4, the row alone 1 failed; token in temp/trace.txt only 1 failed, the same one file

      Each mutated siblings.py was restored and sha256-checked.

    • The 2.47.1 build:

      • Its exec-path (libexec/git-core, 167 entries) has only git-remote-ext and git-remote-fd; /usr/lib/git-core has git-remote-http and git-remote-https.
      • My direct 2.47.1 probe: git: 'remote-https' is not a git command, fatal: remote helper 'https' aborted session, exit 128, with 4 token-bearing run_command: git remote-https lines already in GIT_TRACE's file.
    • CI's git 2.55 (inferred, not run): it has the https helper, so the clone should behave as 2.43's does: stop at the dead proxy and exit 128. The row asserts no git message, so its asserts should hold there.

  3. SHOULD 1 in the body.
  4. The body's other corrections.
  5. Runs through run9.sh.
    • git 2.43, the new file, test_update_shape_kept_helper.py and the trio: 357 passed, in 3:03 (START line read dirty=0).
    • git 2.47.1 (git version 2.47.1): the new file, 28 passed, in 1:51.
  6. Merges: against re-fetched origin/main c13c3985, tree 8ae10d0c; against validate-family.py, validate-pins.py and both bootstrap.py copies: a git Windows cannot start for a reason other than 193 that names no file (winerror 216, 1260, 11, or 188-202) is git-not-runnable, exit 2, with the put-git-on-PATH remedy, where each ended in a traceback, exit 1 (#1040) #1062, tree e6ed1ba2. Both exit 0.

Could not verify

  • git 2.55 itself, and whether its GIT_TRACE still writes the run_command: git remote-https line (if it hid it, R4 would survive on CI, though the row would still pass at head).
  • Windows and macOS.
  • The writer's 305-passed run on the predicted merge.
  • A clone or fetch from real github.com that succeeds.

Scratch left

Logs and scripts are under /tmp/claude-1000/-workspace-projects-openRepoShape/50cede29-ab82-4ca3-9f67-d98e1a58c8d2/scratchpad/v118/rv1075/:

  • r2-gate243.log, r2-extra243.log, r2-slotb.log, r2-*.log;
  • r2/body-live-now.md;
  • tools/slotb2.py.

Basetemps are /tmp/claude-1000/-workspace-projects-openRepoShape/50cede29-ab82-4ca3-9f67-d98e1a58c8d2/scratchpad/v110/bt/rv1075-r2-*. The worktrees rv1075-r2-head and rv1075-r2-mut are removed, and the refs refs/l5rv/rv1075-r2 and refs/l5rv/rv1075-r2-pr1062 are deleted. Nothing of mine is running.

@brettheap

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-5, 2026-10-21T00:22Z) #1075 is not landing yet. Lane 5's landing gate found that the new https row can reach GitHub.

  • What happened. The gate ran the predicted squash tree 3e4542ba over main ff0c72eb at 00:10-00:13Z.
    • Unlike the writers' runs, it exported neither GIT_CONFIG_GLOBAL=/dev/null nor GIT_SSH_COMMAND=false.
    • conftest.py's session global file then includes the caller's ~/.gitconfig. Its url.git@github.com:.insteadOf=https://github.com/ turned the row's https://github.com/InkRouter/IRRS.git into an ssh URL, which wins over the token's rewrite of the same prefix.
    • The dead proxy never applied. The clone reached GitHub over ssh with the operator's key and succeeded, because InkRouter/IRRS is a real repository.
    • So make siblings exited 0, and test_an_https_member_with_a_token_in_use_is_in_no_trace failed its returncode == 2.
  • No secret was exposed. The fake token was not used. The clone and the member's own bootstrap ran in the test's temporary folder, which lane 5 has removed.
  • Where the row passes. With the writers' environment, the whole file passes on both the head and the gate's tree: 28 passed each. It should pass on CI too, where no such rewrite exists.
  • Next, after the usage window resets:
    • a round that makes the row hermetic against a caller's global config and ssh, with a row that holds it;
    • then rv1075's re-check and the gate again.
    • Lane 5's runner now exports both variables for every gate.

Lane: openreposhape-5 (openRepoShape-5)

…lobal file, no system file and no ssh of its own, so a caller's `url.<x>.insteadOf` for GitHub's url sends the clone nowhere, and the row runs per stand-in config (#1055)

#1075's landing gate (log c1075.log, #1075 issuecomment-6304830183) ran the https row with neither GIT_CONFIG_GLOBAL=/dev/null nor GIT_SSH_COMMAND=false. conftest.py's session global file includes the caller's, and the operator's ~/.gitconfig has `url.git@github.com:.insteadOf=https://github.com/`, which beats the token's `-c` rewrite of the same prefix: git keeps the first of two equal prefixes, and config files are read before the command line. So the row's https://github.com/InkRouter/IRRS.git went over ssh to a real private repository, the dead proxy never applied, the clone succeeded, `make siblings` exited 0, and the row failed `returncode == 2`. The writers' runs, which export both variables, passed.

- `hermetic_config` gives the row's git a global file of its own, `session_global_config("")` (conftest's own: automatic maintenance off, no include of the caller's file), `GIT_CONFIG_NOSYSTEM=1` and `GIT_SSH_COMMAND=false`. It is the least invasive way: only the one row with an https url sets it, conftest.py and every other row stay as they were, and the other rows' remotes are local paths that no rewrite of GitHub's url reaches.
- `url_after_config` checks before the clone that `git ls-remote --get-url` leaves the url unrewritten under the row's config, so a config that reaches the row fails by name, not by cloning something.
- The row is parametrized by `CALLERS_CONFIG`: none, and a scratch global and a scratch system config of the test's own that rewrite the url to the family's own IRRS remote (`as_the_callers_config`, built from tmp files, no network, never the real ~/.gitconfig). Without that, the two stand-ins clone locally and exit 0; with it the clone fails at the proxy with git's 128, IRSS is placed, and the token is in no output and in no file.

The code of siblings.py is not touched.

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

Copy link
Copy Markdown

@brettheap

Copy link
Copy Markdown
Contributor Author

Re-check of #1075 (#1055) at 962ae43c, for lane openRepoShape-5: READY

My re-check finished before the checkpoint, so this is the full report. The round-3 fix holds: a caller's url rewrite in the global or system file can no longer reach the https row, and one in the command scope makes the row fail by name before the clone. One NIT remains: a proxy setting in the caller's command scope can still send the clone through another proxy, unnoticed.

Findings

NIT: a caller's command-scope proxy setting overrides the row's dead proxy, and nothing in the row notices.

  • hermetic_config replaces the global and system files, but keeps whatever GIT_CONFIG_PARAMETERS and GIT_CONFIG_COUNT the caller exports.
  • Two caller settings beat the row's http.proxy=http://127.0.0.1:9, measured on git 2.43.0 with dead local ports:
    • a http.https://github.com/.proxy entry in GIT_CONFIG_COUNT, which matches the url more closely: the request went to port 8;
    • http.proxy in GIT_CONFIG_PARAMETERS, which git reads after GIT_CONFIG_COUNT: port 8 again.
  • In the suite itself, GIT_CONFIG_PARAMETERS="'http.proxy'='http://127.0.0.1:8'" lets the 3 https rows pass (3 passed).
    • command_scope_at drops GIT_CONFIG_PARAMETERS before its own read, so its assert holds.
    • The guard checks only the url.
  • The scenario: someone runs pytest from a git hook or a git -c alias carrying http.proxy= (empty) or their own proxy. The clone then goes to github.com, or through their proxy, with the fake token. So nothing secret leaves the host, but the request does.
  • It is unlikely; neither CI nor lane 5's runner sets proxy keys in the command scope.
  • Fix, if wanted: have hermetic_config drop GIT_CONFIG_PARAMETERS, or assert that no proxy key other than the dead one is in the command scope.

Checked and true

  1. The commit.

    • It is one commit on 041f7831, changing only tests/test_family_siblings_untraced.py (99 lines added, 10 removed). The test file's sha256 is 19c03ab5.
    • siblings.py is the same blob at both commits (8757ad18).
  2. What a caller's config can still do. I probed each place with scratch files only, under the row's settings, asking git ls-remote --get-url. Results were the same on 2.43.0 and 2.47.1.

    where the caller's rewrite is result
    $HOME/.gitconfig, with GIT_CONFIG_GLOBAL set url unrewritten
    $XDG_CONFIG_HOME/git/config url unrewritten
    a system file with an include.path, under GIT_CONFIG_NOSYSTEM=1 (control: the same file without it rewrites) url unrewritten
    the old GIT_CONFIG variable url unrewritten
    pushInsteadOf url unrewritten
    GIT_CONFIG_PARAMETERS or GIT_CONFIG_COUNT url rewritten, but the guard sees it
    • In the suite, a command-scope insteadOf makes all 3 https rows fail at the guard, before any clone.
    • GIT_SSH_COMMAND=false beats a caller's core.sshCommand: the trace shows run_command: ... false.
    • A caller's HTTPS_PROXY or ALL_PROXY loses to the row's http.proxy: the request went to port 9.
    • The body's claim that a same-length rewrite in a config file beats the token's -c holds: git@github.com:InkRouter/IRRS.git on both gits.
  3. The variants and the guard, on git 2.43.0 unless named.

    run result
    T2 (the head's file with hermetic_config and the guard taken out) 2 failed, 1 passed: [global] and [system] at assert 0 == 2 with siblings ok: 2 member(s)
    T2 on git 2.47.1 the same, 2 failed, 1 passed
    T1 (no hermetic global file, guard kept) [global] fails at the guard, which names file:///…/family0/remotes/IRRS.git against https://github.com/InkRouter/IRRS.git
    T3 (no GIT_CONFIG_NOSYSTEM, guard kept) [system] fails at the guard, in the same words
    • The gate's failure reproduces at 041f7831 under L5_GIT_CONFIG_GLOBAL (a scratch file with the rewrite):
      • the https row alone: 1 failed;
      • the whole file: 1 failed, 27 passed, at assert 0 == 2.
    • Under the same setting, the head's 3 https rows pass.
  4. R4 and style.

    • R4 is still killed: the 3 https rows fail on ['temp/trace.txt'], and the whole file gives 3 failed, 27 passed.
    • No assert joins conditions with and or or. The widest line is 77, the file is ASCII, and flake8 finds only F811s (13).
  5. The body.

    • closingIssuesReferences is [1055], and the only closing keyword is Closes #1055.
    • Every Round 3 number that comes from a run I reproduced: 359, 30, 1 failed/27 passed, 2 failed/1 passed on both gits, the T1 and T3 failures, R4's 3 failed/27 passed, and both merge trees.
    • Two numbers I matched only in part:
      • "the new file under L5_GIT_CONFIG_GLOBAL: 30 passed": I ran only the 3 https rows (3 passed);
      • the timings differ under load.
  6. Runs and merges.

Could not verify

Scratch left

  • Logs, the probe files and the scripts are in /tmp/claude-1000/-workspace-projects-openRepoShape/50cede29-ab82-4ca3-9f67-d98e1a58c8d2/scratchpad/v118/rv1075/r3/, vectors/ and tools/r3b.py.
  • Basetemps are /tmp/claude-1000/-workspace-projects-openRepoShape/50cede29-ab82-4ca3-9f67-d98e1a58c8d2/scratchpad/v110/bt/rv1075-r3-*.
  • Before removal, the mutant tree's test file read 19c03ab5 and its siblings.py 19107ad4, so every mutant was restored.
  • The worktrees rv1075-r3-head and rv1075-r3-mut are removed, and the refs refs/l5rv/rv1075-r3 and refs/l5rv/rv1075-r3-pr1088 are deleted.
  • Nothing of mine is running, so I did not write STATE-r3.md.

@brettheap
brettheap merged commit 7e00834 into main Oct 21, 2026
5 checks passed
@brettheap
brettheap deleted the fix/1055-siblings-untraced branch October 21, 2026 12:40
brettheap added a commit that referenced this pull request Oct 23, 2026
…nt of the commit `--to` names, so a tag above it reads as one that reaches it and a tag below it keeps the plain words, and `--points-at` for `--contains` no longer survives (#1019) (#1107)

No test held that a tag at a DESCENDANT of the refused commit counts as a tag that reaches it, so `--points-at` in place of `--contains` survived in `bump-leg.py` and `family.py` (mutant M9 of #988's review, filed as #1019). This adds the rows the issue asks for to the two reachability test files, and no production code changes. Opened as a draft with no milestone: Brett Heap's ruling of 2026-10-21T10:32:04Z ("leave the follow-ups out of v1.2.0") held it until the v1.2.0 tag, and it is now on the v1.2.1 milestone.

Measured on main `2a119f28` before any change, and checked again on `16f9a0e81` and `7e00834c` (the tag questions did not change, only their line numbers):

- `scripts/bump-leg.py` `_remote_tag_reaches` and `scripts/family.py` `_origin_tag_reaches` list the tags with `git for-each-ref --contains <commit> --format=%(objectname) refs/tags/`. No other copy of either exists (the `--contains` in `on_a_remote_branch` and `_on_a_remote_branch` asks about branches and is not this question).
- The mutant (`--contains` -> `--points-at` on that one line in each script) SURVIVES every row on main: `test_bump_leg_reachability_words.py` 34 passed, `test_family_bump_reachability_words.py` 20 passed, 54 rows, as the issue says.
- The production code is right: the new rows pass on unmutated main. So they do not "fail on main"; they fail on the mutant, which is the issue's own acceptance ("With `--points-at`, that row must fail"). No defect was found.

`tests/test_bump_leg_reachability_words.py`:
- `made_in_the_leg(project, name, on="HEAD")` takes an optional `on`, the commit the new commit is made on top of. Every existing call is unchanged.
- `test_a_tag_at_a_descendant_of_the_commit_reaches_it`: a commit `made` and a child `above` on top of it, on no branch of the leg's remote. The leg and the remote each tag `above` (`v1-w1019`), made apart, in three kinds: both annotated (the reviewer's case J), leg lightweight and remote annotated, and the reverse. Both runs (dry run and real) refuse `made` `bump-leg-commit-local-only` in the TAG words, through `origin` and through `--local-remote-dir` (3 kinds x 2 routes = 6 rows), with nothing moved and the leg's own tag as it was. The premises say the tag names the child, that no tag is AT `made` (`for-each-ref --points-at`), and, through #1087's `only_a_tag_reaches`, that a tag of the remote reaches it, no branch does and no remote-tracking ref of the leg does.
- `test_a_tag_at_an_ancestor_of_the_commit_alone_keeps_the_plain_words`: the control (x 2 routes). The commit named is the child, and the leg and the remote tag only its parent. The plain words stay, "a pin the rest of the world cannot fetch" included.

`tests/test_family_bump_reachability_words.py`:
- `made_on(holder, env, parent, name)`: a commit never pushed, made in the mount on top of `parent`, through `commit_in_mount` (which checks `parent` out first), so the holder stays clean.
- `test_a_tag_at_a_descendant_of_the_commit_reaches_it`: the same three kinds (3 rows), both `bump --dry-run` and the real `bump` refuse `member-to-local-only` in the tag words, with nothing moved and the mount's own tag as it was.
- `test_a_tag_at_an_ancestor_of_the_commit_alone_keeps_the_plain_words`: the control (1 row).

No assert joins conditions with `and` or `or`; each premise is its own assert. No README, docs, AGENTS.md or handbook text names these tests or their row counts (grep), so none changes.

| Mutant | Rows before (main's) | With these rows |
| --- | --- | --- |
| `--points-at` in `bump-leg.py` | survives: 34 of 34 pass | killed: 6 failed, 36 passed (all six descendant rows; the two controls pass, as they should) |
| `--points-at` in `family.py` | survives: 20 of 20 pass | killed: 3 failed, 21 passed (all three descendant rows) |
| `--merged` in `bump-leg.py` (extra: a check read the other way round) | survives: 34 of 34 pass | killed: 8 failed (6 descendant rows and both controls) |
| `--merged` in `family.py` (extra) | survives: 20 of 20 pass | killed: 4 failed (3 descendant rows and the control) |

The failure is the one the issue names: the plain words ("is reachable from no branch of ...") where the tag words ("and a tag of ... reaches it, but no branch ... does") are expected. Logs: `r-mut-points-at-bump.log`, `r-mut-points-at-family.log`, `r-mut-merged-bump.log`, `r-mut-merged-family.log` (and `mut-main-*.log` for the 34 and 20 on `2a119f28`).

On the pushed head `01fcb259`:
- git 2.43.0 (`/usr/bin/git`): the two test files, 66 passed (42 + 24; 34 + 20 before).
- git 2.43.0: the trio `tests/test_repo_hygiene.py`, `tests/test_agents_md_codes.py`, `tests/test_cli_reference.py` and `tests/test_update_shape_kept_helper.py`, 329 passed.

On `1201680f` (the same two files on top of `16f9a0e8`, before `7e00834c`, whose three files no row of mine touches or runs):
- git 2.43.0: the trio alone, 170 passed; the two test files that import helpers from mine (`test_bump_leg_fetch_refspecs.py`, `test_family_bump_fetch_refspecs.py`), 102 passed; the two test files, 42 and 24 passed.
- git 2.47.1 (`$Q/v115/w764/git247/bin/git` first on PATH, `git --version` printed `git version 2.47.1`): the two test files, 66 passed.

No script changed, so no other test runs a changed script.

- `git merge-tree --write-tree HEAD origin/main` against a re-fetched `origin/main` (`7e00834c`, which is also the merge base): exit 0, tree `3a852e87fcfda41766ece8c1771e47689c622ad6`, equal to this head's tree. Against `16f9a0e8` before the last rebase: exit 0 too.
- Open PRs read with `gh pr list --repo opensoft/openRepoShape --state open --json number,headRefName,isDraft,files`: 8 (#1102, #1101, #1098, #1097, #1091, #1090, #1088, #1075). None touches `tests/test_bump_leg_reachability_words.py` or `tests/test_family_bump_reachability_words.py`, so there is no merge-tree to run against any of them.

- **The NIT.** The docstrings of the two descendant rows said "Every row before this one tagged the commit itself, so ... survived all 54 of them". 54 counts the two files together (34 + 20), several earlier rows tag nothing (the no-`origin` and prune rows), and a count goes stale with the next row.
- **The fix, docstrings only.** Each now says only what holds: "No row before this one tags a descendant of the commit: the tags in them are at the commit itself or below it, so `--points-at` in place of `--contains`, the mutant M9 of #988's review, survived every row of this file." No row, assert or helper changed. The 54 in "What still held on main" above is the two files together, as it says.
- **One commit on top of `01fcb259`, a fast-forward push, no rebase:** head `ee48b5fe9693ebda099d1c0aadaca7bdc16b91c1`, read back with `git ls-remote`. Its tree is `615e571bb62fd1cf481e4152278b0e3628bab059`.
- **Runs** (git 2.43.0, through `run9.sh`, `EMAIL=service@opensoft.dev`): `tests/test_bump_leg_reachability_words.py` 42 passed, `tests/test_family_bump_reachability_words.py` 24 passed. They ran on `7186f3cf`, the same tree as the pushed head; only the commit message was shortened after.
- **Merge-tree** against a re-fetched `origin/main` (`6da757cb`, merge base `7e00834c`): exit 0, tree `453566df33c886cf7fc2b1c39af02acbbff365dc`. Main moved by nine files, none of mine; its `scripts/bump-leg.py` change (9 lines added) leaves `_remote_tag_reaches` and its `--contains` line alone (now line 1214).

Review: rv1104-1107, lane 5's independent opus reviewer, which wrote none of #1107: READY at `ee48b5fe`, the head that lands (issuecomment-6318124100), with no MUST or SHOULD and one optional test NIT that needs no round. CI (Brett Heap's ruling 27): H0 is the head ee48b5f itself, which passed tests, tests-git-bash and SonarCloud; main has touched none of its files since its base 7e00834; and the local gate passed on its predicted squash tree 2f7f587 over main 422a590: the bump_leg_reachability_words, family_bump_reachability_words, repo_hygiene, agents_md_codes, cli_reference sets, 237 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 #1019

Lane: openreposhape-5 (openRepoShape-5)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Oct 23, 2026
…y a member's failed clone ends git's update with 1 before any checkout but a leg's lets it go on, and `killed_said` says git's update was itself killed, not that it went on (#1085) (#1088)

The two texts #1076 (#1056) left in the family root's `bootstrap.py` say what git does. A member's clone that fails ends git's update before any checkout, where a leg's lets it go on, and a run whose git was itself killed is not said to have gone on. This is #1085, found by rv1076, lane 5's independent review of #1076 (its NITs 1 and 2).

Round 1 was one commit, on main `c13c3985` (where #1076 landed); round 2 (below) adds a second. It changes two files: the family root's `templates/family-root/scripts/bootstrap.py` (docstrings, and one line of `killed_said`) and `tests/test_family_bootstrap_stops_named.py` (one row). 2 files, 55 insertions, 14 deletions.

### NIT 1: two docstrings, no behaviour

- **`credential_advice`** said "a clone that fails, a member's or a leg's, gives 1, and git's update goes on". That is true of a leg's clone and not of a member's. It now says:
  - a member's fetch that fails ends the update with 128 (as before);
  - a member's clone that fails a second time, git having retried it, ends the update with 1 before any gitlink at that level is checked out (`quickstop`, git's `update_submodules`);
  - a leg's clone that fails gives its member's update 1, and git's update goes on (rv996's NIT 2 on #1045, kept).
- **`ended_on`** said `128 + N` is the update's code "where a git it ran died of signal N ... and no gitlink after it failed". A member's `git submodule--helper clone` that dies of signal N never gives that code: git retries it, and where the retry fails too the update ends in `quickstop`'s 1, even with nothing after it. The docstring now says so, and what that reads as:
  - **traced (no token in use):** `died_in` names the signal, since `git submodule--helper clone` is a git the update ran, and `signal_met` asks the trace first;
  - **with a token in use** (the update is not traced, #996): the update's code is 1, which reads as the credential's skip, the advice and `ok`, the limit `credential_advice` already says for a signal before a later failure.

### NIT 2: `killed_said`, one line

Where git's trace gave a git's signal (`Died(9, 137)`) and git's update returned `-9` (Python's code for git itself killed), the line said:

```
a git that git's update ran died of signal 9 (SIGKILL, exit 137), and git's update went on and ended with exit -9. Git's own words above say which git and where.
```

git's update did not go on and end: it was killed. It now says:

```
a git that git's update ran died of signal 9 (SIGKILL, exit 137), and git's update was itself killed (exit -9). Git's own words above say which git and where.
```

Every other line is byte for byte as before. I compared main's and this head's `killed_said` over 23,814 `(Died, returncode)` pairs (for each update code from -70 to 299: the update's own `ended_on` signal, where it gives one, and a trace's signal N = 1 to 64 with code `128 + N`), with `ended_on` itself:

- `ended_on` differs for 0 codes of -70..299;
- the pairs where the code is the update's own (the 134 codes that give a signal, #998's texts): 0 differ;
- the pairs where a trace's signal and a positive update code differ: 0;
- 4,480 pairs differ, each with a negative update code and a trace code (70 codes x 64 signals), and each now says "was itself killed (exit -N)".

(A script that loads main's and the head's `bootstrap.py` from `git archive` copies; its output is in lane 5's scratch notes, `v118/w1085/runs/probe-texts.txt`.) The failure line `(a git it ran died of signal N)` is unchanged, as it is true.

### Texts that pin those words

A grep of `tests/`, `README.md`, `docs/` (the handbook included), `AGENTS.md` and `contracts/` for "went on and ended", "killed_said", "went on", "member's or a leg's", "goes on (rv996", "UPDATE KILLED" and "quickstop" finds the words only in `bootstrap.py` and in `tests/test_family_bootstrap_stops_named.py`. There, `WENT_ON` pins "went on and ended with exit {ended}" with `ended` 128 and 1 (real runs, a positive code), which is unchanged and still passes. The README, the docs and the handbook say none of them.

The stops-named file's module docstring gains one paragraph for the new row.

I read, and did not run (no network clone, no real gh, no token):

- **git 2.47.1**: the local build tree of the git the lane's runs use first on PATH (`builtin/submodule--helper.c`, `run-command.c`, `trace2/tr2_tgt_event.c`, `usage.c`, `git-submodule.sh`).
- **git 2.43.0**: upstream's `v2.43.0` tag, the same five files (`builtin/submodule--helper.c` is byte for byte the copy an earlier lane 5 writer kept, w648c on #682, sha256 `9383338c...`; the other four I fetched as raw files from the tag, a read, not a clone). `/usr/bin/git` here is Ubuntu's `2.43.0-1ubuntu7.3`, which I did not read the patches of.

| claim | source (2.47.1 line, 2.43.0 line) |
|---|---|
| a member's clone is a `git` child, `git submodule--helper clone` | `prepare_to_clone_next_submodule`: `child->git_cmd = 1`, args `submodule--helper clone` (:2141-2146, :2053-2058); trace2's `child_start` writes `git` first for a `git_cmd` child (`tr2_tgt_event.c` :358-361, :359-361) |
| a child killed by signal N gives 128 + N | `wait_or_whine` `code += 128` (`run-command.c` :576, :569); `finish_command` hands it on and writes `child_exit` (:986-993, :976-983) |
| that code never reaches the update's own | `pp_collect_finished` passes it to `update_clone_task_finished` (:1700-1703, :1690-1693), which answers 0 ("Retry scheduled", :2265) the first time and, the second, sets `quickstop` and answers 1 ("a second time, aborting", :2276-2280; 2.43.0 :2185-2189) |
| `quickstop` ends the update with 1 before any checkout | `update_submodules`: `if (suc.quickstop) { ret = 1; goto cleanup; }` (:2737-2740, :2621-2624), before the loop that runs `update_submodule` for each gitlink |
| a member's fetch that fails ends it with 128 | `run_update_procedure`: `return die_message(...)`, and `die_message` returns 128 (`usage.c` :230-238, :227-235); the loop stops at 128 only (:2772, :2647) |
| a leg's clone that fails gives its member's update 1 and git's update goes on | the same `update_submodules` runs in the member's own `git submodule--helper update` (`update_submodule`: `ret = run_command(&cp)`, :2700, :2584, which discards `die_message`'s 128 and returns the member's 1); the holder's loop keeps going on any code but 128 |
| `git submodule update` passes the helper's code on | `git-submodule.sh`: the helper is `cmd_update`'s last command, no `\|\| exit` (:380-403, :341-363) |

Two offline probes, local paths only (no network, no gh, no token, `git config` never run, `GIT_CONFIG_GLOBAL=/dev/null`; lane 5's scratch notes, `v118/w1085/probe/quickstop.sh`, with its outputs `out243.txt` and `out247.txt`), on git 2.43.0 and 2.47.1, give the same on both:

| state | git's update | what is in the work trees |
|---|---|---|
| holder with `members/A`, `members/B` (a url that is not there), `members/C` | exit 1, `Failed to clone 'members/B'. Retry scheduled`, then `a second time, aborting` | A and C cloned, **none checked out** (`.git` only), B empty |
| holder with `members/M` (its leg `legs/L2` has a url that is not there) and `members/N` after it | exit 1, `Failed to clone 'legs/L2'. Retry scheduled` | **M and N both checked out** (git's update went on past M's 1); M's leg L1 cloned, not checked out |

**Not run:**
- `bootstrap.py` itself over a member's clone killed by a signal. Git's update over one was measured in round 2 below (rv1088's probe, which I ran again), and the trace's `child_start` argv for it is from `tr2_tgt_event.c`;
- git 2.43.0's remote helper or any network clone;
- Windows and macOS (CI is Linux only, #597).

One thing I read and left alone, as the brief says every other line stays byte for byte: where the trace names a signal and the update's code is a positive 1 from `quickstop`, `killed_said` still says "git's update went on and ended with exit 1". It ended there rather than went on. It is a wording nit of the same kind, not a behaviour; I did not change it here.

`test_an_update_itself_killed_after_a_signal_is_not_said_to_go_on` (new; 48 rows before, 49 after). It loads the family root's `bootstrap.py` as a module (`loaded`, from `tests/test_family_holder_bootstrap_texts.py`, which the stops-named file already imports from) and calls `killed_said(Died(9, 137), -9)`, as `signal_met` hands it its arguments. No run here reaches that line short of the OOM killer taking a checkout and later the top `git`. One assert, `said == KILLED_AFTER.format(...)`, no `and` or `or`.

Main's `bootstrap.py` bytes (sha256 `f1c156e2...`) against this branch's 49-row file, git 2.43.0: **1 failed, 48 passed**. The failure, from `v118/w1085/runs/mut-mainbytes.log`:

```
E         - s update was itself killed (exit -9). Git's own words above say which git and where.
E         + s update went on and ended with exit -9. Git's own words above say which git and where.
```

The other 48 rows pass on main's bytes (the old `WENT_ON` rows say only positive codes). At this head it passes.

Each applied alone to the change's `bootstrap.py` (a script replaces one exact string and asserts it occurs once), the stops-named file run on git 2.43.0 (`/usr/bin/git`) through `run9.sh`, the file put back from the head's bytes and its sha256 checked (`a52483c8...`) after each. Lane 5's scratch notes, `v118/w1085/runs/mutants-summary.txt`.

| | mutant | result |
|---|---|---|
| M1 | the new branch dropped (the ternary replaced by the old wording alone) | killed: 1 failed (the new row), 48 passed |
| M2 | the negative test inverted (`returncode > 0`) | killed: 7 failed, 42 passed: the new row and the six that say "went on" (both killed-checkout rows, P3, the both-named row, `two-members`, `two-legs`) |
| M3 | the old "went on and ended with exit N" kept for negative codes (the branch's text is the old wording, its test kept) | killed: 1 failed (the new row) |
| M4 | "went on" kept beside the new words (`went on and was itself killed (exit -9)`) | killed: 1 failed (the new row) |
| M5 | the test asks the trace's code, not the update's (`died.code < 0`) | killed: 1 failed (the new row) |
| M6 | the code not said (`was itself killed`) | killed: 1 failed (the new row) |

M1 and M3 behave the same (both give the old wording for a negative code) and differ in the text of the mutation only.

All through `run9.sh` (the real `gh` replaced by a stub, `GH_TOKEN` a fake, every `LANES_*` variable cleared, `EMAIL=service@opensoft.dev`, `GIT_SSH_COMMAND=false`, `GIT_CONFIG_GLOBAL=/dev/null`), at most two at once, never the full suite. At this head, `bootstrap.py` sha256 `a52483c8...`, the test file `2fb6b4dd...`.

| run | git | result |
|---|---|---|
| `tests/test_family_bootstrap_stops_named.py` (49 rows) | 2.43.0 (`/usr/bin/git`) | **49 passed** |
| the same | 2.47.1 (`git --version` printed `git version 2.47.1`; the lane's local 2.47.1 build first on PATH) | **47 passed, 2 skipped** (the two P3 rows: that git exits 128 there, where 2.43.0's crashes with 139) |
| `tests/test_family.py`, `tests/test_update_shape_kept_helper.py` (the two files `grep -l 'family-root/scripts/bootstrap' tests/*.py` gives) and `tests/test_family_holder_bootstrap_texts.py` | 2.43.0 | **328 passed** |
| `tests/test_pinned_copy_text.py` and the trio (`test_repo_hygiene.py`, `test_agents_md_codes.py`, `test_cli_reference.py`), with `test_template_text_placeholders.py` | 2.43.0 | **399 passed** |
| the six mutants and main's bytes | 2.43.0 | see above |

flake8 on `bootstrap.py` shows the 3 findings main has (E501 at line 7 and 2327, E127 at 1168), and on the test file the same 120 F811 findings main's file has (the fixture imports). New lines are 79 characters at most, ASCII only.

`git merge-tree --write-tree` of this head `88af1b7b8` against `origin/main`, fetched again with `-c gc.auto=0 -c maintenance.auto=false` just before the push (it moved once while this was open: `06cbd4789`, #1081, one test file), and against each open PR that touches `bootstrap.py`, read with `gh pr list --state open --json number,headRefName,files` (9 open; #1062 touches the family root's `bootstrap.py`; #1075's files are `siblings.py` and two test files; #1087, new since the first read, touches neither):

| against | result |
|---|---|
| `origin/main` `06cbd4789` (one commit past this branch's base `c13c3985`; it touches only `tests/test_doctor_gitlink_repairs.py`) | clean, tree `a10ffa31c` (against `c13c3985` the merge was this head's own tree, `deeab934f`) |
| #1062 `fix/1040-windows-start-errors` `957ba0386` (touches `bootstrap.py`, in `git_not_started`) | clean, tree `46f25219c`; the merged family root `bootstrap.py` parses, and holds `died_in`, `quickstop`, "was itself killed (exit" and #1062's `4551` |
| #1075 `fix/1055-siblings-untraced` `041f7831b` (lane 5's: `siblings.py`) | clean, tree `02b01b276`; the merged `siblings.py` imports `REWRITE_TARGETS`, `credential`, `member_make_environment`, `member_path`, `members_of` and `untraced` from `bootstrap.py`, and all six are defined at module level in the merged `bootstrap.py` |

None of the other open PRs touches `bootstrap.py` or the stops-named file. This change defines and renames nothing a caller imports.

`templates/family-root/scripts/bootstrap.py` is in `FAMILY_COPIED_VERBATIM`, so each holder takes one `upstream-changed` row for it, as for #1045 and #1076. Nothing else is pinned. No Windows or macOS run was made.

rv1088 (#1088 issuecomment-6314332205) found the review READY with one NIT: the sentence this PR added to `ended_on` said git's update retries a killed member clone, and it does not always. This round is one commit on top of `88af1b7b8` (the branch was not rebased), docstring only, one file (`templates/family-root/scripts/bootstrap.py`, +8 -4). No behaviour changes, and `killed_said`'s "went on and ended with exit 1" line is left as it is (rv1088 found it true).

**Measured, offline, on git 2.43.0 and 2.47.1** (rv1088's `killprobe.sh`: a remote helper that SIGKILLs the `git submodule--helper clone` above it; I ran both modes again on both gits and got the same):

| the member's clone | git's update | work trees |
|---|---|---|
| killed on both attempts | exit 1: `Retry scheduled`, then `a second time, aborting` | A and C hold `.git` only, B empty: nothing checked out |
| killed once, its own `git clone` child left running (it writes `members/B/.git`) | exit 0: `Retry scheduled`, then `BUG: submodule considered for cloning, doesn't need cloning any more?`, no second attempt | A, B and C all checked out |

`ended_on` now says: git's update retries a killed member clone where it left no `.git` at the member's path, and where the retry fails too ends in `quickstop`'s 1 before any gitlink at that level is checked out; where its own `git clone` lived on and wrote that `.git`, git retries nothing and goes on, and exits 0 with every gitlink checked out (measured, git 2.43.0 and 2.47.1). "Read, not run" is now "measured". `signal_met` already says nothing for an exit of 0, and `credential_advice`'s "fails a second time, git having retried it" stays true, so neither changes.

No other text pins those words: a grep of `tests/`, `README.md`, `docs/` (the handbook included) and `AGENTS.md` for "retries it", "doesn't need cloning", "never gives that code" and "quickstop" finds them only in `bootstrap.py`, and the round-1 words "(read in git 2.43.0's and 2.47.1's submodule--helper.c, not run)" are gone everywhere. "not run" alone appears in other files only in unrelated senses (rv1088's re-check). flake8 shows the same 3 findings as before (the lines moved by 4).

**Runs** at this head, through `run9.sh` with `EMAIL=service@opensoft.dev` (`GIT_SSH_COMMAND=false` and `GIT_CONFIG_GLOBAL=/dev/null` set by the runner), the stops-named file and the trio at the same time:

| run | git | result |
|---|---|---|
| `tests/test_family_bootstrap_stops_named.py` | 2.43.0 (`/usr/bin/git`) | **49 passed** |
| `tests/test_repo_hygiene.py`, `tests/test_agents_md_codes.py`, `tests/test_cli_reference.py` | 2.43.0 | **170 passed** |
| `tests/test_family_bootstrap_stops_named.py` (`git --version` printed `git version 2.47.1`, the lane's 2.47.1 build first on PATH) | 2.47.1 | **47 passed, 2 skipped** (the two P3 rows, as before) |

**Merges**, `git merge-tree --write-tree` of this head against `origin/main` re-fetched with `-c gc.auto=0 -c maintenance.auto=false`:

| against | result |
|---|---|
| `origin/main` `16f9a0e81` (#1087 and the commits since this branch's base; #1062 has landed, so main's `bootstrap.py` moved) | clean, tree `b3b454168`; the merged family root `bootstrap.py` parses and holds `died_in`, `git_not_started`, "was itself killed (exit" and the new BUG sentence |
| #1075 `fix/1055-siblings-untraced` `962ae43c1` (its head moved since round 1) | clean, tree `a4c72197c`; the merged `siblings.py` imports `REWRITE_TARGETS`, `credential`, `member_make_environment`, `member_path`, `members_of` and `untraced` from `bootstrap.py`, and all six are defined in the merged `bootstrap.py` |

The other open PRs touch neither `bootstrap.py` nor the stops-named file. #1088 stays a draft.

Review: rv1088, lane 5's independent opus reviewer, which wrote none of #1088: READY at `26f49d85`, H0 of this landing (issuecomment-6316937429), with no MUST or SHOULD; its one body NIT is fixed in the body. CI (Brett Heap's ruling 27 and his fast-refresh ruling of 2026-10-15T22:48:35Z): H0 26f49d8 passed tests, tests-git-bash and SonarCloud; the head dedc0da is a clean update-branch merge of main 0d76e4c, with tests-git-bash and SonarCloud green; and the local gate passed on its predicted squash tree 377691f over main 0d76e4c: the family_bootstrap_stops_named, repo_hygiene, agents_md_codes, cli_reference sets, 220 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 #1085

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
…rlier apply kept, read `unchanged`, as a module a written copy imports from and as an importer of a written one, so a plain apply that stops either on its import is refused (#1082) (#1133)

**#1082: `update-shape.py apply` refuses, before any write, every run that would leave a copy importing a name from a copy that does not define it, wherever the kept copy came from: this run's `--accept-local`, or an earlier run's.**

Fix round 1 (`53791e45`, on a merge of main `038dafba`) answers rv1133's review: its SHOULD 1 (the known limit was inside this Goal) is fixed here rather than filed, and its SHOULD 2 and NIT are taken.

All probes are offline. `gh` is a stub that exits 97, `GIT_SSH_COMMAND=false`, `GIT_CONFIG_GLOBAL=/dev/null`, and git is 2.43.0. The tool is main's `update-shape.py` (sha256 `ca47f90e`, blob `a7ae7505`, the same at `2a119f28`, `6da757cb`, `4ed7c7bf` and `038dafba`). Scripts and logs are in lane 5's scratch `v118/w1082/` (`tools/probe1023.sh`, `tools/probe1075.sh`, `probe-*`) and, for fix round 1, `v120/f1133/` (`tools/knownlimit.sh`, `tools/probe1075.sh`, `limit-*`, `probe-*1075`).

**#1075's `untraced`, on main's own history.** #1075 has now landed, so main itself holds the case.
- A holder cut at `6476c170` has a `bootstrap.py` with no `untraced`. Its own edit to that file is committed.
- `apply --at 2a119f2 --accept-local scripts/bootstrap.py --branch shape/first` exits 0. That was main before #1075, and its `siblings.py` takes no `untraced`.
- `check --at 6da757c` reads `unchanged scripts/bootstrap.py` and `upstream-changed scripts/siblings.py`. #1075 changed `siblings.py` alone, and `bootstrap.py` is blob `3c1060d3` at both commits.
- `apply --at 6da757c --branch shape/second` exits 0, prints `copied scripts/siblings.py`, and commits.
- Then `scripts/siblings.py --help` exits 1 with `ImportError: cannot import name 'untraced' from 'bootstrap'`.
- The one-step run, `apply --at 6da757c --accept-local scripts/bootstrap.py`, exits 2 with `scripts/bootstrap.py (kept) lacks untraced, which scripts/siblings.py imports from it`. The holder is unchanged.

**#1023's `member_make_environment`.** A holder is cut at `f1946d04`, the parent of #1023's `9e154d87`. Its `bootstrap.py` has no `member_make_environment`, and its `siblings.py` does not name it. Its own edit to `bootstrap.py` is committed.
- **Main's history alone does not reach the gap for this name.** #1023 defined the name in `bootstrap.py` and added it to `siblings.py`'s import in one commit. `git log -S member_make_environment` over the two files names `9e154d87` alone.
  - So `check --at <main>` reads `both scripts/bootstrap.py`.
  - `apply --at <main>` is `update-conflict`.
  - `apply --at <main> --accept-local scripts/bootstrap.py` is refused already: `scripts/bootstrap.py (kept) lacks member_make_environment, untraced, which scripts/siblings.py imports from it` at `6da757cb`, and `lacks member_make_environment` at `2a119f28`. The holder is unchanged.
- **With #1023 landed in two halves, as #1045 and #1075 landed `untraced`, it does reach it.** T1 is a commit on main with `siblings.py` as it was at `f1946d04`. That is `7c695d71` on `2a119f28`, and `3d5a7eda` on `6da757cb`.
  - `apply --at T1 --accept-local scripts/bootstrap.py --branch shape/first` exits 0.
  - `check --at <main>` reads `unchanged scripts/bootstrap.py` and `upstream-changed scripts/siblings.py`.
  - `apply --at <main> --branch shape/second` exits 0, copies `siblings.py`, and commits.
  - `scripts/siblings.py --help` then exits 1 with `ImportError: cannot import name 'member_make_environment' from 'bootstrap'`.
  - Measured against both `2a119f28` and `6da757cb`.

**The reverse direction: a copy an earlier run kept, as the importer (rv1133's SHOULD 1).** rv1133 measured it with main's tool and with `aa7ddf59`'s; fix round 1 measured it again with main's (`limit-m/`).
- A family holder is cut at `4ed7c7bf`. Its own edit to `siblings.py` adds `from bootstrap import told_past`, which the upstream's `siblings.py` does not import, and is committed. `siblings.py --help` still exits 0.
- `apply --at U1 --accept-local scripts/siblings.py --branch shape/first` exits 0. U1 is main plus an unrelated change to `validate-family.py`.
- U2 then renames `told_past` in `bootstrap.py` alone. `check --at U2` reads `upstream-changed scripts/bootstrap.py` and `unchanged scripts/siblings.py`.
- `apply --at U2 --branch shape/second`, with no `--accept-local`, exits 0, prints `copied scripts/bootstrap.py`, and commits. `siblings.py --help` then exits 1 with `ImportError: cannot import name 'told_past' from 'bootstrap'`.
- This run's own write breaks a copy that worked before it, so it is the Goal's case. The one-step run, `--accept-local scripts/siblings.py`, was already refused by #604 with `scripts/bootstrap.py (written) no longer defines told_past, which scripts/siblings.py (kept) imports from it`.

`update-shape.py`, `_refuse_kept_copies_missing_names` (#536, #604):

- **Each written `.py` copy is asked against its module as this run leaves it, whoever kept it.** The reading is `_copies_as_left`, `_written_copies_lacking` and `_names_as_left`. The module may be:
  - **kept by this run's `--accept-local`:** as before, the line says `(kept)`;
  - **an `unchanged` row whose pin records the project's own bytes**, which is a copy an earlier run kept that the upstream has not changed since. The line says `(kept by an earlier run)`;
  - **the upstream's own bytes at the target:** written by this run (an `upstream-changed` row or an `--add` file), `already-at-target`, or `unchanged` with the target's bytes. The line says `(the upstream's)`. No shipped copy meets this today: the reader rows of `tests/test_update_shape_kept_helper.py` find every name each shipped importer takes defined beside it. So it refuses only a target whose own copies disagree.
- **A copy an earlier run kept is asked as an importer too (fix round 1).** #604 asked only a copy this run's `--accept-local` keeps against a module this run writes (`_kept_copies_importing_dropped`). The rows `_copies_as_left` reads as `kept by an earlier run` are now passed beside the kept ones, under a new side, `WRITTEN_UNDER_EARLIER`.
  - Its line says who kept the importer: `scripts/bootstrap.py (written) no longer defines told_past, which scripts/siblings.py (kept by an earlier run) imports from it`. `KEPT_IMPORTERS` maps each `(written)` side to its importer's word, so a `(kept)` importer's line is unchanged.
  - `_names_dropped` still limits the refusal to names this run takes away: a name the module's bytes on disk define and its written bytes do not. A copy an earlier run kept whose names all stay, or that takes a name the module lacked before too, still applies.
  - An `unchanged` copy holding the upstream's own bytes is still not asked as an importer: that pair is two of the target's own copies.
- **The early `if not kept: return` is gone.** A run with no `--accept-local` is asked too. The guard still stands after `update-conflict`, `update-local-drift` and `update-rewrite-symlink`, and before `confirm` and any write.
- **The refusal's code is kept, and so are its words where every line is one this run's `--accept-local` makes.**
  - A `(kept)` or `(written)`-under-`(kept)` refusal prints byte for byte what main prints. In fix round 1, with the head `53791e45`: `cmp` of the masked one-step logs gives 1312 bytes for #1075's `(kept)` refusal and 1331 bytes for the `told_past` `(written)` refusal, both identical to main's tool. A #604 row now pins that opening (SHOULD 2).
  - Where a line is of another kind, the refusal opens with words true of every kind of line, now including a `(written)` line under a copy an earlier run kept: `a copy imports a name from a copy beside it whose bytes, as this run leaves them, do not define it, and this run writes one of the two`.
- **The remediation gives each kind its exit.**
  - **For a copy an earlier run kept,** it says why `check` reads it `unchanged`, then gives two exits. As a module that lacks a name: merge the upstream's version by hand so it defines every name its line names. As an importer of a name the written module drops (fix round 1): change it so it no longer takes any name its `(written) no longer defines` line names. Then commit, and run again with `--accept-local on each copy an earlier run kept that you changed, whose row then reads locally-modified`. Or write the upstream's bytes over it (the remedy's own `git show` line names that copy), commit, and run again. All four exits are tested end to end, and each checks the state `check` then reads: `locally-modified` after the first, `already-at-target` after the second.
  - **For two of the upstream's own copies,** it says `run this again with an --at whose copies agree`.
- **The known limit is gone.** The docstring paragraph that named it, and the row that pinned it, are replaced by the rows below that hold the refusal. `KEPT_LEAD`, the code and every `(kept)` line are as main has them.

`tests/test_update_shape_kept_helper.py`:
- **A fixture, `earlier_standard`,** models #1023 in two halves in the suite's own clone, from A's bytes, with no historical commit:
  - `before-1023`: the holder's `bootstrap.py` spells `member_make_environment` by #950's `member_make_env`, and its `siblings.py` defines its own;
  - a holder cut there;
  - `bootstrap-1023`: A's `bootstrap.py` back;
  - `siblings-no-name`: the control;
  - A is the second half.
- **Fix round 1 adds `kept-no-name` to `dropping_standard`,** a branch off `kept-base` that changes `validate-family.py` and adds or takes away no name. A holder cut at `kept-base` keeps its own `bootstrap.py`, which imports `DROPPED_NAME` from `repo_shape.py`, through an earlier apply there (`kept_by_an_earlier_apply`, shared with `kept_earlier`); `dropped` is the next apply's target. This is rv1133's `told_past` scenario in the file's own fixtures.
- **Rows, in the file's style and reusing its helpers.**
  - `refusal()` gains `left=` and `at_target=`, and `assert_stops_on_the_import` gains `script=`.
  - Fix round 1: `assert_the_kept_copy_starts_as_before` and `assert_the_exit_applies` are shared by the `--accept-local` rows and the earlier-kept rows.
  - No assert joins conditions with `and` or `or` (205 asserts, checked on the syntax tree).
- #1075's docstring line (`six names`) is kept as main has it.

- **Main merged.** `git merge origin/main` (`038dafba`) was clean: merge commit `7aee5efa`, tree `27a2785e`, the same as `git merge-tree --write-tree aa7ddf5 038dafb`. The fix is one new commit on it, `53791e45`. Nothing was amended, rebased or forced.
- **SHOULD 1 (the known limit).** Fixed as above, rather than filed. New rows:
  - `test_an_earlier_kept_copy_is_asked_when_the_next_apply_drops_its_name`: end to end, `check` reads the kept `bootstrap.py` `unchanged` and `repo_shape.py` `upstream-changed`; the plain apply to `dropped` is refused with nothing written, one detail line, the general opening, and that copy's exits, and none of the other kinds' exits. Its premise is measured: the kept `bootstrap.py` beside `dropped`'s `repo_shape.py` stops on the import.
  - `test_an_earlier_kept_copy_importing_what_this_run_keeps_applies` (2 controls): a name `repo_shape.py` lacked before too, and one the written `repo_shape.py` still defines. Both apply with no `--accept-local`, and the kept copy starts after the run exactly where it started before.
  - `test_either_exit_for_an_earlier_kept_importer_applies` (2): each exit the remedy gives that copy, with the state `check` reads after it; the apply then writes `repo_shape.py`, and `bootstrap.py` starts.
  - `test_an_importer_an_earlier_run_kept_is_asked_against_a_written_module` (2, in process): the line, the opening, the exits, and the `again` wording alone and beside a copy this run keeps.
  - `test_an_importer_holding_the_targets_bytes_is_not_asked` replaces `test_an_importer_this_run_leaves_alone_is_not_asked`, whose second param pinned the limit.
- **The scenario itself, with the head's tool** (`limit-h/`): the second apply exits 2 with `scripts/bootstrap.py (written) no longer defines told_past, which scripts/siblings.py (kept by an earlier run) imports from it`, the holder is unchanged, and `siblings.py --help` exits 0. The one-step, first-apply and `check` logs match main's tool once paths and shas are masked. #1075's probe (`probe-h1075/`) is refused at its second apply too, with `(kept by an earlier run) lacks untraced`.
- **SHOULD 2.** `test_keeping_a_copy_that_imports_a_name_the_run_drops_is_refused`, a #604 end-to-end `(written)` row, now asserts `startswith(KEPT_LEAD)`. rv1133's R2 is killed (below).
- **NIT.** The exits rows (`test_either_exit_the_remedy_names_applies` and the new one) check that `check` reads `locally-modified` after the change exit and `already-at-target` after the drop exit. The first exit's id now says the edit was changed by hand, as the remedy's `changed` does.

The new test file (185 rows) was run against main's `update-shape.py` (`038dafba`, sha256 `ca47f90e`, in a scratch clone): **17 failed, 168 passed** (`base-main.log`). Against `aa7ddf59`'s: **13 failed, 172 passed** (`base-aa7.log`). With the change: **185 passed**.

| row | on main | on `aa7ddf59` |
|---|---|---|
| `test_an_earlier_kept_copy_is_asked_when_the_next_apply_writes_its_importer` | `assert 0 == 2`: the second apply copies `siblings.py` | fails: the old opening |
| `test_with_branch_an_earlier_kept_copy_is_refused_before_any_branch` | `assert 0 == 2` | passes |
| `test_either_exit_the_remedy_names_applies` (2 params) | `assert 0 == 2`: the apply the remedy follows is not refused | passes |
| `test_a_written_copy_is_asked_against_its_module_as_this_run_leaves_it` (4 params) | no refusal | fails: the old opening |
| `test_each_kind_of_module_is_given_its_own_exit` (2 params) | no refusal | 1 fails: its remedy says `merged`, not `changed` |
| `test_every_kind_of_line_in_one_run_is_one_refusal` (2 params) | only the `(kept)` line | fails: the old opening |
| **new:** `test_an_earlier_kept_copy_is_asked_when_the_next_apply_drops_its_name` | `assert 0 == 2`: the plain apply copies `repo_shape.py` | the same |
| **new:** `test_either_exit_for_an_earlier_kept_importer_applies` (2 params) | `assert 0 == 2`: the apply the remedy follows is not refused | the same |
| **new:** `test_an_importer_an_earlier_run_kept_is_asked_against_a_written_module` (2 params) | no refusal; beside a kept copy, only its `(kept)` line | the same |

These pass on main and after: the one-step row (`test_the_one_step_run_is_refused_as_it_was`, which pins the old opening), the #604 row that now pins `KEPT_LEAD`, the controls (`test_an_earlier_kept_copy_that_defines_every_name_applies`, the two new earlier-kept importer controls, the four module-defines-every-name controls), and `test_an_importer_holding_the_targets_bytes_is_not_asked`.

Each was applied alone in a scratch clone, run on the test file, and restored by checkout with its sha256 checked.

**Fix round 1, at `53791e45` (185 rows):**

| mutant | result |
|---|---|
| N1, the earlier-kept rows not passed to `_kept_copies_importing_dropped` (`{}`), as `aa7ddf59` had it | killed, 5 failed: the 5 new rows that fail on main |
| N2, an earlier-kept importer called `(kept)` in its line | killed, 3 failed: the end-to-end refusal row and both in-process rows |
| N3, an earlier-kept importer given the `(written)`-under-`(kept)` side | killed, 3 failed: the same three |
| N4, the `KEPT_LEAD` opening kept for a `(written)` line under a copy an earlier run kept | killed, 3 failed: the same three |
| N5, the change exit for an earlier-kept importer dropped | killed, 3 failed: the same three |
| N6, the `unchanged` why and the `--accept-local on each copy an earlier run kept` exit given to an earlier-kept module only | killed, 3 failed: the same three |
| N7, the module, not the earlier-kept importer, named for `git show` | killed, 3 failed: the same three |
| N8, the "what the upstream's own copy of that file imports instead" clause for a `(written)`-under-`(kept)` line only | killed, 1 failed: the end-to-end refusal row |
| N9, every `unchanged` row asked as an importer, the target's own bytes too | killed, 1 failed: `test_an_importer_holding_the_targets_bytes_is_not_asked` |
| N10, the old general opening (`a copy this run writes or keeps imports ...`) | killed, 10 failed |
| rv1133's R2, a `(written)`-only refusal given the general opening (`sides <= {KEPT_MODULE}`) | killed, 1 failed: `test_keeping_a_copy_that_imports_a_name_the_run_drops_is_refused` (SHOULD 2); it survived at `aa7ddf59` |
| the first round's M1, the old `kept` set restored | killed, 17 failed: the 17 rows that fail on main |
| the first round's M3, the guard called after `_write_copies` | killed, 15 failed: every end-to-end refusal row, the 3 new ones among them |
| the first round's M7, earlier-kept and upstream's own swapped | killed, 12 failed |

No survivor. Every mutant was restored by checkout with its sha256 checked (`5b386f8b`), and each clone's status was clean at the end (`mutants1.out`, `mutants2.out`, `mut-*.log`).

**The first round, at `7b447602` (179 rows):**

| mutant | result |
|---|---|
| M1, the old `kept` set restored (`left = dict.fromkeys(kept, KEPT_MODULE)`) | killed, 12 failed: the 12 rows that fail on main |
| M2, `unchanged` rows asked as importers (against the modules this run writes) but the written importers asked against the old `kept` set only | killed, 14 failed |
| M3, the guard called after `_write_copies` (refused after a write) | killed, 12 failed: every end-to-end refusal row sees `copied` |
| M4, a copy an earlier run kept called `(kept)` | killed, 5 failed |
| M5, the upstream's own modules not asked | killed, 5 failed |
| M6, the new opening always used | killed, 1 failed: the one-step row's old opening |
| M7, earlier-kept and upstream's own swapped | killed, 6 failed |
| M8, the `--accept-local on each copy an earlier run kept` exit dropped | killed, 3 failed |
| M9, a written module read from its bytes on disk | killed, 4 failed |
| M10, the `unchanged` sentence dropped | killed, 2 failed |

All runs went through lane 5's `run9.sh`, with the stub `gh`, `GIT_SSH_COMMAND=false`, `GIT_CONFIG_GLOBAL=/dev/null` and `EMAIL` exported. No more than two ran at once. The full suite was not run.

**Fix round 1, at `53791e45`** (each `START` line records the head and `dirty=0`; logs in `v120/f1133/`):

| run | git | result | log |
|---|---|---|---|
| the new file, `tests/test_update_shape_kept_helper.py` | 2.43.0 | **185 passed** in 100.27s | `own243.log` |
| the same | 2.47.1 | **185 passed** in 95.67s | `own247.log` |
| `tests/test_update_shape.py`, `tests/test_update_shape_messages.py`, `tests/test_update_shape_paste_lines.py` | 2.43.0 | **144 passed** in 181.71s | `runC.log` |
| `tests/test_update_shape_apply_lines_and_headers.py`, `tests/test_update_shape_merged_away.py`, `tests/test_update_shape_rewrite_symlink.py`, `tests/test_update_shape_landing.py` | 2.43.0 | **163 passed** in 223.09s | `runD.log` |
| `tests/test_update_shape_add_not_a_directory.py` | 2.43.0 | **13 passed** in 33.11s | `runF.log` |
| `tests/test_path_guard_hygiene.py` and the trio (`tests/test_repo_hygiene.py`, `tests/test_agents_md_codes.py`, `tests/test_cli_reference.py`) | 2.43.0 | **219 passed** in 54.58s | `runE.log` |

No failure and no skip in any run at this head. The two runs of the new file went through `run9.sh` on 2026-10-22 at 13:31Z, the next three on 2026-10-22 from 15:16Z to 15:21Z, and `runF.log` at 15:23Z; each `START` line reads `head=53791e45...` and `dirty=0`. Of the 19 `tests/test_update_shape*.py` files, these 9 ran at this head. The other 10 did not run at this head: all 10 ran at the first round's head `7b447602` in shard A or B (below), over the first round's `update-shape.py`, which this round's commit changed. The full suite was not run.

**The first round, at `7b447602`** (tree `e0e2d19e`, the same tree as `aa7ddf59`): the new file 179 passed on git 2.43.0 and on git 2.47.1; shard A (29 files) 2031 passed, 5 skipped; shard B (29 files) 1814 passed, 5 skipped; the trio 170 passed. No failure in any run.

Read at `53791e45`, after a re-fetch of main (`origin/main` is still `038dafba`, the second parent of the branch's merge commit `7aee5efa`):

- **`git merge-tree --write-tree origin/main HEAD`** exits 0 and writes tree `bfa8c1d4`, which is `HEAD`'s own tree: the branch holds `038dafba` as an ancestor, so the merge adds nothing.
- **The merge commit `7aee5efa`** (parents `aa7ddf59` and `038dafba`) has tree `27a2785e`, the same tree as `git merge-tree --write-tree aa7ddf5 038dafb`, which exits 0. It was clean.
- **Open pull requests:** 37 were listed with `gh pr list --state open --limit 100 --json number,headRefName,headRefOid,files`. None but this one touches `update-shape.py` or `tests/test_update_shape_kept_helper.py`, so there is no other head to merge against. The list is `open-prs-d1.json` in `v120/f1133/`.
- `git diff --name-only origin/main HEAD` names those two files and no other.

rc1133 re-checked `53791e45` (#1133 issuecomment-6347273216): READY, with three NITs. This round takes all three, in one new commit, `a7bfee1b`, on `53791e45`. It was pushed as a fast-forward of the branch; nothing was amended, rebased or forced. The commit changes `tests/test_update_shape_kept_helper.py` alone (+7, -4). `update-shape.py` is `53791e45`'s, sha256 `5b386f8b`, so the fix and every measurement in the sections above are unchanged.

- **NIT 1 (taken): a refusal in both directions at once now has its opening pinned.** `_missing_name_refusal` opens with `KEPT_LEAD` where every line is one this run's `--accept-local` makes. A run with a `(kept) lacks` line and a `(written) no longer defines` line is such a run, but `test_both_directions_in_one_run_are_one_refusal` pinned its lines, its clause and its remedy, not its opening. rc1133's mutant O1 (`lead = (KEPT_LEAD if len(sides) == 1 and sides <= {KEPT_MODULE, WRITTEN_MODULE} else LEFT_LEAD)`) therefore survived the 185 rows. The row now asserts `result.stderr.startswith(KEPT_LEAD), result.stderr`, right after `assert_refused_writing_nothing`, and its docstring says why the opening is that one.
- **NIT 2 (taken): the Merges section lacked #1166.** Its row is in "Merges at `a7bfee1b`" below, and it replaces the Merges section's "Open pull requests" line.
- **NIT 3 (taken): the title** named only the first round's direction, a written copy asked against its module. It now names the copy an earlier apply kept in both roles: as the module a written copy imports from, and as an importer of a written module. It is 249 characters and ASCII. rc1133's 242-character rewrite was close, but its "a plain apply that would break it" is true only where the kept copy is the importer: where it is the module, the apply breaks the written importer. The new title says "either".

### Mutant O1, before and after

Applied alone in this worktree and restored by checkout, sha256 `5b386f8b` checked, status clean afterwards. The mutant is rc1133's (`mutate_rc.py`, copied unchanged).

| test file | run | result |
|---|---|---|
| `53791e45`'s, without the new assert | the row alone, with O1 applied | **survives**: 1 passed, 184 deselected (`o1-before-row.log`). rc1133 measured the whole file: 185 passed |
| `a7bfee1b`'s, with the new assert | the whole file, with O1 applied | **killed**: 1 failed, 184 passed (`o1-after-file.log`). The failure is `test_both_directions_in_one_run_are_one_refusal` at the new assert: the opening read `a copy imports a name from a copy beside it whose bytes, as this run leaves them, do not define it, ...` |

No other mutant was needed: the round adds no behaviour and no new row, and the earlier rounds' 14 + 10 mutants stand at an unchanged `update-shape.py`.

### Rows that fail on main

None, and that is measured, not assumed. The new assert pins an opening the head keeps exactly as main prints it, so it cannot fail on main. rc1133 had that only by reading, so it is measured here: the row, new assert included, run with main's `update-shape.py` (`origin/main` `961640c6`, sha256 `ca47f90e`) swapped into the worktree, gives **1 passed** (`main-row.log`); the file was restored to `5b386f8b` afterwards. The assert guards that opening against a later regression like O1, as SHOULD 2's assert guards the one-step opening against R2.

### Runs at `a7bfee1b`

All through lane 5's `run9.sh`, each `START` line at `head=a7bfee1b...` and `dirty=0`, with the stub `gh` and `EMAIL` exported; two ran at once, and the full suite was not run. No failure and no skip. Logs are in lane 5's scratch `v124/n1133/`.

| run | git | result | log |
|---|---|---|---|
| `tests/test_update_shape_kept_helper.py` and `tests/test_path_guard_hygiene.py` | 2.43.0 | **234 passed** (185 + 49) in 61.57s | `own243.log` |
| `tests/test_update_shape_kept_helper.py` | 2.47.1 (first on PATH, printed in the log) | **185 passed** in 48.73s | `own247.log` |

The commit changes no function, no path guard and no refusal text, so no other test file names anything it changed. The test file still has no assert that joins conditions with `and` or `or` (206 asserts, checked on the syntax tree), no non-ASCII byte, and its widest line is 79 characters.

### Merges at `a7bfee1b`

Read after a re-fetch of main: `origin/main` is `961640c6` (#1155, which adds `docs/release-notes/v1.2.0.md` and nothing else). It touches neither of this PR's files.

- **`git merge-tree --write-tree origin/main HEAD`** exits 0 and writes tree `98765a8b`. The branch does not hold `961640c6`, and the predicted merge differs from `HEAD` by `docs/release-notes/v1.2.0.md` alone.
- **#1166** (`fix/1139-anchored-patterns`, head `2593952c95`, opened 2026-10-22T17:31:57Z) touches `update-shape.py`, at lines well apart from `_missing_name_refusal` (rc1133's reading), and does not touch the test file. `git merge-tree --write-tree a7bfee1 2593952` exits 0 and writes tree `67858a0b`. Against `53791e45` the same command writes `37498506`, rc1133's tree, which is reproduced. rc1133's tests on the predicted merge of `53791e45` and #1166 (`c5a98365`: this file, `test_value_pattern_line_break.py`, `test_update_shape_unreadable.py`, `test_path_guard_hygiene.py`) were 311 passed; they were not re-run at `a7bfee1b`, which changes one assert and a docstring in a test file #1166 does not touch.
- **Open pull requests:** 41 were listed with `gh pr list --state open --limit 100 --json number,headRefName,headRefOid,files`. Besides this one, only #1166 touches `update-shape.py`, and none but this one touches `tests/test_update_shape_kept_helper.py`, so no other head needs a merge-tree. The list is `open-prs.json`.

Review: lane 5's independent re-check, by a reviewer that wrote none of #1133: READY at `a7bfee1b` (issuecomment-6352252707), with no MUST, SHOULD or NIT left. CI (Brett Heap's ruling 27 and his fast-refresh ruling of 2026-10-15T22:48:35Z): H0 a7bfee1 passed tests, tests-git-bash and SonarCloud; the head 08339c9 is a clean update-branch merge of main 105b5da, with tests-git-bash and SonarCloud green; and the local gate passed on its predicted squash tree b7073af over main 105b5da: the update_shape_kept_helper, repo_hygiene, agents_md_codes, cli_reference sets, 356 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 #1082

Lane: openreposhape-5 (openRepoShape-5)
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

Labels

None yet

Projects

None yet

1 participant