Repository navigation
bootstrap.py: a validator's exit 2 ends bootstrap REFUSED, exit 2, an unreadable upstream path, cache or pin is no answer and a reftable leg's cherry-pick or revert is read from its ref store, so no traceback or ended pick follows (#764) - #815
Conversation
…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>
… or an ignored file in the way is refused, P9 names the branch it left, a HEAD outside `refs/heads/` is left on every path, and no git is `git-not-runnable`, so `make bootstrap` no longer ends a merge, overwrites a person's file or ends in a traceback (#704) (#694) #704, measured on lane 5's #682 head 8b39cd1 with git 2.43.0: - Line 1. The four moves that change files (P9's checkout of the branch at the pin from another branch, T7's checkout after `branch -f`, the on-branch `merge --ff-only` and the missing branch's `checkout -b`) overwrote an IGNORED file at a path the pin tracks, silently. Each now carries `--no-overwrite-ignore`, so git refuses it as it refuses an untracked file, in its own words. The missing-branch path now prints git's lines too, through `_say_refused`, and says where the leg stays (`leaving it on <branch>`), where it said `leaving detached` of a leg on a branch. - Line 2 (Brett Heap's R1). A checkout ENDED a merge, a cherry-pick or a revert in progress (head file gone, staged result carried onto the branch) from another branch, from HEAD detached at the pin and from HEAD already on the branch at the pin, and put a stopped rebase or `am` on the branch. `checkout_tracking_branch` now reads the leg's git dir (`rev-parse --git-dir`) for `rebase-apply/applying`, `rebase-apply`, `rebase-merge`, `MERGE_HEAD`, `CHERRY_PICK_HEAD` and `REVERT_HEAD`, in that order, and refuses before any path is chosen: it names the operation, says nothing moved, and prints `git -C <leg> <op> --continue` and `--abort`. - Line 3. `place_at_pin` calls `_say_left` when HEAD was on another branch, as `fast_forward_to_pin` does. - Line 4 (R2). `templates/assembly-root/AGENTS-shape.md` now says bootstrap places a leg at the pin ONLY where that loses no commit, and otherwise leaves its checkout where it is, saying why. The code is unchanged. - Line 5 (the issue's recommended answer). A HEAD that is a symbolic ref outside `refs/heads/` is left by `place_at_pin` (`which is neither`, as T7 already said) and by the missing-branch path (`which is not a branch`), since a commit made there lands on that ref. - Line 7. The family root's docstring paragraph (b) says the member puts its legs on their tracking branches at their pins where that loses no commit, and says why where it does not. The missing-branch path moves into its own function, `create_at_pin`, so `checkout_tracking_branch` stays within Sonar's cognitive-complexity limit. #694: with no git on PATH both roots ended in a `FileNotFoundError` traceback, exit 1. The assembly root's step (a) (per leg) and its offline upstream notice now run git inside `_git_runs`, and the family root's step (a) inside a guard around `git submodule update`; each refuses `git-not-runnable`, the validators' code with their first remedy sentence, and exits 2. The run stops there: every later step runs git too. Which `OSError` is git's is read by NAME, as `validate-pins.py` reads it. Tests: tests/test_bootstrap_leg_moves.py (lines 1, 2, 5, every path the issue names), tests/test_bootstrap_without_git.py (#694, both roots), and tests/test_bootstrap_after_bump.py's P9 tests now read the line naming the branch left. 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>
…ed by hand is refused as in progress, the missing-branch path names the branch it left and judges `checkout -b` by where HEAD is, and HEAD's ref is read as bytes, so rv704's S1-S3 no longer happen (#704) The fix round for rv704's review of #745 at 4e2b211. Every change is in templates/assembly-root/scripts/bootstrap.py and tests/test_bootstrap_leg_moves.py. - S1. When `git cherry-pick A B` or `git revert A B` stops and the person commits the stop by hand, the head file is gone and only `sequencer/todo` is left. Bootstrap moved the leg onto `main`, and the `--continue` that `git status` names then committed B there. `_in_progress` now asks `sequencer/todo` after the `REVERT_HEAD` row. The todo's first word picks the label: `revert` is a revert, anything else is a cherry-pick. Docstring (a) and `_say_in_progress` say so. - S2. `create_at_pin` prints `_say_left` under `created` when HEAD was on another branch, as P9 and T7 do. - S3. `create_at_pin` judges `checkout -b` by where HEAD is afterwards, as `_check_out` does (#533). Under a post-checkout hook that fails, it now says `created`, with git's words after it. It used to print `could not create ...` and `nothing else is tried` over a leg that was on `main` at the pin. - The S3 check reads HEAD back right after `checkout -b`. `_head_of` read `symbolic-ref` through `git_out`, which decodes strict UTF-8, so a branch whose name is not UTF-8 (`--branch` a name holding U+DCFF) would end the first run in a `UnicodeDecodeError`. On base the second run already ended that way, in `place_at_pin`. `_head_of` now reads the ref as bytes and decodes it with `os.fsdecode`. - N1. Docstring (d) now names where the exit is 2: step (a) and the upstream notice. - N2. The composite assert in the rebase helper is now two asserts. New tests: a sequence of each kind whose stop was committed by hand (the refusal, an unchanged snapshot, then the typed `--continue` finishing it on `other`), the failing hook from detached and from `other`, and a non-UTF-8 branch run twice. The ignored-file test now asserts the line saying the leg left `other`, on every path. 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>
…h main 2fac979) into fix/704-bootstrap-leg-moves: the family root's bootstrap.py keeps #694's `git_not_started` above #682's `cures` and `gitmodules_sections` (#704 x #648) One conflict region, in templates/family-root/scripts/bootstrap.py, where both sides inserted a function above `def gitmodules_paths`. - This branch inserted `git_not_started` (#694). - #682 inserted `cures` and renamed the function to `gitmodules_sections`, with a new docstring. The resolution keeps `git_not_started`, then #682's `cures` and its `gitmodules_sections` header and docstring, and drops the old `gitmodules_paths` header. It is byte for byte rv704's modelled resolution (`family-bootstrap-resolved.py`, e1322312). Everything else merged clean. The merged tree differs from 45c512e only in this branch's six files. 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>
…er) into fix/704-bootstrap-leg-moves: the family root's bootstrap.py takes `.gitmodules` as git's own parser reads it, a url-less member as its own reason and a cure true for a gitlink outside members/, with this branch's six files kept (#704 x #648) 23e1105 is one commit on 45c512e, which this branch already holds. It touches templates/family-root/scripts/bootstrap.py and tests/test_family_bootstrap_gate_order.py only. `git merge` took it with no conflict, and `git merge-tree --write-tree` had said the same before the merge. tests/test_family_bootstrap_gate_order.py still has `import re`. Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Sonnet 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>
…r) into fix/704-bootstrap-leg-moves: the family root's bootstrap.py takes a `.gitmodules` a merge left unmerged in the index as its own reason, with this branch's six files kept (#704 x #648) f74f640 is one commit on 23e1105, which this branch already holds. It touches templates/family-root/scripts/bootstrap.py and tests/test_family_bootstrap_gate_order.py only. `git merge` took it with no conflict, and `git merge-tree --write-tree` had said the same before the merge. tests/test_family_bootstrap_gate_order.py still has `import re`. Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… no answer and a reftable leg's cherry-pick or revert is read from its ref store, so neither ends in a traceback or an ended pick; docs/cli.md says #704's condition (#764) rv704's checklist candidates C1 to C4 on #745, filed as #764. Stacked on #745 (fix/704-bootstrap-leg-moves, f4935e1). The assembly root's bootstrap.py is a pinned copy, already re-pinned in this shape bump. - C1. `shape_upstream_notice` with `SHAPE_UPSTREAM_PATH` set to a directory this process may not enter ended `make bootstrap` in a PermissionError traceback, exit 1: `subprocess` raised it for the `cwd` of `git rev-parse HEAD`, naming the directory, which `_git_runs` passes on as not git's, and for a directory under one, Python 3.12's `Path.is_dir` raised it. `_clone_tip` now catches OSError around both reads and returns no answer, so the cache is asked next, as for a missing clone. A git that cannot be started is still `git-not-runnable`, exit 2: that is a Refusal, which `except OSError` does not catch. - The same notice, under the issue's goal that `make bootstrap` never ends in a traceback over it: a `.shape-upstream-tip` nobody may read (PermissionError) or whose bytes are not UTF-8 (UnicodeDecodeError) is no answer (`_cached_tip`); so is a `contracts/` nobody may enter, at the notice's own `is_file`, and a `contracts/shape-pin.yaml` that is a list (AttributeError), which `validate-pins.py` has refused by then. - C4. Measured with git 2.47.1: on a reftable repository a stopped cherry-pick and a stopped revert leave no CHERRY_PICK_HEAD or REVERT_HEAD file in the git dir; each is in the ref store. A stopped merge leaves MERGE_HEAD a file. Reproduced on f4935e1 with that git: a scaffold cloned with GIT_DEFAULT_REF_FORMAT=reftable, a cherry-pick (or revert) stopped on `other` in the spec leg, then bootstrap, which moved the leg onto `main` and ended the operation. `_in_progress` now asks the ref store for those two markers too, by the exact name (`git show-ref --exists`, git 2.43 and later), so a branch or a tag of that name is no operation, and the refusal says `is in its ref store` where the marker is there. A git before 2.43 refuses the option and is answered no; it has no reftable either. - C3. The generated `docs/cli.md` row for the assembly root's bootstrap.py takes R2's condition from the pinned AGENTS-shape.md ("only where that loses no commit, and otherwise leaves its checkout where it is, saying why"), through `scripts/render-cli-reference.py`. The three pinned sentences that say "or says why not" (the Makefile's help, the family holder's AGENTS-shape.md and README) are true as they stand and left, as R2 left them, so no project or holder takes another `upstream-changed` row for them. - C2 (a validator's exit-2 refusal reported as `bootstrap FAILED: ... (exit 2)`, exit 1) needs a ruling and is measured, not changed. New tests: tests/test_bootstrap_upstream_notice.py (the notice's reads, end to end and, for `contracts/`, in process), tests/test_bootstrap_ref_store.py (a reftable clone, skipped where git makes none, and a branch and a tag named as markers on every git) and tests/test_bootstrap_placement_texts.py (the reference row against R2's sentence). Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
(lane openRepoShape-5, 2026-10-17T11:28Z) @codex review |
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
Reviewer's GuideThe PR hardens bootstrap's upstream notice against unreadable inputs, prevents cherry-pick or revert operations from being accidentally ended on reftable legs by detecting ref-store markers, and updates the generated CLI documentation to describe conditional leg placement. Sequence diagram for resilient upstream notice lookupsequenceDiagram
participant Bootstrap as bootstrap.py
participant Clone as SHAPE_UPSTREAM_PATH
participant Cache as .shape-upstream-tip
participant Notice as Upstream notice
Bootstrap->>Clone: _clone_tip()
alt clone is unreadable
Clone-->>Bootstrap: OSError
Bootstrap->>Cache: _cached_tip()
else clone is readable
Clone-->>Bootstrap: git rev-parse HEAD
end
alt cache is unreadable or invalid
Cache-->>Bootstrap: None
Bootstrap-->>Notice: no notice, bootstrap continues
else valid tip is available
Cache-->>Bootstrap: commit tip
Bootstrap-->>Notice: compare tip with pinned commit
end
Sequence diagram for reftable operation detectionsequenceDiagram
participant Bootstrap as bootstrap.py
participant Leg as Leg repository
participant GitDir as Leg git dir
participant RefStore as Leg ref store
Bootstrap->>Leg: _in_progress()
Bootstrap->>GitDir: Check CHERRY_PICK_HEAD or REVERT_HEAD
alt marker is a file
GitDir-->>Bootstrap: operation marker
Bootstrap-->>Leg: _say_in_progress(..., git dir)
else marker is not a file
Bootstrap->>RefStore: _in_ref_store(name)
RefStore-->>Bootstrap: git show-ref --exists name
Bootstrap-->>Leg: _say_in_progress(..., ref store)
end
Leg-->>Bootstrap: leave checkout unchanged
Flow diagram for conditional bootstrap leg placementflowchart TD
Start([Bootstrap leg]) --> Detect{Operation in progress?}
Detect -->|Yes| Refuse[Report marker location and leave checkout unchanged]
Detect -->|No| Loss{Would placement lose a commit?}
Loss -->|Yes| Explain[Leave checkout where it is and say why]
Loss -->|No| Place[Place leg on tracking branch at pinned commit]
Refuse --> End([Bootstrap completes])
Explain --> End
Place --> End
File-Level Changes
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
|
Codex Review: Didn't find any major issues. 🎉 Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
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". |
Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
(lane openRepoShape-5, 2026-10-17T13:02:17Z) Brett Heap ruled on #764's line C2, in lane 5's session at 2026-10-17T13:01:10Z: "take B for #764's C2".
Lane 5's writer w764b is adding this to #815 as one new commit, with tests and the body's C2 section updated. Reviewers: please review from the head that commit makes. |
…name> (exit 2)`, exit 2, and a finding still ends it `bootstrap FAILED`, exit 1, so a script or a setup run that reads the code can tell a refusal from a finding (#764) #764 line C2, option B, on Brett Heap's ruling of 2026-10-17T13:01:10Z: "take B for #764's C2". A validator that cannot check (`validate-pins.py` in a plain clone, which refuses `pin-submodule-uninitialized`) exits 2, and bootstrap said `bootstrap FAILED: pins (exit 2)` and exited 1, the code of a finding. Every other refusal in the file exits 2. - A validator's exit 2 now prints `bootstrap REFUSED: <name> (exit 2)` and bootstrap exits 2. - Any other non-zero exit is a finding and stays `bootstrap FAILED: <name> (exit <code>)`, exit 1. - One validator that finds and another that refuses: both lines, the findings first, and exit 2, since a check that could not run outranks a finding. `_run_neutral_validators` returns each failed validator's label and exit code, and the new `_end_of_run` says them and returns the exit. The step (d) paragraph of the module docstring says what the ending does. `make bootstrap` exits 2 for any failing recipe, so only its `Error 1` line becomes `Error 2`. `setup-project.py` step (7) and `siblings.py` print or pass on the code bootstrap gave, and no text there named exit 1. `tests/test_bootstrap_refusal_exit.py` runs bootstrap in a plain clone (a refusal: REFUSED, exit 2), in a recursive one with a manifest finding (FAILED, exit 1, as before) and in a plain one with both (both lines, exit 2), once through `make bootstrap`, and asks `_end_of_run` of nine mixes of codes, an exit 3 and an exit 127 included. Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
(lane openRepoShape-3, 2026-10-17T18:17:36Z) RV815's re-check of #815's NIT round (head The first review's SHOULDs are done:
The taken NITs:
NIT 1 left out: acceptable.
Nothing else changed: Runs (stub gh first on PATH,
Mutants: X3 is killed (2), and F1, the flush removed, is killed (2). Merges: clean against main CI at Lane: openreposhape-3 (openRepoShape-3) |
…t stopped in a holder whose refs are a reftable, asking its ref store for `CHERRY_PICK_HEAD` and `REVERT_HEAD` by their exact names where no file holds them (#684) (#846) R612 E4, the last item of #684 (#816 landed the other five). Lane 5's probe, on git 2.47.1 with `git init --ref-format=reftable`: a stopped cherry-pick and a stopped revert keep `CHERRY_PICK_HEAD` and `REVERT_HEAD` in the ref store and in no file, and `MERGE_HEAD`, `rebase-merge/` and `sequencer/todo` stay files. `_operation_in_progress` asked for files alone, so `add`, `bump` and `remove` found neither and went on to their commit. - `_in_ref_store` asks `git show-ref --exists <mark>`, the exact name, in the `CHERRY_PICK_HEAD` and `REVERT_HEAD` rows, where no file is there. - Exit 0 is held, and 2 is not. - 129 is not held either: that is a git before 2.43, which cannot open a reftable at all (measured on 2.43 and 2.40.4). - Any other exit is a failed git command, exit 2, before the first write. It is never read as "nothing in progress". - The refusal, and `yaml-unparsable`'s remediation, say git keeps the mark "in its ref store, not as a file". A mark that is a file is named at its path, byte for byte as before. Every file check stays. `MERGE_HEAD` is a file on both backends, so its file is the whole answer. - `tests/test_family_operation_reftable.py` has 18 cases: - a stand-in git holds the refusal under both runs, the 129 answer and git's own failure (this host's git, 2.43, makes no reftable); - the exact-name rows (a branch `CHERRY_PICK_HEAD` and a tag `REVERT_HEAD`) and the order rows run on every git; - the stand-in rows and the order rows check that the ask runs in the holder (rv846's M10, which asked in `Path.cwd()`, is killed on git 2.43); - four real-reftable rows skip, saying why, where `git init --ref-format=reftable` fails. - Mirrors #815's `_in_ref_store` in the assembly root's `bootstrap.py`, without importing it. Closes #684 Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…hild is told the encoding its words are read in, so a cp1252 stdout gets one encoding from setup, and `make bootstrap` passes a project whose name the code page cannot hold (#851) `setup-project.py`'s `main` reconfigures its stdout and stderr to UTF-8, and `run` started every child with no `env=`, so a Python child took its streams' encoding from its own environment. Under `PYTHONIOENCODING=cp1252:strict` or `cp1252:surrogateescape` (Windows' ANSI code page on a stdout that is not the console), a `--yes --local-remote-dir` run exited 0 with 9 of its 129 stdout lines in cp1252 among UTF-8 ones, and the plan run again to read why it was refused, the one captured child, wrote cp1252 into a pipe `run` decodes as UTF-8. The assembly root's `scripts/bootstrap.py` started the validators the same way, so on a project whose `name:` the code page cannot hold, `validate-manifest.py` raised `UnicodeEncodeError` and `make bootstrap` ended `bootstrap FAILED: manifest (exit 1)`: #254's defect, in a pinned copy. - `setup-project.py`: `child_environment(capture)`, #254's rule as `scripts/shape_materialize.py` writes it in `inherited_child_environment` and `captured_child_environment`. `run` passes it as `env=`, so all six Python children get it, and the next one will: `self_bootstrap`, `check_names`, `plan_and_confirm`, `why_the_plan_did_not_go_through` (the captured one), `scaffold` and `clone_and_bootstrap`. It holds a copy because it runs before any checkout of the standard exists. - `templates/assembly-root/scripts/bootstrap.py`: `inherited_child_environment()`, a copy of the library's, which `_run` passes. A project does not carry `shape_materialize.py`, and a pinned script that imported a new name out of `repo_shape.py` would be refused at `update-shape.py apply` (`update-accept-local-missing-name`, #536), so the copy keeps `repo_shape.py` untouched. This is a shape bump: every project re-pins the copy. - `tests/test_validator_child_encoding.py`: the `setup-project.py` row leaves `NOT_YET`, and `LAUNCHERS` gains the assembly `bootstrap.py`. `COPIES` holds both copies to the library's answer for every stream `INHERITED` names. `run` is shown to tell each child what it is read in, both tools run end to end under cp1252, and `INHERITING` pins that `siblings.py`'s child inherits an environment its code page agrees with: measured, it needs no change. It follows lane 5's #808 (#759), landed as 677f9bf, whose `NOT_YET` row this takes out, and lane 4's #824 (#710), landed as 0dee22b, which also edits the assembly `bootstrap.py`. Lane 5's #815 (#764) edits it too; the two merge clean, and whichever lands second re-gates. Closes #836 Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e a Windows editor saved with a byte-order mark reads as the text it holds, so the upstream notice names the tip, a marked `project.yaml` is no longer `schema_version is None`, and every reader of a marked pin, the doctor's included, reads it alike (#855) (#868) Issue #855 has two lines, filed by lane 5 from rv815's NIT 8 on #815. Both are measured on main 9f235da by loading the shipped scripts and calling them, and by running them in a scaffold. Line 1. `shape_upstream_notice` read `.shape-upstream-tip` with `read_text(encoding="utf-8").strip()`. `strip()` keeps U+FEFF, so a cache holding EF BB BF and a commit, with LF or CRLF, did not match `COMMIT_40` and the notice said nothing. The cache is read as `utf-8-sig` now, and each of the three says `shape: the upstream is at bbbbbbbbbbbb and this project is pinned at aaaaaaaaaaaa.` Line 2. `load_yaml` read the mark as the first character of the first key (U+FEFF and `commit`), and refused a file whose first line is a comment: `line 1: expected `key: value`, got '\ufeff# a comment'`. In a scaffold a marked `project.yaml` was `FINDING manifest-schema-version: schema_version is None, expected 1`, `make bootstrap` ended `bootstrap FAILED: manifest (exit 1)`, the doctor said `INVALID (manifest)`, and `update-shape.py apply` re-pinned and rolled back (`REFUSED update-validators-red`). `parse_yaml` takes off every byte-order mark the text starts with now, as `shape_advisory.py` already did, and `load_yaml` still decodes as `utf-8`, so its not-UTF-8 refusal still counts the byte from the file's first byte. The same scaffold is `manifest ok`, `pins ok`, `bootstrap ok` with the notice, `COMPLIANT`, and `apply` re-pins and keeps the mark in both files. WHY THE PARSER, NOT `load_yaml` (the coordinator's ruling, as first given, was `utf-8-sig` at both reads, on the condition that every reader agrees). Five readers hand `parse_yaml` a pin's text without `load_yaml`: the doctor's `pin_at`, `update-shape.py`'s `_committed_pin` and `_accepted_in_the_pin`, `adopt-project.py`'s `_pinned_shape_paths`, and `shape_materialize.pin_values_in`. With the mark taken off in `load_yaml` alone, a committed pin a person saved with the mark above a comment line, re-pinned by `apply` and read by every validator, was one `pin_at` read as broken; with the working pin then broken, the doctor's NEXT was `git checkout '<the hand-saved commit>~1' -- contracts/shape-pin.yaml`: the pin from before the hand-save, naming the commit `apply` had moved it off. Taken off in the parser, the NEXT is `checkout HEAD -- contracts/shape-pin.yaml`. tests/test_bom_reads.py: both lines in each shape the issue gives, with no mark, one and two; the four readers above against `load_yaml`; the advisory against `load_yaml`; a marked scaffold bootstrapping ok; the not-UTF-8 byte counted from the file's start; and the doctor's NEXT after a re-pin of a hand-saved pin. Round 1 (RV868's SHOULD-1) pins the strip to the start of the text: a mark inside a quoted value, at the start of a value or at the start of a later key stays text, as PyYAML reads it, and the notice says nothing of a pin whose commit holds one. A mutant that took off every mark anywhere survived 749 tests before it, and is killed now. Lane: openreposhape-4 (openRepoShape-4) 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) (#819) Written on #682 (`fix/648-bootstrap-and-gate-order`, `f74f640c`), which landed as `dc7d3fbb` while this was written. The commit is `fa69ead9`. The branch then merged #682's final head `5afb24b9` and main, taking its own side for #682's files: those files are the same at `5afb24b9` and `dc7d3fbb`, and main changed none of them after the squash. The head is `c12b5f8a`, and its diff against main is this PR's four files. **Pinned copies.** This changes the family root's `templates/family-root/scripts/bootstrap.py` and `templates/family-root/scripts/validate-family.py`. Each holder sees them as `upstream-changed` on its next `update-shape.py check`, and they are re-pinned in the next shape bump; this PR re-pins nothing. No test pins either file's bytes or digest. README, the handbook, `AGENTS.md` and `docs/cli.md` are unchanged: no `--help` text changes, and no sentence in them is made false. CI's run 39031235254 (git 2.55.0) failed one row of the new test file, and lane 2's review rv819 of `c12b5f8a` (NOT READY) found one MUST, four SHOULDs and five NITs. This commit answers each of them. It changes `templates/family-root/scripts/bootstrap.py` and `templates/family-root/scripts/validate-family.py`, both pinned and both already changed by this PR, and `tests/test_family_gitmodules_values.py`. No other pinned file changes. ### What git does with a `branch` that has no value Measured on git 2.43.0 (the host's) and 2.47.1 (a local build). The probe adds a `branch` line with no value after a submodule's url, commits it, then runs `git submodule update --init --recursive` in a plain clone: | `.gitmodules` read from | git 2.43.0 | git 2.47.1 | |---|---|---| | the work tree's file | crash, exit 139 (`Segmentation fault`) | `error: missing value`, `fatal: bad config line 4 in file`, exit 128 | | the index's (work tree file lost) | crash, exit 139 | `error: bad config line 4 in blob`; it reads on, checks the submodule out, exit 0 | | HEAD's (index entry lost too) | crash, exit 139 | the same as the index's | A `url` with no value gives 2.47.1's answers on both gits. `branch =`, `branch = .` and `branch = -x` are taken by both gits from every source (exit 0), as both tools read them. CI's log shows git 2.55.0 giving 2.47.1's answer for the work tree's file. From git's source, `submodule-config.c`: the `branch` case has `if (!value) ret = config_error_nonbool(var);` at v2.43.1 and v2.44.0. v2.43.0, v2.42.4 and v2.39.5 go straight to `xstrdup(value)`, which crashes. ### Each finding 1. **MUST: the row pinned git 2.43's crash. Fixed.** Reproduced on git 2.47.1 at `c12b5f8a`: `assert 128 == 139`, as on CI (1 failed, 27 passed). The table's exit column is gone. The premise is now that git's update died and checked nothing out; every other assertion is about the tools' own output. The two `shutil.Error` setup errors in that run are #645's copy race, which #713 fixed on main. 2. **SHOULD: the index and HEAD rule fixed `branch` to git 2.43's crash. Fixed, by measurement.** On 2.47.1 at `c12b5f8a`, with the work tree's file lost and no credential, bootstrap said `NOT CHECKED OUT, and not for want of a credential: ... git cannot read .gitmodules`, right under git's own `bad config line 7 in blob`, and failed. New `crashes_on_branch` reads `git version`. Before 2.43.1, or where it cannot read a version, the key is refused as before. From 2.43.1 on, the line ends the reading of a blob, as a `url` with no value does (`ends_blob_reading`). The gate needs no change: it reads the work tree's file and HEAD's, which a clone checks out as its work tree's file, and every git dies on the key there. The comments that said "git crashes" now say which git does. 3. **SHOULD: with every member checked out, a `.gitmodules` git cannot parse ended `family bootstrap ok`. Fixed.** `Gitmodules` now carries `unreadable`. It is set where the work tree's file is one `git config --list` cannot parse, which git dies reading. With every member checked out, the run prints `NO MEMBER UPDATED, and not for want of a credential: git cannot parse .gitmodules. Mend .gitmodules by hand (`git config -f .gitmodules --list` says where it breaks), and commit it.` and fails on `.gitmodules (git cannot read it)`, as it does for a refused value. A row left not checked out now gets that reason too, where before it was "no section of .gitmodules gives that path". Tested with a conflict marker and with an open quote. A blob is not marked: see the checklist candidates. 4. **SHOULD: mutants survived. Each now fails a test:** - Reading HEAD's file in place of the index's: `test_the_index_s_file_is_read_where_it_differs_from_head_s` stages `update = None` without committing it. - `update` without `merge` in bootstrap, or without `rebase` in the gate: `update = rebase` and `update = merge` are now in `TAKEN`. - The stop-reading rule applied inside ignored sections: `test_a_key_with_no_value_in_a_section_git_ignores_reads_on`. - `pattern_names` matching a prefix without the `/`: a new `a-string-prefix` row, with two `submodule.active` patterns, `members/IRRS` and `members/IRS`. - `active` reading the first value: OTHER's own `active` is now set twice, `true` then `false`. - `_written_back` keeping a url that begins with `-`: a new `url-after-a-dashed-one` row. - The `<url>` placeholder: `test_a_url_no_row_gives_one_for_is_written_back_as_a_placeholder`. - `_submodule_entries` without the backslash rule: the ignored-section test now runs on `..\x` too. - `if refused_here:` made `if refused:`: `test_a_value_head_s_file_alone_gives_leaves_the_work_tree_s_read`. 5. **SHOULD: cognitive complexity. Split.** These are estimates from an AST count of Sonar's rules. At `c12b5f8a` it gives the same figures rv819 gave. - `gitmodules_sections`: 33 → 5. Its parts are `gitmodules_source` (8), `gitmodules_entries` (9), `ends_blob_reading` (2) and `read_sections` (13). - `unfetchable`: 16 → 10, with `row_reason` (8). - `cures`: 24 → 22. It was 23 on main, so this PR no longer raises it; its `.gitmodules` sentences moved to `gitmodules_cures` (3). - `_check_gitmodules_values`: 22 → 10, with `_value_said` (2) and `_value_taken_out` (5). - `fetch_members`: 10 → 13. 6. **NIT: duplicated test code. Done in this file.** The gate-in-three-modes block is now `found_in_every_mode_and_typed`, and the good-run block is `assert_a_good_holder_s_run`. The 4-line `EXITED_0` assertion it shares with `test_family_bootstrap_gate_order.py` stays: it is shorter than a duplicated block, and sharing it means editing a file #682 landed. 7. **NIT: the rules are written twice. Left.** One copy in `repo_shape.py` would change the module every root imports, and every pinned copy of it, which is more than a fix round should carry. Filed below as a candidate. 8. **NIT: a false sentence in the HEAD-only case. Fixed.** Where HEAD's file alone gives the value, the finding now ends "and git dies reading a file that does, so in a clone neither `git submodule update` nor make bootstrap checks out any member." 9. **NIT: the person's own config could change the boolean answer. Fixed.** Measured: with `probe.value = notabool` in a global config, in `GIT_CONFIG_COUNT`, or given with `-c`, the old probe dies (`bad boolean config value 'notabool'`). Both tools now ask `git config --file <null device> --type=bool --default=<value> --get probe.value`, which reads no config scope (measured on 2.43.0 and 2.47.1). New test: `test_a_probe_value_in_the_person_s_own_config_changes_no_answer`. 10. **NIT: a count in this body. Corrected here.** Under "Fail on base", seven of the 28 tests pass on the base, not six. The seventh is the control `[a-leading-directory]`. The totals, 24 failed and 18 passed, were right. ### Tests `tests/test_family_gitmodules_values.py` now has 52 tests, up from 28. Every run used `run9.sh`: a `gh` stub, a bogus token and an empty `GH_CONFIG_DIR`. | run | tree | git | result | |---|---|---|---| | the file | e1379a9's | 2.47.1 | `52 passed in 67.32s` | | the file, `test_repo_hygiene.py`, `test_agents_md_codes.py`, `test_cli_reference.py`, `test_pinned_copy_text.py`, `test_bootstrap_spelling.py`, `test_validate_family_without_git.py`, `test_family_bootstrap_gate_order.py`, `test_family_head_lost_gitmodules.py` | e1379a9's | 2.43.0 | `573 passed in 252.33s` | | wide half A: 26 files that load or run the family root's `bootstrap.py` or `validate-family.py`, this file among them | e1379a9's but for three docstrings and one byte match in a test | 2.43.0 | `1474 passed, 22 skipped, 1 xfailed in 1321.42s` | | wide half B: 29 more such files, the three above among them | the same | 2.43.0 | `1933 passed, 1 skipped in 1097.64s` | | the file, `test_family_bootstrap_gate_order.py`, `test_bootstrap_after_bump.py`, `test_family_head_lost_gitmodules.py`, `test_repo_hygiene.py` | the predicted merge with main `64ec47c3` | 2.43.0 | `250 passed in 229.25s` | The wide set is the 49 files w768 found with `git grep`, plus 6 more that name the family root's `bootstrap.py` (`test_family_siblings_placement.py`, `test_park_resume_targets.py`, `test_pins_child_kind_and_siblings_root_spelling.py`, `test_readme_windows_quoting_sentence.py`, `test_setup_project_py.py`, `test_windows_paths.py`). Both halves ran in a clone with that tree committed, since the `update-shape` tests fail on a work tree whose copies are not at HEAD. ### Fails on base The new file was run against `c12b5f8a`'s scripts: - on git 2.47.1: 18 failed, 34 passed (`r2-base247.log`); - on git 2.43.0: 16 failed, 36 passed (`r2-base243.log`). The tests that fail there: - the three unparseable-file tests; - the two blob `branch` tests (2.47.1 only); - the nine `git version` rows; - the three HEAD-only texts; - the probe test. The tests added for finding 4 pass on the base, as they should: each catches a mutant, not a defect of the base. The original file fails on git 2.47.1 at `c12b5f8a` with CI's assertion: 1 failed, 27 passed. ### Mutants Each mutant was applied alone to the final tree and caught (`mutants2.log`): | id | change | caught by | |---|---|---| | M1 | a blob's `branch` never ends the reading | blob `branch` tests, git 2.47.1 (2.43.0 passes, as it should) | | M2 | a blob's `branch` always ends the reading | blob `branch` tests, git 2.43.0 (2.47.1 passes, as it should) | | M3 | `BRANCH_READ_SINCE` is (2, 44, 0) | `which_git[2.43.1]` | | M4 | `<` made `<=` | `which_git[2.43.1]` | | M5 | an unreadable version taken for one that does not crash | `which_git[unread]` | | M6 | the version read with two numbers | `which_git[2.43.1]` | | M7 | `unreadable` never set | the three unparseable-file tests | | M9 | `fetch_members` asks `refused` alone | the every-member unparseable tests | | M10 | no mend sentence | the three unparseable-file tests | | M11 | `row_reason` without `unreadable` | the per-row unparseable test | | M12 | bootstrap's old boolean probe | the probe test | | M13 | a blob never ends the reading | the `url` index test and the blob `branch` tests, git 2.47.1 | | M14 | ignored sections asked for refused values | the ignored-section tests | | M15 | HEAD's file read in place of the index's | the staged-index test | | M16 | the stop rule inside ignored sections | the ignored-section blob test | | M17 | `pattern_names` matches a bare prefix | `[a-string-prefix]` | | M18 | `active` reads the first value | the `active` test | | M19 | bootstrap's `update` without `merge` | `TAKEN[update-merge]` | | V1 | the HEAD-only sentence never used | the three HEAD-only texts | | V2 | the gate's old boolean probe | the probe test | | V3 | `_written_back` keeps a url that begins with `-` | `[url-after-a-dashed-one]` | | V4 | no `<url>` placeholder | the placeholder test | | V5 | `_submodule_entries` without the backslash rule | `[backslash]` | | V6 | `if refused:` for `if refused_here:` | the HEAD-only `gitmodules-path-twice` test | | V7 | the gate's `update` without `rebase` | `TAKEN[update-rebase]` | ### Merge checks `git merge-tree --write-tree` of this head with: - main `64ec47c3`: clean, and its tests pass (see Tests); - #747 `ff7c59b8`: clean; - #822 `ea86cb60`: clean; - #815 `f6f318d1`: clean; - #820 `9e2dc7d9`: conflicts in `validate-family.py`, in the same three regions as before. The conflict blocks are byte for byte the ones `c12b5f8a` gives, so the resolution rv819 checked still applies. ### Checklist candidates - **A blob git cannot fully parse is read as no section at all.** Measured on 2.43.0 and 2.47.1: - Where the work tree has no `.gitmodules` and the index's file holds a bad line, git reads the sections before that line, says `error: bad config line 4 in blob`, and checks the members out. - `git config --blob <oid> --list` prints those entries but exits 128. So `gitmodules_sections` reads nothing. - With no credential, every row is then named "no section of .gitmodules gives that path" and the run fails, where the members are only the credential's. - This came with #648's blob reading. This round leaves it as it was. - **One copy of the `.gitmodules` value rules** (rv819's NIT 7). Bootstrap's `NO_VALUE_REFUSED`, `UPDATE_VALUES`, `refused_value`, `git_bool` and `TAKES` and the gate's `_values_refused`, `_git_takes_bool` and `GIT_TAKES` could live in `repo_shape.py`, so a change to git's rules is made once. Git 2.43 dies reading a family holder's `.gitmodules` before `git submodule update` checks out any member when a section gives: - a `path`, `url`, `update`, `ignore` or `branch` with no value (`bad config line`; a `branch` with none crashes git, exit 139); - an `update` other than `none`, `checkout`, `rebase` or `merge`, a `!command` included (`invalid value`); - a `shallow` that is no boolean (`bad boolean config value`); - a `fetchRecurseSubmodules` that is neither a boolean nor `on-demand`. The holder's bootstrap listed the file with `git config --list`, which reads all of these, and ended `family bootstrap ok`, exit 0. The gate refused `member-uninitialized` members checked and under `--pins`, naming the update that dies, and said `family ok` under `--no-members`. ### Line 1: bootstrap `gitmodules_sections` checks each `submodule.*` entry against those rules (`refused_value`). Where one would make git die, it returns those entries and no sections, as it returns `unmerged`, and `unfetchable` asks that second, after `unmerged`. Every row not checked out gets one reason: ``` NOT CHECKED OUT, and not for want of a credential: members/Atlas (git cannot read .gitmodules: `submodule.members/Nova.update` is `None`), members/Nova (git cannot read .gitmodules: `submodule.members/Nova.update` is `None`). Give `submodule.members/Nova.update` a value git takes, one of `none`, `checkout`, `rebase` or `merge`, or take that line out, and commit .gitmodules. `python3 scripts/validate-family.py --no-members` prints the commands. family bootstrap FAILED: Atlas (not checked out), Nova (not checked out) ``` A key with no value says "has no value". As for every other reason, the holder's records decide, never git's stderr. **Where every member is checked out**, as in the holder itself, git dies before it updates any of them, so no row is left to name, and the run used to say ok over git's `fatal:` line. It now prints `NO MEMBER UPDATED, and not for want of a credential: git cannot read .gitmodules: ...` with the same cure, and fails on `.gitmodules (git cannot read it)`. That is my reading of the goal, "so neither one says ok over a file git refuses". The issue's direction named only the rows not checked out. ### Line 2: the gate A new finding, `gitmodules-value-refused`, for each such key in the work tree's `.gitmodules` and in HEAD's (HEAD's as `member-gitmodules-absent` reads it). It is asked before any member check, so `--no-members` makes it too. Where either file gives such a value, a member that is not checked out is not refused `member-uninitialized`: git checked none out, and the finding fails the run, exit 1, in all three modes. The remedy prints one command to a line, as `gitmodules-path-unnamed` does, says which values git takes, and ends with the commit: ``` FINDING gitmodules-value-refused: .gitmodules gives `submodule.members/Nova.update` the value `None` here and in HEAD, which a clone reads, and git dies reading a file that does, before `git submodule update` checks out any member, so neither that nor make bootstrap checks one out. Git takes one of `none`, `checkout`, `rebase` or `merge` for that key. Take the key out by hand, in a POSIX shell, then stage and commit .gitmodules: git config -f .gitmodules --unset-all submodule.members/Nova.update ``` Decisions: - **A member's `path` or `url` is written back.** `--unset-all` takes out every line of the key. For `update`, `ignore`, `shallow`, `fetchRecurseSubmodules` and `branch`, git reads a section without one, so that is the cure. In the issue's `url` state, Nova's section has its url and one more `url` line with no value, and `--unset-all` alone would leave no url (git: `No url found`). So for a member's own section the finding writes the key back on a second line: the value the section last gave that git takes, or, where it gave none, what `family.py add` writes (the section's name for `path`, `https://github.com/<repository>.git` for `url`, `<url>` where the row's `repository:` is not `<org>/<Name>`). - **Which file gives it.** The finding says "here and in HEAD", "in the work tree, not in HEAD" (nothing to commit), or "in HEAD, not in the work tree" (it says to stage and commit the work tree's file, and prints no command). Where the work tree has no `.gitmodules`, the first such finding prints `git checkout HEAD -- .gitmodules` first, and only the first: a second would put back the key the first took out. - **The other `.gitmodules` checks wait.** 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. Without that, a `path` with no value was read as the empty path, and `gitmodules-path-unnamed` printed `--unset-all` for every key of the member's own section (measured on `f74f640c`, probe c8). Those checks run again once the file is mended. - **A boolean is git's own.** `shallow` and `fetchRecurseSubmodules` are read by `git -c probe.value=<value> config --type=bool --get probe.value`, which uses the same `git_parse_maybe_bool` the submodule parser does. That covers `0x10`, `010`, `1k` and the int range (`2097152k` and `-2097152k` die, `2097151k` does not) with no copy of git's number parsing. ### Line 3: what #682 left approximate - **Where git reads the file from.** Where the work tree has no `.gitmodules`, `gitmodules_sections` reads the index's stage-0 blob, then `HEAD:.gitmodules`, with `git config --blob`, as `config_from_gitmodules` does. The unmerged check now uses `git ls-files -s`, which gives both the conflict stages and the stage-0 blob. One difference from a file is measured and kept: in a blob, a key with no value is an error, not a die. Git stops reading at that line and goes on, so `gitmodules_sections` stops there too (probe b2: the last line, git exits 0 and checks both out; b2b: the first section, git dies `No url found` at the second). - **`submodule.<name>.active`.** `submodule_config` now returns each key's values. Where `submodule.active` is set, `init_stop` asks the section's own `active` first, its last value read as a boolean, as `is_tree_submodule_active` does. - **`submodule.active`'s patterns.** A plain path pattern, as `git clone --recurse-submodules=<path>` writes one, names that path and what is under it, and `.` names every path. A leading `./` and a trailing `/` are read past. A pattern with pathspec magic (a leading `:`), a glob character or a backslash, or an empty, `.` or `..` component, keeps the old reading: it names every path. Measured with git 2.43.0 in plain clones of a holder built by this branch's `scaffold-project.py` and `family.py` (Atlas and Nova), each line added to Nova's section and committed (`p1.sh`; 32 states). On `f74f640c` every state git's update dies on ended `family bootstrap ok` with `--no-members` saying `family ok`; on this head, bootstrap fails and the gate finds `gitmodules-value-refused` in all three modes on exactly the states where git's update exits non-zero, and on none of the others: | added to Nova's section | git's update | `f74f640c` bootstrap / `--no-members` | this head | |---|---|---|---| | `update = None`, `update = !true`, `update = Merge` | 128, `invalid value` | ok / `family ok` | exit 1 / finding | | `url`, `update`, `ignore` (no value) | 128, `bad config line 7` | ok / `family ok` | exit 1 / finding | | `path` (no value) | 128, `bad config line 7` | exit 1, wrong reasons / `gitmodules-path-unnamed` for the path "" | exit 1 / finding | | `branch` (no value) | 139, `Segmentation fault` | ok / `family ok` | exit 1 / finding | | `shallow = maybe`, `08`, `3000000000`, `2097152k`, `-2097152k`, `-2097153k` | 128, `bad boolean config value` | ok / `family ok` | exit 1 / finding | | `fetchRecurseSubmodules = bogus`, `ON-DEMAND` | 128, `bad ... argument` | ok / `family ok` | exit 1 / finding | | `update = none` | 0, Nova skipped | exit-0 skip | the same exit-0 skip, no finding | | `update = checkout`, `ignore = bogus`, `shallow` (no value), `shallow = true`, `""`, `" 1"`, `TRUE`, `1 `, `0x10`, `010`, `1k`, `2097151k`, `fetchRecurseSubmodules` (no value), `= on-demand`, `= 0` | 0 | ok / `family ok` | ok / `family ok` | `p2.sh` (index and HEAD fallback, `active`), on `f74f640c` then this head: | state | git | `f74f640c` | this head | |---|---|---|---| | work tree's file deleted, not staged, no credential | reads the index's; fails on the fetch | exit 1, "no section of .gitmodules gives that path" | the credential's skip, ok | | the index's taken out too | reads HEAD's; fails on the fetch | exit 1, the same | the credential's skip, ok | | `update = None` committed, work tree's file deleted | 128, dies on the index's blob | exit 1, "no section" | exit 1, "git cannot read .gitmodules" | | Nova's url taken out, subset `members/Atlas`, no credential | inits Atlas alone; fails on its fetch | Atlas "git stopped at members/Nova" | Atlas the credential's; Nova "git has no url" | | the same with `submodule.active=.` and `submodule.members/Nova.active=false` | inits Atlas alone; fails on its fetch | Atlas "git stopped at members/Nova" | Atlas the credential's; Nova "git has no url" | A section under a name git ignores whole (`../x`) with `update = None` is ignored by git (exit 0, a warning) and by both tools (`p3.sh`). `tests/test_family_gitmodules_values.py`, new, beside #682's `tests/test_family_bootstrap_gate_order.py`, whose helpers it imports (28 tests): - `test_a_value_git_dies_on_fails_bootstrap_and_every_mode_of_the_gate`, one case for each row of the issue's table, `update = None` first, and `branch` with no value: git's update exits 128 (139 for `branch`) and checks nothing out; bootstrap exits 1 with its last two stderr lines exact; the gate finds the whole finding in all three modes, exit 1; its commands are typed as printed and committed; HEAD's `.gitmodules` is then byte for byte the one `family.py add` wrote; the same bootstrap is a good holder's, and the gate passes in every mode. - No misfire: `update = checkout`, `shallow = true`, `fetchRecurseSubmodules = on-demand`, `ignore = bogus` (a good holder's run and gate); `update = none` (the exit-0 skip as before, no finding); a section git ignores whole. - Every member checked out: the holder itself fails on `.gitmodules`, and mended as the gate prints it, passes. - The index's file and HEAD's (no credential: the skip, ok); a key with no value at the end of the index's file (read past, the skip); `update = None` in the index's file (named, and the gate's `git checkout HEAD -- .gitmodules` first); two keys there (one checkout line). - `submodule.members/<P>.active=false` with `submodule.active=.`; the subset's pattern as `members/<P>`, `./members/<P>/` and `members`. - Which file gives it (HEAD's alone; the work tree's alone); a `url` or `path` left with no value, written back as `family.py add` writes it. **Fail on base.** The file run against `f74f640c`'s two scripts: 21 of 28 fail, and the three changed cases of `tests/test_validate_family_without_git.py` fail too: 24 failed, 18 passed (`base1.log`). The six that pass there are the no-misfire cases, which must pass on both. **Runs on this head** (Linux, git 2.43.0; through `run9.sh`, the real `gh` unreachable): - Every test file that loads or runs the family root's `bootstrap.py` or `validate-family.py`, found with `git grep` (49 files, the issue's Verify set, `test_family.py` whole, `tests/test_repo_hygiene.py`, `tests/test_agents_md_codes.py`, `tests/test_cli_reference.py` and `tests/test_pinned_copy_text.py` among them), in two halves on `fa69ead9` with a clean tree: `1309 passed, 22 skipped in 1431.96s` (`fa.log`) and `1644 passed, 1 skipped in 1258.67s` (`fb.log`). - `render-cli-reference.py --check`: `[ok] docs/cli.md is what the generator produces`. **One existing test changed.** `tests/test_validate_family_without_git.py::test_with_no_gitmodules_the_gitlink_listing_is_named` pins which check meets a missing git first in a holder whose work tree lost `.gitmodules`. That is now the reading of HEAD's `.gitmodules` for a value git dies on, so the `git-not-runnable` refusal says "HEAD's .gitmodules could not be read as git reads it" where it named the gitlink listing. Exit 2, the code and the remedy are as they were. No open branch edits that file. **A dirty-tree artifact, not a failure.** Run on the uncommitted tree first, `tests/test_update_shape.py`, `tests/test_update_shape_header_comments.py` and `tests/test_update_shape_pr_base.py` failed (`update-conflict`: the copy is not the template at the pinned commit), because their fixtures pin HEAD while the copies came from the edited templates. On a committed tree they pass: `171 passed` (`c1.log`), and in the halves above. **Mutants.** One for each hunk that changes behaviour, each run against the new file's tests that name it: 30 mutants, all killed (`mutants.log`, `mutants-2.log`). Three survived the first round; each showed a test that did not reach the hunk, and the test was made to reach it: | file | hunk | mutant | killed by | |---|---|---|---| | bootstrap.py | `NO_VALUE_REFUSED` | `branch` dropped | `[branch-no-value]` | | bootstrap.py | `fetch_members` | the every-member-checked-out branch never taken | `test_a_holder_with_every_member_checked_out_fails_on_it_too` | | bootstrap.py | `reason` | "has no value" reworded | `[url-no-value]` | | bootstrap.py | `cures` | the refused sentence dropped | `[update-None]` | | bootstrap.py | `gitmodules_sections`, source | the work tree's file always read | the index's and HEAD's tests | | bootstrap.py | `gitmodules_sections`, blob stop | no `break` | `test_a_key_with_no_value_ends_git_s_reading_of_the_index_s_file` | | bootstrap.py | `gitmodules_sections`, refused | never recorded | `[update-None]` | | bootstrap.py | `gitmodules_sections`, ignored sections | asked for refused values | `test_a_value_in_a_section_git_ignores_whole_is_no_finding` (survived until it ran bootstrap with no credential first) | | bootstrap.py | `refused_value`, `update` | every value taken | `[update-None]` | | bootstrap.py | `refused_value`, `on-demand` | not exempt | `[fetch-on-demand]` (survived until it ran with no credential first) | | bootstrap.py | `git_bool` | every value a boolean | `[shallow-maybe]` | | bootstrap.py | `submodule_config` | values dropped | the `active` and pattern tests | | bootstrap.py | `init_stop` | the old condition | the `active` and pattern tests | | bootstrap.py | `active` | own key not read | `test_a_member_whose_own_active_is_false_is_not_where_git_stops` | | bootstrap.py | `pattern_names` | every pattern names every path | `[a-path]` | | bootstrap.py | `unfetchable`, read with no row left | not read | `test_a_holder_with_every_member_checked_out_fails_on_it_too` | | bootstrap.py | `unfetchable`, the reason | not given | `[update-None]` | | validate-family.py | `_gitmodules_listed` | returns nothing | `[update-None]` | | validate-family.py | `_head_listed` | HEAD's listing empty | `[head-only]` (the first, badly written mutant still read it) | | validate-family.py | `GIT_TAKES` | `update`'s values reworded | `[update-None]` | | validate-family.py | HEAD-only branch | never taken | `[head-only]` | | validate-family.py | `git checkout HEAD -- .gitmodules` | never printed | `test_a_value_git_dies_on_in_the_index_s_file_is_named_and_mended` | | validate-family.py | the checkout printed once | printed for each key | `test_two_keys_with_no_gitmodules_in_the_work_tree_put_head_s_back_once` | | validate-family.py | the commit clause | always said | `[work-tree-only]` | | validate-family.py | `_submodule_entries` | ignored sections kept | `test_a_value_in_a_section_git_ignores_whole_is_no_finding` | | validate-family.py | `_values_refused` | `branch` dropped | `[branch-no-value]` | | validate-family.py | `_git_takes_bool` | every value a boolean | `[shallow-maybe]` | | validate-family.py | `_written_back` | never writes back | `[url-no-value]` | | validate-family.py | `_check_members`, blanking | sections read all the same | `[path]` of the written-back test (`gitmodules-path-unnamed` for the path "") | | validate-family.py | `_check_members`, skipped checkouts | `member-uninitialized` again | `[update-None]` (members mode exits 2) | `git merge-tree --write-tree` of `fa69ead9`: | against | result | |---|---| | `origin/main` `dc7d3fbb` | conflicts in both scripts, because #682 is squashed into main; with `--merge-base=f74f640c`, clean | | #682's last head `5afb24b9` (`fix/648-bootstrap-and-gate-order`, deleted on merge) | clean | | #747 `fix/681-validate-family-remedies` `58d6c50f` | clean | | #745 `fix/704-bootstrap-leg-moves` `f4935e11` | clean (a first draft conflicted twice with it, in `fetch_members`' docstring and where it adds `git_not_started`; both are moved out of its way) | | w764 `fix/764-bootstrap-notice-exit-texts` `271710ba` | clean | | w761 `bbb961f8`, w757 `6b5e9074`, w758 `b6e15591`, w759 `052410b6`, w760 `7569ffa8`, w777 `854f6ed9` | clean | | w762 `fix/762-validate-family-remedies-texts` `1038d7fa` (local) | **conflicts, three regions in `validate-family.py`**: the EXIT CODES sentence (each adds its exit-1 findings), the end of `_check_members`' docstring (each adds a paragraph), and the `_check_member_checkout` call (this adds `set(gitlinks) if refused else unregistered`, w762 adds `stop`). Each resolves by keeping both: `_check_member_checkout(root, asked, report, set(gitlinks) if refused else unregistered, stop)` | The other `fix/*` branches that touch these files and conflict (`fix/539-...`, `fix/607-...`, `fix/610-...`, `fix/bootstrap-print-name-escaped`, `fix/family-add-ghost-section`, `fix/family-bump-remove-rollback`, `fix/pin-files-not-a-list`, `fix/validate-family-member-duplicate`, `fix/validate-family-unnamed-mounts`) are old branches whose PRs landed long ago; `fix/611-...` is clean. No `l3-*` branch touches them. - Values in `siblings.py`'s `submodule_urls` (lane 4's `fix/690-siblings-inputs`), which the issue leaves out. - A `submodule.<name>.active` that is no boolean makes git die (`bad boolean config value`); `init_stop` reads past it to the patterns. - A blob `.gitmodules` with a config syntax error: git reads the entries before the bad line; `gitmodules_sections` reads none, as before. 1. **The credential advice prints where the line says no credential would help.** Whenever git's update exits non-zero, `fetch_members` prints "If the members are PRIVATE, set SHAPE_LEGS_TOKEN ... then re-run", and then, for a reason found in the holder's records (this PR's `.gitmodules` value, and #648's unmerged file, missing gitlink, missing section, missing url and stopped init), the line "NOT CHECKED OUT, and not for want of a credential". The first sends a person to a token that cannot help. Measured in every refused state above. 2. **`_sections_listed` reads a key with no value as the empty string.** In HEAD's file, Nova's `path` with no value reads as the path "", so `member-gitmodules-absent` also fires for Nova beside `gitmodules-value-refused`. Its line writes the same path the new finding writes back, so typing both is harmless, but it is a second finding for one fault (the `[path]` case of the written-back test pins it as it is). In the work tree's file the new blanking keeps it out. Review: lane 2's independent review (issuecomment-6236559204) and its re-verification of e1379a9, READY (issuecomment-6237963983). `233a334e` is its merge round after #820 (`e32ed55a`, both sides kept, `stop` only where `not refused`, `as_read` for `_check_removals_staged`) with a regression test; lane 2's re-check found it READY (#819 issuecomment-6244287619). CI (Brett Heap's ruling 27): H0 is the head 233a334 itself, which passed tests, tests-git-bash and SonarCloud; main has touched none of its files since its base b9b7069; and the local gate passed on its predicted squash tree caf6706 over main fe99b8b: the adopt_execute_refusals, adopt_filter_repo_failure, adopt_github_exits, adopt_late_failure, adopt_leg_keys, adopt_pins, adopt_split_branch, adopt_temp_work_roots, family_gitmodules_values, gh_guard, pin_annotated_tag, pin_source_top_level, template_values_single_pass, validate_family_without_git, repo_hygiene, agents_md_codes, cli_reference sets, 1025 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 #768 Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ip reads .shape-upstream-tip as utf-8-sig, after #855 The one conflict was in shape_upstream_notice of templates/assembly-root/scripts/bootstrap.py: main's inline read of the cache, now `utf-8-sig` (#855), against this branch's `_cached_tip(root)`. The call stays, and `_cached_tip` reads `utf-8-sig`, with #855's comment and a docstring that says the mark is taken off. Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
Merge round of #815 (#764) with main: The merge commit is The conflict and how it was resolved
The call stays, so the three reads of the answer keep one home. The hunk as merged: tip = _clone_tip(os.environ.get(SHAPE_UPSTREAM_ENV))
if tip is None:
tip = _cached_tip(root)
if not tip or not COMMIT_40.match(tip):
returnand the helper as merged: def _cached_tip(root: Path) -> str | None:
"""What `.shape-upstream-tip` holds, less a byte-order mark it starts
with (#855), or `None` where it cannot be read: not there, a file this
process may not read, or bytes that are not UTF-8, none of which is a
commit (#764). The last two ended `make bootstrap` in a `PermissionError`
or a `UnicodeDecodeError` traceback."""
cache = root / SHAPE_UPSTREAM_CACHE
try:
# `utf-8-sig` takes off the byte-order mark a Windows editor may
# start the file with (#855). `strip()` keeps U+FEFF, so a marked
# tip read as no commit and the notice said nothing.
return (cache.read_text(encoding="utf-8-sig").strip()
if cache.is_file() else None)
except (OSError, UnicodeDecodeError):
return None
The rest of #855's squash, as checked
Every other read in the assembly root's
RunsAll through
The 3 skips on 2.43.0 are the reftable cases in One thing to know about the first full set: a stray The mutant
#815's own nine notice tests pass under the mutant: none of them holds a mark. #855's tests are the ones that pin the mark, and they reach
|
|
Re-check of #815 (#764)'s merge round at I found no MUST and no SHOULD. There are three NITs, all in the PR body. I ran as uid 1000, not root, so the mode-000 cases are real and none was skipped. I changed no branch, pushed nothing and posted nothing. FindingsNIT 1:
NIT 2:
NIT 3:
Checked and true1. The resolution against both sides.
2. Behaviour, measured through I ran a probe of 19 cache states against
3. Runs at
4. Main since the merge.
5. The PR body's updated sentences.
6. Sonar-style checks.
Could not verify
Scratch leftNothing of mine is running. All four worktrees are clean and detached, under
Basetemps are I created no refs. The only shared-repo write was |
|
|
Sorry @brettheap, you've used your own review budget of 250,000 diff characters for the last 7 days. You can request another review in 3 days and 8 hours by commenting |
…ith #815, #770, #869 and #802 (#883) Round 5 of #883, a refresh. Each line comes from its squash body and was checked against the code at main 0d5d2b1: - #815 (#764): a project's bootstrap ends a validator's exit 2 as `bootstrap REFUSED`, exit 2, takes an upstream path, cache or pin it cannot read as no answer, and leaves a reftable leg's cherry-pick or revert alone. A line in "What changed", one in "What 1.1.1 fixes", and a note under "What a script sees" on the direct run's exit code. Its body's leftovers, two tracebacks on Python 3.12 and 3.13 and two "failed" texts beneath a REFUSED line, are a new limit. - #770 (#755) and #869 (#853, #854): tests, in the Tests line. The test limit for #853 and #854, and the skip #845 left, which #869 removes, are retired. - #802 (#675): update-shape.py's `git show` lines are shell-quoted and its kept-copy readers read some Python as Python does. Its one-hop read and what it reads as running are a new limit. 67 pull requests; 69 of the milestone's 72 issues closed at 11:31Z, and the open list is the 3 that read leaves: #707, #879 and #884. The shape-copy lists stand: the assembly root's bootstrap.py, which #815 changes, is on them. No other file changes. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…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>
… the release commit, #778's leg-clone sentence and #802's readers sentence, each checked against runs at v1.1.0 and main (#883 follow-up) (#893) The v1.1.1 notes landed as the release commit `5017830` (#883), and RV883's round 5 held two SHOULDs and five NITs for after it (#883 issuecomment-6250558821). This applies them to `docs/release-notes/v1.1.1.md`, with two more sentences lane 1's review found: 1. **#778's leg-clone sentence** says what runs at `v1.1.0` and `5017830` show. A leg clone whose own `AGENTS.md` says so keeps its leg row and its Triad row over each fault, where v1.1.0 gave `FINDING the way in refused` over a tab line and a traceback, exit 1, over a pattern that does not compile or a policy that raised on its name. A leg known only by its name gets a repository's two `n/a` rows. 2. **"Is", not "stays", exit 2,** in the pattern case, which was exit 1 in v1.1.0. 3. **#802's clause is dropped:** v1.1.0's `_blocks_inside` already read a `try`'s handlers, `else` and `finally` outside its guard. 4. **"What 1.1.1 fixes"** names "or cannot be used", and `the other way in`. 5. **#815's leftover tracebacks** were measured on Python 3.12; 3.13 is read, not run. 6. **#871 lets `_` through for any owner,** an organisation's name included. 7. **#869 asks the disk** only about a name that is not UTF-8. 8. **#802's readers:** at v1.1.0 only the import reader skipped an `if TYPE_CHECKING:` body; the binding reader skips one now. 9. **A known limit names #940:** under the `C` or POSIX locale, the bash `openRepoShape` and `setup.sh`'s guards refuse a URL, path or ref with a letter past ASCII as "a control character", a regression from v1.1.0 in the launcher through #872 (#866); a UTF-8 locale takes such a URL or path. The draft-time lines in the sections marked "remove at tag time" say main `2d372bb`, where #778 landed file for file as its reviewed head `8259ea1`, and that #879 moved to v1.2.0. The release body never carries those sections. Review: lane 1's independent reviewer found `b795ef74` NOT READY, on the leg-clone sentence, and `ca49f125` READY, with each changed sentence checked against runs at `v1.1.0` and `5017830`. Lane 1's second reviewer found `6cdb5551`, which adds the #940 limit, READY, with each clause measured against the launcher at `64464d31` and at v1.1.0. It lands by fast refresh (Brett Heap, 2026-10-15T22:48:35Z). Its reviewed head `6cdb5551` is green on Linux and SonarCloud. The branch merged main once and was never rebased; the last merge is update-branch's, clean, giving f60654e, which is green on SonarCloud and Git Bash. Lane 1's gate passed on its tree: the 5 test files that read the release notes or the workflows (`test_repo_hygiene.py`, `test_release_workflow.py`, `test_workflow_concurrency.py`, `test_agents_md_codes.py` and `test_cli_reference.py`), 221 passed and 0 skipped. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>



Lane: openreposhape-5 (openRepoShape-5)
Claim: #764 (comment)
Against main. #745 landed as
24a977c, and this branch is restacked onto main asf6f318d1: it merges #745's final head1945154c, then main, taking this branch's side inbootstrap.py, wheregit diff 1945154c 24a977c0over #745's files is empty. The restacked tree is7884460e, the tree the merge checks below predicted.Fix round: lane 3's RV815 NITs 2 to 7 (
05a1ebb3)Lane 3's RV815 reviewed
f6f318d1: READY, no MUST. One new commit,05a1ebb3, onf6f318d1, pushed tofix/764-bootstrap-notice-exit-textsas a fast-forward. Nothing was amended and main is not merged in that round. The merge round61aed8d2merged it later (#815 issuecomment-6247909102). It changestemplates/assembly-root/scripts/bootstrap.py(+13/-8),tests/test_bootstrap_refusal_exit.py(+62) andtests/test_bootstrap_upstream_notice.py(+30/-2), and nothing else. README, the handbook,docs/cli.mdand every other pinned file are untouched.The pinned digest moves again. After the merge round with main (
61aed8d2, #815 issuecomment-6247909102),templates/assembly-root/scripts/bootstrap.pyis sha25689ff92e5e469003fb5a1f13a5adbe5be2f9c2913ad152eb3f49e8545f0a8c3cc. Main's is6a7847f3d78ab6c3a350914bf7f9c81e11e235a6d9f8ad2b5782418e8b794e57, since #855. At the NIT round it was09238ebd4202193a..., where the body says0cb334c260493313.... The "Pinned files changed" paragraph below gives the current digests; the rest of that paragraph stands (the file is alreadyupstream-changedfor a v1.1.0 project once, and #824 changes it too). In "What changed", the count becomes: three commits afterf4935e11(271710ba,c2977425,05a1ebb3), the restack's two merges (3aa9a0fe,f6f318d1), and the merge round with main (61aed8d2).What each NIT came to
SHAPE_UPSTREAM_PATH(a name of 300 characters, and a path of over 5000) now has a test:bootstrap ok, exit 0, no notice, no tracebacktest_an_over_long_upstream_path_is_an_absent_answer(2 cases)sys.stdout.flush()at the top of_end_of_run, as thegit-not-runnablepath doestest_the_ending_follows_step_c_in_a_log_of_both_streams(2 cases: REFUSED and FAILED)(exit <code>); both lines are said, the finding first, where the run reaches its end; step (a) or the notice refusinggit-not-runnablestops the run with neither line!= 0== 2test_a_shape_pin_that_is_not_a_mapping_ends_as_the_validators_end_ittest_a_plain_clone_with_no_git_ends_refused_with_exit_2:bootstrap REFUSED: pins (exit 2), exit 2, no FAILED line, step (c) still reached. A second test holds the sentence step (d) now says: no git, a manifest finding and a directory named bySHAPE_UPSTREAM_PATHrefusegit-not-runnablefirst, with neither linetest_a_plain_clone_with_no_git_ends_refused_with_exit_2,test_no_git_and_a_clone_named_refuses_before_either_linegit -C <upstream>from the root61aed8d2, with #855's readNIT 3, as measured. With
> log 2>&1, stdout is block-buffered and stderr is not. Without the flush,bootstrap REFUSED: pins (exit 2)(andbootstrap FAILED:for a finding, as on main) printed before step (c)'s heading and its lines, and the log did not end on it. The test merges the two streams throughrun_script(merge_stderr=True)and blanksPYTHONUNBUFFERED, so a runner that exports it cannot hide the fault.NIT 4, as measured. With no git on
PATH,SHAPE_UPSTREAM_PATHnaming a directory and a manifest finding, the run endsREFUSED git-not-runnable: the clone SHAPE_UPSTREAM_PATH names ..., exit 2, with neitherbootstrap FAILEDnorbootstrap REFUSED. That predates this PR (#694), so the docstring said too much, not the code. A validator killed by a signal endsbootstrap FAILED: <name> (exit -9), exit 1 (_end_of_run([("pins", -9)])returns 1 and says exactly that), which the(exit <code>)wording now covers.NIT 1 is not taken, because it changes an exit path on Linux. The change is
_try_git(["-C", str(Path(upstream).absolute()), "rev-parse", "HEAD"], root). Its point (no process starts inside the upstream directory, so a WindowscwdthatCreateProcessrefuses cannot be read as git's ownPermissionError) is reasoned, and cannot be measured on Linux. I measured the Linux side: the old_clone_tipand the new one run throughshape_upstream_noticeover 22 values ofSHAPE_UPSTREAM_PATHand twoPATHs (git, and an empty directory), 44 rows:link/.., a subdirectory of a clone, a directory named-x, a bare repository, an unbornHEAD, a plain directory, a file, a missing path, a name with spaces and a newline, a name with a non-UTF-8 byte, an over-long name and path, the empty string, a mode-000 directory, one under it, and a clone under a directory with no search bit;PATHand an upstream directory nobody may enter. The old code answers "no clone" (itsPermissionErrornames the directory, which_git_runspasses on), so the cache speaks; the new code starts no process there, finds git missing, and refusesgit-not-runnable, exit 2.The brief for this round asked for NIT 1 only if every exit path stayed byte-identical, so it is left. It is two lines (and
_clone_tiptakes the root) if lane 3 or Brett Heap wants that row to refuse; the new row's answer is defensible, since the clone cannot be read either way.NIT 7: the C3 account, one more paragraph. Two more sentences say "AT its pinned commit" with no condition, and both are true where they stand, so neither is changed:
scaffold-project.py:1957, the scaffold's NEXT STEPS: "make bootstrapputs each leg on<branch>AT its pinned commit". It is printed once, for a person who is about to take a fresh recursive clone and runmake bootstrapfor the first time. In that clone every leg is detached AT its pin, with no operation in progress and no commit of its own, so nothing can be lost and the placement happens as said.tests/test_push_branch_refspec.pyasserts the sentence for a+xbranch and then runs that first bootstrap (the legs end on it at their pins), andtests/test_scaffold_e2e.pyruns it on the default branch and checks the pin did not move.templates/assembly-root/project.yaml:64, the comment ontracking_branch: "The branchscripts/bootstrap.pyplaces each leg on, AT its pinned commit". It names which commit the branch is placed at and says the pin stays exact (.gitmodulescarries nobranch=); where bootstrap leaves a leg alone isAGENTS-shape.md's sentence, which R2 narrowed and which is pinned beside it.Tests (this round)
All runs used
run9.sh(gh stub first onPATH,GH_TOKEN=bogus-l5, emptyGH_CONFIG_DIR), on Python 3.12.3, at05a1ebb3with a clean tree.test_bootstrap_refusal_exit,test_bootstrap_upstream_noticetest_bootstrap_ref_store,test_bootstrap_placement_texts,test_bootstrap_leg_moves,test_bootstrap_without_git,test_bootstrap_after_bump,test_bootstrap_spelling,test_bootstrap_branch_bytes, and the trio (test_repo_hygiene,test_agents_md_codes,test_cli_reference),test_readme_sections,test_pinned_copy_texttest_family, 16test_adopt_*files,test_family_makefile_siblings,test_siblings_family_nametest_setup_project_py,test_setup_sh,test_shape_doctor, 8 moretest_shape_doctor.py, not this PR's)bootstrap.py(a path, a docstring, a pinned-file list)The lists are the ones
c2977425's runs used;git grepovertests/forbootstrap.pyfinds no file outside them and the nine named-only files.Fails on the head this round started from (
f6f318d1, the two touched files with the new tests copied in): 2 failed, 24 passed. The two are the NIT 3 cases. NIT 2, 5 and 6 hold behaviourf6f318d1already has, so they cannot fail there; they fail on the earlier bases:24a977c0), both over-long cases fail withOSError: [Errno 36] File name too longout ofPath.is_dir;271710ba, the plain-clone-with-no-git case fails:bootstrap FAILED: pins (exit 2), exit 1;make bootstrapwith no git on PATH ends in aFileNotFoundErrortraceback, exit 1, where the validators refusegit-not-runnable, exit 2 #694's behaviour, which step (d) now describes.Mutants (this round)
One per new hunk that has behaviour, applied to
05a1ebb3'sbootstrap.pyin a scratch clone and run against the two touched files (26 cases). Each is caught._end_of_runremoved_clone_tipcatchesPermissionErroronlyPath.is_diranswers False, it would survive!= 0version of that test passes under N4, the new== 2does not_clone_tipalso catchesRefusalThe docstring hunk has no behaviour. The three things it says about the ending are held as above: REFUSED alone (the plain clone with no git), both lines with the finding first (
test_a_finding_and_a_refusal_say_both_and_exit_2), and neither line (the no-git-and-a-clone-named case).Merge checks (this round)
git merge-tree --write-treefrom05a1ebb3on 2026-10-17 at about 17:50Z:42aed788: clean. It would be tree1ba653a4.fix/710-non-utf8-branch-names,0a5e82bf): clean. Both changebootstrap.py; the hunks do not overlap.fix/*,l3-*anddocs/*branch on origin besides this one (55, bootstrap.py: step (a) decodes git's output as the system decodes a name it is given, so a branch whose upstream is not UTF-8 is moved back, and a good branch this system's encoding cannot write gets a line of its own, not a traceback (#710) #824 among them): 51 clean, 4 conflict. The four,docs/release-notes-v1.0.0,fix/758-git-command-scope,l3-677-pins-hand-step-wipandl3-678-naming-checkout-wip, each conflict with main itself, in files none of which this commit changes.Checklist candidates (not filed)
_cached_tipreads a cache that starts with a byte-order mark as no answer. Measured: a.shape-upstream-tipofEF BB BFthen a 40-hex commit reads back as'\ufeff0123...', which is not a commit, so the notice stays silent over a cache a Windows editor wrote.encoding="utf-8-sig"in_cached_tipwould accept it. Outside bootstrap.py (the assembly root's): aSHAPE_UPSTREAM_PATHdirectory nobody may enter ends in a traceback, a validator's refusal is reported asFAILED, exit 1, a reftable leg's cherry-pick goes unseen, anddocs/cli.mdsays bootstrap places each leg AT the pin with no condition #764; the same decode question as bootstrap.py and repo_shape.py: a leg on a branch whose name is not UTF-8 makes every latermake bootstrapend in a UnicodeDecodeError, and a character the filesystem encoding lacks still ends step (a) in a traceback #710's. Done since by the merge round with main (61aed8d2): bootstrap.py (the assembly root's) and repo_shape.py: a.shape-upstream-tipor a YAML file saved with a byte-order mark reads as no commit, or as a first key spelled with U+FEFF, somake bootstrap's upstream notice stays silent #855 (3059efc1) reads the cache asutf-8-sig, and the merge carries that into_cached_tip. bootstrap.py (the assembly root's) and repo_shape.py: a.shape-upstream-tipor a YAML file saved with a byte-order mark reads as no commit, or as a first key spelled with U+FEFF, somake bootstrap's upstream notice stays silent #855's tests intests/test_bom_reads.pypin it, and a mutant back toutf-8fails three of them.What changed and why
rv704's checklist candidates C1 to C4 on #745. Three commits after
f4935e11(271710ba,c2977425,05a1ebb3), the restack's two merges (3aa9a0fe,f6f318d1), and the merge round with main (61aed8d2).271710ba: C1, C3 and C4, with C2 measured and put to a ruling.c2977425: C2, option B, on Brett Heap's ruling (below).Pinned files changed:
templates/assembly-root/scripts/bootstrap.pyonly, and its digest changes: sha2566a7847f3d78ab6c3...on main,89ff92e5e469003f...here after the merge round61aed8d2(09238ebd4202193a...at the NIT round05a1ebb3,0cb334c260493313...before it). #764 is v1.1.1 and lands after the v1.1.0 tag, so a project taken to v1.1.0 seesscripts/bootstrap.pyasupstream-changedonupdate-shape.py check --root ., and re-pins it withupdate-shape.py apply. (#682 changed the family root'sbootstrap.py, not this one; an earlier draft of this paragraph said otherwise, and lane 3's RV815 corrected it.) #824 (#710) changes the same file, so the two should land in one release, for one row, as #764 asks. The Makefile, both roots'AGENTS-shape.mdand the holder's README are not changed (see C3).C1: the upstream notice and a directory nobody may enter
With
SHAPE_UPSTREAM_PATHset to a directory this process may not enter,make bootstrapended in aPermissionErrortraceback, exit 1, after every validator had passed. There are two ways in:subprocessraises the error for thecwdofgit rev-parse HEAD, naming the directory._git_runspasses it on, since it is not git's (bootstrap.py (the family root's and the assembly root's):make bootstrapwith no git on PATH ends in aFileNotFoundErrortraceback, exit 1, where the validators refusegit-not-runnable, exit 2 #694).Path.is_dirraises it (3.13 too; 3.14 answers False).The new
_clone_tipcatchesOSErroraround both reads and returns no answer, as an absent answer does. The cache is then asked, as it is when no clone is named.A git that cannot be STARTED is still
git-not-runnable, exit 2._git_runsraises that as aRefusal, whichexcept OSErrordoes not catch, andtests/test_bootstrap_without_git.pystill passes. A mutant that also catchesRefusalfails it.The same function, under the issue's goal that
make bootstrapnever ends in a traceback over the notice. These go past C1's own line, and each is measured onf4935e11:.shape-upstream-tipnobody may readPermissionError, exit 1_cached_tip).shape-upstream-tipwhose bytes are not UTF-8UnicodeDecodeError, exit 1contracts/nobody may enterPermissionErrorat the notice'sis_file, after the validators' own answercontracts/shape-pin.yamlthat is a YAML listAttributeError, aftervalidate-pins.pyrefusedshape-pin-unreadableC4: a cherry-pick or a revert on a reftable leg
The probe's result is on #764 (issuecomment-6237519513), as its Verify asks, and on #684 for lane 4's R612 E4 (issuecomment-6237529261).
The probe. This host's git (2.43.0) has no reftable, so I built git 2.47.1 from its source tag on GitHub, in my scratch directory. In a repository made by
git init --ref-format=reftable:git revertleave NOCHERRY_PICK_HEADorREVERT_HEADfile in.git;git show-ref --existsfinds each in the ref store, andgit statussays the operation is in progress;MERGE_HEADa FILE, as on the files backend.So the ref store is where the two live, and the issue's second branch applies.
The defect, reproduced with that git on
f4935e11:GIT_DEFAULT_REF_FORMAT=reftable, so the root and both legs are reftable, and bootstrapped once.otherin the spec leg.on main at <pin>andthe leg was on other. The cherry-pick was ENDED, and its staged README was carried ontomain.A revert ended the same way, and its staged removal was gone. That is #704's line 2, which R1 refuses, met again on the other backend. A merge was refused, by its file.
The fix.
_in_progressnow asks the leg's ref store forCHERRY_PICK_HEADandREVERT_HEADtoo (IN_REF_STORE,_in_ref_store), in the same row order. It asks withgit show-ref --exists <name>(git 2.43 and later), which takes the exact name. The issue'sgit rev-parse -q --verify CHERRY_PICK_HEADalso resolves a branch or a tag of that name.rev-parse --symbolic-full-namewas measured too: it answers nothing, with exit 0, when a pseudoref and a branch of the same name both exist.A git before 2.43 refuses the option and is answered no. Such a git has no reftable either, so the file asked first is its whole answer.
The refusal now says where git holds the marker:
A marker found as a file still says
is in its git dir, byte for byte as before.family.py. The same probe answers #684's R612 E4 forfamily.py's_operation_in_progress, which is lane 4's: on reftable the two markers are refs, not files. This PR does not touch it.C3: the reference row
docs/cli.md:47said bootstrap "places each leg on its tracking branch AT the pinned commit", with no condition.scripts/render-cli-reference.py) now carries R2's condition, word for word as the pinnedAGENTS-shape.mdsays it: "only where that loses no commit, and otherwise leaves its checkout where it is, saying why".docs/cli.mdis regenerated, andrender-cli-reference.py --checkpasses.The other three are left, by choice. The Makefile's help (
:34-35), the holder'sAGENTS-shape.md(:63-65) and its README (:14-17) all say "or says why not". So doesAGENTS-shape.md's own table of the five commands (:91-92). Each is true as it stands, which is why R2 left them. Changing them would give every project (the Makefile) and every holder (the other two) anupstream-changedrow that fixes nothing.AGENTS.md's section 3 ("Say that bootstrap put each leg on its tracking branch AT the pinned commit") is true where it stands too: it is the fresh scaffold's first bootstrap.C2: option B, on Brett Heap's ruling
Brett Heap ruled on this line in lane 5's session at 2026-10-17T13:01:10Z: "take B for #764's C2". The first commit measured the line and left it for that ruling. The second commit,
c2977425, is option B.The defect, measured on
271710ba, in a plain clone (no--recurse-submodules).validate-pins.pyrefusespin-submodule-uninitialized, exit 2, and bootstrap saidbootstrap FAILED: pins (exit 2)and exited 1, which is a finding's code. Every other refusal in the file exits 2. Any person who forgot--recurse-submodulesmeets it, not only a machine with no git (git-not-runnable).What B does:
bootstrap REFUSED: <name> (exit 2), exit 2.bootstrap FAILED: <name> (exit <code>), exit 1. An exit 3 or 127 stays a finding._end_of_run;test_a_finding_and_a_refusal_say_both_and_exit_2and two of the ending cases hold the rule as it is (mutant N5 below)._run_neutral_validatorsnow returns each failed validator's label and exit code, and the new_end_of_runsays them and returns the exit.VALIDATOR_REFUSED = 2names the code. The step (d) paragraph of the module docstring says what the ending does. The only shipped file changed isbootstrap.py, the pinned file this PR already changes.271710ba)c2977425)scripts/bootstrap.py, plain clone: pins refusebootstrap FAILED: pins (exit 2), exit 1bootstrap REFUSED: pins (exit 2), exit 2bootstrap FAILED: manifest (exit 1), pins (exit 2), exit 1bootstrap FAILED: manifest (exit 1), thenbootstrap REFUSED: pins (exit 2), exit 2bootstrap FAILED: manifest (exit 1), exit 1make bootstrap, plain clonemake: *** [Makefile:45: bootstrap] Error 1... Error 2bootstrap.pydirectlyits own bootstrap exited 1exited 2make bootstrap(make bootstrap exited 2)The first four rows are measured, on
271710baand onc2977425(the new tests). The last three are read from the code:setup-project.pyhands bootstrap's code toshell_status, which keeps a non-negative code;siblings.pyprints the code; the family root runsmake, which exits 2 for any failing recipe.The issue says a script testing
make bootstrap's exit would see a change under B. It does not: GNU make exits 2 for any failing recipe, as the Makefile's own comment (:12-16) says, and onlyError 1becameError 2. What changes is the exit ofbootstrap.pyrun directly, by setup-project.py, by a person without make, or by a script.Docs.
git grepfound no sentence inAGENTS.md,README.md,docs/handbook.html,templates/assembly-root/AGENTS-shape.md, the Makefile ordocs/cli.mdthat says bootstrap exits 1 for a refusal or printsFAILEDfor one. None is changed, and neither README nor the handbook is touched.render-cli-reference.py --checkpasses on this head.Tests
All runs used
run9.sh(gh stub first onPATH,GH_TOKEN=bogus-l5, emptyGH_CONFIG_DIR), on Python 3.12.3.New:
tests/test_bootstrap_upstream_notice.py, 7 cases in 5 tests.SHAPE_UPSTREAM_PATHat a mode-000 directory and at a directory under one: exit 0, no traceback,bootstrap ok, no notice.shape-pin.yamlthat is a list: no traceback, step (c) reached.contracts/nobody may enter, asked of the notice in process, since the validators before it have their own answer to that folder.New:
tests/test_bootstrap_ref_store.py, 4 cases in 2 tests.--abortit prints is typed from the root, and the next run places the leg.CHERRY_PICK_HEADand a tag calledREVERT_HEAD, with no operation: the leg is placed. This one runs on every git.New:
tests/test_bootstrap_placement_texts.py, 2 tests. The reference row carries R2's condition, and the pinnedAGENTS-shape.mdstill words it so.New (C2):
tests/test_bootstrap_refusal_exit.py, 13 cases in 5 tests.validate-pins.pyrefuses, and the run endsbootstrap REFUSED: pins (exit 2), exit 2, with noFAILEDline. Step (c) is still reached.bootstrap FAILED: manifest (exit 1), exit 1, as before. This is the control.make bootstrapin a plain clone: exit 2,REFUSED, and make'sError 2, notError 1._end_of_runasked of nine mixes of codes: none, one finding, two findings, one refusal, two refusals, an exit 3, an exit 127, a refusal then a finding, and all three. Each says its lines and exits by the worst.Runs on head (
c2977425):test_bootstrap_leg_moves,test_bootstrap_without_git,test_bootstrap_after_bump,test_cli_reference,test_readme_sections,test_pinned_copy_text,test_repo_hygiene),test_bootstrap_spelling,test_bootstrap_branch_bytesandtest_agents_md_codestest_bootstrap_leg_moves,test_bootstrap_without_git,test_bootstrap_after_bumptest_scaffold_e2e,test_update_shape,test_update_shape_kept_helper,test_doctor_pin_restore_merges,test_doctor_legs_members_next,test_family_head_lost_gitmodules,test_family_bootstrap_gate_order,test_family_siblings_placement,test_windows_paths,test_stdout_encoding,test_template_text_placeholders,test_scaffold_ignored_shape_files,test_validate_pins_lockstep_lines,test_push_branch_refspec,test_leg_path_yaml_scalars,test_scaffold_manifest_values)test_family, 16test_adopt_*files that run bootstrap,test_family_makefile_siblings,test_siblings_family_nametest_setup_project_py,test_setup_sh,test_shape_doctor,test_doctor_next_is_a_command,test_bump_leg,test_bump_leg_rollback,test_update_shape_merged_away,test_update_shape_add,test_naming_policy,test_park_resume_targets,test_scaffold_mount_ignoredbootstrap.py:test_git_output_decoding,test_shape_advisory,test_template_quoted_values,test_validate_family_branch_stepThe
coreandwide 1files hold what the first commit's runs held;test_bootstrap_spellingandtest_bootstrap_branch_bytesmoved from wide 1 to core. The other test files that namebootstraponly name its path or its output, in comments, docstrings and expected text. The one helper that runs it, intests/conftest.py, ends inbootstrap ok, and the files that use it are in wide 3.Fails on base (
f4935e11, the new files copied in):test_bootstrap_upstream_notice.py: all 7 cases fail.test_bootstrap_placement_texts.py: the row test fails; the guard on R2's sentence passes, by design.test_bootstrap_ref_store.pyon git 2.47.1: the cherry-pick and revert cases fail. The merge case (a file on both backends) and the exact-name case pass on base, by design: they are controls.Fails on base for C2 (
271710ba,tests/test_bootstrap_refusal_exit.pycopied in): 3 failed, 1 passed, 9 errors of the 13 cases.bootstrap FAILED: pins (exit 2), exit 1.bootstrap FAILED: manifest (exit 1), pins (exit 2)and exited 1.make bootstrapfails: it saidError 1._end_of_runcases error, since the function is not there.render-cli-reference.py --check:[ok] docs/cli.md is what the generator produces.Mutants
For C1, C3 and C4 (
271710ba), each one breaks a single new hunk, in a scratch clone. Every one is caught:_in_progressrev-parse -q --verifygit dirwhateverheldis_clone_tipcatches nothingis_diroutside thetry_clone_tipalso catchesRefusaltest_the_offline_upstream_notice_with_no_git_refuses_too_cached_tipcatches onlyOSError/ onlyUnicodeDecodeErrorRefusalcontracts/testpin or {}again"", so the cache is skippeddocs/cli.mdboth revertedtest_cli_reference.pyalone stays green)For C2 (
c2977425), 13 more, one per new hunk or branch of_end_of_run, each caught (each is applied to the pushed file in a scratch clone and run againsttests/test_bootstrap_refusal_exit.py):VALIDATOR_REFUSED = 1failed.append((label, 1)))bootstrap oknot saidmaindiscards the ending's codeMerge checks
Run on 2026-10-17 at about 16:00Z with
git merge-tree --write-tree, from headc2977425, against origin/main40e678e6and everyfix/*,l3-*anddocs/*branch on origin (58 besides this one).origin/main: conflicts, in
templates/assembly-root/scripts/bootstrap.pyonly. That is the stack, not this change. #682 (dc7d3fbb) and #745 (24a977c0) landed as squashes, and this branch still holds their commits.bootstrap.pyon main is byte for byte whatf4935e11has:git diff f4935e11 origin/main -- templates/assembly-root/scripts/bootstrap.pyis empty.The restack, predicted: clean. The two commits after
f4935e11, applied onto origin/main withgit merge-tree --write-tree --merge-base=f4935e11 origin/main c2977425, give tree7884460e. Against main it changes seven files:bootstrap.py,docs/cli.md,scripts/render-cli-reference.pyand the four new test files.That predicted head (a scratch commit object, no ref) against the 58 branches:
docs/release-notes-v1.0.0,fix/695-family-docs,fix/758-git-command-scope,fix/759-quoting-launcher-guards,fix/760-bin-dir-control-chars,fix/762-validate-family-remedies-texts,l3-677-pins-hand-step-wip,l3-678-naming-checkout-wipandl3-733-ignored-sections.fix/710-non-utf8-branch-names(bootstrap.py),fix/759-quoting-launcher-guards(render-cli-reference.py),fix/796-adopt-source-spellingsandfix/799-adopt-plan-out-checked(docs/cli.md), andl3-563-docsandl3-rehearsal-v1.1.0(both).render-cli-reference.py --checkpasses on the predicted head and on its merge with each offix/710,fix/759,fix/796,fix/799andl3-563-docs.#710's branch now exists and edits the same
bootstrap.py, so whichever lands second re-gates.Left out
make bootstrapend in a UnicodeDecodeError, and a character the filesystem encoding lacks still ends step (a) in a traceback #710's decode).Checklist candidates (not filed)
checkout_tracking_branch's(sub / ".git").exists()for a leg directory of mode 000 raisesPermissionError: ... spec/.git._git_runspasses it on, since it names a path._register_paths'sis_file()under agovernance/of mode 000 raises one too, though step (c)'s docstring says the readout "never fails".Path.is_file()/is_dir()/exists()guard in the shipped tools answers False to any OSError on Python 3.14 (3.12 and 3.13 raise PermissionError), so a directory this user cannot search reads as ABSENT: validate-pins.py skips its workflow@<sha>check, update-shape.py sayscopy-missingandupdate-pin-missing("adopt the project") for files that are there, the doctor says DRIFTED (5 copy-missing), and bump-leg.py's remaining guards, and every other tool's, skip or misreport instead of refusing by name #430'sPath.is_dirsweep, which may already hold them.validate-pins.pywith acontracts/nobody may enter ends in aPermissionErrortraceback at_load_leg_pin'spin_path.is_file(), exit 1 (measured on this head). The validators' half of the same class.setup-project.pystep (7) endsbootstrap failed (see above). The clone is at <path>., andsiblings.pysays<project>: its own bootstrap exited <code>; the clone is there and its legs may not be placed. Both carry bootstrap's code (now 2 for a refusal), and the REFUSED line is printed above each. Neither is false; a person reading the last line only could take a refusal for a finding. Cosmetic, probably not worth an issue.Closes #764
🤖 Generated with Claude Code
Summary by Sourcery
Make bootstrap report validator refusals accurately, tolerate unreadable upstream state, and preserve in-progress reftable operations.
New Features:
Bug Fixes:
Enhancements:
Documentation:
Tests: