@joshuafolkken/kit 1.1387.0 → 1.1453.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 (278) 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-park.md +2 -0
  5. package/.claude/skills/workflow-commands/backlogrun-steps.md +13 -10
  6. package/.claude/skills/workflow-commands/backlogrun.md +3 -2
  7. package/.claude/skills/workflow-commands/chain-rule.md +4 -2
  8. package/.claude/skills/workflow-commands/fullrun-steps.md +1 -1
  9. package/.claude/skills/workflow-commands/halfrun.md +1 -1
  10. package/.claude/skills/workflow-commands/issue-comments.md +6 -0
  11. package/.claude/skills/workflow-commands/kickoff.md +12 -13
  12. package/.claude/skills/workflow-commands/observation-filing.md +1 -1
  13. package/.claude/skills/workflow-commands/observation-ledger.md +3 -2
  14. package/.claude/skills/workflow-commands/pre-gate-cut.md +5 -3
  15. package/.claude/skills/workflow-commands/prerequisite.md +6 -0
  16. package/.claude/skills/workflow-commands/retrospective.md +5 -5
  17. package/.claude/skills/workflow-commands/split-assessment.md +2 -2
  18. package/README.md +27 -4
  19. package/dist/hooks/audit-provision.js +2 -2
  20. package/dist/hooks/batch-guard.js +7 -7
  21. package/dist/hooks/{chunk-2YSW3CXH.js → chunk-3C4XRYG6.js} +35 -11
  22. package/dist/hooks/{chunk-GJ7JPIHN.js → chunk-5C7L6FRC.js} +2704 -190
  23. package/dist/hooks/{chunk-FZ6FUSBM.js → chunk-723HKZO5.js} +1 -1
  24. package/dist/hooks/{chunk-STDSEPWU.js → chunk-D3NJXN5M.js} +4 -4
  25. package/dist/hooks/{chunk-NEC4AT4Z.js → chunk-DGT54IH5.js} +4 -0
  26. package/dist/hooks/{chunk-LMWBRJIP.js → chunk-EF5GQ6WS.js} +3 -3
  27. package/dist/hooks/{chunk-JBBMAPC4.js → chunk-IX6TTTI6.js} +4 -0
  28. package/dist/hooks/{chunk-IYVOLN6A.js → chunk-LBFYEOXJ.js} +3 -3
  29. package/dist/hooks/{chunk-SL4GQ2XP.js → chunk-MBJYQPEK.js} +106 -23
  30. package/dist/hooks/{chunk-WNFJLBXI.js → chunk-NO4AALP5.js} +3 -3
  31. package/dist/hooks/{chunk-J3KSS7II.js → chunk-OLMO72ON.js} +1 -1
  32. package/dist/hooks/{chunk-DCY2GWHC.js → chunk-SHYUQOIS.js} +5 -5
  33. package/dist/hooks/{chunk-EAQEGME7.js → chunk-UT7YC3N2.js} +1 -1
  34. package/dist/hooks/{chunk-EBSHN25W.js → chunk-W5ZKGTSY.js} +4 -4
  35. package/dist/hooks/{chunk-GNDJ7NJ7.js → chunk-Y3GKIKGW.js} +209 -56
  36. package/dist/hooks/{chunk-U32VSRNG.js → chunk-Z4XI7CZS.js} +89 -34
  37. package/dist/hooks/codex-hook-adapter.js +15 -15
  38. package/dist/hooks/duplicate-read-guard.js +8 -8
  39. package/dist/hooks/format-edited.js +5 -5
  40. package/dist/hooks/inputs.json +261 -201
  41. package/dist/hooks/investigation-guard.js +8 -8
  42. package/dist/hooks/pretool-guard.js +14 -14
  43. package/dist/hooks/session-lang.js +69 -16
  44. package/dist/hooks/stop-guard.js +13 -38
  45. package/dist/josh.js +95 -19
  46. package/package.json +4 -3
  47. package/prompts/collaboration-workflow/issue-template.md +10 -8
  48. package/prompts/review.md +30 -4
  49. package/scripts/agent/claude-agent-argv.ts +13 -1
  50. package/scripts/auth/auth-setup-cli.ts +101 -0
  51. package/scripts/auth/claude-token-output.ts +78 -0
  52. package/scripts/auth/claude-token.ts +68 -0
  53. package/scripts/auto-ok/auto-ok-judge-cli.ts +125 -0
  54. package/scripts/auto-ok/auto-ok-judge-read.ts +95 -0
  55. package/scripts/auto-ok/auto-ok-judge-write.ts +109 -0
  56. package/scripts/auto-ok/auto-ok-judge.ts +67 -0
  57. package/scripts/backlog/backlog-drive-cli.ts +14 -0
  58. package/scripts/backlog/backlog-drive-epic.ts +24 -8
  59. package/scripts/backlog/backlog-drive-launch.ts +3 -0
  60. package/scripts/backlog/backlog-drive-named-offer.ts +7 -2
  61. package/scripts/backlog/backlog-drive-named.ts +4 -2
  62. package/scripts/backlog/backlog-drive-unread.ts +56 -0
  63. package/scripts/backlog/backlog-drive.ts +38 -11
  64. package/scripts/backlog/backlog-overlap.ts +22 -5
  65. package/scripts/backlog/backlog-ready.ts +1 -3
  66. package/scripts/backlog/backlogrun-args.ts +13 -2
  67. package/scripts/backlog/backlogrun-cli.ts +91 -19
  68. package/scripts/backlog/backlogrun-launch.ts +24 -0
  69. package/scripts/backlog/backlogrun-replace.ts +166 -0
  70. package/scripts/backlog/backlogrun-stop.ts +178 -0
  71. package/scripts/behavior/behavior-cli.ts +6 -1
  72. package/scripts/doctor/doctor-io.ts +60 -9
  73. package/scripts/doctor/doctor.ts +20 -1
  74. package/scripts/document/document-byte-budget.ts +3 -3
  75. package/scripts/epic/epic-bundle-cli.ts +8 -8
  76. package/scripts/epic/epic-bundle-referenced.ts +2 -5
  77. package/scripts/epic/epic-bundle.ts +39 -4
  78. package/scripts/exports/unused-members-cli.ts +2 -1
  79. package/scripts/followup/followup-checks-event.ts +82 -0
  80. package/scripts/followup/git-followup-finish.ts +16 -2
  81. package/scripts/followup/git-followup-workflow.ts +5 -2
  82. package/scripts/followup/git-pr-followup-checks.ts +18 -4
  83. package/scripts/followup/git-pr-followup-wrapup.ts +5 -0
  84. package/scripts/followup/git-pr-followup.ts +4 -1
  85. package/scripts/fresh-machine/fresh-machine-cli.ts +204 -0
  86. package/scripts/fresh-machine/fresh-machine-docker.ts +65 -0
  87. package/scripts/fresh-machine/fresh-machine-image.ts +62 -0
  88. package/scripts/fresh-machine/fresh-machine-log.ts +61 -0
  89. package/scripts/fresh-machine/fresh-machine-patterns.ts +175 -0
  90. package/scripts/fresh-machine/fresh-machine-run.ts +215 -0
  91. package/scripts/fresh-machine/fresh-machine-script.ts +45 -0
  92. package/scripts/fresh-machine/fresh-machine-spec.ts +57 -0
  93. package/scripts/fresh-machine/specs/kit-test-backlogrun-python.json +34 -0
  94. package/scripts/fresh-machine/specs/kit-test-backlogrun.json +31 -0
  95. package/scripts/gate/gate-report.ts +70 -15
  96. package/scripts/gate/project-checks.ts +18 -4
  97. package/scripts/gate/scoped-green.ts +2 -1
  98. package/scripts/gate/verification-gate.ts +4 -3
  99. package/scripts/gh/gh-author-association.ts +14 -0
  100. package/scripts/gh/git-gh-conditional.ts +66 -0
  101. package/scripts/gh/git-gh-exec.ts +1 -1
  102. package/scripts/gh/git-gh-issue-read.ts +6 -3
  103. package/scripts/gh/git-gh-issue-rest.ts +1 -0
  104. package/scripts/gh/git-gh-issue-write.ts +5 -1
  105. package/scripts/gh/git-gh-issue.ts +13 -1
  106. package/scripts/gh/git-pr-checks-eval.ts +13 -0
  107. package/scripts/gh/git-pr-checks-parse.ts +8 -1
  108. package/scripts/gh/git-pr-checks-watch.ts +11 -4
  109. package/scripts/gh/git-pr-checks.ts +36 -2
  110. package/scripts/git/git-command.ts +16 -4
  111. package/scripts/git/git-schemas.ts +1 -0
  112. package/scripts/git/git-spawn-sync.ts +1 -1
  113. package/scripts/hooks/pretool-guard.ts +3 -1
  114. package/scripts/init/hook-command-rewrite.ts +31 -0
  115. package/scripts/init/init-actions.ts +7 -4
  116. package/scripts/init/init-ai-copy.ts +34 -11
  117. package/scripts/init/init-basic.ts +9 -4
  118. package/scripts/init/init-bootstrap.ts +6 -2
  119. package/scripts/init/init-dry-run-report.ts +150 -0
  120. package/scripts/init/init-dry-run.ts +189 -0
  121. package/scripts/init/init-install.ts +8 -1
  122. package/scripts/init/init-logic-toolchain.ts +76 -0
  123. package/scripts/init/init-logic.ts +2 -0
  124. package/scripts/init/init.ts +62 -42
  125. package/scripts/init/kit-setup-state.ts +11 -17
  126. package/scripts/init/own-formatter.ts +21 -0
  127. package/scripts/init/project-profile.ts +60 -12
  128. package/scripts/init/start-dry-run.ts +70 -0
  129. package/scripts/init/start-exec.ts +27 -1
  130. package/scripts/init/start-plan.ts +23 -3
  131. package/scripts/init/start-setup-pr.ts +13 -24
  132. package/scripts/init/start-steps.ts +62 -1
  133. package/scripts/init/start.ts +38 -17
  134. package/scripts/init/workflow-mark.ts +35 -0
  135. package/scripts/init/working-tree.ts +116 -0
  136. package/scripts/issue/issue-ack-cli.ts +84 -0
  137. package/scripts/issue/issue-auto-ok.ts +68 -99
  138. package/scripts/issue/issue-file-cli.ts +17 -17
  139. package/scripts/issue/issue-file.ts +1 -6
  140. package/scripts/issue/issue-labels.ts +46 -10
  141. package/scripts/issue/issue-release-cli.ts +69 -23
  142. package/scripts/issue/issue-release.ts +3 -1
  143. package/scripts/issue/issue-state-cli.ts +14 -4
  144. package/scripts/issue/issue-watch-diff.ts +152 -0
  145. package/scripts/issue/issue-watch-gate.ts +81 -0
  146. package/scripts/issue/issue-watch-hook.ts +54 -0
  147. package/scripts/issue/issue-watch.ts +240 -0
  148. package/scripts/issue/measure-rerun-cli.ts +3 -4
  149. package/scripts/josh/josh-command-map.ts +4 -0
  150. package/scripts/josh/josh-commands-ai.ts +11 -0
  151. package/scripts/josh/josh-commands-backlog.ts +2 -2
  152. package/scripts/josh/josh-commands-development.ts +1 -1
  153. package/scripts/josh/josh-commands-issue.ts +10 -3
  154. package/scripts/josh/josh-commands-lane.ts +11 -2
  155. package/scripts/josh/josh-commands-project.ts +26 -2
  156. package/scripts/josh/josh-verdict.ts +33 -4
  157. package/scripts/josh/process-identity.ts +32 -6
  158. package/scripts/lane/claude-lane-broker.ts +142 -0
  159. package/scripts/lane/lane-await.ts +6 -2
  160. package/scripts/lane/lane-broker-server.ts +69 -0
  161. package/scripts/lane/lane-cache.ts +37 -7
  162. package/scripts/lane/lane-capacity.ts +8 -6
  163. package/scripts/lane/lane-child-invocation.ts +1 -0
  164. package/scripts/lane/lane-cli.ts +15 -1
  165. package/scripts/lane/lane-close.ts +15 -6
  166. package/scripts/lane/lane-dispatch.ts +36 -10
  167. package/scripts/lane/lane-environment.ts +1 -1
  168. package/scripts/lane/lane-launch-cli.ts +13 -4
  169. package/scripts/lane/lane-ledger.ts +21 -3
  170. package/scripts/lane/lane-measure-cli.ts +10 -2
  171. package/scripts/lane/lane-open.ts +9 -1
  172. package/scripts/lane/lane-preserve.ts +49 -0
  173. package/scripts/lane/lane-reap.ts +20 -5
  174. package/scripts/lane/lane-registry.ts +6 -0
  175. package/scripts/lane/lane-restart-cli.ts +51 -0
  176. package/scripts/lane/lane-restart.ts +175 -0
  177. package/scripts/lane/lane-session-broker.ts +363 -0
  178. package/scripts/lane/lane-spent.ts +195 -0
  179. package/scripts/lane/lane-stage-stats.ts +27 -3
  180. package/scripts/lane/lane-stop-stats.ts +96 -0
  181. package/scripts/lane/openai-lane-supervisor.ts +2 -2
  182. package/scripts/lane/process-tools.ts +31 -0
  183. package/scripts/lib/buffered-process.ts +36 -2
  184. package/scripts/lib/pnpm-reach.ts +27 -0
  185. package/scripts/lib/status-icons.ts +5 -1
  186. package/scripts/lint/lint-parallel.ts +19 -2
  187. package/scripts/observations/observation-ledger-home.ts +1 -0
  188. package/scripts/observations/observation-ledger-migrate.ts +3 -15
  189. package/scripts/release/release-issue-close.ts +133 -0
  190. package/scripts/release/release-publish.ts +12 -1
  191. package/scripts/repo/repository-labels.ts +11 -1
  192. package/scripts/review/comment-ranges.ts +199 -0
  193. package/scripts/review/review-attest-cli.ts +21 -6
  194. package/scripts/review/review-brief-cli.ts +24 -7
  195. package/scripts/review/review-code-tree.ts +119 -0
  196. package/scripts/review/review-comment-round.ts +114 -0
  197. package/scripts/review/review-record-cli.ts +39 -17
  198. package/scripts/review/review-round2-cli.ts +16 -5
  199. package/scripts/review/review-round2.ts +30 -10
  200. package/scripts/review/review-stamps.ts +22 -1
  201. package/scripts/rules/implementation-cut.ts +3 -3
  202. package/scripts/rules/pre-gate-cut.ts +1 -1
  203. package/scripts/run/board/run-board-checks.ts +42 -0
  204. package/scripts/run/board/run-board-cli.ts +2 -0
  205. package/scripts/run/board/run-board-every.ts +3 -1
  206. package/scripts/run/board/run-board-fit.ts +16 -5
  207. package/scripts/run/board/run-board-fresh.ts +64 -0
  208. package/scripts/run/board/run-board-header.ts +129 -53
  209. package/scripts/run/board/run-board-labels.ts +47 -4
  210. package/scripts/run/board/run-board-lane-event.ts +66 -0
  211. package/scripts/run/board/run-board-layout.ts +57 -9
  212. package/scripts/run/board/run-board-now.ts +234 -0
  213. package/scripts/run/board/run-board-operation-read.ts +75 -0
  214. package/scripts/run/board/run-board-operation.ts +150 -0
  215. package/scripts/run/board/run-board-phase.ts +26 -1
  216. package/scripts/run/board/run-board-plan.ts +4 -6
  217. package/scripts/run/board/run-board-read.ts +49 -3
  218. package/scripts/run/board/run-board-reason.ts +124 -0
  219. package/scripts/run/board/run-board-render-checks.ts +55 -0
  220. package/scripts/run/board/run-board-render-legend.ts +12 -3
  221. package/scripts/run/board/run-board-render-slot.ts +2 -2
  222. package/scripts/run/board/run-board-render-tail.ts +128 -0
  223. package/scripts/run/board/run-board-render-thanks.ts +50 -0
  224. package/scripts/run/board/run-board-render-title.ts +58 -0
  225. package/scripts/run/board/run-board-render.ts +96 -80
  226. package/scripts/run/board/run-board-row-yield.ts +32 -0
  227. package/scripts/run/board/run-board-setup.ts +61 -13
  228. package/scripts/run/board/run-board-state.ts +10 -0
  229. package/scripts/run/board/run-board-status.ts +66 -7
  230. package/scripts/run/board/run-board-thanks-csv.ts +71 -0
  231. package/scripts/run/board/run-board-thanks-read.ts +112 -0
  232. package/scripts/run/board/run-board-thanks.ts +53 -0
  233. package/scripts/run/board/run-board-tick.ts +50 -21
  234. package/scripts/run/board/run-board-usual.ts +44 -0
  235. package/scripts/run/carry/run-carry-args.ts +9 -1
  236. package/scripts/run/carry/run-carry-cli.ts +2 -45
  237. package/scripts/run/carry/run-carry-end.ts +73 -0
  238. package/scripts/run/carry/run-carry.ts +8 -1
  239. package/scripts/run/entry/run-entry-cli.ts +5 -0
  240. package/scripts/run/event/run-event-checks.ts +54 -0
  241. package/scripts/run/event/run-event-render.ts +2 -0
  242. package/scripts/run/event/run-event-scope.ts +1 -0
  243. package/scripts/run/event/run-event-setup.ts +55 -11
  244. package/scripts/run/event/run-event-stream.ts +27 -11
  245. package/scripts/run/merge/run-merge-cli.ts +7 -4
  246. package/scripts/run/merge/run-merge-ship.ts +61 -0
  247. package/scripts/run/merge/run-merge-steps.ts +7 -14
  248. package/scripts/run/merge/run-merge-token.ts +16 -2
  249. package/scripts/run/run-headless.ts +1 -0
  250. package/scripts/run/run-step.ts +2 -0
  251. package/scripts/run/ship/run-ship-cli.ts +22 -2
  252. package/scripts/run/ship/run-ship-return.ts +31 -13
  253. package/scripts/run/ship/run-ship-review-steps.ts +17 -4
  254. package/scripts/run/ship/run-ship-stage.ts +1 -0
  255. package/scripts/run/wake/run-wake-await.ts +130 -0
  256. package/scripts/run/wake/run-wake-cli.ts +60 -71
  257. package/scripts/run/wake/run-wake-describe.ts +34 -2
  258. package/scripts/run/wake/run-wake-driver.ts +11 -4
  259. package/scripts/run/wake/run-wake-session.ts +20 -3
  260. package/scripts/run/wake/run-wake-stop.ts +37 -0
  261. package/scripts/run/wake/run-wake-supervise.ts +44 -0
  262. package/scripts/sync/claude-settings-merge.ts +144 -0
  263. package/scripts/sync/sync-ai-files.ts +28 -3
  264. package/scripts/sync/sync.ts +15 -0
  265. package/scripts/test/project-test-script.ts +73 -0
  266. package/scripts/test/skip-marker.ts +14 -1
  267. package/scripts/test/test-related.ts +3 -2
  268. package/scripts/test/test-unit-guard.ts +78 -14
  269. package/scripts/time-runtime/time-reported-failure.ts +11 -3
  270. package/scripts/uninstall/distributed-files.ts +92 -0
  271. package/scripts/uninstall/uninstall-plan.ts +119 -0
  272. package/scripts/uninstall/uninstall-report.ts +61 -0
  273. package/scripts/uninstall/uninstall.ts +206 -0
  274. package/templates/AGENTS.basic.md +4 -0
  275. package/templates/GEMINI.basic.md +6 -0
  276. package/dist/hooks/chunk-6V5OPIUU.js +0 -13
  277. package/scripts/lane/openai-review-broker.ts +0 -262
  278. package/scripts/run/board/run-board-track.ts +0 -22
