@joshuafolkken/kit 1.1383.0 → 1.1435.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/epic-commands/SKILL.md +1 -1
- package/.claude/skills/epic-commands/epic-bundle.md +5 -0
- package/.claude/skills/workflow-commands/backlogrun-lanes.md +5 -3
- package/.claude/skills/workflow-commands/backlogrun-steps.md +4 -3
- package/.claude/skills/workflow-commands/backlogrun.md +1 -0
- package/.claude/skills/workflow-commands/chain-rule.md +4 -2
- package/.claude/skills/workflow-commands/issue-comments.md +6 -0
- package/.claude/skills/workflow-commands/observation-ledger.md +3 -2
- package/.claude/skills/workflow-commands/pre-gate-cut.md +5 -3
- package/README.md +25 -0
- package/dist/hooks/audit-provision.js +2 -2
- package/dist/hooks/batch-guard.js +7 -7
- package/dist/hooks/{chunk-LOJTBU5V.js → chunk-2G5F2YCD.js} +3 -3
- package/dist/hooks/{chunk-4MGHBTOM.js → chunk-3LE6VLSR.js} +25 -8
- package/dist/hooks/{chunk-Y5R47QHU.js → chunk-4BHUPFKF.js} +1 -1
- package/dist/hooks/{chunk-KC6LUZZ2.js → chunk-5SJSSCJG.js} +1 -1
- package/dist/hooks/{chunk-FZ6FUSBM.js → chunk-723HKZO5.js} +1 -1
- package/dist/hooks/{chunk-RYP3TSUJ.js → chunk-7KMN5QB5.js} +3 -3
- package/dist/hooks/{chunk-NEC4AT4Z.js → chunk-DGT54IH5.js} +4 -0
- package/dist/hooks/{chunk-YKUITFT7.js → chunk-EMB55BDF.js} +4 -4
- package/dist/hooks/{chunk-JBBMAPC4.js → chunk-IX6TTTI6.js} +4 -0
- package/dist/hooks/{chunk-6MAW7BCM.js → chunk-JXQBK323.js} +2677 -190
- package/dist/hooks/{chunk-KCF2YE7O.js → chunk-MSYSUSY6.js} +5 -5
- package/dist/hooks/{chunk-LC56R53A.js → chunk-MU4LJFIV.js} +4 -4
- package/dist/hooks/{chunk-7VSZ2N4B.js → chunk-QL6QTPWN.js} +3 -3
- package/dist/hooks/{chunk-TT742SIO.js → chunk-XER6SSIB.js} +92 -19
- package/dist/hooks/{chunk-2X2ART5G.js → chunk-YW452ZAX.js} +500 -29
- package/dist/hooks/{chunk-6AC4C2IO.js → chunk-YZXYOAZ3.js} +187 -443
- package/dist/hooks/codex-hook-adapter.js +15 -15
- package/dist/hooks/duplicate-read-guard.js +8 -8
- package/dist/hooks/format-edited.js +5 -5
- package/dist/hooks/inputs.json +262 -201
- package/dist/hooks/investigation-guard.js +8 -8
- package/dist/hooks/pretool-guard.js +14 -14
- package/dist/hooks/session-lang.js +124 -15
- package/dist/hooks/stop-guard.js +98 -55
- package/dist/josh.js +63 -10
- package/package.json +4 -3
- package/prompts/review.md +22 -0
- package/scripts/agent/claude-agent-argv.ts +13 -1
- package/scripts/auth/auth-setup-cli.ts +101 -0
- package/scripts/auth/claude-token-output.ts +78 -0
- package/scripts/auth/claude-token.ts +68 -0
- package/scripts/backlog/backlog-drive-cli.ts +2 -4
- package/scripts/backlog/backlog-drive-epic.ts +24 -8
- package/scripts/backlog/backlog-drive-launch.ts +5 -0
- package/scripts/backlog/backlog-drive-named-offer.ts +7 -2
- package/scripts/backlog/backlog-drive-named.ts +4 -2
- package/scripts/backlog/backlog-drive-owner.ts +72 -5
- package/scripts/backlog/backlog-drive-unread.ts +56 -0
- package/scripts/backlog/backlog-drive.ts +16 -8
- package/scripts/backlog/backlog-overlap.ts +22 -5
- package/scripts/backlog/backlog-ready.ts +1 -3
- package/scripts/backlog/backlogrun-args.ts +13 -2
- package/scripts/backlog/backlogrun-cli.ts +91 -19
- package/scripts/backlog/backlogrun-launch.ts +80 -5
- package/scripts/backlog/backlogrun-replace.ts +166 -0
- package/scripts/backlog/backlogrun-stop.ts +178 -0
- package/scripts/behavior/behavior-cli.ts +6 -1
- package/scripts/doctor/doctor-io.ts +60 -9
- package/scripts/doctor/doctor.ts +20 -1
- package/scripts/document/document-byte-budget.ts +3 -3
- package/scripts/epic/epic-bundle-cli.ts +8 -8
- package/scripts/epic/epic-bundle-referenced.ts +2 -5
- package/scripts/epic/epic-bundle.ts +39 -4
- package/scripts/exports/unused-members-cli.ts +2 -1
- package/scripts/followup/followup-checks-event.ts +82 -0
- package/scripts/followup/git-followup-finish.ts +16 -2
- package/scripts/followup/git-followup-workflow.ts +5 -2
- package/scripts/followup/git-pr-followup-checks.ts +18 -4
- package/scripts/followup/git-pr-followup-wrapup.ts +5 -0
- package/scripts/followup/git-pr-followup.ts +4 -1
- package/scripts/fresh-machine/fresh-machine-cli.ts +204 -0
- package/scripts/fresh-machine/fresh-machine-docker.ts +65 -0
- package/scripts/fresh-machine/fresh-machine-image.ts +62 -0
- package/scripts/fresh-machine/fresh-machine-log.ts +61 -0
- package/scripts/fresh-machine/fresh-machine-patterns.ts +130 -0
- package/scripts/fresh-machine/fresh-machine-run.ts +215 -0
- package/scripts/fresh-machine/fresh-machine-script.ts +45 -0
- package/scripts/fresh-machine/fresh-machine-spec.ts +57 -0
- package/scripts/fresh-machine/specs/kit-test-backlogrun-python.json +34 -0
- package/scripts/fresh-machine/specs/kit-test-backlogrun.json +31 -0
- package/scripts/gate/gate-report.ts +70 -15
- package/scripts/gate/project-checks.ts +13 -2
- package/scripts/gate/scoped-green.ts +2 -1
- package/scripts/gate/verification-gate.ts +4 -3
- package/scripts/gh/gh-author-association.ts +14 -0
- package/scripts/gh/git-gh-conditional.ts +66 -0
- package/scripts/gh/git-gh-exec.ts +1 -1
- package/scripts/gh/git-gh-issue-read.ts +6 -3
- package/scripts/gh/git-gh-issue-write.ts +5 -1
- package/scripts/gh/git-gh-issue.ts +4 -1
- package/scripts/gh/git-pr-checks-eval.ts +13 -0
- package/scripts/gh/git-pr-checks-parse.ts +8 -1
- package/scripts/gh/git-pr-checks-watch.ts +11 -4
- package/scripts/gh/git-pr-checks.ts +36 -2
- package/scripts/git/git-command.ts +16 -4
- package/scripts/git/git-schemas.ts +1 -0
- package/scripts/git/git-spawn-sync.ts +1 -1
- package/scripts/hooks/pretool-guard.ts +3 -1
- package/scripts/init/hook-command-rewrite.ts +31 -0
- package/scripts/init/init-actions.ts +4 -1
- package/scripts/init/init-ai-copy.ts +33 -10
- package/scripts/init/init-basic.ts +8 -3
- package/scripts/init/init-logic-toolchain.ts +76 -0
- package/scripts/init/init-logic.ts +2 -0
- package/scripts/init/init.ts +5 -23
- package/scripts/init/project-profile.ts +36 -7
- package/scripts/init/start-exec.ts +27 -1
- package/scripts/init/start-plan.ts +30 -4
- package/scripts/init/start-steps.ts +77 -3
- package/scripts/init/start.ts +21 -13
- package/scripts/init/workflow-mark.ts +35 -0
- package/scripts/issue/issue-ack-cli.ts +84 -0
- package/scripts/issue/issue-labels.ts +40 -8
- package/scripts/issue/issue-state-cli.ts +14 -4
- package/scripts/issue/issue-watch-diff.ts +152 -0
- package/scripts/issue/issue-watch-gate.ts +81 -0
- package/scripts/issue/issue-watch-hook.ts +54 -0
- package/scripts/issue/issue-watch.ts +240 -0
- package/scripts/issue/measure-rerun-cli.ts +3 -4
- package/scripts/josh/josh-command-map.ts +4 -0
- package/scripts/josh/josh-commands-backlog.ts +2 -2
- package/scripts/josh/josh-commands-development.ts +1 -1
- package/scripts/josh/josh-commands-issue.ts +7 -0
- package/scripts/josh/josh-commands-lane.ts +11 -2
- package/scripts/josh/josh-commands-project.ts +25 -1
- package/scripts/josh/josh-verdict.ts +33 -4
- package/scripts/lane/claude-lane-broker.ts +142 -0
- package/scripts/lane/lane-await.ts +6 -2
- package/scripts/lane/lane-cache.ts +37 -7
- package/scripts/lane/lane-capacity.ts +8 -6
- package/scripts/lane/lane-child-invocation.ts +1 -0
- package/scripts/lane/lane-cli.ts +15 -1
- package/scripts/lane/lane-close.ts +15 -6
- package/scripts/lane/lane-dispatch.ts +36 -10
- package/scripts/lane/lane-environment.ts +1 -1
- package/scripts/lane/lane-launch-cli.ts +13 -4
- package/scripts/lane/lane-ledger.ts +21 -3
- package/scripts/lane/lane-measure-cli.ts +10 -2
- package/scripts/lane/lane-open.ts +9 -1
- package/scripts/lane/lane-preserve.ts +49 -0
- package/scripts/lane/lane-reap.ts +20 -5
- package/scripts/lane/lane-registry.ts +6 -0
- package/scripts/lane/lane-restart-cli.ts +51 -0
- package/scripts/lane/lane-restart.ts +175 -0
- package/scripts/lane/{openai-review-broker.ts → lane-session-broker.ts} +106 -28
- package/scripts/lane/lane-spent.ts +195 -0
- package/scripts/lane/lane-stage-stats.ts +27 -3
- package/scripts/lane/lane-stop-stats.ts +96 -0
- package/scripts/lane/openai-lane-supervisor.ts +2 -2
- package/scripts/lane/process-tools.ts +31 -0
- package/scripts/lib/buffered-process.ts +36 -2
- package/scripts/lib/pnpm-reach.ts +27 -0
- package/scripts/lib/status-icons.ts +5 -1
- package/scripts/lint/lint-parallel.ts +19 -2
- package/scripts/observations/observation-ledger-home.ts +1 -0
- package/scripts/review/review-attest-cli.ts +21 -6
- package/scripts/review/review-brief-cli.ts +2 -2
- package/scripts/review/review-comment-round.ts +114 -0
- package/scripts/review/review-record-cli.ts +39 -17
- package/scripts/rules/implementation-cut.ts +3 -3
- package/scripts/rules/pre-gate-cut.ts +1 -1
- package/scripts/run/board/run-board-checks.ts +42 -0
- package/scripts/run/board/run-board-cli.ts +2 -0
- package/scripts/run/board/run-board-every.ts +3 -1
- package/scripts/run/board/run-board-fit.ts +16 -5
- package/scripts/run/board/run-board-fresh.ts +64 -0
- package/scripts/run/board/run-board-header.ts +139 -37
- package/scripts/run/board/run-board-labels.ts +47 -4
- package/scripts/run/board/run-board-lane-event.ts +66 -0
- package/scripts/run/board/run-board-layout.ts +57 -9
- package/scripts/run/board/run-board-now.ts +234 -0
- package/scripts/run/board/run-board-operation-read.ts +75 -0
- package/scripts/run/board/run-board-operation.ts +150 -0
- package/scripts/run/board/run-board-phase.ts +26 -1
- package/scripts/run/board/run-board-plan.ts +4 -6
- package/scripts/run/board/run-board-read.ts +58 -5
- package/scripts/run/board/run-board-reason.ts +124 -0
- package/scripts/run/board/run-board-render-checks.ts +55 -0
- package/scripts/run/board/run-board-render-legend.ts +12 -3
- package/scripts/run/board/run-board-render-slot.ts +2 -2
- package/scripts/run/board/run-board-render-tail.ts +128 -0
- package/scripts/run/board/run-board-render-thanks.ts +50 -0
- package/scripts/run/board/run-board-render-title.ts +58 -0
- package/scripts/run/board/run-board-render.ts +96 -80
- package/scripts/run/board/run-board-row-yield.ts +32 -0
- package/scripts/run/board/run-board-setup.ts +121 -0
- package/scripts/run/board/run-board-span.ts +49 -7
- package/scripts/run/board/run-board-state.ts +10 -0
- package/scripts/run/board/run-board-status.ts +76 -13
- package/scripts/run/board/run-board-thanks-csv.ts +71 -0
- package/scripts/run/board/run-board-thanks-read.ts +112 -0
- package/scripts/run/board/run-board-thanks.ts +53 -0
- package/scripts/run/board/run-board-tick.ts +53 -16
- package/scripts/run/board/run-board-usual.ts +44 -0
- package/scripts/run/carry/run-carry-added.ts +1 -1
- package/scripts/run/carry/run-carry-args.ts +9 -1
- package/scripts/run/carry/run-carry-change.ts +1 -1
- package/scripts/run/carry/run-carry-cli.ts +39 -52
- package/scripts/run/carry/run-carry-end.ts +73 -0
- package/scripts/run/carry/run-carry-ended.ts +9 -1
- package/scripts/run/carry/run-carry.ts +24 -1
- package/scripts/run/entry/run-entry-cli.ts +5 -0
- package/scripts/run/event/run-event-checks.ts +54 -0
- package/scripts/run/event/run-event-render.ts +3 -0
- package/scripts/run/event/run-event-scope.ts +1 -0
- package/scripts/run/event/run-event-setup.ts +91 -0
- package/scripts/run/event/run-event-stream.ts +46 -9
- package/scripts/run/merge/run-merge-cli.ts +7 -4
- package/scripts/run/merge/run-merge-ship.ts +61 -0
- package/scripts/run/merge/run-merge-steps.ts +7 -14
- package/scripts/run/merge/run-merge-token.ts +16 -2
- package/scripts/run/run-headless.ts +1 -0
- package/scripts/run/run-step.ts +2 -0
- package/scripts/run/run-stranded-detect.ts +58 -12
- package/scripts/run/ship/run-ship-cli.ts +22 -2
- package/scripts/run/ship/run-ship-return.ts +31 -13
- package/scripts/run/ship/run-ship-review-steps.ts +17 -4
- package/scripts/run/ship/run-ship-stage.ts +1 -0
- package/scripts/run/wake/run-wake-await.ts +130 -0
- package/scripts/run/wake/run-wake-cli.ts +108 -74
- package/scripts/run/wake/run-wake-describe.ts +36 -2
- package/scripts/run/wake/run-wake-driver.ts +21 -5
- package/scripts/run/wake/run-wake-handoff.ts +1 -1
- package/scripts/run/wake/run-wake-session.ts +20 -3
- package/scripts/run/wake/run-wake-stop.ts +37 -0
- package/scripts/run/wake/run-wake-supervise.ts +44 -0
- package/scripts/sync/claude-settings-merge.ts +144 -0
- package/scripts/sync/sync-ai-files.ts +28 -3
- package/scripts/sync/sync.ts +15 -0
- package/scripts/test/project-test-script.ts +73 -0
- package/scripts/test/skip-marker.ts +14 -1
- package/scripts/test/test-related.ts +3 -2
- package/scripts/test/test-unit-guard.ts +78 -14
- package/scripts/time-runtime/time-reported-failure.ts +11 -3
- package/scripts/uninstall/distributed-files.ts +92 -0
- package/scripts/uninstall/uninstall-plan.ts +119 -0
- package/scripts/uninstall/uninstall-report.ts +61 -0
- package/scripts/uninstall/uninstall.ts +206 -0
- package/templates/AGENTS.basic.md +4 -0
- package/templates/GEMINI.basic.md +6 -0
- package/dist/hooks/chunk-6V5OPIUU.js +0 -13
- package/scripts/run/board/run-board-track.ts +0 -22
|
@@ -3,10 +3,14 @@ import {
|
|
|
3
3
|
IN_PROGRESS_LABEL,
|
|
4
4
|
NEEDS_DECISION_LABEL,
|
|
5
5
|
} from '#scripts/issue/issue-labels'
|
|
6
|
+
import type { PrChecks } from '#scripts/run/event/run-event-checks'
|
|
6
7
|
import { run_event_scope } from '#scripts/run/event/run-event-scope'
|
|
7
8
|
import { run_event_stream, type RunEvent } from '#scripts/run/event/run-event-stream'
|
|
9
|
+
import { run_board_checks } from './run-board-checks'
|
|
8
10
|
import type { ClosedIssue } from './run-board-closed'
|
|
11
|
+
import { run_board_lane_event, type LaneEvent } from './run-board-lane-event'
|
|
9
12
|
import { run_board_phase, type Phase } from './run-board-phase'
|
|
13
|
+
import { run_board_setup, type SetupRead } from './run-board-setup'
|
|
10
14
|
|
|
11
15
|
// What each issue of a run is doing now, read from the run's own event stream and its open lanes.
|
|
12
16
|
// Local reads only — no `gh` call — so `run:board` can redraw it every few seconds; the plan these
|
|
@@ -24,11 +28,29 @@ interface ItemStatus {
|
|
|
24
28
|
lane?: string | undefined
|
|
25
29
|
// Every phase the child passed while running, in order (`run-board-phase.ts`) — kept once it settles.
|
|
26
30
|
track?: ReadonlyArray<Phase> | undefined
|
|
31
|
+
// When a running child entered the phase its track ends on — the event that moved it there.
|
|
32
|
+
phase_ms?: number | undefined
|
|
27
33
|
}
|
|
28
34
|
|
|
29
|
-
|
|
30
|
-
|
|
35
|
+
interface LatestEvent {
|
|
36
|
+
text: string
|
|
37
|
+
at_ms: number
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// The run as a whole, read off the same stream. The setup half is `run-board-setup.ts`'s, absent
|
|
41
|
+
// where a stream carries no stage.
|
|
42
|
+
interface RunActivity extends Partial<SetupRead> {
|
|
31
43
|
last_event_ms: number | undefined
|
|
44
|
+
// The newest thing the run did — its newest position, or its newest event where it has none yet —
|
|
45
|
+
// in the event's own words, and when.
|
|
46
|
+
latest?: LatestEvent | undefined
|
|
47
|
+
// When the run last did anything of its own — its newest position or stage, whichever is newer. A
|
|
48
|
+
// stage that has ended still counts: what follows it is counted from its end, not from before it.
|
|
49
|
+
idle_ms?: number | undefined
|
|
50
|
+
// Each child's newest `lane-phase` or `ship-stage`, by issue (`run-board-lane-event.ts`).
|
|
51
|
+
lane_events?: ReadonlyMap<number, LaneEvent> | undefined
|
|
52
|
+
// Each child's newest `pr-checks`, by issue (`run-board-checks.ts`).
|
|
53
|
+
pr_checks?: ReadonlyMap<number, PrChecks> | undefined
|
|
32
54
|
// The run's last position is a `stop` that names no child — the run itself ended.
|
|
33
55
|
is_stopped: boolean
|
|
34
56
|
}
|
|
@@ -42,13 +64,30 @@ const ENDING_STATES: Readonly<Partial<Record<string, ItemState>>> = {
|
|
|
42
64
|
}
|
|
43
65
|
// A cut or a resume says the child is still running, on whatever phase it had reached.
|
|
44
66
|
const CONTINUING_KINDS: ReadonlySet<string> = new Set([KIND.CUT, KIND.RESUME])
|
|
67
|
+
// A person's restart starts the child over in a fresh session, so its track starts over too.
|
|
68
|
+
const LAUNCH_KINDS: ReadonlySet<string> = new Set([KIND.CHILD_LAUNCH, KIND.RESTART])
|
|
45
69
|
|
|
46
70
|
function moment(event: RunEvent): number {
|
|
47
71
|
return Date.parse(event.at)
|
|
48
72
|
}
|
|
49
73
|
|
|
50
74
|
function launched(event: RunEvent): ItemStatus {
|
|
51
|
-
|
|
75
|
+
const at_ms = moment(event)
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
state: 'running',
|
|
79
|
+
started_ms: at_ms,
|
|
80
|
+
track: [run_board_phase.LAUNCHED_PHASE],
|
|
81
|
+
phase_ms: at_ms,
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// The status with its track moved on by `event`. The phase's start moves only where the phase the
|
|
86
|
+
// track ends on changes, so a second session's first edit does not restart the time of its phase.
|
|
87
|
+
function moved(previous: ItemStatus, track: ReadonlyArray<Phase>, event: RunEvent): ItemStatus {
|
|
88
|
+
const is_same = previous.phase_ms !== undefined && track.at(-1) === previous.track?.at(-1)
|
|
89
|
+
|
|
90
|
+
return { ...previous, track, phase_ms: is_same ? previous.phase_ms : moment(event) }
|
|
52
91
|
}
|
|
53
92
|
|
|
54
93
|
// The status a child settles into: its start and the track it had reached, so a settled row still
|
|
@@ -64,7 +103,9 @@ function ended(previous: ItemStatus | undefined, state: ItemState, event: RunEve
|
|
|
64
103
|
}
|
|
65
104
|
|
|
66
105
|
function continued(previous: ItemStatus | undefined): ItemStatus {
|
|
67
|
-
|
|
106
|
+
const { started_ms, track, phase_ms } = previous ?? {}
|
|
107
|
+
|
|
108
|
+
return { state: 'running', started_ms, track, phase_ms }
|
|
68
109
|
}
|
|
69
110
|
|
|
70
111
|
// A stage line carrying its attempt redraws the track of any child launched on this stream — a settled
|
|
@@ -73,11 +114,12 @@ function continued(previous: ItemStatus | undefined): ItemStatus {
|
|
|
73
114
|
function replayed(
|
|
74
115
|
previous: ItemStatus | undefined,
|
|
75
116
|
attempt: ReadonlyArray<Phase>,
|
|
117
|
+
event: RunEvent,
|
|
76
118
|
): ItemStatus | undefined {
|
|
77
119
|
if (previous === undefined) return previous
|
|
78
120
|
if (previous.track === undefined && previous.state !== 'running') return previous
|
|
79
121
|
|
|
80
|
-
return
|
|
122
|
+
return moved(previous, run_board_phase.replayed(previous.track ?? [], attempt), event)
|
|
81
123
|
}
|
|
82
124
|
|
|
83
125
|
// A phase moves only a running child, so a three-word `report` stage after its merge — or a `plan`
|
|
@@ -87,18 +129,18 @@ function phased(previous: ItemStatus | undefined, event: RunEvent): ItemStatus |
|
|
|
87
129
|
|
|
88
130
|
if (phase === undefined || previous?.state !== 'running') return previous
|
|
89
131
|
|
|
90
|
-
return
|
|
132
|
+
return moved(previous, run_board_phase.history_after(previous.track ?? [], phase), event)
|
|
91
133
|
}
|
|
92
134
|
|
|
93
135
|
function advanced(previous: ItemStatus | undefined, event: RunEvent): ItemStatus | undefined {
|
|
94
136
|
const attempt = run_board_phase.attempt_of(event)
|
|
95
137
|
|
|
96
|
-
return attempt === undefined ? phased(previous, event) : replayed(previous, attempt)
|
|
138
|
+
return attempt === undefined ? phased(previous, event) : replayed(previous, attempt, event)
|
|
97
139
|
}
|
|
98
140
|
|
|
99
141
|
// One event folded into the status of the child it names; an event the board does not track keeps it.
|
|
100
142
|
function next_status(previous: ItemStatus | undefined, event: RunEvent): ItemStatus | undefined {
|
|
101
|
-
if (event.kind
|
|
143
|
+
if (LAUNCH_KINDS.has(event.kind)) return launched(event)
|
|
102
144
|
|
|
103
145
|
const ending = ENDING_STATES[event.kind]
|
|
104
146
|
|
|
@@ -125,16 +167,17 @@ function fold_events(events: ReadonlyArray<RunEvent>): Map<number, ItemStatus> {
|
|
|
125
167
|
function is_running_lane(
|
|
126
168
|
issue: number,
|
|
127
169
|
status: ItemStatus | undefined,
|
|
170
|
+
events: ReadonlyArray<RunEvent>,
|
|
128
171
|
in_run: ReadonlySet<number>,
|
|
129
172
|
): boolean {
|
|
130
|
-
if (status
|
|
173
|
+
if (status !== undefined) return status.state === 'running'
|
|
131
174
|
|
|
132
|
-
return
|
|
175
|
+
return in_run.has(issue) && !run_board_setup.is_lane_pending(events, issue)
|
|
133
176
|
}
|
|
134
177
|
|
|
135
178
|
// Every child the run has touched, with the lane each running child holds. A lane the stream never saw
|
|
136
179
|
// launched — a run restored after the stream rolled — still reads as running while its issue is in the
|
|
137
|
-
// run's plan (`in_run`).
|
|
180
|
+
// run's plan (`in_run`); one the run is still setting up for stays waiting until its launch.
|
|
138
181
|
function statuses_of(
|
|
139
182
|
events: ReadonlyArray<RunEvent>,
|
|
140
183
|
lanes: ReadonlyArray<string>,
|
|
@@ -146,7 +189,7 @@ function statuses_of(
|
|
|
146
189
|
const issue = Number(lane)
|
|
147
190
|
const status = statuses.get(issue)
|
|
148
191
|
|
|
149
|
-
if (is_running_lane(issue, status, in_run)) {
|
|
192
|
+
if (is_running_lane(issue, status, events, in_run)) {
|
|
150
193
|
statuses.set(issue, { state: 'running', ...status, lane: `lane ${lane}` })
|
|
151
194
|
}
|
|
152
195
|
}
|
|
@@ -244,6 +287,21 @@ function is_run_stop(event: RunEvent | undefined): boolean {
|
|
|
244
287
|
return event?.kind === KIND.STOP && run_event_scope.issue_named(event) === undefined
|
|
245
288
|
}
|
|
246
289
|
|
|
290
|
+
function latest_of(event: RunEvent | undefined): LatestEvent | undefined {
|
|
291
|
+
return event === undefined ? undefined : { text: event.text, at_ms: moment(event) }
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function is_own(event: RunEvent): boolean {
|
|
295
|
+
return event.kind === KIND.SETUP || !run_event_stream.TRACE_KINDS.has(event.kind)
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// A stream of traces alone has done nothing of its own yet, so its newest event stands in.
|
|
299
|
+
function idle_of(events: ReadonlyArray<RunEvent>): number | undefined {
|
|
300
|
+
const own = events.findLast((event) => is_own(event)) ?? events.at(-1)
|
|
301
|
+
|
|
302
|
+
return own === undefined ? undefined : moment(own)
|
|
303
|
+
}
|
|
304
|
+
|
|
247
305
|
// `is_ended` is a run `run:carry --end` closed: its record says it stopped, whatever its last event
|
|
248
306
|
// was.
|
|
249
307
|
function activity_of(events: ReadonlyArray<RunEvent>, is_ended = false): RunActivity {
|
|
@@ -251,7 +309,12 @@ function activity_of(events: ReadonlyArray<RunEvent>, is_ended = false): RunActi
|
|
|
251
309
|
const position = events.findLast((event) => !run_event_stream.TRACE_KINDS.has(event.kind))
|
|
252
310
|
|
|
253
311
|
return {
|
|
312
|
+
...run_board_setup.read(events),
|
|
254
313
|
last_event_ms: last === undefined ? undefined : moment(last),
|
|
314
|
+
latest: latest_of(position ?? last),
|
|
315
|
+
idle_ms: idle_of(events),
|
|
316
|
+
lane_events: run_board_lane_event.newest_of(events),
|
|
317
|
+
pr_checks: run_board_checks.newest_of(events),
|
|
255
318
|
is_stopped: is_ended || is_run_stop(position),
|
|
256
319
|
}
|
|
257
320
|
}
|
|
@@ -259,4 +322,4 @@ function activity_of(events: ReadonlyArray<RunEvent>, is_ended = false): RunActi
|
|
|
259
322
|
const run_board_status = { activity_of, settle_closed, statuses_of }
|
|
260
323
|
|
|
261
324
|
export { run_board_status }
|
|
262
|
-
export type { ItemState, ItemStatus, OpenRead, RunActivity }
|
|
325
|
+
export type { ItemState, ItemStatus, LatestEvent, OpenRead, RunActivity }
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { parse } from 'csv-parse/sync'
|
|
2
|
+
import { run_board_render_title } from './run-board-render-title'
|
|
3
|
+
|
|
4
|
+
// The reviewers who helped kit, read off the published roster's CSV. The roster is a spreadsheet anyone
|
|
5
|
+
// with edit access changes, so nothing in it is trusted as drawn: a sheet whose header is not `name,url`
|
|
6
|
+
// is not the roster, a row without a name or with a link that is not `https://` is dropped, and a name
|
|
7
|
+
// loses every control character — an escape in a cell would otherwise be written to the terminal as one.
|
|
8
|
+
|
|
9
|
+
const HEADER = ['name', 'url'] as const
|
|
10
|
+
// Enough for any person's or project's name, short enough that one cell cannot fill the line; a
|
|
11
|
+
// longer one is cut to an ellipsis, as a row's title is.
|
|
12
|
+
const NAME_LIMIT = 40
|
|
13
|
+
const CONTROL = /\p{Cc}/gu
|
|
14
|
+
const SAFE_URL = /^https:\/\/[^\s\p{Cc}]+$/u
|
|
15
|
+
|
|
16
|
+
interface Thanker {
|
|
17
|
+
name: string
|
|
18
|
+
// The page the name opens; `undefined` draws the name unlinked.
|
|
19
|
+
url: string | undefined
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
type Thanks = ReadonlyArray<Thanker>
|
|
23
|
+
|
|
24
|
+
function name_of(cell: string): string {
|
|
25
|
+
return run_board_render_title.title_of(cell.replaceAll(CONTROL, '').trim(), NAME_LIMIT)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// An empty cell is a name without a link; any other value has to be a whole `https://` URL.
|
|
29
|
+
function url_of(cell: string): string | undefined | false {
|
|
30
|
+
const url = cell.trim()
|
|
31
|
+
|
|
32
|
+
if (url === '') return undefined
|
|
33
|
+
|
|
34
|
+
return SAFE_URL.test(url) && URL.canParse(url) ? url : false
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function thanker_of(row: ReadonlyArray<string>): Array<Thanker> {
|
|
38
|
+
const [name_cell = '', url_cell = ''] = row
|
|
39
|
+
const name = name_of(name_cell)
|
|
40
|
+
const url = url_of(url_cell)
|
|
41
|
+
|
|
42
|
+
if (name === '' || url === false) return []
|
|
43
|
+
|
|
44
|
+
return [{ name, url }]
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function is_roster(header: ReadonlyArray<string> | undefined): boolean {
|
|
48
|
+
return header?.length === HEADER.length && HEADER.every((cell, index) => header[index] === cell)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function rows_of(csv: string): Array<Array<string>> | undefined {
|
|
52
|
+
try {
|
|
53
|
+
return parse(csv, { relax_column_count: true, skip_empty_lines: true, bom: true })
|
|
54
|
+
} catch {
|
|
55
|
+
return undefined
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// `undefined` for a text that is not the roster, so a failed read is told apart from an empty roster.
|
|
60
|
+
function thanks_of(csv: string): Thanks | undefined {
|
|
61
|
+
const [header, ...rows] = rows_of(csv) ?? []
|
|
62
|
+
|
|
63
|
+
if (!is_roster(header)) return undefined
|
|
64
|
+
|
|
65
|
+
return rows.flatMap((row) => thanker_of(row))
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const run_board_thanks_csv = { NAME_LIMIT, thanks_of }
|
|
69
|
+
|
|
70
|
+
export { run_board_thanks_csv }
|
|
71
|
+
export type { Thanker, Thanks }
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { PROJECT_ROOT } from '#scripts/init/init-paths'
|
|
2
|
+
import { stamp_file } from '#scripts/josh/stamp-file'
|
|
3
|
+
import { json_value } from '#scripts/lib/json-value'
|
|
4
|
+
import { timed_fetch } from '#scripts/lib/timed-fetch'
|
|
5
|
+
import { run_progress } from '#scripts/run/progress/run-progress'
|
|
6
|
+
import { z } from 'zod'
|
|
7
|
+
import { run_board_thanks_csv, type Thanks } from './run-board-thanks-csv'
|
|
8
|
+
|
|
9
|
+
// The roster `run:board` thanks its reviewers from: a Google spreadsheet published as CSV, changed
|
|
10
|
+
// apart from any build or release, so it is read at run time. Its answer is kept in the project's
|
|
11
|
+
// record beside the board's other state and reused for five minutes — Google's own cache holds it as
|
|
12
|
+
// long — so neither a redraw nor a fresh `--once` asks again inside that window. A read that fails
|
|
13
|
+
// keeps the last answer, and with none draws no line; nothing about the failure reaches the screen.
|
|
14
|
+
|
|
15
|
+
const ROSTER_URL =
|
|
16
|
+
'https://docs.google.com/spreadsheets/d/e/2PACX-1vR8Ia38Wiw6Kfyk9Or594J8ZRS8okynV30CpOtq5B6_GyJG7A1-t8Au64jTYvmzgIOlbafcKKgrNFVJ/pub?gid=0&single=true&output=csv'
|
|
17
|
+
const RECORD_PREFIX = 'josh-run-board-thanks-'
|
|
18
|
+
const TTL_MINUTES = 5
|
|
19
|
+
const TTL_MS = TTL_MINUTES * run_progress.MS_PER_MINUTE
|
|
20
|
+
|
|
21
|
+
const thanker_schema = z.object({ name: z.string(), url: z.string().optional() })
|
|
22
|
+
const record_schema = z.object({ checked_ms: z.number(), thanks: z.array(thanker_schema) })
|
|
23
|
+
|
|
24
|
+
interface ThanksRecord {
|
|
25
|
+
checked_ms: number
|
|
26
|
+
thanks: Thanks
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// What the read works through, injected in tests.
|
|
30
|
+
interface ThanksDependencies {
|
|
31
|
+
fetch_text: () => Promise<string>
|
|
32
|
+
record_path: string
|
|
33
|
+
now: () => number
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function read_record(record_path: string): ThanksRecord | undefined {
|
|
37
|
+
const raw = stamp_file.read_stamp_text(record_path)
|
|
38
|
+
const parsed = raw === undefined ? undefined : json_value.parse_with(raw, record_schema)
|
|
39
|
+
|
|
40
|
+
if (parsed === undefined) return undefined
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
checked_ms: parsed.checked_ms,
|
|
44
|
+
thanks: parsed.thanks.map(({ name, url }) => ({ name, url })),
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// A record that cannot be written leaves the next read to ask again — never a reason to fail the board.
|
|
49
|
+
function write_record(record_path: string, record: ThanksRecord): void {
|
|
50
|
+
try {
|
|
51
|
+
stamp_file.replace_stamp(record_path, record)
|
|
52
|
+
} catch {
|
|
53
|
+
// The board draws what it read either way.
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function fetch_roster(read: () => Promise<string>): Promise<Thanks | undefined> {
|
|
58
|
+
try {
|
|
59
|
+
return run_board_thanks_csv.thanks_of(await read())
|
|
60
|
+
} catch {
|
|
61
|
+
return undefined
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// The recorded roster while it is younger than the record's life; `undefined` once it is stale. A
|
|
66
|
+
// record dated after now — the clock stepped back — is stale too, or it would hold until the clock
|
|
67
|
+
// caught up with it.
|
|
68
|
+
function fresh_thanks(record: ThanksRecord | undefined, now_ms: number): Thanks | undefined {
|
|
69
|
+
if (record === undefined) return undefined
|
|
70
|
+
|
|
71
|
+
const age_ms = now_ms - record.checked_ms
|
|
72
|
+
|
|
73
|
+
return age_ms >= 0 && age_ms < TTL_MS ? record.thanks : undefined
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// A response that is not a success is a failed read, so its body is never taken for the roster.
|
|
77
|
+
async function fetch_text(): Promise<string> {
|
|
78
|
+
const response = await timed_fetch(ROSTER_URL)
|
|
79
|
+
|
|
80
|
+
if (!response.ok) throw new Error(`roster read answered ${String(response.status)}`)
|
|
81
|
+
|
|
82
|
+
return await response.text()
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const LIVE_DEPS: ThanksDependencies = {
|
|
86
|
+
fetch_text,
|
|
87
|
+
record_path: stamp_file.stamp_path(RECORD_PREFIX, PROJECT_ROOT),
|
|
88
|
+
now: () => Date.now(),
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function read_with(deps: ThanksDependencies): Promise<Thanks> {
|
|
92
|
+
const now_ms = deps.now()
|
|
93
|
+
const record = read_record(deps.record_path)
|
|
94
|
+
const fresh = fresh_thanks(record, now_ms)
|
|
95
|
+
|
|
96
|
+
if (fresh !== undefined) return fresh
|
|
97
|
+
|
|
98
|
+
const thanks = (await fetch_roster(deps.fetch_text)) ?? record?.thanks ?? []
|
|
99
|
+
|
|
100
|
+
write_record(deps.record_path, { checked_ms: now_ms, thanks })
|
|
101
|
+
|
|
102
|
+
return thanks
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function read_thanks(deps: ThanksDependencies = LIVE_DEPS): Promise<Thanks> {
|
|
106
|
+
return await read_with(deps)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const run_board_thanks_read = { TTL_MS, read_thanks }
|
|
110
|
+
|
|
111
|
+
export { run_board_thanks_read }
|
|
112
|
+
export type { ThanksDependencies }
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { run_board_fetch, type Fetch } from './run-board-fetch'
|
|
2
|
+
import { run_board_state, type BoardPorts, type BoardState, type Pace } from './run-board-state'
|
|
3
|
+
import type { Thanks } from './run-board-thanks-csv'
|
|
4
|
+
import { run_board_thanks_read } from './run-board-thanks-read'
|
|
5
|
+
|
|
6
|
+
// The reviewers' roster a `run:board` redraw draws above its legend. Like the GitHub reads
|
|
7
|
+
// (`run-board-github.ts`), **a live board never waits on it**: the read is launched and folded in on
|
|
8
|
+
// the redraw after it lands; one plain frame waits for it. It is launched again no sooner than the
|
|
9
|
+
// roster's own record goes stale. A board with no `read_thanks` port — the chat frame — reads none and
|
|
10
|
+
// draws none.
|
|
11
|
+
|
|
12
|
+
const { TTL_MS } = run_board_thanks_read
|
|
13
|
+
const { is_due } = run_board_state
|
|
14
|
+
|
|
15
|
+
type ThanksState = Pick<BoardState, 'thanks' | 'thanks_fetch' | 'thanks_attempted_ms'>
|
|
16
|
+
|
|
17
|
+
function launch_thanks(state: BoardState, ports: BoardPorts, now_ms: number): BoardState {
|
|
18
|
+
const { read_thanks } = ports
|
|
19
|
+
|
|
20
|
+
if (read_thanks === undefined || state.thanks_fetch !== undefined) return state
|
|
21
|
+
if (!is_due(state.thanks_attempted_ms, TTL_MS, now_ms)) return state
|
|
22
|
+
|
|
23
|
+
const thanks_fetch: Fetch<Thanks> = run_board_fetch.launch(read_thanks, state.thanks)
|
|
24
|
+
|
|
25
|
+
return { ...state, thanks_fetch, thanks_attempted_ms: now_ms }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function fold_thanks(state: BoardState): BoardState {
|
|
29
|
+
const answer = state.thanks_fetch?.answer()
|
|
30
|
+
|
|
31
|
+
if (answer === undefined) return state
|
|
32
|
+
|
|
33
|
+
return { ...state, thanks: answer.value, thanks_fetch: undefined }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Folds what landed and launches what is due; at the `settled` pace, waits for it to land.
|
|
37
|
+
async function gather_thanks(
|
|
38
|
+
state: BoardState,
|
|
39
|
+
ports: BoardPorts,
|
|
40
|
+
now_ms: number,
|
|
41
|
+
pace: Pace,
|
|
42
|
+
): Promise<BoardState> {
|
|
43
|
+
const launched = launch_thanks(fold_thanks(state), ports, now_ms)
|
|
44
|
+
|
|
45
|
+
if (pace === 'settled' && launched.thanks_fetch !== undefined) await launched.thanks_fetch.landed
|
|
46
|
+
|
|
47
|
+
return fold_thanks(launched)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const run_board_thanks = { gather_thanks }
|
|
51
|
+
|
|
52
|
+
export { run_board_thanks }
|
|
53
|
+
export type { ThanksState }
|
|
@@ -8,10 +8,12 @@ import { run_board_link } from './run-board-link'
|
|
|
8
8
|
import { run_board_machine } from './run-board-machine'
|
|
9
9
|
import { run_board_notes, type Titles } from './run-board-notes'
|
|
10
10
|
import type { LocalRead } from './run-board-read'
|
|
11
|
+
import { run_board_reason } from './run-board-reason'
|
|
11
12
|
import { run_board_render, type FrameBounds } from './run-board-render'
|
|
12
13
|
import { run_board_spin, type Spun } from './run-board-spin'
|
|
13
14
|
import { run_board_state, type BoardPorts, type BoardState, type Pace } from './run-board-state'
|
|
14
|
-
import { run_board_status } from './run-board-status'
|
|
15
|
+
import { run_board_status, type RunActivity } from './run-board-status'
|
|
16
|
+
import { run_board_thanks } from './run-board-thanks'
|
|
15
17
|
import { run_board_usage } from './run-board-usage'
|
|
16
18
|
|
|
17
19
|
// One `run:board` redraw. **Three speeds**: the frame is redrawn once a second, on the second, so
|
|
@@ -30,6 +32,7 @@ const LOCAL_READ_SECONDS = 5
|
|
|
30
32
|
const LOCAL_READ_MS = LOCAL_READ_SECONDS * run_progress.MS_PER_SECOND
|
|
31
33
|
const { FRESH_STATE, is_due } = run_board_state
|
|
32
34
|
const { PLAN_RETRY_MS, gather_github, statuses_for } = run_board_github
|
|
35
|
+
const { gather_thanks } = run_board_thanks
|
|
33
36
|
|
|
34
37
|
async function reread(state: BoardState, ports: BoardPorts, now_ms: number): Promise<BoardState> {
|
|
35
38
|
if (!is_due(state.read_ms, LOCAL_READ_MS, now_ms)) return state
|
|
@@ -88,17 +91,17 @@ function titles_of(state: BoardState): Titles {
|
|
|
88
91
|
return with_closed(state.plan, state.closed).context.titles
|
|
89
92
|
}
|
|
90
93
|
|
|
91
|
-
//
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
run_board_status.statuses_of(local.events, local.lanes, new Set()),
|
|
99
|
-
)
|
|
100
|
-
}
|
|
94
|
+
// A run `run:carry --end` closed and one nobody drives any more are both drawn as stopped.
|
|
95
|
+
function activity_here(local: LocalRead): RunActivity {
|
|
96
|
+
return run_board_status.activity_of(
|
|
97
|
+
local.events,
|
|
98
|
+
local.ended_ms !== undefined || local.is_stranded,
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
101
|
|
|
102
|
+
// The plan laid under its statuses, each waiting row saying why it waits.
|
|
103
|
+
function planned_layout(state: BoardState, plan: BoardPlan, local: LocalRead): BoardLayout {
|
|
104
|
+
const { closed } = state
|
|
102
105
|
const read = {
|
|
103
106
|
open_numbers: plan.context.open_numbers,
|
|
104
107
|
read_ms: state.fetched_ms,
|
|
@@ -106,8 +109,25 @@ function layout_for(state: BoardState, local: LocalRead): BoardLayout {
|
|
|
106
109
|
labels: plan.labels,
|
|
107
110
|
}
|
|
108
111
|
const statuses = run_board_status.settle_closed(statuses_for(plan, local), read)
|
|
112
|
+
const reason_of = run_board_reason.reader({
|
|
113
|
+
plan,
|
|
114
|
+
statuses,
|
|
115
|
+
events: local.events,
|
|
116
|
+
free_lanes: local.free_lanes,
|
|
117
|
+
is_stopped: activity_here(local).is_stopped,
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
return run_board_layout.layout_of(with_closed(plan, closed), statuses, reason_of)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// The statuses scoped to the run's plan, a running child the plan's listing no longer holds settled.
|
|
124
|
+
// Before the plan is read, the run's own children alone.
|
|
125
|
+
function layout_for(state: BoardState, local: LocalRead): BoardLayout {
|
|
126
|
+
if (state.plan !== undefined) return planned_layout(state, state.plan, local)
|
|
109
127
|
|
|
110
|
-
return run_board_layout.
|
|
128
|
+
return run_board_layout.local_layout_of(
|
|
129
|
+
run_board_status.statuses_of(local.events, local.lanes, new Set()),
|
|
130
|
+
)
|
|
111
131
|
}
|
|
112
132
|
|
|
113
133
|
// What one redraw draws with.
|
|
@@ -116,6 +136,22 @@ interface Redraw {
|
|
|
116
136
|
now_ms: number
|
|
117
137
|
}
|
|
118
138
|
|
|
139
|
+
// What the header reads off the local read.
|
|
140
|
+
type LocalHeader = Pick<
|
|
141
|
+
BoardHeader,
|
|
142
|
+
'started_ms' | 'ended_ms' | 'activity' | 'operations' | 'usual'
|
|
143
|
+
>
|
|
144
|
+
|
|
145
|
+
function local_header(local: LocalRead): LocalHeader {
|
|
146
|
+
return {
|
|
147
|
+
started_ms: local.started_ms,
|
|
148
|
+
ended_ms: local.ended_ms,
|
|
149
|
+
activity: activity_here(local),
|
|
150
|
+
operations: local.operations,
|
|
151
|
+
usual: local.usual,
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
119
155
|
function header_of(
|
|
120
156
|
state: BoardState,
|
|
121
157
|
local: LocalRead,
|
|
@@ -124,9 +160,7 @@ function header_of(
|
|
|
124
160
|
): BoardHeader {
|
|
125
161
|
return {
|
|
126
162
|
now_ms: redraw.now_ms,
|
|
127
|
-
|
|
128
|
-
ended_ms: local.ended_ms,
|
|
129
|
-
activity: run_board_status.activity_of(local.events, local.ended_ms !== undefined),
|
|
163
|
+
...local_header(local),
|
|
130
164
|
layout,
|
|
131
165
|
baseline_total: state.baseline_total,
|
|
132
166
|
plan_fetched_ms: state.fetched_ms,
|
|
@@ -153,6 +187,8 @@ function frame_of(state: BoardState, local: LocalRead, layout: BoardLayout, redr
|
|
|
153
187
|
header: header_of(state, local, layout, redraw),
|
|
154
188
|
notes: run_board_notes.notes_of(local.events, titles_of(state)),
|
|
155
189
|
resume: local.resume,
|
|
190
|
+
thanks: state.thanks,
|
|
191
|
+
link: redraw.ports.link,
|
|
156
192
|
...bounds,
|
|
157
193
|
})
|
|
158
194
|
|
|
@@ -195,8 +231,9 @@ async function tick(
|
|
|
195
231
|
}
|
|
196
232
|
|
|
197
233
|
const gathered = await gather_github(state_for(read, local), { ports, local, now_ms, pace })
|
|
234
|
+
const thanked = await gather_thanks(gathered, ports, now_ms, pace)
|
|
198
235
|
|
|
199
|
-
return draw_run(await resample(
|
|
236
|
+
return draw_run(await resample(thanked, ports, now_ms), local, { ports, now_ms })
|
|
200
237
|
}
|
|
201
238
|
|
|
202
239
|
const run_board_tick = {
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { lane_ledger, type LedgerEntry } from '#scripts/lane/lane-ledger'
|
|
2
|
+
import { lane_stage_stats } from '#scripts/lane/lane-stage-stats'
|
|
3
|
+
import { run_board_phase, type Phase } from './run-board-phase'
|
|
4
|
+
|
|
5
|
+
// How long each phase usually takes: the lane ledger's median of the stage the phase is
|
|
6
|
+
// (`lane-stage-stats.ts`), so a running child that has stood at its phase far longer stands out
|
|
7
|
+
// (joshuafolkken/kit#3769). A phase is a `josh ship` stage under the names `run-board-phase.ts` gives
|
|
8
|
+
// them, and `implement` is the ledger's derived one. A phase no stage records — `investigate`, `plan`,
|
|
9
|
+
// `failed` — has no usual time, and is never drawn as long.
|
|
10
|
+
|
|
11
|
+
type UsualTimes = ReadonlyMap<Phase, number>
|
|
12
|
+
|
|
13
|
+
const IMPLEMENT_PHASE: Phase = 'implement'
|
|
14
|
+
|
|
15
|
+
// Each phase's ledger stage, from the stage-to-phase names a row's phase is drawn with. The ledger's
|
|
16
|
+
// `implement` runs from the dispatch, so it also holds the investigation and the plan the row draws
|
|
17
|
+
// before its `implement` phase: the usual time is the longer, which can only draw a phase as long
|
|
18
|
+
// later, never sooner.
|
|
19
|
+
const PHASE_STAGES: ReadonlyArray<readonly [string, Phase | undefined]> = [
|
|
20
|
+
[lane_stage_stats.IMPLEMENT, IMPLEMENT_PHASE],
|
|
21
|
+
...Object.entries(run_board_phase.SHIP_PHASES),
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
function usual_of(entries: ReadonlyArray<LedgerEntry>): UsualTimes {
|
|
25
|
+
const medians = lane_stage_stats.medians(entries)
|
|
26
|
+
|
|
27
|
+
return new Map(
|
|
28
|
+
PHASE_STAGES.flatMap(([stage, phase]) => {
|
|
29
|
+
const median = medians.get(stage)
|
|
30
|
+
|
|
31
|
+
return phase === undefined || median === undefined ? [] : [[phase, median] as const]
|
|
32
|
+
}),
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// The repository's whole ledger, read where the board reads its stream; an absent one has no usual time.
|
|
37
|
+
function read(repository: string): UsualTimes {
|
|
38
|
+
return usual_of(lane_ledger.read_entries(lane_ledger.target_of(repository)))
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const run_board_usual = { read, usual_of }
|
|
42
|
+
|
|
43
|
+
export { run_board_usual }
|
|
44
|
+
export type { UsualTimes }
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { issue_number_shape } from '#scripts/issue/issue-number-shape'
|
|
2
2
|
import { cli_flags } from '#scripts/lib/cli-flags'
|
|
3
|
+
import { run_headless } from '#scripts/run/run-headless'
|
|
3
4
|
import { run_invocation } from '#scripts/run/run-invocation'
|
|
4
5
|
import { run_carry, type CarryChange, type CarryClaimRequest, type CarryOwner } from './run-carry'
|
|
5
6
|
|
|
@@ -178,6 +179,13 @@ function group_count(values: ParsedValues): number {
|
|
|
178
179
|
return groups.filter(Boolean).length
|
|
179
180
|
}
|
|
180
181
|
|
|
182
|
+
// The mark rides the environment the run launched its session with; the record keeps it beside the
|
|
183
|
+
// owner, so `josh backlogrun --stop` knows that owner for the run's own. An attached owner is left as
|
|
184
|
+
// it is, so its record is the one it always was.
|
|
185
|
+
function marked(owner: CarryOwner): CarryOwner {
|
|
186
|
+
return run_headless.is_headless_parent(process.env) ? { ...owner, is_headless: true } : owner
|
|
187
|
+
}
|
|
188
|
+
|
|
181
189
|
// **`--owner` is what makes liveness answerable at all.** The process that runs this command is a
|
|
182
190
|
// short-lived `tsx`, so its own pid says nothing about whether the run is still going; the caller
|
|
183
191
|
// names the process that is — its own session — and a `backlogrun` passes `--owner "$PPID"`. Absent,
|
|
@@ -192,7 +200,7 @@ function to_owner(value: OptionValue): CarryOwner | undefined {
|
|
|
192
200
|
|
|
193
201
|
const pid = Number(text)
|
|
194
202
|
|
|
195
|
-
return pid < MIN_PID ? undefined : run_carry.owner_of(pid)
|
|
203
|
+
return pid < MIN_PID ? undefined : marked(run_carry.owner_of(pid))
|
|
196
204
|
}
|
|
197
205
|
|
|
198
206
|
// A non-empty `--summary`, or nothing. An empty string is nothing here, so it reads as absent and the
|