@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,617 @@
1
+ /** AgentRunner adapter for the operator's locally installed Pi CLI. */
2
+ import semver from 'semver'
3
+ import type { StreamPart } from '../../store/streams/types'
4
+ import {
5
+ agentInvocation,
6
+ type AgentContinueOpts,
7
+ type AgentRunner,
8
+ type AgentSessionHandle,
9
+ type AgentStartOpts,
10
+ type AgentTurnFailure,
11
+ type AgentTurnResult,
12
+ type SessionStreamEmitter,
13
+ type Transcript,
14
+ } from '../types'
15
+ import type { OneShotCompletion, OneShotCompletionInput, OneShotCompletionResult } from './one-shot'
16
+ import { classifyProviderError, configurationFailure, credentialFailure } from './provider-error'
17
+ import { createPiRpcSession, PI_MODEL_ARG, PI_RPC_MODE_ARG } from './pi-rpc'
18
+ import {
19
+ abortPart,
20
+ errorPart,
21
+ finishPart,
22
+ finishStepPart,
23
+ promptPart,
24
+ reasoningDeltaPart,
25
+ reasoningEndPart,
26
+ reasoningStartPart,
27
+ startPart,
28
+ startStepPart,
29
+ textDeltaPart,
30
+ textEndPart,
31
+ textStartPart,
32
+ toolInputPart,
33
+ toolOutputPart,
34
+ } from './stream-parts'
35
+ import type { RuntimeUsabilityInput, RuntimeUsabilityResult } from './runtime'
36
+ import { sessionEnv } from './session-env'
37
+
38
+ export const MINIMUM_PI_VERSION = '0.84.3'
39
+ const PI_TOOLS = ['read', 'bash', 'edit', 'write', 'grep', 'find', 'ls'] as const
40
+
41
+ export interface PiModelRef {
42
+ provider: string
43
+ id: string
44
+ }
45
+
46
+ export function parsePiModel(model: string): PiModelRef {
47
+ const slash = model.indexOf('/')
48
+ if (slash <= 0 || slash === model.length - 1) {
49
+ throw new Error(
50
+ `pi runtime: model "${model}" is not provider-qualified — expected "<provider>/<id>". ` +
51
+ 'Run `ab models <query>` to look up an id.',
52
+ )
53
+ }
54
+ return { provider: model.slice(0, slash), id: model.slice(slash + 1) }
55
+ }
56
+
57
+ export function parsePiModelCatalog(output: string): PiModelRef[] {
58
+ const lines = output
59
+ .split(/\r?\n/)
60
+ .map((line) => line.trim())
61
+ .filter((line) => line.length > 0)
62
+ const header = lines.shift()?.split(/\s{2,}/)
63
+ if (
64
+ header === undefined ||
65
+ header.length < 3 ||
66
+ header[0] !== 'provider' ||
67
+ header[1] !== 'model'
68
+ ) {
69
+ throw new Error('expected a header beginning with "provider" and "model" columns')
70
+ }
71
+
72
+ return lines.map((line, index) => {
73
+ const columns = line.split(/\s{2,}/)
74
+ if (columns.length !== header.length || columns[0] === '' || columns[1] === '') {
75
+ throw new Error(`row ${index + 1} does not match the catalog header`)
76
+ }
77
+ return { provider: columns[0]!, id: columns[1]! }
78
+ })
79
+ }
80
+
81
+ export interface PiCliInvocation {
82
+ args: string[]
83
+ cwd: string
84
+ env: Record<string, string>
85
+ }
86
+
87
+ export interface PiCliResult {
88
+ stdout: string
89
+ stderr: string
90
+ exitCode: number
91
+ }
92
+
93
+ export type PiCliRunFn = (invocation: PiCliInvocation) => Promise<PiCliResult>
94
+
95
+ export const runPiCli: PiCliRunFn = async (invocation) => {
96
+ const proc = Bun.spawn(['pi', ...invocation.args], {
97
+ cwd: invocation.cwd,
98
+ env: invocation.env,
99
+ stdin: 'ignore',
100
+ stdout: 'pipe',
101
+ stderr: 'pipe',
102
+ })
103
+ const [stdout, stderr, exitCode] = await Promise.all([
104
+ new Response(proc.stdout).text(),
105
+ new Response(proc.stderr).text(),
106
+ proc.exited,
107
+ ])
108
+ return { stdout, stderr, exitCode }
109
+ }
110
+
111
+ export interface PiPrerequisite {
112
+ version: string
113
+ }
114
+
115
+ export async function checkLocalPi(
116
+ cwd: string,
117
+ env: Record<string, string>,
118
+ runCli: PiCliRunFn = runPiCli,
119
+ ): Promise<PiPrerequisite> {
120
+ let result: PiCliResult
121
+ try {
122
+ result = await runCli({ args: ['--version'], cwd, env })
123
+ } catch (error) {
124
+ if (isEnoent(error)) {
125
+ throw new Error(
126
+ 'pi runtime: Pi CLI executable "pi" was not found. Install Pi, run `pi` and log in, then retry.',
127
+ )
128
+ }
129
+ throw new Error(`pi runtime: failed to launch Pi CLI executable "pi": ${errorText(error)}`)
130
+ }
131
+ const detected = result.stdout.trim()
132
+ if (result.exitCode !== 0) {
133
+ throw new Error(
134
+ `pi runtime: "pi --version" exited with code ${result.exitCode}: ${result.stderr.trim() || detected || 'no diagnostic'}`,
135
+ )
136
+ }
137
+ const version = semver.valid(detected)
138
+ if (version === null) {
139
+ throw new Error(`pi runtime: could not parse detected Pi version "${detected || '(empty)'}"`)
140
+ }
141
+ if (semver.lt(version, MINIMUM_PI_VERSION)) {
142
+ throw new Error(
143
+ `pi runtime: detected Pi ${version}, but Autobuild requires Pi ${MINIMUM_PI_VERSION} or newer. Upgrade the local "pi" executable.`,
144
+ )
145
+ }
146
+ return { version }
147
+ }
148
+
149
+ export async function isPiRuntimeUsable(
150
+ input: RuntimeUsabilityInput,
151
+ runCli: PiCliRunFn = runPiCli,
152
+ ): Promise<RuntimeUsabilityResult> {
153
+ if (input.models.length === 0) {
154
+ return { usable: false, reason: 'Pi has no default model configured to probe' }
155
+ }
156
+ const env = cleanEnv(input.env)
157
+ try {
158
+ await checkLocalPi(input.cwd, env, runCli)
159
+ let catalog: PiModelRef[] | undefined
160
+ for (const model of new Set(input.models)) {
161
+ const modelRef = parsePiModel(model)
162
+ const result = await runCli({
163
+ args: ['auth', 'check', '--model', model, '--json'],
164
+ cwd: input.cwd,
165
+ env,
166
+ })
167
+ let payload: unknown
168
+ try {
169
+ payload = JSON.parse(result.stdout)
170
+ } catch {
171
+ return {
172
+ usable: false,
173
+ reason: `Pi authentication check for model "${model}" returned malformed JSON`,
174
+ }
175
+ }
176
+ const status = isRecord(payload) ? payload.status : undefined
177
+ if (result.exitCode !== 0 || status !== 'ready') {
178
+ const reason =
179
+ isRecord(payload) && typeof payload.reason === 'string' ? ` (${payload.reason})` : ''
180
+ return {
181
+ usable: false,
182
+ reason: `Pi model "${model}" is not ready with the configured provider credentials${reason}`,
183
+ }
184
+ }
185
+
186
+ if (catalog === undefined) {
187
+ let catalogResult: PiCliResult
188
+ try {
189
+ catalogResult = await runCli({ args: ['--list-models'], cwd: input.cwd, env })
190
+ } catch (error) {
191
+ return {
192
+ usable: false,
193
+ reason: `Pi local model catalog probe failed to launch: ${errorText(error)}`,
194
+ }
195
+ }
196
+ if (catalogResult.exitCode !== 0) {
197
+ return {
198
+ usable: false,
199
+ reason: `Pi local model catalog probe failed: "pi --list-models" exited with code ${catalogResult.exitCode}: ${catalogResult.stderr.trim() || catalogResult.stdout.trim() || 'no diagnostic'}`,
200
+ }
201
+ }
202
+ try {
203
+ catalog = parsePiModelCatalog(catalogResult.stdout)
204
+ } catch (error) {
205
+ return {
206
+ usable: false,
207
+ reason: `Pi local model catalog probe returned a malformed table: ${errorText(error)}`,
208
+ }
209
+ }
210
+ }
211
+ if (
212
+ !catalog.some(
213
+ (candidate) => candidate.provider === modelRef.provider && candidate.id === modelRef.id,
214
+ )
215
+ ) {
216
+ return {
217
+ usable: false,
218
+ reason: `Pi model "${model}" is unavailable from the local Pi model catalog`,
219
+ }
220
+ }
221
+ }
222
+ return { usable: true, reason: 'Local Pi CLI, model catalog, and authentication are available' }
223
+ } catch (error) {
224
+ return { usable: false, reason: errorText(error) }
225
+ }
226
+ }
227
+
228
+ /** Options that select the model or the RPC protocol parsed by the adapter. */
229
+ export const PI_OWNED_ARGS = [PI_RPC_MODE_ARG, PI_MODEL_ARG] as const
230
+
231
+ export interface PiTurn {
232
+ text: string
233
+ usage: { inputTokens: number; outputTokens: number }
234
+ failure?: AgentTurnFailure
235
+ }
236
+
237
+ export interface PiSession {
238
+ readonly sessionId: string
239
+ prompt(text: string, env: Record<string, string>, signal?: AbortSignal): Promise<PiTurn>
240
+ dispose(): Promise<void> | void
241
+ }
242
+
243
+ export type PiCreateSessionFn = (opts: {
244
+ cwd: string
245
+ model?: PiModelRef
246
+ tools: readonly string[]
247
+ args: readonly string[]
248
+ skill?: string
249
+ env: Record<string, string>
250
+ /** Session-event observer for stream translation (SPEC §9); fakes may
251
+ * invoke it to deliver native session events during a prompt. */
252
+ onEvent?: (event: JsonRecord) => void
253
+ }) => Promise<PiSession>
254
+
255
+ const createLocalPiSession: PiCreateSessionFn = async (opts) => {
256
+ await checkLocalPi(opts.cwd, opts.env)
257
+ return createPiRpcSession(opts)
258
+ }
259
+
260
+ interface TurnRecord {
261
+ turn: number
262
+ prompt: string
263
+ text: string
264
+ usage: { inputTokens: number; outputTokens: number }
265
+ failure?: AgentTurnFailure
266
+ }
267
+
268
+ type JsonRecord = Record<string, unknown>
269
+
270
+ /** Per-session event hook: the current turn's translator, or none between
271
+ * turns. Prompts are sequential per session, so one slot is safe. */
272
+ interface PiTurnEventHook {
273
+ translator?: PiTurnTranslator
274
+ }
275
+
276
+ interface SessionState {
277
+ opts: AgentStartOpts
278
+ model?: string
279
+ session?: PiSession
280
+ /** Set whenever the session was created through a path that could deliver
281
+ * native events; a turn without a stream leaves it unused. */
282
+ turnEvents?: PiTurnEventHook
283
+ turns: TurnRecord[]
284
+ }
285
+
286
+ /**
287
+ * Maps Pi's native session events onto the stream part vocabulary (SPEC §9).
288
+ * Unrecognized events produce no parts (fail open); the harness outcome is
289
+ * remembered so a turn ending in a provider abort emits an `abort` part
290
+ * rather than `error`.
291
+ */
292
+ interface PiTurnTranslator {
293
+ onEvent(event: JsonRecord): void
294
+ /** Set when `message_end` carried stopReason "aborted". */
295
+ aborted(): boolean
296
+ }
297
+
298
+ function createPiTurnTranslator(emit: (parts: StreamPart[]) => void): PiTurnTranslator {
299
+ let nextId = 0
300
+ let textId: string | undefined
301
+ let thinkingId: string | undefined
302
+ let aborted = false
303
+
304
+ const blockId = (event: JsonRecord, current: string | undefined): string => {
305
+ const existing = event.id
306
+ if (typeof existing === 'string' && existing.length > 0) return existing
307
+ return current ?? `pi-${++nextId}`
308
+ }
309
+
310
+ return {
311
+ onEvent(event) {
312
+ switch (event.type) {
313
+ case 'agent_start':
314
+ case 'turn_start':
315
+ emit([startStepPart()])
316
+ break
317
+ case 'message_update': {
318
+ const update = isRecord(event.assistantMessageEvent) ? event.assistantMessageEvent : {}
319
+ switch (update.type) {
320
+ case 'text_start':
321
+ textId = blockId(update, textId ?? `pi-${++nextId}`)
322
+ emit([textStartPart(textId)])
323
+ break
324
+ case 'text_delta':
325
+ textId ??= `pi-${++nextId}`
326
+ if (typeof update.delta === 'string') emit([textDeltaPart(textId, update.delta)])
327
+ break
328
+ case 'text_end':
329
+ emit([textEndPart(blockId(update, textId ?? `pi-${++nextId}`))])
330
+ textId = undefined
331
+ break
332
+ case 'thinking_start':
333
+ thinkingId = blockId(update, thinkingId ?? `pi-${++nextId}`)
334
+ emit([reasoningStartPart(thinkingId)])
335
+ break
336
+ case 'thinking_delta':
337
+ thinkingId ??= `pi-${++nextId}`
338
+ if (typeof update.delta === 'string') {
339
+ emit([reasoningDeltaPart(thinkingId, update.delta)])
340
+ }
341
+ break
342
+ case 'thinking_end':
343
+ emit([reasoningEndPart(blockId(update, thinkingId ?? `pi-${++nextId}`))])
344
+ thinkingId = undefined
345
+ break
346
+ default:
347
+ break
348
+ }
349
+ break
350
+ }
351
+ case 'tool_execution_start': {
352
+ const toolCallId =
353
+ typeof event.toolCallId === 'string' ? event.toolCallId : `pi-${++nextId}`
354
+ const toolName = typeof event.toolName === 'string' ? event.toolName : 'tool'
355
+ emit([toolInputPart(toolCallId, toolName, event.args ?? {})])
356
+ break
357
+ }
358
+ case 'tool_execution_end': {
359
+ const toolCallId =
360
+ typeof event.toolCallId === 'string' ? event.toolCallId : `pi-${++nextId}`
361
+ emit([toolOutputPart(toolCallId, event.result ?? null)])
362
+ break
363
+ }
364
+ case 'message_end': {
365
+ const message = isRecord(event.message) ? event.message : {}
366
+ if (message.stopReason === 'aborted') aborted = true
367
+ break
368
+ }
369
+ case 'turn_end':
370
+ emit([finishStepPart()])
371
+ break
372
+ default:
373
+ break
374
+ }
375
+ },
376
+ aborted() {
377
+ return aborted
378
+ },
379
+ }
380
+ }
381
+
382
+ export class PiAgentRunner implements AgentRunner, OneShotCompletion {
383
+ readonly name = 'pi'
384
+ private readonly createSessionFn: PiCreateSessionFn
385
+ private readonly createSessionId: () => string
386
+ private readonly sessions = new Map<string, SessionState>()
387
+
388
+ constructor(opts: { createSessionFn?: PiCreateSessionFn; createSessionId?: () => string } = {}) {
389
+ this.createSessionFn = opts.createSessionFn ?? createLocalPiSession
390
+ this.createSessionId = opts.createSessionId ?? (() => crypto.randomUUID())
391
+ }
392
+
393
+ async complete(input: OneShotCompletionInput): Promise<OneShotCompletionResult> {
394
+ const model = input.model !== undefined ? parsePiModel(input.model) : undefined
395
+ const session = await this.createSessionFn({
396
+ cwd: input.cwd,
397
+ ...(model !== undefined ? { model } : {}),
398
+ tools: [],
399
+ args: input.args ?? [],
400
+ env: sessionEnv(input.env),
401
+ })
402
+ try {
403
+ const turn = await session.prompt(input.prompt, sessionEnv(input.env), input.signal)
404
+ if (turn.failure !== undefined) throw new Error(turn.failure.message)
405
+ return { text: turn.text }
406
+ } finally {
407
+ await session.dispose()
408
+ }
409
+ }
410
+
411
+ async start(
412
+ opts: AgentStartOpts,
413
+ ): Promise<{ session: AgentSessionHandle; result: AgentTurnResult }> {
414
+ const model = opts.model !== undefined ? parsePiModel(opts.model) : undefined
415
+ const prompt = `/skill:${opts.skill} ${agentInvocation(opts)}`
416
+ let native: PiSession | undefined
417
+ let turn: PiTurn
418
+ const turnEvents: PiTurnEventHook = {}
419
+ try {
420
+ native = await this.createSessionFn({
421
+ cwd: opts.workspacePath,
422
+ ...(model !== undefined ? { model } : {}),
423
+ tools: PI_TOOLS,
424
+ args: opts.args ?? [],
425
+ skill: opts.skill,
426
+ env: sessionEnv(opts.env),
427
+ // Stream translation is per turn; the hook stays live for later
428
+ // continued turns even when turn 1 streams nothing.
429
+ onEvent: (event) => turnEvents.translator?.onEvent(event),
430
+ })
431
+ turn = await this.runPrompt(
432
+ native,
433
+ prompt,
434
+ this.turnEnv(opts.env),
435
+ opts.signal,
436
+ opts.stream,
437
+ turnEvents,
438
+ )
439
+ } catch (error) {
440
+ turn = {
441
+ text: '',
442
+ usage: { inputTokens: 0, outputTokens: 0 },
443
+ failure: localPiFailure(error),
444
+ }
445
+ }
446
+ const id = native?.sessionId ?? this.createSessionId()
447
+ const handle: AgentSessionHandle = {
448
+ id,
449
+ runner: this.name,
450
+ ...(opts.model !== undefined ? { model: opts.model } : {}),
451
+ }
452
+ this.sessions.set(id, {
453
+ opts,
454
+ ...(opts.model !== undefined ? { model: opts.model } : {}),
455
+ ...(native !== undefined ? { session: native } : {}),
456
+ ...(native !== undefined ? { turnEvents } : {}),
457
+ turns: [this.turnRecord(1, prompt, turn)],
458
+ })
459
+ return { session: handle, result: this.toResult(turn) }
460
+ }
461
+
462
+ async continue(
463
+ handle: AgentSessionHandle,
464
+ message: string,
465
+ opts?: AgentContinueOpts,
466
+ ): Promise<AgentTurnResult> {
467
+ const state = this.liveState(handle, 'continue')
468
+ if (state.session === undefined) {
469
+ throw new Error(`pi: cannot continue session "${handle.id}" because local Pi failed to start`)
470
+ }
471
+ const scoped = opts?.env !== undefined ? { ...state.opts.env, ...opts.env } : state.opts.env
472
+ const turn = await this.runPrompt(
473
+ state.session,
474
+ message,
475
+ this.turnEnv(scoped),
476
+ opts?.signal,
477
+ opts?.stream,
478
+ state.turnEvents,
479
+ )
480
+ state.turns.push(this.turnRecord(state.turns.length + 1, message, turn))
481
+ return this.toResult(turn)
482
+ }
483
+
484
+ async end(handle: AgentSessionHandle): Promise<Transcript> {
485
+ const state = this.liveState(handle, 'end')
486
+ this.sessions.delete(handle.id)
487
+ await state.session?.dispose()
488
+ const usage = { inputTokens: 0, outputTokens: 0, turns: 0 }
489
+ for (const turn of state.turns) {
490
+ usage.inputTokens += turn.usage.inputTokens
491
+ usage.outputTokens += turn.usage.outputTokens
492
+ usage.turns += 1
493
+ }
494
+ return {
495
+ content: JSON.stringify(
496
+ {
497
+ session: handle.id,
498
+ skill: state.opts.skill,
499
+ invocation: agentInvocation(state.opts),
500
+ ...(state.opts.buildSlug !== undefined ? { buildSlug: state.opts.buildSlug } : {}),
501
+ turns: state.turns,
502
+ },
503
+ null,
504
+ 2,
505
+ ),
506
+ metadata: {
507
+ runner: this.name,
508
+ ...(state.model !== undefined ? { model: state.model } : {}),
509
+ usage,
510
+ },
511
+ }
512
+ }
513
+
514
+ private async runPrompt(
515
+ session: PiSession,
516
+ prompt: string,
517
+ env: Record<string, string>,
518
+ signal?: AbortSignal,
519
+ stream?: SessionStreamEmitter,
520
+ turnEvents?: PiTurnEventHook,
521
+ ): Promise<PiTurn> {
522
+ if (stream !== undefined) stream.append([promptPart(prompt), startPart(crypto.randomUUID())])
523
+ const translator =
524
+ stream !== undefined ? createPiTurnTranslator((parts) => stream.append(parts)) : undefined
525
+ if (translator !== undefined && turnEvents !== undefined) turnEvents.translator = translator
526
+ try {
527
+ const turn = await session.prompt(prompt, env, signal)
528
+ if (stream !== undefined) {
529
+ if (turn.failure === undefined) {
530
+ stream.append([finishPart()])
531
+ } else if (translator?.aborted() === true || signal?.aborted === true) {
532
+ stream.append([abortPart(turn.failure.message)])
533
+ } else {
534
+ stream.append([errorPart(turn.failure.message)])
535
+ }
536
+ }
537
+ return turn
538
+ } catch (error) {
539
+ const failure = localPiFailure(error)
540
+ if (stream !== undefined) {
541
+ if (signal?.aborted) {
542
+ stream.append([abortPart(error instanceof Error ? error.message : String(error))])
543
+ } else if (translator?.aborted() === true) {
544
+ stream.append([abortPart(failure.message)])
545
+ } else {
546
+ stream.append([errorPart(failure.message)])
547
+ }
548
+ }
549
+ return {
550
+ text: '',
551
+ usage: { inputTokens: 0, outputTokens: 0 },
552
+ failure,
553
+ }
554
+ } finally {
555
+ if (translator !== undefined && turnEvents !== undefined) turnEvents.translator = undefined
556
+ }
557
+ }
558
+
559
+ private liveState(handle: AgentSessionHandle, operation: 'continue' | 'end'): SessionState {
560
+ const state = this.sessions.get(handle.id)
561
+ if (state === undefined) throw new Error(`pi: ${operation} on unknown session "${handle.id}"`)
562
+ return state
563
+ }
564
+
565
+ private turnRecord(number: number, prompt: string, turn: PiTurn): TurnRecord {
566
+ return {
567
+ turn: number,
568
+ prompt,
569
+ text: turn.text,
570
+ usage: turn.usage,
571
+ ...(turn.failure !== undefined ? { failure: turn.failure } : {}),
572
+ }
573
+ }
574
+
575
+ private toResult(turn: PiTurn): AgentTurnResult {
576
+ const base = { text: turn.text, usage: { ...turn.usage, turns: 1 } }
577
+ return turn.failure === undefined
578
+ ? { kind: 'completed', ...base }
579
+ : { kind: 'failed', ...base, failure: turn.failure }
580
+ }
581
+
582
+ private turnEnv(scoped: Record<string, string>): Record<string, string> {
583
+ return sessionEnv(scoped)
584
+ }
585
+ }
586
+
587
+ function localPiFailure(error: unknown): AgentTurnFailure {
588
+ const message = errorText(error)
589
+ if (
590
+ /executable "pi" was not found|requires Pi .* or newer|could not parse detected Pi version/.test(
591
+ message,
592
+ )
593
+ ) {
594
+ return configurationFailure(message)
595
+ }
596
+ if (/not ready|authentication|logged in|credentials/i.test(message))
597
+ return credentialFailure(message)
598
+ return classifyProviderError(message)
599
+ }
600
+
601
+ function cleanEnv(env: Readonly<Record<string, string | undefined>>): Record<string, string> {
602
+ return Object.fromEntries(
603
+ Object.entries(env).filter((entry): entry is [string, string] => entry[1] !== undefined),
604
+ )
605
+ }
606
+
607
+ function isEnoent(error: unknown): boolean {
608
+ return isRecord(error) && error.code === 'ENOENT'
609
+ }
610
+
611
+ function isRecord(value: unknown): value is Record<string, unknown> {
612
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
613
+ }
614
+
615
+ function errorText(error: unknown): string {
616
+ return error instanceof Error ? error.message : String(error)
617
+ }
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Production runtime registrations shared by every sessionless CLI path that
3
+ * needs agent judgment. Keeping the shipped adapters and their model families
4
+ * here prevents dispatch and other non-phase one-shots from drifting apart.
5
+ */
6
+ import {
7
+ CLAUDE_OWNED_ARGS,
8
+ CLAUDE_PROMPT_BOUNDARY,
9
+ ClaudeAgentRunner,
10
+ isClaudeRuntimeUsable,
11
+ } from './claude'
12
+ import {
13
+ CODEX_OWNED_ARGS,
14
+ CODEX_PROMPT_BOUNDARY,
15
+ CodexAgentRunner,
16
+ isCodexRuntimeUsable,
17
+ } from './codex'
18
+ import { isPiRuntimeUsable, PI_OWNED_ARGS, PiAgentRunner } from './pi'
19
+ import type { SessionStreamInfo, RuntimeRegistry } from './runtime'
20
+ import type { SessionStreamSink } from '../types'
21
+
22
+ export interface ProductionRuntimes {
23
+ runtimes: RuntimeRegistry
24
+ }
25
+
26
+ export function createProductionRuntimes(): ProductionRuntimes {
27
+ // Each adapter carries both the resumable AgentRunner contract and its
28
+ // optional tool-free OneShotCompletion capability.
29
+ const claude = new ClaudeAgentRunner()
30
+ const codex = new CodexAgentRunner()
31
+ const pi = new PiAgentRunner()
32
+
33
+ return {
34
+ runtimes: {
35
+ claude: {
36
+ runner: claude,
37
+ oneShot: claude,
38
+ initUsable: isClaudeRuntimeUsable,
39
+ servesModels: ['claude-'],
40
+ ownedArgs: CLAUDE_OWNED_ARGS,
41
+ promptBoundary: CLAUDE_PROMPT_BOUNDARY,
42
+ openSessionStream: openSessionStreamLabel,
43
+ },
44
+ codex: {
45
+ runner: codex,
46
+ oneShot: codex,
47
+ initUsable: isCodexRuntimeUsable,
48
+ // Codex CLI model ids are unqualified; an omitted model delegates to
49
+ // the operator's configured Codex default.
50
+ servesModels: ['gpt-'],
51
+ ownedArgs: CODEX_OWNED_ARGS,
52
+ promptBoundary: CODEX_PROMPT_BOUNDARY,
53
+ openSessionStream: openSessionStreamLabel,
54
+ },
55
+ pi: {
56
+ runner: pi,
57
+ oneShot: pi,
58
+ initUsable: isPiRuntimeUsable,
59
+ // The local Pi installation owns the provider catalog. Any
60
+ // provider-qualified model it resolves is valid here.
61
+ servesModels: ['*/*'],
62
+ defaultModel: 'kimi-coding/k3',
63
+ ownedArgs: PI_OWNED_ARGS,
64
+ openSessionStream: openSessionStreamLabel,
65
+ },
66
+ },
67
+ }
68
+ }
69
+
70
+ /** The one shared opener: a session bracket's stream is labeled by its
71
+ * Autobuild session id, never a harness-native session/thread id. */
72
+ function openSessionStreamLabel(sink: SessionStreamSink, info: SessionStreamInfo): Promise<string> {
73
+ return sink.open(`session:${info.session}`)
74
+ }