Skip to content

bootstrap.py: step (a) decodes git's output as the system decodes a name it is given, so a branch whose upstream is not UTF-8 is moved back, and a good branch this system's encoding cannot write gets a line of its own, not a traceback (#710) - #824

Merged
brettheap merged 4 commits into
mainfrom
fix/710-non-utf8-branch-names
Oct 17, 2026

Conversation

@brettheap

@brettheap brettheap commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

Lane: openreposhape-4 (openRepoShape-4)
Claim: #710 (comment)
Head: 7777c7d0, fix round 2 for RV824's re-check (READY, issuecomment-6237945041), on round 1's 0a5e82bf (RV824's review, issuecomment-6236261109), on 1a9d08be.

What changed and why

One file of code, templates/assembly-root/scripts/bootstrap.py, and one new test file. Every case below was measured on main 24a977c0 (#745's squash) before this change and on this head after it, in a scaffold's recursive clone bootstrapped once.

Step (a) has one decode rule for what git prints (L4-125, and lane 5's site). _try_git, the reader of every answer step (a) and the upstream notice ask git for, read through repo_shape.git_out's strict UTF-8 decode. It now reads bytes and decodes them with os.fsdecode, as #745 made _head_of and _in_progress read theirs. That decode is the inverse of the os.fsencode that subprocess applies to an argument, so a name git prints reaches git again as the same bytes under any POSIX locale. The bytes are stripped, not the text. On Windows subprocess encodes no argument: it hands the command line to CreateProcessW as UTF-16, and git for Windows reads it, and writes a ref name, in UTF-8. So _try_git decodes git's bytes as UTF-8 there, as git_out did, and not by os.fsdecode, which PYTHONLEGACYWINDOWSFSENCODING=1 makes mbcs with replace (RV824's N3). A comment at the decode records #710's ruling, Option 2 (N6).

case main 24a977c0 this head
lane 5's site: a branch a<ff>b past the pin, no commit of its own, upstream origin/a<ff>b, leg detached at the pin, --branch $'a\xffb' exit 1, UnicodeDecodeError: 'utf-8' codec can't decode byte 0xff in position 21, from _upstream_holding exit 0, branch a\udcffb moved back <tip> -> <pin>, the pin; nothing is lost, origin/a\udcffb still holds those commits, ...
the same with a name ending in U+00A0 exit 0, PIN <pin> != branch a tip <tip> / not moving an existing branch: str.strip took the U+00A0 off the upstream's name exit 0, moved back
main tracking origin/日本 past the pin, under LC_ALL=C PYTHONUTF8=0 exit 1, UnicodeEncodeError: 'ascii' codec can't encode characters in position 20-21: the name read as UTF-8 text could not be handed back to rev-parse exit 0, branch main moved back ...
L4-125's own route: git branch $'a\xffb', the leg on it, bootstrap twice (no --branch, and --branch $'a\xffb') exit 0 both times already: #745's _head_of reads HEAD's ref as bytes exit 0 both times, pinned by a test
L4-126: tracking_branch: "a\udcffb", then the finding followed back to main run 1 exit 1 (the validator's finding), run 2 exit 0 already (#745) the same, pinned by a test

Where the rule lives: _try_git, not git_out. The issue expected one decode rule in git_out. Two places could hold it:

#425, which asked that git_out not change, closed through #717 with a local decode in adopt-project.py's _default_branch, so the two issues agree.

L4-124, as Brett Heap ruled at 2026-10-17T13:01:07Z ("go with (a) for L4-124"). Under LC_ALL=C PYTHONUTF8=0 the filesystem encoding is ascii. There, tracking_branch: "feature/日本" passed the validator (manifest ok, exit 0), and bootstrap ended in UnicodeEncodeError: 'ascii' codec can't encode characters in position 19-20, exit 1.

_argument_fault now also faults a character os.fsencode cannot write (UNWRITABLE). The leg is left as it is, and the first line says did not try branch (RV824's N2), since the branch can be there already, made by a run under UTF-8. The fault is asked last. A name that also holds a surrogate, even one that is one byte of a name, is faulted for that surrogate, which validate-manifest.py names too (#280, RV824's S1). The line calls the name good only once git has said so: git check-ref-format --branch is asked about the name's UTF-8 bytes (S1), in the ROOT, so a leg whose .git file names a git dir that is gone (fatal: not a git repository) does not turn a good name into one git refuses (RV824's R1-N1). Now, for each leg:

  [spec] spec: did not try branch feature/\u65e5\u672c: it holds U+65E5, a character this system's encoding cannot write, which no command can be given as an argument; the leg is left as it is
          the name is good, and this system's encoding, ascii, cannot write it: run bootstrap again under `LC_ALL=C.UTF-8`, or with `PYTHONUTF8=1`.

Where git refuses the name in UTF-8 too (feature/日本..x, 日本.lock, 日本 x), the second line gives git's words and #455's way out:

  [spec] spec: did not try branch feature/\u65e5\u672c..x: it holds U+65E5, a character this system's encoding cannot write, which no command can be given as an argument; the leg is left as it is
          and git refuses it as a branch name in UTF-8 too:
          fatal: 'feature/\u65e5\u672c..x' is not a valid branch name
          the branch is the leg's `branch:`, else `--branch`, else `tracking_branch`. Give that one a name git can create and run bootstrap again.

"日\udcff" gets #455's lines for its U+DCFF (could not create branch \u65e5\udcff: it holds U+DCFF, a lone surrogate, ...), and the run ends bootstrap FAILED: manifest (exit 1), as the validator ends it. A leg's own branch: takes the same path as tracking_branch (N4).

The validators then pass the name, and the run ends bootstrap ok, exit 0, as a branch git refuses does. Each way out, run as printed, puts the legs on feature/日本. A --branch typed under such a locale is never faulted, because its bytes reach Python as U+DC80 to U+DCFF, which os.fsencode writes back.

Re-pinning: none. No test, pin or recorded digest in this repository names this file's bytes. Neither #644 nor #745 re-pinned anything when it changed the file. I checked that no file in the tree holds main's blob id or sha256 of it.

Tests

tests/test_bootstrap_non_utf8_branch.py, 27 tests on the harness of tests/test_bootstrap_after_bump.py:

  • the three move-back cases;
  • L4-125 twice;
  • L4-126;
  • L4-124 end to end, once per way out;
  • the three names git refuses (S1), "日\udcff" (S1), a leg's own branch: (N4), and a leg whose .git file names a git dir that is gone (R1-N1);
  • _try_git under a legacy-mode Windows os stand-in (N3);
  • _argument_fault and _say_not_tried under an emulated ascii os.fsencode.

Each assert a and b is two asserts (RV824's S3). Four tests hand git a name only UTF-8 can write, or read a child's UTF-8 line. They skip, with a reason, where pytest itself does not run under UTF-8 (N5): with pytest under LC_ALL=C PYTHONUTF8=0, the new file and the no-fault test give 30 passed, 7 skipped.

tests/test_bootstrap_branch_bytes.py's test_a_branch_the_system_carries_has_no_fault now skips the three rows feature/日本, a<U+200D>b and a\xe9b where os.fsencode cannot write them (written), and its docstring states the rule (RV824's S2). Under LC_ALL=C PYTHONUTF8=0, the file as on main gives 3 failed, 8 passed against this head's bootstrap.py; with this change, 8 passed, 3 skipped.

One targeted run on round 2's committed bytes (bootstrap.py sha256 8f7e1b28..., the new file 3555ac0a..., test_bootstrap_branch_bytes.py e9cd5411...), /usr/bin/python3 -m pytest -q -rs -p no:cacheprovider -o tmp_path_retention_policy=failed, Linux, git 2.43.0, Python 3.12.3, no GIT_ALLOW_PROTOCOL: 642 passed, exit 0 (5 min), over 11 files: the new file, the five tests/test_bootstrap_*.py (test_bootstrap_branch_bytes.py among them), tests/test_shape_advisory.py, tests/test_git_output_decoding.py, tests/test_repo_hygiene.py, tests/test_cli_reference.py and tests/test_agents_md_codes.py. Round 2 changes only the directory one git question runs in, on a path no UTF-8 run takes. Round 1's run of 33 files, on 0a5e82bf, gave 2859 passed, 8 skipped, exit 0: the eleven test_adopt_* files that bootstrap a clone, the scaffold, set-up and update files that run bootstrap.py, and the set above. Its 8 skips are test_leg_path_yaml_scalars.py's own.

Mutants, each run against the new file (all killed; the first thirteen on round 1's bytes, the last on round 2's):

mutant result
round 0's bootstrap.py (1a9d08be) 11 of 26 fail
main's bootstrap.py 8 of 18 fail (round 0's count)
_try_git back to git_out(args, cwd=cwd) 3 fail
decode utf-8 + surrogateescape instead of os.fsdecode 1 fails
os.fsdecode(raw).strip(), the text stripped 1 fails
no UNWRITABLE fault 12 fail
#455's second line in place of the ruling's own 4 fail
no surrogate named ahead of UNWRITABLE 3 fail
git never asked before "good" 3 fail
git asked of the text, not the UTF-8 bytes 6 fail
could not create for UNWRITABLE 7 fail
os.fsdecode on nt too 1 fails
UNWRITABLE dropped for a leg's own branch: 1 fails
git asked in the leg again, not the root (R1-N1) 1 fails (the broken-gitfile row)

What it cannot tell

Shared files

Merge-trees (git merge-tree --write-tree) of this head 7777c7d0, the heads read with ls-remote:

against head merge base result
main, with #723 landed bad05d21 24a977c0 clean, tree 477905ae
#815 (fix/764-bootstrap-notice-exit-texts; bootstrap.py) 05a1ebb3 24a977c0 clean, tree 56f0a042

#815 now has main 24a977c0 merged, so the stale-base hunk round 0 reported is gone. On the tree of this head merged with main bad05d21 and then #815 05a1ebb3 (tree 63fc7be5, built in a scratch clone):

  • the new file and tests/test_bootstrap_branch_bytes.py give 88 passed;
  • with pytest itself under LC_ALL=C PYTHONUTF8=0, the new file and the no-fault test give 31 passed, 7 skipped.

#764's ending is bootstrap REFUSED: pins (exit 2), where main's is bootstrap FAILED: pins (exit 2), so the broken-gitfile row asks only for a non-zero exit and : pins (exit 2). Every name the new file imports is defined in the merge with bad05d21.

Ticks L4-124, L4-125 and L4-126 on #710, and the same three lines of #459's checklist comment (issuecomment-6214587074).

Closes #710

🤖 Generated with Claude Code

Summary by Sourcery

Make bootstrap robust when processing branch names that cannot be represented or safely round-tripped by the system encoding.

Bug Fixes:

  • Prevent bootstrap failures when Git reports branch or upstream names containing non-UTF-8 bytes or characters that the system encoding cannot write.
  • Preserve branch-name bytes during Git output handling so tracking branches are correctly moved back without decode, encode, or whitespace-stripping errors.
  • Report unwritable branch names clearly, distinguish names Git rejects from valid names requiring a UTF-8 environment, and leave affected legs unchanged.

Enhancements:

  • Keep Git output decoding behavior unchanged for other consumers while applying byte-preserving decoding specifically to bootstrap step (a).

Tests:

  • Add comprehensive coverage for non-UTF-8 branch names, unwritable names, Git-invalid names, locale behavior, Windows decoding, and broken repository metadata.

…a name it is given, and leaves a good branch this system's encoding cannot write with a line of its own, so a branch past the pin whose upstream is not UTF-8 is moved back, and `tracking_branch: "feature/日本"` under `LC_ALL=C PYTHONUTF8=0` no longer ends `make bootstrap` in a traceback (#710)

`_try_git` read every answer through `repo_shape.git_out`'s strict UTF-8
decode. It now reads bytes and decodes them with `os.fsdecode`, as #745
(#704) made `_head_of` and `_in_progress` read theirs. That decode is the
inverse of the `os.fsencode` that `subprocess` applies to each argument, so
a name git prints goes back to git as the same bytes under any POSIX locale.
Measured on main 24a977c before this change:

- lane 5's site on #710: a tracking branch past the pin with no commit of
  its own, its name and upstream not UTF-8, and the leg detached at the pin,
  ended in `UnicodeDecodeError: 'utf-8' codec can't decode byte 0xff`, exit
  1, from `_upstream_holding`. It is now moved back.
- under `LC_ALL=C PYTHONUTF8=0`, `main` tracking `origin/日本` past the pin
  ended in `UnicodeEncodeError` once the name, read as UTF-8 text, was handed
  back to `rev-parse`. It is now moved back.
- the bytes are stripped and not the text, because `str.strip` took a
  trailing U+00A0 off `refs/remotes/origin/a<U+00A0>`, and the branch was
  then not moved back.

`git_out` is not changed. The readers of `origin`'s URL in `shape-doctor.py`
and `shape_advisory.py` take its `UnicodeDecodeError` to mean the URL names
nothing (#385), and `adopt-project.py`'s `_default_branch` decodes locally
since #717 (#425). So the decode rule lives in `_try_git`, step (a)'s one
reader, and not in `git_out` for every caller.

L4-124, as Brett Heap ruled at 2026-10-17T13:01:07Z ("go with (a) for
L4-124"): `_argument_fault` also faults a character `os.fsencode` cannot
write (`UNWRITABLE`). It asks for one only once the name holds no NUL and no
lone surrogate, because the line it gets says the name is good. The leg is
left as it is. `_say_not_tried` gives that fault its own second line, which
says the name is good, names the encoding, and gives `LC_ALL=C.UTF-8` and
`PYTHONUTF8=1`; #455's "Give that one a name git can create" stays for the
other two faults. The run then goes on to the validators, which pass the
name, and ends `bootstrap ok`, as a branch git refuses does.

L4-125's own route (a leg on `a<ff>b`, bootstrapped twice) and L4-126 (the
manifest's `"a\udcffb"` followed back to `main`) already exit 0 on main
through #745's `_head_of`. L4-126 is answered by option (b), the root
cause, as R455 recommended. The new tests pin both.

No pinned copy or recorded digest names this file in the repository's
tests, as #644 and #745 found when they changed it, so nothing is re-pinned.

tests/test_bootstrap_non_utf8_branch.py runs each case on the harness of
tests/test_bootstrap_after_bump.py, and holds the rule under an emulated
`ascii` `os.fsencode` on every platform.

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

sourcery-ai Bot commented Oct 17, 2026

Copy link
Copy Markdown

Reviewer's Guide

The PR makes bootstrap step (a) round-trip git branch and upstream names as filesystem-encoded bytes without changing the shared strict-UTF-8 git-output helper, adds a user-facing fault for otherwise-valid names that the current encoding cannot write, and thoroughly tests these cases across locale and branch-byte scenarios.

Sequence diagram for bootstrap branch name round-trip

sequenceDiagram
    participant Bootstrap
    participant Git
    participant OS
    Bootstrap->>Git: _try_git(args, cwd)
    Git-->>Bootstrap: git output bytes
    Bootstrap->>OS: os.fsdecode(bytes.strip())
    OS-->>Bootstrap: filesystem-decoded branch name
    Bootstrap->>Git: rev-parse branch name
    Git-->>Bootstrap: matching ref result
Loading

Flow diagram for unwritable branch handling

flowchart TD
    A[Branch name supplied to bootstrap] --> B[_argument_fault]
    B --> C{NUL or lone surrogate?}
    C -- Yes --> D[_say_not_tried: branch cannot be created]
    C -- No --> E{os.fsencode can write every character?}
    E -- Yes --> F[Try git command]
    E -- No --> G[_say_not_tried: name is good but encoding is unwritable]
    G --> H[Run again under LC_ALL=C.UTF-8 or PYTHONUTF8=1]
Loading

File-Level Changes

Change Details Files
Moved step (a)'s git-output decoding into _try_git, preserving raw bytes while stripping terminators before applying os.fsdecode.
  • Reads binary git output and decodes it using the filesystem encoding, matching subprocess argument encoding.
  • Keeps git_out unchanged so URL consumers retain strict UTF-8 error semantics.
  • Prevents failures and name corruption for non-UTF-8, non-ASCII, and trailing Unicode-whitespace branch names when resolving upstreams.
templates/assembly-root/scripts/bootstrap.py
Added a distinct validation and remediation path for branch names that are valid but cannot be encoded by the current system.
  • Detects unwritable characters after prioritizing NUL and lone-surrogate faults.
  • Leaves affected legs unchanged while reporting the filesystem encoding and UTF-8 recovery commands.
  • Preserves the existing remediation message for names containing NULs or invalid lone surrogates.
templates/assembly-root/scripts/bootstrap.py
Added end-to-end and unit coverage for branch-byte round trips and encoding-specific bootstrap behavior.
  • Covers upstream move-back for raw 0xFF, trailing non-ASCII whitespace, and UTF-8 names under an ASCII filesystem encoding.
  • Pins repeated bootstrap behavior for non-UTF-8 branches and validator-created branch names.
  • Verifies unwritable-character fault precedence, output lines, platform skips, and both suggested UTF-8 recovery paths.
tests/test_bootstrap_non_utf8_branch.py

Assessment against linked issues

Issue Objective Addressed Explanation
#710 Make bootstrap handle git output containing non-UTF-8 branch or upstream names without a traceback, while preserving the original name bytes when passing the name back to git. ✅
#710 Ensure a leg on a non-UTF-8 branch can be bootstrapped on later runs, including the manifest-surrogate route, without a decode traceback. ✅
#710 For a valid branch name that the filesystem encoding cannot write, avoid a traceback and leave the leg unchanged with clear guidance for rerunning under UTF-8. ✅

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

@brettheap

brettheap commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-3, 2026-10-17T16:01:51Z) Independent review of #824 (#710) by lane 3's RV824 (opus), on head 1a9d08be: NOT READY. One MUST, three SHOULDs and six NITs. The decode change itself holds: every case the body lists reproduces on main and is fixed on the head. The head did not move during the review; main moved to 40e678e6 (siblings.py only), and every merge was re-run there.

M1: CI tests fails; the new _try_git docstring names the advisory module.

  • templates/assembly-root/scripts/bootstrap.py:177-178 names "shape-doctor.py and shape_advisory.py".
  • tests/test_shape_advisory.py::test_only_the_three_tools_read_the_advisory (assert :882) refuses any template .py holding the token shape_advisory.
  • CI run 39040501022 gives 1 failed, 11839 passed, and that failure is this test. Locally: head 1 failed, main 1 passed. The 32-file run left that file out.
  • Fix: reword without the token, e.g. "in shape-doctor.py and in the advisory it reads". Then test_shape_advisory.py plus the new file give 65 passed.

S1: the L4-124 line says "the name is good" about names that are not (bootstrap.py:261-263, :289-292).

  • Names git refuses (LC_ALL=C PYTHONUTF8=0): feature/日本..x, 日本.lock and 日本 x each print "the name is good … run bootstrap again under LC_ALL=C.UTF-8". Following it, git answers fatal: … is not a valid branch name. Both runs exit 0.
  • A name the validator refuses: \u65e5\udcff is "good" in step (a), while step (b) of the same run prints FINDING manifest-tracking-branch: … lone surrogate and fails.
  • Fix:
    1. Return the surrogate's fault ahead of UNWRITABLE.
    2. Before "good", ask git check-ref-format --branch about the bytes a UTF-8 run would use (branch.encode("utf-8", "surrogateescape"); POSIX only). Under ascii it gives 0 for feature/日本 and 128 for each bad name.
    3. Add rows for these cases.

S2: rows of tests/test_bootstrap_branch_bytes.py:379-391 now fail under an ascii encoding, and the test's docstring states the rule this PR reverses.

S3: SonarCloud reports 5 open S9073 issues at tests/test_bootstrap_non_utf8_branch.py:195, 211, 218, 304, 312. Split each assert a and b.

NITs:

Held:

  • every _try_git value round-trips through POSIX surrogateescape;
  • legs are never moved when the fault fires;
  • both ways out work through make bootstrap;
  • the family-root bootstrap.py has no _try_git;
  • "Re-pinning: none" is right, with no blob or digest of the file pinned anywhere;
  • no docs needed (147 passed);
  • the new tests against main's bootstrap.py: 8 failed, 10 passed.

Mutants: of 6, only MK (UNWRITABLE dropped for a leg's own branch:) survives (N4).

Merges:

against result
main 24a977c0 clean
main 40e678e6 clean
#723 clean
#815's head 10 conflicting hunks (main alone has 9)
#815's own commit, rebuilt on main clean

So lane 4's conflict claim measures as stated.

Lane: openreposhape-3 (openRepoShape-3)

…s it in UTF-8 and the name holds no surrogate, says `did not try`, and on Windows git's bytes are read as UTF-8, so `feature/日本..x` or `"日\udcff"` under `LC_ALL=C PYTHONUTF8=0` is no longer called good, and the advisory test passes (#710)

Fix round 1 for RV824's review of #824 (lane 3, issuecomment-6236261109),
under the lane 4 coordinator's rulings.

- M1: `_try_git`'s docstring named the advisory module, which
  `tests/test_shape_advisory.py` refuses in any template. It now says "in
  `shape-doctor.py` and in the advisory it reads".
- S1: three changes, so the ruled line never calls a bad name good.
  - `_argument_fault` asks `UNWRITABLE` last, and names any surrogate in
    the name first, a byte-carrying one included, because
    `validate-manifest.py` names every surrogate (#280).
  - `_git_refuses_in_utf8` asks `git check-ref-format --branch` about the
    name's UTF-8 bytes before the line calls it good. `feature/日本..x`,
    `日本.lock` and `日本 x` get git's own words and #455's way out.
  - The new tests have rows for those three names and for `"日\udcff"`.
- S2: `tests/test_bootstrap_branch_bytes.py`'s no-fault test skips
  `feature/日本`, `a<U+200D>b` and `a\xe9b` where `os.fsencode` cannot
  write them (`written`), and its docstring states the rule. The parameter
  list and docstring change; #723's lines do not.
- S3: each `assert a and b` in the new test file is now two asserts.
- N2: the first line for `UNWRITABLE` says `did not try`.
- N3: on Windows `_try_git` decodes git's bytes as UTF-8, since
  `subprocess` hands git its arguments through `CreateProcessW`.
  `os.fsdecode` there is `mbcs` with `replace` under
  `PYTHONLEGACYWINDOWSFSENCODING=1`. #745's `_head_of` is not changed.
- N4: a row sends `UNWRITABLE` through a leg's own `branch:`.
- N5: the four new-file tests that hand git a name only UTF-8 can write,
  or read a child's UTF-8 line, skip where pytest itself is not under UTF-8.
- N6: a comment at the decode records #710's ruling, Option 2: `git_out`
  keeps its strict UTF-8.

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

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-4, W710) Fix round 1 for RV824's review (issuecomment-6236261109), on head 0a5e82bf. One commit on 1a9d08be. Each finding is taken under the lane 4 coordinator's rulings. Measured on Linux, git 2.43.0, Python 3.12.3.

M1, taken. _try_git's docstring now says "in shape-doctor.py and in the advisory it reads". bootstrap.py no longer holds shape_advisory or single-repository.yaml. tests/test_shape_advisory.py is in the targeted run: 2859 passed, 8 skipped, exit 0 (29 min; the 8 skips are test_leg_path_yaml_scalars.py's own, the same 8 as round 0).

S1, taken, all three parts.

  1. _argument_fault asks UNWRITABLE last, and a name holding any surrogate is faulted for its first surrogate, a byte-carrying one included, as validate-manifest.py names it (validate-manifest.py accepts a display name holding a lone surrogate (name: "a\ud83db") and then raises UnicodeEncodeError printing it, so make validate and shape-doctor.py fail on a traceback instead of a finding while bump-leg.py and update-shape.py apply pass the same project once #254 lands #280).
  2. Before the line calls a name good, _git_refuses_in_utf8 asks git check-ref-format --branch about branch.encode("utf-8", "surrogateescape"), handed over through os.fsdecode so that POSIX's os.fsencode writes exactly those bytes. Where git refuses, the line gives git's words and assembly-root bootstrap.py: a lone surrogate or NUL in tracking_branch (or a leg's branch:) ends make bootstrap in a subprocess traceback in step (a), and validate-manifest.py says manifest ok, so no tool names it #455's way out, and never "good". Asked directly, git 2.43 gives 0 for feature/日本 and 128 for feature/日本..x, 日本.lock and 日本 x.
  3. There are now rows for all four names. Measured on a fresh scaffold of this head, under LC_ALL=C PYTHONUTF8=0:
[dotdot] exit 0
  [spec] spec: did not try branch feature/\u65e5\u672c..x: it holds U+65E5, a character this system's encoding cannot write, which no command can be given as an argument; the leg is left as it is
          and git refuses it as a branch name in UTF-8 too:
          fatal: 'feature/\u65e5\u672c..x' is not a valid branch name
          the branch is the leg's `branch:`, else `--branch`, else `tracking_branch`. Give that one a name git can create and run bootstrap again.
[lock] exit 0    ... fatal: '\u65e5\u672c.lock' is not a valid branch name
[space] exit 0   ... fatal: '\u65e5\u672c x' is not a valid branch name
[surrogate] exit 1; bootstrap FAILED: manifest (exit 1)
  [spec] spec: could not create branch \u65e5\udcff: it holds U+DCFF, a lone surrogate, which no command can be given as an argument; the leg is left as it is
          the branch is the leg's `branch:`, else `--branch`, else `tracking_branch`. Give that one a name git can create and run bootstrap again.
[good] exit 0
  [spec] spec: did not try branch feature/\u65e5\u672c: it holds U+65E5, a character this system's encoding cannot write, which no command can be given as an argument; the leg is left as it is
          the name is good, and this system's encoding, ascii, cannot write it: run bootstrap again under `LC_ALL=C.UTF-8`, or with `PYTHONUTF8=1`.

The tests are test_a_name_git_refuses_in_utf8_too_is_never_called_good (three rows) and test_a_name_holding_a_surrogate_too_is_named_for_the_surrogate. There are also two unit rows: 日\udcff gives U+DCFF, a lone surrogate, and \udc80日 gives U+DC80, a lone surrogate.

S2, taken. In tests/test_bootstrap_branch_bytes.py, the rows feature/日本, a<U+200D>b and a\xe9b of test_a_branch_the_system_carries_has_no_fault now go through a new written(). It skips the row, at collection, where os.fsencode cannot write it. The docstring now states the rule. Under LC_ALL=C PYTHONUTF8=0 for pytest itself:

The change is to the parameter list (:394-395), a helper after SECOND_LINE (:81-92) and docstring lines :398-401. #723's lines (:388-390 and its insertions) are untouched. git merge-tree --write-tree of this head against #723 6ba8c5d2 is clean, tree ead848f1.

S3, taken. Each of the five assert a and b in the new test file is now two asserts. grep -n " and " tests/test_bootstrap_non_utf8_branch.py | grep assert prints nothing.

N1, no code change, as ruled. It is in the PR body's "What it cannot tell", for #459's checklist: the printed git -C spec merge --ff-only 'origin/a\udcff' does not work pasted into sh, because the \udcff is #391's spelling, not the byte.

N2, taken. For UNWRITABLE the first line says did not try branch; #455's NUL and surrogate faults keep could not create branch. A leg already on feature/日本, made by a PYTHONUTF8=1 run and bootstrapped under ascii, prints did not try branch feature/\u65e5\u672c: ....

N3, taken. _try_git returns raw.decode("utf-8") if os.name == "nt" else os.fsdecode(raw). Its docstring now says that on Windows subprocess encodes no argument: it hands the command line to CreateProcessW as UTF-16, and git for Windows reads it, and writes a ref name, in UTF-8. test_on_windows_what_git_prints_is_read_as_utf8 puts a legacy-mode os stand-in into the bootstrap module alone (nt, with an os.fsdecode that replaces) and reads refs/remotes/origin/日本 back whole. #745's _head_of is main's code and is not touched; the body records the same limit there.

N4, taken. test_a_legs_own_branch_this_encoding_cannot_write_is_said_too sets branch: "feature/日本" on the spec leg under ascii. The spec leg gets the two ruled lines and the code leg on main at <pin> (branch tip == pin), exit 0, and nothing moves. The mutant that drops UNWRITABLE for a leg's own branch: (fault is not None and not (leg.get("branch") and fault.endswith(UNWRITABLE))) is now killed: 1 failed, 25 passed.

N5, taken, in the new test file only. NEEDS_A_UTF8_PROCESS skips, with a reason, the three tests the review counted:

  • [ending-in-U+00A0];
  • test_under_an_ascii_encoding_an_upstream_named_in_utf8_is_moved_back;
  • test_a_tracking_branch_the_validator_names_is_followed_and_then_placed.

It also skips this round's N3 test, which hands git 日本 too. With pytest under LC_ALL=C PYTHONUTF8=0, the new file plus the no-fault test give 30 passed, 7 skipped: those four, and S2's three. tests/conftest.py is not changed.

N6, taken. The body says three rows and lists test_shape_advisory.py in the run. _try_git has a comment at the decode: "#710's ruling, Option 2: the decode is step (a)'s own, here, and repo_shape.git_out keeps its strict UTF-8 for every other caller." The docstring says the same.

Mutants, each run against the new file, all killed:

mutant result
the previous head's bootstrap.py (1a9d08be) 11 of 26 fail (this round's checks)
_try_git back to git_out(args, cwd=cwd) 3 fail
UTF-8 + surrogateescape in place of os.fsdecode 1 fails
the text stripped 1 fails
no UNWRITABLE 12 fail
#455's line for a good name 4 fail
no surrogate ahead of UNWRITABLE 3 fail
git never asked 3 fail
git asked of the text, not the UTF-8 bytes 6 fail
could not create for UNWRITABLE 7 fail
os.fsdecode on nt too 1 fails
UNWRITABLE dropped for a leg's own branch: (MK) 1 fails

Run: 2859 passed, 8 skipped, exit 0 (29 min; the 8 skips are test_leg_path_yaml_scalars.py's own, the same 8 as round 0), 33 files: the round-0 32 plus tests/test_shape_advisory.py.

Merges of 0a5e82bf:

against result
main 40e678e6 clean, tree cc706122
#723 6ba8c5d2 clean, tree ead848f1
#815 271710ba bootstrap.py conflicts, 10 hunks; main 40e678e6 alone against #815 has 9
#815's own commit, rebuilt on 40e678e6 clean, tree 1c297316

Lane: openreposhape-4 (openRepoShape-4)

@brettheap

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-3, 2026-10-17T17:46:06Z) RV824's re-check of #824's fix round 1 (head 0a5e82bf): READY.

  • Every item of the first review is fixed, each one measured.
  • CI is green: tests gives 11975 passed, 145 skipped, 1 xfailed, and SonarCloud has 0 open issues.
  • One new NIT comes from this round's own check-ref-format call; it does not block.
  • The head did not move during the re-check. Main moved to fa04d2cd (family.py and update-shape.py only), and the merges were re-run there.

The first review's items, on 0a5e82bf:

R1-N1 (new NIT): a broken leg makes a good name read as one git refuses (bootstrap.py:345, :926).

  • _git_refuses_in_utf8 runs git check-ref-format --branch with the LEG as its working directory.
  • With the spec leg's .git file naming a missing git dir, under ascii, tracking_branch: "feature/日本" gives spec "git refuses it … fatal: not a git repository", while the code leg in the same run says "the name is good". check-ref-format --branch exits 128 in a broken leg.
  • It needs a broken gitfile too, and the pins validator already fails that leg in the same run, so it is only a NIT.
  • Fix: ask git in the root, _git_refuses_in_utf8(root, branch) at :926. Both legs then say "the name is good", and 80 tests pass. Add a broken-gitfile row.

R1-N2: the body's first "What it cannot tell" bullet ("the decode is mbcs") now applies only to #745's _head_of. Reword it.

Runs (stub gh first on PATH, GH_TOKEN=bogus, empty GH_CONFIG_DIR):

run result
the 9-file set 528 passed (the first review had 1 failed, 501 passed)
the new file + branch_bytes, pytest under ascii 73 passed, 7 skipped

CI on 0a5e82bf: tests, tests-git-bash, package and SonarCloud all pass.

Mutants: all 11 killed:

Merges:

Lane: openreposhape-3 (openRepoShape-3)

…nnot write is good in the root, not in the leg, so a leg whose `.git` file names a git dir that is gone no longer turns a good name into one git refuses (#710)

Fix round 2 for RV824's re-check of #824 (lane 3, issuecomment-6237945041,
READY), its R1-N1. `_git_refuses_in_utf8` ran `git check-ref-format
--branch` in the leg. `--branch` makes git set up the repository it runs in,
so in a spec leg whose `.git` file named a missing git dir it exited 128
(`fatal: not a git repository`). Under `LC_ALL=C PYTHONUTF8=0`,
`tracking_branch: "feature/日本"` then read as a name git refuses on that
leg, while the code leg in the same run called it good. Git is asked in the
root now, the repository bootstrap was found in, and the answer is about
the name alone: every leg says the name is good, and the pins validator
names the broken leg, as it did.

`tests/test_bootstrap_non_utf8_branch.py` gains that row. The mutant that
asks in the leg again fails it.

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

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-4, 2026-10-17T19:01:15Z) Round 2 for RV824's re-check (issuecomment-6237945041, READY), on head 7777c7d0. One commit on 0a5e82bf, under the lane 4 coordinator's rulings. Measured on Linux, git 2.43.0, Python 3.12.3.

R1-N1, taken. checkout_tracking_branch now calls _git_refuses_in_utf8(root, branch), so git check-ref-format --branch runs in the root and not in the leg. The function's docstring says why: --branch makes git set up the repository it runs in, and in a leg whose .git file names a missing git dir it exits 128.

Measured on a fresh scaffold of this head under LC_ALL=C PYTHONUTF8=0, with tracking_branch: "feature/日本" and spec/.git set to gitdir: ../.git/modules/gone:

asked in the root (this head), exit 1, bootstrap FAILED: pins (exit 2):
  [spec] spec: did not try branch feature/\u65e5\u672c: it holds U+65E5, ...; the leg is left as it is
          the name is good, and this system's encoding, ascii, cannot write it: run bootstrap again under `LC_ALL=C.UTF-8`, or with `PYTHONUTF8=1`.
  [code] code: did not try branch feature/\u65e5\u672c: ...
          the name is good, and this system's encoding, ascii, cannot write it: ...
asked in the leg (the mutant):
  [spec] spec: did not try branch feature/\u65e5\u672c: ...
          and git refuses it as a branch name in UTF-8 too:
          fatal: not a git repository: .../spec/../.git/modules/gone
          the branch is the leg's `branch:`, ... Give that one a name git can create and run bootstrap again.
  [code] code: ... the name is good, ...

The new row is test_a_leg_whose_git_dir_is_gone_still_has_a_good_name_called_good. The mutant that asks in the leg again gives 1 failed, 26 passed, the failure being that row. Round 1's MS1b, git never asked, still gives 3 failed.

The row asserts both legs' step (a) lines exactly, a non-zero exit and : pins (exit 2). It does not assert bootstrap FAILED: on the tree merged with #815 05a1ebb3, #764's ending is bootstrap REFUSED: pins (exit 2), exit 2. My first version asserted code == 1, which failed there. I caught that on the merged tree before the push and made the row hold on both endings.

R1-N2, taken (body only). The first "What it cannot tell" bullet now says that _try_git decodes git's output as strict UTF-8 on Windows, whatever PYTHONLEGACYWINDOWSFSENCODING says. The mbcs sentence moved to the _head_of bullet, the one place it still applies, since #745's code is untouched here.

N6. A comment for #710 recording the Option 2 ruling on git_out is drafted for the coordinator to post.

Run: 642 passed, exit 0 (5 min), over 11 files: the new file, the five tests/test_bootstrap_*.py (test_bootstrap_branch_bytes.py among them), tests/test_shape_advisory.py, tests/test_git_output_decoding.py, tests/test_repo_hygiene.py, tests/test_cli_reference.py and tests/test_agents_md_codes.py.

On the merged tree (this head with main bad05d21, which has #723, then with #815 05a1ebb3; tree 63fc7be5, in a scratch clone):

  • the new file and tests/test_bootstrap_branch_bytes.py give 88 passed;
  • with pytest under LC_ALL=C PYTHONUTF8=0, the new file and the no-fault test give 31 passed, 7 skipped.

Merges of 7777c7d0:

against result
main bad05d21 (#723 landed) clean, tree 477905ae
#815 05a1ebb3 clean, tree 56f0a042

#815 has merged main 24a977c0 since round 0, so its stale-base hunk is gone.

Lane: openreposhape-4 (openRepoShape-4)

@brettheap

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-3, 2026-10-17T19:05:21Z) RV824's re-check of #824's round 2 (head 7777c7d0): READY. R1-N1's fix holds, and the body and issue edits read correctly. Nothing else changed since 0a5e82bf. CI's tests job on 7777c7d0 was still running at 19:05Z, so it must be green before #824 lands. package, tests-git-bash and SonarCloud pass.

  • R1-N1, fixed.
    • RV824's broken-gitfile probe: the spec leg's .git names a missing git dir, with tracking_branch: "feature/日本", under LC_ALL=C PYTHONUTF8=0. "The name is good" now appears for BOTH legs, and "git refuses it" is gone. The run still ends pins (exit 2) on the broken leg, as before.
    • The mutant that asks git in the leg again (_git_refuses_in_utf8(sub, branch)) is killed by the new row test_a_leg_whose_git_dir_is_gone_still_has_a_good_name_called_good.
  • R1-N2, fixed: the body says _try_git decodes as strict UTF-8 on Windows, "whatever PYTHONLEGACYWINDOWSFSENCODING says".
  • N6, fixed: the Option 2 ruling is on bootstrap.py and repo_shape.py: a leg on a branch whose name is not UTF-8 makes every later make bootstrap end in a UnicodeDecodeError, and a character the filesystem encoding lacks still ends step (a) in a traceback #710 (issuecomment-6239116961).
  • Nothing else: 0a5e82bf..7777c7d0 is two files, +34 −3. In bootstrap.py the parameter is renamed sub → root, with a docstring paragraph and the caller at :933; the new file gains one test.
  • NIT: tests/test_bootstrap_non_utf8_branch.py:428-430 has three blank lines before the new test, where PEP 8 wants two. It is cosmetic.

Runs (stub gh first on PATH, GH_TOKEN=bogus, empty GH_CONFIG_DIR):

run result
the new file, test_bootstrap_branch_bytes.py and test_repo_hygiene.py 158 passed
the new file and branch_bytes, pytest under ascii 74 passed, 7 skipped
merged with #815 93 passed, 3 skipped

SonarCloud: 0 open issues, gate OK.

Merges: clean against main fc867207, #815 05a1ebb3, #823 153bab90 and #723.

Lane: openreposhape-3 (openRepoShape-3)

brettheap pushed a commit that referenced this pull request Oct 17, 2026
…a branch holding a character that does not print or is white space is refused, so `tracking_branch: main` followed by U+00A0, which the reader now reads as YAML does, makes no second `main` and lets no commit onto `main` (#715)

Fix round 1 for RV823's review of #823 (issuecomment-6236502554), on the coordinator's rulings.

MUST-1, option (B). Since this PR the reader reads `tracking_branch: main` followed by U+00A0 as PyYAML reads it, `'main\xa0'`, where main read `'main'`. git takes that name, so on #823's first head bootstrap made a second `main` in every leg and checked it out, and bump-leg's guard of the tracking branch let a commit be made on `main`. The reader stays as it is, and every reader of a branch now refuses what `siblings.py` refuses, a character `str.isprintable()` rejects or `str.isspace()` matches:
- `validate-manifest.py`'s `_branch_finding` names it as `manifest-tracking-branch` or `manifest-leg-branch`, the value spelled and the code point named.
- `bootstrap.py`'s `_argument_fault` returns `UNSEEN`, so step (a) makes no ref and says why on the leg's line. A byte-carrying surrogate (U+DC80 to U+DCFF) is not asked, so a name that is not UTF-8 still reaches git.
- `bump-leg.py`'s `Project.tracking` reads the value through `checked_value`, as `family.py` does, so every run is `unsafe-value`, exit 2, before anything is fetched or written.
- `update-shape.py`'s `tracking_branch_of`, the same guard for `apply --branch`, reads it through `checked_value` too, and the ledger rolls the rewrite back.

The rule now refuses three branches #455's tests passed to git: a zero-width joiner, a space and an ESC. Those rows move to the fault tables. #391's spelling tests took `tb` U+2028 `x` as their subject, a branch git accepts and a terminal must not be handed raw. Step (a) now refuses it, so they move onto `tb\udcffx`, the byte 0xff, the one name that does not print and still reaches git. They are POSIX-only and skipped where the disk refuses the byte, and one refusal row keeps U+2028. `test_the_branch_and_its_upstream_are_spelled_in_the_move_back` is skipped with its reason: reading an upstream named with that byte is #710's traceback, which #824 fixes, and whichever of #823 and #824 lands second drops the skip.

NIT-1: a quoted flow sequence item's value is asked for a bracket before it is split again, so `k: ["a": b[: c]` names the `[` in #301's words. NIT-2: `startswith` at two tests. NIT-3: `_read_as` returns `_reading_in_words(value)`. SHOULD-2: `_block_scalar_body`'s docstring says a line of Unicode spaces is text only at the block's own indentation or deeper.

Lane: openreposhape-4 (openRepoShape-4)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap pushed a commit that referenced this pull request Oct 17, 2026
…r a character that does not print or is white space, so `feature/` and CJK, `développement` and `a&b` are read as they are, #209's quoted base is back, and a quoted key whose value opens a flow collection keeps the single-pair words (#715)

Fix round 2, after RV823's re-check of `73b7994c` (READY, no MUST), on Brett Heap's ruling on its SHOULD-1, "go with option A for #823" (2026-10-17T20:23:58Z, #823 issuecomment-6240468893).

- Option A. `Project.tracking` and `tracking_branch_of` read through `checked_value` since `53af978e`, whose `SAFE_ARG_RE` refused names git creates and the root's own gate passes: `feature/` and CJK, `développement` (NFC and NFD) and `a&b`. They now read through `readable_branch`, a new helper in `scripts/repo_shape.py` beside `checked_value`. It refuses a character for which `str.isprintable()` is false or `str.isspace()` is true, as `validate-manifest.py`'s `_branch_finding` and `bootstrap.py`'s `_argument_fault` do, in their words: `REFUSED unseen-branch: tracking_branch is 'main\xa0', which holds U+00A0, a character that does not print or is white space: a person reading the name cannot tell it from the name without it`. It also refuses a leading `-` (`unsafe-value`): `update-shape.py` hands the name to `gh pr create --base` as one argument, and git creates no branch whose name begins with one. `bump-leg.py` never hands it to a command; it compares it and prints it.
- #209's `test_a_value_the_manifest_holds_is_quoted_for_the_shell_that_reads_the_line` is restored as it was, `tracking_branch: "a&b"` quoted after `--base`, and its refusal twin is now `test_a_tracking_branch_no_person_can_see_never_reaches_the_line`, on `main\xa0`.
- Rows: the four names are taken by bump-leg (real run and `--dry-run`, on `main`), by `apply --branch shape/x` on a root (its NEXT line's `--base` reads back as the name), and by `tracking_branch_of` in-process; `tracking_branch_of` refuses every name the validator does, and a leading `-`.
- NIT-1. `["a": [b]]`, `["a": {b: c}]` and `['a': [b, c]]` got #301's plain-scalar words from round 1's bracket test of a quoted item's value. A value that opens a flow collection is not asked it, so they keep the single-pair words main gives them; PyYAML reads each as one pair.
- NIT-2. The three composite asserts this PR added (`code == … and "Traceback" not in err`) are split.
- NIT-3. A validator row for `a b`, the one character `isspace` adds to `isprintable`.
- NIT-4. The #710 test is `xfail(strict=True, raises=AssertionError)`, not skipped, so the merge round that brings #823 and #824 together sees it pass.

Lane: openreposhape-4 (openRepoShape-4)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@brettheap
brettheap marked this pull request as ready for review October 17, 2026 21:50
@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 hours and 56 minutes by commenting @sourcery-ai review. Upgrade to get a review now.

@sonarqubecloud

Copy link
Copy Markdown

@brettheap
brettheap merged commit 0dee22b into main Oct 17, 2026
5 checks passed
@brettheap
brettheap deleted the fix/710-non-utf8-branch-names branch October 17, 2026 21:53
brettheap pushed a commit that referenced this pull request Oct 17, 2026
…branch it started on as git's own bytes, the `branch -m` remedy is quoted for a POSIX shell, and a branch that is not UTF-8 is named by its bytes, so the rollback no longer raises under a C locale and README's quoting sentence holds (#765)

Fix round 1 for #849, after r765's review (issuecomment-6242841859) and the coordinator's rulings.

- scripts/family.py: `_holder_branch`'s `branch -m` remedy is `shell_line([...], "posix")`, so `tests/test_readme_windows_quoting_sentence.py` passes again (finding 1, Fix A).
- update-shape.py: `_as_git_argument` spells a name read from git as the argument that hands git the same bytes under any locale, as #824 does for bootstrap.py; `Rollback._undo_branch`'s checkout uses it. Under `LC_ALL=C PYTHONUTF8=0` a rollback from `feat<U+00A0>` raised `UnicodeEncodeError` part way (finding 2).
- tests: r765's `test_a_rollback_puts_head_back_on_the_branch_it_started_on`, in both locales (finding 3); `held_by_git` also holds each `.git/modules` git directory's refs, objects and FETCH_HEAD, so "before the fetch" is asserted (finding 4).
- The three `*-branch-not-utf8` refusals name the branch by its bytes, `b'caf<0xE9>'`, as `trailer-not-utf8` names a trailer.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Oct 18, 2026
…hild is told the encoding its words are read in, so a cp1252 stdout gets one encoding from setup, and `make bootstrap` passes a project whose name the code page cannot hold (#851)

`setup-project.py`'s `main` reconfigures its stdout and stderr to UTF-8,
and `run` started every child with no `env=`, so a Python child took its
streams' encoding from its own environment. Under
`PYTHONIOENCODING=cp1252:strict` or `cp1252:surrogateescape` (Windows'
ANSI code page on a stdout that is not the console), a `--yes
--local-remote-dir` run exited 0 with 9 of its 129 stdout lines in cp1252
among UTF-8 ones, and the plan run again to read why it was refused, the
one captured child, wrote cp1252 into a pipe `run` decodes as UTF-8. The
assembly root's `scripts/bootstrap.py` started the validators the same
way, so on a project whose `name:` the code page cannot hold,
`validate-manifest.py` raised `UnicodeEncodeError` and `make bootstrap`
ended `bootstrap FAILED: manifest (exit 1)`: #254's defect, in a pinned
copy.

- `setup-project.py`: `child_environment(capture)`, #254's rule as
  `scripts/shape_materialize.py` writes it in
  `inherited_child_environment` and `captured_child_environment`. `run`
  passes it as `env=`, so all six Python children get it, and the next
  one will: `self_bootstrap`, `check_names`, `plan_and_confirm`,
  `why_the_plan_did_not_go_through` (the captured one), `scaffold` and
  `clone_and_bootstrap`. It holds a copy because it runs before any
  checkout of the standard exists.
- `templates/assembly-root/scripts/bootstrap.py`:
  `inherited_child_environment()`, a copy of the library's, which `_run`
  passes. A project does not carry `shape_materialize.py`, and a pinned
  script that imported a new name out of `repo_shape.py` would be refused
  at `update-shape.py apply` (`update-accept-local-missing-name`, #536),
  so the copy keeps `repo_shape.py` untouched. This is a shape bump:
  every project re-pins the copy.
- `tests/test_validator_child_encoding.py`: the `setup-project.py` row
  leaves `NOT_YET`, and `LAUNCHERS` gains the assembly `bootstrap.py`.
  `COPIES` holds both copies to the library's answer for every stream
  `INHERITED` names. `run` is shown to tell each child what it is read
  in, both tools run end to end under cp1252, and `INHERITING` pins that
  `siblings.py`'s child inherits an environment its code page agrees
  with: measured, it needs no change.

It follows lane 5's #808 (#759), landed as 677f9bf, whose `NOT_YET`
row this takes out, and lane 4's #824 (#710), landed as 0dee22b,
which also edits the assembly `bootstrap.py`. Lane 5's #815 (#764)
edits it too; the two merge clean, and whichever lands second re-gates.

Closes #836
Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap pushed a commit that referenced this pull request Oct 18, 2026
…gument_fault` asks a NUL and a lone surrogate, then a character no person can see, then one this system's encoding cannot write, so the line that calls a name good is never said of one #715 refuses (#715)

#823 is the second of #823 and #824 to land, so this is the merge round agreed on #473. RV823's re-check of round 2 (#823 issuecomment-6241904307) measured the merge, and its resolution is taken.

The four textual hunks:
- `templates/assembly-root/scripts/bootstrap.py`, `_argument_fault`'s docstring: #824's `UNWRITABLE` paragraph. #823's sentence "A character the system's encoding merely lacks is not this fix's." is dropped, since #824 makes it a fault. #823's `UNSEEN` paragraph now says it is asked before `UNWRITABLE`, and why.
- `_argument_fault`'s tail: #823's `UNSEEN` loop runs after the NUL and surrogate loop and BEFORE #824's unwritable test. #824 asks `UNWRITABLE` last because its line can call the name good. A name holding a character no person can see is not good under any encoding, and its remedy (rename the branch) holds in every locale.
- `_say_not_tried`: #824's `refused=` signature and lines, with #823's `UNSEEN` lines as an early return first.
- `tests/test_bootstrap_branch_bytes.py`, the no-fault list: #824's `written(...)` rows for `feature/日本` and `a\xe9b`. The zero-width joiner, `a b` and `a\x1bb` are dropped: they are `UNSEEN` faults, and `test_a_character_no_person_can_see_is_the_fault` holds all three.

The three test conflicts `git merge-tree` cannot see (RV823's MUST-1):
1. `tests/test_bootstrap_non_utf8_branch.py`, `[ending-in-U+00A0]`. `--branch a ` is now an `UNSEEN` refusal. The row's subject is the UPSTREAM's name (lane 5's site: a trailing U+00A0 stripped from git's answer). So the branch moves to a printable `aé`, and the upstream keeps its U+00A0. The test takes the two names apart, and the expected line is built from both.
2. Its `a-space` row (`日本 x`) of `test_a_name_git_refuses_in_utf8_too_is_never_called_good` becomes `a-tilde` (`日本~x`), a name git refuses with no character no person can see. A space is `UNSEEN` now: two unit rows under the emulated `ascii` encoding (`日本 x`, `日本` followed by U+00A0) and one end-to-end row hold that it is named before `UNWRITABLE`, never called good, and that the validator ends the run.
3. `tests/test_bootstrap_after_bump.py`, `test_the_branch_and_its_upstream_are_spelled_in_the_move_back`. The strict xfail did not XPASS: every bootstrap assertion passed, and the last one failed in `head_ref`'s text-mode `git` over `refs/heads/tb\xffx`. HEAD is read as bytes, and asked to be exactly that branch. The mark is gone, and the test is plain: it holds the literal escape the line must print, from `tracking_branch:`, where #824's own row goes through `--branch` and builds its expectation with `spelled()`.

Also, #824's three blank lines before `test_a_leg_whose_git_dir_is_gone_still_has_a_good_name_c…` are two, the house's.

Runs on this tree before the commit: `test_bootstrap_non_utf8_branch.py` and `test_bootstrap_branch_bytes.py`, 92 passed; the move-back test, 1 passed.

Lane: openreposhape-4 (openRepoShape-4)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap pushed a commit that referenced this pull request Oct 18, 2026
…pinned `repo_shape.py`, a lone surrogate and a NUL are named as the validator names them, and no line says `siblings.py` refuses a space (#715)

RV823's re-check of round 2 (#823 issuecomment-6241904307), after the merge with #824.

- SHOULD-1. `readable_branch` and `UNSEEN` were in `scripts/repo_shape.py`, a shape copy every root digest-pins, though only `bump-leg.py` and `update-shape.py` call them. They are now in `scripts/shape_materialize.py`, in a section of their own after #283's branch-name rule, under the same "it lives here and not in `scripts/repo_shape.py`" reason. Both tools import the name on a line of its own, and `update-shape.py`'s `from repo_shape import (...)` list is as main has it again. The docstring's reason is restated: the root's validator and bootstrap DO import from `repo_shape.py`, and they spell the rule themselves because a root's copies may run beside an older `repo_shape.py` (the skew rule, `validate-manifest.py`'s `_library_takes_remedies`, #661), which has no such function.
- NIT-2. A byte-carrying U+DCFF is refused as "a lone surrogate: no UTF-8 file or console can hold it, so no branch name is spelled with one", and a NUL as "a NUL: no argument, file name or ref name can hold one", as `validate-manifest.py` words them (#280); any other such character keeps the `UNSEEN` words. The leading-`-` refusal says only "which begins with `-`, and git creates no branch whose name begins with one", which holds for bump-leg, which hands the name to no command, as well as for update-shape. New rows hold both.
- NIT-1. `bootstrap.py`, `validate-manifest.py` and the fault test's docstring in `tests/test_bootstrap_branch_bytes.py` no longer say `siblings.py` refuses every such name: it asks `isprintable()` alone, and takes a space. The fault test's docstring no longer says git takes each of its names: git refuses `a b` and `a\x1bb`.

Run on this tree before the commit: the helper's callers in `tests/test_yaml_reader_spaces.py` and `tests/test_update_shape_pr_base.py`, and the fault test, 75 passed.

Lane: openreposhape-4 (openRepoShape-4)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Oct 18, 2026
…space opens reads as PyYAML reads it, and validate-manifest, bootstrap, bump-leg and update-shape refuse a branch holding a character that does not print or is white space (#715) (#823)

What #635 left in the reader, and three texts about it: lane 4's lines L4-148 to L4-155 on #459's checklist, from R392's review of #635. Fix round 1 answers lane 3's review RV823.

- L4-150. Every strip of a line in the reader (`_strip_comment`, `_split_key`, `_scalar`, `_flow`, `_split_flow`, `_significant_text` and `_block_scalar_body`) was `str.strip()` with no argument, and `_DASH_RE` used `\s`. Both also take U+001F and sixteen Unicode spaces (U+00A0, U+1680, U+2000 to U+200A, U+202F, U+205F and U+3000), which YAML keeps as text. So `k: \u3000]`, `k: \u2003@a` and `k: \xa0%a` were refused quoting `']'`, `'@A'` and `'%a'`, texts the file does not hold, and `k: [a,\u3000,b]` was refused as an empty entry; `k: \xa0` read as no value, `- \u3000` as `[None]` and `k: [a, \xa0]` as `['a']`. PyYAML reads each whole. One constant, `_YAML_WHITE = " \t"`, now names YAML's white space at every strip, and `_DASH_RE` is `^-([ \t]+|$)`. Measured with PyYAML 6.0.3 over 123 probe documents: 77 readings changed, 31 of them now as PyYAML reads them and 37 now refusals. The other 9 (7 inputs) now read a document PyYAML refuses, each in a class this reader already reads: U+001F as text, as a raw ESC is; a tab after a Unicode space inside a plain scalar, as in `k: a\tb`; and a `:` opening a value inside a block value that a Unicode space opens, as in `k: a: b` and `k: a:`.
- RV823 MUST-1. So `tracking_branch: main\xa0`, which read `main`, reads `'main\xa0'`, a name no person can tell from `main`. Four tools now refuse a branch holding a character for which `str.isprintable()` is false or `str.isspace()` is true, and bump-leg and update-shape refuse no other: `validate-manifest.py`'s `_branch_finding` reports `manifest-tracking-branch` and `manifest-leg-branch` with the value as `{value!r}`; `bootstrap.py`'s `_argument_fault` refuses it (`UNSEEN`), so no ref is made and the leg is left as it is, while the byte-carrying surrogates U+DC80 to U+DCFF still reach git (#455); and `bump-leg.py`'s `Project.tracking` and `update-shape.py`'s `tracking_branch_of` read through `readable_branch`, in `scripts/shape_materialize.py`, which no root carries, and which refuses it as `unseen-branch` in the validator's words, a NUL and a lone surrogate in theirs too, exit 2, before any fetch or write, and refuses a leading `-` as `unsafe-value`, since update-shape hands the name to `gh pr create --base`. Read raw, `update-shape.py apply --branch main` on `main` committed onto `main` and printed `gh pr create --base 'main\xa0' --head main`. On Brett Heap's ruling (option A, 2026-10-17T20:23:58Z) the two tools do not read it through `checked_value`, which refused `feature/` and CJK, `développement` and `a&b`, names git creates and the root's gate passes; #209's quoted `--base a&b` stands. #391's spelling tests move onto the byte name `tb\udcffx` (POSIX only, skipped with a reason where the disk refuses the byte), one row keeps `tb\u2028x` as a refusal that spells the name, and the zero-width joiner, `a b` and `a\x1bb` move to the fault tables. `test_the_branch_and_its_upstream_are_spelled_in_the_move_back`, which waited on #710, is a plain test again, with HEAD read as bytes.
- The merge with #824 (#710), main `0dee22b3`. `_argument_fault` asks a NUL and a lone surrogate with no byte, then `UNSEEN`, then #824's `UNWRITABLE` last: its line calls the name good, and a name holding a character no person can see is not good in any encoding. `_say_not_tried` keeps #824's `refused=` lines, with `UNSEEN`'s first. In #824's `tests/test_bootstrap_non_utf8_branch.py`, the row whose branch ended in U+00A0 has a printable branch `aé` and keeps the upstream ending in U+00A0 it was written for, and the `日本 x` row of the names git refuses is `日本~x`; three rows hold that a space is named before `UNWRITABLE`.
- L4-148. `- :` read `[{'': None}]` and `: a` read `{'': 'a'}`, where PyYAML refuses both. `_refuse_unsupported` now refuses a line or an item that opens with a `:` opening a value, beside the explicit and merge keys and in their words: `line N: empty keys are not supported`. A quoted `"": a` still reads `{'': 'a'}`. The fuzz in `tests/test_yaml_indicator_openers.py` no longer drops `- :` (`EMPTY_BLOCK_KEY` is gone), and seeds 1 and 3 draw it again.
- L4-149. `k: {?: 1}` read the key `'?'`, where PyYAML reads `{None: 1}`. Of the issue's two options it is refused, by the one test `_refuse_entry_indicator` already makes of `[?]`, `{?}` and `{a: ?}`: a key that is a lone `?`, a `?` whose `:` opens a value, is `explicit keys are not supported: '?: 1'`. `[?: 1]` gives that refusal too, where it gave the single-pair one. `{?a: 1}` and `[?:a]` stay the text #252 keeps, and a block `?: 1` is the key `'?'` to PyYAML as well.
- L4-155. `k: [a: b: c]` was refused with the single-pair words. A flow sequence item whose pair's value splits again at a `:` opening a value is now `a flow sequence entry with a second `:` opening a value is not valid YAML: 'a: b: c'`. `[a:b: c]` and `[a: "b: c"]` are real single pairs to PyYAML and keep the single-pair words. A quoted item's value is asked the bracket test first, so `["a": b[: c]` keeps #301's words, unless it opens a flow collection: `["a": [b]]` keeps the single-pair words, as on main.
- L4-153. `_read_back` called `parse_yaml` unguarded. `checked_value` admits a leg path opening with `@`, which opens no plain scalar since #393, so a bare leg-path template line holding `--spec-path @x` raised `YamlError` out of `leg_path_problem`. The refusal is now the reading, and `_read_as` returns `_reading_in_words(value)`, "nothing at all, because it refuses the line (...)", so the scaffold refuses the value as `scaffold-bad-leg-path` with `LEG_PATH_REMEDIATION`. `_misread_reason`'s docstring now says the alphabet holds `@`. The four shipped leg-path lines are quoted (#259), so `--spec-path @x` still plans.
- L4-154. `_neutral_pin_entry`'s docstring gave `id: -` as "the text", which the reader refuses since #393. Its example is now `  - - a`, a list of its own, where `id: - a` is the text.

Pinned copies: `scripts/repo_shape.py` (assembly and family roots), `scripts/validate-manifest.py` and `scripts/bootstrap.py` (assembly roots) are digest-pinned in each root's `contracts/shape-pin.yaml`. No digest is recorded in this repository, so nothing here is re-pinned. `update-shape.py check` shows each as `upstream-changed` and `apply` re-copies and re-pins it; after that a root's gate refuses a quoted scalar followed by a Unicode space, `- :`, `: a`, `{?: 1}`, `[?: 1]` and a branch name holding a character that does not print or is white space. Until a root re-pins, its own validators and bootstrap read `main\xa0` as `main`. `with_scalar` on main wrote `commit: "NEW"\xa0 # c`, which this reader refuses; it now writes `commit: "NEW" # c`.

Tests: `tests/test_yaml_reader_spaces.py` (new, 339 tests) holds a row for each case, with each of U+3000, U+2003 and U+00A0, compares each with PyYAML where it is installed, finds the seventeen characters rather than listing them, and runs a project's own `validate-manifest.py`, bootstrap, bump-leg, update-shape and one scaffold `--dry-run`. Thirty-two mutants, each reverting one part, are each killed: two put `checked_value` back and fail the rows that take the readable names, and one asks `UNSEEN` after #824's `UNWRITABLE` and fails the rows that hold the order. The targeted run at the head, which is the merge with main `ca47b71d`, covered 73 files: the changed test files, #824's bootstrap gate set, every file that imports the reader, every bump-leg and update-shape file, the doctor, and the hygiene, CLI-reference and AGENTS.md checks. It gave 5718 passed and 34 skipped.

Closes #715

Lane: openreposhape-4 (openRepoShape-4)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Oct 18, 2026
…read as the bytes git wrote, so a branch ending in U+00A0 is no longer read as its neighbour, and a name that is not UTF-8 is refused by name before anything is written (#765) (#849)

The three tools that read the branch HEAD is on read it as text with `strip()` or `errors="replace"` (#765). Measured on main `fc867207`:

- **A branch ending in U+00A0, U+2028 or U+0085 read as its neighbour.** With `feat<U+00A0>` checked out beside `feat`:
  - `family.py remove` committed on `feat<U+00A0>` and printed a push line for `feat`, which pasted pushed `feat` at main's commit;
  - `update-shape.py apply --branch feat --push` exited 0 saying `committed on feat` and `pushed feat to origin`, though the commit was on `feat<U+00A0>`;
  - `update-shape.py`'s rollback switched HEAD back to `feat`, the neighbour;
  - `bump-leg.py` read `main<U+00A0>` as `main` and refused `bump-leg-on-tracking-branch`.
- **A branch whose name is not UTF-8** (`caf<0xE9>`) was committed on, and then named in a NEXT line, or a rollback's by-hand line, as `caf<U+FFFD>` or `\udce9`, which name no branch. A rollback could not put HEAD back on it.

Now:

- **One read in each tool,** as bytes: `update-shape.py`'s `_symbolic_head`, `family.py`'s `_holder_branch` and `bump-leg.py`'s `current_branch`. Each reads `git symbolic-ref -q HEAD`, takes off exactly one trailing newline and exactly `refs/heads/`, and decodes the rest with `surrogateescape`. A branch ending in U+00A0, U+2028 or U+0085 reads as itself, and the push line names it exactly.
- **A name that is not UTF-8 is refused by name before anything is committed or written:** `update-branch-not-utf8`, `family-branch-not-utf8` and `bump-leg-branch-not-utf8`, each under its tool's prefix. The refusal names the branch by its bytes (`b'caf\xe9'`), as `trailer-not-utf8` names a trailer. Its remedy ends with `git -C <root> branch -m <a new name>`, quoted for a POSIX shell in all three tools, which renames the branch HEAD is on without spelling the old name. No push line is printed for a name the tool cannot spell. In family.py and bump-leg.py the detail names the holder or root through `shown_path`, as the other refusals of the same checks do since #766.
- **`update-shape.py`'s `cmd_apply`** asks `current_branch` beside `_refuse_what_head_does_not_commit` when `--branch` is given, so the refusal comes before the first write.
- **A rollback hands git the branch it started on as git's own bytes.** `Rollback._undo_branch` goes through a new `_as_git_argument`, as #824 does for `bootstrap.py`. Under `LC_ALL=C PYTHONUTF8=0`, a rollback from `feat<U+00A0>` no longer raises `UnicodeEncodeError` part way, leaving the copies written, and one from `<U+65E5><U+672C>`, which raised on main too, exits 2 with every byte git holds as it was. family.py and bump-leg.py hand the branch they read only to printed lines, never to git.
- `scripts/repo_shape.py` is not changed.

Review: lane 1's independent adversarial review found defects at `25692a37`. The fix round fixed them, and the re-verification found `d98df568` PR-READY. Main was then merged (`da3fe1c8`, over #766's `shown_path` changes to the same checks), and the re-check found that head PR-READY too.

It lands by fast refresh (Brett Heap, 2026-10-15T22:48:35Z). Its reviewed head is green on Linux and SonarCloud. The branch merged main twice and was never rebased; the last merge is update-branch's, clean, giving 7c47760, which is green on SonarCloud and Git Bash. The full suite passed on its tree in lane 1's gate: 242 test files, 14,940 passed and 69 skipped.

Closes #765

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Oct 18, 2026
…an unreadable upstream path, cache or pin is no answer and a reftable leg's cherry-pick or revert is read from its ref store, so no traceback or ended pick follows (#764) (#815)

Against main. #745 landed as `24a977c`, and this branch is restacked onto main as `f6f318d1`: it merges #745's final head `1945154c`, then main, taking this branch's side in `bootstrap.py`, where `git diff 1945154 24a977c` over #745's files is empty. The restacked tree is `7884460e`, the tree the merge checks below predicted.

Lane 3's RV815 reviewed `f6f318d1`: READY, no MUST. One new commit, `05a1ebb3`, on `f6f318d1`, pushed to `fix/764-bootstrap-notice-exit-texts` as a fast-forward. Nothing was amended and main is not merged in that round. The merge round `61aed8d2` merged it later (#815 issuecomment-6247909102). It changes `templates/assembly-root/scripts/bootstrap.py` (+13/-8), `tests/test_bootstrap_refusal_exit.py` (+62) and `tests/test_bootstrap_upstream_notice.py` (+30/-2), and nothing else. README, the handbook, `docs/cli.md` and every other pinned file are untouched.

**The pinned digest moves again.** After the merge round with main (`61aed8d2`, #815 issuecomment-6247909102), `templates/assembly-root/scripts/bootstrap.py` is sha256 `89ff92e5e469003fb5a1f13a5adbe5be2f9c2913ad152eb3f49e8545f0a8c3cc`. Main's is `6a7847f3d78ab6c3a350914bf7f9c81e11e235a6d9f8ad2b5782418e8b794e57`, since #855. At the NIT round it was `09238ebd4202193a...`, where the body says `0cb334c260493313...`. The "Pinned files changed" paragraph below gives the current digests; the rest of that paragraph stands (the file is already `upstream-changed` for a v1.1.0 project once, and #824 changes it too). In "What changed", the count becomes: three commits after `f4935e11` (`271710ba`, `c2977425`, `05a1ebb3`), the restack's two merges (`3aa9a0fe`, `f6f318d1`), and the merge round with main (`61aed8d2`).

**What each NIT came to**

| NIT | taken | what changed | held by |
|---|---|---|---|
| 2: mutant X3 survives | yes | no shipped change; an over-long `SHAPE_UPSTREAM_PATH` (a name of 300 characters, and a path of over 5000) now has a test: `bootstrap ok`, exit 0, no notice, no traceback | `test_an_over_long_upstream_path_is_an_absent_answer` (2 cases) |
| 3: no flush before the ending | yes | `sys.stdout.flush()` at the top of `_end_of_run`, as the `git-not-runnable` path does | `test_the_ending_follows_step_c_in_a_log_of_both_streams` (2 cases: REFUSED and FAILED) |
| 4: docstring (d) overstated | yes | step (d) now says a finding is any exit but 2 and reads `(exit <code>)`; both lines are said, the finding first, where the run reaches its end; step (a) or the notice refusing `git-not-runnable` stops the run with neither line | the no-git cases below, and the nine ending cases that were already there |
| 5: `!= 0` | yes | the shape-pin list case asserts `== 2` | `test_a_shape_pin_that_is_not_a_mapping_ends_as_the_validators_end_it` |
| 6: no test for "a plain clone with no git" | yes | `test_a_plain_clone_with_no_git_ends_refused_with_exit_2`: `bootstrap REFUSED: pins (exit 2)`, exit 2, no FAILED line, step (c) still reached. A second test holds the sentence step (d) now says: no git, a manifest finding and a directory named by `SHAPE_UPSTREAM_PATH` refuse `git-not-runnable` first, with neither line | `test_a_plain_clone_with_no_git_ends_refused_with_exit_2`, `test_no_git_and_a_clone_named_refuses_before_either_line` |
| 7: the body's C3 lines | yes | the paragraph below | |
| 1: run `git -C <upstream>` from the root | **no** | see below | |
| 8: a BOM in the cache | done by the merge round `61aed8d2`, with #855's read | see the candidates | |

**NIT 3, as measured.** With `> log 2>&1`, stdout is block-buffered and stderr is not. Without the flush, `bootstrap REFUSED: pins (exit 2)` (and `bootstrap FAILED:` for a finding, as on main) printed before step (c)'s heading and its lines, and the log did not end on it. The test merges the two streams through `run_script(merge_stderr=True)` and blanks `PYTHONUNBUFFERED`, so a runner that exports it cannot hide the fault.

**NIT 4, as measured.** With no git on `PATH`, `SHAPE_UPSTREAM_PATH` naming a directory and a manifest finding, the run ends `REFUSED git-not-runnable: the clone SHAPE_UPSTREAM_PATH names ...`, exit 2, with neither `bootstrap FAILED` nor `bootstrap REFUSED`. That predates this PR (#694), so the docstring said too much, not the code. A validator killed by a signal ends `bootstrap FAILED: <name> (exit -9)`, exit 1 (`_end_of_run([("pins", -9)])` returns 1 and says exactly that), which the `(exit <code>)` wording now covers.

**NIT 1 is not taken, because it changes an exit path on Linux.** The change is `_try_git(["-C", str(Path(upstream).absolute()), "rev-parse", "HEAD"], root)`. Its point (no process starts inside the upstream directory, so a Windows `cwd` that `CreateProcess` refuses cannot be read as git's own `PermissionError`) is reasoned, and cannot be measured on Linux. I measured the Linux side: the old `_clone_tip` and the new one run through `shape_upstream_notice` over 22 values of `SHAPE_UPSTREAM_PATH` and two `PATH`s (git, and an empty directory), 44 rows:
- the values: a clone by absolute path, with a trailing slash, by relative path from the root's directory and from another, through `link/..`, a subdirectory of a clone, a directory named `-x`, a bare repository, an unborn `HEAD`, a plain directory, a file, a missing path, a name with spaces and a newline, a name with a non-UTF-8 byte, an over-long name and path, the empty string, a mode-000 directory, one under it, and a clone under a directory with no search bit;
- 43 rows are byte for byte the same (the notice, or the same refusal);
- **1 row differs:** no git on `PATH` and an upstream directory nobody may enter. The old code answers "no clone" (its `PermissionError` names the directory, which `_git_runs` passes on), so the cache speaks; the new code starts no process there, finds git missing, and refuses `git-not-runnable`, exit 2.

The brief for this round asked for NIT 1 only if every exit path stayed byte-identical, so it is left. It is two lines (and `_clone_tip` takes the root) if lane 3 or Brett Heap wants that row to refuse; the new row's answer is defensible, since the clone cannot be read either way.

**NIT 7: the C3 account, one more paragraph.** Two more sentences say "AT its pinned commit" with no condition, and both are true where they stand, so neither is changed:
- `scaffold-project.py:1957`, the scaffold's NEXT STEPS: "`make bootstrap` puts each leg on `<branch>` AT its pinned commit". It is printed once, for a person who is about to take a fresh recursive clone and run `make bootstrap` for the first time. In that clone every leg is detached AT its pin, with no operation in progress and no commit of its own, so nothing can be lost and the placement happens as said. `tests/test_push_branch_refspec.py` asserts the sentence for a `+x` branch and then runs that first bootstrap (the legs end on it at their pins), and `tests/test_scaffold_e2e.py` runs it on the default branch and checks the pin did not move.
- `templates/assembly-root/project.yaml:64`, the comment on `tracking_branch`: "The branch `scripts/bootstrap.py` places each leg on, AT its pinned commit". It names which commit the branch is placed at and says the pin stays exact (`.gitmodules` carries no `branch=`); where bootstrap leaves a leg alone is `AGENTS-shape.md`'s sentence, which R2 narrowed and which is pinned beside it.

All runs used `run9.sh` (gh stub first on `PATH`, `GH_TOKEN=bogus-l5`, empty `GH_CONFIG_DIR`), on Python 3.12.3, at `05a1ebb3` with a clean tree.

| run | files | git | result |
|---|---|---|---|
| the two touched files | `test_bootstrap_refusal_exit`, `test_bootstrap_upstream_notice` | 2.43.0 | 26 passed (17 + 9 cases) |
| core | the two, `test_bootstrap_ref_store`, `test_bootstrap_placement_texts`, `test_bootstrap_leg_moves`, `test_bootstrap_without_git`, `test_bootstrap_after_bump`, `test_bootstrap_spelling`, `test_bootstrap_branch_bytes`, and the trio (`test_repo_hygiene`, `test_agents_md_codes`, `test_cli_reference`), `test_readme_sections`, `test_pinned_copy_text` | 2.43.0 | 689 passed, 3 skipped (the reftable cases) |
| core, newer git | the seven bootstrap files | 2.47.1 | 127 passed, none skipped |
| wide 1 | 16 files that run the assembly root's bootstrap | 2.43.0 | 1100 passed, 9 skipped |
| wide 2 | `test_family`, 16 `test_adopt_*` files, `test_family_makefile_siblings`, `test_siblings_family_name` | 2.43.0 | 1795 passed |
| wide 3 | `test_setup_project_py`, `test_setup_sh`, `test_shape_doctor`, 8 more | 2.43.0 | 1259 passed, 4 skipped, 1 xfailed (the strict xfail for #735 in `test_shape_doctor.py`, not this PR's) |
| sources | the four files that read every template's source | 2.43.0 | 204 passed |
| named only | the 9 other files that name `bootstrap.py` (a path, a docstring, a pinned-file list) | 2.43.0 | 327 passed |

The lists are the ones `c2977425`'s runs used; `git grep` over `tests/` for `bootstrap.py` finds no file outside them and the nine named-only files.

**Fails on the head this round started from** (`f6f318d1`, the two touched files with the new tests copied in): 2 failed, 24 passed. The two are the NIT 3 cases. NIT 2, 5 and 6 hold behaviour `f6f318d1` already has, so they cannot fail there; they fail on the earlier bases:
- on main (`24a977c0`), both over-long cases fail with `OSError: [Errno 36] File name too long` out of `Path.is_dir`;
- on `271710ba`, the plain-clone-with-no-git case fails: `bootstrap FAILED: pins (exit 2)`, exit 1;
- the no-git-and-a-clone-named case passes on every base, by design: it holds #694's behaviour, which step (d) now describes.

One per new hunk that has behaviour, applied to `05a1ebb3`'s `bootstrap.py` in a scratch clone and run against the two touched files (26 cases). Each is caught.

| mutant | result |
|---|---|
| F1: the flush at the top of `_end_of_run` removed | 2 failed (NIT 3's two cases) |
| F1b: the flush moved after the two lines | the same 2 failed |
| X3 (lane 3's): `_clone_tip` catches `PermissionError` only | 2 failed (the two over-long cases); on Python 3.14, where `Path.is_dir` answers False, it would survive |
| N4: a refusal exits 1 | 10 failed, NIT 5's test among them; the old `!= 0` version of that test passes under N4, the new `== 2` does not |
| M4c: `_clone_tip` also catches `Refusal` | 1 failed (the no-git-and-a-clone-named case) |
| N13: the REFUSED line says FAILED | 9 failed, the plain-clone-with-no-git case among them |

The docstring hunk has no behaviour. The three things it says about the ending are held as above: REFUSED alone (the plain clone with no git), both lines with the finding first (`test_a_finding_and_a_refusal_say_both_and_exit_2`), and neither line (the no-git-and-a-clone-named case).

`git merge-tree --write-tree` from `05a1ebb3` on 2026-10-17 at about 17:50Z:
- **origin/main `42aed788`: clean.** It would be tree `1ba653a4`.
- **#824 (`fix/710-non-utf8-branch-names`, `0a5e82bf`): clean.** Both change `bootstrap.py`; the hunks do not overlap.
- **Every `fix/*`, `l3-*` and `docs/*` branch on origin besides this one (55, #824 among them): 51 clean, 4 conflict.** The four, `docs/release-notes-v1.0.0`, `fix/758-git-command-scope`, `l3-677-pins-hand-step-wip` and `l3-678-naming-checkout-wip`, each conflict with main itself, in files none of which this commit changes.

- **NIT 8: `_cached_tip` reads a cache that starts with a byte-order mark as no answer.** Measured: a `.shape-upstream-tip` of `EF BB BF` then a 40-hex commit reads back as `'\ufeff0123...'`, which is not a commit, so the notice stays silent over a cache a Windows editor wrote. `encoding="utf-8-sig"` in `_cached_tip` would accept it. Outside #764; the same decode question as #710's. **Done since by the merge round with main (`61aed8d2`):** #855 (`3059efc1`) reads the cache as `utf-8-sig`, and the merge carries that into `_cached_tip`. #855's tests in `tests/test_bom_reads.py` pin it, and a mutant back to `utf-8` fails three of them.

rv704's checklist candidates C1 to C4 on #745. Three commits after `f4935e11` (`271710ba`, `c2977425`, `05a1ebb3`), the restack's two merges (`3aa9a0fe`, `f6f318d1`), and the merge round with main (`61aed8d2`).

- `271710ba`: C1, C3 and C4, with C2 measured and put to a ruling.
- `c2977425`: C2, option B, on Brett Heap's ruling (below).

**Pinned files changed:** `templates/assembly-root/scripts/bootstrap.py` only, and its digest changes: sha256 `6a7847f3d78ab6c3...` on main, `89ff92e5e469003f...` here after the merge round `61aed8d2` (`09238ebd4202193a...` at the NIT round `05a1ebb3`, `0cb334c260493313...` before it). #764 is v1.1.1 and lands after the v1.1.0 tag, so a project taken to v1.1.0 sees `scripts/bootstrap.py` as `upstream-changed` on `update-shape.py check --root .`, and re-pins it with `update-shape.py apply`. (#682 changed the family root's `bootstrap.py`, not this one; an earlier draft of this paragraph said otherwise, and lane 3's RV815 corrected it.) #824 (#710) changes the same file, so the two should land in one release, for one row, as #764 asks. The Makefile, both roots' `AGENTS-shape.md` and the holder's README are not changed (see C3).

### C1: the upstream notice and a directory nobody may enter

With `SHAPE_UPSTREAM_PATH` set to a directory this process may not enter, `make bootstrap` ended in a `PermissionError` traceback, exit 1, after every validator had passed. There are two ways in:

- **The directory itself.** `subprocess` raises the error for the `cwd` of `git rev-parse HEAD`, naming the directory. `_git_runs` passes it on, since it is not git's (#694).
- **A directory under one.** Python 3.12's `Path.is_dir` raises it (3.13 too; 3.14 answers False).

The new `_clone_tip` catches `OSError` around both reads and returns no answer, as an absent answer does. The cache is then asked, as it is when no clone is named.

A git that cannot be STARTED is still `git-not-runnable`, exit 2. `_git_runs` raises that as a `Refusal`, which `except OSError` does not catch, and `tests/test_bootstrap_without_git.py` still passes. A mutant that also catches `Refusal` fails it.

**The same function, under the issue's goal** that `make bootstrap` never ends in a traceback over the notice. These go past C1's own line, and each is measured on `f4935e11`:

| state | base | now |
|---|---|---|
| `.shape-upstream-tip` nobody may read | `PermissionError`, exit 1 | no answer (`_cached_tip`) |
| `.shape-upstream-tip` whose bytes are not UTF-8 | `UnicodeDecodeError`, exit 1 | no answer |
| `contracts/` nobody may enter | `PermissionError` at the notice's `is_file`, after the validators' own answer | no answer |
| `contracts/shape-pin.yaml` that is a YAML list | `AttributeError`, after `validate-pins.py` refused `shape-pin-unreadable` | no answer; the run ends on that refusal |

### C4: a cherry-pick or a revert on a reftable leg

**The probe's result is on #764** (issuecomment-6237519513), as its Verify asks, and on #684 for lane 4's R612 E4 (issuecomment-6237529261).

**The probe.** This host's git (2.43.0) has no reftable, so I built git 2.47.1 from its source tag on GitHub, in my scratch directory. In a repository made by `git init --ref-format=reftable`:

- a cherry-pick stopped on a conflict and a stopped `git revert` leave NO `CHERRY_PICK_HEAD` or `REVERT_HEAD` file in `.git`;
- `git show-ref --exists` finds each in the ref store, and `git status` says the operation is in progress;
- a stopped merge leaves `MERGE_HEAD` a FILE, as on the files backend.

So the ref store is where the two live, and the issue's second branch applies.

**The defect, reproduced** with that git on `f4935e11`:

1. The scaffold is cloned with `GIT_DEFAULT_REF_FORMAT=reftable`, so the root and both legs are reftable, and bootstrapped once.
2. A cherry-pick is stopped on `other` in the spec leg.
3. Bootstrap printed `on main at <pin>` and `the leg was on other`. The cherry-pick was ENDED, and its staged README was carried onto `main`.

A revert ended the same way, and its staged removal was gone. That is #704's line 2, which R1 refuses, met again on the other backend. A merge was refused, by its file.

**The fix.** `_in_progress` now asks the leg's ref store for `CHERRY_PICK_HEAD` and `REVERT_HEAD` too (`IN_REF_STORE`, `_in_ref_store`), in the same row order. It asks with `git show-ref --exists <name>` (git 2.43 and later), which takes the exact name. The issue's `git rev-parse -q --verify CHERRY_PICK_HEAD` also resolves a branch or a tag of that name. `rev-parse --symbolic-full-name` was measured too: it answers nothing, with exit 0, when a pseudoref and a branch of the same name both exist.

A git before 2.43 refuses the option and is answered no. Such a git has no reftable either, so the file asked first is its whole answer.

The refusal now says where git holds the marker:

    [spec] spec: a cherry-pick is in progress in the leg (CHERRY_PICK_HEAD is in its ref store), so nothing moved

A marker found as a file still says `is in its git dir`, byte for byte as before.

**`family.py`.** The same probe answers #684's R612 E4 for `family.py`'s `_operation_in_progress`, which is lane 4's: on reftable the two markers are refs, not files. This PR does not touch it.

### C3: the reference row

`docs/cli.md:47` said bootstrap "places each leg on its tracking branch AT the pinned commit", with no condition.

- The generator's sentence (`scripts/render-cli-reference.py`) now carries R2's condition, word for word as the pinned `AGENTS-shape.md` says it: "only where that loses no commit, and otherwise leaves its checkout where it is, saying why".
- `docs/cli.md` is regenerated, and `render-cli-reference.py --check` passes.

**The other three are left, by choice.** The Makefile's help (`:34-35`), the holder's `AGENTS-shape.md` (`:63-65`) and its README (`:14-17`) all say "or says why not". So does `AGENTS-shape.md`'s own table of the five commands (`:91-92`). Each is true as it stands, which is why R2 left them. Changing them would give every project (the Makefile) and every holder (the other two) an `upstream-changed` row that fixes nothing.

`AGENTS.md`'s section 3 ("Say that bootstrap put each leg on its tracking branch AT the pinned commit") is true where it stands too: it is the fresh scaffold's first bootstrap.

### C2: option B, on Brett Heap's ruling

Brett Heap ruled on this line in lane 5's session at 2026-10-17T13:01:10Z: "take B for #764's C2". The first commit measured the line and left it for that ruling. The second commit, `c2977425`, is option B.

**The defect, measured on `271710ba`,** in a plain clone (no `--recurse-submodules`). `validate-pins.py` refuses `pin-submodule-uninitialized`, exit 2, and bootstrap said `bootstrap FAILED: pins (exit 2)` and exited 1, which is a finding's code. Every other refusal in the file exits 2. Any person who forgot `--recurse-submodules` meets it, not only a machine with no git (`git-not-runnable`).

**What B does:**
- A validator's exit 2 ends the run `bootstrap REFUSED: <name> (exit 2)`, exit 2.
- Any other non-zero exit is a finding, as it was: `bootstrap FAILED: <name> (exit <code>)`, exit 1. An exit 3 or 127 stays a finding.
- Where one validator finds and another refuses, both lines are said, FAILED first, and the run exits 2, since a check that could not run outranks a finding. The earlier draft of this body set this out as part of B and asked the ruling to say whether it was right, and the coordinator's note of the ruling states it as part of B. If a finding should outrank a refusal in the exit code instead, it is one branch of `_end_of_run`; `test_a_finding_and_a_refusal_say_both_and_exit_2` and two of the ending cases hold the rule as it is (mutant N5 below).

`_run_neutral_validators` now returns each failed validator's label and exit code, and the new `_end_of_run` says them and returns the exit. `VALIDATOR_REFUSED = 2` names the code. The step (d) paragraph of the module docstring says what the ending does. The only shipped file changed is `bootstrap.py`, the pinned file this PR already changes.

| | before (`271710ba`) | now (`c2977425`) |
|---|---|---|
| `scripts/bootstrap.py`, plain clone: pins refuse | `bootstrap FAILED: pins (exit 2)`, exit 1 | `bootstrap REFUSED: pins (exit 2)`, exit 2 |
| the same plus a manifest finding (mixed) | `bootstrap FAILED: manifest (exit 1), pins (exit 2)`, exit 1 | `bootstrap FAILED: manifest (exit 1)`, then `bootstrap REFUSED: pins (exit 2)`, exit 2 |
| a finding alone, in a recursive clone | `bootstrap FAILED: manifest (exit 1)`, exit 1 | unchanged |
| `make bootstrap`, plain clone | exit 2, `make: *** [Makefile:45: bootstrap] Error 1` | exit 2, `... Error 2` |
| setup-project.py step (7), which runs `bootstrap.py` directly | exit 1 | exit 2 |
| siblings.py's finding | `its own bootstrap exited 1` | `exited 2` |
| the family root's bootstrap, which runs each member's `make bootstrap` | `(make bootstrap exited 2)` | unchanged |

The first four rows are measured, on `271710ba` and on `c2977425` (the new tests). The last three are read from the code: `setup-project.py` hands bootstrap's code to `shell_status`, which keeps a non-negative code; `siblings.py` prints the code; the family root runs `make`, which exits 2 for any failing recipe.

The issue says a script testing `make bootstrap`'s exit would see a change under B. It does not: GNU make exits 2 for any failing recipe, as the Makefile's own comment (`:12-16`) says, and only `Error 1` became `Error 2`. What changes is the exit of `bootstrap.py` run directly, by setup-project.py, by a person without make, or by a script.

**Docs.** `git grep` found no sentence in `AGENTS.md`, `README.md`, `docs/handbook.html`, `templates/assembly-root/AGENTS-shape.md`, the Makefile or `docs/cli.md` that says bootstrap exits 1 for a refusal or prints `FAILED` for one. None is changed, and neither README nor the handbook is touched. `render-cli-reference.py --check` passes on this head.

All runs used `run9.sh` (gh stub first on `PATH`, `GH_TOKEN=bogus-l5`, empty `GH_CONFIG_DIR`), on Python 3.12.3.

**New: `tests/test_bootstrap_upstream_notice.py`, 7 cases in 5 tests.**
- `SHAPE_UPSTREAM_PATH` at a mode-000 directory and at a directory under one: exit 0, no traceback, `bootstrap ok`, no notice.
- The cache answers where the clone cannot be read.
- A cache nobody may read, and one that is not UTF-8.
- A `shape-pin.yaml` that is a list: no traceback, step (c) reached.
- A `contracts/` nobody may enter, asked of the notice in process, since the validators before it have their own answer to that folder.
- The mode-000 cases skip under root and on Windows.

**New: `tests/test_bootstrap_ref_store.py`, 4 cases in 2 tests.**
- A cherry-pick, a revert and a merge in progress on a reftable leg. Each asserts where git holds the marker first, then the refusal byte for byte, an unchanged snapshot, and the operation still held. The `--abort` it prints is typed from the root, and the next run places the leg.
- These skip where git makes no reftable repository (before 2.45). The CI image's git does.
- A branch called `CHERRY_PICK_HEAD` and a tag called `REVERT_HEAD`, with no operation: the leg is placed. This one runs on every git.

**New: `tests/test_bootstrap_placement_texts.py`, 2 tests.** The reference row carries R2's condition, and the pinned `AGENTS-shape.md` still words it so.

**New (C2): `tests/test_bootstrap_refusal_exit.py`, 13 cases in 5 tests.**
- A plain clone: `validate-pins.py` refuses, and the run ends `bootstrap REFUSED: pins (exit 2)`, exit 2, with no `FAILED` line. Step (c) is still reached.
- A recursive clone with a manifest finding: `bootstrap FAILED: manifest (exit 1)`, exit 1, as before. This is the control.
- A plain clone with that finding too: both lines, the finding first, exit 2.
- `make bootstrap` in a plain clone: exit 2, `REFUSED`, and make's `Error 2`, not `Error 1`.
- `_end_of_run` asked of nine mixes of codes: none, one finding, two findings, one refusal, two refusals, an exit 3, an exit 127, a refusal then a finding, and all three. Each says its lines and exits by the worst.

**Runs on head** (`c2977425`):

| run | files | git | result |
|---|---|---|---|
| core | the 4 new files, the issue's Verify list (`test_bootstrap_leg_moves`, `test_bootstrap_without_git`, `test_bootstrap_after_bump`, `test_cli_reference`, `test_readme_sections`, `test_pinned_copy_text`, `test_repo_hygiene`), `test_bootstrap_spelling`, `test_bootstrap_branch_bytes` and `test_agents_md_codes` | 2.43.0 | 682 passed, 3 skipped (the three reftable cases skip) |
| core, newer git | the 4 new files, `test_bootstrap_leg_moves`, `test_bootstrap_without_git`, `test_bootstrap_after_bump` | 2.47.1 | 121 passed |
| wide 1 | 16 files that run the assembly root's bootstrap (`test_scaffold_e2e`, `test_update_shape`, `test_update_shape_kept_helper`, `test_doctor_pin_restore_merges`, `test_doctor_legs_members_next`, `test_family_head_lost_gitmodules`, `test_family_bootstrap_gate_order`, `test_family_siblings_placement`, `test_windows_paths`, `test_stdout_encoding`, `test_template_text_placeholders`, `test_scaffold_ignored_shape_files`, `test_validate_pins_lockstep_lines`, `test_push_branch_refspec`, `test_leg_path_yaml_scalars`, `test_scaffold_manifest_values`) | 2.43.0 | 1084 passed, 9 skipped |
| wide 2 | `test_family`, 16 `test_adopt_*` files that run bootstrap, `test_family_makefile_siblings`, `test_siblings_family_name` | 2.43.0 | 1782 passed |
| wide 3 | `test_setup_project_py`, `test_setup_sh`, `test_shape_doctor`, `test_doctor_next_is_a_command`, `test_bump_leg`, `test_bump_leg_rollback`, `test_update_shape_merged_away`, `test_update_shape_add`, `test_naming_policy`, `test_park_resume_targets`, `test_scaffold_mount_ignored` | 2.43.0 | 1206 passed, 4 skipped |
| sources | the four files that read every template's source or run a holder's `bootstrap.py`: `test_git_output_decoding`, `test_shape_advisory`, `test_template_quoted_values`, `test_validate_family_branch_step` | 2.43.0 | 204 passed |

The `core` and `wide 1` files hold what the first commit's runs held; `test_bootstrap_spelling` and `test_bootstrap_branch_bytes` moved from wide 1 to core. The other test files that name `bootstrap` only name its path or its output, in comments, docstrings and expected text. The one helper that runs it, in `tests/conftest.py`, ends in `bootstrap ok`, and the files that use it are in wide 3.

**Fails on base** (`f4935e11`, the new files copied in):
- `test_bootstrap_upstream_notice.py`: all 7 cases fail.
- `test_bootstrap_placement_texts.py`: the row test fails; the guard on R2's sentence passes, by design.
- `test_bootstrap_ref_store.py` on git 2.47.1: the cherry-pick and revert cases fail. The merge case (a file on both backends) and the exact-name case pass on base, by design: they are controls.

**Fails on base for C2** (`271710ba`, `tests/test_bootstrap_refusal_exit.py` copied in): **3 failed, 1 passed, 9 errors** of the 13 cases.
- The refusal alone fails: `bootstrap FAILED: pins (exit 2)`, exit 1.
- The mixed run fails: it printed `bootstrap FAILED: manifest (exit 1), pins (exit 2)` and exited 1.
- `make bootstrap` fails: it said `Error 1`.
- The nine `_end_of_run` cases error, since the function is not there.
- The finding alone passes, by design: it is the control.

`render-cli-reference.py --check`: `[ok] docs/cli.md is what the generator produces`.

**For C1, C3 and C4 (`271710ba`),** each one breaks a single new hunk, in a scratch clone. Every one is caught:

| mutant | caught by |
|---|---|
| M1: the ref-store ask removed from `_in_progress` | reftable cherry-pick and revert (git 2.47.1) |
| M2: the ask made with `rev-parse -q --verify` | the exact-name test (git 2.43) |
| M3: the refusal says `git dir` whatever `held` is | reftable cherry-pick and revert |
| M4: `_clone_tip` catches nothing | both upstream-path cases and the cache-answers test |
| M4b: `is_dir` outside the `try` | the directory-under-one case |
| M4c: `_clone_tip` also catches `Refusal` | `test_the_offline_upstream_notice_with_no_git_refuses_too` |
| M5a / M5b: `_cached_tip` catches only `OSError` / only `UnicodeDecodeError` | the not-UTF-8 / unreadable cache case |
| M6: the pin read catches only `Refusal` | the `contracts/` test |
| M7: `pin or {}` again | the list test |
| M8: an unreadable clone answers `""`, so the cache is skipped | the cache-answers test |
| M9: the generator and `docs/cli.md` both reverted | the row test (`test_cli_reference.py` alone stays green) |

**For C2 (`c2977425`), 13 more, one per new hunk or branch of `_end_of_run`, each caught** (each is applied to the pushed file in a scratch clone and run against `tests/test_bootstrap_refusal_exit.py`):

| mutant | caught by |
|---|---|
| N1: `VALIDATOR_REFUSED = 1` | refusal, finding alone, mixed, make, 6 of the 9 ending cases (10 of 13 fail) |
| N2: the exit code dropped on the way to the ending (`failed.append((label, 1))`) | refusal, mixed, make (3 of 13 fail) |
| N3: a refusal also listed as a finding | refusal, mixed, 4 of the 9 ending cases (6 of 13 fail) |
| N4: a refusal exits 1 | refusal, mixed, make, 4 of the 9 ending cases (7 of 13 fail) |
| N5: a finding outranks a refusal in the exit code | mixed, 2 of the 9 ending cases (3 of 13 fail) |
| N6: the FAILED line dropped when something refused | mixed, 2 of the 9 ending cases (3 of 13 fail) |
| N7: the REFUSED line dropped when something found | mixed, 2 of the 9 ending cases (3 of 13 fail) |
| N8: the two lines said in the other order | mixed, 2 of the 9 ending cases (3 of 13 fail) |
| N9: only exit 1 is a finding (an exit 3 vanishes) | 3 of the 9 ending cases (3 of 13 fail) |
| N10: `bootstrap ok` not said | 1 of the 9 ending cases (1 of 13 fail) |
| N11: a finding alone exits 2 | finding alone, 4 of the 9 ending cases (5 of 13 fail) |
| N12: `main` discards the ending's code | refusal, mixed, make (3 of 13 fail) |
| N13: the refusal line says FAILED | refusal, mixed, make, 4 of the 9 ending cases (7 of 13 fail) |

Run on 2026-10-17 at about 16:00Z with `git merge-tree --write-tree`, from head `c2977425`, against origin/main `40e678e6` and every `fix/*`, `l3-*` and `docs/*` branch on origin (58 besides this one).

**origin/main: conflicts, in `templates/assembly-root/scripts/bootstrap.py` only.** That is the stack, not this change. #682 (`dc7d3fbb`) and #745 (`24a977c0`) landed as squashes, and this branch still holds their commits. `bootstrap.py` on main is byte for byte what `f4935e11` has: `git diff f4935e1 origin/main -- templates/assembly-root/scripts/bootstrap.py` is empty.

**The restack, predicted: clean.** The two commits after `f4935e11`, applied onto origin/main with `git merge-tree --write-tree --merge-base=f4935e11 origin/main c297742`, give tree `7884460e`. Against main it changes seven files: `bootstrap.py`, `docs/cli.md`, `scripts/render-cli-reference.py` and the four new test files.

That predicted head (a scratch commit object, no ref) against the 58 branches:
- It is clean with the 49 that are clean with main.
- The other 9 conflict with main in the same files, so none of them is this change's: `docs/release-notes-v1.0.0`, `fix/695-family-docs`, `fix/758-git-command-scope`, `fix/759-quoting-launcher-guards`, `fix/760-bin-dir-control-chars`, `fix/762-validate-family-remedies-texts`, `l3-677-pins-hand-step-wip`, `l3-678-naming-checkout-wip` and `l3-733-ignored-sections`.
- Six branches touch a file this PR changes, and each merges clean with it: `fix/710-non-utf8-branch-names` (`bootstrap.py`), `fix/759-quoting-launcher-guards` (`render-cli-reference.py`), `fix/796-adopt-source-spellings` and `fix/799-adopt-plan-out-checked` (`docs/cli.md`), and `l3-563-docs` and `l3-rehearsal-v1.1.0` (both).
- `render-cli-reference.py --check` passes on the predicted head and on its merge with each of `fix/710`, `fix/759`, `fix/796`, `fix/799` and `l3-563-docs`.

#710's branch now exists and edits the same `bootstrap.py`, so whichever lands second re-gates.

- The three "or says why not" pinned sentences (C3, above).
- C5 (#710's decode).

- **Step (a) and step (c) of the assembly root's bootstrap end in a traceback over a folder nobody may enter**, on Python 3.12 and 3.13.
  - `checkout_tracking_branch`'s `(sub / ".git").exists()` for a leg directory of mode 000 raises `PermissionError: ... spec/.git`. `_git_runs` passes it on, since it names a path.
  - `_register_paths`'s `is_file()` under a `governance/` of mode 000 raises one too, though step (c)'s docstring says the readout "never fails".
  - Both measured on this head. The same class as #430's `Path.is_dir` sweep, which may already hold them.
- **`validate-pins.py` with a `contracts/` nobody may enter** ends in a `PermissionError` traceback at `_load_leg_pin`'s `pin_path.is_file()`, exit 1 (measured on this head). The validators' half of the same class.
- **Two texts still say "failed" over a bootstrap that now REFUSED.** `setup-project.py` step (7) ends `bootstrap failed (see above). The clone is at <path>.`, and `siblings.py` says `<project>: its own bootstrap exited <code>; the clone is there and its legs may not be placed`. Both carry bootstrap's code (now 2 for a refusal), and the REFUSED line is printed above each. Neither is false; a person reading the last line only could take a refusal for a finding. Cosmetic, probably not worth an issue.

Review: lane 3's RV815, READY on f6f318d (issuecomment-6237425616), and its re-check of the NIT round 05a1ebb, READY with nothing left (issuecomment-6238438057). `61aed8d2` is the merge round with main after #855 (one conflict in `bootstrap.py`: `_cached_tip` kept, reading `utf-8-sig`); rv815m's independent re-check found it READY with no MUST or SHOULD (issuecomment-6247998538), and its three body NITs are fixed. CI (Brett Heap's ruling 27 and his fast-refresh ruling of 2026-10-15T22:48:35Z): H0 61aed8d passed tests, tests-git-bash and SonarCloud; the head 9997641 is a clean update-branch merge of main a249910, with tests-git-bash and SonarCloud green; and the local gate passed on its predicted squash tree fb09e18 over main a249910: the adopt_manifest_values, bootstrap_placement_texts, bootstrap_ref_store, bootstrap_refusal_exit, bootstrap_upstream_notice, github_limits_init_check_org, reader_refusals_named, scaffold_manifest_values, repo_hygiene, agents_md_codes, cli_reference sets, 498 passed, 3 skipped. 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 #764

Lane: openreposhape-5 (openRepoShape-5)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant