Skip to content

bootstrap.py: the paste words it prints quote a leading =, are made of the bytes git is handed (UTF-8 on Windows), and the NOT INITIALIZED command names a leg's path as one word after a --, with ./ before a : (#961) - #973

Merged
brettheap merged 15 commits into
mainfrom
fix/961-paste-words
Oct 20, 2026

Conversation

@brettheap

@brettheap brettheap commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor

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

#944 (#932) made each command step (a) prints for a person to paste name its leg path and its upstream as ONE shell word, through _paste_word. rv932's independent review of it (#944 issuecomment-6252637720) left a SHOULD and four NITs in that function, and #961 filed them, with a SHOULD for the one printed command #944 did not word: the NOT INITIALIZED line. This takes them all, in templates/assembly-root/scripts/bootstrap.py, tests/test_bootstrap_pasteable_lines.py, tests/test_assembly_root_unreadable_paths.py (#958's file), tests/test_bootstrap_spelling.py and the docstring of tests/test_bootstrap_after_bump.py.

The branch is restacked on main af0e60f2, which holds #958 (#430 slice D, which moved the NOT INITIALIZED line into _initialized): #958's final head, then its squash, then main (nothing newer). The squash's tree is identical to #958's final head, so the merges brought in no code of their own. Against main this PR changes those five files and no other, and git diff -U0 against main has the same changed lines as this PR's own commits on #958's head 7e87e05f (checked from objects, see "What this adds to main").

Each item, before and after

