@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.
Files changed (236) hide show
  1. package/.claude/skills/epic-commands/SKILL.md +1 -1
  2. package/.claude/skills/epic-commands/epic-bundle.md +5 -0
  3. package/.claude/skills/workflow-commands/backlogrun-lanes.md +5 -3
  4. package/.claude/skills/workflow-commands/backlogrun-steps.md +4 -3
  5. package/.claude/skills/workflow-commands/backlogrun.md +1 -0
  6. package/.claude/skills/workflow-commands/chain-rule.md +4 -2
  7. package/.claude/skills/workflow-commands/issue-comments.md +6 -0
  8. package/.claude/skills/workflow-commands/observation-ledger.md +3 -2
  9. package/.claude/skills/workflow-commands/pre-gate-cut.md +5 -3
  10. package/README.md +25 -0
  11. package/dist/hooks/audit-provision.js +2 -2
  12. package/dist/hooks/batch-guard.js +7 -7
  13. package/dist/hooks/{chunk-IYVOLN6A.js → chunk-2G5F2YCD.js} +3 -3
  14. package/dist/hooks/{chunk-2YSW3CXH.js → chunk-3LE6VLSR.js} +17 -6
  15. package/dist/hooks/{chunk-J3KSS7II.js → chunk-4BHUPFKF.js} +1 -1
  16. package/dist/hooks/{chunk-EAQEGME7.js → chunk-5SJSSCJG.js} +1 -1
  17. package/dist/hooks/{chunk-FZ6FUSBM.js → chunk-723HKZO5.js} +1 -1
  18. package/dist/hooks/{chunk-LMWBRJIP.js → chunk-7KMN5QB5.js} +3 -3
  19. package/dist/hooks/{chunk-NEC4AT4Z.js → chunk-DGT54IH5.js} +4 -0
  20. package/dist/hooks/{chunk-STDSEPWU.js → chunk-EMB55BDF.js} +4 -4
  21. package/dist/hooks/{chunk-JBBMAPC4.js → chunk-IX6TTTI6.js} +4 -0
  22. package/dist/hooks/{chunk-GJ7JPIHN.js → chunk-JXQBK323.js} +2674 -187
  23. package/dist/hooks/{chunk-DCY2GWHC.js → chunk-MSYSUSY6.js} +5 -5
  24. package/dist/hooks/{chunk-EBSHN25W.js → chunk-MU4LJFIV.js} +4 -4
  25. package/dist/hooks/{chunk-WNFJLBXI.js → chunk-QL6QTPWN.js} +3 -3
  26. package/dist/hooks/{chunk-SL4GQ2XP.js → chunk-XER6SSIB.js} +91 -18
  27. package/dist/hooks/{chunk-U32VSRNG.js → chunk-YW452ZAX.js} +89 -34
  28. package/dist/hooks/{chunk-GNDJ7NJ7.js → chunk-YZXYOAZ3.js} +202 -55
  29. package/dist/hooks/codex-hook-adapter.js +15 -15
  30. package/dist/hooks/duplicate-read-guard.js +8 -8
  31. package/dist/hooks/format-edited.js +5 -5
  32. package/dist/hooks/inputs.json +288 -229
  33. package/dist/hooks/investigation-guard.js +8 -8
  34. package/dist/hooks/pretool-guard.js +14 -14
  35. package/dist/hooks/session-lang.js +69 -16
  36. package/dist/hooks/stop-guard.js +13 -38
  37. package/dist/josh.js +62 -9
  38. package/package.json +4 -3
  39. package/prompts/review.md +22 -0
  40. package/scripts/agent/claude-agent-argv.ts +13 -1
  41. package/scripts/auth/auth-setup-cli.ts +101 -0
  42. package/scripts/auth/claude-token-output.ts +78 -0
  43. package/scripts/auth/claude-token.ts +68 -0
  44. package/scripts/backlog/backlog-drive-epic.ts +24 -8
  45. package/scripts/backlog/backlog-drive-launch.ts +3 -0
  46. package/scripts/backlog/backlog-drive-named-offer.ts +7 -2
  47. package/scripts/backlog/backlog-drive-named.ts +4 -2
  48. package/scripts/backlog/backlog-drive-unread.ts +56 -0
  49. package/scripts/backlog/backlog-drive.ts +16 -8
  50. package/scripts/backlog/backlog-overlap.ts +22 -5
  51. package/scripts/backlog/backlog-ready.ts +1 -3
  52. package/scripts/backlog/backlogrun-args.ts +13 -2
  53. package/scripts/backlog/backlogrun-cli.ts +91 -19
  54. package/scripts/backlog/backlogrun-launch.ts +24 -0
  55. package/scripts/backlog/backlogrun-replace.ts +166 -0
  56. package/scripts/backlog/backlogrun-stop.ts +178 -0
  57. package/scripts/behavior/behavior-cli.ts +6 -1
  58. package/scripts/doctor/doctor-io.ts +60 -9
  59. package/scripts/doctor/doctor.ts +20 -1
  60. package/scripts/document/document-byte-budget.ts +3 -3
  61. package/scripts/epic/epic-bundle-cli.ts +8 -8
  62. package/scripts/epic/epic-bundle-referenced.ts +2 -5
  63. package/scripts/epic/epic-bundle.ts +39 -4
  64. package/scripts/exports/unused-members-cli.ts +2 -1
  65. package/scripts/followup/followup-checks-event.ts +82 -0
  66. package/scripts/followup/git-followup-finish.ts +16 -2
  67. package/scripts/followup/git-followup-workflow.ts +5 -2
  68. package/scripts/followup/git-pr-followup-checks.ts +18 -4
  69. package/scripts/followup/git-pr-followup-wrapup.ts +5 -0
  70. package/scripts/followup/git-pr-followup.ts +4 -1
  71. package/scripts/fresh-machine/fresh-machine-cli.ts +204 -0
  72. package/scripts/fresh-machine/fresh-machine-docker.ts +65 -0
  73. package/scripts/fresh-machine/fresh-machine-image.ts +62 -0
  74. package/scripts/fresh-machine/fresh-machine-log.ts +61 -0
  75. package/scripts/fresh-machine/fresh-machine-patterns.ts +130 -0
  76. package/scripts/fresh-machine/fresh-machine-run.ts +215 -0
  77. package/scripts/fresh-machine/fresh-machine-script.ts +45 -0
  78. package/scripts/fresh-machine/fresh-machine-spec.ts +57 -0
  79. package/scripts/fresh-machine/specs/kit-test-backlogrun-python.json +34 -0
  80. package/scripts/fresh-machine/specs/kit-test-backlogrun.json +31 -0
  81. package/scripts/gate/gate-report.ts +70 -15
  82. package/scripts/gate/project-checks.ts +13 -2
  83. package/scripts/gate/scoped-green.ts +2 -1
  84. package/scripts/gate/verification-gate.ts +4 -3
  85. package/scripts/gh/gh-author-association.ts +14 -0
  86. package/scripts/gh/git-gh-conditional.ts +66 -0
  87. package/scripts/gh/git-gh-exec.ts +1 -1
  88. package/scripts/gh/git-gh-issue-read.ts +6 -3
  89. package/scripts/gh/git-gh-issue-write.ts +5 -1
  90. package/scripts/gh/git-gh-issue.ts +4 -1
  91. package/scripts/gh/git-pr-checks-eval.ts +13 -0
  92. package/scripts/gh/git-pr-checks-parse.ts +8 -1
  93. package/scripts/gh/git-pr-checks-watch.ts +11 -4
  94. package/scripts/gh/git-pr-checks.ts +36 -2
  95. package/scripts/git/git-command.ts +16 -4
  96. package/scripts/git/git-schemas.ts +1 -0
  97. package/scripts/git/git-spawn-sync.ts +1 -1
  98. package/scripts/hooks/pretool-guard.ts +3 -1
  99. package/scripts/init/hook-command-rewrite.ts +31 -0
  100. package/scripts/init/init-actions.ts +4 -1
  101. package/scripts/init/init-ai-copy.ts +33 -10
  102. package/scripts/init/init-basic.ts +8 -3
  103. package/scripts/init/init-logic-toolchain.ts +76 -0
  104. package/scripts/init/init-logic.ts +2 -0
  105. package/scripts/init/init.ts +5 -23
  106. package/scripts/init/project-profile.ts +36 -7
  107. package/scripts/init/start-exec.ts +27 -1
  108. package/scripts/init/start-plan.ts +15 -0
  109. package/scripts/init/start-steps.ts +46 -0
  110. package/scripts/init/start.ts +1 -0
  111. package/scripts/init/workflow-mark.ts +35 -0
  112. package/scripts/issue/issue-ack-cli.ts +84 -0
  113. package/scripts/issue/issue-labels.ts +40 -8
  114. package/scripts/issue/issue-state-cli.ts +14 -4
  115. package/scripts/issue/issue-watch-diff.ts +152 -0
  116. package/scripts/issue/issue-watch-gate.ts +81 -0
  117. package/scripts/issue/issue-watch-hook.ts +54 -0
  118. package/scripts/issue/issue-watch.ts +240 -0
  119. package/scripts/issue/measure-rerun-cli.ts +3 -4
  120. package/scripts/josh/josh-command-map.ts +4 -0
  121. package/scripts/josh/josh-commands-backlog.ts +2 -2
  122. package/scripts/josh/josh-commands-development.ts +1 -1
  123. package/scripts/josh/josh-commands-issue.ts +7 -0
  124. package/scripts/josh/josh-commands-lane.ts +11 -2
  125. package/scripts/josh/josh-commands-project.ts +24 -0
  126. package/scripts/josh/josh-verdict.ts +33 -4
  127. package/scripts/lane/claude-lane-broker.ts +142 -0
  128. package/scripts/lane/lane-await.ts +6 -2
  129. package/scripts/lane/lane-cache.ts +37 -7
  130. package/scripts/lane/lane-capacity.ts +8 -6
  131. package/scripts/lane/lane-child-invocation.ts +1 -0
  132. package/scripts/lane/lane-cli.ts +15 -1
  133. package/scripts/lane/lane-close.ts +15 -6
  134. package/scripts/lane/lane-dispatch.ts +36 -10
  135. package/scripts/lane/lane-environment.ts +1 -1
  136. package/scripts/lane/lane-launch-cli.ts +13 -4
  137. package/scripts/lane/lane-ledger.ts +21 -3
  138. package/scripts/lane/lane-measure-cli.ts +10 -2
  139. package/scripts/lane/lane-open.ts +9 -1
  140. package/scripts/lane/lane-preserve.ts +49 -0
  141. package/scripts/lane/lane-reap.ts +20 -5
  142. package/scripts/lane/lane-registry.ts +6 -0
  143. package/scripts/lane/lane-restart-cli.ts +51 -0
  144. package/scripts/lane/lane-restart.ts +175 -0
  145. package/scripts/lane/{openai-review-broker.ts → lane-session-broker.ts} +106 -28
  146. package/scripts/lane/lane-spent.ts +195 -0
  147. package/scripts/lane/lane-stage-stats.ts +27 -3
  148. package/scripts/lane/lane-stop-stats.ts +96 -0
  149. package/scripts/lane/openai-lane-supervisor.ts +2 -2
  150. package/scripts/lane/process-tools.ts +31 -0
  151. package/scripts/lib/buffered-process.ts +36 -2
  152. package/scripts/lib/pnpm-reach.ts +27 -0
  153. package/scripts/lib/status-icons.ts +5 -1
  154. package/scripts/lint/lint-parallel.ts +19 -2
  155. package/scripts/observations/observation-ledger-home.ts +1 -0
  156. package/scripts/review/review-attest-cli.ts +21 -6
  157. package/scripts/review/review-brief-cli.ts +2 -2
  158. package/scripts/review/review-comment-round.ts +114 -0
  159. package/scripts/review/review-record-cli.ts +39 -17
  160. package/scripts/rules/implementation-cut.ts +3 -3
  161. package/scripts/rules/pre-gate-cut.ts +1 -1
  162. package/scripts/run/board/run-board-checks.ts +42 -0
  163. package/scripts/run/board/run-board-cli.ts +2 -0
  164. package/scripts/run/board/run-board-every.ts +3 -1
  165. package/scripts/run/board/run-board-fit.ts +16 -5
  166. package/scripts/run/board/run-board-fresh.ts +64 -0
  167. package/scripts/run/board/run-board-header.ts +129 -53
  168. package/scripts/run/board/run-board-labels.ts +47 -4
  169. package/scripts/run/board/run-board-lane-event.ts +66 -0
  170. package/scripts/run/board/run-board-layout.ts +57 -9
  171. package/scripts/run/board/run-board-now.ts +234 -0
  172. package/scripts/run/board/run-board-operation-read.ts +75 -0
  173. package/scripts/run/board/run-board-operation.ts +150 -0
  174. package/scripts/run/board/run-board-phase.ts +26 -1
  175. package/scripts/run/board/run-board-plan.ts +4 -6
  176. package/scripts/run/board/run-board-read.ts +49 -3
  177. package/scripts/run/board/run-board-reason.ts +124 -0
  178. package/scripts/run/board/run-board-render-checks.ts +55 -0
  179. package/scripts/run/board/run-board-render-legend.ts +12 -3
  180. package/scripts/run/board/run-board-render-slot.ts +2 -2
  181. package/scripts/run/board/run-board-render-tail.ts +128 -0
  182. package/scripts/run/board/run-board-render-thanks.ts +50 -0
  183. package/scripts/run/board/run-board-render-title.ts +58 -0
  184. package/scripts/run/board/run-board-render.ts +96 -80
  185. package/scripts/run/board/run-board-row-yield.ts +32 -0
  186. package/scripts/run/board/run-board-setup.ts +61 -13
  187. package/scripts/run/board/run-board-state.ts +10 -0
  188. package/scripts/run/board/run-board-status.ts +66 -7
  189. package/scripts/run/board/run-board-thanks-csv.ts +71 -0
  190. package/scripts/run/board/run-board-thanks-read.ts +112 -0
  191. package/scripts/run/board/run-board-thanks.ts +53 -0
  192. package/scripts/run/board/run-board-tick.ts +50 -21
  193. package/scripts/run/board/run-board-usual.ts +44 -0
  194. package/scripts/run/carry/run-carry-args.ts +9 -1
  195. package/scripts/run/carry/run-carry-cli.ts +2 -45
  196. package/scripts/run/carry/run-carry-end.ts +73 -0
  197. package/scripts/run/carry/run-carry.ts +8 -1
  198. package/scripts/run/entry/run-entry-cli.ts +5 -0
  199. package/scripts/run/event/run-event-checks.ts +54 -0
  200. package/scripts/run/event/run-event-render.ts +2 -0
  201. package/scripts/run/event/run-event-scope.ts +1 -0
  202. package/scripts/run/event/run-event-setup.ts +55 -11
  203. package/scripts/run/event/run-event-stream.ts +27 -11
  204. package/scripts/run/merge/run-merge-cli.ts +7 -4
  205. package/scripts/run/merge/run-merge-ship.ts +61 -0
  206. package/scripts/run/merge/run-merge-steps.ts +7 -14
  207. package/scripts/run/merge/run-merge-token.ts +16 -2
  208. package/scripts/run/run-headless.ts +1 -0
  209. package/scripts/run/run-step.ts +2 -0
  210. package/scripts/run/ship/run-ship-cli.ts +22 -2
  211. package/scripts/run/ship/run-ship-return.ts +31 -13
  212. package/scripts/run/ship/run-ship-review-steps.ts +17 -4
  213. package/scripts/run/ship/run-ship-stage.ts +1 -0
  214. package/scripts/run/wake/run-wake-await.ts +130 -0
  215. package/scripts/run/wake/run-wake-cli.ts +60 -71
  216. package/scripts/run/wake/run-wake-describe.ts +34 -2
  217. package/scripts/run/wake/run-wake-driver.ts +11 -4
  218. package/scripts/run/wake/run-wake-session.ts +20 -3
  219. package/scripts/run/wake/run-wake-stop.ts +37 -0
  220. package/scripts/run/wake/run-wake-supervise.ts +44 -0
  221. package/scripts/sync/claude-settings-merge.ts +144 -0
  222. package/scripts/sync/sync-ai-files.ts +28 -3
  223. package/scripts/sync/sync.ts +15 -0
  224. package/scripts/test/project-test-script.ts +73 -0
  225. package/scripts/test/skip-marker.ts +14 -1
  226. package/scripts/test/test-related.ts +3 -2
  227. package/scripts/test/test-unit-guard.ts +78 -14
  228. package/scripts/time-runtime/time-reported-failure.ts +11 -3
  229. package/scripts/uninstall/distributed-files.ts +92 -0
  230. package/scripts/uninstall/uninstall-plan.ts +119 -0
  231. package/scripts/uninstall/uninstall-report.ts +61 -0
  232. package/scripts/uninstall/uninstall.ts +206 -0
  233. package/templates/AGENTS.basic.md +4 -0
  234. package/templates/GEMINI.basic.md +6 -0
  235. package/dist/hooks/chunk-6V5OPIUU.js +0 -13
  236. 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 { BoardPlan } from './run-board-layout'
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 = new Set(children_of(lead).map((child) => child.number))
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 { lane_registry } from '#scripts/lane/lane-registry'
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 track's own order and whatever is on screen: a row's phase
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 = [...state_legend(drawn), ...kind_named, ...waits_legend(rows), ...notes_named]
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 track, one width on every row it is drawn on, so every
8
- // track starts in one column: a running row's CPU and memory, a finished row's finish time, and
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 }