@joshuafolkken/kit 1.1387.0 → 1.1435.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (236) hide show
  1. package/.claude/skills/epic-commands/SKILL.md +1 -1
  2. package/.claude/skills/epic-commands/epic-bundle.md +5 -0
  3. package/.claude/skills/workflow-commands/backlogrun-lanes.md +5 -3
  4. package/.claude/skills/workflow-commands/backlogrun-steps.md +4 -3
  5. package/.claude/skills/workflow-commands/backlogrun.md +1 -0
  6. package/.claude/skills/workflow-commands/chain-rule.md +4 -2
  7. package/.claude/skills/workflow-commands/issue-comments.md +6 -0
  8. package/.claude/skills/workflow-commands/observation-ledger.md +3 -2
  9. package/.claude/skills/workflow-commands/pre-gate-cut.md +5 -3
  10. package/README.md +25 -0
  11. package/dist/hooks/audit-provision.js +2 -2
  12. package/dist/hooks/batch-guard.js +7 -7
  13. package/dist/hooks/{chunk-IYVOLN6A.js → chunk-2G5F2YCD.js} +3 -3
  14. package/dist/hooks/{chunk-2YSW3CXH.js → chunk-3LE6VLSR.js} +17 -6
  15. package/dist/hooks/{chunk-J3KSS7II.js → chunk-4BHUPFKF.js} +1 -1
  16. package/dist/hooks/{chunk-EAQEGME7.js → chunk-5SJSSCJG.js} +1 -1
  17. package/dist/hooks/{chunk-FZ6FUSBM.js → chunk-723HKZO5.js} +1 -1
  18. package/dist/hooks/{chunk-LMWBRJIP.js → chunk-7KMN5QB5.js} +3 -3
  19. package/dist/hooks/{chunk-NEC4AT4Z.js → chunk-DGT54IH5.js} +4 -0
  20. package/dist/hooks/{chunk-STDSEPWU.js → chunk-EMB55BDF.js} +4 -4
  21. package/dist/hooks/{chunk-JBBMAPC4.js → chunk-IX6TTTI6.js} +4 -0
  22. package/dist/hooks/{chunk-GJ7JPIHN.js → chunk-JXQBK323.js} +2674 -187
  23. package/dist/hooks/{chunk-DCY2GWHC.js → chunk-MSYSUSY6.js} +5 -5
  24. package/dist/hooks/{chunk-EBSHN25W.js → chunk-MU4LJFIV.js} +4 -4
  25. package/dist/hooks/{chunk-WNFJLBXI.js → chunk-QL6QTPWN.js} +3 -3
  26. package/dist/hooks/{chunk-SL4GQ2XP.js → chunk-XER6SSIB.js} +91 -18
  27. package/dist/hooks/{chunk-U32VSRNG.js → chunk-YW452ZAX.js} +89 -34
  28. package/dist/hooks/{chunk-GNDJ7NJ7.js → chunk-YZXYOAZ3.js} +202 -55
  29. package/dist/hooks/codex-hook-adapter.js +15 -15
  30. package/dist/hooks/duplicate-read-guard.js +8 -8
  31. package/dist/hooks/format-edited.js +5 -5
  32. package/dist/hooks/inputs.json +288 -229
  33. package/dist/hooks/investigation-guard.js +8 -8
  34. package/dist/hooks/pretool-guard.js +14 -14
  35. package/dist/hooks/session-lang.js +69 -16
  36. package/dist/hooks/stop-guard.js +13 -38
  37. package/dist/josh.js +62 -9
  38. package/package.json +4 -3
  39. package/prompts/review.md +22 -0
  40. package/scripts/agent/claude-agent-argv.ts +13 -1
  41. package/scripts/auth/auth-setup-cli.ts +101 -0
  42. package/scripts/auth/claude-token-output.ts +78 -0
  43. package/scripts/auth/claude-token.ts +68 -0
  44. package/scripts/backlog/backlog-drive-epic.ts +24 -8
  45. package/scripts/backlog/backlog-drive-launch.ts +3 -0
  46. package/scripts/backlog/backlog-drive-named-offer.ts +7 -2
  47. package/scripts/backlog/backlog-drive-named.ts +4 -2
  48. package/scripts/backlog/backlog-drive-unread.ts +56 -0
  49. package/scripts/backlog/backlog-drive.ts +16 -8
  50. package/scripts/backlog/backlog-overlap.ts +22 -5
  51. package/scripts/backlog/backlog-ready.ts +1 -3
  52. package/scripts/backlog/backlogrun-args.ts +13 -2
  53. package/scripts/backlog/backlogrun-cli.ts +91 -19
  54. package/scripts/backlog/backlogrun-launch.ts +24 -0
  55. package/scripts/backlog/backlogrun-replace.ts +166 -0
  56. package/scripts/backlog/backlogrun-stop.ts +178 -0
  57. package/scripts/behavior/behavior-cli.ts +6 -1
  58. package/scripts/doctor/doctor-io.ts +60 -9
  59. package/scripts/doctor/doctor.ts +20 -1
  60. package/scripts/document/document-byte-budget.ts +3 -3
  61. package/scripts/epic/epic-bundle-cli.ts +8 -8
  62. package/scripts/epic/epic-bundle-referenced.ts +2 -5
  63. package/scripts/epic/epic-bundle.ts +39 -4
  64. package/scripts/exports/unused-members-cli.ts +2 -1
  65. package/scripts/followup/followup-checks-event.ts +82 -0
  66. package/scripts/followup/git-followup-finish.ts +16 -2
  67. package/scripts/followup/git-followup-workflow.ts +5 -2
  68. package/scripts/followup/git-pr-followup-checks.ts +18 -4
  69. package/scripts/followup/git-pr-followup-wrapup.ts +5 -0
  70. package/scripts/followup/git-pr-followup.ts +4 -1
  71. package/scripts/fresh-machine/fresh-machine-cli.ts +204 -0
  72. package/scripts/fresh-machine/fresh-machine-docker.ts +65 -0
  73. package/scripts/fresh-machine/fresh-machine-image.ts +62 -0
  74. package/scripts/fresh-machine/fresh-machine-log.ts +61 -0
  75. package/scripts/fresh-machine/fresh-machine-patterns.ts +130 -0
  76. package/scripts/fresh-machine/fresh-machine-run.ts +215 -0
  77. package/scripts/fresh-machine/fresh-machine-script.ts +45 -0
  78. package/scripts/fresh-machine/fresh-machine-spec.ts +57 -0
  79. package/scripts/fresh-machine/specs/kit-test-backlogrun-python.json +34 -0
  80. package/scripts/fresh-machine/specs/kit-test-backlogrun.json +31 -0
  81. package/scripts/gate/gate-report.ts +70 -15
  82. package/scripts/gate/project-checks.ts +13 -2
  83. package/scripts/gate/scoped-green.ts +2 -1
  84. package/scripts/gate/verification-gate.ts +4 -3
  85. package/scripts/gh/gh-author-association.ts +14 -0
  86. package/scripts/gh/git-gh-conditional.ts +66 -0
  87. package/scripts/gh/git-gh-exec.ts +1 -1
  88. package/scripts/gh/git-gh-issue-read.ts +6 -3
  89. package/scripts/gh/git-gh-issue-write.ts +5 -1
  90. package/scripts/gh/git-gh-issue.ts +4 -1
  91. package/scripts/gh/git-pr-checks-eval.ts +13 -0
  92. package/scripts/gh/git-pr-checks-parse.ts +8 -1
  93. package/scripts/gh/git-pr-checks-watch.ts +11 -4
  94. package/scripts/gh/git-pr-checks.ts +36 -2
  95. package/scripts/git/git-command.ts +16 -4
  96. package/scripts/git/git-schemas.ts +1 -0
  97. package/scripts/git/git-spawn-sync.ts +1 -1
  98. package/scripts/hooks/pretool-guard.ts +3 -1
  99. package/scripts/init/hook-command-rewrite.ts +31 -0
  100. package/scripts/init/init-actions.ts +4 -1
  101. package/scripts/init/init-ai-copy.ts +33 -10
  102. package/scripts/init/init-basic.ts +8 -3
  103. package/scripts/init/init-logic-toolchain.ts +76 -0
  104. package/scripts/init/init-logic.ts +2 -0
  105. package/scripts/init/init.ts +5 -23
  106. package/scripts/init/project-profile.ts +36 -7
  107. package/scripts/init/start-exec.ts +27 -1
  108. package/scripts/init/start-plan.ts +15 -0
  109. package/scripts/init/start-steps.ts +46 -0
  110. package/scripts/init/start.ts +1 -0
  111. package/scripts/init/workflow-mark.ts +35 -0
  112. package/scripts/issue/issue-ack-cli.ts +84 -0
  113. package/scripts/issue/issue-labels.ts +40 -8
  114. package/scripts/issue/issue-state-cli.ts +14 -4
  115. package/scripts/issue/issue-watch-diff.ts +152 -0
  116. package/scripts/issue/issue-watch-gate.ts +81 -0
  117. package/scripts/issue/issue-watch-hook.ts +54 -0
  118. package/scripts/issue/issue-watch.ts +240 -0
  119. package/scripts/issue/measure-rerun-cli.ts +3 -4
  120. package/scripts/josh/josh-command-map.ts +4 -0
  121. package/scripts/josh/josh-commands-backlog.ts +2 -2
  122. package/scripts/josh/josh-commands-development.ts +1 -1
  123. package/scripts/josh/josh-commands-issue.ts +7 -0
  124. package/scripts/josh/josh-commands-lane.ts +11 -2
  125. package/scripts/josh/josh-commands-project.ts +24 -0
  126. package/scripts/josh/josh-verdict.ts +33 -4
  127. package/scripts/lane/claude-lane-broker.ts +142 -0
  128. package/scripts/lane/lane-await.ts +6 -2
  129. package/scripts/lane/lane-cache.ts +37 -7
  130. package/scripts/lane/lane-capacity.ts +8 -6
  131. package/scripts/lane/lane-child-invocation.ts +1 -0
  132. package/scripts/lane/lane-cli.ts +15 -1
  133. package/scripts/lane/lane-close.ts +15 -6
  134. package/scripts/lane/lane-dispatch.ts +36 -10
  135. package/scripts/lane/lane-environment.ts +1 -1
  136. package/scripts/lane/lane-launch-cli.ts +13 -4
  137. package/scripts/lane/lane-ledger.ts +21 -3
  138. package/scripts/lane/lane-measure-cli.ts +10 -2
  139. package/scripts/lane/lane-open.ts +9 -1
  140. package/scripts/lane/lane-preserve.ts +49 -0
  141. package/scripts/lane/lane-reap.ts +20 -5
  142. package/scripts/lane/lane-registry.ts +6 -0
  143. package/scripts/lane/lane-restart-cli.ts +51 -0
  144. package/scripts/lane/lane-restart.ts +175 -0
  145. package/scripts/lane/{openai-review-broker.ts → lane-session-broker.ts} +106 -28
  146. package/scripts/lane/lane-spent.ts +195 -0
  147. package/scripts/lane/lane-stage-stats.ts +27 -3
  148. package/scripts/lane/lane-stop-stats.ts +96 -0
  149. package/scripts/lane/openai-lane-supervisor.ts +2 -2
  150. package/scripts/lane/process-tools.ts +31 -0
  151. package/scripts/lib/buffered-process.ts +36 -2
  152. package/scripts/lib/pnpm-reach.ts +27 -0
  153. package/scripts/lib/status-icons.ts +5 -1
  154. package/scripts/lint/lint-parallel.ts +19 -2
  155. package/scripts/observations/observation-ledger-home.ts +1 -0
  156. package/scripts/review/review-attest-cli.ts +21 -6
  157. package/scripts/review/review-brief-cli.ts +2 -2
  158. package/scripts/review/review-comment-round.ts +114 -0
  159. package/scripts/review/review-record-cli.ts +39 -17
  160. package/scripts/rules/implementation-cut.ts +3 -3
  161. package/scripts/rules/pre-gate-cut.ts +1 -1
  162. package/scripts/run/board/run-board-checks.ts +42 -0
  163. package/scripts/run/board/run-board-cli.ts +2 -0
  164. package/scripts/run/board/run-board-every.ts +3 -1
  165. package/scripts/run/board/run-board-fit.ts +16 -5
  166. package/scripts/run/board/run-board-fresh.ts +64 -0
  167. package/scripts/run/board/run-board-header.ts +129 -53
  168. package/scripts/run/board/run-board-labels.ts +47 -4
  169. package/scripts/run/board/run-board-lane-event.ts +66 -0
  170. package/scripts/run/board/run-board-layout.ts +57 -9
  171. package/scripts/run/board/run-board-now.ts +234 -0
  172. package/scripts/run/board/run-board-operation-read.ts +75 -0
  173. package/scripts/run/board/run-board-operation.ts +150 -0
  174. package/scripts/run/board/run-board-phase.ts +26 -1
  175. package/scripts/run/board/run-board-plan.ts +4 -6
  176. package/scripts/run/board/run-board-read.ts +49 -3
  177. package/scripts/run/board/run-board-reason.ts +124 -0
  178. package/scripts/run/board/run-board-render-checks.ts +55 -0
  179. package/scripts/run/board/run-board-render-legend.ts +12 -3
  180. package/scripts/run/board/run-board-render-slot.ts +2 -2
  181. package/scripts/run/board/run-board-render-tail.ts +128 -0
  182. package/scripts/run/board/run-board-render-thanks.ts +50 -0
  183. package/scripts/run/board/run-board-render-title.ts +58 -0
  184. package/scripts/run/board/run-board-render.ts +96 -80
  185. package/scripts/run/board/run-board-row-yield.ts +32 -0
  186. package/scripts/run/board/run-board-setup.ts +61 -13
  187. package/scripts/run/board/run-board-state.ts +10 -0
  188. package/scripts/run/board/run-board-status.ts +66 -7
  189. package/scripts/run/board/run-board-thanks-csv.ts +71 -0
  190. package/scripts/run/board/run-board-thanks-read.ts +112 -0
  191. package/scripts/run/board/run-board-thanks.ts +53 -0
  192. package/scripts/run/board/run-board-tick.ts +50 -21
  193. package/scripts/run/board/run-board-usual.ts +44 -0
  194. package/scripts/run/carry/run-carry-args.ts +9 -1
  195. package/scripts/run/carry/run-carry-cli.ts +2 -45
  196. package/scripts/run/carry/run-carry-end.ts +73 -0
  197. package/scripts/run/carry/run-carry.ts +8 -1
  198. package/scripts/run/entry/run-entry-cli.ts +5 -0
  199. package/scripts/run/event/run-event-checks.ts +54 -0
  200. package/scripts/run/event/run-event-render.ts +2 -0
  201. package/scripts/run/event/run-event-scope.ts +1 -0
  202. package/scripts/run/event/run-event-setup.ts +55 -11
  203. package/scripts/run/event/run-event-stream.ts +27 -11
  204. package/scripts/run/merge/run-merge-cli.ts +7 -4
  205. package/scripts/run/merge/run-merge-ship.ts +61 -0
  206. package/scripts/run/merge/run-merge-steps.ts +7 -14
  207. package/scripts/run/merge/run-merge-token.ts +16 -2
  208. package/scripts/run/run-headless.ts +1 -0
  209. package/scripts/run/run-step.ts +2 -0
  210. package/scripts/run/ship/run-ship-cli.ts +22 -2
  211. package/scripts/run/ship/run-ship-return.ts +31 -13
  212. package/scripts/run/ship/run-ship-review-steps.ts +17 -4
  213. package/scripts/run/ship/run-ship-stage.ts +1 -0
  214. package/scripts/run/wake/run-wake-await.ts +130 -0
  215. package/scripts/run/wake/run-wake-cli.ts +60 -71
  216. package/scripts/run/wake/run-wake-describe.ts +34 -2
  217. package/scripts/run/wake/run-wake-driver.ts +11 -4
  218. package/scripts/run/wake/run-wake-session.ts +20 -3
  219. package/scripts/run/wake/run-wake-stop.ts +37 -0
  220. package/scripts/run/wake/run-wake-supervise.ts +44 -0
  221. package/scripts/sync/claude-settings-merge.ts +144 -0
  222. package/scripts/sync/sync-ai-files.ts +28 -3
  223. package/scripts/sync/sync.ts +15 -0
  224. package/scripts/test/project-test-script.ts +73 -0
  225. package/scripts/test/skip-marker.ts +14 -1
  226. package/scripts/test/test-related.ts +3 -2
  227. package/scripts/test/test-unit-guard.ts +78 -14
  228. package/scripts/time-runtime/time-reported-failure.ts +11 -3
  229. package/scripts/uninstall/distributed-files.ts +92 -0
  230. package/scripts/uninstall/uninstall-plan.ts +119 -0
  231. package/scripts/uninstall/uninstall-report.ts +61 -0
  232. package/scripts/uninstall/uninstall.ts +206 -0
  233. package/templates/AGENTS.basic.md +4 -0
  234. package/templates/GEMINI.basic.md +6 -0
  235. package/dist/hooks/chunk-6V5OPIUU.js +0 -13
  236. package/scripts/run/board/run-board-track.ts +0 -22
