Repository navigation
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
Conversation
… 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>
|
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 |
Reviewer's GuideThis PR hardens Sequence diagram for token-safe sibling clone and fetchsequenceDiagram
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
Flow diagram for conditional sibling git tracingflowchart 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
File-Level Changes
Assessment against linked issues
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
|
(lane openRepoShape-5, 2026-10-20T21:05Z) @codex review |
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
|
Codex Review: Didn't find any major issues. Keep it up! Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
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". |
|
Independent review of #1075 (#1055) at 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. FindingsSHOULD 1: the body says "No
SHOULD 2: every row uses a local file remote, so a leak through
NIT: three places say the member's bootstrap "is given no token": Checked and found true
Could not verify
Checklist candidates
Scratch left
|
…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>
|
Re-check of #1075 (#1055) at 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. FindingsNIT: the body's #1076 merge tree comes from the commit before the amend, not from
Checked and found true
Could not verify
Scratch leftLogs and scripts are under
Basetemps are |
|
(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.
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>
|
|
Re-check of #1075 (#1055) at 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. FindingsNIT: a caller's command-scope proxy setting overrides the row's dead proxy, and nothing in the row notices.
Checked and true
Could not verify
Scratch left
|
…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>
…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>
…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>



Lane: openreposhape-5 (openRepoShape-5)
Claim: #1055 (comment)
Goal
make siblingsandfamily.py siblingsnow write the token inSHAPE_LEGS_TOKENorSHAPE_LEGS_APP_TOKENto no trace the caller asked git for. #996 (#1045) did the same formake 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.pygives the credential's-c url.https://x-access-token:<token>@github.com/.insteadOf=...rewrite, in argv, to two gits:place, for a member with no clone beside the holder;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
startevent of each, one line per member.scrubkept it out of the output, and only that part held.Now:
rewrite_environment(token), a new function insiblings.py:untraced()from thebootstrap.pybeside it. That is everyGIT_TRACE*variable taken out, andGIT_TRACE2,GIT_TRACE2_EVENTandGIT_TRACE2_PERFset to0, which also turns off a target the global or the system config names;env=rewrite_environment(token). Nothing else in the file changes environment.scripts/bootstrap.py, whichplaceruns after a clone, is given no token in its argv, and noenv, so it keeps the caller's environment and its traces. That environment holdsSHAPE_LEGS_TOKENandSHAPE_LEGS_APP_TOKENas before; the shipped member bootstrap (templates/assembly-root/scripts/bootstrap.py) reads neither. A test row holds this, and so doestests/test_validator_child_encoding.py, which reads that call as handing noenv.MAKEFLAGSas 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:dispatchstill runs each member's make withmember_make_environment(make), and noMAKEFLAGSreading changed.git_prefixsays 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, andrewrite_environmentsays what was measured.One thing the issue did not ask for.
GIT_TRACE_PERFORMANCEwrites a git's whole command line,-cincluded, 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: everyGIT_TRACE*variable goes.This follows #1045's
untraced()(#996): the clone and the fetch run in the very environmentmake bootstrapgives every git it hands the token.Where the helper comes from: imported, not copied
siblings.pyimportsuntracedfrom thebootstrap.pybeside it, next toREWRITE_TARGETS,credential,member_make_environment,member_pathandmembers_of.The issue left this to the writer, because a holder may keep a
bootstrap.pyolder than itssiblings.py(update-shape.py apply --accept-local), and an import of a name that copy lacks would stopmake siblings,parkandresumewith anImportError. The issue's text was written against main1a47ed07, wheresiblings.pystill kept its own copy of theMAKEFLAGSreadings. Since then #1023 (9e154d87, lane 4's) has decided the question for this pair of files:member_make_environment, because a copy in each script failed SonarCloud's duplication gate;bootstrap.pywhileapplywritessiblings.pyis refused,update-accept-local-missing-name(update-shape.pyapply --accept-local scripts/repo_shape.pycan keep an old helper that a newly copied file imports from, andmake bootstrapthen fails with ImportError #536, update-shape.pyapply --accept-local: a kept.pycopy that imports a name the run's newrepo_shape.pydropped passes, andmake bootstrapthen dies on an ImportError #604), before a byte is written, and mergesbootstrap.pyby hand first.untracedtakes the same road for the same reasons:make bootstrapandmake siblingsalike. A copy here could drift frombootstrap.py's, and the drift would be a token in a file.bootstrap.pyis refused in the one-step case, and not in a two-step one. The guard reads names.untracedis new with the family root's bootstrap.py: with SHAPE_LEGS_TOKEN or SHAPE_LEGS_APP_TOKEN set, a member's commit no branch of its remote holds (B4) still endsfamily bootstrap ok, with the advice to set the token that is already set #996 (the family root's bootstrap.py: git's update ended by a signal fails the run by name, and B4 with a token in use is told from fetches redone without a trace, so the token reaches no file (#998, #996) #1045), so a keptbootstrap.pyfrom before it is refused by that name, where--accept-localnames it in the sameapply. Measured withupdate-shape.py's own readers, in process:siblings.pyat this head takesREWRITE_TARGETS,credential,member_make_environment,member_path,members_ofanduntracedfrombootstrap;bootstrap.pyat main00f6d7f7lacks none of them;bootstrap.pyat6476c170(main just before the family root's bootstrap.py: git's update ended by a signal fails the run by name, and B4 with a token in use is told from fetches redone without a trace, so the token reaches no file (#998, #996) #1045) lacksuntracedalone, which is the line the refusal names.6476c170, andapply --at c3cf5154 --accept-local scripts/bootstrap.py: exit 2,scripts/bootstrap.py (kept) lacks untraced, which scripts/siblings.py imports from it, and the holder's files, HEAD and status unchanged.update-accept-local-missing-nameasks only the copies this run's--accept-localnames, so a copy an earlier apply kept (readunchangedafter) is never asked, and the next apply writes asiblings.pythat cannot import from it #1082). A holder whose earlier apply kept abootstrap.pywithoutuntraced(rv1075 applied--at 00f6d7f7 --accept-local scripts/bootstrap.py: that target'ssiblings.pyimports nountraced, so there was nothing to refuse), and then an apply at this head without--accept-local, getssiblings.pycopied.scripts/siblings.pythen fails withImportError: cannot import name 'untraced' from 'bootstrap', and so domake siblings,parkandresume. The cause is the guard inupdate-shape.py(:4288-4289): it checks only the files--accept-localnames in the same run. That is the gap of update-shape.pyapply --accept-local scripts/repo_shape.pycan keep an old helper that a newly copied file imports from, andmake bootstrapthen fails with ImportError #536's guard, and it is on main already for 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'sMAKEFLAGSas it is, so it runs its jobs together again; every other make still gets no jobserver and no-j(#987) #1023'smember_make_environment; this change putsuntracedinto it and does not widen it. The text above this paragraph said noImportErrorcould reach a holder, which was too strong; update-shape.py:update-accept-local-missing-nameasks only the copies this run's--accept-localnames, so a copy an earlier apply kept (readunchangedafter) is never asked, and the next apply writes asiblings.pythat cannot import from it #1082 is the follow-up, and it is not done here.tests/test_update_shape_kept_helper.py'stest_what_siblings_takes_from_the_holders_bootstrap_is_found_definedalready holds that every namesiblings.pytakes is defined in the holder'sbootstrap.py. Its docstring now says six names, not five.Probes, before (main
00f6d7f7) and after (this change)Setup:
/usr/bin/git) and git 2.47.1, Python 3.12.3;tests/test_family.py(two members, local bare remotes, the file transport allowed throughGIT_CONFIG_COUNT), laid out as<tmp>/InkRouter/InkRouter;ghfirst on PATH, a bogusGH_TOKEN, an emptyGH_CONFIG_DIR,GIT_SSH_COMMAND=false;scripts/siblings.pywas main's (sha25686bf57e0...) or this change's;w1055-a-token-no-forge-takes.Each case was run three ways:
make siblings, which clones both members;make siblingsagain, 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:
GIT_TRACE,GIT_TRACE2,GIT_TRACE2_EVENT,GIT_TRACE2_PERF,GIT_TRACE_PACKET,GIT_TRACE_CURLandGIT_TRACE_PERFORMANCE, each set to a file under the run's TMPDIR;trace2.eventTarget,normalTargetandperfTargetin a scratchGIT_CONFIG_GLOBAL;GIT_CONFIG_SYSTEM.Each way was run with
SHAPE_LEGS_TOKEN, withSHAPE_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/configincluded) were searched for the token.trace2-event.json,trace2-normal.txt,trace2-perf.txt,trace-performance.txt: 2 lines each, in the clone, the fetch andfamily.pyalikecfg-event.json,cfg-normal.txt,cfg-perf.txt: 2 lines eachsys-event.json,sys-normal.txt,sys-perf.txt: 2 lines eachWhat the table rests on:
b1e7bb21...) exited 0 and endedsiblings ok: 2 member(s) beside the holder in ..., with the token in neither stdout nor stderr.start(orperformance:) line of the git given the-c, one for each member. Each carries the token twice, once for each spelling the rewrite covers.GIT_TRACE's file was written and held no token, andGIT_TRACE_CURLwrote nothing, with no http remote. An https member is a different case, measured below.GIT_TRACE_PACKET's was still written, by the run's other gits and the member's own bootstrap;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.gitmodulesurl ishttps://github.com/<org>/<repo>.gitis 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:make siblings, a fakeSHAPE_LEGS_TOKEN, and the seven variables set to files, on git 2.43.0 and 2.47.1. Main'ssiblings.py(86bf57e0) held the token intrace.txt,trace2-event.json,trace2-normal.txt,trace2-perf.txtandtrace-performance.txt; this head held it in no file and no output. The clone failed cleanly,git clone exited 128, andmake siblingsexited 2.GIT_TRACE's file holds it too, in therun_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).Still not measured: a github.com clone or fetch that succeeds, since no run here reaches the network.
Tests
tests/test_family_siblings_untraced.pyis new. It usestests/test_family.py'sfamilyandestate,offlinefromtests/test_family_add_refusals.py,files_holdingandCREDENTIALSfromtests/test_family_bootstrap_stops_named.py, andNO_CREDENTIALandneeds_makefromtests/test_siblings_family_name.py. No assert joins conditions withand. The token is the fakew1055-a-token-no-forge-takes.The rows. Each entry point is crossed with each way traces are asked for:
make siblingscloning,make siblingsagain fetching (after a first run with no token and no trace), andfamily.py siblingscloning;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:siblings ok: 2 member(s) ...;.git/config;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 okand the same trace files written. It also asserts the trace2 event file holdssiblings.py's own git at each member, read from the events:git clone -q --no-local ...to each member's slot, orgit fetch --quiet originin each member's clone.Fails on main. The file was run against main's
siblings.py(sha25686bf57e0..., 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.txtandtrace2-perf.txt;the global-config rows found it in
cfg-event.json,cfg-normal.txtandcfg-perf.txt;the system-config rows found it in
sys-event.json,sys-normal.txtandsys-perf.txt.test_an_https_member_with_a_token_in_use_is_in_no_trace: 3 rows, one for each ofCALLERS_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.pysha256b776c69b...: this head's code; it differs from the head only in two docstring lines;bootstrap.pysha25650a16751..., main's, unchanged here.All are killed.
untraced()'s environment alwaysGIT_TRACE2_EVENTnot set to0(TRACE2_OFF)GIT_TRACE2not set to0GIT_TRACE2_PERFnot set to0GIT_TRACE*removal dropped (env = dict(os.environ))GIT_TRACE_PERFORMANCE)GIT_TRACE2M5 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 to0whatever the prefix does. With an https remoteGIT_TRACEwrites it too, in therun_command: git remote-httpslines; that is what the row of Round 2 holds, and the mutant R4 below, which keeps the caller'sGIT_TRACE, passed all 27 earlier rows.Runs
How every run was made:
/usr/bin/python3 -m pytest -q -rs -p no:cacheprovider -o tmp_path_retention_policy=failed, one basetemp per run;LANES_*cleared, a stubghfirst on PATH,GH_TOKEN=bogus, an emptyGH_CONFIG_DIR;EMAIL=service@opensoft.dev,GIT_SSH_COMMAND=false,GIT_CONFIG_GLOBAL=/dev/nullat the start of the session.test_family_siblings_untraced.py, on this change before the docstring fixsiblings.pytest_family_siblings_untraced.pyWhat the 41 gate files are:
test_family_member_make_jobserver.py;grep -l siblings tests/names;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;test_repo_hygiene.py,test_agents_md_codes.pyandtest_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.pysha256b1e7bb21...and the new test file sha2567ba7a416..., at main00f6d7f7(this branch's base; no rebase was needed, see Merges). The runner'sSTARTline readdirty=2, the two tracked files this commit changes, and a sha256 check after the runs foundsiblings.pyunchanged (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:
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;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:
test_shape_doctor.py(the PowerShell line is printed, andpowershell.exeexists, only on a Windows host) and 1 intest_family_member_make_jobserver.py(make -j2here writes--jobserver-auth=3,4, not GNU make 4.4's named pipe);test_doctor_legs_members_next.py(this git crashes onsubmodule init, exit 139, as git 2.43 does on a barebranch), 4 in the same file (this git dies on ashallowof -2**31, as git 2.43 does), and 2 intest_no_auto_maintenance.py(the session began withGIT_CONFIG_GLOBALset, and its file includes that one);The full suite was not run.
Static checks:
siblings.pyonly the three lines it finds at main (two E501, one E127, each moved down by the added lines);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-treeof the first headc3cf5154against (round 2's are in its own section):6c861635: clean, exit 0, tree225b2a99.00f6d7f7:9cbba9d5(scripts/family.py and tests:family.py init --dry-run's help says its folder answer isos.access's and that it leaves nothing that stays, and a test pins the seed NOTE's GitHub clause (#923, #921) #1063:scripts/family.py,scripts/shape_materialize.py,docs/cli.mdand two tests) and6c861635(adopt-project.py: a--work-dirthat is, or runs through, a symlink round a loop is refused in words that name the link and the loop, not "(File exists)", and a symlink to a file is named as one (#978) #1065:adopt-project.pyand three tests).siblings.py,bootstrap.py,tests/test_update_shape_kept_helper.pyor the new test file, so there was no rebase and the gate runs above stand.gitWindows cannot start for a reason other than 193 that names no file (winerror216, 1260, 11, or 188-202) isgit-not-runnable, exit 2, with the put-git-on-PATH remedy, where each ended in a traceback, exit 1 (#1040) #1062 (fix/1040-windows-start-errors, head957ba038), the one open PR that touches a family-root script this change reads (templates/family-root/scripts/bootstrap.py, with the assembly root's andvalidate-family.py,validate-pins.pyand three tests): clean, exit 0, tree84d4b54a.untraced().bootstrap.pystill definesuntraced(and the five other namessiblings.pytakes from it), andsiblings.pyis byte-identical to this head's (sha256b1e7bb21...)..gitmoduleswith no section asks nothing and says how to remove it, a submodule beside one is named, so is a file'sanswered_paths:row with a/, and an identical drop counts asdrop(#914) #1073, adopt-project.py and family.py: a source this user cannot enter issource-unreadable, a UTF-8 plan reads its source under the C locale, and a member on a branch that is not UTF-8 is put back on it (#918) #1072, tests: five adopt tests fail when the thing they guard is removed: bytes on stdin, a stub gh that keeps its #! line, a cleared GIT_ALLOW_PROTOCOL, each guard of two pairs reached alone, and three name shapes (#916) #1071, adopt-project.py: a bad link thatplan-empty-symlinkorplan-bad-symlink-targetnames is no longer named again byplan-root-blocks-shape-copyorplan-root-holds-pin, so it gets one finding, and the #908 comments say Linux's bound (#979) #1070, validate-family.py:gitmodules-path-unnamedreads the file git reads where the work tree's.gitmodulesis gone, the index's then HEAD's, and its lines put that file back first, so a sectiongit submodule addstaged is no longer committed past a passing gate (#1036) #1069, bump-leg.py: the leg's fetch leaves out every refspec of the leg's own that can bring a tag underorigin/, and one onto the branches' names from another namespace, by the rulefamily.py bumpasks, now one function both tools import, so a branch and a tag of one name no longer fail it and a tag no longer answers for a branch (#1037) #1068, validate-pins.py: a--pin-sourcekey is the longest declared pin before an=that leaves a path, as a--link-sourcekey is since #980, soopen=y=<path>verifies the pinopen=yandopen=y=is refused as naming no path (#1053) #1067, scripts/shape_materialize.py, family.py and adopt-project.py'scheck: a manifest the template's own reader makes no mapping of is told it is a defect in the copy of the standard, naming the template file, andmanifest_invalid's docstring is true for the holder (#1059) #1064, adopt-project.py:check's throwaway repositories are made in the work directory with the source's object format and no hook of this machine's; a path git refuses isplan-path-git-refuses, a repository git cannot make thereadopt-git-unasked(#910) #1031): none touchessiblings.py,tests/test_update_shape_kept_helper.py, or a file the new test imports from (conftest.py,test_family.py,test_family_add_refusals.py,test_family_bootstrap_stops_named.py,test_siblings_family_name.py), read fromgh pr list --state open --json number,headRefName,headRefOid,files.The pinned copy
templates/family-root/scripts/siblings.pyis inFAMILY_COPIED_VERBATIM, so each holder gets oneupstream-changedrow for it, as #1045 gave one forbootstrap.py:bootstrap.pyfrom the family root's bootstrap.py: git's update ended by a signal fails the run by name, and B4 with a token in use is told from fetches redone without a trace, so the token reaches no file (#998, #996) #1045 or later needs nothing more;bootstrap.pywith--accept-localin the same apply is refused,update-accept-local-missing-name, naminguntraced, before anything is written;bootstrap.pywithoutuntraced, and that then applies this change without--accept-local, is not refused:siblings.pyis copied, andmake siblings,parkandresumemeet anImportError. That is the gap of update-shape.pyapply --accept-local scripts/repo_shape.pycan keep an old helper that a newly copied file imports from, andmake bootstrapthen fails with ImportError #536's guard, whichmember_make_environmenthas on main already, and it is update-shape.py:update-accept-local-missing-nameasks only the copies this run's--accept-localnames, so a copy an earlier apply kept (readunchangedafter) is never asked, and the next apply writes asiblings.pythat cannot import from it #1082's.No Windows or macOS run was made, since CI is Linux only (#597).
Round 2: rv1075's review, head
041f7831rv1075, lane 5's independent reviewer, found
c3cf5154READY (#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:
ImportErrorreaches a holder, and two applies in a row make one): answered in the body above, in "Where the helper comes from" and "The pinned copy". The gap is update-shape.pyapply --accept-local scripts/repo_shape.pycan keep an old helper that a newly copied file imports from, andmake bootstrapthen fails with ImportError #536's guard's, it is on main formember_make_environmentalready, and it is update-shape.py:update-accept-local-missing-nameasks only the copies this run's--accept-localnames, so a copy an earlier apply kept (readunchangedafter) is never asked, and the next apply writes asiblings.pythat cannot import from it #1082's; this PR does not changeupdate-shape.py.GIT_TRACEon an https member had no test, and rv1075's mutant R4 passed all 27 rows): answered by the new row below.siblings.py's module docstring,rewrite_environment's docstring and the test file's docstring now say it is given no token in its argv. It inheritsSHAPE_LEGS_TOKENandSHAPE_LEGS_APP_TOKENin its environment, becauserun_member_bootstrappasses noenv, and the shipped one reads neither.rewrite_environment's docstring also names the https measurement.The code of
siblings.pyis unchanged. Its syntax tree with the docstrings taken out is the same atc3cf5154and at041f7831(compared withast); the file's sha256 moved fromb1e7bb21...to19107ad4...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:tests/test_family.py, withIRRS's url in the holder's.gitmodulesgiven ashttps://github.com/InkRouter/IRRS.git, andIRSSstill 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, andNO_PROXYandno_proxyempty, so nothing leaves the host;SHAPE_LEGS_TOKENand the seven trace variables set to files;make siblings, cloning.It asserts:
http.proxy=http://127.0.0.1:9is in the command scope the run's git reads (command_scope_at, which runsgit config --show-origin --listand writes nothing);IRRS: git clone exited 128:on stderr, which is how the head fails (rv1075 measured the same);IRSSwas cloned andIRRSwas not;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_TRACEhas written therun_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(sha25686bf57e0..., put in place and then restored, sha256 checked each time):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.txtandtemp/trace2-perf.txt. The round 1 rows find 4 of those, since a local path leavestemp/trace.txtclean;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'sGIT_TRACEfor the clone and the fetch. Applied alone, then restored and sha256-checked:temp/trace.txtthe one file holding the token;The runs on the pushed head
041f7831(the runner'sSTARTline readdirty=0), through the same runner as round 1: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)git version 2.47.1, printed from the exact path first on PATH)siblings.pysiblings.pytest_update_shape_kept_helper.py,test_pinned_copy_text.py,test_family_makefile_siblings.pyThe full suite was not run. flake8 finds in
siblings.pythe 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 insiblings.py.The merges of round 2. Fetched with
-c gc.auto=0 -c maintenance.auto=falseat about 23:00Z;git merge-tree --write-treeof041f7831against:c13c3985(the family root's bootstrap.py: a git that git's update ran dying of 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's squash, which touchestemplates/family-root/scripts/bootstrap.pyandtests/test_family_bootstrap_stops_named.py, among others): clean, exit 0, tree8ae10d0c. None of the three files this PR changes is among the files main changed since the base00f6d7f7, so there is no rebase. In that treesiblings.pyis byte-identical to this head's,untraced()has the same syntax tree as at the base, andCREDENTIALSandfiles_holding, which the new file imports fromtest_family_bootstrap_stops_named.py, are still defined. The 305-passed run above is on that tree;gitWindows cannot start for a reason other than 193 that names no file (winerror216, 1260, 11, or 188-202) isgit-not-runnable, exit 2, with the put-git-on-PATH remedy, where each ended in a traceback, exit 1 (#1040) #1062 (fix/1040-windows-start-errors, head957ba038): clean, exit 0, treee6ed1ba2. Itsuntraced()has the same syntax tree as main's;check's throwaway repositories are made in the work directory with the source's object format and no hook of this machine's; a path git refuses isplan-path-git-refuses, a repository git cannot make thereadopt-git-unasked(#910) #1031, validate-family.py:gitmodules-path-unnamedreads the file git reads where the work tree's.gitmodulesis gone, the index's then HEAD's, and its lines put that file back first, so a sectiongit submodule addstaged is no longer committed past a passing gate (#1036) #1069, adopt-project.py: a bad link thatplan-empty-symlinkorplan-bad-symlink-targetnames is no longer named again byplan-root-blocks-shape-copyorplan-root-holds-pin, so it gets one finding, and the #908 comments say Linux's bound (#979) #1070, adopt-project.py and family.py: a source this user cannot enter issource-unreadable, a UTF-8 plan reads its source under the C locale, and a member on a branch that is not UTF-8 is put back on it (#918) #1072, adopt-project.py and shape-doctor.py: a.gitmoduleswith no section asks nothing and says how to remove it, a submodule beside one is named, so is a file'sanswered_paths:row with a/, and an identical drop counts asdrop(#914) #1073, test_doctor_gitlink_repairs.py: the row that names both bump-leg commands pinsLANES_LANEblank in its own process, asrun_scriptdoes for the doctor it runs, so a shell that exports a lane no longer puts a--traileron the expectation alone (#1060) #1081, family.py: the by-hand checkout line a failed rollback prints names a branch that is not UTF-8 by its exact bytes, as"$(printf 'caf\351')", not as'caf\udce9', which typed names no branch (#1079) #1084): none touchessiblings.py,tests/test_update_shape_kept_helper.py,bootstrap.py, or a file the new test imports from, read fromgh pr list --state open --json number,headRefName,headRefOid,filesat about 23:00Z. the family root's bootstrap.py: a git that git's update ran dying of 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 (fix/1056-signal-anywhere, head73906948) was open when the runs were made, and merged clean with this head then (tree6ae3e65f); it has landed since asc13c3985, above.Round 3: the landing gate's failure, head
962ae43cThe failure. Lane 5's landing gate (log
c1075.log, #1075 issuecomment-6304830183) failed the https row of round 2 at041f7831: 1 failed, 356 passed. The change insiblings.pywas not at fault; the row depended on the caller's git config.GIT_CONFIG_GLOBAL=/dev/nullnorGIT_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, whereGIT_CONFIG_GLOBALis unset). The operator's~/.gitconfighasurl.git@github.com:.insteadOf=https://github.com/.insteadOfprefixes of the same length, and the config files are read before the command line, so that rewrite beat the token's-crewrite 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.gitprintedgit@github.com:InkRouter/IRRS.git.https://github.com/InkRouter/IRRS.gitwent over ssh toInkRouter/IRRS, a real private repository. The dead proxy never applied, the clone succeeded,make siblingsexited 0 and the placed members were both there, and the row failedassert result.returncode == 2.The fix is in
tests/test_family_siblings_untraced.pyonly. No other file changed in this round, andsiblings.pyis byte-identical to041f7831's (sha25619107ad4...).hermetic_configgives 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=1andGIT_SSH_COMMAND=false.conftest.pyand 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_GLOBALset takes git's two user-level files out of the reading, andGIT_CONFIG_NOSYSTEMthe system file whateverGIT_CONFIG_SYSTEMsays (checked on git 2.43.0 and 2.47.1 with scratch files).GIT_SSH_COMMAND=falseis a second wall: a url somehow sent over ssh runsfalse. No mutant can show it needed, since the runner exports it too.url_after_configis a new assert before the clone:git ls-remote --get-url <url>(it applies the config'sinsteadOfand 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.CALLERS_CONFIG:none(round 2's row), andglobalandsystem, each a stand-in for a caller's config that would send the url somewhere reachable.as_the_callers_configwrites 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'sGIT_CONFIG_GLOBAL(the session file includes it) orGIT_CONFIG_SYSTEM. It is built from tmp files only: no network, and never the real~/.gitconfig. The file now has 30 rows.make siblingsexits 2,IRSSis placed andIRRSis 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 honoursL5_GIT_CONFIG_GLOBAL=<scratch file>as the caller's global config, with ssh impossible. The scratch file rewrotehttps://github.com/InkRouter/IRRS.gitto the run's own IRRS remote (afile://url under the run's basetemp). The whole file, git 2.43.0: 1 failed, 27 passed, the https row atassert 0 == 2, the gate's failure reproduced with no network.041f7831's row plus the variants: the new file withhermetic_configand theurl_after_configassert taken out. The https rows, git 2.43.0: 2 failed, 1 passed,[global]and[system]atassert 0 == 2(the clone went to the local remote and exited 0), and[none]passes. On git 2.47.1 the same.[global]fails at the assert, which seesfile:///...IRRS.gitfor the url; the other two pass.GIT_CONFIG_NOSYSTEMdropped and the assert kept:[system]fails at the assert; the other two pass.R4 stays killed. rv1075's R4 (
rewrite_environmentkeeps the caller'sGIT_TRACE), git 2.43.0, the whole file: 3 failed, 27 passed, the three https rows.The runs on the pushed head
962ae43c(STARTreaddirty=0), through the same runner as before:test_update_shape_kept_helper.py, and the trio (test_repo_hygiene.py,test_agents_md_codes.py,test_cli_reference.py)git version 2.47.1, printed from the exact path first on PATH)L5_GIT_CONFIG_GLOBALnaming that scratch file041f7831's file, with the sameThe 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=falseat about 10:53Z;git merge-tree --write-treeof962ae43cagainst:2a119f28(shape-doctor.py: a leg's gitlink that no.gitmodulessection registers, beside one whose sections register other paths or beside none, is named with its own way out, as #914 names one beside a.gitmoduleswith no section (#1080) #1092): clean, exit 0, treeb5ea0114. Main has landed the family root's bootstrap.py: a git that git's update ran dying of 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 (c13c3985) and validate-family.py, validate-pins.py and both bootstrap.py copies: agitWindows cannot start for a reason other than 193 that names no file (winerror216, 1260, 11, or 188-202) isgit-not-runnable, exit 2, with the put-git-on-PATH remedy, where each ended in a traceback, exit 1 (#1040) #1062 (ff0c72eb) since the last round, and has changedtemplates/family-root/scripts/bootstrap.pyandtests/test_family_bootstrap_stops_named.pyamong others, but none of this PR's three files. In that treesiblings.pyis byte-identical to this head's,untraced()has the same syntax tree as at the base00f6d7f7, andCREDENTIALSandfiles_holdingare still defined where the new file imports them from.credential_adviceandended_onsay a member's failed clone ends git's update with 1 before any checkout but a leg's lets it go on, andkilled_saidsays git's update was itself killed, not that it went on (#1085) #1088 (fix/1085-nit-texts, head88af1b7b), which touches the same two files of main's: clean, exit 0, tree365df092, with the same findings.family.py bumpandbump-leg.pyno longer fail where the remote hasxandrelease/xand the repository's owngit fetch originworks (#1066) #1087, shape_materialize.py, adopt-project.py and bump-leg.py: a tag a clone holds without its commit is one it cannot answer for,checksays a commit it cannot ask was not asked, and a pinned product YAML 1.1 or 1.2 reads as no text is quoted (#912) #1090, adopt-project.py:checknames what the clone's.githolds as git keeps refs, and asks a pin's file through the links the root keeps, so a link into.gitor onto a pin's file is refused before any leg or adopted with the copy beside it (#904) #1091, tests.yml: the full suite keeps its temporary files underrunner.temp, not/tmp, so it runs on Ubuntu 26.04 and thetestsjob goes back toubuntu-latest(#1096) #1097): none touchessiblings.py,bootstrap.py,tests/test_update_shape_kept_helper.pyor a file the new test imports from, read fromgh pr list --state open --json number,headRefName,headRefOid,filesat about 10:53Z.Closes #1055
Lane: openreposhape-5 (openRepoShape-5)
🤖 Generated with Claude Code