Repository navigation
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
Conversation
…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>
|
(lane openRepoShape-5, 2026-10-18T09:59Z) @codex review |
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
Reviewer's GuideUpdates the pinned family-root bootstrap implementation to parse GNU Make stderr incrementally and depth-aware, so nested and Sequence diagram for streaming make stderr and reporting the innermost failuresequenceDiagram
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
Flow diagram for depth-aware GNU Make error parsingflowchart 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
File-Level Changes
Assessment against linked issues
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
💡 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 |
There was a problem hiding this comment.
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 👍 / 👎.
|
Independent review of #886 (#884) at 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. Findings, most severe firstSHOULD 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:
NIT 1:
NIT 2: the docstrings state the innermost rule with no conditions, and the limits are only in the PR body (
NIT 3: the signal sentence in the
NIT 4 (PR body only):
Checked and trueLine 2, the reading. GNU Make 4.3. Each case ran under the holder's
Line 2's signal docstring:
Line 3, on a pty (
Line 1:
The pty test:
Fail on base: the head test file on Runs at
Merge checks (
Pinned copy and docs:
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
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>
|
(lane openRepoShape-5, 2026-10-18T11:15:47Z) Fix round 1 for #886 (#884), from rv884's review (issuecomment-6248806027): head One commit on SHOULD 1, X7, X11 and X12. NIT 1, the first match on a line. NIT 2, the limits in the docstrings.
NIT 3, the signal sentence. It is now true of rv884's 24 cases in its
NIT 4, the body. Tests. Still 57 items;
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
Merge checks after a fresh fetch:
Lane: openreposhape-5 (openRepoShape-5) |
|
Re-check of #886 (#884)'s fix round 1 at All five of my first-round findings are closed. I found no MUST and no SHOULD. The new Findings, most severe firstNIT 1: the
NIT 2 (docstring only): the list of limits in
Checked and trueMy first-round findings:
The
All my earlier cases: over 133 case-modes, head differs from Chunking: 250 streams in 137,515 chunkings, none differing from a whole read. Mutants at
Runs at
Fail on older code, with the round's test file:
w884's logs: 218+394+437 = 1049 passed on 2.43 and on 2.47.1, head Main: the merge with Sonar-style checks:
A note on the body's evidence: Could not verify
Checklist candidates
Scratch left
|
|
|
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 |
Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…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>
…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>
…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>



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.pyand intests/test_family_holder_bootstrap_texts.py, and nothing else.Base: origin/main
281df263, where #876 has landed as41baf920. Each line was probed there first, under the holder's ownmake bootstrap, with GNU Make 4.3, dash 0.5.12 as/bin/shand git 2.43.0. The probes reuse rv876'sp2/ptyrun.pyand 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.
credential_adviceignoringunreadableorrefused):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.gitmodulesis givenupdate = Noneor 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.pyasks only the last two lines, which is why the mutants passed there.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-fetchruns under a real parentmake -k, as the holder'smake bootstrapruns it, so the member's make printsmake[1]:. One member exits 1. Another has two commands that exit 1 and then 2, and the line must name the last.passed_on'ssink.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 asksContinue?on stderr andreads the answer. The test typesyesonly 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 on281df263it showed only at 8.1 s, after the blind answer, when the run ended.test_a_git_mv_staged_with_work_in_the_checkout_is_undone_as_printednow 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 withtest_a_checkout_moved_on_at_the_variant_is_moved_with_f's own store assert.281df263, with rv876's ownmut/mutants.pyunchanged, every one of the five is caught by the test named above, and those tests pass on the unmutated code:281df263named[0][last-under-make]sink.flush()test_a_member_s_question_shows_before_its_answer_is_typed^make:[last-under-make]unreadabletest_no_advice_beside_no_member_updated_with_every_member_checked_out[unparsed]refusedtest_no_advice_beside_no_member_updated_with_every_member_checked_out[value-refused]2. The final line misled in three cases (
run_make,MakeErrors).281df263and on this head, each member under the holder'smake bootstrapand withbootstrap.pyrun straight (-kwhere the case needs it). The last five rows came from rv884's review:281df263$(MAKE) inner, inner command exits 1exited 2exited 1$(MAKE) inner, inner command exits 2exited 2exited 2$(MAKE) nothere, the inner make's own failureexited 2Error <code>line for a command of the member's$(MAKE) -s nothere; exit 3exited 3exited 3make -k:Error 1, then 5000 bytes on stderrError <code>line (false)exited 1make -k: two commands exit 1, then 3exited 3exited 3exited 137exited 137kill -TERM $$in a recipeError <code>line$(MAKE) -s nothere || exit 1exited 1exited 1$(MAKE) -k inner, whose second target prints after the innerError 1exited 2exited 1make -k: a finding, then a command thatkill -KILL $$endsexited 1exited 1-$(MAKE) inner(inner exits 1), then the recipe's ownexit 2exited 2exited 2(82a84f88readexited 1)printf 'x: *** y' >&2; exit 1, so make's line follows it on one lineexited 1exited 1(82a84f88named none)make bootstrap exited 2, and the wrong-case move'sgit rm --cachedmay 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.make[N]:, ormake:for 0).Error 2at 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.No rule to make target), no command's code is named, and the line is the one for make's own failure.-kname the last, as before.-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 ownError 2after 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 (itstools/cases1.pytocases6.py, at both depths), head with it differs from82a84f88only in that case and in the last-match case below. Across 21 more of mine it differs only in that case, in-$(MAKE) notherethenexit 2, and in the last-match case, each now as281df263read 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 anexit 2, readsexited 2).x: *** y, followed by make'sError 1on the same line, named no code at82a84f88.saidnow takes the last match on a line (findall), and that case readsexited 1, as281df263read it.MakeErrors' docstring, not only in this body (rv884's NIT 2):|| true, or;), or with a-in a make run with-s(which prints no(ignored)line), and then exits 2 itself: thatError 2is read as the inner make's report, and the inner command's code is named. Measured:$(MAKE) inner || true; ...; exit 2and-$(MAKE) innerunder the holder'smake -sreadexited 1;281df263readexited 2;env -u MAKELEVEL, which prints its parent's depth: the parent'sError 2is named, as on281df263;-j, a job of the parent's that fails while the inner make is still running: its line comes between, and the parent'sError 2is named, as on281df263;x[3]: *** y) before it exits 2 itself: itsError 2is passed over.Errorline:run_makekept the last 4096 bytes of make's stderr and read that tail at the end.MakeErrorsnow 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'stools/chunks.py, pointed at this head, fed 174 captured streams toMakeErrorsin 81,406 chunkings; none read differently from the whole.probe2.log: dash 0.5.12 and bash 5.2.21; a command run with no shell, one command,kill $$, a command thenecho, a command thenexit $?,echothen a command; SIGKILL and SIGTERM; at both depths).KilledorTerminated, and names no code. That is: a command make runs with no shell; the shell akill -KILL $$kills; or a command bash runs in its own place, which is a line's last command, a line of one command included.KilledorTerminated, and exits 137 or 143, which make names. dash outlives every command; bash outlives one that is not a line's last.; echo afterends the recipe with 0.Error 137, and a command may also exit 137 by itself.test_the_line_names_the_code_of_the_member_s_innermost_command, with four sets:[innermost-under-make]and[innermost]runINNERMOST'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 anexit 2of its own, thenexit 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/shis:sh -c 'kill -KILL $$'; exit $?, which the shell outlives (137), andkill -KILL $$, which kills the shell (no code).make:.test_a_line_that_comes_in_pieces_is_read_whole(the pieces test) feedsMakeErrorsanErrorline cut in two, and a line ended by no newline, which stays atLINE_KEPTbytes.test_a_finding_a_refusal_and_make_s_own_failure_are_told_apart(depth 0, no-k) passes unchanged. It now shareswith_membersand 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.281df263and head (documented)test -t 2in the membererr 1afterout 4, another all sixoutlines firstsleep 6 &( sleep 1; echo late >&2 ) &lateshownlatelost, and the writer died at that write: its status file was never writtencafand 0xE9)2>log), as the docstring says.Error <code>line. Measured:make --debug=jprints 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 readmake bootstrap exited 2, and the wrong-case move'sgit rm --cachedmay refuse #839, which this change keeps closed: make still runs.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, thenlate, before the run ends;stderr: no terminalandstdout: a terminalfrom the member.Pinned copy and docs.
bootstrap.pyis inFAMILY_COPIED_VERBATIM, so each holder takes oneupstream-changedrow for it on its next update. Its mode stays 100755.repo_shapeare unchanged, so the skew rule (tests/test_pinned_copy_text.py) andtests/test_update_shape_kept_helper.pyhold.--helptext changed, sodocs/cli.mdis unchanged.AGENTS.md,templates/family-root/AGENTS-shape.md,templates/family-root/README.mdorREADME.mdspeaks 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. On82a84f88's code (this pull request before fix round 1): 2 failed, 55 passed.281df263, the 3 failures are the tests that pin line 2's change:[innermost-under-make],[innermost]andtest_a_line_that_comes_in_pieces_is_read_whole. On82a84f88, the 2 failures are both[innermost*].281df263by design:[last-under-make](R1, R8), the pty test (R7) and bothtest_no_advice_beside_no_member_updated_with_every_member_checked_outcases (R10, R11);[signal],test_what_a_member_s_make_leaves_on_stderr_is_passed_on_as_written, and the store assert in bothtest_a_git_mv_staged_with_work_in_the_checkout_is_undone_as_printedcases.INNERMOSTmember, run straight with-k(depth 0):281df26382a84f88RecurseRecurseRefuseInnerOwnInnerThenOwnInnerThenFinding(X12)LongKeepsGoing(X11)ThenKilled(X7)LetFailInnerLetsOwnStars82a84f88with each mutant, read their member wrong:ThenKilledsays none,KeepsGoingsays 2 andInnerThenFindingsays 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).Errorline kept[last-under-make], both[innermost*]sink.flush()^make()?:[last-under-make], both[innermost*], the pieces testtest_no_advice_beside_no_member_updated_with_every_member_checked_outcase***line with no code not read; H9 the 4096-byte tail again[innermost*]<=for<in the depth test[last-under-make], both[innermost*]make:read as depth 9[innermost]told_apartcases, both[innermost*][last-under-make]!=), X2 (depth only on coded lines), X4 (depth group dropped), X7, X11, X12[innermost*](X1 also bothtold_apartcases)[:LINE_KEPT])splitlines)[signal][innermost*](OwnStars)(ignored)line not read[innermost*](LetFail)(ignored)line always sets the depth; N5, it falls through[innermost*](InnerLets)maxformin[innermost*](LetFail,InnerLets)Runs, each through
run9s.shwithSHARDS=3at 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.pyandtests/test_pinned_copy_text.py; the two thatgit grep -l 'family-root/scripts/bootstrap' -- testsnames,tests/test_family.pyandtests/test_update_shape_kept_helper.py; the trio,tests/test_repo_hygiene.py,tests/test_agents_md_codes.pyandtests/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.pyandtests/test_family_makefile_siblings.py.dbd3a6dd.dbd3a6dd.Merge checks,
git merge-tree --write-treeof this head, after a fresh fetch:71f36fe0, where lane 3's #877 has landed asc9ca9a45, editing other parts ofbootstrap.py8b5826db201c6f09, after #865, #871 and #815 too8c3d8e13; on a commit at that tree, this test file,tests/test_family_gitmodules_values.pyandtests/test_pinned_copy_text.pygave 291 passed, rc 0 (git 2.43, rv884's re-check, #886 issuecomment-6249507909)35b214b2b80a2ba0Sonar-style checks.
tests/,scripts/ortemplates/. At 6 lines, only the import block matches another file.and. The oneorassert in the file is the family root's bootstrap.py and validate-family.py: the credential advice and a member's exit code say what happened, and the gate'sgit rm --cached, guardedmvand section lines type as printed where they failed (#839) #876's.defline had to stay as it was.saidcounts each boolean sequence:depth_said or 0,code == b"2" and ..., andcode and not ....281df26382a84f88run_makeMakeErrors.readMakeErrors.saidread_until(test)made,with_members(test)test_the_line_names_the_code_of_the_member_s_innermost_commandtest_a_finding_a_refusal_and_make_s_own_failure_are_told_aparttest_a_git_mv_staged_with_work_in_the_checkout_is_undone_as_printedNot here.
Errorline keep make 3.81's form by its source, but the line-2 test already asserts make 4's quoting.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:
make -kerror codes even when additional stderr exceeds the previous buffer limit.Enhancements:
Tests:
make -k, signals, partial stderr lines, prompts, forwarded output, and credential-advice edge cases.