@joshuafolkken/kit 1.1379.0 → 1.1387.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/workflow-commands/backlogrun-progress.md +2 -3
- package/.claude/skills/workflow-commands/backlogrun-steps.md +2 -4
- package/.claude/skills/workflow-commands/retrospective.md +1 -2
- package/dist/hooks/batch-guard.js +6 -6
- package/dist/hooks/{chunk-RW4XMF47.js → chunk-2YSW3CXH.js} +133 -234
- package/dist/hooks/{chunk-4RWRP3FF.js → chunk-DCY2GWHC.js} +5 -5
- package/dist/hooks/{chunk-DHSKBAQ2.js → chunk-EAQEGME7.js} +1 -1
- package/dist/hooks/{chunk-E7LZL5AN.js → chunk-EBSHN25W.js} +3 -3
- package/dist/hooks/{chunk-HGA4EG5S.js → chunk-GJ7JPIHN.js} +15 -15
- package/dist/hooks/{chunk-TG2AB3W6.js → chunk-GNDJ7NJ7.js} +88 -501
- package/dist/hooks/{chunk-546X4PX3.js → chunk-IYVOLN6A.js} +3 -3
- package/dist/hooks/{chunk-NBEAGPS4.js → chunk-J3KSS7II.js} +1 -1
- package/dist/hooks/{chunk-7ZM464OW.js → chunk-LMWBRJIP.js} +3 -3
- package/dist/hooks/{chunk-TORRSNDJ.js → chunk-SL4GQ2XP.js} +3 -3
- package/dist/hooks/{chunk-PV3FWDSQ.js → chunk-STDSEPWU.js} +2 -2
- package/dist/hooks/{chunk-YSDAH73S.js → chunk-U32VSRNG.js} +420 -4
- package/dist/hooks/{chunk-QARPP36N.js → chunk-WNFJLBXI.js} +3 -3
- package/dist/hooks/codex-hook-adapter.js +13 -13
- package/dist/hooks/duplicate-read-guard.js +7 -7
- package/dist/hooks/format-edited.js +3 -3
- package/dist/hooks/inputs.json +213 -212
- package/dist/hooks/investigation-guard.js +7 -7
- package/dist/hooks/pretool-guard.js +12 -12
- package/dist/hooks/session-lang.js +65 -9
- package/dist/hooks/stop-guard.js +104 -32
- package/dist/josh.js +2 -2
- package/package.json +1 -1
- package/prompts/collaboration-workflow/upstream-interrupt.md +1 -1
- package/scripts/backlog/backlog-budget-cli.ts +2 -20
- package/scripts/backlog/backlog-budget.ts +72 -109
- package/scripts/backlog/backlog-drive-cli.ts +12 -7
- package/scripts/backlog/backlog-drive-launch.ts +2 -0
- package/scripts/backlog/backlog-drive-owner.ts +72 -5
- package/scripts/backlog/backlog-drive.ts +5 -2
- package/scripts/backlog/backlog-offer-cli.ts +1 -15
- package/scripts/backlog/backlogrun-launch.ts +56 -5
- package/scripts/document/document-byte-budget.ts +1 -1
- package/scripts/epic/epic-candidate-confirm.ts +1 -1
- package/scripts/epic/epic-cross-repo.ts +4 -4
- package/scripts/epic/epic-next.ts +1 -1
- package/scripts/hooks/stop-guard.ts +20 -9
- package/scripts/init/start-plan.ts +15 -4
- package/scripts/init/start-steps.ts +31 -3
- package/scripts/init/start.ts +20 -13
- package/scripts/josh/josh-commands-ai.ts +1 -1
- package/scripts/josh/josh-commands-project.ts +1 -1
- package/scripts/lane/lane-cli.ts +1 -3
- package/scripts/run/board/run-board-header.ts +137 -88
- package/scripts/run/board/run-board-labels.ts +18 -17
- package/scripts/run/board/run-board-machine.ts +21 -8
- package/scripts/run/board/run-board-read.ts +9 -2
- package/scripts/run/board/run-board-render.ts +1 -5
- package/scripts/run/board/run-board-setup.ts +73 -0
- package/scripts/run/board/run-board-span.ts +49 -8
- package/scripts/run/board/run-board-status.ts +10 -23
- package/scripts/run/board/run-board-tick.ts +11 -2
- package/scripts/run/carry/run-carry-added.ts +1 -1
- package/scripts/run/carry/run-carry-change.ts +1 -1
- package/scripts/run/carry/run-carry-cli.ts +36 -60
- package/scripts/run/carry/run-carry-ended.ts +10 -2
- package/scripts/run/carry/run-carry.ts +38 -68
- package/scripts/run/cut/run-cut.ts +4 -4
- package/scripts/run/event/run-event-render.ts +1 -1
- package/scripts/run/event/run-event-scope.ts +4 -8
- package/scripts/run/event/run-event-setup.ts +47 -0
- package/scripts/run/event/run-event-stream-emit.ts +0 -25
- package/scripts/run/event/run-event-stream.ts +41 -28
- package/scripts/run/merge/run-merge-collect.ts +1 -1
- package/scripts/run/merge/run-merge-steps.ts +1 -1
- package/scripts/run/progress/run-progress-read.ts +1 -1
- package/scripts/run/run-headless.ts +2 -2
- package/scripts/run/run-status-cli.ts +1 -5
- package/scripts/run/run-step.ts +2 -9
- package/scripts/run/run-stop-notify.ts +1 -1
- package/scripts/run/run-stranded-detect.ts +61 -15
- package/scripts/run/run-stranded.ts +25 -17
- package/scripts/run/wake/run-wake-cli.ts +58 -15
- package/scripts/run/wake/run-wake-describe.ts +5 -5
- package/scripts/run/wake/run-wake-driver.ts +13 -4
- package/scripts/run/wake/run-wake-handoff.ts +1 -1
- package/scripts/run/wake/run-wake-loop.ts +4 -21
- package/scripts/run/wake/run-wake-restart.ts +1 -1
- package/scripts/run/wake/run-wake.ts +6 -7
- package/scripts/backlog/backlog-idle.ts +0 -88
|
@@ -1,33 +1,36 @@
|
|
|
1
1
|
import { styleText } from 'node:util'
|
|
2
2
|
import { backlog_budget } from '#scripts/backlog/backlog-budget'
|
|
3
|
-
import {
|
|
3
|
+
import { run_board_fit } from './run-board-fit'
|
|
4
4
|
import { run_board_labels } from './run-board-labels'
|
|
5
5
|
import { run_board_layout, type BoardLayout, type BoardRow } from './run-board-layout'
|
|
6
6
|
import { run_board_machine, type MachineGauges } from './run-board-machine'
|
|
7
|
+
import { run_board_render_slot } from './run-board-render-slot'
|
|
7
8
|
import type { RunActivity } from './run-board-status'
|
|
8
9
|
import type { LaneUsages } from './run-board-usage'
|
|
9
10
|
|
|
10
|
-
// The top of `run:board`: whether the run is moving, how long it has run, how
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
11
|
+
// The top of `run:board`: whether the run is moving, how long it has run, how fresh its last event is,
|
|
12
|
+
// 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.
|
|
21
|
+
|
|
22
|
+
const { HEADER_ICONS, STATE_ICONS, WORDS, bar_of, minute_of, span_of } = run_board_labels
|
|
23
|
+
const { cell_of } = run_board_render_slot
|
|
22
24
|
const UNKNOWN = '-'
|
|
23
|
-
const
|
|
25
|
+
const BLANK = ' '
|
|
26
|
+
const REPO_SEPARATOR = '/'
|
|
27
|
+
const { GAP, PERCENT_WIDTH } = run_board_machine
|
|
24
28
|
// A running run whose newest event is older than this is drawn as stale, so a hang stands out; past
|
|
25
29
|
// twice that it is drawn as alarming.
|
|
26
30
|
const STALE_MINUTES = 15
|
|
27
31
|
const STALE_MS = STALE_MINUTES * backlog_budget.MS_PER_MINUTE
|
|
28
32
|
const ALARM_FACTOR = 2
|
|
29
33
|
const ALARM_MS = STALE_MS * ALARM_FACTOR
|
|
30
|
-
const INDENT = ' '
|
|
31
34
|
const RUN_NAME = 'backlogrun'
|
|
32
35
|
|
|
33
36
|
// Who reads the frame: a person on a screen, or a chat a session answers a progress question in, which
|
|
@@ -56,6 +59,8 @@ interface BoardHeader {
|
|
|
56
59
|
// terminal and they draw their still icons.
|
|
57
60
|
spinner: string | undefined
|
|
58
61
|
form: BoardForm
|
|
62
|
+
// The repository the plan was read for, as `owner/repo`; `undefined` before a plan is read.
|
|
63
|
+
repo: string | undefined
|
|
59
64
|
// An issue reference as the board draws it — `3450` or `owner/repo#12` — made a link where the
|
|
60
65
|
// terminal opens one.
|
|
61
66
|
link: (reference: string) => string
|
|
@@ -69,13 +74,15 @@ interface BoardCounts {
|
|
|
69
74
|
remaining: number
|
|
70
75
|
}
|
|
71
76
|
|
|
72
|
-
|
|
77
|
+
// `setup` is a run with no child running that is getting one ready (`run-board-setup.ts`).
|
|
78
|
+
type RunMark = 'running' | 'setup' | 'idle' | 'human' | 'stopped'
|
|
73
79
|
|
|
74
80
|
type TextColor = Parameters<typeof styleText>[0]
|
|
75
81
|
|
|
76
82
|
// The run's state as a colored glyph before its name.
|
|
77
83
|
const RUN_MARKS: Readonly<Record<RunMark, { glyph: string; color: TextColor }>> = {
|
|
78
84
|
running: { glyph: '▶', color: 'green' },
|
|
85
|
+
setup: { glyph: '▶', color: 'green' },
|
|
79
86
|
idle: { glyph: '⏸', color: 'yellow' },
|
|
80
87
|
human: { glyph: '✋', color: 'magenta' },
|
|
81
88
|
stopped: { glyph: '■', color: 'gray' },
|
|
@@ -94,45 +101,90 @@ function counts_of(layout: BoardLayout): BoardCounts {
|
|
|
94
101
|
return { total, settled, parked, running, remaining: total - settled - running }
|
|
95
102
|
}
|
|
96
103
|
|
|
104
|
+
// The marks of a run that is working: they turn the spinner, and only they can go stale.
|
|
105
|
+
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
|
+
|
|
97
110
|
// Nothing running: the run is waiting on a person when all the plan has left is what
|
|
98
|
-
// `needs-decision` holds,
|
|
99
|
-
function quiet_mark(
|
|
100
|
-
|
|
111
|
+
// `needs-decision` holds; otherwise it is getting a child ready, or idle.
|
|
112
|
+
function quiet_mark(header: BoardHeader): RunMark {
|
|
113
|
+
const { layout } = header
|
|
114
|
+
|
|
115
|
+
if (layout?.waves.length === 0 && layout.people.length > 0) return 'human'
|
|
116
|
+
|
|
117
|
+
return header.activity.is_preparing === true ? 'setup' : 'idle'
|
|
101
118
|
}
|
|
102
119
|
|
|
103
120
|
// The run's state, each state overriding the ones below it.
|
|
104
121
|
function mark_of(header: BoardHeader, running: number): RunMark {
|
|
105
122
|
if (header.activity.is_stopped) return 'stopped'
|
|
106
|
-
if (running > 0) return 'running'
|
|
107
123
|
|
|
108
|
-
return
|
|
124
|
+
return running > 0 ? 'running' : quiet_mark(header)
|
|
109
125
|
}
|
|
110
126
|
|
|
111
|
-
// A
|
|
127
|
+
// A working run turns the spinner in place of ▶, both one column wide; every other mark stands still.
|
|
112
128
|
function run_part(mark: RunMark, spinner: string | undefined): string {
|
|
113
129
|
const { glyph, color } = RUN_MARKS[mark]
|
|
114
|
-
const shown = mark
|
|
130
|
+
const shown = WORKING_MARKS.has(mark) ? (spinner ?? glyph) : glyph
|
|
115
131
|
|
|
116
132
|
return styleText(color, `${shown} ${RUN_NAME}`)
|
|
117
133
|
}
|
|
118
134
|
|
|
135
|
+
// The repository's own name — `kit` of `owner/kit` — under the run's, past the mark's columns, so a
|
|
136
|
+
// board open beside another repository's is told apart. Blank before a plan names one.
|
|
137
|
+
function repo_part(header: BoardHeader, mark: RunMark): string {
|
|
138
|
+
const name = header.repo?.split(REPO_SEPARATOR).at(-1) ?? ''
|
|
139
|
+
const indent = run_board_fit.width_of(RUN_MARKS[mark].glyph) + BLANK.length
|
|
140
|
+
|
|
141
|
+
return `${BLANK.repeat(indent)}${name}`
|
|
142
|
+
}
|
|
143
|
+
|
|
119
144
|
function aged(part: string, age_ms: number): string {
|
|
120
145
|
if (age_ms > ALARM_MS) return styleText('red', part)
|
|
121
146
|
|
|
122
147
|
return age_ms > STALE_MS ? styleText('yellow', part) : part
|
|
123
148
|
}
|
|
124
149
|
|
|
125
|
-
// How long ago the stream last moved
|
|
126
|
-
//
|
|
127
|
-
function
|
|
150
|
+
// How long ago the stream last moved. An ended run has no heartbeat to keep: one counting on would
|
|
151
|
+
// read as a hang.
|
|
152
|
+
function age_of(header: BoardHeader): number | undefined {
|
|
128
153
|
const last = header.activity.last_event_ms
|
|
129
154
|
|
|
130
155
|
if (last === undefined || header.ended_ms !== undefined) return undefined
|
|
131
156
|
|
|
132
|
-
|
|
133
|
-
|
|
157
|
+
return header.now_ms - last
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// The minute an ended run ended, standing where its heartbeat did. Blank while the run has none to draw.
|
|
161
|
+
function ended_part(header: BoardHeader): string {
|
|
162
|
+
const { ended_ms } = header
|
|
134
163
|
|
|
135
|
-
return
|
|
164
|
+
return ended_ms === undefined ? '' : `${HEADER_ICONS.ended} ${minute_of(ended_ms)}`
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// The `💓` under the run's age, in the `⏰`'s own format with its digits right-aligned to `width` —
|
|
168
|
+
// 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.
|
|
170
|
+
function beat_part(header: BoardHeader, mark: RunMark, width: number): string {
|
|
171
|
+
const age = age_of(header)
|
|
172
|
+
|
|
173
|
+
if (age === undefined) return ended_part(header)
|
|
174
|
+
|
|
175
|
+
const part = `${HEADER_ICONS.heartbeat} ${span_of(age).padStart(width)}`
|
|
176
|
+
|
|
177
|
+
return WORKING_MARKS.has(mark) ? aged(part, age) : part
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// A run's age over the `💓`, or over the minute an ended run ended: the shorter of the two lengths is
|
|
181
|
+
// right-aligned to the longer, so the digits stand in one column when only one has passed ten hours.
|
|
182
|
+
function time_parts(header: BoardHeader, mark: RunMark): Array<string> {
|
|
183
|
+
const elapsed = span_of((header.ended_ms ?? header.now_ms) - header.started_ms)
|
|
184
|
+
const age = age_of(header)
|
|
185
|
+
const width = Math.max(elapsed.length, age === undefined ? 0 : span_of(age).length)
|
|
186
|
+
|
|
187
|
+
return [`${HEADER_ICONS.elapsed} ${elapsed.padStart(width)}`, beat_part(header, mark, width)]
|
|
136
188
|
}
|
|
137
189
|
|
|
138
190
|
// A plan fetch is said only when it failed — the board otherwise keeps quiet about a routine read.
|
|
@@ -152,23 +204,6 @@ function loading_part(header: BoardHeader): string | undefined {
|
|
|
152
204
|
return is_plan_loading && spinner !== undefined ? `${HEADER_ICONS.loading} ${spinner}` : undefined
|
|
153
205
|
}
|
|
154
206
|
|
|
155
|
-
// A running run's age and time left before the cut-off, or an ended run's frozen duration and the
|
|
156
|
-
// minute it ended — an ended run has no cut-off left.
|
|
157
|
-
function time_parts(header: BoardHeader): Array<string> {
|
|
158
|
-
const { now_ms, started_ms, ended_ms } = header
|
|
159
|
-
|
|
160
|
-
if (ended_ms !== undefined) {
|
|
161
|
-
return [
|
|
162
|
-
`⏱ ${elapsed_of(ended_ms - started_ms)}`,
|
|
163
|
-
`${HEADER_ICONS.ended} ${minute_of(ended_ms)}`,
|
|
164
|
-
]
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
const cutoff = started_ms + backlog_budget.WHOLE_RUN_BUDGET_MS
|
|
168
|
-
|
|
169
|
-
return [`⏱ ${elapsed_of(now_ms - started_ms)}`, `⌛ ${left_of(cutoff - now_ms)}`]
|
|
170
|
-
}
|
|
171
|
-
|
|
172
207
|
function layout_of(header: BoardHeader): BoardLayout {
|
|
173
208
|
return header.layout ?? run_board_layout.EMPTY_LAYOUT
|
|
174
209
|
}
|
|
@@ -186,14 +221,25 @@ function plus_of(counts: BoardCounts, header: BoardHeader): string {
|
|
|
186
221
|
return added > 0 ? ` (+${String(added)})` : ''
|
|
187
222
|
}
|
|
188
223
|
|
|
189
|
-
//
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
224
|
+
// The plan's total as the tally draws it, unknown until the plan is read.
|
|
225
|
+
function total_of(counts: BoardCounts, header: BoardHeader): string {
|
|
226
|
+
return is_plan_read(header) ? String(counts.total) : UNKNOWN
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// The width the tally and the machine's first figure are both right-aligned to — the wider of a full
|
|
230
|
+
// tally and a full percent — so the progress bar and the gauge under it start in one column, and
|
|
231
|
+
// neither moves as a count or a percent gains a digit.
|
|
232
|
+
function figure_width(total: string): number {
|
|
233
|
+
return Math.max(PERCENT_WIDTH, `${total}/${total}`.length)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Icon, figure, then bar, as the machine gauges. Arrivals since the board first looked follow the
|
|
237
|
+
// bar, where they cannot move it.
|
|
238
|
+
function tally_part(counts: BoardCounts, header: BoardHeader): string {
|
|
193
239
|
const { settled } = counts
|
|
194
|
-
const total =
|
|
195
|
-
const tally = `${String(settled)}/${total}`.padStart(
|
|
196
|
-
const bar =
|
|
240
|
+
const total = total_of(counts, header)
|
|
241
|
+
const tally = `${String(settled)}/${total}`.padStart(figure_width(total))
|
|
242
|
+
const bar = is_plan_read(header) ? bar_of(settled, counts.total) : bar_of(0, 0)
|
|
197
243
|
|
|
198
244
|
return `${STATE_ICONS.merged} ${tally} ${bar}`
|
|
199
245
|
}
|
|
@@ -202,45 +248,32 @@ function progress_parts(counts: BoardCounts, header: BoardHeader): Array<string>
|
|
|
202
248
|
const is_read = is_plan_read(header)
|
|
203
249
|
|
|
204
250
|
return [
|
|
205
|
-
`${tally_part(counts,
|
|
251
|
+
`${tally_part(counts, header)}${is_read ? plus_of(counts, header) : ''}`,
|
|
206
252
|
`${STATE_ICONS.running} ${String(counts.running)}`,
|
|
207
253
|
`${STATE_ICONS.waiting} ${is_read ? String(counts.remaining) : UNKNOWN}`,
|
|
208
254
|
`${STATE_ICONS.parked} ${String(counts.parked)}`,
|
|
209
255
|
]
|
|
210
256
|
}
|
|
211
257
|
|
|
212
|
-
// The
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
...progress_parts(counts, header),
|
|
218
|
-
heartbeat_part(header, mark),
|
|
219
|
-
loading_part(header),
|
|
220
|
-
plan_warning(header),
|
|
221
|
-
]
|
|
258
|
+
// The stage a run is at before its first child launches, standing where the progress will: a tally of
|
|
259
|
+
// nothing settled says less than what the run is doing. Only a run marked as setting up is at a stage:
|
|
260
|
+
// a stopped run and one waiting on a person keep their progress.
|
|
261
|
+
function stage_part(counts: BoardCounts, header: BoardHeader, mark: RunMark): string | undefined {
|
|
262
|
+
const { setup } = header.activity
|
|
222
263
|
|
|
223
|
-
|
|
224
|
-
|
|
264
|
+
if (setup === undefined || mark !== 'setup') return undefined
|
|
265
|
+
if (!is_plan_read(header)) return setup
|
|
225
266
|
|
|
226
|
-
|
|
227
|
-
const ending = idle.bound === 'idle' ? WORDS.idle_end_idle : WORDS.idle_end_run
|
|
228
|
-
const left = `${elapsed_of(idle.until_ms - now_ms)} ${WORDS.idle_left}`
|
|
229
|
-
const until = `${WORDS.idle_until} ${clock_of(idle.until_ms)} (${left}) → ${ending}`
|
|
230
|
-
const next_check = `${WORDS.next_check} ${clock_of(backlog_idle.next_check_ms(idle))}`
|
|
231
|
-
|
|
232
|
-
return `${INDENT}${RUN_MARKS.idle.glyph} ${until} · ${next_check}`
|
|
267
|
+
return `${setup}${STAGE_SEPARATOR}${String(counts.total)} ${PLANNED}`
|
|
233
268
|
}
|
|
234
269
|
|
|
235
|
-
// The
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
const
|
|
240
|
-
|
|
241
|
-
if (is_stopped || idle === undefined || counts_of(layout_of(header)).running > 0) return []
|
|
270
|
+
// The run's stage or its progress, then what its plan read is doing.
|
|
271
|
+
function progress_part(counts: BoardCounts, header: BoardHeader, mark: RunMark): string {
|
|
272
|
+
const stage = stage_part(counts, header, mark)
|
|
273
|
+
const progress = stage === undefined ? progress_parts(counts, header) : [stage]
|
|
274
|
+
const parts = [...progress, loading_part(header), plan_warning(header)]
|
|
242
275
|
|
|
243
|
-
return
|
|
276
|
+
return parts.filter((part) => part !== undefined).join(GAP)
|
|
244
277
|
}
|
|
245
278
|
|
|
246
279
|
// The gauges the machine line draws: every one on a screen, the memory alone in a chat.
|
|
@@ -257,19 +290,35 @@ const MACHINE_UNKNOWN = [HEADER_ICONS.cpu, HEADER_ICONS.memory, HEADER_ICONS.swa
|
|
|
257
290
|
.map((icon) => `${icon} ${UNKNOWN}`)
|
|
258
291
|
.join(GAP)
|
|
259
292
|
|
|
260
|
-
// The
|
|
261
|
-
function
|
|
262
|
-
const
|
|
293
|
+
// The machine the run is on, its first figure as wide as the tally over it.
|
|
294
|
+
function machine_part(counts: BoardCounts, header: BoardHeader): string {
|
|
295
|
+
const width = figure_width(total_of(counts, header))
|
|
263
296
|
|
|
264
|
-
return
|
|
297
|
+
return run_board_machine.line_of(machine_of(header), width) ?? MACHINE_UNKNOWN
|
|
265
298
|
}
|
|
266
299
|
|
|
267
|
-
//
|
|
300
|
+
// Both lines' cells of one column, the narrower padded to the wider as the terminal counts them.
|
|
301
|
+
function column_of(cells: ReadonlyArray<string>): Array<string> {
|
|
302
|
+
const width = Math.max(...cells.map((cell) => run_board_fit.width_of(cell)))
|
|
303
|
+
|
|
304
|
+
return cells.map((cell) => cell_of(cell, width))
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// The run's name, age and progress, then its repository, heartbeat and machine under them, in every
|
|
308
|
+
// state the board can be in.
|
|
268
309
|
function header_lines(header: BoardHeader): Array<string> {
|
|
269
|
-
|
|
310
|
+
const counts = counts_of(layout_of(header))
|
|
311
|
+
const mark = mark_of(header, counts.running)
|
|
312
|
+
const columns = [
|
|
313
|
+
column_of([run_part(mark, header.spinner), repo_part(header, mark)]),
|
|
314
|
+
column_of(time_parts(header, mark)),
|
|
315
|
+
[progress_part(counts, header, mark), machine_part(counts, header)],
|
|
316
|
+
]
|
|
317
|
+
|
|
318
|
+
return [0, 1].map((line) => columns.map((column) => column[line]).join(GAP))
|
|
270
319
|
}
|
|
271
320
|
|
|
272
|
-
const run_board_header = { counts_of, header_lines,
|
|
321
|
+
const run_board_header = { counts_of, header_lines, is_plan_read }
|
|
273
322
|
|
|
274
323
|
export { run_board_header }
|
|
275
324
|
export type { BoardCounts, BoardForm, BoardHeader }
|
|
@@ -6,16 +6,16 @@ import type { BoardNote } from './run-board-notes'
|
|
|
6
6
|
import type { Phase } from './run-board-phase'
|
|
7
7
|
import type { ItemState } from './run-board-status'
|
|
8
8
|
|
|
9
|
-
// The words and the clock `run:board` draws with. A moment on the board is a local `HH:MM
|
|
10
|
-
// how long
|
|
11
|
-
//
|
|
9
|
+
// The words and the clock `run:board` draws with. A moment on the board is a local clock, `HH:MM`
|
|
10
|
+
// where a person reads it to the minute; how long a row or the heartbeat has run is a short `MM:SS`,
|
|
11
|
+
// and how long the run has run or its wait has left is `H:MM:SS` — two separators are a length and one
|
|
12
|
+
// is a time of day, so the header's time column never reads a length as a moment.
|
|
12
13
|
|
|
13
14
|
const CLOCK_WIDTH = 2
|
|
14
15
|
const CLOCK_PAD = '0'
|
|
15
16
|
const MS_PER_SECOND = 1000
|
|
16
17
|
const SECONDS_PER_MINUTE = 60
|
|
17
18
|
const MINUTES_PER_HOUR = 60
|
|
18
|
-
const MS_PER_MINUTE = MS_PER_SECOND * SECONDS_PER_MINUTE
|
|
19
19
|
// Every gauge is this wide — the plan, the machine and a row's phase.
|
|
20
20
|
const BAR_WIDTH = 10
|
|
21
21
|
// A centered square done and a thin line left: the shapes alone tell the two apart where no color is
|
|
@@ -58,11 +58,6 @@ const WORDS = {
|
|
|
58
58
|
followup: 'followup',
|
|
59
59
|
report: 'report',
|
|
60
60
|
failed: 'failed',
|
|
61
|
-
idle_until: 'wait ends',
|
|
62
|
-
idle_left: 'left',
|
|
63
|
-
idle_end_idle: 'ends and reports if nothing new',
|
|
64
|
-
idle_end_run: 'ends at the whole-run cut-off and reports',
|
|
65
|
-
next_check: 'next check',
|
|
66
61
|
notes: 'findings and decisions',
|
|
67
62
|
more: 'more',
|
|
68
63
|
filed: 'filed',
|
|
@@ -168,13 +163,16 @@ const KIND_WORDS: Readonly<Record<FiledKind, keyof Words>> = {
|
|
|
168
163
|
}
|
|
169
164
|
|
|
170
165
|
// The header's gauges and marks. ⚡ rather than 🔥: a fire beside a gauge drawn green reads as an
|
|
171
|
-
// alarm.
|
|
166
|
+
// alarm. ⏰ rather than ⏱, a text symbol some terminals draw one column wide: under it the 💓's digits
|
|
167
|
+
// would stand a column off.
|
|
172
168
|
const HEADER_ICONS = {
|
|
173
169
|
cpu: '⚡',
|
|
174
170
|
memory: '🧠',
|
|
175
171
|
swap: '💾',
|
|
176
172
|
ended: '🔚',
|
|
177
173
|
loading: '⏳',
|
|
174
|
+
elapsed: '⏰',
|
|
175
|
+
heartbeat: '💓',
|
|
178
176
|
} as const
|
|
179
177
|
|
|
180
178
|
// **The machine gauges' three colors, decided here alone**: Activity Monitor's green, yellow and red
|
|
@@ -207,8 +205,8 @@ function minute_of(ms: number): string {
|
|
|
207
205
|
return [date.getHours(), date.getMinutes()].map((value) => two_digits(value)).join(':')
|
|
208
206
|
}
|
|
209
207
|
|
|
210
|
-
// How long
|
|
211
|
-
//
|
|
208
|
+
// How long a row's child has run, as `MM:SS` whose minutes never carry into hours (`61:05`), so every
|
|
209
|
+
// row reads one short column; a negative span reads as zero.
|
|
212
210
|
function elapsed_of(ms: number): string {
|
|
213
211
|
const seconds = Math.floor(Math.max(0, ms) / MS_PER_SECOND)
|
|
214
212
|
const minutes = Math.floor(seconds / SECONDS_PER_MINUTE)
|
|
@@ -216,12 +214,15 @@ function elapsed_of(ms: number): string {
|
|
|
216
214
|
return `${two_digits(minutes)}:${two_digits(seconds % SECONDS_PER_MINUTE)}`
|
|
217
215
|
}
|
|
218
216
|
|
|
219
|
-
// How long
|
|
220
|
-
|
|
221
|
-
|
|
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 {
|
|
220
|
+
const seconds = Math.floor(Math.max(0, ms) / MS_PER_SECOND)
|
|
221
|
+
const minutes = Math.floor(seconds / SECONDS_PER_MINUTE)
|
|
222
222
|
const hours = Math.floor(minutes / MINUTES_PER_HOUR)
|
|
223
|
+
const tail = [minutes % MINUTES_PER_HOUR, seconds % SECONDS_PER_MINUTE]
|
|
223
224
|
|
|
224
|
-
return
|
|
225
|
+
return [String(hours), ...tail.map((value) => two_digits(value))].join(':')
|
|
225
226
|
}
|
|
226
227
|
|
|
227
228
|
function is_shade(color: Paint): color is Shade {
|
|
@@ -289,9 +290,9 @@ const run_board_labels = {
|
|
|
289
290
|
bar_of,
|
|
290
291
|
clock_of,
|
|
291
292
|
elapsed_of,
|
|
292
|
-
left_of,
|
|
293
293
|
minute_of,
|
|
294
294
|
painted,
|
|
295
|
+
span_of,
|
|
295
296
|
spinner_of,
|
|
296
297
|
}
|
|
297
298
|
|
|
@@ -157,28 +157,40 @@ function alert_of(spec: GaugeSpec, value: number, pressure: number | undefined):
|
|
|
157
157
|
return verdict ?? threshold_alert(value, spec)
|
|
158
158
|
}
|
|
159
159
|
|
|
160
|
+
// One gauge's reading: its value, the width its figure is right-aligned to where that is wider than
|
|
161
|
+
// the figure's own, and the kernel's pressure verdict where the gauge takes one.
|
|
162
|
+
interface Reading {
|
|
163
|
+
value: number | undefined
|
|
164
|
+
width: number
|
|
165
|
+
pressure?: number | undefined
|
|
166
|
+
}
|
|
167
|
+
|
|
160
168
|
// Icon, figure, then bar: the figure is what a person reads, and its fixed
|
|
161
169
|
// width keeps every bar starting in one column. The bar stops full past `spec.full`; the figure still
|
|
162
170
|
// says how far past, and takes the warning colors only, so a quiet machine's figures stay the terminal's
|
|
163
171
|
// own.
|
|
164
|
-
function gauge(spec: GaugeSpec,
|
|
172
|
+
function gauge(spec: GaugeSpec, reading: Reading): string | undefined {
|
|
173
|
+
const { value, width } = reading
|
|
174
|
+
|
|
165
175
|
if (value === undefined) return undefined
|
|
166
176
|
|
|
167
|
-
const alert = alert_of(spec, value, pressure)
|
|
177
|
+
const alert = alert_of(spec, value, reading.pressure)
|
|
168
178
|
const warning = alert === 'normal' ? undefined : GAUGE_SHADES[alert]
|
|
169
179
|
const bar = bar_of(value, spec.full, undefined, warning ?? spec.normal)
|
|
170
180
|
|
|
171
|
-
return `${spec.icon} ${painted(warning, spec.format(value))} ${bar}`
|
|
181
|
+
return `${spec.icon} ${painted(warning, spec.format(value).padStart(width))} ${bar}`
|
|
172
182
|
}
|
|
173
183
|
|
|
174
|
-
// `undefined` when not one gauge could be read, so the header draws no line at all.
|
|
175
|
-
|
|
184
|
+
// `undefined` when not one gauge could be read, so the header draws no line at all. `width` is the
|
|
185
|
+
// header's figure column: a figure narrower than it is right-aligned to it, so the first gauge's bar
|
|
186
|
+
// starts under the progress bar whichever gauge that is.
|
|
187
|
+
function line_of(gauges: MachineGauges | undefined, width = 0): string | undefined {
|
|
176
188
|
if (gauges === undefined) return undefined
|
|
177
189
|
|
|
178
190
|
const parts = [
|
|
179
|
-
gauge(SPECS.cpu, gauges.cpu_percent),
|
|
180
|
-
gauge(SPECS.memory, gauges.memory_percent, gauges.memory_pressure),
|
|
181
|
-
gauge(SPECS.swap, gauges.swap_mb_per_s),
|
|
191
|
+
gauge(SPECS.cpu, { value: gauges.cpu_percent, width }),
|
|
192
|
+
gauge(SPECS.memory, { value: gauges.memory_percent, width, pressure: gauges.memory_pressure }),
|
|
193
|
+
gauge(SPECS.swap, { value: gauges.swap_mb_per_s, width }),
|
|
182
194
|
].filter((part) => part !== undefined)
|
|
183
195
|
|
|
184
196
|
return parts.length === 0 ? undefined : parts.join(GAP)
|
|
@@ -188,6 +200,7 @@ const run_board_machine = {
|
|
|
188
200
|
CPU_YELLOW,
|
|
189
201
|
GAP,
|
|
190
202
|
MEMORY_YELLOW,
|
|
203
|
+
PERCENT_WIDTH,
|
|
191
204
|
gauges_of,
|
|
192
205
|
line_of,
|
|
193
206
|
percent_text,
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import type { NamedPlan } from '#scripts/backlog/backlog-plan'
|
|
2
|
+
import { backlogrun_launch } from '#scripts/backlog/backlogrun-launch'
|
|
2
3
|
import { lane_registry } from '#scripts/lane/lane-registry'
|
|
3
4
|
import { run_carry } from '#scripts/run/carry/run-carry'
|
|
4
5
|
import { run_carry_ended } from '#scripts/run/carry/run-carry-ended'
|
|
5
6
|
import { run_event_stream, type RunEvent } from '#scripts/run/event/run-event-stream'
|
|
7
|
+
import { run_stranded_detect } from '#scripts/run/run-stranded-detect'
|
|
6
8
|
import { run_board_scope } from './run-board-scope'
|
|
7
9
|
import { run_board_span, type BoardSpan } from './run-board-span'
|
|
8
10
|
|
|
@@ -18,6 +20,9 @@ interface LocalRead {
|
|
|
18
20
|
lanes: ReadonlyArray<string>
|
|
19
21
|
// The session a stopped run is resumed from; `undefined` otherwise.
|
|
20
22
|
resume: string | undefined
|
|
23
|
+
// The run is carried but nothing drives it — its owner and its supervisor are both gone
|
|
24
|
+
// (`run-stranded.ts`). Drawn as a stopped run, since no one is looking for its next issue.
|
|
25
|
+
is_stranded: boolean
|
|
21
26
|
}
|
|
22
27
|
|
|
23
28
|
interface SpanHere {
|
|
@@ -28,10 +33,11 @@ interface SpanHere {
|
|
|
28
33
|
function span_here(repository: string): SpanHere {
|
|
29
34
|
const read = run_carry.read_carry(run_carry.carry_path(repository))
|
|
30
35
|
const ended = run_carry_ended.read_ended(run_carry_ended.ended_path(repository))
|
|
36
|
+
const launch = backlogrun_launch.pending_launch(repository)
|
|
31
37
|
|
|
32
38
|
return {
|
|
33
|
-
span: run_board_span.span_of(read, ended),
|
|
34
|
-
resume: run_board_span.resume_of(read, ended),
|
|
39
|
+
span: run_board_span.span_of(read, ended, launch),
|
|
40
|
+
resume: run_board_span.resume_of(read, ended, launch),
|
|
35
41
|
}
|
|
36
42
|
}
|
|
37
43
|
|
|
@@ -53,6 +59,7 @@ async function read_local(): Promise<LocalRead | undefined> {
|
|
|
53
59
|
events: run_board_span.events_of(events, span),
|
|
54
60
|
lanes: lanes.map((lane) => lane.issue),
|
|
55
61
|
resume,
|
|
62
|
+
is_stranded: run_stranded_detect.is_stranded_at(repository),
|
|
56
63
|
}
|
|
57
64
|
}
|
|
58
65
|
|
|
@@ -373,11 +373,7 @@ function bounded(parts: FrameParts, bounds: FrameBounds): Array<string> {
|
|
|
373
373
|
|
|
374
374
|
function render(view: BoardView): Array<string> {
|
|
375
375
|
const { header, notes } = view
|
|
376
|
-
const above = [
|
|
377
|
-
...run_board_header.header_lines(header),
|
|
378
|
-
...resume_lines(view.resume),
|
|
379
|
-
...run_board_header.idle_lines(header),
|
|
380
|
-
]
|
|
376
|
+
const above = [...run_board_header.header_lines(header), ...resume_lines(view.resume)]
|
|
381
377
|
const parts = {
|
|
382
378
|
above,
|
|
383
379
|
plan: plan_sections(header, view.size),
|
|
@@ -0,0 +1,73 @@
|
|
|
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
|
+
|
|
4
|
+
// What the run's `setup` events (`run-event-setup.ts`) tell `run:board`: the stage a run is at before
|
|
5
|
+
// its first child launches, whether it is preparing a launch now, and which lanes are not children yet.
|
|
6
|
+
// The events carry it all — nothing here infers a stage from which records exist.
|
|
7
|
+
|
|
8
|
+
const KIND = run_event_stream.EVENT_KIND
|
|
9
|
+
|
|
10
|
+
interface SetupRead {
|
|
11
|
+
// The newest stage's own words while the run is starting; `undefined` from the first launch — or a
|
|
12
|
+
// drain before one — on, when the plan's counts say more than a stage does.
|
|
13
|
+
setup: string | undefined
|
|
14
|
+
// The run is getting a child ready: any stage while it is starting, a lane opening after that.
|
|
15
|
+
is_preparing: boolean
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const NO_SETUP: SetupRead = { setup: undefined, is_preparing: false }
|
|
19
|
+
|
|
20
|
+
function is_setup(event: RunEvent): boolean {
|
|
21
|
+
return event.kind === KIND.SETUP
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function has_launched(events: ReadonlyArray<RunEvent>): boolean {
|
|
25
|
+
return events.some((event) => event.kind === KIND.CHILD_LAUNCH)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// The start is over at the first launch, or at a drain that came before one: a run whose backlog
|
|
29
|
+
// emptied with nothing launched is watching, and the hand-off each wake of that watch writes is not a
|
|
30
|
+
// run getting ready.
|
|
31
|
+
function has_started(events: ReadonlyArray<RunEvent>): boolean {
|
|
32
|
+
return has_launched(events) || events.some((event) => event.kind === KIND.DRAIN)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// A lane is opening while the stage naming its issue is newer than the run's newest position: the
|
|
36
|
+
// launch it leads to, or whatever the run did instead, ends it. A stage that names no issue — a
|
|
37
|
+
// hand-off between two children — prepares nothing a person waits on.
|
|
38
|
+
function is_opening(events: ReadonlyArray<RunEvent>, stage: RunEvent): boolean {
|
|
39
|
+
if (run_event_scope.issue_named(stage) === undefined) return false
|
|
40
|
+
|
|
41
|
+
const position = events.findLast((event) => !run_event_stream.TRACE_KINDS.has(event.kind))
|
|
42
|
+
|
|
43
|
+
return position === undefined || position.pos < stage.pos
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function read(events: ReadonlyArray<RunEvent>): SetupRead {
|
|
47
|
+
const stage = events.findLast((event) => is_setup(event))
|
|
48
|
+
|
|
49
|
+
if (stage === undefined) return NO_SETUP
|
|
50
|
+
if (!has_started(events)) return { setup: stage.text, is_preparing: true }
|
|
51
|
+
|
|
52
|
+
return { setup: undefined, is_preparing: is_opening(events, stage) }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function names(event: RunEvent, issue: number): boolean {
|
|
56
|
+
return Number(run_event_scope.issue_named(event)) === issue
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// A lane the stream saw no launch for is not yet this run's child when the run says it is still
|
|
60
|
+
// setting up: it has launched nothing at all, so any lane here is an earlier run's, or it is opening
|
|
61
|
+
// this very lane. A stream with no stage on it says neither, and the lane is read as before.
|
|
62
|
+
function is_lane_pending(events: ReadonlyArray<RunEvent>, issue: number): boolean {
|
|
63
|
+
const stages = events.filter((event) => is_setup(event))
|
|
64
|
+
|
|
65
|
+
if (stages.length === 0) return false
|
|
66
|
+
|
|
67
|
+
return !has_launched(events) || stages.some((stage) => names(stage, issue))
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const run_board_setup = { is_lane_pending, read }
|
|
71
|
+
|
|
72
|
+
export { run_board_setup }
|
|
73
|
+
export type { SetupRead }
|