Skip to content

the family root's bootstrap.py: credential_advice and ended_on say a member's failed clone ends git's update with 1 before any checkout but a leg's lets it go on, and killed_said says git's update was itself killed, not that it went on (#1085) - #1088

Merged
brettheap merged 3 commits into
mainfrom
fix/1085-nit-texts
Oct 23, 2026

Conversation

@brettheap

@brettheap brettheap commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

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

Goal

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

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

What changed

NIT 1: two docstrings, no behaviour

NIT 2: killed_said, one line

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

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

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

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

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

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

Texts that pin those words

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

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

What I read in git's source, and what I did not run

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

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

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

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

Not run:

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

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

The row that fails on main, and passes with the change

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

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

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

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

Mutants

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

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

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

Runs

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

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

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

Merges

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

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

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

The pinned copy

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

Round 2 (rv1088's NIT 1, head 26f49d850)

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

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

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

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

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

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

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

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

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

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

Closes #1085

🤖 Generated with Claude Code

Summary by Sourcery

Correct family bootstrap diagnostics to accurately report Git update termination and continuation behavior.

Bug Fixes:

  • Correct bootstrap status messages so member clone failures and killed updates accurately describe whether Git continued, retried, or stopped before checkout.

Enhancements:

  • Clarify bootstrap documentation for Git’s handling of failed or signal-terminated member and leg clones, including credential and signal interpretation.
  • Add coverage ensuring an update killed after a traced Git signal is not reported as having continued.

Tests:

  • Add a regression test for the corrected killed-update message.

…y a member's failed clone ends git's update with 1 before any checkout but a leg's lets it go on, and `killed_said` says git's update was itself killed, not that it went on (#1085)

rv1076, lane 5's independent review of #1076 (#1056), found two texts in
the family root's `bootstrap.py` that say less than git does.

`credential_advice` said a clone that fails, a member's or a leg's, gives
1 and git's update goes on. That is true of a leg's clone, which the
member's update ends on with 1, and the holder's update goes on past
that 1 (rv996's NIT 2 on #1045). A member's clone that fails a second
time sets `quickstop`, and `update_submodules` ends with 1 before it
checks out any gitlink at that level. `ended_on` now says what that
means for a signal: a member's `git submodule--helper clone` killed by
one is retried, then ends in `quickstop`'s 1, so the update's code is
1 even with nothing after it. With a token in use, where the update is
not traced, that reads as the credential's skip; with none, the trace
names it.

`killed_said` said "git's update went on and ended with exit -9" where
git's trace gave a git's signal and git's update itself was killed. It
did not go on: it says it was itself killed, with its code. Every other
line is as it was, the 134 signal codes' texts included.

Read against git 2.43.0 and 2.47.1 (`builtin/submodule--helper.c`,
`run-command.c`, `trace2/tr2_tgt_event.c`, `git-submodule.sh`), not run:
no network clone was made. One row calls `killed_said` itself, as no run
reaches the line short of the OOM killer taking a checkout and then the
top `git`.

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

sourcery-ai Bot commented Oct 20, 2026

Copy link
Copy Markdown

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

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

@sourcery-ai

sourcery-ai Bot commented Oct 20, 2026

Copy link
Copy Markdown

Reviewer's Guide

Refines family bootstrap’s failure wording to match Git’s submodule update semantics, especially member clone quick-stop behavior and updates killed by signals, with a regression test for the corrected killed_said message.

Sequence diagram for Git submodule clone failure handling

sequenceDiagram
    participant Update as Git update
    participant Clone as git submodule--helper clone
    participant Checkout as Gitlink checkout

    Update->>Clone: clone member
    Clone-->>Update: failure 128 + N
    Update->>Clone: retry clone
    Clone-->>Update: failure 128 + N
    Update->>Update: quickstop returns 1
    Note over Update,Checkout: No gitlink at that level is checked out
Loading

Sequence diagram for leg clone failure continuation

sequenceDiagram
    participant Update as Git update
    participant Member as Member update
    participant Leg as Leg clone
    participant Next as Next gitlink

    Update->>Member: update member
    Member->>Leg: clone leg
    Leg-->>Member: failure
    Member-->>Update: return 1
    Update->>Next: continue update
    Next-->>Update: checkout succeeds
Loading

Flow diagram for killed_said message selection

flowchart TD
    A[Trace signal differs from update return code] --> B{returncode < 0?}
    B -->|Yes| C[Say update was itself killed]
    B -->|No| D[Say update went on and ended with exit code]
    E[Trace signal equals update return code] --> F{returncode < 0?}
    F -->|Yes| G[Say update ended on the signal]
    F -->|No| H[Say update ended on the signal from a child git]
Loading

File-Level Changes

Change Details Files
Corrected bootstrap diagnostics and documentation to distinguish member clone quick-stops, leg clone continuation, and updates killed by signals.
  • Updated credential advice to document member clone retries ending with code 1 before checkout while leg failures let the parent update continue.
  • Clarified ended_on behavior for signal-killed member clones, including traced and credential-token cases.
  • Changed killed_said to report negative update codes as the update being killed rather than continuing.
  • Added focused regression coverage for a traced signal followed by a negatively terminated update.
templates/family-root/scripts/bootstrap.py
tests/test_family_bootstrap_stops_named.py

Assessment against linked issues

Issue Objective Addressed Explanation
#1085 Correct credential_advice and ended_on documentation to describe how failed or signal-killed member clones end the update, distinguish that from a failed leg clone, and explain how the result is reported with and without a token. ✅
#1085 Change killed_said so a negative update return code says the update was itself killed rather than implying it went on, and add a regression test that directly exercises that case. ✅

Tips and commands

Interacting with Sourcery

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

Customizing Your Experience

Access your dashboard to:

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

Getting Help

@brettheap

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-5, 2026-10-20T23:26Z) @codex review

@chatgpt-codex-connector

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

Copy link
Copy Markdown

Codex Review Summary

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

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-20T23:29:56.324357Z 88af1b7 Manual request
ℹ️ About Codex in GitHub

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

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

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

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. What shall we delve into next?

Reviewed commit: 88af1b7b83

ℹ️ About Codex in GitHub

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

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

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

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

@brettheap
brettheap marked this pull request as draft October 21, 2026 10:49
@brettheap

Copy link
Copy Markdown
Contributor Author

Independent review of #1088 (#1085) at 88af1b7b, for lane openRepoShape-5: READY

The head reads 88af1b7b832ec3467f4ab6deec280b2ba95ab0fe: one commit on c13c3985, two files, 55 insertions and 14 deletions. It is READY because the only finding is a NIT in a docstring. All runs went through run9.sh, never more than two at once. I made no pushes, posts or filings.

Findings

NIT 1: ended_on's new sentence says git always retries a killed member clone, and it does not. (templates/family-root/scripts/bootstrap.py:925-930)

  • The text: "git's update retries it, and where the retry fails too ends in quickstop's 1, before any gitlink at that level is checked out ... (read in git 2.43.0's and 2.47.1's submodule--helper.c, not run)".
  • I ran it on both gits, offline, with a remote helper that SIGKILLs the git submodule--helper clone above it ($Q/v118/rv1088/tools/killprobe.sh).
  • Killed on both attempts: "Retry scheduled", then C is cloned, then "a second time, aborting", exit 1, and nothing is checked out. The sentence is true here.
  • Killed only, with the helper's own git clone child left running:
    • The orphaned clone writes members/B/.git, so git prints BUG: submodule considered for cloning, doesn't need cloning any more? and retries nothing.
    • No quickstop follows. The update checks out A, B and C and exits 0, the same on 2.43.0 and 2.47.1.
    • So "retries it" is false whenever the signal reaches the helper but not its git clone, as a kill -9 <pid> of that one process does.
  • Effect: none on behaviour. signal_met already says nothing for an exit of 0, and the credential_advice sentence is worded "fails a second time, git having retried it", so it stays true.
  • Suggested wording: "git's update retries it where it left no .git at the member's path, and where the retry fails too ends in quickstop's 1 ...; where its own git clone lived on and wrote that .git, git retries nothing and goes on (measured, 2.43.0 and 2.47.1: every gitlink checked out, exit 0)." "Not run" can then become "measured".

The writer's leftover is true, so it needs no change here or in a follow-up. For a trace signal with quickstop's positive 1, the line is "git's update went on and ended with exit 1". In my run, after B's first SIGKILL git did go on: it cloned C and retried B, and only then aborted with exit 1. The line makes no claim about checkouts. A follow-up could add "and checked nothing out", but that is optional polish.

Checked and true

1. The docstrings, against git's source

  • 2.47.1 came from $Q/v115/w764/dl/git-2.47.1.tar.gz, extracted to $Q/v118/rv1088/src247. 2.43.0 came from the writer's copies; the helper's sha256 is 9383338c…, as the body says.
  • Every claim holds in both versions:
    • quickstop is set in update_clone_task_finished and update_clone_start_failure;
    • update_submodules checks if (suc.quickstop) { ret = 1; goto cleanup; } before the checkout loop;
    • the loop stops only on 128;
    • the recursion returns ret = run_command(&cp);
    • wait_or_whine gives code += 128, and finish_command passes it to trace2_child_exit;
    • trace2's child_start prepends git for a git_cmd child;
    • die_message returns 128;
    • git-submodule.sh ends on the helper, so its code is passed on.
  • All 17 cited line ranges read as the body says.

2. The writer's two probes, re-run on 2.43.0 and 2.47.1, give the same on both

  • A member's clone that fails: exit 1, then "Retry scheduled" and "a second time, aborting". A and C hold .git only, and B is empty.
  • A leg's clone that fails: exit 1. M and N are both checked out, and L1 holds .git only.

3. Traced, died_in names the killed member clone

  • On both gits the trace's child_start argv is ["git","submodule--helper","clone",…], and traced_in gives Died(9, 137).
  • With a token in use, the code path reads as the docstring says: ended_on(1) gives None, B is the credential's skip, then the advice, and no failure lines. I read this; I did not run bootstrap.

4. killed_said against main

  • AST: with docstrings stripped, killed_said is the only one of 139 top-level nodes that differs.
  • Wide grid:
    • 626,703 pairs: return codes -300 to 599, signals 0 to 99, and seven code shapes.
    • 179,001 differ, which is exactly the set with a negative code that is not the trace's own.
    • Each differs only in "went on and ended with exit N", which becomes "was itself killed (exit N)".
  • The writer's grid, reproduced:
    • 23,814 pairs, of which 4,480 differ;
    • the 134 codes that give the update's own signal: 0 differ;
    • a trace's signal with a code of 0 or more: 0 differ.
  • Other functions: ended_on (-1000 to 999), died_of and signal_met (3,600 cases) differ 0 times. told_past differs in 320 of 1,574 cases, all with a negative code.

5. The new row

  • It has one == assert, with no and or or, in the file's style.
  • With current main 2a119f28's bootstrap.py (sha256 88639e44…): 1 failed (the new row), 48 passed, with the same diff the writer quotes. At the head it passes.

6. Mutants, each applied alone, the whole stops-named file on 2.43

  • M1, M2 and M3 have the same bytes as the writer's (sha256 c9ad7c1f, 534e4c28, b4c68d57).
mutant result
M1: the new branch dropped 1 failed (the new row)
M2: the negative test inverted 7 failed: the new row, both killed-checkout rows, P3, the both-named row, two-members, two-legs
M3: the old wording kept for negative codes 1 failed
R2: exit {died.code} in the killed text 1 failed
R3: exit {-returncode} 1 failed
R4: != changed to > 2 failed (two-members, two-legs)
R5: the went-on text says died.code 6 failed
R1: returncode <= 0 survives (49 passed)

R1 is equivalent: killed_said is reached only through signal_met, which returns None for an exit of 0.

7. Runs at the head

run git result
stops-named file 2.43.0 (/usr/bin/git) 49 passed
test_family.py, test_update_shape_kept_helper.py, test_family_holder_bootstrap_texts.py 2.43.0 328 passed
test_pinned_copy_text.py and the trio 2.43.0 257 passed
stops-named file 2.47.1 ($Q/v115/w764/git247/bin/git; git --version printed git version 2.47.1) 47 passed, 2 skipped (the P3 rows at :837 and :1330)

8. Merges

9. Sonar-style checks and the pinned copy

  • killed_said's cognitive complexity is 5 (main's is 3), against 15.
  • No duplicated blocks.
  • Added lines are at most 76 characters, and ASCII only.
  • flake8 gives the same on head and base: 3 findings in bootstrap.py, 120 F811 in the test file.
  • No import changes.
  • bootstrap.py is in FAMILY_COPIED_VERBATIM (via BOOTSTRAP).
  • The phrases appear only in bootstrap.py and the stops-named file: none in README, docs/ or the handbook.

10. Title and body

Could not verify

  • Not re-run: M4, M5 and M6. The body's "399 passed" included test_template_text_placeholders.py, which I did not run; I ran 4 of its 5 files, 257 passed. "9 open PRs" was true at the writer's time; there are 6 now.
  • The 2.43.0 sources: I did not check where the writer's copies of the four files other than the helper came from, and I did not read the patches in Ubuntu's 2.43.0.
  • Not run: a token-in-use bootstrap run over a killed clone (I only read the code); CI's git 2.55.0; Windows and macOS.

Checklist candidates

  1. Pre-existing, outside this PR, read and not run through bootstrap.
    • Where a member's clone fails twice, git checks out no member at that level. The probes show A and C with .git only, on both gits.
    • But checked_out (bootstrap.py:396) asks only whether the mount holds a .git, so (a) counts those members as checked out. It names only the failed member, as the credential's skip or with its reason.
    • The likely case is a private member with no token: the advice and "What did arrive is bootstrapped below", over members whose work trees are empty.
    • It needs a reproduction before anyone files it.
  2. Optional polish: the quickstop 1 line could also say "and checked nothing out".

Scratch I leave

  • Notes, tools, logs, probes and the extracted 2.47.1 files are in /tmp/claude-1000/-workspace-projects-openRepoShape/50cede29-ab82-4ca3-9f67-d98e1a58c8d2/scratchpad/v118/rv1088/:
    • logs are in runs/, including grid.txt;
    • the probe outputs are in probe/: kfail243, kfail247, kserve243, kserve247, run243 and run247.
  • Basetemps are in /tmp/claude-1000/-workspace-projects-openRepoShape/50cede29-ab82-4ca3-9f67-d98e1a58c8d2/scratchpad/v110/bt/: rv1088-a, rv1088-b, rv1088-mutA and rv1088-mutB.
  • The three worktrees rv1088-head, rv1088-mutA and rv1088-mutB are removed with git worktree remove. The refs refs/l5rv/rv1088-pr1088 and refs/l5rv/rv1088-b1075 are deleted.
  • The shared checkout shows only ?? handoffs.
  • Nothing of mine is running.

…es a killed member clone where it left no `.git`, and where the clone's own `git clone` lived on and wrote one, retries nothing and exits 0 (measured, 2.43.0 and 2.47.1) (#1085)

rv1088, lane 5's independent review of #1088 (#1085), measured offline on
git 2.43.0 and 2.47.1, with a remote helper that kills the member's
`git submodule--helper clone` above it, that the sentence this PR added
to `ended_on` ("git's update retries it") holds only where the clone left
no `.git` at the member's path. Killed on both attempts, git retries,
fails a second time, ends in `quickstop`'s 1 and checks nothing out. Killed
once with its own `git clone` child left running, that child writes
members/B/.git: git prints `BUG: submodule considered for cloning,
doesn't need cloning any more?`, retries nothing, checks out every gitlink
and exits 0. I ran both cases again with rv1088's probe and got the same
on both gits.

The docstring now says each, and "read, not run" is "measured". Docstring
only: no behaviour changes, and `killed_said`'s "went on" line is true as
it is.

Lane: openreposhape-5 (openRepoShape-5)
Co-Authored-By: Claude Sonnet 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>
@brettheap

Copy link
Copy Markdown
Contributor Author

Re-check of #1088 (#1085) at 26f49d85, for lane openRepoShape-5: READY

Round 2 fixes the NIT from round 1: the sentence now says what I measured, on both gits. A third probe also found that the "exits 0" case is not a race. The one new finding is a NIT in the PR body. I re-checked as a delta from 88af1b7b, read-only and offline.

Findings

  • NIT, PR body only, Round 2 section.
    • Claim: a grep for "retries it", "not run", "doesn't need cloning", "never gives that code" and "quickstop" "finds them only in bootstrap.py".
    • For "not run" that is false: the phrase is in 56 files under tests/, README.md, docs/ and AGENTS.md. Every one of those uses is unrelated ("does not run", or "read, not run" about other things). The only hit in the stops-named file is its line 845, about Windows not running a shell script.
    • The four other phrases are found only in bootstrap.py, and the old wording "(read in git 2.43.0's and 2.47.1's submodule--helper.c, not run)" is gone everywhere.
    • So no text pins the old words; only the body's sentence is too broad. It can be left as it is.

Checked and true

  • The delta
    • It is one commit on 88af1b7b8 (no rebase), authored Brett Heap <service@opensoft.dev>, changing only templates/family-root/scripts/bootstrap.py, +8 -4.
    • With docstrings stripped, the code is the same as round 1's, so nothing else changed.
    • Added lines are at most 74 characters and ASCII only.
    • flake8 gives the same 3 findings, 4 lines lower (7, 1172, 2331).
  • The sentence matches my measurements on both gits. Measured offline with my probe at $Q/v118/rv1088/tools/killprobe.sh, on git 2.43.0 and 2.47.1:
    • Clone killed on both attempts: git retries, then quickstop, exit 1, nothing checked out.
    • Clone killed once, its own git clone left running: the BUG: submodule considered for cloning, doesn't need cloning any more? line, no retry, exit 0, and A, B and C all checked out.
  • The "exits 0" case is not a race. I ran it again with the orphaned clone held back 4 seconds before it served (r2/kslow243, r2/kslow247). The result was the same on both gits: exit 0, every gitlink checked out. Git's trace shows why:
    • the killed helper's child_exit 137 came about 4.2 s after its child_start;
    • so git reaps the helper only after the orphan, which holds the helper's stderr pipe, has finished.
  • Runs at 26f49d85, through run9.sh on 2.43.0: the stops-named file, test_pinned_copy_text.py and the trio, 306 passed and 0 failed. That matches the body's 49 for the stops-named file plus 170 for the trio, with 87 for the pinned-copy file.
  • Merge with main 6da757cbe9a4: git merge-tree --write-tree 6da757cb 26f49d85 is clean, tree a74ae589.
  • The body's Round 2 numbers I reproduced

Could not verify

  • The writer's 2.47.1 run (47 passed, 2 skipped): I ran only on 2.43 this round.
  • The writer's own re-runs of my probe: I re-ran the probe myself instead.
  • CI's git 2.55.0, Windows and macOS.
  • The case where the orphaned clone is killed too, which would leave a broken .git. The sentence does not claim it.

Scratch

  • Round 2 notes, logs and probes are in /tmp/claude-1000/-workspace-projects-openRepoShape/50cede29-ab82-4ca3-9f67-d98e1a58c8d2/scratchpad/v118/rv1088/r2/. I added a delay option (RVX_SLEEP) to my own tools/bin/git-remote-rvx.
  • The basetemp /tmp/claude-1000/-workspace-projects-openRepoShape/50cede29-ab82-4ca3-9f67-d98e1a58c8d2/scratchpad/v110/bt/rv1088-a was reused.
  • I removed the worktree rv1088-r2 with git worktree remove and deleted the ref refs/l5rv/rv1088-r2. The shared checkout shows only ?? handoffs.
  • Nothing of mine is running. The run9.sh runs still on the host belong to rv1091, w1017 and w904.

(Posted by lane openRepoShape-5's coordinator. rv1088 is lane 5's independent opus reviewer; it wrote none of #1088. Its body NIT is fixed in the body's Round 2 section. #1088 stays a DRAFT until the v1.2.0 tag.)

Lane: openreposhape-5 (openRepoShape-5)

@brettheap brettheap added this to the v1.2.1 milestone Oct 23, 2026
brettheap added a commit that referenced this pull request Oct 23, 2026
…nt of the commit `--to` names, so a tag above it reads as one that reaches it and a tag below it keeps the plain words, and `--points-at` for `--contains` no longer survives (#1019) (#1107)

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

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

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

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

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

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

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

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

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

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

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

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

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

Review: rv1104-1107, lane 5's independent opus reviewer, which wrote none of #1107: READY at `ee48b5fe`, the head that lands (issuecomment-6318124100), with no MUST or SHOULD and one optional test NIT that needs no round. CI (Brett Heap's ruling 27): H0 is the head ee48b5f itself, which passed tests, tests-git-bash and SonarCloud; main has touched none of its files since its base 7e00834; and the local gate passed on its predicted squash tree 2f7f587 over main 422a590: the bump_leg_reachability_words, family_bump_reachability_words, repo_hygiene, agents_md_codes, cli_reference sets, 237 passed. Landed on Brett Heap's word of 2026-10-23T13:10:26Z, relayed to every lane by lane 3 (#473 issuecomment-6365697123): "merge v1.2.1 PRs once reviewed and green".

Closes #1019

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

Copy link
Copy Markdown

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

sourcery-ai Bot commented Oct 23, 2026

Copy link
Copy Markdown

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

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

@brettheap
brettheap merged commit e223497 into main Oct 23, 2026
5 checks passed
@brettheap
brettheap deleted the fix/1085-nit-texts branch October 23, 2026 19:18
brettheap added a commit that referenced this pull request Nov 8, 2026
…s message spells a path byte that is not UTF-8 as `_shown` does, an escape, and no longer says it names it with a `?` in its place, which #900 replaced (#1018) (#1104)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Closes #1018

Lane: openreposhape-5 (openRepoShape-5)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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