@joshuafolkken/kit 1.1383.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 (244) 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-LOJTBU5V.js → chunk-2G5F2YCD.js} +3 -3
  14. package/dist/hooks/{chunk-4MGHBTOM.js → chunk-3LE6VLSR.js} +25 -8
  15. package/dist/hooks/{chunk-Y5R47QHU.js → chunk-4BHUPFKF.js} +1 -1
  16. package/dist/hooks/{chunk-KC6LUZZ2.js → chunk-5SJSSCJG.js} +1 -1
  17. package/dist/hooks/{chunk-FZ6FUSBM.js → chunk-723HKZO5.js} +1 -1
  18. package/dist/hooks/{chunk-RYP3TSUJ.js → chunk-7KMN5QB5.js} +3 -3
  19. package/dist/hooks/{chunk-NEC4AT4Z.js → chunk-DGT54IH5.js} +4 -0
  20. package/dist/hooks/{chunk-YKUITFT7.js → chunk-EMB55BDF.js} +4 -4
  21. package/dist/hooks/{chunk-JBBMAPC4.js → chunk-IX6TTTI6.js} +4 -0
  22. package/dist/hooks/{chunk-6MAW7BCM.js → chunk-JXQBK323.js} +2677 -190
  23. package/dist/hooks/{chunk-KCF2YE7O.js → chunk-MSYSUSY6.js} +5 -5
  24. package/dist/hooks/{chunk-LC56R53A.js → chunk-MU4LJFIV.js} +4 -4
  25. package/dist/hooks/{chunk-7VSZ2N4B.js → chunk-QL6QTPWN.js} +3 -3
  26. package/dist/hooks/{chunk-TT742SIO.js → chunk-XER6SSIB.js} +92 -19
  27. package/dist/hooks/{chunk-2X2ART5G.js → chunk-YW452ZAX.js} +500 -29
  28. package/dist/hooks/{chunk-6AC4C2IO.js → chunk-YZXYOAZ3.js} +187 -443
  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 +262 -201
  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 +124 -15
  36. package/dist/hooks/stop-guard.js +98 -55
  37. package/dist/josh.js +63 -10
  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-cli.ts +2 -4
  45. package/scripts/backlog/backlog-drive-epic.ts +24 -8
  46. package/scripts/backlog/backlog-drive-launch.ts +5 -0
  47. package/scripts/backlog/backlog-drive-named-offer.ts +7 -2
  48. package/scripts/backlog/backlog-drive-named.ts +4 -2
  49. package/scripts/backlog/backlog-drive-owner.ts +72 -5
  50. package/scripts/backlog/backlog-drive-unread.ts +56 -0
  51. package/scripts/backlog/backlog-drive.ts +16 -8
  52. package/scripts/backlog/backlog-overlap.ts +22 -5
  53. package/scripts/backlog/backlog-ready.ts +1 -3
  54. package/scripts/backlog/backlogrun-args.ts +13 -2
  55. package/scripts/backlog/backlogrun-cli.ts +91 -19
  56. package/scripts/backlog/backlogrun-launch.ts +80 -5
  57. package/scripts/backlog/backlogrun-replace.ts +166 -0
  58. package/scripts/backlog/backlogrun-stop.ts +178 -0
  59. package/scripts/behavior/behavior-cli.ts +6 -1
  60. package/scripts/doctor/doctor-io.ts +60 -9
  61. package/scripts/doctor/doctor.ts +20 -1
  62. package/scripts/document/document-byte-budget.ts +3 -3
  63. package/scripts/epic/epic-bundle-cli.ts +8 -8
  64. package/scripts/epic/epic-bundle-referenced.ts +2 -5
  65. package/scripts/epic/epic-bundle.ts +39 -4
  66. package/scripts/exports/unused-members-cli.ts +2 -1
  67. package/scripts/followup/followup-checks-event.ts +82 -0
  68. package/scripts/followup/git-followup-finish.ts +16 -2
  69. package/scripts/followup/git-followup-workflow.ts +5 -2
  70. package/scripts/followup/git-pr-followup-checks.ts +18 -4
  71. package/scripts/followup/git-pr-followup-wrapup.ts +5 -0
  72. package/scripts/followup/git-pr-followup.ts +4 -1
  73. package/scripts/fresh-machine/fresh-machine-cli.ts +204 -0
  74. package/scripts/fresh-machine/fresh-machine-docker.ts +65 -0
  75. package/scripts/fresh-machine/fresh-machine-image.ts +62 -0
  76. package/scripts/fresh-machine/fresh-machine-log.ts +61 -0
  77. package/scripts/fresh-machine/fresh-machine-patterns.ts +130 -0
  78. package/scripts/fresh-machine/fresh-machine-run.ts +215 -0
  79. package/scripts/fresh-machine/fresh-machine-script.ts +45 -0
  80. package/scripts/fresh-machine/fresh-machine-spec.ts +57 -0
  81. package/scripts/fresh-machine/specs/kit-test-backlogrun-python.json +34 -0
  82. package/scripts/fresh-machine/specs/kit-test-backlogrun.json +31 -0
  83. package/scripts/gate/gate-report.ts +70 -15
  84. package/scripts/gate/project-checks.ts +13 -2
  85. package/scripts/gate/scoped-green.ts +2 -1
  86. package/scripts/gate/verification-gate.ts +4 -3
  87. package/scripts/gh/gh-author-association.ts +14 -0
  88. package/scripts/gh/git-gh-conditional.ts +66 -0
  89. package/scripts/gh/git-gh-exec.ts +1 -1
  90. package/scripts/gh/git-gh-issue-read.ts +6 -3
  91. package/scripts/gh/git-gh-issue-write.ts +5 -1
  92. package/scripts/gh/git-gh-issue.ts +4 -1
  93. package/scripts/gh/git-pr-checks-eval.ts +13 -0
  94. package/scripts/gh/git-pr-checks-parse.ts +8 -1
  95. package/scripts/gh/git-pr-checks-watch.ts +11 -4
  96. package/scripts/gh/git-pr-checks.ts +36 -2
  97. package/scripts/git/git-command.ts +16 -4
  98. package/scripts/git/git-schemas.ts +1 -0
  99. package/scripts/git/git-spawn-sync.ts +1 -1
  100. package/scripts/hooks/pretool-guard.ts +3 -1
  101. package/scripts/init/hook-command-rewrite.ts +31 -0
  102. package/scripts/init/init-actions.ts +4 -1
  103. package/scripts/init/init-ai-copy.ts +33 -10
  104. package/scripts/init/init-basic.ts +8 -3
  105. package/scripts/init/init-logic-toolchain.ts +76 -0
  106. package/scripts/init/init-logic.ts +2 -0
  107. package/scripts/init/init.ts +5 -23
  108. package/scripts/init/project-profile.ts +36 -7
  109. package/scripts/init/start-exec.ts +27 -1
  110. package/scripts/init/start-plan.ts +30 -4
  111. package/scripts/init/start-steps.ts +77 -3
  112. package/scripts/init/start.ts +21 -13
  113. package/scripts/init/workflow-mark.ts +35 -0
  114. package/scripts/issue/issue-ack-cli.ts +84 -0
  115. package/scripts/issue/issue-labels.ts +40 -8
  116. package/scripts/issue/issue-state-cli.ts +14 -4
  117. package/scripts/issue/issue-watch-diff.ts +152 -0
  118. package/scripts/issue/issue-watch-gate.ts +81 -0
  119. package/scripts/issue/issue-watch-hook.ts +54 -0
  120. package/scripts/issue/issue-watch.ts +240 -0
  121. package/scripts/issue/measure-rerun-cli.ts +3 -4
  122. package/scripts/josh/josh-command-map.ts +4 -0
  123. package/scripts/josh/josh-commands-backlog.ts +2 -2
  124. package/scripts/josh/josh-commands-development.ts +1 -1
  125. package/scripts/josh/josh-commands-issue.ts +7 -0
  126. package/scripts/josh/josh-commands-lane.ts +11 -2
  127. package/scripts/josh/josh-commands-project.ts +25 -1
  128. package/scripts/josh/josh-verdict.ts +33 -4
  129. package/scripts/lane/claude-lane-broker.ts +142 -0
  130. package/scripts/lane/lane-await.ts +6 -2
  131. package/scripts/lane/lane-cache.ts +37 -7
  132. package/scripts/lane/lane-capacity.ts +8 -6
  133. package/scripts/lane/lane-child-invocation.ts +1 -0
  134. package/scripts/lane/lane-cli.ts +15 -1
  135. package/scripts/lane/lane-close.ts +15 -6
  136. package/scripts/lane/lane-dispatch.ts +36 -10
  137. package/scripts/lane/lane-environment.ts +1 -1
  138. package/scripts/lane/lane-launch-cli.ts +13 -4
  139. package/scripts/lane/lane-ledger.ts +21 -3
  140. package/scripts/lane/lane-measure-cli.ts +10 -2
  141. package/scripts/lane/lane-open.ts +9 -1
  142. package/scripts/lane/lane-preserve.ts +49 -0
  143. package/scripts/lane/lane-reap.ts +20 -5
  144. package/scripts/lane/lane-registry.ts +6 -0
  145. package/scripts/lane/lane-restart-cli.ts +51 -0
  146. package/scripts/lane/lane-restart.ts +175 -0
  147. package/scripts/lane/{openai-review-broker.ts → lane-session-broker.ts} +106 -28
  148. package/scripts/lane/lane-spent.ts +195 -0
  149. package/scripts/lane/lane-stage-stats.ts +27 -3
  150. package/scripts/lane/lane-stop-stats.ts +96 -0
  151. package/scripts/lane/openai-lane-supervisor.ts +2 -2
  152. package/scripts/lane/process-tools.ts +31 -0
  153. package/scripts/lib/buffered-process.ts +36 -2
  154. package/scripts/lib/pnpm-reach.ts +27 -0
  155. package/scripts/lib/status-icons.ts +5 -1
  156. package/scripts/lint/lint-parallel.ts +19 -2
  157. package/scripts/observations/observation-ledger-home.ts +1 -0
  158. package/scripts/review/review-attest-cli.ts +21 -6
  159. package/scripts/review/review-brief-cli.ts +2 -2
  160. package/scripts/review/review-comment-round.ts +114 -0
  161. package/scripts/review/review-record-cli.ts +39 -17
  162. package/scripts/rules/implementation-cut.ts +3 -3
  163. package/scripts/rules/pre-gate-cut.ts +1 -1
  164. package/scripts/run/board/run-board-checks.ts +42 -0
  165. package/scripts/run/board/run-board-cli.ts +2 -0
  166. package/scripts/run/board/run-board-every.ts +3 -1
  167. package/scripts/run/board/run-board-fit.ts +16 -5
  168. package/scripts/run/board/run-board-fresh.ts +64 -0
  169. package/scripts/run/board/run-board-header.ts +139 -37
  170. package/scripts/run/board/run-board-labels.ts +47 -4
  171. package/scripts/run/board/run-board-lane-event.ts +66 -0
  172. package/scripts/run/board/run-board-layout.ts +57 -9
  173. package/scripts/run/board/run-board-now.ts +234 -0
  174. package/scripts/run/board/run-board-operation-read.ts +75 -0
  175. package/scripts/run/board/run-board-operation.ts +150 -0
  176. package/scripts/run/board/run-board-phase.ts +26 -1
  177. package/scripts/run/board/run-board-plan.ts +4 -6
  178. package/scripts/run/board/run-board-read.ts +58 -5
  179. package/scripts/run/board/run-board-reason.ts +124 -0
  180. package/scripts/run/board/run-board-render-checks.ts +55 -0
  181. package/scripts/run/board/run-board-render-legend.ts +12 -3
  182. package/scripts/run/board/run-board-render-slot.ts +2 -2
  183. package/scripts/run/board/run-board-render-tail.ts +128 -0
  184. package/scripts/run/board/run-board-render-thanks.ts +50 -0
  185. package/scripts/run/board/run-board-render-title.ts +58 -0
  186. package/scripts/run/board/run-board-render.ts +96 -80
  187. package/scripts/run/board/run-board-row-yield.ts +32 -0
  188. package/scripts/run/board/run-board-setup.ts +121 -0
  189. package/scripts/run/board/run-board-span.ts +49 -7
  190. package/scripts/run/board/run-board-state.ts +10 -0
  191. package/scripts/run/board/run-board-status.ts +76 -13
  192. package/scripts/run/board/run-board-thanks-csv.ts +71 -0
  193. package/scripts/run/board/run-board-thanks-read.ts +112 -0
  194. package/scripts/run/board/run-board-thanks.ts +53 -0
  195. package/scripts/run/board/run-board-tick.ts +53 -16
  196. package/scripts/run/board/run-board-usual.ts +44 -0
  197. package/scripts/run/carry/run-carry-added.ts +1 -1
  198. package/scripts/run/carry/run-carry-args.ts +9 -1
  199. package/scripts/run/carry/run-carry-change.ts +1 -1
  200. package/scripts/run/carry/run-carry-cli.ts +39 -52
  201. package/scripts/run/carry/run-carry-end.ts +73 -0
  202. package/scripts/run/carry/run-carry-ended.ts +9 -1
  203. package/scripts/run/carry/run-carry.ts +24 -1
  204. package/scripts/run/entry/run-entry-cli.ts +5 -0
  205. package/scripts/run/event/run-event-checks.ts +54 -0
  206. package/scripts/run/event/run-event-render.ts +3 -0
  207. package/scripts/run/event/run-event-scope.ts +1 -0
  208. package/scripts/run/event/run-event-setup.ts +91 -0
  209. package/scripts/run/event/run-event-stream.ts +46 -9
  210. package/scripts/run/merge/run-merge-cli.ts +7 -4
  211. package/scripts/run/merge/run-merge-ship.ts +61 -0
  212. package/scripts/run/merge/run-merge-steps.ts +7 -14
  213. package/scripts/run/merge/run-merge-token.ts +16 -2
  214. package/scripts/run/run-headless.ts +1 -0
  215. package/scripts/run/run-step.ts +2 -0
  216. package/scripts/run/run-stranded-detect.ts +58 -12
  217. package/scripts/run/ship/run-ship-cli.ts +22 -2
  218. package/scripts/run/ship/run-ship-return.ts +31 -13
  219. package/scripts/run/ship/run-ship-review-steps.ts +17 -4
  220. package/scripts/run/ship/run-ship-stage.ts +1 -0
  221. package/scripts/run/wake/run-wake-await.ts +130 -0
  222. package/scripts/run/wake/run-wake-cli.ts +108 -74
  223. package/scripts/run/wake/run-wake-describe.ts +36 -2
  224. package/scripts/run/wake/run-wake-driver.ts +21 -5
  225. package/scripts/run/wake/run-wake-handoff.ts +1 -1
  226. package/scripts/run/wake/run-wake-session.ts +20 -3
  227. package/scripts/run/wake/run-wake-stop.ts +37 -0
  228. package/scripts/run/wake/run-wake-supervise.ts +44 -0
  229. package/scripts/sync/claude-settings-merge.ts +144 -0
  230. package/scripts/sync/sync-ai-files.ts +28 -3
  231. package/scripts/sync/sync.ts +15 -0
  232. package/scripts/test/project-test-script.ts +73 -0
  233. package/scripts/test/skip-marker.ts +14 -1
  234. package/scripts/test/test-related.ts +3 -2
  235. package/scripts/test/test-unit-guard.ts +78 -14
  236. package/scripts/time-runtime/time-reported-failure.ts +11 -3
  237. package/scripts/uninstall/distributed-files.ts +92 -0
  238. package/scripts/uninstall/uninstall-plan.ts +119 -0
  239. package/scripts/uninstall/uninstall-report.ts +61 -0
  240. package/scripts/uninstall/uninstall.ts +206 -0
  241. package/templates/AGENTS.basic.md +4 -0
  242. package/templates/GEMINI.basic.md +6 -0
  243. package/dist/hooks/chunk-6V5OPIUU.js +0 -13
  244. 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 }
