Skip to content

the family root's bootstrap.py: make's stderr is read a line at a time and the innermost command named, so a member whose make runs another no longer reads exited 2 for a finding, and make -k keeps its Error line (#884) - #886

Merged
brettheap merged 3 commits into
mainfrom
fix/884-run-make-truths
Oct 18, 2026

Conversation

@brettheap

@brettheap brettheap commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor

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

#884's three lines, from rv876's NIT 2, NIT 3 and NIT 4 on #876 (#876 issuecomment-6246717354), with fix round 1 from rv884's review of this pull request (issuecomment-6248806027). The change is in the family holder's pinned templates/family-root/scripts/bootstrap.py and in tests/test_family_holder_bootstrap_texts.py, and nothing else.

Base: origin/main 281df263, where #876 has landed as 41baf920. Each line was probed there first, under the holder's own make bootstrap, with GNU Make 4.3, dash 0.5.12 as /bin/sh and git 2.43.0. The probes reuse rv876's p2/ptyrun.py and its member Makefiles.

1. Five mutants of #876's code survived its tests, and one docstring claimed what no test checked. Tests only; no code changes for this line.

  • R10 and R11 (credential_advice ignoring unreadable or refused): test_no_advice_beside_no_member_updated_with_every_member_checked_out. Every member is checked out, as in the holder itself, and HEAD's .gitmodules is given update = None or a conflict marker. Bootstrap's own stderr lines after the fetch line are asserted exactly: the "NO MEMBER UPDATED, and not for want of a credential" line and the failure, with no advice between them. tests/test_family_gitmodules_values.py asks only the last two lines, which is why the mutants passed there.
  • R1 (named[0]) and R8 (the regex anchored on ^make: ): test_the_line_names_the_code_of_the_member_s_innermost_command[last-under-make]. bootstrap.py --no-fetch runs under a real parent make -k, as the holder's make bootstrap runs it, so the member's make prints make[1]:. One member exits 1. Another has two commands that exit 1 and then 2, and the line must name the last.
  • R7 (passed_on's sink.flush() dropped): test_a_member_s_question_shows_before_its_answer_is_typed. Bootstrap runs on a pseudo-terminal (its stdin, stdout and stderr). A member recipe asks Continue? on stderr and reads the answer. The test types yes only once the question has shown, and fails if it has not shown within 10 s. It asserts the output before the answer and after it exactly. Measured on the pty: the question showed at 0.13 s at head; with R7 on 281df263 it showed only at 8.1 s, after the blind answer, when the run ended.
  • The store: test_a_git_mv_staged_with_work_in_the_checkout_is_undone_as_printed now asserts what its docstring said. After every line is typed, the checkout's commit (the new commit, or the pin where only an untracked file was added) is still in .git/modules/members/IRRS. A helper, kept, is shared with test_a_checkout_moved_on_at_the_variant_is_moved_with_f's own store assert.
  • On 281df263, with rv876's own mut/mutants.py unchanged, every one of the five is caught by the test named above, and those tests pass on the unmutated code:
rv876's mutant on 281df263 caught by
R1 named[0] [last-under-make]
R7 no sink.flush() test_a_member_s_question_shows_before_its_answer_is_typed
R8 ^make: [last-under-make]
R10 advice ignores unreadable test_no_advice_beside_no_member_updated_with_every_member_checked_out[unparsed]
R11 advice ignores refused test_no_advice_beside_no_member_updated_with_every_member_checked_out[value-refused]

2. The final line misled in three cases (run_make, MakeErrors).

  • Measured on 281df263 and on this head, each member under the holder's make bootstrap and with bootstrap.py run straight (-k where the case needs it). The last five rows came from rv884's review:
member 281df263 head
$(MAKE) inner, inner command exits 1 exited 2 exited 1
$(MAKE) inner, inner command exits 2 exited 2 exited 2
$(MAKE) nothere, the inner make's own failure exited 2 make printed no Error <code> line for a command of the member's
$(MAKE) -s nothere; exit 3 exited 3 exited 3
make -k: Error 1, then 5000 bytes on stderr make printed no Error <code> line (false) exited 1
make -k: two commands exit 1, then 3 exited 3 exited 3
a command killed by SIGKILL under dash exited 137 exited 137
kill -TERM $$ in a recipe make printed no Error <code> line the same
$(MAKE) -s nothere || exit 1 exited 1 exited 1
$(MAKE) -k inner, whose second target prints after the inner Error 1 exited 2 exited 1
make -k: a finding, then a command that kill -KILL $$ ends exited 1 exited 1
-$(MAKE) inner (inner exits 1), then the recipe's own exit 2 exited 2 exited 2 (82a84f88 read exited 1)
printf 'x: *** y' >&2; exit 1, so make's line follows it on one line exited 1 exited 1 (82a84f88 named none)
  • A recursing member: the line names the innermost command's code. Why: the inner make exits 2 for any failure, which is the very code the family root's bootstrap.py and validate-family.py: the credential advice prints beside "not for want of a credential", a member's refusal and finding both read make bootstrap exited 2, and the wrong-case move's git rm --cached may refuse #839 set out to see past. The command the member's author wrote, and whose code tells a finding (1) from a refusal (2), is the innermost one.
    • The depth comes from make's own prefix (make[N]:, or make: for 0).
    • An Error 2 at a shallower depth than the last *** line before it, whatever lines come between, is taken for that make's parent reporting it, and is passed over.
    • Where the inner make failed on its own (No rule to make target), no command's code is named, and the line is the one for make's own failure.
    • A command that runs a failing make and then fails itself, with 3 or with 1, is still named.
    • Two failures at the same depth under -k name the last, as before.
    • Where a - let the inner make fail, make prints [Makefile:N: bootstrap] Error 2 (ignored), with no ***. That line now puts the depth back at the parent's, so the parent's own Error 2 after it is named. rv884 offered this as an option, to adopt only if it changed no other case. Measured: across rv884's 133 case runs (its tools/cases1.py to cases6.py, at both depths), head with it differs from 82a84f88 only in that case and in the last-match case below. Across 21 more of mine it differs only in that case, in -$(MAKE) nothere then exit 2, and in the last-match case, each now as 281df263 read it. An inner make that ignores a failure of its own prints the same kind of line at its own, deeper depth; that line changes nothing (measured: $(MAKE) inner; exit 2, where inner ignores an exit 2, reads exited 2).
  • The last match on a line (rv884's NIT 1). A member's own text with no newline, x: *** y, followed by make's Error 1 on the same line, named no code at 82a84f88. said now takes the last match on a line (findall), and that case reads exited 1, as 281df263 read it.
  • Where the line is not the innermost command's code. This is now written in MakeErrors' docstring, not only in this body (rv884's NIT 2):
    • a recipe that runs a make, lets its failure pass in the shell (|| true, or ;), or with a - in a make run with -s (which prints no (ignored) line), and then exits 2 itself: that Error 2 is read as the inner make's report, and the inner command's code is named. Measured: $(MAKE) inner || true; ...; exit 2 and -$(MAKE) inner under the holder's make -s read exited 1; 281df263 read exited 2;
    • an inner make run with env -u MAKELEVEL, which prints its parent's depth: the parent's Error 2 is named, as on 281df263;
    • under -j, a job of the parent's that fails while the inner make is still running: its line comes between, and the parent's Error 2 is named, as on 281df263;
    • a member's own line shaped like a deeper make's (x[3]: *** y) before it exits 2 itself: its Error 2 is passed over.
    • The module docstring's (b) now says the code is the innermost command's "as far as make's lines tell it".
  • More than 4 KiB after the Error line: run_make kept the last 4096 bytes of make's stderr and read that tail at the end. MakeErrors now reads each line as it passes. It keeps only the last 4096 bytes of a line that no newline has ended yet, so memory stays bounded for a progress meter that writes only \r. rv884's tools/chunks.py, pointed at this head, fed 174 captured streams to MakeErrors in 81,406 chunkings; none read differently from the whole.
  • A signal: the docstring is now true of rv884's 24 cases (probe2.log: dash 0.5.12 and bash 5.2.21; a command run with no shell, one command, kill $$, a command then echo, a command then exit $?, echo then a command; SIGKILL and SIGTERM; at both depths).
    • Where the signal kills what make started, make prints its name, Killed or Terminated, and names no code. That is: a command make runs with no shell; the shell a kill -KILL $$ kills; or a command bash runs in its own place, which is a line's last command, a line of one command included.
    • Where the shell outlives the command, the shell prints the signal's name itself, Killed or Terminated, and exits 137 or 143, which make names. dash outlives every command; bash outlives one that is not a line's last.
    • Where the line goes on after the command, the shell exits as the rest of the line does: ; echo after ends the recipe with 0.
    • The line itself was not changed for a signal. The signal's name is printed right above make's Error 137, and a command may also exit 137 by itself.
  • Tests: test_the_line_names_the_code_of_the_member_s_innermost_command, with four sets:
    • [innermost-under-make] and [innermost] run INNERMOST's 11 members, once under a parent make and once with the member's make at depth 0. They are the first five rows of the table, the last five, and an inner make that ignores an exit 2 of its own, then exit 2 (InnerLets, run without -s, which would keep make from saying so);
    • [last-under-make] is line 1's R1 and R8 test;
    • [signal] runs one of each signal case in a form that does not hang on which shell /bin/sh is: sh -c 'kill -KILL $$'; exit $?, which the shell outlives (137), and kill -KILL $$, which kills the shell (no code).
    • Each set asserts the run's final line exactly. Under the parent make, a premise checks that no member line reads make:.
  • test_a_line_that_comes_in_pieces_is_read_whole (the pieces test) feeds MakeErrors an Error line cut in two, and a line ended by no newline, which stays at LINE_KEPT bytes.
  • The existing test_a_finding_a_refusal_and_make_s_own_failure_are_told_apart (depth 0, no -k) passes unchanged. It now shares with_members and its line helpers with the new tests.

3. What passing make's stderr on changes. I chose to document it in run_make's docstring, after measuring both ways.

effect 281df263 and head (documented) reading only until make exits (prototype, not committed)
test -t 2 in the member false: the pipe false
stdout and stderr order out of order on the pty: one run printed err 1 after out 4, another all six out lines first the same
recipe sleep 6 & 6.14 s 0.29 s
( sleep 1; echo late >&2 ) & 1.12 s, late shown late lost, and the writer died at that write: its status file was never written
non-UTF-8 bytes (caf and 0xE9) passed on as written the same
  • The background holder. Stopping once make has exited and nothing is waiting fixes only the hold, and only by closing the pipe. The left-running process's next stderr write then meets a closed pipe and dies of SIGPIPE, silently. Holding the run is visible, ends when the process lets go of stderr, and the recipe can give the process its own stderr (2>log), as the docstring says.
  • The terminal. Giving make the terminal back would lose its Error <code> line. Measured: make --debug=j prints only "Reaping losing child" on stdout, with no code, and no other output make writes carries the code. That would reopen line 2 of the family root's bootstrap.py and validate-family.py: the credential advice prints beside "not for want of a credential", a member's refusal and finding both read make bootstrap exited 2, and the wrong-case move's git rm --cached may refuse #839, which this change keeps closed: make still runs.
  • The line order and the Windows decoding cannot be fixed by the reader. The docstring says each, with the Windows case read and not run.
  • Tests that hold the docstring:
    • test_what_a_member_s_make_leaves_on_stderr_is_passed_on_as_written (the leaves test) asserts bootstrap's stderr bytes exactly: the non-UTF-8 line as written, then late, before the run ends;
    • the pty test asserts stderr: no terminal and stdout: a terminal from the member.

Pinned copy and docs.

  • bootstrap.py is in FAMILY_COPIED_VERBATIM, so each holder takes one upstream-changed row for it on its next update. Its mode stays 100755.
  • Its imports from repo_shape are unchanged, so the skew rule (tests/test_pinned_copy_text.py) and tests/test_update_shape_kept_helper.py hold.
  • No --help text changed, so docs/cli.md is unchanged.
  • No sentence of AGENTS.md, templates/family-root/AGENTS-shape.md, templates/family-root/README.md or README.md speaks of the make line or of stderr (grep).

Fail on base. The test file at this head, run on 281df263's code: 3 failed, 54 passed. On 82a84f88's code (this pull request before fix round 1): 2 failed, 55 passed.

  • On 281df263, the 3 failures are the tests that pin line 2's change: [innermost-under-make], [innermost] and test_a_line_that_comes_in_pieces_is_read_whole. On 82a84f88, the 2 failures are both [innermost*].
  • These pass on 281df263 by design:
    • the pure mutant-killers [last-under-make] (R1, R8), the pty test (R7) and both test_no_advice_beside_no_member_updated_with_every_member_checked_out cases (R10, R11);
    • the tests that hold a docstring: [signal], test_what_a_member_s_make_leaves_on_stderr_is_passed_on_as_written, and the store assert in both test_a_git_mv_staged_with_work_in_the_checkout_is_undone_as_printed cases.
  • Each INNERMOST member, run straight with -k (depth 0):
member expected 281df263 82a84f88 head
Recurse 1 2 1 1
RecurseRefuse 2 2 2 2
InnerOwn none named 2 none named none named
InnerThenOwn 3 3 3 3
InnerThenFinding (X12) 1 1 1 1
Long 1 none named 1 1
KeepsGoing (X11) 1 2 1 1
ThenKilled (X7) 1 1 1 1
LetFail 2 2 1 2
InnerLets 2 2 2 2
OwnStars 1 1 none named 1
  • rv884's own X7, X11 and X12, its saved copies of 82a84f88 with each mutant, read their member wrong: ThenKilled says none, KeepsGoing says 2 and InnerThenFinding says none.

Mutants at this head, each run against the whole test file: 36 made, 33 caught. The 3 survivors are the equivalents rv884 named: X5 (\s*$ dropped; make's *** lines carry nothing after the code), X6 (the unended remainder read at EOF; make always ends its line) and X9 (passed over only one level deeper; makes nest one level at a time).

mutant caught by
R1, first Error line kept [last-under-make], both [innermost*]
R7, no sink.flush() the pty test
R8, regex anchored on ^make()?: [last-under-make], both [innermost*], the pieces test
R10, R11 the matching test_no_advice_beside_no_member_updated_with_every_member_checked_out case
H1 nothing passed over; H2 any code passed over; H4 depth never read; H6 depth not kept; H8 a *** line with no code not read; H9 the 4096-byte tail again both [innermost*]
H3, <= for < in the depth test [last-under-make], both [innermost*]
H5, make: read as depth 9 [innermost]
H7, depth starting at 99 both told_apart cases, both [innermost*]
H10 an unended line dropped; H11 kept whole the pieces test
H12, only a chunk's last line read [last-under-make]
D1 reading stopped once make exits; D2 the bytes decoded as UTF-8 the leaves test
rv884's X1 (!=), X2 (depth only on coded lines), X4 (depth group dropped), X7, X11, X12 both [innermost*] (X1 also both told_apart cases)
rv884's X3 ([:LINE_KEPT]) the pieces test
rv884's X8 (splitlines) 7 tests
rv884's X10 (one-digit code) [signal]
N1, the first match on a line both [innermost*] (OwnStars)
N2, the (ignored) line not read both [innermost*] (LetFail)
N3, the (ignored) line always sets the depth; N5, it falls through both [innermost*] (InnerLets)
N4, max for min both [innermost*] (LetFail, InnerLets)

Runs, each through run9s.sh with SHARDS=3 at this head, with no tracked changes. The files: tests/test_family_holder_bootstrap_texts.py, tests/test_family_bootstrap_gate_order.py, tests/test_bootstrap_spelling.py and tests/test_pinned_copy_text.py; the two that git grep -l 'family-root/scripts/bootstrap' -- tests names, tests/test_family.py and tests/test_update_shape_kept_helper.py; the trio, tests/test_repo_hygiene.py, tests/test_agents_md_codes.py and tests/test_cli_reference.py; and five more that run the holder's bootstrap: tests/test_family_gitmodules_values.py, tests/test_family_head_lost_gitmodules.py, tests/test_family_members_not_a_list.py, tests/test_bootstrap_without_git.py and tests/test_family_makefile_siblings.py.

  • git 2.43.0: 1049 passed, rc 0 (shards 218, 394 and 437), at dbd3a6dd.
  • git 2.47.1: 1049 passed, rc 0 (shards 218, 394 and 437), at dbd3a6dd.

Merge checks, git merge-tree --write-tree of this head, after a fresh fetch:

against result
origin/main 71f36fe0, where lane 3's #877 has landed as c9ca9a45, editing other parts of bootstrap.py clean, tree 8b5826db
origin/main 201c6f09, after #865, #871 and #815 too clean, tree 8c3d8e13; on a commit at that tree, this test file, tests/test_family_gitmodules_values.py and tests/test_pinned_copy_text.py gave 291 passed, rc 0 (git 2.43, rv884's re-check, #886 issuecomment-6249507909)
#877's head 35b214b2 clean, tree b80a2ba0

Sonar-style checks.

function 281df263 82a84f88 head
run_make 2 1 1
MakeErrors.read new 1 1
MakeErrors.said new 5 6
read_until (test) new 9 9
made, with_members (test) new 2, 1 2, 1
test_the_line_names_the_code_of_the_member_s_innermost_command new 4 4
test_a_finding_a_refusal_and_make_s_own_failure_are_told_apart 3 2 2
test_a_git_mv_staged_with_work_in_the_checkout_is_undone_as_printed 3 3 3

Not here.

  • No Windows or macOS run, since CI is Linux only (ci: run the test suite on Linux only #597). The Windows decoding is read, not run.
  • Make before 4.0 was not run. The prefix and the Error line keep make 3.81's form by its source, but the line-2 test already asserts make 4's quoting.
  • The innermost rule's limits are in MakeErrors' docstring and listed under line 2 above. A make that speaks another language still gets the line for make's own failure.

Closes #884

🤖 Generated with Claude Code

Summary by Sourcery

Improve family bootstrap failure reporting by reading make output line by line and identifying the innermost command responsible for failures.

Bug Fixes:

  • Correctly identify the innermost member command's exit code when GNU Make invokes nested makes.
  • Preserve make -k error codes even when additional stderr exceeds the previous buffer limit.

Enhancements:

  • Process make stderr incrementally while preserving output bytes and supporting bounded handling of incomplete lines.
  • Document and test stderr forwarding, signal behavior, nested-make reporting, terminal interaction, and background writers.
  • Expand bootstrap regression coverage for surviving mutants, member prompts, and Git store preservation.

Tests:

  • Add comprehensive bootstrap tests for nested make failures, make -k, signals, partial stderr lines, prompts, forwarded output, and credential-advice edge cases.

…e and the innermost command named, so a member whose make runs another no longer reads `exited 2` for a finding, and `make -k` keeps its `Error` line (#884)

#884's three lines, from rv876's NIT 2, NIT 3 and NIT 4 on #876, in the
family holder's pinned `templates/family-root/scripts/bootstrap.py` and
`tests/test_family_holder_bootstrap_texts.py`.

Line 1, tests only. A test now holds each of rv876's five surviving
mutants: R10 and R11 (`credential_advice` ignoring `unreadable` or
`refused`), in the two states where every member is checked out and git
read nothing; R1 (the first `Error` line read) and R8 (the line read
only as `make:`), under a real parent `make -k`; R7 (`passed_on`'s flush
dropped), on a pseudo-terminal, where the member's question must show
before the answer is typed. The `git mv` test asserts the store its
docstring names.

Line 2. `run_make` read the last `Error` line in the last 4096 bytes of
make's stderr. `MakeErrors` now reads each line as it passes, so an
`Error` line followed by more than 4096 bytes (`make -k`) is no longer
said to be missing. An `Error 2` that follows straight on a deeper
make's `***` line is that make's parent reporting it, and is passed
over: a member whose make runs `$(MAKE) inner` is named by the inner
command's code (1, a finding), not by make's 2, and an inner make's own
failure names none. The docstring now says what a signal does: make
prints its name where the process make started is killed, and names the
shell's 128+n where the shell, as dash does, outlives the command.

Line 3, docstring. Passing make's stderr on takes the terminal from the
member's stderr, can reorder its stdout and stderr lines, lets a process
left holding stderr hold the run, and passes the bytes on undecoded.
`run_make`'s docstring says each. Reading only until make exits was
measured: the run ends at once, but a process that writes to stderr
afterwards dies of SIGPIPE, unsaid. Giving make the terminal back would
lose the `Error` line, which nothing else make writes carries.

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

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-5, 2026-10-18T09:59Z) @codex review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 18, 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-18T10:02:59.123795Z 82a84f8 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.

@sourcery-ai

sourcery-ai Bot commented Oct 18, 2026

Copy link
Copy Markdown

Reviewer's Guide

Updates the pinned family-root bootstrap implementation to parse GNU Make stderr incrementally and depth-aware, so nested and -k failures report the correct command exit code while preserving stderr streaming semantics; comprehensive tests cover diagnostics, PTY interaction, byte fidelity, background writers, and related bootstrap regressions.

Sequence diagram for streaming make stderr and reporting the innermost failure

sequenceDiagram
    participant Bootstrap
    participant Make
    participant MakeErrors
    participant Terminal

    Bootstrap->>Make: run_make(make, target, cwd)
    Make-->>Bootstrap: stderr chunk
    Bootstrap->>Terminal: passed_on(chunk)
    Bootstrap->>MakeErrors: read(chunk)
    MakeErrors->>MakeErrors: said(line)
    alt nested make reports parent Error 2
        MakeErrors->>MakeErrors: pass over shallower depth Error 2
    else command Error line
        MakeErrors->>MakeErrors: store innermost exit code
    end
    Make-->>Bootstrap: process exit
    Bootstrap-->>Bootstrap: return make exit code and stored command code
Loading

Flow diagram for depth-aware GNU Make error parsing

flowchart TD
    A[Read stderr chunk] --> B[Pass chunk through unchanged]
    B --> C[Split completed lines]
    C --> D[Match make depth and Error code]
    D --> E{Deeper make's parent Error 2?}
    E -->|Yes| F[Ignore propagated Error 2]
    E -->|No| G[Store command exit code]
    F --> H[Keep reading]
    G --> H
    H --> I{Unended line?}
    I -->|Yes| J[Retain last 4096 bytes]
    I -->|No| K[Wait for next chunk]
    J --> K
Loading

File-Level Changes

Change Details Files
Reworked make stderr parsing to stream complete lines, preserve bounded partial-line state, and identify the innermost command’s exit code across nested makes.
  • Added depth-aware parsing that ignores parent make Error 2 reports when they merely propagate a deeper failure.
  • Handles nested make failures, make -k output beyond 4 KiB, make-owned failures, and signal termination without losing the relevant code.
  • Expanded run_make documentation to describe stderr piping, ordering, terminal behavior, inherited background writers, byte preservation, and signal semantics.
templates/family-root/scripts/bootstrap.py
Expanded regression coverage for make diagnostics, stderr streaming behavior, and previously untested bootstrap guarantees.
  • Added tests for nested make diagnostics, make -k, signal cases, split and unterminated stderr lines, parent-make prefixes, and exact final failure messages.
  • Added pseudo-terminal coverage proving prompts are flushed before input and member stderr is piped while stdout remains terminal-bound.
  • Added coverage for preserving non-UTF-8 stderr bytes and output from background processes that retain the pipe.
  • Added tests catching credential-advice mutants when all members are checked out.
tests/test_family_holder_bootstrap_texts.py
Strengthened and refactored existing test helpers and Git store assertions without changing production behavior.
  • Shared member setup, script loading, and Git object-retention helpers across tests.
  • Asserted that the checkout commit remains present in the member Git store after the guarded move scenario.
tests/test_family_holder_bootstrap_texts.py

Assessment against linked issues

Issue Objective Addressed Explanation
#884 Add regression tests that catch the surviving #876 mutants, including the prompt-flush, parent-make parsing, advice, and last-Error-line cases, and assert the member-store state claimed by the existing docstring. ✅
#884 Make the reported member make result truthful for recursive makes and Error lines followed by more than 4 KiB of stderr, and make the signal behavior described by run_make's docstring accurate. ✅
#884 Document the observable effects of piping make's stderr, including terminal detection, output ordering, background-child pipe lifetime, and Windows byte decoding, with tests for the relevant behavior. ✅

Possibly linked issues


Tips and commands

Interacting with Sourcery

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

Customizing Your Experience

Access your dashboard to:

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

Getting Help

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 82a84f88b6

ℹ️ 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".

if found is None:
return
depth, code = int(found.group(1) or 0), found.group(2)
reports_a_make = code == b"2" and depth < self.depth

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Do not discard Error 2 after a handled submake failure

When a member recipe handles a recursive failure (for example, $(MAKE) -s inner || true) and a later command genuinely exits 2, the inner error raises self.depth but produces no parent Error 2 because the shell handled it. The later top-level Error 2 then has a lower depth and is discarded here, leaving the stale inner code (such as 1), so the family summary mislabels an actual member refusal as a finding. Suppression needs to be tied to an unhandled submake failure rather than any previously observed deeper error.

Useful? React with 👍 / 👎.

@brettheap

Copy link
Copy Markdown
Contributor Author

Independent review of #886 (#884) at 82a84f88, for lane openRepoShape-5: READY

I found no MUST: at head the code gives the right answer in every normal case I measured. There is one SHOULD, which is three more test members and no code. I changed no branch, pushed nothing and posted nothing. $Q = /tmp/claude-1000/-workspace-projects-openRepoShape/50cede29-ab82-4ca3-9f67-d98e1a58c8d2/scratchpad. My logs, probes and tools are in $Q/v118/rv884/.

Findings, most severe first

SHOULD 1 (tests only): three of my mutants of #884's new code survive all 57 tests, and a person would see each of them. The body says "19 made, 19 caught, no survivor". That is true of w884's 19, but these three are not in them. Logs: $Q/v118/rv884/mut/summary.txt. I confirmed each one is visible with a patched copy of head (probe5.log, probe6.log).

NIT 1: MAKE_FAILED_RE takes the first : *** on a line. Its code group is optional, so a later Error on the same line is never tried (bootstrap.py:885, :920). Measured at both depths (probe1.log):

  • A member writes printf 'x: *** y' >&2; exit 1, with no newline. Head says "printed no Error <code> line"; base said exited 1. That is a regression, though a contrived one.
  • A member line x[3]: *** y followed by exit 2 is passed over as a deeper make's report. Head names no code; base said exited 2.
  • Neither case is among the body's two "Not here" limits.
  • A fix: use finditer and take the last match on the line. Anchoring on make would be wrong, because make prints its own program name.

NIT 2: the docstrings state the innermost rule with no conditions, and the limits are only in the PR body (bootstrap.py:83, :898-903, :944).

  • "Straight after" is narrower than the code. said passes over an Error 2 that follows the last deeper *** line with any number of other lines in between.
    • Measured: -$(MAKE) inner (inner exits 1), then echo 'own refusal' >&2; exit 2 reads exited 1. The member's own refusal shows as a finding. Base read exited 2.
    • $(MAKE) inner || true; ...; exit 2 gives the same result.
    • This is the body's second limit, measured, and a regression from base for that shape.
  • Other cases where the line is not the innermost command's code:
    • env -u MAKELEVEL $(MAKE) inner reads exited 2 at both depths, as base did.
    • -j interleaving reads exited 2; see "Checked and true".
  • Suggest writing these limits into MakeErrors' docstring.

NIT 3: the signal sentence in the run_make docstring (bootstrap.py:948-955), measured under dash 0.5.12 and bash 5.2.21 (probe2.log, 24 cases at both depths):

  • "the shell prints Killed itself … 143 for SIGTERM" is wrong for SIGTERM. dash prints Terminated, then make prints Error 143.
  • "bash, which runs a recipe line of one command in its own place" is true but incomplete. bash also runs the last command of a list in its own place: echo before >&2; <killed> gives Killed and no code under bash, and 137 under dash.
  • Every other sentence in it is true. See below.

NIT 4 (PR body only):

  • In the complexity table, MakeErrors.said is 5 by Sonar's rules, not 4: the or 0 at :923 is a boolean sequence. It is still well under 15.
  • The merge row for main names 1ef1a018. Main is now 33c5128b, and the merge is still clean (tree 18472b1d).

Checked and true

Line 2, the reading. GNU Make 4.3. Each case ran under the holder's make [-k] bootstrap (make[1]:) and with bootstrap.py run straight (make:), on head and on base 281df263, in the same holder. Logs: probe1.log, probe3.log, probe4.log.

case head (both depths) base
$(MAKE) inner, inner exits 1 / 2 / 3 1 / 2 / 3 2 / 2 / 2
two deep, inner exits 1 / 2 1 / 2 2 / 2
$(MAKE) nothere no code named 2
inner make killed by SIGTERM / SIGKILL no code named (Terminated / Killed) the same
-k: sub-make (inner 1), then plain exit 2 2 (the last) 2
-k: plain exit 2, then sub-make (inner 1) 1 (the last) 2
-j2 inner make, with its Waiting line 1 2
-j2: plain job fails first, then the sub-make's inner 1 2
-j2 interleave: inner 1, then a plain job at the parent's depth fails 1 2 (the documented limit) 2
the same, with only Killed and Waiting (no code) at the parent's depth 2 2
-k: Error 1, then 5000 bytes (ended, and not ended) 1 no code named (false)
5000 bytes, and 100 KB, with no newline before Error 1 1 1
non-UTF-8 bytes; CRLF 1; 1 1; 1
-exit 1 then success; stderr not ended at EOF on success ok; ok ok; ok
  • Can a *** line with no code reset the depth so the parent's Error 2 is taken as a code? Yes, but only under -j, when a job at the parent's depth fails or is killed while the sub-make is still finishing. That is inside the documented -j limit, and base read 2 too.
  • Waiting lines at the inner depth never cause it.
  • Under the holder's make -j2, each member's make warns jobserver unavailable: using -j1 and runs serially.
  • Chunking: 92 captured stderr streams were fed to MakeErrors at every 2-way split, byte by byte, and in 200 random splits: 57,178 chunkings, none differing from the whole read (tools/chunks.py). Separately, read1(7) in place of read1(65536) passed all 57 tests (E1).
  • EOF remainder: make's Error line can never be the unended remainder. make always writes it with its newline, and a mutant that reads the remainder (X6) survives as equivalent.
  • The final-line texts are true in every case above, except the documented limits and NIT 1's contrived lines.
  • stdout is byte-identical between head and base in 68 of 68 runs. stderr is identical apart from the family bootstrap line. Popen leaves stdout alone.

Line 2's signal docstring:

  • make prints Killed or Terminated and no code in these cases: a command make runs without a shell, kill -KILL/-TERM $$ under either shell, and bash running one command.
  • dash running one command prints the signal's name itself, exits 137 or 143, and make names that code.

Line 3, on a pty (pty2.log, ask/), through bootstrap and through the member's make alone:

  • The terminal: stdin and stdout stay a terminal; stderr is not one through bootstrap and is one through make alone.
  • git's progress meter: absent through bootstrap, shown through make alone.
  • Line order: out of order through bootstrap (out 1|out 2|err 1|…, 3 of 3 runs), in order through make alone.
  • A background process: sleep 6 & holds bootstrap 6.16 s, against 0.23 s under make alone. With 2>/dev/null it takes 0.29 s.
  • A late write: ( sleep 1; echo late >&2 ) & takes 1.14 s, late shows, and its status file reads rc=0.
  • w884's SIGPIPE claim, re-measured: with the "stop once make exits" variant (d1-bootstrap.py), the run ends in 0.33 s, late is lost, and no status file is ever written. The writer died at that write, silently.
  • "Giving make the terminal back would lose its Error <code> line" is true. make --debug=j prints only "Reaping losing child …", and --debug=a prints no code anywhere.
  • Documenting rather than changing is sound. The one way to keep both on POSIX would be a pty for make's stderr. It would fix only the isatty(2) effect and does not exist on Windows; it was not weighed.

Line 1:

  • rv876's survivors, restated: R1 → [last-under-make]; R7 → the pty test; R8 (^make()?: ) → 4 tests; R10 → [unparsed]; R11 → [value-refused].
  • The store mutant (S1b: the put-back lines also delete the store's main, expire its reflog and run gc --prune=now) is caught by exactly the new store assert in [commit]. [untracked] cannot catch it, because there the checkout's commit is the pin.
  • My 12 mutants: 6 caught (X1 !=, X2 depth only on lines with a code, X3 [:LINE_KEPT], X4 depth group dropped, X8 splitlines, X10 one-digit code). 3 survive but are equivalent or never seen: X5 (\s*$ dropped: make's *** lines carry nothing after the code), X6, and X9 (depth exactly one less: makes nest one level at a time). The other 3 are SHOULD 1.
  • R7 on the pty: the question shows at 0.04-0.07 s at head; with R7 it shows at 8.06 s, after the blind answer.

The pty test:

  • 20 of 20 passed while my 2.43 run went beside it. The call phase took 0.07-0.13 s each time, against a 10 s window.
  • It cannot hang without bound. If the question never shows, it gives up after 10 s, then 60 s, then proc.wait(timeout=60) fails at about 130 s. It does leave a member that hangs forever still running.
  • With pty made unimportable, the file still collects 57 items and the test is skipped (could not import 'pty').

Fail on base: the head test file on 281df263 gives 3 failed and 54 passed (base-new.log). The 3 are exactly [innermost-under-make], [innermost] and the pieces test. 281df263 is byte-identical to 41baf920 for the touched files.

Runs at 82a84f88 through run9.sh:

  • git 2.43, the 7 files: 532 passed, rc 0 (h243.log).
  • git 2.47.1, the test file: 57 passed, rc 0 (h247.log).
  • w884's logs: 218+394+437 = 1049 passed on both versions, rc 0, head 82a84f88, no tracked changes, the body's 14 files, no skips.

Merge checks (git merge-tree), all clean:

against tree
main 33c5128b 18472b1d
#877 a137741f
#882 e39f0153
#769 881061ec

Pinned copy and docs:

  • bootstrap.py is in FAMILY_COPIED_VERBATIM (scripts/shape_materialize.py:533).
  • update-shape.py:863-865 makes an upstream-changed row per path wherever the pinned bytes differ from the target, so each holder takes one row for it.
  • Imports are unchanged and the mode stays 100755, so the skew rule holds.
  • No sentence in AGENTS.md, AGENTS-shape.md, either README or docs/cli.md speaks of the make line or of stderr.
  • The module docstring's (b) paragraph is true apart from NIT 2's limits.

Sonar-style checks:

The body's other numbers are true: both of its tables, 9 new items (57 in all), 3/54 on base, and 1049 on both versions. The title (225 characters) is in the repo's subject style.

Could not verify

  • Windows decoding: read only. A member on Python 3.15 or later, whose UTF-8 mode is the default (PEP 686), would not garble.
  • macOS, make before 4.0, a make speaking another language (no locale here), and CI's git 2.55.0.
  • SonarCloud's real verdict: the Sonar MCP failed to connect.

Checklist candidates

Scratch left; nothing of mine is running

…derr is read and a `-` that lets an inner make fail puts its depth back, and the innermost rule's limits are in its docstring (#884)

Fix round 1 on #886, from rv884's review (issuecomment-6248806027).

SHOULD 1: three members join `INNERMOST`, for rv884's three surviving
mutants: `$(MAKE) -s -k inner` whose second target prints after the
inner `Error 1` (X11), a `-k` finding followed by `kill -KILL $$` (X7),
and `$(MAKE) -s nothere || exit 1` (X12). Each is now caught.

NIT 1: `MakeErrors.said` takes the last `MAKE_FAILED_RE` match on a line
(`findall`), so `printf 'x: *** y' >&2; exit 1` reads `exited 1` again,
as before #876's change; `OwnStars` holds it.

NIT 2: `MakeErrors`' docstring says what the code does, an `Error 2` at a
shallower depth than the last `***` line, whatever lines come between,
and names where the code is not the innermost command's: a make whose
failure the shell lets pass (`|| true`, `;`) or a `-` in a make run with
`-s`; `env -u MAKELEVEL`; `-j` interleaving; a member's own line shaped
like make's. The module docstring's (b) says "as far as make's lines tell
it". The option rv884 offered is adopted: make's `[...] Error 2
(ignored)` line, for a `-` that lets an inner make fail, puts the depth
back at the parent's, so the parent's own `Error 2` is named (`LetFail`;
`InnerLets` holds that an inner make's own ignored error changes
nothing). Across rv884's 133 case runs it changes no other case.

NIT 3: `run_make`'s signal sentence is true of rv884's 24 cases: dash
prints `Terminated` for SIGTERM, and bash runs a line's last command in
its own place, so make then prints `Killed` and no code.

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

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-5, 2026-10-18T11:15:47Z) Fix round 1 for #886 (#884), from rv884's review (issuecomment-6248806027): head dbd3a6dd

One commit on 82a84f88, touching templates/family-root/scripts/bootstrap.py and tests/test_family_holder_bootstrap_texts.py only.

SHOULD 1, X7, X11 and X12. INNERMOST has three more members: KeepsGoing, which is $(MAKE) -s -k inner whose second target prints after the inner Error 1 (X11); ThenKilled, a -k finding followed by kill -KILL $$ with no sleep (X7); and InnerThenFinding, which is $(MAKE) -s nothere || exit 1, expecting exited 1 (X12). Each mutant, restated at the new head, is caught by both [innermost*]. rv884's own saved copies of 82a84f88 with X7, X11 and X12 each read their member wrong (ThenKilled says none, KeepsGoing says 2, InnerThenFinding says none).

NIT 1, the first match on a line. MakeErrors.said now takes the last MAKE_FAILED_RE match on a line (findall). OwnStars, rv884's printf 'x: *** y' >&2; exit 1, reads exited 1 at both depths, as 281df263 read it; 82a84f88 named none. Mutant N1, which takes the first match, is caught. x[3]: *** y followed by exit 2 stays a limit, written in the docstring.

NIT 2, the limits in the docstrings. MakeErrors' docstring now says what the code does: an Error 2 at a shallower depth than the last *** line, whatever lines come between. It names where the code is not the innermost command's: a make whose failure the shell lets pass (|| true, ;), or a - in a make run with -s, which prints no (ignored) line, followed by the recipe's own exit 2; env -u MAKELEVEL; -j interleaving; and a member's own line shaped like make's. The module docstring's (b) says "as far as make's lines tell it".

  • The optional (ignored) line: adopted, by measurement. make's [Makefile:N: bootstrap] Error 2 (ignored) now puts the depth back at the parent's, but only where it is shallower than the last *** line. So -$(MAKE) inner then exit 2 (LetFail) reads exited 2 again; 82a84f88 read 1.
  • Across rv884's 133 case runs (tools/cases1.py to cases6.py, both depths), head differs from 82a84f88 only there and in NIT 1's case. Across 21 more of mine it differs only there, in -$(MAKE) nothere then exit 2, and in NIT 1's case, each now as 281df263 read it.
  • An inner make that ignores an exit 2 of its own prints the same kind of line at its deeper depth; InnerLets holds that this changes nothing (exited 2). It runs without -s, since -s keeps make from printing the line. Mutants N2-N5 on that branch are each caught.

NIT 3, the signal sentence. It is now true of rv884's 24 cases in its probe2.log:

  • make prints the signal's name and no code where the signal kills what make started: a command run with no shell, the shell a kill $$ kills, or a command bash runs in its own place, which is a line's last command;
  • where the shell outlives the command (dash always, bash for a command that is not the line's last), the shell prints Killed or Terminated itself and exits 137 or 143, which make names;
  • where the line goes on after the command, the shell exits as the rest of the line does.

NIT 4, the body. body-r1 recounts said: 5 at 82a84f88, 6 now with the (ignored) branch. The merge rows name main 71f36fe0, with its tree.

Tests. Still 57 items; INNERMOST has 11 members, run at both depths.

  • On 281df263's code: 3 failed, 54 passed ([innermost-under-make], [innermost], the pieces test).
  • On 82a84f88's code: 2 failed, 55 passed (both [innermost*]: LetFail and OwnStars).
  • At head: 57 passed.

Mutants at head: 36 made (w884's 19 and rv884's X1-X12, restated where the code changed, and N1-N5 for this round's hunks), 33 caught. The 3 survivors are the equivalents rv884 named: X5, X6 and X9.

Runs at dbd3a6dd, through run9s.sh with SHARDS=3, on the body's 14 files:

  • git 2.43.0: 1049 passed, rc 0 (shards 218, 394 and 437).
  • git 2.47.1: 1049 passed, rc 0 (shards 218, 394 and 437).

Merge checks after a fresh fetch:

Lane: openreposhape-5 (openRepoShape-5)

@brettheap

Copy link
Copy Markdown
Contributor Author

Re-check of #886 (#884)'s fix round 1 at dbd3a6dd, for lane openRepoShape-5: READY

All five of my first-round findings are closed. I found no MUST and no SHOULD. The new (ignored) marker gets two NITs: a one-token code change and a docs gap. I changed no branch, pushed nothing and posted nothing. $Q = /tmp/claude-1000/-workspace-projects-openRepoShape/50cede29-ab82-4ca3-9f67-d98e1a58c8d2/scratchpad. Logs and tools are in $Q/v118/rv884b/.

Findings, most severe first

NIT 1: the (ignored) rule could reset the depth fully, which would make two more shapes read right (templates/family-root/scripts/bootstrap.py:947-948, and :889 for Error 2 only).

  • Scenario 1 (probe-ign3.log): the member runs $(MAKE) mid, then its own exit 2. mid runs -$(MAKE) inner, and inner exits 1.
    • make prints make[3]: *** … Error 1, then make[2]: [mid] Error 2 (ignored), then make[1]: *** [bootstrap] Error 2.
    • Head moves the depth back to 2, the ignoring make's own, so the member's Error 2 is passed over.
    • The line says exited 1, so a refusal reads as a finding, at both depths. 82a84f88 also said 1; 281df263 said 2.
  • Scenario 2: $(MAKE) inner || true, then -false, then exit 2. Error 1 (ignored) is not a marker, so head says exited 1. That shape is listed in the docstring.
  • Why a full reset is safe: in a sequential run, a make that has printed (ignored) keeps going. If it fails later, it prints its own *** line first, and that line sets the depth again. So no Error 2 that comes after an (ignored) line, with no deeper *** line between, can be a make reporting a failed sub-make.
  • Measured with patched copies of head, over all 183 case-modes I have (probe-i17.log, probe-i117.log):
    • self.depth = -1 in the (ignored) branch (my I17) differs from head in 3 case-modes;
    • adding any code's (ignored) line as the marker (I1+I17) differs in 5;
    • each difference is one where head names the wrong code and the patch names the member's own exit 2; everything else is unchanged.
  • Both I1 and I17 survive the tests. A person would see them only as lines that become right.

NIT 2 (docstring only): the list of limits in MakeErrors misses two shapes (bootstrap.py:913-925).

  • Scenario 1 above: a - one make below the one that exits 2. NIT 1's change would remove it.
  • Under -k, a: $(MAKE) innerA (exits 1), then b: -$(MAKE) innerB (exits 3). The line names 3, the code of the sub-make a - let fail, though the failing command exited 1. 82a84f88 and 281df263 also said 3 (k-sub1-then-letfail).

Checked and true

My first-round findings:

  • SHOULD 1, closed. X7, X11 and X12, restated at dbd3a6dd, are each caught by both [innermost*] (mut/summary.txt).
  • NIT 1, closed. said takes the last findall match. printf 'x: *** y' >&2; exit 1 reads exited 1 at both depths (82a84f88 named none), and N1, which takes the first match, is caught.
  • NIT 2, closed apart from the two shapes above.
    • The docstring now says "at a shallower depth than the last *** line …, whatever lines come between", and lists || true/;, - under -s, env -u MAKELEVEL, -j, and x[3]: *** y.
    • I measured each of them: s-letfail-then2 reads 1 under the holder's make -s and under MAKEFLAGS=s.
    • The -s behaviour is in the docstring.
  • NIT 3, closed. All three sentences are true of every one of the 24 cases in probe2.log:
    • no code where make started what was killed (no shell, kill $$, or bash's last command);
    • the shell's Killed or Terminated with 137 or 143 where the shell outlives the command (dash always, bash when it is not the last command);
    • ; echo after ends the recipe with 0.
  • NIT 4, closed. said is 6 by my count too. The merge rows are right: main 71f36fe0 gives tree 8b5826db, bootstrap.py: a .gitmodules blob with a line git cannot parse is read up to that line, as git reads it, and a parity test holds the value rules both family scripts write to one answer (#856) #877's head 35b214b2 gives b80a2ba0, and main 201c6f09 gives 8c3d8e13.

The (ignored) marker, measured at both depths on dbd3a6dd, 82a84f88 and 281df263 (probe-ign.log, probe-ign2.log, 25 cases, 150 runs):

case head 82a84f88 281df263
-exit 2 (or -exit 1) first, then a sub-make whose inner command exits 1 1 1 2
sub-make 1 || true, then -false, then own exit 2 1 (listed limit) 1 2
sub-make 1 || true, then -sh -c 'exit 2', then own exit 2 2 1 2
-$(MAKE) inner (inner 1 or 3), then exit 1 / exit 2 / exit 3 1 / 2 / 3 1 / 1 / 3 1 / 2 / 3
-$(MAKE) nothere, then exit 2 2 none named 2
-k: let-fail, then own exit 2 2 1 2
-k: let-fail (inner 3), then sub-make (inner 1) 1 1 2
-j2: sub-make 1 beside a let-fail sub-make 1 1 2
-j2: let-fail beside own exit 2 2 1 2
an ignored exit 2 inside the inner make, then the inner fails 1 1 1 2
an ignored exit 2 inside the inner make, then the member's own exit 2 2 2 2
-$(MAKE) -s inner, then exit 2 (the parent still prints (ignored)) 2 1 2
  • No case reads worse than 281df263 except the shapes already listed as limits and Scenario 1.
  • One case reads worse than 82a84f88: under -j2, an ignored plain exit 2 lands between a still-running sub-make's Error 1 and its parent's report (j-keepsgoing-and-ign-plain2). Head says 2, 82a84f88 said 1, and 281df263 said 2. That is inside the documented -j limit.

All my earlier cases: over 133 case-modes, head differs from 82a84f88 in only 4: the let-fail case and the last-match case, each at both depths, and each now as 281df263 read it (probe-all.log).

Chunking: 250 streams in 137,515 chunkings, none differing from a whole read. read1(7) (E1) passes all 57 tests.

Mutants at dbd3a6dd (22 runs):

  • caught: X1-X4, X7, X8, X10-X12, N1-N5, and my I8 (min with depth + 1);
  • survived, equivalent: X5, X6, X9;
  • survived and visible: I1 and I17, both changes that would make lines right (NIT 1), and I4 (an (ignored) line clears the code). I4 reads "none named" for the -k let-fail shape where head says 3 and the true code is 1, so both are wrong there.

Runs at dbd3a6dd:

  • texts file on 2.43: 57 passed, rc 0;
  • 7-file set on 2.43: 532 passed, rc 0;
  • texts file on 2.47.1: 57 passed, rc 0.

Fail on older code, with the round's test file:

  • on 82a84f88's code: 2 failed (both [innermost*]), 55 passed;
  • on 281df263's code: 3 failed, 54 passed.

w884's logs: 218+394+437 = 1049 passed on 2.43 and on 2.47.1, head dbd3a6dd, no tracked changes. This matches the body.

Main: the merge with 201c6f09 is clean, tree 8c3d8e13. On a detached commit at that tree, the texts file, test_family_gitmodules_values.py and test_pinned_copy_text.py gave 291 passed, rc 0. Merges with #882 and #769 are clean too.

Sonar-style checks:

  • No duplicated window of 6 to 10 lines with exact text.
  • With literals blinded, INNERMOST's own entries match each other in 10-line windows. That matters only if Sonar blinds literals.
  • The only or assert is the old one (:865), and no assert joins conditions with and.
  • said scores 6; no other changed function's code changed.
  • The longest new line is 76 columns.

A note on the body's evidence: merged-main.log for "204 passed on that tree" shows head=281df263 dirty=35, not a commit at the merged tree. My own run on the real merged commit (291 passed) covers that claim.

Could not verify

  • SonarCloud's real verdict (the Sonar MCP is down), Windows, macOS, make before 4.0, and CI's git 2.55.0.

Checklist candidates

  • None new. C1 from my first review stands: under the holder's make -jN bootstrap, every member warns jobserver unavailable.

Scratch left

  • Worktrees: rv884b-head, rv884b-mut and rv884b-main are all removed. My fetched ref refs/l5rv884b/pr886 is deleted.
  • An unreferenced commit: d8802b66 (the merge with main, made with a throwaway identity), beside the earlier 1b541bc9.
  • Basetemps (reused): $Q/v110/bt/rv884-{h243,h247,base,mut,m877,nopty,pty}.
  • Probes and logs: $Q/v118/rv884b/.
  • Processes: none of mine are running.

@sonarqubecloud

Copy link
Copy Markdown

@sourcery-ai

sourcery-ai Bot commented Oct 18, 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 3 days and 6 hours by commenting @sourcery-ai review. Upgrade to get a review now.

@brettheap
brettheap merged commit e7039b7 into main Oct 18, 2026
5 checks passed
@brettheap
brettheap deleted the fix/884-run-make-truths branch October 18, 2026 12:41
brettheap pushed a commit that referenced this pull request Oct 18, 2026
Lane: openreposhape-5 (openRepoShape-5)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap pushed a commit that referenced this pull request Oct 18, 2026
… for: templates/family-root/scripts/bootstrap.py tests/test_family_holder_bootstrap_texts.py

Lane: openreposhape-5 (openRepoShape-5)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap pushed a commit that referenced this pull request Oct 18, 2026
…ith #886, and #778 from its reviewed head (#883)

Round 6 of #883, a refresh. Each line was checked against the code:

- #886 (#884), from its squash e7039b7: a holder's `make bootstrap`
  reads a member's make on stderr a line at a time and names the
  innermost command's code, so a member whose make runs another no longer
  reads `exited 2` for a finding, and `make -k` keeps its `Error` line. A
  line in "What changed" and words in "What 1.1.1 fixes". The #884 limit
  is narrowed to what `run_make`'s and `MakeErrors`' docstrings keep: the
  pipe's three effects and the Windows decoding, the nested-make cases
  the line still misnames, the signal under dash, and make before 4.0
  not run.
- #778 (#707), from its reviewed head 8259ea1, not landed at the fetch
  just before this commit: outside a shape root, a naming policy that
  does not load, or cannot be used, in the doctor's own checkout makes
  the `the way in` rows `n/a` environment rows, where a tab line was a
  FINDING and a bad pattern a traceback, and a leg clone keeps its rows.
  A line in "What changed" and one in "What 1.1.1 fixes". "Before
  tagging" says the line comes from that head.
- "The list runs to" names no commit now: the commit v1.1.1 is tagged on.

The open list is #879 alone: #884 is closed by #886, and #707 by #778
once it lands. The shape-copy range runs to e7039b7; #886 changes the
holder's bootstrap.py, on the lists, and #778's head no pinned copy. No
other file changes.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Oct 18, 2026
…signal, resets the depth fully, so a member's own `exit 2` after a `-` one or two makes below, or after `|| true` and a `-`, reads `exited 2`, and the limits name the `-k` let-fail shape (#890) (#891)

Follows #886 (#884), which landed as `e7039b71`. The branch is restacked onto that squash as `58e4ddf0`: three merges, #886's final head `85c20783`, then its squash with this branch's side for the two files, then main. Its tree is `30ba7a3e`, the merge of `d2be39dc` with main `0d5d2b11`. Fix round 1, from rv884's review of this pull request (issuecomment-6250054923), is one commit on it, `013b9402`. A NIT round, from rv884's re-check (issuecomment-6250467939), is one more, `41f781fd`, which adds a test only and which the runs below cover.

#890's two lines, from rv884's NIT 1 and NIT 2 in its re-check of #886 at `dbd3a6dd` (#886 issuecomment-6249507909). One commit on `dbd3a6dd`, and fix round 1 on the restack, in the family holder's pinned `templates/family-root/scripts/bootstrap.py` and in `tests/test_family_holder_bootstrap_texts.py`, and nothing else. Each shape was probed under the holder's own `make bootstrap` (the member's make at depth 1) and with `bootstrap.py` run straight (depth 0), with GNU Make 4.3, dash 0.5.12 as `/bin/sh` and git 2.43.0.

**1. Three nested-make shapes read a member's refusal as a finding** (`MAKE_FAILED_RE`, `MakeErrors.said`).
- **Scenario 1:** the member runs `$(MAKE) mid`, then its own `exit 2`; `mid` runs `-$(MAKE) inner`, and `inner` exits 1. make prints `make[3]: *** [...] Error 1`, then `make[2]: [mid] Error 2 (ignored)`, then `make[1]: *** [bootstrap] Error 2`. At `dbd3a6dd` the `(ignored)` line put the depth back at 2, the ignoring make's own, so the member's `Error 2` at depth 1 was passed over, and the line said `exited 1`.
- **Scenario 2:** `$(MAKE) inner || true`, then `-false`, then `exit 2`. At `dbd3a6dd` only `Error 2 (ignored)` was a marker, so `Error 1 (ignored)` changed nothing, and the line said `exited 1`.
- **A signal** (rv884's NIT 1 on this pull request): `$(MAKE) inner || true`, then `-@Kill -KILL $$`, then `exit 2`. make prints `make[1]: [Makefile:3: bootstrap] Killed (ignored)`, or `Terminated (ignored)` for SIGTERM. At `d2be39dc` only `Error <code> (ignored)` was a marker, so the line said `exited 1` for either signal; `281df263` said 2.
- **Direction:** make's `[...] <what ended it> (ignored)` line is a marker whatever ended the command: `Error <code> (ignored)` for a code, `Killed (ignored)` or `Terminated (ignored)` for a signal. It sets the depth to -1, as before any `***` line, so no `Error 2` after it is passed over until another `***` line comes. Why that is safe (rv884's argument): in a sequential run, the make that printed the line goes on, and if it fails later it prints a `***` line of its own first, which sets the depth again; so no `Error 2` after an `(ignored)` line, with no `***` line between, can be a make reporting a failed one, whatever ended the ignored command. The rule's code is rv884's patches exactly, its I1 and I17 together and then its Q12 in fix round 1; only comments and docstrings differ from them.
- **Measured before adopting**, with rv884's probe, on `dbd3a6dd`, three patched copies of it (both changes, the reset alone, the marker alone) and `281df263`: rv884's 183 case-modes (its `cases1.py` to `cases6.py`, and its re-check's `ign.py`, `ign2.py` and `ign3.py`) and 41 more (`cases-f886.py`, `cases-f886b.py`, `cases-f886c.py`), each at both depths: 224 case-modes, 1120 runs.
  - Both changes together differ from `dbd3a6dd` in 5 case-modes; the reset alone in 3, and the marker alone in 2.
  - The 5 are Scenario 1 and Scenario 2, each at both depths, and a member's own line shaped like an `(ignored)` line followed by its own `exit 2`, run straight. In each, `dbd3a6dd` named 1 and the change names the member's own `exit 2`, as `281df263` did.
  - No case reads worse than at `dbd3a6dd`.
  - An inner make that ignores a failure of its own still changes nothing (`InnerLets`, `exited 2`), and an `(ignored)` line leaves the code named before it.
- **The signal's line, measured before adopting it** (fix round 1), on `58e4ddf0` and on its copy with rv884's Q12, over the same 224 case-modes and rv884's three new cases (`rv890/tools/extra.py`: SIGKILL, SIGTERM, and a `-` two makes below), each at both depths: 230 case-modes, 460 runs. Q12 differs from `58e4ddf0` in 4: the SIGKILL and SIGTERM cases, each at both depths, each now naming the member's own `exit 2`, as `281df263` did. No case reads worse. The pattern also takes a line written as `make[1]: [Makefile:3: bootstrap] Segmentation fault (core dumped) (ignored)` (checked on the line, not on a run).
- **Tests:** six members join `INNERMOST`, run by `test_the_line_names_the_code_of_the_member_s_innermost_command[innermost-under-make]` and `[innermost]`, at both depths:
  - `LetsBelow`, Scenario 1, expecting `exited 2`;
  - `LetsTwoBelow`, Scenario 1 one make deeper (the member, `mid`, then `mid2`, which runs `-$(MAKE) inner`), expecting `exited 2`, so a depth reset to one above the ignoring make is caught (rv884's Q6, its NIT 2 on this pull request);
  - `PassesThenLets`, Scenario 2 with `-@EXIT 10` for `-false`, so a code of two digits is a marker too, expecting `exited 2`. The form with `-false` is among the measured cases and reads `exited 2` at both depths;
  - `ThenLets`, under `-k`, a finding and then `-false`, expecting `exited 1`: now that every `(ignored)` line is a marker, an `(ignored)` line must leave the code named before it (rv884's I4);
  - `PassesThenKilled`, the signal: `$(MAKE) inner || true`, then `-@Kill -KILL $$`, then the recipe's own refusal, expecting `exited 2`;
  - `PassesThenTerminated`, the same with `-@Kill -TERM $$`, expecting `exited 2`, so a marker that took only `Error <code>` and `Killed` fails (rv884's R4, its NIT on the re-check).
- Each new member, at both depths (the two agree):

| member | expected | `281df263` | `dbd3a6dd` | `58e4ddf0` | head |
|---|---|---|---|---|---|
| `LetsBelow` | 2 | 2 | 1 | 2 | 2 |
| `PassesThenLets` | 2 | 2 | 1 | 2 | 2 |
| `ThenLets` | 1 | 1 | 1 | 1 | 1 |
| `LetsTwoBelow` | 2 | 2 | 1 | 2 | 2 |
| `PassesThenKilled` | 2 | 2 | 1 | 1 | 2 |
| `PassesThenTerminated` | 2 | 2 | 1 | 1 | 2 |

- The 11 earlier members read at head as at `dbd3a6dd`, at both depths. They now share `FINDS` and `REFUSES`, the finding and refusal commands, in place of the seven copies the table held; their bytes are unchanged, checked by evaluating both tables.

**2. `MakeErrors`' list of limits misses a `-k` shape** (docstring only).
- **Direction:** the list now names it: under `-k`, `a: $(MAKE) innerA`, whose command exits 1, then `b: -$(MAKE) innerB`, whose command exits 3. The last `Error` line is the command's in the make the `-` let fail, so 3 is named, though the failing command exited 1. Reading it right would take a code kept per depth; it stays a limit.
- The `|| true` item now reads "with no `(ignored)` line between", since Scenario 2 is no longer a limit, and keeps the `-s` note: a make run with `-s` prints no `(ignored)` line. At `d2be39dc` the item was false for a signal's `(ignored)` line: `$(MAKE) inner || true`, then `-@Kill -KILL $$`, then `exit 2` read `exited 1` though such a line came between (rv884's NIT 1). It is true since fix round 1, which makes that line a marker. Scenario 1 is not listed, since line 1 fixes it.
- **Measured** at both depths: the `-k` shape (rv884's `k-sub1-then-letfail`) reads `exited 3` at head, `dbd3a6dd`, `82a84f88` and `281df263`; `$(MAKE) inner || true; exit 2`, and `-$(MAKE) -s inner` then `exit 2` in a make run with `-s` (the holder's `make -s`, or `MAKEFLAGS=s` run straight), read `exited 1` at head and at `dbd3a6dd`, as the list says.
- **Test:** none, since a limit is a line that reads wrong; no test asserts a wrong code.

**Pinned copy and docs.**
- `bootstrap.py` is in `FAMILY_COPIED_VERBATIM`, so each holder takes one `upstream-changed` row for it on its next update. Its mode stays 100755, and its imports are unchanged.
- No `--help` text changed, so `docs/cli.md` is unchanged. No sentence of `AGENTS.md`, `templates/family-root/AGENTS-shape.md`, `templates/family-root/README.md` or `README.md` speaks of the make line (grep).

**Fail on older code**, with fix round 1's test file (still 57 items: the new members run inside the two `[innermost*]` cases). The NIT round's `PassesThenTerminated` reads 1 on `58e4ddf0` and `dbd3a6dd` and 2 on `281df263`, at both depths, so it adds to the same two failures:
- on `58e4ddf0`'s code (this pull request before fix round 1): 2 failed, 55 passed: `[innermost-under-make]` and `[innermost]`, through `PassesThenKilled`;
- on `dbd3a6dd`'s code: 2 failed, 55 passed: `[innermost-under-make]` and `[innermost]`, through `LetsBelow`, `PassesThenLets`, `LetsTwoBelow` and `PassesThenKilled`;
- on `281df263`'s code: 3 failed, 54 passed: `[innermost-under-make]`, `[innermost]` and `test_a_line_that_comes_in_pieces_is_read_whole`;
- at head: 57 passed.

**Mutants at head**, each run against the whole test file: 45 made, 41 caught. They are #886's fix-round mutants (its first 19, rv884's X1-X12 and N1-N5), restated where the code changed; rv884's I1, I4, I8 and I17 from its re-check of #886, with I1 and I17 reversed, since head holds them; P3 and P4 for this change's first hunks; and rv884's Q6, Q11 and Q12 from its review of this pull request, Q12 reversed, since head holds it. The members in brackets are the ones each mutant reads wrong, measured on a mutated copy at both depths, at `013b9402` (before `PassesThenTerminated`) except for the NIT round's rows.
- **The NIT round, at `41f781fd`:** 4 more runs, 3 caught. rv884's R4 (`(?:Error \d+|Killed)` only) and this round's S1 (what ended it at most 8 bytes, so `Terminated` is missed) are caught by `PassesThenTerminated` alone; Q12 reversed is caught by `PassesThenKilled` and `PassesThenTerminated`. rv884's R7 (no space before `(ignored)`) survives and is equivalent: make always prints the space, and it differs from head in none of the 230 case-modes (rv884's `sweepr7.log`); `PassesThenTerminated` reads 2 under it.

| mutant | caught by |
|---|---|
| rv884's Q12 reversed (`Error \d+` only, `58e4ddf0`'s pattern) | both `[innermost*]` (`PassesThenKilled`; at `41f781fd` also `PassesThenTerminated`) |
| rv884's R4 (`(?:Error \d+\|Killed)` only), at `41f781fd` | both `[innermost*]` (`PassesThenTerminated`) |
| S1 (`[^\n]{0,8}` for what ended it), at `41f781fd` | both `[innermost*]` (`PassesThenTerminated`) |
| rv884's Q6 (`self.depth = depth - 1`) | both `[innermost*]` (`LetsTwoBelow`) |
| rv884's Q11 (the reset only below a deeper line) | both `[innermost*]` (`InnerLets`) |
| rv884's I1 reversed (only `Error 2 (ignored)`); P4 (only `Error \d (ignored)`) | both `[innermost*]` (`PassesThenLets`, `PassesThenKilled`) |
| rv884's I17 reversed (`min(self.depth, depth)`, `dbd3a6dd`'s code) | both `[innermost*]` (`LetsBelow`, `LetsTwoBelow`) |
| rv884's I8 (`min(self.depth, depth + 1)`) | both `[innermost*]` (`LetFail`, `LetsBelow`, `LetsTwoBelow`, `PassesThenLets`, `PassesThenKilled`) |
| rv884's I4 (an `(ignored)` line clears the code) | both `[innermost*]` (`ThenLets`) |
| N2, the `(ignored)` line not read | both `[innermost*]` (`LetFail`, `LetsBelow`, `LetsTwoBelow`, `PassesThenLets`, `PassesThenKilled`) |
| N3, the `(ignored)` line sets its own depth; N5, it falls through | both `[innermost*]` (`InnerLets`, `LetsBelow`, `LetsTwoBelow`) |
| N4, `max(self.depth, depth)` | both `[innermost*]` (`InnerLets`, `LetFail`, `LetsBelow`, `LetsTwoBelow`, `PassesThenLets`, `PassesThenKilled`) |
| R1, R8, H1, H2, H4, H6, H8, H9, N1, X1, X2, X4, X7, X11, X12 | both `[innermost*]` (R1 and R8 also `[last-under-make]`; R8 also the pieces test; X1 also both `told_apart` cases) |
| H3 (`<=` for `<`), H12 | `[last-under-make]` |
| H5 | `[innermost]` |
| H7 | both `told_apart` cases, both `[innermost*]` |
| H10, H11, X3 | the pieces test |
| R7 | `test_a_member_s_question_shows_before_its_answer_is_typed` |
| R10, R11 | the matching `test_no_advice_beside_no_member_updated_with_every_member_checked_out` case |
| D1, D2 | `test_what_a_member_s_make_leaves_on_stderr_is_passed_on_as_written` |
| X8 (`splitlines`) | 7 tests |
| X10 (one-digit code) | `[signal]` |

- The 5 survivors are equivalent: rv884's R7, as above, and X5 (`\s*$` dropped; make's `***` lines carry nothing after the code), X6 (the unended remainder read at EOF; make always ends its line), X9 (passed over only one level deeper; makes nest one level at a time) and P3 (an `(ignored)` line resets the depth to 0, not -1; no line's depth is below 0, so `depth < 0` and `depth < -1` are both always false).

**Runs**, through `run9s.sh` with `SHARDS=3`, with no tracked changes. The files: `tests/test_family_holder_bootstrap_texts.py`, `tests/test_family_bootstrap_gate_order.py`, `tests/test_bootstrap_spelling.py` and `tests/test_pinned_copy_text.py`; the two that `git grep -l 'family-root/scripts/bootstrap' -- tests` names, `tests/test_family.py` and `tests/test_update_shape_kept_helper.py`; the trio, `tests/test_repo_hygiene.py`, `tests/test_agents_md_codes.py` and `tests/test_cli_reference.py`; and five more that run the holder's bootstrap: `tests/test_family_gitmodules_values.py`, `tests/test_family_head_lost_gitmodules.py`, `tests/test_family_members_not_a_list.py`, `tests/test_bootstrap_without_git.py` and `tests/test_family_makefile_siblings.py`.
- git 2.43.0, at `41f781fd`: 1178 passed, rc 0 (shards 222, 429 and 527); the texts file alone, 57 passed, rc 0.
- git 2.47.1: the texts file at `41f781fd`, 57 passed, rc 0; the full set at `013b9402`, which differs from `41f781fd` only in the texts file's new member, 1178 passed, rc 0 (the same shards).
- The count is higher than #886's 1049 because main has since added tests to these files.

**Merge checks**, `git merge-tree --write-tree` of `41f781fd`, after a fresh fetch:

| against | result |
|---|---|
| origin/main `2d372bb5`, one commit past #886's squash `e7039b71`, touching neither file | clean, tree `e55b04f3` |
| lane 4's #889, head `3b15de03`, which edits other parts of `bootstrap.py` | clean, tree `a454759a`; main and #889 merge clean too, tree `5ad6e5d1` |

- Before the restack, `d2be39dc` with the predicted squash of #886 conflicted in both files, and taking this branch's side gave tree `30ba7a3e`, the merge of `d2be39dc` with main `0d5d2b11`. On a detached commit at that tree, `3133e841`, the texts file, `tests/test_family_gitmodules_values.py` and `tests/test_pinned_copy_text.py` gave 291 passed, rc 0 (git 2.43). The restack `58e4ddf0` has that tree.

**Sonar-style checks.**
- No duplicated block: no window of 6 to 10 lines that touches a line added since `dbd3a6dd` appears anywhere else in `tests/`, `scripts/` or `templates/`, in exact text. With literals blinded, the same holds for fix round 1's and the NIT round's lines, and over the whole change since `281df263` no window of 10 or 8 lines matches; at 6 lines only the test file's import block does.
- With literals blinded, `INNERMOST`'s members matched each other in two 10-line windows at `dbd3a6dd` (rv884's note on #886); with `FINDS` and `REFUSES` shared, none do.
- No assert was added, and none joins conditions with `and`.
- Cognitive complexity, counted by hand with Sonar's rules: `MakeErrors.said` stays 6, its branches unchanged (`if not found`, `depth_said or 0`, `if ignored`, `code == b"2" and ...`, `if code and not ...`). No other function's code changed. The longest added line is 75 columns; the NIT round's longest is 74.

**Not here.**
- No Windows or macOS run, since CI is Linux only (#597). Make before 4.0 was not run.
- The `-k` shape of line 2 still names the let-fail command's code; reading it right would take a code kept per depth.

Review: rv884, lane 5's independent reviewer, not the writer (w884): READY at `d2be39dc` (issuecomment-6250054923), READY at fix round 1 `013b9402` (issuecomment-6250467939), and READY with no findings at the NIT round `41f781fd`, the head that lands (issuecomment-6250610517). CI (Brett Heap's ruling 27): H0 is the head 41f781f itself, which passed tests, tests-git-bash and SonarCloud; main has touched none of its files since its base e7039b7; and the local gate passed on its predicted squash tree 6622c40 over main 5017830: the family_holder_bootstrap_texts, repo_hygiene, agents_md_codes, cli_reference sets, 205 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 #890

Lane: openreposhape-5 (openRepoShape-5)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Oct 19, 2026
…its no jobserver from the holder's `make -jN`, so `make -j2 bootstrap`, `validate`, `park` and `resume` no longer warn `jobserver unavailable` of a rule the person does not own (#888) (#950)

#888, from rv884's checklist candidate C1 on #886 (#884), #886 issuecomment-6248806027. The change is in the family holder's pinned `templates/family-root/scripts/bootstrap.py` and `templates/family-root/scripts/siblings.py`, and in a new `tests/test_family_member_make_jobserver.py`. The holder's Makefile is unchanged. Made and measured on `50178305`, moved onto main `64464d31` (after #891's `a9205e00`) with a clean merge as `4758e645`, then round 1 (`1e94ce04`) from lane 5's review (#950 issuecomment-6253417294), READY at `1e94ce04` on lane 5's re-check (issuecomment-6254597377).

**The line.** Under the holder's `make -j2 bootstrap`, `validate`, `park` or `resume`, each member's make printed `make[1]: warning: jobserver unavailable: using -j1.  Add '+' to parent make rule.` and ran its jobs one at a time. Measured on `50178305` with GNU Make 4.3, in a holder `family.py` made, with two members it scaffolded and added, each member's Makefile giving its target two prerequisites that need nothing of each other, each writing a start and a stop line: two warnings per verb, one per member, and every member's trace serial.
- The holder's rules run `scripts/bootstrap.py` and `scripts/siblings.py` with no `+`, so make closes the jobserver's descriptors for them, and `Popen` closes the rest.
- The member's make still found `-j2 --jobserver-auth=3,4` in the `MAKEFLAGS` it inherits.
- The rule the warning names is the holder's pinned Makefile, which the person does not own.

**The fix, the issue's second direction.** `member_make_env()`, in both scripts, gives a member's make this run's environment with the jobserver word (`--jobserver-auth=` or `--jobserver-fds=`) and every `-j` word taken out of `MAKEFLAGS`, where it names a jobserver. It is used by `run_make` (`bootstrap`, and `--members-make validate`) and by `dispatch` (`--make park`, `--make resume`). Now each member's make prints nothing and runs its jobs one at a time, as it did; its flags are those of a plain run.
- Every other word is kept: `-k`, `-s`, and a variable from the holder's command line.
- `MAKEFLAGS` is split as make splits it (`MAKEFLAGS_WORD`, `(?:\\.|[^ \t])+`): only at a space or a tab, a backslash keeping either in a value. So `NOTE='a -j3'` reaches the member whole, and so does a value holding a no-break space (U+00A0), a narrow one (U+202F) or a newline, which make passes through raw (round 1, lane 5's MUST 1: round 0 split at every character Python's `\s` matches, so under `-j2` `NOTE=x<U+00A0>EXTRA=injected` reached the member as `NOTE=x` and `EXTRA=injected`, measured).
- A `MAKEFLAGS` that names no jobserver is left byte for byte, and the environment handed on is a copy: this run's own is untouched.

**Why not hand the jobserver on** (option 1, measured on 4.3): a `+` on the holder's rules with the descriptors passed through ran each member's jobs together, but a `+` line runs under `make -n` too, so `make -n bootstrap` fetched every member and leg; descriptors `MAKEFLAGS` names by number are no proof they are the jobserver's; and the member's make is `make` on PATH, not the holder's (make exports no `MAKE`), so it may not read its parent's jobserver word (4.3 stops at 4.4's `fifo:` path, exit 2). On macOS's 3.81 (`--jobserver-fds=3,4 -j`) members warned and ran serially, and now only run serially. On GNU make 4.4 and later, whose fifo jobserver a member's make may reach today, members now run one job at a time too: #987 is the follow-up for that.

**Written in both scripts, not imported,** so a holder that keeps its own `bootstrap.py` with `update-shape.py apply --accept-local` is not refused `update-accept-local-missing-name` for a new name `siblings.py` would import; the new tests hold the two copies to one answer.

**Tests:** 43 items in the new file:
- under the holder's own `make -j2 bootstrap`, `validate`, `park` and `resume`, no warning, and each member's trace serial with no `-j` in its flags;
- under `make -k -j2 bootstrap NOTE='a -j3'`, each member's flags the same as under `make -k bootstrap NOTE='a -j3'`;
- both scripts' `member_make_env` on 12 spellings of a jobserver: GNU make 4.3's (with `-j16` among them), 4.4's `fifo:`, 3.81's, Windows', and 4.3's with a value holding U+00A0, U+202F or a newline, built from Python escapes;
- both scripts on six `MAKEFLAGS` that name none, left byte for byte;
- both scripts handing back a copy, `os.environ` unchanged (round 1, lane 5's NIT 2).

On main's scripts all 43 fail (5 on the warning, 38 for want of `member_make_env`). The run: 1332 passed on 19 files at `1e94ce04`. 18 mutants, nine edits each made in both scripts, were all killed, among them round 0's splitter (3 failed in each), `-j[0-9]?` (lane 5's NIT 1) and `env = os.environ` (lane 5's NIT 2).

Each holder takes two `upstream-changed` rows, `scripts/bootstrap.py` and `scripts/siblings.py` (`FAMILY_COPIED_VERBATIM`); the Makefile row stays `unchanged`.

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

Labels

None yet

Projects

None yet

1 participant