Skip to content

fix(cli): stop console encoding from failing a command that succeeded - #51

Merged
ProtocolWarden merged 1 commit into
mainfrom
claude/console-encoding-guard
Aug 3, 2026
Merged

fix(cli): stop console encoding from failing a command that succeeded#51
ProtocolWarden merged 1 commit into
mainfrom
claude/console-encoding-guard

Conversation

@ProtocolWarden

Copy link
Copy Markdown
Owner

cl reconcile check computes a GREEN verdict, then dies printing it:

UnicodeEncodeError: 'charmap' codec can't encode character '\u2192'

reconcile/check.py:132 renders 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.yaml for 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 in cli/ledger.py, reconcile/check.py, and cli/loop.py. Any new report line could reintroduce the crash.

ensure_printable_console() prefers UTF-8 and falls back to errors="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:

BEFORE:  UnicodeEncodeError: 'charmap' codec can't encode character '\u2192'
AFTER:   - engine-refresh-telemetry: ... → owner=ContextLifecycle [done]
         check: GREEN — Custodian is reconcilable (prune-ready).

Tests

8 tests. The first is the one that matters:

def test_cp1252_stream_rejects_report_glyphs_before_the_guard():
    """Without the guard the failure is real — otherwise this suite proves nothing."""

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

  • New tests: 8 passed
  • Suite: 449 → 457 passed. The 25 pre-existing failures are byte-identical before and after (diffed by name, not just counted), and the 2 cryptography collection errors in test_committed.py / test_signing.py reproduce on unmodified main — missing optional dep, unrelated
  • Verified the worktree source was the one under test, since the venv has CL installed against the main checkout

🤖 Generated with Claude Code

`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
ProtocolWarden force-pushed the claude/console-encoding-guard branch from 9637824 to d4bfb37 Compare August 3, 2026 19:07
@ProtocolWarden
ProtocolWarden merged commit 246055f into main Aug 3, 2026
5 of 7 checks passed
@ProtocolWarden
ProtocolWarden deleted the claude/console-encoding-guard branch August 3, 2026 19:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant