Address action-demanding transitions to the assignee; neutral verdict label

Send-back/unblock/cancel by someone else now land in the worker's queue (only cancel did — send-backs woke nobody); done stays unaddressed.
The Request changes button drops the send-to-X phrasing — it's a board write, delivery is the queue's job.
This commit is contained in:
Rohit C Prasad
2026-08-17 15:37:09 -07:00
committed by Rohit P
parent b952ed1e1e
commit f26e01c1b6
4 changed files with 38 additions and 6 deletions
+13 -4
View File
@@ -545,12 +545,21 @@ class TeamStore:
f"illegal transition {current.value}{target.value}" f"illegal transition {current.value}{target.value}"
) )
self._check_transition_authority(actor, item, current, target) self._check_transition_authority(actor, item, current, target)
# Canceling an assigned item ADDRESSES the notice to its assignee — the # When someone ELSE moves your assigned item to a state that needs
# top-priority queue entry whose delivery (or in-flight interrupt) makes # YOUR action, the event is addressed to you: a send-back
# the worker actually stop, instead of finishing into the void. # (review→in_progress with feedback), an unblock, a cancel (whose
# delivery/in-flight interrupt makes the worker actually stop). This
# is a board write, not a message — delivery is the queue projection
# doing its job. DONE is deliberately unaddressed: waking a worker to
# say its finished item is finished would burn a turn for nothing.
action_needed = {
ItemState.IN_PROGRESS,
ItemState.BLOCKED,
ItemState.CANCELED,
}
recipient = ( recipient = (
item["assignee"] item["assignee"]
if target is ItemState.CANCELED if target in action_needed
and item["assignee"] and item["assignee"]
and item["assignee"] != actor.id and item["assignee"] != actor.id
else None else None
+1 -1
View File
@@ -111,7 +111,7 @@ test("item detail: timeline with attachment, worker link, request changes", asyn
// Request changes… discloses a comment box; sending returns the item to work // Request changes… discloses a comment box; sending returns the item to work
await detail.getByRole("button", { name: "Request changes…" }).click(); await detail.getByRole("button", { name: "Request changes…" }).click();
await detail.getByPlaceholder("What needs to change?").fill("totals drift on Tom"); await detail.getByPlaceholder("What needs to change?").fill("totals drift on Tom");
await detail.getByRole("button", { name: "Send to security" }).click(); await detail.getByRole("button", { name: "Request changes", exact: true }).click();
await expect(detail).toContainText("In progress"); await expect(detail).toContainText("In progress");
// switching rows switches the pane // switching rows switches the pane
await page.getByTestId("board-item-3").click(); await page.getByTestId("board-item-3").click();
+3 -1
View File
@@ -358,6 +358,8 @@ function DetailActions({
onChange={(e) => setChangesText(e.target.value)} onChange={(e) => setChangesText(e.target.value)}
/> />
<div className="board-changes-row"> <div className="board-changes-row">
{/* A board write, not a message: review → in_progress with the
comment attached; delivery to the assignee is the queue's job. */}
<button <button
className="board-btn primary" className="board-btn primary"
disabled={!changesText.trim()} disabled={!changesText.trim()}
@@ -365,7 +367,7 @@ function DetailActions({
onTransition(detail.id, "in_progress", changesText.trim()) onTransition(detail.id, "in_progress", changesText.trim())
} }
> >
Send to {detail.assignee || "worker"} Request changes
</button> </button>
<button className="board-btn ghost" onClick={() => setChangesOpen(false)}> <button className="board-btn ghost" onClick={() => setChangesOpen(false)}>
Cancel Cancel
+21
View File
@@ -408,6 +408,27 @@ def test_item_detail_timeline_and_blocker_fact(manager):
assert blocked["blocker"] == "need the staging tfvars" assert blocked["blocker"] == "need the staging tfvars"
def test_transitions_by_others_address_the_assignee(store):
"""A send-back or unblock is a board write, not a message — but the queue
projection addresses it to the worker whose action it demands (owner
question 2026-08-17 exposed the gap: only cancel was addressed)."""
item_id = assigned(store)
store.consume("swe-worker", store.pending_for("swe-worker")[-1]["seq"])
store.transition(SPACE, WORKER, item_id, "in_progress")
store.transition(SPACE, WORKER, item_id, "review", comment="ready")
assert store.pending_for("swe-worker") == [] # own moves never self-address
# the lead's send-back reaches the worker's queue, feedback attached
store.transition(SPACE, LEAD, item_id, "in_progress", comment="totals drift")
pending = store.pending_for("swe-worker")
assert [e["payload"]["to"] for e in pending] == ["in_progress"]
assert pending[-1]["payload"]["comment"] == "totals drift"
store.consume("swe-worker", pending[-1]["seq"])
# ...but done is deliberately unaddressed — no wake to hear "it's finished"
store.transition(SPACE, WORKER, item_id, "review", comment="fixed")
store.transition(SPACE, LEAD, item_id, "done")
assert store.pending_for("swe-worker") == []
def test_cancel_notice_is_addressed_to_the_assignee(store): def test_cancel_notice_is_addressed_to_the_assignee(store):
item_id = assigned(store) item_id = assigned(store)
store.consume("swe-worker", store.pending_for("swe-worker")[-1]["seq"]) store.consume("swe-worker", store.pending_for("swe-worker")[-1]["seq"])