@@ -0,0 +1,121 @@
1
+ import { run_event_scope } from '#scripts/run/event/run-event-scope'
2
+ import { run_event_setup } from '#scripts/run/event/run-event-setup'
3
+ import { run_event_stream, type RunEvent } from '#scripts/run/event/run-event-stream'
4
+
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.
8
+
9
+ const KIND = run_event_stream.EVENT_KIND
10
+ const { STAGE } = run_event_setup
11
+
12
+ interface SetupRead {
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.
16
+ setup: string | undefined
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
20
+ }
21
+
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])
27
+
28
+ function is_setup(event: RunEvent): boolean {
29
+ return event.kind === KIND.SETUP
30
+ }
31
+
32
+ function has_launched(events: ReadonlyArray<RunEvent>): boolean {
33
+ return events.some((event) => event.kind === KIND.CHILD_LAUNCH)
34
+ }
35
+
36
+ // The start is over at the first launch, or at a drain that came before one: a run whose backlog
37
+ // emptied with nothing launched is watching, and the hand-off each wake of that watch writes is not a
38
+ // run getting ready.
39
+ function has_started(events: ReadonlyArray<RunEvent>): boolean {
40
+ return has_launched(events) || events.some((event) => event.kind === KIND.DRAIN)
41
+ }
42
+
43
+ // A lane is opening while the stage naming its issue is newer than the run's newest position: the
44
+ // launch it leads to, or whatever the run did instead, ends it. A stage that names no issue — a
45
+ // hand-off between two children — prepares nothing a person waits on.
46
+ function is_opening(events: ReadonlyArray<RunEvent>, stage: RunEvent): boolean {
47
+ if (run_event_scope.issue_named(stage) === undefined) return false
48
+
49
+ const position = events.findLast((event) => !run_event_stream.TRACE_KINDS.has(event.kind))
50
+
51
+ return position === undefined || position.pos < stage.pos
52
+ }
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
+
62
+ function read(events: ReadonlyArray<RunEvent>): SetupRead {
63
+ const stage = events.findLast((event) => is_setup(event))
64
+
65
+ if (stage === undefined || !is_standing(events, stage)) return NO_SETUP
66
+
67
+ return { setup: stage.text, setup_ms: Date.parse(stage.at) }
68
+ }
69
+
70
+ function names(event: RunEvent, issue: number): boolean {
71
+ return Number(run_event_scope.issue_named(event)) === issue
72
+ }
73
+
74
+ // A lane the stream saw no launch for is not yet this run's child when the run says it is still
75
+ // setting up: it has launched nothing at all, so any lane here is an earlier run's, or it is opening
76
+ // this very lane. A stream with no stage on it says neither, and the lane is read as before.
77
+ function is_lane_pending(events: ReadonlyArray<RunEvent>, issue: number): boolean {
78
+ const stages = events.filter((event) => is_setup(event))
79
+
80
+ if (stages.length === 0) return false
81
+
82
+ return !has_launched(events) || stages.some((stage) => names(stage, issue))
83
+ }
84
+
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 }
119
+
120
+ export { run_board_setup }
121
+ export type { LaneStage, SetupRead }
@@ -1,3 +1,4 @@
1
+ import type { PendingLaunch } from '#scripts/backlog/backlogrun-launch'
1
2
  import type { CarryRead } from '#scripts/run/carry/run-carry'
