@defrex/autobuild 0.8.0 → 0.10.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 (182) hide show
  1. package/README.md +1 -1
  2. package/SPEC.md +323 -26
  3. package/bin/ab-build-runner.ts +8 -2
  4. package/docs/architecture.md +166 -14
  5. package/docs/assets/headline-wide.png +0 -0
  6. package/docs/configuration.md +175 -28
  7. package/docs/hosted-dispatcher.md +69 -13
  8. package/docs/operator-api.md +2 -2
  9. package/docs/remote-store-protocol.md +204 -16
  10. package/docs/remote-ticket-protocol.md +1 -1
  11. package/docs/setup.md +12 -9
  12. package/docs/website.md +14 -0
  13. package/package.json +45 -23
  14. package/packages/core/src/cli/artifact.ts +130 -6
  15. package/packages/core/src/cli/binary.ts +9 -1
  16. package/packages/core/src/cli/build-control.ts +110 -13
  17. package/packages/core/src/cli/context.ts +46 -0
  18. package/packages/core/src/cli/dashboard/facts.ts +215 -0
  19. package/packages/core/src/cli/dashboard/frame-parse.ts +223 -0
  20. package/packages/core/src/cli/dashboard/model.ts +141 -153
  21. package/packages/core/src/cli/dashboard/poll.ts +76 -12
  22. package/packages/core/src/cli/dashboard/render.ts +34 -5
  23. package/packages/core/src/cli/dispatch-frontend.ts +54 -5
  24. package/packages/core/src/cli/dispatch-process.ts +1 -13
  25. package/packages/core/src/cli/dispatch.ts +392 -75
  26. package/packages/core/src/cli/harvest.ts +122 -8
  27. package/packages/core/src/cli/help.ts +62 -9
  28. package/packages/core/src/cli/init-readiness-remote.ts +110 -0
  29. package/packages/core/src/cli/init-readiness-shared.ts +72 -0
  30. package/packages/core/src/cli/init-validation.ts +266 -222
  31. package/packages/core/src/cli/installation.ts +50 -8
  32. package/packages/core/src/cli/main.ts +70 -11
  33. package/packages/core/src/cli/mcp.ts +23 -1
  34. package/packages/core/src/cli/plugin.ts +7 -4
  35. package/packages/core/src/cli/remote-poll.ts +277 -0
  36. package/packages/core/src/cli/repo-state.ts +22 -0
  37. package/packages/core/src/cli/repository-status.ts +39 -1
  38. package/packages/core/src/cli/self-update.ts +77 -2
  39. package/packages/core/src/cli/status.ts +150 -28
  40. package/packages/core/src/cli/terminals.ts +2 -1
  41. package/packages/core/src/cli/ticket-assets.ts +247 -0
  42. package/packages/core/src/cli/ticket.ts +453 -8
  43. package/packages/core/src/cli/wait.ts +157 -41
  44. package/packages/core/src/cli/watch.ts +138 -168
  45. package/packages/core/src/config/live.ts +23 -0
  46. package/packages/core/src/config/pipeline-source.ts +388 -0
  47. package/packages/core/src/config/schema.ts +251 -19
  48. package/packages/core/src/events/catalog.ts +1 -0
  49. package/packages/core/src/events/globs.ts +47 -0
  50. package/packages/core/src/events/payloads.ts +52 -2
  51. package/packages/core/src/events/repository.ts +52 -1
  52. package/packages/core/src/events/sessions.ts +69 -10
  53. package/packages/core/src/events/workspace-remote.ts +12 -0
  54. package/packages/core/src/integration/harness.ts +115 -2
  55. package/packages/core/src/kernel/__snapshots__/reducer-registry.test.ts.snap +1369 -0
  56. package/packages/core/src/kernel/auto-merge-default.ts +157 -0
  57. package/packages/core/src/kernel/auto-merge.ts +99 -22
  58. package/packages/core/src/kernel/dispatch-settings.ts +42 -17
  59. package/packages/core/src/kernel/dispatch-status.ts +23 -0
  60. package/packages/core/src/kernel/engine.ts +9 -438
  61. package/packages/core/src/kernel/generators/build-log.ts +259 -0
  62. package/packages/core/src/kernel/generators/repository-journals.ts +206 -0
  63. package/packages/core/src/kernel/generators/session-log.ts +72 -0
  64. package/packages/core/src/kernel/harvest.ts +62 -18
  65. package/packages/core/src/kernel/incremental-contract.ts +75 -0
  66. package/packages/core/src/kernel/incremental-fixtures.ts +211 -0
  67. package/packages/core/src/kernel/incremental.ts +53 -0
  68. package/packages/core/src/kernel/log-index.ts +620 -0
  69. package/packages/core/src/kernel/phases.ts +16 -2
  70. package/packages/core/src/kernel/pr-attachments.ts +164 -46
  71. package/packages/core/src/kernel/reducer-registry.ts +164 -0
  72. package/packages/core/src/kernel/reducer.ts +254 -73
  73. package/packages/core/src/operator/annotations.ts +6 -0
  74. package/packages/core/src/operator/control.ts +19 -5
  75. package/packages/core/src/operator/index.ts +71 -6
  76. package/packages/core/src/operator/query.ts +253 -24
  77. package/packages/core/src/operator/read-view.ts +98 -0
  78. package/packages/core/src/operator/registry.ts +55 -10
  79. package/packages/core/src/operator/requests.ts +9 -6
  80. package/packages/core/src/operator/sandbox-host.ts +140 -0
  81. package/packages/core/src/operator/sandbox.ts +247 -14
  82. package/packages/core/src/operator/ticket-source-backend.ts +47 -0
  83. package/packages/core/src/orchestrator/approvals.ts +115 -0
  84. package/packages/core/src/orchestrator/conversation.ts +167 -0
  85. package/packages/core/src/orchestrator/model.ts +86 -0
  86. package/packages/core/src/orchestrator/prompt.ts +61 -0
  87. package/packages/core/src/orchestrator/tools.ts +117 -0
  88. package/packages/core/src/orchestrator/turn-runner.ts +457 -0
  89. package/packages/core/src/plugin-sdk/index.ts +51 -1
  90. package/packages/core/src/plugins/load.ts +135 -7
  91. package/packages/core/src/plugins/manifest.ts +183 -10
  92. package/packages/core/src/plugins/registry.ts +73 -4
  93. package/packages/core/src/ports/forge/create.ts +1 -1
  94. package/packages/core/src/ports/forge/fake.ts +31 -0
  95. package/packages/core/src/ports/forge/github-transport.ts +13 -2
  96. package/packages/core/src/ports/forge/github.ts +13 -2
  97. package/packages/core/src/ports/forge/local-git.ts +31 -1
  98. package/packages/core/src/ports/runner/claude.ts +2 -2
  99. package/packages/core/src/ports/runner/codex.ts +6 -2
  100. package/packages/core/src/ports/runner/fake.ts +7 -1
  101. package/packages/core/src/ports/runner/pi.ts +8 -6
  102. package/packages/core/src/ports/runner/provider-error.ts +13 -3
  103. package/packages/core/src/ports/tickets/fake.ts +5 -1
  104. package/packages/core/src/ports/tickets/file.ts +169 -48
  105. package/packages/core/src/ports/tickets/{remote-index.ts → hosted-index.ts} +3 -2
  106. package/packages/core/src/ports/tickets/linear.ts +33 -1
  107. package/packages/core/src/ports/tickets/operations.ts +1 -1
  108. package/packages/core/src/ports/tickets/remote.ts +8 -1
  109. package/packages/core/src/ports/types.ts +49 -3
  110. package/packages/core/src/ports/workspace/builtin-capabilities.ts +114 -0
  111. package/packages/core/src/ports/workspace/create.ts +74 -39
  112. package/packages/core/src/ports/workspace/distribution-archive.ts +171 -12
  113. package/packages/core/src/ports/workspace/fake.ts +169 -4
  114. package/packages/core/src/ports/workspace/git-worktree.ts +246 -26
  115. package/packages/core/src/ports/workspace/github-origin.ts +36 -0
  116. package/packages/core/src/ports/workspace/operator-sandbox.ts +7 -1
  117. package/packages/core/src/ports/workspace/provider-capabilities.ts +171 -0
  118. package/packages/core/src/ports/workspace/vercel-capabilities.ts +110 -0
  119. package/packages/core/src/ports/workspace/vercel-sandbox.ts +142 -114
  120. package/packages/core/src/processes/build-child.ts +17 -2
  121. package/packages/core/src/processes/build-execution-state.ts +68 -15
  122. package/packages/core/src/processes/build-runner.ts +238 -93
  123. package/packages/core/src/processes/dispatcher-selectors.ts +123 -0
  124. package/packages/core/src/processes/dispatcher.ts +501 -201
  125. package/packages/core/src/processes/execution-settlement.ts +72 -39
  126. package/packages/core/src/processes/harvest-child.ts +5 -0
  127. package/packages/core/src/processes/harvest-execution-state.ts +41 -29
  128. package/packages/core/src/processes/harvest-runner.ts +66 -19
  129. package/packages/core/src/processes/harvest.ts +196 -24
  130. package/packages/core/src/processes/infrastructure-failure-budget.ts +31 -18
  131. package/packages/core/src/processes/orchestrator-tick.ts +360 -0
  132. package/packages/core/src/processes/publication-state.ts +164 -52
  133. package/packages/core/src/processes/repo-view.ts +1436 -0
  134. package/packages/core/src/processes/sandbox-state.ts +36 -2
  135. package/packages/core/src/processes/tick-state.ts +33 -0
  136. package/packages/core/src/store/adapter.ts +41 -0
  137. package/packages/core/src/store/build-scope.ts +90 -0
  138. package/packages/core/src/store/contract.ts +1205 -0
  139. package/packages/core/src/store/digest.ts +102 -0
  140. package/packages/core/src/store/local/schema.ts +46 -2
  141. package/packages/core/src/store/local/store.ts +445 -4
  142. package/packages/core/src/store/memory.ts +246 -0
  143. package/packages/core/src/store/new-build.ts +17 -0
  144. package/packages/core/src/store/phase-session.ts +94 -0
  145. package/packages/core/src/store/remote/client.ts +310 -2
  146. package/packages/core/src/store/remote/index.ts +14 -2
  147. package/packages/core/src/store/remote/protocol.ts +115 -12
  148. package/packages/core/src/store/remote/version.ts +6 -2
  149. package/packages/core/src/store/repo-state-events.ts +205 -0
  150. package/packages/core/src/store/retention.ts +7 -0
  151. package/packages/core/src/store/session-handle.ts +94 -0
  152. package/packages/core/src/{sessions/reducer.ts → store/session-reducer.ts} +79 -19
  153. package/packages/core/src/store/snapshots.ts +88 -0
  154. package/packages/core/src/store/streams/session-writer.ts +42 -2
  155. package/packages/core/src/store/streams/wait.ts +4 -5
  156. package/packages/core/src/store/ticket-assets.ts +458 -0
  157. package/packages/core/src/store/types.ts +160 -1
  158. package/packages/core/src/testing/index.ts +59 -0
  159. package/skills/code-review/SKILL.md +5 -1
  160. package/skills/guide/SKILL.md +152 -40
  161. package/skills/guide/references/operator-api.md +2 -2
  162. package/skills/guide/references/plugin-authoring.md +17 -0
  163. package/skills/guide/references/remote-store-protocol.md +204 -16
  164. package/skills/guide/references/remote-ticket-protocol.md +1 -1
  165. package/skills/guide/references/setup.md +12 -9
  166. package/skills/implement/SKILL.md +3 -0
  167. package/skills/operate/SKILL.md +126 -0
  168. package/skills/plan/SKILL.md +4 -0
  169. package/skills/plan-review/SKILL.md +5 -1
  170. package/skills/spec/SKILL.md +46 -3
  171. package/skills/tickets/SKILL.md +57 -5
  172. package/packages/core/src/cli/dashboard/frame-image.ts +0 -373
  173. package/packages/core/src/cli/store-opening.contract.ts +0 -289
  174. package/packages/core/src/cli/testkit.ts +0 -194
  175. package/packages/core/src/markdown.ts +0 -67
  176. package/packages/core/src/operator/client.ts +0 -311
  177. package/packages/core/src/operator/server.ts +0 -616
  178. package/packages/core/src/operator/sessions.ts +0 -223
  179. package/packages/core/src/ports/runner/live-contract-fixture.ts +0 -176
  180. package/packages/core/src/ports/tickets/remote-server.ts +0 -185
  181. package/packages/core/src/store/remote/server.ts +0 -923
  182. package/packages/core/src/testing/store-failures.ts +0 -32
@@ -1,616 +0,0 @@
1
- import type { ZodType } from 'zod'
2
- import { controlBuild, BuildControlError, type BuildControlAction } from '../cli/build-control'
3
- import { bulkControlRepository, BulkWalkError } from '../cli/bulk-control'
4
- import { effectiveStatus } from '../cli/dashboard/model'
5
- import type { Via } from '../events/envelope'
6
- import { reduceBuild } from '../kernel/reducer'
7
- import { systemClock, type BuildStore, type Clock } from '../store/types'
8
- import { tokenResource, verifyToken } from '../store/remote/token'
9
- import {
10
- AUTOBUILD_VERSION,
11
- AUTOBUILD_VERSION_HEADER,
12
- REMOTE_STORE_PROTOCOL_VERSION,
13
- REMOTE_STORE_PROTOCOL_VERSION_HEADER,
14
- } from '../store/remote/version'
15
- import {
16
- answerRequestSchema,
17
- buildControlRequestSchema,
18
- buildListScopeSchema,
19
- bulkControlRequestSchema,
20
- harvestControlRequestSchema,
21
- sessionApprovalRequestSchema,
22
- sessionCreateRequestSchema,
23
- sessionMessageRequestSchema,
24
- sessionWakeRequestSchema,
25
- settingRequestSchema,
26
- ticketBlockerRequestSchema,
27
- ticketCreateRequestSchema,
28
- ticketMoveRequestSchema,
29
- ticketUpdateRequestSchema,
30
- } from './protocol'
31
- import {
32
- controlHarvestRun,
33
- OperatorControlError,
34
- setRepositorySetting,
35
- toggleHarvestGate,
36
- toggleRepositorySetting,
37
- } from './control'
38
- import {
39
- getHarvestStatus,
40
- getOperatorBuild,
41
- getOperatorDashboard,
42
- getRepositoryStatus,
43
- listOperatorBuilds,
44
- OperatorQueryError,
45
- } from './query'
46
- import { TicketOperationError } from '../ports/tickets/operations'
47
- import { buildRegistry, RegistryError, type OperatorToolRegistry } from './registry'
48
- import {
49
- answerOperatorApproval,
50
- archiveOperatorSession,
51
- createOperatorSession,
52
- getOperatorSession,
53
- listOperatorSessions,
54
- OperatorSessionError,
55
- postOperatorMessage,
56
- readOperatorTurnStream,
57
- setOperatorWake,
58
- } from './sessions'
59
- import type { OperatorSandboxService } from './sandbox'
60
- import {
61
- getOperatorTicket,
62
- listOperatorTickets,
63
- mutateOperatorTicket,
64
- type OperatorTicketBackend,
65
- } from './tickets'
66
-
67
- export interface OperatorServerOptions {
68
- store: BuildStore
69
- secret: string
70
- clock?: Clock
71
- /** Hosted ticket capability. Omitted deployments retain build-only operator routes. */
72
- ticketBackend?: OperatorTicketBackend
73
- /** Operator-sandbox backend (AUT-340): archiving an operator's last open
74
- * session for a repository releases their sandbox environment. The hosted
75
- * service does not pass one — the hosted sandbox backend is the later
76
- * hosted-transport ticket. */
77
- sandbox?: OperatorSandboxService
78
- /** Observes unexpected backing-store failures without exposing them over HTTP. */
79
- onInternalError?: (error: unknown, request: Request) => unknown | Promise<unknown>
80
- }
81
-
82
- class HttpError extends Error {
83
- constructor(
84
- readonly status: number,
85
- readonly kind: 'validation' | 'auth' | 'not-found' | 'conflict',
86
- message: string,
87
- ) {
88
- super(message)
89
- }
90
- }
91
-
92
- /** The registry failure kinds map onto HTTP statuses deterministically; the
93
- * failure body itself round-trips unchanged (same shape as every route's). */
94
- export const REGISTRY_ERROR_STATUS: Record<string, number> = {
95
- validation: 400,
96
- auth: 403,
97
- 'not-found': 404,
98
- conflict: 409,
99
- refusal: 409,
100
- internal: 500,
101
- }
102
-
103
- function json(status: number, value: unknown): Response {
104
- return Response.json(value, { status })
105
- }
106
- function failure(status: number, kind: string, error: string, extra: object = {}): Response {
107
- return json(status, { kind, error, ...extra })
108
- }
109
- async function body<T>(req: Request, schema: ZodType<T>): Promise<T> {
110
- let value: unknown
111
- try {
112
- value = await req.json()
113
- } catch {
114
- throw new HttpError(400, 'validation', 'request body is not valid JSON')
115
- }
116
- const parsed = schema.safeParse(value)
117
- if (!parsed.success) {
118
- throw new HttpError(400, 'validation', `invalid request body: ${parsed.error.message}`)
119
- }
120
- return parsed.data
121
- }
122
-
123
- export function createOperatorServer(opts: OperatorServerOptions): {
124
- fetch(req: Request): Promise<Response>
125
- } {
126
- const clock = opts.clock ?? systemClock
127
- // The registry executes over the same store and ticket backend the typed
128
- // routes use; the generic tools route is its protocol face. Built once so
129
- // every tool call shares one closed table.
130
- const registry: OperatorToolRegistry = buildRegistry({
131
- store: opts.store,
132
- clock,
133
- ...(opts.ticketBackend !== undefined ? { tickets: opts.ticketBackend } : {}),
134
- })
135
-
136
- function identity(req: Request): void {
137
- const app = req.headers.get(AUTOBUILD_VERSION_HEADER)
138
- const protocol = req.headers.get(REMOTE_STORE_PROTOCOL_VERSION_HEADER)
139
- if (app !== AUTOBUILD_VERSION || protocol !== REMOTE_STORE_PROTOCOL_VERSION) {
140
- throw new HttpError(
141
- 409,
142
- 'conflict',
143
- `remote store version mismatch: client Autobuild ${app ?? '(missing)'} protocol ${protocol ?? '(missing)'}; server Autobuild ${AUTOBUILD_VERSION} protocol ${REMOTE_STORE_PROTOCOL_VERSION}`,
144
- )
145
- }
146
- }
147
-
148
- function operator(req: Request): { user: string; via?: Via } {
149
- const match = /^Bearer\s+(.+)$/i.exec(req.headers.get('authorization') ?? '')
150
- if (match === null) throw new HttpError(401, 'auth', 'missing bearer token')
151
- const scope = verifyToken(opts.secret, match[1]!, clock())
152
- if (scope === null) throw new HttpError(401, 'auth', 'invalid or expired token')
153
- const resource = tokenResource(scope)
154
- if (resource.kind !== 'operator' || !('operator' in scope) || scope.operator === true) {
155
- throw new HttpError(
156
- 403,
157
- 'auth',
158
- `token scoped to ${resource.kind} "${resource.id}" may not access operator operations`,
159
- )
160
- }
161
- return { user: scope.operator.user, via: scope.via }
162
- }
163
-
164
- async function requireRouteBuild(repo: string, slug: string): Promise<void> {
165
- const record = await opts.store.getBuild(slug)
166
- if (record === null || record.repo !== repo) {
167
- throw new HttpError(404, 'not-found', `unknown build "${slug}"`)
168
- }
169
- }
170
-
171
- async function route(req: Request): Promise<Response> {
172
- identity(req)
173
- const scope = operator(req)
174
- const user = scope.user
175
- const url = new URL(req.url)
176
- let parts: string[]
177
- try {
178
- parts = url.pathname.split('/').filter(Boolean).map(decodeURIComponent)
179
- } catch {
180
- throw new HttpError(400, 'validation', `malformed path: ${url.pathname}`)
181
- }
182
- if (parts[0] !== 'operator' || parts[1] !== 'v1' || parts[2] !== 'repos' || !parts[3]) {
183
- throw new HttpError(404, 'not-found', `no route: ${req.method} ${url.pathname}`)
184
- }
185
- const repo = parts[3]
186
- const rest = parts.slice(4)
187
-
188
- // The registry's protocol face: one generic route for the whole closed
189
- // table. The body IS the tool's validated input (repo included); the
190
- // registry enforces schema, identity, and failure mapping, so this route
191
- // adds no authority of its own and no per-tool branch.
192
- if (rest[0] === 'tools' && rest[1] && req.method === 'POST' && rest.length === 2) {
193
- const tool = rest[1]
194
- let input: unknown
195
- try {
196
- input = await req.json()
197
- } catch {
198
- throw new HttpError(400, 'validation', 'request body is not valid JSON')
199
- }
200
- if (typeof input !== 'object' || input === null || Array.isArray(input)) {
201
- throw new HttpError(400, 'validation', 'tool input must be a JSON object')
202
- }
203
- const bodyRepo = (input as { repo?: unknown }).repo
204
- if (bodyRepo !== repo) {
205
- throw new HttpError(400, 'validation', `body repo must match the path repository "${repo}"`)
206
- }
207
- try {
208
- const result = await registry.call(tool, input, {
209
- identity: user,
210
- ...(scope.via !== undefined ? { via: scope.via } : {}),
211
- })
212
- return json(200, result)
213
- } catch (error) {
214
- if (error instanceof RegistryError) {
215
- // The failure body round-trips byte-for-byte: same kind union,
216
- // error text, code, and progress as a typed route's refusal.
217
- return failure(
218
- REGISTRY_ERROR_STATUS[error.body.kind] ?? 500,
219
- error.body.kind,
220
- error.body.error,
221
- {
222
- ...(error.body.code !== undefined ? { code: error.body.code } : {}),
223
- ...(error.body.progress !== undefined ? { progress: error.body.progress } : {}),
224
- },
225
- )
226
- }
227
- throw error
228
- }
229
- }
230
-
231
- if (rest[0] === 'tickets') {
232
- if (opts.ticketBackend === undefined) {
233
- throw new HttpError(409, 'conflict', 'ticket operator backend is not configured')
234
- }
235
- if (req.method === 'GET' && rest.length === 1) {
236
- const state = url.searchParams.get('state')
237
- const hasLabels = url.searchParams.has('label')
238
- return json(
239
- 200,
240
- await listOperatorTickets({
241
- store: opts.store,
242
- repo,
243
- backend: opts.ticketBackend,
244
- ...(state !== null ? { state } : {}),
245
- ...(hasLabels ? { labels: url.searchParams.getAll('label') } : {}),
246
- }),
247
- )
248
- }
249
- if (req.method === 'POST' && rest.length === 1) {
250
- const request = await body(req, ticketCreateRequestSchema)
251
- return json(
252
- 201,
253
- await mutateOperatorTicket({
254
- store: opts.store,
255
- repo,
256
- backend: opts.ticketBackend,
257
- operation: { kind: 'create', ...request },
258
- }),
259
- )
260
- }
261
- const id = rest[1]
262
- if (id && req.method === 'GET' && rest.length === 2) {
263
- return json(
264
- 200,
265
- await getOperatorTicket({ store: opts.store, repo, backend: opts.ticketBackend, id }),
266
- )
267
- }
268
- if (id && req.method === 'PATCH' && rest.length === 2) {
269
- const patch = await body(req, ticketUpdateRequestSchema)
270
- return json(
271
- 200,
272
- await mutateOperatorTicket({
273
- store: opts.store,
274
- repo,
275
- backend: opts.ticketBackend,
276
- operation: { kind: 'update', id, patch },
277
- }),
278
- )
279
- }
280
- if (id && req.method === 'POST' && rest.length === 3 && rest[2] === 'move') {
281
- const request = await body(req, ticketMoveRequestSchema)
282
- return json(
283
- 200,
284
- await mutateOperatorTicket({
285
- store: opts.store,
286
- repo,
287
- backend: opts.ticketBackend,
288
- operation: { kind: 'move', id, state: request.state },
289
- }),
290
- )
291
- }
292
- if (
293
- id &&
294
- req.method === 'POST' &&
295
- rest.length === 3 &&
296
- (rest[2] === 'block' || rest[2] === 'unblock')
297
- ) {
298
- const request = await body(req, ticketBlockerRequestSchema)
299
- return json(
300
- 200,
301
- await mutateOperatorTicket({
302
- store: opts.store,
303
- repo,
304
- backend: opts.ticketBackend,
305
- operation: { kind: rest[2], id, blockerIds: request.blockerIds },
306
- }),
307
- )
308
- }
309
- }
310
-
311
- if (req.method === 'GET' && rest.length === 1 && rest[0] === 'builds') {
312
- const parsed = buildListScopeSchema.safeParse(url.searchParams.get('scope') ?? 'active')
313
- if (!parsed.success)
314
- throw new HttpError(400, 'validation', 'scope must be active, queued, or all')
315
- return json(
316
- 200,
317
- await listOperatorBuilds({ store: opts.store, repo, scope: parsed.data, now: clock() }),
318
- )
319
- }
320
- if (req.method === 'GET' && rest.length === 1 && rest[0] === 'dashboard') {
321
- return json(200, await getOperatorDashboard({ store: opts.store, repo, clock }))
322
- }
323
- if (req.method === 'GET' && rest.length === 1 && rest[0] === 'status') {
324
- return json(200, await getRepositoryStatus(opts.store, repo))
325
- }
326
- if (req.method === 'GET' && rest.join('/') === 'harvest/status') {
327
- return json(200, await getHarvestStatus(opts.store, repo))
328
- }
329
- if (req.method === 'POST' && rest.join('/') === 'bulk-control') {
330
- const request = await body(req, bulkControlRequestSchema)
331
- return json(
332
- 200,
333
- await bulkControlRepository({ store: opts.store, repo, user, direction: request.action }),
334
- )
335
- }
336
- if (req.method === 'POST' && rest.join('/') === 'harvest/control') {
337
- const request = await body(req, harvestControlRequestSchema)
338
- return json(
339
- 200,
340
- request.action === 'toggle-gate'
341
- ? await toggleHarvestGate({ store: opts.store, repo, user })
342
- : await controlHarvestRun({ store: opts.store, repo, user, run: request.run }),
343
- )
344
- }
345
-
346
- if (rest[0] === 'settings' && rest[1] && rest.length >= 2) {
347
- const setting =
348
- rest[1] === 'intake'
349
- ? ('intake' as const)
350
- : rest[1] === 'auto-merge-default'
351
- ? ('auto-merge-default' as const)
352
- : undefined
353
- if (setting !== undefined && req.method === 'PUT' && rest.length === 2) {
354
- const request = await body(req, settingRequestSchema)
355
- return json(
356
- 200,
357
- await setRepositorySetting({
358
- store: opts.store,
359
- repo,
360
- user,
361
- setting,
362
- enabled: request.enabled,
363
- }),
364
- )
365
- }
366
- if (
367
- setting !== undefined &&
368
- req.method === 'POST' &&
369
- rest[2] === 'toggle' &&
370
- rest.length === 3
371
- ) {
372
- return json(200, await toggleRepositorySetting({ store: opts.store, repo, user, setting }))
373
- }
374
- }
375
-
376
- if (rest[0] === 'sessions') {
377
- if (rest.length === 1) {
378
- if (req.method === 'GET') {
379
- return json(200, await listOperatorSessions(opts.store, repo))
380
- }
381
- if (req.method === 'POST') {
382
- const request = await body(req, sessionCreateRequestSchema)
383
- return json(201, await createOperatorSession(opts.store, repo, user, request.title))
384
- }
385
- throw new HttpError(404, 'not-found', `no route: ${req.method} ${url.pathname}`)
386
- }
387
- const sid = rest[1]!
388
- if (rest.length === 2 && req.method === 'GET') {
389
- return json(200, await getOperatorSession(opts.store, repo, sid))
390
- }
391
- if (req.method === 'POST' && rest.length === 3 && rest[2] === 'messages') {
392
- const request = await body(req, sessionMessageRequestSchema)
393
- await postOperatorMessage(opts.store, repo, sid, user, request.text)
394
- return json(200, { ok: true })
395
- }
396
- if (req.method === 'PUT' && rest.length === 3 && rest[2] === 'wake') {
397
- const request = await body(req, sessionWakeRequestSchema)
398
- await setOperatorWake(opts.store, repo, sid, user, request.globs)
399
- return json(200, { ok: true })
400
- }
401
- if (req.method === 'POST' && rest.length === 3 && rest[2] === 'approvals') {
402
- const request = await body(req, sessionApprovalRequestSchema)
403
- await answerOperatorApproval(
404
- opts.store,
405
- repo,
406
- sid,
407
- user,
408
- request.turn,
409
- request.toolCallId,
410
- request.decision,
411
- )
412
- return json(200, { ok: true })
413
- }
414
- if (req.method === 'POST' && rest.length === 3 && rest[2] === 'archive') {
415
- await archiveOperatorSession(opts.store, repo, sid, user, opts.sandbox)
416
- return json(200, { ok: true })
417
- }
418
- if (
419
- req.method === 'GET' &&
420
- rest.length === 5 &&
421
- rest[2] === 'turns' &&
422
- rest[4] === 'stream'
423
- ) {
424
- const since = url.searchParams.get('since')
425
- const wait = url.searchParams.get('wait')
426
- let sinceSeq: number | undefined
427
- if (since !== null) {
428
- if (!/^[0-9]+$/.test(since)) {
429
- throw new HttpError(400, 'validation', 'since must be a nonnegative integer')
430
- }
431
- sinceSeq = Number(since)
432
- }
433
- let waitSeconds: number | undefined
434
- if (wait !== null) {
435
- if (!/^-?[0-9]+$/.test(wait)) {
436
- throw new HttpError(400, 'validation', 'wait must be an integer number of seconds')
437
- }
438
- waitSeconds = Number(wait)
439
- }
440
- return json(
441
- 200,
442
- await readOperatorTurnStream(opts.store, repo, sid, rest[3]!, {
443
- ...(sinceSeq !== undefined ? { since: sinceSeq } : {}),
444
- ...(waitSeconds !== undefined ? { waitSeconds } : {}),
445
- }),
446
- )
447
- }
448
- }
449
-
450
- if (rest[0] === 'builds' && rest[1]) {
451
- const slug = rest[1]
452
- if (req.method === 'GET' && rest.length === 2) {
453
- return json(200, await getOperatorBuild({ store: opts.store, repo, slug, now: clock() }))
454
- }
455
- if (req.method === 'GET' && rest[2] === 'artifacts' && rest[3] && rest.length === 4) {
456
- const rawRev = url.searchParams.get('rev')
457
- let rev: number | undefined
458
- if (rawRev !== null) {
459
- if (!/^[0-9]+$/.test(rawRev)) {
460
- throw new HttpError(400, 'validation', 'rev must be a nonnegative integer')
461
- }
462
- rev = Number(rawRev)
463
- if (!Number.isSafeInteger(rev)) {
464
- throw new HttpError(400, 'validation', 'rev must be a nonnegative integer')
465
- }
466
- }
467
- const record = await opts.store.getBuild(slug)
468
- if (record === null || record.repo !== repo)
469
- throw new HttpError(404, 'not-found', `unknown build "${slug}"`)
470
- const artifact = await opts.store.getArtifact(slug, rest[3], rev)
471
- if (artifact === null)
472
- throw new HttpError(
473
- 404,
474
- 'not-found',
475
- `artifact ${rest[3]}${rev === undefined ? '' : `@${rev}`} not found`,
476
- )
477
- return new Response(Uint8Array.from(artifact.content), {
478
- status: 200,
479
- headers: {
480
- 'content-type': 'application/octet-stream',
481
- 'content-disposition': `attachment; filename="${encodeURIComponent(slug)}-${encodeURIComponent(rest[3])}-${artifact.meta.revision}"`,
482
- 'x-autobuild-artifact-kind': artifact.meta.kind,
483
- 'x-autobuild-artifact-revision': String(artifact.meta.revision),
484
- 'x-autobuild-artifact-blob-ref': artifact.meta.blobRef,
485
- },
486
- })
487
- }
488
- if (req.method === 'POST' && rest[2] === 'control' && rest.length === 3) {
489
- const request = await body(req, buildControlRequestSchema)
490
- await requireRouteBuild(repo, slug)
491
- if (request.action === 'pause' || request.action === 'cancel-pause') {
492
- const state = reduceBuild(await opts.store.getEvents(slug))
493
- const display = effectiveStatus(state)
494
- if (request.action === 'pause' && display === 'pausing') {
495
- throw new BuildControlError(
496
- 'inactive',
497
- `build "${slug}" cannot pause (status: pausing); pause is already pending`,
498
- )
499
- }
500
- if (request.action === 'cancel-pause' && display !== 'pausing') {
501
- throw new BuildControlError(
502
- 'inactive',
503
- `build "${slug}" cannot cancel pause (status: ${state.status}); cancel pause requires a pending pause`,
504
- )
505
- }
506
- }
507
- const action: BuildControlAction =
508
- request.action === 'pause' || request.action === 'cancel-pause'
509
- ? { kind: 'dashboard-pause' }
510
- : request.action === 'resume'
511
- ? { kind: 'dashboard-resume' }
512
- : { kind: request.action }
513
- return json(200, await controlBuild({ store: opts.store, repo, slug, user, action }))
514
- }
515
- if (req.method === 'POST' && rest[2] === 'answer' && rest.length === 3) {
516
- const request = await body(req, answerRequestSchema)
517
- await requireRouteBuild(repo, slug)
518
- const action: BuildControlAction =
519
- request.resolution === 'guidance'
520
- ? { kind: 'answer', text: request.text }
521
- : request.resolution === 'retry'
522
- ? { kind: 'answer' }
523
- : request.resolution === 'dismiss'
524
- ? { kind: 'answer', text: request.text, resolve: { kind: 'dismiss-finding' } }
525
- : request.resolution === 'review-round-ceiling'
526
- ? { kind: 'answer', text: request.text, reviewRoundCeiling: request.ceiling }
527
- : {
528
- kind: 'answer',
529
- text: request.text,
530
- ...(request.ceiling !== undefined
531
- ? { reviewRoundCeiling: request.ceiling }
532
- : {}),
533
- resolve: {
534
- kind: 'revise-spec',
535
- body:
536
- request.origin === 'body'
537
- ? {
538
- kind: 'supplied',
539
- origin: 'operator API body',
540
- read: async () => request.body,
541
- }
542
- : { kind: 'ticket' },
543
- },
544
- }
545
- return json(
546
- 200,
547
- await controlBuild({
548
- store: opts.store,
549
- repo,
550
- slug,
551
- user,
552
- action,
553
- ...(request.resolution === 'revise-spec' && request.origin === 'ticket'
554
- ? { readTicketBody: async () => request.body }
555
- : {}),
556
- }),
557
- )
558
- }
559
- }
560
- throw new HttpError(404, 'not-found', `no route: ${req.method} ${url.pathname}`)
561
- }
562
-
563
- return {
564
- async fetch(req: Request): Promise<Response> {
565
- try {
566
- return await route(req)
567
- } catch (error) {
568
- if (error instanceof HttpError) return failure(error.status, error.kind, error.message)
569
- if (error instanceof TicketOperationError) {
570
- return failure(
571
- error.code === 'not-found' ? 404 : 409,
572
- error.code === 'not-found' ? 'not-found' : 'refusal',
573
- error.message,
574
- { code: error.code },
575
- )
576
- }
577
- if (error instanceof OperatorSessionError) {
578
- return failure(
579
- error.code === 'not-found' ? 404 : error.code === 'forbidden' ? 403 : 409,
580
- error.code === 'not-found'
581
- ? 'not-found'
582
- : error.code === 'forbidden'
583
- ? 'auth'
584
- : 'refusal',
585
- error.message,
586
- )
587
- }
588
- if (error instanceof BuildControlError || error instanceof OperatorControlError) {
589
- return failure(409, 'refusal', error.message, { code: error.code })
590
- }
591
- if (error instanceof BulkWalkError) {
592
- return failure(409, 'refusal', error.message, {
593
- code: 'bulk-partial',
594
- progress: error.progress,
595
- })
596
- }
597
- if (error instanceof OperatorQueryError) {
598
- return failure(
599
- error.code === 'not-found' ? 404 : 409,
600
- error.code === 'not-found' ? 'not-found' : 'conflict',
601
- error.message,
602
- { code: error.code },
603
- )
604
- }
605
- if (opts.onInternalError !== undefined) {
606
- try {
607
- await opts.onInternalError(error, req)
608
- } catch {
609
- // Diagnostics must never replace the protocol response.
610
- }
611
- }
612
- return failure(500, 'internal', 'operator API is unavailable')
613
- }
614
- },
615
- }
616
- }