Repository navigation
bump-leg.py and setup-project.py: the git switch -c line says under 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
Conversation
… it what to do where an earlier act left that branch, so `already exists` is no dead end for a bump or a family's `add` (#647) #607 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): 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 of this standard still printed a `switch -c` and said nothing more. - `scripts/bump-leg.py`: the sentence is its own constant, `BRANCH_LEFT`, with "the project" where the others say "the holder". It follows the command in all three places `_switch_command` is named: after `and re-run.` in the `bump-leg-on-tracking-branch` remedy, after the `bump-leg-root-detached` remedy, and as the last line of the dry run's hint (#477), under it. - `setup-project.py`: the same sentence, word for word, as one more comment line under the family block's `switch -c` line, again its own constant. No command line changes, and none is quoted differently: the sentence is words, not a second command. Four existing tests that pinned the old last lines of those outputs take the sentence (`test_bump_leg_dry_run_tracking.py`, `test_bump_leg_paste_lines.py`, `test_bump_leg_dry_run_truth.py`). The new `tests/test_branch_left_bump_leg_setup.py` types each printed line in a real local project and a real local holder where an earlier act left the branch, shows `already exists` (exit 128), and follows each choice the sentence gives. Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…up.py: #607's equality test holds all four copies of the sentence, so setup-project.py or bump-leg.py cannot drift from family.py's words unseen (#647) #607 landed (95780dd) after the first commit, so the sentence is no longer pinned a second time in the new test module. `test_the_two_tools_say_the_same_words` becomes `test_the_tools_say_the_same_words` and also asserts that `setup-project.py`'s `BRANCH_LEFT` equals `family.py`'s and the holder's word for word, and that `bump-leg.py`'s equals it with "the project" for "the holder". The new module drops its own copies of that test and of #607's typing helpers, and imports them from `test_family_switch_branch_exists.py`. Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
(lane openRepoShape-5, 2026-10-16T12:22Z) @codex review |
Reviewer's GuideThe PR adds a standardized explanation and tested recovery options wherever bump-leg.py and setup-project.py print Flow diagram for existing-branch recovery guidanceflowchart TD
A[Tool prints git switch -c] --> B{Branch already exists?}
B -->|No| C[Create branch and continue]
B -->|Yes| D[Use another branch name]
B -->|Branch contains unmerged work| E[Use git switch instead of git switch -c]
D --> F[New branch starts where the project or holder stands]
E --> G[Return to existing branch and keep its commits]
File-Level Changes
Assessment against linked issues
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
|
Codex Review: Didn't find any major issues. Bravo. Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
|
|
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 8 hours and 5 minutes by commenting |
…-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>
(c6122e8) under #646's move back (#646 x #611) No conflict. main moved from a524542 to 35133f2 (#623, #637, #622, #620, #628, #639, #621, #627, #657, #656). Of those, #637, #622 and #639 change scripts/family.py, in hunks git merged on its own; none changes this branch's three test files. Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
main gained #628 (#600), #639 (#611), #621 (#602), #627 (#606, lane 2's README and handbook), #657 (#647), #656 (#649) and #698. README.md, adopt-project.py and every other file merged as text; docs/handbook.html conflicted at eight marker hunks and nowhere else: - the seven `Starting a project in a new organisation` markers, and the `The three legs` markers in the masthead, `ci` and `legs` that sit in the same hunks: this branch and #627 each moved them for their own README edits; - the one `Adopting an existing repository` marker. Every hunk took this branch's side, and then every marker was recomputed from the merged README.md with tests/test_repo_hygiene.py's `readme_segments` and `readme_fingerprint`: Starting (7) 45a3f886, The three legs (3) 1d9f5bf9, Adopting (1) a7e9e29f. The page's text holds both sides' edits: #627's handbook changes merged as text beside this branch's, so each moved marker reads a section whose page text was amended by the pull request that changed it. Caps: README.md is 1473 lines, its cap: 1422 + 2 (460) + 5 (550) + 12 (563) + 22 (564) + 10 (606). AGENTS.md is 494, its cap. test_repo_hygiene.py and test_readme_sections.py pass (154). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…KEEPS ITS NAME (which `--project` must be), ITS IDENTITY AND ITS FULL HISTORY, and the notes are current to main de92614, with seven more pull requests, lane 2's re-run-`plan` line, lanes 4's and 1's known limits, Developer Mode for a Git Bash symlink and the trusted publishers (#563, L4-47) - L4-47 (#457, lane 4 on #630 at 14:13:32Z): README.md:771-772 and docs/handbook.html:1276-1277 say "it KEEPS ITS NAME (which `--project` must be), ITS IDENTITY AND ITS FULL HISTORY, and becomes the assembly root". Two lines each, no line added: README.md is still 1473, its cap. The § Adopting marker moves to b23e9f98, text and marker; no other marker moves. - The notes, "What changed since v1.0.1": #628 (#600), with lane 2's line for a plan written before v1.1.0 (#473, 12:53:40Z) after it, and #621 (#602) under adopt-project.py; #657 (#647) under update-shape.py, bump-leg.py and a project's own scripts; #639 (#611) under scaffold, setup, family and the shared readers; #656 (#649), #698 (#679) and #627 (#606) under install, CI and the docs. Every pull request since bfed143 is cited (`git log --first-parent bfed143..de92614`, 74). - Known limits: lane openRepoShape-4's ten lines (#630 issuecomment-6217406337: L4-05, -06, -07, -08, -17, -43, -45, -54, -24, -156) and lane openRepoShape-1's twenty-six D9 lines (#457 issuecomment-6214768828), as two lists, one sentence each; and the Git Bash limit says that Windows lets a non-administrator make a symlink only in Developer Mode, as #596 does. - How this release is cut: the trusted publishers are filled from Brett Heap's 13:04:43Z ("the pypi are added") and 13:07:56Z ("they match what you listed"). - The counts (74 pull requests; 83 of 135 milestone issues closed at 17:16Z), the "current to main" sha and the open list (52) are at main de92614, still placeholders for tag time. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e no bytecode under templates/, the launcher guard reads family.py's siblings child, the quoting-sentence inventory follows an aliased quoter, a suffixless tool and each tool's calling functions, and an S5778 pytest.raises builds its argument first (#665) (#724) Four guards held less than they said. One commit, `151a0861`, on `8062810d`. No shipped behaviour changes except one environment variable in the generator's `--help` children. No `--help` text changes, so `docs/cli.md` is not regenerated. No quoting changes. 1. **`scripts/render-cli-reference.py`: `child_env()` sets `PYTHONDONTWRITEBYTECODE=1`.** The template's `siblings.py` does `from bootstrap import ...` from its own directory, so the generator's `--help` child wrote `templates/family-root/scripts/__pycache__/bootstrap.cpython-312.pyc` (ignored by git, so silent). A person who runs the generator now gets the same tree as the test that runs it. The docstring says why. `tests/test_cli_reference.py` gets one test: the variable is in `child_env()` whatever the caller had, and an interpreter started under that environment reports `sys.dont_write_bytecode` as true. 2. **`tests/test_validator_child_encoding.py`: the launcher guard sees `family.py`.** `interpreter_runs` accepts `[sys.executable or PYTHON, ...]` as well as `[sys.executable, ...]`, and `LAUNCHERS` gains `"scripts/family.py": "inherited_child_environment"`. Dropping `env=` from `family.py siblings`' child (#214) now fails the guard. 3. **`tests/test_readme_windows_quoting_sentence.py`: the inventory holds what it claims.** It follows `from repo_shape import shell_line as joined` (and `joined = shell_line`, however many steps). It scans every file `git ls-files` lists (tracked, or new and not ignored) that is a `.py` file or starts with a `python` shebang, which adds the root `bootstrap`. It pins functions, not files, for the two tools the sentence names by particular lines: `scripts/family.py` (`_print_next`, `_switch_command`, `_add_command`, `_print_next_steps`, `_checked_mount_path`) and `setup-project.py` (`family_next_command`). `shape-doctor.py` stays every line, because the sentence says "`shape-doctor.py`'s lines". Only the bodies of `shell_quoted` and `shell_line` in `scripts/repo_shape.py` are exempt, by name; the rest of that file is not. Nothing about quoting changes: the Git Bash dialect (option (a)) is its own v1.1.1 issue, and when it moves `NAMED`, this table moves with it. 4. **`tests/test_bump_leg_paste_lines.py`: Sonar S5778.** `placement = bump_leg_module.leg_placement(leg)` before the `with pytest.raises(LegNotAsFound)` in `test_pasting_the_status_line_reads_that_leg`, as the test above it does. Five lines; the hunk is :401-405, away from #657's hunks. **The reproduction (`.pyc`).** In a fresh shared clone at `8062810d`, with `PYTHONDONTWRITEBYTECODE` unset, `python3 scripts/render-cli-reference.py --check` printed `[ok] docs/cli.md is what the generator produces` and left five `.pyc` files, among them `templates/family-root/scripts/__pycache__/bootstrap.cpython-312.pyc` at 11533 bytes (the size the issue reports). The same command on `151a0861` printed the same `[ok]` line and left none (`find . -name '*.pyc'` found 0). **Mutants.** Each is applied to a copy at the head's test files; "old" is the same mutant under `8062810d`'s own tests. | Mutant | New tests | Old tests | |---|---|---| | M0: drop `PYTHONDONTWRITEBYTECODE` from `child_env()` (the base's behaviour) | `test_child_env_leaves_no_bytecode_in_the_trees_it_runs` fails: `KeyError: 'PYTHONDONTWRITEBYTECODE'` | none to run | | M1: drop `env=` from `family.py`'s `siblings` child (`scripts/family.py:6543`) | `test_the_tools_that_run_the_projects_validators_give_them_an_environment[scripts/family.py]` fails: ``scripts/family.py:6543 runs the interpreter without `env=inherited_child_environment()` `` | 55 passed (survives) | | V2: `from repo_shape import shell_line as joined` and a call, in `update-shape.py` | `test_the_tools_that_quote_for_the_host_...` fails: files are `[family, setup-project, shape-doctor, update-shape]` | 3 passed (survives) | | V3: `family.py`'s `_switch_command` calls `shell_line(..., "posix")` | `test_the_functions_that_quote_for_the_host_...[scripts/family.py]` fails: `_switch_command` missing from the calling functions | 3 passed (survives) | | V5: a host-quoter call in the suffixless `bootstrap` | the file test fails: `bootstrap` is now among the files | 3 passed (survives) | | V6 (the exemption): a third function of `scripts/repo_shape.py` calls `shell_line` | the file test fails: `scripts/repo_shape.py` is now among the files | 3 passed (survives) | Each new failure is one test, and the other tests of the file stay green (`1 failed, 8 passed`; `1 failed, 55 passed`). V2, V3 and V5 are R588's mutants as the issue describes them. On `151a0861`, one run, `run9.sh` (gh stub first on PATH, `GH_TOKEN` bogus, empty `GH_CONFIG_DIR`): `tests/test_cli_reference.py tests/test_validator_child_encoding.py tests/test_readme_windows_quoting_sentence.py tests/test_bump_leg_paste_lines.py tests/test_family_siblings_placement.py tests/test_repo_hygiene.py`: **227 passed in 698.59s**. Per file: `test_cli_reference.py` 23 (one new), `test_validator_child_encoding.py` 56 (one new case, `[scripts/family.py]`), `test_readme_windows_quoting_sentence.py` 9 (6 new: the `scripts/family.py` and `setup-project.py` function pins, the alias, the method name, the exemption, the suffixless file), `test_bump_leg_paste_lines.py` 33, `test_family_siblings_placement.py` and `test_repo_hygiene.py` 106 together. - **origin/main `35133f29`** (includes #657 as `78fe3fff`, which edits `tests/test_bump_leg_paste_lines.py` at :53, :235-244 and `setup-project.py`): clean. On the predicted merge commit, `test_readme_windows_quoting_sentence.py` and `test_validator_child_encoding.py` pass (65 passed), and so does `test_bump_leg_paste_lines.py` (33 passed). `family_next_command` is still the only function in `setup-project.py` that calls `shell_word`. - **#630** (`5b140bd3`): conflicts in `docs/handbook.html` only, and it does so against `8062810d` and `35133f29` without this commit, so not this change's. It changes `scripts/render-cli-reference.py`, `docs/cli.md` and `tests/test_repo_hygiene.py`; none of those hunks overlaps mine, and `merge-tree` reports no conflict in them. - **Lane 4's `family.py` work:** the heads I could fetch are #670 (`pr670`) and #671; `fix/664-surviving-mutants` and `fix/696-setup-family-folder` are on origin too. Clean with each. Lane 4's other `family.py` branches (#684, #686, #688, #689, #693, #674) are not on origin and not in the shared `.git`, so I could not check them. Predicted merges with #670, #671 and lane 5's `fix/646` pass the two guards' files (65 passed each). - **Other open heads touching a tool the inventory reads:** #703 and `fix/696` (`setup-project.py`), #699 (`adopt-project.py`), #700, #701, #702 and #705 (`shape-doctor.py`), `fix/664`: clean, and the inventory file passes on each predicted merge (9 passed each). #682, #698 and #706: clean. - **No quoting change.** The Git Bash dialect (option (a)) is a separate v1.1.1 issue; the inventory only holds what the sentence says today. - **`docs/release-notes/v1.0.0.md:532`** says the `.pyc` is left by `test_cli_reference.py`'s children. That is true of v1.0.0 and I left it. The v1.1.0 notes (#630) may want one line. - **`--others` in the file listing.** A new, untracked tool is scanned before it is added, as `rglob` did; a tracked file deleted from the work tree is skipped. - **A quoter handed on as a value is not a call.** `map(shell_quoted, words)` or `functools.partial(shell_line)` is not seen by the inventory (said in `host_calls`' docstring). Nothing does it today. - **The function pin counts functions, not calls.** A function with two host-dialect calls where only one is switched to POSIX still passes. Today the one-call functions are `_switch_command`, `_print_next`, `_checked_mount_path`. - **`_print_next_steps` prints a `cd {shell_quoted(work)}` line** the sentence's list (`NEXT`, `switch -c`, `add`, `git rm -r --cached`) does not name. It belongs to the Git Bash dialect issue. - **The launcher guard's `LAUNCHERS` still has no row for `src/openreposhape/cli.py` (`run_child([sys.executable, ...], env)`) or `setup-project.py` (its own `run` with a `PYTHON` variable).** Both spell the child differently from `subprocess.run([sys.executable, ...])`. Review: lane 5's independent review approved 151a086 with nits only, and codex reviewed it. CI (Brett Heap's ruling 27 and his fast-refresh ruling of 2026-10-15T22:48:35Z): H0 151a086 passed tests, tests-git-bash and SonarCloud; the head 949fd4d 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 9cd4767 over main 974d482: the bump_leg_paste_lines, cli_reference, readme_windows_quoting_sentence, validator_child_encoding, adopt_plan_leg_path_help, repo_hygiene, agents_md_codes sets, 290 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 #665 Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…dows guards on `make_file` and the `--branch a\udcffb` test by emulation, and the UTF-8 promise and the leg finding's role pinned (#663) (#723) Test only. No tool and no template changes. `templates/assembly-root/scripts/validate-manifest.py` is a pinned copy and is not edited. **No Windows run was made, and no macOS run.** CI is Linux only since #597. What Windows and macOS answer is emulated: `os.fsencode` is replaced for one test by Windows' (`text.encode("utf-8", "surrogatepass")`, three bytes for U+DCFF), and macOS's EILSEQ by a `mkdir` that answers it. The review of `0b1d1ddb` found no MUST and one SHOULD and two NITs. All three are taken, in one commit, `6ba8c5d2`, in three test files: `tests/conftest.py`, `tests/test_non_utf8_name_helpers.py` and `tests/test_non_utf8_git_reads.py`. No tool file changed. - **SHOULD-1: `skip_message` had no test of its own.** - A skip that escapes `skip_message` ends the test SKIPPED, and no run fails on a skip. The review's mutant SK1 (a skip escapes) gave `10 passed, 3 skipped` on the helpers file, so seven emulation tests skipped silently. - `tests/test_non_utf8_name_helpers.py` now holds it with three tests: it returns the reason a call skips with (`skip_message(lambda: pytest.skip("x")) == "x"`), it returns None where the call does not skip, and it lets any other exception go on (`ZeroDivisionError`, raised by a small function and not by `1 / 0`, so no analyser reads a literal division by zero). - The first test catches an escaped skip and fails it with `pytest.fail`. A bare assert would not do: the escaped skip would end that test SKIPPED too, and SK1 would still pass. - SK1 now gives `1 failed, 14 passed, 3 skipped` (before: `10 passed, 3 skipped`), and the one failure is `test_skip_message_returns_the_reason_a_call_skips_with`: `Failed: the skip escaped skip_message, which would end the test SKIPPED, and no run fails on that`. - **NIT-2: `windows_fsencode` took `str` only.** Real `os.fsencode` passes bytes through. `windows_fsencode` in `tests/conftest.py` now does (`if isinstance(text, bytes): return text`). Two tests hold it: a surrogate gives `b"\xed\xb3\xbf"` and text gives UTF-8, and bytes come back as they went in, compared with `os.fsencode` itself. Without the pass-through: `AttributeError: 'bytes' object has no attribute 'encode'`. - **NIT-1: the `"\\x" not in shown` assertion on a Latin-1 file name encoding.** - There `make_file` writes `café.yml` as the single byte 0xE9 and the tool, rightly, prints `caf\xe9.yml`, so the assertion would fail on a tool that is right. - It is now `assert_shown_without_hex_escapes(shown, output)` in `tests/test_non_utf8_git_reads.py`. It skips where `os.fsencode(UTF8) != UTF8.encode("utf-8")` and then asserts. - The test calls it AFTER its other assertions (`.yml` ending, no traceback, nothing moved). The assertion moved from the middle to the end, so a skip loses none of the others. - Two tests hold it: under an emulated Latin-1 `os.fsencode` it skips, and under Windows' (emulated) it does not skip, still fails a line that holds `\xc3\xa9`, and passes `café.yml`. - Where the file name encoding cannot write `café.yml` at all (ASCII), `make_file` skipped already. ### Fix-round tests (head `6ba8c5d2`, `/usr/bin/python3`, gh stubbed, targeted files only) | file | before the round (`0b1d1ddb`) | after | |---|---|---| | `tests/test_non_utf8_name_helpers.py` | 13 | **18 passed** | | `tests/test_non_utf8_git_reads.py` | 50 | **52 passed** | | `tests/test_bootstrap_branch_bytes.py` | 61 | **61 passed** | | `tests/test_repo_hygiene.py` | 77 | **77 passed** | That is 208 passed in two runs (156 in 14.01s and 52 in 19.40s), and no `-rs` line printed a skip. `tests/test_adopt_pins.py` and `tests/test_pin_source_unreadable.py` were not re-run in this round (227 passed on `0b1d1ddb`): the round changes nothing they import, and neither file uses `windows_fsencode` or `skip_message`. ### Fix-round mutants Each in a throwaway clone, one change at a time, with the new test files in place. | mutant | result | |---|---| | **SK1**: a skip escapes `skip_message` (the `try` removed) | `1 failed, 14 passed, 3 skipped`: `test_skip_message_returns_the_reason_a_call_skips_with` | | **SK2**: `skip_message` swallows every other exception | `4 failed, 14 passed`: `DID NOT RAISE ZeroDivisionError`, and the three OSError rows | | **SK3**: `skip_message` returns None where a call skips | `4 failed, 14 passed` | | **SK4**: `skip_message` returns `""` where a call does not skip | `8 failed, 10 passed` | | **FE1**: `windows_fsencode` without the bytes pass-through | `1 failed, 17 passed`: `AttributeError: 'bytes' object has no attribute 'encode'` | | **FE2**: `windows_fsencode` without `surrogatepass` | `3 failed, 15 passed` | | **HX1**: the skip removed from `assert_shown_without_hex_escapes` | `1 failed, 1 passed` (`-k hex_escape`): the Latin-1 emulation, `'\\x' is contained here: M .github/workflows/caf\xe9.yml` | | **HX2**: the assert removed from it | `1 failed, 1 passed`: `DID NOT RAISE AssertionError` | | **HX3**: its condition inverted | `2 failed` | | **R1** again, with the call in place: `scripts/bump-leg.py:1126` decodes with `"ascii"` | `2 failed, 4 passed` (`-k "utf8_under_quote_path_false or hex_escape"`): `[bump]` and `[dry-run]`, `'\\x' is contained here: M .github/workflows/caf\xc3\xa9.yml` | Not probed: the real test's call to the helper. Dropping it drops the check, and a Latin-1 host is not here to show it. R1 failing with the call in place is the evidence that the call asserts. **Also measured**, on a host emulated as ASCII (`PYTHONUTF8=0 LC_ALL=C PYTHONCOERCECLOCALE=0`, `sys.getfilesystemencoding()` is `ascii`): the helpers file gives `18 passed`, and the two utf8 rows skip in `make_file` (`'ascii' codec can't encode character '\xe9'`). No Latin-1 locale is installed, so the Latin-1 host itself is emulated by `os.fsencode` only, not run. ### Merge check `git merge-tree --write-tree HEAD origin/main` (`2fac9790`): clean, rc 0, tree `3e71db8d`. The other branches below were checked at `0b1d1ddb`; this round adds seven tests, one helper function and a small change to `windows_fsencode`, and was not re-run against them. Base: origin/main `8062810d`. Branch head: `6ba8c5d2` (three commits: `d6d1df23` and `0b1d1ddb`, then the fix round). 1. **L2-52, one helper.** `tests/conftest.py` holds `skip_where_a_name_cannot_be_not_utf_8(directory)`. `tests/test_adopt_pins.py` and `tests/test_pin_source_unreadable.py` each had a copy of the body; both copies are gone and both call the shared one. It asks two questions, in this order: - `skip_where_fsencode_gives_a_surrogate_no_byte(text)`: skips where `text` holds a byte-carrying surrogate (U+DC80 to U+DCFF) and `len(os.fsencode("\udcff")) != 1`. That is Windows. - The disk: it makes `probe\udcff` under the directory and removes it. EILSEQ (macOS) skips; any other `OSError` is raised. 2. **`make_file` (R423 N1).** `tests/test_non_utf8_git_reads.py` calls the first question before it writes, for a name that holds the byte. A name that is text (`café.yml`) is still written. The `try/except` stays, for APFS. The module docstring and `make_file`'s docstring now say who skips and why. 3. **The `--branch a\udcffb` test (R455 F1).** `tests/test_bootstrap_branch_bytes.py` calls the shared helper first, in place of its own probe of a file. `test_a_branch_the_system_carries_has_no_fault` asked the same `os.fsencode` question inline; it now asks the same function, so the rule is written once. The file's own emulated test `test_where_a_surrogate_is_three_bytes_and_no_byte_it_is_refused` keeps the Windows half, unchanged. 4. **The UTF-8 promise (R423 N2, mutant R1).** `test_a_name_that_is_utf8_under_quote_path_false_is_still_refused` takes the one stderr line that starts ` M .github/workflows/caf` and asserts it holds no literal `\x`, through `assert_shown_without_hex_escapes`, which skips where this system's file name encoding is not UTF-8 (fix round, NIT-1). A locale decode of UTF-8 `é` is mojibake at worst and never a `\x`, so the ASCII-ends caution stays. 5. **The leg finding's name (R455 F3, mutant R2).** A unit test asks `_leg_branch_findings([{"role": "spec", "path": "contracts", "branch": "a\x00b"}])` and holds that the line starts `FINDING manifest-leg-branch: leg 'spec': branch is 'a\x00b', which holds U+0000, a NUL` and does not hold `contracts`. It runs for `spec` and `code`, on `validate-manifest.py` loaded by `load_without_bytecode`. The file's last test now takes the module from a module fixture, `validator_module`, which the new test shares. 6. **New file `tests/test_non_utf8_name_helpers.py`** (18 tests: 13 in the first round, 5 in the fix round) holds the helpers to this by emulation: - the system is asked first, and the disk is never touched where `os.fsencode` gives three bytes; - EILSEQ skips; EACCES, ENOSPC and EEXIST are raised; - the probe is removed; - text and a surrogate with no byte are not skipped; - Linux is not skipped; - (fix round) `skip_message` and `windows_fsencode` are held too. 7. **Wiring tests under emulation.** The `--branch` test and the no-fault test are each called, as they are, under Windows' `os.fsencode`, and must skip. `make_file` is called the same way: it skips and writes nothing, and a UTF-8 name is still written. Shared helpers in `tests/conftest.py`, all in one block after `a_hook_that_unstages`, away from `fix/645-fixture-copy-maintenance-race`'s hunks at :85 and :137: `NOT_UTF_8_PROBE`, `skip_where_fsencode_gives_a_surrogate_no_byte`, `skip_where_a_name_cannot_be_not_utf_8`, `windows_fsencode` (which passes bytes through, as `os.fsencode` does) and `skip_message`. `skip_message(call, *args)` returns the reason a call skips with, or None. A bare `pytest.raises(pytest.skip.Exception)` is not enough for a test of a skip helper: a helper that skips where it should not ends the test SKIPPED, and no run fails on a skip. - **Two functions, not one.** `make_file` has no directory to probe: its write is the probe. So the system's question is its own function, and the directory helper calls it first. A change to what Windows answers is made once. - **The system first, then the disk.** On Windows the disk takes the name, so a skip that waited for the disk would never come. `test_where_fsencode_makes_three_bytes_the_disk_is_not_asked` passes a directory that does not exist: an `mkdir` under it would raise FileNotFoundError, and the skip is the proof the disk was not asked. - **I read the Windows facts, I did not run them.** The conftest docstring says "most likely" and "read, not run", as the issue does. - **`windows_fsencode` is in conftest.** `test_where_a_surrogate_is_three_bytes_and_no_byte_it_is_refused` has the same lambda inline. I left it, to keep the hunks small. - **A skip test must catch an escaped skip (fix round).** `pytest.fail` in an `except pytest.skip.Exception` is the only way the test for `skip_message` can fail on SK1. - **The Latin-1 check is a function (fix round).** Inline in a test that needs the scaffold fixtures it could not be shown to skip. As `assert_shown_without_hex_escapes` it is called with an emulated `os.fsencode`, and fails a test when its skip or its assertion is removed. | file | result | |---|---| | `tests/test_non_utf8_git_reads.py` | 50 passed (47 on base) | | `tests/test_bootstrap_branch_bytes.py` | 61 passed (54 on base) | | `tests/test_non_utf8_name_helpers.py` | 13 passed (new) | | `tests/test_adopt_pins.py` + `tests/test_pin_source_unreadable.py` | 227 passed (146 + 81, no change in count) | | `tests/test_repo_hygiene.py` | 77 passed | No test skipped on Linux (`-rs` printed none). No test file imports `skip_where_*` besides those four and the new file. The fix round's counts are above. Each in a throwaway clone at the head, one change at a time. "Base file" is the file as it was on `8062810d`, run under the mutant. | mutant | the base file | the new tests | |---|---|---| | **R1**: `scripts/bump-leg.py:1126` decodes with `"ascii"` | 47 passed (R423's count) | 2 failed: `test_a_name_that_is_utf8_under_quote_path_false_is_still_refused[bump]` and `[dry-run]`, `'\\x' is contained here: M .github/workflows/caf\xc3\xa9.yml` | | **R2**: `leg.get('path')` in place of `leg.get('role')` | 54 passed (R455's count) | 2 failed: `test_the_leg_finding_names_the_leg_by_its_role_and_not_its_path[spec]` and `[code]`, `leg 'contracts': branch is ...` | | guard removed from `make_file` | | 1 failed: `test_make_file_skips_where_fsencode_makes_no_byte_and_writes_nothing`, `make_file wrote the name instead of skipping` | | helper call removed from the `--branch` test | | 1 failed: `test_the_branch_test_skips_where_fsencode_makes_no_byte_of_the_surrogate` | | guard removed from the no-fault test | | 2 failed: the two skipped rows of `test_the_no_fault_test_skips_where_fsencode_makes_no_byte_of_the_branch` | | guard removed from the shared helper | | 1 failed: `..._the_disk_is_not_asked` (FileNotFoundError) | | disk asked before the system | | 1 failed: the same test | | helper skips on any `OSError` | | 3 failed: EACCES, ENOSPC and EEXIST rows of `test_any_other_error_from_the_disk_is_the_tests_own` (`DID NOT RAISE OSError`) | | probe not removed | | 1 failed: `test_a_disk_that_takes_the_name_is_not_skipped_and_the_probe_is_removed` | | guard skips any text | | 5 failed: the five rows of `test_where_fsencode_makes_three_bytes_a_name_that_is_text_is_not_skipped` | | guard inverted (`== 1`) | | 8 failed | All rows are local; no `gh`, no network. The issue's rows are about what a test does on Windows or macOS, so they are shown by emulation on Linux, and by the mutants above (a mutant that drops a guard is what base did): - **L2-52:** `git grep -n 'skip_where_a_name_cannot_be_not_utf_8\|skip_where_no_file_name_is_not_utf_8'` on base shows two bodies; on the head it shows one in `tests/conftest.py` and four call sites. - **N1, F1:** on base, the guard-removed mutants are what a Windows run did: `make_file` writes `caf\udce9.yml` and the branch test runs on. On the head both skip under Windows' `os.fsencode`. - **N2, F3:** R1 and R2, above. `git merge-tree --write-tree HEAD <ref>`, with the refs fetched read-only (and deleted after): - **origin/main `de926142`: clean.** Main had moved since `8062810d` by #657 (`scripts/bump-leg.py`), the Git Bash change and a doctor test; none touches these files. (The fix round's check against `2fac9790` is above.) - **`fix/645-fixture-copy-maintenance-race`: clean** (its conftest hunks are at :85 and :141). - **`fix/651-adopt-annotated-tag-pin`** (lane 1; edits `tests/test_adopt_pins.py`): clean. - **`fix/664-surviving-mutants`** (lane 4; touches none of these files): clean. - **`fix/655-adopt-doctor-plan-overrides`** (edits `validate-manifest.py`, which I do not): clean. - **Every other branch on origin** (`fix/388`, `389`, `417`, `425`, `610`, `646`, `648`, `650`, `653`, `658`, `659`, `696`, `l3-668`, `669`, `677`, `678`, `l3-rehearsal-v1.1.0`): clean. - `docs/release-notes-v1.0.0` and `l3-563-docs` conflict (`docs/release-notes/v1.0.0.md`, `docs/handbook.html`); they conflict with origin/main alone, so it is not this change. - **Not checked:** lane 4's `fix/674-update-shape-apply` was not on origin yet, and no `l1-*` branch existed on origin. - Four more tests make a directory with a non-UTF-8 byte and skip on failure: `tests/test_stdout_encoding.py:209`, `tests/test_adopt_git_reads.py:431`, `tests/test_adopt_template_values.py:296` and `tests/test_clone_url_bytes.py:143` (`directory_named`). They build the name from bytes, which raises on Windows, so they skip there already. They are not in #663's lines. - `test_where_a_surrogate_is_three_bytes_and_no_byte_it_is_refused` keeps its inline lambda. - **The other four byte-named-directory probes could use one helper.** The four above each write their own "make a directory with a byte that is not UTF-8, or skip". Folding them into `tests/conftest.py` next to `skip_where_a_name_cannot_be_not_utf_8` would give a fifth name for the same question one place to change. v1.1.1, optional. - **`test_make_file_writes_a_name_that_is_utf8_where_fsencode_makes_no_byte` fails on an ASCII file name encoding.** Found in the fix round, and it was so at `0b1d1ddb`: on a host with `PYTHONUTF8=0 LC_ALL=C PYTHONCOERCECLOCALE=0` (`sys.getfilesystemencoding()` is `ascii`), `make_file` cannot write `café.yml`, skips with `this filesystem cannot hold a file name that is not UTF-8`, and the test's `assert reason is None` fails (`1 failed, 2 passed` for `-k make_file`). Nothing in CI has that encoding (Linux, macOS and Windows are UTF-8 by default), and a host needs three opt-outs to get it. The direction is the neighbouring control test's own two lines: `if reason is not None and "filesystem" in reason: pytest.skip(reason)`. v1.1.1, optional. Review: lane 5's independent review approved 0b1d1dd with nits; 6ba8c5d adds its SHOULD-1, a self-test of skip_message. CI (Brett Heap's ruling 27 and his fast-refresh ruling of 2026-10-15T22:48:35Z): H0 6ba8c5d passed tests, tests-git-bash and SonarCloud; the head 4cdb427 is a clean update-branch merge of main f6d17bc, with tests-git-bash and SonarCloud green; and the local gate passed on its predicted squash tree bf8dda2 over main f6d17bc: the adopt_pins, bootstrap_branch_bytes, non_utf8_git_reads, non_utf8_name_helpers, pin_source_unreadable, repo_hygiene, agents_md_codes, cli_reference sets, 505 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 #663 Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>



Lane: openreposhape-5 (openRepoShape-5)
Claim: #647 (comment)
What changed and why
#607 (#637) gave
family.pyand the holder'svalidate-family.pyone sentence for aswitch -cthat failsfatal: a branch named '<branch>' already exists(exit 128):Two tools still printed a
switch -cand 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_commandis named:bump-leg-on-tracking-branchremedy: afterand re-run.;bump-leg-root-detachedremedy: on the line under it;--dry-run, step 1 of advancing a leg, is refusedbump-leg-on-tracking-branchon every fresh project, before theold -> newline the human is to read #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'sswitch -cline. 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/.Decisions and evidence
and re-run., not between the command andand re-run.The remedy readsRun:, 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 andand re-run.keep their places. The sentence is still under theswitch -cline.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.shlex.splitand compared to the argv it always meant (switch_argv); setup's byhost_words, on a folder with a space and one with a space beside an apostrophe. The block still has exactly two command lines.switch -cremedy says to give it another name, or go back only if it holds work not merged yet, and the docs sayfamily/add-<Project>(#607) #637's equality test, nowtest_the_tools_say_the_same_words, also asserts thatsetup-project.py's constant equalsfamily.py's and the validator's, and that bump-leg's equals it with "the project" for "the holder". It was renamed fromtest_the_two_tools_say_the_same_words, because "two" is no longer true. The project's wording is pinned as text once, asBRANCH_LEFT_PROJECTintests/test_bump_leg_paste_lines.py, beside the lines it follows; the holder's is family.py and validate-family.py: where an earlier act left the branch, theswitch -cremedy says to give it another name, or go back only if it holds work not merged yet, and the docs sayfamily/add-<Project>(#607) #637's pinned copy intests/test_validate_family_branch_step.py.test_the_two_pinned_texts_differ_in_the_one_word_onlyties the two pinned copies together.switch -cremedy says to give it another name, or go back only if it holds work not merged yet, and the docs sayfamily/add-<Project>(#607) #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.Tests
New module
tests/test_branch_left_bump_leg_setup.py, 19 tests: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 leftbump/spec-<id>holding a commit of its own, the printed line failsalready 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, andHEAD:specis the new commit) (3), and going back withswitchforswitch -cworks (back on the branch at its own commit, the bump goes through there on top of it) (3).addline, two command lines (2, one per folder name); a realsetup-project.pyrun prints the sentence once, under the right comment (1); in a real local holder (thefamilyfixture's, on its tracking branch), with a real member: the printed line makes the branch where none is left, and where an earlier try leftfamily/add-<project>with a commit of its own, another name works andfamily.py addgoes through on it, and going back works andaddgoes 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) andtest_bump_leg_dry_run_truth.py(2: the hint's branch name,[local-remote-dir]and[origin]); and, beside them, the extendedtest_the_tools_say_the_same_wordsintest_family_switch_branch_exists.py.Runs, on be1bff9 (BASE a524542 plus main 95780dd), through
run9.sh,ghstub first on PATH:switch -cremedy says to give it another name, or go back only if it holds work not merged yet, and the docs sayfamily/add-<Project>(#607) #637's module:44 passed in 61.90s.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).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), plustest_setup_by_hand_commands,test_setup_project_py,test_setup_sh:592 passed in 506.74s.switch -cremedy says to give it another name, or go back only if it holds work not merged yet, and the docs sayfamily/add-<Project>(#607) #637 landed, on 6ac545f: set 11042 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_wordsfails (noBRANCH_LEFTin either new place), and the seven edited tests fail. The one new test that passes on BASE istest_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 gave20 failed, 1 passed.Reproductions (local bare remotes, the
ghstub first on PATH)A real project (
scaffold-project.pyinto local remotes, a recursive clone, the spec leg advanced on a branch of its remote) and a real holder (family.py init, then a realsetup-project.py Sample --yes --family Orbitrun). An earlier act left the branch with a commit of its own.bump-leg.pyon the tracking branch endsRun:/ theswitch -cline /and re-run.and nothing more. The line typed as printed givesfatal: 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 theswitch -c,# a branch first: add refuses theand# holder's tracking branch, then theadd. Typed,fatal: a branch named 'family/add-sample' already exists, exit 128.<line>-2): exit 0;bump-leg.pygoes through on it, on top of the commit the root stood on;family.py addgoes through on it;switchforswitch -c): exit 0, at the earlier commit;bump-leg.pyandfamily.py addgo through, their commit on top of it.bump/spec-<id>, then went back tomain. There the root showsM spec(the leg is still at the bump's commit), and the nextbump-leg.pyis refusedbump-leg-root-dirtybefore it reaches theswitch -cline. Aftergit submodule updateputs the leg back, the line appears, failsalready exists, and the other name works. See the checklist candidates.Mutation probes
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_wordswas run. Every mutation makes new tests fail:BRANCH_LEFTsays "the holder"BRANCH_LEFTdropsonly ifbump-leg-root-detachedremedy without the sentencedetachedcase of each of four tests)bump-leg-on-tracking-branchremedy without the sentencetrackingcase of each)and re-run.dry-runcase of each)_switch_commandprintsswitch -C-Cmakes a new branch tooBRANCH_LEFTsays "the project"BRANCH_LEFTdropsonly ifThe 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.Merge checks (
git merge-tree --write-tree be1bff90 <ref>)scripts/family.py,scaffold-project.py,docs/cli.md)setup-project.pysteps (3) and (4); landed as abb956e)family_next_commandbump-leg.pyandfamily.py)scripts/family.py, and the same conflict arises against main alone: not from this branch.bump-leg.pymerges clean.bump-leg.py)fix/bump-leg-tag-dry-run)asked = commit); its stale pull head conflicts with main alone, so it is not an open PR to checkLeft out, and why
git switch -c"), README.md:157 and 1011,docs/cli.md(generated; no--helptext changes) and the handbook. None becomes false.write_textwrote, and read setup's lines withhost_words.git switch <branch>fail (family.py and validate-family.py: where an earlier act left the branch, theswitch -cremedy says to give it another name, or go back only if it holds work not merged yet, and the docs sayfamily/add-<Project>(#607) #637's "Left out", N1); the sentence is not made longer for it.scripts/bump-leg.py's comment near :1164 still says "the three tools follow one rule"; there are four copies of the sentence now, andsetup-project.py's comment names two. Left for thebump-leg.pyissue filed from Tracking: update-shape.py and bump-leg.py, keeping a project's shape copies and leg pins current #458's lines, which lands after this and edits that file._switch_commandcaller to the sentence, as family.py and validate-family.py: where an earlier act left the branch, theswitch -cremedy says to give it another name, or go back only if it holds work not merged yet, and the docs sayfamily/add-<Project>(#607) #637'stest_every_remedy_that_names_a_switch_line_ends_with_the_sentenceholds the validator's.family.py's_branch_saidprints it, which is what lets the byte-for-byte equality tests hold (decision 1).Checklist candidates (not fixed here)
M spec) andbump-leg-root-dirty's remedy, "commit or stash them first", is wrong advice. The leg is left at the bump's commit andgit switchdoes not move it (README says the leg is left detached at the new commit).git submodule update(ormake 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 theswitch -csentence is reached only after that step in this flow.git switchto the branch that holds the earlier bump leaves the leg behind, so the re-run is refusedbump-leg-root-dirty; aftergit submodule updateit saysnothing to do, and only the push is left.NEXT git -C <root> checkout -b shape/update-<12> && git … commit(rv647, read only): it failsalready existsthe same way, and the&&chain then commits nothing. It ischeckout -b, outside bump-leg.py and setup-project.py: thegit switch -cline fails "already exists" when an earlier act left that branch, and nothing says what to do (#607's sentence, in the two tools #607 left) #647.addand a switch back: another name works for the switch, butfamily.py addis then refusedmember-path-taken, because git left the populatedmembers/<Project>checkout behind (rv647; identical on the base). It bears on family.py and validate-family.py: where an earlier act left the branch, theswitch -cremedy says to give it another name, or go back only if it holds work not merged yet, and the docs sayfamily/add-<Project>(#607) #637's "another name works".Closes #647
🤖 Generated with Claude Code
Summary by Sourcery
Make all relevant branch-creation guidance explain how to recover when the target branch already exists.
New Features:
git switch -cinstruction inbump-leg.pyandsetup-project.pyfor handling branches that already exist.Bug Fixes:
Enhancements:
Tests: