@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
@@ -1,26 +1,34 @@
1
1
  import { styleText } from 'node:util'
2
2
  import { backlog_budget } from '#scripts/backlog/backlog-budget'
3
3
  import { run_board_fit } from './run-board-fit'
4
+ import { run_board_fresh } from './run-board-fresh'
4
5
  import { run_board_labels } from './run-board-labels'
5
6
  import { run_board_layout, type BoardLayout, type BoardRow } from './run-board-layout'
6
7
  import { run_board_machine, type MachineGauges } from './run-board-machine'
8
+ import { run_board_now, type NowLine } from './run-board-now'
9
+ import type { SessionOperations } from './run-board-operation-read'
7
10
  import { run_board_render_slot } from './run-board-render-slot'
8
11
  import type { RunActivity } from './run-board-status'
9
12
  import type { LaneUsages } from './run-board-usage'
13
+ import type { UsualTimes } from './run-board-usual'
10
14
 
11
15
  // The top of `run:board`: whether the run is moving, how long it has run, how fresh its last event is,
12
16
  // and how far through the plan it is. Every time comes from a record (the carry's start, the stream's
13
- // events); nothing here guesses one. Lines of symbols rather than sentences: the `⏰` that moves every
14
- // second is the proof the board is live, so no `updated` line is drawn. **Always two lines, in three
15
- // columns**: the run's mark and name over its repository's, the run's age over the `💓`, then the
16
- // progress over the machine the run is on — so the first line reads as how far the run is and the
17
- // second as what it runs on, a slow run apart from a stuck one. Both are written the same while the
18
- // run works and while it waits, so a wait is read off the mark and the `💓` rather than off a line
19
- // that changes shape. A value not read yet is drawn `-`, never `0`, so a header still loading reads
20
- // as unknown rather than as an empty run.
17
+ // events); nothing here guesses one. Two lines of symbols rather than sentences: the `⏰` that moves
18
+ // every second is the proof the board is live, so no `updated` line is drawn. **Always three lines,
19
+ // the first two in three columns**: the run's mark and name over its repository's, the run's age over
20
+ // the `💓`, then the progress over the machine the run is on — so the first line reads as how far the
21
+ // run is and the second as what it runs on, a slow run apart from a stuck one. The third says what the
22
+ // run is doing now (`run-board-now.ts`): the spinner, its time and its kind's icon under the second and
23
+ // third columns' icons and digits, so the mark itself never turns. All three are written the
24
+ // same while the run works and while it waits, so a wait is read off the mark, the `💓` and the third
25
+ // line's words rather than off a line that changes shape. A value not read yet is drawn `-`, never
26
+ // `0`, so a header still loading reads as unknown rather than as an empty run.
21
27
 
22
28
  const { HEADER_ICONS, STATE_ICONS, WORDS, bar_of, minute_of, span_of } = run_board_labels
23
29
  const { cell_of } = run_board_render_slot
30
+ const { padded } = run_board_fresh
31
+ const aged_fresh = run_board_fresh.aged
24
32
  const UNKNOWN = '-'
25
33
  const BLANK = ' '
26
34
  const REPO_SEPARATOR = '/'
