@joshuafolkken/kit 1.3092.0 → 1.3101.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/.github/actions/setup-pnpm/action.yml +12 -4
- package/dist/.inputs-verified.json +1 -1
- package/dist/commands/backlog-drive-cli.js +17 -17
- package/dist/commands/backlogrun-cli.js +21 -20
- package/dist/commands/check-related.js +18 -18
- package/dist/commands/{chunk-VXE3V3OC.js → chunk-2CNVIA35.js} +2 -2
- package/dist/commands/{chunk-374XGAWD.js → chunk-36XHHYNU.js} +1 -1
- package/dist/commands/{chunk-W3MDLQZF.js → chunk-3LUPGBW4.js} +1 -1
- package/dist/commands/{chunk-T6OUNP7E.js → chunk-3VLDFM5I.js} +6 -6
- package/dist/commands/{chunk-4K7KLR7L.js → chunk-6AHAY5VZ.js} +1 -1
- package/dist/commands/{chunk-ET6IRQ3L.js → chunk-6FN37DER.js} +3 -3
- package/dist/commands/{chunk-LBMAJDGN.js → chunk-6JD4VNO4.js} +2 -2
- package/dist/commands/{chunk-KD3SEAMV.js → chunk-7HYYHCTN.js} +2 -2
- package/dist/commands/{chunk-4GUGC4IW.js → chunk-7K5ITPHR.js} +2 -2
- package/dist/commands/{chunk-XTJA34ND.js → chunk-7ULYLGUK.js} +10 -10
- package/dist/commands/{chunk-IEKW3ON6.js → chunk-ABM2MZBF.js} +1 -1
- package/dist/commands/{chunk-IOKOHB7P.js → chunk-BBLEC4YQ.js} +1 -1
- package/dist/commands/{chunk-V2T5DP3B.js → chunk-C7FGDZWP.js} +1 -1
- package/dist/commands/{chunk-MHXIJKGP.js → chunk-EHPTXSAT.js} +20 -9
- package/dist/commands/{chunk-PF3PMT2Q.js → chunk-EPICTV2P.js} +1 -1
- package/dist/commands/{chunk-GVDRRBHZ.js → chunk-EVVQJJFV.js} +2 -2
- package/dist/commands/{chunk-NOSG3RUP.js → chunk-F57LACNC.js} +2 -2
- package/dist/commands/{chunk-BAEDHPXW.js → chunk-FA7CN75F.js} +4 -4
- package/dist/commands/{chunk-62NYXZCC.js → chunk-FDASVHZD.js} +1 -1
- package/dist/commands/{chunk-YMAEYY5R.js → chunk-FEYZ575X.js} +1 -1
- package/dist/commands/{chunk-PILH72AV.js → chunk-FTAWHAI7.js} +1 -1
- package/dist/commands/{chunk-MK4CARBI.js → chunk-G3BBCAAH.js} +1 -1
- package/dist/commands/{chunk-Z6QLFPFB.js → chunk-GABDPTDK.js} +4 -4
- package/dist/commands/{chunk-VHLDQ3PH.js → chunk-HHS3LYLV.js} +2 -2
- package/dist/commands/{chunk-EE4VOZJP.js → chunk-HQZ65PJE.js} +3 -3
- package/dist/commands/{chunk-KHGDZQRG.js → chunk-HSZECERZ.js} +2 -2
- package/dist/commands/{chunk-BLUAO4QW.js → chunk-IY3DSDRW.js} +1 -1
- package/dist/commands/{chunk-2XZF3YFY.js → chunk-J2RUI4KN.js} +1 -1
- package/dist/commands/{chunk-2Q7MGFYB.js → chunk-J3PCRUVY.js} +27 -12
- package/dist/commands/{chunk-RDUFVQVJ.js → chunk-JU2MAYQ3.js} +7 -7
- package/dist/commands/{chunk-ZEUKX3RZ.js → chunk-KJPZPTS4.js} +1 -1
- package/dist/commands/{chunk-7E43YW53.js → chunk-MHF6BDQI.js} +1 -1
- package/dist/commands/{chunk-BE6F4YK2.js → chunk-OHN3QARP.js} +10 -10
- package/dist/commands/{chunk-AT46EY7H.js → chunk-OKBZGLD5.js} +1 -1
- package/dist/commands/{chunk-XCCT6GKA.js → chunk-OZN4HOZT.js} +2 -2
- package/dist/commands/{chunk-ZJ5S4VFD.js → chunk-RAQ3E6OT.js} +1 -1
- package/dist/commands/{chunk-XSKJAG72.js → chunk-S5GX3H4P.js} +1 -1
- package/dist/commands/{chunk-HESWKELD.js → chunk-SC57VV6A.js} +1 -1
- package/dist/commands/{chunk-EZYW54OQ.js → chunk-SOETJZWH.js} +221 -118
- package/dist/commands/{chunk-HNFBEB4L.js → chunk-UUA63LCI.js} +1 -1
- package/dist/commands/{chunk-ZGBXUY7R.js → chunk-UZDKCGDC.js} +1 -1
- package/dist/commands/{chunk-B5XYAWNN.js → chunk-VPRQFM2T.js} +1 -1
- package/dist/commands/{chunk-M4WG74Q7.js → chunk-VUCDGQ6Y.js} +1 -1
- package/dist/commands/{chunk-UX3HCPLX.js → chunk-W7X7MZZN.js} +1 -1
- package/dist/commands/{chunk-EN65ZS3S.js → chunk-WORHUJ6T.js} +1 -1
- package/dist/commands/chunk-WRHMYSW7.js +36 -0
- package/dist/commands/{chunk-K5H3CLZZ.js → chunk-YANDBVAT.js} +1 -1
- package/dist/commands/{chunk-GWTBIGYC.js → chunk-YD7XK2TX.js} +1 -1
- package/dist/commands/{chunk-M5HA2ZCG.js → chunk-YT5JR2CB.js} +2 -2
- package/dist/commands/{chunk-EWCUT7U2.js → chunk-Z2NUK47W.js} +1 -1
- package/dist/commands/{chunk-4MGTNY2L.js → chunk-ZI3SABMH.js} +1 -1
- package/dist/commands/{chunk-TTTZZN7F.js → chunk-ZJJLROJY.js} +3 -3
- package/dist/commands/disposition-cli.js +15 -15
- package/dist/commands/git-followup-workflow.js +18 -7
- package/dist/commands/inputs.json +1668 -1663
- package/dist/commands/issue-already-done-cli.js +8 -8
- package/dist/commands/issue-measured-cli.js +9 -9
- package/dist/commands/issue-plan-cli.js +16 -16
- package/dist/commands/issue-premise-unmet-cli.js +8 -8
- package/dist/commands/lane-breakdown-cli.js +17 -17
- package/dist/commands/lane-cli.js +20 -20
- package/dist/commands/lane-measure-cli.js +23 -23
- package/dist/commands/lane-plan-cli.js +15 -15
- package/dist/commands/lane-restart-cli.js +20 -20
- package/dist/commands/lane-why-cli.js +4 -4
- package/dist/commands/lint-parallel-main.js +2 -2
- package/dist/commands/lint-related.js +6 -6
- package/dist/commands/lint-rule.js +2 -2
- package/dist/commands/plan-scaffold-cli.js +20 -20
- package/dist/commands/pre-commit-type-check.js +7 -7
- package/dist/commands/pre-push-unit.js +9 -9
- package/dist/commands/prose-stale-cli.js +5 -5
- package/dist/commands/review-brief-main.js +23 -23
- package/dist/commands/review-pair-cli.js +19 -19
- package/dist/commands/review-record-main.js +19 -19
- package/dist/commands/review-round2-cli.js +4 -4
- package/dist/commands/review-submit-cli.js +23 -23
- package/dist/commands/run-board-main.js +2 -1
- package/dist/commands/run-chain-cli.js +19 -19
- package/dist/commands/run-cut-cli.js +19 -19
- package/dist/commands/run-entry-cli.js +27 -27
- package/dist/commands/run-hold-main.js +7 -7
- package/dist/commands/run-next-cli.js +20 -20
- package/dist/commands/run-park-cli.js +8 -8
- package/dist/commands/run-pause-cli.js +8 -8
- package/dist/commands/run-prep-main.js +20 -20
- package/dist/commands/run-prime-cli.js +10 -10
- package/dist/commands/run-review-cli.js +26 -26
- package/dist/commands/run-ship-cli.js +29 -29
- package/dist/commands/run-ship-evidence-cli.js +4 -4
- package/dist/commands/run-ship-wait-main.js +5 -5
- package/dist/commands/run-split-cli.js +8 -8
- package/dist/commands/run-step-cli.js +27 -27
- package/dist/commands/run-wake-main.js +19 -19
- package/dist/commands/selfcheck-cli.js +8 -8
- package/dist/commands/stop-guard.js +7 -7
- package/dist/commands/test-e2e-guard.js +6 -6
- package/dist/commands/test-pinned-main.js +8 -8
- package/dist/commands/test-reach-cli.js +4 -4
- package/dist/commands/{test-related-Z6YY2GFH.js → test-related-XBDH2IOH.js} +11 -11
- package/dist/commands/test-related-main.js +13 -13
- package/dist/commands/verification-gate.js +17 -17
- package/dist/hooks/inputs.json +511 -510
- package/dist/hooks/stop-guard.js +68 -59
- package/dist/inputs.json +64 -64
- package/package.json +1 -1
- package/scripts/followup/git-followup-finish.ts +16 -1
- package/scripts/gate/verification-inputs.ts +2 -1
- package/scripts/run/board/run-board-layout.ts +9 -4
- package/scripts/run/board/run-board-read.ts +29 -69
- package/scripts/run/board/run-board-render-lanes.ts +26 -9
- package/scripts/run/board/run-board-render-lead.ts +9 -0
- package/scripts/run/board/run-board-root-layout.ts +69 -0
- package/scripts/run/board/run-board-single.ts +69 -4
- package/scripts/run/board/run-board-span-read.ts +85 -0
- package/scripts/run/board/run-board-tick.ts +12 -4
- package/scripts/run/hold/run-hold-ended.ts +50 -0
- package/scripts/test/test-pinned-cache-logic.ts +13 -7
- package/scripts/test/test-pinned-cache.ts +33 -11
- package/scripts/test/test-runner-config.ts +19 -0
|
@@ -109,6 +109,8 @@ interface BoardLayout {
|
|
|
109
109
|
// The earlier run's lanes that still hold a place under the limit, and the lanes with no record —
|
|
110
110
|
// drawn after the run's own lanes, and no row the header counts.
|
|
111
111
|
earlier?: ReadonlyArray<BoardRow> | undefined
|
|
112
|
+
// The root checkout's single run beside the run, drawn above its lanes and no row the header counts.
|
|
113
|
+
root?: { name: string; row: BoardRow } | undefined
|
|
112
114
|
}
|
|
113
115
|
|
|
114
116
|
// What an active row is named and classified by — each issue's title and labels.
|
|
@@ -382,9 +384,10 @@ function rows_of(layout: BoardLayout): Array<BoardRow> {
|
|
|
382
384
|
return [...layout.active, ...queued_rows(layout), ...layout.people, ...layout.unreached]
|
|
383
385
|
}
|
|
384
386
|
|
|
385
|
-
// Every row the frame draws — the counted ones and the earlier run's
|
|
387
|
+
// Every row the frame draws — the root checkout's single run's, the counted ones and the earlier run's
|
|
388
|
+
// lanes — what the legend reads.
|
|
386
389
|
function drawn_rows(layout: BoardLayout): Array<BoardRow> {
|
|
387
|
-
return [
|
|
390
|
+
return [layout.root?.row ?? [], rows_of(layout), layout.earlier ?? []].flat()
|
|
388
391
|
}
|
|
389
392
|
|
|
390
393
|
// Every row waiting on a person's decision: the touched children parked on one, then the untouched
|
|
@@ -395,11 +398,12 @@ function people_of(layout: BoardLayout): Array<BoardRow> {
|
|
|
395
398
|
return [...asked, ...layout.people]
|
|
396
399
|
}
|
|
397
400
|
|
|
398
|
-
// The run's own rows `apart` does not name, then the earlier
|
|
401
|
+
// The root checkout's single run's row, the run's own rows `apart` does not name, then the earlier
|
|
402
|
+
// run's lanes still holding a place.
|
|
399
403
|
function lane_rows(layout: BoardLayout, apart: ReadonlyArray<BoardRow>): Array<BoardRow> {
|
|
400
404
|
const own = layout.active.filter((row) => !apart.includes(row))
|
|
401
405
|
|
|
402
|
-
return [
|
|
406
|
+
return [layout.root?.row ?? [], own, layout.earlier ?? []].flat()
|
|
403
407
|
}
|
|
404
408
|
|
|
405
409
|
// The lanes section's rows: those neither finished nor waiting on a person's decision.
|
|
@@ -420,6 +424,7 @@ function listed_parks(layout: BoardLayout): ReadonlySet<number> {
|
|
|
420
424
|
|
|
421
425
|
const run_board_layout = {
|
|
422
426
|
EMPTY_LAYOUT,
|
|
427
|
+
active_order,
|
|
423
428
|
drawn_rows,
|
|
424
429
|
is_blocked,
|
|
425
430
|
is_decision: run_board_human.is_decision,
|
|
@@ -1,20 +1,17 @@
|
|
|
1
1
|
import { backlog_drive_memory } from '#scripts/backlog/backlog-drive-memory'
|
|
2
2
|
import type { NamedPlan } from '#scripts/backlog/backlog-plan'
|
|
3
3
|
import { backlog_ready } from '#scripts/backlog/backlog-ready'
|
|
4
|
-
import { backlogrun_launch } from '#scripts/backlog/backlogrun-launch'
|
|
5
4
|
import { lane_held_stats } from '#scripts/lane/lane-held-stats'
|
|
6
5
|
import { lane_ledger } from '#scripts/lane/lane-ledger'
|
|
7
6
|
import type { TableRead } from '#scripts/lane/lane-process-table'
|
|
8
7
|
import { lane_registry, type LaneInfo } from '#scripts/lane/lane-registry'
|
|
9
8
|
import { lane_state, type LaneStateRecord } from '#scripts/lane/lane-state'
|
|
10
9
|
import { stamped_memo } from '#scripts/lib/stamped-memo'
|
|
11
|
-
import { run_carry, type
|
|
12
|
-
import { run_carry_ended } from '#scripts/run/carry/run-carry-ended'
|
|
10
|
+
import { run_carry, type RunCarry } from '#scripts/run/carry/run-carry'
|
|
13
11
|
import { run_event_scope } from '#scripts/run/event/run-event-scope'
|
|
14
12
|
import { run_event_stream, type RunEvent } from '#scripts/run/event/run-event-stream'
|
|
15
13
|
import { run_event_tail } from '#scripts/run/event/run-event-tail'
|
|
16
14
|
import { run_merge_usage_limit } from '#scripts/run/merge/run-merge-usage-limit'
|
|
17
|
-
import { run_stranded_detect } from '#scripts/run/run-stranded-detect'
|
|
18
15
|
import { run_wake } from '#scripts/run/wake/run-wake'
|
|
19
16
|
import { run_board_found, type FoundOf } from './run-board-found'
|
|
20
17
|
import { run_board_lane_result } from './run-board-lane-result'
|
|
@@ -26,10 +23,10 @@ import {
|
|
|
26
23
|
} from './run-board-operation-read'
|
|
27
24
|
import type { ProcessRoots } from './run-board-process-groups'
|
|
28
25
|
import type { QuotaProvider } from './run-board-quota-read'
|
|
29
|
-
import { run_board_roots } from './run-board-roots'
|
|
30
26
|
import { run_board_scope } from './run-board-scope'
|
|
31
|
-
import { run_board_single } from './run-board-single'
|
|
27
|
+
import { run_board_single, type EndedRoot, type RootRun } from './run-board-single'
|
|
32
28
|
import { run_board_span, type BoardSpan } from './run-board-span'
|
|
29
|
+
import { run_board_span_read } from './run-board-span-read'
|
|
33
30
|
import { run_board_throughput, type ThroughputRow } from './run-board-throughput'
|
|
34
31
|
import { run_board_usual, type UsualTimes } from './run-board-usual'
|
|
35
32
|
|
|
@@ -42,6 +39,10 @@ interface LocalRead {
|
|
|
42
39
|
ended_ms: number | undefined
|
|
43
40
|
// The single run's name the header draws; `undefined` for a `backlogrun` (`run-board-single.ts`).
|
|
44
41
|
run_name?: string | undefined
|
|
42
|
+
// The root checkout's single run beside a carried run, drawn above its lanes.
|
|
43
|
+
root?: RootRun | undefined
|
|
44
|
+
// The root checkout's single runs a merge ended inside the run, drawn in `✅ done`.
|
|
45
|
+
ended_roots?: ReadonlyArray<EndedRoot> | undefined
|
|
45
46
|
// What the run was asked to do, from its invocation.
|
|
46
47
|
scope: NamedPlan
|
|
47
48
|
events: ReadonlyArray<RunEvent>
|
|
@@ -103,27 +104,6 @@ interface LocalRead {
|
|
|
103
104
|
found?: FoundOf | undefined
|
|
104
105
|
}
|
|
105
106
|
|
|
106
|
-
const NO_SPAN: SpanHere = {
|
|
107
|
-
span: undefined,
|
|
108
|
-
resume: undefined,
|
|
109
|
-
transcript: undefined,
|
|
110
|
-
roots: {},
|
|
111
|
-
carry: undefined,
|
|
112
|
-
is_stranded: false,
|
|
113
|
-
is_session_active: false,
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
interface SpanHere {
|
|
117
|
-
span: BoardSpan | undefined
|
|
118
|
-
resume: string | undefined
|
|
119
|
-
// The parent session's transcript, where the run's record names one.
|
|
120
|
-
transcript: string | undefined
|
|
121
|
-
roots: ProcessRoots
|
|
122
|
-
carry: RunCarry | undefined
|
|
123
|
-
is_stranded: boolean
|
|
124
|
-
is_session_active: boolean
|
|
125
|
-
}
|
|
126
|
-
|
|
127
107
|
// The main work tree, resolved once: it does not move while the board is open, and resolving it runs a
|
|
128
108
|
// `git` subprocess the redraw would wait on (joshuafolkken/kit#4263). A failed resolve is forgotten,
|
|
129
109
|
// so a `git` that timed out once is asked again on the next redraw rather than failing every one.
|
|
@@ -142,44 +122,6 @@ async function main_root(): Promise<string> {
|
|
|
142
122
|
}
|
|
143
123
|
}
|
|
144
124
|
|
|
145
|
-
// Every process the run's record names is asked of `table`, the redraw's own `ps` snapshot — only once
|
|
146
|
-
// a run is found, so a repository with no run waits on no `ps`.
|
|
147
|
-
async function recorded_span_here(
|
|
148
|
-
repository: string,
|
|
149
|
-
table: TableRead | undefined,
|
|
150
|
-
read: CarryRead,
|
|
151
|
-
): Promise<SpanHere> {
|
|
152
|
-
const ended = run_carry_ended.read_ended(run_carry_ended.ended_path(repository))
|
|
153
|
-
const launch = backlogrun_launch.pending_launch(repository)
|
|
154
|
-
const span = run_board_span.span_of(read, ended, launch)
|
|
155
|
-
|
|
156
|
-
if (span === undefined) return NO_SPAN
|
|
157
|
-
|
|
158
|
-
const same_process = await run_board_roots.same_in(table)
|
|
159
|
-
const stranded_ports = run_stranded_detect.ports_with(same_process)
|
|
160
|
-
|
|
161
|
-
return {
|
|
162
|
-
span,
|
|
163
|
-
resume: run_board_span.resume_of(read, ended, launch),
|
|
164
|
-
transcript: read.kind === 'carried' ? read.carry.owner_transcript : undefined,
|
|
165
|
-
roots: run_board_roots.roots_of(repository, read, same_process),
|
|
166
|
-
is_session_active: run_board_roots.session_is_active(repository, read, same_process),
|
|
167
|
-
carry: read.kind === 'carried' ? read.carry : undefined,
|
|
168
|
-
is_stranded: run_stranded_detect.is_stranded_at(repository, stranded_ports),
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
// A single run this checkout holds is the one drawn (`run-board-single.ts`), before the ended run and,
|
|
173
|
-
// outside a lane, before the carried one; it names no process.
|
|
174
|
-
async function span_here(repository: string, table: TableRead | undefined): Promise<SpanHere> {
|
|
175
|
-
const read = run_carry.read_carry(run_carry.carry_path(repository))
|
|
176
|
-
const single = await run_board_single.span_here(read.kind !== 'none')
|
|
177
|
-
|
|
178
|
-
return single === undefined
|
|
179
|
-
? await recorded_span_here(repository, table, read)
|
|
180
|
-
: { ...NO_SPAN, span: single }
|
|
181
|
-
}
|
|
182
|
-
|
|
183
125
|
// Where each session of the run writes what it does: a lane's child to its dispatch log, in the lane's
|
|
184
126
|
// own checkout; the parent and a judgment session to theirs, in this one.
|
|
185
127
|
function sources_of(
|
|
@@ -327,11 +269,16 @@ async function span_events_here(
|
|
|
327
269
|
}
|
|
328
270
|
}
|
|
329
271
|
|
|
272
|
+
type RootFields = Pick<LocalRead, 'root' | 'ended_roots'>
|
|
273
|
+
|
|
274
|
+
// The span's own fields, with the root checkout's single runs beside it.
|
|
330
275
|
function span_fields(
|
|
331
276
|
span: BoardSpan,
|
|
332
|
-
|
|
277
|
+
root: RootFields,
|
|
278
|
+
): Pick<LocalRead, 'started_ms' | 'ended_ms' | 'scheduler_provider' | 'run_name'> & RootFields {
|
|
333
279
|
const run_name = run_board_single.name_of(span.invocation)
|
|
334
280
|
const times = {
|
|
281
|
+
...root,
|
|
335
282
|
started_ms: span.started_ms,
|
|
336
283
|
ended_ms: span.ended_ms,
|
|
337
284
|
...(run_name !== undefined && { run_name }),
|
|
@@ -342,12 +289,25 @@ function span_fields(
|
|
|
342
289
|
return { ...times, scheduler_provider: span.scheduler_provider === 'openai' ? 'codex' : 'claude' }
|
|
343
290
|
}
|
|
344
291
|
|
|
292
|
+
// The root checkout's single run beside the span, and its runs a merge ended inside it.
|
|
293
|
+
async function roots_here(
|
|
294
|
+
span: BoardSpan,
|
|
295
|
+
root: BoardSpan | undefined,
|
|
296
|
+
events: ReadonlyArray<RunEvent>,
|
|
297
|
+
): Promise<RootFields> {
|
|
298
|
+
const ended_roots = await run_board_single.ended_here(span)
|
|
299
|
+
|
|
300
|
+
return root === undefined
|
|
301
|
+
? { ended_roots }
|
|
302
|
+
: { root: run_board_single.root_of(root, events), ended_roots }
|
|
303
|
+
}
|
|
304
|
+
|
|
345
305
|
// `undefined` when no run has started here. The lane count reads `table`, the redraw's process
|
|
346
306
|
// snapshot, so it agrees with the usage columns.
|
|
347
307
|
async function read_local(table?: TableRead): Promise<LocalRead | undefined> {
|
|
348
308
|
const repository = await run_carry.repository_directory()
|
|
349
|
-
const { span, resume, transcript, roots, carry, is_stranded, is_session_active } =
|
|
350
|
-
|
|
309
|
+
const { span, root, resume, transcript, roots, carry, is_stranded, is_session_active } =
|
|
310
|
+
await run_board_span_read.span_here(repository, table)
|
|
351
311
|
|
|
352
312
|
if (repository === undefined || span === undefined) return undefined
|
|
353
313
|
|
|
@@ -356,7 +316,7 @@ async function read_local(table?: TableRead): Promise<LocalRead | undefined> {
|
|
|
356
316
|
const lane_read = await lanes_here(lanes, repository, events, span.started_ms)
|
|
357
317
|
|
|
358
318
|
return {
|
|
359
|
-
...span_fields(span),
|
|
319
|
+
...span_fields(span, await roots_here(span, root, events)),
|
|
360
320
|
scope: run_board_scope.scope_of(span.invocation, carry?.added),
|
|
361
321
|
...(await span_events_here(events, span, table, carry)),
|
|
362
322
|
throughput: run_board_throughput.rows_of(events, Date.now()),
|
|
@@ -20,7 +20,8 @@ import { run_board_usage_text, type CellLimits } from './run-board-usage-text'
|
|
|
20
20
|
const { HEADER_ICONS } = run_board_labels
|
|
21
21
|
const { cell_widths, cells_of, lane_limits } = run_board_usage_text
|
|
22
22
|
const { columns_rule } = run_board_render_section
|
|
23
|
-
const { ELAPSED_START, LANES_LABEL, USAGE_START, column_offset, lane_label } =
|
|
23
|
+
const { ELAPSED_START, LANES_LABEL, USAGE_START, column_offset, lane_label, root_label } =
|
|
24
|
+
run_board_render_lead
|
|
24
25
|
const { width_of } = run_board_fit
|
|
25
26
|
const { columns_cut } = run_board_render_title
|
|
26
27
|
const { reasons_of } = run_board_seat
|
|
@@ -150,20 +151,17 @@ function seat_lines(header: BoardHeader, columns: number | undefined): Array<Pla
|
|
|
150
151
|
}))
|
|
151
152
|
}
|
|
152
153
|
|
|
153
|
-
//
|
|
154
|
-
// cells where they are drawn
|
|
155
|
-
function
|
|
156
|
-
|
|
154
|
+
// A section of lane rows under `label`'s rule, ⏰ over the run's time and the usage labels over their
|
|
155
|
+
// cells where they are drawn; none without a line.
|
|
156
|
+
function rows_section(
|
|
157
|
+
lines: Array<PlanLine>,
|
|
158
|
+
label: string,
|
|
157
159
|
cells: LaneCells | undefined,
|
|
158
160
|
header: BoardHeader,
|
|
159
|
-
columns?: number,
|
|
160
161
|
): Array<PlanSection> {
|
|
161
|
-
const lines = [...rows, ...seat_lines(header, columns)]
|
|
162
|
-
|
|
163
162
|
if (lines.length === 0) return []
|
|
164
163
|
|
|
165
164
|
const offset = column_offset(header)
|
|
166
|
-
const label = lane_label(header)
|
|
167
165
|
const elapsed = { label: HEADER_ICONS.elapsed, start: ELAPSED_START + offset }
|
|
168
166
|
const usage = cells === undefined ? [] : label_columns(USAGE_START + offset, cells.gauge)
|
|
169
167
|
const rule = columns_rule(label, [elapsed, ...usage])
|
|
@@ -171,6 +169,25 @@ function lanes_section(
|
|
|
171
169
|
return [{ head: ['', rule], lines, label, key: 'lanes' }]
|
|
172
170
|
}
|
|
173
171
|
|
|
172
|
+
// The root checkout's single run's row, the first of `rows` (`run_board_layout.lane_rows`), under its
|
|
173
|
+
// own `🏠` rule above the run's lanes (joshuafolkken/kit#7524); then the run's own rows under the lanes
|
|
174
|
+
// rule and the empty seats' reasons.
|
|
175
|
+
function lanes_section(
|
|
176
|
+
rows: ReadonlyArray<PlanLine>,
|
|
177
|
+
cells: LaneCells | undefined,
|
|
178
|
+
header: BoardHeader,
|
|
179
|
+
columns?: number,
|
|
180
|
+
): Array<PlanSection> {
|
|
181
|
+
const root = header.layout?.root
|
|
182
|
+
const root_count = root === undefined ? 0 : 1
|
|
183
|
+
const lanes = [...rows.slice(root_count), ...seat_lines(header, columns)]
|
|
184
|
+
|
|
185
|
+
return [
|
|
186
|
+
...rows_section(rows.slice(0, root_count), root_label(root?.name ?? ''), cells, header),
|
|
187
|
+
...rows_section(lanes, lane_label(header), cells, header),
|
|
188
|
+
]
|
|
189
|
+
}
|
|
190
|
+
|
|
174
191
|
const run_board_render_lanes = {
|
|
175
192
|
COLUMN_LABELS,
|
|
176
193
|
LANES_LABEL,
|
|
@@ -33,10 +33,18 @@ const ELAPSED_START =
|
|
|
33
33
|
// The column a lane row's usage cells start at, after the lead and one blank; the processes rows' too.
|
|
34
34
|
const USAGE_START = ELAPSED_START + TIME_WIDTH + SPACE
|
|
35
35
|
const LANES_LABEL = `${run_board_labels.LANES_ICON} lanes`
|
|
36
|
+
// A single run's rule (joshuafolkken/kit#7523): it runs in the root checkout, not in a lane, so its
|
|
37
|
+
// row stands under the command it runs rather than under the lanes.
|
|
38
|
+
const SINGLE_RUN_ICON = '🏠'
|
|
36
39
|
// Two leading rules, a space, and the separator before the first column.
|
|
37
40
|
const RULE_ROOM = 6
|
|
38
41
|
|
|
42
|
+
function root_label(run_name: string): string {
|
|
43
|
+
return `${SINGLE_RUN_ICON} ${run_name}`
|
|
44
|
+
}
|
|
45
|
+
|
|
39
46
|
function lane_label(header: BoardHeader): string {
|
|
47
|
+
if (header.run_name !== undefined) return root_label(header.run_name)
|
|
40
48
|
if (header.occupied_count === undefined) return LANES_LABEL
|
|
41
49
|
|
|
42
50
|
return `${LANES_LABEL} ${String(header.occupied_count)}/${String(header.lane_limit ?? '—')}`
|
|
@@ -102,6 +110,7 @@ const run_board_render_lead = {
|
|
|
102
110
|
indent_of,
|
|
103
111
|
lead_of,
|
|
104
112
|
lane_label,
|
|
113
|
+
root_label,
|
|
105
114
|
running_fresh,
|
|
106
115
|
}
|
|
107
116
|
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { run_board_layout, type BoardLayout, type BoardRow, type RowRead } from './run-board-layout'
|
|
2
|
+
import type { LocalRead } from './run-board-read'
|
|
3
|
+
import type { EndedRoot } from './run-board-single'
|
|
4
|
+
import { run_board_status, type ItemStatus } from './run-board-status'
|
|
5
|
+
|
|
6
|
+
// The root checkout's single runs beside a carried run. A running one is drawn under 🏠 above the lanes
|
|
7
|
+
// (joshuafolkken/kit#7524), its row read off its own span's events and kept out of `active`, so the
|
|
8
|
+
// header counts no lane and no 🔄 for it. One a merge ended inside the run leaves 🏠 and is listed in
|
|
9
|
+
// `✅ done` as a lane's finished row is, in its place by its end (joshuafolkken/kit#7525).
|
|
10
|
+
|
|
11
|
+
type RootRead = Pick<LocalRead, 'root' | 'ended_roots' | 'lane_states'>
|
|
12
|
+
|
|
13
|
+
function live_of(local: RootRead, read: RowRead): Pick<BoardLayout, 'root'> {
|
|
14
|
+
const { root } = local
|
|
15
|
+
|
|
16
|
+
if (root === undefined) return {}
|
|
17
|
+
const status = run_board_status.statuses_of(root.events, local.lane_states).get(root.issue)
|
|
18
|
+
|
|
19
|
+
if (status === undefined) return {}
|
|
20
|
+
const statuses = new Map([[root.issue, status]])
|
|
21
|
+
const [row] = run_board_layout.local_layout_of(statuses, undefined, read).active
|
|
22
|
+
|
|
23
|
+
return row === undefined ? {} : { root: { name: root.command, row } }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// A merge released the run's hold, so it ended merged.
|
|
27
|
+
function ended_status(run: EndedRoot): ItemStatus {
|
|
28
|
+
return { state: 'merged', started_ms: run.started_ms, ended_ms: run.ended_ms }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// The ended runs' rows among the run's own, any row of the same issue replaced.
|
|
32
|
+
function active_of(
|
|
33
|
+
active: ReadonlyArray<BoardRow>,
|
|
34
|
+
ended: ReadonlyArray<EndedRoot>,
|
|
35
|
+
read: RowRead,
|
|
36
|
+
): ReadonlyArray<BoardRow> {
|
|
37
|
+
if (ended.length === 0) return active
|
|
38
|
+
|
|
39
|
+
const statuses = new Map(ended.map((run) => [run.issue, ended_status(run)]))
|
|
40
|
+
const rows = run_board_layout.local_layout_of(statuses, undefined, read).active
|
|
41
|
+
const kept = active.filter((row) => !statuses.has(row.number))
|
|
42
|
+
|
|
43
|
+
return [...kept, ...rows].toSorted(run_board_layout.active_order)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function layout_of(layout: BoardLayout, local: RootRead, read: RowRead): BoardLayout {
|
|
47
|
+
return {
|
|
48
|
+
...layout,
|
|
49
|
+
active: active_of(layout.active, local.ended_roots ?? [], read),
|
|
50
|
+
...live_of(local, read),
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// The baseline the header counts arrivals against: the rows the ended runs add to the run's are no
|
|
55
|
+
// arrival to its plan.
|
|
56
|
+
function baseline_of(
|
|
57
|
+
baseline_total: number | undefined,
|
|
58
|
+
run_layout: BoardLayout,
|
|
59
|
+
layout: BoardLayout,
|
|
60
|
+
): number | undefined {
|
|
61
|
+
if (baseline_total === undefined) return undefined
|
|
62
|
+
const { rows_of } = run_board_layout
|
|
63
|
+
|
|
64
|
+
return baseline_total + rows_of(layout).length - rows_of(run_layout).length
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const run_board_root_layout = { baseline_of, layout_of }
|
|
68
|
+
|
|
69
|
+
export { run_board_root_layout }
|
|
@@ -6,7 +6,8 @@ import { stamped_memo } from '#scripts/lib/stamped-memo'
|
|
|
6
6
|
import { run_event_scope } from '#scripts/run/event/run-event-scope'
|
|
7
7
|
import { run_event_stream, type RunEvent } from '#scripts/run/event/run-event-stream'
|
|
8
8
|
import { run_hold, type HoldRead } from '#scripts/run/hold/run-hold'
|
|
9
|
-
import type
|
|
9
|
+
import { run_hold_ended, type EndedHold } from '#scripts/run/hold/run-hold-ended'
|
|
10
|
+
import { run_board_span, type BoardSpan } from './run-board-span'
|
|
10
11
|
|
|
11
12
|
// A single `fullrun #N` or `halfrun #N` has no carry record; the only record that says it is running
|
|
12
13
|
// here is this checkout's hold (`run-hold.ts`), so the board draws it from there — no store of its own
|
|
@@ -24,6 +25,15 @@ interface SingleRun {
|
|
|
24
25
|
issue: number
|
|
25
26
|
}
|
|
26
27
|
|
|
28
|
+
interface RootRun extends SingleRun {
|
|
29
|
+
events: ReadonlyArray<RunEvent>
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface EndedRoot extends SingleRun {
|
|
33
|
+
started_ms: number
|
|
34
|
+
ended_ms: number
|
|
35
|
+
}
|
|
36
|
+
|
|
27
37
|
// The checkout's own git directory, resolved once per working directory: resolving it runs a `git`
|
|
28
38
|
// subprocess the redraw would wait on, as `run-board-read.ts`'s main work tree does.
|
|
29
39
|
const directories = stamped_memo.create<Promise<string | undefined>>()
|
|
@@ -68,8 +78,8 @@ async function directory_here(): Promise<string | undefined> {
|
|
|
68
78
|
}
|
|
69
79
|
|
|
70
80
|
// The single run held in this checkout; `undefined` where none is. A lane's hold is its `backlogrun`
|
|
71
|
-
// child's, drawn as that run's row, so only outside a lane
|
|
72
|
-
// `fullrun` in
|
|
81
|
+
// child's, drawn as that run's row, so only outside a lane is a hold read beside the carry record — a
|
|
82
|
+
// `fullrun` in the root checkout beside a running `backlogrun`.
|
|
73
83
|
async function span_here(is_carried: boolean): Promise<BoardSpan | undefined> {
|
|
74
84
|
if (is_carried && lane_paths.lane_issue_of(process.cwd()) !== undefined) return undefined
|
|
75
85
|
|
|
@@ -80,6 +90,41 @@ async function span_here(is_carried: boolean): Promise<BoardSpan | undefined> {
|
|
|
80
90
|
return span_of(run_hold.read_hold(run_hold.hold_path(directory)))
|
|
81
91
|
}
|
|
82
92
|
|
|
93
|
+
// A run the root checkout's merge released, as `run-hold-ended.ts` kept it; `undefined` where its
|
|
94
|
+
// record names no issue or no moment.
|
|
95
|
+
function ended_of(ended: EndedHold): EndedRoot | undefined {
|
|
96
|
+
if (!issue_number_shape.is_issue_number(ended.issue)) return undefined
|
|
97
|
+
|
|
98
|
+
const started_ms = run_event_scope.moment_of(ended.taken_at)
|
|
99
|
+
const ended_ms = run_event_scope.moment_of(ended.ended_at)
|
|
100
|
+
|
|
101
|
+
if (started_ms === undefined || ended_ms === undefined) return undefined
|
|
102
|
+
|
|
103
|
+
const command = ended.is_fullrun === true ? FULLRUN : HALFRUN
|
|
104
|
+
|
|
105
|
+
return { command, issue: Number(ended.issue), started_ms, ended_ms }
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function is_inside(run: EndedRoot, span: BoardSpan): boolean {
|
|
109
|
+
return run.ended_ms >= span.started_ms && run.ended_ms <= (span.ended_ms ?? Infinity)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// The released runs that ended inside `span`, which the board lists in `✅ done` (joshuafolkken/kit#7525).
|
|
113
|
+
function ended_in(records: ReadonlyArray<EndedHold>, span: BoardSpan): Array<EndedRoot> {
|
|
114
|
+
return records
|
|
115
|
+
.map((record) => ended_of(record))
|
|
116
|
+
.filter((run): run is EndedRoot => run !== undefined && is_inside(run, span))
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// This checkout's released single runs inside `span`; none in a lane, where no merge records one.
|
|
120
|
+
async function ended_here(span: BoardSpan): Promise<Array<EndedRoot>> {
|
|
121
|
+
const directory = await directory_here()
|
|
122
|
+
|
|
123
|
+
if (directory === undefined) return []
|
|
124
|
+
|
|
125
|
+
return ended_in(run_hold_ended.read_ended(run_hold_ended.ended_path(directory)), span)
|
|
126
|
+
}
|
|
127
|
+
|
|
83
128
|
// The command and issue of a single run's invocation; `undefined` for any other run's.
|
|
84
129
|
function single_of(invocation: string): SingleRun | undefined {
|
|
85
130
|
const { command, issue } = SINGLE_INVOCATION.exec(invocation)?.groups ?? {}
|
|
@@ -118,6 +163,26 @@ function name_of(invocation: string): string | undefined {
|
|
|
118
163
|
return single_of(invocation)?.command
|
|
119
164
|
}
|
|
120
165
|
|
|
121
|
-
|
|
166
|
+
// The root checkout's single run beside a carried run, with its own span's events its row's phase is
|
|
167
|
+
// read from (joshuafolkken/kit#7524).
|
|
168
|
+
function root_of(span: BoardSpan, events: ReadonlyArray<RunEvent>): RootRun | undefined {
|
|
169
|
+
const single = single_of(span.invocation)
|
|
170
|
+
|
|
171
|
+
if (single === undefined) return undefined
|
|
172
|
+
|
|
173
|
+
return { ...single, events: events_of(span, run_board_span.events_of(events, span)) }
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const run_board_single = {
|
|
177
|
+
ended_here,
|
|
178
|
+
ended_in,
|
|
179
|
+
events_of,
|
|
180
|
+
name_of,
|
|
181
|
+
root_of,
|
|
182
|
+
scope_of,
|
|
183
|
+
span_here,
|
|
184
|
+
span_of,
|
|
185
|
+
}
|
|
122
186
|
|
|
123
187
|
export { run_board_single }
|
|
188
|
+
export type { EndedRoot, RootRun }
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { backlogrun_launch } from '#scripts/backlog/backlogrun-launch'
|
|
2
|
+
import type { TableRead } from '#scripts/lane/lane-process-table'
|
|
3
|
+
import { run_carry, type CarryRead, type RunCarry } from '#scripts/run/carry/run-carry'
|
|
4
|
+
import { run_carry_ended } from '#scripts/run/carry/run-carry-ended'
|
|
5
|
+
import { run_stranded_detect } from '#scripts/run/run-stranded-detect'
|
|
6
|
+
import type { ProcessRoots } from './run-board-process-groups'
|
|
7
|
+
import { run_board_roots } from './run-board-roots'
|
|
8
|
+
import { run_board_single } from './run-board-single'
|
|
9
|
+
import { run_board_span, type BoardSpan } from './run-board-span'
|
|
10
|
+
|
|
11
|
+
// What `run:board` reads of the run drawn here: its span, the session it resumes from and the
|
|
12
|
+
// processes its record names.
|
|
13
|
+
interface SpanHere {
|
|
14
|
+
span: BoardSpan | undefined
|
|
15
|
+
resume: string | undefined
|
|
16
|
+
// The parent session's transcript, where the run's record names one.
|
|
17
|
+
transcript: string | undefined
|
|
18
|
+
roots: ProcessRoots
|
|
19
|
+
carry: RunCarry | undefined
|
|
20
|
+
is_stranded: boolean
|
|
21
|
+
is_session_active: boolean
|
|
22
|
+
// The root checkout's single run beside the carried one.
|
|
23
|
+
root?: BoardSpan | undefined
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const NO_SPAN: SpanHere = {
|
|
27
|
+
span: undefined,
|
|
28
|
+
resume: undefined,
|
|
29
|
+
transcript: undefined,
|
|
30
|
+
roots: {},
|
|
31
|
+
carry: undefined,
|
|
32
|
+
is_stranded: false,
|
|
33
|
+
is_session_active: false,
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Every process the run's record names is asked of `table`, the redraw's own `ps` snapshot — only once
|
|
37
|
+
// a run is found, so a repository with no run waits on no `ps`.
|
|
38
|
+
async function recorded_span_here(
|
|
39
|
+
repository: string,
|
|
40
|
+
table: TableRead | undefined,
|
|
41
|
+
read: CarryRead,
|
|
42
|
+
): Promise<SpanHere> {
|
|
43
|
+
const ended = run_carry_ended.read_ended(run_carry_ended.ended_path(repository))
|
|
44
|
+
const launch = backlogrun_launch.pending_launch(repository)
|
|
45
|
+
const span = run_board_span.span_of(read, ended, launch)
|
|
46
|
+
|
|
47
|
+
if (span === undefined) return NO_SPAN
|
|
48
|
+
|
|
49
|
+
const same_process = await run_board_roots.same_in(table)
|
|
50
|
+
const stranded_ports = run_stranded_detect.ports_with(same_process)
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
span,
|
|
54
|
+
resume: run_board_span.resume_of(read, ended, launch),
|
|
55
|
+
transcript: read.kind === 'carried' ? read.carry.owner_transcript : undefined,
|
|
56
|
+
roots: run_board_roots.roots_of(repository, read, same_process),
|
|
57
|
+
is_session_active: run_board_roots.session_is_active(repository, read, same_process),
|
|
58
|
+
carry: read.kind === 'carried' ? read.carry : undefined,
|
|
59
|
+
is_stranded: run_stranded_detect.is_stranded_at(repository, stranded_ports),
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// A single run this checkout holds is the one drawn (`run-board-single.ts`) before the ended run; it
|
|
64
|
+
// names no process. Beside a carried run it is the root checkout's, drawn as a row above the carried
|
|
65
|
+
// run's lanes, which stays the frame (joshuafolkken/kit#7524). No repository, no run.
|
|
66
|
+
async function span_here(
|
|
67
|
+
repository: string | undefined,
|
|
68
|
+
table: TableRead | undefined,
|
|
69
|
+
): Promise<SpanHere> {
|
|
70
|
+
if (repository === undefined) return NO_SPAN
|
|
71
|
+
const read = run_carry.read_carry(run_carry.carry_path(repository))
|
|
72
|
+
const is_carried = read.kind !== 'none'
|
|
73
|
+
const single = await run_board_single.span_here(is_carried)
|
|
74
|
+
|
|
75
|
+
if (single === undefined) return await recorded_span_here(repository, table, read)
|
|
76
|
+
const recorded = is_carried ? await recorded_span_here(repository, table, read) : NO_SPAN
|
|
77
|
+
|
|
78
|
+
// A carried record that reads no span leaves the single run the one drawn.
|
|
79
|
+
return recorded.span === undefined ? { ...NO_SPAN, span: single } : { ...recorded, root: single }
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const run_board_span_read = { span_here }
|
|
83
|
+
|
|
84
|
+
export { run_board_span_read }
|
|
85
|
+
export type { SpanHere }
|
|
@@ -13,6 +13,7 @@ import type { LocalRead } from './run-board-read'
|
|
|
13
13
|
import { run_board_reason, type ReasonRead } from './run-board-reason'
|
|
14
14
|
import { run_board_released } from './run-board-released'
|
|
15
15
|
import { run_board_render } from './run-board-render'
|
|
16
|
+
import { run_board_root_layout } from './run-board-root-layout'
|
|
16
17
|
import { run_board_seat, type SeatRead } from './run-board-seat'
|
|
17
18
|
import { run_board_setup } from './run-board-setup'
|
|
18
19
|
import type { Known } from './run-board-snapshot'
|
|
@@ -321,13 +322,20 @@ function draw(ports: BoardPorts, lines: ReadonlyArray<string>): void {
|
|
|
321
322
|
}
|
|
322
323
|
|
|
323
324
|
// The first total the plan gives is the baseline every later arrival is counted against; the run's own
|
|
324
|
-
// children before it are no total.
|
|
325
|
+
// children before it are no total. The root checkout's single runs are laid over the run's.
|
|
325
326
|
function draw_run(state: BoardState, local: LocalRead, redraw: Redraw): BoardState {
|
|
326
|
-
const
|
|
327
|
+
const read = closed_under(known_of(state), state.closed)
|
|
328
|
+
const run_layout = layout_for(state, local, redraw.now_ms)
|
|
329
|
+
const layout = run_board_root_layout.layout_of(run_layout, local, read)
|
|
327
330
|
const first_total =
|
|
328
|
-
state.plan === undefined ? undefined : run_board_header.counts_of(
|
|
331
|
+
state.plan === undefined ? undefined : run_board_header.counts_of(run_layout).total
|
|
329
332
|
const settled = { ...state, baseline_total: state.baseline_total ?? first_total }
|
|
330
|
-
const
|
|
333
|
+
const baseline_total = run_board_root_layout.baseline_of(
|
|
334
|
+
settled.baseline_total,
|
|
335
|
+
run_layout,
|
|
336
|
+
layout,
|
|
337
|
+
)
|
|
338
|
+
const { lines, spots } = frame_of({ ...settled, baseline_total }, local, layout, redraw)
|
|
331
339
|
|
|
332
340
|
draw(redraw.ports, lines)
|
|
333
341
|
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { stamp_file } from '#scripts/josh/stamp-file'
|
|
2
|
+
import { json_value } from '#scripts/lib/json-value'
|
|
3
|
+
import { z } from 'zod'
|
|
4
|
+
import type { HoldRead } from './run-hold'
|
|
5
|
+
|
|
6
|
+
// The runs whose hold a merge released here. A single `fullrun` or `halfrun` in the root checkout has
|
|
7
|
+
// no record but its hold (`run-board-single.ts`), and the release removes it, so without this
|
|
8
|
+
// `run:board` would lose the run the moment it merged; this keeps when each began and ended for the
|
|
9
|
+
// board's `✅ done` (joshuafolkken/kit#7525). The newest few only: the board reads the ones that ended
|
|
10
|
+
// inside the run on screen.
|
|
11
|
+
|
|
12
|
+
const ENDED_PREFIX = 'josh-run-hold-ended-'
|
|
13
|
+
const KEPT_RUNS = 20
|
|
14
|
+
|
|
15
|
+
const ended_hold_schema = z.object({
|
|
16
|
+
issue: z.string(),
|
|
17
|
+
is_fullrun: z.boolean().optional(),
|
|
18
|
+
taken_at: z.string(),
|
|
19
|
+
ended_at: z.string(),
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
type EndedHold = z.infer<typeof ended_hold_schema>
|
|
23
|
+
|
|
24
|
+
// Keyed like the hold, on the work tree's own git directory.
|
|
25
|
+
function ended_path(git_directory: string): string {
|
|
26
|
+
return stamp_file.stamp_path(ENDED_PREFIX, git_directory)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// None where no run has ended here, or the record does not read.
|
|
30
|
+
function read_ended(target: string): ReadonlyArray<EndedHold> {
|
|
31
|
+
const raw = stamp_file.read_stamp_text(target)
|
|
32
|
+
|
|
33
|
+
return raw === undefined ? [] : (json_value.parse_with(raw, z.array(ended_hold_schema)) ?? [])
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Records the hold a merge is about to release; a read with no live hold names no run.
|
|
37
|
+
function record_ended(target: string, read: HoldRead, now: Date = new Date()): void {
|
|
38
|
+
if (read.kind !== 'held') return
|
|
39
|
+
|
|
40
|
+
const { issue, is_fullrun, taken_at } = read.hold
|
|
41
|
+
const ended: EndedHold = { issue, taken_at, ended_at: now.toISOString() }
|
|
42
|
+
|
|
43
|
+
if (is_fullrun === true) ended.is_fullrun = true
|
|
44
|
+
stamp_file.replace_stamp(target, [...read_ended(target), ended].slice(-KEPT_RUNS))
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const run_hold_ended = { KEPT_RUNS, ended_path, read_ended, record_ended }
|
|
48
|
+
|
|
49
|
+
export { run_hold_ended }
|
|
50
|
+
export type { EndedHold }
|