@joshuafolkken/kit 1.1387.0 → 1.1435.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/epic-commands/SKILL.md +1 -1
- package/.claude/skills/epic-commands/epic-bundle.md +5 -0
- package/.claude/skills/workflow-commands/backlogrun-lanes.md +5 -3
- package/.claude/skills/workflow-commands/backlogrun-steps.md +4 -3
- package/.claude/skills/workflow-commands/backlogrun.md +1 -0
- package/.claude/skills/workflow-commands/chain-rule.md +4 -2
- package/.claude/skills/workflow-commands/issue-comments.md +6 -0
- package/.claude/skills/workflow-commands/observation-ledger.md +3 -2
- package/.claude/skills/workflow-commands/pre-gate-cut.md +5 -3
- package/README.md +25 -0
- package/dist/hooks/audit-provision.js +2 -2
- package/dist/hooks/batch-guard.js +7 -7
- package/dist/hooks/{chunk-IYVOLN6A.js → chunk-2G5F2YCD.js} +3 -3
- package/dist/hooks/{chunk-2YSW3CXH.js → chunk-3LE6VLSR.js} +17 -6
- package/dist/hooks/{chunk-J3KSS7II.js → chunk-4BHUPFKF.js} +1 -1
- package/dist/hooks/{chunk-EAQEGME7.js → chunk-5SJSSCJG.js} +1 -1
- package/dist/hooks/{chunk-FZ6FUSBM.js → chunk-723HKZO5.js} +1 -1
- package/dist/hooks/{chunk-LMWBRJIP.js → chunk-7KMN5QB5.js} +3 -3
- package/dist/hooks/{chunk-NEC4AT4Z.js → chunk-DGT54IH5.js} +4 -0
- package/dist/hooks/{chunk-STDSEPWU.js → chunk-EMB55BDF.js} +4 -4
- package/dist/hooks/{chunk-JBBMAPC4.js → chunk-IX6TTTI6.js} +4 -0
- package/dist/hooks/{chunk-GJ7JPIHN.js → chunk-JXQBK323.js} +2674 -187
- package/dist/hooks/{chunk-DCY2GWHC.js → chunk-MSYSUSY6.js} +5 -5
- package/dist/hooks/{chunk-EBSHN25W.js → chunk-MU4LJFIV.js} +4 -4
- package/dist/hooks/{chunk-WNFJLBXI.js → chunk-QL6QTPWN.js} +3 -3
- package/dist/hooks/{chunk-SL4GQ2XP.js → chunk-XER6SSIB.js} +91 -18
- package/dist/hooks/{chunk-U32VSRNG.js → chunk-YW452ZAX.js} +89 -34
- package/dist/hooks/{chunk-GNDJ7NJ7.js → chunk-YZXYOAZ3.js} +202 -55
- package/dist/hooks/codex-hook-adapter.js +15 -15
- package/dist/hooks/duplicate-read-guard.js +8 -8
- package/dist/hooks/format-edited.js +5 -5
- package/dist/hooks/inputs.json +288 -229
- package/dist/hooks/investigation-guard.js +8 -8
- package/dist/hooks/pretool-guard.js +14 -14
- package/dist/hooks/session-lang.js +69 -16
- package/dist/hooks/stop-guard.js +13 -38
- package/dist/josh.js +62 -9
- package/package.json +4 -3
- package/prompts/review.md +22 -0
- package/scripts/agent/claude-agent-argv.ts +13 -1
- package/scripts/auth/auth-setup-cli.ts +101 -0
- package/scripts/auth/claude-token-output.ts +78 -0
- package/scripts/auth/claude-token.ts +68 -0
- package/scripts/backlog/backlog-drive-epic.ts +24 -8
- package/scripts/backlog/backlog-drive-launch.ts +3 -0
- package/scripts/backlog/backlog-drive-named-offer.ts +7 -2
- package/scripts/backlog/backlog-drive-named.ts +4 -2
- package/scripts/backlog/backlog-drive-unread.ts +56 -0
- package/scripts/backlog/backlog-drive.ts +16 -8
- package/scripts/backlog/backlog-overlap.ts +22 -5
- package/scripts/backlog/backlog-ready.ts +1 -3
- package/scripts/backlog/backlogrun-args.ts +13 -2
- package/scripts/backlog/backlogrun-cli.ts +91 -19
- package/scripts/backlog/backlogrun-launch.ts +24 -0
- package/scripts/backlog/backlogrun-replace.ts +166 -0
- package/scripts/backlog/backlogrun-stop.ts +178 -0
- package/scripts/behavior/behavior-cli.ts +6 -1
- package/scripts/doctor/doctor-io.ts +60 -9
- package/scripts/doctor/doctor.ts +20 -1
- package/scripts/document/document-byte-budget.ts +3 -3
- package/scripts/epic/epic-bundle-cli.ts +8 -8
- package/scripts/epic/epic-bundle-referenced.ts +2 -5
- package/scripts/epic/epic-bundle.ts +39 -4
- package/scripts/exports/unused-members-cli.ts +2 -1
- package/scripts/followup/followup-checks-event.ts +82 -0
- package/scripts/followup/git-followup-finish.ts +16 -2
- package/scripts/followup/git-followup-workflow.ts +5 -2
- package/scripts/followup/git-pr-followup-checks.ts +18 -4
- package/scripts/followup/git-pr-followup-wrapup.ts +5 -0
- package/scripts/followup/git-pr-followup.ts +4 -1
- package/scripts/fresh-machine/fresh-machine-cli.ts +204 -0
- package/scripts/fresh-machine/fresh-machine-docker.ts +65 -0
- package/scripts/fresh-machine/fresh-machine-image.ts +62 -0
- package/scripts/fresh-machine/fresh-machine-log.ts +61 -0
- package/scripts/fresh-machine/fresh-machine-patterns.ts +130 -0
- package/scripts/fresh-machine/fresh-machine-run.ts +215 -0
- package/scripts/fresh-machine/fresh-machine-script.ts +45 -0
- package/scripts/fresh-machine/fresh-machine-spec.ts +57 -0
- package/scripts/fresh-machine/specs/kit-test-backlogrun-python.json +34 -0
- package/scripts/fresh-machine/specs/kit-test-backlogrun.json +31 -0
- package/scripts/gate/gate-report.ts +70 -15
- package/scripts/gate/project-checks.ts +13 -2
- package/scripts/gate/scoped-green.ts +2 -1
- package/scripts/gate/verification-gate.ts +4 -3
- package/scripts/gh/gh-author-association.ts +14 -0
- package/scripts/gh/git-gh-conditional.ts +66 -0
- package/scripts/gh/git-gh-exec.ts +1 -1
- package/scripts/gh/git-gh-issue-read.ts +6 -3
- package/scripts/gh/git-gh-issue-write.ts +5 -1
- package/scripts/gh/git-gh-issue.ts +4 -1
- package/scripts/gh/git-pr-checks-eval.ts +13 -0
- package/scripts/gh/git-pr-checks-parse.ts +8 -1
- package/scripts/gh/git-pr-checks-watch.ts +11 -4
- package/scripts/gh/git-pr-checks.ts +36 -2
- package/scripts/git/git-command.ts +16 -4
- package/scripts/git/git-schemas.ts +1 -0
- package/scripts/git/git-spawn-sync.ts +1 -1
- package/scripts/hooks/pretool-guard.ts +3 -1
- package/scripts/init/hook-command-rewrite.ts +31 -0
- package/scripts/init/init-actions.ts +4 -1
- package/scripts/init/init-ai-copy.ts +33 -10
- package/scripts/init/init-basic.ts +8 -3
- package/scripts/init/init-logic-toolchain.ts +76 -0
- package/scripts/init/init-logic.ts +2 -0
- package/scripts/init/init.ts +5 -23
- package/scripts/init/project-profile.ts +36 -7
- package/scripts/init/start-exec.ts +27 -1
- package/scripts/init/start-plan.ts +15 -0
- package/scripts/init/start-steps.ts +46 -0
- package/scripts/init/start.ts +1 -0
- package/scripts/init/workflow-mark.ts +35 -0
- package/scripts/issue/issue-ack-cli.ts +84 -0
- package/scripts/issue/issue-labels.ts +40 -8
- package/scripts/issue/issue-state-cli.ts +14 -4
- package/scripts/issue/issue-watch-diff.ts +152 -0
- package/scripts/issue/issue-watch-gate.ts +81 -0
- package/scripts/issue/issue-watch-hook.ts +54 -0
- package/scripts/issue/issue-watch.ts +240 -0
- package/scripts/issue/measure-rerun-cli.ts +3 -4
- package/scripts/josh/josh-command-map.ts +4 -0
- package/scripts/josh/josh-commands-backlog.ts +2 -2
- package/scripts/josh/josh-commands-development.ts +1 -1
- package/scripts/josh/josh-commands-issue.ts +7 -0
- package/scripts/josh/josh-commands-lane.ts +11 -2
- package/scripts/josh/josh-commands-project.ts +24 -0
- package/scripts/josh/josh-verdict.ts +33 -4
- package/scripts/lane/claude-lane-broker.ts +142 -0
- package/scripts/lane/lane-await.ts +6 -2
- package/scripts/lane/lane-cache.ts +37 -7
- package/scripts/lane/lane-capacity.ts +8 -6
- package/scripts/lane/lane-child-invocation.ts +1 -0
- package/scripts/lane/lane-cli.ts +15 -1
- package/scripts/lane/lane-close.ts +15 -6
- package/scripts/lane/lane-dispatch.ts +36 -10
- package/scripts/lane/lane-environment.ts +1 -1
- package/scripts/lane/lane-launch-cli.ts +13 -4
- package/scripts/lane/lane-ledger.ts +21 -3
- package/scripts/lane/lane-measure-cli.ts +10 -2
- package/scripts/lane/lane-open.ts +9 -1
- package/scripts/lane/lane-preserve.ts +49 -0
- package/scripts/lane/lane-reap.ts +20 -5
- package/scripts/lane/lane-registry.ts +6 -0
- package/scripts/lane/lane-restart-cli.ts +51 -0
- package/scripts/lane/lane-restart.ts +175 -0
- package/scripts/lane/{openai-review-broker.ts → lane-session-broker.ts} +106 -28
- package/scripts/lane/lane-spent.ts +195 -0
- package/scripts/lane/lane-stage-stats.ts +27 -3
- package/scripts/lane/lane-stop-stats.ts +96 -0
- package/scripts/lane/openai-lane-supervisor.ts +2 -2
- package/scripts/lane/process-tools.ts +31 -0
- package/scripts/lib/buffered-process.ts +36 -2
- package/scripts/lib/pnpm-reach.ts +27 -0
- package/scripts/lib/status-icons.ts +5 -1
- package/scripts/lint/lint-parallel.ts +19 -2
- package/scripts/observations/observation-ledger-home.ts +1 -0
- package/scripts/review/review-attest-cli.ts +21 -6
- package/scripts/review/review-brief-cli.ts +2 -2
- package/scripts/review/review-comment-round.ts +114 -0
- package/scripts/review/review-record-cli.ts +39 -17
- package/scripts/rules/implementation-cut.ts +3 -3
- package/scripts/rules/pre-gate-cut.ts +1 -1
- package/scripts/run/board/run-board-checks.ts +42 -0
- package/scripts/run/board/run-board-cli.ts +2 -0
- package/scripts/run/board/run-board-every.ts +3 -1
- package/scripts/run/board/run-board-fit.ts +16 -5
- package/scripts/run/board/run-board-fresh.ts +64 -0
- package/scripts/run/board/run-board-header.ts +129 -53
- package/scripts/run/board/run-board-labels.ts +47 -4
- package/scripts/run/board/run-board-lane-event.ts +66 -0
- package/scripts/run/board/run-board-layout.ts +57 -9
- package/scripts/run/board/run-board-now.ts +234 -0
- package/scripts/run/board/run-board-operation-read.ts +75 -0
- package/scripts/run/board/run-board-operation.ts +150 -0
- package/scripts/run/board/run-board-phase.ts +26 -1
- package/scripts/run/board/run-board-plan.ts +4 -6
- package/scripts/run/board/run-board-read.ts +49 -3
- package/scripts/run/board/run-board-reason.ts +124 -0
- package/scripts/run/board/run-board-render-checks.ts +55 -0
- package/scripts/run/board/run-board-render-legend.ts +12 -3
- package/scripts/run/board/run-board-render-slot.ts +2 -2
- package/scripts/run/board/run-board-render-tail.ts +128 -0
- package/scripts/run/board/run-board-render-thanks.ts +50 -0
- package/scripts/run/board/run-board-render-title.ts +58 -0
- package/scripts/run/board/run-board-render.ts +96 -80
- package/scripts/run/board/run-board-row-yield.ts +32 -0
- package/scripts/run/board/run-board-setup.ts +61 -13
- package/scripts/run/board/run-board-state.ts +10 -0
- package/scripts/run/board/run-board-status.ts +66 -7
- package/scripts/run/board/run-board-thanks-csv.ts +71 -0
- package/scripts/run/board/run-board-thanks-read.ts +112 -0
- package/scripts/run/board/run-board-thanks.ts +53 -0
- package/scripts/run/board/run-board-tick.ts +50 -21
- package/scripts/run/board/run-board-usual.ts +44 -0
- package/scripts/run/carry/run-carry-args.ts +9 -1
- package/scripts/run/carry/run-carry-cli.ts +2 -45
- package/scripts/run/carry/run-carry-end.ts +73 -0
- package/scripts/run/carry/run-carry.ts +8 -1
- package/scripts/run/entry/run-entry-cli.ts +5 -0
- package/scripts/run/event/run-event-checks.ts +54 -0
- package/scripts/run/event/run-event-render.ts +2 -0
- package/scripts/run/event/run-event-scope.ts +1 -0
- package/scripts/run/event/run-event-setup.ts +55 -11
- package/scripts/run/event/run-event-stream.ts +27 -11
- package/scripts/run/merge/run-merge-cli.ts +7 -4
- package/scripts/run/merge/run-merge-ship.ts +61 -0
- package/scripts/run/merge/run-merge-steps.ts +7 -14
- package/scripts/run/merge/run-merge-token.ts +16 -2
- package/scripts/run/run-headless.ts +1 -0
- package/scripts/run/run-step.ts +2 -0
- package/scripts/run/ship/run-ship-cli.ts +22 -2
- package/scripts/run/ship/run-ship-return.ts +31 -13
- package/scripts/run/ship/run-ship-review-steps.ts +17 -4
- package/scripts/run/ship/run-ship-stage.ts +1 -0
- package/scripts/run/wake/run-wake-await.ts +130 -0
- package/scripts/run/wake/run-wake-cli.ts +60 -71
- package/scripts/run/wake/run-wake-describe.ts +34 -2
- package/scripts/run/wake/run-wake-driver.ts +11 -4
- package/scripts/run/wake/run-wake-session.ts +20 -3
- package/scripts/run/wake/run-wake-stop.ts +37 -0
- package/scripts/run/wake/run-wake-supervise.ts +44 -0
- package/scripts/sync/claude-settings-merge.ts +144 -0
- package/scripts/sync/sync-ai-files.ts +28 -3
- package/scripts/sync/sync.ts +15 -0
- package/scripts/test/project-test-script.ts +73 -0
- package/scripts/test/skip-marker.ts +14 -1
- package/scripts/test/test-related.ts +3 -2
- package/scripts/test/test-unit-guard.ts +78 -14
- package/scripts/time-runtime/time-reported-failure.ts +11 -3
- package/scripts/uninstall/distributed-files.ts +92 -0
- package/scripts/uninstall/uninstall-plan.ts +119 -0
- package/scripts/uninstall/uninstall-report.ts +61 -0
- package/scripts/uninstall/uninstall.ts +206 -0
- package/templates/AGENTS.basic.md +4 -0
- package/templates/GEMINI.basic.md +6 -0
- package/dist/hooks/chunk-6V5OPIUU.js +0 -13
- package/scripts/run/board/run-board-track.ts +0 -22
|
@@ -5,7 +5,7 @@ import { backlog_plan_read, type OpenListing, type Plan } from '#scripts/backlog
|
|
|
5
5
|
import { backlog_waves } from '#scripts/backlog/backlog-waves'
|
|
6
6
|
import type { EpicChild } from '#scripts/epic/epic-graph'
|
|
7
7
|
import type { OpenIssueData } from '#scripts/git/git-schemas'
|
|
8
|
-
import type
|
|
8
|
+
import { run_board_layout, type BoardPlan } from './run-board-layout'
|
|
9
9
|
|
|
10
10
|
// The plan `run:board` lays the run over, scoped to what the run was asked to do.
|
|
11
11
|
// **`--only`** draws the named issues alone — a named epic expanded into its children; **a named prefix
|
|
@@ -56,6 +56,7 @@ async function board_of(
|
|
|
56
56
|
tracked: plan.tracked,
|
|
57
57
|
context: backlog_plan_read.context_of(plan, read.listing),
|
|
58
58
|
labels: labels_of(read.listing.rows),
|
|
59
|
+
declared: plan.scope.declared,
|
|
59
60
|
}
|
|
60
61
|
}
|
|
61
62
|
|
|
@@ -75,10 +76,6 @@ async function named_board(named: NamedPlan, read: PlanRead): Promise<BoardPlan
|
|
|
75
76
|
return await board_of({ kind: 'read', issues, cutoff: 'none' }, [], read)
|
|
76
77
|
}
|
|
77
78
|
|
|
78
|
-
function children_of(plan: BoardPlan): Array<EpicChild> {
|
|
79
|
-
return [...plan.waves.waves.flat(), ...plan.waves.unreached]
|
|
80
|
-
}
|
|
81
|
-
|
|
82
79
|
function not_held(children: ReadonlyArray<EpicChild>, held: ReadonlySet<number>): Array<EpicChild> {
|
|
83
80
|
return children.filter((child) => !held.has(child.number))
|
|
84
81
|
}
|
|
@@ -86,7 +83,7 @@ function not_held(children: ReadonlyArray<EpicChild>, held: ReadonlySet<number>)
|
|
|
86
83
|
// The named waves lead and the pool's follow, a child the named plan already holds left out of the pool
|
|
87
84
|
// so a named epic's children are not drawn twice.
|
|
88
85
|
function joined(lead: BoardPlan, pool: BoardPlan): BoardPlan {
|
|
89
|
-
const held =
|
|
86
|
+
const held = run_board_layout.numbers_of(lead)
|
|
90
87
|
|
|
91
88
|
return {
|
|
92
89
|
waves: {
|
|
@@ -96,6 +93,7 @@ function joined(lead: BoardPlan, pool: BoardPlan): BoardPlan {
|
|
|
96
93
|
tracked: new Map([...pool.tracked, ...lead.tracked]),
|
|
97
94
|
context: pool.context,
|
|
98
95
|
labels: pool.labels,
|
|
96
|
+
declared: new Map([...(pool.declared ?? []), ...(lead.declared ?? [])]),
|
|
99
97
|
}
|
|
100
98
|
}
|
|
101
99
|
|
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
import type { NamedPlan } from '#scripts/backlog/backlog-plan'
|
|
2
2
|
import { backlogrun_launch } from '#scripts/backlog/backlogrun-launch'
|
|
3
|
-
import {
|
|
3
|
+
import { lane_capacity } from '#scripts/lane/lane-capacity'
|
|
4
|
+
import { lane_registry, type LaneInfo } from '#scripts/lane/lane-registry'
|
|
4
5
|
import { run_carry } from '#scripts/run/carry/run-carry'
|
|
5
6
|
import { run_carry_ended } from '#scripts/run/carry/run-carry-ended'
|
|
6
7
|
import { run_event_stream, type RunEvent } from '#scripts/run/event/run-event-stream'
|
|
7
8
|
import { run_stranded_detect } from '#scripts/run/run-stranded-detect'
|
|
9
|
+
import { run_wake } from '#scripts/run/wake/run-wake'
|
|
10
|
+
import {
|
|
11
|
+
run_board_operation_read,
|
|
12
|
+
type OperationSources,
|
|
13
|
+
type SessionOperations,
|
|
14
|
+
} from './run-board-operation-read'
|
|
8
15
|
import { run_board_scope } from './run-board-scope'
|
|
9
16
|
import { run_board_span, type BoardSpan } from './run-board-span'
|
|
17
|
+
import { run_board_usual, type UsualTimes } from './run-board-usual'
|
|
10
18
|
|
|
11
19
|
// What `run:board` reads locally each tick: the run's span and scope, its events and its open lanes.
|
|
12
20
|
// The run is the running one, or the one that last ended until the next run begins.
|
|
@@ -23,11 +31,30 @@ interface LocalRead {
|
|
|
23
31
|
// The run is carried but nothing drives it — its owner and its supervisor are both gone
|
|
24
32
|
// (`run-stranded.ts`). Drawn as a stopped run, since no one is looking for its next issue.
|
|
25
33
|
is_stranded: boolean
|
|
34
|
+
// The newest operation of each session of the run, read off the end of its log.
|
|
35
|
+
operations?: SessionOperations | undefined
|
|
36
|
+
// How many more lanes the limit allows beside the live ones; `undefined` where the limit could not
|
|
37
|
+
// be read, so nothing is said about it.
|
|
38
|
+
free_lanes?: number | undefined
|
|
39
|
+
// How long each phase usually takes, from the repository's lane ledger.
|
|
40
|
+
usual?: UsualTimes | undefined
|
|
26
41
|
}
|
|
27
42
|
|
|
43
|
+
async function free_lanes_of(lanes: ReadonlyArray<LaneInfo>): Promise<number | undefined> {
|
|
44
|
+
const choice = await lane_capacity.lane_limit()
|
|
45
|
+
|
|
46
|
+
if (choice.kind !== 'limit') return undefined
|
|
47
|
+
|
|
48
|
+
return lane_capacity.free_lanes(choice.limit, lane_registry.live_count(lanes))
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const NO_SPAN: SpanHere = { span: undefined, resume: undefined, transcript: undefined }
|
|
52
|
+
|
|
28
53
|
interface SpanHere {
|
|
29
54
|
span: BoardSpan | undefined
|
|
30
55
|
resume: string | undefined
|
|
56
|
+
// The parent session's transcript, where the run's record names one.
|
|
57
|
+
transcript: string | undefined
|
|
31
58
|
}
|
|
32
59
|
|
|
33
60
|
function span_here(repository: string): SpanHere {
|
|
@@ -38,14 +65,30 @@ function span_here(repository: string): SpanHere {
|
|
|
38
65
|
return {
|
|
39
66
|
span: run_board_span.span_of(read, ended, launch),
|
|
40
67
|
resume: run_board_span.resume_of(read, ended, launch),
|
|
68
|
+
transcript: read.kind === 'carried' ? read.carry.owner_transcript : undefined,
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Where each session of the run writes what it does: a lane's child to its dispatch log, in the lane's
|
|
73
|
+
// own checkout; the parent and a judgment session to theirs, in this one.
|
|
74
|
+
function sources_of(
|
|
75
|
+
repository: string,
|
|
76
|
+
lanes: ReadonlyArray<LaneInfo>,
|
|
77
|
+
transcript: string | undefined,
|
|
78
|
+
): OperationSources {
|
|
79
|
+
const root = process.cwd()
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
lanes: lanes.map((lane) => ({ issue: lane.issue, log: lane.output, root: lane.directory })),
|
|
83
|
+
owner: { log: transcript, root },
|
|
84
|
+
judgment: { log: run_wake.wake_log_path(repository), root },
|
|
41
85
|
}
|
|
42
86
|
}
|
|
43
87
|
|
|
44
88
|
// `undefined` when no run has started here.
|
|
45
89
|
async function read_local(): Promise<LocalRead | undefined> {
|
|
46
90
|
const repository = await run_carry.repository_directory()
|
|
47
|
-
const { span, resume } =
|
|
48
|
-
repository === undefined ? { span: undefined, resume: undefined } : span_here(repository)
|
|
91
|
+
const { span, resume, transcript } = repository === undefined ? NO_SPAN : span_here(repository)
|
|
49
92
|
|
|
50
93
|
if (repository === undefined || span === undefined) return undefined
|
|
51
94
|
|
|
@@ -60,6 +103,9 @@ async function read_local(): Promise<LocalRead | undefined> {
|
|
|
60
103
|
lanes: lanes.map((lane) => lane.issue),
|
|
61
104
|
resume,
|
|
62
105
|
is_stranded: run_stranded_detect.is_stranded_at(repository),
|
|
106
|
+
operations: run_board_operation_read.read(sources_of(repository, lanes, transcript)),
|
|
107
|
+
free_lanes: await free_lanes_of(lanes),
|
|
108
|
+
usual: run_board_usual.read(repository),
|
|
63
109
|
}
|
|
64
110
|
}
|
|
65
111
|
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { backlog_overlap } from '#scripts/backlog/backlog-overlap'
|
|
2
|
+
import type { EpicChild } from '#scripts/epic/epic-graph'
|
|
3
|
+
import { epic_triage } from '#scripts/epic/epic-triage'
|
|
4
|
+
import { run_event_setup } from '#scripts/run/event/run-event-setup'
|
|
5
|
+
import type { RunEvent } from '#scripts/run/event/run-event-stream'
|
|
6
|
+
import { run_board_layout, type Overlay, type ReasonOf, type RowReason } from './run-board-layout'
|
|
7
|
+
import { run_board_setup } from './run-board-setup'
|
|
8
|
+
|
|
9
|
+
// Why each waiting row waits — pure, so the words are tested apart from the screen. A row whose lane is
|
|
10
|
+
// being opened says the stage that lane is at; any other says what `backlog:next` would answer for it,
|
|
11
|
+
// read off the same rules: `epic_triage` for an untriaged issue, `backlog_overlap` for a declared path a
|
|
12
|
+
// running issue holds, the lane limit for a full pool. A blocker stays the row's `🔗`; only a row that
|
|
13
|
+
// nobody has judged says so beside it, since judging it is what a person can do meanwhile.
|
|
14
|
+
// Every word is English whatever the session language, as every other word on the board is.
|
|
15
|
+
|
|
16
|
+
const { LANE_STAGE } = run_event_setup
|
|
17
|
+
const NO_FREE_LANES = 0
|
|
18
|
+
|
|
19
|
+
const WORDS = {
|
|
20
|
+
untriaged: 'untriaged',
|
|
21
|
+
held: 'held by triage',
|
|
22
|
+
no_lane: 'no free lane',
|
|
23
|
+
overlaps: 'overlaps',
|
|
24
|
+
} as const
|
|
25
|
+
|
|
26
|
+
// A lane's stage as its row says it: the event's own words, cut to what fits beside a title.
|
|
27
|
+
const STAGE_WORDS: ReadonlyArray<readonly [stage: string, word: string]> = [
|
|
28
|
+
[LANE_STAGE.OPENING, 'opening lane'],
|
|
29
|
+
[LANE_STAGE.INSTALLING, 'installing'],
|
|
30
|
+
[LANE_STAGE.DISPATCHING, 'dispatching'],
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
// What the reasons are read from, beside the plan and the statuses a layout is drawn from.
|
|
34
|
+
interface ReasonRead extends Overlay {
|
|
35
|
+
events: ReadonlyArray<RunEvent>
|
|
36
|
+
// How many more lanes the limit allows; `undefined` where it could not be read.
|
|
37
|
+
free_lanes: number | undefined
|
|
38
|
+
// A stopped run opens no lane, so a stage its stream ends on is not one any lane is at.
|
|
39
|
+
is_stopped: boolean
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// What every row of one layout is weighed against, read once.
|
|
43
|
+
interface Pool {
|
|
44
|
+
// An untriaged issue nothing blocks withholds every candidate (`epic-triage.ts`).
|
|
45
|
+
is_held: boolean
|
|
46
|
+
running: ReadonlyArray<number>
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function stage_word(text: string): string | undefined {
|
|
50
|
+
return STAGE_WORDS.find(([stage]) => text.startsWith(stage))?.[1]
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function stage_of(child: EpicChild, read: ReasonRead): RowReason | undefined {
|
|
54
|
+
const stage = read.is_stopped ? undefined : run_board_setup.lane_stage(read.events, child.number)
|
|
55
|
+
const text = stage === undefined ? undefined : stage_word(stage.text)
|
|
56
|
+
|
|
57
|
+
if (stage === undefined || text === undefined) return undefined
|
|
58
|
+
|
|
59
|
+
return { text, since_ms: stage.at_ms }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function is_untriaged_candidate(child: EpicChild, read: ReasonRead): boolean {
|
|
63
|
+
return !epic_triage.is_triaged(child) && !run_board_layout.is_blocked(child, read)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function running_of(read: ReasonRead): Array<number> {
|
|
67
|
+
return [...read.statuses]
|
|
68
|
+
.filter(([, status]) => status.state === 'running')
|
|
69
|
+
.map(([issue]) => issue)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function pool_of(read: ReasonRead): Pool {
|
|
73
|
+
const waiting = run_board_layout.waiting_children(read)
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
is_held: waiting.some((child) => is_untriaged_candidate(child, read)),
|
|
77
|
+
running: running_of(read),
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function overlap_of(child: EpicChild, read: ReasonRead, pool: Pool): string | undefined {
|
|
82
|
+
const { declared = new Map(), context } = read.plan
|
|
83
|
+
const holding = { holders: pool.running, repo: context.repo }
|
|
84
|
+
const clash = backlog_overlap.running_clash(child, declared, holding)
|
|
85
|
+
|
|
86
|
+
return clash === undefined
|
|
87
|
+
? undefined
|
|
88
|
+
: `${WORDS.overlaps} ${String(clash.holder)} (${clash.path})`
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// A candidate nothing blocks, in the order `backlog:next` answers: the pool held for a judgment, then
|
|
92
|
+
// a path a running issue declares, then a pool with no lane left.
|
|
93
|
+
function offer_wait(child: EpicChild, read: ReasonRead, pool: Pool): string | undefined {
|
|
94
|
+
if (pool.is_held) return WORDS.held
|
|
95
|
+
|
|
96
|
+
const overlap = overlap_of(child, read, pool)
|
|
97
|
+
|
|
98
|
+
if (overlap !== undefined) return overlap
|
|
99
|
+
|
|
100
|
+
return read.free_lanes === NO_FREE_LANES ? WORDS.no_lane : undefined
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function wait_of(child: EpicChild, read: ReasonRead, pool: Pool): RowReason | undefined {
|
|
104
|
+
if (!epic_triage.is_triaged(child)) return { text: WORDS.untriaged }
|
|
105
|
+
if (run_board_layout.is_blocked(child, read)) return undefined
|
|
106
|
+
|
|
107
|
+
const text = offer_wait(child, read, pool)
|
|
108
|
+
|
|
109
|
+
return text === undefined ? undefined : { text }
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// What each waiting row of one layout says, the pool-wide facts read once for all of them.
|
|
113
|
+
function reader(read: ReasonRead): ReasonOf {
|
|
114
|
+
const pool = pool_of(read)
|
|
115
|
+
|
|
116
|
+
return function reason_of(child: EpicChild): RowReason | undefined {
|
|
117
|
+
return stage_of(child, read) ?? wait_of(child, read, pool)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const run_board_reason = { reader }
|
|
122
|
+
|
|
123
|
+
export { run_board_reason }
|
|
124
|
+
export type { ReasonRead }
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { styleText } from 'node:util'
|
|
2
|
+
import type { CodeRabbitState, PrChecks } from '#scripts/run/event/run-event-checks'
|
|
3
|
+
import type { TextColor } from './run-board-labels'
|
|
4
|
+
|
|
5
|
+
// A followup row's pull request (joshuafolkken/kit#3770): its number, the CI checks counted by how
|
|
6
|
+
// they stand and CodeRabbit's state — `PR 3765 ✓ 6 ⏳ 2 🐰 ⏳`. A count of none is left out, a
|
|
7
|
+
// failure is red and a rate limit yellow, so what needs a person stands out of a row of green.
|
|
8
|
+
|
|
9
|
+
const GAP = ' '
|
|
10
|
+
const CHECK_ICONS = { pass: '✓', pending: '⏳', fail: '✗' } as const
|
|
11
|
+
const CODERABBIT_ICON = '🐰'
|
|
12
|
+
const CODERABBIT_MARKS: Readonly<Record<CodeRabbitState, string | undefined>> = {
|
|
13
|
+
running: CHECK_ICONS.pending,
|
|
14
|
+
done: CHECK_ICONS.pass,
|
|
15
|
+
'rate-limited': '⛔',
|
|
16
|
+
failed: CHECK_ICONS.fail,
|
|
17
|
+
none: undefined,
|
|
18
|
+
}
|
|
19
|
+
const CODERABBIT_COLORS: Readonly<Partial<Record<CodeRabbitState, TextColor>>> = {
|
|
20
|
+
'rate-limited': 'yellow',
|
|
21
|
+
failed: 'red',
|
|
22
|
+
}
|
|
23
|
+
// What the legend says of the part, once some row draws it.
|
|
24
|
+
const LEGEND = [
|
|
25
|
+
`${CHECK_ICONS.pass} ${CHECK_ICONS.pending} ${CHECK_ICONS.fail} checks passed · pending · failed`,
|
|
26
|
+
`${CODERABBIT_ICON} CodeRabbit (${CODERABBIT_MARKS['rate-limited'] ?? ''} rate-limited)`,
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
function tinted(text: string, color: TextColor | undefined): string {
|
|
30
|
+
return color === undefined ? text : styleText(color, text)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function count_part(icon: string, count: number, color?: TextColor): Array<string> {
|
|
34
|
+
return count === 0 ? [] : [tinted(`${icon} ${String(count)}`, color)]
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function coderabbit_part(state: CodeRabbitState): Array<string> {
|
|
38
|
+
const mark = CODERABBIT_MARKS[state]
|
|
39
|
+
|
|
40
|
+
return mark === undefined ? [] : [tinted(`${CODERABBIT_ICON} ${mark}`, CODERABBIT_COLORS[state])]
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function checks_part(checks: PrChecks): string {
|
|
44
|
+
return [
|
|
45
|
+
`PR ${String(checks.pr)}`,
|
|
46
|
+
...count_part(CHECK_ICONS.pass, checks.pass),
|
|
47
|
+
...count_part(CHECK_ICONS.pending, checks.pending),
|
|
48
|
+
...count_part(CHECK_ICONS.fail, checks.fail, 'red'),
|
|
49
|
+
...coderabbit_part(checks.coderabbit),
|
|
50
|
+
].join(GAP)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const run_board_render_checks = { LEGEND, checks_part }
|
|
54
|
+
|
|
55
|
+
export { run_board_render_checks }
|
|
@@ -15,7 +15,7 @@ const { FILED_KIND_ICONS, KIND_WORDS, PHASE_ICONS, PHASE_WORDS, STATE_ICONS, WAI
|
|
|
15
15
|
run_board_labels
|
|
16
16
|
const GAP = ' '
|
|
17
17
|
|
|
18
|
-
// Every phase a run passes through, in the
|
|
18
|
+
// Every phase a run passes through, in the order a run passes them and whatever is on screen: a row's phase
|
|
19
19
|
// moves between redraws, so fixed lines keep each icon where the eye last found it — six to a line, so
|
|
20
20
|
// the twelve fit a narrow pane.
|
|
21
21
|
const PHASES_PER_LINE = 6
|
|
@@ -59,17 +59,26 @@ function waits_legend(rows: ReadonlyArray<BoardRow>): Array<string> {
|
|
|
59
59
|
|
|
60
60
|
// The phases on their lines and what the screen draws on one more, drawn only when it names something.
|
|
61
61
|
// The header's gauges and marks read by their place, so the legend leaves them.
|
|
62
|
-
// `drawn` is the icons the rows lead with
|
|
62
|
+
// `drawn` is the icons the rows lead with; `named` the activity kinds the header's third line and the
|
|
63
|
+
// rows' tails draw, each named once, where their own words do not name them (joshuafolkken/kit#3767,
|
|
64
|
+
// joshuafolkken/kit#3769).
|
|
63
65
|
function legend_of(
|
|
64
66
|
rows: ReadonlyArray<BoardRow>,
|
|
65
67
|
drawn: ReadonlySet<string>,
|
|
66
68
|
notes: ReadonlyArray<BoardNote>,
|
|
69
|
+
named: ReadonlyArray<string> = [],
|
|
67
70
|
): Array<string> {
|
|
68
71
|
const kinds = kinds_of(rows)
|
|
69
72
|
const kind_icons = kinds.map((kind) => FILED_KIND_ICONS[kind])
|
|
70
73
|
const notes_named = run_board_render_notes.notes_legend(notes, new Set([...drawn, ...kind_icons]))
|
|
71
74
|
const kind_named = kinds.map((kind) => `${FILED_KIND_ICONS[kind]} ${WORDS[KIND_WORDS[kind]]}`)
|
|
72
|
-
const shown = [
|
|
75
|
+
const shown = [
|
|
76
|
+
...new Set(named),
|
|
77
|
+
...state_legend(drawn),
|
|
78
|
+
...kind_named,
|
|
79
|
+
...waits_legend(rows),
|
|
80
|
+
...notes_named,
|
|
81
|
+
]
|
|
73
82
|
const lines = shown.length === 0 ? PHASE_LEGEND : [...PHASE_LEGEND, shown.join(GAP)]
|
|
74
83
|
|
|
75
84
|
return lines.map((line) => styleText('dim', line))
|
|
@@ -4,8 +4,8 @@ import type { ItemState } from './run-board-status'
|
|
|
4
4
|
import type { LaneUsages } from './run-board-usage'
|
|
5
5
|
import { run_board_usage_text } from './run-board-usage-text'
|
|
6
6
|
|
|
7
|
-
// The column between a row's time and its
|
|
8
|
-
//
|
|
7
|
+
// The column between a row's time and its tail, one width on every row it is drawn on, so every
|
|
8
|
+
// tail starts in one column: a running row's CPU and memory, a finished row's finish time, and
|
|
9
9
|
// blanks on any other row.
|
|
10
10
|
|
|
11
11
|
const { finish_of, text_of } = run_board_usage_text
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { styleText } from 'node:util'
|
|
2
|
+
import { backlog_budget } from '#scripts/backlog/backlog-budget'
|
|
3
|
+
import type { PrChecks } from '#scripts/run/event/run-event-checks'
|
|
4
|
+
import { run_board_header, type BoardHeader } from './run-board-header'
|
|
5
|
+
import { run_board_labels, type TextColor } from './run-board-labels'
|
|
6
|
+
import type { BoardRow } from './run-board-layout'
|
|
7
|
+
import { run_board_now, type TimedLine } from './run-board-now'
|
|
8
|
+
import type { Phase } from './run-board-phase'
|
|
9
|
+
import { run_board_render_checks } from './run-board-render-checks'
|
|
10
|
+
|
|
11
|
+
// The end of a running row (joshuafolkken/kit#3769): how long the child has stood at the phase its row
|
|
12
|
+
// leads with, then the kind and the age of the newest thing it did. Both are the row's own `MM:SS`, the
|
|
13
|
+
// minutes never carried into hours (`61:05`). The phase's time is right-aligned to the widest any
|
|
14
|
+
// running row draws, so a three-digit `125:30` and a `00:40` end in one column and the age after them
|
|
15
|
+
// starts in one column whichever row it is on — where the phase track before them grew with every
|
|
16
|
+
// round. A settled or a waiting row draws neither.
|
|
17
|
+
|
|
18
|
+
const { NOW_ICONS, elapsed_of } = run_board_labels
|
|
19
|
+
// A phase past twice its usual time is drawn as long, past three times as alarming.
|
|
20
|
+
const LONG_FACTOR = 2
|
|
21
|
+
const ALARM_FACTOR = 3
|
|
22
|
+
// The least a usual time counts as: the ledger's median for a stage of seconds (preflight, sync,
|
|
23
|
+
// report) is near zero, and scaled as it is it would draw a healthy few-second stage as alarming.
|
|
24
|
+
const USUAL_FLOOR_MINUTES = 2
|
|
25
|
+
const USUAL_FLOOR_MS = USUAL_FLOOR_MINUTES * backlog_budget.MS_PER_MINUTE
|
|
26
|
+
|
|
27
|
+
function phase_color(elapsed_ms: number, usual_ms: number | undefined): TextColor | undefined {
|
|
28
|
+
if (usual_ms === undefined) return undefined
|
|
29
|
+
const usual = Math.max(usual_ms, USUAL_FLOOR_MS)
|
|
30
|
+
|
|
31
|
+
if (elapsed_ms <= usual * LONG_FACTOR) return undefined
|
|
32
|
+
|
|
33
|
+
return elapsed_ms > usual * ALARM_FACTOR ? 'red' : 'yellow'
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function tinted(text: string, color: TextColor | undefined): string {
|
|
37
|
+
return color === undefined ? text : styleText(color, text)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// The phase the row leads with and when the child got there, `undefined` where no event said when.
|
|
41
|
+
function current_of(row: BoardRow): { phase: Phase; since_ms: number } | undefined {
|
|
42
|
+
const { track = [], phase_ms } = row.status ?? {}
|
|
43
|
+
const phase = track.at(-1)
|
|
44
|
+
|
|
45
|
+
return phase === undefined || phase_ms === undefined ? undefined : { phase, since_ms: phase_ms }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// The phase column's width: the widest phase time any running row draws in this frame, and `MM:SS`'s
|
|
49
|
+
// at the least, so a row with none still blanks a column's width.
|
|
50
|
+
function phase_width(header: BoardHeader): number {
|
|
51
|
+
const spans = (header.layout?.active ?? []).map((row) => {
|
|
52
|
+
const current = row.state === 'running' ? current_of(row) : undefined
|
|
53
|
+
|
|
54
|
+
return current === undefined ? 0 : elapsed_of(header.now_ms - current.since_ms).length
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
return Math.max(elapsed_of(0).length, ...spans)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// How long the child has stood at its phase, colored against the phase's usual time; blanks where no
|
|
61
|
+
// event said when it got there, so the age after it still starts in its column.
|
|
62
|
+
function phase_part(row: BoardRow, header: BoardHeader): string {
|
|
63
|
+
const current = current_of(row)
|
|
64
|
+
const width = phase_width(header)
|
|
65
|
+
|
|
66
|
+
if (current === undefined) return ' '.repeat(width)
|
|
67
|
+
|
|
68
|
+
const elapsed = header.now_ms - current.since_ms
|
|
69
|
+
const text = elapsed_of(elapsed).padStart(width)
|
|
70
|
+
|
|
71
|
+
return tinted(text, phase_color(elapsed, header.usual?.get(current.phase)))
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// The newer of the child's newest operation and its newest event, read as the header's third line
|
|
75
|
+
// reads it, so the two never disagree on what is newest.
|
|
76
|
+
function newest_of(row: BoardRow, header: BoardHeader): TimedLine | undefined {
|
|
77
|
+
return run_board_now.newest_of({
|
|
78
|
+
operations: header.operations,
|
|
79
|
+
lane_events: header.activity.lane_events,
|
|
80
|
+
children: new Set([row.number]),
|
|
81
|
+
})
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// The newest activity as its kind's icon and its age, aged as the `💓` is.
|
|
85
|
+
function activity_part(row: BoardRow, header: BoardHeader): string | undefined {
|
|
86
|
+
const newest = newest_of(row, header)
|
|
87
|
+
|
|
88
|
+
if (newest?.since_ms === undefined) return undefined
|
|
89
|
+
|
|
90
|
+
const age = header.now_ms - newest.since_ms
|
|
91
|
+
|
|
92
|
+
return run_board_header.aged(`${newest.icon} ${elapsed_of(age)}`, age)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// A row in followup with its pull request's checks on the stream (joshuafolkken/kit#3770), whose end
|
|
96
|
+
// draws them in place of its newest activity.
|
|
97
|
+
function checks_of(row: BoardRow, header: BoardHeader): PrChecks | undefined {
|
|
98
|
+
if (row.state !== 'running' || current_of(row)?.phase !== 'followup') return undefined
|
|
99
|
+
|
|
100
|
+
return header.activity.pr_checks?.get(row.number)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// A running row's end, nothing for any other.
|
|
104
|
+
function tail_of(row: BoardRow, header: BoardHeader): Array<string | undefined> {
|
|
105
|
+
if (row.state !== 'running') return []
|
|
106
|
+
|
|
107
|
+
const checks = checks_of(row, header)
|
|
108
|
+
const last =
|
|
109
|
+
checks === undefined ? activity_part(row, header) : run_board_render_checks.checks_part(checks)
|
|
110
|
+
|
|
111
|
+
return [phase_part(row, header), last]
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Whether a row draws its pull request's checks, so the legend names them.
|
|
115
|
+
function is_checks_drawn(row: BoardRow, header: BoardHeader): boolean {
|
|
116
|
+
return checks_of(row, header) !== undefined
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Whether a row draws the operation's icon, so the legend names it.
|
|
120
|
+
function is_operation_drawn(row: BoardRow, header: BoardHeader): boolean {
|
|
121
|
+
if (row.state !== 'running' || is_checks_drawn(row, header)) return false
|
|
122
|
+
|
|
123
|
+
return newest_of(row, header)?.icon === NOW_ICONS.operation
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const run_board_render_tail = { is_checks_drawn, is_operation_drawn, tail_of }
|
|
127
|
+
|
|
128
|
+
export { run_board_render_tail }
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { run_board_fit } from './run-board-fit'
|
|
2
|
+
import type { Link } from './run-board-link'
|
|
3
|
+
import type { Thanks } from './run-board-thanks-csv'
|
|
4
|
+
|
|
5
|
+
// The reviewers who helped kit, drawn above the legend as names led by one 🙏 — no label, no dimming. A name
|
|
6
|
+
// with a page opens it on Cmd+click through the same OSC 8 link the issue numbers use; one without
|
|
7
|
+
// is plain text. The names run on one line and wrap onto the next where the pane is too narrow,
|
|
8
|
+
// always between two names; a frame no terminal bounds draws them on one line.
|
|
9
|
+
|
|
10
|
+
const GAP = ' '
|
|
11
|
+
const INDENT = ' '
|
|
12
|
+
const MARK = '🙏 '
|
|
13
|
+
|
|
14
|
+
function named(thanker: Thanks[number], link: Link): string {
|
|
15
|
+
return thanker.url === undefined ? thanker.name : link(thanker.name, thanker.url)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// The lines with `name` added to the last where it still fits, and on a line of its own where it does not.
|
|
19
|
+
function placed(lines: ReadonlyArray<string>, name: string, columns: number): Array<string> {
|
|
20
|
+
const last = lines.at(-1)
|
|
21
|
+
const joined = `${last ?? ''}${GAP}${name}`
|
|
22
|
+
|
|
23
|
+
if (last !== undefined && run_board_fit.width_of(joined) <= columns) {
|
|
24
|
+
return [...lines.slice(0, -1), joined]
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return [...lines, `${INDENT}${name}`]
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// The names packed into lines no wider than `columns`; a name wider than the pane alone takes a line.
|
|
31
|
+
function wrapped(names: ReadonlyArray<string>, columns: number): Array<string> {
|
|
32
|
+
let lines: Array<string> = []
|
|
33
|
+
|
|
34
|
+
for (const name of names) lines = placed(lines, name, columns)
|
|
35
|
+
|
|
36
|
+
return lines
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// The lines under a blank that parts them from what is above; none with no one to thank.
|
|
40
|
+
function thanks_lines(thanks: Thanks, link: Link, columns: number | undefined): Array<string> {
|
|
41
|
+
if (thanks.length === 0) return []
|
|
42
|
+
|
|
43
|
+
const [first = '', ...rest] = thanks.map((thanker) => named(thanker, link))
|
|
44
|
+
|
|
45
|
+
return ['', ...wrapped([`${MARK}${first}`, ...rest], columns ?? Infinity)]
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const run_board_render_thanks = { thanks_lines }
|
|
49
|
+
|
|
50
|
+
export { run_board_render_thanks }
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { run_board_fit } from './run-board-fit'
|
|
2
|
+
import { run_board_labels } from './run-board-labels'
|
|
3
|
+
import type { BoardRow } from './run-board-layout'
|
|
4
|
+
|
|
5
|
+
// A row's title and the reason it makes room for — pure. A title is cut to the title column; a row
|
|
6
|
+
// that says why it waits keeps saying it on a narrow pane, its title giving up the columns the line
|
|
7
|
+
// runs over, where every other row wraps.
|
|
8
|
+
|
|
9
|
+
// The columns a terminal row gives its title: a longer one is cut to an ellipsis.
|
|
10
|
+
const TITLE_LIMIT = 48
|
|
11
|
+
// The least a title is cut to where a narrow pane keeps the row's reason: enough to tell rows apart.
|
|
12
|
+
const TITLE_FLOOR = 8
|
|
13
|
+
const ELLIPSIS = '…'
|
|
14
|
+
|
|
15
|
+
// A title cut to the title column, so every time after it starts in one column.
|
|
16
|
+
function title_of(title = '', limit = TITLE_LIMIT): string {
|
|
17
|
+
return title.length > limit ? `${title.slice(0, limit - 1)}${ELLIPSIS}` : title
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Why a waiting row waits, after its blockers; a lane's stage with how long the lane has stood at it.
|
|
21
|
+
function reason_text(row: BoardRow, now_ms: number): string | undefined {
|
|
22
|
+
const { reason } = row
|
|
23
|
+
|
|
24
|
+
if (reason?.since_ms === undefined) return reason?.text
|
|
25
|
+
|
|
26
|
+
return `${reason.text} ${run_board_labels.stage_span_of(now_ms - reason.since_ms)}`
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// The columns a line runs past the pane, for a row that says why it waits; none for any other row, and
|
|
30
|
+
// none for a frame that is not kept within a pane.
|
|
31
|
+
function overrun_of(text: string, row: BoardRow, columns: number | undefined): number {
|
|
32
|
+
if (columns === undefined || row.reason === undefined) return 0
|
|
33
|
+
|
|
34
|
+
return Math.max(0, run_board_fit.width_of(text) - columns)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// The title column once it has given up `overrun` columns, never under the floor: the blanks a short
|
|
38
|
+
// title is padded with go first, so a title that fits keeps whole.
|
|
39
|
+
function narrowed_limit(overrun: number): number {
|
|
40
|
+
return Math.max(TITLE_FLOOR, TITLE_LIMIT - overrun)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// A row's line as `draw` draws it at a title limit: at the title column's, or at a narrower one where
|
|
44
|
+
// that line would run past the pane and the row has a reason to keep.
|
|
45
|
+
function fitted(
|
|
46
|
+
row: BoardRow,
|
|
47
|
+
columns: number | undefined,
|
|
48
|
+
draw: (title_limit: number) => string,
|
|
49
|
+
): string {
|
|
50
|
+
const text = draw(TITLE_LIMIT)
|
|
51
|
+
const overrun = overrun_of(text, row, columns)
|
|
52
|
+
|
|
53
|
+
return overrun === 0 ? text : draw(narrowed_limit(overrun))
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const run_board_render_title = { ELLIPSIS, TITLE_LIMIT, fitted, reason_text, title_of }
|
|
57
|
+
|
|
58
|
+
export { run_board_render_title }
|