Skip to content

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's git rm --cached, guarded mv and section lines type as printed where they failed (#839) - #876

Merged
brettheap merged 25 commits into
mainfrom
fix/839-holder-bootstrap-texts
Oct 18, 2026

Conversation

@brettheap

@brettheap brettheap commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor

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

Stacked on #819 (fix/768-gitmodules-values, 233a334e); retarget to main once it lands.

#839's seven lines, in the family holder's two pinned scripts, templates/family-root/scripts/bootstrap.py and templates/family-root/scripts/validate-family.py. Each line was probed on 233a334e first; one already held there (5).

The lines

1. The credential advice beside "not for want of a credential" (fetch_members).

  • Before: "If the members are PRIVATE, set SHAPE_LEGS_TOKEN ..." was printed for every failed git submodule update, and then "NOT CHECKED OUT, and not for want of a credential" for every row unfetchable named.
  • Now credential_advice prints it only where git asked for a credential. It is not printed where git's init died at a gitlink it has no url for (stopper), or where git read nothing from .gitmodules: a file a merge left unmerged, a value git dies on, or a file it cannot parse. Git fetched nothing in those states.
  • Beside a denial, the advice reads "If the members the next line does not name are PRIVATE". This is the Direction's "both kinds" case.
  • One step past the Direction's literal rule: where every member is checked out and a leg's fetch failed for want of a credential, no row is denied and the advice stands. The token's name is SHAPE_LEGS_TOKEN, and the legs are what it is for.
  • Tests: test_no_advice_is_printed_beside_its_denial covers nine states, every reason unfetchable gives where git exits non-zero (no section, the variant, no url, unmerged, a refused value, an unparsable file, a stray, a hand mount, a gitlink outside members/). It asserts bootstrap's own stderr lines exactly: the fetch line, the denial, the failure, and no advice. test_the_credential_s_skip_prints_the_advice_alone (three states), test_a_leg_the_credential_s_keeps_the_advice_with_every_member_out and test_beside_a_denial_the_advice_is_for_the_members_it_does_not_name pin the other side.

2. A member's refusal and its finding both read make bootstrap exited 2 (run_member_target). Chosen: tell them apart, by reading make's own Error <code> line.

  • Measured on GNU Make 4.3: make exits 2 for a command exiting 1, 2, 3 or 126, and 2 again for a failure of its own (No rule to make target). It prints make: *** [Makefile:N: bootstrap] Error <code> (make[1]: under another make) for a failing command, and no such line for its own failure.
  • make siblings runs the member's scripts/bootstrap.py directly and prints "its own bootstrap exited N". The direct run was not chosen: --members-make validate, which the holder's own make validate runs, has no script to call, and the docs say the holder runs each member's own make bootstrap.
  • So make still runs. run_make passes make's stderr on as it comes and reads the code from the last Error <code> line.
  • What the person reads now, at the end of the run:
    • a member's finding: IRRS (make bootstrap: the member's command exited 1), under the member's own bootstrap FAILED: ... and make's Error 1;
    • a member's refusal: IRRS (make bootstrap: the member's command exited 2), under its REFUSED ... and make's Error 2;
    • make's own failure: IRRS (make bootstrap exited 2 and printed no `Error <code>` line for a command of the member's: make's own message above says what failed), under make's own message.
    • A make speaking another language, or a command killed by a signal, gets the third line too, which is still true there.
  • Test: test_a_finding_a_refusal_and_make_s_own_failure_are_told_apart, for bootstrap and for validate: four members, a finding, a pass, a refusal and no rule, with the final line asserted exactly and the members' and make's own lines asserted on stderr. tests/test_bootstrap_spelling.py's make line follows the new text.

3. The git rm --cached that refuses over a checkout of its own.

  • Reproduced on the gate's own state, as the Direction asks: mkdir Members && git mv members/IRRS Members/IRRS, staged, then a commit (or only an untracked file) in the moved checkout.
  • In that state the gate does not print the wrong-case move. It prints three findings in every mode:
    • member-gitlink-unnamed for Members/IRRS, a second path to the member, whose line is git rm --cached -- Members/IRRS;
    • gitmodules-path-unnamed, which prints no command;
    • member-gitlink-removal-staged for the row's own gitlink.
  • Typed as printed, the git rm refused on git 2.43: error: the following file has staged content different from both the file and the HEAD, exit 1. It refused with the commit, with the untracked file alone, and with both.
  • _forced reads the state git refuses: the index stages the gitlink at a commit HEAD does not record there, and a .git entry is on disk at it. There the line takes -f, and the finding says what -f does, in family.py add: the move back of a member's gitlink points its section of .gitmodules back, sets aside first what the index holds in its way and puts back a member whose removal is staged, its closing mv is guarded and leaves a checkout to delete by hand, and --sparse follows the sparse rules, so no printed line leaves a section naming another spelling or un-registers a member (#761) #828's words: "-f takes out a staged gitlink whose checkout has moved on or holds a file git does not track, which git rm otherwise refuses, and with --cached touches no file".
  • Where HEAD records nothing at the variant, the finding also says that nothing of it is left to commit. The row's own removal-staged lines put the member back, and a commit between the two would have un-registered it.
  • The wrong-case move's own git rm -r --cached --ignore-unmatch takes -f the same way. That state is a committed variant whose checkout moved on, staged, with an untracked file in it.
  • Tests: test_a_git_mv_staged_with_work_in_the_checkout_is_undone_as_printed (commit, untracked) types every printed line as printed, then finds nothing left to commit, the gate passing in every mode and HEAD recording the member at its pin. test_a_checkout_moved_on_at_the_variant_is_moved_with_f pins the move's -f. test_a_staged_second_path_with_nothing_on_disk_takes_no_f checks there is no -f where nothing is on disk.

4. The guard that follows a link, and its missing nothing-there clause (_wrong_case_move, now _guarded_move). Comment 6237599648 is the same fault and is fixed here too.

5. Comment 6237354577 (a), bootstrap's gitmodules_sections reading only the work tree's file. This already held at 233a334e: #819's gitmodules_source reads the work tree's file, then :.gitmodules, then HEAD:.gitmodules. #819's own test_where_the_work_tree_has_no_gitmodules_git_reads_the_index_s_or_head_s pins it, and test_the_credential_s_skip_prints_the_advice_alone's index-read and HEAD-read cases ask it again here. No code change.

6. Comment 6237354577 (b), member-gitmodules-absent's git config -f lines where the work tree's .gitmodules is gone.

7. Comment 6237599648, the move printed unguarded for an absent variant. Line 4's nothing-there clause: the nothing rows above.

Tests and runs

Pinned copies, docs and codes

  • Both files are in FAMILY_COPIED_VERBATIM, so each holder takes one upstream-changed row per file.
  • No committed digest or byte comparison names either file: templates/family-root/contracts/shape-pin.yaml holds only placeholders, and git grep finds no sha256 of them in tests/.
  • docs/cli.md takes only validate-family.py's first docstring line, which is unchanged.
  • No finding code or refusal code is new or renamed. AGENTS.md, templates/family-root/AGENTS-shape.md and both READMEs say nothing these lines change. Each script's module docstring says what its lines now do.
  • Cognitive complexity, counted by hand:
    • fetch_members drops from 15 to 14, since its failure report moved to fetch_incomplete.
    • _check_gitlinks_registered was over 15 on base (18) and drops to 17; its def line keeps its first line.
    • _check_members stays at 14.
    • Every new function is at 6 or under.
  • The new tests share their helpers within the file, and no assert joins conditions with and.

Merge checks

git merge-tree --write-tree of 77f8a6c8, fetched 2026-10-18 at 05:02Z:

Left out

Closes #839

🤖 Generated with Claude Code

Summary by Sourcery

Make the family holder’s bootstrap and validation diagnostics accurately explain failures and provide remedies that execute safely across affected repository states.

Bug Fixes:

  • Report credential guidance only for fetch failures where Git actually requested credentials, and clarify which members the guidance applies to.
  • Distinguish member command failures and refusals from failures in make itself by reporting the command exit code when make provides it.
  • Make generated git remediation commands handle staged checkouts, symbolic links, and absent paths correctly, including forcing gitlink removal when required.
  • Preserve existing .gitmodules sections by restoring a missing work-tree file only before the first remediation that rewrites it.

Enhancements:

  • Improve validation remediation text so printed commands accurately describe their effects and the conditions under which they are safe.

Tests:

  • Add comprehensive coverage for credential advice, make exit-code reporting, forced gitlink removal, guarded moves, and .gitmodules restoration ordering.

Brett Heap and others added 23 commits October 15, 2026 22:59
…HEAD's `.gitmodules` gives is `member-gitmodules-absent`, so a holder whose HEAD lost the file no longer says `family ok`, `remove` refuses it before `deinit` instead of leaving the mount empty, and the gitlink remedies name a case variant and the commit `add` needs (#611)

