Skip to content

siblings.py and the family-root README: make siblings, park and resume print the family's name and every member row spelled and never end in a traceback on name: "a\ud83db" or a dash under PYTHONIOENCODING=ascii, and the holder's README names its folder after the repository, so --family Quill --name QuillWorks lays out as its README says (#454, #629) - #642

Merged
brettheap merged 1 commit into
mainfrom
fix/454-629-siblings-family-name
Oct 16, 2026

Conversation

@brettheap

@brettheap brettheap commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Lane: openreposhape-4 (openRepoShape-4)

Claim: #454 (comment) and #629 (comment)

What changed and why

Two issues, one writer, because both rewrite the header lines of templates/family-root/scripts/siblings.py.

#454: make siblings, park and resume print what a manifest says and are never ended by it. The two reports opened with a plain print of family.yaml's name: (_run_placement, dispatch_all) and every member row was printed raw. On main 8f453d0e, name: "a\ud83db" (a lone surrogate) ended siblings.py --dry-run in UnicodeEncodeError (exit 1), make siblings, make park and make resume in exit 2, and make park parked nothing; the line holds a literal dash, so under PYTHONIOENCODING=ascii it raised for every name, InkRouter included; a raw ESC or CR reached the terminal as the control it is. All of it reproduced first, in a real holder from family.py init into local remotes.

The house pattern is #391's (ec4a7a0e), done in the same words: escape_unencodable_stdout() is the first statement of main, and spelled() goes round every value a line prints that a manifest, a member row or a disk gave. Both are #391's, in scripts/repo_shape.py; siblings.py adds them to the from repo_shape import line it already has and defines neither. Spelled: the name and root (both headers), the folder and the link, a member's project, path, url and the reason its tracking branch is not known, Sibling.row's project and branch, the FINDING lines' project, directory, origin and --make-args, and the member's own [project] and --- lines. The member's own make is still handed the RAW target, the mv lines a person pastes stay quoted and unspelled, and git's own words are left as git wrote them. A plain name prints exactly what it printed before (a golden of both runs' whole output, which passes on the base too).

#629: the holder's README names its folder after the repository. family.py init --family Quill --name QuillWorks wrote a README that said cd QuillWorks and laid out QuillWorks/QuillWorks, while family_folder_name asks for the repository's name, Quill: a holder laid out as its README said got WARNING the holder's parent folder is 'QuillWorks', not 'Quill', cloned nothing and make exited 2.

Pinned shape copies. templates/family-root/scripts/siblings.py is in FAMILY_COPIED_VERBATIM and digest-pinned in every holder, so a holder that re-syncs reads upstream-changed for it. No digest or blob of it is recorded in this repository (#578 changed the file with no pin edit, as #391 did for bootstrap.py). README.md of the family root is templated, not pinned. scripts/repo_shape.py is not changed. A holder that keeps its own repo_shape.py without the two names siblings.py now imports is refused by apply as update-accept-local-missing-name (#536), which already counts siblings.py among the importers. No --help text moved, so docs/cli.md is unchanged; AGENTS.md, README.md and the handbook say nothing this makes false.

Tests

New file tests/test_siblings_family_name.py, 150 tests, on the harness of test_bootstrap_spelling.py (stdouts answered in bytes and decoded strictly in the encoding given; imports its helpers and family_template) and test_family.py (a real holder with a scaffolded member, local bare remotes, no gh):

Against main 8f453d0e (the new file with the unfixed templates): 85 failed, 65 passed; the 65 are the byte-identity, plain-golden and fixture-sanity rows that must pass there too.

Targeted run (log /tmp/claude-1000/-workspace-projects-openRepoShape/11c6c0ef-495a-4a05-94cf-811b7b0a1012/scratchpad/k454/final-run.log, run over the same bytes as this commit): the new file; test_family.py, test_family_siblings_placement.py, test_family_makefile_siblings.py, test_family_printed_quoting.py, test_park_resume_targets.py, test_family_tracking_branch.py, test_family_init_values.py, test_family_init_landing.py, test_free_plan_hint.py, test_template_values_single_pass.py, test_template_text_placeholders.py, test_template_quoted_values.py, test_update_shape.py, test_update_shape_add.py, test_update_shape_kept_helper.py, test_bootstrap_spelling.py, test_windows_paths.py, test_scaffold_leg_path_collisions.py, test_shape_doctor.py, test_repo_hygiene.py, test_cli_reference.py, test_agents_md_codes.py: 2081 passed, 22 skipped, exit 0. The skips are test_family_init_values.py:219 (16) and :291 (3), whose templates quote the placeholder, and test_shape_doctor.py:9965 (3), a PowerShell line that needs a Windows host.

Mutants (each reverts one part of the fix, run over the new file):

mutant result
main without escape_unencodable_stdout() killed, 40 failed
family_folder_name prefers name: (R605's M4, which survived 166 tests) killed, 70 failed
the README's cd {{FAMILY}} back to cd {{FAMILY_NAME}} killed, 4 failed
the member would clone line prints the raw project, url and path killed, 2 failed
family_name_note returns nothing killed, 2 failed
Sibling.row prints the project raw killed, 10 failed

What it cannot tell

Shared files

git merge-tree --write-tree of this head 1a42df41 (read from ls-remote just before) against main 4cf5fb07 and each open PR head: clean (exit 0) against main and #590, #620, #621, #622 (5263d788 at the first read, 227bc3b0 now), #623, #626 (207d4e1d), #627, #628, #630 (82f9403d, which lists templates/family-root files), #631, #632, #633, #634, #635, #636, #637 (a2776959, which also lists templates/family-root/README.md), #638, #639. #599 conflicts in .github/workflows/tests.yml, README.md, docs/handbook.html, install.sh, openRepoShape and tests/test_install_sh.py, and main alone conflicts with it in those six and in tests/test_openreposhape_command.py too; none of them is this PR's file. Main moved from 8f453d0e to 4cf5fb07 while this was written (#583, #614, #618, #619, #596, #594, #617); git diff --name-only between them names none of this PR's three files.

Closes #454
Closes #629

🤖 Generated with Claude Code

Summary by Sourcery

Make family sibling operations robust for unusual text and consistently base holder-folder naming on the repository name.

Bug Fixes:

  • Prevent sibling placement and dispatch commands from failing or emitting unsafe terminal control characters when manifest, member, path, branch, or URL text cannot be safely written to stdout.
  • Align the family-root README and sibling layout checks on the repository name rather than the display name, including clear messaging when the names differ.

Enhancements:

  • Add explicit output explaining which family name determines the holder folder while preserving raw values for filesystem paths and executed commands.

Documentation:

  • Update the family-root README to describe the repository-based folder layout and distinguish it from the display name.

Tests:

  • Add comprehensive coverage for output spelling across encodings and unusual Unicode/control characters, sibling placement and dispatch behavior, and repository-versus-display-name folder layouts.

…resume` print the family's name and every member row spelled and never end in a traceback on `name: "a\ud83db"` or a dash under `PYTHONIOENCODING=ascii`, and the holder's README names its folder after the repository, so `--family Quill --name QuillWorks` lays out as its README says (#454, #629)

#454. `templates/family-root/scripts/siblings.py` opened each of its two reports with a plain `print` of `family.yaml`'s `name:` (`_run_placement` for `make siblings`, `dispatch_all` for `--make <target>`, which `make park` and `make resume` call) and printed every member row raw. On origin/main 8f453d0:

- `name: "a\ud83db"` is a string holding a lone surrogate, which no UTF-8 stdout can write: `siblings.py --dry-run` ended in `UnicodeEncodeError` at the first line (exit 1), `make siblings`, `make park` and `make resume` exited 2, and `make park`, which exists to save work, parked nothing.
- The line holds a literal dash, so under `PYTHONIOENCODING=ascii` it raised for EVERY name, `InkRouter` included.
- A raw ESC or CR in a value reached the terminal as the control it is.

#391 gave both roots' `bootstrap.py` `escape_unencodable_stdout()` and `spelled()`, written once in `scripts/repo_shape.py`. `siblings.py` now does the same in the same way:

- both names join the `from repo_shape import` line it already has, and it defines neither;
- `escape_unencodable_stdout()` is the first statement of `main`;
- `spelled` is applied to every value a line prints that a manifest, a member row or a disk gave: the name and the root (both headers), the folder and the link, a member's project, path, url and the reason its tracking branch is not known, the `Sibling.row` project and branch, the `FINDING` lines' project, directory, `origin` and `--make-arg`s, and the member's own `[project]` and `---` header lines. The member's own `make` is still handed the RAW target, the `mv` lines a person pastes stay quoted and unspelled (an escape in a command would not run as typed), and git's own words are left as git wrote them.

A plain name prints exactly what it printed before: the tests spell out the whole output of both runs for a plain holder, and that golden passes on the base too.

#629. `family.py init --family Quill --name QuillWorks` wrote a holder whose README said `cd QuillWorks` and laid the folders out as `QuillWorks/QuillWorks`, with "the doubled `QuillWorks/QuillWorks` is deliberate". `siblings.py`'s `family_folder_name` names the folder after the REPOSITORY (the basename of `repository:`, `Quill`), so a holder laid out exactly as its README said got `WARNING the holder's parent folder is 'QuillWorks', not 'Quill'`, cloned nothing, and `make` exited 2. No test built a holder whose `name:` differed from its repository and ran `make siblings`.

- `templates/family-root/README.md` names the folder with `{{FAMILY}}`, the placeholder `family.py init` already fills with `--family` (the same string `family_folder_name` reads out of `repository:`): the `cd`, the two layout lines and the sentence about the doubled name. `{{FAMILY_NAME}}` is left only as the README's title. A sentence says both are named after the REPOSITORY and that the display name names no folder. No new placeholder, and `scripts/family.py` is untouched.
- `templates/family-root/AGENTS.md` is unchanged: its two uses of `{{FAMILY_NAME}}` (`the family holder **X**`, `X's own`) are titles and name no folder, which the issue's list had as lines 3 and 7.
- `siblings.py` says which name it means. Where `name:` and the repository's name differ, the placement header gets one line under the folder's own, on a stopped run and a passing one alike: `the folder is named after the holder's REPOSITORY, Quill, and never after name: QuillWorks, which is only a display name`. Where they are one string nothing is added. The module docstring says which name `<Family>` is. `--make` asks nothing of the folder and keeps its header.
- `family_folder_name` and the README now agree, and a test holds them to it.

**Pinned shape copies.** `templates/family-root/scripts/siblings.py` is in `FAMILY_COPIED_VERBATIM` and digest-pinned in every holder: a holder that re-syncs reads `upstream-changed` for it, and `apply` copies it with `scripts/repo_shape.py` (no digest of either is recorded in this repository: the files: rows are computed from the bytes each tool writes, as for #391 and #578). `templates/family-root/README.md` is TEMPLATED, not pinned: an existing holder keeps its own README, and only a holder made after this has the corrected one. `scripts/repo_shape.py` is not changed. A holder that keeps its own edit of `repo_shape.py` without the two names `siblings.py` now imports is refused by `apply` as `update-accept-local-missing-name` (#536), which already lists `siblings.py` as an importer.

**Files.**

- `templates/family-root/scripts/siblings.py`
- `templates/family-root/README.md`
- `tests/test_siblings_family_name.py` (new)

`docs/cli.md` is unchanged: no `--help` text moved, and `AGENTS.md`, `README.md` and `docs/handbook.html` say nothing this makes false ("named after the family" is `--family`'s name, as before).

Closes #454
Closes #629

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

sourcery-ai Bot commented Oct 16, 2026

Copy link
Copy Markdown

Reviewer's Guide

Updates siblings.py to safely spell all user-controlled output without changing raw filesystem or command arguments, preventing traceback failures under problematic names and encodings, while standardizing holder-folder documentation and validation on the repository name instead of the display name; adds comprehensive regression coverage for both issues.

Sequence diagram for safe siblings output and member operations

sequenceDiagram
    participant CLI
    participant Siblings as siblings.py
    participant RepoShape as repo_shape.py
    participant Filesystem
    participant GitOrMake as git_or_make

    CLI->>Siblings: main(argv)
    Siblings->>RepoShape: escape_unencodable_stdout()
    Siblings->>Filesystem: load manifest and inspect members
    Siblings->>RepoShape: spelled(manifest_and_disk_values)
    RepoShape-->>Siblings: printable text
    Siblings->>GitOrMake: raw paths, targets and arguments
    GitOrMake-->>Siblings: operation result
    Siblings->>RepoShape: spelled(result_summary_values)
    Siblings-->>CLI: safe status output
Loading

Flow diagram for repository-based family folder validation

flowchart TD
    Init["family.py init --family Quill --name QuillWorks"] --> Manifest["family.yaml: repository = Quill, name = QuillWorks"]
    Manifest --> Folder["family_folder_name uses repository name: Quill"]
    Folder --> README["README documents Quill/Quill and cd Quill"]
    Folder --> Check{"Holder parent folder is Quill?"}
    Check -->|yes| Place["Clone members beside holder"]
    Check -->|no| Warning["Warn, print mv command, stop without cloning"]
Loading

File-Level Changes

Change Details Files
Make all manifest-, member-, and disk-derived text printed by siblings.py safe and unambiguous across stdout encodings and terminal controls.
  • Initialize stdout escaping before argument parsing or any output.
  • Route printed names, paths, URLs, branches, findings, command previews, and member rows through the shared spelled() helper while preserving raw values for filesystem and subprocess operations.
  • Use the shared escape_unencodable_stdout() helper to prevent UnicodeEncodeError failures in siblings, park, and resume.
  • Add extensive encoding, control-character, surrogate, dry-run, real-clone, and dispatch coverage.
templates/family-root/scripts/siblings.py
tests/test_siblings_family_name.py
Align the family-root README and siblings.py folder semantics around the repository name rather than the display name.
  • Change README folder, cd, layout, and doubled-name text to use {{FAMILY}}, leaving {{FAMILY_NAME}} only for the title.
  • Keep family_folder_name based on the manifest repository and add a note when repository and display names differ.
  • Document and test the Quill/QuillWorks layout, including refusal of the legacy doubled-display-name layout and successful cloning from the README-described layout.
templates/family-root/README.md
templates/family-root/scripts/siblings.py
tests/test_siblings_family_name.py

Assessment against linked issues

Issue Objective Addressed Explanation
#454 Make siblings.py's placement and dispatch reports—including make siblings, make park, and make resume—safe to print for arbitrary family names and member-row values: avoid encoding tracebacks on restrictive stdout encodings and spell terminal controls and unencodable characters. ✅
#629 Make the holder README name its folder after the holder repository, so the documented layout passes the folder check and make siblings clones members. ✅
#629 Clarify which family name siblings.py uses when the display name differs from the repository name, and test the documented layout with a real holder and member clone. ✅

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

@sonarqubecloud

Copy link
Copy Markdown

@sourcery-ai

sourcery-ai Bot commented Oct 16, 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 11 hours and 47 minutes by commenting @sourcery-ai review. Upgrade to get a review now.

@brettheap
brettheap merged commit 0ed1b84 into main Oct 16, 2026
4 checks passed
@brettheap
brettheap deleted the fix/454-629-siblings-family-name branch October 16, 2026 10:45
brettheap pushed a commit that referenced this pull request Oct 16, 2026
Main gained #626 (`family.py add`, `bump` and `remove` refuse before their
first write), #633, #642, #590 and #641 (`git status --porcelain` and
`git config user.name` read as bytes) since this branch's base 8f453d0.

One conflict, in scripts/family.py: the comment above the single shared
`from shape_materialize import unwritable_reason` line. #433 and #641 each
added a use of that import and each wrote its own comment above it. The
resolution keeps both uses and all three issue numbers (#426 for the creator
read from `git config user.name`, #255 and #433 for the holder's clone URL):

    # Whether a value `init` writes into the holder is one line of text a file
    # can hold: the creator read from `git config user.name` (#426), and the
    # holder's clone URL under `--local-remote-dir` (#255, #433). It is the
    # question the scaffold and `adopt-project.py` ask of theirs.

scaffold-project.py auto-merged. No other hunk conflicted.

Lane: openreposhape-4 (openRepoShape-4)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
brettheap pushed a commit that referenced this pull request Oct 16, 2026
main gained #563's own #590 (fe237ec) and #599 (32c22b0), whose trees
this branch already held through #599's 750366b (#599's final head,
eaa0cda, is 750366b plus a merge of main), and #626, #633, #642, #641,
#643, #644, #632, #636, #631, #638, #635, #640, #623, #637 and #622. No
conflict: the merged tree is 9be0d0d, the tree `git merge-tree
--write-tree` predicted. AGENTS.md, docs/cli.md and
tests/test_repo_hygiene.py merged as text; README.md and
docs/handbook.html are unchanged by the merge. README.md is 1461 lines,
its cap (1449 + 12, tests/readme-cap.d/563.txt), and AGENTS.md 494, its
cap.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap pushed a commit that referenced this pull request Oct 16, 2026
… tab shows the one-line install for Git Bash, held to the other copies by a test, and the notes are current to main d4a7fab with the fifteen pull requests since 4cf5fb0, the environments, the tag ruleset and the rehearsal (#563, #461)

The handbook (#461's line "The handbook's step 3 has no Git Bash path",
which lane 3 told lane 5 on #473 that #630 settles): step 3's install
block was `data-platform="linux mac wsl"`, so a reader on the Windows tab
never saw the one line the Windows panel says Git Bash runs. The Windows
tab now has a lead, "In Git Bash, the bash that Git for Windows installs,
the one-line install works as on Linux:", the Linux tab's block copied
byte for byte as `data-lang="sh"`, and the panel's caveat: the preflight
it ends with cannot take a typed `yes` in Git Bash. README.md is
unchanged: no test asked for it, and it is at its cap.

tests/test_repo_hygiene.py: the test above it finds one copy of the one
line per file; `test_every_handbook_copy_of_the_one_line_install_is_the_same`
holds every handbook copy (the masthead's and step 3's two) to
ONE_LINE_INSTALL, so a copy edited on one tab alone fails. A drifted
Windows copy fails it; the page as committed passes.

The release notes:
- "What changed since v1.0.1" gains one entry per pull request that
  landed after 4cf5fb0 and is not #563's own, from `git log
  --first-parent 4cf5fb0..origin/main` at d4a7fab: under update-shape.py
  and bump-leg.py, #644, #633 and #631; under scaffold, family and the
  materializer, #622, #643, #640, #641, #623, #638, #635, #626, #637 and
  #642; under shape-doctor.py, #636 and #632. Each from its squash
  commit and the issues GitHub says it closes.
- The list's placeholder says it is current to main d4a7fab and stays a
  placeholder for what lands after it; the counts are refreshed inside
  their placeholder (66 pull requests, 75 of 97 milestone issues closed).
- #590 and #599 are cited beside #563, and "Before tagging" says they
  landed (fe237ec, 32c22b0) and lists the 22 open milestone issues.
- "How this release is cut": Brett Heap's word at 2026-10-16T12:06:40Z,
  the environments testpypi (12:07:31Z) and pypi (12:07:39Z), the tag
  ruleset 25237399 (12:08:14Z), and the rehearsal, run 38898735926
  (12:10:53Z-12:12:00Z, success, both uploads skipped), each read back
  from the API. The trusted publishers' date stays a placeholder.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Oct 16, 2026
… it what to do where an earlier act left that branch, so `already exists` is no dead end (#607's sentence, in the two tools it left) (#657)

#607 (#637) gave `family.py` and the holder's `validate-family.py` one sentence for a `switch -c` that fails `fatal: a branch named '<branch>' already exists` (exit 128):

> If `git switch -c` says that branch already exists, an earlier act left it: give the branch another name, which starts it where the holder stands, or, only if that branch holds work not merged yet, the same command with `switch` in place of `switch -c` goes back to it and keeps every commit on it.

Two tools still printed a `switch -c` and said nothing more. Both now say the same sentence under that line, each as its own constant, `BRANCH_LEFT`, as #637 does:

- **`scripts/bump-leg.py`**, with "the project" where the others say "the holder". It follows the command in all three places `_switch_command` is named:
  - the `bump-leg-on-tracking-branch` remedy: after `and re-run.`;
  - the `bump-leg-root-detached` remedy: on the line under it;
  - the dry run's closing hint (#477): the new last line, under the hint.
- **`setup-project.py`**: the same sentence, word for word (the holder), as one more comment line under the family block's `switch -c` line. The family block's two comment lines stay, and the line comes after them.

No command line changes, and none is quoted differently. The sentence is words, not a second command, so no PowerShell quoting is added (#532) and no README sentence about quoting becomes false.

Pinned files: none. This changes no file under `templates/`.

1. **Where the sentence goes in bump-leg's tracking-branch remedy: after `and re-run.`, not between the command and `and re-run.`** The remedy reads `Run:`, the command on its own line, `and re-run.` With the sentence between, `and re-run.` would hang under a full sentence. After it, the text before the sentence is byte for byte what it was, so the command line and `and re-run.` keep their places. The sentence is still under the `switch -c` line.
2. **setup's sentence is one comment line, unbroken (about 300 columns).** The brief asked for one comment line, word for word. `family.py`'s dry run prints it unbroken too. The block's other comments are hand-wrapped at 78 columns. A wrapped block would be a small change if the coordinator prefers it; the test would then join the comment lines.
3. **Every printed command line is unchanged**, and the tests prove it: bump-leg's line is read back by `shlex.split` and compared to the argv it always meant (`switch_argv`); setup's by `host_words`, on a folder with a space and one with a space beside an apostrophe. The block still has exactly two command lines.
4. **One rule, four copies.** #637's equality test, now `test_the_tools_say_the_same_words`, also asserts that `setup-project.py`'s constant equals `family.py`'s and the validator's, and that bump-leg's equals it with "the project" for "the holder". It was renamed from `test_the_two_tools_say_the_same_words`, because "two" is no longer true. The project's wording is pinned as text once, as `BRANCH_LEFT_PROJECT` in `tests/test_bump_leg_paste_lines.py`, beside the lines it follows; the holder's is #637's pinned copy in `tests/test_validate_family_branch_step.py`. `test_the_two_pinned_texts_differ_in_the_one_word_only` ties the two pinned copies together.
5. **Existing tests that pinned the old last lines take the sentence.** Seven tests in three files (listed under Tests). Each read the last line of an output, or the exact end of a remedy; none changes what it checks.
6. **The new module imports #637's typing helpers** (`leave_branch`, `refused_as_left`, `type_other_name`, `typed`, `without_c`, `head`, `on_branch`) instead of copying them, so the duplication gate sees one body.

New module `tests/test_branch_left_bump_leg_setup.py`, 19 tests:

- bump-leg, each of three places (`tracking`, `detached`, `dry-run`): the command is the one it always was and what stands under it is the sentence (3); the printed line makes the branch where none is left (3); where an earlier bump left `bump/spec-<id>` holding a commit of its own, the printed line fails `already exists` (exit 128) and changes nothing, then **another name** works (the new branch starts where the root stands, the bump goes through on it, its commit sits on the commit the root stood on, and `HEAD:spec` is the new commit) (3), and **going back** with `switch` for `switch -c` works (back on the branch at its own commit, the bump goes through there on top of it) (3).
- setup, the family block: the line, the two comments, the sentence, the `add` line, two command lines (2, one per folder name); a real `setup-project.py` run prints the sentence once, under the right comment (1); in a real local holder (the `family` fixture's, on its tracking branch), with a real member: the printed line makes the branch where none is left, and where an earlier try left `family/add-<project>` with a commit of its own, **another name** works and `family.py add` goes through on it, and **going back** works and `add` goes through on top of the earlier commit (3).
- `test_the_two_pinned_texts_differ_in_the_one_word_only` (1).

Edited tests: seven cases in six test functions of the three bump-leg files, `test_bump_leg_dry_run_tracking.py` (4: the dry run's report, the difference from a dry run on a branch, the hint pasted for a root with a space, the real refusal byte for byte), `test_bump_leg_paste_lines.py` (1: the exact remedies) and `test_bump_leg_dry_run_truth.py` (2: the hint's branch name, `[local-remote-dir]` and `[origin]`); and, beside them, the extended `test_the_tools_say_the_same_words` in `test_family_switch_branch_exists.py`.

Runs, on be1bff9 (BASE a524542 plus main 95780dd), through `run9.sh`, `gh` stub first on PATH:

- New module with #637's module: `44 passed in 61.90s`.
- Set 1, 21 files: every `tests/test_bump_leg*.py` (7), the new module, `test_bootstrap_after_bump`, `test_stdout_encoding`, `test_non_utf8_git_reads`, `test_agents_md_codes`, `test_commit_trailers`, `test_update_shape_paste_lines`, `test_validate_pins_lockstep_lines`, `test_pin_rewrite_layout`, `test_validator_child_encoding`, `test_validate_pins_unreadable_workflows_dir`, `test_template_text_placeholders`, `test_repo_hygiene`, `test_cli_reference`: `1040 passed, 2 skipped in 459.92s`. Both skips were there before: a case-ignoring disk (`test_bump_leg.py:693`) and a Windows default (`test_stdout_encoding.py:286`).
- Set 2, 10 files: every file `git grep -l 'switch -c' tests/` names outside set 1 (`test_dry_run_asks_remotes`, `test_family_printed_quoting`, `test_family_refuse_up_front`, `test_family_switch_branch_exists`, `test_family_tracking_branch`, `test_validate_family_branch_step`, `test_validate_family_unnamed_mounts`), plus `test_setup_by_hand_commands`, `test_setup_project_py`, `test_setup_sh`: `592 passed in 506.74s`.
- Before #637 landed, on 6ac545f: set 1 `1042 passed, 2 skipped`, set 2 (9 files) `441 passed`.

**Fails on BASE.** The same five test files from the head, run on main 95780dd (no source change): `26 failed, 80 passed in 168.33s`. 18 of the 19 new tests fail, `test_the_tools_say_the_same_words` fails (no `BRANCH_LEFT` in either new place), and the seven edited tests fail. The one new test that passes on BASE is `test_the_two_pinned_texts_differ_in_the_one_word_only`: it reads text only. On the first commit alone, against a524542, the old module copy gave `20 failed, 1 passed`.

A real project (`scaffold-project.py` into local remotes, a recursive clone, the spec leg advanced on a branch of its remote) and a real holder (`family.py init`, then a real `setup-project.py Sample --yes --family Orbit` run). An earlier act left the branch with a commit of its own.

- **On BASE a524542:** `bump-leg.py` on the tracking branch ends `Run:` / the `switch -c` line / `and re-run.` and nothing more. The line typed as printed gives `fatal: a branch named 'bump/spec-712e4ce5d766' already exists`, exit 128. The detached refusal and the dry run's hint end on the command the same way. setup's block is the `switch -c`, `# a branch first: add refuses the` and `#   holder's tracking branch`, then the `add`. Typed, `fatal: a branch named 'family/add-sample' already exists`, exit 128.
- **On the head:** each of the four prints the sentence under the line, and the same line gives the same 128. Then:
  - another name (`<line>-2`): exit 0; `bump-leg.py` goes through on it, on top of the commit the root stood on; `family.py add` goes through on it;
  - going back (`switch` for `switch -c`): exit 0, at the earlier commit; `bump-leg.py` and `family.py add` go through, their commit on top of it.
- Both choices already worked on BASE: the sentence tells a person so. No row needs real GitHub.
- **A real earlier bump, not a made-up branch.** I ran a real bump on `bump/spec-<id>`, then went back to `main`. There the root shows ` M spec` (the leg is still at the bump's commit), and the next `bump-leg.py` is refused `bump-leg-root-dirty` before it reaches the `switch -c` line. After `git submodule update` puts the leg back, the line appears, fails `already exists`, and the other name works. See the checklist candidates.

Each hunk was reverted or broken alone in a scratch clone of the head, and the new module plus `test_the_tools_say_the_same_words` was run. Every mutation makes new tests fail:

| Mutation | Result |
|---|---|
| bump-leg `BRANCH_LEFT` says "the holder" | 13 failed: the equality test, and the three places in each of four tests |
| bump-leg `BRANCH_LEFT` drops `only if` | 13 failed, the same |
| `bump-leg-root-detached` remedy without the sentence | 4 failed (the `detached` case of each of four tests) |
| `bump-leg-on-tracking-branch` remedy without the sentence | 4 failed (the `tracking` case of each) |
| the same remedy with the sentence above `and re-run.` | 4 failed, the same |
| dry run's hint without the sentence | 4 failed (the `dry-run` case of each) |
| `_switch_command` prints `switch -C` | 9 failed (the "another name" and "going back" cases, and the three "command is the one it was" cases); the control, where no branch is left, still passes, as `-C` makes a new branch too |
| setup `BRANCH_LEFT` says "the project" | 7 failed (the equality test and six) |
| setup block without the sentence | 6 failed |
| setup sentence above the block's two comments | 6 failed |
| setup `BRANCH_LEFT` drops `only if` | 7 failed |

The other hunks change only docstrings and comments (`_switch_command`, `_branch_to_bump_from`, `family_next_command`, the comment above the dry run's hint). They change no behaviour.

| Ref | Result |
|---|---|
| origin/main d4a7fab (one commit past my merge of 95780dd; touches `scripts/family.py`, `scaffold-project.py`, `docs/cli.md`) | clean |
| #637 (landed as 95780dd) | merged into this branch once, clean |
| #623 (lane 2, `setup-project.py` steps (3) and (4); landed as abb956e) | merged in with #637, clean: its hunks are far from `family_next_command` |
| #639 08b6245 (lane 5, #611) | clean |
| #641 e87fcb0 (touches `bump-leg.py` and `family.py`) | conflicts in `scripts/family.py`, and the same conflict arises against main alone: not from this branch. `bump-leg.py` merges clean. |
| #615 6ad2303 (touches `bump-leg.py`) | clean |
| #631, #636 (lane 4) | clean |
| #642, #643, #644 | clean |
| lane-5 siblings fix/645 e2110f8, fix/646 2216e1c, fix/648 852c5c4, fix/649 d91b314 | clean |
| #561 (lane 4's `fix/bump-leg-tag-dry-run`) | its work is on main already (7d9bec9, `asked = commit`); its stale pull head conflicts with main alone, so it is not an open PR to check |

- No README, AGENTS.md or handbook edit. I checked every sentence about these lines: AGENTS.md:337-351 and 374 (the dry run "prints the `git switch -c`"), README.md:157 and 1011, `docs/cli.md` (generated; no `--help` text changes) and the handbook. None becomes false.
- Windows and macOS are not checked here, and no CI job checks them since #597. The tests type argvs, run no shell, compare no file that `write_text` wrote, and read setup's lines with `host_words`.
- A branch checked out in another worktree makes `git switch <branch>` fail (#637's "Left out", N1); the sentence is not made longer for it.
- I did not tick or mark the lines on #458 and #459.
- **Review rv647 (APPROVE WITH NITS, 0 blockers, 0 should-fix, 5 nits), and what is left of it:**
  - Commit 6ac545f's message says "Four existing tests … take the sentence". It is six test functions (seven cases) in three files, as the Tests section says. The squash message is this body.
  - `scripts/bump-leg.py`'s comment near :1164 still says "the three tools follow one rule"; there are four copies of the sentence now, and `setup-project.py`'s comment names two. Left for the `bump-leg.py` issue filed from #458's lines, which lands after this and edits that file.
  - No AST guard holds a fourth `_switch_command` caller to the sentence, as #637's `test_every_remedy_that_names_a_switch_line_ends_with_the_sentence` holds the validator's.
  - Setup's sentence prints as one comment line of about 317 columns in a block wrapped at 78. It is unbroken as `family.py`'s `_branch_said` prints it, which is what lets the byte-for-byte equality tests hold (decision 1).

- **bump-leg.py: after a real bump on a branch and a switch back to an older branch, the root is dirty (` M spec`) and `bump-leg-root-dirty`'s remedy, "commit or stash them first", is wrong advice.** The leg is left at the bump's commit and `git switch` does not move it (README says the leg is left detached at the new commit). `git submodule update` (or `make bootstrap`) puts it back and clears the refusal. The remedy could say so where the one dirty path is a leg's mount. It also means the `switch -c` sentence is reached only after that step in this flow.
- **The same flow's "go back" choice meets that refusal too** (rv647): `git switch` to the branch that holds the earlier bump leaves the leg behind, so the re-run is refused `bump-leg-root-dirty`; after `git submodule update` it says `nothing to do`, and only the push is left.
- **update-shape.py:5279 prints `NEXT git -C <root> checkout -b shape/update-<12> && git … commit`** (rv647, read only): it fails `already exists` the same way, and the `&&` chain then commits nothing. It is `checkout -b`, outside #647.
- **family.py, after a real earlier `add` and a switch back:** another name works for the switch, but `family.py add` is then refused `member-path-taken`, because git left the populated `members/<Project>` checkout behind (rv647; identical on the base). It bears on #637's "another name works".

Review: rv647 found 0 blockers, 0 should-fix and 5 nits on be1bff9 (APPROVE WITH NITS); nits 1 and 2 are answered in this body, nit 3 (the stale "three tools" comment in `scripts/bump-leg.py`) rides with #672, and nits 4 and 5 are noted above. CI (Brett Heap's ruling 27, with his fast-refresh ruling of 2026-10-15T22:48:35Z): H0 be1bff9 passed tests (Linux), tests-git-bash and SonarCloud; the head ac8a810 is a clean update-branch merge of main 8062810, with SonarCloud, tests-git-bash and package green on it, and the local gate passed on the head itself: the bump-leg set, 1040 passed (2 skipped), and the `switch -c` and setup set, 592 passed. Landed on Brett Heap's word of 2026-10-16T13:07:34Z, "merge #656 and #657 when green".

Closes #647

Lane: openreposhape-5 (openRepoShape-5)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Oct 16, 2026
…-letter `$OPENREPOSHAPE_REPO` is a path, a Windows-spelled `$OPENREPOSHAPE_BIN_DIR` is respelled or refused, and `tests-git-bash` runs #598's installer cases (#649) (#656)

#596's Git Bash follow-ups, the five lines of #649. Two commits on main a524542: **d91b3144**, the fix, and **e85573ac**, fix round 1 from rv649's review. Together they change `openRepoShape`, `.github/workflows/tests.yml` and `tests/test_openreposhape_command.py`, and nothing else. `git diff --numstat`, d91b314: `openRepoShape` +42/-4, `tests.yml` +6/-1, the test file +182/-7. e85573a: +9/-6 (comments only), +3/-2 (a comment only), +56/-27. README, the handbook and `docs/cli.md` are unchanged: every sentence about these blocks stays true (below).

### Fix round 1 (rv649)

rv649 found the launcher's code correct and asked for changes: 0 blockers, 1 should, 6 nits. e85573a answers each point. It changes no line of the launcher's code and no command of the workflow: three comments and the tests.
- **SHOULD-1, the `D:/…` arm had no test.** Both BIN_DIR tests now run over `D:\tools\bin` and `D:/tools/bin`, and `fake_cygpath` takes the typed value. On Windows they use `str(...)` and `.as_posix()` of a path under `tmp_path`. With the arm dropped the suite stayed green (91 passed); now 5 cases fail.
- **NIT-1, the refusal's drive-answer arm had no test.** A third refusal case: a `cygpath` that answers `D:/tools/bin` with exit 0. With that arm dropped the suite stayed green; now 2 cases fail.
- **NIT-2, a `python` that is Python 2, or older than 3.9, reads as present.** Named as a cost in decision 1 and in the comment above `machine_report`. No code change: `--preflight` still refuses those machines.
- **NIT-3, the refusal's reason was false in Git Bash with `cygpath` off `PATH`.** Git Bash does read `D:\…` as a drive. The comment in `install_commands` and decision 3 now give the true reason.
- **NIT-4, the job's opening comment named two files.** It names the three.
- **NIT-5, on Windows the refusal test's value was a real `D:` drive path.** It is now a path under `tmp_path`, and the assert names that value, so a refusal that regressed would place the command where the test looks.
- **NIT-6, four claims.** Fixed in this body: "rows 1-3 are checked through the tests" (now says what runs), the clone URL for each slash (`C:\src\…` gave `https://github.com/C:\src\openRepoShape.git`), the Store stub clause (dropped, see decision 1) and the report's diffstat totals (the exact `--numstat` counts are at the top). d91b314's own commit message keeps its `....` typo and its one-URL sentence in history; the squash message is this body.
- **Composition and merge table.** Refreshed against origin/main 879d804 (below). #630 no longer conflicts.
- **Checklist candidate 3**, from rv649, is added below.

### The fix

**1. The `machine:` row for Python** (`machine_report`).
- Before: the block asked `command -v python3` and nothing else. Where Python came from python.org, Git Bash has `python` and no `python3`, so the row said `python3 MISSING` and printed the line pointing at `--preflight`, which then passed with `python`.
- Now: it asks for `python3`, then `python`, in `find_python`'s order. When only `python` answers, the row says `python3          present, as python`, and nothing points at `--preflight` for it. With neither, it says `MISSING`, as before. With `python3`, it says `present`, byte for byte as before.

**2. A drive-letter `$OPENREPOSHAPE_REPO`** (`shape_clone_url`).
- Before: `C:/src/openRepoShape` and `C:\src\openRepoShape` were read as an owner/name, so setup.sh's clone and `--doctor`'s were handed `https://github.com/C:/src/openRepoShape.git` for the first and `https://github.com/C:\src\openRepoShape.git` for the second.
- Now: a letter, a colon and then `/` or `\` is a path, and goes through as it is, as `/…` and `.…` do. No owner/name holds a colon. The package's own `clone_url` (`src/openreposhape/standard.py`, `DRIVE_RE`) already read a drive letter this way.

**3. A Windows-spelled `$OPENREPOSHAPE_BIN_DIR`** (`install_commands`).
- Before: `--install` named the directory as typed. With `D:\tools\bin` it printed `installed at D:\tools\bin/openRepoShape` and `export PATH="D:\tools\bin:$PATH"`, which the drive's colon splits in two. It also never found the directory on a `$PATH` that holds `/d/tools/bin`, so it printed that line every time.
- Now: a drive-letter value, in either slash, is spelled again with `cygpath -u`, which Git Bash has, before anything is checked or placed. Every line then names `/d/tools/bin`, and the PATH note appears only when that directory is not on `$PATH`.
- Where no `cygpath` answers (none on PATH, it fails, or it answers a drive spelling again), the value is refused before anything is placed: "--install would place this command in 'D:\tools\bin', which is spelled for Windows, and no cygpath here spells it the way bash does, so the directory could not go on $PATH: the drive's colon splits it. Nothing was placed. Set $OPENREPOSHAPE_BIN_DIR to the directory as this shell spells it, then run this again."

**4. `tests-git-bash` runs `tests/test_install_sh_filter_repo.py`.** That file holds #598's uv install, the two readinesses and the pasted next steps, which ran on Linux only, while README says Git Bash runs step 3's one line "tested in CI". It is the third path on the job's pytest line, with a comment saying the installer's suite is two files. The job's other lines, the header's "the launcher's and the installer's suites, and nothing else" and #630's release-notes sentence of the same meaning stay true.

**5. The stale docstring.** `test_a_ref_the_doctor_would_check_out_is_validated` said the refusal "has to arrive before any clone does". The command clones first, then checks the ref, and refuses before `git checkout` is handed it. The docstring now says that. The order is unchanged.

- **Line 1: `command -v` for both names, and no version check.**
  - The block promises to execute nothing but `gh auth status`, and `test_the_machine_report_runs_no_installer` holds it to that: every fake on its PATH logs a run. Running `find_python`'s check would break that promise.
  - What answers is therefore never asked its version, and that has a cost. The Microsoft Store's `python3` stub reads as present, as macOS's Command Line Tools stubs for `python3` and `git` already do (setup.sh's comment at :41-47). That is not new: the base asked `command -v python3` too, and the Store installs its `python.exe` and `python3.exe` aliases together, so `python3` answers first.
  - What is new is a `python` that is Python 2, or older than 3.9, where there is no `python3`. On the base such a machine said `python3 MISSING` and printed the `--preflight` pointer. Now it says `present, as python` and prints no pointer. It is rare (CentOS 7, macOS 10.14 or older, Python 2.7 from python.org), and `--preflight` still refuses it with "no Python 3.9 or newer on this machine (tried python3, python)". The comment above `machine_report` says so.
  - The row keeps its name `python3`, and the state carries the name that answered, in the form `gh`'s row already uses (`present, logged in`). So README's sentence ("`git`, `gh` and whether it is logged in, `python3`, and `git-filter-repo` … each `present` or `MISSING`") and the handbook's stay true, and README is not edited. Labelling the row `python` would have made both false in Git Bash.
- **Line 3: respell, and refuse only where respelling is not possible.** Respelling keeps a Git Bash user's value working: `cp`, `mkdir` and the record took the Windows spelling already, and only the printed lines were wrong. Where no `cygpath` answers, nothing in the command can spell the value as bash's `$PATH` needs it: in Git Bash with `cygpath` off `PATH` the drive is real but its colon still splits `$PATH`, and on Linux `D:\tools\bin` is a relative directory with a colon in its name. No `$PATH` line could work there, so it refuses. A `cygpath` that answers a drive spelling again is refused alike.
- **The tests' `install_with_machine` now names its directory with `bash_path`.** Its `$PATH` holds only the programs `--install` runs, and no `cygpath`. On Windows the old `str(bin_dir)` is a drive path, which would now be refused. Off Windows, `bash_path` changes nothing. Other tests that pass `str(...)` keep the full PATH, so on Windows they now go through Git Bash's own `cygpath`.
- **`run_cmd` takes a `cwd`.** On the base, and under a mutation, the new BIN_DIR tests' `D:\tools\bin` is a relative directory. With `cwd=tmp_path` it lands in the test's own directory, never in the checkout.
- **Both BIN_DIR tests run over both slashes** (`BIN_DIR_SPELLINGS`, `typed_bin_dir`). Off Windows the typed value is the fixed `D:\tools\bin` or `D:/tools/bin`. On Windows it is a path under `tmp_path`, so neither test can place a command on the machine's own `D:` drive.

New, in `tests/test_openreposhape_command.py`, 27 cases:
- `test_the_machine_report_finds_a_python_by_either_name`: `python` alone, both, neither (3).
- `test_a_windows_spelled_bin_dir_is_spelled_the_way_bash_spells_it`: not on `$PATH`, on `$PATH`, each in both slashes (4). On Windows it is a real drive path (`str(bin_dir)` or its `as_posix()`) and Git Bash's own `cygpath`; elsewhere a fake `cygpath` answers as that one would.
- `test_a_windows_spelled_bin_dir_is_refused_where_no_cygpath_spells_it`: no `cygpath`, a `cygpath` that fails, a `cygpath` that answers `D:/tools/bin`, each in both slashes (6), on the machine report's own `$PATH`.
- `test_a_clone_is_handed_a_path_or_url_as_it_is`: owner/name, URL, scp, POSIX path, `C:/…`, `C:\…` and `d:\…`, each through setup.sh (a setup.sh that prints `$OPENREPOSHAPE_REPO`) and through `--doctor` (a `git` that prints the URL it is handed and fails) (14). Nothing is cloned or fetched.

**On the base a524542** (this head's test file over the base's command): **17 failed, 80 passed** in 62.32s. The failures are exactly the new cases that need the fix: `python-alone`, the four respelling cases, the six refusal cases and the six drive-letter cases. The other 10 new cases pass there too, as they should.

**On the head e85573a** (Linux, `dirty=0`):
- `tests/test_openreposhape_command.py`, `tests/test_install_sh.py`, `tests/test_install_sh_filter_repo.py`, `tests/test_openreposhape_package.py`: **220 passed** in 175.18s. The package suite imports 13 of the launcher's cases and swaps `run_cmd`, so it is in the set.
- `tests/test_repo_hygiene.py`, `tests/test_workflow_concurrency.py`, `tests/test_release_workflow.py`: **138 passed** in 14.46s.
- The head merged with main 879d804 (a probe commit, b6cfafe3): the launcher's, the two installer files, the package suite and `tests/test_repo_hygiene.py`, **297 passed** in 188.95s.
- **The three files `tests-git-bash` runs, with Windows' `write_text` emulated**: a plugin made every `Path.write_text` in the test process write CRLF, as it does on `windows-latest`. **162 passed** in 121.06s (97 + 47 + 18). A probe test shows the plugin takes effect (it passes with the plugin and fails without it).
- **actionlint 1.7.12**, with shellcheck on PATH, on `tests.yml` alone and on the four files the CI job names: exit 0.

d91b314's own `tests-git-bash` run was green on `windows-latest`. It ran the first round's 21 new cases and `tests/test_install_sh_filter_repo.py` there, so Git Bash's own `cygpath` answered the backslash respelling on a real drive path, and the five existing tests that pass `str(...)` and now go through it passed. What was checked, and what was not:
- **Checked, from #596's Windows runs (113 passed on 5f001de, and its probes):** a fake that Python writes with `#!/bin/sh` runs under Git Bash with no mode bits, and `command -v` finds it. A script piped in text mode reaches Git Bash's bash as CRLF, and bash drops the CR. A `$OPENREPOSHAPE_BIN_DIR` set from Python reaches bash unconverted: #596's probe 4 printed `D:\a\_temp\…\bin/openRepoShape`, which is why the respelling now runs in those tests.
- **Checked on Linux:** LF bytes (the CRLF emulation above). No assert in the test file compares a path bash printed with a Python path.
- **Not checked, until e85573a's own run:** on Windows, the slash spelling (`as_posix()`) through Git Bash's real `cygpath`, the refusal's values under `tmp_path` on the machine report's `$PATH`, and the new drive-answer case. If one fails there, its log shows the typed value and the directory.

One script ran each row against the base's command and the head's, offline, with a `gh` stub first on PATH. Row 3's slash lines ran again for this round.

| Row | Base a524542 | Head e85573a |
|---|---|---|
| 1. PATH with `git`, a logged-in `gh`, `python`, `git-filter-repo`, no `python3` | `python3          MISSING`, then `run: openRepoShape --preflight …`; the log holds only `gh auth status` | `python3          present, as python`, no `run:` line; the log holds only `gh auth status` |
| 2. `OPENREPOSHAPE_REPO=C:/src/openRepoShape` | setup.sh clones `https://github.com/C:/src/openRepoShape.git`; the doctor's `git clone` too | `C:/src/openRepoShape`, both |
| 2. `OPENREPOSHAPE_REPO=C:\src\openRepoShape` | `https://github.com/C:\src\openRepoShape.git`, both | `C:\src\openRepoShape`, both |
| 2. `OPENREPOSHAPE_REPO=acme/openRepoShape` | `https://github.com/acme/openRepoShape.git` | the same |
| 3. `OPENREPOSHAPE_BIN_DIR='D:\tools\bin'`, no `cygpath` | exit 0, `installed at D:\tools\bin/openRepoShape`, `export PATH="D:\tools\bin:$PATH"` | exit 2, the refusal above, nothing placed |
| 3. `OPENREPOSHAPE_BIN_DIR='D:/tools/bin'`, no `cygpath` | exit 0, `installed at D:/tools/bin/openRepoShape`, `export PATH="D:/tools/bin:$PATH"` (on Linux, a relative directory named `D:` in the working directory) | exit 2, the same refusal with `'D:/tools/bin'`, nothing placed |
| 3. the same, a fake `cygpath -u` answering `<dir>/d/tools/bin` | the same as with none: `cygpath` is never asked | exit 0, `installed at <dir>/d/tools/bin/openRepoShape`, `export PATH="<dir>/d/tools/bin:$PATH"` |
| 4. `tests-git-bash`'s pytest line | two files | three files |
| 5. the docstring | "the refusal has to arrive before any clone does" | "clones this checkout first, then checks the ref" |

Rows 1-3 need Windows to be real: there, `python` is a real python.org install, and `cygpath` and the drive letter are Git Bash's own. On Linux they are fakes. The first Windows run runs rows 1-3's tests under Git Bash: row 3 with Git Bash's own `cygpath` on a real drive path, rows 1 and 2 with a fake `python` and a fake `git`, so a real python.org install and git's own reading of `C:/…` stay unchecked.

Each row reverts one hunk of the launcher, or one part of the BIN_DIR hunk, in a throwaway clone, then runs the 27 new cases.

| Hunk reverted | Result | Failing cases |
|---|---|---|
| none (the head) | 27 passed | — |
| `shape_clone_url`'s drive-letter arm | 6 failed | the six drive-letter cases, setup.sh and doctor |
| `machine_report`'s `python` arm | 1 failed | `python-alone` |
| the BIN_DIR block, whole | 10 failed | the four respelling cases and the six refusal cases |
| the `cygpath -u` call (always refuse) | 4 failed | the four respelling cases |
| the refusal (keep the value as typed) | 6 failed | the six refusal cases |
| `\|\| spelled=""` after the `cygpath` call | 2 failed | `cygpath-fails`, both slashes: `set -e` exits with `cygpath`'s status, 1, not the refusal's 2 |
| **the `D:/…` arm of the drive case (SHOULD-1)** | 5 failed | the two slash respelling cases and the three slash refusal cases (was green) |
| the `D:\…` arm of the drive case | 5 failed | the same five, backslash |
| **the refusal's drive-answer arm, `[A-Za-z]:*` (NIT-1)** | 2 failed | `cygpath-answers-a-drive`, both slashes (was green) |
| the refusal's empty-answer arm, `""` | 4 failed | `no-cygpath` and `cygpath-fails`, both slashes |
| `--` dropped from `cygpath -u -- "$dir"` | 4 failed | the four respelling cases: the fake answers only `-u -- <value>` |
| the respelling after the placement, not before | 4 failed | the four respelling cases |

Not probed by a test: the `tests.yml` line (CI configuration, checked by actionlint and by this PR's own Windows run), the docstring, and `install_with_machine`'s `bash_path` (identity off Windows; on Windows it is what keeps four machine-report tests from the new refusal).

| Against | Result |
|---|---|
| origin/main 879d804 (d4a7fab, then 879d804, since the last table's 95780dd) | **clean**. Of this PR's files, the three suites', README and the handbook, only `tests/conftest.py` has moved since the base a524542 (new helpers for setup-project's tests), and nothing since the last table's 95780dd. The head merged with main (a probe commit, b6cfafe3) passes the suites below. |
| lane 3's #630 61b3907 | **clean**. It conflicted at ca5c49f, at the `tests-git-bash` pytest line, because its base was older than main's #590; its head now holds main d4a7fab, and it does not change `tests.yml`. It changes `AGENTS.md`, README, `adopt-project.py`, the handbook, `docs/cli.md`, the release notes and four test or render files, none of which this PR touches. |
| #620 9f2b67c, #621 10dda17, #622 55666f3, #623 fc08a28, #627 83ca2db, #628 d64252a, #631 21bdac8, #634 b7472df, #636 30a52c0, #637 62263d2, #638 b393e1f, #639 08b6245, #642 1a42df4, #643 1fe3764, #644 733f853 | **clean** (#623 changes `tests/conftest.py`; #627 changes README and the handbook) |
| #641 e87fcb0 | conflicts in `scripts/family.py`, and so does #641 against main alone: its own conflict, not an overlap with this PR. |
| lane 5's `fix/645-fixture-copy-maintenance-race` accc7d2, `fix/646-add-member-variant-remedy` 79754b9, `fix/647-branch-left-bump-leg-setup` be1bff9 (#657), `fix/648-bootstrap-and-gate-order` 852c5c4 | **clean**, all four |

- **The Store stub check.** See the decision on line 1. `--preflight` is where a stub is told apart.
- **`install.sh`'s own lines.** It names a Windows-spelled `$OPENREPOSHAPE_BIN_DIR` as typed in its `preflight:` and `done:` lines, though the command it installs now prints `/d/…`. #649 names `--install`'s PATH note, and `install.sh` prints no PATH line. Checklist candidate 1.
- **`--help`.** Its `$OPENREPOSHAPE_REPO` and `$OPENREPOSHAPE_BIN_DIR` lines stay true, so `docs/cli.md` is not regenerated.
- **`D:`, `D:tools` and UNC values.** Checklist candidate 3.

1. **`install.sh` echoes a Windows-spelled `$OPENREPOSHAPE_BIN_DIR` as typed.** It builds `target="$bin_dir/openRepoShape"` from the raw value, so in Git Bash with `D:\tools\bin` its `preflight:` and `done:` lines say `D:\tools\bin/openRepoShape`, while the command's own lines say `/d/tools/bin/openRepoShape`. Both spellings work in Git Bash, so nothing breaks. On a machine with no `cygpath`, the command's `--install` now refuses the value, and `install.sh` stops with "the <version> openRepoShape's --install refused (exit 2); its own refusal above says why", which is right but could refuse earlier, before the fetch.
2. **The PATH note is not quoted for every directory.** `--install` prints `export PATH="$dir:$PATH"` with the directory between double quotes as it is. A directory holding `"`, `$`, a backquote or `\` prints a line that does not do what it says, and one holding `:` cannot be on `$PATH` at all. A drive letter was the one case a person in Git Bash meets by default; the rest need a deliberately odd directory name.
3. **Two kinds of value slip past both new arms** (rv649). `$OPENREPOSHAPE_BIN_DIR` set to `D:` or `D:tools` matches neither respelling arm, so it is kept as typed and prints `export PATH="D:tools:$PATH"`, as on the base. A UNC `\\server\share\…`, as `$OPENREPOSHAPE_BIN_DIR` or `$OPENREPOSHAPE_REPO`, is never respelled (the PATH note's `\\` collapses) or becomes a github.com URL. The package's `clone_url` passes a UNC path through on Windows (`os.path.isabs`).

Review: rv649 found 0 blockers, 1 should-fix and 6 nits on d91b314 (CHANGES NEEDED); the fix round e85573a answers all seven, so both mutations that survived now fail a test, and lane 5's coordinator checked it. CI (Brett Heap's ruling 27, with his fast-refresh ruling of 2026-10-15T22:48:35Z): H0 e85573a passed tests (Linux), tests-git-bash (windows-latest) and SonarCloud; the head 022bd50 is a clean update-branch merge of main 8062810, with SonarCloud, tests-git-bash and package green on it, and the local gate passed on the head itself: the launcher, installer and package set, 220 passed, and the hygiene, workflow and README-section set, 214 passed. Landed on Brett Heap's word of 2026-10-16T13:07:34Z, "merge #656 and #657 when green".

Closes #649

Lane: openreposhape-5 (openRepoShape-5)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Oct 17, 2026
… symlink and a looping symlink named `.yml`, and both of siblings.py's headers print the holder's root and folder spelled, so the `S_ISREG` child check made `lambda mode: True`, or either path printed raw, now fails a test (#664) (#706)

Tests only: no product file, pin, digest or template copy changes. Two mutants survived review, and each is now killed.

validate-pins.py, the assembly-root template (#434, #633). `_workflow_refs` skips a child of `.github/workflows/` that is no regular file, by `_is_a(path, stat.S_ISREG)`. Replacing the kind with `lambda mode: True` left every validate-pins suite that reads the scan passing (130 tests on main `8062810d`), though over a directory named `.github/workflows/sub.yml` the gate then refused a tree with nothing unreadable in it: `REFUSED workflow-unreadable: .github/workflows/sub.yml: cannot be read (Is a directory)`. A directory named `sub.yml` and one named `sub.yaml` are now each skipped: the scan returns the references it returned before the entry was made, the leg's among them, and the run says `pins ok`. A dangling and a looping symlink are the same row, and kill a different mutant than that one: `_is_a` answers False to a `stat` that found nothing (ENOENT, ELOOP) before it asks the kind, so the symlinks pass under `lambda mode: True`, and fail under a child check that lets that `OSError` through (a bare `path.stat()`), which refuses `dangling.yml` as `cannot be read (No such file or directory)`. The rows are mode-free, so they run as root and on every platform; the symlink rows skip where a link cannot be made.

siblings.py, the family-root template (#454, #642). Both reports open with `siblings: <name> (<root>) — N member(s)` and a `family folder` line, each spelled. `tests/test_siblings_family_name.py` always stands its holder at a plain path, so printing the root raw in the placement header left all 150 of its tests passing, and so did each of the other three spellings (the dispatch header's root, and the `family folder` line of each run). The holder now stands under a folder named with U+2028, U+202E or ESC (ESC skips on Windows, which refuses a control character in a name), at `<tmp>/<folder>/InkRouter/InkRouter`, so the run still passes its folder check. For the placement and the dispatch dry run alike, the first line and the `family folder` line hold the path with the character written out, the last line is the run's own, and no character a terminal must not be handed is printed.

Each mutant was applied to the product file, failed the new rows, and was restored to the committed bytes (checked by sha256).

Closes #664

Lane: openreposhape-4 (openRepoShape-4)
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 added a commit that referenced this pull request Oct 17, 2026
… `--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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment