Skip to content

the family root's bootstrap.py and siblings.py: under GNU make 4.4's named-pipe jobserver, a member's make that says it is 4.4 or later is handed the holder's MAKEFLAGS as it is, so it runs its jobs together again; every other make still gets no jobserver and no -j (#987) - #1023

Merged
brettheap merged 6 commits into
mainfrom
fix/987-make-44-fifo-jobserver
Oct 20, 2026

Conversation

@brettheap

@brettheap brettheap commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

Closes #987.

Goal of #987: under the holder's make -jN bootstrap (or validate, park, resume), a member's make runs its jobs together where it can share the holder's jobserver safely, and one at a time, with no warning, everywhere else.

The change is in the family holder's pinned templates/family-root/scripts/bootstrap.py and templates/family-root/scripts/siblings.py, and in tests/test_family_member_make_jobserver.py (and one word of tests/test_update_shape_kept_helper.py's docstrings). The holder's Makefile is unchanged. The commits: 1cc5b21b, the change; bfc98d0f, which has siblings.py import the jobserver function from bootstrap.py instead of carrying a second copy; 5cf60365, GitHub's update-branch merge of main e2f9fc42; and fe1e6bcf, RV1023's fix round: the function's new name, member_make_environment (choice 8), #950's escapes restored in one table, and one more test item; then f00a855f, a merge of main 1a47ed07, which had changed bootstrap.py (#1028's git_not_started, away from the jobserver code). The jobserver code and the other three files are byte for byte as at fe1e6bcf.

The line

#950 (#888, 8264c05) takes the jobserver word and every -j out of the MAKEFLAGS a member's make inherits (member_make_env, in both scripts), wherever MAKEFLAGS names a jobserver. On GNU Make 4.3 that only removed a warning: the member's make ran serially before and after.

GNU make 4.4 and later use a named pipe where mkfifo exists, and write --jobserver-auth=fifo:<path> into MAKEFLAGS. A make opens that pipe by its path, and 4.4's NEWS says "sub-makes will connect to the jobserver even if they are not marked as recursive". So under a 4.4 holder, a member's make probably ran its jobs together before #950, and runs them one at a time after it.

The change: the issue's first direction

