@defrex/autobuild 0.8.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 (251) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +192 -0
  3. package/SPEC.md +2269 -0
  4. package/bin/ab-build-runner.ts +47 -0
  5. package/bin/ab-dev.ts +27 -0
  6. package/bin/ab-dispatch-kernel.ts +92 -0
  7. package/bin/ab-harvest-runner.ts +29 -0
  8. package/bin/ab-init-probe.ts +19 -0
  9. package/bin/ab-process-group-reaper.ts +32 -0
  10. package/bin/ab.ts +6 -0
  11. package/bin/agent/ab +3 -0
  12. package/docs/answering-blocked-builds.md +89 -0
  13. package/docs/architecture.md +726 -0
  14. package/docs/assets/headline-wide.png +0 -0
  15. package/docs/configuration.md +1623 -0
  16. package/docs/hosted-dispatcher-cutover-validation.md +178 -0
  17. package/docs/hosted-dispatcher.md +204 -0
  18. package/docs/mcp.md +119 -0
  19. package/docs/operator-api.md +186 -0
  20. package/docs/remote-store-protocol.md +972 -0
  21. package/docs/remote-ticket-protocol.md +79 -0
  22. package/docs/setup.md +289 -0
  23. package/docs/spec-standard.md +63 -0
  24. package/docs/ticket-dependencies.md +59 -0
  25. package/docs/vercel-sandbox-rollout-probe.md +7 -0
  26. package/package.json +135 -0
  27. package/packages/core/src/cli/args.ts +74 -0
  28. package/packages/core/src/cli/artifact.ts +182 -0
  29. package/packages/core/src/cli/binary.ts +198 -0
  30. package/packages/core/src/cli/build-control.ts +789 -0
  31. package/packages/core/src/cli/build-progress.ts +55 -0
  32. package/packages/core/src/cli/bulk-control.ts +385 -0
  33. package/packages/core/src/cli/context.ts +470 -0
  34. package/packages/core/src/cli/dashboard/actions.ts +59 -0
  35. package/packages/core/src/cli/dashboard/cells.ts +92 -0
  36. package/packages/core/src/cli/dashboard/composer.ts +230 -0
  37. package/packages/core/src/cli/dashboard/detail.ts +100 -0
  38. package/packages/core/src/cli/dashboard/frame-image.ts +373 -0
  39. package/packages/core/src/cli/dashboard/live.ts +152 -0
  40. package/packages/core/src/cli/dashboard/model.ts +1173 -0
  41. package/packages/core/src/cli/dashboard/poll.ts +196 -0
  42. package/packages/core/src/cli/dashboard/render.ts +1568 -0
  43. package/packages/core/src/cli/dashboard/selection.ts +66 -0
  44. package/packages/core/src/cli/dashboard/session-feed.ts +244 -0
  45. package/packages/core/src/cli/dashboard/session-view.ts +506 -0
  46. package/packages/core/src/cli/dashboard/transcript.ts +108 -0
  47. package/packages/core/src/cli/dev.ts +46 -0
  48. package/packages/core/src/cli/dispatch-frontend.ts +1045 -0
  49. package/packages/core/src/cli/dispatch-process.ts +259 -0
  50. package/packages/core/src/cli/dispatch.ts +3692 -0
  51. package/packages/core/src/cli/dotenv.ts +48 -0
  52. package/packages/core/src/cli/env.ts +206 -0
  53. package/packages/core/src/cli/harvest.ts +698 -0
  54. package/packages/core/src/cli/help.ts +713 -0
  55. package/packages/core/src/cli/init-agent.ts +106 -0
  56. package/packages/core/src/cli/init-validation.ts +583 -0
  57. package/packages/core/src/cli/init.ts +756 -0
  58. package/packages/core/src/cli/installation.ts +333 -0
  59. package/packages/core/src/cli/keyboard.ts +261 -0
  60. package/packages/core/src/cli/main.ts +1399 -0
  61. package/packages/core/src/cli/mcp.ts +279 -0
  62. package/packages/core/src/cli/models.ts +65 -0
  63. package/packages/core/src/cli/observe.ts +55 -0
  64. package/packages/core/src/cli/plugin.ts +227 -0
  65. package/packages/core/src/cli/pr-attachments.ts +138 -0
  66. package/packages/core/src/cli/pr-summary.ts +124 -0
  67. package/packages/core/src/cli/publication-settlement.ts +188 -0
  68. package/packages/core/src/cli/repo-state.ts +213 -0
  69. package/packages/core/src/cli/repository-status.ts +80 -0
  70. package/packages/core/src/cli/self-update.ts +443 -0
  71. package/packages/core/src/cli/status.ts +895 -0
  72. package/packages/core/src/cli/store-opening.contract.ts +289 -0
  73. package/packages/core/src/cli/store-opening.ts +167 -0
  74. package/packages/core/src/cli/store-ref.ts +33 -0
  75. package/packages/core/src/cli/terminal-restore.ts +174 -0
  76. package/packages/core/src/cli/terminal.ts +361 -0
  77. package/packages/core/src/cli/terminals.ts +995 -0
  78. package/packages/core/src/cli/testkit.ts +194 -0
  79. package/packages/core/src/cli/ticket.ts +484 -0
  80. package/packages/core/src/cli/upgrade-agent.ts +180 -0
  81. package/packages/core/src/cli/upgrade-commit.ts +468 -0
  82. package/packages/core/src/cli/upgrade-notice.ts +91 -0
  83. package/packages/core/src/cli/upgrade-progress.ts +110 -0
  84. package/packages/core/src/cli/upgrade.ts +888 -0
  85. package/packages/core/src/cli/wait.ts +587 -0
  86. package/packages/core/src/cli/watch.ts +932 -0
  87. package/packages/core/src/config/live.ts +255 -0
  88. package/packages/core/src/config/load.ts +98 -0
  89. package/packages/core/src/config/roles.ts +438 -0
  90. package/packages/core/src/config/schema.ts +774 -0
  91. package/packages/core/src/distribution.ts +24 -0
  92. package/packages/core/src/events/catalog.ts +201 -0
  93. package/packages/core/src/events/envelope.ts +59 -0
  94. package/packages/core/src/events/payloads.ts +618 -0
  95. package/packages/core/src/events/repository.ts +559 -0
  96. package/packages/core/src/events/sessions.ts +180 -0
  97. package/packages/core/src/harvest/schema.ts +147 -0
  98. package/packages/core/src/ids.ts +45 -0
  99. package/packages/core/src/integration/harness.ts +681 -0
  100. package/packages/core/src/kernel/auto-merge.ts +259 -0
  101. package/packages/core/src/kernel/converge.ts +117 -0
  102. package/packages/core/src/kernel/dispatch-settings.ts +60 -0
  103. package/packages/core/src/kernel/dispatch-status.ts +141 -0
  104. package/packages/core/src/kernel/engine.ts +1047 -0
  105. package/packages/core/src/kernel/harvest.ts +823 -0
  106. package/packages/core/src/kernel/origin.ts +69 -0
  107. package/packages/core/src/kernel/phases.ts +196 -0
  108. package/packages/core/src/kernel/plan-verify-selection.ts +105 -0
  109. package/packages/core/src/kernel/pr-attachments.ts +82 -0
  110. package/packages/core/src/kernel/reducer.ts +752 -0
  111. package/packages/core/src/kernel/stall.ts +155 -0
  112. package/packages/core/src/kernel/verify-applicability.ts +52 -0
  113. package/packages/core/src/markdown.ts +67 -0
  114. package/packages/core/src/ontology.ts +288 -0
  115. package/packages/core/src/open-map.ts +203 -0
  116. package/packages/core/src/operator/annotations.ts +187 -0
  117. package/packages/core/src/operator/client.ts +311 -0
  118. package/packages/core/src/operator/control.ts +117 -0
  119. package/packages/core/src/operator/index.ts +62 -0
  120. package/packages/core/src/operator/presentation.ts +22 -0
  121. package/packages/core/src/operator/protocol.ts +98 -0
  122. package/packages/core/src/operator/query.ts +201 -0
  123. package/packages/core/src/operator/registry.ts +1043 -0
  124. package/packages/core/src/operator/requests.ts +138 -0
  125. package/packages/core/src/operator/sandbox.ts +458 -0
  126. package/packages/core/src/operator/server.ts +616 -0
  127. package/packages/core/src/operator/sessions.ts +223 -0
  128. package/packages/core/src/operator/tickets.ts +177 -0
  129. package/packages/core/src/plugin-sdk/index.ts +174 -0
  130. package/packages/core/src/plugins/contract-entry.ts +74 -0
  131. package/packages/core/src/plugins/contract-env.ts +3 -0
  132. package/packages/core/src/plugins/load.ts +259 -0
  133. package/packages/core/src/plugins/manifest.ts +236 -0
  134. package/packages/core/src/plugins/registry.ts +274 -0
  135. package/packages/core/src/plugins/runtimes.ts +75 -0
  136. package/packages/core/src/ports/forge/contract.ts +258 -0
  137. package/packages/core/src/ports/forge/create.ts +74 -0
  138. package/packages/core/src/ports/forge/fake.ts +488 -0
  139. package/packages/core/src/ports/forge/github-pr-attachments.ts +351 -0
  140. package/packages/core/src/ports/forge/github-transport.ts +352 -0
  141. package/packages/core/src/ports/forge/github.ts +1162 -0
  142. package/packages/core/src/ports/forge/local-git.ts +568 -0
  143. package/packages/core/src/ports/runner/claude.ts +873 -0
  144. package/packages/core/src/ports/runner/cli-stream.ts +90 -0
  145. package/packages/core/src/ports/runner/codex.ts +873 -0
  146. package/packages/core/src/ports/runner/contract.ts +492 -0
  147. package/packages/core/src/ports/runner/fake.ts +213 -0
  148. package/packages/core/src/ports/runner/json-record.ts +2 -0
  149. package/packages/core/src/ports/runner/live-contract-fixture.ts +176 -0
  150. package/packages/core/src/ports/runner/one-shot.ts +29 -0
  151. package/packages/core/src/ports/runner/pi-bridge-core.ts +136 -0
  152. package/packages/core/src/ports/runner/pi-bridge.js +6 -0
  153. package/packages/core/src/ports/runner/pi-rpc.ts +454 -0
  154. package/packages/core/src/ports/runner/pi.ts +617 -0
  155. package/packages/core/src/ports/runner/production.ts +74 -0
  156. package/packages/core/src/ports/runner/provider-error.ts +121 -0
  157. package/packages/core/src/ports/runner/routing.ts +234 -0
  158. package/packages/core/src/ports/runner/runtime.ts +214 -0
  159. package/packages/core/src/ports/runner/session-env.ts +34 -0
  160. package/packages/core/src/ports/runner/stream-parts.ts +103 -0
  161. package/packages/core/src/ports/tickets/contract.ts +570 -0
  162. package/packages/core/src/ports/tickets/create.ts +135 -0
  163. package/packages/core/src/ports/tickets/fake.ts +215 -0
  164. package/packages/core/src/ports/tickets/file.ts +559 -0
  165. package/packages/core/src/ports/tickets/linear.ts +811 -0
  166. package/packages/core/src/ports/tickets/operations.ts +161 -0
  167. package/packages/core/src/ports/tickets/remote-index.ts +13 -0
  168. package/packages/core/src/ports/tickets/remote-protocol.ts +80 -0
  169. package/packages/core/src/ports/tickets/remote-server.ts +185 -0
  170. package/packages/core/src/ports/tickets/remote.ts +130 -0
  171. package/packages/core/src/ports/tickets/update.ts +51 -0
  172. package/packages/core/src/ports/types.ts +528 -0
  173. package/packages/core/src/ports/workspace/build-execution.ts +123 -0
  174. package/packages/core/src/ports/workspace/contract.ts +136 -0
  175. package/packages/core/src/ports/workspace/create.ts +132 -0
  176. package/packages/core/src/ports/workspace/distribution-archive.ts +317 -0
  177. package/packages/core/src/ports/workspace/fake.ts +432 -0
  178. package/packages/core/src/ports/workspace/git-worktree.ts +848 -0
  179. package/packages/core/src/ports/workspace/harvest-execution.ts +111 -0
  180. package/packages/core/src/ports/workspace/in-process-build-execution.ts +30 -0
  181. package/packages/core/src/ports/workspace/local-build-execution.ts +88 -0
  182. package/packages/core/src/ports/workspace/operator-sandbox.ts +142 -0
  183. package/packages/core/src/ports/workspace/phase-scratch.ts +156 -0
  184. package/packages/core/src/ports/workspace/vercel-sandbox.ts +2541 -0
  185. package/packages/core/src/processes/build-child-exit.ts +78 -0
  186. package/packages/core/src/processes/build-child.ts +119 -0
  187. package/packages/core/src/processes/build-execution-state.ts +95 -0
  188. package/packages/core/src/processes/build-parent-watch.ts +35 -0
  189. package/packages/core/src/processes/build-runner.ts +2614 -0
  190. package/packages/core/src/processes/dispatcher.ts +2825 -0
  191. package/packages/core/src/processes/execution-settlement.ts +171 -0
  192. package/packages/core/src/processes/harvest-child.ts +115 -0
  193. package/packages/core/src/processes/harvest-execution-state.ts +121 -0
  194. package/packages/core/src/processes/harvest-runner.ts +1656 -0
  195. package/packages/core/src/processes/harvest.ts +595 -0
  196. package/packages/core/src/processes/infrastructure-failure-budget.ts +146 -0
  197. package/packages/core/src/processes/phase-failure-budget.ts +52 -0
  198. package/packages/core/src/processes/process-group-reaper.ts +35 -0
  199. package/packages/core/src/processes/process-group.ts +65 -0
  200. package/packages/core/src/processes/publication-loss.ts +62 -0
  201. package/packages/core/src/processes/publication-state.ts +93 -0
  202. package/packages/core/src/processes/sandbox-state.ts +101 -0
  203. package/packages/core/src/registry.ts +24 -0
  204. package/packages/core/src/sessions/reducer.ts +185 -0
  205. package/packages/core/src/skills.ts +7 -0
  206. package/packages/core/src/spec-standard.ts +47 -0
  207. package/packages/core/src/store/adapter.ts +80 -0
  208. package/packages/core/src/store/build-scope.ts +272 -0
  209. package/packages/core/src/store/contract.ts +2581 -0
  210. package/packages/core/src/store/local/blobs.ts +38 -0
  211. package/packages/core/src/store/local/schema.ts +180 -0
  212. package/packages/core/src/store/local/store.ts +1713 -0
  213. package/packages/core/src/store/memory.ts +1138 -0
  214. package/packages/core/src/store/phase-session.ts +425 -0
  215. package/packages/core/src/store/remote/client.ts +780 -0
  216. package/packages/core/src/store/remote/index.ts +12 -0
  217. package/packages/core/src/store/remote/protocol.ts +348 -0
  218. package/packages/core/src/store/remote/server.ts +923 -0
  219. package/packages/core/src/store/remote/token.ts +137 -0
  220. package/packages/core/src/store/remote/version.ts +10 -0
  221. package/packages/core/src/store/retention.ts +91 -0
  222. package/packages/core/src/store/session-handle.ts +265 -0
  223. package/packages/core/src/store/streams/assemble.ts +79 -0
  224. package/packages/core/src/store/streams/lock.ts +42 -0
  225. package/packages/core/src/store/streams/session-writer.ts +306 -0
  226. package/packages/core/src/store/streams/types.ts +171 -0
  227. package/packages/core/src/store/streams/wait.ts +127 -0
  228. package/packages/core/src/store/subscribe.ts +105 -0
  229. package/packages/core/src/store/types.ts +466 -0
  230. package/packages/core/src/testing/fixed.ts +44 -0
  231. package/packages/core/src/testing/store-failures.ts +32 -0
  232. package/packages/core/src/zod-issues.ts +91 -0
  233. package/skills/code-review/SKILL.md +100 -0
  234. package/skills/finalize/SKILL.md +44 -0
  235. package/skills/guide/SKILL.md +1781 -0
  236. package/skills/guide/references/operator-api.md +164 -0
  237. package/skills/guide/references/plugin-authoring.md +354 -0
  238. package/skills/guide/references/remote-store-protocol.md +972 -0
  239. package/skills/guide/references/remote-ticket-protocol.md +79 -0
  240. package/skills/guide/references/setup.md +289 -0
  241. package/skills/guide/references/spec-standard.md +63 -0
  242. package/skills/guide/references/ticket-dependencies.md +59 -0
  243. package/skills/harvest/SKILL.md +66 -0
  244. package/skills/harvest-review/SKILL.md +63 -0
  245. package/skills/implement/SKILL.md +82 -0
  246. package/skills/plan/SKILL.md +123 -0
  247. package/skills/plan-review/SKILL.md +121 -0
  248. package/skills/reconcile/SKILL.md +65 -0
  249. package/skills/spec/SKILL.md +132 -0
  250. package/skills/tickets/SKILL.md +139 -0
  251. package/templates/autobuild.toml +36 -0
