Repository navigation
bump-leg.py (a real bump) and update-shape.py apply run the project's own validators as children that cannot print its display name on a cp1252 stdout, so a project named 日本 is refused and rolled back as bump-leg-validators-red or update-validators-red under PYTHONIOENCODING=cp1252:surrogateescape, and the refusal blames the validators #254
Description
Lane: openreposhape-5 (openRepoShape-5)
Found by this lane's adversarial review of #214's fix (PR #246, fix/plan-report-stdout-encoding) as S1, and scoped out of that PR as a separate change. It composes with lane 3's PR #230 (fix/decode-git-output-utf8), which rewrites the same bump-leg.py run_validator call (see Expected). Pre-existing on main 54aa69a: there the tools raise earlier, at their own prints, which #246 fixes; the validators below fail the same way on main, behind that.
What is wrong. bump-leg.py and update-shape.py apply run the PROJECT's own validators as child processes after they rewrite the pin: bump-leg.py's run_validator (scripts/bump-leg.py:386) captures the child's output, and update-shape.py's (update-shape.py:1424) lets the child inherit stdout. The children are shape copies (scripts/validate-pins.py, scripts/validate-manifest.py), and they do not call #246's escape_what_stdout_cannot_encode. A child takes its stdout CONFIGURATION from the environment (PYTHONIOENCODING, the locale) but not the parent's reconfigure. So on a stdout whose code page cannot hold the project's display name, validate-manifest.py raises UnicodeEncodeError at print(f"manifest ok: {manifest.get('name')} "... (templates/assembly-root/scripts/validate-manifest.py:707), exits 1, and the bump or apply is refused and rolled back for a reason that has nothing to do with the pins. The reach is any project whose display name holds a character the console's code page lacks: a redirected stdout on a Western-European Windows is cp1252 with surrogateescape (CPython's Windows default), which holds no CJK name; a Japanese Windows uses cp932, which holds 日本, and other ANSI code pages differ. I cannot run Windows; the run below emulates it with PYTHONIOENCODING=cp1252:surrogateescape.
SHAPE=<checkout of fix/plan-report-stdout-encoding at 2d60c3e (PR #246)>
SHAPE_BUMP=<the same checkout with fix/decode-git-output-utf8 at fbdf8db (PR #230) merged in, uncommitted>
TMP=$(mktemp -d); cd "$TMP"
ENC=cp1252:surrogateescape # a redirected stdout on a Western-European Windows
GIT='git -c user.name=T -c user.email=t@example.invalid'
# the shape at A (a clone of $SHAPE), a project scaffolded from it, and the shape's commit B: one template changes
git clone -q "$SHAPE" shape && ( cd shape && git checkout -q --detach HEAD )
python3 shape/scaffold-project.py --org testorg --project Atlas --elected-by 'T H' --elected-on 2026-09-02 \
--local-remote-dir remotes --work-dir work >scaffold.out 2>&1 || exit 1
git -c protocol.file.allow=always clone -q --recurse-submodules remotes/Atlas.git base || exit 1
git clone -q remotes/Atlas-spec.git adv && ( cd adv && printf '# new\n' > NEW.md && git add NEW.md \
&& $GIT commit -qm advance && git push -q origin HEAD:refs/heads/adv ) && NEW=$(git -C adv rev-parse HEAD)
( cd shape && printf '\n# an upstream fix\n' >> templates/assembly-root/scripts/validate-manifest.py \
&& $GIT commit -qm B -- templates/assembly-root/scripts/validate-manifest.py ) && B=$(git -C shape rev-parse HEAD)
# the project is renamed to a display name cp1252 cannot encode (a Japanese Windows' cp932 can, others cannot)
rename() { sed -i 's/^name: "Atlas"$/name: "日本"/' project.yaml && $GIT commit -qam rename && git checkout -q -b work; }
run() { # run <label> <command...>: on a fresh copy of the renamed project
label=$1; shift; rm -rf "p-$label"; cp -a base "p-$label"; ( cd "p-$label" && rename )
( cd "p-$label" && "$@" ) >"o-$label" 2>"e-$label"; echo "== $label: exit $?"
grep -a -E '^UnicodeEncodeError|^REFUSED [a-z-]*' "o-$label" "e-$label" | sed 's/^[^:]*://' | cut -c1-90 | sort -u
}
export PYTHONIOENCODING=$ENC
run validator python3 scripts/validate-manifest.py
run bump python3 "$SHAPE_BUMP/scripts/bump-leg.py" --root . --leg spec --to "$NEW" --local-remote-dir "$TMP/remotes"
run update python3 "$SHAPE/update-shape.py" apply --root . --yes --upstream "$TMP/shape" --at "$B"
unset PYTHONIOENCODING
rm -rf p-control && cp -a base p-control && ( cd p-control && git checkout -q -b work \
&& PYTHONIOENCODING=$ENC python3 "$SHAPE/update-shape.py" apply --root . --yes --upstream "$TMP/shape" --at "$B" >/dev/null 2>&1; echo "== control (no rename), update-shape apply: exit $?" )Observed (the script cuts each line at 90 characters; each REFUSED line goes on, in the tool's own output, with "Every byte this command wrote has been rolled back"):
== validator: exit 1
UnicodeEncodeError: 'charmap' codec can't encode characters in position 13-14: character m
== bump: exit 2
REFUSED bump-leg-validators-red: scripts/validate-manifest.py exited 1 after the rewrite.
UnicodeEncodeError: 'charmap' codec can't encode characters in position 13-14: character m
== update: exit 2
REFUSED update-validators-red: after the rewrite, scripts/validate-manifest.py refused. Ev
UnicodeEncodeError: 'charmap' codec can't encode characters in position 13-14: character m
== control (no rename), update-shape apply: exit 0
The control, with the display name Atlas, applies and exits 0, so the refusal is the name. Both tools roll back correctly, so no tree is damaged: a project named 日本 simply cannot be bumped or re-synced under such a stdout, and the refusal blames the validators. PYTHONIOENCODING=utf-8:strict takes the same commands to exit 0. On Linux the … in the pin validator's output is read by a UTF-8 locale as U+FFFD, an artefact of the emulation: on a Western-European Windows the locale codec is cp1252 and reads it back. Without #230 merged, the bump run dies earlier on the parent's decode of that output (UnicodeDecodeError, #230's subject), not on this validator.
Expected. A real bump and an apply on a project named 日本 succeed under a cp1252 stdout. The fix belongs in the two tools, not in the shipped validators (shape copies, digest-pinned in each project's contracts/shape-pin.yaml); shape-doctor.py:823 already sets PYTHONIOENCODING=utf-8 for the same validators. So give the child an explicit PYTHONIOENCODING:
- Where the output is captured (
bump-leg.py),utf-8, decoded the same way, so the two interpreters agree whatever the locale is. - For
update-shape.py's inherited fd,<the parent's stdout encoding>:backslashreplace. A child cannot use a handler only the parent registered: a child givenPYTHONIOENCODING=<encoding>:<unknown handler>still STARTS, then fails withLookupError: unknown error handler name ...at the first character it cannot encode (only-X devfails at startup, withinit_sys_streams), so the handler name adopt-project.py and five other tools escape a name their stdout cannot encode, soplanon日本.mdunder a cp1252 stdout (strict, or Windows' default surrogateescape) no longer exits 1 after writing the plan (#214) #246 registers,openreposhape.surrogateescape-then-backslashreplace, must not be passed on.
It composes with #230, which gives bump-leg.py's run_validator errors="replace" and no encoding=, "so that codec is the one that reads it back": the two changes choose one decoding for that call. The #214 writer's trial git merge-tree of the utf-8 variant against #230's head conflicted in scripts/bump-leg.py (not re-run here), so this lands after #230 and chooses one decoding for that call: errors="replace" with an explicit encoding="utf-8", once the child is told to write UTF-8. Tests: a real (not --dry-run) bump against local bare remotes on a project renamed 日本, and update-shape.py apply the same way, each under PYTHONIOENCODING=cp1252:surrogateescape, as the script above runs them.
Reach. Anyone whose project display name holds a character outside the Windows code page of a stdout that is not the console (a file, tee, CI, an agent: the audience of AGENTS.md), running a real bump-leg.py or update-shape.py apply. A --dry-run bump runs no validator and is unaffected. shape-doctor.py is not affected, since it sets the child's encoding. Nothing in this repository writes such a name: scaffold-project.py writes name: from --project and refuses a --project outside [A-Za-z0-9._/@+~-] (REFUSED unsafe-value, checked on main with --project 日本), so the name reaches the manifest by a hand edit.
Found by. handoffs/openRepoShape/attachments/lane-openRepoShape-5/review214-report.md (S1) and rereview214-report.md (S1 scoped out; R3 corrected two sentences of the first text) in opensoft/brett-wip. The three runs above are new for this issue, on #246's head 2d60c3e and a throwaway merge of #230's head fbdf8db.
Not claimed.
🤖 Generated with Claude Code
Activity
adopt-project.py and five other tools escape a name their stdout cann…
brettheap commentedon Oct 11, 2026
CLAIMED — lane openRepoShape-5, session e62c2fd7-176d-429d-89fb-0653dd08ad5f@Eagle, 2026-10-11T22:26:55Z, for #254
Logged in
lanes/log/openRepoShape-5.mdatfe2cb84cce60d6f767954631822269a4762f3f8d.The three reads (lane-collision-protocol Rule 1):
tests: test_validator_child_encoding compares the applied copy with t…
Merge origin/main (b421e6b) into fix/adopt-template-values-utf8: #231'…
Merge origin/main b421e6b into fix/adopt-relative-submodule-url: #196'…
Merge origin/main (71b58a1: #238 via #287, #196 via #231, #254 via #281…
Merge origin/main (71b58a1) into fix/adopt-plan-quote-yaml-keywords
Merge origin/main (71b58a1) into fix/adopt-late-failure-notice
Merge origin/main 71b58a1 into fix/adopt-root-kept-symlinks: #204, #203…
siblings.py and family.py: the printed move runs as typed on a disk t…
siblings.py and family.py: `make siblings` asks about the family fold…
tests/: the quoting-sentence inventory sees a quoter handed on as a v…
setup-project.py and the assembly root's bootstrap.py: every Python c…