Before: reviewer rejected a publish → audit log flipped, topic stayed
in reviewing, no way to feed the critique back into the run pipeline.
Reviewers with revision notes had to eat them or hand-message the
coordinator.
Now: reject accepts an optional `notes` field. When present:
- Persisted on the research_publish_approvals row (migration 0039).
- Topic flips `reviewing → standby` so the next `start_topic` is legal.
- `start_topic` reads the most recent rejected-approval notes for the
topic and prepends "PRIOR REVIEW NOTES (address these in this
revision):\n<notes>\n---" to the coordinator task.
Loop closes through the same run pipeline — no new spawn code path,
which means the reviewer's guidance flows through the same
topology_worker, run_events, outcome-writer chain and lands as a fresh
research_outcomes row (versioned, prior drafts preserved). No notes on
reject = legacy behavior (topic stays in reviewing, publish requests
still allowed).
Migration 0039 adds nullable `notes TEXT` to
research_publish_approvals. `decide()` gains a `notes: Option<&str>`
parameter (only one caller, updated inline). New
`latest_rejection_notes(pool, topic_id)` helper for start_topic.
Frontend:
- rejectPublish(id, notes?) now sends a JSON body when notes are
provided.
- ResearchCanvas reject button opens an inline form with a textarea +
Cancel/"Send back for revision" pair. Empty notes → plain reject.
- Button label switches: "Send back for revision" when notes present,
"Reject without notes" when empty.
Follow-up:
- Notes shown in the review UI on the resulting draft so the next
reviewer sees what changed.
- Multiple rejection rounds — currently only the LATEST rejection's
notes surface. Accumulating history is a schema-only tweak.