@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,454 @@
1
+ import { fileURLToPath } from 'node:url'
2
+ import type { PiCatalogEntry } from '../../cli/models'
3
+ import { classifyProviderError } from './provider-error'
4
+ import type { PiModelRef, PiSession, PiTurn } from './pi'
5
+
6
+ export const PI_BRIDGE_PATH = fileURLToPath(new URL('./pi-bridge.js', import.meta.url))
7
+ export const PI_RPC_MODE_ARG = '--mode'
8
+ export const PI_MODEL_ARG = '--model'
9
+ const CATALOG_PREFIX = 'autobuild-pi-catalog:'
10
+
11
+ export interface PiRpcInvocation {
12
+ args: string[]
13
+ cwd: string
14
+ env: Record<string, string>
15
+ }
16
+
17
+ export interface PiRpcProcess {
18
+ write(line: string): void
19
+ closeInput(): void
20
+ output: AsyncIterable<Uint8Array | string>
21
+ stderr: Promise<string>
22
+ exited: Promise<number>
23
+ kill(signal?: number): void
24
+ }
25
+
26
+ export type PiRpcSpawnFn = (invocation: PiRpcInvocation) => PiRpcProcess
27
+
28
+ export const spawnPiRpc: PiRpcSpawnFn = (invocation) => {
29
+ const proc = Bun.spawn(['pi', ...invocation.args], {
30
+ cwd: invocation.cwd,
31
+ env: invocation.env,
32
+ stdin: 'pipe',
33
+ stdout: 'pipe',
34
+ stderr: 'pipe',
35
+ })
36
+ return {
37
+ write(line) {
38
+ proc.stdin.write(line)
39
+ proc.stdin.flush()
40
+ },
41
+ closeInput() {
42
+ proc.stdin.end()
43
+ },
44
+ output: proc.stdout,
45
+ stderr: new Response(proc.stderr).text(),
46
+ exited: proc.exited,
47
+ kill(signal) {
48
+ proc.kill(signal)
49
+ },
50
+ }
51
+ }
52
+
53
+ export class JsonlDecoder {
54
+ private readonly decoder = new TextDecoder()
55
+ private buffer = ''
56
+
57
+ push(chunk: Uint8Array | string): string[] {
58
+ this.buffer += typeof chunk === 'string' ? chunk : this.decoder.decode(chunk, { stream: true })
59
+ return this.takeLines(false)
60
+ }
61
+
62
+ end(): string[] {
63
+ this.buffer += this.decoder.decode()
64
+ return this.takeLines(true)
65
+ }
66
+
67
+ private takeLines(final: boolean): string[] {
68
+ const lines: string[] = []
69
+ while (true) {
70
+ const index = this.buffer.indexOf('\n')
71
+ if (index < 0) break
72
+ let line = this.buffer.slice(0, index)
73
+ this.buffer = this.buffer.slice(index + 1)
74
+ if (line.endsWith('\r')) line = line.slice(0, -1)
75
+ if (line.length > 0) lines.push(line)
76
+ }
77
+ if (final && this.buffer.length > 0) {
78
+ lines.push(this.buffer.endsWith('\r') ? this.buffer.slice(0, -1) : this.buffer)
79
+ this.buffer = ''
80
+ }
81
+ return lines
82
+ }
83
+ }
84
+
85
+ type JsonRecord = Record<string, unknown>
86
+
87
+ interface PendingResponse {
88
+ command: string
89
+ resolve: (value: JsonRecord) => void
90
+ reject: (error: Error) => void
91
+ }
92
+
93
+ interface TurnCapture {
94
+ text: string[]
95
+ failure?: string
96
+ settled: Promise<void>
97
+ resolve: () => void
98
+ reject: (error: Error) => void
99
+ }
100
+
101
+ export class PiRpcClient implements PiSession {
102
+ readonly sessionId: string
103
+ private nextId = 0
104
+ private closed = false
105
+ private fatal: Error | undefined
106
+ private readonly pending = new Map<string, PendingResponse>()
107
+ private readonly notifications = new Set<(value: JsonRecord) => void>()
108
+ private activeTurn: TurnCapture | undefined
109
+ /** Optional observer invoked with every non-response session event
110
+ * (SPEC §9 translation hook). Fires regardless of turn state. */
111
+ onEvent: ((event: JsonRecord) => void) | undefined
112
+
113
+ private constructor(
114
+ private readonly process: PiRpcProcess,
115
+ sessionId: string,
116
+ private readonly configuredTools: readonly string[],
117
+ ) {
118
+ this.sessionId = sessionId
119
+ void this.readOutput()
120
+ void process.exited.then((code) => {
121
+ if (!this.closed)
122
+ void this.fail(new Error(`pi runtime: RPC process exited with code ${code}`))
123
+ })
124
+ }
125
+
126
+ static async launch(
127
+ invocation: PiRpcInvocation,
128
+ spawn: PiRpcSpawnFn = spawnPiRpc,
129
+ configuredTools: readonly string[] = [],
130
+ onEvent?: (event: JsonRecord) => void,
131
+ ): Promise<PiRpcClient> {
132
+ const process = spawn(invocation)
133
+ const client = new PiRpcClient(process, crypto.randomUUID(), configuredTools)
134
+ client.onEvent = onEvent
135
+ try {
136
+ const state = await client.command('get_state')
137
+ const data = record(state.data)
138
+ const id = typeof data?.sessionId === 'string' ? data.sessionId : undefined
139
+ if (id === undefined || id.length === 0)
140
+ throw new Error('pi runtime: RPC get_state returned no sessionId')
141
+ Object.defineProperty(client, 'sessionId', { value: id })
142
+ return client
143
+ } catch (error) {
144
+ await client.dispose()
145
+ throw error
146
+ }
147
+ }
148
+
149
+ async prompt(text: string, env: Record<string, string>, signal?: AbortSignal): Promise<PiTurn> {
150
+ if (this.activeTurn !== undefined)
151
+ throw new Error('pi runtime: concurrent RPC prompts are not supported')
152
+ if (signal?.aborted) throw abortReason(signal)
153
+
154
+ await this.configure(env)
155
+ const before = await this.stats()
156
+ let resolve!: () => void
157
+ let reject!: (error: Error) => void
158
+ const settled = new Promise<void>((res, rej) => {
159
+ resolve = res
160
+ reject = rej
161
+ })
162
+ const capture: TurnCapture = { text: [], settled, resolve, reject }
163
+ this.activeTurn = capture
164
+
165
+ let aborting: Promise<unknown> | undefined
166
+ const onAbort = (): void => {
167
+ aborting ??= this.command('abort').catch(() => undefined)
168
+ }
169
+ signal?.addEventListener('abort', onAbort, { once: true })
170
+ try {
171
+ await this.command('prompt', { message: text })
172
+ await capture.settled
173
+ if (aborting !== undefined) await aborting
174
+ if (signal?.aborted) throw abortReason(signal)
175
+ const after = await this.stats()
176
+ return {
177
+ text: capture.text.join(''),
178
+ usage: {
179
+ inputTokens: tokenDelta(after.input, before.input),
180
+ outputTokens: tokenDelta(after.output, before.output),
181
+ },
182
+ ...(capture.failure !== undefined
183
+ ? { failure: classifyProviderError(capture.failure) }
184
+ : {}),
185
+ }
186
+ } finally {
187
+ signal?.removeEventListener('abort', onAbort)
188
+ if (this.activeTurn === capture) this.activeTurn = undefined
189
+ }
190
+ }
191
+
192
+ async catalog(availableOnly: boolean): Promise<PiCatalogEntry[]> {
193
+ const nonce = crypto.randomUUID()
194
+ const payload = encode({ nonce, availableOnly })
195
+ const prefix = `${CATALOG_PREFIX}${nonce}:`
196
+ const matches: string[] = []
197
+ const listener = (event: JsonRecord): void => {
198
+ if (event.type !== 'extension_ui_request' || event.method !== 'notify') return
199
+ const message = typeof event.message === 'string' ? event.message : ''
200
+ if (message.startsWith(prefix)) matches.push(message.slice(prefix.length))
201
+ }
202
+ this.notifications.add(listener)
203
+ try {
204
+ await this.command('prompt', { message: `/autobuild-models ${payload}` })
205
+ await Promise.resolve()
206
+ if (matches.length === 0) throw new Error('pi runtime: missing model-catalog bridge response')
207
+ if (matches.length > 1) throw new Error('pi runtime: duplicate model-catalog bridge response')
208
+ try {
209
+ return parseCatalog(decode(matches[0]!))
210
+ } catch (error) {
211
+ throw asError(error, 'pi runtime: malformed model-catalog bridge response')
212
+ }
213
+ } finally {
214
+ this.notifications.delete(listener)
215
+ }
216
+ }
217
+
218
+ async dispose(): Promise<void> {
219
+ if (this.closed) return
220
+ this.closed = true
221
+ this.process.closeInput()
222
+ this.process.kill()
223
+ await Promise.race([this.process.exited.catch(() => -1), delay(1_000)])
224
+ }
225
+
226
+ private async configure(environment: Record<string, string>): Promise<void> {
227
+ await this.command('prompt', {
228
+ message: `/autobuild-configure ${encode({
229
+ environment,
230
+ tools: this.configuredTools,
231
+ })}`,
232
+ })
233
+ }
234
+
235
+ private async stats(): Promise<{ input: number; output: number }> {
236
+ const response = await this.command('get_session_stats')
237
+ const data = record(response.data)
238
+ const tokens = record(data?.tokens)
239
+ return { input: token(tokens?.input), output: token(tokens?.output) }
240
+ }
241
+
242
+ private command(type: string, fields: JsonRecord = {}): Promise<JsonRecord> {
243
+ if (this.fatal !== undefined) return Promise.reject(this.fatal)
244
+ if (this.closed) return Promise.reject(new Error('pi runtime: RPC process is closed'))
245
+ const id = `ab-${++this.nextId}`
246
+ return new Promise((resolve, reject) => {
247
+ this.pending.set(id, { command: type, resolve, reject })
248
+ try {
249
+ this.process.write(`${JSON.stringify({ id, type, ...fields })}\n`)
250
+ } catch (error) {
251
+ this.pending.delete(id)
252
+ reject(asError(error, 'pi runtime: failed to write RPC command'))
253
+ }
254
+ })
255
+ }
256
+
257
+ private async readOutput(): Promise<void> {
258
+ const decoder = new JsonlDecoder()
259
+ try {
260
+ for await (const chunk of this.process.output) {
261
+ for (const line of decoder.push(chunk)) this.receive(line)
262
+ }
263
+ for (const line of decoder.end()) this.receive(line)
264
+ } catch (error) {
265
+ await this.fail(asError(error, 'pi runtime: failed to read RPC output'))
266
+ }
267
+ }
268
+
269
+ private receive(line: string): void {
270
+ let value: unknown
271
+ try {
272
+ value = JSON.parse(line)
273
+ } catch {
274
+ void this.fail(new Error(`pi runtime: malformed JSONL from RPC process: ${line}`))
275
+ return
276
+ }
277
+ if (!isRecord(value)) {
278
+ void this.fail(new Error('pi runtime: RPC process emitted a non-object JSON record'))
279
+ return
280
+ }
281
+ if (value.type === 'response' && typeof value.id === 'string') {
282
+ const pending = this.pending.get(value.id)
283
+ if (pending === undefined) return
284
+ this.pending.delete(value.id)
285
+ if (value.command !== pending.command) {
286
+ pending.reject(new Error(`pi runtime: RPC response command mismatch for ${value.id}`))
287
+ } else if (value.success !== true) {
288
+ pending.reject(
289
+ new Error(
290
+ typeof value.error === 'string' ? value.error : `pi RPC ${pending.command} failed`,
291
+ ),
292
+ )
293
+ } else {
294
+ pending.resolve(value)
295
+ }
296
+ return
297
+ }
298
+
299
+ for (const listener of this.notifications) listener(value)
300
+ this.onEvent?.(value)
301
+ const capture = this.activeTurn
302
+ if (capture === undefined) return
303
+ if (value.type === 'message_update') {
304
+ const update = record(value.assistantMessageEvent)
305
+ if (update?.type === 'text_delta' && typeof update.delta === 'string')
306
+ capture.text.push(update.delta)
307
+ }
308
+ if (value.type === 'message_end') {
309
+ const message = record(value.message)
310
+ if (message?.role === 'assistant') {
311
+ const stopReason = typeof message.stopReason === 'string' ? message.stopReason : undefined
312
+ if (stopReason === 'error' || stopReason === 'aborted') {
313
+ capture.failure =
314
+ typeof message.errorMessage === 'string' && message.errorMessage.length > 0
315
+ ? message.errorMessage
316
+ : `pi runtime: assistant turn ended with stopReason "${stopReason}"`
317
+ } else if (stopReason !== undefined) {
318
+ capture.failure = undefined
319
+ }
320
+ }
321
+ }
322
+ if (value.type === 'agent_settled') capture.resolve()
323
+ }
324
+
325
+ private async fail(error: Error): Promise<void> {
326
+ if (this.fatal !== undefined) return
327
+ this.fatal = error
328
+ for (const pending of this.pending.values()) pending.reject(error)
329
+ this.pending.clear()
330
+ this.activeTurn?.reject(error)
331
+ if (!this.closed) this.process.kill()
332
+ }
333
+ }
334
+
335
+ export interface PiRpcSessionOptions {
336
+ cwd: string
337
+ model?: PiModelRef
338
+ tools: readonly string[]
339
+ args: readonly string[]
340
+ skill?: string
341
+ env: Record<string, string>
342
+ spawn?: PiRpcSpawnFn
343
+ /** Session-event observer handed to the RPC client (SPEC §9). */
344
+ onEvent?: (event: JsonRecord) => void
345
+ }
346
+
347
+ export async function createPiRpcSession(opts: PiRpcSessionOptions): Promise<PiSession> {
348
+ const args = [
349
+ PI_RPC_MODE_ARG,
350
+ 'rpc',
351
+ '--no-session',
352
+ '--no-approve',
353
+ '--no-context-files',
354
+ '--no-prompt-templates',
355
+ '--no-themes',
356
+ ]
357
+ if (opts.model !== undefined) args.push(PI_MODEL_ARG, `${opts.model.provider}/${opts.model.id}`)
358
+ // `--tools` filters both builtin and package tools out of Pi's registry.
359
+ // Keep the complete registry for build sessions and let the bridge activate
360
+ // exactly the configured builtin/package subset before every agent prompt.
361
+ if (opts.tools.length === 0) args.push('--no-tools')
362
+ args.push('--no-skills')
363
+ if (opts.skill !== undefined) args.push('--skill', `${opts.cwd}/.agents/skills/${opts.skill}`)
364
+ // Disable ambient discovery unconditionally. Explicit repeatable
365
+ // --extension paths in configured args still load alongside the bridge.
366
+ args.push('--no-extensions', '--extension', PI_BRIDGE_PATH)
367
+ args.push(...opts.args)
368
+ return PiRpcClient.launch(
369
+ { args, cwd: opts.cwd, env: opts.env },
370
+ opts.spawn,
371
+ opts.tools,
372
+ opts.onEvent,
373
+ )
374
+ }
375
+
376
+ export async function readLocalPiCatalog(opts: {
377
+ cwd: string
378
+ env: Record<string, string>
379
+ availableOnly: boolean
380
+ spawn?: PiRpcSpawnFn
381
+ }): Promise<PiCatalogEntry[]> {
382
+ const client = await PiRpcClient.launch(
383
+ {
384
+ args: [
385
+ PI_RPC_MODE_ARG,
386
+ 'rpc',
387
+ '--no-session',
388
+ '--no-approve',
389
+ '--no-context-files',
390
+ '--no-tools',
391
+ '--no-skills',
392
+ '--no-extensions',
393
+ '--no-prompt-templates',
394
+ '--no-themes',
395
+ '--extension',
396
+ PI_BRIDGE_PATH,
397
+ ],
398
+ cwd: opts.cwd,
399
+ env: opts.env,
400
+ },
401
+ opts.spawn,
402
+ )
403
+ try {
404
+ return await client.catalog(opts.availableOnly)
405
+ } finally {
406
+ await client.dispose()
407
+ }
408
+ }
409
+
410
+ function parseCatalog(value: unknown): PiCatalogEntry[] {
411
+ if (!Array.isArray(value)) throw new Error('catalog is not an array')
412
+ return value.map((entry) => {
413
+ if (!isRecord(entry) || typeof entry.provider !== 'string' || typeof entry.id !== 'string') {
414
+ throw new Error('catalog entry is malformed')
415
+ }
416
+ return { provider: entry.provider, id: entry.id }
417
+ })
418
+ }
419
+
420
+ function encode(value: unknown): string {
421
+ return Buffer.from(JSON.stringify(value), 'utf8').toString('base64url')
422
+ }
423
+
424
+ function decode(value: string): unknown {
425
+ return JSON.parse(Buffer.from(value, 'base64url').toString('utf8'))
426
+ }
427
+
428
+ function record(value: unknown): JsonRecord | undefined {
429
+ return isRecord(value) ? value : undefined
430
+ }
431
+
432
+ function isRecord(value: unknown): value is JsonRecord {
433
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
434
+ }
435
+
436
+ function token(value: unknown): number {
437
+ return typeof value === 'number' && Number.isFinite(value) ? Math.max(0, Math.round(value)) : 0
438
+ }
439
+
440
+ function tokenDelta(after: number, before: number): number {
441
+ return Math.max(0, Math.round(after - before))
442
+ }
443
+
444
+ function abortReason(signal: AbortSignal): Error {
445
+ return signal.reason instanceof Error ? signal.reason : new Error('pi runtime: prompt aborted')
446
+ }
447
+
448
+ function asError(error: unknown, prefix: string): Error {
449
+ return new Error(`${prefix}: ${error instanceof Error ? error.message : String(error)}`)
450
+ }
451
+
452
+ function delay(ms: number): Promise<void> {
453
+ return new Promise((resolve) => setTimeout(resolve, ms))
454
+ }