@@ -0,0 +1,234 @@
1
+ import { issue_cite } from '#scripts/issue/issue-cite'
2
+ import { run_event_setup } from '#scripts/run/event/run-event-setup'
3
+ import { run_board_labels } from './run-board-labels'
4
+ import type { LaneEvent } from './run-board-lane-event'
5
+ import { run_board_operation, type Operation } from './run-board-operation'
6
+ import type { SessionOperations } from './run-board-operation-read'
7
+ import type { RunActivity } from './run-board-status'
8
+
9
+ // The header's third line: what the run is doing now, as its kind, when it happened and its words.
10
+ // Always one line while the stream holds an event — the newer of a running child's newest operation and
11
+ // its newest event, or the newest operation of the session a stage is the work of, else the stage the
12
+ // run is at, else how many children run, else how long it has watched, each of the last three with why;
13
+ // an ended run keeps the newest thing it did. **An operation changes with every call a session makes**,
14
+ // where a stage or a count stands for minutes. Every time is an event's or a call's own, so the line
15
+ // never guesses one; the header ages it and draws it in its time column (joshuafolkken/kit#3767).
16
+
17
+ const { KIND_BLANK, NOW_ICONS, PHASE_ICONS, STAGE_ICONS, STATE_ICONS, WORDS } = run_board_labels
18
+ const { STAGE } = run_event_setup
19
+ const REASON_SEPARATOR = ' — '
20
+ const OPERATION_SEPARATOR = ' · '
21
+ const WORD_SEPARATOR = ' '
22
+ // The stages a session is at work on, and whose log says what it does. Every other stage is the
23
+ // driver's or the supervisor's own step, which writes no log of calls.
24
+ const STAGE_SESSIONS: Readonly<Partial<Record<string, 'owner' | 'judgment'>>> = {
25
+ [STAGE.PLANNING]: 'owner',
26
+ [STAGE.SESSION]: 'judgment',
27
+ }
28
+
29
+ interface NowRead {
30
+ now_ms: number
31
+ // Set once the run has ended: the newest event's age stops there.
32
+ ended_ms: number | undefined
33
+ activity: RunActivity
34
+ // The newest operation of each session of the run, `undefined` where none was read.
35
+ operations?: SessionOperations | undefined
36
+ // The issues of the children running now: a lane left by an earlier run is not this run's work.
37
+ children?: ReadonlySet<number> | undefined
38
+ running: number
39
+ // What the plan has left for the run, `undefined` until the plan is read.
40
+ waiting: number | undefined
41
+ // How many of those wait on a person.
42
+ people: number
43
+ }
44
+
45
+ interface NowLine {
46
+ // The kind's icon, drawn under the `⚡`.
47
+ icon: string
48
+ // The icon's word in the legend, `undefined` where the line's own words or the phase legend name it.
49
+ legend?: string | undefined
50
+ // How long ago what the line says happened — up to an ended run's end — `undefined` where no event
51
+ // gave it a time.
52
+ age_ms: number | undefined
53
+ text: string
54
+ }
55
+
56
+ // A line before it is aged: when what it says happened.
57
+ type TimedLine = Omit<NowLine, 'age_ms'> & { since_ms: number | undefined }
58
+
59
+ // What stands behind the pool once nothing runs: nothing, only what a person has to decide, or issues
60
+ // the run cannot take yet.
61
+ function pool_reason(waiting: number, people: number): string {
62
+ if (waiting === 0) return WORDS.pool_empty
63
+
64
+ return waiting === people
65
+ ? `${String(waiting)} ${WORDS.on_person}`
66
+ : `${String(waiting)} ${WORDS.waiting}`
67
+ }
68
+
69
+ function running_of(running: number): string {
70
+ return `${String(running)} ${WORDS.in_progress}`
71
+ }
72
+
73
+ // Why the run is where it is: the children it runs, else what its plan has left. Nothing before the
74
+ // plan is read.
75
+ function reason_of(read: NowRead): string | undefined {
76
+ if (read.running > 0) return running_of(read.running)
77
+
78
+ return read.waiting === undefined ? undefined : pool_reason(read.waiting, read.people)
79
+ }
80
+
81
+ function with_reason(line: string, read: NowRead): string {
82
+ const reason = reason_of(read)
83
+
84
+ return reason === undefined ? line : `${line}${REASON_SEPARATOR}${reason}`
85
+ }
86
+
87
+ // The newest thing an ended run did.
88
+ function latest_line(read: NowRead): TimedLine | undefined {
89
+ const { latest } = read.activity
90
+
91
+ if (latest === undefined) return undefined
92
+
93
+ return { icon: NOW_ICONS.ended, legend: WORDS.ended, since_ms: latest.at_ms, text: latest.text }
94
+ }
95
+
96
+ // A run with nothing running and at no stage is watching its pool, since the newest thing it did — a
97
+ // stage it has left included, so a judgment session's minutes are not counted as watching.
98
+ function watch_line(read: NowRead): TimedLine | undefined {
99
+ const { idle_ms } = read.activity
100
+
101
+ if (idle_ms === undefined) return undefined
102
+
103
+ return { icon: NOW_ICONS.watching, since_ms: idle_ms, text: with_reason(WORDS.watching, read) }
104
+ }
105
+
106
+ // `words`, then what the session did — words that end on a command or a path.
107
+ function operated(words: string, operation: Operation): string {
108
+ return `${words}${OPERATION_SEPARATOR}${run_board_operation.words_of(operation)}`
109
+ }
110
+
111
+ function session_operation(read: NowRead, setup: string): Operation | undefined {
112
+ const session = STAGE_SESSIONS[setup]
113
+
114
+ return session === undefined ? undefined : read.operations?.[session]
115
+ }
116
+
117
+ // The newest operation of the session a stage is the work of. One older than the stage is an earlier
118
+ // session's, or what the parent did before it — the watcher's log runs on across runs.
119
+ function stage_operation(read: NowRead, setup: string): Operation | undefined {
120
+ const operation = session_operation(read, setup)
121
+
122
+ if (operation === undefined) return undefined
123
+
124
+ return operation.at_ms < (read.activity.setup_ms ?? 0) ? undefined : operation
125
+ }
126
+
127
+ // A stage's icon, keyed by its words before the issue a lane's stage names; a stage the board does not
128
+ // know keeps the icon's columns blank, so its words start where every other line's do.
129
+ function stage_icon(setup: string): string {
130
+ const known = Object.keys(STAGE_ICONS).find(
131
+ (words) => setup === words || setup.startsWith(`${words}${WORD_SEPARATOR}`),
132
+ )
133
+
134
+ return (known === undefined ? undefined : STAGE_ICONS[known]) ?? KIND_BLANK
135
+ }
136
+
137
+ // The stage, then what its session last did and when; the stage, since when the run has stood there
138
+ // and why where no session is at work on it, or none could be read.
139
+ function stage_line(read: NowRead, setup: string): TimedLine {
140
+ const operation = stage_operation(read, setup)
141
+ const icon = stage_icon(setup)
142
+
143
+ return operation === undefined
144
+ ? { icon, since_ms: read.activity.setup_ms, text: with_reason(setup, read) }
145
+ : { icon, since_ms: operation.at_ms, text: operated(setup, operation) }
146
+ }
147
+
148
+ // A running child's words, named by its issue.
149
+ function child_said(issue: number, words: string): string {
150
+ return `${issue_cite.plain(issue)}${OPERATION_SEPARATOR}${words}`
151
+ }
152
+
153
+ // What the running children's newest activity is read from: each session's newest operation and each
154
+ // child's newest event, kept to the children named.
155
+ interface ChildSources {
156
+ operations?: SessionOperations | undefined
157
+ lane_events?: ReadonlyMap<number, LaneEvent> | undefined
158
+ children?: ReadonlySet<number> | undefined
159
+ }
160
+
161
+ // The running children's newest operations, each named by its issue.
162
+ function operation_lines(read: ChildSources): Array<TimedLine> {
163
+ const lanes = read.operations?.lanes.filter((lane) => read.children?.has(lane.issue)) ?? []
164
+
165
+ return lanes.map(({ issue, operation }) => ({
166
+ icon: NOW_ICONS.operation,
167
+ legend: WORDS.operation,
168
+ since_ms: operation.at_ms,
169
+ text: child_said(issue, run_board_operation.words_of(operation)),
170
+ }))
171
+ }
172
+
173
+ // The running children's newest events, each in its row's phase icon and its own words.
174
+ function event_lines(read: ChildSources): Array<TimedLine> {
175
+ const events = [...(read.lane_events ?? [])]
176
+
177
+ return events
178
+ .filter(([issue]) => read.children?.has(issue))
179
+ .map(([issue, event]) => ({
180
+ icon: PHASE_ICONS[event.phase],
181
+ since_ms: event.at_ms,
182
+ text: child_said(issue, event.words),
183
+ }))
184
+ }
185
+
186
+ function newest_first(left: TimedLine, right: TimedLine): number {
187
+ return (right.since_ms ?? 0) - (left.since_ms ?? 0)
188
+ }
189
+
190
+ // The newest of the children's operations and events — an operation where the two are as new.
191
+ // `undefined` where neither could be read. A row reads its own child's the same way
192
+ // (joshuafolkken/kit#3769), so the header and the row never disagree on what is newest.
193
+ function newest_of(sources: ChildSources): TimedLine | undefined {
194
+ return [...operation_lines(sources), ...event_lines(sources)].toSorted(newest_first).at(0)
195
+ }
196
+
197
+ // The running children's newest, or `undefined`, and the line counts the children instead.
198
+ function child_line(read: NowRead): TimedLine | undefined {
199
+ const { operations, children, activity } = read
200
+
201
+ return newest_of({ operations, children, lane_events: activity.lane_events })
202
+ }
203
+
204
+ function quiet_line(read: NowRead): TimedLine | undefined {
205
+ if (read.running === 0) return watch_line(read)
206
+
207
+ return { icon: STATE_ICONS.running, since_ms: undefined, text: running_of(read.running) }
208
+ }
209
+
210
+ function timed_line(read: NowRead): TimedLine | undefined {
211
+ const { is_stopped, setup } = read.activity
212
+
213
+ if (is_stopped) return latest_line(read)
214
+ if (setup !== undefined) return stage_line(read, setup)
215
+
216
+ return child_line(read) ?? quiet_line(read)
217
+ }
218
+
219
+ // The line aged to now, or to an ended run's end: an age counting on after it would read as a hang.
220
+ function line_of(read: NowRead): NowLine | undefined {
221
+ const line = timed_line(read)
222
+
223
+ if (line === undefined) return undefined
224
+
225
+ const { since_ms, ...rest } = line
226
+ const until_ms = read.ended_ms ?? read.now_ms
227
+
228
+ return { ...rest, age_ms: since_ms === undefined ? undefined : until_ms - since_ms }
229
+ }
230
+
231
+ const run_board_now = { line_of, newest_of }
232
+
233
+ export { run_board_now }
234
+ export type { ChildSources, NowLine, NowRead, TimedLine }
@@ -0,0 +1,75 @@
1
+ import { time_density_hook } from '#scripts/time-runtime/time-density-hook'
2
+ import { run_board_operation, type Operation } from './run-board-operation'
3
+
4
+ // The disk half of the third line's operation: the end of each session's log, read every time the
5
+ // board re-reads its run. **A fixed, small read**: a dispatch log grows to tens of megabytes and the
6
+ // watcher's log across runs, while the board re-reads every five seconds. The newest call is nearly
7
+ // always inside it; where one line is longer than the read, there is no operation and the line says
8
+ // what it said before. Nothing here may fail — a log that is missing or unreadable is no operation.
9
+
10
+ const TAIL_BYTES = 65_536
11
+
12
+ // A session's log and the checkout its paths are drawn from.
13
+ interface OperationSource {
14
+ log: string | undefined
15
+ root: string | undefined
16
+ }
17
+
18
+ interface LaneSource extends OperationSource {
19
+ issue: string
20
+ }
21
+
22
+ interface OperationSources {
23
+ lanes: ReadonlyArray<LaneSource>
24
+ // The parent session's transcript.
25
+ owner: OperationSource
26
+ // The watcher's log, which a judgment session appends to.
27
+ judgment: OperationSource
28
+ }
29
+
30
+ interface LaneOperation {
31
+ issue: number
32
+ operation: Operation
33
+ }
34
+
35
+ // The newest operation of every session the run may have at work; which of them is at work now is the
36
+ // reader's to say.
37
+ interface SessionOperations {
38
+ lanes: ReadonlyArray<LaneOperation>
39
+ owner: Operation | undefined
40
+ judgment: Operation | undefined
41
+ }
42
+
43
+ type ReadTail = (file_path: string, max_bytes: number) => string
44
+
45
+ function operation_of(source: OperationSource, read_tail: ReadTail): Operation | undefined {
46
+ if (source.log === undefined) return undefined
47
+
48
+ try {
49
+ return run_board_operation.latest_of(read_tail(source.log, TAIL_BYTES), source.root)
50
+ } catch {
51
+ return undefined
52
+ }
53
+ }
54
+
55
+ function lane_operation(source: LaneSource, read_tail: ReadTail): Array<LaneOperation> {
56
+ const operation = operation_of(source, read_tail)
57
+
58
+ return operation === undefined ? [] : [{ issue: Number(source.issue), operation }]
59
+ }
60
+
61
+ function read(
62
+ sources: OperationSources,
63
+ read_tail: ReadTail = time_density_hook.read_tail,
64
+ ): SessionOperations {
65
+ return {
66
+ lanes: sources.lanes.flatMap((lane) => lane_operation(lane, read_tail)),
67
+ owner: operation_of(sources.owner, read_tail),
68
+ judgment: operation_of(sources.judgment, read_tail),
69
+ }
70
+ }
71
+
72
+ const run_board_operation_read = { TAIL_BYTES, read }
73
+
74
+ export { run_board_operation_read }
75
+ export type { LaneOperation, OperationSources, SessionOperations }
@@ -0,0 +1,150 @@
1
+ import path from 'node:path'
2
+ import { json_value } from '#scripts/lib/json-value'
3
+ import { z } from 'zod'
4
+
5
+ // The newest thing a session did, read off the end of its log — a lane child's dispatch log, the
6
+ // parent's transcript or the watcher's log, all one JSON object a line with the tool calls of an
7
+ // `assistant` line under `message.content`. **Only what a machine decided is kept**: the tool's name
8
+ // and what it was pointed at. The call's `description` is the session's own prose, in the session's
9
+ // language, and the board is written in English.
10
+
11
+ interface Operation {
12
+ // The tool's name: `Bash`, `Edit`, `Read`, …
13
+ kind: string
14
+ // The head of the command, or the file's path; `undefined` for a tool pointed at neither.
15
+ target: string | undefined
16
+ at_ms: number
17
+ }
18
+
19
+ // A command is cut here, so a long argument — or a value passed in one — is not drawn.
20
+ const TARGET_LIMIT = 40
21
+ const CUT_MARK = '…'
22
+ const TOOL_USE = 'tool_use'
23
+ // A line that holds no call is never parsed: one can be tens of thousands of characters long.
24
+ const TOOL_USE_MARK = `"${TOOL_USE}"`
25
+ const NEWLINE = '\n'
26
+ // One variable set ahead of the command, with what parts it from the command: it carries a value,
27
+ // never what the command does. The value is read as the shell reads one word — a quoted stretch with
28
+ // its escapes and line breaks, a command run for its output, an escaped character, a plain one.
29
+ const QUOTED = /"(?:[^"\\]|\\.)*"|'[^']*'/su
30
+ const SUBSTITUTED = /\$\((?:[^()]|\([^()]*\))*\)|`[^`]*`/su
31
+ const PLAIN = /\\.|[^\s"'`;&|()\\]/su
32
+ const VALUE = `(?:${QUOTED.source}|${SUBSTITUTED.source}|${PLAIN.source})*`
33
+ const ASSIGNMENT = new RegExp(String.raw`^\w+=${VALUE}(?:\s*(?:&&|;)\s*|\s+|$)`, 'su')
34
+ // A variable still at the head once every readable one is gone: its value could not be read as one.
35
+ const UNREAD_ASSIGNMENT = /^\w+=/u
36
+ const SPACES = /\s+/gu
37
+ // The printable ASCII a target begins with: what follows may be the session's own language.
38
+ const ASCII_HEAD = /^[ -~]*/u
39
+
40
+ // A call names its tool; every other block of a line — its text, its thinking — is not one.
41
+ const CALL_SCHEMA = z.looseObject({
42
+ type: z.literal(TOOL_USE),
43
+ name: z.string(),
44
+ input: z.unknown(),
45
+ })
46
+
47
+ function is_dated(text: string): boolean {
48
+ return !Number.isNaN(Date.parse(text))
49
+ }
50
+
51
+ const MESSAGE_SCHEMA = z.looseObject({ content: z.array(z.unknown()) })
52
+ const LINE_SCHEMA = z.looseObject({
53
+ timestamp: z.string().refine(is_dated),
54
+ message: MESSAGE_SCHEMA,
55
+ })
56
+ // An input that is no object reads as one pointed at nothing.
57
+ const INPUT_SCHEMA = z
58
+ .looseObject({ command: z.string().optional(), file_path: z.string().optional() })
59
+ .catch({})
60
+
61
+ type Call = z.infer<typeof CALL_SCHEMA>
62
+ type Input = z.infer<typeof INPUT_SCHEMA>
63
+
64
+ function ascii_head(text: string): string {
65
+ return (ASCII_HEAD.exec(text)?.[0] ?? '').trim()
66
+ }
67
+
68
+ function cut(text: string): string {
69
+ return text.length > TARGET_LIMIT ? `${text.slice(0, TARGET_LIMIT)}${CUT_MARK}` : text
70
+ }
71
+
72
+ // The command without every variable set ahead of it, one at a time.
73
+ function without_assignments(command: string): string {
74
+ const rest = command.replace(ASSIGNMENT, '')
75
+
76
+ return rest === command ? command : without_assignments(rest)
77
+ }
78
+
79
+ // The first line of what the command runs, cut to the limit. A command whose variable cannot be read
80
+ // to its end is drawn as nothing: a part of the value would stand where the command should.
81
+ function command_target(command: string): string {
82
+ const [first = ''] = without_assignments(command.trim()).split(NEWLINE)
83
+ if (UNREAD_ASSIGNMENT.test(first)) return ''
84
+
85
+ return cut(ascii_head(first.replaceAll(SPACES, ' ')))
86
+ }
87
+
88
+ // The path from `root` down; a file outside it is drawn by its name alone, never by where it is.
89
+ function path_target(file_path: string, root: string | undefined): string {
90
+ const relative = root === undefined ? undefined : path.relative(root, file_path)
91
+ const is_inside = relative !== undefined && !relative.startsWith('..') && relative !== ''
92
+
93
+ return ascii_head(is_inside ? relative : path.basename(file_path))
94
+ }
95
+
96
+ function shown(target: string): string | undefined {
97
+ return target === '' ? undefined : target
98
+ }
99
+
100
+ function target_of(input: Input, root: string | undefined): string | undefined {
101
+ if (input.command !== undefined) return shown(command_target(input.command))
102
+
103
+ return input.file_path === undefined ? undefined : shown(path_target(input.file_path, root))
104
+ }
105
+
106
+ function is_call(block: unknown): block is Call {
107
+ return CALL_SCHEMA.safeParse(block).success
108
+ }
109
+
110
+ function last_call(content: ReadonlyArray<unknown>): Call | undefined {
111
+ return content.findLast((block) => is_call(block))
112
+ }
113
+
114
+ // The last call a line holds, `undefined` for a line that is cut, is not JSON, or holds none.
115
+ function line_operation(line: string, root: string | undefined): Operation | undefined {
116
+ const parsed = json_value.parse_with(line, LINE_SCHEMA)
117
+ const call = last_call(parsed?.message.content ?? [])
118
+
119
+ if (parsed === undefined || call === undefined) return undefined
120
+
121
+ return {
122
+ kind: call.name,
123
+ target: target_of(INPUT_SCHEMA.parse(call.input), root),
124
+ at_ms: Date.parse(parsed.timestamp),
125
+ }
126
+ }
127
+
128
+ // The newest operation in `tail` — the end of a log, whose first line is usually cut mid-way and so
129
+ // reads as no call. `root` is the checkout the session works in, which its paths are drawn from.
130
+ function latest_of(tail: string, root?: string): Operation | undefined {
131
+ const lines = tail.split(NEWLINE).filter((line) => line.includes(TOOL_USE_MARK))
132
+
133
+ for (const line of lines.toReversed()) {
134
+ const operation = line_operation(line, root)
135
+
136
+ if (operation !== undefined) return operation
137
+ }
138
+
139
+ return undefined
140
+ }
141
+
142
+ // The operation as the board writes it: `Bash pnpm josh test:related`, or the tool alone.
143
+ function words_of(operation: Operation): string {
144
+ return operation.target === undefined ? operation.kind : `${operation.kind} ${operation.target}`
145
+ }
146
+
147
+ const run_board_operation = { TARGET_LIMIT, latest_of, words_of }
148
+
149
+ export { run_board_operation }
150
+ export type { Operation }
@@ -37,6 +37,7 @@ const { STAGE } = run_ship_stage
37
37
  const LAUNCHED_PHASE: Phase = 'investigate'
38
38
  const WORD_SEPARATOR = ' '
39
39
  const STAGE_INDEX = 1
40
+ const PHASE_INDEX = 2
40
41
 
41
42
  // `josh ship`'s stages, each its own phase; the preflight is the ship's own start.
42
43
  const SHIP_PHASES: Readonly<Partial<Record<string, Phase>>> = {
@@ -64,6 +65,11 @@ function stage_word_of(event: RunEvent): string {
64
65
  return event.text.split(WORD_SEPARATOR)[STAGE_INDEX] ?? ''
65
66
  }
66
67
 
68
+ // The word after the stage — whether it started, succeeded, failed or was passed over.
69
+ function phase_word_of(event: RunEvent): string {
70
+ return event.text.split(WORD_SEPARATOR)[PHASE_INDEX] ?? ''
71
+ }
72
+
67
73
  // A stage marks its phase when it starts; its `done`, and one a resumed ship passed over, mark none.
68
74
  function ship_phase_of(event: RunEvent): Phase | undefined {
69
75
  if (!run_event_stream.is_stage_start(event)) return undefined
@@ -79,6 +85,16 @@ function phase_of(event: RunEvent): Phase | undefined {
79
85
  return KIND_PHASES[event.kind]
80
86
  }
81
87
 
88
+ // The phase a `lane-phase` or `ship-stage` event's stage belongs to, started or not, so a line written
89
+ // from it draws the icon its row's track does — a stage that failed is the ship's failure.
90
+ function stage_phase_of(event: RunEvent): Phase | undefined {
91
+ if (event.kind === KIND.LANE_PHASE) return LANE_PHASES[stage_word_of(event)]
92
+
93
+ if (phase_word_of(event) === run_ship_stage.PHASE.FAILED) return 'failed'
94
+
95
+ return SHIP_PHASES[stage_word_of(event)]
96
+ }
97
+
82
98
  // A failed ship keeps only its own start: everything after the newest `ship` folds into `failed`.
83
99
  function failed_after(track: ReadonlyArray<Phase>): Array<Phase> {
84
100
  const ship = track.lastIndexOf('ship')
@@ -130,7 +146,16 @@ function replayed(track: ReadonlyArray<Phase>, attempt: ReadonlyArray<Phase>): A
130
146
  return [...track.slice(0, start), ...(is_covered ? attempt : new Set([...current, ...attempt]))]
131
147
  }
132
148
 
133
- const run_board_phase = { LAUNCHED_PHASE, PHASES, attempt_of, history_after, phase_of, replayed }
149
+ const run_board_phase = {
150
+ LAUNCHED_PHASE,
151
+ PHASES,
152
+ SHIP_PHASES,
153
+ attempt_of,
154
+ history_after,
155
+ phase_of,
156
+ replayed,
157
+ stage_phase_of,
158
+ }
134
159
 
135
160
  export { run_board_phase }
136
161
  export type { Phase }
@@ -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