@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.
- package/.claude/skills/epic-commands/SKILL.md +1 -1
- package/.claude/skills/epic-commands/epic-bundle.md +5 -0
- package/.claude/skills/workflow-commands/backlogrun-lanes.md +5 -3
- package/.claude/skills/workflow-commands/backlogrun-steps.md +4 -3
- package/.claude/skills/workflow-commands/backlogrun.md +1 -0
- package/.claude/skills/workflow-commands/chain-rule.md +4 -2
- package/.claude/skills/workflow-commands/issue-comments.md +6 -0
- package/.claude/skills/workflow-commands/observation-ledger.md +3 -2
- package/.claude/skills/workflow-commands/pre-gate-cut.md +5 -3
- package/README.md +25 -0
- package/dist/hooks/audit-provision.js +2 -2
- package/dist/hooks/batch-guard.js +7 -7
- package/dist/hooks/{chunk-IYVOLN6A.js → chunk-2G5F2YCD.js} +3 -3
- package/dist/hooks/{chunk-2YSW3CXH.js → chunk-3LE6VLSR.js} +17 -6
- package/dist/hooks/{chunk-J3KSS7II.js → chunk-4BHUPFKF.js} +1 -1
- package/dist/hooks/{chunk-EAQEGME7.js → chunk-5SJSSCJG.js} +1 -1
- package/dist/hooks/{chunk-FZ6FUSBM.js → chunk-723HKZO5.js} +1 -1
- package/dist/hooks/{chunk-LMWBRJIP.js → chunk-7KMN5QB5.js} +3 -3
- package/dist/hooks/{chunk-NEC4AT4Z.js → chunk-DGT54IH5.js} +4 -0
- package/dist/hooks/{chunk-STDSEPWU.js → chunk-EMB55BDF.js} +4 -4
- package/dist/hooks/{chunk-JBBMAPC4.js → chunk-IX6TTTI6.js} +4 -0
- package/dist/hooks/{chunk-GJ7JPIHN.js → chunk-JXQBK323.js} +2674 -187
- package/dist/hooks/{chunk-DCY2GWHC.js → chunk-MSYSUSY6.js} +5 -5
- package/dist/hooks/{chunk-EBSHN25W.js → chunk-MU4LJFIV.js} +4 -4
- package/dist/hooks/{chunk-WNFJLBXI.js → chunk-QL6QTPWN.js} +3 -3
- package/dist/hooks/{chunk-SL4GQ2XP.js → chunk-XER6SSIB.js} +91 -18
- package/dist/hooks/{chunk-U32VSRNG.js → chunk-YW452ZAX.js} +89 -34
- package/dist/hooks/{chunk-GNDJ7NJ7.js → chunk-YZXYOAZ3.js} +202 -55
- package/dist/hooks/codex-hook-adapter.js +15 -15
- package/dist/hooks/duplicate-read-guard.js +8 -8
- package/dist/hooks/format-edited.js +5 -5
- package/dist/hooks/inputs.json +288 -229
- package/dist/hooks/investigation-guard.js +8 -8
- package/dist/hooks/pretool-guard.js +14 -14
- package/dist/hooks/session-lang.js +69 -16
- package/dist/hooks/stop-guard.js +13 -38
- package/dist/josh.js +62 -9
- package/package.json +4 -3
- package/prompts/review.md +22 -0
- package/scripts/agent/claude-agent-argv.ts +13 -1
- package/scripts/auth/auth-setup-cli.ts +101 -0
- package/scripts/auth/claude-token-output.ts +78 -0
- package/scripts/auth/claude-token.ts +68 -0
- package/scripts/backlog/backlog-drive-epic.ts +24 -8
- package/scripts/backlog/backlog-drive-launch.ts +3 -0
- package/scripts/backlog/backlog-drive-named-offer.ts +7 -2
- package/scripts/backlog/backlog-drive-named.ts +4 -2
- package/scripts/backlog/backlog-drive-unread.ts +56 -0
- package/scripts/backlog/backlog-drive.ts +16 -8
- package/scripts/backlog/backlog-overlap.ts +22 -5
- package/scripts/backlog/backlog-ready.ts +1 -3
- package/scripts/backlog/backlogrun-args.ts +13 -2
- package/scripts/backlog/backlogrun-cli.ts +91 -19
- package/scripts/backlog/backlogrun-launch.ts +24 -0
- package/scripts/backlog/backlogrun-replace.ts +166 -0
- package/scripts/backlog/backlogrun-stop.ts +178 -0
- package/scripts/behavior/behavior-cli.ts +6 -1
- package/scripts/doctor/doctor-io.ts +60 -9
- package/scripts/doctor/doctor.ts +20 -1
- package/scripts/document/document-byte-budget.ts +3 -3
- package/scripts/epic/epic-bundle-cli.ts +8 -8
- package/scripts/epic/epic-bundle-referenced.ts +2 -5
- package/scripts/epic/epic-bundle.ts +39 -4
- package/scripts/exports/unused-members-cli.ts +2 -1
- package/scripts/followup/followup-checks-event.ts +82 -0
- package/scripts/followup/git-followup-finish.ts +16 -2
- package/scripts/followup/git-followup-workflow.ts +5 -2
- package/scripts/followup/git-pr-followup-checks.ts +18 -4
- package/scripts/followup/git-pr-followup-wrapup.ts +5 -0
- package/scripts/followup/git-pr-followup.ts +4 -1
- package/scripts/fresh-machine/fresh-machine-cli.ts +204 -0
- package/scripts/fresh-machine/fresh-machine-docker.ts +65 -0
- package/scripts/fresh-machine/fresh-machine-image.ts +62 -0
- package/scripts/fresh-machine/fresh-machine-log.ts +61 -0
- package/scripts/fresh-machine/fresh-machine-patterns.ts +130 -0
- package/scripts/fresh-machine/fresh-machine-run.ts +215 -0
- package/scripts/fresh-machine/fresh-machine-script.ts +45 -0
- package/scripts/fresh-machine/fresh-machine-spec.ts +57 -0
- package/scripts/fresh-machine/specs/kit-test-backlogrun-python.json +34 -0
- package/scripts/fresh-machine/specs/kit-test-backlogrun.json +31 -0
- package/scripts/gate/gate-report.ts +70 -15
- package/scripts/gate/project-checks.ts +13 -2
- package/scripts/gate/scoped-green.ts +2 -1
- package/scripts/gate/verification-gate.ts +4 -3
- package/scripts/gh/gh-author-association.ts +14 -0
- package/scripts/gh/git-gh-conditional.ts +66 -0
- package/scripts/gh/git-gh-exec.ts +1 -1
- package/scripts/gh/git-gh-issue-read.ts +6 -3
- package/scripts/gh/git-gh-issue-write.ts +5 -1
- package/scripts/gh/git-gh-issue.ts +4 -1
- package/scripts/gh/git-pr-checks-eval.ts +13 -0
- package/scripts/gh/git-pr-checks-parse.ts +8 -1
- package/scripts/gh/git-pr-checks-watch.ts +11 -4
- package/scripts/gh/git-pr-checks.ts +36 -2
- package/scripts/git/git-command.ts +16 -4
- package/scripts/git/git-schemas.ts +1 -0
- package/scripts/git/git-spawn-sync.ts +1 -1
- package/scripts/hooks/pretool-guard.ts +3 -1
- package/scripts/init/hook-command-rewrite.ts +31 -0
- package/scripts/init/init-actions.ts +4 -1
- package/scripts/init/init-ai-copy.ts +33 -10
- package/scripts/init/init-basic.ts +8 -3
- package/scripts/init/init-logic-toolchain.ts +76 -0
- package/scripts/init/init-logic.ts +2 -0
- package/scripts/init/init.ts +5 -23
- package/scripts/init/project-profile.ts +36 -7
- package/scripts/init/start-exec.ts +27 -1
- package/scripts/init/start-plan.ts +15 -0
- package/scripts/init/start-steps.ts +46 -0
- package/scripts/init/start.ts +1 -0
- package/scripts/init/workflow-mark.ts +35 -0
- package/scripts/issue/issue-ack-cli.ts +84 -0
- package/scripts/issue/issue-labels.ts +40 -8
- package/scripts/issue/issue-state-cli.ts +14 -4
- package/scripts/issue/issue-watch-diff.ts +152 -0
- package/scripts/issue/issue-watch-gate.ts +81 -0
- package/scripts/issue/issue-watch-hook.ts +54 -0
- package/scripts/issue/issue-watch.ts +240 -0
- package/scripts/issue/measure-rerun-cli.ts +3 -4
- package/scripts/josh/josh-command-map.ts +4 -0
- package/scripts/josh/josh-commands-backlog.ts +2 -2
- package/scripts/josh/josh-commands-development.ts +1 -1
- package/scripts/josh/josh-commands-issue.ts +7 -0
- package/scripts/josh/josh-commands-lane.ts +11 -2
- package/scripts/josh/josh-commands-project.ts +24 -0
- package/scripts/josh/josh-verdict.ts +33 -4
- package/scripts/lane/claude-lane-broker.ts +142 -0
- package/scripts/lane/lane-await.ts +6 -2
- package/scripts/lane/lane-cache.ts +37 -7
- package/scripts/lane/lane-capacity.ts +8 -6
- package/scripts/lane/lane-child-invocation.ts +1 -0
- package/scripts/lane/lane-cli.ts +15 -1
- package/scripts/lane/lane-close.ts +15 -6
- package/scripts/lane/lane-dispatch.ts +36 -10
- package/scripts/lane/lane-environment.ts +1 -1
- package/scripts/lane/lane-launch-cli.ts +13 -4
- package/scripts/lane/lane-ledger.ts +21 -3
- package/scripts/lane/lane-measure-cli.ts +10 -2
- package/scripts/lane/lane-open.ts +9 -1
- package/scripts/lane/lane-preserve.ts +49 -0
- package/scripts/lane/lane-reap.ts +20 -5
- package/scripts/lane/lane-registry.ts +6 -0
- package/scripts/lane/lane-restart-cli.ts +51 -0
- package/scripts/lane/lane-restart.ts +175 -0
- package/scripts/lane/{openai-review-broker.ts → lane-session-broker.ts} +106 -28
- package/scripts/lane/lane-spent.ts +195 -0
- package/scripts/lane/lane-stage-stats.ts +27 -3
- package/scripts/lane/lane-stop-stats.ts +96 -0
- package/scripts/lane/openai-lane-supervisor.ts +2 -2
- package/scripts/lane/process-tools.ts +31 -0
- package/scripts/lib/buffered-process.ts +36 -2
- package/scripts/lib/pnpm-reach.ts +27 -0
- package/scripts/lib/status-icons.ts +5 -1
- package/scripts/lint/lint-parallel.ts +19 -2
- package/scripts/observations/observation-ledger-home.ts +1 -0
- package/scripts/review/review-attest-cli.ts +21 -6
- package/scripts/review/review-brief-cli.ts +2 -2
- package/scripts/review/review-comment-round.ts +114 -0
- package/scripts/review/review-record-cli.ts +39 -17
- package/scripts/rules/implementation-cut.ts +3 -3
- package/scripts/rules/pre-gate-cut.ts +1 -1
- package/scripts/run/board/run-board-checks.ts +42 -0
- package/scripts/run/board/run-board-cli.ts +2 -0
- package/scripts/run/board/run-board-every.ts +3 -1
- package/scripts/run/board/run-board-fit.ts +16 -5
- package/scripts/run/board/run-board-fresh.ts +64 -0
- package/scripts/run/board/run-board-header.ts +129 -53
- package/scripts/run/board/run-board-labels.ts +47 -4
- package/scripts/run/board/run-board-lane-event.ts +66 -0
- package/scripts/run/board/run-board-layout.ts +57 -9
- package/scripts/run/board/run-board-now.ts +234 -0
- package/scripts/run/board/run-board-operation-read.ts +75 -0
- package/scripts/run/board/run-board-operation.ts +150 -0
- package/scripts/run/board/run-board-phase.ts +26 -1
- package/scripts/run/board/run-board-plan.ts +4 -6
- package/scripts/run/board/run-board-read.ts +49 -3
- package/scripts/run/board/run-board-reason.ts +124 -0
- package/scripts/run/board/run-board-render-checks.ts +55 -0
- package/scripts/run/board/run-board-render-legend.ts +12 -3
- package/scripts/run/board/run-board-render-slot.ts +2 -2
- package/scripts/run/board/run-board-render-tail.ts +128 -0
- package/scripts/run/board/run-board-render-thanks.ts +50 -0
- package/scripts/run/board/run-board-render-title.ts +58 -0
- package/scripts/run/board/run-board-render.ts +96 -80
- package/scripts/run/board/run-board-row-yield.ts +32 -0
- package/scripts/run/board/run-board-setup.ts +61 -13
- package/scripts/run/board/run-board-state.ts +10 -0
- package/scripts/run/board/run-board-status.ts +66 -7
- package/scripts/run/board/run-board-thanks-csv.ts +71 -0
- package/scripts/run/board/run-board-thanks-read.ts +112 -0
- package/scripts/run/board/run-board-thanks.ts +53 -0
- package/scripts/run/board/run-board-tick.ts +50 -21
- package/scripts/run/board/run-board-usual.ts +44 -0
- package/scripts/run/carry/run-carry-args.ts +9 -1
- package/scripts/run/carry/run-carry-cli.ts +2 -45
- package/scripts/run/carry/run-carry-end.ts +73 -0
- package/scripts/run/carry/run-carry.ts +8 -1
- package/scripts/run/entry/run-entry-cli.ts +5 -0
- package/scripts/run/event/run-event-checks.ts +54 -0
- package/scripts/run/event/run-event-render.ts +2 -0
- package/scripts/run/event/run-event-scope.ts +1 -0
- package/scripts/run/event/run-event-setup.ts +55 -11
- package/scripts/run/event/run-event-stream.ts +27 -11
- package/scripts/run/merge/run-merge-cli.ts +7 -4
- package/scripts/run/merge/run-merge-ship.ts +61 -0
- package/scripts/run/merge/run-merge-steps.ts +7 -14
- package/scripts/run/merge/run-merge-token.ts +16 -2
- package/scripts/run/run-headless.ts +1 -0
- package/scripts/run/run-step.ts +2 -0
- package/scripts/run/ship/run-ship-cli.ts +22 -2
- package/scripts/run/ship/run-ship-return.ts +31 -13
- package/scripts/run/ship/run-ship-review-steps.ts +17 -4
- package/scripts/run/ship/run-ship-stage.ts +1 -0
- package/scripts/run/wake/run-wake-await.ts +130 -0
- package/scripts/run/wake/run-wake-cli.ts +60 -71
- package/scripts/run/wake/run-wake-describe.ts +34 -2
- package/scripts/run/wake/run-wake-driver.ts +11 -4
- package/scripts/run/wake/run-wake-session.ts +20 -3
- package/scripts/run/wake/run-wake-stop.ts +37 -0
- package/scripts/run/wake/run-wake-supervise.ts +44 -0
- package/scripts/sync/claude-settings-merge.ts +144 -0
- package/scripts/sync/sync-ai-files.ts +28 -3
- package/scripts/sync/sync.ts +15 -0
- package/scripts/test/project-test-script.ts +73 -0
- package/scripts/test/skip-marker.ts +14 -1
- package/scripts/test/test-related.ts +3 -2
- package/scripts/test/test-unit-guard.ts +78 -14
- package/scripts/time-runtime/time-reported-failure.ts +11 -3
- package/scripts/uninstall/distributed-files.ts +92 -0
- package/scripts/uninstall/uninstall-plan.ts +119 -0
- package/scripts/uninstall/uninstall-report.ts +61 -0
- package/scripts/uninstall/uninstall.ts +206 -0
- package/templates/AGENTS.basic.md +4 -0
- package/templates/GEMINI.basic.md +6 -0
- package/dist/hooks/chunk-6V5OPIUU.js +0 -13
- package/scripts/run/board/run-board-track.ts +0 -22
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { styleText } from 'node:util'
|
|
2
|
+
import { run_progress } from '#scripts/run/progress/run-progress'
|
|
3
|
+
|
|
4
|
+
// **The one place a freshly updated part of `run:board` is emphasized** — the header's `💓` and third
|
|
5
|
+
// line, an issue row's phase, start and end (joshuafolkken/kit#3784). The emphasis fades in three
|
|
6
|
+
// one-second stages, decided by the part's age alone, so no frame remembers the one before it. Only a
|
|
7
|
+
// live screen draws it: a chat, a `--once` frame or a pipe gets the text as it is.
|
|
8
|
+
|
|
9
|
+
type FreshStage = 'inverse' | 'bold-color' | 'bold' | 'none'
|
|
10
|
+
|
|
11
|
+
type TextFormat = Parameters<typeof styleText>[0]
|
|
12
|
+
|
|
13
|
+
// What decides whether a frame emphasizes at all: a screen frame with a spinner is a live terminal.
|
|
14
|
+
interface FreshFrame {
|
|
15
|
+
now_ms: number
|
|
16
|
+
form: 'screen' | 'chat'
|
|
17
|
+
spinner: string | undefined
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const STAGE_MS = run_progress.MS_PER_SECOND
|
|
21
|
+
const FADING: ReadonlyArray<FreshStage> = ['inverse', 'bold-color', 'bold']
|
|
22
|
+
const FORMATS: Readonly<Record<Exclude<FreshStage, 'none'>, TextFormat>> = {
|
|
23
|
+
inverse: 'inverse',
|
|
24
|
+
'bold-color': ['bold', 'cyan'],
|
|
25
|
+
bold: 'bold',
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// The stage a part `age_ms` old is drawn in; an unknown or a future age is never emphasized.
|
|
29
|
+
function stage_of(age_ms: number | undefined): FreshStage {
|
|
30
|
+
if (age_ms === undefined || age_ms < 0) return 'none'
|
|
31
|
+
|
|
32
|
+
return FADING[Math.floor(age_ms / STAGE_MS)] ?? 'none'
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function paint(text: string, stage: FreshStage): string {
|
|
36
|
+
if (stage === 'none' || text === '') return text
|
|
37
|
+
|
|
38
|
+
return styleText(FORMATS[stage], text)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function is_live(frame: FreshFrame): boolean {
|
|
42
|
+
return frame.form === 'screen' && frame.spinner !== undefined
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// `text` emphasized as a part `age_ms` old, where the frame is a live screen.
|
|
46
|
+
function aged(text: string, age_ms: number | undefined, frame: FreshFrame): string {
|
|
47
|
+
return is_live(frame) ? paint(text, stage_of(age_ms)) : text
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// `text` emphasized as a part that changed at `at_ms`.
|
|
51
|
+
function since(text: string, at_ms: number | undefined, frame: FreshFrame): string {
|
|
52
|
+
return aged(text, at_ms === undefined ? undefined : frame.now_ms - at_ms, frame)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// `text` right-aligned to `width` with only the text emphasized, never the blanks before it, so the
|
|
56
|
+
// column keeps its width and an inverse stage does not light up the padding.
|
|
57
|
+
function padded(text: string, width: number, emphasized: string): string {
|
|
58
|
+
return `${' '.repeat(Math.max(0, width - text.length))}${emphasized}`
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const run_board_fresh = { aged, padded, paint, since, stage_of }
|
|
62
|
+
|
|
63
|
+
export { run_board_fresh }
|
|
64
|
+
export type { FreshFrame, FreshStage }
|
|
@@ -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.
|
|
14
|
-
// second is the proof the board is live, so no `updated` line is drawn. **Always
|
|
15
|
-
// columns**: the run's mark and name over its repository's, the run's age over
|
|
16
|
-
// progress over the machine the run is on — so the first line reads as how far the
|
|
17
|
-
// second as what it runs on, a slow run apart from a stuck one.
|
|
18
|
-
// run
|
|
19
|
-
//
|
|
20
|
-
//
|
|
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,7 +86,7 @@ interface BoardCounts {
|
|
|
74
86
|
remaining: number
|
|
75
87
|
}
|
|
76
88
|
|
|
77
|
-
// `setup` is a run with no child running that is
|
|
89
|
+
// `setup` is a run with no child running that is itself at work on a stage (`run-board-setup.ts`).
|
|
78
90
|
type RunMark = 'running' | 'setup' | 'idle' | 'human' | 'stopped'
|
|
79
91
|
|
|
80
92
|
type TextColor = Parameters<typeof styleText>[0]
|
|
@@ -103,18 +115,15 @@ function counts_of(layout: BoardLayout): BoardCounts {
|
|
|
103
115
|
|
|
104
116
|
// The marks of a run that is working: they turn the spinner, and only they can go stale.
|
|
105
117
|
const WORKING_MARKS: ReadonlySet<RunMark> = new Set<RunMark>(['running', 'setup'])
|
|
106
|
-
// What follows the stage once the plan is read: how many issues the run is setting up for.
|
|
107
|
-
const STAGE_SEPARATOR = ' · '
|
|
108
|
-
const PLANNED = 'planned'
|
|
109
118
|
|
|
110
|
-
// Nothing running: the run is
|
|
111
|
-
//
|
|
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.
|
|
112
121
|
function quiet_mark(header: BoardHeader): RunMark {
|
|
113
122
|
const { layout } = header
|
|
114
123
|
|
|
115
|
-
if (
|
|
124
|
+
if (header.activity.setup !== undefined) return 'setup'
|
|
116
125
|
|
|
117
|
-
return
|
|
126
|
+
return layout?.waves.length === 0 && layout.people.length > 0 ? 'human' : 'idle'
|
|
118
127
|
}
|
|
119
128
|
|
|
120
129
|
// The run's state, each state overriding the ones below it.
|
|
@@ -124,12 +133,11 @@ function mark_of(header: BoardHeader, running: number): RunMark {
|
|
|
124
133
|
return running > 0 ? 'running' : quiet_mark(header)
|
|
125
134
|
}
|
|
126
135
|
|
|
127
|
-
//
|
|
128
|
-
function run_part(mark: RunMark
|
|
136
|
+
// The mark stands still in every state: the spinner turns on the third line.
|
|
137
|
+
function run_part(mark: RunMark): string {
|
|
129
138
|
const { glyph, color } = RUN_MARKS[mark]
|
|
130
|
-
const shown = WORKING_MARKS.has(mark) ? (spinner ?? glyph) : glyph
|
|
131
139
|
|
|
132
|
-
return styleText(color, `${
|
|
140
|
+
return styleText(color, `${glyph} ${RUN_NAME}`)
|
|
133
141
|
}
|
|
134
142
|
|
|
135
143
|
// The repository's own name — `kit` of `owner/kit` — under the run's, past the mark's columns, so a
|
|
@@ -141,6 +149,8 @@ function repo_part(header: BoardHeader, mark: RunMark): string {
|
|
|
141
149
|
return `${BLANK.repeat(indent)}${name}`
|
|
142
150
|
}
|
|
143
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`).
|
|
144
154
|
function aged(part: string, age_ms: number): string {
|
|
145
155
|
if (age_ms > ALARM_MS) return styleText('red', part)
|
|
146
156
|
|
|
@@ -166,25 +176,75 @@ function ended_part(header: BoardHeader): string {
|
|
|
166
176
|
|
|
167
177
|
// The `💓` under the run's age, in the `⏰`'s own format with its digits right-aligned to `width` —
|
|
168
178
|
// only a working run can be stale, so only it is colored. Only an ended run draws anything else there:
|
|
169
|
-
// the minute it ended.
|
|
179
|
+
// the minute it ended. Its digits are emphasized for a few seconds after a new event resets them
|
|
180
|
+
// (`run-board-fresh.ts`).
|
|
170
181
|
function beat_part(header: BoardHeader, mark: RunMark, width: number): string {
|
|
171
182
|
const age = age_of(header)
|
|
172
183
|
|
|
173
184
|
if (age === undefined) return ended_part(header)
|
|
174
185
|
|
|
175
|
-
const
|
|
186
|
+
const span = span_of(age)
|
|
187
|
+
const part = `${HEADER_ICONS.heartbeat} ${padded(span, width, aged_fresh(span, age, header))}`
|
|
176
188
|
|
|
177
189
|
return WORKING_MARKS.has(mark) ? aged(part, age) : part
|
|
178
190
|
}
|
|
179
191
|
|
|
180
|
-
//
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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 {
|
|
184
209
|
const age = age_of(header)
|
|
185
|
-
const width = Math.max(elapsed.length, age === undefined ? 0 : span_of(age).length)
|
|
186
210
|
|
|
187
|
-
return
|
|
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
|
+
]
|
|
188
248
|
}
|
|
189
249
|
|
|
190
250
|
// A plan fetch is said only when it failed — the board otherwise keeps quiet about a routine read.
|
|
@@ -255,25 +315,32 @@ function progress_parts(counts: BoardCounts, header: BoardHeader): Array<string>
|
|
|
255
315
|
]
|
|
256
316
|
}
|
|
257
317
|
|
|
258
|
-
// The
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
function stage_part(counts: BoardCounts, header: BoardHeader, mark: RunMark): string | undefined {
|
|
262
|
-
const { setup } = header.activity
|
|
263
|
-
|
|
264
|
-
if (setup === undefined || mark !== 'setup') return undefined
|
|
265
|
-
if (!is_plan_read(header)) return setup
|
|
318
|
+
// The run's progress, then what its plan read is doing.
|
|
319
|
+
function progress_part(counts: BoardCounts, header: BoardHeader): string {
|
|
320
|
+
const parts = [...progress_parts(counts, header), loading_part(header), plan_warning(header)]
|
|
266
321
|
|
|
267
|
-
return
|
|
322
|
+
return parts.filter((part) => part !== undefined).join(GAP)
|
|
268
323
|
}
|
|
269
324
|
|
|
270
|
-
// The
|
|
271
|
-
function
|
|
272
|
-
const
|
|
273
|
-
const progress = stage === undefined ? progress_parts(counts, header) : [stage]
|
|
274
|
-
const parts = [...progress, loading_part(header), plan_warning(header)]
|
|
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')
|
|
275
328
|
|
|
276
|
-
return
|
|
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
|
+
})
|
|
277
344
|
}
|
|
278
345
|
|
|
279
346
|
// The gauges the machine line draws: every one on a screen, the memory alone in a chat.
|
|
@@ -297,28 +364,37 @@ function machine_part(counts: BoardCounts, header: BoardHeader): string {
|
|
|
297
364
|
return run_board_machine.line_of(machine_of(header), width) ?? MACHINE_UNKNOWN
|
|
298
365
|
}
|
|
299
366
|
|
|
300
|
-
//
|
|
367
|
+
// The lines' cells of one column, the narrower padded to the wider as the terminal counts them.
|
|
301
368
|
function column_of(cells: ReadonlyArray<string>): Array<string> {
|
|
302
369
|
const width = Math.max(...cells.map((cell) => run_board_fit.width_of(cell)))
|
|
303
370
|
|
|
304
371
|
return cells.map((cell) => cell_of(cell, width))
|
|
305
372
|
}
|
|
306
373
|
|
|
307
|
-
// The run's name, age and progress, then its repository, heartbeat and machine under them,
|
|
308
|
-
//
|
|
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.
|
|
309
377
|
function header_lines(header: BoardHeader): Array<string> {
|
|
310
378
|
const counts = counts_of(layout_of(header))
|
|
311
379
|
const mark = mark_of(header, counts.running)
|
|
312
|
-
const
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
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)
|
|
317
393
|
|
|
318
|
-
return
|
|
394
|
+
return now?.legend === undefined ? undefined : `${now.icon} ${now.legend}`
|
|
319
395
|
}
|
|
320
396
|
|
|
321
|
-
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 }
|
|
322
398
|
|
|
323
399
|
export { run_board_header }
|
|
324
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
|
-
//
|
|
218
|
-
|
|
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
|
-
|
|
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 }
|