Skip to content

Show when a task is parked on a third party - #434

Open
dkrattiger wants to merge 1 commit into
dimitri/pending-fixesfrom
panopticon/waiting-on-external
Open

dkrattiger wants to merge 1 commit into
dimitri/pending-fixesfrom
panopticon/waiting-on-external

Conversation

@dkrattiger

Copy link
Copy Markdown
Contributor

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=user and 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 at turn=user, several parked on a reviewer.

Why not a third Actor

That was the obvious shape and it doesn't work:

  • turn is machine-driven. The container's Stop hook sets it to user; its UserPromptSubmit hook sets it to agent. Every turn boundary rewrites it, so a third value survives until the agent does one more thing.
  • Actor is load-bearing across turn_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 None and 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 — and blocked still outranks this in the cell, since stuck needs attention while parked doesn't.

The derivation refuses to guess

PR state Result Why
Open, REVIEW_REQUIRED external-review
Open, checks pending ci Review wins if both — it outlives a check run
Open, CHANGES_REQUESTED none The reviewer handed it back. That's work.
Open, check failed none Also work — only unfinished is a wait
Merged / closed none Self-clearing
Unreadable unchanged See below

That last row is the one that matters. gh absent, rate-limited, or unauthenticated leaves the previous value alone rather than reporting None — marking a parked task actionable because a shell command failed is exactly the error this feature exists to prevent.

Notes

  • Throttled to one read per task per minute. The host wakes on the change feed, not a timer, so an unthrottled watcher would be one gh call per task per tick. Review state moves on human timescales.
  • Skips paused tasks — no container, no triage value, no reason to spend the call.
  • Unchanged values aren't re-posted, so it doesn't churn the change feed the dashboard long-polls.
  • Migration is nullable, so no server_default needed (unlike the paused one).
  • Unknown reasons render as their raw string rather than vanishing — a slightly ugly cell beats silently showing a parked task as actionable.

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, blocked beats 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

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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant