Skip to content

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

Merged
brettheap merged 86 commits into
mainfrom
l3-563-docs
Oct 17, 2026

Conversation

@brettheap

@brettheap brettheap commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Lane: openreposhape-3 (openRepoShape-3)
Claim: #563 (comment)

Refs #563. This is #563's third pull request, the docs, and it does not close the issue.

Head aef568f0, on main 6f64eea2. It lands LAST, before the tag, because pipx install openreposhape works only once 1.1.0 is on PyPI. Rounds A, A3, A4, B1 and both parts of B2 of the final refresh are in; every other v1.1.0 pull request has landed. Lane 2's #627 (README and the handbook) has landed and is merged here. #563's other two pull requests have landed: #590, the package (fe237ec), and #599, the release workflow (32c22b0). The base is now main. The diff against it is this pull request's twelve files: README.md, AGENTS.md, docs/handbook.html, docs/cli.md, scripts/render-cli-reference.py, adopt-project.py, docs/release-notes/v1.1.0.md, tests/readme-cap.d/563.txt, tests/readme-cap.d/460.txt, tests/readme-cap.d/683.txt, tests/test_repo_hygiene.py and tests/test_agents_md_codes.py. The commit list also shows #590's and #599's branch commits, because those branches were merged here before they were squashed on main.

What