571(b). In a holder whose HEAD lost `.gitmodules` (the commit #407
describes), `family.py remove --dry-run` exited 0, and the real run did
`git submodule deinit` and `git rm`, then died at its commit's `git
--literal-pathspecs add -f -- .gitmodules family.yaml` (`pathspec
'.gitmodules' did not match any files`); the rollback left `members/<P>`
empty. `_checked_gitmodules_held` now refuses that holder, before any git
command that writes and under `--dry-run` alike: no `.gitmodules` in the
work tree, the index or HEAD while the index records the member's gitlink.
Refused, not committed without the file, as #407 refuses a `.gitmodules`
the work tree lacks. The remedy writes back, with `git config -f
.gitmodules`, the section `family.py add` writes for each member whose
gitlink the index records, then `git add -f .gitmodules`; `remove` then
runs as it always has.

571(c), #407's decision 3. `validate-family.py` reads HEAD's `.gitmodules`
(`_head_gitmodules`, HEAD's root tree listed with no pathspec) and makes
`member-gitmodules-absent` for each row at `members/<project>` whose
gitlink no section gives, in every mode, `--no-members` included. Its two
lines write the section back as `family.py add` writes one, keeping the url
of a section of that name that has one. A member it names that is not
checked out is not refused `member-uninitialized`: that refusal named a `git
submodule update` that exits 0 there and dropped the finding, which is what
a fresh clone and CI met. It reads HEAD, not the work tree, so a deletion
nobody committed, an unstaged edit and a sparse checkout that leaves the
file out are no finding, and a holder with no members is asked nothing.

574/2 (#363's item 3). A row whose gitlink HEAD records only at a case
variant of its path (`Members/<P>`) is refused `member-gitlink-absent`
naming each variant, and the move that puts it back in the index alone:
`git rm -r --cached` of the variants and `git update-index --add
--cacheinfo 160000,<pin>,members/<P>`, then a commit. `family.py add`, the
plain refusal's remedy, cannot help there: the row exists.

574/3. A hand mount's `member-gitlink-unnamed` remedy says to commit the
`git rm` before the `add`: since #574, `add` refuses a path under another
spelling of `members/` that HEAD holds and the index no longer does.

`templates/family-root/scripts/validate-family.py` is a pinned copy: each
holder sees it as `upstream-changed` on its next `update-shape.py check`.

Tests: tests/test_family_head_lost_gitmodules.py (new, 24); the pinned
remedy text and the case-variant states in
tests/test_validate_family_unnamed_mounts.py; a docstring in
tests/test_family_remove_gitmodules.py.

Lane: openreposhape-5 (openRepoShape-5)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s-absent` reads and the members `remove`'s refusal is for are pinned, so a finding for a row with no gitlink or a hand-edited path, or the refusal over a member with no gitlink, no longer passes (#611)

