Repository navigation
Show when a task is parked on a third party - #434
dkrattiger wants to merge 2 commits into
Conversation
A task awaiting someone else's review sits at `turn=user` and renders yellow — indistinguishable from work that is actually yours. The only way to find out was to open it, which is the round trip this removes. Of 36 live tasks, 32 were at `turn=user`; several were waiting on a reviewer. Adds `Task.waiting_on` (`WaitingOn`: external-review | ci), derived by the session service from the forge and rendered dim in the turn cell. NOT a third `Actor`, which was the obvious shape and the wrong one. `turn` is machine-driven — the container's Stop hook sets it to `user` and its UserPromptSubmit hook to `agent` on every turn boundary — so a third turn value would be clobbered the moment the agent did anything. And `Actor` is load-bearing across `turn_on_enter`, `advanced_by`, and responsibility gating (25 call sites), where a third party has no meaning: nothing external ever *advances* a task. Derived, not declared, and deliberately so. Nothing has to remember to set it and nothing has to remember to clear it: when the PR is approved or the checks go green the next pass reports None and the marker disappears. An agent skill that forgets to clear leaves a task looking parked forever — worse than no marker, because you learn to distrust it. Also distinct from `blocked`, which is the agent's own "I am stuck" and is cleared explicitly. Different lifecycles, so they stay separate fields; `blocked` still outranks this in the cell, since being stuck needs attention. The derivation refuses to guess. An unreadable PR (gh absent, rate-limited, unauthenticated) leaves the previous value alone rather than reporting None — marking a parked task actionable because a shell command failed is precisely the error this exists to prevent. CHANGES_REQUESTED and failing checks are *work*, not waits, so neither sets the marker. Reads are throttled to once a minute per task: the host wakes on the change feed, not a timer, so an unthrottled watcher would be one `gh` call per task per tick. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`reviewDecision == "REVIEW_REQUIRED"` is not evidence of an external wait. On a
branch with a protection rule it means only that *a* review is required and none
has been given — which, with nobody requested, makes that review ours.
Every open task PR here reads exactly that way:
#351 REVIEW_REQUIRED author=dkrattiger reviewRequests=[]
#10990 REVIEW_REQUIRED author=dkrattiger reviewRequests=[]
#10983 REVIEW_REQUIRED author=dkrattiger reviewRequests=[]
#10980 REVIEW_REQUIRED author=dkrattiger reviewRequests=[]
So the first cut would have labelled all four "ext review" and dimmed them —
hiding precisely the work most in need of attention, which is worse than no
marker at all. Verified against the live PRs: all four now derive to None.
Ask the narrower question instead: is a review pending from someone *other than
us*? Reads `reviewRequests` and compares against the authenticated login
(`gh api user`, resolved once and cached for the daemon's life). A requested team
counts as external — we are never a team.
Where it can't tell, it errs toward "ours". An unresolvable identity, or a request
naming only us, both fall through to no marker. The asymmetry is deliberate: a
wrong "external" hides work, while a missing one costs a glance.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Correction pushed — the derivation was backwards for this repo's PRs
So the first cut would have labelled all four "ext review" and dimmed them — hiding exactly the work most in need of attention. Worse than no marker, since you'd learn to distrust the column. The fix asks the narrower question: is a review pending from someone other than us? It reads Where it can't tell, it errs toward "ours": an unresolvable identity, or a request naming only us, both fall through to no marker. Deliberate asymmetry — a wrong "external" hides work, a missing one costs a glance. Verified against the live PRs — all four now derive to 7 new tests cover the shapes: nobody requested, only-us requested, case-insensitive login, team requested, mixed including us, unresolvable identity, and that the identity lookup happens once. 18 watcher tests, 498 across everything the change touches. 🤖 Generated with Claude Code |
What
Adds
Task.waiting_on— why a task is parked on someone who is neither you nor the agent — derived from the forge and rendered dim in the dashboard's turn cell.Why
A task awaiting someone else's review sits at
turn=userand renders yellow, indistinguishable from work that's actually yours. The only way to find out was to open it. On the live fleet: 32 of 36 tasks atturn=user, several parked on a reviewer.Why not a third
ActorThat was the obvious shape and it doesn't work:
turnis machine-driven. The container's Stop hook sets it touser; its UserPromptSubmit hook sets it toagent. Every turn boundary rewrites it, so a third value survives until the agent does one more thing.Actoris load-bearing acrossturn_on_enter,advanced_by, and responsibility gating — 25 call sites. A third party has no meaning there: nothing external ever advances a task.So this is a separate axis, which is what the turn can't carry.
Why derived, not declared
Nothing has to remember to set it, and — the part that matters — nothing has to remember to clear it. When the PR is approved or checks go green, the next pass reports
Noneand the marker disappears. An agent skill that forgets to clear leaves a task looking parked forever, which is worse than no marker at all, because you learn to distrust it.It's also distinct from
blocked(the agent's own "I am stuck", cleared explicitly). Different lifecycles, so separate fields — andblockedstill outranks this in the cell, since stuck needs attention while parked doesn't.The derivation refuses to guess
REVIEW_REQUIREDexternal-reviewciCHANGES_REQUESTEDThat last row is the one that matters.
ghabsent, rate-limited, or unauthenticated leaves the previous value alone rather than reportingNone— marking a parked task actionable because a shell command failed is exactly the error this feature exists to prevent.Notes
ghcall per task per tick. Review state moves on human timescales.server_defaultneeded (unlike thepausedone).Tests
11 for the watcher (full derivation table, the three gates, the throttle, and the unreadable-PR case), 3 for the API (round-trip through the store, lifecycle untouched, unknown reason → 422), 3 for the rendering (dim beats the turn colour,
blockedbeats dim, unknown reason still dims).Full suite passes (1,262). The two
test_tarot_*failures are pre-existing — they assert tarot isn't installed, and it is on this machine.🤖 Generated with Claude Code