1. A leading = (SHOULD 1). _paste_word quoted with shlex.quote, which leaves =x bare, and zsh expands a bare =word to the path of a command.

  • Before, read back by /usr/bin/zsh 5.9 (printf %s <word>): =x, =ls, ==x and =% did not print themselves; =ls was /usr/bin/ls and =x was zsh: x not found. dash and bash read them all.
  • After: the word is '=x', written locally with quoted.startswith("="), as repo_shape.shell_quoted quotes it (family.py and siblings.py print commands with unquoted paths, so under a folder with a space the member-git-dir-cached remedy's rm -rf would delete the wrong directory, and add's push line fails #486). bootstrap.py imports nothing new from repo_shape.py, and a test holds the import set. A word shlex.quote quotes already (=a b) is not quoted again, and a=b stays bare.
  • The test row ("=x", "=x") is now ("=x", "'=x'"), in its own test, because the one it was in asserts "byte for byte what it was".
  • rv932's probe (every byte 0x01 to 0xff in seven places, about 160 special names, 3,194 names, each word read by dash, bash and /usr/bin/zsh under LC_ALL=C and C.UTF-8, as set -- <word>, one argument and the bytes git is handed): 450 mismatches at 450279ca, all zsh and all a leading = (225 names in each locale), and 0 at the first head, with 603 names that begin with = among those read. The 4 names that end in a line feed have no word (item 3).

2. The codec (NIT 2). _prints_as_it_is asked the stdout's codec only, but git is handed another set of bytes.

3. The round-trip guard (NIT 3). The guard os.fsdecode(raw) != name kept the escape text for sp\udcc3\udca9c, though "$(printf -- 'sp\303\251c')" reads back exactly the bytes git is handed (the directory spéc).

  • After: no name is read back. git IS handed those bytes, so a name os.fsdecode would read as another has its word. A name that ends in a line feed keeps today's text, as the brief says. (On Windows the guard was replaced by item 8.)
  • An addition inside "what can be pasted": a name with a NUL has no word either. $(...) drops a NUL, so the old word "$(printf -- 'a\000b')" printed ab in dash and bash (zsh keeps it). It keeps today's text now. It cannot be reached from a real leg path or ref, so this is a guard and a unit row, nothing more.

4. R6 and R8 (NIT 4). Two unit rows, in process:

  • R6, except UnicodeEncodeError alone: a sys.stdout stub whose encoding is no-such-codec (a LookupError, not a UnicodeEncodeError); the word is the printf word and nothing raises.
  • R8, name.encode("utf-8", "surrogateescape") for os.fsencode: a shim os whose fsencode is latin-1, and café + tab; the word is "$(printf -- 'caf\351\011')", and /bin/sh prints the shim's bytes.

5. Stale words (NIT 5). tests/test_bootstrap_after_bump.py's docstring no longer says the command is "spelled" (#391): the names are, the command is _paste_word's. The pasteable test file's docstring now says what the rule is, and which tests run what was printed and which ask the helper. The module paragraph says where the stdout cannot write the name, the leading =, the new command, and the names with no word. _prints_as_it_is and _paste_word have new docstrings.

6. The NOT INITIALIZED command (SHOULD 3). _initialized printed git submodule update --init {spelled(path)}, bare, for a leg that is not there.

  • Before, pasted from the root (rv973 measured 40 awkward paths in sh, dash, bash and zsh): my spec was two pathspecs; a path with an ESC, a U+2028, the byte 0xff, %s or a tab was its escape text, which names no path; sp'ec was a syntax error in every shell; =spec failed in zsh (zsh:1: spec not found); -spec was an option.
  • After: git submodule update --init -- <word>, the path as _paste_word words it, after a --. The leg is checked out at its pin again, in every shell and on git 2.43 and 2.47.1 (rv973: 40 of 40). The label [spec] <path>: is still spelled.
  • A path that is not a name printf can print (a lone surrogate, a NUL, a final line feed) keeps its text, as in item 3.
  • rv973 asked for a landing condition: the PR does not land without this commit.

7. A path that begins with : (rv973's NIT 1 on the staged commit). Git reads a word that begins with : as pathspec magic, even after the --, so --init -- :spec matched nothing and the leg stayed empty (all four shells, git 2.43 and 2.47.1). _initialized words ./:spec: ./ goes inside the one word, so a path that needs quoting is './:my spec' and a byte path is "$(printf -- './:sp\377ec')". Only this command has it: git -C :spec is a directory, not a pathspec.

8. Windows: the bytes are UTF-8 (rv973's NIT 1 and NIT 2). git for Windows is handed UTF-16 and stores UTF-8, and Git Bash reads a paste as UTF-8.

  • _handed_to_git(text) is os.fsencode(text) on POSIX and strict text.encode("utf-8") where os.name == "nt". _prints_as_it_is compares the stdout with it, and _paste_word makes its printf bytes from it.
  • Before, under PYTHONLEGACYWINDOWSFSENCODING=1 (where os.fsencode is the code page's): on main a UTF-8 stdout got "$(printf -- 'origin/caf\351')" for origin/café and \2005 for €5, which Git Bash holds as other names, where the quoted word was right; and a codec rule made of os.fsencode would have given a cp1252 stdout the same kind of word (it was the staged commit's case, rv973's NIT 2). A lone surrogate had a word of three junk bytes in the default mode.
  • After: the quoted word stays on a UTF-8 stdout, the printf word is UTF-8 in either mode, and a lone surrogate, which has no UTF-8 bytes, keeps its text. The Windows read-back guard os.fsdecode(raw) != name is gone, and with it the one os.name test in _paste_word. POSIX is unchanged.
  • I have no Windows host: the evidence is in-process shims, as in bootstrap.py: the merge --ff-only, --continue and --abort lines it prints word a leg's path and an upstream as a shell reads them back, so a byte that is not UTF-8 is a printf and no longer escape text that names no ref (#932) #944.

Tests

tests/test_bootstrap_pasteable_lines.py is now 128 tests (45 in #944). tests/test_assembly_root_unreadable_paths.py has 11 more rows, and tests/test_bootstrap_spelling.py's NOT INITIALIZED test asks for the word.

Mutants

Each applied alone to the head's bootstrap.py, the changed test files run with -x.

Runs

All through run9.sh (the real gh unreachable, EMAIL, GIT_SSH_COMMAND=false and GIT_CONFIG_GLOBAL=/dev/null set), at the restacked head d286b289.

git files result
2.43.0 the pasteable file, test_assembly_root_unreadable_paths.py, test_bootstrap_non_utf8_branch.py, test_bootstrap_after_bump.py, test_bootstrap_spelling.py, test_pinned_copy_text.py, test_adopt_reencoded_files.py and the trio 805 passed
2.47.1 the same 805 passed

Earlier heads of this branch also ran the other eight test_bootstrap*.py files (343 passed, 3 skipped for the reftable rows), and a predicted merge with main that added the casefold and create-twins files (662 passed).

What this adds to main

git diff origin/main HEAD touches five files: templates/assembly-root/scripts/bootstrap.py, tests/test_assembly_root_unreadable_paths.py, tests/test_bootstrap_after_bump.py, tests/test_bootstrap_pasteable_lines.py and tests/test_bootstrap_spelling.py. Their changed lines, in order, are the same as git diff -U0 7e87e05f e23137c7 gives (this PR's own commits on #958's head): 109, 54, 19, 483 and 25 lines, no difference. The three merges are: #958's final head 7c06c578 (clean), its squash af0e60f2 (two conflicts, bootstrap.py and tests/test_assembly_root_unreadable_paths.py, both taken from this branch's side, which holds #958's content plus this change; the squash's tree equals the final head's) and main (already merged).

Left out

A pinned copy: every project's bootstrap.py takes an upstream-changed row, and re-pins it.

Closes #961

🤖 Generated with Claude Code

Brett Heap and others added 3 commits October 18, 2026 15:46
…s user cannot read is named, never read as absent, so a `scripts/` nobody may search no longer SKIPs every validator into `bootstrap ok` (#430, slice D)

Slice D of lane 1's plan on #430 (issuecomment-6251069236). Each guard
in the two pinned copies asked `Path.is_file()`, `exists()` or
`os.path.lexists`. Up to Python 3.13 the first two raised a refused `stat`
out of the run, a traceback with exit 1; from 3.14 they answer False to any
`OSError`, and `lexists` always has. Measured at 5017830:

- validate-manifest.py: a `project.yaml` this user cannot reach was
  `manifest-missing`, and a declared referent's pin file in a `contracts/`
  it cannot search was the finding `naming-referent-missing`.
- bootstrap.py: such a `project.yaml` was a project that elected nothing,
  and a `scripts/` it cannot search SKIPPED all three validators, each run
  ending `bootstrap ok`, exit 0. A leg mount it cannot search was NOT
  INITIALIZED, a `governance/` it cannot search had no register, and, on
  every Python, a leg whose `sequencer/` it cannot search was put on its
  tracking branch over a cherry-pick not done.

Each file keeps a local `_is_a`, a copy of validate-pins.py's, with the
one errno set: ENOENT, ENOTDIR, EBADF and ELOOP, and Windows' 21, 123 and
1921, mean absent; any other `OSError` is named `<path relative to the
root>: cannot be read (<reason>)`, with validate-pins.py's cure, or its
OTHER_REASON for a cause that is not a permission. Nothing new is imported
from repo_shape.py (#370, #610).

- A `project.yaml`, a validator, or a declared referent's pin file is a
  refusal, exit 2: `yaml-unreadable` and, for a validator, the new
  `validator-unreadable`, raised before step (b) runs any. A `project.yaml`
  this user cannot open says the same line as one it cannot reach.
- A leg's `.git`, or an operation's marker in its git dir, is a line in
  step (a), and the leg is left as it is; a register is a line in step (c),
  and nothing is claimed from it, one that cannot be opened included.
  Neither step fails the run, as neither did; validate-pins.py refuses such
  a leg. A missing validator is still SKIPPED, a missing leg NOT
  INITIALIZED, and readable paths print what they printed.

Both files are pinned copies: each project takes one `upstream-changed` row
for each. tests/test_assembly_root_unreadable_paths.py holds every site, as
a project runs it with the mode taken away and in-process under
`refused_stat`, and the two must say the same.

Lane: openreposhape-5 (openRepoShape-5)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… commit sit beside its other new helpers, so the #896 branch merges with it cleanly (#430, slice D)

A pure move, no line changed: `_Unreadable`, `_found`, `_say_unreadable`,
`_initialized` and `_stays_for_an_operation` move from between
`_on_a_branch` and `_in_progress`, where lane 5's #896 branch
(fix/896-casefold-branch-f, 80353dd) adds `_folded_twin`, `TWIN_RISKS`
and `_say_twin`, to after `_load_manifest`. `git merge-tree` of the two
heads conflicted at that one spot, two insertions in one place; with the
move it is clean.

Lane: openreposhape-5 (openRepoShape-5)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…I one only where the stdout writes the bytes git is handed, and a character's byte escapes get their `printf` word, so zsh no longer expands a leg's path (#961)

#944 (#932) made each command step (a) prints for a person to paste name its
leg path and its upstream as ONE shell word (`_paste_word`). rv932's review
of it left five findings, and this takes them. They are all in the one
function and its helper, in the module paragraph and in three docstrings.

- A word that begins with `=` is quoted, as `repo_shape.shell_quoted` quotes
  it (#486), and written locally: bootstrap.py imports nothing new from
  repo_shape.py. zsh expands a bare `=ls` to `/usr/bin/ls` and stops a line
  at `=x` with `zsh: x not found`; 225 of 603 names that begin with `=`
  failed there, in two locales, and dash and bash read them all.
- `_prints_as_it_is` asked the stdout's codec only, but git is handed
  `os.fsencode`'s bytes. A non-ASCII name now keeps its quoted word only
  where the stdout writes those same bytes; otherwise it gets the `printf`
  word, which is ASCII. Under a latin-1 stdout the line for `origin/café`
  named the byte 0xe9, where the ref is 0xc3 0xa9.
- On POSIX the round-trip guard is gone: git IS handed `os.fsencode`'s
  bytes, so a name `os.fsdecode` reads as another (`sp` + U+DCC3 U+DCA9 +
  `c`, the directory `spéc`) has its word. The guard stays on Windows, where
  a command is not handed those bytes. A name with a NUL has no word either
  (`$(...)` drops it, so the word named another path), as one that ends in a
  line feed has none.
- Two unit rows that kill the mutants rv932 found alive: a stdout with an
  unknown codec (R6) and a latin-1 filesystem codec through an in-process
  shim (R8).
- The docstrings of `tests/test_bootstrap_after_bump.py`, of the pasteable
  lines test file and of the module paragraph say what the command holds.

Not changed: the NOT INITIALIZED line in `checkout_tracking_branch`, which
slice D (#958) moves, and `_say_twin`, `fast_forward_to_pin`, `place_at_pin`
and `create_at_pin`, which #948 edits.

A pinned copy: every project's `bootstrap.py` takes an `upstream-changed` row, and re-pins it.

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

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-5, 2026-10-18T18:47Z) @codex review

@sourcery-ai

sourcery-ai Bot commented Oct 18, 2026

Copy link
Copy Markdown

Reviewer's Guide

Updates _paste_word so bootstrap commands remain pasteable across zsh and differing stdout/filesystem encodings: leading-= words are quoted, non-ASCII text is quoted only when its emitted bytes match Git’s bytes, and safe printf fallbacks are used or avoided for edge cases such as NULs and trailing newlines. The accompanying tests exercise shell round trips, codec combinations, POSIX/Windows behavior, and the revised documentation.

Sequence diagram for encoding-aware pasteable output

sequenceDiagram
    participant Bootstrap
    participant Stdout
    participant Filesystem
    participant Git
    Bootstrap->>Bootstrap: _paste_word(name)
    Bootstrap->>Bootstrap: spelled(quoted)
    Bootstrap->>Stdout: encode(encoding)
    Bootstrap->>Filesystem: os.fsencode(name)
    Filesystem-->>Bootstrap: Git bytes
    alt ASCII or stdout bytes equal Git bytes
        Bootstrap-->>Bootstrap: Return quoted word
    else Bytes differ or stdout cannot encode
        Bootstrap->>Bootstrap: Build printf octal escapes
        Bootstrap-->>Bootstrap: Return printf word
    end
    Bootstrap->>Git: Paste command with selected word
Loading

State diagram for shell-word safety decisions

stateDiagram-v2
    [*] --> Quoted
    Quoted --> Quoted: leading =
    Quoted --> Direct: _prints_as_it_is
    Quoted --> Fallback: stdout or byte mismatch
    Direct --> [*]: return quoted
    Fallback --> Spelled: os.fsencode fails
    Fallback --> Spelled: NUL or trailing newline
    Fallback --> Spelled: Windows read-back mismatch
    Fallback --> Printf: safe byte round-trip
    Spelled --> [*]: return spelled text
    Printf --> [*]: return printf word
Loading

Flow diagram for pasteable shell word selection

flowchart TD
    A[_paste_word name] --> B[shlex.quote]
    B --> C{Leading equals sign?}
    C -->|Yes| D[Quote the word]
    C -->|No| E[_prints_as_it_is]
    D --> E
    E -->|Yes| F[Return quoted word]
    E -->|No| G[os.fsencode]
    G -->|Encoding fails| H[Return spelled text]
    G -->|Encoding succeeds| I{NUL or trailing newline?}
    I -->|Yes| H
    I -->|No| J{Windows read-back mismatch?}
    J -->|Yes| H
    J -->|No| K[Return printf word]
Loading

File-Level Changes

Change Details Files
Harden shell-word generation against zsh expansion for names beginning with =.
  • Add explicit quoting for words whose rendered form starts with = while avoiding double quoting already-quoted names.
  • Cover leading-equals behavior and shell round trips across sh, dash, bash, and zsh.
templates/assembly-root/scripts/bootstrap.py
tests/test_bootstrap_pasteable_lines.py
Make direct quoting contingent on the bytes stdout emits matching the bytes Git receives.
  • Compare encoded stdout output with os.fsencode for non-ASCII names, while retaining an ASCII fast path.
  • Fall back to an ASCII printf word when codecs differ or the stdout codec is unavailable/invalid.
templates/assembly-root/scripts/bootstrap.py
tests/test_bootstrap_pasteable_lines.py
Refine the conditions under which a name has no safe printf representation and expand byte-oriented coverage.
  • Permit POSIX paths whose surrogate escapes round-trip to the bytes Git receives.
  • Keep spelled output for trailing newlines, NULs, unencodable names, and Windows decode mismatches.
  • Add tests for NUL handling, codec shims, filesystem encodings, and printf byte output.
templates/assembly-root/scripts/bootstrap.py
tests/test_bootstrap_pasteable_lines.py
Synchronize documentation and test descriptions with the updated paste-word contract.
  • Document the leading-= zsh hazard, stdout/filesystem byte agreement, and no-word cases.
  • Clarify that command arguments use _paste_word rather than being uniformly spelled.
templates/assembly-root/scripts/bootstrap.py
tests/test_bootstrap_after_bump.py
tests/test_bootstrap_pasteable_lines.py

Assessment against linked issues

Issue Objective Addressed Explanation
#961 Quote paste words beginning with = so zsh does not expand them as command paths and the words read back as themselves across supported shells. ✅
#961 Choose a quoted non-ASCII word only when the stdout encoding writes the same bytes that git receives from the filesystem encoding; otherwise emit a printf word, and allow POSIX byte sequences that can be pasted to round-trip. ✅
#961 Print the NOT INITIALIZED submodule-update command as a pasteable shell word, and update the relevant tests and documentation. ❌ The PR explicitly leaves the NOT INITIALIZED line unchanged, pending #958; it remains unquoted and is not produced through _paste_word. The documentation and tests for the changes this PR does make are updated.

Possibly linked issues


Tips and commands

Interacting with Sourcery

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

Customizing Your Experience

Access your dashboard to:

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

Getting Help

@chatgpt-codex-connector

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

Copy link
Copy Markdown

Codex Review Summary

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

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-18T18:50:04.847901Z b2887cf Manual request
ℹ️ About Codex in GitHub

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

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

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

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. 🚀

Reviewed commit: b2887cf56b

ℹ️ About Codex in GitHub

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

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

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

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

@brettheap

Copy link
Copy Markdown
Contributor Author

Independent review of #973 (#961) at b2887cf5, for lane openRepoShape-5: READY

No MUST. The paste words are now exact in dash, bash and zsh. I read every word back in both locales and found 0 mismatches. Every output outside the categories the body names prints byte for byte what 450279ca prints. One condition stands: the PR must not land without its promised NOT INITIALIZED commit (SHOULD 1).

Findings

SHOULD 1 (landing condition). Closes #961 is false at b2887cf5.

NIT 1 (the codec rule on Windows). Under the legacy filesystem mode, the comparison checks the wrong bytes.

  • Where: bootstrap.py:777-790, written == os.fsencode(text).

  • On POSIX the rule is right, and better than comparing codec names. os.fsencode is exactly what subprocess hands git, and comparing bytes keeps the quoted word wherever two codecs agree on this name: cp1252 and latin-1 both write é as e9.

  • On Windows in default mode it equals a UTF-8 comparison, which is right: Git Bash converts the printf bytes from UTF-8. In-process shim with a cp1252 stdout: origin/café becomes "$(printf -- 'origin/caf\303\251')".

  • On Windows in legacy mode (PYTHONLEGACYWINDOWSFSENCODING=1, so os.fsencode is mbcs) the reference is wrong. git for Windows is handed UTF-16 and stores UTF-8, as _try_git already assumes there. Measured with the shim on a UTF-8 stdout:

    • origin/café becomes "$(printf -- 'origin/caf\351')" (base: 'origin/café');
    • €5 becomes "$(printf -- '\2005')" (base: '€5').

    Those are cp1252 bytes, which name another path in Git Bash, where base's quoted word was correct. Comparing against sys.getfilesystemencoding() would be wrong the same way.

  • Suggestion: when os.name == "nt", compare against text.encode("utf-8", "surrogatepass"). It is a rare configuration and the sh dialect on Windows is already out of scope, hence NIT.

NIT 2. One mutant survives.

  • O3: move os.fsencode(text) out of _prints_as_it_is's try. It survives the pasteable, non_utf8_branch and after_bump files (176 passed).
  • Effect: with a latin-1 filesystem shim and a UTF-8 stdout, head gives '日本' (no word), and O3 raises UnicodeEncodeError.
  • Reachability: none from the callers. Such a leg path fails Path.exists, and git's names are read with os.fsdecode. A unit row would still pin it.

NIT 3. The module paragraph (:50-60) lists the line-feed and NUL names as having no word, but not the names os.fsencode cannot write. _paste_word's docstring does list them.

Checked and true

  1. Each the assembly root's bootstrap.py: a paste word that begins with = is expanded by zsh, the NOT INITIALIZED line is neither quoted nor a paste word, and _paste_word asks the stdout's codec where git is handed the filesystem's bytes #961 item (check 1):

    • SHOULD 1, the leading =: fixed. quoted.startswith("=") wraps a bare word only, and a bare shlex word holds no '. The test row is now '=x', held equal to shell_quoted(name, "posix").
    • NIT 2, the codec: fixed for POSIX (Windows caveat in NIT 1).
    • NIT 3, the guard: the read-back now runs on Windows only, and sp\udcc3\udca9c gets "$(printf -- 'sp\303\251c')".
    • NIT 4, R6 and R8: now killed (item 6).
    • NIT 5, stale words: the after_bump docstring, the pasteable file's docstring and the module paragraph are updated.
  2. The word probe (check 4). 2,123 names went through head's _paste_word: every byte 0x01 to 0xff in seven places, the special names, and extras such as =, ==x, =%, =a b, a=b, origin/=x, =ls, =w, sp\udcc3\udca9c and a NUL.

  3. The NUL guard (check 3) is right and harmless.

    • Measured: base's word "$(printf -- 'a\000b')" reads back as ab in dash and bash (bash warns "ignored null byte"). zsh's builtin keeps the NUL, but an external program receives a. No shell word can hand a NUL to git.
    • It is unreachable: a NUL path fails Path.exists, refs cannot hold NUL, and _argument_fault refuses a NUL branch.
  4. bootstrap.py: a leg whose HEAD is on a branch that differs from the tracking branch only in case (Main for main) is left where it is, so on a disk that folds case branch -f main no longer moves the ref HEAD stands on (#896) #948 (check 5). On a three-way predicted merge of main 5f62ea48, bootstrap.py: a leg whose HEAD is on a branch that differs from the tracking branch only in case (Main for main) is left where it is, so on a disk that folds case branch -f main no longer moves the ref HEAD stands on (#896) #948 45415296 and bootstrap.py: the paste words it prints quote a leading =, are made of the bytes git is handed (UTF-8 on Windows), and the NOT INITIALIZED command names a leg's path as one word after a --, with ./ before a : (#961) #973 (tree 98979bfa), the merged bootstrap.py parses. _say_twin still reads f"git -C {_paste_word(path)} switch {_paste_word(branch)}". It prints git -C '=spec' switch main, git -C spec switch '=x' and git -C '=ls' switch '=w', and zsh reads each back exactly. The casefold and pasteable files pass on that tree: 129 passed.

  5. Unchanged output (check 6). I drove every caller over 2,126 names: _say_in_progress for 7 rows, and move_back_to_pin for 4 outcomes with the name as path and as upstream, 31,890 line sets in all. 31,054 are byte-identical to 450279ca. The other 836 all fall in a named category (a leading =, a NUL, escapes that os.fsdecode reads as another name); none changed otherwise.

  6. Mutants (check 7):

    Mutant Result
    w961's R6 killed by the no-such-codec row
    w961's R8 killed by 2 rows
    w961's I3a (POSIX guard kept) killed by 3 rows
    O1: = quoted anywhere in the word killed (a=b and origin/=x rows)
    O2: the ASCII short-cut joined with and killed (9 failures)
    O4: NUL guard only for a leading NUL killed (the NUL row)
    O5: the = quote after the printable check killed (11 failures)
    O3 survives (NIT 2)
  7. Base check: the new pasteable file against 450279ca's bootstrap.py: 19 failed and 78 passed, as the body says.

  8. Runs at b2887cf5 (check 8):

    • git 2.43.0: the pasteable file, test_bootstrap_non_utf8_branch.py, test_bootstrap_after_bump.py, test_pinned_copy_text.py and the trio: 414 passed, 0 skipped, in 138 s.
    • git 2.47.1: the pasteable file: 97 passed, 0 skipped.
  9. Merges (check 9): origin/main has moved to 5f62ea48, re-fetched; it is a standard.py and conftest.py change.

  10. Sonar-style checks and the pinned copy (check 10):

    • Complexity by hand: _prints_as_it_is 4, _paste_word 7.
    • Duplication: no repeated run of 4 or more lines in the pasteable file. The after_bump change is a docstring only.
    • Asserts: none added that joins conditions with and.
    • Line length: added lines are at most 78 columns.
    • Import set: the repo_shape import list is unchanged, and test_pinned_copy_text.py passes.

Could not verify

  • A real Windows host (only the in-process shims), Git Bash's MSYS argv conversion, and macOS /bin/sh.
  • git 2.55 (CI's version), and Sonar's own analyser.
  • The body's own probe numbers (3,194 names, 450 mismatches at base, 603 = names). My set differs (2,123 names, 261 = words), with the same 0-mismatch result at head.

Checklist candidates

  1. The NOT INITIALIZED line after the assembly root's validate-manifest.py and bootstrap.py: a path this user cannot read is named, never read as absent, so a scripts/ nobody may search no longer SKIPs every validator into bootstrap ok (#430, slice D) #958: git submodule update --init -- {_paste_word(path)}, with tests for my spec, =spec, -spec and sp<0xff>ec.
  2. On Windows, compare the stdout bytes with UTF-8, not os.fsencode (NIT 1).
  3. A unit row for a filesystem codec that cannot write a printable name, to kill O3.
  4. zsh rows skip where /bin/zsh is absent (CI may lack it); the quoted-word test still pins the word.

brettheap added a commit that referenced this pull request Oct 18, 2026
…racking branch only in case (`Main` for `main`) is left where it is, so on a disk that folds case `branch -f main` no longer moves the ref HEAD stands on (#896) (#948)

**Head 4541529** (fix round 1: part 1 `48222b5e`, then the merge `60df34fc` of origin/main `450279ca` (#944), then part 2), on the first head `80353dd1`, on origin/main `50178305` (the v1.1.1 release commit). origin/main is now `450279ca`: `git merge-tree --write-tree` of this head against it is clean, tree `aabc059b`. One file of `templates/` changes: `templates/assembly-root/scripts/bootstrap.py`. One new test file.

**Fix round 1, part 2 (rv896's SHOULD 1, one commit after the merge of main, `45415296`).** #944 (#932) landed as `450279ca`, so `_paste_word` is on main. `_say_twin`'s command is now `git -C {_paste_word(path)} switch {_paste_word(branch)}`: for a branch named with a byte that is not UTF-8 the old `spelled(shlex.quote(...))` form printed `switch 'a\udcffb'`, which `/bin/sh` reads as the eight characters it holds, so the pasted command named no branch. `_say_twin`'s docstring and the docstring of `test_the_way_out_is_quoted_for_a_shell_...` now say `_paste_word`, where they said the line was printed as `move_back_to_pin` printed its own before #944. A non-UTF-8 twin test runs the printed `switch` through `/bin/sh -c` as printed, and five unit rows read each word back through `/bin/sh`.

**Fix round 1, part 1 (rv896's NITs 1 to 4, one commit, `48222b5e`).** rv896 found this change READY (issuecomment-6252823490). Part 1 takes the four NITs that need nothing from #944: the `make` risk now says "the commits a branch spelled like main holds" (an unborn twin holds none); the missing-branch lines print the pin's sha, `branch main does not exist (the pin is <sha12>)`; two `_folded_twin` rows, `refs/notes/Main` and `refs/tags/xMain`, kill the one surviving mutant; the module and `_say_twin` docstrings say what MAKING a branch hides. The `switch` line was left for part 2, below, which switches it to #944's `_paste_word` now that #944 has landed.

A pinned copy: every project's `bootstrap.py` takes an `upstream-changed` row, and re-pins it.

Where HEAD's ref and `refs/heads/<branch>` are equal under `str.casefold()` but differ in bytes, bootstrap leaves the leg. No ref moves and none is made. It says where the leg is, why it is left, and the way out.

- `_folded_twin(current, branch)` says whether HEAD's ref is such a twin. A detached HEAD (`None`) and a ref outside `refs/heads/` are never twins. It compares the names after `refs/heads/`, so a name held as surrogate escapes (#704) raises nothing.
- `_say_twin(label, path, branch, current, state, act)` prints the three lines. Each of the three path functions asks the helper first, before any move, and grows by one `if`. They take the leg's `path` for the command, as `move_back_to_pin` does; `checkout_tracking_branch` passes it at its three call sites only (its `(sub / ".git").exists()` lines are untouched).
- The words hold on a disk that keeps case too, where `Main` and `main` are two branches. They never say the two are one ref: they say that a disk that folds case (macOS, Windows) reads them as one branch, and what that could do.

Before (all three said the leg was on `Main`, which keeps its commits (a branch with no commit yet, for the unborn twin below), and moved or made `main`):

    T7 (main behind the pin)   branch main fast-forwarded <old> -> <pin>, the pin; ...
                               the leg was on Main, which keeps its commits; it is on main now
    P9 (main at the pin)       on main at <pin> (branch tip == pin)
                               the leg was on Main, which keeps its commits; it is on main now
    missing branch             branch main created at <pin> - at the pin
                               the leg was on Main, which keeps its commits; it is on main now

Now (spaces and quoting as printed; the leg is `spec`):

    T7:
      [spec] spec: branch main tip <old> is behind the PIN <pin>, and the checkout is on Main, which differs from main only in case
              a disk that folds case (macOS, Windows) reads Main and main as one branch, so moving main could move the ref HEAD stands on and leave the index at the old commit; the leg is left as it is
              run `git -C spec switch main`, which moves no branch, and then run bootstrap again.
    P9:
      [spec] spec: branch main is at the pin <pin>, and the checkout is on Main, which differs from main only in case
              a disk that folds case (macOS, Windows) reads Main and main as one branch, so the leg is already on main there, and bootstrap would say it left a branch that keeps its commits; the leg is left as it is
              run `git -C spec switch main`, which moves no branch, and then run bootstrap again.
    missing branch:
      [spec] spec: branch main does not exist (the pin is <pin>), and the checkout is on Main, which differs from main only in case
              a disk that folds case (macOS, Windows) reads Main and main as one branch, so making main could hide the commits a branch spelled like main holds behind the new ref; the leg is left as it is
              not moving the checkout off it. Keep any commit made there on a branch whose name differs from main by more than case, check out the pin in the leg, and run bootstrap again.

T7 and P9 give `git -C <leg> switch <branch>`, the path and the branch each one `_paste_word` (#932), as `move_back_to_pin`'s path and upstream are: a name that prints as it is stays `shlex.quote`d, and one with a byte that is not UTF-8 is a `printf` that prints the bytes. On a disk that folds case it re-spells HEAD and moves no file, and the next run takes T7's on-branch `merge --ff-only` path or P9's no-op checkout. Where the two are branches, it puts the leg on the real `main`, and `Main` keeps its commits. Every other state prints what it printed: HEAD on `refs/heads/main` itself, on `feature`, on `mains`, detached, on a ref outside `refs/heads/`.

CI is Linux only, so a disk that folds case is emulated, as the issue's own probe did: a symlink `.git/refs/heads/Main -> main`, which git's files backend reads through, and HEAD on `refs/heads/Main`. T7 is the issue's state: `main` loose and behind the pin, the pin's commit adding `NEW-CONTRACT.md` and editing `README.md`.

At `50178305`:

    before: HEAD -> refs/heads/Main | Main = main = 536671ad3925 | pin = f27b6a92a433 | status ''
    bootstrap: branch main fast-forwarded 536671ad3925 -> f27b6a92a433, the pin; origin/main is 536671ad3925, which the pin deliberately does NOT follow
               the leg was on Main, which keeps its commits; it is on main now
    after:  HEAD -> refs/heads/main | HEAD = main = f27b6a92a433 | status 'D  NEW-CONTRACT.md;M  README.md'

At this head (a second scaffold, so other shas):

    before: HEAD -> refs/heads/Main | Main = main = bf6b7be3d5fa | pin = 19051708a6c9 | status ''
    bootstrap: the three T7 lines above
    after:  HEAD -> refs/heads/Main | Main = main = bf6b7be3d5fa | status ''

P9 on the base printed `on main at <pin>` and the `the leg was on Main` line; at this head it prints the three P9 lines, and HEAD, `main` and the status are as before. The way out, `git -C spec switch main` run as printed, puts HEAD on `refs/heads/main`, moves no branch, and the next run prints `fast-forwarded` (T7) or `on main at <pin> (branch tip == pin)` (P9), with `git status --short` empty.

At `50178305`, git 2.43.0 and 2.47.1, the same result. `checkout -q --no-overwrite-ignore -b main <pin>`, from HEAD on `Main`, `main` not there:

- **A packed twin with commits** (`git pack-refs --all`, so no loose `Main`, no `main`). Before: HEAD on `Main`, HEAD = `Main` = the twin's commit, `main` missing, status clean. The checkout exits 0 and writes the loose file `main`. On a disk that folds case that file is the file `Main` reads, so `Main` now names the pin, and the twin's commit is in no branch and no ref (`git branch -a --contains`, `git for-each-ref --contains`: nothing); only HEAD's reflog lists it. **This is the harm.** The emulation makes the link after the checkout: a link made before a packed `Main` is a DANGLING link, git reads `Main` as missing through it (it falls back to packed-refs only when `lstat` says ENOENT, and a folding disk would say that), HEAD reads as unborn, and the staged `g.txt` is left. That before-state is not a folding disk's, so the probe has no link before the checkout, and the committed packed test has none at all: a real `Main` with a commit, packed, is the state of a disk that keeps case, and the lines are the same on both.
- **An unborn twin** (HEAD on `Main` with no commit, the dangling link `Main -> main`, which a folding disk reads as one missing ref). The checkout moves HEAD to `main` at the pin and the index is consistent. Nothing is hidden: the twin holds no commit. **It is harmless.** It is refused all the same: one rule, one helper, and no state of HEAD to branch on; `head is not None` in the missing-branch path would exempt it, if the coordinator prefers that.

The words follow the packed probe: `switch main` is no way out of a leg with no `main` on a disk that keeps case, so the line says to keep the commits on a branch whose name differs from `main` by more than case, check out the pin (whose sha the first line prints), and run bootstrap again. The risk says "the commits a branch spelled like main holds", which is true of a packed twin, of an unborn twin (it holds none, so nothing is hidden) and of a third spelling beside either (rv896's probe: an unborn `Main` beside a packed `MAIN` hides that commit the same way). The committed packed test runs the way out: `git branch kept-from-Main`, `git checkout --detach <the printed sha>`, bootstrap prints `branch main created at <pin> - at the pin`, `kept-from-Main` and `Main` both hold the commit.

`tests/test_bootstrap_casefold_twin.py`, 32 tests. The leg set-up is `test_bootstrap_after_bump.py`'s fixtures, `bumped` and `fast_forwarded`, and `test_bootstrap_leg_moves.py`'s `typed` and `main_of`; the helper `bootstrap_module` is `test_bootstrap_branch_bytes.py`'s. Every test uses the module's own local bare remotes. Each symlink is `os.symlink`, and the test skips on Windows, and where it raises `OSError` (a disk that folds case refuses the link: `Main` is `main`), with the reason. A test that makes a real `Main` skips on a disk that folds case. Every assert on the output compares step (a)'s spec-leg lines exactly.

1. T7 under the emulation: `main` unchanged, HEAD still `refs/heads/Main`, `git status --short` empty, the three lines exact.
2. The way out runs as printed, then bootstrap again: `fast-forwarded`, HEAD `refs/heads/main` at the pin, status empty.
3. P9 under the emulation, the lines exact, and its way out.
4. The missing branch: an unborn twin under the emulation, and a packed twin with a commit, with the way out run.
5. A disk that keeps case: a real loose `Main` with a commit of its own and HEAD on it, T7's state. The same lines. The way out puts the leg on the real `main`, bootstrap fast-forwards it, `Main` keeps its commit.
6. Unchanged byte for byte: HEAD on `main` itself, on `feature` and on `mains` (3 tests).
7. `_folded_twin`'s table (15 rows): `Main`, `MAIN`, the twin the other way, the branch itself, `Main` against `Main`, detached, `refs/remotes/origin/main`, another branch, a longer name, `refs/Heads/main`, `refs/notes/Main` and `refs/tags/xMain` (each 11 characters before `Main`, as `refs/heads/` is), a final sigma against a sigma (`casefold` folds it, `lower()` does not), and a name that is not UTF-8, itself and as a twin. Plus three tests of `_say_twin` itself: the command is shell-quoted as before for an ordinary pair, a name that is not UTF-8 or holds an ESC is spelled (output is ASCII, no raise), and (part 2) five rows read the two words of the command back through `/bin/sh` and compare them with the bytes of the names: an ordinary pair, a path with the byte, a branch with the byte, both with a leading dash, and a path with a percent.
8. (Part 2) A tracking branch `a<0xff>b` and HEAD on its twin `A<0xff>b` (T7, `--branch`, POSIX, on a disk that takes the byte; skipped on Windows, and on macOS, whose disk refuses the byte): the lines name both spelled, the printed command is `git -C spec switch "$(printf -- 'a\377b')"`, run through `/bin/sh -c` as printed it puts HEAD on `refs/heads/a<0xff>b` byte for byte, and the next run fast-forwards the branch.

**On the base (`50178305`, the first 26 tests copied in): 23 failed, 3 passed.** The three that pass are case 6, by design. Part 2's new test cannot run on the base (it imports `test_bootstrap_pasteable_lines.py`, which #944 added); on the merged tree before the part 2 change it failed, at the command: `- git -C spec switch "$(printf -- 'a\377b')"` expected, `+ git -C spec switch 'a\udcffb'` printed. **At this head: 32 passed**, on git 2.43.0 and 2.47.1.

| mutant | result |
|---|---|
| compare byte for byte again (`_folded_twin` never true) | 11 failed |
| the check dropped from T7 / from P9 / from `create_at_pin` | 3 / 1 / 2 failed |
| `casefold()` -> `lower()` | 1 failed (the sigma row). Equivalent for ASCII; the sigma row tells them apart, so no `ss` pair was needed |
| the `None` guard dropped (`None.startswith`) | `make bootstrap`'s first run raises in the fixture: 9 errors and 1 failed |
| a twin with `current` `None` (returns true for it) | the same 9 errors and 1 failed |
| `switch` -> `checkout` in the printed way out | 4 failed (the exact-lines tests). Case 2, which only runs the printed command, still passes: `git checkout main` does the same here |
| the `refs/heads/` guard dropped (whole refs compared) | 1 failed (`refs/Heads/main`) |
| `held != branch` dropped (the branch itself a twin) | 7 failed |
| the path / the branch not shell-quoted | 1 failed each |
| the twin / the branch not spelled | 1 failed each |
| the missing-branch path printing the `switch` command | 3 failed |
| the missing-branch way out saying `a branch` for `a branch whose name differs ... by more than case` | 3 failed |
| **R2** (round 1): the `refs/heads/` guard dropped, the 11-character slice kept | **24 passed on the first 24 tests: it survived. 2 failed on the 26 (`refs/notes/Main`, `refs/tags/xMain`): killed.** |
| round 1's re-runs on the 26: the check dropped from `create_at_pin`; `casefold()` -> `lower()` | 2 failed; 1 failed |
| **part 2:** `_paste_word` dropped from the path / from the branch / the old `spelled(shlex.quote(...))` form restored | 3 / 3 / 5 failed of the 32 (the byte-path rows; the end-to-end test and the byte-branch rows; all of them but the ordinary pair). None survives |

The host's load was 85 to 270, so the coordinator scoped them (one pytest process at a time, 15:53Z and 16:15Z).

- **git 2.43.0**, `tests/test_bootstrap_casefold_twin.py` and every `tests/test_bootstrap*.py` (11 files, the issue's `test_bootstrap_after_bump.py`, `test_bootstrap_leg_moves.py` and `test_bootstrap_non_utf8_branch.py` among them), `test_family_add_spelling_leftovers.py`, `test_pinned_copy_text.py` and the trio (`test_repo_hygiene.py`, `test_agents_md_codes.py`, `test_cli_reference.py`): **751 passed, 3 skipped** (the three skip `test_bootstrap_ref_store.py:102`: 2.43 makes no reftable repository) in 14:47. Then `test_bom_reads.py` and `test_validator_child_encoding.py`: **188 passed**.
- **git 2.47.1**, the new file, `test_bootstrap_leg_moves.py`, `test_bootstrap_after_bump.py` and the trio: **248 passed** in 3:40.
- **Round 1, part 2, at the merged tree** (the load was about 50, two processes at a time): git 2.43.0, the new file, `test_bootstrap_pasteable_lines.py`, `test_bootstrap_non_utf8_branch.py`, `test_bootstrap_leg_moves.py`, `test_bootstrap_after_bump.py` and the trio: **329 passed** in 7:04. git 2.47.1, the new file and `test_bootstrap_pasteable_lines.py`: **72 passed** in 2:09. Both ran before the five unit rows were added (a change to the test file alone); the new file was then run again on both: **32 passed** each.
- **Round 1, at the part 1 tree** (the load was about 40, two processes at a time): git 2.43.0, the new file, `test_bootstrap_leg_moves.py`, `test_bootstrap_after_bump.py` and the trio: **250 passed** in 3:44. git 2.47.1, the new file: **26 passed** in 20 s.
- **The predicted merge with origin/main `64464d31`** (a commit of tree `d7d8ca11`, never pushed): the new file, the trio and `test_release_workflow.py`: **231 passed** (git 2.43.0).
- A first run of 115 files in 3 shards (git 2.43.0) was stopped at 35%, 28% and 45% when the load reached 270: about 3,100 tests, none failed. It is not counted.

**Left to CI's `tests` job, which runs the whole suite on the head:** every file not named above. Of the files `git grep -l -E 'assembly-root/scripts/bootstrap|make bootstrap|bootstrap\.py' -- tests` names, these 66 were not run: test_adopt_attributes_eol_block.py, test_adopt_clone_template.py, test_adopt_e2e.py, test_adopt_ignored_shape_files.py, test_adopt_mount_ignored.py, test_adopt_nested_attributes.py, test_adopt_pins.py, test_adopt_plan_yaml_quoting.py, test_adopt_reencoded_files.py, test_adopt_relative_submodule_url.py, test_adopt_root_manifest.py, test_adopt_root_symlinks.py, test_adopt_shape_pin_and_work_dir.py, test_adopt_submodules.py, test_adopt_verify.py, test_bump_leg.py, test_bump_leg_dry_run_truth.py, test_bump_leg_rollback.py, test_bump_leg_unreadable.py, test_doctor_legs_members_next.py, test_doctor_machine_row.py, test_doctor_next_is_a_command.py, test_doctor_paste_through_git.py, test_doctor_pin_restore_merges.py, test_family.py, test_family_bootstrap_gate_order.py, test_family_bump_remove_rollback.py, test_family_gitmodules_values.py, test_family_head_lost_gitmodules.py, test_family_holder_bootstrap_texts.py, test_family_makefile_siblings.py, test_family_members_not_a_list.py, test_family_siblings_placement.py, test_leg_path_yaml_scalars.py, test_legs_readers_not_a_list.py, test_load_yaml_refusals.py, test_naming_policy.py, test_non_utf8_name_helpers.py, test_park_resume_targets.py, test_push_branch_refspec.py, test_scaffold_e2e.py, test_scaffold_ignored_shape_files.py, test_scaffold_leg_path_collisions.py, test_scaffold_manifest_values.py, test_scaffold_mount_ignored.py, test_setup_by_hand_commands.py, test_setup_project_py.py, test_setup_sh.py, test_shape_doctor.py, test_siblings_family_name.py, test_siblings_inputs.py, test_template_text_placeholders.py, test_update_shape.py, test_update_shape_add.py, test_update_shape_apply_lines_and_headers.py, test_update_shape_kept_helper.py, test_update_shape_landing.py, test_update_shape_merged_away.py, test_update_shape_pr_base.py, test_validate_family_branch_step.py, test_validate_family_member_path_glob.py, test_validate_family_remedies_typed.py, test_validate_family_typed_states.py, test_validate_pins_lockstep_lines.py, test_validate_workflow_gitmodules_urls.py, test_windows_paths.py. On git 2.47.1 the 2.43 list's other files were not run either.

- origin/main `450279ca` (#944 landed): this head merges clean, tree `aabc059b`. The merge commit `60df34fc` itself is a clean merge, tree `664513ff`, as predicted.
- Slice D's head `0ef875c0` (#958): clean, tree `af0a54b7`.
- The other lanes' open branches, `fix/415-legs-members-next-tests`, `fix/430-a-bump-leg-unsearchable`, `fix/879-holder-remedies-compose`, `fix/880-check-plan-name-docstring`, `fix/884-ignored-depth`, `fix/885-one-line-install-release-notes`, `fix/897-kept-copy-reraise` and `fix/898-bump-leg-reachability`: none touches `bootstrap.py` or this test file, and each merges clean with this head. `l3-677-pins-hand-step-wip` and `l3-678-naming-checkout-wip` conflict, in `shape-doctor.py`, `validate-pins.py` and tests this change does not touch, and conflict the same way with origin/main itself.
- Slice D's earlier local head, `df1bb186`, conflicted with the first head (both added a block after `_on_a_branch`); it was moved at `0ef875c0`, which merges clean.

- **Docs.** `bootstrap.py`'s module docstring (step (a)) and the docstrings of the three path functions and `_move_and_check_out` say the rule. I grepped `AGENTS.md`, `README.md`, `docs/`, the assembly root's `AGENTS-shape.md` and README, and the family root's, for a sentence about step (a)'s paths. None is false: each says a leg is placed `only where that loses no commit, and otherwise ... left where it is, saying why`, or `or says why not`, and a twin leg is left, saying why. Nothing else was edited.
- **Cognitive complexity (S3776, 15 allowed), counted by hand and with an AST counter that follows Sonar's rules** (each `if`, `elif`, `else`, `for`, `except` and ternary, plus nesting; one per run of one boolean operator): `place_at_pin` 5 -> 6, **`fast_forward_to_pin` 14 -> 15, at the limit**, `create_at_pin` 12 -> 13, `_folded_twin` 3, `_say_twin` 1, `checkout_tracking_branch` 13 (unchanged). If Sonar counts one more in `fast_forward_to_pin`, the cheapest cure is a helper for its `note` (two points). Round 1 changes only words, so none changes. The commit message of 80353dd says `14, 6 and 12`; that was my hand count before the counter found the `why or []` argument in `fast_forward_to_pin` and `create_at_pin`. The numbers here are right, and the second commit's message says so; the squash message should not repeat the first commit's sentence.
- **Width.** Every new line is within 79 columns in both files. No duplicated block; no `assert` joins conditions with `and`.

- **One rule for all three paths,** the unborn twin included (see the probe). rv896 ruled to keep refusing it and not to add `head is not None`: an unborn twin can stand beside a packed third spelling whose commit `checkout -b` hides, and the refusal costs one step (`git -C spec checkout --detach <pin>`, then bootstrap).
- **`casefold()` can over-match a real disk:** `ss` and the sharp s fold under it, which a disk's own simple folding may not do. The leg is then left, with true words, where the disk would not need it.
- **The rule is by name, so a reftable leg is left too,** where `Main` and `main` are two refs on any disk. The way out is true there as well.
- **NFC and NFD twins are out of scope** (below).

1. **An NFC/NFD twin does the same harm, and is not caught.** APFS also reads a name's NFC and NFD forms as one file. Emulated as the issue's probe was (a symlink from the NFD name to the NFC branch `caf` + U+00E9, HEAD on the NFD name, `--branch` the NFC name, git 2.43.0), at `50178305` AND at this head: `branch caf<e9> fast-forwarded <old> -> <pin>`, `the leg was on cafe<U+0301>, which keeps its commits; it is on caf<e9> now`, status `D  NEW-CONTRACT.md` and `M  README.md`. `casefold()` does not normalize; `unicodedata.normalize("NFC", ...)` in `_folded_twin` before the compare is the one-line cure, with a test.
2. **`bump-leg.py` and `family.py` compare the root's or the holder's branch with `tracking_branch` byte for byte** (`branch == project.tracking`, `branch != family.tracking()`): on a disk that folds case a root on `Main` is on `main`, and the pull-request-only refusal (`bump-leg-on-tracking-branch`, `family-on-tracking-branch`) is not given. Reasoned from the code; not probed.
3. **A HEAD on a ref whose directory is spelled otherwise** (`git symbolic-ref HEAD refs/Heads/main`) is a twin on a disk that folds case, and is not one here: the rule leaves refs outside `refs/heads/` alone, as the brief says. Reasoned; very rare.
4. **From rv896, for later:** a follow-up for `create_at_pin`: the same lost-commit harm happens from a HEAD detached at the pin over a packed `Main` and no `main`, which the rule does not see (it asks only about HEAD's ref); before `checkout -b`, ask `git for-each-ref refs/heads/` for any name that casefolds to the branch, packed or loose.

Review: rv896, lane 5's independent reviewer, not the writer (w896): READY at `80353dd1` (issuecomment-6252823490), and its re-check of fix round 1 READY at `45415296`, the head that lands (issuecomment-6253509985), with no MUST and no SHOULD. Its one NIT, a leading `=`, is #961's, open as #973. CI (Brett Heap's ruling 27): H0 is the head 4541529 itself, which passed tests, tests-git-bash and SonarCloud; main has touched none of its files since its base 450279c; and the local gate passed on its predicted squash tree 67a22e1 over main 3db58c4: the bootstrap_casefold_twin, repo_hygiene, agents_md_codes, cli_reference sets, 202 passed. Landed on Brett Heap's word of 2026-10-18T14:45:28Z, relayed to every lane by lane 3 (#473 issuecomment-6251255596): "merge v1.2.0 PRs once reviewed and green".

Closes #896

Lane: openreposhape-5 (openRepoShape-5)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brett Heap and others added 2 commits October 18, 2026 20:06
Lane: openreposhape-5 (openRepoShape-5)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ot read is named relative to the root with the remedy, each validator is opened before any runs, and step (c) names a register under an absolute leg path whole and heads one whose stat was refused `register, if one is there` (#958 fix round 1, #430 slice D)

On lane 4's RV958 review of #958 at 0ef875c:

- M1: step (c)'s header goes through `_named`, so a register under a
  leg whose `path:` is absolute is named whole, where `relative_to`
  ended the run in a `ValueError` traceback, readable or not.
- N1: a register whose `stat` was refused is headed `register, if one
  is there:` (`_a_register` now returns the label), so the line does
  not say that one is there.
- S1: `NamingPolicy.load` goes through `_load`, so a policy in a
  `contracts/` this user cannot search is `yaml-unreadable` naming
  `contracts/repository-naming.yaml`, with the remedy, not the
  absolute path alone.
- N3: `_unreadable_file` spells the path it names (`_shown`, a copy of
  validate-pins.py's).
- S2: `_a_validator` opens each validator after its `stat`, before any
  runs, as bump-leg.py's `_validators_readable` does (#934); one at
  mode 000 is `validator-unreadable` by name, not the child's own
  `can't open file` line.
- S3 and N5: tests for `_is_a`'s `ValueError` clause in both files, for
  the spelling in `_named` on both of its branches, for the I/O-error
  cause at every site that reads it, for a directory where a file goes,
  and for the out-of-root naming in both files.

Lane: openreposhape-5 (openRepoShape-5)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Oct 19, 2026
…eads/` branch, packed or loose, folds to it, and a twin folds in NFC first, so an NFD name moves no leg, and the way out renames the twin, HEAD's twin included (#962) (#984)

Follows #948 (#896), which landed as `ebbd9fa4`.

**Head `f1cceddf`**: `414e9e0a` (round 0, on #948's head `45415296`), `5a435acd` (the merge of origin/main `ebbd9fa4`, #948's squash) and `f1cceddf` (fix round 1, on rv984's review of `5a435acd`). The squash is not an ancestor of this branch, so the merge conflicted in `bootstrap.py` in seven hunks, and this branch's side was taken whole: main's `bootstrap.py` (blob `e31385cd`) is `45415296`'s byte for byte, so nothing of main's was dropped. Against `ebbd9fa4` the branch is three files, `templates/assembly-root/scripts/bootstrap.py`, `tests/test_bootstrap_casefold_twin.py` (three tests changed) and the new `tests/test_bootstrap_create_twins.py` (+737, -68). `git merge-tree --write-tree` of the head against origin/main `c4dd19dc` is clean. One file of `templates/` changes.

A pinned copy: every project's `bootstrap.py` takes an `upstream-changed` row, and re-pins it.

#948 leaves a leg alone when HEAD's own ref is a TWIN of the tracking branch, a branch that folds to it on a disk that folds case (`Main` for `main`). Two things were not covered:

1. **Only HEAD's ref was asked.** `create_at_pin` asked about HEAD's ref, so a leg DETACHED AT THE PIN over a packed `Main`, and a leg on an unborn `Main` beside a packed `MAIN`, ran `checkout -b main <pin>` with no refusal. On a disk that folds case that writes the loose file `Main` and `main` are both read from. What such a disk then does to the packed `Main` is reasoned from git's files backend (rv984), and not measured on APFS or NTFS: the packed entry stays, `for-each-ref` goes on listing it at its old sha while `rev-parse Main` reads the new file `main`, and a later `git branch -D Main` unlinks that file. The commit is still in a ref; its name is shadowed, and the state is fragile.
2. **The fold was `casefold()` alone.** APFS also reads the NFC and the NFD spelling of a name as one file, so a leg on the NFD spelling of `caf` and U+00E9 is a twin that `_folded_twin` did not see: bootstrap fast-forwarded the NFC branch under HEAD's feet and the index was left at the old commit (`D NEW-CONTRACT.md;M README.md`), the same harm as #896's.

`create_at_pin` asks `git for-each-ref refs/heads/` (packed refs are listed too) for a branch that is a twin of the tracking branch (`_folded_spelling`), just before the `checkout -b` and after every refusal that was already there. If one is found, `_say_spelling` leaves the leg and prints three lines: where the leg is and the pin's sha, why it is left, and the way out. Nothing is moved and nothing is made.

- **EVERY twin is a reason to leave**, whether or not a tag, a remote-tracking ref, another branch, the stash or the pin holds its commits (fix round 1, below).
- A twin is only ever left in place, never moved (#948's rule, kept), and every output that is not a twin's is byte for byte what it was. `fast_forward_to_pin` is not changed in complexity (15, as it was).

`_fold(name)` is `unicodedata.normalize("NFC", name).casefold()`, used by `_folded_twin` and so by all three step (a) paths, and by item 1. A name held as surrogate escapes (#704) passes through `normalize` as the text it is and raises nothing (a test). NFKC is not used: APFS does not fold compatibility forms, and `main` with U+00B2 stays no twin of `main2` (two rows, and the NFKC mutant is killed).

`_twin_words` words the refusal by what the twin differs in. A twin that is one under `casefold()` alone prints #896's words byte for byte (`only in case`; `a disk that folds case (macOS, Windows)`). Any other says it differs `only in case or Unicode form`, and that `a disk that folds case or Unicode forms (macOS folds both, Windows folds case)` reads the two as one, because Windows folds case only and the words must not say it folds Unicode forms.

**This replaces round 0's `_kept_elsewhere`, a departure from the brief that rv984 found breaks #962's Goal, and lane 5 ruled to follow the Goal.** Round 0 spared a twin whose commit was an ancestor of the pin, or was held by another branch, a tag, a remote-tracking ref or the stash, and its way out kept the commits on another branch and ran again. rv984 measured that in each case that made `main`, a folding disk would read `Main` as the pin: the Goal says bootstrap never makes, with `checkout -b`, a ref that a folding disk reads as a branch that already exists, and these cases broke its letter. The loop the sparing avoided is real only for a way out that leaves the twin where it is: #948's keep line ("keep any commit ... check out the pin") leaves `Main` in place, so the next run refuses again. A way out that REMOVES the twin ends it.

- **`_kept_elsewhere` and `_spelling_at_risk` are gone.** `_folded_spelling(sub, branch)` returns the first `refs/heads/` ref, packed or loose, that folds to the branch, whatever else holds its commits. `_heads(sub)` is the shared listing.
- **The way out is a rename:** ``git -C <leg> branch -m <twin> <name>``, the path and both names through `_paste_word`. The twin disappears, its commits stay on the renamed branch, and the next run makes `main`.
- **The line can be pasted as printed.** `_kept_name(sub, twin)` chooses a name that is not there: `<twin>-kept`, then `-kept-2` and on, the first that no branch of the leg folds to (so `Main-kept` is taken when `main-kept` is there, as on a folding disk they are one file). It is longer than the twin, so it differs from the tracking branch by more than case or Unicode form.
- **#948's HEAD-twin keep line is amended the same way.** A leg ON a twin with `main` missing goes through `_say_spelling` with `where` = `the checkout is on`: its first two lines are byte for byte #948's, and the third is the rename. `git branch -m` moves HEAD with the rename of the branch HEAD is on, an unborn branch included (probed on git 2.43.0 and 2.47.1, with and without the dangling link the emulation uses), so the next run takes the ordinary `the leg was on <name>, which keeps its commits` path. `_say_twin` keeps T7 and P9, whose `switch` way out is unchanged.
- **`_say_keep`** is back to the one-parameter text main had before #948: no twin line uses its second parameter now, and no non-twin line changes.

**What it costs:** several twins are named one at a time, each rename one run. A name is chosen when the line prints; if something makes it first, git refuses the rename and the person picks another. The cost of the old sparing, a shadowed name on a folding disk, is gone with it.

At `5a435acd` and at `f1cceddf`. A packed `Main` holding a commit, `main` missing:

    detached over a packed Main, at 5a435ac:
      [spec] spec: branch main does not exist (the pin is <pin>), and the leg has the branch Main, which differs from main only in case
              ... the leg is left as it is
              keep the commits Main holds on a branch whose name differs from main by more than case: `git -C spec branch <another name> Main`, and run bootstrap again.
    at f1ccedd, the third line is:
              rename Main to a name that differs from main by more than case, its commits staying on it: `git -C spec branch -m Main Main-kept`, and run bootstrap again.
      run 2 after the pasted line: [spec] spec: branch main created at <pin> - at the pin
      after: HEAD -> refs/heads/main, heads: Main-kept (the commit), main (the pin)

    the same, the commit held by a tag (rv984's SHOULD 1 case):
      at 5a435ac: [spec] spec: branch main created at <pin> - at the pin, with Main left in packed-refs
      at f1ccedd: the refusal above, and `Main-kept` after the pasted rename

    HEAD on the packed Main (#948's keep line):
      at 5a435ac: "not moving the checkout off it. Keep any commit made there on a branch whose name differs from main by more than case, check out the pin in the leg, and run bootstrap again."
      at f1ccedd: the rename line, then HEAD -> refs/heads/Main-kept; run 2: branch main created at <pin>, "the leg was on Main-kept, which keeps its commits; it is on main now"

    HEAD on an unborn Main beside a packed MAIN:
      at f1ccedd: run 1 names Main (rename Main Main-kept; HEAD goes with it); run 2 names MAIN (rename MAIN MAIN-kept); run 3 makes main, "the leg was on Main-kept, a branch with no commit yet"

The NFD twin of an NFC tracking branch, T7, is as in round 0: at `45415296` bootstrap fast-forwarded `caf` and U+00E9 under HEAD (`D NEW-CONTRACT.md;M README.md`); at this head the leg is left, `switch` is printed, and nothing moves.

`tests/test_bootstrap_create_twins.py`, 31 tests. It uses #948's emulation and helpers (`test_bootstrap_casefold_twin`), `main_of` (`test_bootstrap_leg_moves`), `head_bytes` (`test_bootstrap_non_utf8_branch`) and `pasted` (`test_bootstrap_pasteable_lines`); the lines are compared exactly, and every way out is run as printed through `/bin/sh`.

1. A leg detached at the pin over a real spelling `Main`, packed and loose (2): the lines exact, nothing moved, the rename run as printed, the next run makes `main`, and the commit is on `Main-kept`.
2. A leg ON a packed `Main` with `main` missing: the rename moves HEAD, and the next run makes `main` and says the leg was on `Main-kept`.
3. An unborn `Main` beside a packed `MAIN`: each twin named in turn and renamed, the third run makes `main`.
4. Two twins, `MAIN` and `Main`: the second rename is `Main-kept-2`, not `Main-kept`, which differs from `MAIN-kept` only in case.
5. A twin is a reason to leave however its commit is held (5 rows: the pin alone, another branch, a tag, a remote-tracking ref, the stash).
6. `mains` is no twin: `main` is made, as it was.
7. The NFD T7 test, and a missing branch beside its NFD spelling, left and renamed.
8. `_kept_name` (4 rows, and the NFC/NFD one), and a shell reads each word of the rename as the bytes of its names (4 rows, a byte that is not UTF-8 and a dash-led path included).
9. `_folded_twin` over Unicode forms (8 rows, with `main` and U+00B2 against `main2`, and U+2460 against `1`, expecting False), and the fold raises nothing on a name that is not UTF-8.

**At `5a435acd`, with these test files copied in: 23 failed, 40 passed.** 20 of the 31 tests of the new file fail there (cases 1 to 5, the NFD missing-branch test, the five `_kept_name` tests and the four shell rows) and the three changed tests of the casefold file fail (the unborn twin, the packed twin, the non-UTF-8 lines). The 11 that pass there pass by design: `mains`, the NFD T7 test, the eight `_folded_twin` rows (the two NFKC rows guard a mutant, not a base fault) and the fold test. At this head: 63 passed.

| mutant | failed |
|---|---|
| the check dropped from `create_at_pin` (item 1) | 10 |
| `_fold`: `casefold()` alone, no NFC | 6 |
| `_twin_words`: always the case words | 2 |
| `_say_spelling` names the branch, not the twin | 14 |
| `_fold`: NFKC in place of NFC (rv984's O1) | 2 (the two new rows) |
| the printed `branch -m` is a plain `branch` (a copy, not a rename) | 17 |
| the rename's two arguments swapped | 17 |
| `_kept_name` never loops (always `-kept`) | 4 |
| `_kept_name` compares byte for byte, with no fold | 3 |
| `_kept_name` folds the taken names but not the candidate | 3 |
| the HEAD-twin line says `the leg has the branch` for `the checkout is on` | 4 |
| the HEAD-twin check dropped from `create_at_pin` | 4 |
| the sparing put back (a twin another ref holds is left alone) | 5 |
| the new name not `_paste_word`ed (`spelled(shlex.quote(...))`) | 2 |
| the twin not `_paste_word`ed | 2 |
| `_heads` lists loose refs only | 13 |

Round 0's four mutants of `_kept_elsewhere` (the ancestor rule, the holders, heads-only holders, the first twin) are gone with the code.

Cognitive complexity (S3776, counted from the ast): `create_at_pin` 14, `fast_forward_to_pin` 15 (unchanged), `place_at_pin` 6, `_folded_spelling` 3, `_folded_twin` 3, `_heads` 1, `_kept_name` 1, `_twin_words` 1, `_say_spelling` 0, `_say_twin` 0. New lines are within 79 columns and ASCII; the docstrings spell the Unicode forms (`cafe` and U+0301, `caf` and U+00E9). `_paste_word` (#973 edits it) and the NOT INITIALIZED line (#958 moves it) are not touched.

At the tree of `f1cceddf` (the head), git 2.43.0: the new file, `test_bootstrap_casefold_twin.py`, `test_bootstrap_pasteable_lines.py`, `test_bootstrap_leg_moves.py`, `test_repo_hygiene.py`, `test_agents_md_codes.py`, `test_cli_reference.py` and `test_pinned_copy_text.py`, **392 passed**. git 2.47.1: the new file and the casefold file, **63 passed**. Output that is not a twin's: a differential of 27 states (the tracking branch missing, at the pin, behind it, ahead of it, by HEAD on `main`, detached at the pin, detached elsewhere, on `feature`, on an unborn `feature`, on a ref outside `refs/heads/`, on `mains`), the leg and the printed lines read after each run: byte for byte the same at `ebbd9fa4` and at this head. The rest of the suite is left to CI's `tests` job.

`git merge-tree --write-tree` of this head against: origin/main `c4dd19dc` clean; `fix/961-paste-words` (#973, `b2887cf5`) clean; `fix/430-d-assembly-root-guards` (#958, `0ef875c0`) and slice D's round 1 (`wip/l5-430d-r1-20261018`, `b49f34e1`) one conflict each, the import block of `bootstrap.py` (`import unicodedata` here against `from collections.abc import Callable, Iterator` there). The resolution keeps `import unicodedata` and slice D's `from collections.abc import Callable, Iterator`, and does not keep both `collections.abc` lines, which would be a duplicate import (rv984's NIT 2). Round 0's scratch merges of those two (95 and 222 passed) were not re-run for round 1, whose changes lie below the import block.

- A real APFS or NTFS disk is not available in CI: the shadowing a folding disk does to a packed twin is reasoned from git's files backend, not measured. The tests emulate the fold with a symlink, and run the real names where the disk keeps case.
- Several twins are named one at a time, and each rename takes one run.

Review: rv984b, lane 5's independent reviewer, not the writer (w962): READY at `f1cceddf`, the head that lands (issuecomment-6266764236), with no MUST or SHOULD. Lane 5's ruling on round 0's SHOULD 1 is carried out as ruled; its two NITs (a `-kept` name with a branch stored under it, and a test row for a twin with a slash) go to a follow-up. CI (Brett Heap's ruling 27): H0 is the head f1ccedd itself, which passed tests, tests-git-bash and SonarCloud; main has touched none of its files since its base ebbd9fa; and the local gate passed on its predicted squash tree daeb11e over main c7a3ff1: the bootstrap_casefold_twin, bootstrap_create_twins, repo_hygiene, agents_md_codes, cli_reference sets, 233 passed. Landed on Brett Heap's word of 2026-10-18T14:45:28Z, relayed to every lane by lane 3 (#473 issuecomment-6251255596): "merge v1.2.0 PRs once reviewed and green".

Closes #962

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

Two resolutions, both mechanical:

- templates/assembly-root/scripts/bootstrap.py, the one conflict: the
  import block. Every module either side imports is kept, once: #984's
  `import unicodedata`, and this branch's ONE
  `from collections.abc import Callable, Iterator`; main's
  `from collections.abc import Iterator` is dropped, since this branch's
  line imports `Iterator` too. With this branch's `import errno` and
  `import stat`, the block is main's in alphabetical order; no name is
  imported twice and none is lost.
- templates/assembly-root/scripts/validate-manifest.py, merged without a
  conflict marker but defining `_shown` twice: this branch's
  `_shown(typed)` is deleted and #981's `_shown(path)` is kept. Both of
  this branch's calls pass a `str`, which #981's `str(path)` leaves as it
  is, so each prints the same bytes.

Lane: openreposhape-5 (openRepoShape-5)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brett Heap and others added 2 commits October 19, 2026 20:41
No conflict: main's commits since 2a78ae1 touch none of #958's files.

Lane: openreposhape-5 (openRepoShape-5)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… under `*.py working-tree-encoding=UTF-16`, which refuses every pinned copy whatever its size, not UTF-16LE-BOM, which refuses only an odd byte count (#958)

CI's `tests` job failed at cf8d566 (run 39268984740) on
`test_check_finds_what_the_split_would_store_otherwise[copies-refused-by-an-encoding]`:
`check` said the source's rules store the 5 pinned shape copies as other
bytes, where the case expected git to refuse them.

Git refuses `working-tree-encoding=UTF-16LE-BOM` only for a file of an odd
byte count; an even one it decodes as UTF-16LE and stores as other bytes
(measured with git 2.43.0 and 2.47.1). Main's copies include two of odd
size, `validate-manifest.py` and `bootstrap.py`; this branch made both even,
so all five are even and the case drew `check`'s other finding. `check`
is right both ways; the case's expectation turned on the copies' sizes.

`UTF-16` with no byte order requires a BOM, which no pinned copy has, so
git refuses every copy at any size: the case now pins that refusal and
passes on main's copies and on this branch's.

Lane: openreposhape-5 (openRepoShape-5)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Oct 19, 2026
…branch stored under it, `Main-kept` over `Main-kept/x`, which git refuses, so the pasted line works and the next run makes the tracking branch (#1000) (#1006)

Follow-up of #962 (landed as #984, `2a78ae1a`): the two NITs of rv984b's READY re-check (#984 issuecomment-6266764236), filed as #1000. One commit, `9b8c643f`, on `2a78ae1a`. Two files change: `templates/assembly-root/scripts/bootstrap.py` and `tests/test_bootstrap_create_twins.py` (+177, -10).

A pinned copy: every project's `bootstrap.py` takes an `upstream-changed` row, and re-pins it. No digest of the template is kept anywhere, and #984 handled it the same way.

The line step (a) prints to rename a twin, `git -C <leg> branch -m <twin> <name>`, can be pasted as it is. `_kept_name`'s docstring says so (#962), and #984 chose the concrete `<twin>-kept` name to keep that promise. It broke when a branch is stored UNDER the chosen name.

`_kept_name` skipped `<twin>-kept` only when a branch of the leg FOLDED to it. With the leg detached at the pin, `Main` packed and holding a commit, and a branch `Main-kept/x`, the line was `git -C spec branch -m Main Main-kept`, which git refuses: exit 128, `error: 'refs/heads/Main-kept/x' exists; cannot create 'refs/heads/Main-kept'`, `fatal: branch rename failed`. The next run printed the same line again, so the person had to find a name alone. On a disk that folds case, `main-kept/x` blocks `Main-kept` the same way.

- **`_taken(held, name)`** (new, `bootstrap.py`): the name is taken when a held branch, folded, is `_fold(name)` or starts with `_fold(name) + "/"`. `_kept_name` loops `while _taken(held, name)`: `<twin>-kept`, then `-kept-2` and on. `Main-kept-2` is printed, the pasted line works, and the next run makes the tracking branch.
- **The docstrings** of `_taken` and `_kept_name` state the rule, why a branch that is a directory PREFIX of the name needs no check, and what the helper cannot see (below).
- **Item 2 (#1000): a test row for a twin whose name has a slash.** `--branch team/main` with a packed `Team/main`, detached over it and ON it. rv984b's mutant O3 (`_folded_spelling` returning `ref.rsplit("/", 1)[-1]` instead of `ref[len("refs/heads/"):]`) survived 63 of 63 tests; the rows kill it, and kill the same cut at the HEAD-twin site (O3b).
- **13 tests added**, the file is 44 tests: seven `_kept_name` rows (a branch under the name; under it in another case; deeper under it; under each of the first two; a name that only starts with it is no bar; a branch under another name that holds it is no bar; a twin with a slash), two Unicode rows (NFD under an NFC name, NFC under an NFD name), two end-to-end rows (`Main-kept/x` loose and packed: `Main-kept-2` printed, pasted through `/bin/sh`, the next run makes `main`) and the two slash rows. Helpers: `missing` and `created` take a branch, and `twin_with_a_commit`. Cognitive complexity: `_taken` 1, `_kept_name` 1, `create_at_pin` 14 and `fast_forward_to_pin` 15 (both unchanged). New lines are within 79 columns, and the new `bootstrap.py` lines are ASCII.

Probed on git 2.43.0 and 2.47.1 in throw-away repositories (`gp/gitfacts*.sh`):

- **Handled, exit 128:** a branch stored under the name, loose or packed, at any depth, for a twin with a slash too, and with the reftable backend.
- **Not refused, so not skipped:** a name that only starts with the candidate (`Main-kept-x`), a tag, a remote-tracking ref or the stash of that name, and an unborn HEAD twin, whose rename only rewrites HEAD and checks nothing.
- **A branch that is a directory prefix of the name** (`team` for `Team/main-kept`) is not handled, on purpose: the name has every directory its twin has, and the twin folds to the tracking branch, so git will not make that prefix beside the twin (`cannot lock ref 'refs/heads/Team'`), and a held `team` stops `checkout -b team/main` itself, packed or loose. No `-kept` name helps.
- **Not handled, and said in `_kept_name`'s docstring:** a stale reflog directory under the name, which git refuses (`directory not empty`, exit 128), `for-each-ref` does not list, and `branch -D` removes with the branch, so only a ref file removed by hand leaves one; and a twin whose last path component is 246 bytes or more, whose `-kept` name is refused at the file-name limit.

A real repository from the test scaffold, local bare remotes, the printed line pasted through `/bin/sh`. The leg is detached at the pin, `Main` packed with a commit, `Main-kept/x` loose or packed, `main` missing.

    at 2a78ae1:
      run 1 prints  rename Main to a name that differs from main by more than case, its commits staying on it: `git -C spec branch -m Main Main-kept`, and run bootstrap again.
      pasted        exit 128  error: 'refs/heads/Main-kept/x' exists; cannot create 'refs/heads/Main-kept' / fatal: branch rename failed
      runs 2 and 3 print the same line again; heads stay Main, Main-kept/x.
    with this change:
      run 1 prints  ... `git -C spec branch -m Main Main-kept-2`, and run bootstrap again.
      pasted        exit 0
      run 2 prints  [spec] spec: branch main created at <pin> - at the pin
      heads after   Main-kept-2 (the commit), Main-kept/x, main (the pin)

    HEAD unborn on Main, Main-kept/x held:
      at 2a78ae1   prints Main-kept and the rename works (exit 0, HEAD rewritten); run 2 makes main.
      with this     prints Main-kept-2, also works; run 2 makes main, "the leg was on Main-kept-2, a branch with no commit yet".

The final test file copied onto `2a78ae1a`: **9 failed, 35 passed (44 tests).** The 9: the five `_kept_name` rows with a branch under the name (under it; under it in another case; deeper; under each of the first two; a twin with a slash), the two Unicode rows and the two end-to-end rows. The 4 new tests that pass there pass by design: the two no-bar rows, and the two slash rows, which pin code that was already right and fail only for O3 and O3b.

| mutant | failed |
|---|---|
| O3: `_folded_spelling` returns `ref.rsplit("/", 1)[-1]` | 1 (the slash row, detached) |
| O3b: the same cut at the HEAD-twin site | 1 (the slash row, on the twin) |
| the new check removed (`return folded in held`) | 9 |
| the `"/"` dropped (a plain `startswith(folded)`) | 1 (a name that only starts with it) |
| `folded + "/" in h` for `startswith` | 1 (a branch under another name that holds it) |
| the check on the unfolded name | 8 |
| `all` for `any` (guarded against an empty set; unguarded it hangs) | 9 |
| the equality dropped | 4 |
| the immediate parent only | 1 (deeper) |
| the first path component only | 1 (a twin with a slash) |
| the name `casefold`ed, not `_fold`ed | 1 (NFC under, NFD name) |
| the held names not folded | 9 |
| the loop dropped (`while False`) | 13 |

At the committed head `9b8c643f`, through `run9.sh` (gh stub first on PATH, `GH_TOKEN=bogus`, an empty `GH_CONFIG_DIR`, `GIT_SSH_COMMAND=false`, `GIT_CONFIG_GLOBAL=/dev/null`):

- **git 2.43.0** (`/usr/bin/git`): the create-twins, casefold, `test_bootstrap_pasteable_lines.py`, `test_bootstrap_leg_moves.py` and `test_pinned_copy_text.py` files and the trio (`test_repo_hygiene.py`, `test_agents_md_codes.py`, `test_cli_reference.py`): **405 passed**.
- **git 2.47.1** (exactly `$Q/v115/w764/git247/bin/git`, first on PATH): the create-twins and casefold files: **76 passed**.
- The rest of the suite is left to CI's `tests` job.

`git merge-tree --write-tree` of the head against: origin/main `04923f10` clean (tree `b2486824`); `fix/430-d-assembly-root-guards` (#958, `cf8d5667`) clean (tree `12cb36fb`); `fix/961-paste-words` (#973, `b2887cf5`) clean (tree `de0bb093`). Main has moved on since `2a78ae1a` and has not touched `bootstrap.py` or the twin tests, so the branch needs no merge and no new run.

Lane: openreposhape-5 (openRepoShape-5)

Review: rv984b, lane 5's independent reviewer, not the writer (w1000): READY at `9b8c643f`, the head that lands (issuecomment-6276993923), with no MUST or SHOULD. Its three NITs are words only (two in `_taken`'s docstring, one in the body). CI (Brett Heap's ruling 27): H0 is the head 9b8c643 itself, which passed tests, tests-git-bash and SonarCloud; main has touched none of its files since its base 2a78ae1; and the local gate passed on its predicted squash tree 3d2f109 over main 46cf924: the bootstrap_create_twins, repo_hygiene, agents_md_codes, cli_reference sets, 214 passed. Landed on Brett Heap's word of 2026-10-18T14:45:28Z, relayed to every lane by lane 3 (#473 issuecomment-6251255596): "merge v1.2.0 PRs once reviewed and green".

Closes #1000

Lane: openreposhape-5 (openRepoShape-5)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap and others added 2 commits October 19, 2026 18:37
…l word after a `--`, and the Windows stdout check asks the UTF-8 git is handed, so `--init my spec` is not two pathspecs and `=spec` is not expanded by zsh (#961)

#973 (#961) left one line of rv932's list for #958 (#430 slice D), which
moves it into `_initialized`: `git submodule update --init {spelled(path)}`,
the command the NOT INITIALIZED line gives a person to paste. This commit is
that line, on #958's head (7e87e05), and takes rv973's three NITs.

- The path is `_paste_word(path)`, after a `--`. `my spec` was two
  pathspecs, `=spec` was expanded by zsh, a byte that is not UTF-8 was its
  escape text, which names no path, and `-spec` was read as an option.
- `_handed_to_git` is the bytes the stdout is compared with: `os.fsencode`'s
  on POSIX, UTF-8 on Windows, where git is handed UTF-16 and stores UTF-8.
  Under PYTHONLEGACYWINDOWSFSENCODING=1 `os.fsencode` is the code page's, and
  a UTF-8 stdout got the printf word of those bytes for `origin/caf` +
  e-acute, where the quoted word was right (rv973's NIT 1).
- The module paragraph lists the names `os.fsencode` cannot write among those
  with no word (NIT 3), and the new command among the ones it prints.
- A unit row pins a name the filesystem encoding cannot write, which kills
  the mutant that asks the comparison outside its `try` (NIT 2).
- Tests: `not_initialized` in `tests/test_assembly_root_unreadable_paths.py`
  takes the word, and six rows run bootstrap with a path that needs quoting.
  `tests/test_bootstrap_spelling.py`'s NOT INITIALIZED row asks for the word.
  The pasteable file empties a leg (`git submodule deinit`) and pastes the
  printed command into a shell.

A pinned copy: every project's `bootstrap.py` takes an `upstream-changed` row, and re-pins it.

Lane: openreposhape-5 (openRepoShape-5)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…t begins with `:`, and on Windows the `printf` word is made of UTF-8, so `--init -- :spec` names the leg and a legacy code page prints no other path (#961)

rv973's re-check of 2a2bc8f (#961 staged on #958) found it READY with four
NITs, and this takes them.

- NIT 1. git reads a word that begins with `:` as pathspec magic, even after
  the `--`, so `--init -- :spec` matched nothing and the leg stayed empty,
  in every shell and on git 2.43 and 2.47.1. `_initialized` words `./:spec`
  instead: `./` goes before the path, and the whole is the one word.
- NIT 2. `_paste_word` made its bytes with `os.fsencode`, which under
  PYTHONLEGACYWINDOWSFSENCODING=1 is the code page's, so a cp1252 stdout got
  the `printf` word of `\351` for `origin/caf` + e-acute: another path to Git
  Bash, which reads a paste as UTF-8. It takes the bytes from
  `_handed_to_git` now, which is strict UTF-8 on Windows, and the read-back
  of `os.fsdecode` is gone: a lone surrogate has no UTF-8 bytes, so it keeps
  its text, as the read-back kept it.
- NIT 3. A path with a space at either end gets a row, which kills
  `_paste_word(path.strip())`. The old composite assert in the spelling
  test is two asserts.

Tests: the pasteable file pastes `:spec` through sh, dash, bash and zsh and
re-initializes the leg, and checks the Windows words; the unreadable-paths
file has rows for a `:`, for a space at either end, and for `./` inside the
one word.

A pinned copy: every project's `bootstrap.py` takes an `upstream-changed` row, and re-pins it.

Lane: openreposhape-5 (openRepoShape-5)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Oct 20, 2026
…s user cannot read is named, never read as absent, so a `scripts/` nobody may search no longer SKIPs every validator into `bootstrap ok` (#430, slice D) (#958)

# Title

the assembly root's validate-manifest.py and bootstrap.py: a path this user cannot read is named, never read as absent, so a `scripts/` nobody may search no longer SKIPs every validator into `bootstrap ok` (#430, slice D)

# Body

Part of #430 (slice D of lane 1's plan, #430 issuecomment-6251069236). A v1.2.0 DRAFT.

Head `cf8d5667`, on `50178305` (the v1.1.1 release commit): the change (`df1bb186`); a pure move in `bootstrap.py` (`0ef875c0`) that clears a merge conflict with lane 5's #896 branch; the merge of origin/main `ebbd9fa4`, with #896's twin refusals (#948) in it (`bb58c441`, clean); fix round 1 on lane 4's RV958 review (`b420171f`); and the merge of origin/main `2a78ae1a`, with #984 (#962) in it (`cf8d5667`), which keeps #981's `_shown` in `validate-manifest.py` and one `collections.abc` import line in `bootstrap.py`. Files: `templates/assembly-root/scripts/validate-manifest.py`, `templates/assembly-root/scripts/bootstrap.py` and a new `tests/test_assembly_root_unreadable_paths.py`.

Every guard below asked `Path.is_file()`, `Path.exists()` or `os.path.lexists`. Up to Python 3.13 the first two raise a refused `stat` (`PermissionError`) out of the run: a traceback, exit 1, which is a finding's code. From 3.14 they answer False to any `OSError`, and `os.path.lexists` always has. So a path this user cannot read was taken for one that is not there.

Measured at `50178305` in a scaffold's recursive clone (local bare remotes, a stub `gh`), on Python 3.12.3 with the mode taken away, and under a `sitecustomize` shim that answers as 3.14's `is_file`, `is_dir`, `exists` and `is_symlink` do. A `project.yaml` or a pin file is reached through a symlink into a folder nobody may enter, since no tool finds a root it cannot search.

| Cell | 3.12 | 3.14-like | Now, on both |
|---|---|---|---|
| validate-manifest.py, `project.yaml` behind such a folder | traceback, exit 1 | `REFUSED manifest-missing`, exit 2 | `REFUSED yaml-unreadable: project.yaml: cannot be read (Permission denied)` and its remedy, exit 2 |
| validate-manifest.py, a declared referent's pin file behind such a folder | traceback, exit 1 | `FINDING naming-referent-missing`, exit 1 | `REFUSED yaml-unreadable: contracts/<pin>: cannot be read (Permission denied)`, exit 2 |
| bootstrap.py, `project.yaml` behind such a folder | traceback, exit 1 | no leg placed, no validator run, `bootstrap ok`, exit 0 | `REFUSED yaml-unreadable: project.yaml: ...`, exit 2, before step (a) |
| bootstrap.py run with `--root`, `scripts/` at 000 | traceback in step (b), exit 1 | all three validators `SKIPPED`, `bootstrap ok`, exit 0 | `REFUSED validator-unreadable: scripts/validate-repository-naming.py: cannot be read (Permission denied)`, exit 2, before any validator runs |
| bootstrap.py, `governance/` at 000 | traceback in step (c), exit 1 | `authority is not wallet-carried in this org`, exit 0 | step (c) heads it `register, if one is there:`, names it as `cannot be read`, with the cure, and claims nothing from it; exit 0 |
| bootstrap.py, `spec/` at 000 | traceback in step (a), exit 1 | `[spec] spec: NOT INITIALIZED`, then pins refuses, exit 2 | `[spec] spec: spec/.git: cannot be read (Permission denied), so the leg is left as it is`, with the cure; pins refuses; exit 2 |
| bootstrap.py, the spec leg's `sequencer/` at 000 over a `todo` | the leg is put on `main`, exit 0 | the same | `.git/modules/spec/sequencer/todo: cannot be read (Permission denied), so the leg is left as it is`; the leg stays detached; exit 0 |
| validate-manifest.py, `contracts/` at 000, no `--policy` (RV958's S1) | `REFUSED yaml-unreadable: <ROOT>/contracts/repository-naming.yaml: cannot be read (Permission denied)`, no remedy, exit 2 | the same | `REFUSED yaml-unreadable: contracts/repository-naming.yaml: cannot be read (Permission denied)` and its remedy, exit 2 |
| bootstrap.py, `scripts/validate-pins.py` at 000 (RV958's S2) | two validators run, then `/usr/bin/python3: can't open file '<ROOT>/scripts/validate-pins.py': [Errno 13] Permission denied`, `bootstrap REFUSED: pins (exit 2)` | the same | `REFUSED validator-unreadable: scripts/validate-pins.py: cannot be read (Permission denied)` and its remedy, exit 2, before any validator runs |
| bootstrap.py, a register under a leg whose `path:` is absolute, in a folder at 000 or readable (RV958's M1) | `ValueError: ... is not in the subpath of ...` in step (c), exit 1 | the same | the register named whole; refused: `register, if one is there:` and nothing claimed; readable: its rows; exit 0 |

The last three rows were measured at `00a6f839` (this branch's first merge of main) by lane 4's RV958 review and again by this branch; the first seven at `50178305`. The `sequencer/` row is wrong on every Python: `bootstrap.py` moved a leg over a cherry-pick that was not done, and the pick's `--continue` then goes on over the tracking branch (#704's harm). Each readable control printed the same at base and at head, byte for byte, with shas and paths made equal.

| Site at `50178305` | Class | What changed |
|---|---|---|
| validate-manifest.py `:1085`, `main`, `manifest_path.is_file()` | OPEN-FALSE | FIXED. `_is_file` refuses `yaml-unreadable`, naming `project.yaml`. One this user cannot OPEN (mode 000, `load_yaml` at `:1092`) says the same line now; it was `<absolute path>: cannot be read (Permission denied)` with no remedy. |
| validate-manifest.py `:406`, `_naming_referent_missing_pin_finding`, `pin_path.is_file()` | OPEN-FALSE | FIXED. A refusal, `yaml-unreadable`, naming the pin file, exit 2; `main`'s `try` now covers `_findings`. |
| bootstrap.py `:1409`, `_run_neutral_validators`, the validator skip | OPEN-SKIP | FIXED. Every validator is looked for before any runs. One whose `stat` is refused is `validator-unreadable`, exit 2; one that is not there is still `SKIPPED`. |
| bootstrap.py `:1328`, `_load_root_and_manifest`, `manifest_path.is_file()` | OPEN-FALSE | FIXED. `yaml-unreadable`, naming `project.yaml`, exit 2, before step (a). One this user cannot open says the same line. |
| bootstrap.py `:1191`, `_register_paths`, `c.is_file()` | OPEN-FALSE | FIXED. A register whose `stat` is refused is kept, and step (c) names it `cannot be read` with the cure. A register this user cannot open is named the same way, relative to the root, where its absolute path was printed. |
| bootstrap.py `:1056`, `checkout_tracking_branch`, `(sub / ".git").exists()` | OPEN-FALSE | FIXED. Step (a) names the leg's `.git` and leaves the leg as it is (`_initialized`). |
| bootstrap.py `:717`, `_in_progress`, `os.path.lexists(gitdir / marker)` | OPEN-FALSE | FIXED. A marker whose `lstat` is refused is named, and the leg is left as it is (`_stays_for_an_operation`). |
| bootstrap.py `:750`, `_sequence_in_progress`, `os.path.lexists(todo)` | OPEN-FALSE | FIXED. The same. |
| validate-manifest.py `main`, `NamingPolicy.load(policy_path)` (round 1, RV958's S1) | OPEN-FALSE (named, no remedy) | FIXED. Loaded through `_load`, so an `OSError` cause is `_unreadable_file`'s refusal, named relative to the root, with the remedy. A missing policy, which was the absolute path alone, now says the same line with `OTHER_REASON`. |
| bootstrap.py `_run_neutral_validators`, a validator it can `stat` and cannot open (round 1, RV958's S2) | OPEN-SKIP's twin (the child's own words) | FIXED. `_a_validator` opens each after its `stat`, before any runs, as #934's `_validators_readable` does. |
| bootstrap.py `_report_one_register`, `relative_to(root)` (round 1, RV958's M1) | a traceback for an absolute leg path | FIXED. The header goes through `_named`, whole where the path is not under the root. |

Left as BENIGN: the upstream notice's three reads (`:1123`, `:1159` and `:1180`), each an absent answer by #764's ruling and the last the tool's own cache; `:753`, where a `todo` that cannot be read is taken for a cherry-pick and the leg is still left; and each file's `Path(__file__).resolve()`. Neither file has an OPEN-DEST site.

- **One rule.** Each file keeps a local `_is_a`, a copy of `validate-pins.py`'s, with the one errno set: ENOENT, ENOTDIR, EBADF and ELOOP, and Windows' 21, 123 and 1921, mean absent. Any other `OSError` is named `<path relative to the root>: cannot be read (<strerror>)`. The cure is `validate-pins.py`'s (`chmod u+r` on a file, `chmod u+rx` on a directory and every directory above it, or fix who owns it), or its `OTHER_REASON` for a cause that is not a permission. Nothing new is imported from `repo_shape.py` (#370, #610), and `scripts/repo_shape.py` is untouched. bootstrap.py's `_is_a` takes `link=True` for the two `lexists` sites, which ask with `lstat`.
- **Refusals.** `yaml-unreadable`, the id `load_yaml` and `validate-pins.py` already give such a file, for `project.yaml` in both files and for the referent's pin file. One new id, `validator-unreadable`, for a validator bootstrap cannot reach or open. Each remedy says what the file is read for, then the cure, then `Then run the same command again.`
- **A referent's pin file is a refusal, not a finding.** The check cannot run, so the run is refused, exit 2, as `validate-pins.py` refuses the same file.
- **Steps (a) and (c) still never fail the run.** A leg or a register this user cannot read is a line in its step, and the step goes on. In a normal run `validate-pins.py` refuses such a leg (`pin-submodule-unreadable`), so the run ends `bootstrap REFUSED: pins (exit 2)`. Under `--skip-validators` the run ends `bootstrap ok`, as it did for a leg NOT INITIALIZED.
- **A register whose `stat` is refused is kept, and not said to be there.** Whether one is there cannot be told, so step (c) does not say the degrade line, and heads it `register, if one is there: <leg>/governance/review-authority/register.yaml` (RV958's N1). A register whose `stat` works and whose open is refused keeps `register:`.
- **The open as well as the stat.** A validator is opened, and closed unread, before any runs (RV958's S2). A `project.yaml`, the naming policy (RV958's S1) or a register this user cannot open is read off the refusal's `__cause__`, as `validate-pins.py`'s `_load` reads it (#712), so the two causes say one line. Every other `load_yaml` refusal keeps its words; `tests/test_manifest_validator.py`'s not-UTF-8 case, which pins the absolute path, passes.
- **Every path is spelled.** bootstrap.py names each through `spelled` (#391); validate-manifest.py's `_unreadable_file` through a local `_shown`, validate-pins.py's (RV958's N3). A path not under the root (a leg's absolute `path:`, a `--policy` given elsewhere) is named whole.
- **Readable paths print what they printed**, which lane 1's slice A (`fix/430-a-bump-leg-unsearchable`) relies on when it runs `validate-manifest.py`.

Both files are pinned copies (`COPIED_VERBATIM`), so each project takes one `upstream-changed` row for each. No `--help` text changes, so `docs/cli.md` is unchanged. `AGENTS.md`, `templates/assembly-root/AGENTS-shape.md`, the template README and README's degrade rule stay true.

`tests/test_assembly_root_unreadable_paths.py`, 71 tests (46 in round 0, 25 added in round 1), on `conftest.scaffolded` through the `project` fixture.

- Each refusal is asked twice and must say the same, exactly, stdout and stderr: as a project runs the tool, with the mode taken away and put back in a `finally` (`mode_of`; skipped on Windows and as root), and in-process under `refused_stat` from `tests/test_bump_leg_unopenable.py`, which answers as 3.14 does on any Python. `failing` from `tests/test_validate_pins_named_and_spelled.py` gives an I/O error from a `stat`, an open or a read, whose advice is `OTHER_REASON`, and a refused `lstat`.
- Round 1 adds: the naming policy (mode 000 with no `--policy`; an I/O error; a `--policy` outside the root whose name holds an ESC); a validator at mode 000 and one whose open fails; a register under an absolute leg path, readable and refused; a leg in a folder at 000 whose name holds an ESC, inside and outside the root; a leg path holding a NUL; the relative branch of validate-manifest.py's spelling; and, for the survivors RV958 named, an I/O-error row at every site that reads the cause, a directory where `project.yaml`, a validator or a register goes, and anything at a leg's name for `_found`.
- The controls: a referent's pin file that is a dangling link (ENOENT) or a link to itself (ELOOP) is still `naming-referent-missing`; a validator that is not there, or a directory in its place, is still `SKIPPED`; a directory where `project.yaml` goes is still `manifest-missing` to the gate and no manifest to `bootstrap.py`; a directory where a register goes is no register.
- The errno set: each file's `_is_a` answers False to the seven absent causes and to a name no file can have (a NUL, a lone surrogate), and raises EACCES, EPERM, EIO and ENAMETOOLONG.

**At origin/main** (`ebbd9fa4`; both scripts as main has them; the new file kept): 64 of the 71 do not pass (38 fail, and 26 error at setup because main has no `_is_a`). The 7 that pass there by design are the controls: `test_a_referent_pin_that_is_not_there_is_still_missing[dangling-link]` and `[link-to-itself]`, `test_a_validator_that_is_not_there_is_still_skipped[taken-away]` and `[a-directory-in-its-place]`, `test_a_manifest_that_is_a_directory_is_no_manifest_to_either_tool`, `test_only_a_file_is_a_register_where_the_stat_works[a-directory]` and `test_a_leg_path_holding_a_nul_is_not_initialized`.

**At `00a6f839`**, this branch's first merge of main (`2f5cabf5`), before round 1: 14 fail, 57 pass. The 14 are round 1's eleven new cells for its changes and the three cells whose step (c) header N1 changed. The 14 new cells that pass there do so by design: each pins behaviour the branch already had and kills a survivor (see Mutants).

**Round 1:** 27 mutants, one string each, run against the new file at the round's code on the earlier merge (`b220336b`'s scripts; #948's merge changes none of the mutated lines, and each mutant's text is found once at the head): 27 killed, 0 survived. B* and V* are RV958's thirteen survivors (B2, B3, B5, B6, B7, B8, B10, B11, B15, V1, V2, V4, V5) as this branch reconstructs them, since RV958 did not publish its mutant texts, with B5b for the second branch of `_named`; K* are further guards of the same kind; R* are one or more for each change of the round.

| Mutant | What it changes | Result |
|---|---|---|
| B2 | bootstrap.py `_is_a` without its `except ValueError` | killed: 3 test(s): test_a_leg_path_holding_a_nul_is_not_initialized, test_a_name_no_file_can_have_is_absent[bootstrap.py-a-lone-surrogate], test_a_name_no_file_can_have_is_absent[bootstrap.py-a-nul] |
| V1 | validate-manifest.py `_is_a` without its `except ValueError` | killed: 2 test(s): test_a_name_no_file_can_have_is_absent[validate-manifest.py-a-lone-surrogate], test_a_name_no_file_can_have_is_absent[validate-manifest.py-a-nul] |
| B5 | bootstrap.py `_named`, the relative branch unspelled | killed: 1 test(s): test_a_leg_in_a_folder_nobody_may_search_is_named_and_spelled[inside-the-root] |
| B5b | bootstrap.py `_named`, the whole-path branch unspelled | killed: 1 test(s): test_a_leg_in_a_folder_nobody_may_search_is_named_and_spelled[outside-the-root] |
| B6 | bootstrap.py `_named`, out of the root: `path.name` for the whole path | killed: 2 test(s): test_a_leg_in_a_folder_nobody_may_search_is_named_and_spelled[outside-the-root], test_a_register_under_a_leg_path_outside_the_root_is_read |
| V5 | validate-manifest.py `_unreadable_file`, out of the root: `path.name` | killed: 1 test(s): test_a_naming_policy_the_gate_cannot_open_is_named_on_every_python[given-outside-the-root-with-an-esc] |
| B7 | bootstrap.py `_is_file` takes only `PermissionError` | killed: 2 test(s): test_a_manifest_bootstrap_cannot_stat_or_open_is_the_same_refusal[stat-io-error], test_a_validator_whose_stat_is_refused_stops_step_b_before_any_runs[stat-io-error] |
| B8 | bootstrap.py `_a_register` takes only `PermissionError` | killed: 1 test(s): test_a_register_whose_stat_is_refused_is_not_dropped[io-error] |
| B10 | bootstrap.py `_load_manifest` maps only a `PermissionError` cause | killed: 1 test(s): test_a_manifest_bootstrap_cannot_stat_or_open_is_the_same_refusal[open-io-error] |
| B11 | bootstrap.py `_load_register_rows` names only a `PermissionError` cause | killed: 1 test(s): test_a_register_that_cannot_be_opened_claims_nothing_on_every_python |
| V4 | validate-manifest.py `_load` maps only a `PermissionError` cause | killed: 1 test(s): test_a_naming_policy_the_gate_cannot_open_is_named_on_every_python[io-error] |
| B3 | bootstrap.py `_is_file` takes anything for a file | killed: 2 test(s): test_a_manifest_that_is_a_directory_is_no_manifest_to_either_tool, test_a_validator_that_is_not_there_is_still_skipped[a-directory-in-its-place] |
| B15 | bootstrap.py `_a_register` takes anything for a register | killed: 1 test(s): test_only_a_file_is_a_register_where_the_stat_works[a-directory] |
| V2 | validate-manifest.py `_is_file` takes anything for a file | killed: 1 test(s): test_a_manifest_that_is_a_directory_is_no_manifest_to_either_tool |
| K1 | bootstrap.py `_is_a` ignores `kind` | killed: 3 test(s): test_a_manifest_that_is_a_directory_is_no_manifest_to_either_tool, test_a_validator_that_is_not_there_is_still_skipped[a-directory-in-its-place], test_only_a_file_is_a_register_where_the_stat_works[a-directory] |
| K2 | validate-manifest.py `_is_a` ignores `kind` | killed: 1 test(s): test_a_manifest_that_is_a_directory_is_no_manifest_to_either_tool |
| K3 | bootstrap.py `_found` asks for a regular file | killed: 2 test(s): test_anything_at_a_name_in_a_leg_is_found[a-directory], test_anything_at_a_name_in_a_leg_is_found[a-link-to-nothing-asked-of-the-name] |
| K4 | bootstrap.py `_found` drops `link` | killed: 3 test(s): test_a_marker_whose_lstat_is_refused_keeps_the_leg_on_every_python[a-merge], test_a_marker_whose_lstat_is_refused_keeps_the_leg_on_every_python[a-sequence], test_anything_at_a_name_in_a_leg_is_found[a-link-to-nothing-asked-of-the-name] |
| K5 | bootstrap.py `_found` takes only `PermissionError` | killed: 1 test(s): test_a_leg_whose_stat_is_refused_is_not_uninitialized[io-error] |
| R1 | M1: the header back to `relative_to(root)` | killed: 2 test(s): test_a_leg_in_a_folder_nobody_may_search_is_named_and_spelled[outside-the-root], test_a_register_under_a_leg_path_outside_the_root_is_read |
| R2 | N1: a refused `stat` headed `register` again | killed: 6 test(s): test_a_leg_bootstrap_cannot_search_is_left_as_it_is, test_a_leg_in_a_folder_nobody_may_search_is_named_and_spelled[inside-the-root], test_a_leg_in_a_folder_nobody_may_search_is_named_and_spelled[outside-the-root], test_a_register_bootstrap_cannot_read_is_named_not_absent[a-folder-nobody-may-enter], test_a_register_whose_stat_is_refused_is_not_dropped[io-error], test_a_register_whose_stat_is_refused_is_not_dropped[permission] |
| R3 | S2: `_a_validator` opens nothing | killed: 2 test(s): test_a_validator_at_mode_000_is_refused_before_any_runs, test_a_validator_whose_stat_is_refused_stops_step_b_before_any_runs[open-io-error] |
| R4 | S2: `_a_validator` takes only `PermissionError` | killed: 1 test(s): test_a_validator_whose_stat_is_refused_stops_step_b_before_any_runs[open-io-error] |
| R5 | S1: the policy back to `NamingPolicy.load(policy_path)` | killed: 3 test(s): test_a_naming_policy_the_gate_cannot_open_is_named_on_every_python[given-outside-the-root-with-an-esc], test_a_naming_policy_the_gate_cannot_open_is_named_on_every_python[io-error], test_a_naming_policy_the_gate_cannot_reach_is_refused_by_name |
| R6 | N3: `_shown` returns the name as it is | killed: 2 test(s): test_a_name_that_does_not_print_is_spelled_under_the_root_too, test_a_naming_policy_the_gate_cannot_open_is_named_on_every_python[given-outside-the-root-with-an-esc] |
| R7 | N3: the relative branch unspelled | killed: 1 test(s): test_a_name_that_does_not_print_is_spelled_under_the_root_too |
| R8 | N3: the whole-path branch unspelled | killed: 1 test(s): test_a_naming_policy_the_gate_cannot_open_is_named_on_every_python[given-outside-the-root-with-an-esc] |

**Round 0:** 18 mutants, one string each, run against the new file at `df1bb186` (the move commit after it changes no line). All were killed; none survived.

| Mutant | Caught by |
|---|---|
| M1 validate-manifest.py `:1085`: `_is_file` back to `manifest_path.is_file()` | killed: 2 test(s): test_a_manifest_the_gate_cannot_reach_or_open_is_refused_by_name[behind-a-folder-nobody-may-enter], test_a_manifest_whose_stat_is_refused_is_the_same_refusal |
| M2 validate-manifest.py: `_load` back to `load_yaml` | killed: 1 test(s): test_a_manifest_the_gate_cannot_reach_or_open_is_refused_by_name[at-mode-000] |
| M3 validate-manifest.py `:406`: `_is_file` back to `pin_path.is_file()` | killed: 4 test(s): test_a_contracts_folder_the_gate_cannot_search_names_the_pin, test_a_referent_pin_the_gate_cannot_reach_is_refused_not_missing, test_a_referent_pin_whose_stat_is_refused_is_refused_on_every_python[io-error], test_a_referent_pin_whose_stat_is_refused_is_refused_on_every_python[permission] |
| M4 validate-manifest.py: `_findings` back outside `main`'s `try` | killed: 4 test(s): test_a_contracts_folder_the_gate_cannot_search_names_the_pin, test_a_referent_pin_the_gate_cannot_reach_is_refused_not_missing, test_a_referent_pin_whose_stat_is_refused_is_refused_on_every_python[io-error], test_a_referent_pin_whose_stat_is_refused_is_refused_on_every_python[permission] |
| M5 bootstrap.py `:1328`: `_is_file` back to `manifest_path.is_file()` | killed: 2 test(s): test_a_manifest_bootstrap_cannot_reach_or_open_is_refused_first[behind-a-folder-nobody-may-enter], test_a_manifest_bootstrap_cannot_stat_is_the_same_refusal |
| M6 bootstrap.py: `_load_manifest` back to `load_yaml` | killed: 1 test(s): test_a_manifest_bootstrap_cannot_reach_or_open_is_refused_first[at-mode-000] |
| M7 bootstrap.py `:1409`: the look-ahead back to `(root / argv_[0]).is_file()` | killed: 2 test(s): test_a_scripts_folder_bootstrap_cannot_search_is_refused_not_skipped, test_a_validator_whose_stat_is_refused_stops_step_b_before_any_runs |
| M8 bootstrap.py `:1056`: `_found` back to `(sub / ".git").exists()` | killed: 3 test(s): test_a_leg_bootstrap_cannot_search_is_left_as_it_is, test_a_leg_whose_stat_is_refused_is_not_uninitialized[io-error], test_a_leg_whose_stat_is_refused_is_not_uninitialized[permission] |
| M9 bootstrap.py `:717`: `_found(..., link=True)` back to `os.path.lexists` | killed: 1 test(s): test_a_marker_whose_lstat_is_refused_keeps_the_leg_on_every_python[a-merge] |
| M10 bootstrap.py `:750`: the same for `sequencer/todo` | killed: 2 test(s): test_a_marker_whose_lstat_is_refused_keeps_the_leg_on_every_python[a-sequence], test_an_operation_bootstrap_cannot_see_keeps_the_leg_where_it_is |
| M11 bootstrap.py `:1191`: `_a_register` back to `c.is_file()` | killed: 3 test(s): test_a_leg_bootstrap_cannot_search_is_left_as_it_is, test_a_register_bootstrap_cannot_read_is_named_not_absent[a-folder-nobody-may-enter], test_a_register_whose_stat_is_refused_is_not_dropped |
| M12 bootstrap.py: a register's open refusal said as before (`unread = False`) | killed: 3 test(s): test_a_leg_bootstrap_cannot_search_is_left_as_it_is, test_a_register_bootstrap_cannot_read_is_named_not_absent[a-folder-nobody-may-enter], test_a_register_bootstrap_cannot_read_is_named_not_absent[at-mode-000] |
| M13 validate-manifest.py: ELOOP dropped from `_ABSENT_ERRNOS` | killed: 2 test(s): test_a_name_that_is_not_there_is_absent[validate-manifest.py-ELOOP], test_a_referent_pin_that_is_not_there_is_still_missing[link-to-itself] |
| M14 validate-manifest.py: EACCES added to `_ABSENT_ERRNOS` | killed: 6 test(s): test_a_contracts_folder_the_gate_cannot_search_names_the_pin, test_a_manifest_the_gate_cannot_reach_or_open_is_refused_by_name[behind-a-folder-nobody-may-enter], test_a_manifest_whose_stat_is_refused_is_the_same_refusal, test_a_referent_pin_the_gate_cannot_reach_is_refused_not_missing, test_a_referent_pin_whose_stat_is_refused_is_refused_on_every_python[permission], test_every_other_refusal_is_raised[validate-manifest.py-EACCES] |
| M15 bootstrap.py: ELOOP dropped from `_ABSENT_ERRNOS` | killed: 1 test(s): test_a_name_that_is_not_there_is_absent[bootstrap.py-ELOOP] |
| M16 bootstrap.py: EACCES added to `_ABSENT_ERRNOS` | killed: 12 test(s): test_a_leg_bootstrap_cannot_search_is_left_as_it_is, test_a_leg_whose_stat_is_refused_is_not_uninitialized[permission], test_a_manifest_bootstrap_cannot_reach_or_open_is_refused_first[behind-a-folder-nobody-may-enter], test_a_manifest_bootstrap_cannot_stat_is_the_same_refusal, test_a_marker_whose_lstat_is_refused_keeps_the_leg_on_every_python[a-merge], test_a_marker_whose_lstat_is_refused_keeps_the_leg_on_every_python[a-sequence], test_a_register_bootstrap_cannot_read_is_named_not_absent[a-folder-nobody-may-enter], test_a_register_whose_stat_is_refused_is_not_dropped, test_a_scripts_folder_bootstrap_cannot_search_is_refused_not_skipped, test_a_validator_whose_stat_is_refused_stops_step_b_before_any_runs, test_an_operation_bootstrap_cannot_see_keeps_the_leg_where_it_is, test_every_other_refusal_is_raised[bootstrap.py-EACCES] |
| M17 bootstrap.py: `_cure` gives the `chmod` advice for every cause | killed: 1 test(s): test_a_leg_whose_stat_is_refused_is_not_uninitialized[io-error] |
| M18 validate-manifest.py: the `chmod` advice for every cause | killed: 1 test(s): test_a_referent_pin_whose_stat_is_refused_is_refused_on_every_python[io-error] |

Each run is `run9s.sh` with `SHARDS=3` (three `run9.sh` processes: the gh stub first on `PATH`, `GH_TOKEN=bogus`, an empty `GH_CONFIG_DIR`, a blank git identity), one run at a time, at head `b420171f`.

- **git 2.43.0, the first brief's whole list, 134 files: 9656 passed, 25 skipped (longest shard 40 min), rc 0.** 2026-10-19T11:37:42Z to 2026-10-19T12:17:59Z. The list is every test file that names the assembly root's `bootstrap.py` or `validate-manifest.py`, or runs a scaffolded root's `make bootstrap` or its validators, with #948's new `test_bootstrap_casefold_twin.py`, and `test_pinned_copy_text.py`, `test_bom_reads.py`, `test_validator_child_encoding.py`, `test_repo_hygiene.py`, `test_agents_md_codes.py` and `test_cli_reference.py`.
  The 25 skips: 8 of `test_leg_path_yaml_scalars.py` (empty parameter sets, and the templates read the value back); 6 of `test_doctor_legs_members_next.py` (git 2.43 dies on a `shallow` of -2**31, and crashes on `submodule init`; CI's 2.55 does neither); 3 reftable cases of `test_bootstrap_ref_store.py` (git 2.45 and later); 3 NTFS junction cases of `test_update_shape_add_symlink.py`, 3 PowerShell lines of `test_shape_doctor.py` and 1 of `test_stdout_encoding.py` (Windows only); 1 of `test_bump_leg.py` (a disk that ignores case).
- **git 2.47.1 (a build first on `PATH`), 24 files: 1764 passed (longest shard 4 min), rc 0.** 2026-10-18T20:07:50Z to 2026-10-18T20:11:58Z. Files: `test_agents_md_codes.py`, `test_assembly_root_unreadable_paths.py`, `test_bom_reads.py`, `test_bootstrap_after_bump.py`, `test_bootstrap_branch_bytes.py`, `test_bootstrap_casefold_twin.py`, `test_bootstrap_leg_moves.py`, `test_bootstrap_non_utf8_branch.py`, `test_bootstrap_pasteable_lines.py`, `test_bootstrap_placement_texts.py`, `test_bootstrap_ref_store.py`, `test_bootstrap_refusal_exit.py`, `test_bootstrap_spelling.py`, `test_bootstrap_upstream_notice.py`, `test_bootstrap_without_git.py`, `test_bump_leg_unopenable.py`, `test_bump_leg_unreadable.py`, `test_cli_reference.py`, `test_manifest_validator.py`, `test_naming_policy.py`, `test_pinned_copy_text.py`, `test_repo_hygiene.py`, `test_validate_pins_named_and_spelled.py`, `test_validator_child_encoding.py`.
- A first 2.43.0 run of the same list at the same head, on 2026-10-18, was stopped from outside at 20:31Z (a signal; its shards had reached 52%, 29% and 41% with no failure); the run above is its re-run.
- Before #948 landed, the same list less `test_bootstrap_casefold_twin.py` (23 files), on git 2.47.1 at the round's commit on the earlier merge (`b220336b`): 1713 passed, rc 0.

<details><summary>The 134 files of the git 2.43.0 run</summary>

`test_adopt_attributes_eol_block.py`, `test_adopt_clone_template.py`, `test_adopt_e2e.py`, `test_adopt_git_reads.py`, `test_adopt_ignored_shape_files.py`, `test_adopt_manifest_values.py`, `test_adopt_mount_ignored.py`, `test_adopt_nested_attributes.py`, `test_adopt_origin_repository.py`, `test_adopt_pins.py`, `test_adopt_plan_yaml_quoting.py`, `test_adopt_reencoded_files.py`, `test_adopt_relative_submodule_url.py`, `test_adopt_root_manifest.py`, `test_adopt_root_symlinks.py`, `test_adopt_shape_pin_and_work_dir.py`, `test_adopt_spec_only.py`, `test_adopt_submodules.py`, `test_adopt_verify.py`, `test_agents_md_codes.py`, `test_assembly_root_unreadable_paths.py`, `test_bom_reads.py`, `test_bootstrap_after_bump.py`, `test_bootstrap_branch_bytes.py`, `test_bootstrap_casefold_twin.py`, `test_bootstrap_leg_moves.py`, `test_bootstrap_non_utf8_branch.py`, `test_bootstrap_pasteable_lines.py`, `test_bootstrap_placement_texts.py`, `test_bootstrap_ref_store.py`, `test_bootstrap_refusal_exit.py`, `test_bootstrap_spelling.py`, `test_bootstrap_upstream_notice.py`, `test_bootstrap_without_git.py`, `test_bump_leg.py`, `test_bump_leg_branch_and_push_line.py`, `test_bump_leg_dry_run_tracking.py`, `test_bump_leg_dry_run_truth.py`, `test_bump_leg_paste_lines.py`, `test_bump_leg_rollback.py`, `test_bump_leg_unopenable.py`, `test_bump_leg_unreadable.py`, `test_chain_remedy_callers.py`, `test_cli_reference.py`, `test_doctor_gitlink_repairs.py`, `test_doctor_gitmodules_not_utf8.py`, `test_doctor_legs_members_next.py`, `test_doctor_machine_row.py`, `test_doctor_members_spelled.py`, `test_doctor_next_is_a_command.py`, `test_doctor_no_lazy_fetch.py`, `test_doctor_non_ascii_paths.py`, `test_doctor_partial_clone.py`, `test_doctor_paste_through_git.py`, `test_doctor_pin_restore_merges.py`, `test_doctor_plan_overrides.py`, `test_doctor_recorded_answers.py`, `test_doctor_unfinished_merge.py`, `test_doctor_unreadable_pin.py`, `test_doctor_validator_rows.py`, `test_doctor_validator_site.py`, `test_doctor_validator_verdicts.py`, `test_family.py`, `test_family_bootstrap_gate_order.py`, `test_family_bump_remove_rollback.py`, `test_family_gitmodules_values.py`, `test_family_head_lost_gitmodules.py`, `test_family_holder_bootstrap_texts.py`, `test_family_makefile_siblings.py`, `test_family_member_name.py`, `test_family_members_not_a_list.py`, `test_family_siblings_placement.py`, `test_git_output_decoding.py`, `test_leg_path_yaml_scalars.py`, `test_legs_not_a_list.py`, `test_legs_readers_not_a_list.py`, `test_manifest_validator.py`, `test_naming_policy.py`, `test_non_list_project_fields.py`, `test_non_utf8_oserror_sites.py`, `test_park_resume_targets.py`, `test_pin_source_env_collisions.py`, `test_pinned_copy_text.py`, `test_pins_child_kind_and_siblings_root_spelling.py`, `test_push_branch_refspec.py`, `test_reference_default.py`, `test_repo_hygiene.py`, `test_scaffold_e2e.py`, `test_scaffold_free_text_values.py`, `test_scaffold_ignored_shape_files.py`, `test_scaffold_leg_path_collisions.py`, `test_scaffold_manifest_values.py`, `test_scaffold_mount_ignored.py`, `test_scaffold_pin_and_reuse.py`, `test_scaffold_referent_chain_links.py`, `test_setup_by_hand_commands.py`, `test_setup_project_py.py`, `test_setup_sh.py`, `test_shape_doctor.py`, `test_siblings_family_name.py`, `test_siblings_inputs.py`, `test_stdout_encoding.py`, `test_template_quoted_values.py`, `test_template_text_placeholders.py`, `test_template_values_single_pass.py`, `test_update_shape.py`, `test_update_shape_add.py`, `test_update_shape_add_ignored.py`, `test_update_shape_add_not_a_directory.py`, `test_update_shape_add_symlink.py`, `test_update_shape_apply_lines_and_headers.py`, `test_update_shape_branch_any_directory.py`, `test_update_shape_hardlink.py`, `test_update_shape_header_comments.py`, `test_update_shape_kept_helper.py`, `test_update_shape_landing.py`, `test_update_shape_merged_away.py`, `test_update_shape_messages.py`, `test_update_shape_next_quoting.py`, `test_update_shape_partial_rollback.py`, `test_update_shape_paste_lines.py`, `test_update_shape_pr_base.py`, `test_update_shape_rewrite_symlink.py`, `test_validate_family_branch_step.py`, `test_validate_family_member_path_glob.py`, `test_validate_family_remedies_typed.py`, `test_validate_family_typed_states.py`, `test_validate_pins_lockstep_lines.py`, `test_validate_pins_named_and_spelled.py`, `test_validate_workflow_gitmodules_urls.py`, `test_validator_child_encoding.py`, `test_windows_paths.py`, `test_yaml_indicator_openers.py`, `test_yaml_reader_spaces.py`

</details>

The branch merged main twice. Most recently, `cf8d5667` merges origin/main `2a78ae1a` (#984, landed) into round 1's head `b420171f`: its one conflict, the import block of `bootstrap.py`, keeps #984's `import unicodedata` and this branch's `from collections.abc import Callable, Iterator`, and the branch's own `_shown` in `validate-manifest.py` is deleted in favour of #981's `_shown(path)` (the account of that merge is on the PR). Before that, `bb58c441` merges origin/main `ebbd9fa4` (#948, #896's twin refusals, landed) into `0ef875c0` with no conflict (`bootstrap.py` auto-merged), and the round's commit `b420171f` sits on it. The round was first written on an earlier merge of main (`00a6f839`, `2f5cabf5`); that merge was replaced, unpushed, once #948 landed. The round's commit was reapplied unchanged (the same `git patch-id`). Its tree is the one `git merge-tree` predicted for the earlier round commit with `ebbd9fa4` (`9f4c2991`).

`git merge-tree --write-tree` of head `b420171f`, each ref fetched with `gc.auto=0` and `maintenance.auto=false` just before:

- origin/main `c7a3ff16`, re-fetched on 2026-10-19 after the head was pushed: clean as text (tree `977f0980`), and NOT merged here: the landing pilot refreshes the head under the fast-refresh ruling. Since `ebbd9fa4`, main's #981 (`ca6a0b40`) changes `validate-manifest.py`, and its own `_shown(path)` (`text = str(path)`, then `isprintable` or `repr`) lands beside this branch's `_shown(typed)`: the merged file defines `_shown` twice. The two give the same answer for every string this branch passes, the later definition wins, and the new file passes on the merged tree (71 passed, `$Q/v118/w430d/r1/m981.log`), but the refresh should delete this branch's `_shown` and keep #981's. Main's other changes in the files near this slice are to `validate-pins.py` and `tests/conftest.py`, which this branch does not touch.
- #948 at `45415296`: clean (tree `9f4c2991`, the head's own).
- #973 at `b2887cf5` (`fix/961-paste-words`): clean. #973 does not yet change the NOT INITIALIZED line; when it does, the new file's `not_initialized` helper is the one place to follow it.
- Each clean merge's `bootstrap.py` parses, with no top-level name defined twice.
- #962's `wip/l5-962-create-twins-20261018` at `414e9e0a`: conflicts in `bootstrap.py`, 8 hunks. 7 of them are with main alone: #962 is stacked on #948's head before #948 was squashed, and `git merge-tree` of origin/main with it gives the same 7. The 8th is this branch's only one, the import lines: `from collections.abc import Callable, Iterator` here against #962's `import unicodedata` and `from collections.abc import Iterator`. Taking both resolves it.

- No duplicated block: the new file shares no run of five code lines with any test file but an import block, reuses `as_a_descendant_by_chain`, `link_tree`, `refused_stat`, `failing`, `os_error`, `mode_of`, `CANNOT_RUN_AS_ROOT` and the cure constants instead of copying them, and puts its own repeated lines in helpers (`refused`, `left_as_it_is`, `nothing_claimed`, `not_initialized`, `refusing`, `leg_path`, `up_to_step_b`, `skipping_validators`, `a_register`).
- No assert joins conditions with `and`.
- Cognitive complexity, counted by hand: `checkout_tracking_branch` 13, unchanged; `_load_register_rows` 11; `_in_progress` 8; `_run_neutral_validators` 7; `_report_one_register` 6; `_register_paths` 4; `read_authority` 4; `validate-manifest.py`'s `main` 9 and `_load` 3; every other new function 2 or less.
- Every added line is within 79 columns.

Round 0's first two candidates (the naming policy named by its absolute path, and a validator the child cannot open) are fixed in round 1, on RV958's S1 and S2.

- **The assembly root's `validate-repository-naming.py` names a naming policy it cannot read by its absolute path, with no remedy.** Measured with `contracts/` at 000 in a full `bootstrap.py` run with round 1's change: `REFUSED yaml-unreadable: <ROOT>/contracts/repository-naming.yaml: cannot be read (Permission denied)`, while `validate-manifest.py`'s refusal just after it names `contracts/repository-naming.yaml` with the remedy. Not this slice's file.
- **`OTHER_REASON` tells a validator's I/O error to "take away one nobody wants".** A validator taken away is `SKIPPED`. #934 gives bump-leg.py a `git checkout --` line instead. These are validate-pins.py's words, kept as the plan rule asks.
- **The cure's "(and on every directory above it inside the root)"** reads oddly for a folder outside the root (an absolute leg path, a `--policy` given elsewhere). validate-pins.py's words, as RV958's N2 found for a symlinked `project.yaml`.
- **Seen in the code, not measured:** validate-manifest.py prints `project.yaml`'s absolute path raw in `manifest-missing`, `manifest-unreadable` and `<n> finding(s) in <path>`, where `validate-pins.py` and `bootstrap.py` spell every path they print (#391, #712).

Part of #430

Review: RV958, lane 4's independent reviewer, not the writer (w430d): READY at `7e87e05f` (issuecomment-6278103469), the reviewed head, after its READY on round 1 at `cf8d5667` (issuecomment-6271860975); the delta was a merge of main and one test-only commit for CI. CI (Brett Heap's ruling 27): H0 is the head 7c06c57 itself, which passed tests, tests-git-bash and SonarCloud; main has touched none of its files since its base 051176c; and the local gate passed on its predicted squash tree 194700b over main 051176c: the adopt_reencoded_files, assembly_root_unreadable_paths, repo_hygiene, agents_md_codes, cli_reference sets, 294 passed. Landed on Brett Heap's word of 2026-10-18T14:45:28Z, relayed to every lane by lane 3 (#473 issuecomment-6251255596): "merge v1.2.0 PRs once reviewed and green".


Lane: openreposhape-5 (openRepoShape-5)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brett Heap added 2 commits October 20, 2026 03:15
Conflicts, each taken from this branch's side (git checkout --ours), #958's
own files:

  templates/assembly-root/scripts/bootstrap.py
  tests/test_assembly_root_unreadable_paths.py

The squash's tree is identical to #958's final head 7c06c57 (git diff
7c06c57 af0e60f is empty), and 7c06c57 is already merged here, so this
branch's side holds all of #958's content plus #961's change. The merge's
tree is the tree of the commit before it.
@sonarqubecloud

Copy link
Copy Markdown

@brettheap brettheap changed the title bootstrap.py: a paste word that begins with = is quoted, a non-ASCII one only where the stdout writes the bytes git is handed, and a character's byte escapes get their printf word, so zsh no longer expands a leg's path (#961) bootstrap.py: the paste words it prints quote a leading =, are made of the bytes git is handed (UTF-8 on Windows), and the NOT INITIALIZED command names a leg's path as one word after a --, with ./ before a : (#961) Oct 20, 2026
@brettheap

Copy link
Copy Markdown
Contributor Author

Re-check of #973 (#961)'s NOT INITIALIZED change at 2a2bc8f9 (staged on #958), for lane openRepoShape-5: READY

The staged commit meets the condition of my READY on b2887cf5: the NOT INITIALIZED line now prints git submodule update --init -- {_paste_word(path)}. In real scaffolds with local bare remotes I pasted that line through sh, dash, bash and zsh. For every awkward path I tried, it re-initializes the leg at its pin. At the merged base cc779c9c the same paste failed for most of them. The partial fix of my NIT 1 is acceptable. I found no MUST and no SHOULD; there are four NITs.

MUST

None.

SHOULD

None.

NIT

  1. A leg path that begins with : still names nothing.

    • Git reads a word starting with : as pathspec magic, and the -- does not stop that. So git submodule update --init -- :spec fails with error: pathspec ':spec' did not match any file(s) known to git (exit 1), and the leg stays empty.
    • I measured it in all four shells, on git 2.43.0 and 2.47.1.
    • It is the same at cc779c9c, so it is not a regression.
    • Each of these re-initializes the leg (exit 0, .git back): -- ./:spec, -- ':(literal):spec', and git --literal-pathspecs submodule update --init -- :spec.
    • The smallest fix would put ./ in front of only a path that begins with :. A follow-up, or a checklist line beside the writer's leading-- candidate, is enough. I did not check what validate-manifest.py says of such a path.
  2. The partial NIT 1 fix: acceptable, but the account's "no worse than the base" is not true in every case. I called _paste_word under the account's Windows shims, at cc779c9c and at 2a2bc8f9:

    case cc779c9c 2a2bc8f9
    default mode, all 6 rows (UTF-8 and cp1252 stdout; café, €5, 日本) same same
    legacy mode, UTF-8 stdout, origin/café "$(printf -- 'origin/caf\351')" 'origin/café'
    legacy mode, UTF-8 stdout, €5 "$(printf -- '\2005')" '€5'
    legacy mode, cp1252 stdout, origin/café 'origin/café' "$(printf -- 'origin/caf\351')"
    legacy mode, cp1252 stdout, €5 '€5' "$(printf -- '\2005')"
    • The two legacy-mode UTF-8 rows are the fix.
    • The two legacy-mode cp1252 rows go the other way. In Git Bash the printf word names another path. The old quoted text pasted correctly from a console that decodes cp1252; it was only wrong where the terminal reads that output as UTF-8 (mintty reading a cp1252 pipe).
    • This case is narrow: it needs PYTHONLEGACYWINDOWSFSENCODING=1 and a stdout that is cp1252, which a Windows console has not been since PEP 528 unless PYTHONLEGACYWINDOWSSTDIO is also set.
    • Keeping it as a checklist candidate is fine. The consistent fix is small: in _paste_word, take the bytes from _handed_to_git(name) instead of os.fsencode(name), and replace the Windows read-back guard with a strict UTF-8 encode.
    • Until then, the module paragraph's "a printf that prints those bytes again" is not true for legacy mode, and the account's "no worse" should read "no worse where the terminal reads the output as UTF-8".
  3. Test strength.

    • My mutant R-H, _paste_word(path.strip()), passes all 397 tests of the three changed files: no row has a path with a leading or trailing space. A "spec " row would pin it.
    • The writer edited test_a_legs_path_is_spelled_where_it_is_not_initialized, which keeps an older assert code == 0 and "Traceback" not in err. It could be split while the test is open.
    • None of the added asserts joins conditions with and or or.
  4. The account misattributes a commit. It says bootstrap.py: a leg whose HEAD is on a branch that differs from the tracking branch only in case (Main for main) is left where it is, so on a disk that folds case branch -f main no longer moves the ref HEAD stands on (#896) #948 landed as 39694097. 39694097 is bootstrap.py: the name the rename line gives a twin skips one with a branch stored under it, Main-kept over Main-kept/x, which git refuses, so the pasted line works and the next run makes the tracking branch (#1000) #1006 (bootstrap.py: _kept_name skips a -kept name with a branch stored under it, and a test row for a twin with a slash (follow-up of #962) #1000); bootstrap.py: a leg whose HEAD is on a branch that differs from the tracking branch only in case (Main for main) is left where it is, so on a disk that folds case branch -f main no longer moves the ref HEAD stands on (#896) #948 landed as ebbd9fa4. This is in the account only.

What I checked

  1. The paste, before and after.

    • Paths: my spec, =spec, ESC, U+2028, the byte 0xff, sp\udcc3\udca9c, sp%s\udcffec, sp'ec, a tab and sp*ec, plus :spec and -spec.
    • Method: the spec leg is moved to the path, emptied with submodule deinit -f, and the printed command is run from the root through each shell.
    • At 2a2bc8f9, 40 of 40 pastes work on 2.43.0 and 40 of 40 on 2.47.1: exit 0, the leg's .git is back, and its HEAD is the gitlink the root's index records.
    • At cc779c9c, the line was --init <spelled>:
      • my spec became two pathspecs;
      • ESC, U+2028, the 0xff byte, the UTF-8-as-bytes path, %s and the tab each named their escape text;
      • sp'ec was a syntax error in every shell;
      • =spec failed in zsh only (zsh:1: spec not found);
      • sp*ec worked only because the shell's glob matched the directory itself.
    • -spec: git ignores that submodule, so deinit did not empty it. The writer's test covers it, and checks that git's stderr names path '-spec'.
  2. The rest of the account's NIT 1–3 claims.

    • _handed_to_git is used only in _prints_as_it_is, inside its try. My O3 is killed by the CJK row (NIT 2).
    • The module paragraph names the new command and the names os.fsencode cannot write (NIT 3).
  3. Mutants.

    mutant failed
    N2, no -- (writer's) 17
    W1, the Windows branch dropped (writer's) 2
    O3, the comparison outside the try (writer's) 1
    R-F, _paste_word(spelled(path)) (mine) 8
    R-H, _paste_word(path.strip()) (mine) survives (NIT 3)
  4. Gates at 2a2bc8f9. The account's nine-file set, with the gh stub, GH_TOKEN=bogus, an empty GH_CONFIG_DIR, GIT_SSH_COMMAND=false, GIT_CONFIG_GLOBAL=/dev/null and EMAIL set: 733 passed on 2.43.0 and 733 on 2.47.1.

  5. Merges.

  6. Sonar-style checks.

    • Cognitive complexity: _handed_to_git 1, _prints_as_it_is 4, _paste_word 7 and _initialized 2, the last three unchanged.
    • Every added line is at most 79 characters. The changed bootstrap.py lines are ASCII except the em dash that was already in the NOT INITIALIZED sentence.
    • The repo_shape import set is unchanged, and test_pinned_copy_text.py passes.

Reviewer: rv973, lane 5's independent reviewer, not the writer (w961).

Lane: openreposhape-5 (openRepoShape-5)

@brettheap

Copy link
Copy Markdown
Contributor Author

Delta check of #973's round 2 at e23137c7 (2a2bc8f9..e23137c7), for lane openRepoShape-5: READY

All four NITs from my re-check are dealt with, and I found nothing new. A leg path that begins with : now pastes and re-initializes the leg in all four shells on both gits. The Windows change regresses nothing in any shim I tried and changes nothing on POSIX. The space rows kill R-H.

MUST

None.

SHOULD

None.

NIT

None.

What I checked

  1. The : paste, in real scaffolds with local bare remotes.

    • Method: the spec leg is moved to the path, emptied, and the printed command is run from the root through /bin/sh, dash, bash and zsh.
    • Paths: :spec, :my spec, :sp<0xff>ec, ::spec, :=spec, :-spec, spec , spec, my spec, =spec, sp<0xff>ec and ESC.
    • At e23137c7: 48 of 48 on git 2.43.0 and 48 of 48 on 2.47.1. Each exits 0, the leg's .git is back, and its HEAD is the gitlink the root's index records.
    • The printed words put ./ inside the one word:
      • ./:spec
      • './:my spec'
      • "$(printf -- './:sp\377ec')"
      • ./::spec, ./:=spec and ./:-spec
    • At 2a2bc8f9 (2.43.0), all six : paths failed in every shell, 0 of 24:
      • pathspec ':spec' did not match (and the same for :my spec, :sp\xffec and ::spec);
      • fatal: Unimplemented pathspec magic '=' for :=spec, and '-' for :-spec.
    • The other rows passed there, as designed.
    • Only _initialized takes the ./. The git -C <path> lines name a directory, not a pathspec, so they need none.
  2. The Windows change, by in-process shims. I compared _paste_word at 2a2bc8f9 and at e23137c7.

    • POSIX. 143,560 calls, identical at both commits. That covers the real os and a latin-1 filesystem encoding; stdouts in UTF-8, latin-1, cp1252, ascii and an unknown codec; 20 hand-picked names; and every code point from U+0000 to U+2FFF plus every surrogate, each inside a name.
    • Windows default mode (UTF-8 with surrogatepass): only names with a lone surrogate change. They go from a printf word of junk UTF-8 bytes (a\355\263\277b), which named another path, to their spelled text. Everything else is unchanged.
    • Legacy mode (cp1252 with replace): every change goes from a wrong word to the UTF-8 bytes Git Bash reads a paste as.
      • origin/café and €5: the code page's bytes (\351, \2005) become \303\251 and \342\202\254.
      • 日本, U+2028, U+202E and NFD café: these were kept as escape or unwritable text, and now get a correct UTF-8 printf word.
    • bootstrap.py: the merge --ff-only, --continue and --abort lines it prints word a leg's path and an upstream as a shell reads them back, so a byte that is not UTF-8 is a printf and no longer escape text that names no ref (#932) #944's LegacyWindowsOs: the same pattern.
    • Across all three shims, three stdouts and the full sweep, no call at e23137c7 raises.
  3. The space rows kill R-H. _paste_word(path.strip()) fails 4 tests: the trailing-space and leading-space rows of the paste test, and of the unreadable-paths test.

  4. Mutants: all killed.

    mutant failed
    the writer's C5, ./ glued in front of the quoted word 2 (leading-colon-and-space, leading-colon-and-byte-0xff)
    no ./ 7 (4 shell rows, 3 text rows)
    errors="replace" in the Windows branch of _handed_to_git (mine) 3 (the lone-surrogate rows)
    errors="surrogateescape" there (mine) 3 (the same rows)

    The surrogateescape mutant is only killed because the row's name a\udcffb uses a surrogate in the U+DC80–U+DCFF range, which surrogateescape maps to a byte.

  5. Gates at e23137c7. The nine-file set, with the gh stub, GH_TOKEN=bogus, an empty GH_CONFIG_DIR, GIT_SSH_COMMAND=false, GIT_CONFIG_GLOBAL=/dev/null and EMAIL set: 752 passed on 2.43.0 and 752 on 2.47.1.

  6. Merges. Clean against main e076e3e8 (tree 2ba475e7) and against the assembly root's validate-manifest.py and bootstrap.py: a path this user cannot read is named, never read as absent, so a scripts/ nobody may search no longer SKIPs every validator into bootstrap ok (#430, slice D) #958's head 5aaeac64 (tree 95d79984); both match the account.

  7. Style and Sonar-style checks.

    • No added assert joins conditions with and or or, and the old composite assert is split.
    • No added line is over 79 characters. The two non-ASCII added bootstrap.py lines are older text re-wrapped: the em dash and spéc.
    • Cognitive complexity: _initialized 4, _handed_to_git 1, _prints_as_it_is 4, and _paste_word 6 (down from 7, since the read-back guard is gone).
    • The repo_shape imports are unchanged.
  8. Docstrings and account. The module paragraph, _paste_word, _handed_to_git and _initialized now say what the code does, including the ./ and the UTF-8 printf bytes on Windows. NIT 4 is corrected in the account.

Could not verify

The Windows behaviour rests on in-process shims, which is the writer's checklist item 3. A real Windows host, Git Bash's argument conversion and legacy mode itself are unmeasured.

Reviewer: rv973, lane 5's independent reviewer, not the writer (w961).

Lane: openreposhape-5 (openRepoShape-5)

@brettheap

Copy link
Copy Markdown
Contributor Author

Restack account: #973 (#961) on #958's landing

Writer w961, lane openRepoShape-5, 2026-10-20. ASCII only.

What was pushed

ref before after
fix/961-paste-words (#973's head, origin) b2887cf5 d286b289f0fa341a50a4779e342dcdb832239a28
wip/l5-961-on-958-20261019 (origin) e23137c7 unchanged
stage/961-on-958 (local, the worktree's branch) e23137c7 d286b289

The push was git push origin HEAD:refs/heads/fix/961-paste-words, a fast-forward (b2887cf5 is an ancestor of the new head; the output reads b2887cf56..d286b289f), no force. Read back with git ls-remote: d286b289f0fa341a50a4779e342dcdb832239a28 refs/heads/fix/961-paste-words.

The three merges (each merge --no-ff, identity Brett Heap service@opensoft.dev)

State at the start: origin/main was af0e60f2, the squash of #958, whose parent is 051176ce; fix/430-d-assembly-root-guards was gone from origin (a fetch naming it fails), and my local remote-tracking ref for it was 7c06c578, #958's final head. Both objects were in the repository.

  1. the assembly root's validate-manifest.py and bootstrap.py: a path this user cannot read is named, never read as absent, so a scripts/ nobody may search no longer SKIPs every validator into bootstrap ok (#430, slice D) #958's final head 7c06c578 into e23137c7: 42804af0. Clean, no conflict. It is 7e87e05f (which this branch already held) plus two merges of main.
  2. the assembly root's validate-manifest.py and bootstrap.py: a path this user cannot read is named, never read as absent, so a scripts/ nobody may search no longer SKIPs every validator into bootstrap ok (#430, slice D) #958's squash af0e60f2: d286b289. Two conflicts, both in the assembly root's validate-manifest.py and bootstrap.py: a path this user cannot read is named, never read as absent, so a scripts/ nobody may search no longer SKIPs every validator into bootstrap ok (#430, slice D) #958's own files, both taken from this branch's side with git checkout --ours:
  3. origin/main: Already up to date. Main's tip IS the squash af0e60f2 (nothing has landed since; git merge-base --is-ancestor origin/main HEAD is true), so step 2 had already brought in everything main has. No commit was made, and no conflict arose to abort.

The diff check, from objects

git diff origin/main HEAD touches exactly five files, and no other:

  • templates/assembly-root/scripts/bootstrap.py
  • tests/test_assembly_root_unreadable_paths.py
  • tests/test_bootstrap_after_bump.py
  • tests/test_bootstrap_pasteable_lines.py
  • tests/test_bootstrap_spelling.py

I compared it with git diff 7e87e05f e23137c7, which is this PR's own change on top of #958's head: the commits b2887cf5, 2a2bc8f9 and e23137c7, with cc779c9c's clean merge of #958. (The brief's wording was a base of 450279ca minus #958's part; 7e87e05f is that, exactly, since 7e87e05f is #958's head and nothing of #958 is left in the difference.) The method: both diffs with -U0, the + and - lines kept in file order, hunk headers dropped, since main's line numbers moved.

file changed lines, S = 7e87e05f..e23137c7 R = origin/main..HEAD identical, in order
bootstrap.py 109 109 yes
tests/test_assembly_root_unreadable_paths.py 54 54 yes
tests/test_bootstrap_after_bump.py 19 19 yes
tests/test_bootstrap_pasteable_lines.py 483 483 yes
tests/test_bootstrap_spelling.py 25 25 yes

No difference. The file sets are equal, and so are the changed lines of each file. So the restack carries only #973's own change: #958's work on main is not in it, and none of #948's lines (which e23137c7 did not hold and main does) shows up as a change.

The runs

Through run9.sh, with EMAIL=service@opensoft.dev, GIT_SSH_COMMAND=false, GIT_CONFIG_GLOBAL=/dev/null, the gh stub, at d286b289. Files: the pasteable file, test_assembly_root_unreadable_paths.py, test_bootstrap_non_utf8_branch.py, test_bootstrap_after_bump.py, test_bootstrap_spelling.py, test_pinned_copy_text.py, the trio (test_repo_hygiene.py, test_agents_md_codes.py, test_cli_reference.py) and test_adopt_reencoded_files.py.

git result log
2.43.0 (/usr/bin/git) 805 passed, 177 s restack-gate-243.log
2.47.1 ($Q/v115/w764/git247/bin/git) 805 passed, 175 s restack-gate-247.log

(The nine-file set had 752 at round 2. This run adds test_adopt_reencoded_files.py, and main's changes since then add tests to the files of the set; I did not split the difference.)

What the two rounds did (full account: round-on-958.md, same directory)

The PR body

body-973.md, same directory, is written from my body draft body.md (the version in that directory, which already held the coordinator's edit to its "Left out" section), not from gh pr view 973: I did not run gh at all. It adds items 6 to 8 (the NOT INITIALIZED command, the ./ for :, the Windows bytes), the new test, mutant and run numbers, the restack and the diff check, and "Left out" no longer says the line waits for #958. Its title ends (#961) and is 223 characters; it ends with the pinned-copy sentence and Closes #961. body-guard.py passes on it. It has no Lane or Claim line and no footer.

Left on disk, nothing running

  • Worktree /workspace/projects/openRepoShape-worktrees/l5-961-paste-words: clean, on the LOCAL branch stage/961-on-958 at d286b289.
  • Scratch clones under /workspace/projects/openRepoShape-worktrees/_lane5-scratch/: w961-base, w961-mut1, w961-mut2, w961-stg.
  • Basetemps $Q/v110/bt/w961-rs-*; the logs above under $Q/v118/w961/.
  • Local remote-tracking refs for main and fix/430-d-assembly-root-guards (the branch is gone from origin).

Lane 5's check of the restack, from objects: the -U0 changed lines of git diff af0e60f2 d286b289 (over #958's squash) hash the same as those of git diff 7e87e05f e23137c7 (the staged change rv973 found READY, above), and d286b289 is a fast-forward from b2887cf5. So rv973's READY at e23137c7 stands at d286b289, the head that lands.

Lane: openreposhape-5 (openRepoShape-5)

@brettheap
brettheap marked this pull request as ready for review October 20, 2026 04:29
@sourcery-ai

sourcery-ai Bot commented Oct 20, 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 3 days and 16 hours by commenting @sourcery-ai review. Upgrade to get a review now.

@brettheap
brettheap merged commit 5957f8c into main Oct 20, 2026
5 checks passed
@brettheap
brettheap deleted the fix/961-paste-words branch October 20, 2026 04:30
brettheap pushed a commit that referenced this pull request Oct 20, 2026
…lict, so #1025 is tested against main as it stands, with #983's change to validate-family.py and #1026's fix of main's two red rows (#936)

Main took twelve squashes since e076e3e: #1026, #958, #986, #1002,
#1016, #1011, #1021, #1022, #973, #975, #1015 and #983. Only #983
touches validate-family.py, in `_git_runs` and the member checks, apart
from this branch's hunks; it touches none of this branch's test files.

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