Two tests. In a holder whose HEAD lost `.gitmodules`: a row with no
gitlink (`member-gitlink-absent`'s) and a row whose `path:` was edited by
hand (`member-path`'s, its mount `member-gitlink-unnamed`'s) get no
`member-gitmodules-absent`; and `remove` of a member with no gitlink is
not that refusal, but the dry run's `did not match any file(s) known to
git`, as before. Each holds a condition of the first commit that its
other tests did not: both pass on BASE 396c0ff and on that commit.

Lane: openreposhape-5 (openRepoShape-5)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
….gitmodules` over an unmerged gitlink too, and the case-variant move prints `--ignore-unmatch` and the `mv` a case-sensitive disk needs, so the mount is no longer emptied there and the printed lines work half typed or with both directories on disk (#611)

Fix round 1, from the review of 6f938ed.

`_checked_gitmodules_held` (scripts/family.py) took the member's gitlink
only at stage 0. `git submodule deinit` and `git rm` run on an unmerged
gitlink as on a merged one, so with `members/<P>` held only at stages 1,
2 and 3 in a holder whose HEAD lost `.gitmodules`, the dry run exited 0
and the real run emptied the mount and died at its commit. It now takes a
gitlink at any stage, and both runs refuse `member-gitmodules-absent`
before `deinit`, with the holder byte for byte as it was. The docstring
says so.

The case-variant refusal of `member-gitlink-absent`
(templates/family-root/scripts/validate-family.py, pinned) now prints
`git rm -r --cached --ignore-unmatch -- <variants>`: a variant whose
removal is staged and not committed is still named from HEAD, and the
plain line failed there (`pathspec ... did not match`). Its remediation
also says what is left on disk: where nothing is checked out at the row's
path, as in a fresh clone, `git submodule update --init --recursive` then
checks the member out; on a case-sensitive disk a directory left at a
variant stays, untracked, and `family.py add` refuses it as
`family-members-dir-variant`, so it gives an `mv` of each out of the
holder, to `../<variant with / as ->`, and says to leave it on macOS and
Windows, where it is the row's own directory.

The validator's module docstring names `member-gitmodules-absent`, says
what a staged, uncommitted `.gitmodules` does (HEAD's file is what counts,
as #407's decision 3 says: an uncommitted `add` is the finding for its
member, a staged section removal is none, a staged restore is still the
finding), and gives EXIT CODES' one exception, an unregistered member that
is not checked out (exit 1, not 2).

Tests (tests/test_family_head_lost_gitmodules.py, 12 more, 38 in all):
the unmerged gitlink refused alike and repaired as printed; one test per
condition no test held (a checked-out member still gets its tree check
beside the finding; HEAD's unreadable file refused in every mode; the
`<url>` placeholder; `remove`'s remedy names only members with a gitlink
in the index; one finding per path for a duplicate row); the variant move
half typed (both disks); and, on a case-sensitive disk, every printed
line typed with both directories present, after which the gate passes and
another member is added. tests/test_validate_family_unnamed_mounts.py
pins the `--ignore-unmatch` line.

Lane: openreposhape-5 (openRepoShape-5)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…modules: the gitlink remedy keeps #611's commit-the-removal clause and ends with BRANCH_LEFT (#611 x #607)

This merges origin/main at 95780dd, which
holds #637's squash (95780dd, "family.py and validate-family.py: where an
earlier act left the branch ...", #607). It adds `BRANCH_LEFT` to
scripts/family.py and templates/family-root/scripts/validate-family.py, and
the guard `test_every_remedy_that_names_a_switch_line_ends_with_the_sentence`
in tests/test_family_switch_branch_exists.py. Lane 2's #634 (#610) is not in
main yet, so `_check_member_gitlink` is untouched here.

Two hand resolutions, one sentence in two places, as #639's body named it:

- templates/family-root/scripts/validate-family.py, the end of
  `member-gitlink-unnamed`'s remedy: #611's "and commit that removal, since
  family.py add refuses a path under another spelling of members/ that HEAD
  holds and the index no longer does, then mount it with the third, ..."
  clause is kept, and #607's `BRANCH_LEFT` ends the prose, as
  "... if one is still there. {BRANCH_LEFT}\n".
- tests/test_validate_family_unnamed_mounts.py, `GITLINK_REMEDY`: the same
  clause, ending ".git/modules if one is still there. " + BRANCH_LEFT.

No other file conflicted. #637's guard test passes on this merge without a
further change: the remedies #611 added name no `_switch_line`, so no
BRANCH_LEFT is appended beyond the one resolved above.

Lane: openreposhape-5 (openRepoShape-5)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ck out fails the family bootstrap, every row's gitlink is asked before any row's checkout, and `--no-members` prints the case-variant move, so a clone of a holder whose HEAD lost `.gitmodules` or of #363's state no longer ends `family bootstrap ok` or hides the variant behind `member-uninitialized` (#648)

(a) `templates/family-root/scripts/bootstrap.py` said "every member and
leg fetched" and ended `family bootstrap ok`, exit 0, over members it had
not checked out: in a fresh recursive clone of a holder whose HEAD lost
`.gitmodules` (git exits 0), and in a plain clone of that holder or of
#363's state, where git dies `No url found for submodule path` (exit 128).
After the fetch it now asks this holder's own records of each row whose
member is not checked out: no gitlink at the row's path
(`recorded_gitlink`, as `member-gitlink-absent` reads it), or no section
of `.gitmodules` giving that path, or, where git exited 0, any such row.
Each is named on one `NOT CHECKED OUT, and not for want of a credential`
line with why, which names `validate-family.py`, and the run ends
`family bootstrap FAILED: <P> (not checked out)`, exit 1. "every member
and leg fetched" is said only where git exited 0 and every row is checked
out. A row git could not fetch for want of a credential is still the skip
it was (`test_bootstrap_degrades_when_a_member_cannot_be_fetched`), and a
good holder's run prints what it printed.

(b) `validate-family.py` asked each row whole, gitlink then checkout,
before the next. In a plain clone of #363's state plus `git submodule
update --init --recursive`, which dies at `Members/<P>` before it checks
out any member, an earlier row was refused `member-uninitialized` (naming
that same update) and the later row's variant refusal was never reached.
`_check_member` now stops at the gitlink and returns the member
(`Gitlinked`); `_check_member_checkout` asks the digest and identity of
each once every row's gitlink has been asked.

(c) Under `--no-members`, `member-gitlink-unnamed` for a member's only
gitlink, in the wrong case, printed no command. Where the row's path is
its canonical one and its pin a commit, it now prints the index move the
row's members-mode refusal prints, word for word (`_wrong_case_move`,
shared by both), to the pinned commit, every variant on one line.

Docs: the validator's WHAT IS CHECKED docstring (the order; the move
under --no-members) and bootstrap's (a) and (c). README.md's "A member it
cannot fetch is reported and skipped, never a failure" is no longer true,
so it says "One it lacks a credential for is reported and skipped, not a
failure", in the same line count (README is at its 1449-line cap), and
docs/handbook.html says the same, with its § Families marker set to the
segment's new blob.

Pinned copies changed: templates/family-root/scripts/bootstrap.py and
templates/family-root/scripts/validate-family.py.

Lane: openreposhape-5 (openRepoShape-5)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tstrap-and-gate-order, as lane 5's coordinator asked (#648 x #611)

This merges origin/fix/611-validate-family-head-gitmodules at
08b6245, #639's head after its own merge
of main 95780dd (#637's squash, #607), keeping that merge's one hand
resolution: `member-gitlink-unnamed`'s hand-mount remedy ends with #611's
commit-the-removal clause and then `BRANCH_LEFT`.

No file conflicted. The remedy #648 adds under `--no-members`
(`_wrong_case_move`) names no `_switch_line`, so #637's guard asks no
`BRANCH_LEFT` of it. README.md's § Families segment, which #648 edits, is
the same in both parents, so docs/handbook.html's marker for it stays the
blob #648 set.

Lane: openreposhape-5 (openRepoShape-5)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…x/648-bootstrap-and-gate-order, before main takes #639's squash (#648 x #611)

Lane: openreposhape-5 (openRepoShape-5)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…der, keeping this tree: main's tree a25f083 is #639's final head 159a08f's, merged in the commit before (#648 x #611)

Lane: openreposhape-5 (openRepoShape-5)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…member THE LOCKSTEP RULE rides on `Gitlinked`, so the two-pass gate (#648) passes it to the digest finding (#648 x #610)

One file conflicted, `templates/family-root/scripts/validate-family.py`,
in two regions, both where this branch split `_check_member` in two
(the gitlink first, the checkout once every row's gitlink is asked) and
#634 (#610) made THE LOCKSTEP RULE a per-member `_lockstep(...)` value
passed to the gitlink and tree checks:

1. `_check_member`'s gitlink call. #634 computes
   `rule = _lockstep(root, project, str(row.get("id") or project))`
   and passes it to `_check_member_gitlink`; this branch returns a
   `Gitlinked` there. Resolved: the call passes `rule`, as #634 has it,
   and `Gitlinked` gains a sixth field, `rule`, which the return
   carries.
2. `_check_member`'s tail, which this branch moved into
   `_check_member_checkout`. #634 passes `rule` to `_check_member_tree`.
   Resolved: `_check_member_checkout` passes `asked.rule`.

So each member's gitlink-mismatch and digest-mismatch findings end at
the same rule #634 gives them, naming that member, and the closing
count's `_lockstep(root)` is #634's, merged clean. `Gitlinked`'s
docstring names the new field. No other file conflicted. The
resolution is the one rv648 built and tested in its scratch clone
(the same blob of #634's file, 519b71f), plus that docstring.

Lane: openreposhape-5 (openRepoShape-5)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n gives a url fails the run naming every row, and a member git exited 0 over is given the update that checks it out, so a stray case variant, a hand mount or a url-less section no longer ends `family bootstrap ok` (#648, rv648)

The fix round for rv648's review of 8b39cd1 (1 MUST, 2 SHOULD, 5 NIT).

M1. In a plain clone, `git submodule update --init` dies in its init
step at the first gitlink, in index order, that no section of
`.gitmodules` gives a url, and checks out no member. `unfetchable` asked
only each row's own gitlink and section, so a stray `Members/<P>`
beside `members/<P>` (#361's STRAY), a member mounted by hand with no
section, or a row's section that lost its url (rv648's S1, H1 and U1)
passed every row as the credential skip, and the run ended ok.

`init_stop` now asks the holder's records which gitlink git's init
stops at: the index's gitlinks in order, each against the work tree's
sections (`gitmodules_sections`, which replaces `gitmodules_paths`) and
the git config. Measured before it was taken, rv648's prototype failed
three states that are a missing credential: a recursive clone of the
stray or of the hand mount with no credential, and a url-less section
an earlier `git submodule init` registered. So two of git's rules are
kept: where the config sets `submodule.active` (a recursive clone sets
`.`), init asks only the gitlinks a section gives, and a section with no
url does not stop it where the config holds a url under its name. Where
git exited non-zero and init stops at a gitlink, every row not checked
out is named `git stopped at <gitlink>, a gitlink no section of
.gitmodules gives a url`, and the run fails.

What cures it: `validate-family.py --no-members` names a gitlink no row
names and the commit that cures it (members checked, it refuses
`member-uninitialized` for the first row in these states, a checklist
candidate). Where a row names the gitlink, nothing in the validator does,
so the line says to write that section back as `family.py add` writes
one. Each why now carries its own sentence.

S1. For "git exited 0 and checked nothing out there" (a subset clone, an
`update = none`), the line printed the validator sentence, whose
`member-uninitialized` remedy exits 0 and checks nothing out. It prints
`git submodule update --init --checkout --recursive -- <path>` instead;
typed, it checks the member out in both states and the run ends ok.

S2. `test_a_missing_credential_is_still_the_skip_it_was` asserts that
"  every member and leg fetched" is not printed (rv648's mutant R2).

NITs. README and the handbook say `make bootstrap` skips a member it
lacks a credential for, and the § Families marker is the new segment's
blob, b3d19092; README stays at 1459 lines, its cap. The docstrings say
"an empty mapping" and "a missing credential exits non-zero too", and
`validate-family.py`'s WHAT IS CHECKED says the move under
`--no-members` is printed only for a canonical path and a 40-hex pin.

Tests: the variant-plain case and the round trip name MEMBER too; new
tests for S1 and H1 (typed through `--no-members` and committed, the run
checks both out), U1 (the section written back), the two recursive
clones with no credential and the registered url (still the skip), and
the update that exits 0, by `update = none` and by a subset clone, with
its printed line typed.

Lane: openreposhape-5 (openRepoShape-5)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ing in #414's url-less member state now fails, as 1a69505 makes it, so the test pins exit 1 and `git stopped at members/Ghost` where it pinned the old `ok` (#648, rv648's U1)

`test_414_3_there_the_bootstrap_cures_nothing_and_the_git_line_cures_one`
builds rv648's U1 state: a member, Ghost, whose `.gitmodules` section
gives its path and no url. Git's init stops the holder's update there,
before it checks out the other member. The test asserted bootstrap's
exit 0, the false `ok` rv648's M1 asked to end. It asserts exit 1 now,
and the line naming where git stopped. Its point is unchanged: the
bootstrap cures nothing, and the doctor's git line cures the other
member.

Found by the targeted run of every test file that drives the family
bootstrap (1 failed, 3797 passed, 32 skipped); the file now passes
alone, 124 passed.

Lane: openreposhape-5 (openRepoShape-5)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ember git has no url for is its own reason, and a gitlink outside members/ gets a cure true for it, so a later duplicate section, a section under a name git ignores or a url git drops no longer ends `family bootstrap ok`, and a subset clone's printed update no longer dies `No url found` (#648, rv648's verify)

rv648's verify of 45c512e (1 MUST, 1 SHOULD, 3 NIT).

M1. `init_stop` read `.gitmodules` with `git config --file --list`, which
keeps entries git's submodule parser drops or overrides. In three plain
clones git's init died (`No url found`, exit 128) and the run still said
ok: a second section for a member's path, with no url, after its own
(D1); the member's section under the name `../Nova` (Q1); and its url
`-nowhere` (O1). `gitmodules_sections` now returns a `Gitmodules` with
git 2.43's own reading beside the one the validator's findings use:

- a section whose name is empty or has a `..` component, split on `/`
  and on a backslash, is ignored whole (`check_submodule_name`);
- a `path` or `url` that begins with `-` is dropped, and the value
  before it stands (`looks_like_command_line_option`);
- where two sections give one path, the later decides, and a section
  that gives a path again lets go of the one it gave, even where a later
  section gave that one too (`cache_put_path`, `cache_remove_path`).

`has_url` asks the section git reads for a path, then the config under
that section's name. `init_stop` and the reasons ask it. Where more than
one section gives the path git has no url for, the line names
`validate-family.py --no-members`, whose `gitmodules-path-twice` says to
take the others out: a section written back would be one more (D1).

S1. In a subset clone of a member whose section has no url (A1), git
exits 0 and the line printed the exit-0 skip's update, which dies `No
url found` when typed. "git has no url for that path" is now its own
reason, asked before "git exited 0", with the write-it-back cure. The
stop's sentence says "a gitlink it has no url for", which is true in D1
too, where a section does give the path a url.

NIT 1. A gitlink outside members/ that git stopped at (H2) was sent to
`--no-members`, which reads only members/ and says `family ok` there. It
now gets `git rm --cached -- <path>` and a commit: measured, that ends
ok and the gate passes in every mode, while a section written for it
leaves `gitmodules-path-unnamed` in every mode.

NIT 2. The `cures` docstring says what members mode refuses for #363's
variant (`member-gitlink-absent`, with the move) as well as for a stray
or a hand mount (`member-uninitialized`).

Tests in tests/test_family_bootstrap_gate_order.py: U1, Q1 (three names)
and O1 in one parametrized test, each written back; D1, typed through
`gitmodules-path-twice`; D2, a name with dots and a url `-x` after a
url, each still the credential's skip and then a true ok; A1, typed
through both rounds; and H2, typed and the gate passing in every mode.

Lane: openreposhape-5 (openRepoShape-5)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ts own reason, since git reads no section from it, so a plain clone mid-merge whose update dies `No url found` no longer ends `family bootstrap ok` (#648, rv648's MM1)

rv648's verify of 23e1105 (1 MUST). In a plain clone, a merge that
conflicts in `.gitmodules`, resolved in the work tree and not added,
leaves the index holding the file only at its three conflict stages.
Git then reads no `.gitmodules` at all (`repo_read_gitmodules` asks
`is_gitmodules_unmerged` first), so its init dies at the first gitlink,
exit 128. `gitmodules_sections` read the work tree's file, found every
section with its url, and the run ended ok.

`gitmodules_sections` now asks `git ls-files -u -z -- .gitmodules`
first. Where the index holds the file at a conflict stage, it returns
nothing, with `Gitmodules.unmerged` set. `unfetchable` asks that first:
every row not checked out gets "git reads no .gitmodules while a merge
leaves it unmerged", and the line says to resolve the merge in
.gitmodules and `git add .gitmodules`, then run this again. Added, the
same run checks both members out.

Test: rv648's MM1 in tests/test_family_bootstrap_gate_order.py, the two
sides of the merge each giving OTHER's section another `branch`; the
cure typed, and the run ends ok.

Lane: openreposhape-5 (openRepoShape-5)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n as it reads the file fails `make bootstrap` naming the key, and the family gate names it in every mode, so `update = None` no longer ends `family bootstrap ok` (#768)

Git 2.43 dies reading a `.gitmodules` that gives a submodule a `path`,
`url`, `update`, `ignore` or `branch` with no value (a `branch` with none
crashes it, exit 139), an `update` other than `none`, `checkout`,
`rebase` or `merge`, a `shallow` that is no boolean, or a
`fetchRecurseSubmodules` that is neither a boolean nor `on-demand`, before
`git submodule update` checks out any member. The family holder's
bootstrap listed the file with `git config --list`, which reads all of
them, and ended `family bootstrap ok`; the gate refused
`member-uninitialized` members checked, naming the update that dies, and
said `family ok` under `--no-members`.

bootstrap.py (the family root's, pinned):
- `gitmodules_sections` checks each entry against git's rules
  (`refused_value`; a boolean is read by git itself, `git config
  --type=bool`). Where one would make git die it returns those entries
  and no sections, and `unfetchable` gives every row not checked out the
  reason "git cannot read .gitmodules: `<key>` is `<value>`" (or "has no
  value"). The cure names each key and what git takes for it. Where every
  member was checked out, the run fails on `.gitmodules` all the same,
  since git updated none of them.
- Where the work tree has no `.gitmodules`, it reads the index's, then
  HEAD's, with `git config --blob`, as git does. A key with no value ends
  the reading of such a blob, where git calls the line bad and goes on.
- `init_stop` reads a section's own `submodule.<name>.active` before
  `submodule.active`'s patterns, and matches a plain path pattern, as
  `git clone --recurse-submodules=<path>` writes one, to that path and
  what is under it. A pattern with pathspec magic or a glob keeps the old
  reading.

validate-family.py (the family root's, pinned):
- A new finding, `gitmodules-value-refused`, for each such key in the
  work tree's `.gitmodules` and in HEAD's, asked before any member check
  and in every mode. Its remedy prints `git config -f .gitmodules
  --unset-all submodule.<name>.<key>`, one command to a line, writes a
  member's `path` or `url` back where taking it out would leave none,
  says which values git takes, and ends with the commit. It says which
  file gives the value; where HEAD's alone does, it says to commit the
  work tree's, and where the work tree has no `.gitmodules`, it puts
  HEAD's back first.
- Where the work tree's file gives such a value, `gitmodules-path-twice`
  and `gitmodules-path-unnamed` read nothing from it, as git reads
  nothing, so no command they print meets one of its lines. Where either
  file gives one, a member that is not checked out is not refused
  `member-uninitialized`: the finding fails the run.

tests/test_family_gitmodules_values.py: every row of the issue's table
and `branch` with no value, in a plain clone, through bootstrap and the
gate in three modes, the printed commands typed; the no-misfire values;
the index's and HEAD's file; `submodule.<name>.active` and the subset's
patterns; and which file gives the value.

tests/test_validate_family_without_git.py: with no git and no work tree
`.gitmodules`, the first check to run git is now the reading of HEAD's
`.gitmodules`, so the `git-not-runnable` refusal names that read, where it
named the gitlink listing. Exit 2 and the remedy are as they were.

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

# Conflicts:
#	templates/family-root/scripts/bootstrap.py
#	templates/family-root/scripts/validate-family.py
…ues.py: a `branch` with no value is read as the git that runs reads it, a work tree `.gitmodules` git cannot parse fails the run with every member checked out, and the boolean probe reads no config of the person's, so CI's git 2.55 passes the row (#768, rv819)

CI's run 39031235254 (git 2.55.0) failed
`test_a_value_git_dies_on_fails_bootstrap_and_every_mode_of_the_gate[branch-no-value]`,
and lane 2's review rv819 of c12b5f8 (NOT READY) found one MUST, four
SHOULDs and five NITs. This commit answers them.

MUST 1. The row pinned git's own update at exit 139, the crash w768
measured on git 2.43.0. Git 2.55 refuses the line, `error: missing value
for 'submodule.members/IRSS.branch'` and `fatal: bad config line 7`, exit
128; the same assertion fails on git 2.47.1 (`assert 128 == 139`). The
table's exit column is gone: the premise is that git's update died and
checked nothing out, and every other assertion is the tools' own.

SHOULD 2. Measured on git 2.43.0 and 2.47.1, a `branch` with no value
after a submodule's url, then `git submodule update --init --recursive`
in a plain clone: from the work tree's file, 2.43.0 crashes (exit 139)
and 2.47.1 dies (exit 128); from the index's file or HEAD's, a blob,
2.43.0 crashes and 2.47.1 says `error: bad config line 4 in blob`, reads
every section before it, checks the submodule out and exits 0, as for a
`url` with no value. git's submodule-config.c gained `if (!value) ret =
config_error_nonbool(var);` for `branch` in v2.43.1 and v2.44.0; v2.43.0,
v2.42.4 and v2.39.5 lack it. Bootstrap kept 2.43.0's rule for every git,
so on 2.43.1 and later such a blob had every member not checked out named
"git cannot read .gitmodules" and the run failed, where git failed only
for want of a credential (measured on 2.47.1 at c12b5f8).
`crashes_on_branch` now reads `git version`: before 2.43.1, or a version
it cannot read, the key is refused as before; from 2.43.1 on, the line
ends the reading of a blob (`ends_blob_reading`). The gate is unchanged:
it reads the work tree's file and HEAD's, which a clone checks out as
its work tree's, and every git dies on the key there. The comments that
said git crashes say which git does.

SHOULD 3. Where every member was checked out, a work tree `.gitmodules`
`git config --list` cannot parse (a conflict marker, an open quote) ended
`family bootstrap ok` over git's `fatal: bad config line`. `Gitmodules`
now says `unreadable`, and the run says NO MEMBER UPDATED and fails on
`.gitmodules`, as for a refused value; a row not checked out is said to
be that (`git cannot parse .gitmodules`), where it was "no section gives
that path", and the cure is the file mended by hand.

SHOULD 4. New tests catch each survivor rv819 named: the index's file
staged apart from HEAD's; `update = rebase` and `update = merge`; a key
with no value inside an ignored section of a blob; a `submodule.active`
pattern that is only the start of a path; `submodule.<name>.active` set
twice; a url beginning with `-` before the one with no value; the
`<url>` placeholder; a section name with a backslash; and HEAD's value
alone leaving the work tree's file to `gitmodules-path-twice`.

SHOULD 5. `gitmodules_sections` is split into `gitmodules_source`,
`gitmodules_entries`, `ends_blob_reading` and `read_sections`;
`unfetchable` gives each row's reason to `row_reason`; `cures` gives the
`.gitmodules` sentences to `gitmodules_cures`; `_check_gitmodules_values`
gives its text to `_value_said` and `_value_taken_out`.

NIT 6: the gate-in-three-modes block and the good-run block are
helpers. NIT 8: where HEAD's file alone gives the value, the finding says
git dies on it in a clone. NIT 9: both boolean probes ask git for the
`--default` of a key in an empty file (`git config --file <null>`), so no
`probe.value` in the person's config is read; one in it made both tools
refuse `shallow = true`. NIT 7 (one copy of the rules) and NIT 10 (the
body's count) are not code changes here.

tests/test_family_gitmodules_values.py: 52 tests, from 28.

Lane: openreposhape-5 (openRepoShape-5)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…762) landed, so validate-family.py keeps both sides, `stop` is asked only where `not refused`, and `_check_removals_staged` is handed the sections as read

#820 (#762, `b9b70692`) landed on main after #819's last head `e1379a9d`. The
two PRs conflict in `templates/family-root/scripts/validate-family.py` only;
`templates/family-root/scripts/bootstrap.py` merges by itself. The resolution
is the one lane 2 measured on #819 and #820 (rv819, rv820): keep both sides,
with two changes that its notes for the second lander ask for.

Conflicts, all in `templates/family-root/scripts/validate-family.py`:

1. The module docstring's EXIT CODES sentence. #819 added "and any while
   `.gitmodules` gives a value git dies on" with `gitmodules-value-refused`;
   #820 added "or gives a url, or whose removal is staged" with
   `member-gitmodules-url-absent` and `member-gitlink-removal-staged`. Now one
   sentence names all four exit-1 codes.
2. The `_check_members` docstring. Both paragraphs stay: #820's sentence
   about `_init_stop_said` ends the #648 paragraph, and #819's paragraph about
   a value git dies on follows it, with a clause that `_init_stop_said` is not
   asked there.
3. The `_check_member_checkout` call: `set(gitlinks) if refused else
   unregistered` (#819) and `stop` (#820), as `_check_member_checkout(root,
   asked, report, set(gitlinks) if refused else unregistered, stop)`.

Two lines outside the blocks, which lane 2's notes ask for:

- `stop` is computed only where `not refused`. Given #819's emptied
  `sections`, `_init_stop_said` names the first gitlink as a stop git never
  reaches when the work tree's file gives a value git dies on. Today
  `set(gitlinks)` keeps that text from being printed, but only by ordering.
- `_check_removals_staged` is handed `as_read`, the sections before #819
  empties them. Given `{}`, a staged `git rm --cached` printed
  `git checkout HEAD -- .gitmodules` as putting back "the .gitmodules section
  git rm took out", and `git rm --cached` took none (rv820).

Lane: openreposhape-5 (openRepoShape-5)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…side a value git dies on in the work tree's `.gitmodules` is a finding that prints the reset alone, so `_check_removals_staged` handed the emptied sections no longer puts `git checkout HEAD -- .gitmodules` in it (#768, point 3 of the merge with `b9b70692`)

The merge of main `b9b70692` (#820, #762) into this branch hands
`_check_removals_staged` the work tree's sections as read (`as_read`), not
as #768 empties them where the work tree's file gives a value git dies on
(rv820's note for whichever of #819 and #820 landed second). No test held
that line: a mutant giving it `sections` again passed all 191 tests of
`test_validate_family_typed_states.py`, `test_family_gitmodules_values.py`,
`test_family_head_lost_gitmodules.py`,
`test_validate_family_remedies_typed.py` and
`test_validate_family_unnamed_mounts.py`.

`test_a_staged_removal_beside_a_refused_value_puts_no_section_back` holds
the state: the work tree's file gives OTHER's section `update = None`, and
`git rm --cached -- members/IRRS` is staged. In all three modes the gate
finds `gitmodules-value-refused` and `member-gitlink-removal-staged`, whole
(`taken_out` and `removal_staged`), the second's commands are
`git reset -q -- members/IRRS` alone, and `git checkout HEAD --
.gitmodules` is not in it.

The test passes on `e32ed55a`'s validator. With the call changed back to
`sections` it fails at the first mode: the finding's last line is the
`git checkout HEAD -- .gitmodules` it should not print.

Lane: openreposhape-5 (openRepoShape-5)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… advice and a member's exit code say what happened, and the gate's `git rm --cached`, guarded `mv` and section lines type as printed where they failed (#839)

Seven lines of #839, in the holder's two pinned scripts, on #819's head
(233a334):

1. `fetch_members` printed "If the members are PRIVATE, set
   SHAPE_LEGS_TOKEN ..." for every failed `git submodule update`, then
   "NOT CHECKED OUT, and not for want of a credential" for every row
   `unfetchable` names. `credential_advice` prints it only where git
   asked for a credential: not where its init died at a gitlink it has
   no url for, nor where it read nothing from `.gitmodules` (unmerged, a
   value git dies on, a file it cannot parse). Beside a denial it says it
   is for the members that line does not name. A leg left unfetched in a
   checked-out member keeps it.
2. `run_member_target` said `make <target> exited 2` for a member's
   finding (1), its refusal (2) and make's own failure alike: GNU make
   exits 2 for each (measured, GNU Make 4.3). `run_make` passes make's
   stderr on as it comes and reads the code from its last `Error <code>`
   line: `make bootstrap: the member's command exited N`, or `make
   bootstrap exited 2 and printed no ... line` where make printed none.
   Make is still what runs, so `--members-make validate` gets it too.
3. In the gate's state after `git mv members/<P> Members/<P>`, staged,
   with a commit or an untracked file in the moved checkout, the printed
   `git rm --cached -- Members/<P>` (the STRAY finding) refused, exit 1.
   `_forced` reads where the index stages a variant at a commit HEAD does
   not record there over a checkout; the STRAY line and the wrong-case
   move's `git rm` then carry `-f` and say what it does, as family.py's
   move back says it (#828).
4. The wrong-case move's guard followed a link (`-ef`, `-e`), so a link
   at the variant to the row's own directory or to a clone was kept, and
   `family.py add` refused it; with nothing there the `mv` failed `cannot
   stat`. `_guarded_move` prints `[ ! -L V ] && { [ ! -e V ] || [ V -ef R
   ] || [ -e V/.git ]; } || mv -- V ../<V>`, run in dash and bash on seven
   layouts. Comment 6237599648 is the same fault.
5. Comment 6237354577 (a): bootstrap already reads the work tree's, the
   index's, then HEAD's `.gitmodules` at 233a334 (#819); a test asks it.
6. Comment 6237354577 (b): `member-gitmodules-absent`'s `git config -f`
   lines made a one-section file where the work tree's `.gitmodules` is
   gone. The first finding that writes the file puts it back first
   (`_gitmodules_put_back`, `_back_first`), and no other, nor any after a
   `gitmodules-value-refused` that put HEAD's back; a second
   `member-gitmodules-url-absent` put the file back over the first's url.

Tests: tests/test_family_holder_bootstrap_texts.py (48), and
`variant_refused` in tests/test_family_head_lost_gitmodules.py and the
make line in tests/test_bootstrap_spelling.py follow the new texts.

Lane: openreposhape-5 (openRepoShape-5)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…as `_check_gitmodules_values` returns it, so `_check_members` gains no condition for the put-back `gitmodules-value-refused` already printed (#839)

Where the work tree holds `.gitmodules`, `_gitmodules_put_back` names no
line to put it back either way, so `refused` alone says the same as
`refused and not os.path.lexists(root / GITMODULES)`, and the cognitive
complexity of `_check_members` stays at its base's.

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-18T05:11Z) @codex review

@sourcery-ai

sourcery-ai Bot commented Oct 18, 2026

Copy link
Copy Markdown

Reviewer's Guide

Updates the family-root bootstrap and validation scripts so credential advice, Make exit reporting, gitlink removal commands, guarded moves, and .gitmodules remediation text accurately reflect the underlying Git and Make states, with extensive typed-command and integration coverage.

Sequence diagram for member bootstrap exit reporting

sequenceDiagram
    participant Bootstrap as bootstrap.py
    participant Make as make
    participant Member as member command

    Bootstrap->>Make: run_make(make, target, path)
    Make->>Member: run target
    Member-->>Make: exit code
    Make-->>Bootstrap: stderr chunks and make exit code
    Bootstrap->>Bootstrap: parse last MAKE_ERROR_RE line
    alt Error <code> line found
        Bootstrap-->>Bootstrap: report member's command exit code
    else no Error <code> line
        Bootstrap-->>Bootstrap: report make's own failure
    end
Loading

Flow diagram for credential advice after fetch failure

flowchart TD
    A[git submodule update] --> B{git exits non-zero}
    B --> C[unfetchable and module state analysis]
    C --> D{stopper, unmerged, refused, or unreadable?}
    D -->|yes| E[print fetch failure only]
    D -->|no| F{members denied as not credential-fixable?}
    F -->|yes| G[print advice for members the denial does not name]
    F -->|no| H[print credential advice for the members]
    E --> I[report not checked out members]
    G --> I
    H --> I
Loading

Flow diagram for guarded variant move and gitlink removal

flowchart TD
    A[variant path] --> B{symbolic link?}
    B -->|yes| C[mv variant to outside holder]
    B -->|no| D{missing, same directory, or checkout .git entry?}
    D -->|yes| E[do not move]
    D -->|no| C
    C --> F{git rm --cached needs force?}
    E --> F
    F -->|yes| G[print git rm --cached -f]
    F -->|no| H[print git rm --cached]
    G --> I[commit index-only removal]
    H --> I
Loading

Flow diagram for preserving .gitmodules sections during remediation

flowchart TD
    A[inspect HEAD and work-tree .gitmodules] --> B{work-tree file absent?}
    B -->|yes| C[put file back before first remediation]
    B -->|no| D[keep current file]
    C --> E[write first missing section or URL]
    D --> E
    E --> F[clear put-back action]
    F --> G[process later findings without restoring again]
Loading

File-Level Changes

Change Details Files
Restrict credential guidance to fetch failures where Git actually requested credentials, while preserving precise denial wording and fetch diagnostics.
  • Extract fetch failure reporting and credential-advice selection into dedicated helpers.
  • Suppress advice for missing, invalid, unreadable, or otherwise uninitialized .gitmodules/gitlink states.
  • Clarify that denial-adjacent advice applies to members not named by the denial.
  • Add exhaustive tests for credential-requested and non-requested failure states.
templates/family-root/scripts/bootstrap.py
tests/test_family_holder_bootstrap_texts.py
Distinguish a member command’s exit status from GNU Make’s exit status by streaming stderr and parsing the final Error <code> diagnostic.
  • Add make execution that forwards stderr incrementally and retains a bounded diagnostic tail.
  • Report the member command’s code when Make identifies it, and explicitly describe Make’s own or unrecognized failures otherwise.
  • Update spelling expectations and cover findings, refusals, successful commands, and missing targets for bootstrap and validate.
templates/family-root/scripts/bootstrap.py
tests/test_bootstrap_spelling.py
tests/test_family_holder_bootstrap_texts.py
Make validation remedies accurately handle staged gitlink moves, including when git rm --cached requires -f, and print the corresponding command rationale.
  • Detect forced removal from index/HEAD state plus an on-disk checkout.
  • Add -f to stray and wrong-case remediation commands only when required, with explanatory text.
  • Test committed and untracked work in moved checkouts, no-worktree cases, and preservation of member commits.
templates/family-root/scripts/validate-family.py
tests/test_family_head_lost_gitmodules.py
tests/test_family_holder_bootstrap_texts.py
Replace the wrong-case move guard with a POSIX-shell-safe guard that is no-op for absent paths and always moves symbolic links.
  • Check -L before dereferencing existence/equivalence tests, while retaining protections for the row directory and checkouts.
  • Add the explicit nothing-there clause and document Git Bash/POSIX-shell behavior.
  • Type the emitted command in dash and bash across link, checkout, directory, and absent-path layouts.
templates/family-root/scripts/validate-family.py
tests/test_family_head_lost_gitmodules.py
tests/test_family_holder_bootstrap_texts.py
Preserve the work-tree .gitmodules contents while emitting multiple remediation findings that modify it.
  • Restore the file from the source Git reads before the first config-writing finding when it is absent.
  • Track that restoration across findings so later remedies do not overwrite earlier changes or restore after a value-refused finding.
  • Add tests for absent, URL-less, and value-refused combinations and commit the printed remedies.
templates/family-root/scripts/validate-family.py
tests/test_family_holder_bootstrap_texts.py

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 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-18T05:17:22.577709Z 77f8a6c Manual request
ℹ️ About Codex in GitHub

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

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

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

@chatgpt-codex-connector 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: 77f8a6c894

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

held, sections = head
given = Counter(keys["path"] for keys in sections.values()
if "path" in keys)
back = ("", "") if refused else _gitmodules_put_back(root)[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 Check preserved HEAD sections before restoring .gitmodules

When the work-tree .gitmodules is absent and HEAD contains both a missing/url-less member section and an unrelated section whose path no member names, _check_gitmodules_named sees the empty work tree and never reports the unrelated section. This restoration then copies HEAD wholesale and repairs only the member sections, so following every printed command and committing leaves the unrelated section behind; I reproduced this with --no-members, a deleted .gitmodules, a URL-less members/IRRS, and submodule.weird.path=vendor/IRSS, after which the next run newly reported gitmodules-path-unnamed. Inspect the preserved HEAD sections in the same run or include their remediation, since the repository procedure relies on each finding naming its exact fix.

AGENTS.md reference: AGENTS.md:L235-L240

Useful? React with 👍 / 👎.

@brettheap

Copy link
Copy Markdown
Contributor Author

Independent review of #876 (#839) at 77f8a6c8, for lane openRepoShape-5: READY

I found no MUST. The one SHOULD is a fix to the PR body only, so no code changes and W856 has nothing to merge for it. The rest are NITs and new issues to file. I changed no branch, pushed nothing and posted nothing. $Q below is /tmp/claude-1000/-workspace-projects-openRepoShape/50cede29-ab82-4ca3-9f67-d98e1a58c8d2/scratchpad; my logs are in $Q/v118/rv876/. While I worked, main moved from 3bf2d3c9 to acc5a7bf.

Findings, most severe first

SHOULD 1 (PR body only): two claims in line 6 are false in two states I measured. Both states behave the same on 233a334e, so neither is a regression.

  • The body says "Now the first finding that writes the file puts it back first" and "No later finding puts it back". The code's own docstring (validate-family.py:2282-2290) is narrower: "the first of these findings". That narrower claim is true.
  • P5b. The state: OTHER recorded only at Members/IRSS, with HEAD's section giving that path; MEMBER's url gone; the work tree's .gitmodules deleted.
    • --no-members prints the wrong-case move first. Its lines are git config -f .gitmodules submodule.members/IRSS.path members/IRSS and git add -- .gitmodules (_move_context, :689-704).
    • member-gitmodules-url-absent follows with git checkout -- .gitmodules, which restores the one-key file the move has just staged.
    • Typed as printed and committed, HEAD's file holds only [submodule "members/IRSS"] path = members/IRSS and [submodule "members/IRRS"] url = …. The gate then reports member-gitmodules-absent and member-gitmodules-url-absent in every mode.
    • P5e. In members mode the move is a refusal printed alone. Typed, it leaves HEAD with one key, so every other section is lost.
  • P5d. The state: git rm --cached members/IRRS, the file gone, and OTHER's section gone from HEAD.
    • The absent finding puts the file back and writes OTHER's section.
    • Then member-gitlink-removal-staged prints git checkout HEAD -- .gitmodules. _put_back_lines (:2434-2435) reads "section taken" from the empty work tree, and that line wipes OTHER's section.
    • The commit then fails with nothing to commit, and member-gitmodules-absent comes back.
  • Fix: narrow the body to "the first of the url-absent and absent findings". File the two code faults as new issues (candidates C1 and C2 below).

NIT 1 (line 3): _forced (validate-family.py:717-738) is not exactly git's rule. I mapped 15 disk layouts × 3 index states × both git rm forms. The verdicts are identical on 2.43 and 2.47.1 ($Q/v118/rv876/p3/w243.txt, w247.txt).

  • Matches git on every checkout layout:
    • a .git file at another commit, with an untracked file, or with a modified file;
    • a .git directory, clean or with an untracked file;
    • a checkout moved there by a plain mv;
    • a link to the row's checkout;
    • nothing on disk, or an empty directory.
  • -f printed where git needs none (4 rows): a clean checkout at the staged commit. This includes the plain git mv state with nothing in the checkout. It loses no work.
  • -f missing where git refuses (14 rows):
    • a staged gitlink over a non-empty directory without .git, a regular file, or a dangling link (git exits 1);
    • a broken .git file with the index matching HEAD (git exits 128, not a git repository).
    • At gate level (P3, no-dot-git), the gate printed git rm --cached -- Members/IRRS with no -f, and typed it exited 1.
  • Why NIT and not MUST. By the brief's rule as written this is a MUST. I grade it NIT because:
    • each state needs a hand-made object at the variant; no git command or printed remedy makes one;
    • git's own error says (use -f to force removal);
    • base prints the same line.
  • If you hold the rule literally, the fix is a check for anything at the path that is not an empty directory, in place of the .git check.
  • In every row where git refused, -f touched no file. In P3b the untracked file stayed, and the checkout's commit was still in .git/modules/members/IRRS.

NIT 2 (line 2): what piping make's stderr changes. Measured on a pty with $Q/v118/rv876/p2/ptyrun.py.

  • Prompts and progress: fine, not a MUST. Continue? showed at 0.17 s, before the answer; progress dots arrived 1 s apart.
  • The member no longer has a terminal on stderr. Base: "stderr is a terminal"; head: "stderr is NOT a terminal". So git's default progress meters and any isatty(2) check go quiet. Colours that key on stdout are unchanged.
  • Line order is lost. A member's stdout and stderr lines no longer come out in order. Base alternated out 1|err 1|… three times out of three. Head once printed all six out lines before any err line.
  • A background child now holds the run. A recipe sleep 6 & … takes 6.1 s on head against 0.1 s on base. run_make reads until EOF, so a daemon that inherits stderr blocks the holder's make bootstrap until it exits.
  • Windows, read only: a member's Python writes stderr to the pipe in the ANSI code page, and passed_on writes those bytes to a console that expects UTF-8, so non-ASCII text comes out garbled.
  • None of this is in the docstring. Document it, or stop reading once make has exited and nothing is waiting in the pipe.

NIT 3 (line 2): three cases where the final line misleads. All under the holder's make, GNU Make 4.3.

  • A recursing member. With $(MAKE) inner and the inner command exiting 1, the line reads "the member's command exited 2", which looks like a refusal. No shape template's bootstrap or validate recurses, but an adopted root may keep its own Makefile.
  • make -k with more than 4 KiB on stderr after the Error line. The line says make "printed no Error <code> line", which is false: the line is there, just outside the last 4096 bytes.
  • A signal under dash. dash prints Killed and exits 137, so the line reads "exited 137". The run_make docstring says a signal leaves make's code alone.

NIT 4 (tests): my mutants. 7 of 13 are caught; 6 survive the new file ($Q/v118/rv876/mut/summary.txt).

  • R1, named[0]: survives. No test makes two Error lines. Also, w839's "M6, first Error line" was really named[0] + 1, so it does not test the first line.
  • R2, the regex without $: survives, but it is nearly equivalent to the original.
  • R7, sink.flush() dropped: survives 253 tests. On the pty that mutant hid Continue? until after the answer, which is the brief's MUST scenario, and no test pins it.
  • R8, regex anchored on ^make: : survives. The line-2 test never runs under a parent make, though make bootstrap always prints make[1]:.
  • R10 and R11, credential_advice ignoring unreadable or refused: both survive. In all nine no-advice states, stopper alone drops the advice.
    • With every member checked out and a value git refuses (or an unparsable file), the mutants print the advice right beside "NO MEMBER UPDATED, and not for want of a credential". That is line 1's own contradiction. Head is right there (probe5-*.log).
  • Caught: R3, R4, R5, R5b, R6, R9 and R12.
  • A docstring claim with no assert: test_a_git_mv_staged_with_work_in_the_checkout_is_undone_as_printed says "the checkout's commit is still in the member's store" but never checks it. It is true: I measured it in P3b.

NIT 5 (line 4, the link clause): scripts/family.py in #865 should carry it, not this PR.

The added item: RV877's O1

Checked and true

Line 1: the credential advice. Probe output: $Q/v118/rv876/p1/probe1-243.txt and probe1-247.txt.

  • In every state where the advice is now dropped, git's init dies before any clone starts. Exit 128 and no clone attempt, on both gits, in each of:
    • a url-less gitlink after a private member (with or without submodule.active=.), and one before it;
    • a value git refuses;
    • an unparsable file;
    • a stray gitlink;
    • an unmerged .gitmodules.
  • A control with no fault made 3 clone attempts, so the probe does see clones.
  • With every member checked out and a leg refused for want of a credential, the advice stands. The test pins that.
  • The token is scrubbed: fetch_incomplete replaced it with <redacted> in git's text. No other line prints a URL.

Line 2: the make cases. Under the holder's make bootstrap:

  • exit 1, exit 2 and exit 3 read "the member's command exited 1/2/3";
  • "no rule" gets the third line;
  • kill -TERM $$ makes make print Terminated, and the third line follows;
  • an ignored error then success is no failure;
  • an ignored error then a missing prerequisite gets the third line (the (ignored) line has no ***);
  • make -j2 prints a jobserver warning per member, as base does (11 of 11).
  • Make's stdout is byte-identical between base and head (1122 bytes).
  • No shape template's bootstrap or validate target recurses (templates/assembly-root/Makefile:44-50, family-root/Makefile:74-83).

Line 4: the guarded mv.

Line 3: what is left to commit. "Nothing of it is left to commit" holds in the clean, commit and untracked git mv states: nothing is staged after typing. Where HEAD records another commit at the variant, the finding says "commit that", and after the commit the gate passes in every mode.

Line 6: three findings that each write .gitmodules (P5a). A value git refuses in HEAD, MEMBER without a url, and OTHER's section gone:

  • in all three modes, with the file deleted or its deletion staged, the value finding is printed first and puts the file back;
  • typed as printed and committed, the gate passes in every mode.

Line 13: the moved checkout left behind. Confirmed in all three git mv states. After typing, the gate passes, git status reads ?? Members/, and family.py add --dry-run refuses with family-members-dir-variant, mv -- Members/IRRS <new name>. It belongs in an issue of its own (C4).

Pinned copies and docs.

  • Both scripts are in FAMILY_COPIED_VERBATIM (shape_materialize.py:528-530).
  • update-shape.py decides upstream-changed per row from P != T (:721-736), so each holder takes one row per file.
  • No test pins either file's bytes. The scripts' imports are unchanged and their mode stays 100755. No finding code is new.
  • No doc is touched. README.md:966-969, templates/family-root/README.md, AGENTS-shape.md and docs/cli.md still say only true things.

Fail on base. With the PR's three test files on 233a334e: 27 failed, 7 passed, 14 errors. The same IDs as w839's log, and the 7 passes are the body's 7.

Runs at 77f8a6c8.

  • 2.43, the 13 files: 747 passed, rc 0 (h243.log).
  • 2.47.1, the new file: 48 passed, rc 0 (h247.log).
  • w839's logs:
    • Both versions read 6335 passed and 59 skipped, rc 0, head 77f8a6c8, no tracked changes.
    • The shards are 1596+5, 2203+27 and 2536+27.
    • The skips break down as 27 + 22 + 5 + 5, none in the new file. All of this matches the body.

The restack.

  • simrestack.sh printed:
    • #819: simulated update-branch 58d88ca8
    • #876 77f8a6c8 over #819 58d88ca8: main acc5a7bf, predicted squash 2e584b6c (tree aab6f75d)
    • #876: simulated restack 46969fee (tree 0b54842c), clean
  • On that tree: 426 passed (restack.log).

Merge checks. merge-tree of 77f8a6c8 is clean against:

against tree
main acc5a7bf 0b54842c
main 3bf2d3c9 0c9cf07c
#865 f7072d08 73b10b76
#870 c0d8dd16 f87829fb

Sonar-style checks.

  • Duplication: no 10-line duplicate in the new file. At 8 lines, only the import block (9 of 600 lines) matches another file.
  • Asserts: none joins conditions with and at the top level.
  • Cognitive complexity (rv865b's counter, checked by hand):
function base head
fetch_members 16 14
_check_gitlinks_registered 19 18
_check_gitlinks_named 21 21 (the body omits it)
_check_members 14 14
new functions — 3 or under

The functions over 15 keep the first line of their def unchanged, so SonarCloud should count them as old code.

Candidates for new issues

  • C1. The wrong-case move's git config -f .gitmodules …path line, where the work tree's file is gone, commits a one-key file (P5e). In --no-members it also defeats the later put-back (P5b).
  • C2. member-gitlink-removal-staged prints git checkout HEAD -- .gitmodules when the work tree's file is gone, and that undoes an earlier finding's writes (P5d).
  • C3. A committed value git refuses, plus a stray gitlink. The value finding's --unset-all leaves .gitmodules unstaged, and the stray's git rm --cached then exits 128, "please stage your changes to .gitmodules" (P5g, the same on base). -f does not help.
  • C4. "validate-family.py: after the git mv state's lines are typed as printed, the checkout git mv moved stays at Members/<P>, untracked and never named, so family.py add refuses family-members-dir-variant. After the update, git -C Members/<P> status reads the row's path, so the checkout's untracked file goes unseen."
  • C5. RV877's O1, above.

Could not verify

  • Windows, macOS, Git Bash sh and bash 3.2. No busybox is installed here.
  • A make in another language: this host has only the C, C.utf8 and POSIX locales and no make.mo.
  • SonarCloud's real verdict, including whether S9073 flags the or assert at line 534. The Sonar MCP would not connect.
  • CI's git 2.55.0.

Scratch left; nothing of mine is running

  • Worktrees under /workspace/projects/openRepoShape-worktrees/_lane5-scratch/:
    • rv876-head, with an untracked tests/test_rv876_probe.py;
    • rv876-mut, with the scripts restored and an untracked probe copy;
    • rv876-base, with the PR's tests copied in and the probe file.
    • Each has untracked or changed files, so I left them, as you asked. rv876-restack was removed.
  • Logs and probes: $Q/v118/rv876/.
  • Basetemps: $Q/v110/bt/rv876-{h243,h247,probe,base,baseprobe,mut,restack}.
  • Git objects: simrestack.sh left unreferenced commits in the shared repo: 58d88ca8, 2e584b6c, 3f01e34d and 46969fee.
  • Processes: none of mine. The runs I see belong to c7-c819.

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

Copy link
Copy Markdown

@brettheap
brettheap marked this pull request as ready for review October 18, 2026 08:40
@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 5 days and 16 hours by commenting @sourcery-ai review. Upgrade to get a review now.

@brettheap
brettheap merged commit 41baf92 into main Oct 18, 2026
5 checks passed
@brettheap
brettheap deleted the fix/839-holder-bootstrap-texts branch October 18, 2026 08:41
brettheap pushed a commit that referenced this pull request Oct 18, 2026
…s squash 41baf92

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 18, 2026
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 18, 2026
…b-rules

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
brettheap pushed a commit that referenced this pull request Oct 18, 2026
…-blob-rules

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
brettheap pushed a commit that referenced this pull request Oct 18, 2026
…and its round 2, and run to main 33c5128 (#883)

Round 2b of #883. It answers RV883's full NIT list on ede678c (lane 3,
2026-10-18T09:29:28Z) and RV883's round 2 on 86baec5 (#883
issuecomment-6248362246). Each finding was checked against the code at
main 33c5128 before the text changed:

- The #843 limit is two: what `adopt-work-dir-shared` leaves open (group
  write, Windows, root as the other user, this user's own processes, the
  default work directory), and what it now refuses that v1.1.0 used
  (another user's directory, a run as root included, a 0777 directory
  without the sticky bit, a drive mounted without ownership), with the
  way out. "What a script sees" names `adopt-work-dir-shared` and
  `adopt-work-dir-occupied`.
- The doctor's NEXT lines, not its `members` row, read no url from an
  unmerged `.gitmodules`; the `make siblings` limit names that row.
  `siblings.py` has no unmerged rule; the `validate.yml` templates do.
- `validate-family.py`'s `family ok` is scoped to `--no-members` and to a
  holder whose members are checked out.
- The wording points: `update-pr-no-repository` under `--pr`, family.py's
  `git rm -r --cached` line, `MSYSTEM` set and not empty, `..` and
  `naming-unclassified`, the keys git refuses with no value, the url the
  root's config keeps, `contracts: cannot be listed`, install.sh's newest
  tag and redirect location, the scan test's name,
  `update-rewrite-symlink`, `os.access` for root, `LC_ALL=C`,
  `update-validators-red`, `heads/main` in both keys, where
  `plan written to` came, junctions on Python 3.12, the approval click and
  the run's approvals, and the back-port route's one run.
- #457's L1-17 is narrowed by #825; #853 and #854 join #834 as test
  limits.

The refresh past c5359df adds #870 (#864), #876 (#839), #791 (#787),
#859 (#838), #780 (#776), #873 (#857) and #875 (#874): 57 pull requests.
It drops the limits for #776, #839 and #874's ref rule, rewrites the #838
clauses, and adds the limits those squashes name (#863, #879, #884, an
older git without `GIT_NO_LAZY_FETCH`, family.py's link guard). The open
list is the 14 that the 09:42Z milestone read leaves after #875: #675,
#707, #739, #755, #764, #834, #840, #853, #854, #856, #863, #878, #879
and #884.

The six-and-six shape-copy lists stand: from c5359df to 33c5128 the
templates and repo_shape.py changed only in the family root's
bootstrap.py and validate-family.py, both listed. No other file changes.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Oct 18, 2026
…section's path outside `members/` out of the index before the section, and a gitlink there that no section gives is `gitlink-unnamed`, so the remedy typed as printed no longer leaves a gitlink at which a plain clone's `git submodule update --init` exits 128 while the gate passes (#878) (#882)

`_member_gitlinks` listed gitlinks in `members/` alone, so no mode asked
about one elsewhere. `git submodule add <IRSS> tools/x`, committed, or a
member added at `Members/IRRS` and moved with `git mv` to `renamed-1`:
the gate printed only the section's `--unset-all` lines, and typed as
printed they left the gitlink. The gate then passed in every mode while
`git submodule status` and a plain clone's update exited 128 with no
member checked out, which is how the holder's CI checks its members out.

- `_member_gitlinks` lists every gitlink (`_in_members` says which are
  in `members/`); `member-gitlink-unnamed` still asks only those.
- `gitmodules-path-unnamed` for a section at such a gitlink prints `git
  rm --cached -- <path>` and its commit first, then its own lines, once
  per path (`_gitlink_out_first`). Where HEAD's `.gitmodules` holds no
  section of its name, it says to stage the file and that nothing of it
  is left to commit.
- `gitlink-unnamed` (`_check_gitlinks_elsewhere`): a gitlink outside
  `members/` that no section of the work tree's `.gitmodules` gives,
  with bootstrap's cure, `git rm --cached` and the commit, or the commit
  alone where its removal is staged (`_second_path_out`).
- Each of those `git rm --cached` lines takes `-f`, with #828's clause,
  where the index stages the gitlink at a commit HEAD does not record
  and a checkout is on disk at it (`_forced`, #876), and comes after
  `git add -- .gitmodules` where lines printed before it may leave that
  file unstaged, or it is (`_stage_first`): git takes no gitlink out of
  the index then (`please stage your changes to .gitmodules`, exit 128),
  measured after a value finding's lines (#879's C3 order), an earlier
  unnamed section's, and a hand edit.

Tests: tests/test_family_unnamed_gitlink.py (new, 17 cases), and two
tests that pinned the old scope: `test_only_members_is_asked_about` and
rv648's H2 test, whose `--no-members` now names bootstrap's own line.

Lane: openreposhape-4 (openRepoShape-4)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Oct 18, 2026
…ad up to that line, as git reads it, and a parity test holds the value rules both family scripts write to one answer (#856) (#877)

Where the index's or HEAD's `.gitmodules` held a line git cannot parse,
`git config --blob` refused the whole blob, so bootstrap read no sections,
while git's own submodule reader takes the sections before that line. A
no-credential run then failed wrongly. Bootstrap now reads a `--blob`
listing that exits 128 for what it printed, by the exit code and not git's
English message, with `--no-includes` as git's submodule reader reads it.

Where the read was cut short, by such a line or by a key git takes no value
for (a bare `url`, `path`, `update` or `ignore`), the unread rows get no
write-back cure, which could not cure. They get one sentence instead: put the
file back, mend the line (or the key it names), and commit it. The index's
copy is put back with `git checkout --ignore-skip-worktree-bits --
.gitmodules`, which keeps what was staged and works in a sparse checkout;
HEAD's with `git checkout HEAD -- .gitmodules`. An index source that exits
128 with nothing printed is cut; `HEAD:.gitmodules` with nothing printed is
not, as #611 needs.

The value rules bootstrap and `validate-family.py` each write are held to
one answer by a parity test that runs one table through both scripts, so
neither can change alone. No pinned file or `scripts/repo_shape.py` function
is added. Both scripts are pinned copies, so each holder takes one
`upstream-changed` row per file.

Lane 4's independent reviewer RV877 (opus) found the first round NOT READY
(#877 issuecomment-6246457284). Rounds 1 and 2 answered it, and RV877 found
them READY (issuecomment-6247919456). Round 3 took its remaining SHOULD and
NITs, and RV877 found it READY (issuecomment-6248428136). Its optional NIT-5,
a git shim that also rewrites git's `error:` and `fatal:` prefixes, is left
out. After #876 landed, #877 was restacked onto #876's squash by three
merges, and its changed lines equal the reviewed delta. It lands on Brett
Heap's word "merge lane 3's v1.1.1 drafts after the v1.1.0 tag"
(2026-10-17T13:00:57Z).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap pushed a commit that referenced this pull request Oct 18, 2026
…derr is read and a `-` that lets an inner make fail puts its depth back, and the innermost rule's limits are in its docstring (#884)

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

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

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

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

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

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

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

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

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

| rv876's mutant on `281df263` | caught by |
|---|---|
| R1 `named[0]` | `[last-under-make]` |
| R7 no `sink.flush()` | `test_a_member_s_question_shows_before_its_answer_is_typed` |
| R8 `^make: ` | `[last-under-make]` |
| R10 advice ignores `unreadable` | `test_no_advice_beside_no_member_updated_with_every_member_checked_out[unparsed]` |
| R11 advice ignores `refused` | `test_no_advice_beside_no_member_updated_with_every_member_checked_out[value-refused]` |

**2. The final line misled in three cases** (`run_make`, `MakeErrors`).
- Measured on `281df263` and on this head, each member under the holder's `make bootstrap` and with `bootstrap.py` run straight (`-k` where the case needs it). The last five rows came from rv884's review:

| member | `281df263` | head |
|---|---|---|
| `$(MAKE) inner`, inner command exits 1 | `exited 2` | `exited 1` |
| `$(MAKE) inner`, inner command exits 2 | `exited 2` | `exited 2` |
| `$(MAKE) nothere`, the inner make's own failure | `exited 2` | make printed no `Error <code>` line for a command of the member's |
| `$(MAKE) -s nothere; exit 3` | `exited 3` | `exited 3` |
| `make -k`: `Error 1`, then 5000 bytes on stderr | make printed no `Error <code>` line (false) | `exited 1` |
| `make -k`: two commands exit 1, then 3 | `exited 3` | `exited 3` |
| a command killed by SIGKILL under dash | `exited 137` | `exited 137` |
| `kill -TERM $$` in a recipe | make printed no `Error <code>` line | the same |
| `$(MAKE) -s nothere \|\| exit 1` | `exited 1` | `exited 1` |
| `$(MAKE) -k inner`, whose second target prints after the inner `Error 1` | `exited 2` | `exited 1` |
| `make -k`: a finding, then a command that `kill -KILL $$` ends | `exited 1` | `exited 1` |
| `-$(MAKE) inner` (inner exits 1), then the recipe's own `exit 2` | `exited 2` | `exited 2` (`82a84f88` read `exited 1`) |
| `printf 'x: *** y' >&2; exit 1`, so make's line follows it on one line | `exited 1` | `exited 1` (`82a84f88` named none) |

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

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

| effect | `281df263` and head (documented) | reading only until make exits (prototype, not committed) |
|---|---|---|
| `test -t 2` in the member | false: the pipe | false |
| stdout and stderr order | out of order on the pty: one run printed `err 1` after `out 4`, another all six `out` lines first | the same |
| recipe `sleep 6 &` | 6.14 s | 0.29 s |
| `( sleep 1; echo late >&2 ) &` | 1.12 s, `late` shown | `late` lost, and the writer died at that write: its status file was never written |
| non-UTF-8 bytes (`caf` and 0xE9) | passed on as written | the same |

- **The background holder.** Stopping once make has exited and nothing is waiting fixes only the hold, and only by closing the pipe. The left-running process's next stderr write then meets a closed pipe and dies of SIGPIPE, silently. Holding the run is visible, ends when the process lets go of stderr, and the recipe can give the process its own stderr (`2>log`), as the docstring says.
- **The terminal.** Giving make the terminal back would lose its `Error <code>` line. Measured: `make --debug=j` prints only "Reaping losing child" on stdout, with no code, and no other output make writes carries the code. That would reopen line 2 of #839, which this change keeps closed: make still runs.
- **The line order and the Windows decoding** cannot be fixed by the reader. The docstring says each, with the Windows case read and not run.
- **Tests that hold the docstring:**
  - `test_what_a_member_s_make_leaves_on_stderr_is_passed_on_as_written` (the leaves test) asserts bootstrap's stderr bytes exactly: the non-UTF-8 line as written, then `late`, before the run ends;
  - the pty test asserts `stderr: no terminal` and `stdout: a terminal` from the member.

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

**Fail on base.** The test file at this head, run on `281df263`'s code: 3 failed, 54 passed. On `82a84f88`'s code (this pull request before fix round 1): 2 failed, 55 passed.
- On `281df263`, the 3 failures are the tests that pin line 2's change: `[innermost-under-make]`, `[innermost]` and `test_a_line_that_comes_in_pieces_is_read_whole`. On `82a84f88`, the 2 failures are both `[innermost*]`.
- These pass on `281df263` by design:
  - the pure mutant-killers `[last-under-make]` (R1, R8), the pty test (R7) and both `test_no_advice_beside_no_member_updated_with_every_member_checked_out` cases (R10, R11);
  - the tests that hold a docstring: `[signal]`, `test_what_a_member_s_make_leaves_on_stderr_is_passed_on_as_written`, and the store assert in both `test_a_git_mv_staged_with_work_in_the_checkout_is_undone_as_printed` cases.
- Each `INNERMOST` member, run straight with `-k` (depth 0):

| member | expected | `281df263` | `82a84f88` | head |
|---|---|---|---|---|
| `Recurse` | 1 | 2 | 1 | 1 |
| `RecurseRefuse` | 2 | 2 | 2 | 2 |
| `InnerOwn` | none named | 2 | none named | none named |
| `InnerThenOwn` | 3 | 3 | 3 | 3 |
| `InnerThenFinding` (X12) | 1 | 1 | 1 | 1 |
| `Long` | 1 | none named | 1 | 1 |
| `KeepsGoing` (X11) | 1 | 2 | 1 | 1 |
| `ThenKilled` (X7) | 1 | 1 | 1 | 1 |
| `LetFail` | 2 | 2 | 1 | 2 |
| `InnerLets` | 2 | 2 | 2 | 2 |
| `OwnStars` | 1 | 1 | none named | 1 |

- rv884's own X7, X11 and X12, its saved copies of `82a84f88` with each mutant, read their member wrong: `ThenKilled` says none, `KeepsGoing` says 2 and `InnerThenFinding` says none.

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

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

**Runs**, each through `run9s.sh` with `SHARDS=3` at this head, with no tracked changes. The files: `tests/test_family_holder_bootstrap_texts.py`, `tests/test_family_bootstrap_gate_order.py`, `tests/test_bootstrap_spelling.py` and `tests/test_pinned_copy_text.py`; the two that `git grep -l 'family-root/scripts/bootstrap' -- tests` names, `tests/test_family.py` and `tests/test_update_shape_kept_helper.py`; the trio, `tests/test_repo_hygiene.py`, `tests/test_agents_md_codes.py` and `tests/test_cli_reference.py`; and five more that run the holder's bootstrap: `tests/test_family_gitmodules_values.py`, `tests/test_family_head_lost_gitmodules.py`, `tests/test_family_members_not_a_list.py`, `tests/test_bootstrap_without_git.py` and `tests/test_family_makefile_siblings.py`.
- git 2.43.0: 1049 passed, rc 0 (shards 218, 394 and 437), at `dbd3a6dd`.
- git 2.47.1: 1049 passed, rc 0 (shards 218, 394 and 437), at `dbd3a6dd`.

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

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

**Sonar-style checks.**
- No duplicated block: no 10-line window that touches an added line appears anywhere else in `tests/`, `scripts/` or `templates/`. At 6 lines, only the import block matches another file.
- No assert joins conditions with `and`. The one `or` assert in the file is #876's.
- Cognitive complexity, counted by hand with Sonar's rules: no changed function is over 15, so no `def` line had to stay as it was. `said` counts each boolean sequence: `depth_said or 0`, `code == b"2" and ...`, and `code and not ...`.

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

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

Review: rv884, lane 5's independent review, READY at `82a84f88` (issuecomment-6248806027), with no MUST, one SHOULD (three surviving mutants) and four NITs. w884's fix round 1, `dbd3a6dd`, answered each (issuecomment-6249230294), and rv884's re-check found it READY with no MUST or SHOULD (issuecomment-6249507909). Its two NITs on the `(ignored)` rule, which cover rarer nested-make shapes, go to a follow-up. CI (Brett Heap's ruling 27 and his fast-refresh ruling of 2026-10-15T22:48:35Z): H0 dbd3a6d passed tests, tests-git-bash and SonarCloud; the head 85c2078 is a clean update-branch merge of main 0d5d2b1, with tests-git-bash and SonarCloud green; and the local gate passed on its predicted squash tree 5c273a1 over main 0d5d2b1: the family_holder_bootstrap_texts, repo_hygiene, agents_md_codes, cli_reference sets, 205 passed. Landed on Brett Heap's word of 2026-10-17T21:47:28Z, in lane 5's session (posted on #473 as issuecomment-6241758069): "merge lane 5's v1.1.1 PRs once reviewed and green".

Closes #884

Lane: openreposhape-5 (openRepoShape-5)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap pushed a commit that referenced this pull request Oct 18, 2026
…en back key by key is staged, no removal puts HEAD's `.gitmodules` back over an earlier finding's write, a clone of its own at a case variant is said to be one, a checkout `git mv` moved out of `members/` is named, and a run with no row says nothing was updated (#879)

Fix round 1 for #889: lane 1's review (issuecomment-6249673233), every
item taken.

- MUST 1: `_keys_back` ends the key-by-key lines with `git add --
  .gitmodules` and the text says to commit the file (P5); the first
  removal with the file gone puts back HEAD's where the index's lacks a
  section another staged removal needs (P4, as #876's head printed).
- SHOULD 1: `Report.wrote`, set by every finding that writes the file or
  says to commit it as it stands; a removal after one writes its section
  key by key and stages it, on disk too, leaving out a key HEAD's gives a
  value git dies on.
- SHOULDs 2 and 3: `_moved_checkouts` names each gitlink whose `.git`
  file names the member's store (`_store`, `_names_store`), at any path,
  and says of a case variant whose `.git` is a directory what the
  wrong-case move says of a clone of its own.
- SHOULD 4: bootstrap.py's docstrings say "where git's init in this
  holder stopped", and name the exit 128 its records do not show.
- NITs: the value finding's put-back in the module docstring (1); a
  symbolic link row (2); `_wrong_case_move`'s docstring and
  `_forced_over` per variant (3); `updated_none` asks nothing of the rows
  (4); `NOTHING UPDATED` under no row (5); a row for MX1 (6).

Tests: 16 new rows in tests/test_family_holder_remedies_compose.py, and
the C2 and C4 rows' expected text.

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

Labels

None yet

Projects

None yet

1 participant