@@ -4,10 +4,9 @@ import {
4
4
  type NotesPart,
5
5
  type PlanLine,
6
6
  type PlanSection,
7
- type RowYield,
8
7
  type TerminalSize,
9
- type YieldStage,
10
8
  } from './run-board-fit'
9
+ import { run_board_fresh } from './run-board-fresh'
11
10
  import { run_board_header, type BoardHeader } from './run-board-header'
12
11
  import { run_board_labels } from './run-board-labels'
13
12
  import {
@@ -16,13 +15,17 @@ import {
16
15
  type BoardRow,
17
16
  type WaveEntry,
18
17
  } from './run-board-layout'
18
+ import type { Link } from './run-board-link'
19
19
  import type { BoardNote } from './run-board-notes'
20
- import type { Phase } from './run-board-phase'
20
+ import { run_board_render_checks } from './run-board-render-checks'
21
21
  import { run_board_render_legend } from './run-board-render-legend'
22
22
  import { run_board_render_notes } from './run-board-render-notes'
23
23
  import { run_board_render_slot, type UsageSlot } from './run-board-render-slot'
24
- import type { ItemState } from './run-board-status'
25
- import { run_board_track } from './run-board-track'
24
+ import { run_board_render_tail } from './run-board-render-tail'
25
+ import { run_board_render_thanks } from './run-board-render-thanks'
26
+ import { run_board_render_title } from './run-board-render-title'
27
+ import { run_board_row_yield } from './run-board-row-yield'
28
+ import type { Thanks } from './run-board-thanks-csv'
26
29
 
27
30
  // The whole `run:board` screen as lines — pure, so what a person sees is tested apart from the
28
31
  // terminal. The header says where the run is; the sections below it follow the plan's order; the
@@ -31,14 +34,14 @@ import { run_board_track } from './run-board-track'
31
34
  // what differs between rows. Every word is English whatever the session language: one set of words
32
35
  // reads the same on every board.
33
36
 
34
- const { FILED_KIND_ICONS, KIND_BLANK, NOTES_ICON, PHASE_ICONS, STATE_ICONS, WAITS_ICON, WORDS } =
37
+ const { FILED_KIND_ICONS, KIND_BLANK, NOTES_ICON, NOW_ICONS, PHASE_ICONS, STATE_ICONS } =
35
38
  run_board_labels
39
+ const { WAITS_ICON, WORDS } = run_board_labels
36
40
  const { clock_of, elapsed_of } = run_board_labels
37
41
  const { NOTE_LIMIT } = run_board_render_notes
38
42
  const { cell_of, column_width, slot_of, usage_slot } = run_board_render_slot
39
- // The columns a terminal row gives its title: a longer one is cut to an ellipsis.
40
- const TITLE_LIMIT = 48
41
- const ELLIPSIS = '…'
43
+ const { ELLIPSIS, TITLE_LIMIT, fitted, reason_text, title_of } = run_board_render_title
44
+ const { padded } = run_board_fresh
42
45
  const GAP = ' '
43
46
  const INDENT = ' '
44
47
  const BRANCH = ' ├ '
@@ -59,6 +62,9 @@ interface BoardView extends FrameBounds {
59
62
  notes: ReadonlyArray<BoardNote>
60
63
  // The session a stopped run resumes from.
61
64
  resume?: string | undefined
65
+ // The reviewers thanked above the legend, and the link a name with a page is wrapped in.
66
+ thanks?: Thanks | undefined
67
+ link?: Link | undefined
62
68
  }
63
69
 
64
70
  // What every row of one frame is drawn with: the header, how wide the time column is, so a three-digit
@@ -69,11 +75,10 @@ interface RowFrame {
69
75
  time_width: number
70
76
  usage: UsageSlot | undefined
71
77
  slot_width: number
72
- }
73
-
74
- // A title cut to the title column, so every time after it starts in one column.
75
- function title_of(title = ''): string {
76
- return title.length > TITLE_LIMIT ? `${title.slice(0, TITLE_LIMIT - 1)}${ELLIPSIS}` : title
78
+ // The columns a row's title is cut to: the title column's, or fewer where a row gives some up.
79
+ title_limit: number
80
+ // The pane's width where the frame is kept within one.
81
+ columns: number | undefined
77
82
  }
78
83
 
79
84
  // A row drawn before the plan is read has no title yet, and says so.
@@ -105,45 +110,37 @@ function time_of(row: BoardRow, now_ms: number): string | undefined {
105
110
  return elapsed === undefined ? undefined : elapsed_of(elapsed)
106
111
  }
107
112
 
108
- // The phases a row's track draws — every one the child passed while running, kept once it settles.
109
- function drawn_phases(row: BoardRow): ReadonlyArray<Phase> {
110
- return row.status?.track ?? []
111
- }
112
-
113
- // A row's phases as its track, drawn last: it grows with every round, so the
114
- // columns before it stay where they are whatever its length. A settled row's ends on its state icon.
115
- function track_text(row: BoardRow): string | undefined {
116
- const phases = drawn_phases(row)
117
-
118
- return phases.length === 0 ? undefined : run_board_track.track_of(phases, row.state)
119
- }
120
-
121
113
  function waits_of(row: BoardRow, header: BoardHeader): string | undefined {
122
114
  if (row.waits.length === 0) return undefined
123
115
 
124
116
  return `${WAITS_ICON} ${row.waits.map((reference) => header.link(reference)).join(', ')}`
125
117
  }
126
118
 
127
- // The title is padded to the title column where a time or the usage column follows it, so the times
128
- // form one column; a row with no time draws blanks in its place before the usage column.
129
- function timed_parts(
119
+ // A running row's part that changed at `at_ms`, emphasized while fresh (`run-board-fresh.ts`) — its
120
+ // start and its phase; a settled row's are long past.
121
+ function running_fresh(
130
122
  row: BoardRow,
131
- frame: RowFrame,
132
- is_slotted: boolean,
133
- ): Array<string | undefined> {
134
- const time = time_of(row, frame.header.now_ms)
135
-
136
- const title = title_of(row_title(row, frame.header))
123
+ text: string,
124
+ at_ms: number | undefined,
125
+ header: BoardHeader,
126
+ ): string {
127
+ return row.state === 'running' ? run_board_fresh.since(text, at_ms, header) : text
128
+ }
137
129
 
138
- if (time === undefined && !is_slotted) return [title]
130
+ // The title padded to the title column and the time to the time column — blanks where a row has none —
131
+ // so the times form one column, and what follows them starts in one column on every row.
132
+ function timed_parts(row: BoardRow, frame: RowFrame): Array<string> {
133
+ const time = time_of(row, frame.header.now_ms) ?? ''
134
+ const title = title_of(row_title(row, frame.header), frame.title_limit)
135
+ const fresh = running_fresh(row, time, row.status?.started_ms, frame.header)
139
136
 
140
- return [title.padEnd(TITLE_LIMIT), (time ?? '').padStart(frame.time_width)]
137
+ return [title.padEnd(frame.title_limit), padded(time, frame.time_width, fresh)]
141
138
  }
142
139
 
143
- // A running row leads with the icon of the phase it is in now — the rightmost
144
- // icon of its track — and with 🔄 while its track draws none yet.
140
+ // A running row leads with the icon of the phase it is in now — the last its track reached — and
141
+ // with 🔄 while it has reached none yet.
145
142
  function state_icon(row: BoardRow): string {
146
- const current = row.state === 'running' ? drawn_phases(row).at(-1) : undefined
143
+ const current = row.state === 'running' ? row.status?.track?.at(-1) : undefined
147
144
 
148
145
  return current === undefined ? STATE_ICONS[row.state] : PHASE_ICONS[current]
149
146
  }
@@ -156,15 +153,24 @@ function indent_of(prefix: string, row: BoardRow, header: BoardHeader): string {
156
153
  return `${header.spinner}${prefix.slice(1)}`
157
154
  }
158
155
 
159
- // A row's usage column where the frame draws one, padded to one width on every row so every track
160
- // starts in one column. A plan row the run has not touched has no time, usage or track to align, so it
161
- // draws none and what it waits on stays beside its title.
156
+ // A row's usage column as drawn: a settled row's finish time emphasized while it is fresh.
157
+ function slot_text(row: BoardRow, usage: UsageSlot, header: BoardHeader): string | undefined {
158
+ const slot = slot_of(row, usage, header.now_ms)
159
+ const ended = row.state === 'running' ? undefined : row.status?.ended_ms
160
+
161
+ return slot === undefined ? slot : run_board_fresh.since(slot, ended, header)
162
+ }
163
+
164
+ // A row's usage column where the frame draws one, padded to one width on every row so every tail
165
+ // starts in one column. A plan row the run has not touched draws blanks in its place, so what it waits
166
+ // on starts in the tail's column.
162
167
  function cell_text(row: BoardRow, frame: RowFrame): string | undefined {
163
168
  const { usage, slot_width } = frame
164
169
 
165
- if (usage === undefined || slot_width === 0 || row.status === undefined) return undefined
170
+ if (usage === undefined || slot_width === 0) return undefined
171
+ if (row.status === undefined) return ' '.repeat(slot_width)
166
172
 
167
- return cell_of(slot_of(row, usage, frame.header.now_ms), slot_width)
173
+ return cell_of(slot_text(row, usage, frame.header), slot_width)
168
174
  }
169
175
 
170
176
  function joined(parts: ReadonlyArray<string | undefined>): string {
@@ -175,52 +181,39 @@ function joined(parts: ReadonlyArray<string | undefined>): string {
175
181
  // ends at its icon, so what follows keeps its usual gap.
176
182
  function lead_of(row: BoardRow, header: BoardHeader, title: string): string {
177
183
  const kind = row.kind === undefined ? KIND_BLANK : FILED_KIND_ICONS[row.kind]
178
- const lead = `${state_icon(row)} ${header.link(String(row.number))} ${kind}`
184
+ const icon = running_fresh(row, state_icon(row), row.status?.phase_ms, header)
185
+ const lead = `${icon} ${header.link(String(row.number))} ${kind}`
179
186
 
180
187
  return title === '' ? lead : `${lead} ${title}`
181
188
  }
182
189
 
183
- // A row up to its usage column — the columns that stay where they are whatever the track's length.
190
+ // A row up to its usage column — the columns that stay where they are on every row.
184
191
  function head_text(row: BoardRow, frame: RowFrame): string {
185
- const cell = cell_text(row, frame)
186
- const [title = '', ...time] = timed_parts(row, frame, cell !== undefined)
192
+ const [title = '', time] = timed_parts(row, frame)
187
193
 
188
- return joined([lead_of(row, frame.header, title), ...time, cell])
194
+ return joined([lead_of(row, frame.header, title), time, cell_text(row, frame)])
189
195
  }
190
196
 
191
- // Every row carries its state's icon, a waiting one ⏳ too; a row with nothing
192
- // after its padded columns ends where its text does.
197
+ // Every row carries its state's icon, a waiting one ⏳ too; a running row ends on its tail
198
+ // (`run-board-render-tail.ts`), and a row with nothing after its padded columns ends where its text does.
193
199
  function row_text(row: BoardRow, frame: RowFrame): string {
194
- return joined([head_text(row, frame), track_text(row), waits_of(row, frame.header)]).trimEnd()
195
- }
196
-
197
- // The stage each state's row gives way in when the pane is short.
198
- const YIELD_STAGES: Readonly<Record<ItemState, YieldStage>> = {
199
- merged: 'finished',
200
- done: 'finished',
201
- waiting: 'pending',
202
- human: 'pending',
203
- parked: 'parked',
204
- stopped: 'parked',
205
- running: 'running',
206
- }
207
-
208
- // A running row is as old as its start; a settled one as its end, or its start where it has none.
209
- function aged_ms(row: BoardRow): number | undefined {
210
- const started = row.status?.started_ms
211
-
212
- return row.state === 'running' ? started : (row.status?.ended_ms ?? started)
213
- }
200
+ const { header } = frame
201
+ const tail = run_board_render_tail.tail_of(row, header)
202
+ const notes = [waits_of(row, header), reason_text(row, header.now_ms)]
214
203
 
215
- function yield_of(row: BoardRow): RowYield {
216
- return { stage: YIELD_STAGES[row.state], at_ms: aged_ms(row) }
204
+ return joined([head_text(row, frame), ...tail, ...notes]).trimEnd()
217
205
  }
218
206
 
219
207
  // A plan line that draws one issue row, and one that draws none — a blank, a rule, an epic's own line.
220
208
  function row_line(prefix: string, row: BoardRow, frame: RowFrame): PlanLine {
221
- const text = `${indent_of(prefix, row, frame.header)}${row_text(row, frame)}`
209
+ const indent = indent_of(prefix, row, frame.header)
210
+ const text = fitted(
211
+ row,
212
+ frame.columns,
213
+ (title_limit) => `${indent}${row_text(row, { ...frame, title_limit })}`,
214
+ )
222
215
 
223
- return { text, row: yield_of(row) }
216
+ return { text, row: run_board_row_yield.yield_of(row) }
224
217
  }
225
218
 
226
219
  function bare(text: string): PlanLine {
@@ -281,8 +274,8 @@ function time_width(header: BoardHeader): number {
281
274
  }
282
275
 
283
276
  // Whether every row's usage column still fits one terminal line — measured as the deepest, an epic's
284
- // child — where a frame is kept within a terminal. The track after it is left out: it grows without
285
- // bound, so one long history would otherwise take the usage column off every row.
277
+ // child — where a frame is kept within a terminal. The tail and the notes after it are left out: a
278
+ // row's reason makes room for itself, so one long reason would otherwise take the column off every row.
286
279
  function is_usage_fitting(
287
280
  rows: ReadonlyArray<BoardRow>,
288
281
  frame: RowFrame,
@@ -309,6 +302,8 @@ function frame_of(
309
302
  time_width: time_width(header),
310
303
  usage,
311
304
  slot_width: usage === undefined ? 0 : column_width(rows, usage, header.now_ms),
305
+ title_limit: TITLE_LIMIT,
306
+ columns: size?.columns,
312
307
  }
313
308
 
314
309
  return is_usage_fitting(rows, frame, size) ? frame : { ...frame, usage: undefined }
@@ -352,6 +347,18 @@ function resume_lines(resume: string | undefined): Array<string> {
352
347
  return ['', `${STATE_ICONS.human} ${WORDS.resume}${GAP}claude --resume ${resume}`]
353
348
  }
354
349
 
350
+ // The activity kinds the legend names: the header's third line's, 🔧 where a row's tail draws it, and
351
+ // a followup row's checks (joshuafolkken/kit#3770).
352
+ function activity_named(header: BoardHeader, rows: ReadonlyArray<BoardRow>): Array<string> {
353
+ const now = run_board_header.now_legend(header)
354
+ const is_drawn = rows.some((row) => run_board_render_tail.is_operation_drawn(row, header))
355
+ const operation = is_drawn ? [`${NOW_ICONS.operation} ${WORDS.operation}`] : []
356
+ const is_checks = rows.some((row) => run_board_render_tail.is_checks_drawn(row, header))
357
+ const checks = is_checks ? run_board_render_checks.LEGEND : []
358
+
359
+ return [...(now === undefined ? [] : [now]), ...operation, ...checks]
360
+ }
361
+
355
362
  // No legend is drawn before a plan is read: there are no rows whose symbols it would name.
356
363
  function legend_lines(header: BoardHeader, notes: ReadonlyArray<BoardNote>): Array<string> {
357
364
  const { layout } = header
@@ -360,8 +367,16 @@ function legend_lines(header: BoardHeader, notes: ReadonlyArray<BoardNote>): Arr
360
367
 
361
368
  const rows = run_board_layout.rows_of(layout)
362
369
  const drawn = new Set(rows.map((row) => state_icon(row)))
370
+ const named = activity_named(header, rows)
371
+
372
+ return ['', ...run_board_render_legend.legend_of(rows, drawn, notes, named)]
373
+ }
374
+
375
+ // Names with no link where no terminal opens one: a frame with no `link` draws them plain.
376
+ function thanks_part(view: BoardView): Array<string> {
377
+ const link = view.link ?? ((text: string): string => text)
363
378
 
364
- return ['', ...run_board_render_legend.legend_of(rows, drawn, notes)]
379
+ return run_board_render_thanks.thanks_lines(view.thanks ?? [], link, view.size?.columns)
365
380
  }
366
381
 
367
382
  // A live frame is kept within its terminal; any other is drawn whole.
@@ -378,6 +393,7 @@ function render(view: BoardView): Array<string> {
378
393
  above,
379
394
  plan: plan_sections(header, view.size),
380
395
  notes: notes_part(notes, header),
396
+ thanks: thanks_part(view),
381
397
  legend: legend_lines(header, notes),
382
398
  footer: view.footer ?? [],
383
399
  }
@@ -0,0 +1,32 @@
1
+ import type { RowYield, YieldStage } from './run-board-fit'
2
+ import type { BoardRow } from './run-board-layout'
3
+ import type { ItemState } from './run-board-status'
4
+
5
+ // When an issue row gives way to a short pane (`run-board-fit.ts`): the stage its state yields in,
6
+ // and how old it is within that stage.
7
+
8
+ // The stage each state's row gives way in when the pane is short.
9
+ const YIELD_STAGES: Readonly<Record<ItemState, YieldStage>> = {
10
+ merged: 'finished',
11
+ done: 'finished',
12
+ waiting: 'pending',
13
+ human: 'pending',
14
+ parked: 'parked',
15
+ stopped: 'parked',
16
+ running: 'running',
17
+ }
18
+
19
+ // A running row is as old as its start; a settled one as its end, or its start where it has none.
20
+ function aged_ms(row: BoardRow): number | undefined {
21
+ const started = row.status?.started_ms
22
+
23
+ return row.state === 'running' ? started : (row.status?.ended_ms ?? started)
24
+ }
25
+
26
+ function yield_of(row: BoardRow): RowYield {
27
+ return { stage: YIELD_STAGES[row.state], at_ms: aged_ms(row) }
28
+ }
29
+
30
+ const run_board_row_yield = { yield_of }
31
+
32
+ export { run_board_row_yield }
@@ -1,21 +1,29 @@
1
1
  import { run_event_scope } from '#scripts/run/event/run-event-scope'
2
+ import { run_event_setup } from '#scripts/run/event/run-event-setup'
2
3
  import { run_event_stream, type RunEvent } from '#scripts/run/event/run-event-stream'
3
4
 
4
- // What the run's `setup` events (`run-event-setup.ts`) tell `run:board`: the stage a run is at before
5
- // its first child launches, whether it is preparing a launch now, and which lanes are not children yet.
6
- // The events carry it all — nothing here infers a stage from which records exist.
5
+ // What the run's `setup` events (`run-event-setup.ts`) tell `run:board`: the stage the run is at now,
6
+ // and which lanes are not children yet. The events carry it all — nothing here infers a stage from
7
+ // which records exist.
7
8
 
8
9
  const KIND = run_event_stream.EVENT_KIND
10
+ const { STAGE } = run_event_setup
9
11
 
10
12
  interface SetupRead {
11
- // The newest stage's own words while the run is starting; `undefined` from the first launch — or a
12
- // drain before one — on, when the plan's counts say more than a stage does.
13
+ // The stage the run itself is working at, in the stage's own words: any stage while the run is
14
+ // starting; from the first launch — or a drain before one — on, a lane opening or a judgment session.
15
+ // `undefined` where the run is at none.
13
16
  setup: string | undefined
14
- // The run is getting a child ready: any stage while it is starting, a lane opening after that.
15
- is_preparing: boolean
17
+ // When the run entered that stage — its event's own time, so how long the run has stood there is
18
+ // counted from a record; `undefined` wherever `setup` is.
19
+ setup_ms: number | undefined
16
20
  }
17
21
 
18
- const NO_SETUP: SetupRead = { setup: undefined, is_preparing: false }
22
+ const NO_SETUP: SetupRead = { setup: undefined, setup_ms: undefined }
23
+
24
+ // The stages of a hand-back to a session. Only the supervisor's next hand-off ends one, whatever the
25
+ // session writes to the stream meanwhile.
26
+ const JUDGMENT_STAGES: ReadonlySet<string> = new Set([STAGE.JUDGING, STAGE.SESSION])
19
27
 
20
28
  function is_setup(event: RunEvent): boolean {
21
29
  return event.kind === KIND.SETUP
@@ -43,13 +51,20 @@ function is_opening(events: ReadonlyArray<RunEvent>, stage: RunEvent): boolean {
43
51
  return position === undefined || position.pos < stage.pos
44
52
  }
45
53
 
54
+ // Whether the run is at `stage` now: at any stage while it is starting; after that only at one it is
55
+ // itself working through.
56
+ function is_standing(events: ReadonlyArray<RunEvent>, stage: RunEvent): boolean {
57
+ if (!has_started(events) || JUDGMENT_STAGES.has(stage.text)) return true
58
+
59
+ return is_opening(events, stage)
60
+ }
61
+
46
62
  function read(events: ReadonlyArray<RunEvent>): SetupRead {
47
63
  const stage = events.findLast((event) => is_setup(event))
48
64
 
49
- if (stage === undefined) return NO_SETUP
50
- if (!has_started(events)) return { setup: stage.text, is_preparing: true }
65
+ if (stage === undefined || !is_standing(events, stage)) return NO_SETUP
51
66
 
52
- return { setup: undefined, is_preparing: is_opening(events, stage) }
67
+ return { setup: stage.text, setup_ms: Date.parse(stage.at) }
53
68
  }
54
69
 
55
70
  function names(event: RunEvent, issue: number): boolean {
@@ -67,7 +82,40 @@ function is_lane_pending(events: ReadonlyArray<RunEvent>, issue: number): boolea
67
82
  return !has_launched(events) || stages.some((stage) => names(stage, issue))
68
83
  }
69
84
 
70
- const run_board_setup = { is_lane_pending, read }
85
+ // A stage of one lane's launch, as its event wrote it, and when the lane entered it.
86
+ interface LaneStage {
87
+ text: string
88
+ at_ms: number
89
+ }
90
+
91
+ // What the run did for `issue`: a stage of its lane's launch, or a position naming it.
92
+ function is_step_for(event: RunEvent, issue: number): boolean {
93
+ if (!names(event, issue)) return false
94
+
95
+ return is_setup(event) || !run_event_stream.TRACE_KINDS.has(event.kind)
96
+ }
97
+
98
+ // A stage that names no issue: a hand-off to the driver, a hand-back to a session, or a launch that
99
+ // returned without a child. A lane's launch runs inside one driver pass, so a newer one of these says
100
+ // that launch has returned.
101
+ function is_run_stage(event: RunEvent): boolean {
102
+ return is_setup(event) && run_event_scope.issue_named(event) === undefined
103
+ }
104
+
105
+ // The stage `issue`'s own lane is at: the newest stage naming it, until the run does anything else for
106
+ // that issue — its launch, or whatever ended the attempt — or writes a stage naming none, which is all
107
+ // a launch that left no child puts on the stream: the driver says so itself, since a refusal it waits
108
+ // out is followed by no hand-off. Read per issue, so two lanes opened side by side each stand at their
109
+ // own stage whatever the other has reached.
110
+ function lane_stage(events: ReadonlyArray<RunEvent>, issue: number): LaneStage | undefined {
111
+ const step = events.findLast((event) => is_run_stage(event) || is_step_for(event, issue))
112
+
113
+ if (step === undefined || !is_setup(step) || !names(step, issue)) return undefined
114
+
115
+ return { text: step.text, at_ms: Date.parse(step.at) }
116
+ }
117
+
118
+ const run_board_setup = { is_lane_pending, lane_stage, read }
71
119
 
72
120
  export { run_board_setup }
73
- export type { SetupRead }
121
+ export type { LaneStage, SetupRead }
@@ -9,6 +9,7 @@ import type { Link } from './run-board-link'
9
9
  import type { MachineGauges, MachineMark } from './run-board-machine'
10
10
  import type { LocalRead } from './run-board-read'
11
11
  import type { Spot } from './run-board-spin'
12
+ import type { Thanks } from './run-board-thanks-csv'
12
13
  import type { LaneUsages, UsageMark } from './run-board-usage'
13
14
 
14
15
  // What a `run:board` redraw reads through and folds forward, shared by the
@@ -38,6 +39,8 @@ interface BoardPorts {
38
39
  // Records that a progress report was just given, as `run:progress --mark` does, so the next scheduled
39
40
  // one waits a full interval from here.
40
41
  mark: () => Promise<void>
42
+ // The reviewers' roster (`run-board-thanks-read.ts`); none where the frame thanks nobody — the chat.
43
+ read_thanks?: (() => Promise<Thanks>) | undefined
41
44
  // Sends one chat frame off-screen, for `--every` alone.
42
45
  push: (frame: string) => Promise<void>
43
46
  // Runs `leave` however the process ends: a normal exit, Ctrl+C or SIGTERM.
@@ -79,6 +82,10 @@ interface BoardState {
79
82
  // them.
80
83
  usage: UsageMark | undefined
81
84
  usages: LaneUsages | undefined
85
+ // The reviewers drawn above the legend, the read of them in flight, and when it was last launched.
86
+ thanks: Thanks
87
+ thanks_fetch: Fetch<Thanks> | undefined
88
+ thanks_attempted_ms: number | undefined
82
89
  }
83
90
 
84
91
  const FRESH_STATE: BoardState = {
@@ -101,6 +108,9 @@ const FRESH_STATE: BoardState = {
101
108
  spots: [],
102
109
  usage: undefined,
103
110
  usages: undefined,
111
+ thanks: [],
112
+ thanks_fetch: undefined,
113
+ thanks_attempted_ms: undefined,
104
114
  }
105
115
 
106
116
  function is_due(last_ms: number | undefined, interval_ms: number, now_ms: number): boolean {
@@ -3,9 +3,12 @@ import {
3
3
  IN_PROGRESS_LABEL,
4
4
  NEEDS_DECISION_LABEL,
5
5
  } from '#scripts/issue/issue-labels'
6
+ import type { PrChecks } from '#scripts/run/event/run-event-checks'
6
7
  import { run_event_scope } from '#scripts/run/event/run-event-scope'
7
8
  import { run_event_stream, type RunEvent } from '#scripts/run/event/run-event-stream'
9
+ import { run_board_checks } from './run-board-checks'
8
10
  import type { ClosedIssue } from './run-board-closed'
11
+ import { run_board_lane_event, type LaneEvent } from './run-board-lane-event'
9
12
  import { run_board_phase, type Phase } from './run-board-phase'
10
13
  import { run_board_setup, type SetupRead } from './run-board-setup'
11
14
 
@@ -25,12 +28,29 @@ interface ItemStatus {
25
28
  lane?: string | undefined
26
29
  // Every phase the child passed while running, in order (`run-board-phase.ts`) — kept once it settles.
27
30
  track?: ReadonlyArray<Phase> | undefined
31
+ // When a running child entered the phase its track ends on — the event that moved it there.
32
+ phase_ms?: number | undefined
33
+ }
34
+
35
+ interface LatestEvent {
36
+ text: string
37
+ at_ms: number
28
38
  }
29
39
 
30
40
  // The run as a whole, read off the same stream. The setup half is `run-board-setup.ts`'s, absent
31
41
  // where a stream carries no stage.
32
42
  interface RunActivity extends Partial<SetupRead> {
33
43
  last_event_ms: number | undefined
44
+ // The newest thing the run did — its newest position, or its newest event where it has none yet —
45
+ // in the event's own words, and when.
46
+ latest?: LatestEvent | undefined
47
+ // When the run last did anything of its own — its newest position or stage, whichever is newer. A
48
+ // stage that has ended still counts: what follows it is counted from its end, not from before it.
49
+ idle_ms?: number | undefined
50
+ // Each child's newest `lane-phase` or `ship-stage`, by issue (`run-board-lane-event.ts`).
51
+ lane_events?: ReadonlyMap<number, LaneEvent> | undefined
52
+ // Each child's newest `pr-checks`, by issue (`run-board-checks.ts`).
53
+ pr_checks?: ReadonlyMap<number, PrChecks> | undefined
34
54
  // The run's last position is a `stop` that names no child — the run itself ended.
35
55
  is_stopped: boolean
36
56
  }
@@ -44,13 +64,30 @@ const ENDING_STATES: Readonly<Partial<Record<string, ItemState>>> = {
44
64
  }
45
65
  // A cut or a resume says the child is still running, on whatever phase it had reached.
46
66
  const CONTINUING_KINDS: ReadonlySet<string> = new Set([KIND.CUT, KIND.RESUME])
67
+ // A person's restart starts the child over in a fresh session, so its track starts over too.
68
+ const LAUNCH_KINDS: ReadonlySet<string> = new Set([KIND.CHILD_LAUNCH, KIND.RESTART])
47
69
 
48
70
  function moment(event: RunEvent): number {
49
71
  return Date.parse(event.at)
50
72
  }
51
73
 
52
74
  function launched(event: RunEvent): ItemStatus {
53
- return { state: 'running', started_ms: moment(event), track: [run_board_phase.LAUNCHED_PHASE] }
75
+ const at_ms = moment(event)
76
+
77
+ return {
78
+ state: 'running',
79
+ started_ms: at_ms,
80
+ track: [run_board_phase.LAUNCHED_PHASE],
81
+ phase_ms: at_ms,
82
+ }
83
+ }
84
+
85
+ // The status with its track moved on by `event`. The phase's start moves only where the phase the
86
+ // track ends on changes, so a second session's first edit does not restart the time of its phase.
87
+ function moved(previous: ItemStatus, track: ReadonlyArray<Phase>, event: RunEvent): ItemStatus {
88
+ const is_same = previous.phase_ms !== undefined && track.at(-1) === previous.track?.at(-1)
89
+
90
+ return { ...previous, track, phase_ms: is_same ? previous.phase_ms : moment(event) }
54
91
  }
55
92
 
56
93
  // The status a child settles into: its start and the track it had reached, so a settled row still
@@ -66,7 +103,9 @@ function ended(previous: ItemStatus | undefined, state: ItemState, event: RunEve
66
103
  }
67
104
 
68
105
  function continued(previous: ItemStatus | undefined): ItemStatus {
69
- return { state: 'running', started_ms: previous?.started_ms, track: previous?.track }
106
+ const { started_ms, track, phase_ms } = previous ?? {}
107
+
108
+ return { state: 'running', started_ms, track, phase_ms }
70
109
  }
71
110
 
72
111
  // A stage line carrying its attempt redraws the track of any child launched on this stream — a settled
@@ -75,11 +114,12 @@ function continued(previous: ItemStatus | undefined): ItemStatus {
75
114
  function replayed(
76
115
  previous: ItemStatus | undefined,
77
116
  attempt: ReadonlyArray<Phase>,
117
+ event: RunEvent,
78
118
  ): ItemStatus | undefined {
79
119
  if (previous === undefined) return previous
80
120
  if (previous.track === undefined && previous.state !== 'running') return previous
81
121
 
82
- return { ...previous, track: run_board_phase.replayed(previous.track ?? [], attempt) }
122
+ return moved(previous, run_board_phase.replayed(previous.track ?? [], attempt), event)
83
123
  }
84
124
 
85
125
  // A phase moves only a running child, so a three-word `report` stage after its merge — or a `plan`
@@ -89,18 +129,18 @@ function phased(previous: ItemStatus | undefined, event: RunEvent): ItemStatus |
89
129
 
90
130
  if (phase === undefined || previous?.state !== 'running') return previous
91
131
 
92
- return { ...previous, track: run_board_phase.history_after(previous.track ?? [], phase) }
132
+ return moved(previous, run_board_phase.history_after(previous.track ?? [], phase), event)
93
133
  }
94
134
 
95
135
  function advanced(previous: ItemStatus | undefined, event: RunEvent): ItemStatus | undefined {
96
136
  const attempt = run_board_phase.attempt_of(event)
97
137
 
98
- return attempt === undefined ? phased(previous, event) : replayed(previous, attempt)
138
+ return attempt === undefined ? phased(previous, event) : replayed(previous, attempt, event)
99
139
  }
100
140
 
101
141
  // One event folded into the status of the child it names; an event the board does not track keeps it.
102
142
  function next_status(previous: ItemStatus | undefined, event: RunEvent): ItemStatus | undefined {
103
- if (event.kind === KIND.CHILD_LAUNCH) return launched(event)
143
+ if (LAUNCH_KINDS.has(event.kind)) return launched(event)
104
144
 
105
145
  const ending = ENDING_STATES[event.kind]
106
146
 
@@ -247,6 +287,21 @@ function is_run_stop(event: RunEvent | undefined): boolean {
247
287
  return event?.kind === KIND.STOP && run_event_scope.issue_named(event) === undefined
248
288
  }
249
289
 
290
+ function latest_of(event: RunEvent | undefined): LatestEvent | undefined {
291
+ return event === undefined ? undefined : { text: event.text, at_ms: moment(event) }
292
+ }
293
+
294
+ function is_own(event: RunEvent): boolean {
295
+ return event.kind === KIND.SETUP || !run_event_stream.TRACE_KINDS.has(event.kind)
296
+ }
297
+
298
+ // A stream of traces alone has done nothing of its own yet, so its newest event stands in.
299
+ function idle_of(events: ReadonlyArray<RunEvent>): number | undefined {
300
+ const own = events.findLast((event) => is_own(event)) ?? events.at(-1)
301
+
302
+ return own === undefined ? undefined : moment(own)
303
+ }
304
+
250
305
  // `is_ended` is a run `run:carry --end` closed: its record says it stopped, whatever its last event
251
306
  // was.
252
307
  function activity_of(events: ReadonlyArray<RunEvent>, is_ended = false): RunActivity {
@@ -256,6 +311,10 @@ function activity_of(events: ReadonlyArray<RunEvent>, is_ended = false): RunActi
256
311
  return {
257
312
  ...run_board_setup.read(events),
258
313
  last_event_ms: last === undefined ? undefined : moment(last),
314
+ latest: latest_of(position ?? last),
315
+ idle_ms: idle_of(events),
316
+ lane_events: run_board_lane_event.newest_of(events),
317
+ pr_checks: run_board_checks.newest_of(events),
259
318
  is_stopped: is_ended || is_run_stop(position),
260
319
  }
261
320
  }
@@ -263,4 +322,4 @@ function activity_of(events: ReadonlyArray<RunEvent>, is_ended = false): RunActi
263
322
  const run_board_status = { activity_of, settle_closed, statuses_of }
264
323
 
265
324
  export { run_board_status }
266
- export type { ItemState, ItemStatus, OpenRead, RunActivity }
325
+ export type { ItemState, ItemStatus, LatestEvent, OpenRead, RunActivity }