rvlib-shipkit 2.2.0 → 2.4.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 (140) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/package.json +1 -1
  3. package/src/agent-time/mask.ts +43 -0
  4. package/src/agent-time/render.ts +187 -0
  5. package/src/agent-time/report.ts +335 -0
  6. package/src/agent-time/shape.ts +271 -0
  7. package/src/agent-time/signals.ts +84 -0
  8. package/src/agent-time/store.ts +379 -0
  9. package/src/agent-time/timeline.ts +218 -0
  10. package/src/agent-time/turns.ts +135 -0
  11. package/src/assets/kits/identity-kit.ts +5 -2
  12. package/src/cli/cmd_Commands.ts +9 -0
  13. package/src/cli/cmd_Review.ts +974 -0
  14. package/src/cli/cmd_Sound.ts +209 -0
  15. package/src/cli/cmd_Time.ts +112 -0
  16. package/src/cli/cmd_Trailer.ts +264 -6
  17. package/src/cli/kit/verb.ts +22 -4
  18. package/src/cli.ts +8 -4
  19. package/src/collections/collection-types.ts +6 -1
  20. package/src/collections/providers.ts +3 -0
  21. package/src/collections/review.ts +70 -0
  22. package/src/core/app-icon.ts +10 -6
  23. package/src/core/layout.ts +2 -0
  24. package/src/reflect/prompt-corpus.ts +15 -119
  25. package/src/reflect/transcript.ts +254 -0
  26. package/src/repo-config.ts +3 -0
  27. package/src/review/anim.ts +228 -0
  28. package/src/review/auto.ts +141 -0
  29. package/src/review/format.ts +158 -0
  30. package/src/review/http.ts +23 -0
  31. package/src/review/marks.ts +43 -0
  32. package/src/review/media.ts +45 -0
  33. package/src/review/model.ts +896 -0
  34. package/src/review/project.ts +126 -0
  35. package/src/review/role.ts +48 -0
  36. package/src/review/routes.ts +239 -0
  37. package/src/review/store.ts +1011 -0
  38. package/src/review/threads.ts +237 -0
  39. package/src/review/trailer-desk.ts +385 -0
  40. package/src/review/trailer-model.ts +183 -0
  41. package/src/review/trailer-routes.ts +136 -0
  42. package/src/review/unit.ts +183 -0
  43. package/src/review/watchers.ts +54 -0
  44. package/src/serve/embed.ts +35 -0
  45. package/src/serve/hub-app/activity-view.tsx +1 -1
  46. package/src/serve/hub-app/activity.css +0 -4
  47. package/src/serve/hub-app/admin-svc.ts +1 -1
  48. package/src/serve/hub-app/admin-view.tsx +1 -1
  49. package/src/serve/hub-app/dock-view.tsx +5 -4
  50. package/src/serve/hub-app/hash-route.ts +36 -1
  51. package/src/serve/hub-app/header-bar.tsx +3 -1
  52. package/src/serve/hub-app/home-st.ts +17 -3
  53. package/src/serve/hub-app/home-strip.tsx +3 -2
  54. package/src/serve/hub-app/home.html +1 -0
  55. package/src/serve/hub-app/hub-app.tsx +80 -11
  56. package/src/serve/hub-app/hub.css +83 -14
  57. package/src/serve/hub-app/icons.tsx +15 -0
  58. package/src/serve/hub-app/index.html +1 -0
  59. package/src/serve/hub-app/insights-view.tsx +4 -3
  60. package/src/serve/hub-app/keys.ts +14 -10
  61. package/src/serve/hub-app/layout-st.ts +63 -2
  62. package/src/serve/hub-app/md.ts +32 -0
  63. package/src/serve/hub-app/planned-view.tsx +3 -2
  64. package/src/serve/hub-app/planned.css +0 -1
  65. package/src/serve/hub-app/project-list.css +26 -6
  66. package/src/serve/hub-app/project-list.tsx +46 -10
  67. package/src/serve/hub-app/projects-svc.ts +7 -2
  68. package/src/serve/hub-app/repo-sections.ts +15 -3
  69. package/src/serve/hub-app/repo-st.ts +61 -4
  70. package/src/serve/hub-app/review/clip-st.ts +258 -0
  71. package/src/serve/hub-app/review/kind-view.tsx +240 -0
  72. package/src/serve/hub-app/review/kit.tsx +113 -0
  73. package/src/serve/hub-app/review/review-answer.tsx +269 -0
  74. package/src/serve/hub-app/review/review-board.tsx +46 -0
  75. package/src/serve/hub-app/review/review-list.tsx +156 -0
  76. package/src/serve/hub-app/review/review-page.tsx +96 -0
  77. package/src/serve/hub-app/review/review-st.ts +902 -0
  78. package/src/serve/hub-app/review/review-views.tsx +648 -0
  79. package/src/serve/hub-app/review/trailer-page.tsx +416 -0
  80. package/src/serve/hub-app/review/trailer-st.ts +602 -0
  81. package/src/serve/hub-app/review/trailer-timeline.tsx +207 -0
  82. package/src/serve/hub-app/review.css +389 -0
  83. package/src/serve/hub-app/theme-st.ts +56 -0
  84. package/src/serve/hub-app/tree-build.ts +4 -0
  85. package/src/serve/hub-app/tree-row.tsx +1 -1
  86. package/src/serve/hub-app/tree-svc.ts +4 -0
  87. package/src/serve/hub-app/wiki-st.ts +190 -0
  88. package/src/serve/hub-app/wikis-view.tsx +134 -5
  89. package/src/serve/hub-runtime.ts +9 -7
  90. package/src/serve/project-gutter-route.ts +25 -1
  91. package/src/serve/project-gutter.ts +1 -1
  92. package/src/serve/same-origin.ts +5 -1
  93. package/src/serve/serve-hub.ts +15 -0
  94. package/src/shipkit-files.ts +9 -5
  95. package/src/shot/client.ts +17 -5
  96. package/src/shot/shooter.ts +19 -3
  97. package/src/shot/shot-ask.ts +9 -1
  98. package/src/shot/stepped-clip.ts +53 -0
  99. package/src/trailer/adapter.ts +90 -6
  100. package/src/trailer/build.ts +459 -59
  101. package/src/trailer/ffmpeg.ts +22 -0
  102. package/src/trailer/fonts.ts +55 -0
  103. package/src/trailer/footage.ts +112 -43
  104. package/src/trailer/grammar.ts +864 -127
  105. package/src/trailer/graph.ts +308 -49
  106. package/src/trailer/holes.ts +72 -0
  107. package/src/trailer/load-unit.ts +20 -3
  108. package/src/trailer/look.ts +20 -3
  109. package/src/trailer/motion.ts +207 -0
  110. package/src/trailer/passage.ts +129 -0
  111. package/src/trailer/plan.ts +82 -23
  112. package/src/trailer/prune.ts +204 -0
  113. package/src/trailer/score/compose.ts +256 -0
  114. package/src/trailer/score/dsp.ts +272 -0
  115. package/src/trailer/score/gm-render.ts +43 -0
  116. package/src/trailer/score/gm.ts +90 -0
  117. package/src/trailer/score/midi.ts +143 -0
  118. package/src/trailer/score/palette.ts +188 -0
  119. package/src/trailer/score/pattern.ts +98 -0
  120. package/src/trailer/score/score.ts +228 -0
  121. package/src/trailer/score/sound.ts +123 -0
  122. package/src/trailer/score/synth.ts +383 -0
  123. package/src/trailer/sound/audio.ts +148 -0
  124. package/src/trailer/sound/brand.ts +42 -0
  125. package/src/trailer/sound/comfy.ts +38 -0
  126. package/src/trailer/sound/eleven.ts +72 -0
  127. package/src/trailer/sound/fluid.ts +71 -0
  128. package/src/trailer/sound/gen.ts +139 -0
  129. package/src/trailer/sound/index.ts +60 -0
  130. package/src/trailer/sound/presets.ts +69 -0
  131. package/src/trailer/sound/run.ts +189 -0
  132. package/src/trailer/states.ts +76 -0
  133. package/src/trailer/stitch.ts +92 -9
  134. package/src/trailer/titles.ts +471 -150
  135. package/src/trailer/url.ts +18 -3
  136. package/src/trailer/vocab.ts +34 -4
  137. package/src/trailer/words.ts +275 -0
  138. package/src/wiki/graph-mount.ts +3 -0
  139. package/src/wiki/graph-page.ts +45 -9
  140. package/src/wiki/graph-pages.ts +128 -0
@@ -0,0 +1,218 @@
1
+ // The time math of one transcript stream (a session, or one sub-agent run):
2
+ // how long each tool call took, and where the wall time went.
3
+ //
4
+ // The constraint a reader breaks first: parallel calls overlap. A call's own
5
+ // time is end minus start, and a sum of those is CALL time, which can exceed
6
+ // the wall. The wall is cut once, by a sweep over every boundary, so a second
7
+ // of wall lands in exactly one bucket.
8
+ import type { Outcome } from '../reflect/transcript.ts'
9
+
10
+ export type Call = {
11
+ id: string
12
+ start: number
13
+ /** null while its result has not arrived (a live call, or one never answered) */
14
+ end: number | null
15
+ tool: string
16
+ shape: string
17
+ cmd: string
18
+ outcome: Outcome | null
19
+ code: number | null
20
+ /** it waited for the human to allow it */
21
+ approval: boolean
22
+ /** how it was allowed (transcript.ts Perm) */
23
+ perm: string
24
+ /** it only read (turns.ts isReadOnly, judged when indexed) */
25
+ read: boolean
26
+ }
27
+
28
+ export type Mark = { at: number; kind: 'prompt' | 'wake' | 'message' | 'reply' }
29
+
30
+ export type Stream = { calls: Call[]; marks: Mark[] }
31
+
32
+ /** where a slice of wall time went */
33
+ export type Bucket = 'model' | 'tools' | 'approval' | 'subagent' | 'background' | 'human' | 'idle'
34
+
35
+ export type Breakdown = Record<Bucket, number>
36
+
37
+ /** a wait on him longer than this is the session parked, not a wait */
38
+ export const IDLE_MS = 30 * 60 * 1000
39
+
40
+ export const emptyBreakdown = (): Breakdown => ({ model: 0, tools: 0, approval: 0, subagent: 0, background: 0, human: 0, idle: 0 })
41
+
42
+ /** a foreground sub-agent call: the session waits on another agent's work */
43
+ export const isAgentCall = (c: Call): boolean => c.tool === 'Agent' || c.tool === 'Task'
44
+
45
+ /** the first value of a sorted list above `t`, by bisection */
46
+ function firstAfter(sorted: number[], t: number): number | undefined {
47
+ let lo = 0
48
+ let hi = sorted.length
49
+ while (lo < hi) {
50
+ const mid = (lo + hi) >> 1
51
+ if ((sorted[mid] as number) <= t) lo = mid + 1
52
+ else hi = mid
53
+ }
54
+ return sorted[lo]
55
+ }
56
+
57
+ /** the end every call is measured to: its result, else the next prompt or wake
58
+ * after it (the human moved on, so it was interrupted), else the stream's last
59
+ * event. `open` says the end was inferred */
60
+ export type Closed = Call & { stop: number; open: boolean }
61
+
62
+ /** sorted by start */
63
+ export function closeCalls(s: Stream): Closed[] {
64
+ const turns = s.marks.filter((m) => m.kind !== 'reply').map((m) => m.at).sort((a, b) => a - b)
65
+ let last = 0
66
+ for (const m of s.marks) if (m.at > last) last = m.at
67
+ for (const c of s.calls) if ((c.end ?? c.start) > last) last = c.end ?? c.start
68
+ return s.calls
69
+ .map((c): Closed => {
70
+ if (c.end != null) return { ...c, stop: Math.max(c.start, c.end), open: false }
71
+ const next = firstAfter(turns, c.start)
72
+ return { ...c, stop: next ?? last, open: true }
73
+ })
74
+ .sort((a, b) => a.start - b.start)
75
+ }
76
+
77
+ /** the call's own duration in ms */
78
+ export const callMs = (c: { start: number; stop: number }): number => Math.max(0, c.stop - c.start)
79
+
80
+ type Point = { at: number; d: 'start' | 'stop' | Mark['kind']; lane: 'tool' | 'agent' | 'approval'; ix: number }
81
+
82
+ /** the wall of one stream cut into buckets, clipped to [from, to]. Between two
83
+ * boundaries: a tool in flight → tools; only calls waiting for his allow →
84
+ * approval; only a sub-agent in flight → subagent;
85
+ * nothing in flight and the next thing is his prompt → human (idle past
86
+ * IDLE_MS); the next thing is a wake (a task notification, a hand-back) →
87
+ * background; a message relayed in counts as a prompt (a sub-agent that
88
+ * handed back sits parked until the coordinator or he writes to it); anything else → the model was working.
89
+ *
90
+ * `turnModel` (one slot per closed call) receives the model stretch that
91
+ * ended in each call: the turn that issued it, split evenly over the calls
92
+ * of one batch. A stretch that ends in his prompt or a wake was the final
93
+ * reply and goes to no call */
94
+ export function wallBreakdown(s: Stream, from = -Infinity, to = Infinity, idleMs = IDLE_MS, closed: Closed[] = closeCalls(s), turnModel?: Float64Array): Breakdown {
95
+ const out = emptyBreakdown()
96
+ const points: Point[] = []
97
+ closed.forEach((c, ix) => {
98
+ const lane = isAgentCall(c) ? 'agent' : c.approval ? 'approval' : 'tool'
99
+ points.push({ at: c.start, d: 'start', lane, ix }, { at: c.stop, d: 'stop', lane, ix })
100
+ })
101
+ for (const m of s.marks) points.push({ at: m.at, d: m.kind, lane: 'tool', ix: -1 })
102
+ points.sort((a, b) => a.at - b.at)
103
+ let tools = 0
104
+ let agents = 0
105
+ let approvals = 0
106
+ let pending = 0
107
+ const issued: number[] = []
108
+ let i = 0
109
+ while (i < points.length) {
110
+ const at = (points[i] as Point).at
111
+ issued.length = 0
112
+ let ended = false
113
+ while (i < points.length && (points[i] as Point).at === at) {
114
+ const p = points[i] as Point
115
+ const n = p.d === 'start' ? 1 : p.d === 'stop' ? -1 : 0
116
+ if (p.lane === 'agent') agents += n
117
+ else if (p.lane === 'approval') approvals += n
118
+ else tools += n
119
+ if (p.d === 'start') issued.push(p.ix)
120
+ else if (p.d === 'prompt' || p.d === 'wake' || p.d === 'message') ended = true
121
+ i++
122
+ }
123
+ if (turnModel != null) {
124
+ if (issued.length > 0) for (const ix of issued) turnModel[ix] = (turnModel[ix] ?? 0) + pending / issued.length
125
+ if (issued.length > 0 || ended) pending = 0
126
+ }
127
+ if (i >= points.length) break
128
+ const nextAt = (points[i] as Point).at
129
+ const a = Math.max(at, from)
130
+ const b = Math.min(nextAt, to)
131
+ if (b <= a) continue
132
+ const len = b - a
133
+ let bucket: Bucket
134
+ if (tools > 0) bucket = 'tools'
135
+ else if (approvals > 0) bucket = 'approval'
136
+ else if (agents > 0) bucket = 'subagent'
137
+ else {
138
+ let next = i
139
+ let prompt = false
140
+ let wake = false
141
+ while (next < points.length && (points[next] as Point).at === nextAt) {
142
+ const d = (points[next] as Point).d
143
+ if (d === 'prompt' || d === 'message') prompt = true
144
+ if (d === 'wake') wake = true
145
+ next++
146
+ }
147
+ const gap = nextAt - at
148
+ bucket = prompt ? (gap > idleMs ? 'idle' : 'human') : wake ? 'background' : 'model'
149
+ }
150
+ out[bucket] += len
151
+ if (bucket === 'model') pending += len
152
+ }
153
+ return out
154
+ }
155
+
156
+ export const addBreakdown = (into: Breakdown, b: Breakdown): Breakdown => {
157
+ for (const k of Object.keys(into) as Bucket[]) into[k] += b[k]
158
+ return into
159
+ }
160
+
161
+ /** a failed call is retried when the next call of the same shape in the
162
+ * stream comes within this window */
163
+ export const RETRY_MS = 10 * 60 * 1000
164
+
165
+ export const FAILED: ReadonlySet<Outcome> = new Set<Outcome>(['exit', 'error', 'timeout'])
166
+
167
+ export type Retry = { failed: Closed; next: Closed; blind: boolean }
168
+
169
+ /** every failed call followed by the same shape again: `blind` when the
170
+ * command is the exact same text, so nothing changed between the two.
171
+ * `calls` sorted by start (closeCalls) */
172
+ export function retries(calls: Closed[]): Retry[] {
173
+ const out: Retry[] = []
174
+ const nextOfShape = new Map<string, Closed>()
175
+ for (let i = calls.length - 1; i >= 0; i--) {
176
+ const c = calls[i] as Closed
177
+ const next = nextOfShape.get(c.shape)
178
+ nextOfShape.set(c.shape, c)
179
+ if (c.outcome == null || !FAILED.has(c.outcome)) continue
180
+ if (next == null || next.start - c.stop > RETRY_MS) continue
181
+ out.push({ failed: c, next, blind: next.cmd.trim() === c.cmd.trim() })
182
+ }
183
+ return out.reverse()
184
+ }
185
+
186
+ /** a call that ran longer than this before a complaint is not looked for */
187
+ const LOOKBACK_MS = 3 * 60 * 60 * 1000
188
+
189
+ /** what the agent ran in the minutes before `at`: calls in flight at that
190
+ * moment or started in the window, cut at `at`, longest first. `calls`
191
+ * sorted by start (closeCalls) */
192
+ export function ranBefore(calls: Closed[], at: number, windowMs = 10 * 60 * 1000): Closed[] {
193
+ let lo = 0
194
+ let hi = calls.length
195
+ while (lo < hi) {
196
+ const mid = (lo + hi) >> 1
197
+ if ((calls[mid] as Closed).start <= at) lo = mid + 1
198
+ else hi = mid
199
+ }
200
+ const out: Closed[] = []
201
+ for (let i = lo - 1; i >= 0; i--) {
202
+ const c = calls[i] as Closed
203
+ if (c.start < at - LOOKBACK_MS) break
204
+ if (c.stop >= at - windowMs) out.push({ ...c, stop: Math.min(c.stop, at) })
205
+ }
206
+ return out.sort((a, b) => callMs(b) - callMs(a))
207
+ }
208
+
209
+ /** p in [0, 1] of a list of ms, nearest rank */
210
+ export function quantile(xs: number[], p: number): number {
211
+ return quantileSorted([...xs].sort((a, b) => a - b), p)
212
+ }
213
+
214
+ /** the same over a list already sorted ascending */
215
+ export function quantileSorted(s: number[], p: number): number {
216
+ if (s.length === 0) return 0
217
+ return s[Math.min(s.length - 1, Math.max(0, Math.ceil(p * s.length) - 1))] ?? 0
218
+ }
@@ -0,0 +1,135 @@
1
+ // Turn count: what makes an agent slow when no command is. Every tool call
2
+ // costs a model turn before it, so an agent that reads one file per turn
3
+ // spends its wall on the model, not on the reads. The two questions this
4
+ // module answers on pure data: was a call a single read, and did it go out in
5
+ // parallel with the one before it.
6
+ //
7
+ // The constraint a reader breaks first: a read is judged on the WHOLE command,
8
+ // never on its shape alone. `grep -l x | xargs sed -i` shapes as `grep`, and
9
+ // it writes.
10
+ import { stripHeredocs } from './shape.ts'
11
+ import type { Closed } from './timeline.ts'
12
+
13
+ /** bumped with any read rule below: the index judges every stored call again */
14
+ export const READ_VERSION = 1
15
+
16
+ /** tools that only read */
17
+ export const READ_TOOLS: ReadonlySet<string> = new Set(['Read', 'Grep', 'Glob'])
18
+
19
+ /** Bash shapes that only read (shape.ts names them). `sed` without `-i` prints;
20
+ * `sed -i` is shaped `sed -i (edit)` and `cat > f` is `cat > (write)`, so
21
+ * neither is here */
22
+ export const READ_SHAPES: ReadonlySet<string> = new Set(['grep', 'rg', 'git grep', 'sed', 'cat', 'ls', 'head', 'tail', 'wc', 'find', 'git status', 'git log', 'git diff', 'git show'])
23
+
24
+ /** words a read command may carry beside its read: scene setting, loop and
25
+ * test words, a filter it pipes into, a print */
26
+ const BESIDE = new Set([
27
+ 'cd', 'pushd', 'popd', 'export', 'set', 'true', ':', 'time', 'for', 'do', 'done', 'while', 'if', 'then', 'else', 'fi', '[', '[[', 'test',
28
+ 'echo', 'printf', 'pwd', 'uptime', 'date', 'which', 'basename', 'dirname', 'realpath', 'stat', 'file', 'du', 'tree', 'diff',
29
+ 'sort', 'uniq', 'cut', 'tr', 'nl', 'column', 'jq', 'awk', 'xargs', 'grep', 'egrep', 'rg', 'head', 'tail', 'wc', 'cat', 'sed', 'ls', 'find', 'git',
30
+ ])
31
+
32
+ /** a find action that writes or runs something */
33
+ const FIND_ACTS = /\s-(?:delete|exec|execdir|ok|okdir|fprint0?|fprintf|fls)\b/
34
+
35
+ /** git subcommands that only read */
36
+ const GIT_READS = new Set(['status', 'log', 'diff', 'show', 'grep', 'ls-files', 'ls-tree', 'rev-parse', 'rev-list', 'merge-base', 'cat-file', 'describe', 'shortlog', 'branch', 'blame'])
37
+
38
+ /** the parts of a command line: statements and every pipeline part, quoted
39
+ * text blanked (a pattern is never a separator nor a redirect) */
40
+ const parts = (command: string): string[] =>
41
+ (command.includes('<<') ? stripHeredocs(command) : command)
42
+ .replace(/\\\n/g, ' ')
43
+ .replace(/'[^']*'|"(?:[^"\\]|\\.)*"/g, '""')
44
+ .split(/&&|\|\||[;\n|]|(?<![>&])&(?![>&])/)
45
+
46
+ /** an output redirect to a file: `> f`, `>> f`, `&> f`; `2>&1` and
47
+ * `> /dev/null` write nothing */
48
+ const writesFile = (part: string): boolean => {
49
+ for (const m of part.matchAll(/>{1,2}\s*([^\s>]*)/g)) {
50
+ const target = m[1] ?? ''
51
+ if (target === '' || target.startsWith('&') || target === '/dev/null' || target.startsWith('/dev/std')) continue
52
+ return true
53
+ }
54
+ return false
55
+ }
56
+
57
+ /** the words of a part after env assignments and a leading `X=$(` */
58
+ const HEAD = /^[\s(]*(?:[A-Za-z_][A-Za-z0-9_]*=(?:\$\(|\S*\s+))*(\S+)\s*(.*)$/s
59
+
60
+ /** one part of the line reads only */
61
+ function partReads(part: string, whole: string): boolean {
62
+ if (writesFile(part)) return false
63
+ if (/^\s*[A-Za-z_][A-Za-z0-9_]*=\S*\s*$/.test(part)) return true
64
+ const m = HEAD.exec(part)
65
+ if (m == null) return true
66
+ const head = (m[1] as string).replace(/^.*\//, '').replace(/\)+$/, '')
67
+ const rest = ` ${m[2] ?? ''}`
68
+ if (!BESIDE.has(head)) return false
69
+ if (head === 'sed') return !/\s-[a-zA-Z]*i|\s--in-place/.test(rest)
70
+ if (head === 'find') return !FIND_ACTS.test(rest)
71
+ if (head === 'awk') return !/system\(|print[^;'"]*>/.test(whole)
72
+ if (head === 'xargs') return partReads(rest.replace(/^(?:\s+-\S+(?:\s+\d+)?)*/, ''), whole)
73
+ if (head === 'git') return GIT_READS.has(/^(?:\s+-[Cc]\s+\S+|\s+-\S+)*\s+(\S+)/.exec(rest)?.[1] ?? '')
74
+ if (head === 'do' || head === 'then' || head === 'else') return rest.trim() === '' || partReads(rest, whole)
75
+ return true
76
+ }
77
+
78
+ /** the call only read: a read tool, or a Bash command of a read shape (a for
79
+ * loop over one included) whose every part reads */
80
+ export function isReadOnly(c: { tool: string; shape: string; cmd: string }): boolean {
81
+ if (READ_TOOLS.has(c.tool)) return true
82
+ if (c.tool !== 'Bash' || !READ_SHAPES.has(c.shape.replace(/^\(for\) /, ''))) return false
83
+ return parts(c.cmd).every((p) => p.trim() === '' || partReads(p, c.cmd))
84
+ }
85
+
86
+ /** a call issued this soon after the one before, while that one still ran,
87
+ * went out in the same turn */
88
+ export const PARALLEL_MS = 2000
89
+
90
+ /** one flag per call (sorted by start, closeCalls): it started before the
91
+ * previous call ended, within PARALLEL_MS of its start */
92
+ export function parallelFlags(calls: readonly Closed[]): boolean[] {
93
+ return calls.map((c, i) => {
94
+ const prev = calls[i - 1]
95
+ return prev != null && c.start < prev.stop && c.start - prev.start <= PARALLEL_MS
96
+ })
97
+ }
98
+
99
+ /** the turn metrics of a set of streams */
100
+ export type TurnStats = { calls: number; reads: number; parallel: number; model: number }
101
+
102
+ export const emptyTurns = (): TurnStats => ({ calls: 0, reads: 0, parallel: 0, model: 0 })
103
+
104
+ export const addTurns = (into: TurnStats, t: TurnStats): TurnStats => {
105
+ into.calls += t.calls
106
+ into.reads += t.reads
107
+ into.parallel += t.parallel
108
+ into.model += t.model
109
+ return into
110
+ }
111
+
112
+ /** what a read shape costs in model turns: its calls, and the model time of
113
+ * the turns that issued them */
114
+ export type ReadCost = { shape: string; calls: number; model: number }
115
+
116
+ /** a read call's name in the cost table: its tool, or its Bash shape */
117
+ export const readName = (c: { tool: string; shape: string }): string => (c.tool === 'Bash' ? c.shape : c.tool)
118
+
119
+ /** the turns of one stream: `model` is its model bucket, `turnModel` the
120
+ * per call share wallBreakdown filled; read calls add to `costs` */
121
+ export function turnsOf(calls: readonly Closed[], model: number, turnModel: Float64Array, costs: Map<string, ReadCost>): TurnStats {
122
+ const par = parallelFlags(calls)
123
+ const t: TurnStats = { calls: calls.length, reads: 0, parallel: 0, model }
124
+ calls.forEach((c, i) => {
125
+ if (par[i]) t.parallel++
126
+ if (!c.read) return
127
+ t.reads++
128
+ const name = readName(c)
129
+ const row = costs.get(name) ?? { shape: name, calls: 0, model: 0 }
130
+ row.calls++
131
+ row.model += turnModel[i] ?? 0
132
+ costs.set(name, row)
133
+ })
134
+ return t
135
+ }
@@ -16,6 +16,7 @@ import { dirname, join } from 'path'
16
16
  import sharp from 'sharp'
17
17
  import { defineKind, type CanvasGen, type ComfyGen, type Kind } from '../kit-config.ts'
18
18
  import { HOUSE_FAMILIES, type TrailerLookInput } from '../../trailer/look.ts'
19
+ import type { TrailerSoundInput } from '../../trailer/sound/brand.ts'
19
20
 
20
21
  export type Brand = {
21
22
  name: string
@@ -31,8 +32,10 @@ export type Brand = {
31
32
  * name/tagline right); the hook owns compositing the mark if it wants one */
32
33
  drawOg?: (ctx: SKRSContext2D, brand: Brand, size: { w: number; h: number }) => void
33
34
  /** the look every trailer of the repo wears (titles, card, face, logo);
34
- * each value left out is derived from the palette (trailer/look.ts) */
35
- trailer?: TrailerLookInput
35
+ * each value left out is derived from the palette (trailer/look.ts);
36
+ * its sound section (`comfy`, `elevenKey`, `voice`, `voices`) makes the
37
+ * voice, sfx and music lines (trailer/sound/brand.ts) */
38
+ trailer?: TrailerLookInput & TrailerSoundInput
36
39
  }
37
40
 
38
41
  export const FAVICON_SIZE = 512
@@ -17,6 +17,11 @@ export type CommandRow = {
17
17
  examples: [string, string][]
18
18
  /** a filmable command (cli/kit/verb.ts): takes --out, a clip also --seconds */
19
19
  film?: Film
20
+ /** a filmable command's take version and length (build/trailer.md, Film lines) */
21
+ version?: number | string
22
+ length?: 'own'
23
+ words?: Record<string, unknown>
24
+ stamp?: string[]
20
25
  /** listed on the essentials page */
21
26
  essential?: Essential
22
27
  /** what a kit row runs */
@@ -76,6 +81,10 @@ export function commandRows(plain: Definition[], detailed: Definition[] = [], bi
76
81
  if (c != null) {
77
82
  const m = marksOf(c)
78
83
  if (m.film != null) row.film = m.film
84
+ if (m.version != null) row.version = m.version
85
+ if (m.length != null) row.length = m.length
86
+ if (m.words != null) row.words = m.words
87
+ if (m.stamp != null) row.stamp = m.stamp
79
88
  if (m.essential != null) row.essential = m.essential
80
89
  const v = ROW_OF.get(c)
81
90
  if (v != null) row.runs = runText(v.run)