@joshuafolkken/kit 1.1383.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/dist/hooks/batch-guard.js +6 -6
- package/dist/hooks/{chunk-4MGHBTOM.js → chunk-2YSW3CXH.js} +8 -2
- package/dist/hooks/{chunk-KCF2YE7O.js → chunk-DCY2GWHC.js} +5 -5
- package/dist/hooks/{chunk-KC6LUZZ2.js → chunk-EAQEGME7.js} +1 -1
- package/dist/hooks/{chunk-LC56R53A.js → chunk-EBSHN25W.js} +3 -3
- package/dist/hooks/{chunk-6MAW7BCM.js → chunk-GJ7JPIHN.js} +14 -14
- package/dist/hooks/{chunk-6AC4C2IO.js → chunk-GNDJ7NJ7.js} +59 -462
- package/dist/hooks/{chunk-LOJTBU5V.js → chunk-IYVOLN6A.js} +3 -3
- package/dist/hooks/{chunk-Y5R47QHU.js → chunk-J3KSS7II.js} +1 -1
- package/dist/hooks/{chunk-RYP3TSUJ.js → chunk-LMWBRJIP.js} +3 -3
- package/dist/hooks/{chunk-TT742SIO.js → chunk-SL4GQ2XP.js} +2 -2
- package/dist/hooks/{chunk-YKUITFT7.js → chunk-STDSEPWU.js} +2 -2
- package/dist/hooks/{chunk-2X2ART5G.js → chunk-U32VSRNG.js} +420 -4
- package/dist/hooks/{chunk-7VSZ2N4B.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 +208 -206
- 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 +91 -23
- package/dist/josh.js +1 -1
- package/package.json +1 -1
- package/scripts/backlog/backlog-drive-cli.ts +2 -4
- package/scripts/backlog/backlog-drive-launch.ts +2 -0
- package/scripts/backlog/backlog-drive-owner.ts +72 -5
- package/scripts/backlog/backlogrun-launch.ts +56 -5
- package/scripts/document/document-byte-budget.ts +1 -1
- 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-project.ts +1 -1
- package/scripts/run/board/run-board-header.ts +39 -13
- package/scripts/run/board/run-board-read.ts +9 -2
- package/scripts/run/board/run-board-setup.ts +73 -0
- package/scripts/run/board/run-board-span.ts +49 -7
- package/scripts/run/board/run-board-status.ts +10 -6
- package/scripts/run/board/run-board-tick.ts +10 -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 +37 -7
- package/scripts/run/carry/run-carry-ended.ts +9 -1
- package/scripts/run/carry/run-carry.ts +16 -0
- package/scripts/run/event/run-event-render.ts +1 -0
- package/scripts/run/event/run-event-setup.ts +47 -0
- package/scripts/run/event/run-event-stream.ts +26 -5
- package/scripts/run/run-stranded-detect.ts +58 -12
- package/scripts/run/wake/run-wake-cli.ts +51 -6
- package/scripts/run/wake/run-wake-describe.ts +2 -0
- package/scripts/run/wake/run-wake-driver.ts +10 -1
- package/scripts/run/wake/run-wake-handoff.ts +1 -1
|
@@ -18,6 +18,7 @@ interface StepContext {
|
|
|
18
18
|
profile: ProjectProfile
|
|
19
19
|
visibility: Visibility
|
|
20
20
|
init_command: string | undefined
|
|
21
|
+
push_preflight_command: string | undefined
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
interface RunContext extends StepContext {
|
|
@@ -69,6 +70,13 @@ function self_run_refusal(root: string): string | undefined {
|
|
|
69
70
|
return self_sync_guard.self_sync_refusal(PACKAGE_DIR, root)
|
|
70
71
|
}
|
|
71
72
|
|
|
73
|
+
// A caller's command line runs without a shell, split the way execa splits one.
|
|
74
|
+
function run_command_line(command_line: string, extra: ReadonlyArray<string>, root: string): void {
|
|
75
|
+
const [command = '', ...args] = parseCommandString(command_line)
|
|
76
|
+
|
|
77
|
+
run_local(command, [...args, ...extra], root)
|
|
78
|
+
}
|
|
79
|
+
|
|
72
80
|
function create_git_repository(context: StepContext): void {
|
|
73
81
|
initial_commit.create_repository(context.root)
|
|
74
82
|
}
|
|
@@ -84,9 +92,7 @@ function initialize(context: StepContext): void {
|
|
|
84
92
|
return
|
|
85
93
|
}
|
|
86
94
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
run_local(command, [...args, '--profile', context.profile], context.root)
|
|
95
|
+
run_command_line(context.init_command, ['--profile', context.profile], context.root)
|
|
90
96
|
}
|
|
91
97
|
|
|
92
98
|
function commit_all(context: StepContext): void {
|
|
@@ -122,6 +128,26 @@ function baseline_of(
|
|
|
122
128
|
return start_setup_pr.changed_paths(context.root)
|
|
123
129
|
}
|
|
124
130
|
|
|
131
|
+
// A toolkit's own pre-push hook can need something kit knows nothing about (app-kit's needs a Docker
|
|
132
|
+
// daemon), and a push it refuses lands after `gh repo create` has made the repository. So the caller
|
|
133
|
+
// names a check that runs only when the plan pushes, before the first step: a run that does not push
|
|
134
|
+
// is never stopped by it, and one it stops has written nothing (#3702).
|
|
135
|
+
function run_push_preflight(steps: ReadonlyArray<StartStep>, context: StepContext): void {
|
|
136
|
+
const command_line = context.push_preflight_command
|
|
137
|
+
|
|
138
|
+
if (command_line === undefined || !start_plan.has_push(steps)) return
|
|
139
|
+
|
|
140
|
+
console.info(`\nPush preflight: ${command_line}`)
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
run_command_line(command_line, [], context.root)
|
|
144
|
+
} catch (error) {
|
|
145
|
+
const cause = error_text.message_of(error)
|
|
146
|
+
|
|
147
|
+
throw new Error(start_plan.preflight_report(command_line, cause), { cause: error })
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
125
151
|
type StepAction = (context: RunContext) => void | Promise<void>
|
|
126
152
|
|
|
127
153
|
const STEP_ACTIONS: Readonly<Record<StartStep, StepAction>> = {
|
|
@@ -150,6 +176,8 @@ async function run_step(
|
|
|
150
176
|
|
|
151
177
|
async function run_steps(steps: ReadonlyArray<StartStep>, context: StepContext): Promise<void> {
|
|
152
178
|
const labels = start_plan.step_labels(context.init_command)
|
|
179
|
+
|
|
180
|
+
run_push_preflight(steps, context)
|
|
153
181
|
const run_context: RunContext = { ...context, baseline: baseline_of(steps, context) }
|
|
154
182
|
|
|
155
183
|
for (const [index, step] of steps.entries()) {
|
package/scripts/init/start.ts
CHANGED
|
@@ -15,8 +15,9 @@ import { start_steps } from './start-steps'
|
|
|
15
15
|
const ARGUMENT_START_INDEX = 2
|
|
16
16
|
const READY_MESSAGE = 'GitHub workflow ready. Use kickoff new in your assistant to plan an Issue.'
|
|
17
17
|
const START_USAGE =
|
|
18
|
-
'josh start [--profile basic|full] [--yes] [--github] [--public] [--init-command <command>]'
|
|
18
|
+
'josh start [--profile basic|full] [--yes] [--github] [--public] [--init-command <command>] [--push-preflight-command <command>]'
|
|
19
19
|
const INIT_COMMAND_FLAG = '--init-command'
|
|
20
|
+
const PUSH_PREFLIGHT_COMMAND_FLAG = '--push-preflight-command'
|
|
20
21
|
const YES_FLAG = '--yes'
|
|
21
22
|
const GITHUB_FLAG = '--github'
|
|
22
23
|
const PUBLIC_FLAG = '--public'
|
|
@@ -28,34 +29,38 @@ interface Prepared {
|
|
|
28
29
|
choices: Choices
|
|
29
30
|
}
|
|
30
31
|
|
|
31
|
-
interface
|
|
32
|
-
|
|
32
|
+
interface CommandSplit {
|
|
33
|
+
command: string | undefined
|
|
33
34
|
rest: ReadonlyArray<string>
|
|
34
35
|
}
|
|
35
36
|
|
|
36
37
|
// A value that is blank or another flag means the command itself was left out, which would otherwise
|
|
37
|
-
// swallow the next switch or hand
|
|
38
|
+
// swallow the next switch or hand its step nothing to run.
|
|
38
39
|
function is_command_value(value: string | undefined): value is string {
|
|
39
40
|
return value !== undefined && value.trim() !== '' && !value.startsWith('-')
|
|
40
41
|
}
|
|
41
42
|
|
|
42
43
|
// The command arrives as the one argument after its flag and is removed together with it.
|
|
43
|
-
function
|
|
44
|
-
const index = args.indexOf(
|
|
44
|
+
function split_command(args: ReadonlyArray<string>, flag: string): CommandSplit {
|
|
45
|
+
const index = args.indexOf(flag)
|
|
45
46
|
|
|
46
|
-
if (index === -1) return {
|
|
47
|
+
if (index === -1) return { command: undefined, rest: args }
|
|
47
48
|
|
|
48
49
|
const value = args[index + 1]
|
|
49
50
|
|
|
50
51
|
if (!is_command_value(value)) throw new Error(`Usage: ${START_USAGE}`)
|
|
51
52
|
|
|
52
|
-
return {
|
|
53
|
+
return { command: value, rest: args.filter((_, at) => at !== index && at !== index + 1) }
|
|
53
54
|
}
|
|
54
55
|
|
|
55
|
-
// The switches and the
|
|
56
|
+
// The switches and the caller's commands are peeled off first, so what remains is exactly the
|
|
56
57
|
// `--profile` pair `josh init` accepts — one parser for the profile in both commands.
|
|
57
58
|
function parse_start_options(args: ReadonlyArray<string>): StartOptions {
|
|
58
|
-
const
|
|
59
|
+
const init = split_command(args, INIT_COMMAND_FLAG)
|
|
60
|
+
const { command: push_preflight_command, rest } = split_command(
|
|
61
|
+
init.rest,
|
|
62
|
+
PUSH_PREFLIGHT_COMMAND_FLAG,
|
|
63
|
+
)
|
|
59
64
|
const profile_args = rest.filter((argument) => !SWITCHES.has(argument))
|
|
60
65
|
|
|
61
66
|
return {
|
|
@@ -63,7 +68,8 @@ function parse_start_options(args: ReadonlyArray<string>): StartOptions {
|
|
|
63
68
|
is_yes: rest.includes(YES_FLAG),
|
|
64
69
|
is_github: rest.includes(GITHUB_FLAG),
|
|
65
70
|
visibility: rest.includes(PUBLIC_FLAG) ? 'public' : 'private',
|
|
66
|
-
init_command,
|
|
71
|
+
init_command: init.command,
|
|
72
|
+
push_preflight_command,
|
|
67
73
|
}
|
|
68
74
|
}
|
|
69
75
|
|
|
@@ -112,12 +118,13 @@ async function main(args: ReadonlyArray<string>, is_tty: boolean): Promise<void>
|
|
|
112
118
|
const { steps, interaction, choices } = prepare(args, is_tty, process.cwd())
|
|
113
119
|
|
|
114
120
|
const { root, options } = choices
|
|
115
|
-
const { visibility, init_command } = options
|
|
121
|
+
const { visibility, init_command, push_preflight_command } = options
|
|
116
122
|
|
|
117
123
|
console.info(start_plan.plan_summary(steps, start_plan.step_labels(init_command)))
|
|
118
124
|
const profile = await resolve_profile(choices, interaction)
|
|
125
|
+
const context = { root, profile, visibility, init_command, push_preflight_command }
|
|
119
126
|
|
|
120
|
-
await start_steps.run_steps(steps,
|
|
127
|
+
await start_steps.run_steps(steps, context)
|
|
121
128
|
console.info(`\n${READY_MESSAGE}`)
|
|
122
129
|
}
|
|
123
130
|
|
|
@@ -17,7 +17,7 @@ const PROJECT_COMMANDS: Record<string, CommandEntry> = {
|
|
|
17
17
|
description: 'Set a project up for the GitHub Issue workflow, from git init to the setup PR',
|
|
18
18
|
category: 'Project',
|
|
19
19
|
reference: [
|
|
20
|
-
`${PROFILE_ARGUMENTS} [--yes] [--github] [--public] [--init-command <command>]`,
|
|
20
|
+
`${PROFILE_ARGUMENTS} [--yes] [--github] [--public] [--init-command <command>] [--push-preflight-command <command>]`,
|
|
21
21
|
'developer',
|
|
22
22
|
['files', 'git', 'network', 'processes'],
|
|
23
23
|
],
|
|
@@ -74,13 +74,15 @@ interface BoardCounts {
|
|
|
74
74
|
remaining: number
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
|
|
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'
|
|
78
79
|
|
|
79
80
|
type TextColor = Parameters<typeof styleText>[0]
|
|
80
81
|
|
|
81
82
|
// The run's state as a colored glyph before its name.
|
|
82
83
|
const RUN_MARKS: Readonly<Record<RunMark, { glyph: string; color: TextColor }>> = {
|
|
83
84
|
running: { glyph: '▶', color: 'green' },
|
|
85
|
+
setup: { glyph: '▶', color: 'green' },
|
|
84
86
|
idle: { glyph: '⏸', color: 'yellow' },
|
|
85
87
|
human: { glyph: '✋', color: 'magenta' },
|
|
86
88
|
stopped: { glyph: '■', color: 'gray' },
|
|
@@ -99,23 +101,33 @@ function counts_of(layout: BoardLayout): BoardCounts {
|
|
|
99
101
|
return { total, settled, parked, running, remaining: total - settled - running }
|
|
100
102
|
}
|
|
101
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
|
+
|
|
102
110
|
// Nothing running: the run is waiting on a person when all the plan has left is what
|
|
103
|
-
// `needs-decision` holds,
|
|
104
|
-
function quiet_mark(
|
|
105
|
-
|
|
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'
|
|
106
118
|
}
|
|
107
119
|
|
|
108
120
|
// The run's state, each state overriding the ones below it.
|
|
109
121
|
function mark_of(header: BoardHeader, running: number): RunMark {
|
|
110
122
|
if (header.activity.is_stopped) return 'stopped'
|
|
111
123
|
|
|
112
|
-
return running > 0 ? 'running' : quiet_mark(header
|
|
124
|
+
return running > 0 ? 'running' : quiet_mark(header)
|
|
113
125
|
}
|
|
114
126
|
|
|
115
|
-
// A
|
|
127
|
+
// A working run turns the spinner in place of ▶, both one column wide; every other mark stands still.
|
|
116
128
|
function run_part(mark: RunMark, spinner: string | undefined): string {
|
|
117
129
|
const { glyph, color } = RUN_MARKS[mark]
|
|
118
|
-
const shown = mark
|
|
130
|
+
const shown = WORKING_MARKS.has(mark) ? (spinner ?? glyph) : glyph
|
|
119
131
|
|
|
120
132
|
return styleText(color, `${shown} ${RUN_NAME}`)
|
|
121
133
|
}
|
|
@@ -153,7 +165,7 @@ function ended_part(header: BoardHeader): string {
|
|
|
153
165
|
}
|
|
154
166
|
|
|
155
167
|
// The `💓` under the run's age, in the `⏰`'s own format with its digits right-aligned to `width` —
|
|
156
|
-
// only a
|
|
168
|
+
// only a working run can be stale, so only it is colored. Only an ended run draws anything else there:
|
|
157
169
|
// the minute it ended.
|
|
158
170
|
function beat_part(header: BoardHeader, mark: RunMark, width: number): string {
|
|
159
171
|
const age = age_of(header)
|
|
@@ -162,7 +174,7 @@ function beat_part(header: BoardHeader, mark: RunMark, width: number): string {
|
|
|
162
174
|
|
|
163
175
|
const part = `${HEADER_ICONS.heartbeat} ${span_of(age).padStart(width)}`
|
|
164
176
|
|
|
165
|
-
return mark
|
|
177
|
+
return WORKING_MARKS.has(mark) ? aged(part, age) : part
|
|
166
178
|
}
|
|
167
179
|
|
|
168
180
|
// A run's age over the `💓`, or over the minute an ended run ended: the shorter of the two lengths is
|
|
@@ -243,9 +255,23 @@ function progress_parts(counts: BoardCounts, header: BoardHeader): Array<string>
|
|
|
243
255
|
]
|
|
244
256
|
}
|
|
245
257
|
|
|
246
|
-
// The run
|
|
247
|
-
|
|
248
|
-
|
|
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
|
|
263
|
+
|
|
264
|
+
if (setup === undefined || mark !== 'setup') return undefined
|
|
265
|
+
if (!is_plan_read(header)) return setup
|
|
266
|
+
|
|
267
|
+
return `${setup}${STAGE_SEPARATOR}${String(counts.total)} ${PLANNED}`
|
|
268
|
+
}
|
|
269
|
+
|
|
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)]
|
|
249
275
|
|
|
250
276
|
return parts.filter((part) => part !== undefined).join(GAP)
|
|
251
277
|
}
|
|
@@ -286,7 +312,7 @@ function header_lines(header: BoardHeader): Array<string> {
|
|
|
286
312
|
const columns = [
|
|
287
313
|
column_of([run_part(mark, header.spinner), repo_part(header, mark)]),
|
|
288
314
|
column_of(time_parts(header, mark)),
|
|
289
|
-
[progress_part(counts, header), machine_part(counts, header)],
|
|
315
|
+
[progress_part(counts, header, mark), machine_part(counts, header)],
|
|
290
316
|
]
|
|
291
317
|
|
|
292
318
|
return [0, 1].map((line) => columns.map((column) => column[line]).join(GAP))
|
|
@@ -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
|
|
|
@@ -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 }
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { PendingLaunch } from '#scripts/backlog/backlogrun-launch'
|
|
1
2
|
import type { CarryRead } from '#scripts/run/carry/run-carry'
|
|
2
3
|
import type { EndedRun } from '#scripts/run/carry/run-carry-ended'
|
|
3
4
|
import { run_event_scope } from '#scripts/run/event/run-event-scope'
|
|
@@ -27,9 +28,40 @@ function ended_span(ended: EndedRun | undefined): BoardSpan | undefined {
|
|
|
27
28
|
return { invocation: ended.invocation, started_ms, ended_ms }
|
|
28
29
|
}
|
|
29
30
|
|
|
31
|
+
// A run `josh backlogrun` launched and its parent has not claimed yet: it has no carry record, so the
|
|
32
|
+
// launch record is the only one that says a run is here and since when.
|
|
33
|
+
function launch_span(launch: PendingLaunch | undefined): BoardSpan | undefined {
|
|
34
|
+
const started_ms = launch === undefined ? undefined : moment_of(launch.started_at)
|
|
35
|
+
|
|
36
|
+
if (launch === undefined || started_ms === undefined) return undefined
|
|
37
|
+
|
|
38
|
+
return { invocation: launch.invocation, started_ms, ended_ms: undefined }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// With no carry record: the run on its way, else the one that last ended. **A launch older than that
|
|
42
|
+
// end is the ended run's own** — its parent outlives `run:carry --end` for a moment — so it never
|
|
43
|
+
// redraws a finished run as one starting.
|
|
44
|
+
function idle_span(
|
|
45
|
+
ended: EndedRun | undefined,
|
|
46
|
+
launch: PendingLaunch | undefined,
|
|
47
|
+
): BoardSpan | undefined {
|
|
48
|
+
const last = ended_span(ended)
|
|
49
|
+
const coming = launch_span(launch)
|
|
50
|
+
|
|
51
|
+
if (coming === undefined) return last
|
|
52
|
+
|
|
53
|
+
return (last?.ended_ms ?? 0) >= coming.started_ms ? last : coming
|
|
54
|
+
}
|
|
55
|
+
|
|
30
56
|
// The running run wins over the ended one, so a run that begins replaces the finished board at once.
|
|
31
|
-
|
|
32
|
-
|
|
57
|
+
// **The launch's start is the carry's**: `run:carry --begin` inherits it for the parent the launch
|
|
58
|
+
// started, so the clock does not restart when the record appears.
|
|
59
|
+
function span_of(
|
|
60
|
+
read: CarryRead,
|
|
61
|
+
ended: EndedRun | undefined,
|
|
62
|
+
launch?: PendingLaunch,
|
|
63
|
+
): BoardSpan | undefined {
|
|
64
|
+
if (read.kind !== 'carried') return read.kind === 'none' ? idle_span(ended, launch) : undefined
|
|
33
65
|
|
|
34
66
|
const started_ms = moment_of(read.carry.started_at)
|
|
35
67
|
|
|
@@ -38,12 +70,22 @@ function span_of(read: CarryRead, ended: EndedRun | undefined): BoardSpan | unde
|
|
|
38
70
|
return { invocation: read.carry.invocation, started_ms, ended_ms: undefined }
|
|
39
71
|
}
|
|
40
72
|
|
|
41
|
-
//
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
73
|
+
// Whether the span drawn with no carry record is the ended run's, rather than a launch's.
|
|
74
|
+
function is_ended_drawn(ended: EndedRun | undefined, launch: PendingLaunch | undefined): boolean {
|
|
75
|
+
return idle_span(ended, launch)?.ended_ms !== undefined
|
|
76
|
+
}
|
|
45
77
|
|
|
46
|
-
|
|
78
|
+
// The session a person resumes a stopped run from: only while the ended run is
|
|
79
|
+
// the one drawn, and only when it stopped rather than finished. A run that begins — or is on its way —
|
|
80
|
+
// hides it at once.
|
|
81
|
+
function resume_of(
|
|
82
|
+
read: CarryRead,
|
|
83
|
+
ended: EndedRun | undefined,
|
|
84
|
+
launch?: PendingLaunch,
|
|
85
|
+
): string | undefined {
|
|
86
|
+
if (read.kind !== 'none' || !is_ended_drawn(ended, launch)) return undefined
|
|
87
|
+
|
|
88
|
+
return ended?.stopped === undefined ? undefined : ended.session
|
|
47
89
|
}
|
|
48
90
|
|
|
49
91
|
function is_within(event: RunEvent, span: BoardSpan): boolean {
|
|
@@ -7,6 +7,7 @@ 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 type { ClosedIssue } from './run-board-closed'
|
|
9
9
|
import { run_board_phase, type Phase } from './run-board-phase'
|
|
10
|
+
import { run_board_setup, type SetupRead } from './run-board-setup'
|
|
10
11
|
|
|
11
12
|
// What each issue of a run is doing now, read from the run's own event stream and its open lanes.
|
|
12
13
|
// Local reads only — no `gh` call — so `run:board` can redraw it every few seconds; the plan these
|
|
@@ -26,8 +27,9 @@ interface ItemStatus {
|
|
|
26
27
|
track?: ReadonlyArray<Phase> | undefined
|
|
27
28
|
}
|
|
28
29
|
|
|
29
|
-
// The run as a whole, read off the same stream.
|
|
30
|
-
|
|
30
|
+
// The run as a whole, read off the same stream. The setup half is `run-board-setup.ts`'s, absent
|
|
31
|
+
// where a stream carries no stage.
|
|
32
|
+
interface RunActivity extends Partial<SetupRead> {
|
|
31
33
|
last_event_ms: number | undefined
|
|
32
34
|
// The run's last position is a `stop` that names no child — the run itself ended.
|
|
33
35
|
is_stopped: boolean
|
|
@@ -125,16 +127,17 @@ function fold_events(events: ReadonlyArray<RunEvent>): Map<number, ItemStatus> {
|
|
|
125
127
|
function is_running_lane(
|
|
126
128
|
issue: number,
|
|
127
129
|
status: ItemStatus | undefined,
|
|
130
|
+
events: ReadonlyArray<RunEvent>,
|
|
128
131
|
in_run: ReadonlySet<number>,
|
|
129
132
|
): boolean {
|
|
130
|
-
if (status
|
|
133
|
+
if (status !== undefined) return status.state === 'running'
|
|
131
134
|
|
|
132
|
-
return
|
|
135
|
+
return in_run.has(issue) && !run_board_setup.is_lane_pending(events, issue)
|
|
133
136
|
}
|
|
134
137
|
|
|
135
138
|
// Every child the run has touched, with the lane each running child holds. A lane the stream never saw
|
|
136
139
|
// launched — a run restored after the stream rolled — still reads as running while its issue is in the
|
|
137
|
-
// run's plan (`in_run`).
|
|
140
|
+
// run's plan (`in_run`); one the run is still setting up for stays waiting until its launch.
|
|
138
141
|
function statuses_of(
|
|
139
142
|
events: ReadonlyArray<RunEvent>,
|
|
140
143
|
lanes: ReadonlyArray<string>,
|
|
@@ -146,7 +149,7 @@ function statuses_of(
|
|
|
146
149
|
const issue = Number(lane)
|
|
147
150
|
const status = statuses.get(issue)
|
|
148
151
|
|
|
149
|
-
if (is_running_lane(issue, status, in_run)) {
|
|
152
|
+
if (is_running_lane(issue, status, events, in_run)) {
|
|
150
153
|
statuses.set(issue, { state: 'running', ...status, lane: `lane ${lane}` })
|
|
151
154
|
}
|
|
152
155
|
}
|
|
@@ -251,6 +254,7 @@ function activity_of(events: ReadonlyArray<RunEvent>, is_ended = false): RunActi
|
|
|
251
254
|
const position = events.findLast((event) => !run_event_stream.TRACE_KINDS.has(event.kind))
|
|
252
255
|
|
|
253
256
|
return {
|
|
257
|
+
...run_board_setup.read(events),
|
|
254
258
|
last_event_ms: last === undefined ? undefined : moment(last),
|
|
255
259
|
is_stopped: is_ended || is_run_stop(position),
|
|
256
260
|
}
|
|
@@ -11,7 +11,7 @@ import type { LocalRead } from './run-board-read'
|
|
|
11
11
|
import { run_board_render, type FrameBounds } from './run-board-render'
|
|
12
12
|
import { run_board_spin, type Spun } from './run-board-spin'
|
|
13
13
|
import { run_board_state, type BoardPorts, type BoardState, type Pace } from './run-board-state'
|
|
14
|
-
import { run_board_status } from './run-board-status'
|
|
14
|
+
import { run_board_status, type RunActivity } from './run-board-status'
|
|
15
15
|
import { run_board_usage } from './run-board-usage'
|
|
16
16
|
|
|
17
17
|
// One `run:board` redraw. **Three speeds**: the frame is redrawn once a second, on the second, so
|
|
@@ -116,6 +116,14 @@ interface Redraw {
|
|
|
116
116
|
now_ms: number
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
+
// A run `run:carry --end` closed and one nobody drives any more are both drawn as stopped.
|
|
120
|
+
function activity_here(local: LocalRead): RunActivity {
|
|
121
|
+
return run_board_status.activity_of(
|
|
122
|
+
local.events,
|
|
123
|
+
local.ended_ms !== undefined || local.is_stranded,
|
|
124
|
+
)
|
|
125
|
+
}
|
|
126
|
+
|
|
119
127
|
function header_of(
|
|
120
128
|
state: BoardState,
|
|
121
129
|
local: LocalRead,
|
|
@@ -126,7 +134,7 @@ function header_of(
|
|
|
126
134
|
now_ms: redraw.now_ms,
|
|
127
135
|
started_ms: local.started_ms,
|
|
128
136
|
ended_ms: local.ended_ms,
|
|
129
|
-
activity:
|
|
137
|
+
activity: activity_here(local),
|
|
130
138
|
layout,
|
|
131
139
|
baseline_total: state.baseline_total,
|
|
132
140
|
plan_fetched_ms: state.fetched_ms,
|