Repository navigation
Description
Implementation: FlyBot (ours) — PR follows within 24h. Do not pick up without a pickup-ok comment on this issue.
Written-by: FlyBot Lead
Claimed-by: FlyBot Lead
Claim-Id: 0217a22190204dfc8d1a523db2dca665
Branch: dev/2-blocked-card-age
Base: 5f11035
At: 2026-11-01T11:19:39Z
What
The board's Blocked group answers why a card is stuck and nothing else: one line per card, the inputRequest.question. The Waiting group right next to it answers how long (Dev · 2h) and is sorted oldest-first. So the two groups are not comparable — a card blocked for three days and one blocked for three minutes render identically, and the card that has been stuck longest is not the one on top.
The host already has the stamp: inputRequest.requestedAt is written by core/src/host/tasks.js:841 and :1498, and preserved across a re-ask at :833 ({ ...task.inputRequest, question }). blockedOf simply drops it.
Expected
blockedOfreturns the same age shapewaitingOfdoes:since= therequestedAtstamp ornull,waitedMs=now - sinceornull. It keeps returning{ id, reason, … }—reasonstays the card's own line.buildQueuessortsblockedoldest-first with the comparatorwaiting.sortalready uses ((a.since ?? Infinity) - (b.since ?? Infinity) || a.id.localeCompare(b.id)); a card with no stamp sorts last.- The client's blocked row renders
reason · 2dwhenwaitedMsis a finite number and justreasonwhen it isnull. - A card with no
requestedAtshows no duration. Do not fall back toupdatedAtorcreatedAt: an invented stamp is worse than none. - The Waiting group's output stays byte-for-byte what it is today (existing assertions unchanged).
Reproduction at 5f110356
core/src/host/project-board.js:119—waitingOfreturns{ id, role, since, waitedMs }(since = stampOf(task.createdAt)at:118).core/src/host/project-board.js:135-138—blockedOfreturns{ id, reason }; no stamp is read.core/src/host/project-board.js:497(const blocked = []),:507(blockedOfcall),:508(blocked.push),:511— the only sort inbuildQueuesiswaiting.sort;blockedis returned unsorted.core/src/client/project-board.js:314— the blocked mapping keepsreasononly, so the view never sees an age.core/src/client/project-board.js:506— the blocked note ish('span', { className: 'bt-project-reason' }, row.reason).core/src/client/project-board.js:389-397—waitedLabelalready exists and is exported; nothing new is needed to format.
Acceptance (red first, one assertion per case, each able to go red on its own)
blockedOf({ id: 't1', status: 'blocked', inputRequest: { id: 'q', question: 'why', requestedAt: NOW - 48 * 3600e3 } }, NOW)returns{ id: 't1', reason: 'why', since: NOW - 48 * 3600e3, waitedMs: 172800000 }.- Host:
rt.projectBoardQueues('project-p1')with two blocked cards whoserequestedAtare 1 h and 48 h ago →blocked.map(i => i.id)has the 48 h card first. - Host: a blocked card whose
inputRequestis{ id, question }only →since === null && waitedMs === null(and no duration reaches the view). - View:
projectBoardView({ …, queues: { blocked: [{ id, reason, since, waitedMs: 172800000 }] } })renders a.bt-project-reasonwhose text ends with· 2d; withwaitedMs: nullthe text carries no duration at all. - Regression: the existing waiting assertions stay green unchanged —
core/test/project-board.test.jscompares[id, reason]/[id, role]pairs, andcore/test/project-board-view.test.js:259compares[id, role], so added fields must not break them.
Scope
core/src/host/project-board.js—blockedOf(:135-138) andbuildQueues(:497-512).core/src/client/project-board.js— the blocked mapping (:314) and the blocked note (:506).core/test/project-board.test.js— cases 1–3.core/test/project-board-view.test.js— case 4.core/client.jsand the patch mirrors are generator output: the host mirrorpatches/ds-bot/files/src/host/project-board.jsexists and is carried bypatches/ds-bot/0002-flybot-api-host.patch(only0002matches that path); the client mirrorpatches/ds-bot/files/src/client/project-board.jsexists and is carried bypatches/ds-bot/0008-client-bundle.patch. Rungenerate; do not hand-edit either.
Not in scope
- Changing what a blocked card's
reasonsays, or reading the question text from anywhere else — the reason line stays exactly as it is. - Any change to the Waiting or In flight groups, or to how
waitedLabelformats. - The 3-row cap on the details-panel scheduled-task list (separate).
Dedupe at 5f110356
- Tree grep
waitedMs→ only the waiting side:host/project-board.js:119,client/project-board.js:312,:503,:940;blockedOfhas none. - Tree grep
blocked.sort→ 0 hits (githubcode search onmain, 0 results). - Issue search
project board blocked card duration how long stuck→ 13 results, all closed and none this gap:#1768(introduced queues/leases/blocked cards — the parent feature),#1896(a card that waited under a minute read0m, waiting side),#1814,#1848,#1855,#1701,#1332,#1749,#1767,#1884. - Issue search
blocked queue reason waitedMs sort oldest first→ 1 result,#602(merge queuehold, unrelated).
Notes for whoever picks this up
- The existing fixture at
core/test/project-board.test.js:454has blocked cards withoutrequestedAt; the new cases have to add the stamp themselves rather than rely on it. #2026and#2028are in flight and both regeneratecore/client.js/patches/ds-bot/0008-client-bundle.patch. Branch from livemainright before starting, and rebase + rerungenerate/check/canonical/build-client --checkbefore opening the pull request, so the generated artifacts are computed on the base that will actually merge.
Activity
Project board: a blocked card says why it is stuck but never how long…
HugoXK commentedon Nov 1, 2026
Release
Written-by: FlyBot Lead
Released: 0217a22190204dfc8d1a523db2dca665
Reason: merged into main as 7c6883a
At: 2026-11-01T12:21:52Z