member_make_environment(make) (#950's member_make_env, under a new name: choice 8) now hands MAKEFLAGS on as it is, the -jN beside the pipe included, only where all of these hold:

  1. Every jobserver word it names is 4.4's named pipe (names_a_fifo): --jobserver-auth=fifo:<path>, its path read as make reads it (a backslash keeps the next character), absolute, and a named pipe now (stat.S_ISFIFO).
    • Never kept: a pipe's descriptors (3,4), make 3.81's --jobserver-fds, Windows' semaphore, or a -j without its jobserver.
    • A pipe that is gone, a path holding another kind of file, or a path relative to some other directory than the member's would make a 4.4 member warn that it cannot open the jobserver, so each is taken out.
  2. The member's own make says it is GNU make 4.4 or later (reads_fifo).
    • It is asked as <make> --version, the very program the script then runs: $MAKE or make on PATH in bootstrap.py's run_make, make_program() in siblings.py's dispatch. GNU make exports no MAKE and no MAKE_VERSION to a recipe (measured below), so only the member's make can say what it reads.
    • The first line must read GNU Make X.Y with (X, Y) >= (4, 4), read as numbers, and the exit code must be 0.
    • It is asked once per make and per run (FIFO_READERS), and only where condition 1 already holds. So under a 4.3 holder, or with no -j, no extra process runs.
    • The asked make's own MAKEFLAGS has the jobserver taken out, stdin is closed, and stderr is dropped.
    • A make that is not there, names itself otherwise, is older, exits non-zero, or does not answer in VERSION_TIMEOUT (10) seconds is a no.

Everywhere else the jobserver words and every -j are taken out exactly as #950 takes them. #950's reading of MAKEFLAGS is kept unchanged: MAKEFLAGS_WORD, JOBSERVER_WORD, JOBS_WORD, splitting only at a space or a tab, a backslash keeping either in a value. A MAKEFLAGS that names no jobserver is still left byte for byte, and no make is asked anything.

One copy, in bootstrap.py (bfc98d0f, renamed in fe1e6bcf). SonarCloud's quality gate failed at 1cc5b21b on new_duplicated_lines_density, 15.5% over its 3%: one block, the two scripts' identical copies of #950's three readings of MAKEFLAGS and of FIFO_WORD, GNU_MAKE_VERSION, VERSION_TIMEOUT, FIFO_READERS, names_a_fifo and reads_fifo. They are now defined in bootstrap.py only, with the function that uses them, member_make_environment. siblings.py imports it beside REWRITE_TARGETS, credential, member_path and members_of, as it takes the credential and the member rows, and dispatch calls it with the make it runs, as before. This reverses #950's choice 4, at a cost choice 8 states.

Measurements

The only make on this host is /usr/bin/make, GNU Make 4.3 (make --version: GNU Make 4.3 / Built for x86_64-pc-linux-gnu). No GNU make 4.4 or later is installed here, and none was downloaded, built or run. So the 4.4 case is shown only through its MAKEFLAGS spelling, real named pipes made with os.mkfifo, and a fake make that answers --version as each make does, as #950 showed it.

Measured on 4.3:

  • A recipe under make -s -j2 sees MAKEFLAGS=s -j2 --jobserver-auth=3,4, MFLAGS=-s -j2 --jobserver-auth=3,4 and MAKELEVEL=1. It sees no MAKE and no MAKE_VERSION ([unset] for both).
  • 4.3 does not know --jobserver-style=fifo: unrecognized option, exit 2.
  • Given MAKEFLAGS=' -j2 --jobserver-auth=fifo:<path>', 4.3 stops with make[1]: *** internal error: invalid --jobserver-auth string 'fifo:<path>'. Stop., exit 2. That happens whether a named pipe is at the path or not.
  • make --version under that same MAKEFLAGS still prints GNU Make 4.3, exit 0. The question is asked with the jobserver taken out all the same, so a make asked only its version is handed nothing to open.
  • MAKEFLAGS=' -j2 --jobserver-auth=3,4' with the descriptors closed gives the family root's Makefile: under make -jN, every member's make warns jobserver unavailable: using -j1 and runs one job at a time #888's warning and a serial run. MAKEFLAGS=' -j2' alone runs both jobs together, in a pool of the member's own. That is why a -j never goes without its jobserver.
  • An option 4.3 does not know, in MAKEFLAGS (--jobserver-style=fifo), is passed over silently: exit 0, serial run. So a 4.4 holder's other words cannot stop an older member's make.

Read, not run (4.4.1's NEWS): the named-pipe jobserver is 4.4's default where mkfifo(3) is supported. It is selected away with --jobserver-style=pipe. When the pipe-based jobserver closes its descriptors for a command it does not take for a make, it "will now add a new option to the MAKEFLAGS environment variable that disables the jobserver". Its manual reads negative descriptors in --jobserver-auth=R,W as a disabled jobserver. If that is the option's spelling, JOBSERVER_WORD takes it out as before, and it is never a named pipe, so it is never handed on. If it is not, a member's make of 4.4 reads it as its own, and an older one passes it over, as 4.3 passes over --jobserver-style=fifo (measured). GNU/Hurd and Cygwin are reverted to the pipe in 4.4.1.

Tests

tests/test_family_member_make_jobserver.py: 115 items, 114 passed, 1 skipped at fe1e6bcf (113 items at bfc98d0f, 112 at 1cc5b21b, each with 1 skipped). On main's scripts (3864b18c) the file as at 1cc5b21b gives 102 failed, 9 passed, 1 skipped:

New items (72):

Tests at fe1e6bcf, once, GNU Make 4.3, gh stubbed: the jobserver file, test_update_shape_kept_helper.py, test_pinned_copy_text.py, the trio (test_repo_hygiene.py, test_agents_md_codes.py, test_cli_reference.py), test_family_holder_bootstrap_texts.py and test_siblings_inputs.py: 645 passed, 1 skipped, 0 failed (5 min 8 s); the skip is the real-4.4 item. Earlier, at bfc98d0f, a wider set (the family bootstrap and siblings files, test_shape_doctor.py and test_scaffold_leg_path_collisions.py with the jobserver, kept-helper and trio files) gave 1314 passed, 4 skipped, 0 failed; at 1cc5b21b, 1154 passed, 4 skipped. The full suite was not run here. CI's tests passed on 5cf60365. At the merge f00a855f, the same set with #1028's test_bootstrap_without_git.py and test_validate_family_git_not_runnable.py: 735 passed, 1 skipped, 0 failed (4 min 56 s).

Mutants

At 1cc5b21b, with the code in both scripts: 34 mutants, each of the 16 edits of the shared code in each script, and one edit of each call site. Each is one edit of one script, the jobserver file is run against it, and the script's bytes are then put back from a saved copy and checked by sha256 before the next. All 34 killed; every restore matched. Each cell gives the items that failed, of 112 (the other items passed, and the real-4.4 item skipped).

The edit bootstrap.py siblings.py
reads_fifo dropped: a live named pipe is handed on and no make is asked M1: 20 failed M2: 20 failed
never shared, the make still asked: main's answer under the new signature M3: 10 failed M4: 10 failed
any for all: one named pipe among the jobserver words is enough M5: 1 failed M6: 1 failed
os.path.exists for S_ISFIFO M7: 1 failed M8: 1 failed
os.path.isabs dropped M9: 1 failed M10: 1 failed
the pipe's path read without make's backslash rule M11: 3 failed M12: 3 failed
> for >=: 4.4 itself is a no M13: 8 failed M14: 8 failed
the version compared as bytes, so 4.10 reads older than 4.4 M15: 1 failed M16: 1 failed
--version's exit code ignored M17: 1 failed M18: 1 failed
FIFO_READERS bypassed: asked on every call M19: 3 failed M20: 3 failed
--version asked under this run's environment, jobserver and all M21: 17 failed M22: 17 failed
no timeout M23: 1 failed M24: 1 failed
only TimeoutExpired caught: a make that is not there raises M25: 1 failed M26: 1 failed
the make asked before the pipe is checked M27: 14 failed M28: 14 failed
no early return where MAKEFLAGS names no jobserver M29: 6 failed M30: 6 failed
shared, but with every -j taken out M31: 10 failed M32: 10 failed
run_make asks make, not the make it runs M33: 2 failed -
dispatch asks make, not the make it runs - M34: 2 failed

At bfc98d0f, with the code in bootstrap.py only: the 16 edits in siblings.py's copy have no target left (each old text is found 0 times), so the 16 in bootstrap.py and the two call sites were run again, the same way, against the jobserver file's 113 items. All 18 killed; every restore matched. Each of the 16 fails the same items through bootstrap.py as through siblings.py's import, twice round one's count for that edit in one script; each call site's mutant fails only through its own script.

The edit Mutant Items failed Through bootstrap.py Through siblings.py
reads_fifo dropped: a live named pipe is handed on and no make is asked M1 (bootstrap.py) 40 20 20
never shared, the make still asked: main's answer under the new signature M3 (bootstrap.py) 20 10 10
any for all: one named pipe among the jobserver words is enough M5 (bootstrap.py) 2 1 1
os.path.exists for S_ISFIFO M7 (bootstrap.py) 2 1 1
os.path.isabs dropped M9 (bootstrap.py) 2 1 1
the pipe's path read without make's backslash rule M11 (bootstrap.py) 6 3 3
> for >=: 4.4 itself is a no M13 (bootstrap.py) 16 8 8
the version compared as bytes, so 4.10 reads older than 4.4 M15 (bootstrap.py) 2 1 1
--version's exit code ignored M17 (bootstrap.py) 2 1 1
FIFO_READERS bypassed: asked on every call M19 (bootstrap.py) 6 3 3
--version asked under this run's environment, jobserver and all M21 (bootstrap.py) 34 17 17
no timeout M23 (bootstrap.py) 2 1 1
only TimeoutExpired caught: a make that is not there raises M25 (bootstrap.py) 2 1 1
the make asked before the pipe is checked M27 (bootstrap.py) 28 14 14
no early return where MAKEFLAGS names no jobserver M29 (bootstrap.py) 12 6 6
shared, but with every -j taken out M31 (bootstrap.py) 20 10 10
run_make asks make, not the make it runs M33 (bootstrap.py) 2 2 0
dispatch asks make, not the make it runs M34 (siblings.py) 2 0 2

At fe1e6bcf, renamed, the same 18 again, and RV1023's RV1 (a no not cached), against the jobserver file's 115 items. All 19 killed; every restore matched.

The edit Mutant Items failed Through bootstrap.py Through siblings.py
reads_fifo dropped: a live named pipe is handed on and no make is asked M1 (bootstrap.py) 42 21 21
never shared, the make still asked: main's answer under the new signature M3 (bootstrap.py) 20 10 10
any for all: one named pipe among the jobserver words is enough M5 (bootstrap.py) 2 1 1
os.path.exists for S_ISFIFO M7 (bootstrap.py) 2 1 1
os.path.isabs dropped M9 (bootstrap.py) 2 1 1
the pipe's path read without make's backslash rule M11 (bootstrap.py) 6 3 3
> for >=: 4.4 itself is a no M13 (bootstrap.py) 16 8 8
the version compared as bytes, so 4.10 reads older than 4.4 M15 (bootstrap.py) 2 1 1
--version's exit code ignored M17 (bootstrap.py) 2 1 1
FIFO_READERS bypassed: asked on every call M19 (bootstrap.py) 8 4 4
--version asked under this run's environment, jobserver and all M21 (bootstrap.py) 36 18 18
no timeout M23 (bootstrap.py) 2 1 1
only TimeoutExpired caught: a make that is not there raises M25 (bootstrap.py) 2 1 1
the make asked before the pipe is checked M27 (bootstrap.py) 28 14 14
no early return where MAKEFLAGS names no jobserver M29 (bootstrap.py) 12 6 6
shared, but with every -j taken out M31 (bootstrap.py) 20 10 10
run_make asks make, not the make it runs M33 (bootstrap.py) 2 2 0
dispatch asks make, not the make it runs M34 (siblings.py) 2 0 2
RV1023's RV1: a no is not cached, so asked again on every call RV1 (bootstrap.py) 2 1 1

Merges

git merge-tree --write-tree (git 2.43.0), every head read fresh from GitHub at 2026-10-20 10:35Z:

Choices for the reviewer

  1. The first direction, not the second. the family root's bootstrap.py and siblings.py: under GNU make 4.4 and later, a member's make loses the parallelism it may have had through the holder's fifo: jobserver, since #950 takes every jobserver out of MAKEFLAGS #987's goal line asks for parallelism wherever it can be shared safely. Closing it as documentation would give 4.4's parallelism away for good. The cost is one make --version per run, and only under a 4.4 holder's named pipe.
  2. make --version, not .FEATURES. 4.4 lists jobserver-fifo in .FEATURES (its NEWS). Reading that means running the member's make on a makefile; the issue proposes the version, which needs nothing.
  3. 4.4's release candidates (4.3.90 and on) read as 4.3: their pipe is taken out, and the member runs serially, as after the family root's bootstrap.py and siblings.py: a member's make inherits no jobserver from the holder's make -jN, so make -j2 bootstrap, validate, park and resume no longer warn jobserver unavailable of a rule the person does not own (#888) #950. That is the safe side.
  4. The pipe's path is checked, absolute and a named pipe now. Without the check, a stale MAKEFLAGS from a make that has exited would bring a 4.4 member's cannot open jobserver warning, which the family root's bootstrap.py and siblings.py: a member's make inherits no jobserver from the holder's make -jN, so make -j2 bootstrap, validate, park and resume no longer warn jobserver unavailable of a rule the person does not own (#888) #950 took away.
  5. Not checked: whether this user can open the pipe. The holder's make makes it, as this user. A permission check would also read true for root, so it would prove little.
  6. Silent, as the family root's bootstrap.py and siblings.py: a member's make inherits no jobserver from the holder's make -jN, so make -j2 bootstrap, validate, park and resume no longer warn jobserver unavailable of a rule the person does not own (#888) #950 is: nothing says at run time whether a member shared the holder's jobs.
  7. $MAKE is still not set by make. siblings.py's make_program docstring says make exports MAKE; GNU make 4.3 does not (measured above). Left alone here, since it changes no behaviour: make_program still takes $MAKE first where a person sets it.
  8. One copy of the jobserver code, traded against a refusal the family root's bootstrap.py and siblings.py: a member's make inherits no jobserver from the holder's make -jN, so make -j2 bootstrap, validate, park and resume no longer warn jobserver unavailable of a rule the person does not own (#888) #950 declined. siblings.py imports member_make_environment from bootstrap.py, because SonarCloud's duplication gate failed with a copy in each script (1cc5b21b).

Not measured

Pinned copies

templates/family-root/scripts/bootstrap.py and scripts/siblings.py are in FAMILY_COPIED_VERBATIM (scripts/shape_materialize.py:590). Each holder therefore takes two upstream-changed rows, scripts/bootstrap.py and scripts/siblings.py; the Makefile row stays unchanged. Both files keep mode 100755.

Lane: openreposhape-4 (openRepoShape-4)

🤖 Generated with Claude Code

Summary by Sourcery

Enable safe GNU Make 4.4 named-pipe jobserver sharing for compatible family members while retaining silent serial execution everywhere else.

New Features:

  • Allow GNU Make 4.4+ member builds to share a valid named-pipe jobserver with the family holder when both makes support it.

Bug Fixes:

  • Preserve serial, warning-free member builds for older or incompatible makes and invalid, unavailable, or unsupported jobservers.
  • Prevent member makes from inheriting unusable jobserver flags or standalone job counts.

Enhancements:

  • Centralize member make environment handling in bootstrap.py and reuse it from siblings.py.
  • Detect member make capabilities safely with cached, timeout-bounded version checks and validate named-pipe jobserver paths before sharing them.

Documentation:

  • Update user-facing script and test documentation to describe conditional GNU Make 4.4 named-pipe jobserver sharing.

Tests:

  • Expand family member jobserver coverage for named-pipe sharing, compatibility fallbacks, validation, caching, timeouts, and both script entry points.

…named-pipe jobserver, a member's make that says it is 4.4 or later is handed the holder's `MAKEFLAGS` as it is, so it runs its jobs together again; every other make still gets no jobserver and no `-j` (#987)

#987, the follow-up of #950 (#888, 8264c05). #950's `member_make_env`
takes every jobserver word and every `-j` out of the `MAKEFLAGS` a member's
make inherits. Under GNU make 4.4 and later, whose jobserver is a named
pipe (`--jobserver-auth=fifo:<path>`) a make opens by its path, that cost
the member's make the parallelism it probably had.

`member_make_env(make)` now hands `MAKEFLAGS` on as it is, the `-jN` beside
the pipe included, where every jobserver word it names is that named pipe,
at an absolute path where a named pipe is now (`names_a_fifo`), and the
member's own make says `GNU Make X.Y` with X.Y at least 4.4 on `make
--version`, exit 0, within 10 seconds (`reads_fifo`), asked once per make
and per run, with no jobserver in its own `MAKEFLAGS`, and only where such a
pipe is named. Everywhere else the jobserver and its `-j` are taken out as
#950 takes them, with #950's word splitting kept. `run_make` and `dispatch`
pass the make they run. The two scripts carry the same code text; neither
imports from the other.

Measured on GNU Make 4.3 (the only make on this host): given the fifo word
it stops, `invalid --jobserver-auth string`, exit 2, so it is never handed
one. No GNU make 4.4 here: the 4.4 case is shown through its `MAKEFLAGS`
spelling, a real named pipe and a fake `make` answering `--version`.

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

sourcery-ai Bot commented Oct 19, 2026

Copy link
Copy Markdown

Reviewer's Guide

Updates both family-root member-make launchers to safely share GNU Make 4.4+ named-pipe jobservers when both holder and member conditions are satisfied, while stripping jobserver state everywhere else, with extensive tests for parsing, version detection, caching, failure handling, and end-to-end workflow behavior.

Sequence diagram for safe member make jobserver sharing

sequenceDiagram
    participant HolderMake
    participant Launcher as bootstrap.py_or_siblings.py
    participant MemberMake

    HolderMake->>Launcher: invoke member make with MAKEFLAGS
    Launcher->>Launcher: parse MAKEFLAGS jobserver words
    alt every jobserver word is an existing absolute named pipe
        Launcher->>MemberMake: --version with jobserver removed
        alt MemberMake is GNU Make 4.4+
            Launcher->>MemberMake: run target with original MAKEFLAGS and -jN
            MemberMake->>HolderMake: connect to named-pipe jobserver
        else older, invalid, unavailable, or timed out
            Launcher->>MemberMake: run target with jobserver and -j removed
        end
    else descriptors, semaphore, missing pipe, or no valid named pipe
        Launcher->>MemberMake: run target with jobserver and -j removed
    end
Loading

Flow diagram for member MAKEFLAGS handling

flowchart TD
    A["member_make_env(make)"] --> B[Parse MAKEFLAGS]
    B --> C{Any jobserver word?}
    C -->|No| D[Keep MAKEFLAGS byte for byte]
    C -->|Yes| E{Every jobserver word names an existing absolute FIFO?}
    E -->|No| F[Remove jobserver words and -j words]
    E -->|Yes| G[Run make --version once per make]
    G --> H{GNU Make version >= 4.4 and exit code 0?}
    H -->|Yes| I[Keep original MAKEFLAGS and -jN]
    H -->|No| F
Loading

File-Level Changes

Change Details Files
Conditionally preserve GNU Make 4.4+ named-pipe jobserver settings for compatible member makes while retaining the serial, warning-free fallback elsewhere.
  • Detect and validate absolute, existing FIFO jobserver paths using make-compatible escaping rules.
  • Probe each member make once per run with --version, requiring GNU Make 4.4 or newer, with a sanitized environment, timeout, and failure handling.
  • Pass the original MAKEFLAGS including -jN only when every jobserver is a valid FIFO and the member make supports it; otherwise remove jobserver words and associated -j options.
  • Apply the same implementation independently to both pinned family-root scripts and update their call sites to pass the selected make program.
templates/family-root/scripts/bootstrap.py
templates/family-root/scripts/siblings.py
Expand jobserver coverage to verify named-pipe sharing, compatibility gating, fallback behavior, and consistency across both scripts and holder workflows. tests/test_family_member_make_jobserver.py

Assessment against linked issues

Issue Objective Addressed Explanation
#987 Under a GNU make 4.4-or-later holder using a named-pipe jobserver, preserve the holder's jobserver and its associated -j option for a member's make when that make is itself GNU make 4.4 or later, so the member can share parallelism. ✅
#987 For older or unverified member makes, and for jobservers that cannot safely be shared, remove the jobserver and its associated -j options so the member runs serially without jobserver warnings or incompatible fifo errors; retain the established MAKEFLAGS parsing behavior and keep both scripts consistent. ✅

Possibly linked issues


Tips and commands

Interacting with Sourcery

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

Customizing Your Experience

Access your dashboard to:

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

Getting Help

…rap.py, as it takes the credential and the member rows, so the reading of `MAKEFLAGS` and of a member's make is written once and SonarCloud's duplication gate passes (#987)

SonarCloud's quality gate failed on #1023 at 1cc5b21:
`new_duplicated_lines_density` 15.5%, over its 3%, one block, siblings.py
1033-1081 against bootstrap.py 1416-1478: #950's three readings of
`MAKEFLAGS` and #987's `FIFO_WORD`, `GNU_MAKE_VERSION`, `VERSION_TIMEOUT`,
`FIFO_READERS`, `names_a_fifo` and `reads_fifo`, carried in both scripts.

siblings.py now imports `member_make_env` from bootstrap.py beside
`REWRITE_TARGETS`, `credential`, `member_path` and `members_of`, and
defines none of those names itself; bootstrap.py is unchanged. What a
member's make is handed is the same: `dispatch` calls the one function
with the make it runs.

This reverses #950's choice to copy the code into siblings.py. Its reason
was a holder that keeps an older bootstrap.py with `update-shape.py apply
--accept-local`, whose `make park` would then end in an `ImportError`.
That apply is now refused before a byte is written, as
`update-accept-local-missing-name` (#536, #604): `scripts/bootstrap.py
(kept) lacks member_make_env, which scripts/siblings.py imports from it`
(tests/test_update_shape_kept_helper.py, `both-ways`). The guard reads
names, not signatures: a kept bootstrap.py from #950 (8264c05,
2026-10-19, in no tag) defines a `member_make_env` that takes no make, and
only a holder cut from main since then can keep one.

tests/test_family_member_make_jobserver.py: siblings.py's
`member_make_env` is bootstrap.py's, so each siblings.py item runs the one
function through the import; the too-slow items set bootstrap.py's
`VERSION_TIMEOUT`, which that function reads.
tests/test_update_shape_kept_helper.py: siblings.py takes five names from
bootstrap.py, not four.

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

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-4, Tue Oct 20 00:50:13 UTC 2026)

New head bfc98d0f, a fast-forward on 1cc5b21b. SonarCloud's quality gate failed at 1cc5b21b on new_duplicated_lines_density, 15.5% over its 3%: one block, the jobserver code siblings.py carried as a second copy of bootstrap.py's. siblings.py now imports member_make_env from bootstrap.py, beside REWRITE_TARGETS, credential, member_path and members_of, and defines none of that code; bootstrap.py is unchanged, and what a member's make is handed is the same. This reverses #950's choice 4; update-accept-local-missing-name (#536, #604) now refuses the apply that choice guarded against, a holder keeping an older bootstrap.py (the body's choice 8).

At bfc98d0f: mutants 18 of 18 killed, each of the 16 in the shared code through both scripts; the targeted run 1314 passed, 4 skipped, 0 failed. The body is updated.

Lane: openreposhape-4 (openRepoShape-4)

@brettheap

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-4 RV1023, 2026-10-20T05:43:38Z)

Independent review of #1023 at 5cf60365: READY, subject to CI tests passing (still running when this was posted)

Reviewed head 5cf60365ea764ae1a381450dc7ab18bffa72fa29 (read fresh from gh pr view 1023 --json headRefOid): GitHub's update-branch merge of main e2f9fc424 into bfc98d0f, on 1cc5b21b. I wrote none of it. git diff origin/main 5cf60365 --stat names exactly the four files in scope: templates/family-root/scripts/bootstrap.py (+106/-34), templates/family-root/scripts/siblings.py (+18/-35), tests/test_family_member_make_jobserver.py (+435/-19) and tests/test_update_shape_kept_helper.py (+1/-1). Read first: #987 and its comment, #950's body (choice 4 and "Pinned copies"), this body (choices 1-8), the diff, both test files, AGENTS.md.

No MUST. Three SHOULDs and three NITs, below. CI tests on this head had not finished when this was posted.

Items

SHOULD 1 (choice 8): rename the shared function, so the guard refuses the one kept bootstrap.py that still breaks make park. templates/family-root/scripts/siblings.py:216-217 imports member_make_env and :1191 calls it as member_make_env(make); templates/family-root/scripts/bootstrap.py:1478 defines member_make_env(make). Every bootstrap.py from #950 (8264c05) to main today (e2f9fc424, 5b41d44f) defines member_make_env() with no parameter. update-accept-local-missing-name reads names, not signatures, so it lets that kept copy through. Measured (probe G3): a holder cut with family.py init at main e2f9fc424 that commits its own edit to scripts/bootstrap.py and runs this head's update-shape.py apply --accept-local scripts/bootstrap.py --at 5cf60365:

  • apply exits 0, writes scripts/siblings.py and the pin, and its validators say family ok;
  • the holder's own make park then stops at the first member: TypeError: member_make_env() takes 0 positional arguments but 1 was given, from siblings.py line 1191, then make: *** [Makefile:90: park] Error 1, and exits 2. It does this after --- IRRS: make park ---, before any member's make runs, with no per-member report. This was measured through the suite's own family/holder fixtures with both members cloned beside the holder; the control, the head's own two scripts, exits 0.

The same holder against a probe commit that only renames the function (member_make_env to member_make_environment at its 4 sites in each script, a8ec26eb in my scratch upstream; probe G4) is refused before a byte is written: scripts/bootstrap.py (kept) lacks member_make_environment, which scripts/siblings.py imports from it, apply exit 2.

Why SHOULD and not MUST:

  • 8264c05 is in no tag (git tag --contains lists none; the newest tag, v1.1.1, is 50178305), so no released holder can reach the TypeError. Only a holder cut from main since 2026-10-19 that keeps an edited bootstrap.py can.
  • The failure is loud, and it comes before any member's verb runs.

The rename is about eight identifier edits plus the tests. Without it, the comment at siblings.py:205-215 ("the import is not a dependency this holder can lose") is false for exactly this holder.

SHOULD 2 (choice 8's account of #950, body and bfc98d0f's message): say what the dedupe costs released holders. The body says #950 copied the code "so that a holder keeping an older bootstrap.py ... would not meet an ImportError", and that "that apply is now refused". #950's choice 4 gave the refusal itself as its first reason: "A new name imported from there would make update-shape.py apply --accept-local refuse a holder that keeps its own bootstrap.py (update-accept-local-missing-name, #536, #604)". Its "Pinned copies" adds: "so a holder that keeps its own bootstrap.py with --accept-local is not refused for it". The guard predates #950: bbcfd34d1 (#536) and b9f65be90 (#604), both 2026-10-15. No changed line of update-shape.py from 8264c05 to e2f9fc424 names missing-name, _kept_copies_lacking, _names_imported or _names_defined. What changed is the decision, not the guard. Measured with v1.1.1 holders (family.py init at 50178305), each committing its own edit to scripts/bootstrap.py and running apply --accept-local scripts/bootstrap.py:

So this PR turns a clean apply into a refusal for every released holder (v1.0.0 to v1.1.1) that keeps its own bootstrap.py. Such a holder must now merge bootstrap.py by hand to take the new siblings.py. That is the trade #950 declined. The guard's own docstring (_refuse_kept_copies_missing_names) records #507 avoiding a new import into siblings.py in the same way. It fails safe, before any write and with a remedy, so it is acceptable as a decision. But the lander should read it as one: the SonarCloud duplication gate against this refusal. Choice 8 should say it in those words. The squash message should not carry bfc98d0f's "Its reason was ... an ImportError. That apply is now refused" as the whole story.

SHOULD 3: restore the four Python escapes in TAKEN_OUT, tests/test_family_member_make_jobserver.py:442-445. 1cc5b21b turned #950's \u00a0 and \u202f escapes into raw U+00A0 and U+202F characters (head: 2 raw U+00A0 and 2 raw U+202F, 0 escapes; e2f9fc424 and 3864b18c: 0 raw, 4 escapes). The body does not mention it. Today the bytes test the same thing. But #950's round 1 built these spellings from escapes on purpose, and the raw characters are invisible in review. If anything ever normalises them to ASCII spaces, the two items still pass and stop guarding #950's MUST 1, because " ".join rebuilds the expected value exactly. Measured in-process with #950's pre-round-1 splitter (?:\\.|\S)+:

  • with the head's U+00A0, the item fails, as it should;
  • with an ASCII space in its place, it passes.

NIT 1 (test gap, from my mutant RV1): nothing pins that a make answering "no" is remembered. bootstrap.py:1473 caches every answer in FIFO_READERS, but test_each_script_asks_a_make_once_a_run_and_hands_it_no_jobserver (line 606) asks only a make that says 4.4.1. RV1 returns False without caching and survives: 112 passed, 1 skipped. The scenario: a member's make that hangs on --version would cost VERSION_TIMEOUT (10 s) per member instead of once per run, and no test would say so. One more item, three calls with a too-slow or a 4.3 make, would pin it.

NIT 2 (body, stale since the update-branch):

NIT 3 (squash message): 1cc5b21b's message ends "The two scripts carry the same code text; neither imports from the other." That is false at this head. The explicit squash message should take bfc98d0f's account (with SHOULD 2's correction) instead.

Choice 8, ruled

Correctness against #987 and make 4.4's fifo jobserver

  • Call sites. run_make (bootstrap.py:1384) and dispatch (siblings.py:1191) are the only places the family root starts a member's make. grep -n '\[make,' finds those two and reads_fifo's own [make, "--version"]. Both pass the make they run: $MAKE or make on PATH in run_make, and make_program()'s resolved path in dispatch.
  • 4.4.1's source, read and not run (mirror/make at tag 4.4.1, src/posixos.c), confirms the body's reading of NEWS:
    • jobserver_get_invalid_auth returns NULL for a fifo jobserver, so a non-recursive child such as the holder's bootstrap.py rule keeps the fifo: word. For a pipe jobserver it returns --jobserver-auth=-2,-2, which JOBSERVER_WORD takes out and which is never a fifo. That settles the body's "if that is the option's spelling".
    • jobserver_parse_auth's fifo: branch opens the path O_RDONLY and then O_WRONLY, and reports an error if either fails. So choice 4's absolute-path, named-pipe-now check is what keeps a stale path quiet.
  • Measured on this host's GNU Make 4.3:
    • MAKELEVEL=1 make --version and MAKEFLAGS=w MAKELEVEL=1 make --version still print GNU Make 4.3 on the first line, so a sub-make's directory lines do not defeat GNU_MAKE_VERSION.match.
    • make -s -I '/tmp/a b$c\d' writes -I/tmp/a\ b$c\\d into MAKEFLAGS. Only a blank and a backslash are escaped, which is what names_a_fifo undoes.
    • MAKEFLAGS=p prints # GNU Make 4.3, which .match reads as no: serial, the safe side, and only under make -p. My RV7 (.search) survives for that reason. Not a finding.
  • The other choices. Choices 1 to 7 hold as written.

Tests discriminate

Every run used /usr/bin/python3 -m pytest -q -p no:cacheprovider -o tmp_path_retention_policy=failed on targeted files only (never the full suite), with:

  • a stub gh first on PATH (exit 97), GH_TOKEN=bogus and an empty GH_CONFIG_DIR;

  • GIT_SSH_COMMAND, GIT_ALLOW_PROTOCOL, MAKEFLAGS and LANES_* unset;

  • GNU Make 4.3, git 2.43.0 and Python 3.12.3.

  • Head, three files (test_family_member_make_jobserver.py, test_update_shape_kept_helper.py, test_pinned_copy_text.py): 358 passed, 1 skipped, in 94 s. The skip is the real-4.4 item (make -j2 here writes --jobserver-auth=3,4).

  • The trio plus test_family_holder_bootstrap_texts.py and test_siblings_inputs.py: 285 passed, 0 failed, in 193 s.

  • Main's two scripts (e2f9fc424, byte-identical to 3864b18c for both) put in place in my clone, with the head's test file: 103 failed, 9 passed, 1 skipped of 113.

  • My mutants, none of them the body's 34. Each is one edit of bootstrap.py, which siblings.py imports, so every item runs through both scripts. Each was run against the jobserver file's 113 items, restored and checked by sha256 after each, all 7 restores OK. 5 of 7 killed.

mutant the edit result
RV1 a "no" answer is not cached: asked again on every call survived, 112 passed (NIT 1)
RV2 shared, but MAKEFLAGS rebuilt from its words, not byte for byte killed, 18 failed (4 holder rules, 8 readers, 6 paths)
RV3 reads_fifo catches only OSError: a too-slow make raises TimeoutExpired killed, 2 failed (too-slow x2)
RV4 names_a_fifo without its OSError guard killed, 6 failed (gone x2, the two 4.4-fifo spellings x2)
RV5 threshold 4.3: a 4.3 make is handed the pipe killed, 6 failed, 4 of them the real-4.3 end-to-end items (#987's second verify line)
RV6 JOBS_WORD for JOBSERVER_WORD: a -j alone counts as a jobserver killed, 38 failed
RV7 .search for .match on make --version survived, 112 passed; equivalent but for make -p, the safe side; no finding

Pins and skew

  • Pinned copies. FAMILY_COPIED_VERBATIM (scripts/shape_materialize.py:590) carries both scripts. A holder cut at main e2f9fc424 and checked --at 5cf60365 reads exactly upstream-changed 2 unchanged 8: scripts/bootstrap.py and scripts/siblings.py, with the Makefile unchanged. Both files keep mode 100755.
  • "Five names". test_update_shape_kept_helper.py:560's "five names" is true: REWRITE_TARGETS, credential, member_make_env, member_path and members_of. The test asserts taken <= defined, not a count.
  • Pinned shape copies: AGENTS-shape.md's CONTRACTS_DIR sentence is false for every scaffolded root, and the chain WARNING offers remedies the scaffold lacks (one shape bump before the v1.1.0 tag) #610's skew test (test_pinned_copy_text.py:798, test_no_pinned_script_imports_what_an_older_library_lacks) reads only from repo_shape import, which this PR does not change in either script, so it passes. It does not read from bootstrap import, which is where this PR adds a name newer than every tag's bootstrap.py. Probes G2 and G4 show the guard is what answers for that, and G3 shows where the guard cannot.

The body's claims

  • 113 items. Verified.
  • 70 new items. 113 - 43, and the 30-per-script breakdown adds up.
  • The main-scripts result. Verified as above.
  • Two upstream-changed rows and the 100755 modes. Verified.
  • The mutant tables. Not re-run; my own seven are above.
  • Choice 8's history. Not accurate (SHOULD 2).
  • The merge section. Stale (NIT 2).

CI on 5cf60365

Read at 2026-10-20T05:43:38Z (check-runs for 5cf60365): SonarCloud Code Analysis success, package success, tests-git-bash success. tests is still in progress, started 05:05:13Z. It has not finished, so it is not read here. A failure there would be a finding against this verdict.

Lane: openreposhape-4 (openRepoShape-4)

brettheap added a commit that referenced this pull request Oct 20, 2026
… holding U+202E or another character the launcher refuses, by code point and in the launcher's words, before it prints the value or runs `git clone` (#953) (#1043)

`setup-project.py`'s self-bootstrap refuses an `$OPENREPOSHAPE_REPO` holding a character the launcher refuses (U+202E and the rest of `MISDRAWN_CHARACTERS`). It refuses by code point, in the launcher's words, before it prints the value or hands it to git.

Head `c42afa30`, one commit on origin/main `5b41d44f`. Only `setup-project.py` and a new `tests/test_setup_project_repo_misdrawn.py` change. No flag, no `--help` line and no doc changes.

**`setup-project.py`** (section 0, just above `self_bootstrap`):
- `MISDRAWN_CHARACTERS` is the launcher's list (`openRepoShape:502` at `5b41d44f`), entry for entry, in the package's tuple form. It is a copy, for `checked_value`'s reason: self-bootstrap mode runs before there is a checkout to read the launcher from.
- `misdrawn_characters(value)` names the characters a value holds as `U+XXXX`, in code point order. It is the package's `standard.misdrawn_characters`, and it looks for characters, as `CONTROL_RE` does.
- `checked_repo(value)` asks that first. It refuses with the launcher's `checked_repo` sentence, which names the code points and nothing else of the value. Then it calls `checked_value(..., PATH_RE)`, unchanged, for a leading `-` and a control character.
- `self_bootstrap` reads a non-empty `$OPENREPOSHAPE_REPO` through `checked_repo`. Unset or empty still means the default. The check stays where `--org` is asked, so the order is the same: `--org` first, then the repository, then `openRepoShape setup` is printed, then `mkdtemp` and `git clone`.
- The value's raw bytes are never printed. The refusal goes through `die`, so it is `REFUSED: ...` on stderr with exit 2.

**`tests/test_setup_project_repo_misdrawn.py`**, 22 tests:
- **Parity:** `test_the_misdrawn_table_is_the_launchers_list` reads `MISDRAWN_CHARACTERS=(...)` out of `openRepoShape` and compares the tuple. It also checks that `misdrawn_characters` names all of the launcher tests' `MISDRAWN`, each once and in order. `test_misdrawn_characters_names_the_launchers_and_nothing_else` runs the launcher's `SHARING_ITS_BYTES`. These follow #947's two tests for the package.
- **Direct path, 12 tests:** two runs (`Atlas --org acme`, which is the issue's run, and `--org acme` alone) times six values: U+202E, U+00AD, U+200B, U+E0001 (two, three and four UTF-8 bytes), two characters at once, and a tab before U+202E (the launcher asks for the code point before the control character).
  - Each test runs a lone copy of the file under `call_loggers`' logging git, gh and curl.
  - Each checks the launcher tests' `assert_refused_saying`: exit 2, the launcher's sentence, an empty stdout, and no listed character, escape or `forged` in either stream. So no `E2 80 AE` gets out.
  - The git log must hold exactly one call: the checkout probe.
- **Values that still go on, 7 tests:** the launcher's `NOT_CONTROLS` after `acme/`: e-acute, a no-break space, U+200C and U+200D where a script or an emoji needs them, U+2026, U+FF21 and a subdivision flag. Each is printed in the clone line and handed to `git clone` as before.
- **Package path, 1 test:** a tree with no `.git` (the package's modules under `src/openreposhape/`, plus `setup-project.py`, `scaffold-project.py`, `shape-doctor.py` and `contracts/repository-naming.yaml`). `openRepoShape Atlas --org acme` is run from that tree's `src` with the issue's value. The package prints its one line (`the standard is the checkout this package sits in, <tree>.`) and hands over. The tree's `setup-project.py` refuses: exit 2, the launcher's sentence, no U+202E and no `forged` in either stream, and only the checkout probe in the git log.
- Tests that use the logging stub are skipped on Windows. There `subprocess` starts `git.exe` and never the stub's `.cmd`, so the stub would log nothing. CI is Linux only (#597).
- `setup.sh` has no twin of these tests. Its guard covers control characters and the two separators and no other listed character, by #940's design: the launcher refuses the rest first where it is the way in.

**"No git call", stated exactly.** The refused run makes one git call, `git -C <dir> rev-parse --show-toplevel`. That is `is_shape_checkout`, which runs before `self_bootstrap` on every run past `--help` and `--preflight`. It asks whether the file stands in a checkout, which is how the run knows it is self-bootstrapping at all, and it names nothing of the value. No git call names the value, and there is no clone. Moving the check above the probe would also refuse the value on the developer path, which never reads it. It would also leave `setup-project.py` ordered differently from `setup.sh`, which probes first and guards only on its self-bootstrap path.

These run `$Q/v118/w953/probe.py`. Each case uses a fresh scratch folder, a logging git stub that then runs the real git, a gh that fails, `GH_TOKEN=bogus`, an empty `GH_CONFIG_DIR`, `GIT_SSH_COMMAND=false` and `GIT_CONFIG_GLOBAL=/dev/null`. "Before" is `git archive 5b41d44`; "after" is the worktree. Results are the same under git 2.43.0 and 2.47.1.

| Case | Before (`5b41d44f`) | After |
|---|---|---|
| package, ZIP tree, `Atlas --org acme`, `acme/r[U+202E]forged` | exit 128; `E2 80 AE` on stdout and stderr; git: probe, then `clone --quiet -- acme/r[U+202E]forged /tmp/openreposhape-shape-...` | exit 2; no `E2 80 AE`; git: probe only; stdout is the package's one line |
| direct, `Atlas --org acme`, same value | exit 128; raw bytes in the clone line; probe + clone | exit 2; refusal names `U+202E`; probe only |
| direct, `--org acme`, same value | exit 128; probe + clone | exit 2; probe only |
| direct, `acme/r[U+200B]forged` | exit 128; probe + clone | exit 2; names `U+200B` |
| direct, `acme/r[U+E0001]forged` | exit 128; probe + clone | exit 2; names `U+E0001` |
| direct, `acme/r[U+2026]b` (not listed) | exit 128; probe + clone | the same: probe + clone, exit 128 |

The "after" refusal, from the probe:

```
REFUSED: OPENREPOSHAPE_REPO holds a character a terminal draws as something other than itself (U+202E): one that is invisible, is drawn as another character, ends a line or reorders the text around it. This tool will not put it on a `git clone` command line. Set it to a URL or a path.
```

The new file was run on main's `setup-project.py` (`git show 5b41d44:setup-project.py`, put back byte for byte afterwards): **15 failed, 7 passed**.
- Failed: both parity tests (`AttributeError`: no `MISDRAWN_CHARACTERS`, no `misdrawn_characters`), all 12 direct tests, and the package test.
- Passed: the 7 `NOT_CONTROLS` tests. They pass at main by design, because they guard against a check that is too broad.

Each mutant was applied alone (`$Q/v118/w953/mutate.py`), the new file was run (22 tests), and both files were put back. **All 18 are killed.**

| Mutant | Failed |
|---|---|
| m01 check removed (main's read) | 13 |
| m02a check moved after the `(0) self-bootstrap` line | 13 |
| m02b check moved after the clone line | 13 |
| m03a only the value's first character checked | 15 |
| m03b only the table's first entry checked | 13 |
| m04 only U+202E checked | 8 (U+00AD, U+200B, U+E0001, two-at-once) |
| m05 refusal exits 1 | 13 |
| m06a table drops `E0001` | 4 |
| m06b parity compares the first five entries + table drops `E0001` | 4 (parity's second assert, `SHARING_ITS_BYTES`, both U+E0001 tests) |
| m06c parity compares the first five + table adds `200C` | 2 (`SHARING_ITS_BYTES`, the Persian/U+200C test) |
| m06d parity compares the first five + `2060-206F` narrowed to `206E` | 2 |
| m06e parity uses set inclusion + table drops `E0001` | 4 |
| m07 a range read without its last code point | 15 |
| m08 control check asked before the code points | 2 (both tab-then-U+202E tests) |
| m09 characters named in the value's order | 3 |
| m10 every character past ASCII refused | 7 (every `NOT_CONTROLS` test) |
| m11 refusal quotes the value back | 13 |
| m12 check skipped on a hand-over (`OPENREPOSHAPE_SELF_BOOTSTRAP=1`) | 1 (the package test alone) |

All at `c42afa30`, one pytest process at a time: `/usr/bin/python3 -m pytest -q -p no:cacheprovider -o tmp_path_retention_policy=failed`, one reused basetemp, `EMAIL=service@opensoft.dev`, a stub gh first on PATH, `GH_TOKEN=bogus`, an empty `GH_CONFIG_DIR`, `GIT_SSH_COMMAND=false`, `GIT_CONFIG_GLOBAL=/dev/null`.

| Run | git | Files | Result |
|---|---|---|---|
| h1 | 2.43.0 | `test_setup_project_repo_misdrawn.py` (22), `test_setup_project_ref_parts.py` (27), `test_openreposhape_package.py` (213), `test_readme_windows_quoting_sentence.py` (44), `test_release_install_check_children.py` (30), `test_repo_hygiene.py` (100), `test_agents_md_codes.py` (47), `test_cli_reference.py` (23) | 506 passed in 94.06s |
| h2 | 2.43.0 | `test_setup_project_py.py` | 340 passed in 122.68s |
| h3 | 2.43.0 | `test_setup_sh.py` | 191 passed in 96.62s |
| h4 | 2.47.1 | `test_setup_project_repo_misdrawn.py`, `test_setup_project_ref_parts.py`, `test_setup_project_py.py` | 389 passed in 128.13s |
| h5 | 2.43.0 | `test_no_auto_maintenance.py`, `test_setup_project_dry_run_offers.py`, `test_validator_child_encoding.py`, `test_setup_project_name_edges.py`, `test_setup_project_typed_name.py` | 353 passed, 2 skipped in 73.92s (the two skips: `GIT_CONFIG_GLOBAL` was set) |

No `shell_quoted` call was added (count 0), so the README's and the handbook's Windows-quoting sentences and `NAMED` are unchanged. `setup-project.py` is not a pinned copy. The wheel takes it at build time (`pyproject.toml` `force-include`), and the tests that read its source (`test_setup_project_py.py`, `test_release_install_check_children.py`, `test_repo_hygiene.py`) pass.

- `git merge-tree --write-tree` of `c42afa30` with origin/main `5b41d44f` (fetched with `-c gc.auto=0 -c maintenance.auto=false`) is clean, tree `59de4c261c228274a5cff18aff953211de7002e3`.
- No open PR touches `setup-project.py` or the new test file. Every open PR's head was still merged in, and all ten are clean: #1041 `4a23c6e2`, #1039 `5f924500`, #1038 `fd1de5ab`, #1031 `eea70be1`, #1030 `9b4845bb`, #1028 `0c9eac6c`, #1027 `052f1e24`, #1025 `f83ca618`, #1023 `4d0aa1cf`, #954 `dc7fd7ea`.

- **RV947's companion is left out.** That companion is the package's scaffold run asking the repository even beside a checkout.
  - Once this lands, no package run prints the value or hands it to git. Beside a git checkout, the hand-over takes `setup-project.py`'s developer path, which never reads `$OPENREPOSHAPE_REPO`. `self_bootstrap` and `standard.py` are the only shipped Python that read it. Beside a tree with no `.git`, this check refuses it.
  - The companion would change `standard.prepare`, which `--doctor` and the four tools share. They would then refuse a value they never use. That is the launcher's "checked once, before every run but `--help`" policy, and it is a decision about the package of its own.
  - `src/openreposhape/standard.py` is also open in #1038.
- **Found while measuring, not changed here:** `CONTROL_RE` and `PATH_RE` hold `\x00-\x1f\x7f` only, so a C1 control passes `checked_value`. The launcher and the package refuse C1 (U+0080 to U+009F) in a repository. With `acme/r[U+009B]31mforged`, the clone line prints the raw `C2 9B`, before and after this change. `--into` and `--local-remote-dir` go through the same `PATH_RE` (read in the code, not measured). That would be a follow-up for `checked_value`, which every flag shares.

Review: rv953, lane 5's independent reviewer, not the writer (w953): READY at `c42afa30`, the head that lands (issuecomment-6284626889), with no MUST. It rules #953's Verify met in substance: the refused run's only git call is the checkout probe every run makes, which names nothing of the value, and there is no clone. Its SHOULD is #459's documented C1 known limit (U031, L4-24), left with its test NIT for a follow-up on a ruling. CI (Brett Heap's ruling 27): H0 is the head c42afa3 itself, which passed tests, tests-git-bash and SonarCloud; main has touched none of its files since its base 5b41d44; and the local gate passed on its predicted squash tree e146bb5 over main e656199: the setup_project_repo_misdrawn, repo_hygiene, agents_md_codes, cli_reference sets, 192 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 #953.

Lane: openreposhape-5 (openRepoShape-5)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n siblings.py imports is `member_make_environment`, so `update-shape.py apply` refuses a kept #950-era bootstrap.py rather than letting `make park` end in a `TypeError`, and what the import costs a released holder is said as it is (#987)

RV1023's review of #1023 at 5cf6036 (issuecomment-6284294562).

SHOULD 1. siblings.py imported `member_make_env` from bootstrap.py, and
every bootstrap.py from #950 (8264c05) up to this PR defines a
`member_make_env()` that takes no make. `update-shape.py apply
--accept-local` checks a kept bootstrap.py for the names a written
siblings.py imports, not for their parameters. So a holder cut from main
since #950 that kept its own bootstrap.py was let through, and its `make
park` stopped at the first member with `TypeError: member_make_env() takes
0 positional arguments but 1 was given` (RV1023's probe G3). The function
is now `member_make_environment`, in bootstrap.py and at siblings.py's
import and call, and that holder's apply is refused before a byte is
written, as `update-accept-local-missing-name`, naming it.

SHOULD 2. What the import costs, as it is: every released holder (v1.0.0
to v1.1.1) that keeps its own bootstrap.py through `--accept-local` is
now refused when it takes this siblings.py, and must merge bootstrap.py by
hand first. That is the price of one copy of the jobserver code, traded
against SonarCloud's duplication gate, which failed at 1cc5b21 with two
copies. #950's choice 4 declined that trade, citing this very refusal. The
guard predates #950 (#536 bbcfd34 and #604 b9f65be, both 2026-10-15),
so bfc98d0's message was wrong to give #950's reason as an `ImportError`
the guard had since answered: what changed is the decision, not the guard.
siblings.py's comment and bootstrap.py's docstring say so.

SHOULD 3. 1cc5b21 wrote #950's escapes for U+00A0 and U+202F in
`TAKEN_OUT` as the raw characters. The four lines are main's again, byte
for byte, escapes and all.

NIT 1. `test_each_script_asks_a_make_that_says_no_once_a_run`: a member's
make that says it is GNU make 4.3, asked through three calls, is asked
`--version` once, so a no is cached as a yes is (RV1023's mutant RV1
survived without it).

Lane: openreposhape-4 (openRepoShape-4)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Oct 20, 2026
…hain link before an `=` that leaves a path after it, so the offer the gate prints for a link holding `=` verifies that link when typed as printed (#980) (#1044)

For a declared-unverified chain link whose name holds `=`, the naming gate offers only sources that verify that link's tree when typed as printed. The gate offered `open=y` the source `--link-source open=y=<path>`, and read every `--link-source` key at the value's first `=`: the key was `open`, which no chain names, so the value was a bare PATH for the first link, `open=y` stayed declared-unverified under the same offer, exit 0.

**The issue's direction (a)**, in `scripts/validate-repository-naming.py`. `LinkSources._read_source` reads a `--link-source` key the way `_read_remedy` reads a `--link-remedy` link since #949: the longest text before an `=` that names a link of a chain this run reads, by `pin_product_key`, and leaves a path after it. `__init__` and `no_path` both take the key and the path from it, so the path refused as empty is the one after the key's own `=`. For a chain whose links hold no `=`, every value is read exactly as before: a text holding `=` can only name a link that holds one.

**What (a) keeps from the documented rules, and how each case was checked** (probes below, and one test row or more each):
- A link holding `=`: `open=y=<tree>` verifies `open=y`, as printed and in capitals (`OPEN=Y=<tree>`, by `pin_product_key` as every key is).
- Two links where one is a prefix of the other (`open` and `open=y` in one chain): `open=y=<tree>` is `open=y`'s and `open=<tree>` is `open`'s, in either order. On main both were `open`'s and the later replaced the earlier, so `open=y` read none.
- A path that itself holds `=`: `open=y=<dir>/a=b/tree` is `open=y`'s with that whole path. A text holding `/` names no link, since a link's name is `repo_basename`'d, so an `=` after a `/` never ends the key. A path that begins like a longer link's name is given after `./`: `open=./y=dir` is `open`'s in a chain naming `open=y` too.
- A key that names a link but leaves an empty path: `open=y=` is `PRODUCT=` for `open=y`, refused as `link-source-no-path`, exit 2, as `openXdox=` is (#442). On main it was a bare PATH asked for `openXdox`, a link the person did not name. Where a shorter link leaves a path (`open=y=` with `open` in the chain too), that link is the key, `open` with the path `y=`, as `--link-remedy openXdox=y=` gives `openXdox` the words `y=` where a chain names `openXdox=y`: the longest link that leaves a path wins, and `open=y` leaves none.
- A value that is a real directory spelled like the whole value: `open=y=x` beside a directory `open=y=x` is still `open=y`'s, decided by the chain and not the disk, with `_also_a_directory`'s line; `./open=y=x` asks for the directory, as a bare PATH for the first link.
- The key is read whole, where a remedy's link is stripped and cut at its last `/`: ` open=y=<tree>` and `opensoft/open=y=<tree>` name no link and stay bare PATHs, as on main.

**One difference from `--link-remedy`, deliberately.** `--link-remedy open=y=` is a NOTE (its link falls back to the first `=`, `open`). `--link-source open=y=` is refused, because the #442 rule refuses `PRODUCT=` for every link, and `--link-remedy`'s own `openXdox=` refusal is made by argparse before the chains are known, which is why it cannot catch `open=y=`. `_read_source`'s docstring says so.

**Not (b).** (a) makes the offer the gate already prints true, so the person who types it gets the verification; (b) would only stop offering it. Setup's step (4) is unchanged: it passes the gate its `--pin-source` as an absolute path, with a separator before every `=`, so no text before one names a link.

**Docs.** The module docstring (a new paragraph, "A KEY IS THE LONGEST LINK A VALUE NAMES"), the `LinkSources`, `_link_remedy` and `_read_remedy` docstrings, the `--link-source` help, and `docs/cli.md` regenerated from it. `setup-project.py`'s `as_the_plan_reads_it` docstring stated the first-`=` rule, and now states this one. README, the handbook and AGENTS.md state no rule for how a `--link-source` key is read (README names `--link-source PRODUCT=<path>` only), so they are unchanged. The class docstring records that `validate-pins.py` still ends a `--pin-source` key at the first `=`; no scaffold writes a pin holding one (`--pin` names are `[A-Za-z0-9_.-]`), and it is left as its own change.

Measured from a root scaffolded at main `5b41d44f` (local remotes, no network):
- `update-shape.py check --upstream <this checkout> --at c64e85f` lists `upstream-changed scripts/validate-repository-naming.py`, with `upstream-changed 1  unchanged 10`.
- `update-shape.py apply --yes --branch ...` exits 0, commits `contracts/shape-pin.yaml`, `project.yaml` and the copy, and the project's validators print `pins ok` and `manifest ok`. A fresh `check` prints `unchanged 11`, and the root's own naming gate exits 0.
- The new sha256 of `scripts/validate-repository-naming.py`: `ec167fb0e259f90705db895a1a37f4adebd4cbf03ca2d5ae18f6d9a8df896548` (78664 bytes). No other pinned copy and no template changes.

Run from empty folders, `<D>` a fresh folder, trees as named (`trees/y` declares `openInk`, `trees/x` declares `open=y`; for the chain with `open`, `trees/x2` declares `open` and `trees/o` declares `open=y`). The gate prints an arrow between links; it is written `->` here.

| Probe | At main `5b41d44f` | At head `c64e85fe` |
|---|---|---|
| P1, the issue's row: `--referent-chain openXdox,open=y,openInk --link-source open=y=<D>/trees/y` | `link-source-unreadable ... could not answer for openXdox`, "It was read as a PATH: `open` is not a link", and `open=y` declared-unverified with the same offer; exit 0 | only `openXdox` declared-unverified (no source named it); no `link-source-unreadable`; exit 0 |
| P2, P1 plus `openXdox=<D>/trees/x`, `--explain` | NOTE: the value not used, read as a PATH; `CHAIN openXdox -> open=y -> openInk [declared-unverified]` | `[verified]`, nothing warned or noted |
| P3, chain `openXdox,open,open=y,openInk`, `open=<o>` then `open=y=<y>` | `link-source-unreadable: y=<D>/trees/y (--link-source open=...)`, NOTE `open=<o>` replaced; 2 links declared-unverified | `[verified]`, nothing warned or noted |
| P4, `open=y=` (chain without `open`) | bare PATH `open=y=` asked for `openXdox`; exit 0 | `REFUSED link-source-no-path: --link-source open=y= names no path for open=y`; exit 2 |
| P5, `open=y=` (chain with `open`) | `y= (--link-source open=...) could not answer for open` | the same |
| P6, `open=y=<D>/trees/a=b/y` | read as a PATH, `[declared-unverified]` | `[verified]` |
| P7, `open=y=x` beside a directory `open=y=x` | bare PATH, read as the directory for `openXdox` | `link-source-unreadable: x (--link-source open=y=...) could not answer for open=y`, then "`open=y=x` is also a directory; `--link-source ./open=y=x` reads it as one." |
| P7b, `./open=y=x` | the directory, for `openXdox` | the same |
| P8, chain with `open`: `open=./y=dir`, `open=y=<y>` | `open=y=<y>` read for `open`, replacing `open=./y=dir` | `[verified]` |

Eleven new rows in `tests/test_chain_remedy_callers.py`, section "#980", reusing its helpers (`naming`, `tree`, `gate_warnings`, `UNREACHED`, `GATE_OFFER`, `EQUALS`), with no assert that joins conditions with `and`:
- `test_the_offer_for_a_link_holding_an_equals_verifies_it[as-printed, in-capitals, a-path-holding-an-equals]`: reads the offer the gate prints for `open=y`, types it with a tree, and asserts the one WARNING left is `openXdox`'s, with no `link-source-unreadable` and no NOTE.
- `test_each_link_reads_its_own_source_where_one_name_begins_another[open-first, open=y-first, a-path-that-begins-like-the-longer-link]`
- `test_a_value_that_ends_at_a_link_holding_an_equals_names_no_path`
- `test_a_shorter_link_that_leaves_a_path_is_the_key`
- `test_a_link_holding_an_equals_wins_over_a_directory_spelled_like_it`
- `test_a_key_is_read_whole[a-blank, an-owner]`

**Failing at main.** The test file copied onto a `git archive` of `5b41d44f`: 8 of the 11 fail, each on the defect (the offer row's `open=y` WARNING and `link-source-unreadable`, the prefix rows' `[declared-unverified]`, `open=y=` exiting 0, the keyed reading not winning over the directory). The other 3 pass at main by design: they pin behaviour main already had (`test_a_shorter_link_that_leaves_a_path_is_the_key`, `test_a_key_is_read_whole[a-blank]` and `[an-owner]`), and each kills a mutant below.

**Mutants**, each applied alone to the fixed gate in a scratch repository, then `test_chain_remedy_callers.py`, `test_link_source_chain_defaults.py`, `test_naming_policy.py`, `test_chain_link_fold.py` and `test_chain_pin_sources.py` (400 passed, 4 skipped with no mutant). All 11 killed:

| Mutant | Failed | Killed by (new rows unless named) |
|---|---|---|
| M1 back to `typed.partition("=")` | 8 | the offer rows, the prefix rows, `open=y=`, the directory row |
| M2 the shortest match instead of the longest | 3 | the prefix rows |
| M3 the "leaves a path after it" check dropped | 1 | `test_a_shorter_link_that_leaves_a_path_is_the_key` |
| M4a the `./PATH` rule dropped (`./` stripped from the key) | 3 | the directory row; `test_a_labelled_source_holding_an_equals_is_not_said_to_be_a_path`; `test_naming_policy.py::test_a_link_named_before_the_equals_is_the_key_even_over_a_directory` |
| M4b the key cut at its last `/`, as a remedy's link is | 4 | as M4a, and `test_a_key_is_read_whole[an-owner]` |
| M5 the empty-path fallback dropped | 6 | `open=y=`; `test_a_value_that_names_no_path_is_spelled`; 4 rows of `test_link_source_chain_defaults.py::test_a_link_source_that_names_no_path_is_refused` |
| M6 the fallback at the first `=`, as a remedy's | 1 | `open=y=` |
| M7 `no_path` taken after the first `=` | 1 | `open=y=` |
| M8 the keyed path taken after the first `=` | 7 | the offer rows, the prefix rows, the directory row |
| M10 the key stripped, as a remedy's link is | 1 | `test_a_key_is_read_whole[a-blank]` |
| M12 the key not folded by `pin_product_key` | 56 | `in-capitals` and the prefix rows, and 52 existing rows |

**Runs**, all on head `c64e85fe`, `/usr/bin/python3 -m pytest -q -p no:cacheprovider -o tmp_path_retention_policy=failed`, one reused basetemp per run, with `EMAIL=service@opensoft.dev`, a stub `gh` first on PATH, `GH_TOKEN=bogus`, an empty `GH_CONFIG_DIR`, `GIT_SSH_COMMAND=false` and `GIT_CONFIG_GLOBAL=/dev/null`:
- git 2.43.0 (`/usr/bin/git`), 31 files: the 25 that run the naming gate (`grep -l validate-repository-naming tests/`, the pinned-copy files `test_pinned_copy_text.py`, `test_update_shape_kept_helper.py`, `test_update_shape_unreadable.py` and `test_adopt_reencoded_files.py` among them, and `test_repo_hygiene.py`), the files that scan every script's source (`test_git_output_decoding.py`, `test_shape_advisory.py`, `test_no_auto_maintenance.py`, `test_non_utf8_git_failures.py`), and the trio's other two, `test_agents_md_codes.py` and `test_cli_reference.py`: **2964 passed, 5 skipped** in 1749 s, exit 0.
- git 2.47.1 first on PATH, the 25 naming-gate files: **2680 passed, 3 skipped** in 1181 s, exit 0.

No template changes. `grep` of `tests/` found no fixed digest or `^id:`-style pattern over the gate: the tests that read a root's copy of it (`test_shape_doctor.py`, `test_doctor_validator_verdicts.py`) compare it at run time, and both are in the first run, as are the source scanners above and `test_cli_reference.py`, which holds `docs/cli.md` to the help.

- `git merge-tree --write-tree origin/main HEAD`, origin/main `5b41d44f` (fetched with `-c gc.auto=0 -c maintenance.auto=false`, and `git ls-remote` agrees): clean, tree `82780dc63d7f`, the head's own tree.
- Open pull requests: none of the 9 open (#954, #1023, #1025, #1027, #1028, #1030, #1031, #1038, #1039) touches `scripts/validate-repository-naming.py`, `tests/test_chain_remedy_callers.py`, `setup-project.py` or `docs/cli.md`, so there is no merge-tree to run against them.

Review: rv980, lane 5's independent reviewer, not the writer (w980): READY at `c64e85fe` (issuecomment-6288391970), with no MUST or SHOULD; the landing head only merges main over it. Its NIT-1 (three test rows for the corners of the old first-`=` split) and NIT-2 (a sentence in `validate-pins.py`) go to a follow-up. CI (Brett Heap's ruling 27 and his fast-refresh ruling of 2026-10-15T22:48:35Z): H0 c64e85f passed tests, tests-git-bash and SonarCloud; the head 0f5845b is a clean update-branch merge of main b0babe1, with tests-git-bash and SonarCloud green; and the local gate passed on its predicted squash tree ac9c882 over main b0babe1: the chain_remedy_callers, repo_hygiene, agents_md_codes, cli_reference sets, 233 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 #980.

Lane: openreposhape-5 (openRepoShape-5)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Main touches templates/family-root/scripts/bootstrap.py since this
branch last took it (b0babe1, #1028: `git_not_started`), away from the
jobserver code; git merge-tree was clean.

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

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-4, 2026-10-20T10:42:25Z)

To RV1023: the fix round for your review (issuecomment-6284294562) is fe1e6bcf, and the head is now f00a855f, a merge of main 1a47ed07 (#1028 had changed bootstrap.py's git_not_started; the jobserver code and the other three files are byte for byte as at fe1e6bcf). Item by item:

Tests: at fe1e6bcf, the jobserver, kept-helper and pinned-copy-text files, the trio, test_family_holder_bootstrap_texts.py and test_siblings_inputs.py gave 645 passed, 1 skipped (your 358 + 285, plus the 2 new items). At f00a855f, with #1028's test_bootstrap_without_git.py and test_validate_family_git_not_runnable.py added, they gave 735 passed, 1 skipped. The skip is the real-4.4 item. Mutants at fe1e6bcf: 19 of 19 killed, RV1 included. Each of the 16 shared-code mutants fails as many items through siblings.py's import as through bootstrap.py.

Lane: openreposhape-4 (openRepoShape-4)

@brettheap

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-4 RV1023, 2026-10-20T10:59:58Z)

Re-check of #1023 at f00a855f: READY, subject to CI tests passing on this head (still running when this was posted)

Head. f00a855fef72633a580c2bfb02021f61874e2482, read fresh. It is fe1e6bcf (the fix round on 5cf60365) merged with main 1a47ed07, which is still main as of 10:45Z.

Read first. W987b's note (issuecomment-6289080511), the fix diff 5cf60365..fe1e6bcf, fe1e6bcf's message, the refreshed body, and the lander's draft squash title and body.

Verdict. Every item of my review at 5cf60365 (issuecomment-6284294562) is answered, and each fix is verified below. No new item. No MUST, no SHOULD, no NIT.

My earlier conditional, settled

CI tests on 5cf60365 finished success at 06:39:59Z (run 39372991940).

The merge f00a855f

Item by item

SHOULD 1 (rename): done, and verified with my own probes. I re-ran my G3 probe script against my scratch upstream at f00a855f. Each holder commits its own edit to scripts/bootstrap.py, then runs this head's update-shape.py apply --accept-local scripts/bootstrap.py --at f00a855f:

holder cut at its kept bootstrap.py result
main e2f9fc424 (G3, my earlier state) defines member_make_env() REFUSED update-accept-local-missing-name, exit 2: scripts/bootstrap.py (kept) lacks member_make_environment, which scripts/siblings.py imports from it. "Nothing was written": git status --porcelain empty, HEAD at the holder's own commit, its old siblings.py in place.
main 1a47ed07 defines member_make_env() refused the same way, exit 2, nothing written
v1.1.1 50178305 defines neither name refused the same way, exit 2, nothing written
  • Control. A holder cut at 1a47ed07 that keeps no edit reads upstream-changed 2 unchanged 8 (scripts/bootstrap.py, scripts/siblings.py). apply copies both, family ok, exit 0, and the written scripts/siblings.py --help exits 0.
  • What is left of the old name. git grep -w member_make_env f00a855f finds it only in the two passages that explain the rename: bootstrap.py:1533 and siblings.py:219.

SHOULD 2 (the trade): done. The body's choice 8 and fe1e6bcf's message both say it plainly:

Each claim holds:

  • No tag's bootstrap.py defines member_make_environment (v1.0.0, v1.0.1, v1.1.0 and v1.1.1 all checked), and each tag ships a siblings.py.
  • The v1.1.1 refusal is measured above.
  • The comment at siblings.py:205-219 and the docstring at bootstrap.py:1533-1538 now say the same as the body.

SHOULD 3 (escapes): done, and they are escape text. At f00a855f, tests/test_family_member_make_jobserver.py holds 0 raw U+00A0, 0 raw U+202F and 0 non-ASCII bytes in all. It holds 2 of each backslash-u escape as text. Lines 442-445 hash the same as main 1a47ed07's lines 224-227.

NIT 1 (a cached "no"): done. test_each_script_asks_a_make_that_says_no_once_a_run (2 items) asks a make that says GNU Make 4.3, three calls, one --version. My RV1 (a "no" not cached) is now killed: 2 failed, these two items, one through each script. Before the fix it survived with 112 passed.

NIT 2 (stale body): done. The body names main 1a47ed07 and the 9 open pull requests at the heads it gives. I fetched all 9 at 10:45Z, and the heads match. git merge-tree --write-tree of f00a855f with each one exits 0 with no conflict:

PR head merged tree
#954 fe907c84 5bd89377
#1025 4c0c52d0 15bfb21b
#1031 b83d9c02 3d0bfd65
#1041 c0ccf755 4e5f9105
#1045 b2bbb31c 4690322b
#1046 83b3671b edd733a8
#1050 434a07cb 000583f4
#1051 4a6a55b9 3d9ab4f8
#1052 396668b2 f48a4b94

As the body says, only #954 and #1045 touch either script: the family bootstrap.py. #1025 touches validate-family.py.

NIT 3 (squash text): done. I read w1023land/title-1023.txt and w1023land/squash-body-1023.md in full.

Tests at f00a855f

mutant the edit at 5cf60365 at f00a855f
RV1 a "no" answer not cached survived killed, 2 failed (the new no-once items)
RV2 shared MAKEFLAGS rebuilt from its words killed, 18 killed, 18
RV3 only OSError caught, so a too-slow make raises killed, 2 killed, 2
RV4 names_a_fifo without its OSError guard killed, 6 killed, 6
RV5 threshold 4.3 killed, 6 killed, 8: the 4 real-4.3 end-to-end items, the 2 in-process 4.3 items and the 2 new no-once items
RV6 a -j alone counts as a jobserver killed, 38 killed, 40
RV7 .search for .match survived survived: equivalent except under make -p, where the version line is prefixed # and .match reads a "no" (serial, the safe side); no finding

CI on f00a855f

Read at 2026-10-20T10:59:58Z (check-runs for f00a855f, run 39409665799):

  • SonarCloud Code Analysis: success.
  • package: success.
  • tests-git-bash: success.
  • tests: still in progress (started 10:42:15Z). It has not finished, so it is not read here; on 5cf60365 the same job took 95 minutes. A failure there would be a finding against this verdict.
  • tests on fe1e6bcf (run 39406210182) shows cancelled, superseded by the push of f00a855f.

Lane: openreposhape-4 (openRepoShape-4)

@sourcery-ai

sourcery-ai Bot commented Oct 20, 2026

Copy link
Copy Markdown

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

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

@sonarqubecloud

Copy link
Copy Markdown

@brettheap
brettheap merged commit 9e154d8 into main Oct 20, 2026
5 checks passed
@brettheap
brettheap deleted the fix/987-make-44-fifo-jobserver branch October 20, 2026 12:52
brettheap added a commit that referenced this pull request Oct 20, 2026
…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)

Two stops of git's update in the family root's `scripts/bootstrap.py` still ended `family bootstrap ok` with the credential advice. Both were found by lane 5's review of #954 (issuecomment-6253384652):

- **#998** (checklist candidate 1): git's update ended by a signal. On git 2.43.0, a member's own `.gitmodules` that gives its `code` leg a `branch` with no value crashes `submodule--helper` (signal 11), and the update exits 139.
- **#996** (SHOULD 4): B4 (a member's commit that no branch of its remote holds) with `SHAPE_LEGS_TOKEN` or `SHAPE_LEGS_APP_TOKEN` set. #954 reads B4 from git's trace2 events, and it never traces a run with a token in use.

Both now fail by name, with no advice. Each closes in its own commit:

- `da7b14f8` closes #998;
- `b2bbb31c` closes #996.

**Stacked on #954** (#892, lane 4's), at its head `fe907c84`, and **lands after it**. The branch is `fe907c84` plus these two commits. Until #954 lands, GitHub's diff against main shows #954's changes too.

### #998: a signal (`da7b14f8`)

- **`ended_on(returncode)`** reads the exit code alone, never git's words, as #892 reads 128:
  - `128 + N`, up to `128 + signal.NSIG`: a git that git's update ran died of signal N, and the update went on and passed the code up;
  - Python's `-N`: git itself was killed.
- **Read as before:**
  - 128 is git's `die`. It goes to #892's walk, as it did.
  - A code from `128 + signal.NSIG` up (255, which git exits with where a command of its returns -1) numbers no signal. It reads as before.
- **Where the run then ends:**
  - Where `unfetchable` names rows, or why git updated none, that line stands. This is the case where git 2.43.0 crashes on the holder's own `.gitmodules`, and #768's rows hold it (mutant S7).
  - Otherwise (a) prints:
    ```
      UPDATE KILLED, and not for want of a credential: git's update ended on signal 11 (SIGSEGV, exit 139), 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 11)
    ```
    For `-N` the line ends `(SIGSEGV, exit -11), which killed it.` instead.
- **No credential advice.** `credential_advice` gives none where a signal ended the update. A fetch git cannot make dies with 128, which ends git's update there. So an update that ends with `128 + N` met no failed fetch.

### #996: B4 with a token in use (`b2bbb31c`)

This follows the issue's "Direction (no trace2)".

- **When it runs:** only after git's update exits 128 with a token in use, and only at the first checkout the walk reaches whose store lacks its recorded commit (`commit_held`). The walk ends there either way, so this costs two fetches at most.
- **What it runs** (`refetched`), the two fetches git's update makes there (`run_update_procedure`):
  - `git fetch origin`;
  - then, where the commit is still absent, `git fetch origin <commit>`.
- **How each fetch runs:**
  - with the same `-c url...insteadOf` rewrite in argv as the update;
  - with every `GIT_TRACE*` variable removed;
  - with `GIT_PROTOCOL_FROM_USER=0`. This matches what `git submodule` gives every git it starts. Without it, a plain fetch took the `file` transport that git's update refused (`protocol.file.allow=user`), and it brought a commit git's update could not: the token control row found this.
- **How it is read:**
  - B4 is where the first fetch exits 0 and the commit is still absent after the second.
  - A first fetch that fails is the credential's skip, as now.
  - Where either fetch brings the commit, nothing is said (untold).
- **The token appears in no output and in no file:**
  - git's update and both fetches run in `untraced()`'s environment: every `GIT_TRACE*` variable removed, and trace2's three targets (`GIT_TRACE2`, `GIT_TRACE2_EVENT`, `GIT_TRACE2_PERF`) set to `0`.
  - The `0` also turns off a target that the system or global config names (`trace2.normalTarget`, `trace2.perfTarget`, `trace2.eventTarget`). Git reads those config targets wherever the variable is absent or empty.
  - Before this, with `GIT_TRACE2_EVENT` set by the caller, git's update wrote the token's rewrite into that file. This was measured at #954's head: the probes below, and the control rows failing at base.
  - The token still travels in argv only. No file is written, and what the two fetches print is neither read nor shown.
- **The walk's interface:** it now takes a `Reached` predicate (`fetch_reached`) in place of the trace's work-tree set. With no token, the predicate is the trace's answer, as before. With a token, it is `refetched`.
- **Not done: the issue's optional last paragraph** (advice that names the token in use). It is neither small nor clear:
  - the App token and the PAT need different cures (an installation that does not include the repository, against a PAT without `contents:read`);
  - every token run's advice text would change, while the control rows keep the advice as it is.
  - It is better as its own issue.

**Setup:**
- git 2.43.0 and Python 3.12.3;
- the family fixture of `tests/test_family.py`, in the holder with every member checked out;
- `bootstrap.py --skip-members`;
- a stub `gh`, `GH_TOKEN=bogus`, an empty `GH_CONFIG_DIR`, `GIT_SSH_COMMAND=false`, `GIT_CONFIG_GLOBAL=/dev/null`;
- the token is the fake `w892-a-token-no-forge-takes`.

| state | before | after |
|---|---|---|
| P3: IRRS's own `.gitmodules` gives `code` a bare `branch` | `FETCH INCOMPLETE (git exit 139)`, `error: submodule--helper died of signal 11`, the advice, `family bootstrap ok`, exit 0 | the `UPDATE KILLED ... signal 11 (SIGSEGV, exit 139)` line, `FAILED: git's update (ended on signal 11)`, no advice, exit 1 |
| wrapper `git` answers (a)'s update with 139 | advice, ok, exit 0 | `UPDATE KILLED ... (SIGSEGV, exit 139) ...`, exit 1 |
| wrapper `git` kills itself with SIGSEGV | `(git exit -11)`, advice, ok, exit 0 | `UPDATE KILLED ... (SIGSEGV, exit -11), which killed it.`, exit 1 |
| wrapper `git` answers 128, every member at its pin | advice, ok | advice, ok (unchanged) |
| B4 at IRSS with `SHAPE_LEGS_TOKEN`, then with `SHAPE_LEGS_APP_TOKEN`; the caller sets `GIT_TRACE2_EVENT` and `GIT_TRACE` under TMPDIR | git's own `not our ref` lines, the advice to set the token, ok, exit 0; **the token is in `trace2.json`** | `UPDATE STOPPED, and not for want of a credential: git stopped at members/IRSS, whose fetch reached its remote and did not bring <commit>, ...`, `FAILED: members/IRSS (git stopped at it)`, exit 1; the token is in no file and no output |
| control: token set, the file transport refused, IRSS's pin moved on | advice, ok; **the token is in `trace2.json`** | advice, ok; the token is in no file |

Git's own `git submodule update --init --recursive` exits 139 in P3 and 128 in each B4 and control state.

All rows are in `tests/test_family_bootstrap_stops_named.py`, #954's file. They are written in its style and reuse its helpers (`b4`, `pin_moved_on`, `moved_by_hand`, `unpushed`, `cloning`, `isolated`, `said_after_the_fetch`), and no assert joins conditions with `and`. The file now has 24 tests and 37 rows (25 before).

**#998:**
- `test_p3_a_member_s_gitmodules_git_crashes_on_fails_naming_the_signal`: P3's state. It is skipped where git's own update does not exit 139: 2.43.1 and later refuse the line and die with 128, as `tests/test_doctor_legs_members_next.py` skips a crash.
- `test_an_update_ended_on_a_signal_fails_naming_it[exit-139]` and `[sigsegv]`: the portable row. A wrapper `git` first on PATH runs the real git by its absolute path for everything but (a)'s update, and for that one it exits 139, or kills itself with SIGSEGV (core dumps off).
- `test_a_code_no_signal_gives_is_read_as_before[exit-128]` and `[exit-255]`: controls; the advice and ok.
- The credential control (no credential, pin moved on, exit 128) is #954's `test_the_credential_s_skip_is_as_it_was[pin-moved-on]`, unchanged. It keeps the advice and ok, and it kills S1.

**#996:**
- `test_with_a_credential_in_use_b4_is_named_and_the_token_is_not[SHAPE_LEGS_APP_TOKEN]` and `[SHAPE_LEGS_TOKEN]`: this is `test_with_a_credential_in_use_b4_is_read_as_before` turned round. Each asserts:
  - exit 1, the B4 line and the FAILED line, with no advice;
  - the token absent from stdout, stderr and every file under TMPDIR;
  - every trace the person may ask for is on and is written by the walk's own gits: `GIT_TRACE`, `GIT_TRACE2_EVENT` and `GIT_TRACE_CURL` in the environment, and trace2's normal and perf targets in the global config.
- `test_with_a_credential_in_use_the_credential_s_skip_is_as_it_was[pin-moved-on]` and `[b4]`: a token set, the file transport refused. Each gives the advice and ok, the token is in no file, and git's own update exits 128.
- `test_with_a_credential_in_use_the_two_fetches_are_made_again_untraced`: a wrapper `git` logs the commands bootstrap runs itself, with the token masked. The log reads exactly:
  - the update, with only trace2's three `=0`;
  - `fetch origin`, then `fetch origin <commit>`, each with the token's two `-c`, `GIT_PROTOCOL_FROM_USER=0` and only trace2's three `=0`.
  Every caller trace is on during this row too.
- `test_with_no_credential_the_walk_makes_no_fetch_of_its_own`: with no token, the update runs traced into `TMPDIR/bootstrap-trace-*`, and no fetch is made.
- `test_with_a_credential_in_use_a_commit_a_fetch_brings_is_untold[first-fetch]` and `[second-fetch]`: the wrapper answers the update with 128 having fetched nothing. Each gives the advice and ok.
  - `[first-fetch]`: IRSS's pin is moved to a commit on a branch of its remote, so the first fetch brings it and no second fetch is made.
  - `[second-fetch]`: the commit is at `refs/pins/<commit>`, so only the second fetch brings it.

Wrapper rows are skipped on Windows, where a `#!` script is not run.

**A fix to #954's `unpushed` helper.** It now numbers each commit it makes (`MADE`, a counter for the session). Lane 4's `fe907c84` names the commit after the clone's path. But under `tmp_path_retention_policy=failed`, pytest removes a passed row's directory, and it gives the next row whose name begins with the same 30 characters the same `tmp_path`.
- In the first 22-file run, `[first-fetch]` and `[second-fetch]` both ran in `test_with_a_credential_in_use_0` and made one commit in the same second.
- `[first-fetch]` had pushed that commit to a branch, so `[second-fetch]` failed.
- The same can befall #954's own rows under that policy, but not under CI's default retention, where each row's directory number is new.

**Fails at #954's head.** The new file was run against `bootstrap.py` at `4f97fedb` (sha256 `8282da7a...`, the same bytes at `fe907c84`):

| `bootstrap.py` at | result |
|---|---|
| `4f97fedb` / `fe907c84` (#954) | **10 failed, 27 passed** |
| `da7b14f8` (#998 alone) | **7 failed, 30 passed** (the #996 rows) |
| H0 `b2bbb31c` | **37 passed** (git 2.43.0, three runs); **36 passed, 1 skipped** (git 2.47.1: P3) |

- **The 10 rows that fail at #954's head:**
  - P3;
  - `exit-139` and `sigsegv`;
  - both B4-with-token rows;
  - both token controls, on the token in the caller's trace files;
  - the two-fetches row;
  - both fetch-brings rows.
- **The 3 controls that pass there:** `exit-128`, `exit-255`, and the no-credential row.

Each mutant was applied alone to `b2bbb31c`'s `bootstrap.py` (sha256 `e7372f13...`; the landing head's file, sha256 `50a16751...`, is that change plus #1023's hunks, which change none of the functions the mutants change) and run against the stops-named file, and the bytes were restored and checked by sha256 after each. All are **killed**.

| mutant | result |
|---|---|
| S1 `> 128` made `>= 128` | killed, 23 failed (the 128 controls and every #954 row) |
| S2 the negative `returncode` dropped | killed, 1 failed (`sigsegv`) |
| S3 the advice still printed | killed, 3 failed |
| S4 `ok` still printed (no failure line) | killed, 3 failed |
| S5 the `128 + signal.NSIG` bound dropped | killed, 1 failed (`exit-255`) |
| S6 the `UPDATE KILLED` line dropped | killed, 3 failed |
| S7 the signal said before `unfetchable`'s rows | killed by #768's rows in `test_family_gitmodules_values.py` (3 failed: `branch-no-value`, `index-read`, `head-read`) |
| T1 `GIT_TRACE*` left in the refetch's environment | killed, 6 failed |
| T2 the second fetch skipped | killed, 2 failed |
| T3 a failed first fetch read as B4 | killed, 2 failed (both token controls) |
| T4a the token condition dropped (refetch with no token) | killed, 1 failed (the no-credential row) |
| T4b traced with the token (#954's R10) | killed, 6 failed |
| T5 the `-c` rewrite dropped from the refetch | killed, 2 failed |
| T6 git's update with a token not untraced | killed, 6 failed |
| T7 trace2's targets not set to `0` | killed, 6 failed (the global config's targets) |
| T8 `GIT_PROTOCOL_FROM_USER=0` dropped | killed, 4 failed |
| T9 the store not asked after the first fetch | killed, 1 failed (`[first-fetch]`) |
| T10 the prefix narrowed to `GIT_TRACE2` | killed, 1 failed (the two-fetches row) |
| T11 the store not asked after the second fetch | killed, 1 failed (`[second-fetch]`) |
| R7 (lane 5's, on the refactored predicate) `same_place(checkout) in fetched` made `bool(fetched)` | killed, 1 failed |

All runs used `/usr/bin/python3 -m pytest -q -p no:cacheprovider -o tmp_path_retention_policy=failed`, with one reused basetemp, and this environment:
- `EMAIL=service@opensoft.dev`;
- a stub `gh` first on PATH, `GH_TOKEN=bogus`, an empty `GH_CONFIG_DIR`;
- `GIT_SSH_COMMAND=false`, `GIT_CONFIG_GLOBAL=/dev/null`;
- `LANES_*` and `GIT_ALLOW_PROTOCOL` unset.

| git | files | result |
|---|---|---|
| 2.43.0 | the 22 files below, at `2a0681ef` (this head before the `unpushed` fix, same `bootstrap.py`) | 1 failed, 1638 passed, 2 skipped, in 11:07; the one failure is `[second-fetch]`, fixed by the `unpushed` change |
| 2.43.0 | H0 `b2bbb31c`: the stops-named file + `test_repo_hygiene.py`, `test_agents_md_codes.py`, `test_cli_reference.py`, `test_no_auto_maintenance.py` | 265 passed, 2 skipped |
| 2.47.1 | this head: `test_family_bootstrap_stops_named.py` | 36 passed, 1 skipped (P3) |
| 2.43.0 | after #998 alone: the pinned-copy files and the trio (8 files) | 822 passed, 2 skipped |

**The 22 files:**
- the stops-named file;
- the two that `grep -l 'family-root/scripts/bootstrap' tests/` names: `test_family.py` and `test_update_shape_kept_helper.py`;
- every other file that runs the family root's `bootstrap.py`: `test_family_holder_remedies_compose.py`, `test_family_holder_bootstrap_texts.py`, `test_family_bootstrap_gate_order.py`, `test_family_gitmodules_values.py`, `test_bootstrap_without_git.py`, `test_family_unnamed_gitlink.py`, `test_family_config_appends.py`, `test_family_member_make_jobserver.py`, `test_family_siblings_bootstrap_unreadable.py`, `test_family_head_lost_gitmodules.py`, `test_family_members_not_a_list.py` and `test_family_makefile_siblings.py`;
- the files that pin the file's text: `test_pinned_copy_text.py`, `test_bootstrap_spelling.py`, `test_template_text_placeholders.py` and `test_no_auto_maintenance.py`;
- the trio: `test_repo_hygiene.py`, `test_agents_md_codes.py` and `test_cli_reference.py`.

**Skips and scope:**
- The skips are the environment's: `test_no_auto_maintenance.py` skips 2 rows because the session began with `GIT_CONFIG_GLOBAL` set.
- The full suite was not run.

**Static checks:**
- flake8 finds in `bootstrap.py` only the three lines it found at base: line 7, the E127 in `reason`, and the `--members-make` line in `main`.
- In the test file it finds only the F811s every fixture-importing file has.
- Both files are ASCII apart from the em dash already in `bootstrap.py`, with no raw control characters.

The landing head is `3272d533`: the restack `1fb2fbd4` (`b2bbb31c`'s change on #954's squash `8a057215`) merged with main `eee089ca` by `gh pr update-branch`. The merge is clean, and its tree is `git merge-tree --write-tree`'s own (`642527df`).
- Of main's commits since `8a057215`, only #1023 (`9e154d87`) touches `templates/family-root/scripts/`. The merge brings exactly #1023's `bootstrap.py` hunks into this file (patch-id `5563fd8e` on both sides). This PR's own change is the same before and after it (patch-id `ca48f8b2`), and so is the whole PR against main (`a2dbbd28`).
- #1028 landed as `b0babe18` and is in main, so the conflict with #954's hunk measured at `b2bbb31c` no longer arises.
- rv1045m, lane 5's independent reviewer, delta-checked the merged file READY (#1045 issuecomment-6295632295). The two patches share no function, and neither changes an environment, argv or exit code that the other reads.

`templates/family-root/scripts/bootstrap.py` is in `FAMILY_COPIED_VERBATIM`, so each holder gets one `upstream-changed` row for it, as for #954. No Windows or macOS run was made, since CI is Linux only (#597).

Review: rv996, lane 5's independent reviewer, not the writer (w996): READY at `b2bbb31c` (issuecomment-6288783192), with no MUST or SHOULD; the landing head restacks that change unchanged onto #954's squash (restack3.sh's changed-lines check). Its five NITs and two checklist candidates go to follow-ups. CI (Brett Heap's ruling 27): H0 b2bbb31 passed tests, tests-git-bash and SonarCloud; the head 3272d53 merges #954's final head, then #954's squash 8a05721 (the branch's own side for #954's files), then main, with tests, tests-git-bash and SonarCloud green; and the local gate passed on its predicted squash tree fb7a8df over main 6476c17: the family_bootstrap_stops_named, repo_hygiene, agents_md_codes, cli_reference sets, 207 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 #996
Closes #998

Lane: openreposhape-5 (openRepoShape-5)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Oct 20, 2026
…a signal fails the run by name wherever the update met it, read from git's trace, not only where its code is the update's last; with a token in use that stays a limit (#1056) (#1076)

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

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

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

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

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

### What the run says

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

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

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

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

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

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

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

### Two docstrings corrected

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Closes #1056

Lane: openreposhape-5 (openRepoShape-5)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Oct 21, 2026
… 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)

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

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

`templates/family-root/scripts/siblings.py` gives the credential's `-c url.https://x-access-token:<token>@github.com/.insteadOf=...` rewrite, in argv, to two gits:
- the clone in `place`, for a member with no clone beside the holder;
- the fetch in `place`, for a member whose clone is there.

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

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

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

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

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

The issue left this to the writer, because a holder may keep a `bootstrap.py` older than its `siblings.py` (`update-shape.py apply --accept-local`), and an import of a name that copy lacks would stop `make siblings`, `park` and `resume` with an `ImportError`. The issue's text was written against main `1a47ed07`, where `siblings.py` still kept its own copy of the `MAKEFLAGS` readings. Since then #1023 (`9e154d87`, lane 4's) has decided the question for this pair of files:
- it replaced that copy with an import of `member_make_environment`, because a copy in each script failed SonarCloud's duplication gate;
- it named the price, in the import's comment and its commit: a holder that keeps its own `bootstrap.py` while `apply` writes `siblings.py` is refused, `update-accept-local-missing-name` (#536, #604), before a byte is written, and merges `bootstrap.py` by hand first.

`untraced` takes the same road for the same reasons:
- **One rule.** Which traces a command carrying the token is kept out of has one answer, for `make bootstrap` and `make siblings` alike. A copy here could drift from `bootstrap.py`'s, and the drift would be a token in a file.
- **A kept old `bootstrap.py` is refused in the one-step case, and not in a two-step one.** The guard reads names. `untraced` is new with #996 (#1045), so a kept `bootstrap.py` from before it is refused by that name, where `--accept-local` names it in the same `apply`. Measured with `update-shape.py`'s own readers, in process:
  - `siblings.py` at this head takes `REWRITE_TARGETS`, `credential`, `member_make_environment`, `member_path`, `members_of` and `untraced` from `bootstrap`;
  - the holder's `bootstrap.py` at main `00f6d7f7` lacks none of them;
  - the holder's `bootstrap.py` at `6476c170` (main just before #1045) lacks `untraced` alone, which is the line the refusal names.
- **The one-step case, measured end to end (rv1075).** A holder cut at `6476c170`, and `apply --at c3cf515 --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.
- **Two applies in a row are not refused (rv1075's SHOULD 1, filed as #1082).** A holder whose earlier apply kept a `bootstrap.py` without `untraced` (rv1075 applied `--at 00f6d7f --accept-local scripts/bootstrap.py`: that target's `siblings.py` imports no `untraced`, so there was nothing to refuse), and then an apply at this head without `--accept-local`, gets `siblings.py` copied. `scripts/siblings.py` then fails with `ImportError: cannot import name 'untraced' from 'bootstrap'`, and so do `make siblings`, `park` and `resume`. The cause is the guard in `update-shape.py` (`:4288-4289`): it checks only the files `--accept-local` names in the same run. That is the gap of #536's guard, and it is on main already for #1023's `member_make_environment`; this change puts `untraced` into it and does not widen it. The text above this paragraph said no `ImportError` could reach a holder, which was too strong; #1082 is the follow-up, and it is not done here.
- **No copy, so no test to hold two copies to one answer.** `tests/test_update_shape_kept_helper.py`'s `test_what_siblings_takes_from_the_holders_bootstrap_is_found_defined` already holds that every name `siblings.py` takes is defined in the holder's `bootstrap.py`. Its docstring now says six names, not five.

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

**Each case was run three ways:**
- `make siblings`, which clones both members;
- `make siblings` again, which fetches both;
- `family.py siblings --family-root <holder>` in a fresh layout, which clones both.

**Traces were asked for in one of three ways:**
- the seven variables `GIT_TRACE`, `GIT_TRACE2`, `GIT_TRACE2_EVENT`, `GIT_TRACE2_PERF`, `GIT_TRACE_PACKET`, `GIT_TRACE_CURL` and `GIT_TRACE_PERFORMANCE`, each set to a file under the run's TMPDIR;
- `trace2.eventTarget`, `normalTarget` and `perfTarget` in a scratch `GIT_CONFIG_GLOBAL`;
- the same three in a scratch `GIT_CONFIG_SYSTEM`.

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

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

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

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

**An https remote, measured offline (rv1075, then the row of Round 2).** The rewrite reaches only `github.com`, and the probes above are offline, with local paths. A member whose `.gitmodules` url is `https://github.com/<org>/<repo>.git` is reached by it, and can be tried offline behind a dead proxy, `http.proxy=http://127.0.0.1:9`, so that nothing leaves the host:
- rv1075 set one member's url so, with `make siblings`, a fake `SHAPE_LEGS_TOKEN`, and the seven variables set to files, on git 2.43.0 and 2.47.1. Main's `siblings.py` (`86bf57e0`) held the token in `trace.txt`, `trace2-event.json`, `trace2-normal.txt`, `trace2-perf.txt` and `trace-performance.txt`; this head held it in no file and no output. The clone failed cleanly, `git clone exited 128`, and `make siblings` exited 2.
- `GIT_TRACE`'s file holds it too, in the `run_command: git remote-https origin https://x-access-token:<token>@github.com/...` lines: 3 lines on git 2.43.0, 4 on 2.47.1 (rv1075).
- Round 2 below holds this in a test row.

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

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

**The rows.** Each entry point is crossed with each way traces are asked for:
- entry points: `make siblings` cloning, `make siblings` again fetching (after a first run with no token and no trace), and `family.py siblings` cloning;
- ways: the seven variables, the three targets in a scratch global config (a copy of the session's, which keeps git's automatic maintenance off), and the three targets in a scratch system config.

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

**Fails on main.** The file was run against main's `siblings.py` (sha256 `86bf57e0...`, put in place and then restored, sha256 checked):
- **18 failed, 9 passed**: every row with a token failed, and the 9 controls passed;
- the variables rows found the token in `trace-performance.txt`, `trace2-event.json`, `trace2-normal.txt` and `trace2-perf.txt`;
- the global-config rows found it in `cfg-event.json`, `cfg-normal.txt` and `cfg-perf.txt`;
- the system-config rows found it in `sys-event.json`, `sys-normal.txt` and `sys-perf.txt`.

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

Each mutant was applied alone to the worktree's file and the new test file run (git 2.43.0). The bytes were restored and checked by sha256 after each:
- `siblings.py` sha256 `b776c69b...`: this head's code; it differs from the head only in two docstring lines;
- `bootstrap.py` sha256 `50a16751...`, main's, unchanged here.

All are **killed**.

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

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

**How every run was made:**
- through lane 5's runner: `/usr/bin/python3 -m pytest -q -rs -p no:cacheprovider -o tmp_path_retention_policy=failed`, one basetemp per run;
- `LANES_*` cleared, a stub `gh` first on PATH, `GH_TOKEN=bogus`, an empty `GH_CONFIG_DIR`;
- `EMAIL=service@opensoft.dev`, `GIT_SSH_COMMAND=false`, `GIT_CONFIG_GLOBAL=/dev/null` at the start of the session.

| git | files | result |
|---|---|---|
| 2.43.0 | `test_family_siblings_untraced.py`, on this change before the docstring fix | 27 passed |
| 2.43.0 | the same file against main's `siblings.py` | 18 failed, 9 passed |
| 2.43.0 | gate shard A: 19 of the 41 files below | 1623 passed, 4 skipped, in 19:51 |
| 2.43.0 | gate shard B: the other 22 of the 41 files | 2415 passed, 8 skipped, in 15:03 |
| 2.47.1 | `test_family_siblings_untraced.py` | 27 passed, in 1:14 |

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

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

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

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

The full suite was not run.

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

Fetched with `-c gc.auto=0 -c maintenance.auto=false`, on 2026-10-20 at about 21:03Z. Round 1, `git merge-tree --write-tree` of the first head `c3cf5154` against (round 2's are in its own section):
- **origin/main `6c861635`**: clean, exit 0, tree `225b2a99`.
  - Main has moved two commits past this branch's base `00f6d7f7`: `9cbba9d5` (#1063: `scripts/family.py`, `scripts/shape_materialize.py`, `docs/cli.md` and two tests) and `6c861635` (#1065: `adopt-project.py` and three tests).
  - Neither touches `siblings.py`, `bootstrap.py`, `tests/test_update_shape_kept_helper.py` or the new test file, so there was no rebase and the gate runs above stand.
- **#1062** (`fix/1040-windows-start-errors`, head `957ba038`), the one open PR that touches a family-root script this change reads (`templates/family-root/scripts/bootstrap.py`, with the assembly root's and `validate-family.py`, `validate-pins.py` and three tests): clean, exit 0, tree `84d4b54a`.
  - Its diff does not touch `untraced()`.
  - In the merged tree `bootstrap.py` still defines `untraced` (and the five other names `siblings.py` takes from it), and `siblings.py` is byte-identical to this head's (sha256 `b1e7bb21...`).
- **The other 9 open PRs** (#1073, #1072, #1071, #1070, #1069, #1068, #1067, #1064, #1031): none touches `siblings.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 from `gh pr list --state open --json number,headRefName,headRefOid,files`.

`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`:
- a holder that takes it with `bootstrap.py` from #1045 or later needs nothing more;
- a holder that keeps an older `bootstrap.py` with `--accept-local` in the same apply is refused, `update-accept-local-missing-name`, naming `untraced`, before anything is written;
- a holder whose earlier apply kept a `bootstrap.py` without `untraced`, and that then applies this change without `--accept-local`, is not refused: `siblings.py` is copied, and `make siblings`, `park` and `resume` meet an `ImportError`. That is the gap of #536's guard, which `member_make_environment` has on main already, and it is #1082's.

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

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

**The three findings:**
- **SHOULD 1** (the body said no `ImportError` reaches 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 #536's guard's, it is on main for `member_make_environment` already, and it is #1082's; this PR does not change `update-shape.py`.
- **SHOULD 2** (every row used a local file remote, so a leak through `GIT_TRACE` on an https member had no test, and rv1075's mutant R4 passed all 27 rows): answered by the new row below.
- **NIT** (three places said the member's own bootstrap "is given no token"): `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 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. `rewrite_environment`'s docstring also names the https measurement.

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

**The new row,** `test_an_https_member_with_a_token_in_use_is_in_no_trace` (the file has 28 rows now). Its setup:
- the estate of `tests/test_family.py`, with `IRRS`'s url in the holder's `.gitmodules` given as `https://github.com/InkRouter/IRRS.git`, and `IRSS` still a local path;
- `http.proxy=http://127.0.0.1:9`, a dead proxy, in git's command scope after the entries the session holds, and `NO_PROXY` and `no_proxy` empty, so nothing leaves the host;
- a fake `SHAPE_LEGS_TOKEN` and the seven trace variables set to files;
- `make siblings`, cloning.

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

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

**It fails on main.** The file was run against main's `siblings.py` (sha256 `86bf57e0...`, put in place and then restored, sha256 checked each time):
- git 2.43.0, the whole file: **19 failed, 9 passed**, the 18 rows of round 1 and the new row. The new row fails at `assert files_holding(tmp_path, TOKEN) == []`, with 5 files holding the token: `temp/trace-performance.txt`, `temp/trace.txt`, `temp/trace2-event.json`, `temp/trace2-normal.txt` and `temp/trace2-perf.txt`. The round 1 rows find 4 of those, since a local path leaves `temp/trace.txt` clean;
- git 2.47.1, the row alone: **1 failed**, the same 5 files.

**It kills R4.** R4 is rv1075's mutant, in `rewrite_environment`: `return {**untraced(), **{k: v for k, v in os.environ.items() if k == "GIT_TRACE"}} if token else None`, which keeps the caller's `GIT_TRACE` for the clone and the fetch. Applied alone, then restored and sha256-checked:
- git 2.43.0, the whole file: **1 failed, 27 passed**: the new row alone, with `temp/trace.txt` the one file holding the token;
- git 2.47.1, the row alone: **1 failed**, the same one file.

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

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

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

**The merges of round 2.** Fetched with `-c gc.auto=0 -c maintenance.auto=false` at about 23:00Z; `git merge-tree --write-tree` of `041f7831` against:
- **origin/main `c13c3985`** (#1076's squash, which touches `templates/family-root/scripts/bootstrap.py` and `tests/test_family_bootstrap_stops_named.py`, among others): clean, exit 0, tree `8ae10d0c`. None of the three files this PR changes is among the files main changed since the base `00f6d7f7`, so there is no rebase. In that tree `siblings.py` is byte-identical to this head's, `untraced()` has the same syntax tree as at the base, and `CREDENTIALS` and `files_holding`, which the new file imports from `test_family_bootstrap_stops_named.py`, are still defined. The 305-passed run above is on that tree;
- **#1062** (`fix/1040-windows-start-errors`, head `957ba038`): clean, exit 0, tree `e6ed1ba2`. Its `untraced()` has the same syntax tree as main's;
- **the other open PRs** (#1031, #1069, #1070, #1072, #1073, #1081, #1084): none touches `siblings.py`, `tests/test_update_shape_kept_helper.py`, `bootstrap.py`, or a file the new test imports from, read from `gh pr list --state open --json number,headRefName,headRefOid,files` at about 23:00Z. #1076 (`fix/1056-signal-anywhere`, head `73906948`) was open when the runs were made, and merged clean with this head then (tree `6ae3e65f`); it has landed since as `c13c3985`, above.

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

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

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

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

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

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

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

**The merge-tree of round 3.** Fetched with `-c gc.auto=0 -c maintenance.auto=false` at about 10:53Z; `git merge-tree --write-tree` of `962ae43c` against:
- **origin/main `2a119f28`** (#1092): clean, exit 0, tree `b5ea0114`. Main has landed #1076 (`c13c3985`) and #1062 (`ff0c72eb`) since the last round, and has changed `templates/family-root/scripts/bootstrap.py` and `tests/test_family_bootstrap_stops_named.py` among others, but none of this PR's three files. In that tree `siblings.py` is byte-identical to this head's, `untraced()` has the same syntax tree as at the base `00f6d7f7`, and `CREDENTIALS` and `files_holding` are still defined where the new file imports them from.
- **#1088** (`fix/1085-nit-texts`, head `88af1b7b`), which touches the same two files of main's: clean, exit 0, tree `365df092`, with the same findings.
- **The other open PRs** (#1087, #1090, #1091, #1097): none touches `siblings.py`, `bootstrap.py`, `tests/test_update_shape_kept_helper.py` or a file the new test imports from, read from `gh pr list --state open --json number,headRefName,headRefOid,files` at about 10:53Z.

Lane: openreposhape-5 (openRepoShape-5)

Review: rv1075, lane 5's independent reviewer, not the writers (w1055 and f1055): READY at `c3cf5154` (issuecomment-6302090874) with no MUST; its two SHOULDs and its NIT were taken in one new commit, and the re-check is READY at `041f7831` (issuecomment-6303900180), with no MUST or SHOULD; its one NIT, a merge-tree number in the body, is fixed in the body. CI (Brett Heap's ruling 27): H0 is the head 962ae43 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 fc2afb3 over main 16f9a0e: the family_siblings_untraced, update_shape_kept_helper, repo_hygiene, agents_md_codes, cli_reference sets, 359 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 #1055

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

Labels

None yet

Projects

None yet

1 participant