2
3
  import type { EndedRun } from '#scripts/run/carry/run-carry-ended'
3
4
  import { run_event_scope } from '#scripts/run/event/run-event-scope'
@@ -27,9 +28,40 @@ function ended_span(ended: EndedRun | undefined): BoardSpan | undefined {
27
28
  return { invocation: ended.invocation, started_ms, ended_ms }
28
29
  }
29
30
 
31
+ // A run `josh backlogrun` launched and its parent has not claimed yet: it has no carry record, so the
32
+ // launch record is the only one that says a run is here and since when.
33
+ function launch_span(launch: PendingLaunch | undefined): BoardSpan | undefined {
34
+ const started_ms = launch === undefined ? undefined : moment_of(launch.started_at)
35
+
36
+ if (launch === undefined || started_ms === undefined) return undefined
37
+
38
+ return { invocation: launch.invocation, started_ms, ended_ms: undefined }
39
+ }
40
+
41
+ // With no carry record: the run on its way, else the one that last ended. **A launch older than that
42
+ // end is the ended run's own** — its parent outlives `run:carry --end` for a moment — so it never
43
+ // redraws a finished run as one starting.
44
+ function idle_span(
45
+ ended: EndedRun | undefined,
46
+ launch: PendingLaunch | undefined,
47
+ ): BoardSpan | undefined {
48
+ const last = ended_span(ended)
49
+ const coming = launch_span(launch)
50
+
51
+ if (coming === undefined) return last
52
+
53
+ return (last?.ended_ms ?? 0) >= coming.started_ms ? last : coming
54
+ }
55
+
30
56
  // The running run wins over the ended one, so a run that begins replaces the finished board at once.
