Skip to content

README.md, the handbook, AGENTS.md and the family-root docs: init's --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

Merged
brettheap merged 4 commits into
mainfrom
fix/695-family-docs
Oct 17, 2026

Conversation

@brettheap

@brettheap brettheap commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

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

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.

Fix round: main merged in (2026-10-17)

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:

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:

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

What changed

  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 siblings.py and family.py: make siblings asks about the family folder before it clones, clones each member on its tracking branch, and family.py siblings runs the holder's own copy (#543) #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 Tracking: shape-doctor.py, reporting a repository's compliance #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.

Pinned copy: every holder re-pins

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.

Caps: neither is raised

  • 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 README.md and the handbook: say that a source's own single-repository.yaml record arrives as leg: drop, that the adopted Makefile's CONTRACTS_DIR follows the spec leg's mount, and that a second setup run is refused at the plan, so the three sentences #566, #572 and #573 left false or incomplete are true of main (#606) #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.

Tests

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.

Reproduction

  • 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.

Mutation probes

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.

Merge checks (git merge-tree --write-tree HEAD <ref>, at c42286de)

For lane 3: one sentence for the v1.1.0 release notes

(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.

Left out

  • 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.

Closes #695

🤖 Generated with Claude Code

Summary by Sourcery

Update family documentation and validation tests to accurately describe initialization refusals, sibling cloning behavior, and the required members/ spelling.

Enhancements:

  • Align family documentation with the current behavior of init and make siblings, including their refusal conditions and cloning workflow.
  • Document the required single spelling of members/ and the associated refusal codes across standard and generated family documentation.
  • Keep the handbook’s README section fingerprint synchronized after the documentation updates.

Documentation:

  • Clarify family initialization placement, sibling directory naming and relocation guidance, and the members/ naming rule across README, handbook, AGENTS.md, and family-root templates.

Tests:

  • Add documentation consistency tests covering the five copies of the members/ rule, refusal codes, initialization behavior, and sibling placement and cloning guidance.

… `--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, so three sentences that fell behind #488, #578 and #574 are true of main (#695)

README.md and docs/handbook.html § Families:

- The `--into` sentence says the default (the directory you are standing
  in) is refused inside a checkout of this standard, and names the code.
- The `make siblings` paragraph says what it does since #578: the holder's
  parent folder must be named EXACTLY after its repository, case included,
  on every disk; where it is not, and no link of that name beside it leads to
  it, it WARNS, prints the exact `mv` and clones nothing until it is run, so
  the next run clones each member once. Lane 3's two #460 sentences (L20,
  L21) ride in it: a holder at `~/projects/InkRouter` is moved to
  `~/InkRouter/InkRouter`, beside that folder, not inside it.

The one-spelling rule, one sentence in the same words in AGENTS.md (family
item 2), README.md § Families, the handbook, the holder's README and the
holder's pinned AGENTS-shape.md: a holder keeps `members/` in ONE spelling
on every disk, and `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.

Neither cap is raised. README.md stays at its cap of 1459 lines and
AGENTS.md at 492, by cutting wording that is stated elsewhere: README
§ Families lost the "same rule projects have" aside, the tool's rationale
after "Nothing ever moves a checkout" (the handbook, the holder's README and
the tool's own output keep it), "not merely a repository at the right
commit", the private-legs reason in the `make bootstrap` paragraph and the
policy history in the naming paragraph; AGENTS.md lost the converse in the
family intro, the `bump --trailer` sentence (stated in "Advancing a leg" and
in AGENTS-shape.md) and "on your own initiative" in "Updating" step 2. The
handbook marker for README § Families is recomputed.

templates/family-root/AGENTS-shape.md is pinned: every holder re-pins it
(`upstream-changed`). tests/test_family_docs_one_spelling.py holds the
sentences to the five places and the codes to family.py.

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

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-5, 2026-10-16T17:35Z) @codex review

@sourcery-ai

sourcery-ai Bot commented Oct 16, 2026

Copy link
Copy Markdown

Reviewer's Guide

This documentation-focused PR synchronizes family guidance with current init and make siblings behavior, establishes the members/ one-spelling rule across root and holder docs, updates the pinned template, and adds targeted tests enforcing wording and handbook synchronization.

File-Level Changes

Change Details Files
Updated family documentation to accurately describe initialization refusal behavior and sibling placement requirements.
  • Documented that the default --into location is refused inside a standard checkout via family-landing-in-standard.
  • Clarified that make siblings prints a relocation command and clones nothing until the move is completed.
  • Specified exact repository-name casing and documented the resulting ~/InkRouter/InkRouter placement.
README.md
docs/handbook.html
Established and propagated a single-spelling invariant for family member directories.
  • Added the members/ spelling rule and associated refusal codes to repository guidance and holder documentation.
  • Added the same rule to the pinned family-root agent template so existing holders receive it through shape updates.
AGENTS.md
README.md
docs/handbook.html
templates/family-root/README.md
templates/family-root/AGENTS-shape.md
Added documentation consistency tests for the new family behavior and terminology. tests/test_family_docs_one_spelling.py
Recomputed the handbook synchronization marker and reduced redundant prose to remain within documentation caps.
  • Updated the Families section fingerprint after README changes.
  • Trimmed repeated explanations without raising README.md or AGENTS.md caps.
README.md
AGENTS.md
docs/handbook.html

Assessment against linked issues

Issue Objective Addressed Explanation
#695 Update the README and handbook to explain that family.py init refuses the default --into location when run inside a checkout of the standard, naming family-landing-in-standard. ✅
#695 Update the README and handbook to describe make siblings accurately: it stops before cloning when the holder’s parent has the wrong name and no suitable link exists, and cloning proceeds on the next run after the move. ✅
#695 Document the one-spelling rule for members/ and the related add refusals in the README, handbook, AGENTS.md, and family-root documentation. ✅

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-16T17:38:25.724102Z 73fa3ff Manual request
ℹ️ About Codex in GitHub

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

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

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

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. What shall we delve into next?

Reviewed commit: 73fa3ff4ee

ℹ️ About Codex in GitHub

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

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

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

…e makes "the family folder `~/InkRouter`, beside `~/projects`, and the holder `~/InkRouter/InkRouter`, not `~/projects/InkRouter/InkRouter`", so "beside that folder" no longer reads as the holder itself (#695)

Review nit N1 on 73fa3ff. What sits beside `~/projects` is the family
folder `~/InkRouter`; the holder is one level inside it. The sentence now
says so in README § Families and the handbook, and STRAIGHT_IN_A_FOLDER in
tests/test_family_docs_one_spelling.py holds the new words. The README stays
under its cap (1458 of 1459): the one added line is paid for by cutting the
caption "Eight services, each its own project, one holder:" under "The
InkRouter example", which repeats the section's intro and its block. The
handbook marker for README § Families is recomputed.

Lane: openreposhape-5 (openRepoShape-5)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
brettheap pushed a commit that referenced this pull request Oct 17, 2026
…es every reason THIS checkout cannot answer, and the notes are current to main bf1f099 with the 25 pull requests since de92614 (#563, #683)

- W683 and RV-f's N4 on #742: README's exit-3 list (Keeping a project's
  shape current, :1208-1220) and its handbook twin (`doctor`) named only a
  pin whose commit this checkout does not carry. They now say, as
  shape-doctor.py's EXIT CODES does: a pin's commit it lacks, a partial
  clone missing what is compared (#683), a read that fails
  (`update-upstream-unreadable`), a crashed validator of its own (#278,
  #410), or its own naming policy not loading (#678). +1 line, with
  tests/readme-cap.d/683.txt; README.md is 1474, its cap (1422 + 2 + 5 +
  12 + 22 + 10 + 1). The two `Keeping a project's shape current` markers
  move: `doctor` text and marker, `current` marker only.
- The notes, "What changed since v1.0.1", one entry per pull request from
  `git log --first-parent de92614..bf1f099`, each from its squash commit and
  the issues GitHub says it closes:
  - adopt-project.py: #699, #719, #749, #727, #717, #728 (with #621's
    hint), #743 + #720 + #718, #722, #721;
  - update-shape.py, bump-leg.py and a project's own scripts: #702 (with
    the pinned validators), #634;
  - scaffold, setup, family and the shared readers: #725, #671, #744;
  - shape-doctor.py: #742, #701, #737, #751, #736, #734, #700 + #705,
    #740;
  - install, CI and the docs: the exit-3 sentence above.
- Counts (99 pull requests since bfed143; 109 of 135 milestone issues
  closed at 10:28Z), the "current to main" sha and the open list (26) are
  at main bf1f099, still placeholders for tag time.

Left for round B: lane 5's eight lines (#630 issuecomment-6227129879),
whose pull requests #726, #747, #682, #724, #746, #745 and #738 are open
drafts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Oct 17, 2026
…ck out fails the family bootstrap, every row's gitlink is asked before any checkout, and `--no-members` prints the case-variant move (#648) (#682)

**Head f74f640.** This branch holds #639 (#611), which landed as `c6122e8`, and main 2fac979, merged in bad6c00. Its diff against main is this PR's own change. `git merge-tree --write-tree f74f640 origin/main` is clean at 2a33a56.

**README PR.** It changes one sentence of README.md § Families, which this change makes false, and its twin in docs/handbook.html. The handbook's § Families marker is the segment's new blob, b3d19092. README stays at 1459 lines, its cap, and `tests/readme-cap.d/` is not raised. It lands in lane 3's README turn.

**Pinned copies.** `templates/family-root/scripts/bootstrap.py` and `templates/family-root/scripts/validate-family.py` change. Each holder sees them as `upstream-changed` on its next `update-shape.py check`. No test pins either file's bytes or digest; the tests that copy them into a holder take them from the template. Rounds 2 and 3 change `bootstrap.py` alone. By Brett's 13:36:23Z word (R0) the family root's pinned copy is re-pinned once, in v1.1.0's shape bump, so this PR re-pins nothing.

One commit, f74f640, on 23e1105, in the same two files.

### MM1. FIXED: a `.gitmodules` a merge left unmerged

In a plain clone, a merge conflicts in `.gitmodules`, and the file is resolved in the work tree but not added. The index then holds it only at its three conflict stages (`git ls-files -u` lists 3). Git reads no `.gitmodules` at all while it is unmerged: `repo_read_gitmodules` asks `is_gitmodules_unmerged` before it reads any file. So git's update dies at the first gitlink, `No url found for submodule path 'members/Atlas'`, exit 128. On 23e1105, bootstrap read the work tree's file, found every section with its url, and ended `family bootstrap ok`, exit 0 (rv648's `p10-head.log`).

`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 before every other reason, since no credential helps while git reads no section:

```
  NOT CHECKED OUT, and not for want of a credential: members/Atlas (git reads no .gitmodules while a merge leaves it unmerged), members/Nova (git reads no .gitmodules while a merge leaves it unmerged). Resolve the merge in .gitmodules and `git add .gitmodules`, then run this again.
```

With the file added, the same run checks both members out. That is all the cure needs; committing the merge is the person's own step.

**Test.** `test_a_gitmodules_a_merge_left_unmerged_says_to_add_it` builds MM1. The two sides each give OTHER's section another `branch`: `dev`, pushed from the holder, and `main`, committed in the clone. The merge exits 1, `ls-files -u` lists 3, and the work tree takes stage 2. The run exits 1, with the last two stderr lines exact and git's `No url found ... 'members/IRRS'`. Then `git add .gitmodules`, and the run is a good holder's. Against 23e1105's `bootstrap.py` the test fails on `exit 0`.

**Mutants.** Two more, both killed:

| mutant | result | tests that fail |
|---|---|---|
| U: the unmerged check dropped, so the work tree's file is read as before | 1 failed, 33 passed | MM1 |
| U2: the file read as unmerged, but the reason not given, so the rows fall to the next one | 1 failed, 33 passed | MM1 |

Round 2's eight were re-run on f74f640's two files. All are still killed, by the same tests.

**Results on f74f640.**

- rv648's lean set: `483 passed in 313.92s (0:05:13)`. That is 482 at 23e1105, plus MM1.
- The four files that load the family `bootstrap.py` (`tests/test_update_shape_kept_helper.py tests/test_windows_paths.py tests/test_pinned_copy_text.py tests/test_cli_reference.py`): `259 passed in 41.20s`.
- rv648's `p10.sh`: A1T and H2T as on 23e1105, each typed through, with the gate passing in all three modes. MM1 now exits 1 with the line above.

**A sentence of this body was false.** "Not changed" said "A fresh clone, where bootstrap runs, has the work tree's file and no merge". MM1 is a fresh clone with a merge. That bullet now says what is left.

**Candidate 6** (what git dies on while reading `.gitmodules`) is filed as a v1.1.1 issue (#768), with the approximations below.

One commit, 23e1105, on 45c512e. It changes `templates/family-root/scripts/bootstrap.py` and `tests/test_family_bootstrap_gate_order.py` only. README, the handbook and `validate-family.py` are as they were.

### M1. FIXED: `.gitmodules` is read as git's own parser reads it

`init_stop` read `.gitmodules` with `git config --file --list`. That keeps entries git's submodule parser drops or overrides. So in three states git's init died and bootstrap still said ok:

| state (plain clone) | git's update | 45c512e | 23e1105 |
|---|---|---|---|
| D1: a second section `nova2` gives `members/Nova` with no url, after Nova's own | 128, `No url found ... 'members/Nova'` | `family bootstrap ok`, exit 0 | exit 1, both rows named, `--no-members` |
| Q1: Nova's section is named `../Nova` | 128, `ignoring suspicious submodule name`, then `No url found` | ok, exit 0 | exit 1, both rows named, section written back |
| O1: Nova's url is `-nowhere` | 128, `ignoring ... may be interpreted as a command-line option`, then `No url found` | ok, exit 0 | exit 1, both rows named, section written back |

**The parser rules, as built.** `gitmodules_sections` now returns a `Gitmodules`. Its `read` (`{path: name}`) and `urls` (`{name: url}`) follow git 2.43's `submodule-config.c`, entry by entry, in file order:

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

Its `given` counts every section that gives each path, as `validate-family.py` reads them. That count is what `member-gitmodules-absent` and `gitmodules-path-twice` see, so the line's cures stay true.

`has_url` asks the section git reads for a path, then the git config under that section's name. Where git reads no section for the path, there is no url, whatever the config holds: git's init dies on `!sub` before it looks at the config.

**Checked against git 2.43's own source** (`submodule-config.c`, `submodule.c`, `builtin/submodule--helper.c` and `config.c` at v2.43.0) **and measured** with git 2.43.0 in plain clones (`p8.log`):

| `.gitmodules` form | git 2.43's update |
|---|---|
| name `""`, `nova\..\x`, `a/../Nova` | ignored; `No url found`, 128 |
| name `..Nova..` (dots that are no `..` component) | read; exit 0 |
| `path = -members/Nova` | dropped; `No url found`, 128 |
| a good url, then `url = -x` | the `-x` dropped, the good url kept; exit 0 |
| a second section with Nova's url first, Nova's own with no url after | the later read; `No url found`, 128 |
| `nova1` gives the path, `nova2` gives it too, then `nova1` gives another path | `members/Nova` maps to nothing; `No url found`, 128 |
| `[submodule.Nova]` (the old header) | read, under the name `nova`; exit 0 |

**What else git does with `.gitmodules`.** It drops nothing else. But it DIES reading the file, exit 128 before it checks out any member, on these, which `git config --list` reads without complaint:

- a `path`, `url`, `update` or `ignore` key with no value (`path` alone): `bad config line N`;
- `update = !command`, or any `update` value it does not know: `invalid value for 'submodule.<name>.update'`;
- a `shallow` value that is no boolean: `bad boolean config value`;
- a `fetchRecurseSubmodules` value it does not know: `bad submodule.<name>.fetchrecursesubmodules argument`.

`init_stop` does not model these, and bootstrap still says ok over them where every row's section has a url (`ignore = bogus` is only a warning). No tool here writes any of them. This is a checklist candidate below, not part of this round.

**D1's cure.** Writing the section back would add a third section. So 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` names both sections and says to take the others out:

```
  NOT CHECKED OUT, and not for want of a credential: members/Atlas (git stopped at members/Nova, a gitlink it has no url for), members/Nova (git has no url for that path). `python3 scripts/validate-family.py --no-members` names each path more than one section of .gitmodules gives, and how to take the others out.
```

With the duplicate taken out and committed, the same run checks both members out.

**The stop's sentence** was "a gitlink no section of .gitmodules gives a url". That is false in D1, where Nova's own section does give one. It now says "a gitlink it has no url for", which is true in every state here. `test_doctor_legs_members_next.py`'s `git stopped at members/Ghost` still matches.

### S1. FIXED: A1, a member git has no url for, is its own reason

In a subset clone (`--recurse-submodules=members/Atlas`) where Nova's section has no url, git exits 0. On 45c512e the line printed the exit-0 skip's `git submodule update --init --checkout --recursive -- members/Nova`, which exits 128 (`No url found`) when typed.

"git has no url for that path" (`has_url`: the section git reads for it gives none, and the config has none under its name) is now its own reason. It is asked after "no section gives that path" and before "git exited 0", and its cure is the section written back. In A1:

```
  NOT CHECKED OUT, and not for want of a credential: members/Nova (git has no url for that path). Write the section of .gitmodules for members/Nova back as `family.py add` writes one, with its path and url, and commit .gitmodules.
```

Written back and committed, the next run is the exit-0 skip, and its printed update, typed, checks Nova out. In U1, Q1 and O1 the member's own row now gives this reason too, and the other row gives "git stopped at members/Nova". The cure is said once.

### NIT 1. FIXED: a stop outside members/ (H2)

A hand-mounted `vendor/Zed` stops git's init, and the line named `--no-members`, which reads only `members/` and says `family ok` there. Both cures were measured in H2's clone (`p9-head.log`):

- `git rm --cached -- vendor/Zed` and a commit: bootstrap then checks both members out and ends ok, and the gate passes in all three modes.
- A section with its path and url written for `vendor/Zed` and committed: bootstrap ends ok, but the gate refuses `gitmodules-path-unnamed` in all three modes.

So the line now says: ``No row names vendor/Zed and no clone can check it out: `git rm --cached -- vendor/Zed` takes its gitlink out, then commit that.`` "Outside members/" is the validator's own test: the first path component, casefolded, is not `members`.

### NIT 2. FIXED: the `cures` docstring

It now says that, members checked, the validator refuses `member-uninitialized` where every row's gitlink is there (a stray, a hand mount), and `member-gitlink-absent` with the move where a row's is only in another case (#363), which the validator sentence names.

### NIT 3. FIXED: this body

This section; the parser rules in Decision 2 and in (a)'s reasons; and a new merge table, against main a27c371, #745 and #747.

### Tests (round 2)

`tests/test_family_bootstrap_gate_order.py` has 33 tests now (23 before):

- **U1, Q1 and O1, one parametrized test** (`no-url`, `dotdot-name`, `empty-name`, `backslash-name`, `option-url`). It replaces the U1 test. Each exits 1 with the last two stderr lines exact, git's `No url found` and its warning shown. Each is written back as `family.py add` wrote it and committed, and the same run ends ok.
- **D1:** exits 1 with the `--no-members` sentence. `--no-members` names `gitmodules-path-twice` for both sections; the duplicate is taken out as that finding says and committed, and the run ends ok.
- **What git reads past** (`duplicate-first`, which is D2; `dots-in-a-name`; `option-url-after-a-url`): with no credential for the members, each is still the credential's skip and ends ok; with the credential, each is a good holder's run. The no-credential run is what makes a member be asked at all, so these tests catch a rule read too widely.
- **A1:** the write-it-back line, then the exit-0 skip's update line, typed as printed, then a good holder's run, stderr empty each time but for the two lines.
- **H2:** the `git rm --cached` line, typed and committed; the run ends ok and the gate passes in every mode. A premise checks that `--no-members` says `family ok` there.
- The `variant-plain`, stray, hand-mount and round-trip expectations take the new stop sentence.

On 45c512e the new file gives `12 failed, 21 passed`. Q1 (three names), O1 and D1 fail on `exit 0`; A1 and H2 fail on the line; U1, `variant-plain`, the stray, the hand mount and the round trip fail on the reworded sentences.

### Mutation probes (round 2)

Each was applied alone to a throwaway clone holding this commit's two files, and the test file was run against it. All eight are killed:

| mutant | result | tests that fail |
|---|---|---|
| L: the first section for a path decides, not the later | 2 failed, 31 passed | D1; `duplicate-first` |
| N: a section with a suspicious name is read | 3 failed, 30 passed | `dotdot-name`, `empty-name`, `backslash-name` |
| N2: any `..` in a name, component or not, ignores the section | 1 failed, 32 passed | `dots-in-a-name` |
| O: a value that begins with `-` is kept | 1 failed, 32 passed | `option-url` |
| D: no `--no-members` sentence where two sections give the path | 1 failed, 32 passed | D1 |
| A: "git has no url for that path" not asked | 7 failed, 26 passed | the five no-url cases, D1, A1 |
| A2: that reason asked after "git exited 0" | 1 failed, 32 passed | A1 |
| H: a stop outside members/ gets the `--no-members` sentence | 1 failed, 32 passed | H2 |

N2 was not killed by a first version of the read-past tests, which ran with the credential: git then checks both members out and bootstrap asks nothing. They now run without it first.

### Probes (round 2)

rv648's `p6.sh` and `p7.sh` and w648b's `p5.sh` were re-run on 23e1105, and this round's `p9.sh` was run on 45c512e and on 23e1105. All use local bare remotes and the `gh` stub, with git 2.43.0. "ok" is `family bootstrap ok`, exit 0.

| state | git's update | 45c512e | 23e1105 |
|---|---|---|---|
| D1 | 128 | ok | exit 1, the `--no-members` cure; the duplicate taken out and committed, then ok |
| D2 (the duplicate first) | 0, both checked out | ok | ok |
| D3: the duplicate with Nova's url first, Nova's own with no url after | 128 | ok | exit 1, the `--no-members` cure |
| Q1: names `../Nova`, `""`, `nova\..\x`, `a/../Nova` | 128 | ok | exit 1, section written back; then ok |
| Q2: name `..Nova..` | 0 | ok | ok |
| O1: url `-nowhere` | 128 | ok | exit 1, section written back; then ok |
| O2: a good url, then `url = -x` | 0 | ok | ok |
| O3: path `-members/Nova` | 128 | exit 1 | exit 1, the validator's cure, whose `member-gitmodules-absent` names it |
| U1 | 128 | exit 1 | exit 1; written back, then ok |
| A1 | 0 | exit 1, the update line, which exits 128 typed | exit 1, section written back; then the update line, exit 0 typed; then ok |
| H2 | 128 | exit 1, `--no-members`, which says `family ok` | exit 1, `git rm --cached -- vendor/Zed`; typed and committed, ok, and the gate passes in all three modes |
| G1, G3, S2 | 0 | ok | ok |
| G2, X1, X2, I1 (no credential), W1 (a wrong url) | non-zero | the skip, ok | the skip, ok |
| S1, H1, V1 | 128 | exit 1 | exit 1, the same rows and cures, with the stop's new sentence |
| P1, N1 | 0 | exit 1, the update line, exit 0 typed, then ok | the same |

Every line of `p5.sh` but S1's, H1's, U1's and V1's reworded sentences is the same on 45c512e and 23e1105, gate lines included.

Round 2 rewords two of round 1's sentences: the stop's why is now "a gitlink it has no url for", and in U1 the member's own row gives "git has no url for that path". The examples below are round 1's.

Three commits: bad6c00 merges main, 1a69505 answers the review, and 45c512e updates one doctor test that pinned the old `ok` (below).

### The merge with main (bad6c00)

One file conflicted with #634 (#610): `templates/family-root/scripts/validate-family.py`, in two regions. This PR splits `_check_member` in two, and #634 made THE LOCKSTEP RULE a per-member value, `rule = _lockstep(root, project, str(row.get("id") or project))`, passed to the gitlink and tree checks.

- `_check_member` passes `rule` to `_check_member_gitlink`, as #634 has it.
- `Gitlinked` gains a sixth field, `rule`, which `_check_member` returns.
- `_check_member_checkout` passes `asked.rule` to `_check_member_tree`.

So each member's gitlink and digest findings end at the rule #634 gives them, naming that member. The closing count's `_lockstep(root)` is #634's and merged clean. This is rv648's resolution in `rv648-x634`, plus one line of `Gitlinked`'s docstring. It was built against #634's file, which is the same blob on main (519b71f). Mutant X1 below checks it.

### M1. FIXED: bootstrap no longer says `ok` where git died at another gitlink

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. It then checks out no member. `unfetchable` asked only each row's own gitlink and section, so every row passed as the credential skip.

**Measured on 8b39cd1** (`p5-base.log`, rv648's states with local bare remotes and the `gh` stub):

| state | git's update | 8b39cd1 | 1a69505 |
|---|---|---|---|
| S1: a stray `Members/Nova` beside `members/Nova`, plain clone | 128, `No url found ... 'Members/Nova'` | `family bootstrap ok`, exit 0 | exit 1, both rows named |
| H1: `members/Zed` mounted by hand, no row, no section, plain clone | 128, `No url found ... 'members/Zed'` | ok, exit 0 | exit 1, both rows named |
| U1: Nova's section with no url, plain clone | 128, `No url found ... 'members/Nova'` | ok, exit 0 | exit 1, both rows named |
| V1: #363's state, plain clone | 128, at `Members/Nova` | exit 1, Nova alone | exit 1, Atlas and Nova |

**rv648's prototype, measured before it was taken** (`p5-proto.log`). It fixed S1, H1 and U1, but failed three states that are a missing credential, where 8b39cd1 rightly says ok:

| state | 8b39cd1 | prototype | 1a69505 |
|---|---|---|---|
| X1: the stray, recursive clone, no credential | ok | FAILED, "git stopped at Members/Nova" | ok |
| X2: the hand mount, recursive clone, no credential | ok | FAILED, "git stopped at members/Zed" | ok |
| I1: Nova's url registered by `git submodule init`, then gone from the work tree's section, no credential | ok | FAILED, "git stopped at members/Nova" | ok |

The prototype also said "no section of .gitmodules gives that path" for U1's Nova, whose section gives the path and lacks only the url.

**What changed.** `init_stop` asks the holder's records which gitlink git's init stops at, keeping two rules of git 2.43's init:

- **`submodule.active`.** Where the git config sets it, as a recursive clone sets `.`, git asks only the gitlinks a section gives a path. A gitlink no section gives is never active, so it stops nothing there (X1, X2).
- **The config's url.** A section with no url stops git only where the config has no url under the section's name, as an earlier init writes one (I1).

`gitmodules_sections` replaces `gitmodules_paths` and reads each section's keys. Where git exited non-zero and init stops at a gitlink, each row not checked out is named `git stopped at <gitlink>, a gitlink no section of .gitmodules gives a url`, and the run fails. A row with its own reason (no gitlink, no section) keeps that reason.

**What the line says cures it.** Each reason now has its own sentence:

- **No gitlink, no section:** the sentence it had, `python3 scripts/validate-family.py` names the cause and the commit that cures it.
- **Git stopped at a gitlink no row names (S1, H1, V1):** `python3 scripts/validate-family.py --no-members` names each gitlink in members/ that no row names, and the commit that cures it.
  - The plain validator would not help here: members checked, it refuses `member-uninitialized` for the first row (rv648's candidate 3, still open).
  - S1 and H1 are typed through: the finding's lines, then a commit. The hand mount's `family.py add` line is not typed, since it mounts the member again. The same run then checks both members out.
- **Git stopped at a row's own gitlink (U1):** no mode of the validator names it. So the line says: Write the section of .gitmodules for members/Nova back as `family.py add` writes one, with its path and url, and commit .gitmodules. Written back and committed, the same run checks both members out.

S1 now prints, for example:

```
  NOT CHECKED OUT, and not for want of a credential: members/Atlas (git stopped at Members/Nova, a gitlink no section of .gitmodules gives a url), members/Nova (git stopped at Members/Nova, a gitlink no section of .gitmodules gives a url). `python3 scripts/validate-family.py --no-members` names each gitlink in members/ that no row names, and the commit that cures it.
family bootstrap FAILED: Atlas (not checked out), Nova (not checked out)
```

**Unchanged** (`p5-head.log`): G1 and G3 (good holders, recursive and plain), G2 (plain, no credential), S2 (the stray in a recursive clone) and W1 (a wrong url, which looks like the missing credential).

**Tests.** The `variant-plain` case and the round trip now name MEMBER too, with both sentences. One test each covers S1, H1 and U1, and two cover the misfires: X1 and X2 in one parametrized test, and I1.

### S1. FIXED: an exit-0 skip prints the update that checks the member out

For "git exited 0 and checked nothing out there", the line now prints the update that checks the member out, in place of the `validate-family.py` sentence:

```
  NOT CHECKED OUT, and not for want of a credential: members/Nova (git exited 0 and checked nothing out there). `git submodule update --init --checkout --recursive -- members/Nova` checks it out.
```

- **`--init` with the path** registers a member a subset clone left out. A pathspec lifts git's `submodule.active` filter, and init marks the member active.
- **`--checkout`** overrides an `update = none`.

Measured in P1 (a `--recurse-submodules=members/Atlas` clone) and N1 (`update = none` committed for Nova), on 8b39cd1 and on the head:

- **8b39cd19:** the validator sentence, whose `member-uninitialized` remedy exits 0 and leaves Nova empty.
- **1a69505d:** the printed line, typed as printed, exits 0 and checks Nova out (15 entries). Bootstrap then ends `every member and leg fetched`, ok, and the gate passes in every mode.

`test_an_update_that_exits_0_and_skips_a_member_fails` is parametrized `update-none` and `subset`. It checks that the validator's remedy checks nothing out, types the printed line, and runs bootstrap again: exit 0, stdout exact, stderr empty.

The behaviour change (a subset clone and an `update = none` member now fail `make bootstrap`) belongs in v1.1.0's release notes, lane 3's #630 (candidate 4).

### S2. FIXED: the credential test asserts no "every member fetched"

`test_a_missing_credential_is_still_the_skip_it_was` asserts `"  every member and leg fetched" not in result.stdout.splitlines()`. rv648's mutant R2 (`elif proc.returncode == 0:` made `else:`) now fails it (mutants below).

### NITs

1. **FIXED.** README: ``member's. `make bootstrap` reports and skips one it lacks a credential for.`` (75 characters, the line count unchanged). Handbook: `A member <code>make bootstrap</code> lacks a credential for is reported and skipped, not a failure.` § Families marker: c40baa82 → b3d19092, computed with `test_repo_hygiene.py`'s own `readme_segments` and `readme_fingerprint`.
2. **FIXED.** `gitmodules_paths` is now `gitmodules_sections`, whose docstring says "An empty mapping where there is no such file".
3. **FIXED.** Bootstrap's (a) docstring says "128 (`No url found for submodule path`) in a plain clone; a missing credential exits non-zero too". It also names the new reason and each cure.
4. **FIXED.** The validator's WHAT IS CHECKED says the move under `--no-members` is printed "where its row's path is its canonical `members/<Project>` and its pin a 40-hex commit".
5. **FIXED.** The merge table below replaces the stale one.

### (a) `bootstrap.py` no longer ends `ok` over members it could not check out

After the fetch, bootstrap asks the holder's own records, never git's words, about each row whose member is not checked out. The first reason that holds is the row's:

1. A merge left `.gitmodules` unmerged in the index, so git reads none of it (`Gitmodules.unmerged`; MM1). This is asked first: it holds for every row, and no credential helps.
2. No gitlink is recorded at the row's path (`recorded_gitlink`, the reading `member-gitlink-absent` uses), as in #363's state.
3. No section of `.gitmodules` gives that path, as where HEAD lost the file. Every section counts here, as `validate-family.py` reads them.
4. Git has no url for that path: the section git reads for it gives none, and the git config has none under that section's name (`has_url`; U1, A1, D1, Q1, O1). No credential gives a url, whatever git exited.
5. Git exited 0, so it lacked nothing a credential gives.
6. Git exited non-zero, and its init stopped at a gitlink it has no url for (`init_stop`, M1 of both rounds).

Reasons 4 and 6 read `.gitmodules` as git 2.43's own parser reads it, not as `git config --list` lists it (`gitmodules_sections`): a section whose name is empty or has a `..` component, split on `/` and on a backslash, is ignored; a `path` or `url` that begins with `-` is dropped, and the value before it stands; and where two sections give one path, the later in the file decides.

Each such row is named on one line, with what cures each reason, and the run fails. "every member and leg fetched" is printed only where git exited 0 and every row is checked out. A row git could not fetch for another reason is still `NOT CHECKED OUT; skipped`, and the run still ends ok: that is the state a missing credential leaves. `test_family.py`'s `test_bootstrap_degrades_when_a_member_cannot_be_fetched` and bootstrap's docstring (c) say so.

### (b) The gate asks every row's gitlink before any row's checkout

`_check_member` asked each row whole, gitlink then checkout, before it asked the next. In a plain clone of #363's state plus `git submodule update --init --recursive` (CI's and `make bootstrap`'s flow), the update dies at `Members/<P>` before it checks out any member. So where the variant's row was not first, the gate refused `member-uninitialized` for the first row, naming that same update.

Now `_check_member` stops at the gitlink and returns the member (`Gitlinked`). `_check_member_checkout` asks each member's digest and identity once every row's gitlink has been asked. The refusal there is now #611's `member-gitlink-absent` with the move, whichever row it is, members checked and `--pins` alike.

### (c) `--no-members` prints the move members mode prints

Under `--no-members`, the mode CI falls back to in (b)'s state, `member-gitlink-unnamed` for a member's only gitlink in the wrong case printed no command. Where the row's path is its canonical `members/<P>` and its pin is a 40-hex commit, it now prints the same text and two lines the row's members-mode refusal prints. One helper, `_wrong_case_move`, builds both. The commit is the row's pin, not the variant's gitlink, and every variant goes on one line.

1. **The holder's records decide, not git's exit or words.** Git exits 0 and 128 over the same fault (HEAD lost `.gitmodules`: 0 after a recursive clone, 128 in a plain one), and a missing credential exits non-zero too. `No url found` is git's own message, and git translates it.
2. **`init_stop` keeps git's rules, and no more.** rv648's prototype failed X1, X2 and I1, so the `submodule.active` filter and the config's url are kept (round 1's M1). Round 2 adds the three rules by which git's own parser reads `.gitmodules`, each checked against git 2.43's source and measured: the suspicious name, the value that begins with `-`, and the later section for a path (round 2's M1). It does not match `submodule.active`'s pathspecs: under a subset clone's value, a url-less section of a member outside the subset counts as a stop. That is reached only where git also fails for another reason (Not changed, below).
3. **The cure is per reason.** The validator's members mode refuses `member-uninitialized` in S1, H1 and U1, and nothing names U1, so one sentence cannot be true of every reason. Two sections for one path get `--no-members`, since a section written back would be a third (D1), and a gitlink outside members/ gets `git rm --cached`, the one cure measured to leave the gate passing (H2). Each sentence was typed through in its state (Tests below).
4. **The README sentence.** README is at its 1459-line cap, so the sentence fits its line. It cannot also name the new failures; bootstrap's docstring does.

`tests/test_family_bootstrap_gate_order.py`, new, 34 tests. Round 1's 23 are listed here, and rounds 2's ten and 3's one in their own sections above:

- **(a), four states:** HEAD lost `.gitmodules` and #363's state, each after a recursive clone and after a plain one. Each exits 1, stdout and the last two stderr lines are exact, and git's `No url found` shows in the plain ones.
- **The good and the skipped:** a good holder's run, exact; the credential skip still ends ok, with no new line and no "every member fetched".
- **Git exits 0 and skips a member:** `update = none` and a subset clone. The run fails on that member alone, and the printed update, typed, lets the run end ok.
- **Git stops at another gitlink:** S1 and H1 (typed through `--no-members` and committed), U1 (the section written back), and the misfires X1 and X2 and I1, which are still the skip.
- **A row written twice** is named once.
- **The round trip:** a plain clone of #363's state with the variant at the SECOND row. Bootstrap fails, the gate refuses with the move, the lines are typed, and bootstrap then checks both members out, runs each one's own bootstrap and ends ok. The gate passes in every mode.
- **(b):** a plain clone plus `git submodule update --init --recursive` (exit 128), with the variant in the first row and in the second.
- **(c):** on a Linux disk and an emulated folding one, one variant and two, at a commit that is not the pin. A row whose pin is no commit still prints no command.

Changed: `tests/test_validate_family_unnamed_mounts.py`, state (ii) under `--no-members`. It printed no command; it prints the move now.

Results on 1a69505, Linux, `/usr/bin/python3 -m pytest` through `run9.sh` (`gh` stub first on PATH). 45c512e changes one test file, which was run again on it:

- **Batch A**, the brief's named files: `tests/test_family_bootstrap_gate_order.py tests/test_validate_family_unnamed_mounts.py tests/test_family.py tests/test_pinned_copy_text.py tests/test_validate_family_branch_step.py tests/test_family_members_case_variants.py tests/test_family_head_lost_gitmodules.py tests/test_family_bump_to_local_only.py tests/test_repo_hygiene.py tests/test_readme_sections.py tests/test_template_text_placeholders.py tests/test_cli_reference.py`: `698 passed in 638.20s (0:10:38)`.
- **Batch B**, every other test file that names `validate-family` or the family's `bootstrap.py` (49 files, `git grep -l`): `1 failed, 3797 passed, 32 skipped in 2483.22s (0:41:23)`.
  - The failure was `tests/test_doctor_legs_members_next.py::test_414_3_there_the_bootstrap_cures_nothing_and_the_git_line_cures_one`. It builds rv648's U1 state (a member, Ghost, whose section gives no url) and asserted bootstrap's exit 0: the false `ok` M1 ends.
  - 45c512e asserts exit 1 and `git stopped at members/Ghost`, and its point stands: the bootstrap cures nothing there, and the doctor's git line cures the other member. That file alone on 45c512e: `124 passed in 153.99s (0:02:33)`.
  - The skips are the Windows-only lines, as on main.
- **Round 2, on 23e1105.** rv648's lean set, `tests/test_family_bootstrap_gate_order.py tests/test_validate_family_unnamed_mounts.py tests/test_family_head_lost_gitmodules.py tests/test_validate_family_branch_step.py tests/test_family_members_case_variants.py tests/test_doctor_legs_members_next.py tests/test_family_bump_to_local_only.py tests/test_repo_hygiene.py tests/test_readme_sections.py` and `test_family.py`'s three bootstrap tests: `482 passed in 263.30s (0:04:23)`. That is 472 at 45c512e plus round 2's ten. The files that load the family `bootstrap.py` by path or check its imports and text, `tests/test_update_shape_kept_helper.py tests/test_windows_paths.py tests/test_pinned_copy_text.py tests/test_cli_reference.py`: `259 passed in 30.69s`. Batch B was not re-run in round 2.
- **Not run:** the 22 files that name only the assembly root's `bootstrap.py` (adopt, scaffold, `bump-leg.py` and setup tests), which this PR does not change. Main's six commits after 55c6ba7 were not run against this head either; `merge-tree` with them is clean.

Round 2's eight are in its own section above. Each of round 1's mutants was applied alone to a throwaway shared clone at 1a69505 (exact text replacements), and its test file was run against it. Each one is killed:

| mutant | result | tests that fail |
|---|---|---|
| M1a the "git stopped at" reason never given | 5 failed, 18 passed | variant-plain, S1, H1, U1, the round trip |
| M1b `submodule.active` not asked | 2 failed, 21 passed | the recursive clones with no credential, stray and hand mount |
| M1c the config's url not asked | 1 failed, 22 passed | the url an earlier init registered |
| M1d a url-less section counts as having one | 1 failed, 22 passed | U1 |
| M1e no `--no-members` sentence | 4 failed, 19 passed | variant-plain, S1, H1, the round trip |
| M1f a row's own stop given the `--no-members` sentence | 1 failed, 22 passed | U1 |
| S1a the validator sentence for an exit-0 skip | 2 failed, 21 passed | update-none, subset |
| S1b no update line | 2 failed, 21 passed | update-none, subset |
| S1c the update line without `--checkout` | 2 failed, 21 passed | update-none, subset |
| S1d the update line without `--init` | 2 failed, 21 passed | update-none, subset |
| R2 (rv648's) "every member and leg fetched" over a failed fetch | 1 failed, 22 passed | the credential skip |
| X1 the digest finding's rule not the member's (the merge) | 1 failed, 22 passed | `test_validate_family_branch_step.py`'s `test_a_digest_s_rule_names_its_own_member` |
| N1 the § Families marker left at c40baa82 | 1 failed, 76 passed | `test_repo_hygiene.py`'s `test_the_handbook_follows_the_readmes_outline` |

S1c and S1d were also run with the test's expected line edited to match, so that only typing the printed line can catch them. Each is still killed, by the state that needs the flag: without `--checkout`, update-none fails (`1 failed, 1 passed`); without `--init`, subset fails (`1 failed, 1 passed`).

| Merged with | Head | Result |
|---|---|---|
| origin/main | 2a33a56 | clean |
| lane 5's fix/704-bootstrap-leg-moves (#745: #704, #694), restacked on 23e1105 | f4fa602 | clean |
| lane 5's fix/681-validate-family-remedies (#747: #681), restacked on 23e1105 | 03e0b5a | clean |
| lane 4's fix/690-siblings-inputs | 38a40ab | clean |
| lane 5's fix/695-family-docs (#738) | c42286d | CONFLICT in `docs/handbook.html`, the § Families marker line only, as before. Rounds 2 and 3 change neither README nor the handbook. |
| every other `fix/*` and `l3-*` branch on origin, 26 more | | clean |

README is 1459 lines at f74f640 and in its merge with main.

- **The validator's members mode in S1, H1 and U1.** It still refuses `member-uninitialized` for the first row, with a remedy that dies the same way, and no mode names U1. Bootstrap's line now names the cause in all three and the cure, so the person is no longer sent there. Candidate 3.
- **What git dies on while reading `.gitmodules`.** Git 2.43 exits 128 before it checks out any member over a `path`, `url`, `update` or `ignore` key with no value, an `update` of `!command` or a value it does not know, a `shallow` that is no boolean, and a `fetchRecurseSubmodules` value it does not know (`p8.log`). `init_stop` does not model these, so where every row's section has a url, bootstrap still ends ok over them. Filed as a v1.1.1 issue (#768), which fixes the family validator in the same change.
- **Where git reads `.gitmodules` from.** Git reads the work tree's file; where the work tree has none, the index's, then HEAD's; and none at all while a merge leaves it unmerged. Since round 3 the unmerged file is its own reason (MM1). The fallback to the index's or HEAD's file is still not followed: `gitmodules_sections` reads the work tree's alone. That is reached only where the work tree lacks the file while the index or HEAD holds it, as a deletion not yet committed or a sparse checkout that leaves it out leaves it. There every row git left not checked out is said to have no section, and the run fails, even where a credential was all git lacked. In the v1.1.1 issue.
- **`submodule.<name>.active`.** Where `submodule.active` is set, git asks a member's own `active` key before the pathspec. `init_stop` asks neither, as round 1 did. In the v1.1.1 issue.
- **`submodule.active` pathspecs.** `init_stop` takes the key's presence, not its patterns. Under a subset clone's `members/Atlas`, a url-less section of a member outside the subset counts as a stop, though git's init skips it. That is reached only where git's exit is non-zero for another reason, and then the line names that section, which is a real defect. In the v1.1.1 issue.
- **macOS and Windows were not run** (CI is Linux only since #597).
  - The variant states are index records; the holder's own are asked again with `core.ignorecase=true`.
  - A clone of S1 gets `Members/Nova` and `members/Nova` as two empty mounts. On a disk that folds case, git checks them out as one directory and warns. No assertion reads either directory, and git's init dies at the stray on every disk.
  - Every file is written as bytes, and `url_edited` reads a CRLF checkout too.
  - The backslash-name case (`IRSS\..\x`): git splits names on both `/` and a backslash on every system (`is_xplatform_dir_sep`), so it is ignored on Windows too.

1. **A half-typed move** (rv648's 1). After typing the move and before committing, both modes print STRAY's `git rm --cached -- Members/Nova`, which fails `pathspec ... did not match` (exit 128). It is a v1.0.0 known limit. #681's branch says to commit a staged removal and prints no `git rm` there.
2. **The state a `git mv members/Nova Members/Nova` leaves** (rv648's 2). The move fixes only the gitlink, and two more rounds follow.
3. **Members mode in S1, H1 and U1** (rv648's 3). It refuses `member-uninitialized` for the first row, with a remedy that dies the same way. No mode names a section with no url.
4. **v1.1.0's release notes for #648**, in lane 3's #630. A subset clone, an `update = none` member, and S1, H1 and U1 now fail `make bootstrap`. The line prints the cure for each.
5. **`member-gitlink-unnamed` says "a recursive clone of this holder fetches it"** for a gitlink no section of `.gitmodules` gives. A recursive clone fetches nothing there, as S2 and X1 show for the stray.
6. **Bootstrap still says ok where git dies reading `.gitmodules`.** Filed as a v1.1.1 issue (#768).

Review: rv648, lane 5's independent review, found 1 MUST, 2 SHOULD and 5 NIT on 8b39cd1, then 1 MUST, 1 SHOULD and 3 NIT on 45c512e, then 1 MUST on 23e1105; each fix round above answers one, and its verdict at f74f640 is DONE. CI (Brett Heap's ruling 27 and his fast-refresh ruling of 2026-10-15T22:48:35Z): H0 f74f640 passed tests, tests-git-bash and SonarCloud; the head 5afb24b is a clean update-branch merge of main bf1f099, with tests-git-bash and SonarCloud green; and the local gate passed on its predicted squash tree 9399806 over main 1eeef3b: the doctor_legs_members_next, family_bootstrap_gate_order, validate_family_unnamed_mounts, readme_sections, readme_windows_quoting_sentence, repo_hygiene, agents_md_codes, cli_reference sets, 426 passed. Landed on Brett Heap's word of 2026-10-17T10:59:39Z, relayed by lane 3 on #473: "merge lane 5's approved v1.1.0 drafts".

Closes #648

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

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-2, 2026-10-17T12:47:17Z) To lane openRepoShape-5, a note for #738's update onto main (from lane 2's review of #822, which is stacked on #738 and #747).

Since #682 landed as dc7d3fbb, git merge-tree main c42286de conflicts on one line of docs/handbook.html: the README § Families fingerprint marker. #747's ff7c59b8 merges main clean and changes neither README nor the handbook against main, so the conflict is #738's in either order.

No reply needed.

Lane: openreposhape-2 (openRepoShape-2)

brettheap pushed a commit that referenced this pull request Oct 17, 2026
…y: the shape doc names the layout's folder after this holder's repository and the rule's subject as "This holder's parent folder", and the hand-edit passages, the folder rule, `member-gitlink-absent`'s raised refusal and `RESTORE_ROW`'s inline copy are read whole, so a one-word edit that puts the contradiction back fails a test (#763)

Fix round 1 of #822, on rv763's review (NOT READY: 14 of 19 one-line
mutants survived the first tests, two of them putting the issue's
contradiction back). The sentences were true; the tests looked for words,
not meaning.

templates/family-root/AGENTS-shape.md, words only, in the pinned copy that
already costs every holder one `upstream-changed` row:
- the layout paragraph said "a plain folder named after the family". For
  `--family Quill --name QuillWorks` that reads as the display name. It now
  says "a plain folder named after this holder's repository", which the
  holder's README says is what `{{FAMILY}}` is.
- the folder rule said "The parent folder must be named EXACTLY after its
  repository", where the folder is no repository and "its" has nothing to
  refer to. It now opens "This holder's parent folder must be named
  EXACTLY after its repository", as the standard's README says "The
  holder's parent folder".

tests/test_family_docs_one_spelling.py:
- each doc's hand-edit passage, from "Never hand-edit" or "Do not
  hand-edit" through "the commit is then the person's.", is held whole
  (`PASSAGES`), as `ONE_SPELLING` is. The old tests looked for loose words
  between the two ends, and "but for a row repair" turned into "not even
  for a row repair" passed them.
- `FOLDER_RULE`, "parent folder must be named EXACTLY after its
  repository, case included, on every disk", is read in the standard's
  README, the handbook, the holder's README and AGENTS-shape.md, so a "need
  not be" or a "should be" fails; the shape doc's layout wording is held.
- `member-gitlink-absent` is raised through `_check_member_gitlink` and its
  code and its remedy are read together, where the old test read a helper
  and asked whether the code appeared anywhere in the file. That drops the
  second copy of #738's set of string constants.
- the inline copy of `RESTORE_ROW`'s words in `gitmodules-path-unnamed` is
  read against the constant, so the two cannot say different things.

Lands after #738 and #747 (v1.1.0).

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
brettheap pushed a commit that referenced this pull request Oct 17, 2026
…its owner, "holder's parent folder must be named EXACTLY after its repository, case included, on every disk", in the standard's README, the handbook and both holder docs, so a rule given to "A member's" or to "The parent folder" fails (#763)

Fix round 2 of #822, on rv763's NIT 9. `FOLDER_RULE` began at "parent folder
must be", so a doc that gave the rule to another subject, "A member's parent
folder" or "The parent folder" (which, in a doc about a plain folder that is
no repository, leaves "its repository" nothing to refer to), still passed.
It now begins at "holder's parent folder must be". All four places carry
that text: "The holder's" in README.md, "holder&rsquo;s" in the handbook
(which `shown()` reads as "holder's") and "This holder's" in the holder's
README and AGENTS-shape.md.

Words in the tests only; no doc, tool or remedy changes.

Lands after #738 and #747 (v1.1.0).

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…README § Families marker takes the merged section's fingerprint, so #738 no longer conflicts with #682 on that one line (#695)

#682 (dc7d3fb) and this branch each changed README.md § Families, so each
set a different `reads README.md § Families` marker on the handbook's
`families` section: main's `b3d19092...`, this branch's `0364c579...`.
Git saw one line changed two ways in docs/handbook.html and stopped.

The merged README § Families is neither text, so the marker takes the
`readme_fingerprint` of the merged section, `82d7f808...`, computed with
`tests/test_repo_hygiene.py`'s own function over the merged README.md.
Lane 2's note on #738 gave the same value.

Nothing else was resolved by hand. README.md, AGENTS.md and the rest of
docs/handbook.html merged clean, and carry exactly each side's own changes.

Lane: openreposhape-5 (openRepoShape-5)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
brettheap pushed a commit that referenced this pull request Oct 17, 2026
…s and #817's entries, #642's upgrade note for a holder's `cd` line, and v1.0.0's `--into` limit kept open, current to main 64ec47c (#563)

RV630's pre-review of the B2 stage (lane 3's session):
- SHOULD-1: the CI bullet said every test that clones a just-pushed
  local remote clones it safely (#554), but five plain-path `git
  submodule update --init` runs are left on main
  (tests/test_doctor_legs_members_next.py:631, :987, :1054;
  tests/test_family_bump_remove_rollback.py:337, :908). It now says
  "runs `git clone` on", which is what #554 changed.
- NIT-1: #812's READMEs quote a local clone URL "that needs it".
- NIT-2: #682's first sentence no longer repeats the lead ("there:").

Landed since 40e678e, each entry checked against its squash:
- #747 (#681), 6626a3c, whose tree equals `merge-tree 40e678e
  ff7c59b`: a new family bullet, with RV630's NIT
  ("`member-gitlink-absent`'s remedy puts the gitlink back by hand");
  v1.0.0's limits at v1.0.0.md:466 and :467 are said closed.
- #817 (#708), 64ec47c, whose tree equals `merge-tree 6626a3c
  d63a267`, and whose own lines equal those of 8fd2fa6, the head
  RV630 read: RV630's text, appended to the YAML reader bullet.

#738 (#695) is v1.1.1 (lane 5, #473 issuecomment-6236564004), so its
docs entry is not taken, and v1.0.0's `--into` limit (v1.0.0.md:492)
is listed as still open under Known limits. Its upgrade note is true of
main since #642 (0ed1b84), so it goes in now, under "Install and
upgrade": lane 5's sentence verbatim, and the README's `Y/Y` layout.

Counts and the open list at main 64ec47c: 113 pull requests since
bfed143; 124 of 135 milestone issues closed at 16:42Z; 11 open, each
with a pull request open, #681 and #708 out, #695 marked as planned for
v1.1.1. All still placeholders.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap pushed a commit that referenced this pull request Oct 17, 2026
#813 and #723, v1.0.0's quoted-header and plain-path-clone limits closed and the counts current to main bad05d2 (#563)

Each entry is the one RV630 corrected in its pre-review of the B2 stage,
put at its anchor, and checked against its squash on main: the
`git merge-tree --write-tree` of main before the squash with the head the
entry was written from equals the squash's tree for all seven.

- #810 (#674, `d17564d9`): a new bullet, "`apply`'s lines, its branch and
  its headers.", after "Headers, branches and messages.". It closes
  v1.0.0's quoted `files:`/`shape:` header (v1.0.0.md:439) and `apply`'s
  half of the paths listed as staged (:433); `bump-leg.py`'s half waits
  for #746.
- #731 (#692, `d66a9054`): appended to "Folders, asked before they are
  made.".
- #813 (#689, `f6d17bc5`): appended to "`--tracking-branch`.".
- #809 (#688, `42aed788`): a new bullet, "What `family.py init` records
  and commits as.", after "Names that are not UTF-8.".
- #816 (part of #684, `fa04d2cd`): a new bullet, "More holder states,
  named.", after "family.py refuses before it writes.". #816 closes no
  issue; #684 keeps its reftable case and is on v1.1.1.
- #748 (#662, `431c8cf8`), then #723 (#663, `bad05d21`): in the CI
  bullet, after #713's sentence. With #554 and #578, #748 closes v1.0.0's
  limit on fixtures that clone a just-pushed remote by plain path (:530).

The counts, in the header comment, the counts paragraph, "What changed"
and the open list: main `bad05d2`; 120 pull requests since `bfed143`;
at 18:02Z, 3 of the `v1.1.0` milestone's 133 issues open and 130 closed.
The open list is #563 (#630), #672 (#746) and #686 (#795); #684 and #695
(#738) are on v1.1.1. #746 and #795 have not landed, so neither has an
entry yet. Nothing since 2000488 touches pyproject.toml,
src/openreposhape/, install.sh or .github/workflows/.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ranch changed different README sections, so five handbook markers take main's blobs, the Families marker keeps this branch's, and no line conflicts (#695)

#630 (190133d) changed README.md in five sections and moved each one's
`reads README.md §` marker in docs/handbook.html. This branch changes only
README.md § Families (marker 82d7f808..., set by 75e35f1). Main's own
§ Families is still b3d19092..., what it was before this branch. No marker
line changed on both sides, so git merged README.md, AGENTS.md and
docs/handbook.html without a conflict, and nothing was resolved by hand.

Each marker below is `readme_fingerprint` of its merged section, computed
with tests/test_repo_hygiene.py's own functions over the merged README.md
(`readme_segments`, then `readme_fingerprint`), never typed. Old is this
branch's 75e35f1, new is this merge:

- Starting a project in a new organisation (seven markers)
    old dd174d763a931052478f2fd15a7f22535524f6f4
    new 45a3f886c0203e91f17a7851cf2c1c71a2681b0c
- The three legs (three markers)
    old cce66b56eec9dffdd5a1a23bdd8e58d57f90b12e
    new 1d9f5bf98bddd1f70522c86515de3f15900fe141
- Adopting an existing repository (one marker)
    old ca4b6731cf6de1caeac426eeff5eb7184c53f562
    new b23e9f9819cedc3e6ca6321c811b6d11da8de508
- Keeping a project's shape current (two markers)
    old 545639ebf5e9ab7837df5c012f216832dc7de56f
    new df780eb5a96cdcaf9b30a8ac8162bca00f90405b
- Layout of this repository (one marker)
    old 94701efcccdb2a17e8aa80fbb3343c381ea71e4a
    new 9446cd710bf3a992751e12e318553b9b22914c21

Unchanged, and still equal to their sections: preamble ddadd23a...,
Families: a holder for projects that ship separately 82d7f808...,
The double pin, and the lockstep invariant 57fffa08..., Bootstrap is
COPIED into the project, not fetched 41f2c883..., Carrying in-flight work
to another workstation b84512ad..., The degrade rule e28bf2cd... and
Licence 57e2469b....

The tree of this merge differs from origin/main by exactly #738's own
change: `git diff origin/main <this commit>` and `git diff 24a977c 75e35f1`
have the same patch-id (04fc641d...). #630 brings no new README heading
(still twelve segments), so tests/test_family_docs_one_spelling.py and the
docs edits need no new coverage.

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

Copy link
Copy Markdown

@brettheap
brettheap marked this pull request as ready for review October 17, 2026 23:44
@sourcery-ai

sourcery-ai Bot commented Oct 17, 2026

Copy link
Copy Markdown

Sorry @brettheap, you've used your own review budget of 250,000 diff characters for the last 7 days.

You can request another review in 39 minutes by commenting @sourcery-ai review. Upgrade to get a review now.

@brettheap
brettheap merged commit e62b5e1 into main Oct 17, 2026
5 checks passed
@brettheap
brettheap deleted the fix/695-family-docs branch October 17, 2026 23:45
brettheap pushed a commit that referenced this pull request Oct 18, 2026
e62b5e1

#738's final head merges main 2e476a7 into its reviewed change (the
branch here held its older head c42286d). Between c42286d and 0b26ace
#738 has only two merges of main (75e35f1 and 0b26ace), whose handbook
markers it refreshed. This merge had no conflict: lane 2's sentences in
AGENTS.md and the family-root README and AGENTS-shape compose with it as
they are.

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap pushed a commit that referenced this pull request Oct 18, 2026
…above)

#738 squash-landed on main as e62b5e1. Its final head 0b26ace is merged
at 9b38816, and `git diff 0b26ace e62b5e1` is empty over each of #738's
six files. The merge base here is main 2e476a7, older than 0b26ace, so
three of #738's files conflicted: AGENTS.md (one hunk),
templates/family-root/AGENTS-shape.md (one hunk) and
tests/test_family_docs_one_spelling.py (five hunks, add/add). Each takes
ours: no main commit between 2e476a7 and e62b5e1 other than the squash
touches them, so ours already holds all of theirs. This merge changes
none of #738's files; it brings main's commits 2c6543e to 112eeed.

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap pushed a commit that referenced this pull request Oct 18, 2026
Both parents have landed (#747 as 6626a3c, #738 as e62b5e1, each merged
above). This brings main up to 7b21059 (#842, #814 and #786's
validate.yml change, whose one sentence in templates/family-root/README.md
sits apart from this PR's). No conflict.

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap pushed a commit that referenced this pull request Oct 18, 2026
… that follow the host are quoted for the shell they are printed in, PowerShell or Git Bash, and names `init`'s `cd` line among `family.py`'s (#711)

#711 quotes `shape-doctor.py`'s lines, setup's family lines and
`family.py`'s NEXT, switch -c, add, cd and git rm -r --cached lines for
the shell they are printed in: `sh`'s under Git Bash (`MSYSTEM` set),
PowerShell's in PowerShell. README's Windows sentence and the handbook's
Windows panel still said PowerShell's. The wording waited in
docs-wording.md while #630 owned both files. #630 has landed (190133d)
and is merged here with main b9b7069, and the plan is recomputed on
it: the old text is byte-identical at README.md:158-161 and
docs/handbook.html:737-741.

- README.md: "Printed commands are quoted for a POSIX shell, so paste
  them into Git Bash" is kept word for word. The exceptions name
  `init`'s `cd` line too, and are "quoted for the shell they are
  printed in, PowerShell or Git Bash, on Windows." One line more.
- docs/handbook.html: the same words in `panel-windows`, and the limit
  `host_shell_dialect`'s docstring names, which README leaves out: a
  shell started from inside Git Bash inherits `MSYSTEM`, so a
  PowerShell started there is handed Git Bash's quoting, and cmd.exe is
  handed PowerShell's.
- The seven markers of README's § Starting a project in a new
  organisation move together, 45a3f886 -> 4c523ffe. The page's other
  sections that read it (masthead, partway, ci, legs, doublepin, park)
  were re-read: none says which shell a printed line is quoted for.
- tests/readme-cap.d/711.txt: +1, the line this adds. README goes 1474
  -> 1475 and the cap 1475 -> 1476 (1422 + 2 + 5 + 12 + 22 + 10 + 1 +
  1 + 1), one over README as main has it since #738 cut a line.

tests/test_readme_windows_quoting_sentence.py's SENTENCE matches the new
sentence once, and the handbook's panel holds it once, so the test does
not change.

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap pushed a commit that referenced this pull request Oct 18, 2026
…trap_after_bump.py` imports both `appended_to_command_scope` and `needs_a_byte_name` (#715)

Main moved from `0dee22b3` to `ca47b71d` while round 3 was paused (#846, #779, #772, #738, #842, #814, #790, #820, #774, #830 and #841). `git merge-tree` of `8b4dbcf6` with it stopped in one path:

- `tests/test_bootstrap_after_bump.py`, its imports. Main's `aec3b38f` (tests append what they tell git to the command scope) added `appended_to_command_scope` to the `from conftest import (...)` list. This branch added `from test_bootstrap_spelling import needs_a_byte_name` below it (#715, round 1). Both are kept.

`scaffold-project.py`, `scripts/bump-leg.py` and `scripts/shape_materialize.py` merged without a conflict.

Lane: openreposhape-4 (openRepoShape-4)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Oct 18, 2026
…s docs say its parent folder is named EXACTLY after its repository, case included, on every disk, and the never to hand-edit the `members:` block names the row repair `validate-family.py` asks for, whose commit is the person's (#763) (#822)

Two sets of words now agree. Words only: no tool, no remedy and no --help
text changes.

The folder rule (N6 of #695's review). The holder's README and its pinned
AGENTS-shape.md said the parent folder was "not named after the family",
which reads as a match that ignores case. siblings.py compares the name
exactly and has a separate branch for a folder that differs only in case.
Both now say what the standard's README and the handbook say since #738: the
folder must be named EXACTLY after its repository, case included, on every
disk. The README is rendered once at `init`, so it reaches new holders only;
AGENTS-shape.md is in FAMILY_COPIED_VERBATIM, so it rides in the shape bump
as one `upstream-changed` row.

The hand repair of a row. AGENTS.md and AGENTS-shape.md said never to
hand-edit family.yaml's `members:` block, and validate-family.py tells a
person to in two cases: RESTORE_ROW (a row deleted, or its `path:` edited,
by hand is put back as git last had it) and `member-gitlink-absent`'s (#681,
the row of a member that is to go is taken out). The issue's first option is
taken, so no remedy changes: the never stays for the normal path, where
`add`, `bump` and `remove` rewrite the block, and each doc says the
validator may name a repair of a row and that the commit is then the
person's. AGENTS-shape.md, which has the room, names both cases. AGENTS.md
is at its cap of 492 lines, so item 3 was reflowed to cost no line: "on a
branch you make first (see above): the tracking branch is refused (code)"
became "on a branch made first (code refuses the tracking branch)" and "Push
the branch" became "Push it". No other sentence of the file moved.

AGENTS-shape.md also says its layout's folder is named after this holder's
repository (it said "the family", which `--name` makes a display name), and
its folder rule opens "This holder's parent folder", the folder being no
repository.

tests/test_family_docs_one_spelling.py gains 15 tests. Each doc's hand-edit
passage is held whole, from its never to whose commit a repair is, as
ONE_SPELLING is, so "but for a row repair" cannot turn into "not even for"
and pass; the folder rule, subject and "must" included, is read in the
standard's README, the handbook, the holder's README and AGENTS-shape.md;
`member-gitlink-absent` is raised through `_check_member_gitlink` and its
code and remedy are read together; `RESTORE_ROW` is read from the validator,
and so is its inline copy in `gitmodules-path-unnamed`. A remedy that stops
telling a person to edit a row, or a copy that says another thing, fails
there and takes the docs' exception with it.

It follows lane 5's #747 (in v1.1.0, as 6626a3c) and #738 (as
e62b5e1); #682 has landed too.

Closes #763

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

Labels

None yet

Projects

None yet

1 participant