Skip to content

the family root's bootstrap.py: an (ignored) line, for any code or 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

Merged
brettheap merged 8 commits into
mainfrom
fix/884-ignored-depth
Oct 18, 2026

Conversation

@brettheap

@brettheap brettheap commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor

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

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.

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

Sonar-style checks.

Not here.

  • No Windows or macOS run, since CI is Linux only (ci: run the test suite on 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.

Closes #890

🤖 Generated with Claude Code

Summary by Sourcery

Fix bootstrap failure parsing so ignored command results reset nested-make tracking and subsequent member failures are reported accurately.

Bug Fixes:

  • Correctly identify a member's own failure after any ignored command, including commands terminated by signals, so the reported exit code is preserved across nested makes.

Enhancements:

  • Document the remaining cases where MakeErrors cannot determine the innermost command's exit code, including a let-fail sequence under -k.

Tests:

  • Extend bootstrap parsing coverage for nested let-fail commands, shell-pass-through failures, signal termination, and multi-level make nesting.

Brett Heap and others added 6 commits October 18, 2026 09:45
…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>
…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>
…es no `Error 2` after it passed over, so a member's own refusal after a `-` one make below, or after `|| true` and a `-`, is named (#884)

NIT round on #886, from rv884's re-check (issuecomment-6249507909).

NIT 1: make's `[...] Error <code> (ignored)` line, for a `-` that lets
a command fail whatever its code, sets the depth back to what it is
before any `***` line, so no `Error 2` after it is passed over until
another `***` line comes (rv884's I1 and I17). The make that printed
it goes on, and would print a `***` line of its own before it failed,
so no such `Error 2` can be a make's report. Two shapes now read the
member's own `exit 2`: a `-` in a make below the member's (`LetsBelow`),
and a sub-make's failure that `|| true` let pass, then a `-`
(`PassesThenLets`, with `-@EXIT 10`, so a code of two digits is the
marker too). Over rv884's 183 case-modes and w884's 41, the change
differs from dbd3a6d in 5, each one now naming the member's own
`exit 2`; no case reads worse. `ThenLets` holds that an `(ignored)`
line leaves the code named before it (rv884's I4).

NIT 2: `MakeErrors`' list of limits names the `-k` shape where a `-`
lets a make fail after a command that failed: the let-fail command's
code is named. `|| true` with no `(ignored)` line after it, and the
`-s` note, stay.

`INNERMOST`'s members share `FINDS` and `REFUSES`, the finding and
refusal commands, for the seven copies the table held, so the three new
members add none.

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

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-5, 2026-10-18T12:42Z) @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-18T12:46:39.026095Z 58e4ddf 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.

@brettheap

Copy link
Copy Markdown
Contributor Author

Independent review of d2be39dc (#890, the follow-up to #886), for lane openRepoShape-5: READY

I found no MUST and no SHOULD. There are two NITs: a one-pattern widening of the marker, and a test for a three-level shape. 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/rv890/.

Findings, most severe first

NIT 1: an (ignored) line for a signal is not a marker, so one limit sentence is false for it (templates/family-root/scripts/bootstrap.py:889, and :915-919 in the limits list).

  • The scenario: $(MAKE) inner || true (inner exits 1), then -@kill -KILL $$$$, then the recipe's own exit 2.
    • make prints make[1]: [Makefile:3: bootstrap] Killed (ignored), and Terminated (ignored) for SIGTERM.
    • The pattern takes only Error \d+ (ignored), so the depth stays deep and the member's refusal reads exited 1, at both depths. 281df263 read 2 (probe-extra.log).
  • What it makes false: the limits item says the shell-passes case is wrong only "with no (ignored) line between". Here there is one, and the line is still wrong.
  • The argument behind the rule covers this line too: a make that printed (ignored) goes on, whatever ended the command.
  • Measured fix (my Q12): \[[^\n]*\] [^\n]* \((ignored)\)\s*$ in place of \[[^\n]*\] Error \d+ \((ignored)\)\s*$. It reads 2 in both signal cases and differs from head in none of the 224 case-modes (sweepq12.log). It survives the tests, and a person would see it only as a line that becomes right.
  • How rare: it needs a - in front of a command that a signal ends.

NIT 2 (test gap): mutant Q6, which resets the depth to one above the ignoring make, survives all 57 tests. bootstrap.py:956 changed to self.depth = depth - 1.

  • LetsBelow puts the - one make below the member, and Q6 reads that right.
  • With the - two makes below (member → mid → mid2, which runs -$(MAKE) inner, then the member's own exit 2), Q6 reads exited 1. Head reads 2, as does 281df263 (letfail-three-levels-then-own2).
  • A person would see it only with a three-level recursion. Making LetsBelow one level deeper would catch it.

Checked and true

1. The code against my patch, and the sweep:

  • bootstrap.py at d2be39dc and my i117-bootstrap.py have identical ASTs once docstrings are dropped; only docstrings and comments differ.
  • The sweep: my 183 case-modes plus w884's 41, at both depths, on d2be39dc, dbd3a6dd and 281df263: 224 case-modes, 672 runs (sweep.log).
    • Head differs from dbd3a6dd in 5: Scenario 1 and Scenario 2 at both depths, and own-fake-ignored run straight.
    • In each, head reads the member's own exit 2, as 281df263 did.
    • No case reads worse than at dbd3a6dd.

2. The tests:

  • The new members: each of the 14 INNERMOST members was run at both depths on head and on each mutant copy (probe-innermost.log). Head reads all 14 right, and each mutant is caught by the member the brief names:
mutant caught by
I1 reversed (Error 2 only) PassesThenLets
P4 (\d) PassesThenLets
I17 reversed (min) LetsBelow
I4 (code cleared) ThenLets
  • FINDS and REFUSES: evaluating both tables, all 11 earlier members' bytes are identical to dbd3a6dd's.
  • Fail on older code, with this round's test file:
code under test failed passed failing tests
dbd3a6dd 2 55 both [innermost*]
281df263 3 54 both [innermost*] and the pieces test
head 0 57 none

3. The docstrings: every limit is true as measured at both depths on head.

shape reads
the -k shape (a: $(MAKE) innerA exits 1, then b: -$(MAKE) innerB exits 3) 3
|| true 1
; 1
- under make -s 1
env -u MAKELEVEL 2
-j interleaving 2
x[3]: *** y none named
  • The new paragraph's "however far below it the - was" is true: the three-level case reads 2.
  • The module docstring's (b) is unchanged and still true.

4. Mutants:

mutant result
I1 reversed, I17 reversed, I4, P4 caught
my Q11 (reset only after a deeper line) caught
P3 (self.depth = 0) survives; equivalent, see below
my Q6 survives; NIT 2
my Q12 survives; NIT 1
  • P3 is equivalent: self.depth is read only at :958, in depth < self.depth. depth is never below 0 (int(depth_said or 0)), so the test is false whether self.depth is 0 or -1. Measured: P3 differs from head in 0 of 230 case-modes.

5. Runs at d2be39dc:

run git result
texts file 2.43 57 passed, rc 0
texts file 2.47.1 57 passed, rc 0
7-file set 2.43 532 passed, rc 0
  • w884's logs: r2/logs/gate243.log and gate247.log each read 218+394+437 = 1049 passed, rc 0, head d2be39dc, no tracked changes.

6. The restack:

7. #889: d2be39dc with refs/pull/889/head (8ea0c44e) is clean, tree 39cca5ae. The restack commit with #889 is clean too.

8. Sonar-style checks:

  • Exact text: no duplicated window of 6 to 10 lines.
  • With literals blinded: the windows inside INNERMOST are gone. Nothing at 10 or 8 lines; at 6 lines only the import block.
  • The only or assert is the old one (:874), and no assert joins conditions with and.
  • said stays at 6.
  • The longest new lines are 76 columns in bootstrap.py and 78 in the tests.

Could not verify

  • SonarCloud's real verdict, Windows, macOS, make before 4.0, and CI's git 2.55.0.

Checklist candidates

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

Scratch left

  • Worktrees: rv890-head, rv890-mut and rv890-restack are removed. My refs refs/l5rv890/br and refs/l5rv890/pr889 are deleted.
  • An unreferenced commit: 9b5bc1a3 (the restack probe, made with a throwaway identity).
  • Basetemps (reused): $Q/v110/bt/rv884-{h243,h247,base,mut,m877}.
  • Probes, mutant copies and logs: $Q/v118/rv890/.
  • Processes: none of mine are running. The one pytest still listed belongs to another session.

@sourcery-ai

sourcery-ai Bot commented Oct 18, 2026

Copy link
Copy Markdown

Reviewer's Guide

Corrects MakeErrors attribution for ignored failures by recognizing ignored lines regardless of exit code and resetting depth state, ensuring a member’s later exit 2 is reported instead of an inner failure. The tests add focused nested-make and let-fail scenarios while documenting a remaining -k ambiguity.

Sequence diagram for ignored make failures and exit attribution

sequenceDiagram
    participant Bootstrap as bootstrap.py
    participant MemberMake as Member make
    participant InnerMake as Inner make
    participant Errors as MakeErrors

    MemberMake->>InnerMake: -$(MAKE) inner
    InnerMake-->>MemberMake: Error 1 (ignored)
    MemberMake->>Bootstrap: stderr line
    Bootstrap->>Errors: said(Error 1 (ignored))
    Errors-->>Bootstrap: self.depth = -1
    MemberMake->>MemberMake: exit 2
    MemberMake-->>Bootstrap: Error 2
    Bootstrap->>Errors: said(Error 2)
    Errors-->>Bootstrap: report member exit 2
Loading

File-Level Changes

Change Details Files
Make stderr parsing now treats every ignored-error line as a marker and resets nesting state so later member-owned failures are attributed correctly.
  • Broadened the ignored-error regex to accept any numeric error code.
  • Reset parser depth to -1 on ignored lines, preventing stale nesting state from masking subsequent Error 2 reports.
  • Updated parser documentation to explain the corrected attribution behavior and remaining ambiguous cases.
templates/family-root/scripts/bootstrap.py
Expanded regression coverage for nested makes, let-fail commands, and shared failure fixtures.
  • Added reusable finding/refusal recipe fixtures.
  • Added scenarios covering nested let-fail commands, shell-passed failures, and finding-then-let-fail ordering.
  • Updated expected member outcomes and consolidated repeated recipe text.
tests/test_family_holder_bootstrap_texts.py

Assessment against linked issues

Issue Objective Addressed Explanation
#890 Reset make-error depth to -1 whenever an (ignored) line is seen, regardless of its error code, so a member’s later exit 2 is correctly reported after either a deeper --ignored sub-make or a failure passed through ` true` followed by another ignored command.
#890 Document the remaining -k limitation where a later --ignored sub-make’s error code is reported instead of the earlier failing command’s code. ✅

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: 58e4ddf071

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

Comment on lines 1083 to +1084
if ignored:
self.depth = min(self.depth, depth)
self.depth = -1

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 Preserve nesting after same-depth ignored errors

When a recursive member target has already failed and a later - recipe is ignored in that same sub-make—for example, bootstrap invokes $(MAKE) -k inner, where one prerequisite exits 1 and a subsequent prerequisite runs -false—GNU make prints a depth-1 Error 1, a depth-1 (ignored) line, and then the parent's depth-0 Error 2. Resetting the depth unconditionally makes that final parent report overwrite code with 2, so family bootstrap reports the member's finding as an exit-2 refusal. A same-depth ignored line must preserve the outstanding failure nesting; only an ignored line that closes a deeper failure chain should reset it.

Useful? React with 👍 / 👎.

…er too, so a member's own `exit 2` after a `-` command a signal ends reads `exited 2`, and a `-` two makes below is tested (#890)

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

NIT 1: make prints `[...] Killed (ignored)` (or `Terminated (ignored)`)
where a signal ends a `-` command, and the marker took only
`Error <code> (ignored)`. `MAKE_FAILED_RE` now takes any `[...] <text>
(ignored)` line (rv884's Q12), so `$(MAKE) inner || true`, then
`-@Kill -KILL $$`, then the recipe's own `exit 2` reads `exited 2`, as
`281df263` read it, and the limits' "with no `(ignored)` line between"
is true for it. Over the 224 case-modes and rv884's three extra cases,
at both depths, it differs from `58e4ddf0` only in the two signal cases,
each at both depths, each now naming the member's own `exit 2`.
`PassesThenKilled` holds it.

NIT 2: `LetsTwoBelow` puts the `-` two makes below the member's own
`exit 2` (the member, `mid`, then `mid2`, which runs `-$(MAKE) inner`),
so a depth reset to one above the ignoring make (rv884's Q6) is caught;
`LetsBelow` keeps the one-make shape.

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-18T13:16:58Z) Fix round 1 for #891 (#890), from rv884's review (issuecomment-6250054923): head 013b940241bc615075c9c5120eaef004242076e6

One commit on 58e4ddf0, the restack, touching templates/family-root/scripts/bootstrap.py and tests/test_family_holder_bootstrap_texts.py only.

NIT 1, adopted: a signal's (ignored) line is a marker too (rv884's Q12).

  • MAKE_FAILED_RE's (ignored) alternative is now \[[^\n]*\] [^\n]* \((ignored)\)\s*$, rv884's Q12 exactly, so Killed (ignored) and Terminated (ignored) reset the depth as Error <code> (ignored) does. The comment and MakeErrors' docstring say "whatever ended it". Head's code is 58e4ddf0's with that one line changed, checked by comparing ASTs without docstrings.
  • Measured before adopting, on 58e4ddf0 and its copy with Q12, over the 224 case-modes and rv884's three new cases (rv890/tools/extra.py), each at both depths: 230 case-modes, 460 runs.
    • Q12 differs from 58e4ddf0 in 4: ortrue-then-ignored-kill-then-own2 and ortrue-then-ignored-term-then-own2, each at both depths. In each, 58e4ddf0 names 1 and Q12 names 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).
  • The limits sentence: "with no (ignored) line between" is now true for the signal's line, since that line is a marker. The sentence is unchanged.
  • Test: PassesThenKilled, which is $(MAKE) inner || true, then -@kill -KILL $$, then the recipe's own refusal. It expects exited 2 at both depths, and kills Q12 reversed (Error \d+ only).

NIT 2: LetsTwoBelow catches Q6.

  • LetsTwoBelow is Scenario 1 one make deeper: the member runs $(MAKE) mid, then its own refusal; mid runs $(MAKE) mid2; mid2 runs -$(MAKE) inner; and inner exits 1. It expects exited 2.
  • Q6 (self.depth = depth - 1) reads it as 1 at both depths, so Q6 is caught. LetsBelow keeps the one-make shape as rv884 first gave it.

Each member at both depths (the two agree), on the new head and three older codes:

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

The other 11 members read at head as at dbd3a6dd and 58e4ddf0.

Fail on older code, with this round's test file (still 57 items):

  • on 58e4ddf0's code: 2 failed, 55 passed ([innermost-under-make] and [innermost], through PassesThenKilled);
  • on dbd3a6dd's code: 2 failed, 55 passed (the same two, through LetsBelow, PassesThenLets, LetsTwoBelow and PassesThenKilled);
  • on 281df263's code: 3 failed, 54 passed (the same two and test_a_line_that_comes_in_pieces_is_read_whole);
  • at head: 57 passed.

Mutants at head: 45 made, 41 caught, each against the whole texts file: the 42 from #886's NIT round, restated where the pattern changed, and rv884's Q6, Q11 and Q12, with Q12 reversed, since head holds it.

Runs at 013b9402, through run9s.sh with SHARDS=3, on the body's 14 files, with no tracked changes:

  • git 2.43.0: 1178 passed, rc 0 (shards 222, 429 and 527);
  • git 2.47.1: 1178 passed, rc 0 (shards 222, 429 and 527).
  • The count is higher than the 1049 at d2be39dc because main's tests in these files came in with the restack.

Merge checks (git merge-tree --write-tree, after fetching with -c gc.auto=0 -c maintenance.auto=false):

Sonar-style checks.

  • No duplicated window of 6 to 10 lines touches fix round 1's added lines, in exact text or with literals blinded.
  • No assert was added. said's cognitive complexity stays 6.
  • The longest added line is 75 columns.
  • The body is updated as body-891-r1.

Lane: openreposhape-5 (openRepoShape-5)

@brettheap brettheap changed the title the family root's bootstrap.py: an (ignored) line of any code resets the depth fully, so a member's own exit 2 after a - one make below, or after || true and a -, reads exited 2, and the limits name the -k let-fail shape (#890) the family root's bootstrap.py: an (ignored) line, for any code or 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) Oct 18, 2026
@brettheap

Copy link
Copy Markdown
Contributor Author

Re-check of #891 (#890)'s fix round 1 at 013b9402, for lane openRepoShape-5: READY

Both of my NITs are closed. I found no MUST and no SHOULD; the only new item is one tiny NIT about a test. 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/rv891b/.

Findings, most severe first

NIT 1 (test only): only SIGKILL's (ignored) line is tested, so my mutant R4 survives.

  • R4 changes the pattern at templates/family-root/scripts/bootstrap.py:1017 to \[[^\n]*\] (?:Error \d+|Killed) \((ignored)\)\s*$. It passes all 57 tests.
  • A person would see it with $(MAKE) inner || true, then -@kill -TERM $$$$, then the recipe's own exit 2: R4 reads exited 1 at both depths, where head reads 2 (p3/).
  • The fix: a PassesThenTerminated member, or -TERM in place of -KILL in PassesThenKilled, would catch it.
  • How rare: it needs a - command that SIGTERM ends.

Checked and true

1. The restack 58e4ddf0:

2. NIT 1, closed:

  • The code: head's AST, docstrings dropped, differs from 58e4ddf0's only in the MAKE_FAILED_RE constant, which is my Q12 pattern exactly.
  • The new member: PassesThenKilled reads exited 2 at both depths on head and 1 on 58e4ddf0. Q12 reversed (Error \d+ only) is caught by both [innermost*], and by that member alone (probe-innermost.log).
  • The sweep: 230 case-modes, 690 runs, on 013b9402, 58e4ddf0 and 281df263 (sweep.log).
    • Head differs from 58e4ddf0 in 4: the Killed (ignored) and Terminated (ignored) shapes, each at both depths.
    • In each, head reads the member's own exit 2, as 281df263 does.
    • No case reads worse, and no run errored.

3. NIT 2, closed:

  • LetsTwoBelow catches Q6 at both depths, and it is the only member that does.
  • The 11 members from dbd3a6dd and all 14 from 58e4ddf0 are byte-identical at head; 2 members are new.
  • Head reads all 16 members right at both depths: 32 runs.

4. Mutants:

mutant result
Q6 caught, by LetsTwoBelow
Q11 caught, by InnerLets
Q12 reversed caught, by PassesThenKilled
my R4 (Error or Killed only) survives; NIT 1
my R7 (no space before (ignored)) survives; equivalent: 0 of 230 case-modes differ from head
  • On 58e4ddf0's code, this round's test file gives 2 failed (both [innermost*]) and 55 passed, as the body says.

5. Runs at 013b9402:

run git result
texts file 2.43 57 passed, rc 0
texts file 2.47.1 57 passed, rc 0
7-file set 2.43 532 passed, rc 0
  • w884's logs: r3/logs/gate243.log and gate247.log each read 222+429+527 = 1178 passed, rc 0, head 013b9402, no tracked changes. This matches the body.

6. Merges:

#889 head merge with 013b9402
8ea0c44e clean, tree d74f0c1b (as the body says)
3b15de03 (now) clean, tree 8dda1027

7. Sonar-style checks on the round's bytes:

  • No duplicated window of 6 to 10 lines, in exact text or with literals blinded.
  • No assert added; the only or assert is the old one (:881).
  • said stays at 6, since only the pattern changed.
  • The longest new lines are 73 columns in bootstrap.py and 75 in the tests.

Could not verify

  • SonarCloud's real verdict, Windows, macOS, make before 4.0, and CI's git 2.55.0.

Scratch left

  • Worktrees: rv891b-head and rv891b-mut are removed. My refs refs/l5rv891b/br and refs/l5rv891b/pr889 are deleted.
  • Basetemps (reused): $Q/v110/bt/rv884-{h243,h247,base,mut}.
  • Probes, mutant copies and logs: $Q/v118/rv891b/.
  • Processes: none of mine are running.

…ds is tested beside SIGKILL's, so the `(ignored)` marker taking only `Error <code>` and `Killed` fails (#890)

NIT round on #891, from rv884's re-check (issuecomment-6250467939).

Only SIGKILL's `(ignored)` line was tested, so rv884's R4, the pattern
`\[[^\n]*\] (?:Error \d+|Killed) \((ignored)\)\s*$`, passed all 57 tests.
`PassesThenTerminated` is `$(MAKE) inner || true`, then `-@Kill -TERM $$`,
then the recipe's own refusal, and expects `exited 2`; R4 reads it as 1
at both depths and is caught. The other 16 members' bytes are unchanged.

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

Copy link
Copy Markdown

@brettheap

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-5, 2026-10-18T13:39:17Z) NIT round for #891 (#890), from rv884's re-check (issuecomment-6250467939): head 41f781fd217dcbf51ea79ca65d1c9cb20c44f3fb

One commit on 013b9402, touching tests/test_family_holder_bootstrap_texts.py only; bootstrap.py is unchanged.

NIT 1: SIGTERM's (ignored) line is tested.

  • PassesThenTerminated joins INNERMOST: $(MAKE) inner || true, then -@kill -TERM $$, then the recipe's own refusal, expecting exited 2. make prints Terminated (ignored) for the - command.
  • The other 16 members' bytes are unchanged, checked by evaluating both tables.
  • Measured at both depths (the two agree): it reads 2 at head and on 281df263, and 1 on 58e4ddf0 and dbd3a6dd. Head reads all 17 members right.
  • The test file still has 57 items, since the member runs inside the two [innermost*] cases.

Mutants at head (41f781fd), each run against the whole texts file:

mutant result
rv884's R4, \[[^\n]*\] (?:Error \d+|Killed) \((ignored)\)\s*$ caught by both [innermost*], through PassesThenTerminated alone
rv884's R7, no space before (ignored) survives; equivalent, as rv884 found: make always prints the space, and no member reads differently under it
Q12 reversed, Error \d+ only caught by both [innermost*], through PassesThenKilled and PassesThenTerminated
S1 (this round's), [^\n]{0,8} for what ended the command, so Terminated is missed caught by both [innermost*], through PassesThenTerminated alone
  • The members were found by running each mutated copy over all 17 members at both depths.

Runs, with no tracked changes:

  • texts file, git 2.43.0, at 41f781fd: 57 passed, rc 0;
  • texts file, git 2.47.1, at 41f781fd: 57 passed, rc 0;
  • the 14-file set through run9s.sh with SHARDS=3, git 2.43.0, at 41f781fd: 1178 passed, rc 0 (shards 222, 429 and 527).

Merge checks (git merge-tree --write-tree, after fetching with -c gc.auto=0 -c maintenance.auto=false):

Sonar-style checks. No duplicated window of 6 to 10 lines touches the new lines, in exact text or with literals blinded. No assert was added. The longest new line is 74 columns.

The body is updated as body-891-r2. It names this commit in the restack paragraph, has the new #889 and main rows, and adds the member, the mutants and the runs.

Lane: openreposhape-5 (openRepoShape-5)

@brettheap

Copy link
Copy Markdown
Contributor Author

Re-check of #891 (#890)'s NIT round at 41f781fd, for lane openRepoShape-5: READY

My last NIT is closed and I found nothing new. I changed no branch, pushed nothing and posted nothing. $Q = /tmp/claude-1000/-workspace-projects-openRepoShape/50cede29-ab82-4ca3-9f67-d98e1a58c8d2/scratchpad. Logs are in $Q/v118/rv891c/.

Findings

None.

Checked and true

  1. Scope: 41f781fd is one commit on 013b9402, touching only tests/test_family_holder_bootstrap_texts.py (6 lines added, 2 removed).

    • bootstrap.py is byte-identical to 013b9402's; git diff --quiet is clean on it.
    • The longest new line is 74 columns.
  2. The new member:

    • PassesThenTerminated is $(MAKE) inner || true, then -@kill -TERM $$$$, then the recipe's own refusal.
    • It reads exited 2 at both depths at head, and 1 on 58e4ddf0's code.
    • All 17 members read right at both depths: 34 runs (probe-innermost.log).
    • My R4 (Error \d+ or Killed only) is caught at both depths, by PassesThenTerminated alone.
    • R7 still reads every member right; it is equivalent.
    • The other 16 members are byte-identical to 013b9402's, compared by evaluating both tables.
  3. Runs at 41f781fd:

run git result
texts file 2.43 57 passed, rc 0
texts file 2.47.1 57 passed, rc 0
  • w884's r4/logs/gate243.log reads 222+429+527 = 1178 passed, rc 0, head 41f781fd, no tracked changes. I did not re-run it.
  1. The body's merge rows match a fresh git merge-tree:
merge body fresh
41f781fd with origin/main 2d372bb5 clean, e55b04f3 clean, e55b04f3
41f781fd with #889 3b15de03 clean, a454759a clean, a454759a
main with #889 clean, 5ad6e5d1 clean, 5ad6e5d1

Scratch left

  • Worktree: rv891c-head is removed. My refs refs/l5rv891c/br and refs/l5rv891c/pr889 are deleted.
  • Basetemps (reused): $Q/v110/bt/rv884-{h243,h247}.
  • Probes and logs: $Q/v118/rv891c/.
  • Processes: none of mine are running.

@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 1 day and 9 hours by commenting @sourcery-ai review. Upgrade to get a review now.

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>
brettheap pushed a commit that referenced this pull request Oct 22, 2026
…eviews and the Known limits continuation, with lanes 2, 4 and 5's items: eleven sites left to #1110, the doctor's 000 and 444 mount rows, the #1082 upgrade warning and the #891 and #1103 limits said as main's code does, #1112 and lane 2's two doctor limits named, and the shape copies measured (#921)

The fix round on RV1155a, RV1155b, RV1155c and RV1155d, the continuations
RV1155d2 and RV1155a2, and the comments lanes openRepoShape-4 and
openRepoShape-5 left on #1155:

- RV1155c's two MUSTs: `spec/` at 000 is `cannot be listed` and at 444
  `spec/.git: cannot be read`; `scripts/` at 444 is
  `INVALID (manifest, pins)` on 3.14 as on 3.12.
- Every SHOULD (a 3, b 2, c 3, d 13) and every NIT but RV1155a's NIT 7,
  RV1155a2's eight among them.
- Where lane 5 and a reviewer word one line, one text covers both:
  #891 (d SHOULD 6, lane 5 SHOULD 7), #1103 (d SHOULD 7, lane 5
  SHOULD 4) and #1090 (c's first NIT, lane 5 NIT 14).
- KNOWN_OPEN in tests/test_path_guard_hygiene.py holds eleven sites, so
  both lines say eleven.
- #1112 is a known limit; lane 2's two doctor limits (#975) and its
  test-only gaps (#975, #1050) are listed, and the "INVALID, not DRIFTED"
  sentence holds for a mount this user cannot list or read.
- The shape-copy measurement fills its placeholder; the "Before tagging"
  pointer is gone.
- The pipx claim carries RV1155d's qualifier everywhere it is made.
- v1.0.0's tab-in-a-pin line is closed (#1015); its bare number or
  boolean in a plan's text field is narrowed (#1016).
- `update-shape.py check` is named with `--at <release-commit>`, and the
  measurement says main `038dafb` was the upstream.

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