@@ -0,0 +1,506 @@
1
+ /**
2
+ * The shared session-view projection: protocol parts → display lines.
3
+ *
4
+ * This module is the one place the session view's VOCABULARY is decided. The
5
+ * terminal renderer consumes its output and so will the web app later; nothing
6
+ * outside this file decides what a part says. Two adapters feed one rendering
7
+ * core:
8
+ *
9
+ * - `projectSessionParts` renders the live/chunk path — raw `StreamPart[]`
10
+ * in store-sequence order, including parts that have not assembled (a tool
11
+ * call with input but no output yet).
12
+ * - `projectSessionDocument` renders the finalized `UIMessage[]` artifact
13
+ * that `closeStream` deposits. Note the SDK's document assembly drops
14
+ * `error` and `abort` chunks (they never become message parts), so those
15
+ * elements are only visible on the parts path.
16
+ *
17
+ * Both are pure functions of (input, width): stored parts are never altered,
18
+ * and no line depends on color. Reasoning is marked by a literal `~ ` prefix so
19
+ * it stays distinct from answer text even with the escapes stripped.
20
+ */
21
+ import type { UIMessage } from 'ai'
22
+ import type { StreamPart } from '../../store/streams/types'
23
+ import { cellWidth, displayText, graphemes } from './cells'
24
+ import { wrapDisplay } from './composer'
25
+
26
+ /** Documented cap on the wrapped rows rendered for one tool output. */
27
+ export const SESSION_OUTPUT_MAX_ROWS = 8
28
+
29
+ /** One `data-ab-session` part's bracket identity, the header's source. */
30
+ export interface SessionHeaderInfo {
31
+ session: string
32
+ role: string
33
+ runner: string
34
+ model?: string
35
+ phase: string
36
+ round?: number
37
+ }
38
+
39
+ type DisplayEvent =
40
+ | { kind: 'header'; info: SessionHeaderInfo }
41
+ | { kind: 'prompt'; text: string }
42
+ | { kind: 'text'; text: string }
43
+ | { kind: 'reasoning'; text: string }
44
+ | {
45
+ kind: 'tool'
46
+ toolName: string
47
+ input?: unknown
48
+ output?: unknown
49
+ awaiting: boolean
50
+ errorText?: string
51
+ }
52
+ | { kind: 'step' }
53
+ | { kind: 'error'; text: string }
54
+ | { kind: 'abort'; text: string }
55
+ | { kind: 'truncation'; bytes: number }
56
+ | { kind: 'data'; name: string }
57
+
58
+ /**
59
+ * The dashboard's prose wrap. Duplicated from `render.ts`'s file-local
60
+ * `wrappedText` rather than exported from there: that file already imports
61
+ * this module's output vocabulary, so sharing the helper would create an
62
+ * import cycle for one narrow function. This copy keeps the same byte-stable
63
+ * behavior — paragraph structure preserved, a token wider than the line
64
+ * truncated with a `~`.
65
+ */
66
+ function wrapProse(value: string, width: number, indent = ''): string[] {
67
+ if (width <= 0) return []
68
+ const lines: string[] = []
69
+ for (const paragraph of value.split(/\r?\n/)) {
70
+ const safe = displayText(paragraph)
71
+ if (safe.length === 0) {
72
+ lines.push(indent)
73
+ continue
74
+ }
75
+ const wrapped = wrapDisplay(safe, width, indent)
76
+ lines.push(...(wrapped.length > 0 ? wrapped : [indent]))
77
+ }
78
+ return lines
79
+ }
80
+
81
+ /** Cell-honest one-line cut with a `…` continuation marker. */
82
+ function truncateOneLine(value: string, width: number): string {
83
+ const safe = displayText(value)
84
+ if (width <= 0) return ''
85
+ if (cellWidth(safe) <= width) return safe
86
+ let out = ''
87
+ let used = 0
88
+ const budget = Math.max(0, width - 1)
89
+ for (const cluster of graphemes(safe)) {
90
+ if (used + cluster.width > budget) break
91
+ out += cluster.text
92
+ used += cluster.width
93
+ }
94
+ return `${out}…`
95
+ }
96
+
97
+ function headerLine(info: SessionHeaderInfo | undefined): string {
98
+ if (info === undefined) return 'unknown session'
99
+ const tokens = [
100
+ info.role,
101
+ info.runner,
102
+ ...(info.model !== undefined ? [info.model] : []),
103
+ `phase ${info.phase}`,
104
+ ]
105
+ const round = info.round !== undefined ? ` (round ${info.round})` : ''
106
+ return `${tokens.join(' · ')}${round}`
107
+ }
108
+
109
+ function outputLines(output: unknown, width: number): string[] {
110
+ const text = typeof output === 'string' ? output : JSON.stringify(output)
111
+ if (typeof text !== 'string' || text.length === 0) return []
112
+ return wrapProse(text, width)
113
+ }
114
+
115
+ /** Cap one block at `SESSION_OUTPUT_MAX_ROWS` rows, naming what is withheld. */
116
+ function capRows(rows: string[]): string[] {
117
+ if (rows.length <= SESSION_OUTPUT_MAX_ROWS) return rows
118
+ const withheld = rows.length - SESSION_OUTPUT_MAX_ROWS
119
+ return [...rows.slice(0, SESSION_OUTPUT_MAX_ROWS), `… ${withheld} more rows withheld`]
120
+ }
121
+
122
+ function inputSummary(input: unknown): string {
123
+ return typeof input === 'undefined' ? '' : JSON.stringify(input)
124
+ }
125
+
126
+ function renderEvents(events: readonly DisplayEvent[], width: number): string[] {
127
+ const lines: string[] = []
128
+ let headerSeen = false
129
+ let sawContent = false
130
+ let lastWasStep = false
131
+ const push = (...added: string[]): void => {
132
+ lines.push(...added)
133
+ if (added.length > 0) {
134
+ sawContent = true
135
+ lastWasStep = false
136
+ }
137
+ }
138
+ const pushProse = (text: string, width_: number): void => {
139
+ if (text.length === 0) return
140
+ const wrapped = wrapProse(text, width_)
141
+ if (wrapped.length === 0) return
142
+ push(...wrapped)
143
+ }
144
+ const pushStep = (): void => {
145
+ // The leading boundary would only double-separate the header, and two
146
+ // adjacent boundaries (finish then start) render one separator.
147
+ if (!sawContent || lastWasStep) return
148
+ lines.push('── step ──')
149
+ sawContent = true
150
+ lastWasStep = true
151
+ }
152
+ const pushMarked = (text: string, width_: number, mark: string): void => {
153
+ if (text.length === 0) return
154
+ for (const line of wrapProse(text, Math.max(1, width_ - mark.length))) {
155
+ push(`${mark}${line}`)
156
+ }
157
+ }
158
+
159
+ for (const event of events) {
160
+ switch (event.kind) {
161
+ case 'header': {
162
+ headerSeen = true
163
+ push(headerLine(event.info))
164
+ break
165
+ }
166
+ case 'prompt':
167
+ if (event.text.length > 0) push(...wrapProse(`Prompt: ${event.text}`, width))
168
+ break
169
+ case 'text':
170
+ pushProse(event.text, width)
171
+ break
172
+ case 'reasoning':
173
+ // Distinct from answer text WITHOUT color: a literal prefix that
174
+ // survives `--plain` and every pipe.
175
+ pushMarked(event.text, width, '~ ')
176
+ break
177
+ case 'tool': {
178
+ push(truncateOneLine(`${event.toolName}(${inputSummary(event.input)})`, width))
179
+ if (event.awaiting) push('waiting for output')
180
+ if (event.errorText !== undefined) push(`ERROR: ${event.errorText}`)
181
+ else if (event.output !== undefined) push(...capRows(outputLines(event.output, width)))
182
+ break
183
+ }
184
+ case 'step':
185
+ pushStep()
186
+ break
187
+ case 'error':
188
+ push(`ERROR: ${event.text}`)
189
+ break
190
+ case 'abort':
191
+ push(`ABORTED: ${event.text}`)
192
+ break
193
+ case 'truncation':
194
+ push(`… ${event.bytes} bytes truncated`)
195
+ break
196
+ case 'data':
197
+ push(`data ${event.name}`)
198
+ break
199
+ }
200
+ }
201
+ // An absent session header part still leaves a neutral header line, so the
202
+ // view never opens without naming its bracket.
203
+ if (!headerSeen) lines.unshift('unknown session')
204
+ return lines
205
+ }
206
+
207
+ // ── Parts adapter (the live/chunk path) ──────────────────────────────────────
208
+
209
+ interface OpenRun {
210
+ id: string
211
+ buffer: string
212
+ }
213
+
214
+ function collectPartEvents(parts: readonly StreamPart[]): DisplayEvent[] {
215
+ // Tool outputs arrive in later chunks than their inputs; resolve them in a
216
+ // first pass so the tool call renders complete wherever its input landed.
217
+ const outputs = new Map<string, { output?: unknown; errorText?: string }>()
218
+ for (const part of parts) {
219
+ if (part.type === 'tool-output-available' && typeof part.toolCallId === 'string') {
220
+ outputs.set(part.toolCallId, { output: part.output })
221
+ }
222
+ if (part.type === 'tool-output-error' && typeof part.toolCallId === 'string') {
223
+ outputs.set(part.toolCallId, {
224
+ ...(typeof part.errorText === 'string' ? { errorText: part.errorText } : {}),
225
+ ...(part.output !== undefined ? { output: part.output } : {}),
226
+ })
227
+ }
228
+ }
229
+
230
+ const events: DisplayEvent[] = []
231
+ let headerSeen = false
232
+ let openText: OpenRun | undefined
233
+ let openReasoning: OpenRun | undefined
234
+ const flush = (run: OpenRun | undefined, kind: 'text' | 'reasoning'): void => {
235
+ if (run === undefined || run.buffer.length === 0) return
236
+ events.push(
237
+ kind === 'text'
238
+ ? { kind: 'text', text: run.buffer }
239
+ : { kind: 'reasoning', text: run.buffer },
240
+ )
241
+ }
242
+ // A still-open text/reasoning run renders at the position the stream
243
+ // reached: any other part flushes it, so content that started before a tool
244
+ // call is never painted after it.
245
+ const flushOpenRuns = (): void => {
246
+ flush(openText, 'text')
247
+ flush(openReasoning, 'reasoning')
248
+ openText = undefined
249
+ openReasoning = undefined
250
+ }
251
+ const open = (
252
+ current: OpenRun | undefined,
253
+ id: unknown,
254
+ kind: 'text' | 'reasoning',
255
+ ): OpenRun | undefined => {
256
+ if (typeof id !== 'string') return current
257
+ if (current?.id === id) return current
258
+ flush(current, kind)
259
+ return { id, buffer: '' }
260
+ }
261
+
262
+ for (const part of parts) {
263
+ switch (part.type) {
264
+ case 'data-ab-session': {
265
+ flushOpenRuns()
266
+ const data = part.data
267
+ if (typeof data !== 'object' || data === null) {
268
+ if (!headerSeen) headerSeen = true
269
+ events.push({ kind: 'data', name: part.type })
270
+ break
271
+ }
272
+ const record = data as Record<string, unknown>
273
+ if (!headerSeen && typeof record.role === 'string' && typeof record.runner === 'string') {
274
+ headerSeen = true
275
+ events.push({
276
+ kind: 'header',
277
+ info: {
278
+ ...(typeof record.session === 'string'
279
+ ? { session: record.session }
280
+ : { session: '' }),
281
+ role: record.role,
282
+ runner: record.runner,
283
+ ...(typeof record.model === 'string' ? { model: record.model } : {}),
284
+ ...(typeof record.phase === 'string' ? { phase: record.phase } : { phase: '' }),
285
+ ...(typeof record.round === 'number' ? { round: record.round } : {}),
286
+ },
287
+ })
288
+ } else {
289
+ events.push({ kind: 'data', name: part.type })
290
+ }
291
+ break
292
+ }
293
+ case 'data-ab-prompt': {
294
+ flushOpenRuns()
295
+ const data = part.data
296
+ const text =
297
+ typeof data === 'object' && data !== null
298
+ ? (data as Record<string, unknown>).text
299
+ : undefined
300
+ events.push({ kind: 'prompt', text: typeof text === 'string' ? text : '' })
301
+ break
302
+ }
303
+ case 'data-ab-truncation': {
304
+ flushOpenRuns()
305
+ const data = part.data
306
+ const bytes =
307
+ typeof data === 'object' && data !== null
308
+ ? (data as Record<string, unknown>).omittedBytes
309
+ : undefined
310
+ events.push({ kind: 'truncation', bytes: typeof bytes === 'number' ? bytes : 0 })
311
+ break
312
+ }
313
+ case 'text-start':
314
+ openText = open(openText, part.id, 'text')
315
+ break
316
+ case 'text-delta': {
317
+ openText = open(openText, part.id, 'text')
318
+ if (openText !== undefined && typeof part.delta === 'string') openText.buffer += part.delta
319
+ break
320
+ }
321
+ case 'text-end':
322
+ flush(openText, 'text')
323
+ openText = undefined
324
+ break
325
+ case 'reasoning-start':
326
+ openReasoning = open(openReasoning, part.id, 'reasoning')
327
+ break
328
+ case 'reasoning-delta': {
329
+ openReasoning = open(openReasoning, part.id, 'reasoning')
330
+ if (openReasoning !== undefined && typeof part.delta === 'string') {
331
+ openReasoning.buffer += part.delta
332
+ }
333
+ break
334
+ }
335
+ case 'reasoning-end':
336
+ flush(openReasoning, 'reasoning')
337
+ openReasoning = undefined
338
+ break
339
+ case 'tool-input-available': {
340
+ flushOpenRuns()
341
+ const callId = typeof part.toolCallId === 'string' ? part.toolCallId : ''
342
+ const resolved = outputs.get(callId) ?? {}
343
+ events.push({
344
+ kind: 'tool',
345
+ toolName: typeof part.toolName === 'string' ? part.toolName : 'unknown tool',
346
+ ...(part.input !== undefined ? { input: part.input } : {}),
347
+ ...('output' in resolved && resolved.output !== undefined
348
+ ? { output: resolved.output }
349
+ : {}),
350
+ awaiting: resolved.output === undefined && resolved.errorText === undefined,
351
+ ...('errorText' in resolved && resolved.errorText !== undefined
352
+ ? { errorText: resolved.errorText }
353
+ : {}),
354
+ })
355
+ break
356
+ }
357
+ case 'start-step':
358
+ case 'finish-step':
359
+ flushOpenRuns()
360
+ events.push({ kind: 'step' })
361
+ break
362
+ case 'error':
363
+ flushOpenRuns()
364
+ events.push({
365
+ kind: 'error',
366
+ text: typeof part.errorText === 'string' ? part.errorText : String(part.errorText ?? ''),
367
+ })
368
+ break
369
+ case 'abort':
370
+ flushOpenRuns()
371
+ events.push({
372
+ kind: 'abort',
373
+ text: typeof part.reason === 'string' ? part.reason : String(part.reason ?? ''),
374
+ })
375
+ break
376
+ default: {
377
+ flushOpenRuns()
378
+ // Any other `data-*` extension renders legibly; non-data parts of
379
+ // unknown protocol types are ignored.
380
+ if (part.type.startsWith('data-')) events.push({ kind: 'data', name: part.type })
381
+ break
382
+ }
383
+ }
384
+ }
385
+ flush(openText, 'text')
386
+ flush(openReasoning, 'reasoning')
387
+ return events
388
+ }
389
+
390
+ /** Project a live/chunk part sequence (store order) to display lines. */
391
+ export function projectSessionParts(parts: readonly StreamPart[], width: number): string[] {
392
+ return renderEvents(collectPartEvents(parts), width)
393
+ }
394
+
395
+ // ── Document adapter (the finalized artifact path) ──────────────────────────
396
+
397
+ function collectDocumentEvents(messages: readonly UIMessage[]): DisplayEvent[] {
398
+ const events: DisplayEvent[] = []
399
+ let headerSeen = false
400
+ for (const message of messages) {
401
+ for (const part of message.parts) {
402
+ const type = part.type
403
+ if (type === 'text') {
404
+ if (typeof (part as { text?: unknown }).text === 'string') {
405
+ events.push({ kind: 'text', text: (part as { text: string }).text })
406
+ }
407
+ continue
408
+ }
409
+ if (type === 'reasoning') {
410
+ if (typeof (part as { text?: unknown }).text === 'string') {
411
+ events.push({ kind: 'reasoning', text: (part as { text: string }).text })
412
+ }
413
+ continue
414
+ }
415
+ if (type === 'step-start') {
416
+ events.push({ kind: 'step' })
417
+ continue
418
+ }
419
+ if (type === 'dynamic-tool') {
420
+ const tool = part as {
421
+ toolName?: unknown
422
+ state?: unknown
423
+ input?: unknown
424
+ output?: unknown
425
+ errorText?: unknown
426
+ }
427
+ events.push({
428
+ kind: 'tool',
429
+ toolName: typeof tool.toolName === 'string' ? tool.toolName : 'unknown tool',
430
+ ...(tool.input !== undefined ? { input: tool.input } : {}),
431
+ ...(tool.output !== undefined ? { output: tool.output } : {}),
432
+ awaiting: tool.state === 'input-streaming' || tool.state === 'input-available',
433
+ ...(typeof tool.errorText === 'string' ? { errorText: tool.errorText } : {}),
434
+ })
435
+ continue
436
+ }
437
+ if (type.startsWith('tool-')) {
438
+ const tool = part as {
439
+ state?: unknown
440
+ input?: unknown
441
+ output?: unknown
442
+ errorText?: unknown
443
+ }
444
+ events.push({
445
+ kind: 'tool',
446
+ toolName: type.slice('tool-'.length),
447
+ ...(tool.input !== undefined ? { input: tool.input } : {}),
448
+ ...(tool.output !== undefined ? { output: tool.output } : {}),
449
+ awaiting: tool.state === 'input-streaming' || tool.state === 'input-available',
450
+ ...(typeof tool.errorText === 'string' ? { errorText: tool.errorText } : {}),
451
+ })
452
+ continue
453
+ }
454
+ if (type.startsWith('data-')) {
455
+ const data = (part as { data?: unknown }).data
456
+ if (type === 'data-ab-prompt') {
457
+ const text =
458
+ typeof data === 'object' && data !== null
459
+ ? (data as Record<string, unknown>).text
460
+ : undefined
461
+ events.push({ kind: 'prompt', text: typeof text === 'string' ? text : '' })
462
+ } else if (type === 'data-ab-session') {
463
+ const record =
464
+ typeof data === 'object' && data !== null ? (data as Record<string, unknown>) : {}
465
+ if (!headerSeen && typeof record.role === 'string' && typeof record.runner === 'string') {
466
+ headerSeen = true
467
+ events.push({
468
+ kind: 'header',
469
+ info: {
470
+ ...(typeof record.session === 'string'
471
+ ? { session: record.session }
472
+ : { session: '' }),
473
+ role: record.role,
474
+ runner: record.runner,
475
+ ...(typeof record.model === 'string' ? { model: record.model } : {}),
476
+ ...(typeof record.phase === 'string' ? { phase: record.phase } : { phase: '' }),
477
+ ...(typeof record.round === 'number' ? { round: record.round } : {}),
478
+ },
479
+ })
480
+ } else {
481
+ events.push({ kind: 'data', name: type })
482
+ }
483
+ } else if (type === 'data-ab-truncation') {
484
+ const bytes =
485
+ typeof data === 'object' && data !== null
486
+ ? (data as Record<string, unknown>).omittedBytes
487
+ : undefined
488
+ events.push({ kind: 'truncation', bytes: typeof bytes === 'number' ? bytes : 0 })
489
+ } else {
490
+ events.push({ kind: 'data', name: type })
491
+ }
492
+ }
493
+ // `file`, `source-url`, `custom`, … have no session-view rendering yet.
494
+ // (`error`/`abort`/`finish-step` chunks never become message parts at
495
+ // all — the SDK's assembly drops them — so those elements are only
496
+ // visible on the parts path above.)
497
+ }
498
+ }
499
+ return events
500
+ }
501
+
502
+ /** Project a finalized `UIMessage[]` document (the closed stream's artifact)
503
+ * to the same display lines. */
504
+ export function projectSessionDocument(document: readonly UIMessage[], width: number): string[] {
505
+ return renderEvents(collectDocumentEvents(document), width)
506
+ }
@@ -0,0 +1,108 @@
1
+ export interface TranscriptUsage {
2
+ inputTokens: number
3
+ outputTokens: number
4
+ turns?: number
5
+ }
6
+
7
+ export interface TranscriptTurn {
8
+ prompt: string
9
+ text: string
10
+ usage?: TranscriptUsage
11
+ failure?: string
12
+ }
13
+
14
+ export type TranscriptPresentation =
15
+ | { kind: 'turns'; turns: TranscriptTurn[] }
16
+ | { kind: 'producer-boundary'; notice: string; turns: TranscriptTurn[] }
17
+ | { kind: 'raw'; text: string }
18
+
19
+ function record(value: unknown): Record<string, unknown> | undefined {
20
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
21
+ ? (value as Record<string, unknown>)
22
+ : undefined
23
+ }
24
+
25
+ function usage(value: unknown): TranscriptUsage | undefined {
26
+ const item = record(value)
27
+ if (item === undefined) return undefined
28
+ if (typeof item.inputTokens !== 'number' || typeof item.outputTokens !== 'number')
29
+ return undefined
30
+ return {
31
+ inputTokens: item.inputTokens,
32
+ outputTokens: item.outputTokens,
33
+ ...(typeof item.turns === 'number' ? { turns: item.turns } : {}),
34
+ }
35
+ }
36
+
37
+ function failure(value: unknown): string | undefined {
38
+ const item = record(value)
39
+ return item !== undefined && typeof item.message === 'string' ? item.message : undefined
40
+ }
41
+
42
+ function parseTurn(value: unknown, index: number): TranscriptTurn | undefined {
43
+ const item = record(value)
44
+ if (item === undefined) return undefined
45
+ const result = record(item.result)
46
+ const text = typeof item.text === 'string' ? item.text : result?.text
47
+ if (typeof text !== 'string') return undefined
48
+ const prompt =
49
+ typeof item.prompt === 'string'
50
+ ? item.prompt
51
+ : typeof item.message === 'string'
52
+ ? item.message
53
+ : index === 0
54
+ ? '(initial skill invocation)'
55
+ : `(turn ${index + 1})`
56
+ return {
57
+ prompt,
58
+ text,
59
+ ...(usage(item.usage ?? result?.usage) !== undefined
60
+ ? { usage: usage(item.usage ?? result?.usage)! }
61
+ : {}),
62
+ ...(failure(item.failure ?? result?.failure) !== undefined
63
+ ? { failure: failure(item.failure ?? result?.failure)! }
64
+ : {}),
65
+ }
66
+ }
67
+
68
+ /** Parse opaque transcript bytes as a presentation heuristic, never a schema. */
69
+ export function parseTranscript(content: string): TranscriptPresentation {
70
+ let parsed: unknown
71
+ try {
72
+ parsed = JSON.parse(content)
73
+ } catch {
74
+ return { kind: 'raw', text: content }
75
+ }
76
+
77
+ const root = record(parsed)
78
+ if (root !== undefined && Array.isArray(root.turns)) {
79
+ const turns = root.turns.map(parseTurn)
80
+ if (turns.every((turn): turn is TranscriptTurn => turn !== undefined)) {
81
+ return { kind: 'turns', turns }
82
+ }
83
+ }
84
+
85
+ if (
86
+ root !== undefined &&
87
+ typeof root.note === 'string' &&
88
+ root.note.includes('producer session')
89
+ ) {
90
+ const boundary = record(root.turn)
91
+ if (boundary !== undefined && typeof boundary.text === 'string') {
92
+ return {
93
+ kind: 'producer-boundary',
94
+ notice:
95
+ "Producer boundary record: this session was kept live for continuation; only this round's final turn was deposited.",
96
+ turns: [
97
+ {
98
+ prompt: '(producer round invocation)',
99
+ text: boundary.text,
100
+ ...(usage(boundary.usage) !== undefined ? { usage: usage(boundary.usage)! } : {}),
101
+ },
102
+ ],
103
+ }
104
+ }
105
+ }
106
+
107
+ return { kind: 'raw', text: JSON.stringify(parsed, null, 2) }
108
+ }
@@ -0,0 +1,46 @@
1
+ import type { DashboardRenderer, DashboardRendererResolver } from './dashboard/render'
2
+
3
+ /** State retained on `globalThis` across Bun hot module evaluations. */
4
+ export interface DevCliState {
5
+ renderer?: DashboardRenderer
6
+ run?: Promise<number>
7
+ }
8
+
9
+ export interface EvaluateDevCliOpts {
10
+ state: DevCliState
11
+ renderer: DashboardRenderer
12
+ launch: (resolveDashboardRenderer: DashboardRendererResolver) => Promise<number>
13
+ /** Install the process exit/rejection path for a newly launched run. */
14
+ settle: (run: Promise<number>) => void
15
+ }
16
+
17
+ /**
18
+ * Apply one hot evaluation. Every evaluation publishes the newest renderer,
19
+ * but only the first creates and settles a CLI run. The resolver closes over
20
+ * persistent state—not the first renderer—so an existing DispatchLoop sees a
21
+ * replacement on its next paint without reconstructing any controller state.
22
+ */
23
+ export function evaluateDevCli({
24
+ state,
25
+ renderer,
26
+ launch,
27
+ settle,
28
+ }: EvaluateDevCliOpts): Promise<number> {
29
+ state.renderer = renderer
30
+ if (state.run !== undefined) return state.run
31
+
32
+ const resolveDashboardRenderer = (): DashboardRenderer => {
33
+ const current = state.renderer
34
+ if (current === undefined) {
35
+ throw new Error('dashboard dev renderer is unavailable')
36
+ }
37
+ return current
38
+ }
39
+
40
+ // Defer launch by one microtask so state.run is installed before user wiring
41
+ // can execute. This keeps even a synchronous/re-entrant launch single-shot.
42
+ const run = Promise.resolve().then(() => launch(resolveDashboardRenderer))
43
+ state.run = run
44
+ settle(run)
45
+ return run
46
+ }