blockedAt: persist and return the block's start time, so a blocked row's age is readable without a per-row event read
OPS-227 shipped `bwi blocked` (CLI v1.2.0, 893bc29) and found the triage axis it asked for is not obtainable from the API at any price short of a per-row read - the exact cost that row existed to remove. MEASURED, not assumed, against https://bwi.pensanta.com/api on 2026-10-05: - /workitems?open=1 returns createdAt, updatedAt, closedAt, blockedReason. There is NO blockedAt. - /events returns 404. There is no bulk events endpoint, so the only source for a block's start is GET per row. - 92 blocked rows live. Fetching each one's history to date its block is 950,575 bytes / 92 calls, versus 20,497 / 1 for the list itself. WHY THE MISSING FIELD MATTERS MORE THAN IT LOOKS. Blocked is waiting BY DESIGN, so staleness on a blocked row is not an alarm the way it is on inProgress - 'untouched 94 days' says nothing, 'blocked 94 days' is the thing a triage pass acts on. Without blockedAt the CLI can only print days_touched (last edit of ANY kind, reset by a comment), which it ships with the gap printed to stderr on every run precisely because relabelling it would be worse than omitting it. ASK, cheapest first: 1. Persist blockedAt on the status->blocked transition and return it from /workitems. The transition already writes a blocked event, so the timestamp exists; it is not currently projected onto the item. 2. Backfill it for the 92 open blocked rows from their existing blocked events. 3. (Optional, wider value) a bulk events endpoint so any set-level question about history stops costing N calls. Not self-serve: this is the API/DB side at bwi.pensanta.com (91.99.136.171), not the venus wi-http.service nor the CLI in sh.git. Taking it on my own seat because registry row 189 puts the basquetWi full-scope successor backup here - reassign if a live coder/db seat for that repo exists. Related: 4 of the 92 blocked rows carry an EMPTY blockedReason. Separate finding, worth a triage pass on its own.