31
- function span_of(read: CarryRead, ended: EndedRun | undefined): BoardSpan | undefined {
32
- if (read.kind !== 'carried') return read.kind === 'none' ? ended_span(ended) : undefined
57
+ // **The launch's start is the carry's**: `run:carry --begin` inherits it for the parent the launch
58
+ // started, so the clock does not restart when the record appears.
59
+ function span_of(
60
+ read: CarryRead,
61
+ ended: EndedRun | undefined,
62
+ launch?: PendingLaunch,
63
+ ): BoardSpan | undefined {
64
+ if (read.kind !== 'carried') return read.kind === 'none' ? idle_span(ended, launch) : undefined
33
65
 
34
66
  const started_ms = moment_of(read.carry.started_at)
35
67
 
@@ -38,12 +70,22 @@ function span_of(read: CarryRead, ended: EndedRun | undefined): BoardSpan | unde
38
70
  return { invocation: read.carry.invocation, started_ms, ended_ms: undefined }
39
71
  }
40
72
 
41
- // The session a person resumes a stopped run from: only while the ended run is
42
- // the one drawn, and only when it stopped rather than finished. A run that begins hides it at once.
43
- function resume_of(read: CarryRead, ended: EndedRun | undefined): string | undefined {
44
- if (read.kind !== 'none' || ended?.stopped === undefined) return undefined
73
+ // Whether the span drawn with no carry record is the ended run's, rather than a launch's.
74
+ function is_ended_drawn(ended: EndedRun | undefined, launch: PendingLaunch | undefined): boolean {
75
+ return idle_span(ended, launch)?.ended_ms !== undefined
76
+ }
45
77
 
46
- return ended.session
78
+ // The session a person resumes a stopped run from: only while the ended run is
79
+ // the one drawn, and only when it stopped rather than finished. A run that begins — or is on its way —
80
+ // hides it at once.
81
+ function resume_of(
82
+ read: CarryRead,
83
+ ended: EndedRun | undefined,
84
+ launch?: PendingLaunch,
85
+ ): string | undefined {
86
+ if (read.kind !== 'none' || !is_ended_drawn(ended, launch)) return undefined
87
+
88
+ return ended?.stopped === undefined ? undefined : ended.session
47
89
  }
48
90
 
49
91
  function is_within(event: RunEvent, span: BoardSpan): boolean {
@@ -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 {