@@ -44,6 +52,8 @@ interface BoardHeader {
44
52
  // Set once the run has ended: the title then says when, and how long it took.
45
53
  ended_ms: number | undefined
46
54
  activity: RunActivity
55
+ // The newest operation of each session of the run, `undefined` where none was read.
56
+ operations?: SessionOperations | undefined
47
57
  layout: BoardLayout | undefined
48
58
  // The total the board first saw, so arrivals since then read as `(+N)`.
49
59
  baseline_total: number | undefined
@@ -55,6 +65,8 @@ interface BoardHeader {
55
65
  machine: MachineGauges | undefined
56
66
  // Each running lane's CPU and memory, `undefined` where none could be read.
57
67
  usages?: LaneUsages | undefined
68
+ // How long each phase usually takes; a running row's phase time is colored against it.
69
+ usual?: UsualTimes | undefined
58
70
  // The spinner's frame a running run and a running row turn, `undefined` where the output is not a
59
71
  // terminal and they draw their still icons.
60
72
  spinner: string | undefined
@@ -74,13 +86,15 @@ interface BoardCounts {
74
86
  remaining: number
75
87
  }
76
88
 
77
- type RunMark = 'running' | 'idle' | 'human' | 'stopped'
89
+ // `setup` is a run with no child running that is itself at work on a stage (`run-board-setup.ts`).
90
+ type RunMark = 'running' | 'setup' | 'idle' | 'human' | 'stopped'
78
91
 
79
92
  type TextColor = Parameters<typeof styleText>[0]
80
93
 
81
94
  // The run's state as a colored glyph before its name.
82
95
  const RUN_MARKS: Readonly<Record<RunMark, { glyph: string; color: TextColor }>> = {
83
96
  running: { glyph: '▶', color: 'green' },
97
+ setup: { glyph: '▶', color: 'green' },
84
98
  idle: { glyph: '⏸', color: 'yellow' },
85
99
  human: { glyph: '✋', color: 'magenta' },
86
100
  stopped: { glyph: '■', color: 'gray' },
@@ -99,9 +113,16 @@ function counts_of(layout: BoardLayout): BoardCounts {
99
113
  return { total, settled, parked, running, remaining: total - settled - running }
100
114
  }
101
115
 
102
- // Nothing running: the run is waiting on a person when all the plan has left is what
103
- // `needs-decision` holds, and otherwise idle.
104
- function quiet_mark(layout: BoardLayout | undefined): RunMark {
116
+ // The marks of a run that is working: they turn the spinner, and only they can go stale.
117
+ const WORKING_MARKS: ReadonlySet<RunMark> = new Set<RunMark>(['running', 'setup'])
118
+
119
+ // Nothing running: the run is at work on a stage of its own; else it is waiting on a person when all
120
+ // the plan has left is what `needs-decision` holds, or idle.
121
+ function quiet_mark(header: BoardHeader): RunMark {
122
+ const { layout } = header
123
+
124
+ if (header.activity.setup !== undefined) return 'setup'
125
+
105
126
  return layout?.waves.length === 0 && layout.people.length > 0 ? 'human' : 'idle'
106
127
  }
107
128
 
@@ -109,15 +130,14 @@ function quiet_mark(layout: BoardLayout | undefined): RunMark {
109
130
  function mark_of(header: BoardHeader, running: number): RunMark {
110
131
  if (header.activity.is_stopped) return 'stopped'
111
132
 
112
- return running > 0 ? 'running' : quiet_mark(header.layout)
133
+ return running > 0 ? 'running' : quiet_mark(header)
113
134
  }
114
135
 
115
- // A running run turns the spinner in place of ▶, both one column wide; every other mark stands still.
116
- function run_part(mark: RunMark, spinner: string | undefined): string {
136
+ // The mark stands still in every state: the spinner turns on the third line.
137
+ function run_part(mark: RunMark): string {
117
138
  const { glyph, color } = RUN_MARKS[mark]
118
- const shown = mark === 'running' ? (spinner ?? glyph) : glyph
119
139
 
120
- return styleText(color, `${shown} ${RUN_NAME}`)
140
+ return styleText(color, `${glyph} ${RUN_NAME}`)
121
141
  }
122
142
 
123
143
  // The repository's own name — `kit` of `owner/kit` — under the run's, past the mark's columns, so a
@@ -129,6 +149,8 @@ function repo_part(header: BoardHeader, mark: RunMark): string {
129
149
  return `${BLANK.repeat(indent)}${name}`
130
150
  }
131
151
 
152
+ // A part aged as the `💓` is: yellow once stale, red once alarming — a row's newest activity too
153
+ // (`run-board-render-tail.ts`).
132
154
  function aged(part: string, age_ms: number): string {
133
155
  if (age_ms > ALARM_MS) return styleText('red', part)
134
156
 
@@ -153,26 +175,76 @@ function ended_part(header: BoardHeader): string {
153
175
  }
154
176
 
155
177
  // The `💓` under the run's age, in the `⏰`'s own format with its digits right-aligned to `width` —
156
- // only a running run can be stale, so only it is colored. Only an ended run draws anything else there:
157
- // the minute it ended.
178
+ // only a working run can be stale, so only it is colored. Only an ended run draws anything else there:
179
+ // the minute it ended. Its digits are emphasized for a few seconds after a new event resets them
180
+ // (`run-board-fresh.ts`).
158
181
  function beat_part(header: BoardHeader, mark: RunMark, width: number): string {
159
182
  const age = age_of(header)
160
183
 
161
184
  if (age === undefined) return ended_part(header)
162
185
 
163
- const part = `${HEADER_ICONS.heartbeat} ${span_of(age).padStart(width)}`
186
+ const span = span_of(age)
187
+ const part = `${HEADER_ICONS.heartbeat} ${padded(span, width, aged_fresh(span, age, header))}`
164
188
 
165
- return mark === 'running' ? aged(part, age) : part
189
+ return WORKING_MARKS.has(mark) ? aged(part, age) : part
166
190
  }
167
191
 
168
- // A run's age over the `💓`, or over the minute an ended run ended: the shorter of the two lengths is
169
- // right-aligned to the longer, so the digits stand in one column when only one has passed ten hours.
170
- function time_parts(header: BoardHeader, mark: RunMark): Array<string> {
171
- const elapsed = span_of((header.ended_ms ?? header.now_ms) - header.started_ms)
192
+ // How long ago what the third line says happened, blank where no event gave it a time.
193
+ function now_age(now: NowLine | undefined): string {
194
+ const age = now?.age_ms
195
+
196
+ return age === undefined ? '' : span_of(age)
197
+ }
198
+
199
+ // How long ago what the third line says happened, counted to now: an ended run's age stops at its end,
200
+ // so its last words would otherwise stay fresh on every frame after it.
201
+ function fresh_age(now: NowLine | undefined, header: BoardHeader): number | undefined {
202
+ const age = now?.age_ms
203
+
204
+ return age === undefined ? undefined : age + header.now_ms - (header.ended_ms ?? header.now_ms)
205
+ }
206
+
207
+ // The `💓`'s digits, blank where it draws none.
208
+ function beat_span(header: BoardHeader): string {
172
209
  const age = age_of(header)
173
- const width = Math.max(elapsed.length, age === undefined ? 0 : span_of(age).length)
174
210
 
175
- return [`${HEADER_ICONS.elapsed} ${elapsed.padStart(width)}`, beat_part(header, mark, width)]
211
+ return age === undefined ? '' : span_of(age)
212
+ }
213
+
214
+ // The third line's time: the spinner in the `⏰`'s and `💓`'s column — turning only while the run
215
+ // works, so a watch or an end stands blank — then its age, digits under theirs, emphasized while what
216
+ // the line says is fresh.
217
+ function now_time(
218
+ header: BoardHeader,
219
+ mark: RunMark,
220
+ now: NowLine | undefined,
221
+ width: number,
222
+ ): string {
223
+ const spinner = WORKING_MARKS.has(mark) ? header.spinner : undefined
224
+ const icon = cell_of(spinner, run_board_fit.width_of(HEADER_ICONS.heartbeat))
225
+ const age = now_age(now)
226
+
227
+ return `${icon} ${padded(age, width, aged_fresh(age, fresh_age(now, header), header))}`
228
+ }
229
+
230
+ // What the third line says, emphasized while it is fresh.
231
+ function now_text(now: NowLine, header: BoardHeader): string {
232
+ return `${now.icon} ${aged_fresh(now.text, fresh_age(now, header), header)}`
233
+ }
234
+
235
+ // A run's age over the `💓` — or over the minute an ended run ended — over the third line's age: the
236
+ // shorter lengths are right-aligned to the longest, so the digits stand in one column when only one has
237
+ // passed ten hours.
238
+ function time_parts(header: BoardHeader, mark: RunMark, now: NowLine | undefined): Array<string> {
239
+ const elapsed = span_of((header.ended_ms ?? header.now_ms) - header.started_ms)
240
+ const age = now_age(now)
241
+ const width = Math.max(...[elapsed, beat_span(header), age].map((span) => span.length))
242
+
243
+ return [
244
+ `${HEADER_ICONS.elapsed} ${elapsed.padStart(width)}`,
245
+ beat_part(header, mark, width),
246
+ now_time(header, mark, now, width),
247
+ ]
176
248
  }
177
249
 
178
250
  // A plan fetch is said only when it failed — the board otherwise keeps quiet about a routine read.
@@ -250,6 +322,27 @@ function progress_part(counts: BoardCounts, header: BoardHeader): string {
250
322
  return parts.filter((part) => part !== undefined).join(GAP)
251
323
  }
252
324
 
325
+ // The children drawn as running: only their operations are the run's newest.
326
+ function running_issues(layout: BoardLayout): ReadonlySet<number> {
327
+ const running = layout.active.filter((row) => row.state === 'running')
328
+
329
+ return new Set(running.map((row) => row.number))
330
+ }
331
+
332
+ // What the run is doing now (`run-board-now.ts`).
333
+ function now_line(counts: BoardCounts, header: BoardHeader): NowLine | undefined {
334
+ return run_board_now.line_of({
335
+ now_ms: header.now_ms,
336
+ ended_ms: header.ended_ms,
337
+ activity: header.activity,
338
+ operations: header.operations,
339
+ children: running_issues(layout_of(header)),
340
+ running: counts.running,
341
+ waiting: is_plan_read(header) ? counts.remaining : undefined,
342
+ people: layout_of(header).people.length,
343
+ })
344
+ }
345
+
253
346
  // The gauges the machine line draws: every one on a screen, the memory alone in a chat.
254
347
  function machine_of(header: BoardHeader): MachineGauges | undefined {
255
348
  const { machine } = header
@@ -271,28 +364,37 @@ function machine_part(counts: BoardCounts, header: BoardHeader): string {
271
364
  return run_board_machine.line_of(machine_of(header), width) ?? MACHINE_UNKNOWN
272
365
  }
273
366
 
274
- // Both lines' cells of one column, the narrower padded to the wider as the terminal counts them.
367
+ // The lines' cells of one column, the narrower padded to the wider as the terminal counts them.
275
368
  function column_of(cells: ReadonlyArray<string>): Array<string> {
276
369
  const width = Math.max(...cells.map((cell) => run_board_fit.width_of(cell)))
277
370
 
278
371
  return cells.map((cell) => cell_of(cell, width))
279
372
  }
280
373
 
281
- // The run's name, age and progress, then its repository, heartbeat and machine under them, in every
282
- // state the board can be in.
374
+ // The run's name, age and progress, then its repository, heartbeat and machine under them, then what
375
+ // it is doing now — its time under theirs, its kind's icon under the `⚡`, the name's column left blank
376
+ // and no label — in every state the board can be in. A stream with no event yet leaves it blank.
283
377
  function header_lines(header: BoardHeader): Array<string> {
284
378
  const counts = counts_of(layout_of(header))
285
379
  const mark = mark_of(header, counts.running)
286
- const columns = [
287
- column_of([run_part(mark, header.spinner), repo_part(header, mark)]),
288
- column_of(time_parts(header, mark)),
289
- [progress_part(counts, header), machine_part(counts, header)],
290
- ]
380
+ const now = now_line(counts, header)
381
+ const names = column_of([run_part(mark), repo_part(header, mark), ''])
382
+ const times = column_of(time_parts(header, mark, now))
383
+ const lasts = [progress_part(counts, header), machine_part(counts, header)]
384
+ const third =
385
+ now === undefined ? '' : [names.at(-1), times.at(-1), now_text(now, header)].join(GAP)
386
+
387
+ return [...lasts.map((last, line) => [names[line], times[line], last].join(GAP)), third]
388
+ }
389
+
390
+ // The third line's kind as the legend names it, `undefined` where the line's own words name it.
391
+ function now_legend(header: BoardHeader): string | undefined {
392
+ const now = now_line(counts_of(layout_of(header)), header)
291
393
 
292
- return [0, 1].map((line) => columns.map((column) => column[line]).join(GAP))
394
+ return now?.legend === undefined ? undefined : `${now.icon} ${now.legend}`
293
395
  }
294
396
 
295
- const run_board_header = { counts_of, header_lines, is_plan_read }
397
+ const run_board_header = { aged, counts_of, header_lines, is_plan_read, now_legend }
296
398
 
297
399
  export { run_board_header }
298
400
  export type { BoardCounts, BoardForm, BoardHeader }
@@ -1,6 +1,7 @@
1
1
  import { WriteStream } from 'node:tty'
2
2
  import { styleText } from 'node:util'
3
3
  import type { FiledKind } from '#scripts/run/event/run-event-filed'
4
+ import { run_event_setup } from '#scripts/run/event/run-event-setup'
4
5
  import cli_spinners from 'cli-spinners'
5
6
  import type { BoardNote } from './run-board-notes'
6
7
  import type { Phase } from './run-board-phase'
@@ -67,6 +68,12 @@ const WORDS = {
67
68
  park: 'park',
68
69
  note: 'note',
69
70
  found_during: ' (found during {n})',
71
+ // What the run is doing now, where no stage of its own says it.
72
+ watching: 'watching',
73
+ pool_empty: 'pool empty',
74
+ on_person: 'waiting on a person',
75
+ operation: 'operation',
76
+ ended: 'ended',
70
77
  // The session a stopped run waits in, and what closing the board leaves.
71
78
  resume: 'stopped — resume with',
72
79
  keeps_running: 'Closing this screen leaves the run going · reopen with `pnpm josh backlogrun`',
@@ -175,6 +182,24 @@ const HEADER_ICONS = {
175
182
  heartbeat: '💓',
176
183
  } as const
177
184
 
185
+ // The kind of what the header's third line says, under the `⚡` (joshuafolkken/kit#3767): a call a
186
+ // session made, the run watching its pool, the run ended, or each of the run's own stages, keyed by the
187
+ // stage's words before any issue. A child's event draws its row's phase icon instead. The run ended
188
+ // draws the header's own end mark — 🏁 is already a done row's state icon.
189
+ const NOW_ICONS = { operation: '🔧', watching: '🔭', ended: HEADER_ICONS.ended } as const
190
+ const { STAGE, LANE_STAGE } = run_event_setup
191
+ const STAGE_ICONS: Readonly<Partial<Record<string, string>>> = {
192
+ [STAGE.STARTING]: '🚀',
193
+ [STAGE.PLANNING]: '📝',
194
+ [STAGE.HANDING_OFF]: '🤝',
195
+ [STAGE.JUDGING]: '🤔',
196
+ [STAGE.SESSION]: '💭',
197
+ [STAGE.NO_CHILD]: '🚫',
198
+ [LANE_STAGE.OPENING]: '🚪',
199
+ [LANE_STAGE.INSTALLING]: '📥',
200
+ [LANE_STAGE.DISPATCHING]: '📤',
201
+ }
202
+
178
203
  // **The machine gauges' three colors, decided here alone**: Activity Monitor's green, yellow and red
179
204
  // in the system's dark-mode values. A palette's `green` draws blue and its `red` orange in a VSCode
180
205
  // terminal, so the normal gauge would read as the cyan plan bar and the busy one as yellow.
@@ -214,17 +239,32 @@ function elapsed_of(ms: number): string {
214
239
  return `${two_digits(minutes)}:${two_digits(seconds % SECONDS_PER_MINUTE)}`
215
240
  }
216
241
 
217
- // How long the run has run or its stream has been silent, as `H:MM:SS` with the hours not padded; a negative
218
- // span reads as zero. Always three fields, so a run under an hour (`0:03:25`) never reads as a time.
219
- function span_of(ms: number): string {
242
+ // A length as its hours, and its minutes and seconds within the hour; a negative span reads as zero.
243
+ function span_fields(ms: number): [hours: number, minutes: number, seconds: number] {
220
244
  const seconds = Math.floor(Math.max(0, ms) / MS_PER_SECOND)
221
245
  const minutes = Math.floor(seconds / SECONDS_PER_MINUTE)
222
246
  const hours = Math.floor(minutes / MINUTES_PER_HOUR)
223
- const tail = [minutes % MINUTES_PER_HOUR, seconds % SECONDS_PER_MINUTE]
247
+
248
+ return [hours, minutes % MINUTES_PER_HOUR, seconds % SECONDS_PER_MINUTE]
249
+ }
250
+
251
+ // How long the run has run or its stream has been silent, as `H:MM:SS` with the hours not padded; a negative
252
+ // span reads as zero. Always three fields, so a run under an hour (`0:03:25`) never reads as a time.
253
+ function span_of(ms: number): string {
254
+ const [hours, ...tail] = span_fields(ms)
224
255
 
225
256
  return [String(hours), ...tail.map((value) => two_digits(value))].join(':')
226
257
  }
227
258
 
259
+ // How long the run has stood at its stage, as `M:SS` with the minutes not padded, and as `span_of`'s
260
+ // `H:MM:SS` from an hour on. It stands beside the stage's words, never in the header's time column, so
261
+ // its one separator is not read there as a time of day.
262
+ function stage_span_of(ms: number): string {
263
+ const [hours, minutes, seconds] = span_fields(ms)
264
+
265
+ return hours === 0 ? `${String(minutes)}:${two_digits(seconds)}` : span_of(ms)
266
+ }
267
+
228
268
  function is_shade(color: Paint): color is Shade {
229
269
  return typeof color === 'object' && 'rgb' in color
230
270
  }
@@ -281,9 +321,11 @@ const run_board_labels = {
281
321
  KIND_WORDS,
282
322
  NOTES_ICON,
283
323
  NOTE_ICONS,
324
+ NOW_ICONS,
284
325
  PHASE_ICONS,
285
326
  PHASE_WORDS,
286
327
  SPINNER_INTERVAL_MS,
328
+ STAGE_ICONS,
287
329
  STATE_ICONS,
288
330
  WAITS_ICON,
289
331
  WORDS,
@@ -294,6 +336,7 @@ const run_board_labels = {
294
336
  painted,
295
337
  span_of,
296
338
  spinner_of,
339
+ stage_span_of,
297
340
  }
298
341
 
299
342
  export { run_board_labels }
@@ -0,0 +1,66 @@
1
+ import { run_event_scope } from '#scripts/run/event/run-event-scope'
2
+ import { run_event_stream, type RunEvent } from '#scripts/run/event/run-event-stream'
3
+ import { run_ship_stage } from '#scripts/run/ship/run-ship-stage'
4
+ import { run_board_phase, type Phase } from './run-board-phase'
5
+
6
+ // Each running child's newest event of its own — the `lane-phase` the hook writes or a `ship-stage` its
7
+ // ship writes — in words, with the phase whose icon its row's track draws. A child inside a long
8
+ // `josh ship` makes no call of its own while the ship writes on, so the header's third line reads the
9
+ // newer of this and the child's newest operation (joshuafolkken/kit#3767).
10
+
11
+ interface LaneEvent {
12
+ phase: Phase
13
+ // The event's words after its issue: `implement`, `ship gate`, `ship gate failed`.
14
+ words: string
15
+ at_ms: number
16
+ }
17
+
18
+ const KIND = run_event_stream.EVENT_KIND
19
+ const LANE_KINDS: ReadonlySet<string> = new Set([KIND.LANE_PHASE, KIND.SHIP_STAGE])
20
+ const WORD_SEPARATOR = ' '
21
+ const SHIP = 'ship'
22
+
23
+ // A ship's stage reads as `ship <stage>` once started and names how it ended after that; a lane's phase
24
+ // reads as its own word.
25
+ function words_of(event: RunEvent): string {
26
+ const [, stage = '', phase] = event.text.split(WORD_SEPARATOR)
27
+
28
+ if (event.kind === KIND.LANE_PHASE) return stage
29
+
30
+ const words = [SHIP, stage]
31
+
32
+ return (phase === run_ship_stage.PHASE.START ? words : [...words, phase]).join(WORD_SEPARATOR)
33
+ }
34
+
35
+ function lane_event_of(event: RunEvent): LaneEvent | undefined {
36
+ const phase = LANE_KINDS.has(event.kind) ? run_board_phase.stage_phase_of(event) : undefined
37
+
38
+ return phase === undefined
39
+ ? undefined
40
+ : { phase, words: words_of(event), at_ms: Date.parse(event.at) }
41
+ }
42
+
43
+ // The stream's bound keeps each issue's newest stage start apart from the rest, so the order of the
44
+ // lines is not their age.
45
+ function is_newer(lane: LaneEvent, held: LaneEvent | undefined): boolean {
46
+ return held === undefined || lane.at_ms >= held.at_ms
47
+ }
48
+
49
+ // The newest lane event of every issue the stream names one for.
50
+ function newest_of(events: ReadonlyArray<RunEvent>): ReadonlyMap<number, LaneEvent> {
51
+ const newest = new Map<number, LaneEvent>()
52
+
53
+ for (const event of events) {
54
+ const lane = lane_event_of(event)
55
+ const issue = Number(run_event_scope.issue_named(event))
56
+
57
+ if (lane !== undefined && is_newer(lane, newest.get(issue))) newest.set(issue, lane)
58
+ }
59
+
60
+ return newest
61
+ }
62
+
63
+ const run_board_lane_event = { newest_of }
64
+
65
+ export { run_board_lane_event }
66
+ export type { LaneEvent }
@@ -1,3 +1,4 @@
1
+ import type { Declared } from '#scripts/backlog/backlog-overlap'
1
2
  import { backlog_plan, type PlanContext } from '#scripts/backlog/backlog-plan'
2
3
  import type { WavePlan } from '#scripts/backlog/backlog-waves'
3
4
  import type { EpicChild } from '#scripts/epic/epic-graph'
@@ -25,8 +26,22 @@ interface BoardPlan {
25
26
  // Each issue's label names as the open listing read them, so a running row is drawn as its labels
26
27
  // say it stopped.
27
28
  labels: ReadonlyMap<number, ReadonlyArray<string>>
29
+ // The paths each issue's body declares (`Plan.scope.declared`), which a waiting row is checked
30
+ // against a running one by; a plan read without them checks none.
31
+ declared?: Declared | undefined
28
32
  }
29
33
 
34
+ // Why a waiting row waits, or the stage its lane's launch is at, in the board's own words.
35
+ interface RowReason {
36
+ text: string
37
+ // When the row entered a stage, where the reason is one — how long it has stood there is counted
38
+ // from it.
39
+ since_ms?: number | undefined
40
+ }
41
+
42
+ // What a waiting child's row says after its blockers; `undefined` says nothing.
43
+ type ReasonOf = (child: EpicChild) => RowReason | undefined
44
+
30
45
  interface BoardRow {
31
46
  number: number
32
47
  title: string | undefined
@@ -36,6 +51,8 @@ interface BoardRow {
36
51
  status: ItemStatus | undefined
37
52
  // The standing blockers this row still waits on, as the plan names them.
38
53
  waits: ReadonlyArray<string>
54
+ // Why the row waits beyond its blockers (`run-board-reason.ts`); a row the run has touched has none.
55
+ reason?: RowReason | undefined
39
56
  }
40
57
 
41
58
  type WaveEntry =
@@ -60,6 +77,11 @@ interface Overlay {
60
77
  statuses: ReadonlyMap<number, ItemStatus>
61
78
  }
62
79
 
80
+ // The overlay a layout is drawn from: the plan under the statuses, and what each waiting row says.
81
+ interface Drawn extends Overlay {
82
+ reason_of: ReasonOf | undefined
83
+ }
84
+
63
85
  const SETTLED_STATES: ReadonlySet<ItemState> = new Set(['merged', 'parked', 'done'])
64
86
 
65
87
  function is_settled(state: ItemState): boolean {
@@ -80,13 +102,20 @@ function reference_of(edge: IssueReference, repo: string): string {
80
102
  return edge.repo === repo ? String(edge.number) : issue_cite.plain(edge.number, edge.repo)
81
103
  }
82
104
 
83
- function plan_row(child: EpicChild, state: ItemState, overlay: Overlay): BoardRow {
105
+ // Whether a blocker the plan names still stands for `child` — the wait its row draws as 🔗.
106
+ function is_blocked(child: EpicChild, overlay: Overlay): boolean {
107
+ return child.blocked_by.some((edge) => is_waiting_on(edge, overlay))
108
+ }
109
+
110
+ function plan_row(child: EpicChild, state: ItemState, overlay: Drawn): BoardRow {
84
111
  const { context } = overlay.plan
85
112
  const waits = child.blocked_by
86
113
  .filter((edge) => is_waiting_on(edge, overlay))
87
114
  .map((edge) => reference_of(edge, context.repo))
115
+ const reason = state === 'waiting' ? overlay.reason_of?.(child) : undefined
88
116
 
89
117
  return {
118
+ ...(reason !== undefined && { reason }),
90
119
  number: child.number,
91
120
  title: context.titles.get(child.number),
92
121
  kind: run_board_kind.kind_of(child.labels),
@@ -119,7 +148,7 @@ function active_rows(statuses: ReadonlyMap<number, ItemStatus>, read: RowRead):
119
148
  function place_in_epic(
120
149
  entries: Array<WaveEntry>,
121
150
  epic: number,
122
- overlay: Overlay,
151
+ overlay: Drawn,
123
152
  row: BoardRow,
124
153
  ): void {
125
154
  const group = entries.find((entry) => entry.kind === 'epic' && entry.epic === epic)
@@ -144,7 +173,7 @@ function wave_state(child: EpicChild): ItemState {
144
173
 
145
174
  // One wave, its rows in the plan's order, an epic's children gathered under the epic where the first
146
175
  // of them falls.
147
- function wave_entries(wave: ReadonlyArray<EpicChild>, overlay: Overlay): Array<WaveEntry> {
176
+ function wave_entries(wave: ReadonlyArray<EpicChild>, overlay: Drawn): Array<WaveEntry> {
148
177
  const entries: Array<WaveEntry> = []
149
178
 
150
179
  for (const child of wave) {
@@ -158,7 +187,7 @@ function wave_entries(wave: ReadonlyArray<EpicChild>, overlay: Overlay): Array<W
158
187
  return entries
159
188
  }
160
189
 
161
- function unreached_rows(overlay: Overlay, is_people: boolean): Array<BoardRow> {
190
+ function unreached_rows(overlay: Drawn, is_people: boolean): Array<BoardRow> {
162
191
  const state: ItemState = is_people ? 'human' : 'waiting'
163
192
 
164
193
  return overlay.plan.waves.unreached
@@ -166,8 +195,25 @@ function unreached_rows(overlay: Overlay, is_people: boolean): Array<BoardRow> {
166
195
  .map((child) => plan_row(child, state, overlay))
167
196
  }
168
197
 
169
- function layout_of(plan: BoardPlan, statuses: ReadonlyMap<number, ItemStatus>): BoardLayout {
170
- const overlay = { plan, statuses }
198
+ // Every issue the plan holds, wave or unreached.
199
+ function children_of(plan: BoardPlan): Array<EpicChild> {
200
+ return [...plan.waves.waves.flat(), ...plan.waves.unreached]
201
+ }
202
+
203
+ // The children whose rows wait on the run rather than on a person: the run has not touched them, and
204
+ // no decision is asked of anyone.
205
+ function waiting_children(overlay: Overlay): Array<EpicChild> {
206
+ return children_of(overlay.plan).filter(
207
+ (child) => is_untouched(child, overlay) && !is_human(child),
208
+ )
209
+ }
210
+
211
+ function layout_of(
212
+ plan: BoardPlan,
213
+ statuses: ReadonlyMap<number, ItemStatus>,
214
+ reason_of?: ReasonOf,
215
+ ): BoardLayout {
216
+ const overlay = { plan, statuses, reason_of }
171
217
  const waves = plan.waves.waves.map((wave) =>
172
218
  wave_entries(
173
219
  wave.filter((child) => is_untouched(child, overlay)),
@@ -195,9 +241,9 @@ function local_layout_of(statuses: ReadonlyMap<number, ItemStatus>): BoardLayout
195
241
  }
196
242
  }
197
243
 
198
- // Every issue the plan holds, wave or unreached — the run's scope a lane is checked against.
244
+ // The run's scope a lane is checked against.
199
245
  function numbers_of(plan: BoardPlan): ReadonlySet<number> {
200
- return new Set([...plan.waves.waves.flat(), ...plan.waves.unreached].map((child) => child.number))
246
+ return new Set(children_of(plan).map((child) => child.number))
201
247
  }
202
248
 
203
249
  // Every row the layout draws, an epic's children among them — what the header counts and the legend reads.
@@ -211,12 +257,14 @@ function rows_of(layout: BoardLayout): Array<BoardRow> {
211
257
 
212
258
  const run_board_layout = {
213
259
  EMPTY_LAYOUT,
260
+ is_blocked,
214
261
  is_settled,
215
262
  layout_of,
216
263
  local_layout_of,
217
264
  numbers_of,
218
265
  rows_of,
266
+ waiting_children,
219
267
  }
220
268
 
221
269
  export { run_board_layout }
222
- export type { BoardLayout, BoardPlan, BoardRow, WaveEntry }
270
+ export type { BoardLayout, BoardPlan, BoardRow, Overlay, ReasonOf, RowReason, WaveEntry }