Skip to content

render-cli-reference.py and tests/: the CLI reference's children write no bytecode under templates/, the launcher guard reads family.py's siblings child, the quoting-sentence inventory follows an aliased quoter, a suffixless tool and each tool's calling functions, and an S5778 pytest.raises builds its argument first (#665) - #724

Merged
brettheap merged 2 commits into
mainfrom
fix/665-four-guards
Oct 17, 2026

Conversation

@brettheap

@brettheap brettheap commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

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

Four guards held less than they said. One commit, 151a0861, on 8062810d. No shipped behaviour changes except one environment variable in the generator's --help children. No --help text changes, so docs/cli.md is not regenerated. No quoting changes.

What changed

  1. scripts/render-cli-reference.py: child_env() sets PYTHONDONTWRITEBYTECODE=1. The template's siblings.py does from bootstrap import ... from its own directory, so the generator's --help child wrote templates/family-root/scripts/__pycache__/bootstrap.cpython-312.pyc (ignored by git, so silent). A person who runs the generator now gets the same tree as the test that runs it. The docstring says why. tests/test_cli_reference.py gets one test: the variable is in child_env() whatever the caller had, and an interpreter started under that environment reports sys.dont_write_bytecode as true.
  2. tests/test_validator_child_encoding.py: the launcher guard sees family.py. interpreter_runs accepts [sys.executable or PYTHON, ...] as well as [sys.executable, ...], and LAUNCHERS gains "scripts/family.py": "inherited_child_environment". Dropping env= from family.py siblings' child (adopt-project.py plan raises UnicodeEncodeError in its terminal report, after the plan file is written, for a printable name stdout cannot encode (日本.md under cp1252, as on a redirected Windows stdout), so plan exits 1 before plan written to #214) now fails the guard.
  3. tests/test_readme_windows_quoting_sentence.py: the inventory holds what it claims. It follows from repo_shape import shell_line as joined (and joined = shell_line, however many steps). It scans every file git ls-files lists (tracked, or new and not ignored) that is a .py file or starts with a python shebang, which adds the root bootstrap. It pins functions, not files, for the two tools the sentence names by particular lines: scripts/family.py (_print_next, _switch_command, _add_command, _print_next_steps, _checked_mount_path) and setup-project.py (family_next_command). shape-doctor.py stays every line, because the sentence says "shape-doctor.py's lines". Only the bodies of shell_quoted and shell_line in scripts/repo_shape.py are exempt, by name; the rest of that file is not. Nothing about quoting changes: the Git Bash dialect (option (a)) is its own v1.1.1 issue, and when it moves NAMED, this table moves with it.
  4. tests/test_bump_leg_paste_lines.py: Sonar S5778. placement = bump_leg_module.leg_placement(leg) before the with pytest.raises(LegNotAsFound) in test_pasting_the_status_line_reads_that_leg, as the test above it does. Five lines; the hunk is :401-405, away from bump-leg.py and setup-project.py: the git switch -c line says under it what to do where an earlier act left that branch, so already exists is no dead end (#607's sentence, in the two tools it left) #657's hunks.

Evidence

The reproduction (.pyc). In a fresh shared clone at 8062810d, with PYTHONDONTWRITEBYTECODE unset, python3 scripts/render-cli-reference.py --check printed [ok] docs/cli.md is what the generator produces and left five .pyc files, among them templates/family-root/scripts/__pycache__/bootstrap.cpython-312.pyc at 11533 bytes (the size the issue reports). The same command on 151a0861 printed the same [ok] line and left none (find . -name '*.pyc' found 0).

Mutants. Each is applied to a copy at the head's test files; "old" is the same mutant under 8062810d's own tests.

Mutant New tests Old tests
M0: drop PYTHONDONTWRITEBYTECODE from child_env() (the base's behaviour) test_child_env_leaves_no_bytecode_in_the_trees_it_runs fails: KeyError: 'PYTHONDONTWRITEBYTECODE' none to run
M1: drop env= from family.py's siblings child (scripts/family.py:6543) test_the_tools_that_run_the_projects_validators_give_them_an_environment[scripts/family.py] fails: scripts/family.py:6543 runs the interpreter without `env=inherited_child_environment()` 55 passed (survives)
V2: from repo_shape import shell_line as joined and a call, in update-shape.py test_the_tools_that_quote_for_the_host_... fails: files are [family, setup-project, shape-doctor, update-shape] 3 passed (survives)
V3: family.py's _switch_command calls shell_line(..., "posix") test_the_functions_that_quote_for_the_host_...[scripts/family.py] fails: _switch_command missing from the calling functions 3 passed (survives)
V5: a host-quoter call in the suffixless bootstrap the file test fails: bootstrap is now among the files 3 passed (survives)
V6 (the exemption): a third function of scripts/repo_shape.py calls shell_line the file test fails: scripts/repo_shape.py is now among the files 3 passed (survives)

Each new failure is one test, and the other tests of the file stay green (1 failed, 8 passed; 1 failed, 55 passed). V2, V3 and V5 are R588's mutants as the issue describes them.

Tests

On 151a0861, one run, run9.sh (gh stub first on PATH, GH_TOKEN bogus, empty GH_CONFIG_DIR):

tests/test_cli_reference.py tests/test_validator_child_encoding.py tests/test_readme_windows_quoting_sentence.py tests/test_bump_leg_paste_lines.py tests/test_family_siblings_placement.py tests/test_repo_hygiene.py: 227 passed in 698.59s.

Per file: test_cli_reference.py 23 (one new), test_validator_child_encoding.py 56 (one new case, [scripts/family.py]), test_readme_windows_quoting_sentence.py 9 (6 new: the scripts/family.py and setup-project.py function pins, the alias, the method name, the exemption, the suffixless file), test_bump_leg_paste_lines.py 33, test_family_siblings_placement.py and test_repo_hygiene.py 106 together.

Merge checks (git merge-tree --write-tree HEAD <ref>)

Left out, and why

Checklist candidates

  • A quoter handed on as a value is not a call. map(shell_quoted, words) or functools.partial(shell_line) is not seen by the inventory (said in host_calls' docstring). Nothing does it today.
  • The function pin counts functions, not calls. A function with two host-dialect calls where only one is switched to POSIX still passes. Today the one-call functions are _switch_command, _print_next, _checked_mount_path.
  • _print_next_steps prints a cd {shell_quoted(work)} line the sentence's list (NEXT, switch -c, add, git rm -r --cached) does not name. It belongs to the Git Bash dialect issue.
  • The launcher guard's LAUNCHERS still has no row for src/openreposhape/cli.py (run_child([sys.executable, ...], env)) or setup-project.py (its own run with a PYTHON variable). Both spell the child differently from subprocess.run([sys.executable, ...]).

Closes #665

🤖 Generated with Claude Code

Summary by Sourcery

Harden repository guards and tests so generated trees remain clean and documented launcher and quoting inventories stay accurate.

Bug Fixes:

  • Prevent CLI reference generation children from leaving Python bytecode artifacts in template directories.
  • Ensure the validator launcher guard covers family.py's sibling-process environment propagation.
  • Make the quoting inventory detect aliased and suffixless Python tools and enforce the documented calling functions.
  • Avoid triggering Sonar S5778 by evaluating the leg placement before pytest.raises.

Enhancements:

  • Strengthen static checks for child interpreter launchers and host-dialect quoting against the repository's complete shipped Python tool inventory.

Tests:

  • Add regression coverage for bytecode suppression, family.py launcher validation, aliased quoting calls, function-level quoting inventories, suffixless Python files, and quoter exemptions.

…e no bytecode under templates/, the launcher guard reads family.py's siblings child, the quoting-sentence inventory follows an aliased quoter, a suffixless tool and each tool's calling functions, and an S5778 pytest.raises builds its argument first, so each guard fails when what it names breaks (#665)

Four guards held less than they said.

- scripts/render-cli-reference.py: `child_env()` sets PYTHONDONTWRITEBYTECODE=1,
  so the template's `siblings.py` (`from bootstrap import ...` from its own
  directory) leaves no `bootstrap.cpython-312.pyc` under
  templates/family-root/scripts/ when the generator runs its `--help`
  children. No `--help` text changes, so docs/cli.md is not regenerated
  (`render-cli-reference.py --check` still says it is what the generator
  produces). tests/test_cli_reference.py holds the variable in the
  environment and holds that an interpreter started under it writes none.
- tests/test_validator_child_encoding.py: `interpreter_runs` accepts
  `[sys.executable or PYTHON, ...]` as well as `[sys.executable, ...]`, and
  LAUNCHERS gains scripts/family.py with `inherited_child_environment`, so
  dropping `env=` from `family.py siblings`' child (#214) fails the guard.
- tests/test_readme_windows_quoting_sentence.py: the inventory follows
  `from repo_shape import shell_line as joined` (and `joined = shell_line`),
  scans every file `git ls-files` lists that is a `.py` file or has a python
  shebang (the root `bootstrap`), pins the functions that call a host-dialect
  quoter for scripts/family.py and setup-project.py instead of the file
  (shape-doctor.py stays every line, as the sentence says), and exempts the
  bodies of `shell_quoted` and `shell_line` in scripts/repo_shape.py by name,
  not the whole file. No quoting changes: the Git Bash dialect is a separate
  issue.
- tests/test_bump_leg_paste_lines.py: `leg_placement(leg)` is evaluated
  before the `with pytest.raises(LegNotAsFound)` in
  `test_pasting_the_status_line_reads_that_leg` (Sonar S5778), as the test
  above it does.

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

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-5, 2026-10-16T14:23Z) @codex review

@sourcery-ai

sourcery-ai Bot commented Oct 16, 2026

Copy link
Copy Markdown

Reviewer's Guide

The PR closes four coverage gaps in repository guards: it makes CLI-reference generation bytecode-free, enforces inherited child environments for family.py, strengthens the Windows host-quoting inventory to follow aliases and function-level callers across all shipped tools, and restructures one pytest exception test to satisfy S5778 without changing shipped behavior.

Sequence diagram for bytecode-free CLI reference generation

sequenceDiagram
    participant Generator as render-cli-reference.py
    participant Child as siblings.py --help
    participant Filesystem
    Generator->>Generator: child_env()
    Generator->>Child: subprocess.run(..., env=child_env())
    Child->>Filesystem: import bootstrap
    Filesystem-->>Child: module loaded
    Note over Child,Filesystem: PYTHONDONTWRITEBYTECODE=1
    Child-->>Generator: help text
    Generator-->>Filesystem: no __pycache__ or .pyc written
Loading

File-Level Changes

Change Details Files
Prevents the CLI reference generator's child processes from creating ignored Python bytecode artifacts.
  • Adds PYTHONDONTWRITEBYTECODE=1 to the generated child environment.
  • Adds an integration test verifying both the environment value and Python's effective bytecode behavior.
scripts/render-cli-reference.py
tests/test_cli_reference.py
Expands the validator child-environment guard to cover the family launcher.
  • Recognizes interpreter commands using sys.executable or PYTHON.
  • Adds scripts/family.py to the launcher inventory so its siblings.py child must inherit the validated environment.
tests/test_validator_child_encoding.py
Makes the Windows quoting inventory accurately track aliases, shipped tools, and command-producing functions.
  • Resolves aliased and chained quoter bindings in AST analysis and records calling functions, including class methods.
  • Scans tracked and non-ignored untracked Python files, including suffixless Python-shebang tools such as bootstrap.
  • Pins the named functions for family.py and setup-project.py, preserves shape-doctor.py's all-lines rule, and limits exemptions to the two quoter implementation bodies.
  • Adds mutation-focused tests for aliases, methods, suffixless files, exemptions, and function-level inventory changes.
tests/test_readme_windows_quoting_sentence.py
Avoids Sonar S5778 by evaluating the leg placement before entering pytest.raises.
  • Stores leg_placement(leg) before the exception assertion and passes the result into the tested call.
tests/test_bump_leg_paste_lines.py

Assessment against linked issues

Issue Objective Addressed Explanation
#665 Prevent CLI-reference child processes from writing bytecode into the templates tree, and add a guard that verifies the child environment disables bytecode. ✅
#665 Extend the validator-child launcher guard to recognize the sys.executable or PYTHON form and include scripts/family.py. ✅
#665 Make the quoting-sentence inventory detect aliased quoters, suffixless Python tools, the named tools’ specific calling functions, and host-quoter calls outside the two exempt quoter bodies. ✅
#665 Avoid placing leg_placement(leg) inside the pytest.raises block so an exception from that call cannot satisfy the expected-exception assertion. ✅

Possibly linked issues


Tips and commands

Interacting with Sourcery

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

Customizing Your Experience

Access your dashboard to:

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

Getting Help

@chatgpt-codex-connector

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

Copy link
Copy Markdown

Codex Review Summary

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

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-16T14:26:03.145666Z 151a086 Manual request
ℹ️ About Codex in GitHub

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

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

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

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 151a0861dd

ℹ️ About Codex in GitHub

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

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

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

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

cwd=str(REPO), capture_output=True, check=True).stdout
sources = {}
for raw in sorted(set(listed.split(b"\0"))):
relative = raw.decode("utf-8")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Decode Git paths with filesystem semantics

On POSIX, git ls-files -z returns raw filename bytes, so a tracked or untracked path containing a non-UTF-8 byte causes raw.decode("utf-8") to raise before the code can even determine whether the file is Python source. The previous Path.rglob approach preserved such names through Python's filesystem decoding, and this repository explicitly supports Git paths with non-UTF-8 bytes. Use os.fsdecode(raw) or UTF-8 with surrogateescape so an unrelated filename cannot break this test.

Useful? React with 👍 / 👎.

Comment on lines +240 to +241
calling = set(host_quoting(tool_sources()).get(tool, {}))
assert calling == NAMED[tool], (

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Pin individual host-quoting calls instead of function names

Reducing host_quoting() to its set of function keys makes this guard unable to detect a new or changed command inside an already-approved function. This already masks the cd {shell_quoted(work)} line in family.py::_print_next_steps: the README enumerates that tool's NEXT, switch -c, add, and git rm -r --cached lines, but the test passes because _print_next_steps also contains the approved switch -c call. Comparing the individual calls or command-producing helpers is necessary for the new guard to enforce the sentence it claims to protect.

Useful? React with 👍 / 👎.

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

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

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

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

Copy link
Copy Markdown

@sourcery-ai

sourcery-ai Bot commented Oct 17, 2026

Copy link
Copy Markdown

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

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

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

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

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

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

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

# Conflicts:
#	tests/test_readme_windows_quoting_sentence.py
#	tests/test_validator_child_encoding.py
brettheap added a commit that referenced this pull request Oct 17, 2026
…alue and pins each function's host-dialect calls, and the launcher guard reads `run_child` and `run([PYTHON, ...])`, with rows for `cli.py` and `setup-project.py` (#759) (#808)

**Based on main.** #724 landed as `20004887`; this branch holds it and was restacked onto main as `84ce67c9`.

**Head `08a6ed39`.** The branch holds #759's commit `052410b6`, two merges that took #724's final head and main, and one fix round (`08a6ed39`, lane 2's rv808). Tests only: `tests/test_readme_windows_quoting_sentence.py` and `tests/test_validator_child_encoding.py`. No shipped file changes, and no pinned copy. README, the handbook, `AGENTS.md` and `docs/cli.md` are untouched, and `render-cli-reference.py --check` passes.

One commit, tests only: `tests/test_validator_child_encoding.py` and `tests/test_readme_windows_quoting_sentence.py`. No shipped file changes, so no pinned copy, README, handbook, `AGENTS.md` or `docs/cli.md` text moves. Pushed to `fix/759-quoting-launcher-guards` as a fast-forward of `84ce67c9`, the coordinator's restack onto main (which answers MUST 1, the conflict with main). Nothing was merged, rebased or amended.

### SHOULD 1: the `setup-project.py` row now flips on the fix it names, and the finding says six children

**What failed, measured.** On `84ce67c9`, with the real `setup-project.py` changed in a scratch clone, one change per row. The row is `test_the_tools_that_run_the_projects_validators_give_them_an_environment[setup-project.py]`: `xfailed` means it is still red, `FAILED (XPASS strict)` means it says the finding is fixed. Log: `v116/f808/toolmut/`.

| change to `setup-project.py` | `84ce67c9`'s guard | the new guard |
|---|---|---|
| none (control) | xfailed | xfailed |
| `env=child_environment()` in `run`'s own `subprocess.run`, which reaches all six children | **xfailed: no signal, and `NOT_YET`'s reason stays false** | FAILED (XPASS strict): the row comes out |
| an `env` parameter on `run`, passed at all six calls | FAILED (XPASS strict) | FAILED (XPASS strict) |
| the same, but the call in `why_the_plan_did_not_go_through` left out | **FAILED (XPASS strict), with a child still bare** | xfailed |
| `env=` at the five calls the old guard read, no parameter | **FAILED (XPASS strict), with a child still bare** | xfailed |
| `env=` at four of the calls | xfailed | xfailed |
| a seventh bare child added | xfailed | xfailed, and `test_setup_project_starts_six_children_and_the_guard_reads_each` fails |

Lane 2's two mutants are the bold rows. The first is (a): the guard read the call sites and never `run`. The second is (b): there are six Python children, not five. `why_the_plan_did_not_go_through` runs the plan again, `again = run(plan, cwd=shape_root, capture=True)`, and `plan` is a parameter, so the old `argv_of` could not read the list.

**The sixth child, measured.** I called the real `why_the_plan_did_not_go_through` with a plan that prints a `project      ...` line holding an ellipsis and exits 2, under three encodings (`v116/f808/probe6.txt`):

| `PYTHONIOENCODING` | what the second run's captured stdout holds | the words returned |
|---|---|---|
| `utf-8:strict` | `project      Sample ` and U+2026 | `the plan was refused (see above).` |
| `cp1252:strict` | `project      Sample ` and U+FFFD | the same |
| `cp1252:surrogateescape` | `project      Sample ` and U+FFFD | the same |

So the first five children write into the stream this process reconfigured to UTF-8, which is the two-encodings finding above. The sixth is captured and read as UTF-8 whatever it wrote: a cp1252 byte becomes U+FFFD. Nothing shows today, because the only test on those lines is `startswith(PLAN_STARTS)` and that prefix is ASCII. It is the same defect class, and the fix has to reach it too.

**The fix** (`tests/test_validator_child_encoding.py`):

- `interpreter_runs` reads a list a caller hands a function as a parameter, one level up, and a name bound in each branch of an `if`. A starter's own parameter is left to its callers, which are the runs, so `run`'s own `subprocess.run` is not counted as a seventh. On the shipped files this changes exactly two reads (`v116/f808/probe/survey.py`): `setup-project.py` gains the sixth child (`:2312` on `84ce67c9`), and `scripts/render-cli-reference.py` gains `:403` (NIT 1).
- `environment_of` reads, where a call hands no `env=`, the `env=` of the function it calls when this module defines that function (`run`). A name is read as the LAST thing its function bound it to above the call.
- `NOT_YET` names the six children, says five write into the reconfigured stdout and the sixth is captured, and names both fixes the row reads: an `env=` in `run`'s own `subprocess.run`, or an `env` parameter that all six calls pass.
- `test_the_row_flips_on_the_change_the_finding_names` runs seven changes to a model of `setup-project.py` (the table's rows, on a source string). `test_setup_project_starts_six_children_and_the_guard_reads_each` pins the six functions of the real file: `check_names`, `clone_and_bootstrap`, `plan_and_confirm`, `scaffold`, `self_bootstrap` and `why_the_plan_did_not_go_through`. While the row is an `xfail`, a seventh bare child changes nothing in it, so the count is what would show it.

### NITs 1 to 6: which are taken, which stay

| NIT | outcome |
|---|---|
| **1** `argv_of` read only the first list | **Taken.** Any list the name is bound to above the call counts; the first-or-last choice is gone. `SPELLINGS` has the name bound to two lists in each order (the `render-cli-reference.py` shape), and `NOT_RUNS` has a name bound to the interpreter only after the call. |
| **2** three `host_values` behaviours without a case | **Taken.** `VALUE_USES` gains `self.quote = shell_line`, `a = b = shell_quoted` and a name read in an annotation; the value use in a method sits beside `test_a_method_is_named_with_its_class`. |
| **3** `environment_of` joined every binding | **Taken.** The last binding above the call is read, with cases for a later rebinding, a rebinding after a branch, and one after the call. |
| **4** spellings the launcher guard does not read | **Taken in part.** `interpreter_runs`' docstring lists them, as #821's guard lists "WHAT IT DOES NOT SEE", and `SPELLINGS` gains `subprocess.call`, `check_call` and `check_output`. **Stays unread:** a tuple argv, `[sys.executable, ...] + rest`, a function-local `exe = sys.executable`, `import subprocess as sp` and `from subprocess import Popen`, `os.exec*`. None is in shipped code, and each needs a resolver of its own. |
| **5** aliases counted at the alias | **Taken in part.** `alias_of` follows `q: object = shell_quoted` as it follows `q = shell_quoted`, so `q(x) + q(y)` counts 2 and one switched to `'posix'` counts 1. `names_posix` reads a `'posix'` argument only after the value the quoter would quote, so `functools.partial(quote_arg, 'posix')`, which binds the value, is a host use. **Stays unfollowed:** an alias made by a tuple, a chained assignment, a walrus or a conditional. `host_calls`' docstring says so, and `NOT_FOLLOWED` pins what is counted instead (the statement, once for each quoter it names). None is in shipped code. |
| **6** #821's `calls_to` reads `host_call` only | **Not changed here, and not #821.** See below. |

**NIT 6, for whichever of #808 and #821 lands second.** #821's `calls_to` counts `host_call`'s calls and not `host_values`' uses, so on the merge `skips_the_question` does not see a quoter handed on as a value. Measured on a scratch merge of `08a6ed39` and #821's head `7581ffb9` (`v116/f808/nit6.txt`):

| source | `skips_the_question("scripts/x.py", ...)` as merged | with the fix below |
|---|---|---|
| `' '.join(map(shell_quoted, w)) if os.name == 'nt' else ''` in `def a(w)` | `set()` | `{'a'}` |
| `' '.join(shell_quoted(x) for x in w) if os.name == 'nt' else ''` in `def a(w)` | `{'a'}` | `{'a'}` |

The fix is one expression, which the second lander adds to `calls_to`'s return: `| {function for function, _ in host_values(tree, names)}`. `host_values` names functions as `calls_to` does (`<module>`, `Class.method`). With it, `tests/test_readme_windows_quoting_sentence.py` on the scratch merge gives 43 passed (`v116/f808/nit6-run.log`). The merge itself is clean and needs no other change.

### Tests

On `08a6ed39` (`dirty=0`), through `run9.sh` (gh stub first on PATH, bogus token, empty `GH_CONFIG_DIR`), `v116/f808/final-2.log`:

`tests/test_validator_child_encoding.py tests/test_readme_windows_quoting_sentence.py tests/test_repo_hygiene.py tests/test_agents_md_codes.py tests/test_cli_reference.py`: **297 passed, 1 xfailed in 59.36s.**

Per file: `test_validator_child_encoding.py` 82 to 111 cases (29 new), `test_readme_windows_quoting_sentence.py` 28 to 40 (12 new), the other three 147 together. On `84ce67c9` the two files gave 109 passed, 1 xfailed (`v116/f808/base-head.log`). No other test file imports or drives either one: `git grep` finds no use of `interpreter_runs`, `environment_of`, `LAUNCHERS`, `host_calls`, `host_quoting` or `quoter_names` outside these two files. On the scratch merge with #821 (`4386e5fe`, not pushed), these two files and `test_git_bash_dialect.py`, `test_family_printed_quoting.py` and `test_pinned_copy_text.py` give 417 passed, 1 xfailed (`v116/f808/merge821.log`).

### The new tests on `84ce67c9`

I put the new test files over `84ce67c9` with its helpers back in place of the new ones (`v116/f808/mkbase.py`; a helper that is new is dropped, so a test that uses it fails with a `NameError`): **19 of the 41 new tests fail, 131 pass, 1 xfails** (`v116/f808/basefail-3.log`). The failures: 7 changes to the model of `setup-project.py` and the six-children pin; 3 `SPELLINGS` (a list a caller hands a parameter, by position and by keyword, and a name bound to two lists with the interpreter second); 5 `ENVIRONMENTS` (the last binding, three ways, and the environment `run` hands, two ways); and in the quoting file the `partial(quote_arg, 'posix')` use, the annotated alias statement, and the annotated alias counted at its calls.

The other 22 pass there, because the base code already did what they say. They close mutants that survive `84ce67c9`'s own tests, measured below (six of them: NIT 2's three and NIT 4's three), or pin a limit the new code names.

### Mutants

One mutant per new hunk, each a text change to the head's test files (`v116/f808/mutate.py`, `v116/f808/mut/results.jsonl`). `failed` counts the tests that fail, among the launcher guard's own tests for a mutant of `tests/test_validator_child_encoding.py` and in the whole file for one of `tests/test_readme_windows_quoting_sentence.py`. All 32 are caught.

| hunk | mutant | failed |
|---|---|---|
| `bound_values`, above the call | the filter dropped / line number only / no sort | 2 / 1 / 1 |
| `environment_of`, last binding | the first binding / every binding joined (the old code) | 2 / 2 |
| `argument_for` | positional dropped / keyword dropped / always the first argument | 9 / 1 / 1 |
| `lists_of`, callers | not read / callers of callers read / a starter's own parameter resolved | 10 / 1 / 15 |
| `lists_of`, held lists | first only / last only | 1 / 1 |
| `interpreter_runs`, any list | first only / last only | 1 / 1 |
| `own_environment_of` | returns nothing / the call's own `env=` loses to it / recurses into a starter that calls itself | 3 / 4 / 1 |
| `NOT_YET` row | unmarked / made to pass (`LAUNCHERS` value `""`, the strict `xfail` fires) | 1 / 1 |
| `alias_of` | no annotated assignment / chained assignment read / any target kind read | 2 / 1 / 1 |
| `names_posix` | every argument read / one slot after the quoter / a keyword quoter reads none | 1 / 1 / 1 |
| `host_values`, alias statement | any child of one exempt / the statement a use | 1 / 5 |
| `function_of` | `ClassDef` dropped | 1 |
| `STARTERS` | `subprocess.call` / `check_call` / `check_output` dropped | 1 / 1 / 1 |

Six of these survive `84ce67c9`'s tests, run on its own files with the same text change: `STARTERS` (three; 28 passed, 1 xfailed in the launcher guard's tests) and, in the quoting file (28 passed each), `host_values`' target-count check (`a = b = shell_quoted`), its target-kind check (`self.quote = shell_line`) and `function_of`'s `ClassDef`. On the new files each is caught, as the table says.

### Merge checks (`git merge-tree --write-tree HEAD <ref>`, main at `64ec47c3`)

- **origin/main:** clean.
- **#821 (`fix/711-git-bash-dialect`, `7581ffb9`):** clean, and the only other head that changes `tests/test_readme_windows_quoting_sentence.py`. This branch alone changes `tests/test_validator_child_encoding.py`.
- **Every `fix/*` and `l3-*` head on origin (59, this branch's own among them):** 55 clean. Four conflict: `fix/758-git-command-scope`, `fix/762-validate-family-remedies-texts`, `l3-677-pins-hand-step-wip` and `l3-678-naming-checkout-wip`. Each conflicts the same way with origin/main and with `84ce67c9`, in files this change does not touch.

### Checklist candidates

None new. NIT 6 is the one follow-up, and it belongs to whichever of #808 and #821 lands second.

**1. The inventory sees a quoter handed on as a value, and counts calls.** `tests/test_readme_windows_quoting_sentence.py`:

- `host_calls` also returns a host quoter that is read and not called: an argument or keyword of another call (`map(shell_quoted, words)`, `functools.partial(shell_line)`, `key=shell_quoted`), a list entry, a default or a return value. It is named under its own function (`host_quoting` keys by function), in source order.
- A call told `'posix'` is `sh`'s, as for a direct call: `functools.partial(shell_line, platform="posix")` is not a use.
- `NAMED` maps each function to its number of host-dialect uses (`_add_command` 3, `_print_next_steps` 2, `family_next_command` 2, the other three 1), and the per-function pin compares the counts. One of `_add_command`'s three calls switched to `'posix'` is 3 to 2 and fails, where the set of names it compared before still held `_add_command`. The failure prints the counts it found next to the table's.

**2. The launcher guard reads two more spellings and has two more rows.** `tests/test_validator_child_encoding.py`:

- `interpreter_runs` reads a call to `subprocess.run` and its kin, to the package's `run_child`, and to `run` (`setup-project.py`'s wrapper), whose argv is `[sys.executable, ...]`, `[sys.executable or PYTHON, ...]`, or a list starting with a name the module binds to `sys.executable` (`PYTHON = sys.executable`). The list may be spelled out, held in a local name (`plan = [PYTHON, ...]`, once or in each branch of an `if`), or handed to the calling function as a parameter by a caller in the module. A function that is itself a starter (`run`) is not a run; its callers are. A `PYTHON` imported from `repo_shape` is `python3` as a person types it, and is not read as the interpreter.
- `environment_of` reads an `env=` keyword, or `run_child`'s second argument, through a local name (`cli.py`'s `scaffold` builds `env` and adds a key before it passes it on). Where the call hands none, it reads the `env=` of the function it calls when the module defines that function (`run`). A local name is read as its LAST binding above the call.
- `LAUNCHERS` gets `src/openreposhape/cli.py`: its four `run_child` calls (`:436`, `:462`, `:474`, `:564`) each hand `standard.child_environment()`, which is what the file's docstring promises ("EVERY CHILD GETS `standard.child_environment()`"). That is the package environment's PATH and not an encoding, so it is the helper this row is held to. The row passes.
- `LAUNCHERS` gets `setup-project.py`: its six Python children hand no environment, since `run` has no parameter for one: five `run([PYTHON, ...])` calls (in `self_bootstrap`, `check_names`, `plan_and_confirm`, `scaffold` and `clone_and_bootstrap`) and the sixth, in `why_the_plan_did_not_go_through`, which runs the plan again with `plan` a parameter. That row fails, and it is the finding below.
- A separate test says each row finds at least one run. That keeps a spelling the guard cannot read from passing as "nothing to check".

**Measured, not read.** `PYTHONIOENCODING=cp1252:strict` and `cp1252:surrogateescape` are what the other encoding tests here emulate for Windows' ANSI code page on a stdout that is not the console. A local run, `setup-project.py --project Sample --yes --org demoorg --local-remote-dir <dir> --into <dir>`, with the `gh` stub first on PATH:

| `PYTHONIOENCODING` | exit | lines of stdout | lines that are not valid UTF-8 |
|---|---|---|---|
| `cp1252:strict` | 0 | 129 | 9 (24, 31, 38, 45, 64, 79, 80, 100, 102) |
| `cp1252:surrogateescape` | 0 | 129 | 9 (the same) |
| `utf-8:strict` (control) | 0 | 129 | 0 |

`setup-project.py` reconfigures its own stdout to UTF-8 (`main`, `:2790`), and five of its children inherit that stdout with an environment of their own, so each writes in the code page its environment gives, and the sixth is captured and read as UTF-8 whatever it wrote (the table under "The sixth child, measured"). The lines are the naming validator's `family ...` rows with their em dash (3 lines), the scaffold's `shape ...` and `spec`/`code` lines with their ellipsis (4), and the validators `scripts/bootstrap.py` runs (2). Nothing raises, because `scaffold-project.py` and `scripts/bootstrap.py` escape what their stdout cannot hold. The stream is just two encodings. Log: `v115/w759/probe5/summary.txt`.

**What the test does about it.** The row cannot pass without a change to `setup-project.py`, and this issue changes no tool. It is a strict `xfail` (`NOT_YET`): it passes while the row fails, and it fails the day the row passes, so whoever gives `run` an environment has to take the row out of the table. The mark names `AssertionError`, so any other error in the guard still fails. The fix is an environment from a copy of `inherited_child_environment` (reading `sys.stdout.encoding` after the reconfigure, so `utf-8:replace`), given either as an `env=` in `run`'s own `subprocess.run` or as an `env` parameter that all six calls pass. The row flips on either, and a change that leaves any one of the six bare leaves it red. `setup-project.py` cannot import the helper: it runs before there is a checkout to import from.

- **The use rule is wider than the issue's wording, by one case.** The issue says "an argument of any call, or the value of an assignment the alias walk already follows". The rule here is: any read of a host quoter that is not the `func` of a call and not the alias statement itself (`joined = shell_line`, the assignment `quoter_names` follows). That covers the issue's two forms and also a list entry, a default, a return value and `self.quote = shell_line` (an assignment the walk does not follow). It differs on one point only: an alias that is assigned and never read. The alias statement is not counted, because every use of `joined` is, and counting it would make a module-level alias a `<module>` row that `NAMED` would have to name.
- **A call that takes the quoter as an argument and is told `'posix'` is not a use.** `host_values` has its own small `names_posix` rather than reusing `host_call`'s inline test. `host_call` is left as it is, since #711 (the Git Bash dialect) edits the rule that decides what forces `sh`.
- **The new cases sit in the middle of the file and `NAMED` changes only in its values.** #711 changes `tests/test_readme_windows_quoting_sentence.py` too, so these hunks are additive, and none of them is at the end of the file.
- **The callee set is `subprocess.run/Popen/call/check_call/check_output`, `run` and `run_child`.** A call that only names the interpreter in a list (`shlex.join([sys.executable, ...])`) is not a run. Another spelling is a new entry in `STARTERS`, and `test_every_spelling_of_a_child_of_the_interpreter_is_a_run` is where it is shown.

**On the shipped tools nothing changes.** The new `host_calls` returns what the old one did on every file `git ls-files` lists (`host_quoting` compared across both: identical), so no host-dialect line escaped the sentence before, as the issue says. `NAMED`'s counts match today's code: `_add_command` 3, `_print_next_steps` 2, `_switch_command`, `_print_next` and `_checked_mount_path` 1, `family_next_command` 2.

**The tool mutants,** each run against `151a0861`'s own two test files and then against these. Logs under `v115/w759/mut/`.

| Mutant | base tests | new tests |
|---|---|---|
| M1: `def _w759_mutant(words): return list(map(shell_quoted, words))` added to `update-shape.py` | 9 passed (survives) | `test_the_tools_that_quote_for_the_host_are_the_ones_the_sentence_names` fails: `update-shape.py` is among the files |
| M2: `shell_quoted(org, 'posix')` in `_add_command` (one of three) | 9 passed (survives) | `test_the_functions_that_quote_for_the_host_...[scripts/family.py]` fails: `_add_command` 2, the table 3 |
| M3a-d: `env` dropped from one of `cli.py`'s four `run_child` calls (`:436`, `:462`, `:474`, `:564`), one mutant each | 3 passed (survives) | the `[src/openreposhape/cli.py]` row fails, naming the line: ``src/openreposhape/cli.py:474 runs the interpreter without `env=standard.child_environment()` `` |
| M3e: `env = dict(os.environ)` for `scaffold`'s `env` | 3 passed (survives) | the same row fails |

**Against the base's helpers.** The new cases appended to `151a0861`'s files, without the new helpers: 12 of 19 fail in the quoting file and 12 of 19 in the launcher file (a `NameError` where the helper is new, an assertion where it is not). The 14 that pass are the negative cases (what is not a use, what is not a run) and the two `subprocess.run` spellings the guard already read. The `cli.py` and `setup-project.py` rows have no base row to run. Logs `v115/w759/basefail-quote.log` and `basefail-enc.log`.

**One mutant per new hunk,** applied to the new test files only, each killed (`failed` counts the tests that fail):

| Hunk | Mutant | failed |
|---|---|---|
| `host_calls` adds `host_values` | the value uses left out | 11 of 28 |
| `host_values`, `names_posix` call | a call told `'posix'` still a use | 4 |
| `host_values`, keyword | `ast.keyword` not climbed | 1 |
| `host_values`, alias statement | the alias statement a use | 3 |
| `names_posix` keyword | `platform=`/`posix=` ignored | 3 |
| `names_posix` argument | a `'posix'` argument ignored | 1 |
| `function_of` | every use named `<module>` | 11 |
| source order | no sort by line | 1 |
| `host_use_counts` | a count of 1 for every function | 3 |
| `STARTERS` | `run_child` left out | 6 |
| `STARTERS` | `run` left out | 6 |
| `interpreter_names` | a name bound to `sys.executable` not followed | 6 |
| `argv_of` (now `lists_of` and `argv_lists`; the fix round's own mutants table covers them) | a list held in a name not resolved | 1 |
| `environment_of`, `run_child` | its second argument ignored | 4 |
| `environment_of`, local name | a name not read as what it was bound to | 2 |
| `NOT_YET` | the `xfail` mark dropped (the row is red) | 1 |
| `NOT_YET` | the row made to pass (`LAUNCHERS` value `""`): the strict `xfail` fails | 1 |

**These counts are `052410b6`'s.** At `08a6ed39` they are the fix round's: 297 passed, 1 xfailed over the same five files; the encoding file grows from 82 to 111 cases and the quoting file from 28 to 40.

On `052410b6`, `run9.sh` (gh stub first on PATH, `GH_TOKEN` bogus, empty `GH_CONFIG_DIR`; `v115/w759/final-2.log`):

`tests/test_validator_child_encoding.py tests/test_readme_windows_quoting_sentence.py tests/test_repo_hygiene.py tests/test_agents_md_codes.py tests/test_cli_reference.py`: **256 passed, 1 xfailed in 63.09s.**

Per file: `test_readme_windows_quoting_sentence.py` 9 to 28 cases (19 new), `test_validator_child_encoding.py` 56 to 82 (26 new, one of them the `xfail` row), the other three 147 together. No other test file imports or drives what changed: `git grep` finds no use of `interpreter_runs`, `LAUNCHERS`, `host_calls` or `host_quoting` outside these two files.

- **origin/main `64ec47c3`:** clean. **#724** landed as `20004887`, and nothing is stacked. The fix round's merge checks above are at `08a6ed39`; the rest of this section is `052410b6`'s.
- **Every `fix/*` and `l3-*` head on origin (47):** clean, and none other than #724 touches the two test files. The branches that touch a tool the guards read (`scripts/family.py`: `fix/646`, `fix/684`, `fix/685`, `fix/686`, `fix/688`, `fix/689`, `fix/692`, `fix/693`; `update-shape.py`: `fix/674`; `setup-project.py`: `fix/696`, `fix/729`) are clean too.
- **The wave-7 branches' committed tips** (`fix/757` to `fix/777`; `fix/760` is on origin): clean.
- **Lane 2's #711 (the Git Bash dialect):** no branch on origin at 10:52Z (`git ls-remote origin 'refs/heads/*711*' 'refs/pull/711/head'` is empty), so it is not checked. It changes `NAMED` and this file; whichever lands second re-gates against the other.
- Three older local branches (`fix/bootstrap-fast-forwards-tracking-branch`, `fix/scaffold-mount-ignored-legs`, `fix/scaffold-reuse-seeded-legs`) conflict, in files this does not touch, exactly as they do with origin/main.

- **No change to a tool.** The `setup-project.py` row is the finding, held as a strict `xfail`; fixing `run` is a separate change.
- **Dynamic access is not seen.** `getattr(repo_shape, "shell_line")` and `globals()["shell_line"]` hand a quoter on by name at run time. The inventory reads names in the source.
- **The guard still reads only the tools in `LAUNCHERS`.** A survey with the new `interpreter_runs` over every shipped file found five more: `shape-doctor.py:1697` (sets its own UTF-8, as the table's comment says), `src/openreposhape/standard.py:560` (a digest program with captured output), `scripts/render-cli-reference.py:403` (passes `env=child_env()`, found once NIT 1 was fixed), `templates/assembly-root/scripts/bootstrap.py:148` and `templates/family-root/scripts/siblings.py:567` (neither passes an environment; see below).

- **`templates/assembly-root/scripts/bootstrap.py`'s `_run` (`:148`) starts the validators with no environment, so a manifest name the code page cannot hold ends bootstrap.** Measured on a scaffolded project (a local `setup-project.py` run, then `project.yaml` `name: "日本"`), `PYTHONIOENCODING=cp1252:strict python3 scripts/bootstrap.py`: `validate-manifest.py` raises `UnicodeEncodeError` at `print(f"manifest ok: {manifest.get('name')} ...")` (`:972`) and bootstrap ends `bootstrap FAILED: manifest (exit 1)`, exit 1. With `utf-8:strict`: `manifest ok: 日本 (sample), 3 legs`, exit 0. It is #254's defect in a pinned copy, so a fix is a shape bump, and `make bootstrap` and `setup-project.py` step (7) both run it.
- **`setup-project.py`'s `run` gives its children no environment** (the finding above). If the coordinator files it, the `NOT_YET` entry names the fix and the measurement.
- **`templates/family-root/scripts/siblings.py:567`** starts each member's own `scripts/bootstrap.py` with no environment. Its stdout and the child's agree (neither reconfigures), so it is the first candidate's class and not the second's, and it has no row because it is a pinned copy.

Review: lane 2's independent review (issuecomment-6236457891) and its re-verification of 08a6ed3, READY (issuecomment-6237256937). CI (Brett Heap's ruling 27): H0 is the head 08a6ed3 itself, which passed tests, tests-git-bash and SonarCloud; main has touched none of its files since its base 40e678e; and the local gate passed on its predicted squash tree 817a2ff over main 2c6543e: the readme_windows_quoting_sentence, validator_child_encoding, repo_hygiene, agents_md_codes, cli_reference sets, 298 passed, 1 xfailed. 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 #759

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

1 participant