Decision 6 of #563's design (issuecomment-6191672077), approved by Brett Heap at 2026-10-15T13:32:51Z ("approve all six for #563"): the one-line install stays first, and pipx install openreposhape comes second, and first on native Windows.

  • README.md (1474 lines, at its cap: main's 1459, + 12 from tests/readme-cap.d/563.txt, + 2 from tests/readme-cap.d/460.txt for Tracking: shape-doctor.py, reporting a repository's compliance #460's L1 below, + 1 from tests/readme-cap.d/683.txt for the doctor's exit 3, rounds A3, A4 and B2):
  • AGENTS.md (494 lines, at its cap of 494, raised from main's 492 in test_agents_md_is_short_enough_to_be_read):
    • The first rule: the one-line install.sh and pipx install openreposhape are the human's to run.
    • Section 2: the installed command may be pipx's, native Windows included, and pipx adds the four tool commands.
  • docs/handbook.html carries the same facts in the page's own voice, with every README marker recomputed. Step 3 on the Windows tab:
    • the heading names three ways: "Install with pipx, fetch the flow, or use Git Bash's one line" (:788);
    • for Git Bash it shows the one-line install, "In Git Bash, the one-line install runs too:" (:829-830), with the caveat that its preflight cannot take a typed yes there (:831-832). This settles Tracking: the test suite, CI and the docs #461's line "The handbook's step 3 has no Git Bash path".
    • test_every_handbook_copy_of_the_one_line_install_is_the_same (tests/test_repo_hygiene.py:1578) holds all three handbook copies of the one line to ONE_LINE_INSTALL.
  • adopt-project.py: filter-repo-missing names git-filter-repo's install in install.sh's order: the pipx or uv already there, then brew, the apt/dnf pipx route, then pacman. It says pipx install openreposhape brings it for its adopt-project.
  • docs/cli.md, from scripts/render-cli-reference.py: the checkout group says what pipx install openreposhape places.
  • docs/release-notes/v1.1.0.md, a draft:
    • What 1.1.0 adds; install and upgrade.
    • What changed since v1.0.1, by area, to main fc86720 (121 pull requests).
    • Known limits: the package's and Git Bash's own, then lane openRepoShape-4's ten lines and lane openRepoShape-1's twenty-six D9 lines, as two lists.
    • How this release is cut: the environments, the tag ruleset, the rehearsal run and the trusted publishers, all done.
    • Before tagging.

Also #460's L1 (5b140bd)

#630 also carries one line of #460, which Brett Heap gave to lane 3 at 2026-10-16T12:24:41Z. Lane 3's triage (T460a) put it here: every file it needs is already in #630, and #630 lands after #627 anyway.

Rounds

Round B2 (final), part 1: landed since b2a62ec

Head d697b366e2e2a25a62c5db60acbee992581eb351, on main fc867207, pushed as a fast-forward from b2a62ec7 on 2026-10-17. Part 2 waits only for lane 4's #795 (#686).

Round B2 (final), part 2: #795

Head aef568f02a9c626c9ae93f67e1a508c14684312d, on main 6f64eea2, pushed as a fast-forward from d697b366 on 2026-10-17. RV630 reviewed part 1 (d697b366) READY; this part makes the changes it gave, and nothing else.

Tests

At 6fd33a9, with /usr/bin/python3 -m pytest (3.12.3), a stub gh exiting 97 first on PATH, GH_TOKEN=bogus, an empty GH_CONFIG_DIR, -o tmp_path_retention_policy=failed, and no GIT_ALLOW_PROTOCOL. The set is 14 files:

  • test_repo_hygiene, test_cli_reference, test_agents_md_codes, test_openreposhape_package, test_release_workflow;
  • test_install_sh, test_openreposhape_command, test_install_sh_filter_repo;
  • test_setup_by_hand_commands, test_readme_sections, test_readme_windows_quoting_sentence, test_update_add_codes_documented;
  • test_refusal_default_remediation, test_adopt_filter_repo_failure.

Result: 544 passed in 357.14s (517 at 5b140bd, before main's new tests).

  • render-cli-reference.py --check: [ok].
  • actionlint 1.7.12 over tests.yml, release.yml and both template validate.yml: no findings.
  • git diff --check: clean.
  • Earlier, at ca5c49f: the two Git Bash suites passed 117 plain and 117 with CRLF emulated.
  • Round B2 part 1, at d697b366, ran the same eight files: 300 passed in 37.04s; render-cli-reference.py --check gave [ok], and git diff --check origin/main was clean.
  • Round A4 + B1, at b2a62ec, ran the same eight files: 300 passed in 42.72s; render-cli-reference.py --check gave [ok], and git diff --check was clean.
  • Round A3, at d2d3d76, ran the docs and release-notes set under the same rules: test_repo_hygiene, test_readme_sections, test_cli_reference, test_release_workflow, test_agents_md_codes, test_setup_by_hand_commands, test_readme_windows_quoting_sentence and test_update_add_codes_documented. Result: 293 passed in 45.45s. render-cli-reference.py --check gave [ok], and git diff --check was clean.
  • The full suite was not run.

Open placeholders in the release notes

Each is marked **[placeholder: …]** or <release-commit>, or sits in a "remove at tag time" section:

  1. The counts at tag time. At main fc86720: 121 pull requests since bfed143; at 2026-10-17T19:08Z, 131 of the milestone's 133 issues were closed. The marker and its sentence keep the form the release-body tool fills.
  2. The open milestone list (2 at 19:08Z: Publish openRepoShape to TestPyPI and PyPI as openreposhape, so one pipx install puts the tools and git-filter-repo on any machine (design first) #563 (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) and family.py bump: a mount with a narrowed fetch refspec is refused member-to-local-only on a false detail, a commit only a tag holds is told "a pin the rest of the world cannot fetch", a mount with no origin is told to fix the network #686 (family.py: bump fetches every branch of the member's origin whatever the mount's refspec and drops the origin/ refs no ref of origin backs, says a tag of the remote reaches a commit no branch does instead of "a pin the rest of the world cannot fetch", and tells a mount with no origin it has none instead of to fix the network (#686) #795)), in "Before tagging", to be re-read at tag time.
  3. <release-commit> throughout.

No longer placeholders since B2 part 1: "What changed" runs to main fc86720 and says #795 is added if it lands next (part 2 makes it "The list is complete to the release commit."), and v1.0.0's known limits are re-read, with the marker and "as read so far" cut.

Not done here, for review

🤖 Generated with Claude Code

Brett Heap and others added 30 commits October 14, 2026 23:34
…pe.ref` beside the command it places, and the installed command then fetches setup.sh at that ref, hands setup.sh `--shape-ref` for it and checks the doctor's clone out at it, so a command installed from a release stays on that release while `$OPENREPOSHAPE_REF` still overrides it for one run (#564)

An installed command read `$OPENREPOSHAPE_REF` at every run and fell back
to main, so from v1.0.0 on, with main carrying v1.1.0 work, a command
installed from a release drifted off it within hours. The ref is now NAMED
by the variable or by the record, and both places that honoured only the
exported variable (the doctor's checkout and setup.sh's `--shape-ref`)
honour either.

The record is a sidecar file, never an edit to the command: the placed
command stays byte for byte the file it was installed from, so the
installer's own `(unchanged)` verdict and any copy vendored and checked by
sha256 still compare bytes and never see the record. An install with no
ref records nothing and removes any record already there, which is the
command exactly as before. The ref is checked before anything is placed,
and the record is checked when it is read, naming the file and the two
ways out. `--version` prints the ref in force. docs/cli.md is regenerated
for the `--help` paragraph that says so.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…oShape/releases/latest/download/install.sh | bash`, installs the newest release's openRepoShape pinned to that release, installs git-filter-repo with pipx where pipx is and it is not or prints the one command that would, and ends with `openRepoShape --preflight`; README, the handbook and the v1.0.1 release notes lead with it (#564)

Brett Heap, 2026-10-14: "we need it to be more simple to install ... now
and after, we need a simple install process", and "v1.0.1 soon".

install.sh is attached to every release, so `releases/latest/download/`
is always the newest. It reads the newest tag from github.com's
`releases/latest` redirect with no gh and no login, asking `gh api` only
when the redirect names none; takes a named release as its one argument;
fetches that release's `openRepoShape`, checks it is the command, and runs
its `--install` with `$OPENREPOSHAPE_REF` set to the release, then asks the
installed command's `--version` that it stayed there. Every step says what
it did in one line. It never runs sudo. Exit 0 installed and ready, 1
installed but the preflight or the pin needs attention, 2 nothing
installed; git-filter-repo never moves it. Everything runs from a `main`
called on the last line, so a cut-off download runs nothing.

README.md: step 3 and the worked example lead with the one line, the gh
api and raw-URL lines stay as the by-hand way at main, and Layout gains
install.sh; the cap moves 1422 -> 1450, with the reason in
test_readme_is_short_enough_to_be_read. docs/handbook.html says the same,
names the new README blob and adds the (#564) clause. install.sh joins
SHIPPED_BASH and the macOS job's `/bin/bash -n` step, and one test holds
the one line byte for byte in every file that carries it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rds a ref only with `OPENREPOSHAPE_RECORD_REF=1`, which install.sh sets, so a script that exports `$OPENREPOSHAPE_REF` around it gets v1.0.0's install; every curl that runs is HTTPS only, redirects included, at TLS 1.2 or newer; a link to the command reads its record; `--help` answers past a broken record; and install.sh says which failure it met (#564)

The review on #577 (RV577, FIX-ROUND) and SonarCloud shell:S6506.

openRepoShape:
- The ref is recorded only when asked, or when it came from the command's
  own record (a re-install of itself stays pinned). workBenches exports
  `OPENREPOSHAPE_REF=pinned-by-workBenches-no-fetch` around `--install`;
  that no longer becomes a record every later run would fail on. Any other
  install records nothing and removes a record already there.
- The raw-URL fallback curl carries `--proto '=https' --proto-redir
  '=https' --tlsv1.2`, written on the line.
- The record is found beside the FILE, symlinks followed with a portable
  `readlink` loop (no `-f`, at most 40 links).
- A broken record is kept as a fault and refused after the arguments are
  read, so `--help` still answers; `--version` and every run refuse. A
  record that is a directory is refused in the usual form, by a run and by
  an install, before anything is placed.

install.sh:
- Both curls HTTPS only, redirects included, TLS 1.2 or newer.
- Sets `OPENREPOSHAPE_RECORD_REF=1` for the install.
- A fetch the server refused (curl exit 22) asks whether the release
  exists; no answer at all points at the network instead. A preflight that
  exits 1 names what is missing; any other status says it did not run to
  the end.
- With no pipx, `apt-get` machines are told `sudo apt-get install -y pipx
  && pipx install git-filter-repo==2.47.0` (printed, never run), because
  Debian 12 and Ubuntu 23.04+ refuse `pip install --user`.
- A command that did not record the tag gets its preflight run with the tag
  named, not main's setup.sh; the shell-profile notes say so.
- The last line is `{ main ${1+"$@"}; }`: a download cut inside it runs
  nothing. install.sh itself never runs sudo; the wording now says the
  preflight's offers, some of them sudo, each wait for a typed yes.

Tests: HTTPS_ONLY in the call asserts and a guard on every curl that runs
in both scripts; the opt-in, links, a directory record and `--help` past a
bad record; an upper-case and a relative `location`; a non-default
OPENREPOSHAPE_BIN_DIR; the apt-get branch; the network failure; the
preflight's two failures; three truncated downloads; S9073 composite
asserts split. docs/cli.md regenerated for the `--help` change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…, the fix round on #577: install.sh itself never runs sudo while the preflight it ends with offers what is missing behind a typed yes, the command is pinned to the release tag only when asked, step 3 folds the pin into one paragraph six lines shorter, AGENTS.md says the one-line install is the human's to run, and the notes gain the steps that attach install.sh to the release (#564)

- README.md: step 3's paragraph now carries what "pinned" means and the
  corrected sudo sentence, and leaves the exit statuses to `install.sh
  --help` and the release notes; the worked example's lead-in is one line.
  The cap moves to 1444 (from 1450 in d0cdbf4), with the reason updated in
  test_readme_is_short_enough_to_be_read.
- docs/handbook.html: the same paragraph, the same rule in "For AI
  assistants", the #564 footer clause updated, and the new README blob.
- AGENTS.md: "The one-line install (`install.sh`) is theirs to run: relay
  it, never run it." under "Never answer an install prompt", paid for by
  tightening the `--allow-upstream-org` paragraph's middle clause; still
  490 lines, every rule kept.
- docs/release-notes/v1.0.1.md: the corrected sudo sentence, the opt-in
  (`OPENREPOSHAPE_RECORD_REF=1`) and what a script that exports
  `$OPENREPOSHAPE_REF` now gets, HTTPS only, Git Bash supported but its
  tests skip on the Windows job (a known limit, followed up in v1.1.0), and
  a "Before tagging (remove at tag time)" section: upload install.sh to
  v1.0.1, check the `releases/latest/download/install.sh` redirect, never
  attach it to v1.0.0, and publish a later patch on an older line with
  `--latest=false`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…follow in order (land, prove the squash tree or dispatch the three-OS run on it, tag it annotated, create the release with install.sh attached and --verify-tag, check the asset by redirect and by bytes), Git Bash is said to be meant to work but untested until #585, and only install.sh's own downloads are said to be HTTPS only (#564)

The second fix round on #577 (RV577). The old checklist published before
attaching install.sh, could tag main through `gh release create` with no
tag, uploaded an unchecked working-tree file and never tested the release
commit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…-564-carry-to-main

Conflicts, both resolved to main's side, because main retired the
mechanism #577's side used (#519, #551):
	tests/test_repo_hygiene.py: README.md's cap stays README_CAP_BASE plus
	  tests/readme-cap.d/; #577's inline 1422 -> 1444 raise is not taken here
	  and becomes a ledger file of its own in the next commit.
	docs/handbook.html: the footer keeps main's frozen history and its
	  closing sentence; #577's "added (#564)" clause and whole-README blob
	  are the retired convention, and the sections' markers carry it instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… the one-line install in a ledger file of its own, as main raises it, rather than #577's inline 1422 -> 1444 on release/v1.0 (#564)

README.md on main is 1427 lines at its cap (1422 + 5, #550); #564's lines
take it to 1449. The reason is #577's, carried with its accounting:
ten lines for the two install.sh blocks, nine for step 3's paragraph,
two for the by-hand ways kept at main, one for the Layout row.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…S is a typed yes and the one-line install asks nothing, placing the command and git-filter-repo through pipx without sudo, and the eight sections that read README.md § Starting and § Layout record #564's README after a re-read (#564)

What changed in README.md since the markers were set is #564 alone:
`git diff f6eaab7 HEAD -- README.md` equals `git diff origin/main HEAD --
README.md` (the § Starting markers were set by #579, f6eaab7), and § Layout
moved only by the install.sh row since #551 (5e59e3b) set its marker.

Re-read against the merged README, each section in full:
- masthead: its fastest path is #577's install.sh line with an accurate
  summary (newest release, pinned, git-filter-repo where pipx can, ends in
  the preflight). No change.
- start: carries #577's install step, the by-hand ways at main and "Each way
  installs". One statement was made false: "Every action that changes the
  machine or the account is a typed yes" stood a few lines below install.sh
  installing git-filter-repo with pipx unasked. It now scopes the rule to
  what the tool offers and says what the one-line install does, every fact
  quoted from README step 3. The machine: block paragraph, the preflight
  step, Run it and What setup.sh actually runs stay true as written.
- partway (the WARNING), ci (private legs), legs (the --explain line),
  doublepin (the hand-edited pin), park (openRepoTools' line, which
  --install still prints when install.sh calls it): nothing #564 touched.
  No change.
- layout: carries #577's install.sh row, in README's order; "standard
  library only" stays true (install.sh is bash, git-filter-repo a tool).
  No change.

Markers: § Starting f0693d8a -> cc78d254 in masthead, start, partway, ci,
legs, doublepin, park; § Layout c8be0af4 -> 94701efc in layout; both values
as the hygiene check printed them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings in #580 (#546), dd3f23a. One conflict:
	tests/test_repo_hygiene.py: #564's one-line-install test and #580's
	  DOCTOR_PASSAGES test were added at the same place; both are kept,
	  #564's first.
The launcher, its test, README.md, AGENTS.md, docs/handbook.html and
docs/cli.md merged cleanly, each holding both sides' hunks unchanged.
README.md stays 1449 lines (cap 1449) and AGENTS.md 490 (cap 490). Every
handbook marker of § Starting, § Layout and § Keeping equals its segment's
fingerprint in the merged README after a re-read, so none moves.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…yped yes, beside step 3's one-line install, which installs git-filter-repo with pipx where it can (#564)

"Nothing is installed without that typed `yes`" became "The preflight
installs nothing without that typed `yes`", and the rest of the sentence is
kept. The longer clause re-wraps the NOTE's last eight lines into eight, so
README.md stays at 1449, its ledger cap, and every line stays within 80
columns. `py setup-project.py --preflight` stays on one line, because
test_the_windows_commands_name_files_that_exist reads `py <file>` across
whitespace.

Re-read against the new wording, the seven sections that read
§ Starting: start's consent rules ("Every action the tool offers ... is a
typed yes", "The one-line install asks nothing ...", "no offer at all") and
its four "The tool offers the rest" panels already put the typed yes on the
preflight's offers; its `machine:` block line is about --install, and
partway's "Nothing is created before the typed yes" quotes the WARNING, not
the NOTE; masthead, ci, legs, doublepin and park render nothing from the
NOTE. No page text changes. The markers move as the hygiene check printed:
cc78d254 -> c9aebc08 in masthead, start, partway, ci, legs, doublepin, park.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ore its own two acts, and the preflight it ends with asks before each offer (#564)

"The one-line install asks nothing" was not true as written: install.sh
ends with `openRepoShape --preflight`, which asks `Type yes to continue:`
before each offer, as step 3 above it and README.md:239-241 say. Only
install.sh's own two acts, placing the command and installing
git-filter-repo with pipx, go unasked. Found in #587's review.

Handbook text only, in the same five lines: README.md is unchanged, so no
marker moves, and the hygiene check asks for none.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ch as the openRepoShape pipx places for the openreposhape package, places nothing, and names both ways out (#563)

pipx places the package's commands in ~/.local/bin as links into the
package's own environment, where `--install` places this command. pipx
leaves a file it did not place alone, but `--install` copies with `cp`,
which writes THROUGH a link: an install over pipx's `openRepoShape` would
overwrite the package's command with the bash one and leave the link
standing. So `--install` refuses such a target, a dangling link included,
before anything is collected, placed or recorded, and names the two exits:
keep the package's (`pipx upgrade openreposhape`) or remove the link (`pipx
uninstall openreposhape`, or `rm`) and run it again.

One loop in install_commands and one test after
test_install_refuses_to_also_scaffold, kept apart from the rest of #563 so
that it merges on its own. Approved with #563's design by Brett Heap at
2026-10-15T13:32:51Z ("approve all six for #563").

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…c link, such as the openRepoShape pipx places for the openreposhape package, before it asks the network anything (#563)

The launcher's own `--install` now refuses a linked target, but install.sh
installs a RELEASE's launcher, which may predate that check and copy
through pipx's link into the package's environment. So install.sh checks
the target itself, first, and exits 2 with nothing requested, fetched or
placed, naming the same two exits: keep the package's (`pipx upgrade
openreposhape`) or remove the link and run the line again. Its header's
exit 2 lists the case.

Kept apart from the launcher's commit and the rest of #563 so that it
merges on its own. Approved with #563's design by Brett Heap at
2026-10-15T13:32:51Z ("approve all six for #563").

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…penRepoShape, scaffold-project, adopt-project, update-shape and shape-doctor, each run from a full clone of the standard at the commit the package is stamped with, checked before every run (#563)

Brett Heap's approval of #563's design, 2026-10-15T13:32:51Z: "approve all
six for #563". This is its first pull request's package, as designed:

- THIN. The wheel holds `openreposhape/` (standard library only, in
  SHIPPED) and the tree's `setup-project.py` byte for byte, beside the
  modules, so `openRepoShape --preflight` runs before git is installed.
- THE STAMP. `src/openreposhape/_release.py` holds the version, tag, commit
  and digest; the committed one says 0.0.0 and unstamped, and pyproject.toml
  reads the version from it (`dynamic = ["version"]`). A stamp naming part
  of a release is refused.
- THE CLONE. Every other run uses a full clone at the stamped 40-hex commit,
  made once per commit in the per-user cache (`$XDG_CACHE_HOME`,
  `~/Library/Caches`, `%LOCALAPPDATA%`) and kept. Before each run with no
  override it must be at the commit, carry no change, and digest to the
  stamp with its own `scripts/repo_shape.py` (`sorted-ls-tree-r-v1`).
  `$OPENREPOSHAPE_REF` overrides the commit for one run, with no digest
  checked; `$OPENREPOSHAPE_REPO` names a fork or a mirror. An unstamped build
  follows main and says so. Beside a checkout (`src/` of one), it uses that
  checkout as it stands, the launcher's `--doctor` rule.
- THE COMMANDS. `openRepoShape` re-states the v1.0.1 launcher's modes,
  routing, one-mode rule, `--org` handling, usage, ref checks and
  `--doctor`'s run, and `--version` prints the version, tag, commit, digest
  and clone. `--install` refuses and names `pipx upgrade openreposhape`. A
  scaffold hands over to the clone's setup-project.py as setup.sh does
  (`--into` first, `OPENREPOSHAPE_SELF_BOOTSTRAP=1`), and a typed
  `--shape-ref` is that clone's ref, as it was setup.sh's. The other four run
  their file from the clone with every argument unchanged, in the directory
  typed in.
- git-filter-repo==2.47.0 is the one dependency. Every child gets the
  package environment's scripts directory first on PATH whenever a
  git-filter-repo is there; no tool changes.

`src/`, not the root, because the root holds `openRepoShape`, the launcher,
and `openreposhape/` beside it is the same name on a case-insensitive disk
(macOS's and Windows' default). The wheel still holds `openreposhape/`.
Hatchling 1.27.0 builds it: its `force-include` places setup-project.py,
and it is the newest hatchling that runs on Python 3.9.

TESTS. tests/test_openreposhape_package.py imports the launcher's own cases
from tests/test_openreposhape_command.py, unchanged, and an autouse fixture
of its own points that module's `run_cmd` and `USAGE_LINES` at the Python
command for each test it collects, so the same cases run against both, on
all three OSes, Windows included, where the launcher's module skips. The
four launcher cases that run its file (from stdin or by path) are re-stated
for the Python command. Its own cases cover the stamp, pyproject,
`--version`, the cache (once per commit, reused with the repository gone,
nothing left by a failed clone), the refusals of a changed clone, a clone at
another commit and a wrong digest, the override, main followed, the four
tools' argument passthrough, the scaffold hand-over, `--install`'s refusal,
a machine with no git, and the PATH every child gets, in process and through
a real venv. tests/test_repo_hygiene.py holds every module of the package in
SHIPPED, and the package's copy of openRepoTools' install line to the
others. No existing test or skip condition changed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…heel twice on Python 3.9, checks the two are one file holding this commit's bytes, installs it with pipx and runs the five commands, outside pytest (#563)

The pytest suite stays network-free, as the header says, and now says
which job is the exception: building the wheel needs the backend
pyproject.toml pins and installing it needs git-filter-repo, both from
PyPI. So a fifth job on ubuntu, with no pytest in it:

- builds the wheel twice at the commit's own time (SOURCE_DATE_EPOCH) and
  `cmp`s the two;
- reads every file in the wheel but its metadata against `git show HEAD:`,
  byte for byte, setup-project.py included;
- installs it with pipx into the job's own directories, on Python 3.9, the
  oldest the package supports;
- runs `--version`, `--help`, `--install` (refused, naming pipx upgrade),
  `--preflight`, and `--doctor --json` over an empty repository against
  this commit served from a bare repository on disk, and requires the
  machine row to find the git-filter-repo pipx installed beside the
  package, with none on PATH; then the four tools' `--help`.

actionlint 1.7.12 (the pinned one) passes over the file, with shellcheck.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…the link and where it points in two assertions, not one (#563)

SonarCloud python:S9073 on #590: one `and` hid which half failed. Only the
test ba83231 added changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…[[`, and its test asserts the link and where it points in two assertions (#563)

SonarCloud on #590: shelldre:S7688 on the new test in install.sh (bash
3.2 has `[[`), and python:S9073 on the new test's one `and`. Only the lines
8cae45c added change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… loop is one small function per kind of argument, the repeated literals are named once, and the tests change sys.path only through monkeypatch (#563)

SonarCloud's findings on #590, none of them in behaviour:
- python:S3776: `parse` (cognitive complexity 31) takes each argument
  through `take` and five helpers, and `main_openreposhape` (22) through
  `doctor_alone` and `settle_the_names`. The routing is case for case what
  it was; every refusal keeps its words.
- python:S3516: `version` always returned 0; `print_version` returns
  nothing and the caller exits 0.
- python:S1192: `--placement-plan`, `setup-project.py`,
  `$OPENREPOSHAPE_REF` and `--shape-ref` are constants.
- python:S8997 and S9073 in the tests: `package_module` uses
  `monkeypatch.syspath_prepend` and `monkeypatch.setattr`, and three
  composite assertions are split.

The gate's one failing condition, pythonsecurity:S8705 in `run_child`, is
not changed: handing the person's arguments to the standard's own script,
unchanged and with no shell, is what the four commands are.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…Python 3.9 it installs with, not the runner's pipx with --python (#563)

The job's first run on #590 failed in `pipx install --python <3.9>`: the
runner's pipx 1.16.7 runs on the image's Python 3.12 and shares one pip
with every environment it makes, and that pip cannot run on 3.9
(`TypeError: dataclass() got an unexpected keyword argument 'slots'`). So
the job installs pipx into the 3.9 itself, at 1.11.1, the newest release
that installs on 3.9, and its shared pip is then one 3.9 runs. Rehearsed
locally on Python 3.9.25: the wheel installs, and the doctor's machine row
finds the git-filter-repo pipx installed beside the package.

The first run also showed what the plan left to check: the ubuntu image
ships pipx (1.16.7), actions/setup-python still offers 3.9 (3.9.25), and
two builds of the wheel at the commit's time are one file whose every
module is this commit's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s only (#563)

SonarCloud githubactions:S8541 on #590: a `pip install` without
`--only-binary :all:` may run a package's setup script. pipx 1.11.1 and
everything it needs on Python 3.9 come as wheels (checked on 3.9.25:
argcomplete, click, packaging, platformdirs, tomli, userpath), so the job
says so. The wheel this repository builds is still built from its source,
which is what the job is for.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…3-pypi-package

Brings in #587's refresh onto main fb786c1 (#568, #581), 25b2aee. No
conflict: the refresh touches none of this branch's files, and every hunk
of #563 stays as it was.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…is no link, so `resolved` hands it back unchanged (#563)

RV590's nit: "symlinks followed" described nothing once the refusal above
it made a linked target impossible. The call stays, because it names the
record as `read_recorded_ref` does. Comment only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…py: an annotated tag named in $OPENREPOSHAPE_REF runs its commit, a killed run's half-made clone is swept after an hour, and the clone's fullness, the tag-before-branch order and 128 + the signal are tested (#563)

RV590's fix round:
- A BUG THE NEW TEST FOUND. `remote_commit` looked for a tag's peeled
  line (`refs/tags/<ref>^{}`), but `git ls-remote -- <url> <ref>` prints
  that line only when a pattern names it too, so an annotated tag
  resolved to its tag object; the clone then checked out the tag's commit
  and `verify` refused the run ("it is at <commit>"). `remote_commit` now
  asks for `<ref>` and `<ref>^{}`; a ref is letters, digits and `._/-`,
  so neither pattern holds a wildcard. Shown with protocol v0 and v2.
- SHOULD 1: `test_the_clone_is_full` clones through `file://`, where git
  honours `--depth`, and checks `--is-shallow-repository` is false and
  every commit of the synthetic standard is in the clone.
- SHOULD 2: the bare-run test runs DETACHED, with the logging `git`
  first on PATH and the repository nowhere.
- SHOULD 3: `PythonCommand.detached` sets `$OPENREPOSHAPE_REPO` nowhere
  as `__call__` does, and `__call__` runs in the test's directory; the
  two tests that read the default repository's name set it empty, which
  is unset (`${VAR:-default}`).
- SHOULD 4: the usage says "run from a checkout's src/ (an editable
  install)".
- SHOULD 5: `sweep_incoming` removes an `.incoming-*` directory nothing
  has touched for an hour before a clone is made, never following a
  link and never refusing over one it cannot remove; tested with one two
  hours old (removed) and one fresh (kept).
- SHOULD 6: the pyproject comment says only what the package job shows.
- Nits: `test_a_ref_that_is_a_tag_and_a_branch_is_the_tag` (a lightweight
  tag over a branch of its name, and an annotated tag over one) and
  `test_a_tool_killed_by_a_signal_exits_128_plus_the_signal` (POSIX:
  SIGTERM is 143). The stub sends itself `$STUB_SIGNAL` where it is set.

Each new test fails on its mutation: `--depth 2` (shallow 'true'), the
sweep removed (leftover kept), branches before tags, the peeled pattern
dropped, and the raw status returned (241).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e ASCII (#563)

RV590's nit: the package's `openRepoShape` prints its usage and every
refusal to the console the command was typed in, which on a stock Windows
is PowerShell 5.1's, so its modules are held to `setup-project.py`'s rule
by a new test parametrized over the directory. The existing test is
unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n annotated tag named in full, `refs/tags/<tag>`, in $OPENREPOSHAPE_REF runs its commit (#563)

RV590's re-check of 6179bd7: `remote_commit` looked a full name up as
given, never as `<ref>^{}`, so `OPENREPOSHAPE_REF=refs/tags/<annotated>`
took the tag object for the commit and every run was refused ("...
should be this command's clone of the standard at <tag object>, and it is
at <commit>"). The docstring says a full `refs/...` name works, and the
bash launcher accepts that spelling. The lookup now tries `<ref>^{}`
before `<ref>`; `git ls-remote` already prints that line, because the
peeled pattern asks for it.

`test_a_ref_that_is_a_tag_and_a_branch_is_the_tag` gains the case
`refs/tags/annotated`, which failed before the fix with that refusal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ents.txt: the release's build tools and the install check's pipx are hash locks, hatchling at pyproject.toml's own pin and each of its requirements by digest (#563)

The release workflow installs exactly these files with `--only-binary
:all: --require-hashes`. The build lock names the `build` frontend and
`hatchling==1.27.0`, pyproject.toml's `[build-system]` pin, with
hatchling's own requirements: packaging, pathspec, pluggy,
trove-classifiers, and tomli below Python 3.11 (RV590, SHOULD 6). The pipx
lock is the install check's: pipx 1.11.1 below Python 3.10, the newest
that installs on 3.9, and pipx 1.17.8 above it.

Both are universal (Python 3.9 and later, Linux, macOS and Windows),
resolved by uv 0.12.18 from PyPI as it stood at 2026-10-01T00:00:00Z, and
each header carries the one command that regenerates it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ion> builds the openreposhape wheel twice from `git archive` of a commit that passed tests, tests-macos and tests-windows, uploads it to TestPyPI, installs it with pipx on three systems and Python 3.9, and uploads the same bytes to PyPI (#563)

Decision 5 of #563's design, approved by Brett Heap at
2026-10-15T13:32:51Z ("approve all six for #563"), adapted from
opensoft/openDox-code's release.yml at c94878be.

build refuses unless the version is X.Y.Z and not 0.0.0; the dispatched
ref is the annotated tag v<version> pointing at the run's commit (read
from the forge and from git), or a branch, which is the rehearsal; the
checkout is that commit and clean; both environments name a required
reviewer and admit only the tags v*; `tests`, `tests-macos` and
`tests-windows`, run by tests.yml, passed on the commit itself; and the
checkout's own `scripts/repo_shape.py` digests the commit to what
`tree_digest_from_gh` reads from the forge. It then installs its tools
from the hash lock, extracts the commit twice with `git archive` into two
directories, writes the stamp into each, builds a wheel from each at
SOURCE_DATE_EPOCH, requires the two to be one file, checks every file but
the stamp and the metadata against the commit byte for byte, and records
the sha256.

testpypi and pypi run only on a v* tag, in their environments, by trusted
publishing; they alone get `id-token: write`. Each checks the artifact's
digest, the environment's rules and this run's approval at use time, the
tag again, and that the index holds no other file of the version; after
the upload the index must serve exactly the recorded digest. install runs
on ubuntu, macOS and Windows, and on Python 3.9: pipx from its hash lock
installs TestPyPI's wheel, taken from TestPyPI's file host by the
recorded digest, with git-filter-repo from PyPI alone, then runs
`--version`, `--preflight`, `--doctor` over an empty repository (its
machine row must find git-filter-repo in the package's environment), `git
filter-repo` on the PATH the package gives its children, the four tool
commands, and on Windows `openRepoShape` from PowerShell. On a branch,
both upload jobs are skipped, the install check runs over the build's own
wheel, and both environments' v* limit refuses any copy of this file.

The header's "RELEASING TO PyPI" section is what the docs pull request
points at: no notes file for v1.1.0 exists yet. Nothing here runs until
Brett Heap creates the trusted publishers and the two environments.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ease is where the wheel is shown reproducible, and what the pin alone leaves resolved on the day of a build (#563)

RV590, SHOULD 6: one runner's two builds in one checkout show less than
the comment claimed. The release workflow is the real check: it builds
from `.github/release-build-requirements.txt`, hatchling at this pin and
each of its own requirements by digest, twice, from two `git archive`
extractions of the tagged commit in two directories, and uploads nothing
unless the two are one file. Comment only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s and #817's entries, #642's upgrade note for a holder's `cd` line, and v1.0.0's `--into` limit kept open, current to main 64ec47c (#563)

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

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

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

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Oct 17, 2026
…efusals and `family-operation-in-progress` name a path as `shown_source` names it, so an ESC or a line break in a path no longer reaches the terminal (#692) (#731)

#622's folder refusals printed a path as it was. `family.py init --into <a file named wd-ESC[31mRED> --dry-run` gave `REFUSED family-folder-cannot-be-made: the family folder .../wd-^[[31mRED/Orbit cannot be made: ...` through `cat -v`. A name with a line break and then `WARNING forged-line: pwned` printed that line as a line of its own. R612's `family-operation-in-progress` printed the mark's path across two lines for a holder whose path holds a newline.

Each path is now spelled as `shape_materialize.shown_source` spells a `--pin-source`: as it is, and as Python's `repr` spells it where a character in it is not printable.

- `scripts/shape_materialize.py`: both answers of `folder_cannot_be_made` (a file or a bad stat in the way, and a folder this user cannot write in), the folder in `refuse_unmakeable_folder`, and the folder in `make_folder`.
- `scaffold-project.py`: `scaffold-target-exists`.
- `scripts/family.py`: `family-target-exists`, and the mark's path in `family-operation-in-progress` (`where.as_posix()`).

A printable path is named byte for byte, so no existing row changes and `why.endswith(CANNOT_WRITE_IN)` in `refuse_unmakeable_folder` still holds. No help text changed, so `docs/cli.md` is untouched. No README, handbook or template sentence says how a refusal names a path (`git grep` of `shown_source`, `target-exists` and `folder-cannot-be-made` in AGENTS.md, README.md, `docs/handbook.html` and the templates found none), so no doc changes.

- **`shown_source`, not `shown_path`.** Since #634, `shape_materialize.shown_source` returns `repo_shape.shown_path(source)` (`scripts/shape_materialize.py`: `return shown_path(source)`), so the seven call sites this change adds (four in `shape_materialize.py`, one in `scaffold-project.py` and two in `family.py`) spell a path as `shown_path` does. Renaming them to `shown_path` stays out of this PR: it would only move imports in `scaffold-project.py` and `family.py` and reword docstrings in `family.py`, `shape_materialize.py` and the test file, and the review's N1 left it optional. I did not take it. The predicted merge with main works with the alias: `family.py --help` and `scaffold-project.py --help` exit 0 on it, and `shown_source` and `shown_path` return the same string for an ESC name, a line-break name and a plain path.
- **`shown_source` is imported by name into each file's existing `from shape_materialize import (...)` block.** The first commit put it on a line of its own after the block; lane 4's #660, #688 and #693 add an import at that same place, and `merge-tree` reported add/add conflicts in `scripts/family.py` and `scaffold-project.py`. The second commit moves the name into the block, and those conflicts are gone.
- **"The holder in the other refusals of the same block" is one refusal.** The `# What the holder is in the middle of, and what its sparse checkout leaves out` block holds `family-operation-in-progress`, `family-sparse-excluded` and `member-path-unaddable`. Only the first prints a path of the holder's. `family-sparse-excluded` names `members/<Project>` and `.gitmodules`, and `member-path-unaddable` names `members/<Project>`.
- **No scaffold row for `--local-remote-dir`.** A control character there is refused earlier as `scaffold-unwritable-value`, which already names the path with `repr`. The unit test covers `make_folder`, which only a race reaches by the command line.

`tests/test_dry_run_parity.py`:
- `SCAFFOLD_CASES` gains five rows: ESC and line break in the `--work-dir` name, ESC in a folder name above it, and ESC and line break in a work dir that holds a file named `Atlas` (`scaffold-target-exists`).
- `FAMILY_CASES` gains five rows: ESC and line break in `--work-dir`, ESC in `--into` (`the family folder`), and ESC and line break in a work dir that holds a file named `Orbit` (`family-target-exists`).
- Two rows join `UNWRITABLE_FOLDER_AT` (ESC in the scaffold's and in `init`'s `--work-dir`, a folder this user cannot write in).
- Rows named `ESC-...` and `line-break-...` are skipped on Windows, whose file names cannot hold them (`POSIX_NAMES`, `case_params`). Every expectation spells the path with `repr`, and not with `shown_source`.
- `assert_refused` now fails any stderr line that starts with `WARNING forged-line`.
- Three unit tests: `test_each_folder_question_spells_a_name_that_is_not_printable` (ESC, line break; `folder_cannot_be_made`, `refuse_unmakeable_folder` and `make_folder` by themselves), `test_a_folder_this_user_cannot_write_in_is_spelled_safe_to_show`, and `test_a_printable_name_is_named_byte_for_byte` (a name with a space and an accented letter stays unquoted).

`tests/test_family_refuse_up_front.py`: `test_a_holder_path_with_a_newline_still_shows_its_merge` asserted the raw newline in the refusal (`"Ink\nRouter/.git/worktrees/" in ran.stderr`). It is now `test_a_holder_path_with_a_control_character_still_shows_its_merge`, over a line break, an ESC and a forged line, and asserts the one-line `repr` form (`refusal()` fails unless the stderr is two lines). It is the one existing test this change had to edit.

Results at head `ab55c29f` (Python 3.12.3, Linux):
- `tests/test_dry_run_parity.py tests/test_dry_run_asks_remotes.py tests/test_family_dry_run.py tests/test_family_init_landing.py tests/test_repo_hygiene.py tests/test_pin_source_unreadable.py tests/test_scaffold_leg_path_collisions.py tests/test_scaffold_reuse_seeded_legs.py`: **494 passed**.
- `tests/test_family.py tests/test_family_refuse_up_front.py tests/test_family_init_values.py tests/test_family_add_refusals.py`: **356 passed, 19 skipped** (the skips are `test_family_init_values.py:219` and `:291`, template-quoting premises).
- Not run: the full suite (the coordinator's gate), and macOS and Windows (CI is Linux-only since #597). The new hostile-name rows are POSIX only by design.

On the base code: the review overlaid this head's two test files on main `2fac9790` and ran `tests/test_dry_run_parity.py` and `tests/test_family_refuse_up_front.py` with `-k "dry_run_parity or control_character"`. The result is **20 failed, 87 passed** (76 deselected, 43.5 s). The 20 are exactly the new and edited tests: the 5 scaffold and 5 `init` rows, the 4 unwritable-folder rows, the 3 unit tests (`test_each_folder_question_spells_a_name_that_is_not_printable` in two cases, and `test_a_folder_this_user_cannot_write_in_is_spelled_safe_to_show`), and the 3 cases of the edited `test_a_holder_path_with_a_control_character_still_shows_its_merge`. The one new test that passes on main's code is `test_a_printable_name_is_named_byte_for_byte`, by design: a printable name is named byte for byte on both. An earlier draft of this body said 21 failed, 2 passed. That run was on `8062810d` with a wider selection, and it held a scaffold row for ESC in the local remote dir's name (`ESC-in-the-local-remote-dir-name`) that this head's tests no longer have; its second pass was the existing `test_the_refusal_quotes_gh_s_first_line_and_escapes_what_is_unprintable`, which also passes in the new run, one of the 87.

`family.py init` and `scaffold-project.py --dry-run` against a file named `wd-ESC[31mRED`, a file named `wd-\nWARNING forged-line: pwned`, and a work dir with a file `Atlas` or `Orbit` in it, run with a stub `gh` and no real `gh`:
- On main, through `cat -v`: `... the family folder /.../wd-^[[31mRED/Orbit cannot be made: /.../wd-^[[31mRED is a file, not a folder`, and a stderr line `WARNING forged-line: pwned cannot be made: ...` of its own.
- On this head: `REFUSED family-folder-cannot-be-made: the family folder '.../wd-\x1b[31mRED/Orbit' cannot be made: '.../wd-\x1b[31mRED' is a file, not a folder`, and one line for the forged name: `--work-dir '.../wd-\nWARNING forged-line: pwned' cannot be made: '...' is a file, not a folder`. `scaffold-target-exists` and `family-target-exists` are one line each in the same form.

Each spelled site reverted alone in a throwaway clone at `19d126cd` (the call sites are the same at `ab55c29f`); the new tests that fail for each:
- `folder_cannot_be_made`, the `_not_a_folder` answer: 8 failed (six CLI rows and the two unit tests).
- `folder_cannot_be_made`, the `CANNOT_WRITE_IN` answer: 5 failed (the four unwritable rows and the unit test).
- `refuse_unmakeable_folder`: 12 failed.
- `make_folder`: 2 failed (the unit test only).
- `scaffold-target-exists`: 2 failed (its two rows).
- `family-target-exists`: 2 failed (its two rows).
- `family-operation-in-progress`: 3 failed (line break, ESC, forged line).

`git merge-tree --write-tree` of this head `ab55c29f` against origin/main `20c8873b` and against every ref on origin that could meet it, fetched 2026-10-16: the 41 branches (`fix/*`, `l3-*`, `wip/*`, `docs/*` and `release/v1.0`) and the 55 PR heads from #630 up. That is 96 refs. (The same list was run for #730's head `7f2bb36b`.)
- **origin/main `20c8873b`: clean** (the predicted tree `00b5b24c`, which differs from main in these five files only: 216 lines added, 25 removed).
- **#630's head `6fd33a93`: clean.** It conflicted with main in `docs/handbook.html` when this body was first drafted, and no longer does, with or without this change. This change touches no README or handbook.
- **The refs that edit one of this change's five files**, and so could meet it, all merge clean with this head: `fix/646`, `fix/651`, `fix/654`, `fix/660`, `fix/661`, `fix/684`, `fix/686`, `fix/688`, `fix/689` and `fix/693`, the heads of #670, #725, #728, #743 and #744, and the heads of the landed #634, #638, #639, #640, #643 and #671. #634 has landed (`30676c90`), so the `fix/660` note about a conflict with it is gone.
- **Four refs conflict with origin/main on their own**, and so with this head: `docs/release-notes-v1.0.0` and `release/v1.0`, and the pre-squash heads of the landed #637 and #641. Their conflicts do not come from this change. Every other ref, 92 of the 96 in all, is clean with this head, including the refs that conflict with main in `templates/family-root/scripts/validate-family.py` (#682, #745, #747, `fix/648`, `fix/681`, `fix/704`) or in `shape-doctor.py` (#742, `l3-683`) on their own. So do the old heads of the landed #638 and #639, and lane 3's `l3-677-pins-hand-step-wip` branch, and each of them merges clean with this head too.
- **Tests on the predicted merge**, run by the review on the merge with main `2fac9790`: `test_dry_run_parity`, `test_family_refuse_up_front`, `test_pinned_copy_text`, `test_pin_source_unreadable`, `test_dry_run_asks_remotes`, `test_family_dry_run` and `test_family_init_landing`: 445 passed in 215 s. `test_family`, `test_family_init_values`, `test_family_add_refusals`, `test_derived_id_refusals`, `test_repo_hygiene`, `test_naming_policy`, `test_scaffold_leg_path_collisions` and `test_scaffold_reuse_seeded_legs`: 758 passed, 19 skipped in 243 s. I did not re-run them on `20c8873b`; the merge is clean, and the two commits since `2fac9790` (#700 and #702) change only `shape-doctor.py`, `templates/assembly-root/scripts/validate-pins.py` and four test files, none of them a file of this change.
- Before the second commit, `fix/660`, `fix/688` and `fix/693` each conflicted with this head in the import region of `scripts/family.py` or `scaffold-project.py` (a separate import line each side added at the same place). That is why the import joins the existing block.

#634 is on main, and this change does not edit `shown_source` or its docstring in `scripts/shape_materialize.py`.

The review approved with nits. Its S1, the stale base-run numbers and merge lines, is taken here, in this body: no code changed, and the head is still `ab55c29f`. Its N1, renaming the call sites to `shown_path`, is not taken (see "Decisions and their evidence"). It also re-ran 5 of the 7 mutation probes on `ab55c29f` and got the same failure counts as above (12, 2, 3, 2 and 2), and ran 144 hostile-name dry runs (16 names in 9 scenarios): on main 69 rows put a control character on stderr and 6 print a forged line of their own; on this head all 144 exit 2 with no traceback, no control character and no forged line.

- `scripts/family.py` `init`'s plan prints `folder` and `landing` lines with the path raw (`print(f"landing      {holder}   ...")`, `print(f"folder       {folder}   ...")`). On a dry run or a refused run the plan is on stdout before the refusal, so `WARNING forged-line: pwned/Orbit   (--work-dir: no family folder)` still appears there. It is the plan, not a refusal, and not one of the issue's lines.
- Other refusals that name a holder or folder path raw, none of them in the issue's lines: `family-holder-detached` and `family-on-tracking-branch` (`{family.root}`), `family-index-locked` (`lock.as_posix()` in the detail), `family-landing-in-standard` (`{holder}`, `{checkout}`, `{beside / family / family}`), `family-holder-already-here` (`{into}`), and `family-remote-exists` and `scaffold-remote-exists` (`{target}`, a local remote path).

1. **family.py `init`'s plan prints the holder and folder paths raw.** `scripts/family.py` `landing` and `folder` rows (about :1756-1759) print `holder` and `folder` as they are, so a forged line or an ESC reaches stdout in a `--dry-run` and in a run refused after the plan. Direction: spell them with `shown_source`.
2. **The same raw spelling in the refusals listed above.** `family-holder-detached`, `family-on-tracking-branch`, `family-index-locked`, `family-landing-in-standard`, `family-holder-already-here`, `family-remote-exists` and `scaffold-remote-exists` each print a path the person supplied or git keeps. Direction: the spelling this change gives its sites.

Review: rv692, lane 5's independent review, approved ab55c29 with nits, and its body fix (S1) is published. CI (Brett Heap's ruling 27 and his fast-refresh ruling of 2026-10-15T22:48:35Z): H0 ab55c29 passed tests, tests-git-bash and SonarCloud; the head 13f643d is a clean update-branch merge of main 431c8cf, with tests-git-bash and SonarCloud green; and the local gate passed on its predicted squash tree 4327947 over main 431c8cf: the dry_run_parity, family_refuse_up_front, repo_hygiene, agents_md_codes, cli_reference sets, 330 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 #692

Lane: openreposhape-5 (openRepoShape-5)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brett Heap and others added 4 commits October 17, 2026 18:00
main gained #748 (#662, `431c8cf8`), #731 (#692, `d66a9054`), #810
(#674, `d17564d9`), #809 (#688, `42aed788`), #816 (refs #684,
`fa04d2cd`), #813 (#689, `f6d17bc5`) and #723 (#663, `bad05d21`) since
64ec47c. No conflict, and none of them touches README.md, the handbook,
AGENTS.md, docs/ or the README cap's ledger, so every handbook marker
reads as before. README.md is 1474 lines, its cap; AGENTS.md 494, its cap.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#813 and #723, v1.0.0's quoted-header and plain-path-clone limits closed and the counts current to main bad05d2 (#563)

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

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

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
main gained #746 (#672, `fc867207`) since bad05d2. No conflict, and it
touches no README.md, handbook, AGENTS.md, docs/ or README cap ledger
file, so every handbook marker reads as before. README.md is 1474 lines,
its cap; AGENTS.md 494, its cap.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…it closed as one clause, the list's and the known limits' markers made final, the triage lines named by their issues, and the counts current to main fc86720 (#563)

- #746 (#672, `fc867207`): a new bullet, "`bump-leg.py`'s branch and its
  lines.", after "`bump-leg.py --dry-run` gives the real run's answer.".
  `git merge-tree --write-tree bad05d2 a5139a3`, the head #746 was
  gated and landed on, equals the squash's tree `bb920ac1`. The head the
  entry was written from, `4a014a79`, sat on #723's head before its
  squash and does not merge clean with main; `scripts/bump-leg.py` is one
  blob (`226e7b0d`) at `4a014a79`, `a5d473ee`, `a5139a32` and the squash,
  and the squash's own lines in each of its five files equal that head's,
  so the entry stands as written.
- v1.0.0's limit on the paths listed as staged (v1.0.0.md:433) is one
  clause, in #746's entry: with #810 it closes. #810's half-clause is cut.
  `bump-leg.py --dry-run` over a pin its validators refuse (:440) stays a
  limit, which the dry run now says.
- "What changed" runs to main `fc86720`, and says #795 is added if it
  lands next; Known limits' re-read marker and "as read so far" are cut.
  The counts marker stays as the release-body tool reads it.
- Known limits name each triage by its issue: "the checklist triage of the
  L4 lines on #457 and #459" and "of the L1 lines on #457", where they
  named lanes 4 and 1 and D9, and each id reads "#457's L4-05", "#459's
  L4-24" and so on.
- Counts in the four places: main `fc86720`; 121 pull requests since
  `bfed143`; at 19:08Z, 131 of the `v1.1.0` milestone's 133 issues
  closed, the open two #563 (#630) and #686 (#795).

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

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-3, 2026-10-17T19:23:12Z) RV630's final delta review of #630, part 1 (head d697b366, on main fc867207): READY.

Part 2, after #795 lands:

RV630 then re-checks that delta, and #630 lands under Brett Heap's ruling of 2026-10-17T18:57:19Z ("go with option 1 for #630").

Lane: openreposhape-3 (openRepoShape-3)

Brett Heap and others added 2 commits October 17, 2026 20:15
main gained #795 (#686, `6f64eea2`) since fc86720. No conflict, and it
touches no README.md, handbook, AGENTS.md, docs/ or README cap ledger
file, so every handbook marker reads as before. README.md is 1474 lines,
its cap; AGENTS.md 494, its cap.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…f64eea, RV630's lead and two named limits under Known limits, and the counts current to main 6f64eea (#563)

- #795 (#686, `6f64eea2`): a new bullet, "`bump --to`, reachable or
  not.", before "family.py refuses before it writes.", the pending
  file's text as written. `git merge-tree --write-tree fc86720 <head>`
  equals the squash's tree `0f2d2ddd` for `17bbbaba` and for the final
  head `cf143c69`. For the head the text was written from, `cf5e64b3`,
  the merged tree differs from the squash only in
  `tests/test_family_bump_reachability_words.py`, so `scripts/family.py`
  is as the text says.
- "What changed" runs to main `6f64eea`, with no word of a later pull
  request.
- Known limits, RV630's words: the lead says the list is not all of
  v1.0.0's open limits, and names two this release worked on and left
  open, the `--into` sentence (#695) and `bump-leg.py --dry-run` over a
  pin its validators refuse (#746, #672).
- Counts in the four places: main `6f64eea`; 122 pull requests since
  `bfed143`; at 20:15Z, 132 of the `v1.1.0` milestone's 133 issues
  closed, the one open #563 (#630).

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

Copy link
Copy Markdown

@brettheap

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-3, 2026-10-17T20:25:55Z) RV630b's final delta review of #630, part 2 (d697b366..aef568f0, on main 6f64eea2): READY. There are no MUST or SHOULD items.


RV630b: final delta review of #630, d697b366..aef568f0 on l3-563-docs

Verdict: READY. There are no MUST or SHOULD items. There are three optional NITs, and none blocks the landing. The head was still aef568f02a9c626c9ae93f67e1a508c14684312d and main 6f64eea2 at 20:23:40Z (ls-remote). I made no GitHub write. The predicted squash commit 21fdc03a exists only in my scratch clone and was never pushed.

Checklist

  1. The merge is exact.

    • 016599b0 has parents d697b366 and 6f64eea2. Its tree 679d7341 equals git merge-tree --write-tree d697b366 6f64eea2 (exit 0).
    • git diff --stat d697b366 aef568f0 touches 4 files:
      • docs/release-notes/v1.1.0.md (32 lines);
      • scripts/family.py;
      • tests/test_family_bump_reachability_words.py;
      • tests/test_family_bump_to_local_only.py.
    • The three code files' blobs at the head equal main's (a7660e9c, 6dfdb9ba, 7867db6a). aef568f0 alone changes only the notes (+22 −10).
    • The head's diff against origin/main is still the PR's twelve files. README, AGENTS.md, the handbook, docs/cli.md and readme-cap.d/ are unchanged since d697b366.
  2. family.py: bump fetches every branch of the member's origin whatever the mount's refspec and drops the origin/ refs no ref of origin backs, says a tag of the remote reaches a commit no branch does instead of "a pin the rest of the world cannot fetch", and tells a mount with no origin it has none instead of to fix the network (#686) #795's entry is at v1.1.0.md:449-456, directly before "family.py refuses before it writes.". Each claim was checked against git show 6f64eea2 and scripts/family.py on main:

    Claim Measured on main
    "fetches every branch … whatever fetch refspec the mount has" :6568 is _ORIGIN_BRANCHES = "+refs/heads/*:refs/remotes/origin/*", used in the fetch at :7186. The old fetch at fc867207:6995 was a plain git fetch -q origin.
    "prunes the remote-tracking branches origin has deleted" :7186 runs fetch -q --prune --no-prune-tags origin …. This is Brett's ruling relayed in the squash message ("prune the fetch the tool already makes").
    "a commit a branch holds is no longer refused for a narrowed refspec" / "one only a deleted branch held is no longer pinned" The squash message and the code comment at :7165-7184 agree.
    "only a tag of the remote reaches is told that a tag is not a branch" :6735-6743; the refusal still applies, only the words change.
    "no remote named origin is told so" :6727 (member-to-local-only), :6858 (member-to-unknown) and the note at :7191.
    "where each was told the pin cannot be fetched, or to fix the network" The old words are at fc867207:6589 ("fix the network or the credential") and :6597 ("A pin the rest of the world cannot fetch").
  3. Line 120 reads runs to main \6f64eea`., ending with a plain period. The sentence spans :119-120: "The list" / "runs to main 6f64eea`.".

  4. Known limits.

  5. Counts.

  6. Release-body dry run.

    • Facts: main_sha 21fdc03 (the predicted squash), 2026-10-17 20:22, pr_count 123 (bfed143..21fdc03a), closed 132, open 1, with no fills (no fill marker is left).
    • Run on the notes as they stand in the predicted squash (byte-identical to the head's), with --release-commit 21fdc03a5f6c7110e054816ec6f850f871782773 --repo <clone> --dry-run, under the stub gh.
    • Result: tool-exit=0. It dropped the leading comment (:3-6), "How this release is cut (remove at tag time)" (:828-932) and "Before tagging (remove at tag time)" (:933-950).
    • It filled the counts; replaced <release-commit> ×1; rewrote 3 links and checked all 3 at 21fdc03a; and left no WARNING.
    • Grepping the body for placeholder / remove at tag time / <release-commit> / FIRST DRAFT / <!-- / illustrative finds nothing (exit 1). There are 0 CRs.
    • Body: 826 lines, 52202 bytes, sha256 3672feb3dd2ce64caec36fecbf8f4e2bbe7ccce97a20ab4f989790dbb45ef9d7.
    • Commit ids it lists: 21fdc03 (line 9), bfed143 (line 10), and 6f64eea (line 115, line 120's text, which is deliberate and not stale).
  7. Tests, run in a detached worktree at aef568f0 with /usr/bin/python3, a stub gh first on PATH exiting 97, GH_TOKEN=bogus, an empty GH_CONFIG_DIR and no GIT_ALLOW_PROTOCOL:

    Command Result
    pytest -q -p no:cacheprovider --basetemp=…/bt-RV630b-docs -o tmp_path_retention_policy=failed on the 8 docs files (test_repo_hygiene, test_readme_sections, test_cli_reference, test_release_workflow, test_agents_md_codes, test_setup_by_hand_commands, test_readme_windows_quoting_sentence, test_update_add_codes_documented) 300 passed in 39.63s
    scripts/render-cli-reference.py --check [ok] docs/cli.md is what the generator produces (exit 0)
    git diff --check origin/main...aef568f0 exit 0, clean
    README cap 1474 lines = 1422 + 2 + 5 + 12 + 22 + 10 + 1 = 1474, at the cap
    AGENTS.md cap 494 lines; the cap is <= 494 (test_repo_hygiene.py:667)

    The worktree was clean after the runs.

  8. Predicted squash. git merge-tree --write-tree origin/main aef568f0 exits 0, giving tree dd0f94e86d8c97bc756335ffb68d3a5fd38c3919, which is the same as the head's own tree.

    • My local predicted commit on origin/main is 21fdc03a5f6c7110e054816ec6f850f871782773, and it changes the same 12 files (+1232 −107).
    • The brief named no other lane-3 v1.1.1 PRs, so I merged against none.

SonarCloud and CI on the head

From gh pr checks 630 and the check runs for aef568f0:

  • SonarCloud Code Analysis: success, "Quality Gate passed" (20:17:36Z, head_sha aef568f0).
  • package and tests-git-bash passed.
  • tests was still in progress at 20:23Z. Under Brett's 18:57:19Z ruling it is not awaited; main's push run on the release commit is still required before the tag.

MUST / SHOULD

None.

NITs (optional; READY either way)

Mutants

No pytest reads v1.1.0.md's content: tests/ cites it only in a docstring at test_release_workflow.py:580, and test_repo_hygiene.py:1567 names only v1.0.1.md. Text drift in the entry, line 120 or Known limits is therefore killed by no test, only by review.

The counts marker is guarded by the release-body tool. In a scratch copy I changed "had landed" to "landed", and the tool returned exit 1, an empty body, and REFUSED: line 14: the paragraph under **[placeholder: the counts at tag time.]** is not the sentence the tool fills.

Lane: openreposhape-3 (openRepoShape-3)

@brettheap
brettheap marked this pull request as ready for review October 17, 2026 20:26
@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 2 days and 4 hours by commenting @sourcery-ai review. Upgrade to get a review now.

@brettheap
brettheap merged commit 190133d into main Oct 17, 2026
5 checks passed
@brettheap
brettheap deleted the l3-563-docs branch October 17, 2026 20:27
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>
brettheap pushed a commit that referenced this pull request Oct 17, 2026
…ranch changed different README sections, so five handbook markers take main's blobs, the Families marker keeps this branch's, and no line conflicts (#695)

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

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

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

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

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

Lane: openreposhape-5 (openRepoShape-5)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
brettheap pushed a commit that referenced this pull request Oct 17, 2026
…parent this branch was stacked on, as it landed (#786)

#782 (#741) landed as squash a9d1579, whose tree is a59cb53's (`git diff
--stat a59cb53 a9d1579` is empty). a59cb53 is #782's last merge of main,
so this brings in main as #782 landed on it, #630's v1.1.0 docs (190133d)
among it.

The one conflict is docs/handbook.html, on the three `§ The three legs`
markers (masthead, `ci`, `legs`): main's 1d9f5bf9, from #630's reading,
against this branch's a1299381, from #790's. README.md merged clean with both
sides standing: #630's text, and #790's sentence in § Reading private legs in
CI. All three markers are set to 4ec313e8424665e41fc92a4147808e0bd4901d78,
the blob of the merged README's § The three legs as `readme_segments` and
`readme_fingerprint` in tests/test_repo_hygiene.py compute it (`git
hash-object --no-filters` of the segment agrees). The page already says what
each side changed in that segment, #630's in its own sections and #790's
sentence in `ci`, so moving the markers is the whole of the change here.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap pushed a commit that referenced this pull request Oct 17, 2026
…side #785's `$'…'` sentence (#785)

README.md conflicted in one hunk, the "Windows, natively" paragraph: main's
#630 rewording (the two-liner is the Windows way in that installs nothing,
pipx's `openRepoShape` runs the same flow) is kept whole, and #785's two
lines on `$'…'` follow it, as they followed the old text.

docs/handbook.html conflicted in seven hunks, each one § Starting marker
(masthead, start, partway, ci, legs, doublepin, park). The prose merged
cleanly: #630's pipx text and #785's `$'&hellip;'` sentence stand together in
`start`. The seven markers were recomputed from the merged README.md with
tests/test_repo_hygiene.py's own readme_segments and readme_fingerprint
(3d4a01ffadc322f88dfffd9cef50b1c48c58b9b3), and handbook_marker_problems
reports none. Every other marker is main's and still current.

README.md is 1476 lines, at its cap: main's 1474 + 2 from
tests/readme-cap.d/785.txt. AGENTS.md is 494 lines, at its cap of 494.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Oct 17, 2026
… `--into` sentence names the `family-landing-in-standard` refusal, `make siblings` is described as it stops before cloning, and `members/` has its one-spelling rule (#695) (#738)

The family docs now say what `family.py init`, `make siblings` and the `members/` directory do since #488, #578 and #574. Base: `8062810d`. Head: `c42286de`: the work (`73fa3ff4`) and one commit for the review's nit on L20.

**The branch now holds `origin/main` at `24a977c0`, through one merge commit, `75e35f14`.** No other commit was added, and nothing was rebased or amended.

### What failed

Since #682 landed (`dc7d3fbb`), `git merge-tree --write-tree HEAD origin/main` against `c42286de` stopped on one line of `docs/handbook.html`:

    Auto-merging docs/handbook.html
    CONFLICT (content): Merge conflict in docs/handbook.html

It is the `families` section's marker, `<!-- reads README.md § Families: ... blob <hash> -->`. #682 changed a sentence of README § Families, so main's marker is `b3d19092...`. This PR changed other sentences of the same section, so its marker is `0364c579...`. Git saw one line changed two ways. Lane 2's note on #738 (2026-10-17T12:47:17Z) named the same line. I reproduced the conflict on the head I started from, then merged by hand.

### The fix

The merged README § Families is neither side's text, so the marker takes its fingerprint, `82d7f8087b9ee4d097d33e15fbfe5ae754a04a79`. I computed it with `tests/test_repo_hygiene.py`'s own `readme_fingerprint` over the merged `README.md`. It is the value lane 2's note gave, and it did not move when main moved on to `24a977c0`.

| marker value | where it came from | handbook outline test |
|---|---|---|
| `b3d19092...` | main's side | fails: "has moved since the page read it" |
| `0364c579...` | this PR's side | fails, same message |
| `82d7f808...` | the merged README § Families | passes |

I re-read both sides before taking the value:

- Main's side brought one sentence into README § Families (line 955, "`make bootstrap` reports and skips one it lacks a credential for."), and #682 had already put the matching sentence on the page.
- This PR's side is its own 55 README lines and 27 handbook lines in that section, which merged clean.
- Both sides' sentences stand in the merged README and in the page.

### Did the merge change README or handbook text beyond the conflict line?

No. I compared each changed file's diff against the parent that did not make it:

- `README.md`: the merge against `origin/main` shows exactly this PR's own change, and the merge against `c42286de` shows exactly main's own change since `8062810d` (one line, #682's). No line differs.
- `AGENTS.md`: the same two comparisons, no line differs.
- `docs/handbook.html`: the same two comparisons differ in the marker line only, in both directions.
- README's handbook markers (23 on the page) and README's line cap are untouched. `tests/test_repo_hygiene.py` passes with the cap and the markers as they are.

This PR still lands only in lane 3's README turn. If another README PR changes § Families first, the marker moves again, and the hygiene test names it.

### Tests

All on `75e35f14`, through the gh stub, with a bogus token and an empty `GH_CONFIG_DIR`.

| run | files | result |
|---|---|---|
| core | `test_readme_sections.py`, `test_family_docs_one_spelling.py` (the one test file this PR changes), `test_repo_hygiene.py`, `test_agents_md_codes.py`, `test_cli_reference.py` | 240 passed |
| handbook readers | `test_readme_windows_quoting_sentence.py`, `test_setup_by_hand_commands.py`, `test_update_add_codes_documented.py` | 20 passed |
| family-root templates | `test_pinned_copy_text.py`, `test_template_values_single_pass.py`, `test_family_init_values.py`, `test_family_init_landing.py`, `test_siblings_family_name.py`, `test_family_makefile_siblings.py`, `test_family_siblings_placement.py` | 582 passed, 19 skipped |
| bootstrap (main's new files, over the template this PR reworded) | `test_bootstrap_spelling.py`, `test_bootstrap_without_git.py`, `test_bootstrap_leg_moves.py` | 251 passed |

### Mutants

This round adds no test and no code, so its one hunk is the marker value. Each mutant changes that value in a shared scratch clone at `75e35f14` and runs `tests/test_repo_hygiene.py::test_the_handbook_follows_the_readmes_outline`:

| mutant | result |
|---|---|
| none (control) | 1 passed |
| main's marker `b3d19092...` | 1 failed |
| this PR's marker `0364c579...` | 1 failed |
| the right marker with its last digit changed | 1 failed |
| the unresolved conflict block (`<<<<<<<`, both lines, `>>>>>>>`) | 1 failed |

### Merge checks

- `git merge-tree --write-tree HEAD origin/main` is clean at `24a977c0`.
- Main moved to `40e678e6` (#805, `templates/family-root/scripts/siblings.py` and `tests/test_siblings_inputs.py`) after this merge. It touches no README, handbook or AGENTS text, and the merge-tree against it is clean.
- Lane 2's #822 is stacked on this PR. Its reviewer found it merges clean with this PR landed at the same marker value.

1. **The `--into` sentence** (README.md § Families and the handbook). It gave the default, "the directory you are standing in", and not that `init` refuses that default inside a checkout of this standard. It now says so and names the code: `(the directory you are standing in, by default; refused inside a checkout of this standard, \`family-landing-in-standard\`)`. AGENTS.md already said it.
2. **The `make siblings` paragraph** (README.md § Families and the handbook). It said `make siblings` "WARNS and prints the exact `mv`". Since #578 it also clones nothing until the `mv` is run, so the next run clones each member once, and a link of the family's name beside the folder passes. It now says that. Lane 3's two #460 sentences ride in it (L20, L21, which lane 3 marked `-> #695`):
   - the folder must be named EXACTLY after its repository, case included, on every disk;
   - for a holder at `~/projects/InkRouter` the move makes the family folder `~/InkRouter`, beside `~/projects`, and the holder `~/InkRouter/InkRouter`, not `~/projects/InkRouter/InkRouter`.
   I wrote L20 with the paths spelled out. Lane 3's wording was "beside that folder's parent", which two readings can take. My first wording, "beside that folder, not inside it", still left one: the thing beside `~/projects` is the family folder, not the holder (rv695 N1). The printed move for that holder is `mv ~/projects/InkRouter ~/InkRouter.holder`, `mkdir -p ~/InkRouter`, `mv ~/InkRouter.holder ~/InkRouter/InkRouter` (probe below, and `test_family_siblings_placement.py` pins it).
3. **The one-spelling rule for `members/`.** One sentence, the same words in five places: AGENTS.md (family item 2), README.md § Families, the handbook, the holder's README and the holder's `AGENTS-shape.md`:
   > A holder keeps `members/` in ONE spelling on every disk: `add` refuses `family-members-dir-variant` (`member-path-taken` for a variant of the member's own path) until a human renames what sits under another spelling.

`templates/family-root/AGENTS-shape.md` changes by that one sentence. It is in `FAMILY_COPIED_VERBATIM`, so every holder reads `upstream-changed` for it on its next `update-shape.py check`. The issue's third line names it, and lane 3 asked that it ride with the next pinned-file edit. It can share the one v1.1.0 shape bump with the bootstrap fix. `templates/family-root/README.md` is templated and not pinned: an existing holder keeps its own README.

- README.md is 1458 lines, one under its cap of 1459 (1422 + 5 + 22 + 10 in `tests/readme-cap.d/`); it was 1459 at `73fa3ff4`. The brief said 1449; that is the cap before #627 landed. AGENTS.md is 492, its cap.
- The new words come to about nine README lines and three AGENTS.md lines. I fitted them by cutting wording that is stated elsewhere. **If you would rather keep the cut sentences, the alternative is a `tests/readme-cap.d/695.txt` of about +8, which needs Brett's word.** What I cut:
  - README § Families:
    - the aside "the same lands-where-you-were-standing rule projects have";
    - four rewordings that lose no meaning:
      - the intro of "Use a family when the parts deploy separately": "folding them into one assembly root would give them one gate, one release and one pin, which is the opposite of what 'deploy separately' means" is now "one assembly root would give them one gate, one release and one pin, the opposite of 'deploy separately'";
      - "`--work-dir` remains the override" is now "`--work-dir` is the override";
      - "The doubled name is kept on purpose" is now "The doubled name is on purpose";
      - "`family.yaml` is what tells the two apart" is now "`family.yaml` tells the two apart";
    - the caption "Eight services, each its own project, one holder:" under "The InkRouter example" (the section's intro and the block under it say it; this is the one line cut to make room for the L20 rewording, and the handbook keeps its caption);
    - the tool's rationale after "Nothing ever moves a checkout" (the shell, the editor, the agent lanes, "an act by surprise"); it stays in the handbook, the holder's README and the tool's own output, and README keeps the reason that matters (a linked worktree's `.git` file carries an absolute path);
    - ", not merely a repository at the right commit";
    - the private-legs reason in the `make bootstrap` paragraph;
    - "to a policy two live projects already carry a copy of" in the naming paragraph.
  - AGENTS.md:
    - the converse in the family intro ("one project is what parts that ship together already are"; README keeps it);
    - the sentence "`bump` takes the same repeatable `--trailer`" in family item 3 (stated in "Advancing a leg" step 6 and in the holder's `AGENTS-shape.md`);
    - "Do not pass `--yes` on your own initiative" in "Updating" step 2, now "Never pass `--yes` yourself", the words `test_repo_hygiene.py` requires elsewhere in the file.
- The handbook marker for README § Families is recomputed with `readme_segments` / `readme_fingerprint`: `89ef0ab5ec88...` to `0364c5794c46...` (it was `dc85a9260a67...` at `73fa3ff4`, before the L20 rewording and the caption cut). No other README section changed, so no other marker moves.

New file `tests/test_family_docs_one_spelling.py`, 17 tests: the one-spelling sentence in each of the five docs, the two codes held as strings by `scripts/family.py`, and the `--into`, `make siblings`, "named EXACTLY" and "straight in a folder" sentences in README and the handbook, plus the old "WARNS and prints" sentence gone from both. The handbook is read as the text a browser shows (`<code>` as backticks).

- On base `8062810d` (the file copied in): **15 failed, 2 passed**. The two that pass are the code-exists checks.
- On the head `c42286de`, after the L20 rewording: `test_repo_hygiene.py`, `test_readme_sections.py`, `test_agents_md_codes.py` and the new file: **217 passed** in 3.37s. The new file's `STRAIGHT_IN_A_FOLDER` is in the new README and not in the old one.
- Before that, the targeted runs, each at `8062810d` plus the working tree that was then the head (`73fa3ff4`):
  - `test_repo_hygiene.py`, `test_readme_sections.py`, `test_agents_md_codes.py`, `test_cli_reference.py`, `test_readme_windows_quoting_sentence.py`, `test_setup_by_hand_commands.py`, `test_update_add_codes_documented.py` and the new file: **253 passed**.
  - Fifteen files that read AGENTS.md, the holder's docs or the refusals (`test_family_init_landing`, `test_siblings_family_name`, `test_family_switch_branch_exists`, `test_family_tracking_branch`, `test_template_text_placeholders`, `test_template_values_single_pass`, `test_template_quoted_values`, `test_free_plan_hint`, `test_doctor_recorded_answers`, `test_update_shape_add`, `test_update_shape_header_comments`, `test_family_add_refusals`, `test_family_members_case_variants`, `test_family_member_path`, `test_doctor_legs_members_next`): **987 passed**.
  - Eight files: `test_adopt_plan`, `test_adopt_github_exits`, `test_adopt_late_failure`, `test_setup_project_py`, `test_family`, `test_family_siblings_placement`, `test_family_makefile_siblings`, `test_park_resume_targets`: **598 passed, 1 skipped** (`SHAPE_MEDXEHR_CLONE` unset: a plan test needs a read-only clone of MedxSoft/MedxEHR, a cloud tool).
  - `test_update_shape.py -k "shape_line or stale_readme or check_says_what_apply or readme_with_no_shape"`: 7 passed (the tests that read the holder README's last line). The rest of `test_update_shape.py` and `test_shape_doctor.py` were not run: they copy the family-root tree and match none of the lines I changed (`git grep` of the changed sentences under `tests/` finds nothing), and a first run of them crawled under a load of 90-100 on the machine.
- The full suite was not run, as the brief says.

- Row 1, on base: from a checkout of the standard, `python3 scripts/family.py init --org Acme --family Orbit --dry-run` ends `REFUSED family-landing-in-standard ... Remediation: pass \`--into ..\``. The README sentence did not say it would; it now does.
- Row 2, on head: `parent_folder_warning` of the holder's `siblings.py` for a holder at `<t>/home/projects/InkRouter` prints `NOTHING WAS CLONED` and the move `mv .../projects/InkRouter .../InkRouter.holder`, `mkdir -p .../InkRouter`, `mv .../InkRouter.holder .../InkRouter/InkRouter`. For a folder named `inkrouter` on Linux it stops the same way: the name test is exact on every disk.
- Row 3: the refusals the sentence names are the ones `test_family_members_case_variants.py` and `test_family_add_refusals.py` drive (60 + 65 tests, passed above).
- No row needs real GitHub or a cloud tool.

My own run did not happen: a usage checkpoint stopped it before it began. The reviewer (rv695) ran 30 mutants against `73fa3ff4` in a throwaway clone and **all 30 were killed**: five places, four mutants each for the one-spelling sentence, and five each for the README and the handbook sentences. The L20 sentence has changed since, and `STRAIGHT_IN_A_FOLDER` with it; no mutant was run on the new words. What the guards cover there: the sentence has a parametrized case per doc, so reverting it fails that case, and a reverted README hunk leaves the handbook marker stale.

- origin/main `55c6ba7d` (it moved past `2fac9790` by #700): clean. rv695 found `73fa3ff4` clean against `2fac9790`.
- #630 (lane 3), head `6fd33a93`: clean. It merged main `de926142`, and its handbook conflicts at the three markers are gone. On the merged tree README.md is 1472 lines and AGENTS.md 494 (counted here; rv695 found them under #630's own ledger at `73fa3ff4`).
- #682 (lane 5), head `8b39cd19`: conflicts at one line, the handbook marker for README § Families (`0364c5794c46...` here, `c40baa828dca...` there). README.md and the rest of the handbook merge clean, and the merged paragraph reads correctly, because both PRs change that section. Whichever lands second recomputes the marker from the merged README: on this head plus #682 the recomputed value is `da54c8ec2665...` (computed with `readme_fingerprint`, not test-run).
- I dropped the earlier list of clean PRs: it was taken against older heads, and several of them have landed.

(Brett Heap, 2026-10-16T13:36:23Z, on #459 line 141: "document it in the v1.1.0 release notes". `docs/release-notes/v1.1.0.md` exists only on #630, so this PR does not create it.)

> A holder made with `--family X --name Y` before #642 (the fix for #629) keeps `cd Y` in its own README, and `update-shape.py apply` never re-renders a README, so change that one line by hand to `cd X`: the holder's directory is named after its repository, `X`, never after the display name.

Such a README also shows `Y` in its layout block and its doubled-name sentence, as prose; only the `cd` line fails when run.

- The holder's README does not get lane 3's L20 and L21: lane 3 asked for them in the README paragraph, and the holder's README already names the folder after the repository.
- Checklist candidate, not for this PR (rv695 N6): the holder's own docs say the folder is "named after the family" (`templates/family-root/README.md:73`, `templates/family-root/AGENTS-shape.md:99`) and not "EXACTLY, case included", which the standard's README and handbook now say and `siblings.py` tests (`folder.name == family`). Lane 3 asked for L21 only in the standard's README paragraph, so it is not owed here; it could ride in the same re-pin.

Review: lane 5's independent review approved c42286d with its nits fixed; 75e35f1 is its update onto main (lane 2's note, issuecomment-6234057822); Codex found no major issues. CI (Brett Heap's ruling 27): H0 is the head 0b26ace itself, which passed tests, tests-git-bash and SonarCloud; main has touched none of its files since its base 2e476a7; and the local gate passed on its predicted squash tree 68dd413 over main 112eeed: the family_docs_one_spelling, readme_sections, readme_windows_quoting_sentence, setup_by_hand_commands, update_add_codes_documented, family_tracking_branch, family_switch_branch_exists, pinned_copy_text, repo_hygiene, agents_md_codes, cli_reference, family sets, 527 passed. Landed on Brett Heap's word of 2026-10-17T21:47:28Z, in lane 5's session (posted on #473 as issuecomment-6241758069): "merge lane 5's v1.1.1 PRs once reviewed and green".

Closes #695

Lane: openreposhape-5 (openRepoShape-5)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Oct 18, 2026
…every NEXT, keep line and WHY, as itself where the report's encoding writes its bytes and escaped whole where it does not, so a line naming `Projets/Équipe` or a Latin-1 name runs as printed (#785) (#792)

Closes #785.

`ascii_text` in `shape-doctor.py` replaced every character of a path that is
not ASCII with `?`, in every NEXT, keep line and WHY and in `--json`, so such
a line named another path and did not run as printed. The flattening was there
so that printing never raised, and since #214 `main()` installs a handler that
makes a print safe.

Now `quote_arg` quotes an ASCII value as before, byte for byte. A value that
is not ASCII is quoted as itself where every such character is printable and
the report's encoding writes it (on POSIX, with the bytes the path holds).
Otherwise the whole value is escaped: as `$'...'` with octal escapes for `sh`,
or as `('...' + [char]0x00C9 + '...')` for PowerShell. `ascii_text` still
flattens the typography it always did, and any other character only where the
report cannot show it. `--json` is written for UTF-8 whatever stdout holds,
and a name read back under a legacy multibyte filesystem encoding keeps its
bytes.

`$'...'` runs in bash, zsh and ksh but not in dash, and README.md, the
handbook and AGENTS.md now say so. The PowerShell form is asserted as text and
was not run in PowerShell, which the host lacks.
`tests/test_doctor_non_ascii_paths.py` runs the spellings under several
locales and stdout encodings.

Lane 3's independent reviewer RV-a (opus) found it READY (#792
issuecomment-6232864874), and READY again after two fix rounds (#792
issuecomment-6234129925). RV792m found its merge round with #630 READY (#792
issuecomment-6243295865), and RV792p the round that builds its test's
environment from #818's `paste_env()` (#792 issuecomment-6243694009). It
lands on Brett Heap's word "merge lane 3's v1.1.1
drafts after the v1.1.0 tag" (2026-10-17T13:00:57Z).

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

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

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

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

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Oct 18, 2026
…book: under Git Bash (`MSYSTEM` set) the lines that follow the host are quoted for `sh`, through one question, `host_shell_dialect`, so the doctor's lines, setup's family lines and `family.py`'s `NEXT`, `switch -c`, `add` and `cd` lines paste whole in the shell that printed them, and README's Windows sentence says so (#711) (#821)

The tools that quote a printed line for the host chose PowerShell's
dialect wherever `os.name` was `nt`, and it is `nt` under Git Bash too,
which runs the Windows Python. So Git Bash was handed
`'C:\Users\Jane O''Neill'`, which it reads as `C:\Users\Jane ONeill`, a
bare `C:\work\Atlas`, which it reads as `C:workAtlas`, and `Copy-Item`
and `$env:PYTHONPATH = ...;`, which it has no command for. Brett Heap
chose option (a) of #532's Git Bash question at 2026-10-16T13:07:34Z.

- scripts/repo_shape.py: `host_shell_dialect(name=None, environ=None)`
  answers "nt" (PowerShell) on a Windows host whose environment has no
  `MSYSTEM`, or an empty one, and "posix" (`sh`) everywhere else, Git
  Bash and every MSYS2 shell included. `shell_quoted` and `shell_line`
  ask it when they are given no `platform`; an explicit one keeps its
  meaning. Its docstring names the limit: a PowerShell started from
  inside Git Bash inherits `MSYSTEM` and is answered `sh`'s.
- setup-project.py carries a copy, as it does of the quoting, because it
  runs before any checkout of the standard exists. `SHELL_BARE`, decided
  at import from `os.name`, is gone: `shell_word` asks the question at
  each call and takes the alphabet and the escape from one answer, which
  the old code read twice. Setup's family lines follow it (L2-29).
- shape-doctor.py: `quote_arg`, `restore_from_head`, `validator_command`,
  `bump_leg_command`, `checkout_notes`, `submodule_update_next` and
  `copy_command` ask it, so Git Bash gets `sh`'s quoting, `cp` and
  `env PYTHONPATH=...`. `scaffold_command` keeps its entry point and
  interpreter on the machine (`python setup-project.py` on Windows, from
  either shell) and takes only its quoting from the question. The
  interpreter is `PYTHON`, the machine's, on every line.
- scripts/family.py is unchanged: its host-dialect lines go through
  `shell_quoted` and `shell_line`, `_print_next_steps`'s `cd` included.

Tests: tests/test_git_bash_dialect.py emulates PowerShell, Git Bash, an
empty `MSYSTEM` and POSIX on any host (each module under test gets an
`os` whose `name` is the host's; the real `os.name` is never touched)
and reads each line back with `shlex.split` or `powershell_words`: the
question's answers, setup's copy held to them, `shell_quoted` and
`shell_word` held to one answer on every host, and the lines of setup,
family.py and the doctor. #724's inventory in
tests/test_readme_windows_quoting_sentence.py now fails a function that
quotes for the host and reads `os.name` itself.
tests/test_family_printed_quoting.py reads the host through the
question, and tests/test_setup_project_py.py unsets `MSYSTEM` where it
patched the gone `SHELL_BARE`.

Fix round 1 (rv711, no MUST): no pinned script may import
`host_shell_dialect` (siblings.py's older-library test strips it too, and
the skew test reads all seven pinned scripts that import from repo_shape);
the guard counts the doctor's nine line builders, the functions that hand a
`platform` on to a quoter, as quoters, so `copy_command(s, d, os.name)` in a
new row fails it, and names a quoter's body by `Class.method`; the Git Bash
rows read back a kept repository's line and a lane trailer spelled by the
line's own quoter. A lane name cannot hold an apostrophe (`LANE_NAME_RE`),
so that trailer test hands the quoter one through a stand-in.

README and the handbook: README's Windows sentence keeps "Printed
commands are quoted for a POSIX shell, so paste them into Git Bash",
names `init`'s `cd` line among `family.py`'s, and says the exceptions
are "quoted for the shell they are printed in, PowerShell or Git Bash, on
Windows." #785's sentence after it says a path `shape-doctor.py` cannot
print as itself is written `$'...'` "Off Windows and in Git Bash": its
`quote_arg` now asks `host_shell_dialect` too, so Git Bash is handed
`sh`'s spelling of such a path. The handbook's Windows panel says the
same, and adds that a shell started from inside Git Bash inherits
`MSYSTEM`, so a PowerShell started there is handed Git Bash's quoting,
and a cmd.exe started anywhere else is handed PowerShell's. The seven
markers of README's § Starting a project in a new organisation move to
its new blob, e0d2b672, and tests/readme-cap.d/711.txt files the two
lines this adds (README 1476 -> 1478, the cap 1477 -> 1479).

NIT 6 of lane 2's review of #808, which landed first: the one-question
guard's `calls_to` counts a quoter handed on as a value (`host_values`)
as well as a call, so `map(shell_quoted, w)` beside a branch on
`os.name` is seen.

Stacked on #724, whose final head 949fd4d is merged here and which has
landed as 2000488, merged here with main. #630, which rewrote the
README and handbook around this sentence, has landed too (190133d), and
so has #792 (#785, 5cbec22), which wrote into the same paragraph and
into `quote_arg`; main d7befb9 is merged here with both. The copied
`scripts/repo_shape.py` changes, so it rides in v1.1.1's shape bump.

Closes #711

Lane: openreposhape-2 (openRepoShape-2)
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

Development

Successfully merging this pull request may close these issues.

1 participant