Skip to content

install.sh and openRepoShape: a Windows-spelled $OPENREPOSHAPE_BIN_DIR 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

Merged
brettheap merged 2 commits into
mainfrom
fix/667-install-bin-dir-spelling
Oct 17, 2026

Conversation

@brettheap

@brettheap brettheap commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

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

#649's two leftovers (lane 5's checklist lines on #461). One commit, a3cde99, 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).

What changed and why

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

Decisions and their evidence

Tests (Linux, bash 5.2.21; every run through the lane's run9.sh: gh stub first, bogus token, empty GH_CONFIG_DIR)

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

Mutation probes

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

Reproductions (local fakes only; repro.py)

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

Merge checks (git merge-tree --write-tree, after a fresh fetch; refs deleted afterwards)

The Windows run (what I could not check)

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

Left out

Checklist candidates (not filed)

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

Closes #667

🤖 Generated with Claude Code

Summary by Sourcery

Harden installation across Windows-style directories and unsafe PATH values while ensuring generated PATH instructions remain safe to use.

Bug Fixes:

  • Normalize Windows-style $OPENREPOSHAPE_BIN_DIR values consistently during installation and refuse them safely when they cannot be converted.
  • Generate safe, pasteable PATH instructions for directories containing shell-significant characters.
  • Refuse installation directories containing colons or newlines before placing files.

Enhancements:

  • Expand installer and launcher coverage for Windows path spellings, unsafe PATH names, and failure scenarios.

Tests:

  • Add comprehensive installer and command tests covering path normalization, PATH quoting, refusal behavior, and Windows-specific spellings.

…IR` is respelled with `cygpath -u` or refused before install.sh's fetch, and the PATH note single-quotes a directory bash would still read, and `--install` refuses one holding `:` or a newline, so install.sh names `/d/tools/bin` as the command does and the `export PATH` line does what it says (#667)

#649's two leftovers, both from #656's review.

install.sh read `$OPENREPOSHAPE_BIN_DIR` as typed, so with `D:\tools\bin`
its `preflight:` and `done:` lines (and `install:` for an old release) said
`D:\tools\bin/openRepoShape` while the command's own lines said
`/d/tools/bin/openRepoShape`, and where no `cygpath` answers the command's
`--install` refused (exit 2) only after install.sh had fetched it. install.sh
now carries a copy of the command's rule where it reads the variable, before
the symlink check and the fetch: a value that starts `[A-Za-z]:/` or
`[A-Za-z]:\` is spelled with `cygpath -u --`, and is refused with exit 2 where
no `cygpath` answers, fails, or answers a drive spelling again. The refusal
does not echo the value.

The command's PATH note printed `export PATH="$dir:$PATH"` with the directory
between double quotes as it was, so `a"b$HOME c` printed a line `eval` could
not parse, and a `$`, a backquote or a `\` in a name was read, not kept.
`export_path_line` keeps today's line byte for byte for a directory that
double quotes hold (a space and an apostrophe included) and, for one holding
a `"`, `$`, backquote, `\` or `!` (bash reads that at a prompt as history),
prints it between single quotes with `'\''` for an apostrophe. `--install`
also refuses a directory holding a `:` or a newline, which no `$PATH` line can
name, before anything is placed; the newline's refusal does not echo it.

Tests feed one value through install.sh and through the command, in each
spelling and each state of `cygpath`, and pin every `install:`, `preflight:`
and `done:` line; the PATH note is checked as printed, by `eval` and by
`bash -i`.

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

Copy link
Copy Markdown
Contributor Author

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

@sourcery-ai

sourcery-ai Bot commented Oct 16, 2026

Copy link
Copy Markdown

Reviewer's Guide

The PR makes Windows-spelled bin directories consistent between install.sh and openRepoShape, validates directories before installation, safely quotes generated PATH guidance, and adds extensive tests covering conversion behavior, shell metacharacters, refusal ordering, and installer output.

Sequence diagram for safe bin-directory installation

sequenceDiagram
    participant User
    participant Installer as install.sh
    participant CyPath as cygpath
    participant Network
    participant Command as openRepoShape

    User->>Installer: run installer
    Installer->>Installer: read OPENREPOSHAPE_BIN_DIR
    alt Windows spelling
        Installer->>CyPath: cygpath -u -- bin_dir
        alt path cannot be respelled
            CyPath-->>Installer: failure or Windows spelling
            Installer-->>User: refuse with exit 2
        else path respelled
            CyPath-->>Installer: Unix-style directory
        end
    else shell-style directory
        Installer->>Installer: keep directory
    end
    alt valid directory
        Installer->>Installer: check symlink
        Installer->>Network: fetch release
        Installer->>Command: run --install
        Command-->>Installer: installation result
        Installer-->>User: report one consistent directory
    end
Loading

Sequence diagram for safe PATH guidance generation

sequenceDiagram
    participant User
    participant Command as openRepoShape
    participant Shell

    User->>Command: run --install
    Command->>Command: export_path_line(dir)
    alt dir contains shell-special character
        Command-->>User: export PATH='<dir>':"$PATH"
    else dir is safe for double quotes
        Command-->>User: export PATH="<dir>:$PATH"
    end
    User->>Shell: paste PATH guidance
    Shell-->>User: PATH contains exact directory and prior PATH
Loading

Flow diagram for bin-directory validation

flowchart TD
    A[Read bin directory] --> B{Contains newline?}
    B -->|yes| C[Refuse with exit 2]
    B -->|no| D{Windows drive spelling?}
    D -->|yes| E[Run cygpath -u --]
    E --> F{Successfully respelled?}
    F -->|no| C
    F -->|yes| G[Use respelled directory]
    D -->|no| H[Use original directory]
    G --> I{Contains colon?}
    H --> I
    I -->|yes| C
    I -->|no| J[Install command and print safe PATH guidance]
Loading

File-Level Changes

Change Details Files
Normalize Windows drive-letter bin directories in the standalone installer before any filesystem or network operation.
  • Detect slash and backslash drive spellings, including lowercase drive letters.
  • Use cygpath -u -- when available and refuse unconvertible results without echoing the supplied value.
  • Apply the normalized directory to symlink checks, fetch/install flow, and all status output.
install.sh
Harden --install directory validation and PATH guidance against unsafe or unrepresentable directory names.
  • Add early refusals for newline- and colon-containing directories, including values produced by cygpath.
  • Generate PATH export lines that safely preserve quotes, dollar signs, backquotes, backslashes, apostrophes, and interactive-bash history characters.
  • Keep the existing double-quoted output for names that remain safe under that form.
openRepoShape
Add comprehensive cross-platform coverage for Windows path normalization, safe PATH output, and refusal ordering.
  • Test installer/launcher parity across drive spellings and cygpath success, failure, missing, and invalid-answer states.
  • Verify normalized paths appear in installer diagnostics and symlink checks before network activity.
  • Exercise PATH lines through eval and interactive Bash, plus colon/newline refusal cases and default-directory handling.
tests/test_install_sh.py
tests/test_openreposhape_command.py

Assessment against linked issues

Issue Objective Addressed Explanation
#667 Have install.sh normalize a Windows-spelled $OPENREPOSHAPE_BIN_DIR with cygpath, or refuse it before the symlink check and any network request when it cannot be normalized, so installer output uses the same directory spelling as the command. ✅
#667 Make the PATH line printed by openRepoShape safe to paste for directories containing shell-special characters, while preserving the existing line for directories that need no special quoting; refuse directories containing a colon or newline before placing anything. ✅

Possibly linked issues


Tips and commands

Interacting with Sourcery

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

Customizing Your Experience

Access your dashboard to:

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

Getting Help

@chatgpt-codex-connector

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

Copy link
Copy Markdown

Codex Review Summary

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

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-16T17:12:54.994419Z a3cde99 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 chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a3cde999c3

ℹ️ 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".

Comment thread install.sh
die "\$OPENREPOSHAPE_BIN_DIR 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 installed. Set \$OPENREPOSHAPE_BIN_DIR to the directory as this shell spells it, then run this line again."
;;
esac
bin_dir="$spelled"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Pass the respelled directory to the downloaded launcher

When a Windows-spelled directory is successfully converted, bin_dir changes only as a local shell variable; OPENREPOSHAPE_BIN_DIR in the environment remains unchanged when the downloaded launcher runs. Consequently, installing a named release from before the launcher's own cygpath fix still receives and reports the raw D:\... spelling, prints its broken PATH instruction, and on shells where the two spellings are not aliases may install somewhere other than the normalized $target that this script subsequently executes. Pass the normalized value in the environment of the downloaded launcher so the installer's documented compatibility with older named releases also gets this fix.

Useful? React with 👍 / 👎.

brettheap pushed a commit that referenced this pull request Oct 17, 2026
…es every reason THIS checkout cannot answer, and the notes are current to main bf1f099 with the 25 pull requests since de92614 (#563, #683)

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

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

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

sourcery-ai Bot commented Oct 17, 2026

Copy link
Copy Markdown

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

You can request another review in 12 hours and 9 minutes by commenting @sourcery-ai review. Upgrade to get a review now.

@brettheap
brettheap merged commit dcf62ed into main Oct 17, 2026
5 checks passed
@brettheap
brettheap deleted the fix/667-install-bin-dir-spelling branch October 17, 2026 11:42
brettheap pushed a commit that referenced this pull request Oct 17, 2026
main gained #706 (#664), #703 (#696), #670 (#646), #682 (#648), #726
(#667), #730 (#666) and #724 (#665) since bf1f099. No conflict: #682's
README sentence and its handbook twin (Families) merged as text, with
#682's own Families marker, and every marker reads current. README.md is
1474 lines, its cap; AGENTS.md 494, its cap; `render-cli-reference.py
--check` is ok.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap pushed a commit that referenced this pull request Oct 17, 2026
…d A3 (three SHOULDs, four nits), and notes entries for the seven pull requests landed since bf1f099, current to main 2000488 (#563, #683)

Round A4, from lane 2's review (#630 issuecomment-6233033833, READY):
- SHOULD 1: the doctor's exit-3 list names "a validator of its own that
  crashed or could not be run", so the case of one that cannot be given
  its `PYTHONPATH` (shape-doctor.py's EXIT CODES, #278) is listed too:
  README.md:1208-1222, its handbook twin (`doctor`) and the notes' docs
  line. The paragraph re-wraps to one more line:
  tests/readme-cap.d/683.txt is +2, and its text now says which causes
  the EXIT CODES block names and that #742 added the partial clone and
  #701 the naming policy. README.md is 1475 lines, its cap (1422 + 2 + 5
  + 12 + 22 + 10 + 2). Both `Keeping a project's shape current` markers
  move to c728c970: `doctor` text and marker, `current` marker only.
- SHOULD 2: #727's entry names the leg description, whose em dash stopped
  every `execute` against GitHub at the first leg.
- SHOULD 3: #749's entry names `source-ambiguous`, for an `org/repo` that
  is also a directory here.
- Nits: the doctor section's three near-duplicate pairs are merged (#636
  with #737, #632 with #700 and #705, #586 with #736); #718 has its own
  bullet; #634's entry names `git_out`'s spelling for every caller and
  #744's the gate's `--link-source-label` and `--link-remedy`; the header
  comment says 2026-10-17 at main 2000488.

Wave B1, `git log --first-parent bf1f099..2000488`: #706 (#664), #703
(#696), #670 (#646), #682 (#648), #726 (#667), #730 (#666) and #724
(#665), each from its squash commit; #726's and #682's from lane 5's lines
(#630 issuecomment-6227129879), #682's with the unmerged `.gitmodules`
its last round added, and #724's with lane 5's note that v1.0.0's
test-hygiene line on the generator's `.pyc` files closes. The counts (106
pull requests; 116 of 135 milestone issues closed at 11:53Z), "current to
main" and the open list (19) are at main 2000488, still placeholders.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap pushed a commit that referenced this pull request Oct 17, 2026
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 17, 2026
Lane: openreposhape-5 (openRepoShape-5)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

# Conflicts:
#	install.sh
#	openRepoShape
#	tests/test_install_sh.py
#	tests/test_openreposhape_command.py
brettheap added a commit that referenced this pull request Oct 17, 2026
…holding a control character before they name it, and install.sh refuses a `:` before its fetch, so a carriage return or an escape is no longer echoed as typed (#760) (#804)

Against main. #726 landed as `dcf62ed`, and this branch is restacked onto main as `7e4181c0`: it merges #726's final head `a22b94c4`, then main, taking this branch's side in the four files #726 also changed. Over main it carries exactly its own two commits: `git diff a3cde99 efd061a` and `git diff origin/main 7e4181c` have the same patch-id.

#667's two checklist candidates, with rv667's NIT 2. Two commits on a3cde99: **7569ffa8**, and the fix round's **efd061a7**. Against a3cde99, `git diff --numstat`: `install.sh` +40/-3, `openRepoShape` +30/-16, `tests/test_install_sh.py` +132/-2, `tests/test_openreposhape_command.py` +176/-21, and nothing else. README, the handbook, `AGENTS.md` and `docs/cli.md` are unchanged (`render-cli-reference.py --check`: ok): no documented sentence names these refusals, and neither file is a pinned shape copy, so no `upstream-changed` row follows.

One new commit, **efd061a7**, on 7569ffa, pushed to `fix/760-bin-dir-control-chars` (a fast-forward). The PR is now two commits on a3cde99. `git diff --numstat 7569ffa efd061a`: `install.sh` +12/-7, `openRepoShape` +14/-11, `tests/test_install_sh.py` +34/-29, `tests/test_openreposhape_command.py` +61/-35, and nothing else. Against a3cde99 the PR is `install.sh` +40/-3, `openRepoShape` +30/-16, `tests/test_install_sh.py` +132/-2, `tests/test_openreposhape_command.py` +176/-21. README, the handbook, `AGENTS.md` and `docs/cli.md` are still unchanged.

The red Linux run on 7569ffa was #645's `copytree` race (`shutil.Error` in `test_doctor_pin_restore_merges.py`'s setup), which #713 fixed on main. It is not this PR's fault, and nothing here touches it.

### Finding 1 (should-fix): taken. A C1 control passed the arm under a UTF-8 locale when the value also held a byte that was not UTF-8

**Reproduced first, on 7569ffa, with bash 5.2.21.** `LC_ALL=C.UTF-8`, `$OPENREPOSHAPE_BIN_DIR` set to `<dir>/caf`, the byte 0xE9, U+009B, `31mFORGED/bin`, and `openRepoShape --install`: exit 0, the command placed, and the value printed raw in four lines (`od -c` of the output shows `c a f 351 302 233 3 1 m F O R G E D`). It is lane 1's reproduction, and it is the failure the finding names. Cause: for a value that is not valid in the locale's multibyte encoding, bash matches byte by byte, and read one byte at a time U+009B's two bytes (`0xC2 0x9B`) are not controls, as under `C`.

**The fix is lane 1's, tested by them, in both files** (`install.sh`'s `main` and `openRepoShape`'s `install_commands`), in place of the `case`:

```sh
if (LC_ALL=C; case "$bin_dir" in *[[:cntrl:]]* | *$'\302'[$'\200'-$'\237']*) exit 0 ;; *) exit 1 ;; esac); then
	die "…unchanged…"
fi
```

The command's has `"$dir"`. Under `LC_ALL=C` the class is the ASCII controls and DEL, byte by byte, and the second pattern names the two bytes of each of U+0080 to U+009F (`0xC2`, then `0x80` to `0x9F`). The arm is the same in every locale. The subshell leaves the locale of every later line as it was. I added a `*) exit 1 ;;` arm so the construct has a default case (finding 2). Both files' comments now say what the check does, and no longer say which C1 controls match is the locale's.

**Measured, command `--install` (bash 5.2.21; each cell is exit, whether the command was placed, whether the value reached the output):**

| value | locale | 7569ffa | efd061a |
|---|---|---|---|
| U+009B | `C`, `POSIX` | 0, placed, in 4 lines | 2, nothing placed, nothing echoed |
| U+009B | `C.UTF-8` | 2 | 2 |
| U+0080, U+009F | `C`, `POSIX` | 0, placed | 2, nothing placed |
| U+0080, U+009F | `C.UTF-8` | 2 | 2 |
| 0xE9 then U+009B then `FORGED` | `C`, `POSIX`, `C.UTF-8` | 0, placed, in 4 lines | 2, nothing placed, nothing echoed |
| a `:`, 0xE9, U+009B | all three | 2, the colon refusal names the directory, the C1 control in its line | 2, the control-character refusal, nothing of the value |
| an e with an acute accent, U+00A0 | all three | placed | placed |
| a lone 0x9B byte | all three | placed | placed (see the checklist) |
| U+2028 | `C`, `POSIX` | placed | placed |
| U+2028 | `C.UTF-8` | **2** | **placed** (see below) |

**Measured, install.sh with a fake `curl` that logs each request and exits 22, a version named:**

| value | locale | 7569ffa | efd061a |
|---|---|---|---|
| U+009B, U+0080, U+009F | `C` | exit 2 after 1 request | exit 2, 0 requests |
| 0xE9 then U+009B | `C`, `C.UTF-8` | exit 2 after 1 request | exit 2, 0 requests |
| a `:`, 0xE9, U+009B | `C`, `C.UTF-8` | exit 2, 0 requests, the colon refusal holds the C1 control | exit 2, 0 requests, nothing of the value |
| an e with an acute accent, U+00A0 | `C`, `C.UTF-8` | 1 request (the value is accepted) | 1 request (the value is accepted) |

**Side effect, as lane 1 said: U+2028 and U+2029 are refused in no locale now.** Only a UTF-8 locale's `[[:cntrl:]]` matched them, and no one chose that. They are not controls a terminal acts on, and a line that holds one prints as it is. `NOT_CONTROLS` places both under `C` and `C.UTF-8`, through both files, so the decision is held and not left to the locale. If a ruling wants them refused, `*$'\342\200'[$'\250\251']*` joins the pattern in both files and the two rows turn into refusals.

**What the locale still does, measured:** over every byte string of up to three bytes from a set of fourteen (2955 strings: ASCII, ESC, DEL, 0x80, 0x85, 0x9B, 0x9F, 0xA0, 0xA9, 0xC2, 0xC3, 0xE2, 0xE9), under `C`, `POSIX` and `C.UTF-8`, the new arm gives the same answer in all three locales, and the arm without `LC_ALL=C;` differs from it on exactly one string: U+2029 (0xE2 0x80 0xA9) under `C.UTF-8`. So on glibc the locale's one observable effect is those two characters, and that is what pins `LC_ALL=C` in the tests (see the mutants). It stays in the code as lane 1 specified it, so the match is byte by byte by construction on a libc whose single-byte `iscntrl` says otherwise for 0x80 to 0x9F in a UTF-8 locale. macOS's was not measured.

### Finding 2 (nit, two SonarCloud S131 issues): not taken, partly cleared

The colon `case` in `install.sh` has no `*)` arm and keeps its S131, as nine others open in that file do. The control check's `case` now has one, so its S131 should go. That is expected from the code, not measured: SonarCloud's run on this push says.

### Finding 3 (info): #804 depends on #726

#726 has landed on main as dcf62ed. This branch still descends from a3cde99, so `git merge-tree --write-tree origin/main efd061a` (origin/main 40e678e) conflicts in `install.sh`, `openRepoShape` and both test files, as lane 1 found for a cherry-pick. I did not restack: the coordinator retargets and restacks by the stacked-PR procedure.

### Ruling on the colon refusal naming the directory (lane 1 agreed; record it when #760 closes)

Lane 1 agreed that the colon refusal names the directory, and asked that it be recorded on #760 at close: #760's Verify line says "neither stream holds the value" for all four values, colon included, and the colon's is named, by a choice every `$HOME` default needs. With this commit the premise is whole: a value that reaches the colon refusal holds no ASCII control character, DEL or C1 control, in any locale and whatever else it holds, so naming it cannot forge a line or reach the terminal.

### Tests at efd061a (every run through `run9.sh`: gh stub first on `PATH`, bogus token, empty `GH_CONFIG_DIR`; dirty=0)

Logs are in `$Q/v116/f804/` (`$Q` is the lane's scratchpad).

| Run | Result | Log |
|---|---|---|
| The new and changed cases (`-k "c1_control or unprinted_letter"`) | **36 passed** (18 in each file) | `new-final.log` |
| `test_openreposhape_command.py`, `test_install_sh.py`, `test_install_sh_filter_repo.py`, `test_openreposhape_package.py`, `test_repo_hygiene.py`, `test_agents_md_codes.py`, `test_cli_reference.py` | **478 passed**, 103.92s | `runA-final.log` |
| the three files `tests-git-bash` runs, with every `Path.write_text` writing CRLF (the plugin that wave 7 used) | **274 passed**, 65.38s | `runC-final.log` |
| `test_release_workflow.py`, `test_doctor_validator_rows.py`, `test_doctor_machine_row.py` | **93 passed**, 50.72s | `runB-final.log` |
| `test_shape_doctor.py -k "shim or installed_doctor or shipped_doctor or refused_doctor or signal"` (it runs the launcher's `--doctor`) | **20 passed**, 388 deselected | `runD-final.log` |

- **What changed in the tests.**
  - The C1 case, which skipped where no UTF-8 locale read U+009B as a control, is gone with its helper `utf8_locale_with_c1_controls`. In its place a table of five values (`C1_BIN_DIRS`) runs under both `C` and `C.UTF-8`, through the command and through install.sh. The values are U+009B, U+0080, U+009F, 0xE9 then U+009B, and a `:` beside that. Nothing skips for want of a locale.
  - The placed-name test takes four names (`NOT_CONTROLS`): an e with an acute accent, U+00A0 (the byte after the range's last), U+2028 and U+2029, under both locales, through both files. The installer's version looks for its `preflight:` line among lines split at a newline only, because `str.splitlines` also splits at U+2028 and U+2029.
  - A lone 0xE9 is `chr(0xDCE9)`, which reaches the child as that byte through `surrogateescape`. No escape is typed into any file, and `body-guard.py` is clean on the four files.
- **On the base.** The new and changed cases are 36: **18 fail on 7569ffa's code and 18 pass** (`new-base2.log`). The 18 that pass are guards: U+009B, U+0080 and U+009F under `C.UTF-8` (6, which the head already refused), and the placed names under `C` and the e and U+00A0 under both (12). Of the 18 that fail, 8 surface as a `UnicodeDecodeError` raised inside `subprocess.run` (the head echoes the lone 0xE9, and `run_cmd` and `Net.run` decode strictly) and 10 as assertions. That is a rough failure message and no more: decoding with `errors="surrogateescape"` in those two helpers would turn the 8 into assertions, and I left it, as the round is one commit and no mutant of the helpers can fail on the fixed code. Over a3cde99's code, the larger selection of #760's cases (`-k` of the refusal, installer, default-directory, control, letter and C1 tests, 62 cases) gives **42 failed, 20 passed** (`new-base-a3cde999.log`); over 7569ffa's it gives 18 failed, 44 passed (`new-base-7569ffa8.log`).

### Mutants of the two new hunks, and the earlier commit's rows against the new arm

Each runs in a scratch clone against 95 selected cases (the refusal, installer, default-directory, control, letter, C1, Windows-spelling and symlink cases), one edit at a time, restored after. "Failed" is how many of the 95 failed. **All 28 are caught.** Logs: `mut2/` and `mut3/`; the runner is `mutate2.py`; the counts are in `mutants2.out` and `mutants3.out`.

| Mutation | Command | install.sh | Where it fails |
|---|---|---|---|
| no C1 bytes in the pattern | 10 | 10 | every C1 value, under both locales, in the file mutated |
| `LC_ALL=C;` dropped | 4 | 2 | U+2028 and U+2029 under `C.UTF-8` (the installer's test runs the command too) |
| `[[:cntrl:]]` dropped, C1 bytes kept | 17 | 9 | carriage return, tab, DEL, escape and the drive and colon rows |
| range starts one late (`\201`) | 2 | 2 | U+0080, both locales |
| range ends one early (`\236`) | 2 | 2 | U+009F, both locales |
| range ends one late (`\240`) | 4 | 2 | U+00A0 is refused (the placed-name test) |
| lead byte `\303` for `\302` | 10 | 10 | every C1 value |
| the whole arm removed | 27 | 19 | every control value |
| arm narrowed to a newline | 23 | 17 | every control but a newline |
| `[![:print:]]` for the pattern | 16 | 8 | the e with an acute accent under `C`, and more |
| refusal echoes the value | 27 | 19 | every control value |
| arm after the drive arm (command) | 4 | | the two drive rows (both files) |
| arm after the colon arm | 8 | 5 | the colon and the drive rows, the colon beside a C1 control |
| arm after the symlink loop (command) / check (install.sh) | 9 | 6 | the link case, the colon and drive rows |
| arm after the release lookup (install.sh) | | 19 | a request was made, for every control value |

### Not checked, and known limits

- **bash 3.2.57** is not here. Lane 1 built it and measured the same table on it, and the pattern is theirs. I did not run it.
- **macOS and other libcs.** `LC_ALL=C` is what is meant to hold there. Not run.
- **A GBK or GB18030 locale.** The bytes `0xC2 0x80` to `0xC2 0x9F` are 32 hanzi there (`0xC2 0x9B` decodes to one with Python's `gbk` codec), and the arm now refuses a directory that holds one as "a control character", where `[[:cntrl:]]` placed it. Measured with Python's codecs only: no such locale is installed here. Big5, Shift_JIS (`cp932`), EUC-KR and EUC-JP read those bytes as invalid. A candidate, not a fix: it needs the locale's charset asked first.
- **Windows (Git Bash).** The C1 and placed-name tests skip there, as before; the three files `tests-git-bash` runs pass with CRLF `write_text` emulated (274).

**1. The command refuses any control character in the directory, first** (`install_commands`, `openRepoShape`).
- Before: only a newline was refused first. Measured on a3cde99: `$OPENREPOSHAPE_BIN_DIR` set to `<dir>/bin`, a carriage return, then `FORGED` placed the command, exit 0, and printed `…/bin^MFORGED is not on $PATH`. An escape sequence did the same (`…/bin^[[31mFORGED`). The value reached four lines: `installed at`, `placed in`, `is not on $PATH` and the `export PATH` line.
- Now: the arm is `*[[:cntrl:]]*` and the two bytes of each C1 control (U+0080 to U+009F), run under `LC_ALL=C`, still first, before the Windows spelling and the colon. Its refusal says "holds a control character, such as a newline, a carriage return, a tab or an escape" and nothing of the value, so no refusal after it can echo an ASCII control character, DEL or a C1 control, in any locale and whatever else the value holds, that forges a line or reaches the terminal. Exit 2, nothing placed.

**2. install.sh carries both arms and refuses before any request** (`main`, `install.sh`).
- Before: a `:` or a newline reached the fetch. install.sh fetched the launcher, ran `--install`, and said only "the v9.9.9 openRepoShape's --install refused (exit 2)"; a release older than #667 had no such refusal and placed the command under the directory. Measured with a fake `curl` that logs its calls, version named: a3cde99's install.sh made one request for each of a colon, a newline, a carriage return and an escape; this head makes none.
- Now, copied from the command as #667 copied the drive-letter `case` (install.sh arrives alone down a pipe): the control-character arm right after the variable is read, before the Windows respelling; the colon arm after the respelling, as the command orders them. Both are before the symlink check and every line that names the directory.
- The control-character refusal never names the directory. The colon refusal names it, as the command's does (see decisions).

- **`[[:cntrl:]]`, not `[![:print:]]`**, as the issue directs. `[![:print:]]` is what `checked_repo` uses for `$OPENREPOSHAPE_REPO`, but under `LC_ALL=C` the two bytes of an e with an acute accent are not printable, so it would refuse a directory called `caf` and that letter. A test places that directory under `C` and under `C.UTF-8`, through both files; the mutants "`[![:print:]]`" fail it.
- **A C1 control is refused in every locale.** The check runs under `LC_ALL=C` and names U+0080 to U+009F's two bytes (`\302`, then `\200` to `\237`), whatever else the value holds; U+2028 and U+2029 are refused in none. Left to the locale, `[[:cntrl:]]` matches a C1 control only in a value that is valid UTF-8 (bash 5.2.21, Linux): U+009B alone is refused under `C.UTF-8` and passes under `C` and `POSIX`, and one byte that is not UTF-8 before it (`caf`, 0xE9, U+009B) makes bash match byte by byte, so it passes under `C.UTF-8` too (lane 1 measured the same on bash 3.2.57). The tests run five C1 values under `C` and `C.UTF-8`, through both files, and place U+00A0, U+2028 and U+2029 beside the e with an acute accent.
- **A tab and DEL are refused too.** They are control characters, the issue says every one, and no `$PATH` line prints either as it is.
- **The colon refusal names the directory; the control-character one never does.** The issue's Verify says "neither stream holds the value" for all four values, but the command's colon refusal (#726) names `'$dir'`, and a test of #726's pins that for the `$HOME` default, where the person typed nothing and needs the directory named. A colon value holds no ASCII control character, DEL or C1 control by then, in any locale, so naming it cannot forge a line or reach the terminal. Lane 1 agreed with this ruling (2026-10-17T11:06:51Z) and asked that it be recorded when #760 closes: the issue's Verify line says "neither stream holds the value" for all four values, and the colon's is named. install.sh's colon refusal does the same. The tests hold the control values to "no `forged` tail, no escape, DEL or C1 in either stream", and the colon values to "named, in quotes". If the coordinator rules that a colon must go unnamed too, it is one message in each file and the same assertion.
- **The order is held, not just the width.** A drive spelling with a newline or an escape must be refused as a control character (not as "spelled for Windows" when no `cygpath` answers), a colon with an escape must be refused as a control character, and a link in the directory must not make the symlink refusal print the value.

**The figures in this section are 7569ffa's.** At efd061a they are the fix round's table above: 36 new and changed cases, 478 passed in the seven files, 274 passed with CRLF `write_text`, and 93 and 20 passed in the others. The base figures are 18 failed and 18 passed over 7569ffa's code, and 42 failed and 20 passed over a3cde99's for the 62-case selection.

31 cases in the new and extended tests (26 are new; the command's five older refusal cases run again in the same test), run with the one existing test the selection matches: 32 passed in 1.75s on the head, dirty=0.
- `tests/test_openreposhape_command.py` (15): the refusal over 11 values (a colon, two directories, a drive-relative name, a newline, a carriage return, an escape, a tab, DEL, a drive spelling with a newline, a drive spelling with an escape, a colon with an escape; the first five were 5 cases, six are new), a letter under `C` and `C.UTF-8`, U+009B under a UTF-8 locale, and a link in a directory holding a carriage return. A shared `assert_a_bin_dir_is_refused` holds exit 2, the wording, the ending, and that no `forged` tail, escape, DEL or C1 reaches either stream.
- `tests/test_install_sh.py` (16): the same 11 values through install.sh and the command, in one test (exit 2 for both, the fake `curl` records no call, nothing placed or made in either world, `$TMPDIR` empty), the `$HOME` default with a colon, the symlink case, a letter under both locales (placed, named as typed), U+009B under a UTF-8 locale.
- **On the base a3cde99** (the head's two test files over the base's code): `24 failed, 8 passed` in the 32. The 8 are the command's three colon cases and the `$HOME` default (the base already refuses them), and the letter under both locales through both files (the base places it; they guard the width). Of the 24, 22 fail on behaviour (a request was made, or the value was placed and echoed, or the refusal came from the wrong arm); the command's `a-newline` and `a-drive-spelling-and-a-newline` fail only on the refusal's new wording.
- **On the head 7569ffa, dirty=0:**
  - `tests/test_openreposhape_command.py tests/test_install_sh.py tests/test_install_sh_filter_repo.py tests/test_openreposhape_package.py`: **302 passed** in 68.90s.
  - `tests/test_repo_hygiene.py tests/test_agents_md_codes.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 tests/test_workflow_concurrency.py tests/test_release_workflow.py tests/test_readme_sections.py`: **297 passed** in 38.97s.
  - `tests/test_shape_doctor.py -k "shim or installed_doctor or shipped_doctor or refused_doctor"` (it runs the launcher's `--doctor`): **20 passed**, 388 deselected.
  - **The three files `tests-git-bash` runs, with Windows' `write_text` emulated** (a plugin makes every `Path.write_text` write CRLF): **244 passed** in 52.64s. A probe test takes the plugin's effect: 1 passed with it, 1 failed without.

- **install.sh, version named, each of a colon, a newline, a carriage return, an escape:** a3cde99: exit 2 after 1 request (the launcher's fetch), refused by the fetch's error or by the command. This head: exit 2, 0 requests, install.sh's own refusal (the colon one names the directory; the others name nothing of it).
- **The command, `--install`:** a3cde99: a carriage return and an escape: exit 0, the command placed under the directory, the value in four lines. A newline: exit 2. This head: exit 2 for all three, nothing placed.
- **Needs Windows or another system to be real:** none of these rows. See below for what was not run.

The fix round above adds 14 mutants of the new pattern line and 4 order mutants, and re-ran this table's rows against the new arm (the whole arm removed, narrowed to a newline, `[![:print:]]`, the refusal echoing, and the two later placements). Each is still caught.

Each row changes one arm or hunk in a throwaway clone and runs the new cases and #726's drive and symlink cases (65 selected). "Failed" is how many of the 65 failed.

| Mutation | Failed | Where |
|---|---|---|
| command: no control-character arm | 18 | the command's cases and install.sh's, which runs it |
| command: arm narrowed back to a newline | 14 | carriage return, escape, tab, DEL (both files) |
| command: `[![:print:]]` for `[[:cntrl:]]` | 2 | the letter under `C` (both files) |
| command: arm after the drive arm | 4 | the two drive spelling cases (both files) |
| command: arm after the colon arm | 6 | colon with an escape, the two drive cases |
| command: arm after the symlink loop | 7 | the link case, colon with an escape, the two drive cases |
| command: refusal echoes the value | 18 | every control value |
| install.sh: no control-character arm | 10 | the link case, U+009B, the control values |
| install.sh: arm narrowed to a newline | 8 | carriage return, escape, tab, DEL, the link case, U+009B |
| install.sh: `[![:print:]]` | 1 | the letter under `C` |
| install.sh: arm after the drive block | 2 | the two drive spelling cases |
| install.sh: arm after the symlink check | 4 | the link case, colon with an escape, the two drive cases |
| install.sh: arm after the release lookup | 10 | a request was made, for every control value; and the link refusal named it |
| install.sh: refusal echoes the value | 10 | every control value |
| install.sh: no colon arm | 4 | the three colon values, the `$HOME` default |
| install.sh: colon arm before the drive block | 17 | #726's drive cases (the respelling never runs) |
| install.sh: colon arm after the release lookup | 4 | a request was made, for the colon values |
| install.sh: colon refusal names nothing | 4 | the three colon values, the `$HOME` default |

All 18 are killed, on the final head. A first run on the pre-reorder tests gave the same counts.

- origin/main bf1f099: clean. The parent a3cde99: clean (this head descends from it).
- All 47 `refs/heads/fix/*` and `l3-*` branches on origin (none of `l2-*` or `l4-*` exists; mine excluded): clean. Only #726's branch edits `install.sh`, `openRepoShape`, `tests/test_install_sh.py` or `tests/test_openreposhape_command.py`, as the issue said.
- Lane 5's other wave-7 branches (`fix/757` through `fix/777`, local heads at the time): clean; none edits these four files.

- **bash 3.2** is not here; lane 1 measured the table and the fix on 3.2.57 built from GNU source.
- **Windows.** A control character, or a non-ASCII value, crossing the environment into Git Bash is not run here: the letter and U+009B tests skip on Windows, and the link and `$HOME` tests do too (no file there holds a control character or a `:`). The 11-value test runs there as #726's newline case did, and its result is `tests-git-bash`'s to give. CRLF `write_text` was emulated (244 passed).
- **macOS** and a locale other than `C`, `POSIX` and `C.UTF-8`: not run. No test skips for want of a locale.
- **A GBK or GB18030 locale.** The bytes `0xC2 0x80` to `0xC2 0x9F` are 32 hanzi there, and the arm now refuses a directory that holds one as "a control character". Measured with Python's codecs only: no such locale is installed here.

- **Release notes.** `--install` and install.sh now refuse a control character in `$OPENREPOSHAPE_BIN_DIR` where `--install` placed the command and printed the value as typed, and install.sh refuses a `:` before its fetch. Lane 3's #630 drafts `docs/release-notes/v1.1.0.md`; a line for it: "`install.sh` and `openRepoShape --install` refuse a `$OPENREPOSHAPE_BIN_DIR` holding a control character (a newline, a carriage return, a tab, an escape or a C1 control), and `install.sh` a `:`, with exit 2 before it makes a request." Not edited here.
- README, the handbook, `AGENTS.md`, `docs/cli.md`: no sentence became false.

1. **U+202E, U+200B, U+2028 and U+2029 in `$OPENREPOSHAPE_BIN_DIR` are echoed as typed** (the body's candidate 1, now also the two separators). None is a control a terminal acts on; U+202E can make a directory read as another.
2. **A lone 0x9B byte** (an 8-bit C1 control, not UTF-8) matches in no locale, before and after this commit. Only an 8-bit terminal with C1 controls on acts on it. (Lane 1's candidate 2.)
3. **A GBK or GB18030 locale reads `0xC2 0x80` to `0xC2 0x9F` as hanzi**, so the arm refuses a directory holding one. Measured with codecs only.
4. **Lane 1's other candidates stand as they listed them:** a leading `-` makes `--install` run `mkdir -p "$dir"` with no `--`; U+202E and U+200B; a `$HOME` holding a control character is refused without saying the directory came from `$HOME`; install.sh's Windows-spelling refusal names nothing where the command's names `'$dir'`; macOS's `[[:cntrl:]]` for C1 under `en_US.UTF-8` is unmeasured.

Review: lane 1's re-verification of #804 is PR-READY (#804 issuecomment-6238973516). CI (Brett Heap's ruling 27): the head 7e4181c passed tests, tests-git-bash and SonarCloud; main has touched none of its files since 6626a3c; and the local gate passed on its predicted squash tree cbc85da over main a9d1579: the install_sh, openreposhape_command, repo_hygiene, agents_md_codes and cli_reference sets, 404 passed. Landed on Brett Heap's word of 2026-10-17T21:47:28Z, in lane 5's session: "merge lane 5's v1.1.1 PRs once reviewed and green".

Closes #760

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