Skip to content

openRepoShape in Git Bash: the machine: row finds python, a drive-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

Merged
brettheap merged 3 commits into
mainfrom
fix/649-git-bash-launcher
Oct 16, 2026

Conversation

@brettheap

@brettheap brettheap commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

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

#596's Git Bash follow-ups, the five lines of #649. Two commits on main a524542: d91b314, the fix, and e85573a, 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).

What changed and why

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). README, AGENTS.md, docs/cli.md and the handbook: pipx install openreposhape is the second way in and the first on native Windows, git-filter-repo is installed in install.sh's order and never with a bare pip install, and a first draft of the v1.1.0 release notes (#563) #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.

Decisions and their evidence

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

Tests

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.

The Windows run

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:

Reproductions

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.

Mutation probes

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

Merge checks (git merge-tree --write-tree, head e85573a, after a fresh fetch)

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

Left out, and why

Checklist candidates

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

Closes #649

🤖 Generated with Claude Code

Summary by Sourcery

Improve Git Bash compatibility for Python detection, repository paths, and installation directories while extending CI coverage for installer behavior.

New Features:

  • Handle Python installations exposed as either python3 or python in the machine report.
  • Preserve Windows drive-letter repository paths and normalize Windows-spelled install directories for Git Bash.
  • Run the installer filter-repository tests in the Git Bash CI job.

Bug Fixes:

  • Prevent drive-letter repository values from being converted into invalid GitHub URLs.
  • Refuse Windows-spelled install directories when they cannot be converted into a Bash-compatible path instead of installing an unusable PATH entry.

Enhancements:

  • Expand command tests to cover Python discovery, repository path handling, and Windows path normalization or refusal across slash styles.
  • Correct the doctor test documentation to reflect its clone and ref-validation order.

CI:

  • Extend the Windows Git Bash test job to run both installer test files.

Tests:

  • Add coverage for Python fallback discovery, drive-letter repository paths, and Git Bash installation directory handling.

…there is no `python3`, a drive-letter `$OPENREPOSHAPE_REPO` is a path, a Windows-spelled `$OPENREPOSHAPE_BIN_DIR` is respelled with `cygpath -u` or refused, and `tests-git-bash` runs #598's installer cases, so Git Bash no longer says `python3 MISSING` beside a passing preflight, clones `https://github.com/C:/...` or prints an `export PATH` bash cannot use (#649)

#596's Git Bash follow-ups, five lines, from #585's writer.

- The `machine:` block `--install` ends with probed `python3` alone. Where
  Python came from python.org, Git Bash has `python` and no `python3`, so
  the row said `python3 MISSING` and pointed at `--preflight`, which then
  passed with `python`. It now asks `command -v` for `python3`, then
  `python`, in `find_python`'s order, and says `present, as python` when
  only `python` answers. The row keeps its name, so README's and the
  handbook's description of the block (`python3`, each `present` or
  `MISSING`) stays true. Still a probe and never a run: the Microsoft
  Store's `python3` stub reads as present, as macOS's Command Line Tools
  stubs already do, and `--preflight` runs the version check that tells a
  stub from a Python. Running that check here would break the block's
  promise to execute nothing but `gh auth status`, and the Store stub opens
  the Store when run bare.
