fix(cli): stop console encoding from failing a command that succeeded - #51
Merged
Conversation
`cl reconcile check` computed a GREEN verdict, then died printing it:
UnicodeEncodeError: 'charmap' codec can't encode character '\u2192'
`reconcile/check.py:132` renders cross-repo routing with an arrow glyph, and a
default Windows console is cp1252. Any worksheet carrying a cross-repo item --
the ordinary case, since routing work to its owning repo is the point -- reported
a passing gate as a traceback. The check had already finished; only the
formatting failed.
Fixed at the stream, not the glyph. Replacing the arrow would have been
whack-a-mole: the source carries nine distinct non-ASCII codepoints across ~940
occurrences, with output-bearing lines in `cli/ledger.py`, `reconcile/check.py`
and `cli/loop.py`, and any new report line could reintroduce it.
`ensure_printable_console()` prefers UTF-8 and falls back to `errors="replace"`,
so output degrades to `?` instead of raising. Installed as a Typer root callback,
which runs before every subcommand and takes no options, so the CLI surface is
unchanged.
Same fix and near-identical wording as Custodian's `cli/colors.py`, which hit
this in its verbose audit report. Duplicated rather than shared -- CL does not
depend on Custodian, and it is fifteen lines.
The fallback branch is not theoretical: a stream whose buffer is detached rejects
an encoding change but still accepts an errors change, so the guard retries with
errors alone rather than giving up on not-raising.
8 tests. The first asserts the cp1252 stream really does reject the report
glyphs -- without it the other seven could pass against a stream that was never
capable of failing. Verified end to end on a cp1252 console: crash before, GREEN
after. Suite 449 -> 457 passed; the 25 pre-existing failures are byte-identical
before and after, and the 2 `cryptography` collection errors are untouched.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ProtocolWarden
force-pushed
the
claude/console-encoding-guard
branch
from
August 3, 2026 19:07
9637824 to
d4bfb37
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
cl reconcile checkcomputes a GREEN verdict, then dies printing it:reconcile/check.py:132renders cross-repo routing with→, and a default Windows console is cp1252. So any worksheet carrying a cross-repo item reports a passing gate as a traceback — and that is the ordinary case, since routing work to its owning repo is the point of the field. The check had already finished; only the formatting failed.Found while drafting a
.console/reconcile.yamlfor Custodian that routes one item to ContextLifecycle.Fixed at the stream, not the glyph
Replacing
→would be whack-a-mole. The source carries nine distinct non-ASCII codepoints across ~940 occurrences (—×365,─×352,§×129,→×45,⇒×25,…,≥,✓,✗), with output-bearing lines incli/ledger.py,reconcile/check.py, andcli/loop.py. Any new report line could reintroduce the crash.ensure_printable_console()prefers UTF-8 and falls back toerrors="replace", so output degrades to?rather than raising. It is installed as a Typer root callback — runs before every subcommand, takes no options, so the CLI surface is unchanged (a test pins that).This is the same fix, and near-identical wording, as Custodian's
cli/colors.py, which hit this in its verbose audit report. Duplicated rather than shared: CL does not depend on Custodian, and it is fifteen lines.Before / after
Same cp1252 console, same worksheet:
Tests
8 tests. The first is the one that matters:
Without it, the other seven could pass against a stream that was never capable of failing. The rest cover both streams, idempotency, streams lacking
reconfigure(pytest capture,StringIO), the callback actually being wired, and the CLI surface being unchanged.The
errors-only fallback is not theoretical: a stream whose buffer is detached rejects an encoding change but still accepts an errors change, so the guard retries rather than giving up on not-raising.Verification
cryptographycollection errors intest_committed.py/test_signing.pyreproduce on unmodifiedmain— missing optional dep, unrelated🤖 Generated with Claude Code