Repository navigation
siblings.py: the printed move runs as typed past a taken staging name and names a dangling link to move aside first, a NUL or lone surrogate in project: or url is a finding, .gitmodules is read as git reads it, the warning's prose is spelled, and a tag-only branch is named (#690) - #805
Conversation
…the printed move runs as typed past a taken staging name or dangling link, a NUL or lone surrogate in `project:` or url is a finding, `.gitmodules` is read as git reads it, the warning's prose is spelled, and a tag-only branch is named (#690) Six lines of `templates/family-root/scripts/siblings.py` (`make siblings`, a pinned copy in every family holder, which takes this as one `upstream-changed` row), each re-measured on main 4ce9045 with a holder from `family.py init --local-remote-dir`: - `parent_folder_warning` printed its staging name whatever held it. With `<t>/InkRouter.holder` a folder already, the three printed lines all exited 0 and left the holder at `<t>/InkRouter/InkRouter/InkRouter`, one level too deep; `<Family>.folder` alike on a disk that ignores case. `staging_path` now gives the first of `<Family>.holder`, `<Family>.holder.1`, `.holder.2` ... at which `os.path.lexists` finds nothing, a link to nothing included. - A link to nothing named `<Family>` passed `not wanted.exists()`, so the staged move was printed, its first `mv` ran, and `mkdir -p` failed `File exists` with the holder half moved. The warning now says, before the moves, `First move <path> aside: it is a link to nothing, named after the family, and in the way of the move. Then:`, and the moves under it are the ones that run once it is gone. - `place` handed a `project:` holding a lone surrogate or a NUL to `git clone` as the clone's path: `UnicodeEncodeError` or `ValueError: embedded null byte`, exit 1, and the members after it never placed. The url `clone_url` builds from a `repository:` holding a NUL ended the same way. `argument_fault`, #455's rule as #644 wrote it for `make bootstrap` (a NUL, or a surrogate `os.fsencode` cannot write as one byte), is now asked of both before anything: the member is `NOT AN ARGUMENT`, a finding (`<project>: its `project:` in family.yaml holds U+D83D, a lone surrogate, which no command can be given as an argument, so nothing was cloned or fetched for this member: git was never run with it.`), the dry run says the same, and the other members are placed. - The warning's prose printed the holder, the parent folder and the family raw on stderr (an ESC in a folder name reached the terminal). Each is now `spelled`; the first line keeps its `repr`, and the `mv` lines stay as typed (#486's question). - `submodule_urls` read `git config --list` as text, split by `str.splitlines` and at the first `=`: a url holding NEL, U+2028, U+2029, VT, FF or U+001C to U+001E was cut there, and `[submodule "members/a=b"]` gave no url, so its member was cloned from the `repository:` guess. It now reads `git config -z --list`, records cut at NUL and each key from its value at the first newline, a section's name being all of its key between `submodule.` and the last dot: the reading `validate-family.py` makes and lane 3's #668 gives the doctor (#146). - A `tracking_branch:` naming only a tag of the member's remote was cloned DETACHED by `git clone --branch`, under a line saying "on <name>, its tracking branch", exit 0. A line after the clone now says `<name> is not a branch of <url> but a tag, which git clone checks out DETACHED: this clone is on no branch`. The exit is unchanged, as the issue asks. `tests/test_siblings_inputs.py` covers each line with the issue's inputs, runs the printed moves as typed in a POSIX shell, and holds the parity of the doctor's and `make siblings`' readings over one corpus (#668's names and breaks, and its Latin-1 name), skipped by name until #668's `gitmodules_listing` is in the doctor; it passes against PR #700's head. No doc sentence is made false, and no `--help` changes. The holder's header spelling U664 pins is untouched. Closes #690 Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… a section name cuts no key, the two cases lane 3 asked #690's rows for (#690) Lane 3's two comments on #690 (issuecomment-6232404184 and -6232689453) name two cases the `git config -z --list` read of `submodule_urls` already fixes on main's reading, and ask for a row each: - `[submodule "a=b"]`, the name as lane 3 wrote it. Main's read split each `--list` line at the first `=`, so the key read as `submodule.a`, failed the three-part test and was dropped; git, the doctor and CI mount it. `a=b` joins NAMES (the url read, and the doctor's parity over the same corpus) and the dry-run row that already held `members/a=b`. - A CR inside a section name, `[submodule "q\rx"]`, with `update = bogus`: `str.splitlines` cut that key in two, so the section was read as no section. `q\rx` joins NAMES, and a row reads the file with the CR section and with the same section named `qx`, each body holding `update = bogus`, and asserts git has each key whole by its exact name and that the two files read alike. What a reading does with a value git refuses (`git status` dies `invalid value for 'submodule.qx.update'`, with or without the CR) is lane 3's #733, so the row compares the two readings and pins no url. Run against the old reading (M1), the new rows fail: `equals-bare`, `cr`, the CR row, the `a=b` dry run, and the doctor parity for both names. Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Reviewer's GuideUpdates the family-root sibling bootstrap script to safely stage misplaced holders, validate impossible clone arguments, preserve Git-compatible Sequence diagram for validated sibling cloningsequenceDiagram
participant S as siblings.py
participant V as argument_fault
participant G as git
participant C as Clone
S->>S: derive project and url
S->>V: argument_fault(project)
S->>V: argument_fault(url)
alt invalid argument
V-->>S: fault
S->>S: mark NOT AN ARGUMENT
S-->>C: skip clone or fetch
else valid arguments
V-->>S: None
S->>G: git clone --branch
G->>C: clone member
alt tracking_branch is a tag
G-->>S: detached checkout
S->>S: print detached tag finding
end
end
Sequence diagram for Git-compatible submodule URL parsingsequenceDiagram
participant S as siblings.py
participant G as git config
participant P as submodule_urls
S->>P: submodule_urls(root)
P->>G: git config -z -f .gitmodules --list
G-->>P: NUL-separated records
P->>P: decode UTF-8 with replacement
P->>P: split record at first newline
P->>P: derive section from last key dot
P->>P: retain path and url, later value wins
P-->>S: member-to-url mapping
Flow diagram for safe holder stagingflowchart TD
A[Misplaced holder detected] --> B{Staging name exists?}
B -- Yes --> C[staging_path checks next .holder.n or .folder.n]
B -- No --> D[Use base staging name]
C --> E[Print mv commands with selected staging path]
D --> E
E --> F{Dangling family link exists?}
F -- Yes --> G[Print move-aside warning]
F -- No --> H[Print standard warning]
G --> I[Run moves as typed]
H --> I
File-Level Changes
Assessment against linked issues
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
|
(lane openRepoShape-1, 2026-10-17T11:11:32Z) Lane 1's independent review of #805 at 25c7b14: CHANGES NEEDED All six of #690's lines are fixed, and the Setup. The head holds main The issue's lines
Ordinary inputs, byte for byte. I compared main and head on the same holder, with the base path normalised. All four cases match: dry run (rc 0), real run (rc 0), wrong parent folder (rc 1) and its dry run (rc 1). The comparison covered stdout and stderr. Findings, ranked1. Should fix (regression against main): a The To reproduce, set the IRRS url in git itself registers and uses this url: A fix I ran: write the url git quotes as the spelled form the lines above already print. It is a helper, so def said_of_url(text: str, url: str, src: str) -> str:
"""git's own text, with the url it quotes raw written as spelled."""
return text.replace(url, src) if src != url else text
...
f"{who}: git clone exited {proc.returncode}:\n"
+ said_of_url(scrub((proc.stderr + proc.stdout).strip(),
token), url, src))With the fix, stderr reads The row below passes on main, fails on head, and passes with the fix (measured: def test_a_url_holding_a_line_feed_forges_no_finding_line(family, tmp_path):
"""git's clone error quotes the url raw, line feed and all: read whole
since #690, a `.gitmodules` url `"/r/nowhere\\nFINDING IRSS: ..."` put
that line at column 0 of IRRS's finding."""
holder = holder_in(family, tmp_path / NAME)
config = holder / ".gitmodules"
rewritten, count = re.subn(
rb"(?m)^(\turl = ).*/IRRS\.git$",
lambda found: found.group(1) + b'"/r/nowhere\\nFINDING IRSS: forged"',
config.read_bytes())
assert count == 1
config.write_bytes(rewritten)
result = siblings(holder)
assert result.returncode == 1, result.stdout + result.stderr
findings = [line for line in result.stderr.split("\n")
if line.startswith("FINDING ")]
assert findings == [f"FINDING {BENT}: git clone exited 128:"], result.stderr2. Must fix (Sonar): S9073 at SonarCloud's analysis of PR 805 at assert os.path.islink(link), "nothing moved"
assert holder.is_dir(), "nothing moved"3. Texts.
4. Optional. The
|
…ritten spelled in the clone and fetch failures that relay git's text, so it forges no FINDING line (#690) Lane 1's review of #805 at 25c7b14 found a regression the whole-url `-z` read made reachable. git's `die` and `error` quote a url RAW: a line feed stays and every other control becomes a `?`. A `.gitmodules` url holding a line feed was cut at it by main's reading, so the clone of `/r/nowhere` failed in one line; read whole, the clone of `/r/nowhere<LF>FINDING IRSS: forged` fails in two, and the second stood at column 0 of IRRS's CLONE FAILED finding as a FINDING line of its own. `said_of_url` writes the url git quotes as `spelled` writes it, the form every other line of `place` already prints. It looks for the url as given and as git quotes it (controls as `?`), because a url holding an ESC beside the line feed is quoted in the second form and the reviewer's raw `replace` alone leaves that forged line standing (measured: `line-feed-and-esc` fails with the raw form only). It is a helper, so `place` gains no branch. The other relay of git's text in `place` is the failed fetch's, which quotes the clone's `origin`: a clone made from such a url (a folder named `x<LF>FINDING IRSS: ...` holds one only since the whole-url read) has it for its origin, and its fetch error forged the same line (measured before the change). It goes through the same helper with `git remote get-url origin`. `verify_sibling`, the `cloning` and `would clone` lines and the findings already spell what they name; `run_member_bootstrap` and `--make` inherit the member's stdout and relay nothing of git's. `bootstrap.py`'s FETCH INCOMPLETE relays `git submodule update`'s text raw too, on main as here; it is not in this PR's file and stays for #459. tests/test_siblings_inputs.py: `test_a_url_holding_a_line_feed_forges_no_finding_line` (the reviewer's row, with an ESC variant) passes on main, fails on 25c7b14, and passes with the change; `test_the_clone_failure_names_a_line_feed_url_spelled` pins the spelled url in the same text; `test_a_failed_fetch_forges_no_finding_line` runs a real fetch from a folder whose name holds the line feed. The composite assert at :228 that SonarCloud reports as S9073 is two asserts. Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
project: or url is a finding, .gitmodules is read as git reads it, the warning's prose is spelled, and a tag-only branch is named (#690)project: or url is a finding, .gitmodules is read as git reads it, the warning's prose is spelled, and a tag-only branch is named (#690)
|
(lane openRepoShape-1, 2026-10-17T11:46:08Z) Lane 1's re-verification of #805 at 5ba1f8b: CHANGES NEEDED Findings 2 and 3 are fixed. Finding 1 is fixed for every url git quotes in full, but a url longer than git's message buffer gets past Merges. Main moved twice during this check. Head +
Why the long url gets through. git formats def test_a_url_longer_than_gits_message_forges_no_finding_line(family,
tmp_path):
"""git writes a `die` message into 4096 bytes, so a url longer than that
is quoted cut short, in neither form `said_of_url` looks for."""
url = (b'"/r/nowhere\\nFINDING ' + KEPT.encode() + b': forged/'
+ b"/".join([b"a" * 200] * 30) + b'"')
result = siblings_over_url(family, tmp_path, url)
assert result.returncode == 1, result.stdout + result.stderr
findings = [line for line in result.stderr.split("\n")
if line.startswith("FINDING ")]
assert findings == [f"FINDING {BENT}: git clone exited 128:"], \
result.stderrThe fallback I ran. It goes in if url not in text and quoted not in text:
# git cuts a message at 4096 bytes, so a long url is quoted in
# neither form: each line is spelled, and none after the first
# starts a line of this output.
return "\n ".join(spelled(line) for line in text.split("\n"))With it, the 6061-character url forges nothing ( Texts to fix at the head.
For the checklist (not this PR). Lane: openreposhape-1 (openRepoShape-1) |
… in neither form, has the text that relays it spelled line by line, so it forges no FINDING line (#690) Lane 1's re-verification of #805 at 5ba1f8b: git formats a `die` message into 4096 bytes, so a url longer than that is quoted cut short, and the text holds neither the url as given nor its `?` form, the two `said_of_url` looks for. Nothing was replaced, and a `.gitmodules` url `"/r/nowhere<LF>FINDING IRSS: forged/" + 30 parts of 200 characters` (6061 characters) still put the forged line at column 0 of IRRS's CLONE FAILED finding, where main, which cut the url at the line feed, had none. Where the text holds neither form, `said_of_url` now spells each of its lines and joins them with a newline and four spaces, so no line after the first can begin a FINDING line. The reviewer's five lines, as given. It takes effect only for a url that `spelled` changes and that git's text does not quote in full, so an ordinary failure's text is not changed. tests/test_siblings_inputs.py: `test_a_url_longer_than_gits_message_forges_no_finding_line`, the reviewer's row, next to the `FORGED_URLS` rows. It fails at 5ba1f8b and passes with the fallback; with the fallback removed (M9) it is the one row that fails. Left for #459: `git clone --recurse-submodules` relays git's text for a member's own leg urls raw, on main as here, and so does `make bootstrap`'s FETCH INCOMPLETE. Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
(lane openRepoShape-1, 2026-10-17T11:59:40Z) Lane 1's re-verification of #805 at dc1c8e4: PR-READY The long-url fallback closes the last way a line feed in a url could forge a FINDING line, ordinary failures print exactly what I checked against main
Still open, and not blocking this PR:
Lane: openreposhape-1 (openRepoShape-1) |
|
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 11 hours and 56 minutes by commenting |
|
#805 (#690) rewrote `siblings.py::submodule_urls` to read `git config -z --list` bytes, decoded with "replace", records cut at NUL and each record split at its FIRST newline, a section's name being all of its key between `submodule.` and the last dot. #775 (#733) applied its rules in the old `splitlines` + first `=` loop. They conflicted in that function's docstring and its loop, and nowhere else. The loop is #805's, with #775's rules carried into it in the order the doctor's `submodule_keys` applies them to the same listing: - an empty name, or one with a `..` component (`DOTDOT_COMPONENT`), skips the section; - a key of `VALUED_KEYS` given with no value at all, or a value `git_dies_on`, refuses the whole file (`{}`). In `-z` output a bare key is a record with NO newline (`submodule.x.url\0`, measured on git 2.43) and `url =` is one with a newline and an empty value, so "valued" is whether `record.partition("\n")` found its separator; - a `path` or `url` beginning with `-` is ignored, that key alone; - a url is keyed by its section's `path` alone (#775's return line, which merged cleanly). Everything else is #805's: the later value wins, a name with a space, dot, quote or `=` is read whole, and a byte that is not UTF-8 is replaced (#432). The docstring keeps both paragraphs, #690's on how the listing is read and then #733's on which of it git reads. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ames that differ only in bytes that are not UTF-8 stay two sections, as git and the doctor read them, and an empty `ignore =` or `branch =` and an `update = -x` are pinned (#733, review round 4) RV775r4's review of round 4 (`32048c4f`, #775 issuecomment-6237014956), its two SHOULDs and three NITs: - SHOULD 1: `submodule_urls` decoded the whole `-z` listing with "replace", so `[submodule "caf\xe9"]` and `[submodule "caf\xe8"]` (or `caf` + the UTF-8 of U+FFFD) were one section: it gave `q` alone, and with the path in one and the url in the other a url git does not mount. The listing is now decoded with "surrogateescape", as the doctor's `gitmodules_listing` decodes it (#732), and the VALUE alone is re-decoded with "replace" (#432). New rows beside #805's parity test, `test_two_names_apart_only_in_bytes_not_utf8_stay_two_sections`, check the doctor, `make siblings` and `git submodule init` for all three. - SHOULD 2: `GIT_SKIPS` gains `empty-ignore` and `empty-branch` controls: git takes `ignore =` and `branch =`, so `valued` is the newline, not the value. - NIT 1 and NIT 2: the docstring says a record is the key and, where it is given a value, a newline and the value, and that git may say the value is invalid. - NIT 3: `GIT_SKIPS` gains `update-dash` (`update = -x`, which git dies on), so the `-` skip stays after the refusal. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
main gained #713 (#645), #812 (#693), #767 (#685), #745 (#704, #694) and #805 (#690) since 2000488. No conflict, and none of them touches README.md, the handbook, AGENTS.md, docs/ or the README cap's ledger, so every handbook marker reads as before. README.md is 1475 lines, its cap; AGENTS.md 494, its cap. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…notes entries for the five pull requests landed since 2000488, current to main 40e678e (#563, #683) RV630's review of rounds A3, A4 and B1 (#630 issuecomment-6233641684, READY), its four optional nits: - 1: README.md's drift paragraph ended on the one-word line "finished.". "exactly " and "about the shape " are cut, and the paragraph re-wraps to seven lines with both bold leads whole. README.md is 1474 lines, its cap (1422 + 2 + 5 + 12 + 22 + 10 + 1); tests/readme-cap.d/683.txt is +1, and its text says why. Both `Keeping a project's shape current` markers move to df780eb5; the other 21 read current. - 2: #682's lead reads "A holder's `make bootstrap` that left a member unchecked-out". - 3: #730's entry says "Comments and docstrings", not "Four": its squash makes three claims, comments only. - 4: the doctor's `members` row has one bullet, #705's sentence and #624's together. Wave B2 so far, `git log --first-parent 2000488..40e678e`, each entry from its squash diff: - #713 (#645): the test session turns git's automatic maintenance off (Install, CI and the docs). - #812 (#693): `add --local-remote-dir`'s member clone URL, the remedies and the READMEs' quoted clone URL (the `--local-remote-dir` bullet). - #767 (#685): `members/` renamed with `git mv` is renamed back in two steps, and the git a printed `--sparse` needs (family). - #745 (#704, #694): lane 5's line (#630 issuecomment-6227129879), with v1.0.0's known limit at v1.0.0.md:463 (no git on `PATH`) said closed (Bootstrap). - #805 (#690): `make siblings`'s inputs (family). The counts (111 pull requests; 122 of 135 milestone issues closed at 16:14Z), "current to main" and the open list (13, each with a pull request open) are at main 40e678e, still placeholders. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…anded, among it #805's `-z` read of `.gitmodules` in siblings.py (#786) A clean merge. It brings in what main took after #782's squash, among it #805 (#690, 40e678e), which reads `.gitmodules` in the family template's siblings.py with `git config -z --list`. README.md stays 1475 lines, at the cap of 1475 that tests/readme-cap.d/ gives with this branch's 786.txt (+1); AGENTS.md is main's, 494 lines at its cap of 494; and every handbook marker is current. The tree is 1d73091, what one merge of main into 31c3792 gives with the same resolutions. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ask the row whose section name holds a carriage return, since `siblings.py` has read `.gitmodules` with `-z` since #805 (#690) (#786) `test_siblings_reads_what_each_row_says` left out every row holding a CR, because `siblings.py` cut `git config --list` at `splitlines` and read the section `q\rx`'s `update = bogus` as the key `submodule.q` and `x.update`. #805 (40e678e), which closed #690, reads `git config -z --list`, so the CR stays inside the key and `update` is read as `update`. The exclusion and the docstring's sentence about it go; the docstring now says the row is asked. The rows still skip, all 27 of them, until #775's rules are in the family template's `siblings.py` on main: the `siblings` fixture skips where `DOTDOT_COMPONENT` is missing, and that skip stays. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… `--into` sentence names the `family-landing-in-standard` refusal, `make siblings` is described as it stops before cloning, and `members/` has its one-spelling rule (#695) (#738) The family docs now say what `family.py init`, `make siblings` and the `members/` directory do since #488, #578 and #574. Base: `8062810d`. Head: `c42286de`: the work (`73fa3ff4`) and one commit for the review's nit on L20. **The branch now holds `origin/main` at `24a977c0`, through one merge commit, `75e35f14`.** No other commit was added, and nothing was rebased or amended. ### What failed Since #682 landed (`dc7d3fbb`), `git merge-tree --write-tree HEAD origin/main` against `c42286de` stopped on one line of `docs/handbook.html`: Auto-merging docs/handbook.html CONFLICT (content): Merge conflict in docs/handbook.html It is the `families` section's marker, `<!-- reads README.md § Families: ... blob <hash> -->`. #682 changed a sentence of README § Families, so main's marker is `b3d19092...`. This PR changed other sentences of the same section, so its marker is `0364c579...`. Git saw one line changed two ways. Lane 2's note on #738 (2026-10-17T12:47:17Z) named the same line. I reproduced the conflict on the head I started from, then merged by hand. ### The fix The merged README § Families is neither side's text, so the marker takes its fingerprint, `82d7f8087b9ee4d097d33e15fbfe5ae754a04a79`. I computed it with `tests/test_repo_hygiene.py`'s own `readme_fingerprint` over the merged `README.md`. It is the value lane 2's note gave, and it did not move when main moved on to `24a977c0`. | marker value | where it came from | handbook outline test | |---|---|---| | `b3d19092...` | main's side | fails: "has moved since the page read it" | | `0364c579...` | this PR's side | fails, same message | | `82d7f808...` | the merged README § Families | passes | I re-read both sides before taking the value: - Main's side brought one sentence into README § Families (line 955, "`make bootstrap` reports and skips one it lacks a credential for."), and #682 had already put the matching sentence on the page. - This PR's side is its own 55 README lines and 27 handbook lines in that section, which merged clean. - Both sides' sentences stand in the merged README and in the page. ### Did the merge change README or handbook text beyond the conflict line? No. I compared each changed file's diff against the parent that did not make it: - `README.md`: the merge against `origin/main` shows exactly this PR's own change, and the merge against `c42286de` shows exactly main's own change since `8062810d` (one line, #682's). No line differs. - `AGENTS.md`: the same two comparisons, no line differs. - `docs/handbook.html`: the same two comparisons differ in the marker line only, in both directions. - README's handbook markers (23 on the page) and README's line cap are untouched. `tests/test_repo_hygiene.py` passes with the cap and the markers as they are. This PR still lands only in lane 3's README turn. If another README PR changes § Families first, the marker moves again, and the hygiene test names it. ### Tests All on `75e35f14`, through the gh stub, with a bogus token and an empty `GH_CONFIG_DIR`. | run | files | result | |---|---|---| | core | `test_readme_sections.py`, `test_family_docs_one_spelling.py` (the one test file this PR changes), `test_repo_hygiene.py`, `test_agents_md_codes.py`, `test_cli_reference.py` | 240 passed | | handbook readers | `test_readme_windows_quoting_sentence.py`, `test_setup_by_hand_commands.py`, `test_update_add_codes_documented.py` | 20 passed | | family-root templates | `test_pinned_copy_text.py`, `test_template_values_single_pass.py`, `test_family_init_values.py`, `test_family_init_landing.py`, `test_siblings_family_name.py`, `test_family_makefile_siblings.py`, `test_family_siblings_placement.py` | 582 passed, 19 skipped | | bootstrap (main's new files, over the template this PR reworded) | `test_bootstrap_spelling.py`, `test_bootstrap_without_git.py`, `test_bootstrap_leg_moves.py` | 251 passed | ### Mutants This round adds no test and no code, so its one hunk is the marker value. Each mutant changes that value in a shared scratch clone at `75e35f14` and runs `tests/test_repo_hygiene.py::test_the_handbook_follows_the_readmes_outline`: | mutant | result | |---|---| | none (control) | 1 passed | | main's marker `b3d19092...` | 1 failed | | this PR's marker `0364c579...` | 1 failed | | the right marker with its last digit changed | 1 failed | | the unresolved conflict block (`<<<<<<<`, both lines, `>>>>>>>`) | 1 failed | ### Merge checks - `git merge-tree --write-tree HEAD origin/main` is clean at `24a977c0`. - Main moved to `40e678e6` (#805, `templates/family-root/scripts/siblings.py` and `tests/test_siblings_inputs.py`) after this merge. It touches no README, handbook or AGENTS text, and the merge-tree against it is clean. - Lane 2's #822 is stacked on this PR. Its reviewer found it merges clean with this PR landed at the same marker value. 1. **The `--into` sentence** (README.md § Families and the handbook). It gave the default, "the directory you are standing in", and not that `init` refuses that default inside a checkout of this standard. It now says so and names the code: `(the directory you are standing in, by default; refused inside a checkout of this standard, \`family-landing-in-standard\`)`. AGENTS.md already said it. 2. **The `make siblings` paragraph** (README.md § Families and the handbook). It said `make siblings` "WARNS and prints the exact `mv`". Since #578 it also clones nothing until the `mv` is run, so the next run clones each member once, and a link of the family's name beside the folder passes. It now says that. Lane 3's two #460 sentences ride in it (L20, L21, which lane 3 marked `-> #695`): - the folder must be named EXACTLY after its repository, case included, on every disk; - for a holder at `~/projects/InkRouter` the move makes the family folder `~/InkRouter`, beside `~/projects`, and the holder `~/InkRouter/InkRouter`, not `~/projects/InkRouter/InkRouter`. I wrote L20 with the paths spelled out. Lane 3's wording was "beside that folder's parent", which two readings can take. My first wording, "beside that folder, not inside it", still left one: the thing beside `~/projects` is the family folder, not the holder (rv695 N1). The printed move for that holder is `mv ~/projects/InkRouter ~/InkRouter.holder`, `mkdir -p ~/InkRouter`, `mv ~/InkRouter.holder ~/InkRouter/InkRouter` (probe below, and `test_family_siblings_placement.py` pins it). 3. **The one-spelling rule for `members/`.** One sentence, the same words in five places: AGENTS.md (family item 2), README.md § Families, the handbook, the holder's README and the holder's `AGENTS-shape.md`: > A holder keeps `members/` in ONE spelling on every disk: `add` refuses `family-members-dir-variant` (`member-path-taken` for a variant of the member's own path) until a human renames what sits under another spelling. `templates/family-root/AGENTS-shape.md` changes by that one sentence. It is in `FAMILY_COPIED_VERBATIM`, so every holder reads `upstream-changed` for it on its next `update-shape.py check`. The issue's third line names it, and lane 3 asked that it ride with the next pinned-file edit. It can share the one v1.1.0 shape bump with the bootstrap fix. `templates/family-root/README.md` is templated and not pinned: an existing holder keeps its own README. - README.md is 1458 lines, one under its cap of 1459 (1422 + 5 + 22 + 10 in `tests/readme-cap.d/`); it was 1459 at `73fa3ff4`. The brief said 1449; that is the cap before #627 landed. AGENTS.md is 492, its cap. - The new words come to about nine README lines and three AGENTS.md lines. I fitted them by cutting wording that is stated elsewhere. **If you would rather keep the cut sentences, the alternative is a `tests/readme-cap.d/695.txt` of about +8, which needs Brett's word.** What I cut: - README § Families: - the aside "the same lands-where-you-were-standing rule projects have"; - four rewordings that lose no meaning: - the intro of "Use a family when the parts deploy separately": "folding them into one assembly root would give them one gate, one release and one pin, which is the opposite of what 'deploy separately' means" is now "one assembly root would give them one gate, one release and one pin, the opposite of 'deploy separately'"; - "`--work-dir` remains the override" is now "`--work-dir` is the override"; - "The doubled name is kept on purpose" is now "The doubled name is on purpose"; - "`family.yaml` is what tells the two apart" is now "`family.yaml` tells the two apart"; - the caption "Eight services, each its own project, one holder:" under "The InkRouter example" (the section's intro and the block under it say it; this is the one line cut to make room for the L20 rewording, and the handbook keeps its caption); - the tool's rationale after "Nothing ever moves a checkout" (the shell, the editor, the agent lanes, "an act by surprise"); it stays in the handbook, the holder's README and the tool's own output, and README keeps the reason that matters (a linked worktree's `.git` file carries an absolute path); - ", not merely a repository at the right commit"; - the private-legs reason in the `make bootstrap` paragraph; - "to a policy two live projects already carry a copy of" in the naming paragraph. - AGENTS.md: - the converse in the family intro ("one project is what parts that ship together already are"; README keeps it); - the sentence "`bump` takes the same repeatable `--trailer`" in family item 3 (stated in "Advancing a leg" step 6 and in the holder's `AGENTS-shape.md`); - "Do not pass `--yes` on your own initiative" in "Updating" step 2, now "Never pass `--yes` yourself", the words `test_repo_hygiene.py` requires elsewhere in the file. - The handbook marker for README § Families is recomputed with `readme_segments` / `readme_fingerprint`: `89ef0ab5ec88...` to `0364c5794c46...` (it was `dc85a9260a67...` at `73fa3ff4`, before the L20 rewording and the caption cut). No other README section changed, so no other marker moves. New file `tests/test_family_docs_one_spelling.py`, 17 tests: the one-spelling sentence in each of the five docs, the two codes held as strings by `scripts/family.py`, and the `--into`, `make siblings`, "named EXACTLY" and "straight in a folder" sentences in README and the handbook, plus the old "WARNS and prints" sentence gone from both. The handbook is read as the text a browser shows (`<code>` as backticks). - On base `8062810d` (the file copied in): **15 failed, 2 passed**. The two that pass are the code-exists checks. - On the head `c42286de`, after the L20 rewording: `test_repo_hygiene.py`, `test_readme_sections.py`, `test_agents_md_codes.py` and the new file: **217 passed** in 3.37s. The new file's `STRAIGHT_IN_A_FOLDER` is in the new README and not in the old one. - Before that, the targeted runs, each at `8062810d` plus the working tree that was then the head (`73fa3ff4`): - `test_repo_hygiene.py`, `test_readme_sections.py`, `test_agents_md_codes.py`, `test_cli_reference.py`, `test_readme_windows_quoting_sentence.py`, `test_setup_by_hand_commands.py`, `test_update_add_codes_documented.py` and the new file: **253 passed**. - Fifteen files that read AGENTS.md, the holder's docs or the refusals (`test_family_init_landing`, `test_siblings_family_name`, `test_family_switch_branch_exists`, `test_family_tracking_branch`, `test_template_text_placeholders`, `test_template_values_single_pass`, `test_template_quoted_values`, `test_free_plan_hint`, `test_doctor_recorded_answers`, `test_update_shape_add`, `test_update_shape_header_comments`, `test_family_add_refusals`, `test_family_members_case_variants`, `test_family_member_path`, `test_doctor_legs_members_next`): **987 passed**. - Eight files: `test_adopt_plan`, `test_adopt_github_exits`, `test_adopt_late_failure`, `test_setup_project_py`, `test_family`, `test_family_siblings_placement`, `test_family_makefile_siblings`, `test_park_resume_targets`: **598 passed, 1 skipped** (`SHAPE_MEDXEHR_CLONE` unset: a plan test needs a read-only clone of MedxSoft/MedxEHR, a cloud tool). - `test_update_shape.py -k "shape_line or stale_readme or check_says_what_apply or readme_with_no_shape"`: 7 passed (the tests that read the holder README's last line). The rest of `test_update_shape.py` and `test_shape_doctor.py` were not run: they copy the family-root tree and match none of the lines I changed (`git grep` of the changed sentences under `tests/` finds nothing), and a first run of them crawled under a load of 90-100 on the machine. - The full suite was not run, as the brief says. - Row 1, on base: from a checkout of the standard, `python3 scripts/family.py init --org Acme --family Orbit --dry-run` ends `REFUSED family-landing-in-standard ... Remediation: pass \`--into ..\``. The README sentence did not say it would; it now does. - Row 2, on head: `parent_folder_warning` of the holder's `siblings.py` for a holder at `<t>/home/projects/InkRouter` prints `NOTHING WAS CLONED` and the move `mv .../projects/InkRouter .../InkRouter.holder`, `mkdir -p .../InkRouter`, `mv .../InkRouter.holder .../InkRouter/InkRouter`. For a folder named `inkrouter` on Linux it stops the same way: the name test is exact on every disk. - Row 3: the refusals the sentence names are the ones `test_family_members_case_variants.py` and `test_family_add_refusals.py` drive (60 + 65 tests, passed above). - No row needs real GitHub or a cloud tool. My own run did not happen: a usage checkpoint stopped it before it began. The reviewer (rv695) ran 30 mutants against `73fa3ff4` in a throwaway clone and **all 30 were killed**: five places, four mutants each for the one-spelling sentence, and five each for the README and the handbook sentences. The L20 sentence has changed since, and `STRAIGHT_IN_A_FOLDER` with it; no mutant was run on the new words. What the guards cover there: the sentence has a parametrized case per doc, so reverting it fails that case, and a reverted README hunk leaves the handbook marker stale. - origin/main `55c6ba7d` (it moved past `2fac9790` by #700): clean. rv695 found `73fa3ff4` clean against `2fac9790`. - #630 (lane 3), head `6fd33a93`: clean. It merged main `de926142`, and its handbook conflicts at the three markers are gone. On the merged tree README.md is 1472 lines and AGENTS.md 494 (counted here; rv695 found them under #630's own ledger at `73fa3ff4`). - #682 (lane 5), head `8b39cd19`: conflicts at one line, the handbook marker for README § Families (`0364c5794c46...` here, `c40baa828dca...` there). README.md and the rest of the handbook merge clean, and the merged paragraph reads correctly, because both PRs change that section. Whichever lands second recomputes the marker from the merged README: on this head plus #682 the recomputed value is `da54c8ec2665...` (computed with `readme_fingerprint`, not test-run). - I dropped the earlier list of clean PRs: it was taken against older heads, and several of them have landed. (Brett Heap, 2026-10-16T13:36:23Z, on #459 line 141: "document it in the v1.1.0 release notes". `docs/release-notes/v1.1.0.md` exists only on #630, so this PR does not create it.) > A holder made with `--family X --name Y` before #642 (the fix for #629) keeps `cd Y` in its own README, and `update-shape.py apply` never re-renders a README, so change that one line by hand to `cd X`: the holder's directory is named after its repository, `X`, never after the display name. Such a README also shows `Y` in its layout block and its doubled-name sentence, as prose; only the `cd` line fails when run. - The holder's README does not get lane 3's L20 and L21: lane 3 asked for them in the README paragraph, and the holder's README already names the folder after the repository. - Checklist candidate, not for this PR (rv695 N6): the holder's own docs say the folder is "named after the family" (`templates/family-root/README.md:73`, `templates/family-root/AGENTS-shape.md:99`) and not "EXACTLY, case included", which the standard's README and handbook now say and `siblings.py` tests (`folder.name == family`). Lane 3 asked for L21 only in the standard's README paragraph, so it is not owed here; it could ride in the same re-pin. Review: lane 5's independent review approved c42286d with its nits fixed; 75e35f1 is its update onto main (lane 2's note, issuecomment-6234057822); Codex found no major issues. CI (Brett Heap's ruling 27): H0 is the head 0b26ace itself, which passed tests, tests-git-bash and SonarCloud; main has touched none of its files since its base 2e476a7; and the local gate passed on its predicted squash tree 68dd413 over main 112eeed: the family_docs_one_spelling, readme_sections, readme_windows_quoting_sentence, setup_by_hand_commands, update_add_codes_documented, family_tracking_branch, family_switch_branch_exists, pinned_copy_text, repo_hygiene, agents_md_codes, cli_reference, family sets, 527 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 #695 Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…-reading steps read `.gitmodules` by the rules the doctor and `siblings.py` read it by, so a section git ignores, a section with no path, a `path` or `url` beginning with `-`, an unmerged file or a file git refuses whole no longer puts a repository in the App token or the lists, and a refused file fails the step with its cause (#786) (#790) Closes #786. The two `validate.yml` templates, assembly root and family root, read every `.url` key of `.gitmodules` at three sites each: the step that names the repositories the App token is minted for, the fetch step's list of urls, and the failing step's list. So CI named repositories git never fetches, and where the App could not reach one, the mint step failed the job. Now all three sites read the file by the rules the doctor and `siblings.py` use (#733): only a `submodule.?*.url` key counts; a section named with a `..` component, or with no `path`, is skipped; a `path` or `url` value beginning with `-` is ignored, that value alone, so an earlier one stands; and nothing is read while `.gitmodules` is unmerged. A file git refuses whole fails the names step and the fetch step with an `::error::` that names the cause, the key or git's own `fatal:` line, whatever credential is configured. Before, the fetch step with no credential degraded to green. The refusal comes before the App advice, which is the wrong advice for that file. The printing steps list a key once. `tests/test_validate_workflow_gitmodules_urls.py` holds a row for each rule, checked against `git submodule init`, the doctor and `siblings.py`. README.md, the handbook and both template READMEs now say a `.gitmodules` git refuses fails the run whatever the credential. After the merge of main `2e476a77`, which brings #805's `-z` read of `.gitmodules` into `siblings.py`, `e91b1a5d` drops the #690 exclusion of the row whose section name holds a carriage return from `test_siblings_reads_what_each_row_says`, so that row is asked too. All 27 `siblings` rows skip until #775 lands: the `siblings` fixture skips where the family template's `siblings.py` has no `DOTDOT_COMPONENT`, and main's has none. Lane 3's independent reviewer RV-c (opus) found it READY in its second round (#790 issuecomment-6232685791). Lane 3 checked the test-only NIT round after it, and RV-c's READY stood (#790 issuecomment-6232735343). RV790n found the merge round READY (#790 issuecomment-6243554860). It lands on Brett Heap's word "merge lane 3's v1.1.1 drafts after the v1.1.0 tag" (2026-10-17T13:00:57Z). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…(`[submodule ""]`, a `..` component), a `path` or `url` beginning with `-`, a section with no `path`, a file git refuses for a bare `path`, `url`, `ignore`, `update` or `branch` key or for a value it dies on (`update = !cmd`), and an unmerged `.gitmodules` name no mount url, NEXT line or clone, where git says `No url found` or refuses the file and exits 128 (#733) (#775) Closes #733. `shape-doctor.py` and `siblings.py` read `.gitmodules` more loosely than git does, so they named a mount url, a NEXT line or a clone where `git submodule init` says `No url found` or refuses the file and exits 128. Now both read only what git reads: - a section whose name is empty or holds a `..` component is skipped whole; - a `path` or `url` value beginning with `-` is skipped, that key alone; - a section with no `path` names no mount, and a url is keyed by its section's `path` alone; - a key with no value at all (a bare `path`, `url`, `ignore`, `update` or `branch`) makes git refuse the working tree's file whole, so no url is read from it; an index or HEAD copy is read up to that line, and the sections before it keep their urls; - a value git dies on (an `update` other than `none`, `checkout`, `rebase` or `merge`, a `shallow` that is no boolean, a `fetchRecurseSubmodules` that is neither a boolean nor `on-demand`) refuses the file in any copy; - an unmerged `.gitmodules` names no url. The rules are in `submodule_keys`, `section_url`, `gitmodules_entry` and `mounted_from` of `shape-doctor.py`, and in `submodule_urls` of `templates/family-root/scripts/siblings.py`, where they sit in the `git config -z --list` loop #805 gave it. Section names keep git's bytes, so names that differ only in bytes that are not UTF-8 stay apart, as in git. `tests/test_doctor_legs_members_next.py` and `tests/test_siblings_inputs.py` check the doctor and `make siblings` against `git submodule init` over each case and its controls. git 2.43 crashes on a bare `branch` (exit 139), and the rows that ask it skip there; git 2.55 stops at it as at any other key. Lane 3's independent reviewer RV-c (opus) found it READY in rounds 2 and 3, and RV775r4 (opus) in rounds 4 and 5 and in its check of the last merge of main (#775 issuecomment-6229804341, -6229900472, -6237014956, -6237376002 and -6239488425). After #774 landed, it was restacked onto #774's squash by three merges, and its changed lines equal the reviewed delta (#775 issuecomment-6244253158). It lands on Brett Heap's word "merge lane 3's v1.1.1 drafts after the v1.1.0 tag" (2026-10-17T13:00:57Z). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>



Lane: openreposhape-4 (openRepoShape-4)
Claim: #690 (comment)
What changed and why
One pinned file,
templates/family-root/scripts/siblings.py(make siblings). Every holder takes it as oneupstream-changedrow. Each of #690's six lines was re-measured on main4ce90457with a holder fromfamily.py init --local-remote-dir, then measured again on this branch. Mainbf1f099bis merged in (no conflict; main had not touched this file), lane 3's two requests on #690 are added as test rows (the last row of the table), and lane 1's review of25c7b14cis answered (the bullet after the table).<t>/InkRouter.holderalready a folder, the three printed lines all exit 0 and leave the holder at<t>/InkRouter/InkRouter/InkRouter/family.yaml.staging_pathgives the first of<Family>.holder,.holder.1,.holder.2… at whichos.path.lexistsfinds nothing. The lines printmv <t>/t/InkRouter <t>/InkRouter.holder.1and run as typed to<t>/InkRouter/InkRouter/family.yaml.<Family>.folderworks the same way, and on a disk that ignores case a name in another case counts as taken.mvexits 0, thenmkdir -p <t>/InkRouterfailsFile exists, with the holder left atInkRouter.holder.First move <t>/InkRouter aside: it is a link to nothing, named after the family, and in the way of the move. Then:. The moves under it are the ones that run once the link is gone; moved aside, they run as typed.project:holding a lone surrogate or a NUL (R454 O1)UnicodeEncodeError: … surrogates not allowedorValueError: embedded null byteatgit clone, exit 1. IRSS, after it, is never cloned, and the dry run sayswould clone.argument_faultis asked of theproject:and of the url before anything. It is #455's rule as #644 wrote it formake bootstrap: a NUL, or a surrogateos.fsencodecannot write as one byte. A failing member isNOT AN ARGUMENT, with the findingFINDING IR\ud83dRS: itsproject:in family.yaml holds U+D83D, a lone surrogate, which no command can be given as an argument, so nothing was cloned or fetched for this member: git was never run with it.The run exits 1, the dry run says the same, and IRSS is still cloned. A url built from arepository:holding a NUL ended in the same traceback, and is now the same finding (its url https://github.com/InkRouter/IR\x00RS.git (family.yaml repository:) holds U+0000, a NUL, …).x<ESC>[31my, stderr lines 2 and 4 hold the raw ESC, and{family}is raw on line 2.spelled. The first line keeps itsrepr, and themvlines stay as typed (#486's question). The raw ESC is now on themvline alone.submodule_urlstruncates a url (R432 O2), a section namedmembers/a=b(lane 3, #473 issuecomment-6216310747),[submodule "a=b"](lane 3, #690 issuecomment-6232404184) and a CR in a section name (lane 3, #690 issuecomment-6232689453)<remotes>/caf.members/a=banda=bgive no url (the split at the first=reads the key assubmodule.a), so the member falls back tohttps://github.com/InkRouter/<P>.git. A CR in a name cuts the key in two (str.splitlines), so the section[submodule "q\rx"]is read as no section: measured on a bare file, main gives{'members/IRRS': ...}and this branch{'members/IRRS': ..., 'members/q': '/r/q.git'}.git config -z --list: records are cut at NUL, each key is split from its value at the first newline, and a section's name is everything in its key betweensubmodule.and the last dot. Onlypathandurlcount, and the later value wins. This isvalidate-family.py's reading and the one #668 (PR #700, on main) gives the doctor: for the same file the doctor'ssubmodule_configand this read give one{path: url}. A byte that is not UTF-8 is still replaced (#432).tracking_branch:that is only a tag (#578)git clone --branch v-trackleaves the clone detached, underon v-track, its tracking branch, and the run sayssiblings ok, exit 0.[IRSS] v-track is not a branch of <url> but a tag, which git clone checks out DETACHED: this clone is on no branch. The exit is unchanged; the issue asks for the line, not a finding..gitmodulesgives whole may hold a line feed, which git's own clone error quotes raw (and every other control as a?); the CLONE FAILED finding writes that url spelled, so git's text puts no line of.gitmodulesat column 0. The failed-fetch line does the same with the clone'sorigin. Both go throughsaid_of_url, which looks for the url as given and as git quotes it. A url longer than git's 4096-byte message is quoted cut short, in neither form, so git's text is then spelled line by line, each line after the first indented.docs/handbook.html,docs/cli.mdor a template is made false, and no--helptext changes.Tests
The new file is
tests/test_siblings_inputs.py: 55 tests, 15 of them parity cases that run now that #668 is on main.test_family's family fixture andtest_family_siblings_placement's helpers.os.renameonto an empty folder replaces it, wheremvmoves into it.\nalone, becausestr.splitlinesbreaks a command line at the U+2028 one test puts in it.What each case asserts:
.holdertaken by a folder, by a link to nothing, and twice over (.holder.2). The moves run as typed, what held the names is untouched, and the next dry run exits 0. Then.folder, held in the same case and in another, under a simulated disk that ignores case.NOT AN ARGUMENTrow, nothing made for the member, and IRSS cloned onmain. Plusargument_fault's four answers, including a surrogate that is one byte of a POSIX name, which is still handed on.x<ESC>[31my, with the raw ESC on themvline alone. A family with U+2028 is spelled on every line but the commands..gitmodulescorpus.=(members/a=b), lane 3's barea=b, and a CR (members/q\rx). Each is checked against git's own--getof the exact key.make siblingsclones from a url holding NEL whole, and that url is the clone'sorigin.members/a=b, and overa=b, names the.gitmodulesurl.update = bogus. The file with[submodule "q\rx"]and the file with the section namedqx, each holdingupdate = bogus, read alike. Git holds each key of both whole by its exact name. The row compares the two readings and pins no url: what a read does with a value git refuses is shape-doctor.py and siblings.py: a.gitmodulessection git ignores ([submodule ""], a name with a..component, a url beginning with-) is still read as a member, where git saysNo url foundand exits 128 #733's (shape-doctor.py and siblings.py: a.gitmodulessection git ignores ([submodule ""], a..component), apathorurlbeginning with-, a section with nopath, a file git refuses for a bareurl, and an unmerged.gitmodulesname no mount url, NEXT line or clone, where git saysNo url foundand exits 128 (#733) #775), not this one's.25c7b14c). A.gitmodulesurl holding a line feed, and one holding a line feed beside an ESC (which git quotes as a?), each with a forgedFINDING IRSS: forgedline in it, and a url of 6061 characters, longer than git's 4096-byte message: the only FINDING line is IRRS's own, the url is named spelled in git's text, and no raw ESC reaches stderr. A real fetch, from a clone whoseoriginis a folder namedx<LF>FINDING IRSS: forged, forges no FINDING line either. The forging rows pass on main (which cut the url at the line feed), fail on25c7b14c, and pass now.cloned detachedrow, and IRRS without the line..gitmodulesis split at the first space and at every Unicode line break, and read through a regexp a UTF-8 locale cannot match, so a section name holding a space or a byte that is not UTF-8, or a url holding NEL or U+2028, gives the doctor another mount url than git andmake siblingsuse #668: "whichever half lands second adds a parity test over one corpus"). The doctor'ssubmodule_configandsiblings.submodule_urlsmust give one{path: url}over one corpus: the six names, the eight breaks and shape-doctor.py: a holder's.gitmodulesis split at the first space and at every Unicode line break, and read through a regexp a UTF-8 locale cannot match, so a section name holding a space or a byte that is not UTF-8, or a url holding NEL or U+2028, gives the doctor another mount url than git andmake siblingsuse #668's Latin-1 name, underLC_ALL=C.UTF-8. shape-doctor.py: a holder's.gitmodulesis split at the first space and at every Unicode line break, and read through a regexp a UTF-8 locale cannot match, so a section name holding a space or a byte that is not UTF-8, or a url holding NEL or U+2028, gives the doctor another mount url than git andmake siblingsuse #668 is on main (shape-doctor.py:submodule_configandgitmodules_entryread a holder's.gitmodulesthrough onegit config -z --list, so a section name holding a space or a byte that is not UTF-8, or a url holding NEL or U+2028, gives the doctor the mount url git andmake siblingsuse, under any locale (#668) #700), so all 15 cases run and none is skipped.The run, on the pushed head (
siblings.pysha2566e8daddd…, the test file7115c1b9…):The gate, one background run of 6 files:
test_siblings_inputs.py,test_family_siblings_placement.py,test_siblings_family_name.py,test_repo_hygiene.py,test_cli_reference.pyandtest_agents_md_codes.py. 380 passed, none skipped, in 3:06.The same siblings files on a scratch merge of this head with main
20004887(committed, never pushed), with tests: validate-pins.py's workflow scan skips a directory, a dangling symlink and a looping symlink named.yml, and both of siblings.py's headers print the holder's root and folder spelled, so theS_ISREGchild check madelambda mode: True, or either path printed raw, now fails a test (#664) #706's file below: 244 passed.The earlier rounds' runs, which covered
test_shape_doctor.pyand the other files of the 15-file gate, were on25c7b14c(1272 passed, 3 skipped, 1 xfailed) and5ba1f8b3(815 passed, 3 skipped, 1 xfailed); the 3 skips aretest_shape_doctor.py:10429, the PowerShell line on a Windows host, and the xfail is its stricta-filerow for siblings.py (make siblings): a FILE, or a directory it cannot list, at../<Project>ends in a traceback (NotADirectoryErrororPermissionErroratsiblings.py:558), exit 1, where the doctor calls the slot NOT A CLONE #735. They were not re-run for this round.tests/test_pins_child_kind_and_siblings_root_spelling.py, on main since tests: validate-pins.py's workflow scan skips a directory, a dangling symlink and a looping symlink named.yml, and both of siblings.py's headers print the holder's root and folder spelled, so theS_ISREGchild check madelambda mode: True, or either path printed raw, now fails a test (#664) #706 (bb439a29): its 10 tests pass on the scratch merge with main, among the 244 above. It pins the holder's root spelling in both ofsiblings.py's headers, which this change leaves as they were.Mutants, each reverting one part of the fix in
siblings.pyand running the new file:submodule_urlsback to main's text read (--list,splitlines, split at the first=): killed. Run on the merged tree with lane 3's rows: 26 failed, 7 passed. The 7 are the names main's reading also reads: space, dot and quote in the read, and those three plus the Latin-1 name in the parity. The new rows that fail areequals-bare,cr, the CR row, thea=bdry run, and the doctor parity forequals-bareandcr.argument_faultreturns None: killed, 5 failed.staging_pathtakes the first name whatever holds it: killed, 5 failed.said_of_urlreturns the text unchanged: killed, 5 failed (the forging and spelled rows, and the fetch row).said_of_urllooks for the url as given only, not as git quotes it (?for a control): killed, 2 failed (both ESC rows). This is the reviewer's rawreplacealone.said_of_url: killed, 1 failed.said_of_url: killed, 4 failed.What it cannot tell
.foldercase. It needs a disk that ignores case, which CI (Linux only, ci: run the test suite on Linux only #597) does not have. It is simulated astest_family_siblings_placement'signoring_casesimulates it, withos.path.lexistsandos.path.existsanswering alike.os.fsencodewrites three bytes for every surrogate there, so every lone surrogate is refused. Read from CPython, as bootstrap.py and validate-manifest.py: a branch holding a NUL or a lone surrogate is a finding and step (a) does not hand it to git, sotracking_branch: "a\ud83db"or a leg'sbranch: "a\x00b"no longer endsmake bootstrapin a subprocess traceback under amanifest ok(#455) #644 did, not run..gitmodulessection holdingupdate = bogus, at a path the index holds as a gitlink, makesgit statusandgit submodule statusdie (fatal: invalid value for 'submodule.qx.update', exit 128), with or without a CR in the section's name; with no gitlink at that path, both exit 0.git config -z --listreads the same file, exit 0, and so does this change:make siblingsstill returns the other sections' urls, as main does. That refusal is lane 3's shape-doctor.py and siblings.py: a.gitmodulessection git ignores ([submodule ""], a name with a..component, a url beginning with-) is still read as a member, where git saysNo url foundand exits 128 #733, in shape-doctor.py and siblings.py: a.gitmodulessection git ignores ([submodule ""], a..component), apathorurlbeginning with-, a section with nopath, a file git refuses for a bareurl, and an unmerged.gitmodulesname no mount url, NEXT line or clone, where git saysNo url foundand exits 128 (#733) #775, which rewrites this loop; it is not in siblings.py (make siblings): the printedmvgoes astray over an existing.holder/.folderor a dangling link, a lone surrogate or NUL inproject:is a traceback, a url with NEL or U+2028 is cut, two warnings print paths raw #690's six lines, and the CR row pins only that the CR changes nothing.make siblings): the printedmvgoes astray over an existing.holder/.folderor a dangling link, a lone surrogate or NUL inproject:is a traceback, a url with NEL or U+2028 is cut, two warnings print paths raw #690:<Family>still stops the printedmkdir -pat its first line, with nothing moved.<Family>/<Family>still receives the holder INSIDE it (<t>/InkRouter/InkRouter/InkRouter/family.yaml).Shared files
gh pr list --state open --json files, at about 11:50Z on 2026-10-17, lists 43 open PRs, this one among them. Four touchtemplates/family-root/scripts/siblings.py, all lane 3's drafts: siblings.py: a file, or a foldermake siblingscannot list or enter, at../<Project>is NOT A CLONE, reported and left as it is, so the run no longer ends in a traceback atiterdirbefore the members after it are placed, andmake parkno longer ends in one over a folder it cannot enter (#735) #772 (l3-735-sibling-slot), shape-doctor.py and siblings.py: a.gitmodulessection git ignores ([submodule ""], a..component), apathorurlbeginning with-, a section with nopath, a file git refuses for a bareurl, and an unmerged.gitmodulesname no mount url, NEXT line or clone, where git saysNo url foundand exits 128 (#733) #775 (l3-733-ignored-sections), siblings.py and shape-doctor.py: a link to nothing at../<Project>is NOT A CLONE, reported and left as it is, so the dry run no longer sayswould clonewhere the real run ends CLONE FAILED, and the doctor'smembersrow no longer calls the slotabsent(#784) #788 (l3-784-dangling-slot) and siblings.py and shape-doctor.py: a Windows junction to nothing at../<Project>is NOT A CLONE, as a link to nothing is, so the dry run no longer sayswould cloneand the doctor'smembersrow no longer calls the slotabsent(#793) #794 (l3-793-junction-slot). None touchestests/test_siblings_inputs.py.git ls-remoteand fetched from GitHub just before. Each isgit merge-tree --write-treeof this head,dc1c8e45, and the other:20004887b2cdfeb61d149933ce6cb9323fa5beedtemplates/family-root/scripts/siblings.pyonly, both hunks insidesubmodule_urls: its docstring paragraph and its loope966a684d59fb2e42ffe154cf1e1692b.gitmodulessection git ignores ([submodule ""], a..component), apathorurlbeginning with-, a section with nopath, a file git refuses for a bareurl, and an unmerged.gitmodulesname no mount url, NEXT line or clone, where git saysNo url foundand exits 128 (#733) #775 is the same loop, as lane 3 says on siblings.py (make siblings): the printedmvgoes astray over an existing.holder/.folderor a dangling link, a lone surrogate or NUL inproject:is a traceback, a url with NEL or U+2028 is cut, two warnings print paths raw #690 (issuecomment-6232404184). shape-doctor.py and siblings.py: a.gitmodulessection git ignores ([submodule ""], a..component), apathorurlbeginning with-, a section with nopath, a file git refuses for a bareurl, and an unmerged.gitmodulesname no mount url, NEXT line or clone, where git saysNo url foundand exits 128 (#733) #775 reads the text--listand addsvalued,git_dies_on,DOTDOT_COMPONENTand the-rule; this PR reads-z. If siblings.py (make siblings): the printedmvgoes astray over an existing.holder/.folderor a dangling link, a lone surrogate or NUL inproject:is a traceback, a url with NEL or U+2028 is cut, two warnings print paths raw #690 lands first, lane 3 carries those rules into this loop; nothing here changes for it. For thevaluedflag: in-zform git prints a bare key askey\0with no newline, and a key given an empty value askey\n\0(measured on git 2.43:submodule.x.url\0for a bareurl,submodule.x.update\n\0forupdate =), sokey, valued, value = record.partition("\n")keeps the flag.git grepfor the blob ofsiblings.pybefore and after this change finds nothing.family.py initcomputes them into each holder'scontracts/shape-pin.yaml, which is why siblings.py and the family-root README:make siblings,parkandresumeprint the family's name and every member row spelled and never end in a traceback onname: "a\ud83db"or a dash underPYTHONIOENCODING=ascii, and the holder's README names its folder after the repository, so--family Quill --name QuillWorkslays out as its README says (#454, #629) #642 (0ed1b84) and bootstrap.py and validate-manifest.py: a branch holding a NUL or a lone surrogate is a finding and step (a) does not hand it to git, sotracking_branch: "a\ud83db"or a leg'sbranch: "a\x00b"no longer endsmake bootstrapin a subprocess traceback under amanifest ok(#455) #644 (cd5ffa2) changed only the template and their tests, as this PR does. Main's 29 commits since this branch's base touched none ofsiblings.py, and the merge was clean.submodule_configandgitmodules_entryread a holder's.gitmodulesthrough onegit config -z --list, so a section name holding a space or a byte that is not UTF-8, or a url holding NEL or U+2028, gives the doctor the mount url git andmake siblingsuse, under any locale (#668) #700 (shape-doctor.py: a holder's.gitmodulesis split at the first space and at every Unicode line break, and read through a regexp a UTF-8 locale cannot match, so a section name holding a space or a byte that is not UTF-8, or a url holding NEL or U+2028, gives the doctor another mount url than git andmake siblingsuse #668). It is on main (55c6ba7d). The.gitmodulesreading matches itssubmodule_keys, and the parity test runs against it.Closes #690
🤖 Generated with Claude Code
Summary by Sourcery
Harden
make siblingsagainst malformed inputs, unusual Git metadata, unsafe diagnostics, and filesystem staging conflicts while preserving correct placement behavior.New Features:
.gitmodulesedge cases, and tag-only tracking branches safely inmake siblings.Bug Fixes:
Enhancements:
.gitmodulesparsing with Git and the family doctor so unusual section names and URL values are interpreted consistently.Tests:
.gitmodulesparsing, Git error relaying, and tag checkouts.