- `shape_clone_url` read `C:/src/openRepoShape` and `C:\src\openRepoShape`
  as an owner/name, so setup.sh and `--doctor` were handed
  `https://github.com/C:/src/openRepoShape.git`. A drive letter followed by
  `/` or `\` now goes through as a path, as `/...` and `....` do, and as the
  package's `clone_url` already reads it. No owner/name holds a colon.
- `--install` named a Windows-spelled `$OPENREPOSHAPE_BIN_DIR` as typed, so
  its PATH note printed `export PATH="D:\tools\bin:$PATH"`, which the
  drive's colon splits, and never found the directory on a `$PATH` that
  holds `/d/tools/bin`. It is respelled with `cygpath -u` first, which Git
  Bash has, so every line names the directory as bash does. Where no
  cygpath answers, the value is refused before anything is placed, with the
  variable to set instead: no line about `$PATH` could work there.
- `tests-git-bash` now runs `tests/test_install_sh_filter_repo.py` too: the
  uv install, the two readinesses and the pasted next steps, which README
  says Git Bash runs "tested in CI", ran on Linux only. actionlint 1.7.12
  passes the workflow.
- `test_a_ref_the_doctor_would_check_out_is_validated`'s docstring said the
  refusal arrives before any clone. The command clones first, then checks
  the ref before `git checkout` is handed it; the docstring now says so,
  and the order is unchanged.

Tests: `test_the_machine_report_finds_a_python_by_either_name` (3 cases),
`test_a_windows_spelled_bin_dir_is_spelled_the_way_bash_spells_it` (2),
`test_a_windows_spelled_bin_dir_is_refused_where_no_cygpath_spells_it` (2)
and `test_a_clone_is_handed_a_path_or_url_as_it_is` (7 spellings through
setup.sh and through `--doctor`, 14). Eleven fail on the base, a524542.
Off Windows a fake `cygpath` stands in for Git Bash's; on Windows the
respelling test uses Git Bash's own on a real drive path.
`install_with_machine` now names its directory with `bash_path`, because
its `$PATH` holds no `cygpath`, and `run_cmd` takes a `cwd`.

Closes #649

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

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-5, 2026-10-16T12:03Z) @codex review

@sourcery-ai

sourcery-ai Bot commented Oct 16, 2026

Copy link
Copy Markdown

Reviewer's Guide

This PR adds Git Bash compatibility for Python discovery, drive-letter repository paths, and Windows-spelled installation directories, with safe cygpath-based normalization and refusal behavior. It also extends the Git Bash CI job to run the full installer coverage and adds focused regression tests for the launcher, setup/doctor clone inputs, installation paths, and command execution context.

File-Level Changes

Change Details Files
Improves Git Bash Python detection in the machine report without executing Python installers or version checks.
  • Checks python3 first, then python using command -v.
  • Reports python3 as present, as python when only python is available.
  • Keeps preflight guidance only when neither interpreter is found.
openRepoShape
tests/test_openreposhape_command.py
Preserves drive-letter repository paths and normalizes Windows-spelled install directories for Git Bash.
  • Treats drive-letter values as paths rather than GitHub owner/name references.
  • Uses cygpath -u to convert Windows BIN_DIR values before installation and PATH reporting.
  • Refuses installation with no placement when conversion is unavailable or fails.
  • Adds isolated tests for conversion, refusal, clone handling, and working-directory behavior.
openRepoShape
tests/test_openreposhape_command.py
Expands Git Bash CI coverage to include the complete installer suite.
  • Adds tests/test_install_sh_filter_repo.py to the tests-git-bash pytest invocation.
  • Documents that the installer suite spans two files.
.github/workflows/tests.yml
Updates test helpers and documentation to reflect Git Bash path handling and actual doctor validation order.
  • Allows subprocess tests to specify cwd.
  • Uses shell-spelled BIN_DIR values in restricted machine-report tests.
  • Corrects the doctor test docstring to state that cloning precedes ref validation.
tests/test_openreposhape_command.py

Assessment against linked issues

Issue Objective Addressed Explanation
#649 Have the machine report recognize Python found as either python3 or python, without running Python during the read-only report. ✅
#649 Treat drive-letter values of OPENREPOSHAPE_REPO as paths, and make --install respell Windows-style OPENREPOSHAPE_BIN_DIR values for Bash or refuse them when respelling is unavailable. ✅
#649 Run tests/test_install_sh_filter_repo.py in the Git Bash CI job so its installer cases are covered there. ✅
#649 Correct the doctor test docstring to describe that the command clones before validating the ref. ✅

Possibly linked issues


Tips and commands

Interacting with Sourcery

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

Customizing Your Experience

Access your dashboard to:

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

Getting Help

@chatgpt-codex-connector

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

Copy link
Copy Markdown

Codex Review Summary

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

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-16T12:07:17.546533Z d91b314 Manual request
ℹ️ About Codex in GitHub

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

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

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

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Keep it up!

Reviewed commit: d91b314400

ℹ️ About Codex in GitHub

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

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

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

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

…E_BIN_DIR` is tested in both slashes and against a `cygpath` that answers a drive, so dropping the `D:/…` arm or the drive-answer refusal no longer leaves the suite green; three comments in openRepoShape and tests.yml say what is true (#649)

Fix round 1 on PR #656, from rv649's review.

- The respelling test and the refusal test now run over `D:\tools\bin`
  and `D:/tools/bin`. `fake_cygpath` takes the typed value, because it
  answers only the exact `-u -- <value>` it is asked. With the `D:/…`
  arm dropped the suite stayed green (91 passed), though `D:/tools/bin`
  with no `cygpath` printed `export PATH="D:/tools/bin:$PATH"`, the
  defect #649 names; five of the new cases fail under that mutation.
- The refusal test has a third case: a `cygpath` that answers
  `D:/tools/bin` with exit 0, which the `[A-Za-z]:*` arm of the
  refusal refuses. With that arm dropped the suite was green too; two
  of the new cases fail under that mutation.
- On Windows the refusal test's value is a drive path under `tmp_path`
  (`str(...)` for the backslash, `.as_posix()` for the slash), and the
  assert names that value. It was an absolute `D:\tools\bin`, so a
  refusal that regressed would have put a command on the machine's real
  D: drive, where the `iterdir` check could not see it.
- Three comments are corrected; no code line or command changes. The
  `machine:` row's now names a cost it left out: a `python` that is
  Python 2, or older than 3.9, where there is no `python3`, reads as
  present and loses the `--preflight` pointer. The refusal's no longer
  says the bash running it does not know the drive letter as a drive:
  Git Bash does, and the drive's colon still splits `$PATH`. The
  `tests-git-bash` job's opening comment names the installer's second
  file.

Lane: openreposhape-5 (openRepoShape-5)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This was referenced Oct 16, 2026
@sonarqubecloud

Copy link
Copy Markdown

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
brettheap marked this pull request as ready for review October 16, 2026 13:50
@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 6 hours and 23 minutes by commenting @sourcery-ai review. Upgrade to get a review now.

@brettheap
brettheap merged commit 35133f2 into main Oct 16, 2026
5 checks passed
@brettheap
brettheap deleted the fix/649-git-bash-launcher branch October 16, 2026 13:51
brettheap pushed a commit that referenced this pull request Oct 16, 2026
 (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>
brettheap pushed a commit that referenced this pull request Oct 16, 2026
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>
brettheap pushed a commit that referenced this pull request Oct 16, 2026
…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>
brettheap added a commit that referenced this pull request Oct 17, 2026
…IR` is respelled or refused before install.sh's fetch, and the PATH note quotes the directory it prints and refuses one holding `:` or a newline (#667) (#726)

#649's two leftovers (lane 5's checklist lines on #461). One commit, **a3cde999**, on main 35133f2 (#656 is in it). It changes `install.sh`, `openRepoShape` and the two suites `tests-git-bash` runs, `tests/test_install_sh.py` and `tests/test_openreposhape_command.py`, and nothing else. `git show --numstat`: `install.sh` +32/-4, `openRepoShape` +57/-1, `test_install_sh.py` +155/-2, `test_openreposhape_command.py` +216/-13. README, the handbook and `docs/cli.md` are unchanged: every sentence about `--install`, the PATH note and `$OPENREPOSHAPE_BIN_DIR` stays true (`export PATH=…` is still what they say).

**1. install.sh applies #656's rule where it reads the variable** (`main`, before the symlink check and the fetch).
- Before: `target="$bin_dir/openRepoShape"` was built from the raw value. With `D:\tools\bin`, the `preflight:` line and the four `done:` lines (and `install:` for an old release) said `D:\tools\bin/openRepoShape`, while the command's own lines said `/d/tools/bin/openRepoShape`. With no `cygpath`, the command's `--install` refused (exit 2) after install.sh had fetched it, and install.sh said only "the <version> openRepoShape's --install refused (exit 2)".
- Now: a value that starts `[A-Za-z]:/` or `[A-Za-z]:\` is spelled with `cygpath -u --`, so every line names one directory. Where no `cygpath` answers, fails, or answers a drive spelling again, install.sh refuses with exit 2 before the symlink check and before any request. The `case` is a copy of `install_commands`'s, because install.sh is downloaded alone.
- The refusal does not echo the value. A value that holds a newline would forge a line.

**2. The PATH note quotes the directory it prints** (`export_path_line`, new, in `openRepoShape`).
- Before: `export PATH="$dir:$PATH"`, the directory as it was between double quotes. For `a"b$HOME c` it printed `export PATH="…/a"b$HOME c:$PATH"`, which `eval` rejects (`unexpected EOF while looking for matching `"``). For ``a`echo X` `` it printed a line that RUNS `echo X` when pasted, and for `a$b` one that drops `$b`.
- Now, for a directory holding a `"`, `$`, backquote, `\` or `!`, the line is `export PATH='<dir>':"$PATH"`, with `'\''` for an apostrophe. Every other directory keeps today's line byte for byte, a space or an apostrophe included (both are safe in double quotes).
- `!` is in the set because an interactive bash reads it inside double quotes as history: `echo "a!b"` answers `bash: !b: event not found` at a prompt, and a person pastes this line at a prompt. A profile file never does.
- Where it is called: `say "    $(export_path_line "$dir")"`, in place of the old inline line.

**3. `--install` refuses a directory holding `:` or a newline** (`install_commands`, beside #656's early checks), before anything is placed, with exit 2 and the remedy.
- A `:` splits a `$PATH` whatever the quoting, and a newline breaks the printed line in two, so no line about `$PATH` can name them.
- The newline check runs first, before the Windows spelling, and its message does not name the value, so no later refusal can echo a forged line. The colon check runs after the Windows spelling: a drive spelling holds a `:` by definition, is respelled without one, and refuses what it cannot respell. A `cygpath` answer that holds a `:` is refused there too.
- The directory may come from `$HOME/.local/bin`; the colon refusal names it.

- **install.sh respells and refuses only where respelling is impossible** (the issue offered "respell, or refuse"). It is #656's decision for the command, and install.sh must not answer one value differently: a test feeds each value through both (below).
- **install.sh gets the drive-letter rule only.** The brief and the issue's first line name that rule. The `:` and newline refusals are the command's (issue line 2). Where install.sh is handed such a value, the command refuses after the fetch, as it always did. Named below as a checklist candidate.
- **The line's shape is `export PATH='<dir>':"$PATH"`.** The directory is the only quoted part. The probe `a"b$HOME c` now prints `export PATH='…/a"b$HOME c':"$PATH"`, and `eval` of it sets `$PATH` to exactly the directory plus the old `$PATH`.
- **Any backslash is quoted,** though `a\b` survives double quotes (`\b` is not special there). The issue lists `\`, and one rule is easier to hold than "a backslash before one of four characters".
- **The apostrophes are walked, not replaced with `${dir//…}`.** How a pattern substitution reads a quote in its replacement has changed between bash releases, this runs on 3.2, and `sed` is not one of the programs `--install` uses (`INSTALL_NEEDS`). I could not run bash 3.2 here (5.2.21 only); the loop uses `${x%%"$q"*}` and `${x#*"$q"}`, which are long-standing 3.2 forms.
- **`BIN_DIR_SPELLINGS` gained `lower-case`** (`d:\tools\bin`). #656's two tests now run over it too (+5 cases, 15 in all, all passing on both the base and the head), so `[A-Za-z]` is pinned in the command as well as in the copy.

New: 56 cases. In `tests/test_openreposhape_command.py` (39 with #656's two extended): the PATH note's lines for 12 names, `export_path_line` alone for the same 12 (so a `"` and a `\` are held on Windows, where no file can be named with one), 3 through `bash -i`, 5 refusals (a colon, two directories, a drive-relative name, a newline, a drive spelling holding a newline), the `$HOME` default, a `cygpath` answer holding a colon, and +5 from the `lower-case` spelling. In `tests/test_install_sh.py` (17): one value through install.sh and the command, in 3 spellings × 4 `cygpath` states (12; where it spells, both place the command at the same directory and name no other; where it does not, both refuse with exit 2 and install.sh made no request); every `preflight:`, `install:` and `done:` line with the directory respelled, over the four endings (4); and the symlink check seeing the respelled directory (1).

- **On the base 35133f2** (the head's two test files over the base's code): `47 failed, 19 passed` in the new selection. The 19 are #656's own cases and the plain, space and apostrophe names, which keep today's line.
- **On the head a3cde99, dirty=0:**
  - `tests/test_openreposhape_command.py tests/test_install_sh.py tests/test_install_sh_filter_repo.py tests/test_openreposhape_package.py`: **276 passed** in 589.67s (the machine's load average was 115).
  - `tests/test_repo_hygiene.py tests/test_workflow_concurrency.py tests/test_release_workflow.py tests/test_readme_sections.py tests/test_cli_reference.py tests/test_setup_by_hand_commands.py tests/test_readme_windows_quoting_sentence.py tests/test_gh_guard.py`: **250 passed** in 175.17s.
  - **The three files `tests-git-bash` runs, with Windows' `write_text` emulated** (a plugin makes every `Path.write_text` write CRLF): **218 passed** in 326.60s. A probe test takes the plugin's effect: 1 passed with it, 1 failed without.
- `actionlint`: not run, no workflow is touched (`tests.yml` is as on main).

Each row reverts one arm, refusal or hunk in a throwaway clone and runs the new cases and #656's (66 selected). The first column is how many failed.

| Mutation | Failed | Where |
|---|---|---|
| install.sh: the whole drive block off | 17 | parity (12), line cases (4), symlink |
| install.sh: no backslash arm | 13 | parity `backslash` and `lower-case`, line cases, symlink |
| install.sh: no slash arm | 4 | parity `slash` |
| install.sh: `[A-Z]` for `[A-Za-z]` | 4 | parity `lower-case` |
| install.sh: no `cygpath` call | 8 | parity `spells` (3), line cases, symlink |
| install.sh: no refusal | 9 | parity, every state but `spells` |
| install.sh: no drive-answer refusal | 3 | parity `again` |
| install.sh: no `|| spelled=""` guard | 3 | parity `fails` (set -e exits silently) |
| install.sh: `cygpath -u` without `--` | 8 | parity `spells`, line cases, symlink |
| install.sh: the block moved after the symlink check | 8 | line cases, symlink, parity `spells` |
| install.sh: the block moved after the release lookup | 17 | parity (12: a request was made, or the target was named raw), line cases (4), symlink |
| install.sh: the refusal echoes the value | 9 | parity refusals (typed value in stderr) |
| command: no newline refusal | 2 | `a-newline`, `a-drive-spelling-and-a-newline` |
| command: newline checked after the drive arm | 1 | `a-drive-spelling-and-a-newline` (the value would be echoed) |
| command: no colon refusal | 5 | colon (3), `$HOME` default, respelled answer |
| command: colon checked before the drive arm | 31 | #656's cases, parity, respelled answer |
| command: `"` not quoted | 2 | `a-double-quote`, both tests |
| command: `$` not quoted | 5 | `a-dollar`, `an-apostrophe-and-a-dollar`, both tests, `bash -i` |
| command: backquote not quoted | 2 | `a-backquote`, both tests |
| command: `\` not quoted | 4 | `a-backslash`, `a-trailing-backslash`, both tests |
| command: `!` not quoted | 5 | `a-bang`, `two-apostrophes-and-a-bang`, both tests, `bash -i` |
| command: apostrophes not walked | 4 | the two apostrophe-with-special names, both tests |
| command: an apostrophe alone forces single quotes | 2 | `an-apostrophe`, both tests (today's line kept) |
| command: always single quotes | 9 | the plain, space and apostrophe names in both tests (6), and #656's `not-on-path` cases (3) |
| command: the old inline line at the call site | 11 | the 9 e2e cases with a special character, 2 of `bash -i` |

All 25 mutations are killed. A first version of "an apostrophe alone forces single quotes" was a syntax error, so every case failed; it was rewritten with `\'` and re-run, and the row is that run.

On the base (35133f2 code) and on the head:
- **install.sh, `D:\tools\bin`, a `cygpath` that spells it:** base lines `preflight: D:\tools\bin/openRepoShape --preflight` and `done: … installed at D:\tools\bin/openRepoShape, but the preflight did not run to the end (exit 127)` (the exit 127 is Linux: the drive spelling resolves only in Git Bash). Head: `preflight: <dir>/openRepoShape --preflight` and `done: … installed at <dir>/openRepoShape, pinned to the release tag, and this machine is ready`, exit 0.
- **install.sh, no `cygpath`:** base exit 2 after 2 requests (the redirect and the fetch), with the command's refusal. Head: exit 2, 0 requests, install.sh's own refusal.
- **The PATH note:** base `a"b$HOME c` → `eval` fails with `unexpected EOF`; `a$b` → `$PATH` gets `…/a`; ``a`echo X` `` → `$PATH` gets `…/aX` (the backquote ran). Head: all three give exactly the directory plus the old `$PATH`. A backslash, a bang, an apostrophe, a space and a plain name keep or gain the right line (the first two single-quoted, the last three today's line).
- **A directory holding `:` or a newline:** base exit 0 and the command placed under it, with a line `eval` cannot use. Head exit 2, nothing placed.
- **Needs Windows to be real:** `cygpath -u` on a real drive path, a real `D:`. Not faked as real; the PR's `tests-git-bash` run checks it through the tests (below).

- origin/main de92614 (adds #698, `tests/test_doctor_next_is_a_command.py` only): clean.
- #630 `l3-563-docs` 5b140bd: conflicts in `docs/handbook.html`, and the same single file conflicts between #630 and main de92614 alone (#630 is lane 3's to refresh). #630 touches none of this PR's four files, so my change adds no region.
- Lane 3's `l3-668`, `l3-669`, `l3-677`, `l3-678`; lane 4's `fix/610`, `fix/645`, `fix/646`, `fix/648`, `fix/650`, `fix/651`, `fix/653`, `fix/655`, `fix/658`, `fix/659`, `fix/664`, `fix/696`: clean, and none touches `install.sh`, `openRepoShape` or the five installer and launcher suites.

- Checked on Linux: CRLF `write_text` (218 passed). No new assert compares a path bash printed with a Python path (the PATH-note line is built from `bash_path(tmp_path)`, as #656's tests are).
- Not checked: on `windows-latest`, Git Bash's real `cygpath` for the new install.sh cases (3 spellings, through `put_cygpath`, which puts a `#!/bin/sh` wrapper around `shutil.which("cygpath")` on that `$PATH`, as `put_on_path` does for `mktemp`); the 12 PATH-note names that can be created there (`a$b`, a backquote, `!`, a space, an apostrophe); the `bash -s` runs that carry the script as bytes and the directory in the environment; and a newline or a `:` in `$OPENREPOSHAPE_BIN_DIR` crossing the environment into Git Bash. The names with `"` or `\`, the `bash -i` cases, the `$HOME` default and the respelled-colon case skip on Windows; `export_path_line` is still run for the `"` and `\` names there, by environment, not by a file name. If one fails there, its log shows the typed value and the printed line.
- bash 3.2 (macOS's `/bin/bash`) was not available: only the loop's forms are claimed for it.

- **install.sh's own `:` and newline refusals.** See candidate 1.
- **Release notes.** `--install` now refuses a `:` or newline in the directory, where it used to place the command and print an unusable line: a visible change for `docs/release-notes/v1.1.0.md` (lane 3's #630). Not edited here.
- README, the handbook and `docs/cli.md`: no sentence became false.

1. **install.sh learns of a `:` or a newline in `$OPENREPOSHAPE_BIN_DIR` only after its fetch.** Handed such a value, install.sh fetches the launcher, and the command refuses with exit 2; install.sh says "the <version> openRepoShape's --install refused (exit 2); its own refusal above says why". A release older than this change does not refuse at all. Copying the two `case` arms into `main`, before the symlink check, would make install.sh refuse first and name the variable, as it now does for the drive spelling.
2. **A directory holding another control character** (a carriage return, an escape) is still echoed as typed by the refusals and by the note's `is not on $PATH` line; only the newline is refused first. It cannot forge a line, but an escape sequence reaches the terminal.

Review: rv667, lane 5's independent review, approved a3cde99 with nits, and codex reviewed it. CI (Brett Heap's ruling 27 and his fast-refresh ruling of 2026-10-15T22:48:35Z): H0 a3cde99 passed tests, tests-git-bash and SonarCloud; the head a22b94c 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 7e21cd2 over main dc7d3fb: the install_sh, openreposhape_command, install_sh_filter_repo, openreposhape_package, repo_hygiene, agents_md_codes, cli_reference sets, 422 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 #667

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

Labels

None yet

Projects

None yet

1 participant