@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.
- package/README.md +1 -1
- package/SPEC.md +323 -26
- package/bin/ab-build-runner.ts +8 -2
- package/docs/architecture.md +166 -14
- package/docs/assets/headline-wide.png +0 -0
- package/docs/configuration.md +175 -28
- package/docs/hosted-dispatcher.md +69 -13
- package/docs/operator-api.md +2 -2
- package/docs/remote-store-protocol.md +204 -16
- package/docs/remote-ticket-protocol.md +1 -1
- package/docs/setup.md +12 -9
- package/docs/website.md +14 -0
- package/package.json +45 -23
- package/packages/core/src/cli/artifact.ts +130 -6
- package/packages/core/src/cli/binary.ts +9 -1
- package/packages/core/src/cli/build-control.ts +110 -13
- package/packages/core/src/cli/context.ts +46 -0
- package/packages/core/src/cli/dashboard/facts.ts +215 -0
- package/packages/core/src/cli/dashboard/frame-parse.ts +223 -0
- package/packages/core/src/cli/dashboard/model.ts +141 -153
- package/packages/core/src/cli/dashboard/poll.ts +76 -12
- package/packages/core/src/cli/dashboard/render.ts +34 -5
- package/packages/core/src/cli/dispatch-frontend.ts +54 -5
- package/packages/core/src/cli/dispatch-process.ts +1 -13
- package/packages/core/src/cli/dispatch.ts +392 -75
- package/packages/core/src/cli/harvest.ts +122 -8
- package/packages/core/src/cli/help.ts +62 -9
- package/packages/core/src/cli/init-readiness-remote.ts +110 -0
- package/packages/core/src/cli/init-readiness-shared.ts +72 -0
- package/packages/core/src/cli/init-validation.ts +266 -222
- package/packages/core/src/cli/installation.ts +50 -8
- package/packages/core/src/cli/main.ts +70 -11
- package/packages/core/src/cli/mcp.ts +23 -1
- package/packages/core/src/cli/plugin.ts +7 -4
- package/packages/core/src/cli/remote-poll.ts +277 -0
- package/packages/core/src/cli/repo-state.ts +22 -0
- package/packages/core/src/cli/repository-status.ts +39 -1
- package/packages/core/src/cli/self-update.ts +77 -2
- package/packages/core/src/cli/status.ts +150 -28
- package/packages/core/src/cli/terminals.ts +2 -1
- package/packages/core/src/cli/ticket-assets.ts +247 -0
- package/packages/core/src/cli/ticket.ts +453 -8
- package/packages/core/src/cli/wait.ts +157 -41
- package/packages/core/src/cli/watch.ts +138 -168
- package/packages/core/src/config/live.ts +23 -0
- package/packages/core/src/config/pipeline-source.ts +388 -0
- package/packages/core/src/config/schema.ts +251 -19
- package/packages/core/src/events/catalog.ts +1 -0
- package/packages/core/src/events/globs.ts +47 -0
- package/packages/core/src/events/payloads.ts +52 -2
- package/packages/core/src/events/repository.ts +52 -1
- package/packages/core/src/events/sessions.ts +69 -10
- package/packages/core/src/events/workspace-remote.ts +12 -0
- package/packages/core/src/integration/harness.ts +115 -2
- package/packages/core/src/kernel/__snapshots__/reducer-registry.test.ts.snap +1369 -0
- package/packages/core/src/kernel/auto-merge-default.ts +157 -0
- package/packages/core/src/kernel/auto-merge.ts +99 -22
- package/packages/core/src/kernel/dispatch-settings.ts +42 -17
- package/packages/core/src/kernel/dispatch-status.ts +23 -0
- package/packages/core/src/kernel/engine.ts +9 -438
- package/packages/core/src/kernel/generators/build-log.ts +259 -0
- package/packages/core/src/kernel/generators/repository-journals.ts +206 -0
- package/packages/core/src/kernel/generators/session-log.ts +72 -0
- package/packages/core/src/kernel/harvest.ts +62 -18
- package/packages/core/src/kernel/incremental-contract.ts +75 -0
- package/packages/core/src/kernel/incremental-fixtures.ts +211 -0
- package/packages/core/src/kernel/incremental.ts +53 -0
- package/packages/core/src/kernel/log-index.ts +620 -0
- package/packages/core/src/kernel/phases.ts +16 -2
- package/packages/core/src/kernel/pr-attachments.ts +164 -46
- package/packages/core/src/kernel/reducer-registry.ts +164 -0
- package/packages/core/src/kernel/reducer.ts +254 -73
- package/packages/core/src/operator/annotations.ts +6 -0
- package/packages/core/src/operator/control.ts +19 -5
- package/packages/core/src/operator/index.ts +71 -6
- package/packages/core/src/operator/query.ts +253 -24
- package/packages/core/src/operator/read-view.ts +98 -0
- package/packages/core/src/operator/registry.ts +55 -10
- package/packages/core/src/operator/requests.ts +9 -6
- package/packages/core/src/operator/sandbox-host.ts +140 -0
- package/packages/core/src/operator/sandbox.ts +247 -14
- package/packages/core/src/operator/ticket-source-backend.ts +47 -0
- package/packages/core/src/orchestrator/approvals.ts +115 -0
- package/packages/core/src/orchestrator/conversation.ts +167 -0
- package/packages/core/src/orchestrator/model.ts +86 -0
- package/packages/core/src/orchestrator/prompt.ts +61 -0
- package/packages/core/src/orchestrator/tools.ts +117 -0
- package/packages/core/src/orchestrator/turn-runner.ts +457 -0
- package/packages/core/src/plugin-sdk/index.ts +51 -1
- package/packages/core/src/plugins/load.ts +135 -7
- package/packages/core/src/plugins/manifest.ts +183 -10
- package/packages/core/src/plugins/registry.ts +73 -4
- package/packages/core/src/ports/forge/create.ts +1 -1
- package/packages/core/src/ports/forge/fake.ts +31 -0
- package/packages/core/src/ports/forge/github-transport.ts +13 -2
- package/packages/core/src/ports/forge/github.ts +13 -2
- package/packages/core/src/ports/forge/local-git.ts +31 -1
- package/packages/core/src/ports/runner/claude.ts +2 -2
- package/packages/core/src/ports/runner/codex.ts +6 -2
- package/packages/core/src/ports/runner/fake.ts +7 -1
- package/packages/core/src/ports/runner/pi.ts +8 -6
- package/packages/core/src/ports/runner/provider-error.ts +13 -3
- package/packages/core/src/ports/tickets/fake.ts +5 -1
- package/packages/core/src/ports/tickets/file.ts +169 -48
- package/packages/core/src/ports/tickets/{remote-index.ts → hosted-index.ts} +3 -2
- package/packages/core/src/ports/tickets/linear.ts +33 -1
- package/packages/core/src/ports/tickets/operations.ts +1 -1
- package/packages/core/src/ports/tickets/remote.ts +8 -1
- package/packages/core/src/ports/types.ts +49 -3
- package/packages/core/src/ports/workspace/builtin-capabilities.ts +114 -0
- package/packages/core/src/ports/workspace/create.ts +74 -39
- package/packages/core/src/ports/workspace/distribution-archive.ts +171 -12
- package/packages/core/src/ports/workspace/fake.ts +169 -4
- package/packages/core/src/ports/workspace/git-worktree.ts +246 -26
- package/packages/core/src/ports/workspace/github-origin.ts +36 -0
- package/packages/core/src/ports/workspace/operator-sandbox.ts +7 -1
- package/packages/core/src/ports/workspace/provider-capabilities.ts +171 -0
- package/packages/core/src/ports/workspace/vercel-capabilities.ts +110 -0
- package/packages/core/src/ports/workspace/vercel-sandbox.ts +142 -114
- package/packages/core/src/processes/build-child.ts +17 -2
- package/packages/core/src/processes/build-execution-state.ts +68 -15
- package/packages/core/src/processes/build-runner.ts +238 -93
- package/packages/core/src/processes/dispatcher-selectors.ts +123 -0
- package/packages/core/src/processes/dispatcher.ts +501 -201
- package/packages/core/src/processes/execution-settlement.ts +72 -39
- package/packages/core/src/processes/harvest-child.ts +5 -0
- package/packages/core/src/processes/harvest-execution-state.ts +41 -29
- package/packages/core/src/processes/harvest-runner.ts +66 -19
- package/packages/core/src/processes/harvest.ts +196 -24
- package/packages/core/src/processes/infrastructure-failure-budget.ts +31 -18
- package/packages/core/src/processes/orchestrator-tick.ts +360 -0
- package/packages/core/src/processes/publication-state.ts +164 -52
- package/packages/core/src/processes/repo-view.ts +1436 -0
- package/packages/core/src/processes/sandbox-state.ts +36 -2
- package/packages/core/src/processes/tick-state.ts +33 -0
- package/packages/core/src/store/adapter.ts +41 -0
- package/packages/core/src/store/build-scope.ts +90 -0
- package/packages/core/src/store/contract.ts +1205 -0
- package/packages/core/src/store/digest.ts +102 -0
- package/packages/core/src/store/local/schema.ts +46 -2
- package/packages/core/src/store/local/store.ts +445 -4
- package/packages/core/src/store/memory.ts +246 -0
- package/packages/core/src/store/new-build.ts +17 -0
- package/packages/core/src/store/phase-session.ts +94 -0
- package/packages/core/src/store/remote/client.ts +310 -2
- package/packages/core/src/store/remote/index.ts +14 -2
- package/packages/core/src/store/remote/protocol.ts +115 -12
- package/packages/core/src/store/remote/version.ts +6 -2
- package/packages/core/src/store/repo-state-events.ts +205 -0
- package/packages/core/src/store/retention.ts +7 -0
- package/packages/core/src/store/session-handle.ts +94 -0
- package/packages/core/src/{sessions/reducer.ts → store/session-reducer.ts} +79 -19
- package/packages/core/src/store/snapshots.ts +88 -0
- package/packages/core/src/store/streams/session-writer.ts +42 -2
- package/packages/core/src/store/streams/wait.ts +4 -5
- package/packages/core/src/store/ticket-assets.ts +458 -0
- package/packages/core/src/store/types.ts +160 -1
- package/packages/core/src/testing/index.ts +59 -0
- package/skills/code-review/SKILL.md +5 -1
- package/skills/guide/SKILL.md +152 -40
- package/skills/guide/references/operator-api.md +2 -2
- package/skills/guide/references/plugin-authoring.md +17 -0
- package/skills/guide/references/remote-store-protocol.md +204 -16
- package/skills/guide/references/remote-ticket-protocol.md +1 -1
- package/skills/guide/references/setup.md +12 -9
- package/skills/implement/SKILL.md +3 -0
- package/skills/operate/SKILL.md +126 -0
- package/skills/plan/SKILL.md +4 -0
- package/skills/plan-review/SKILL.md +5 -1
- package/skills/spec/SKILL.md +46 -3
- package/skills/tickets/SKILL.md +57 -5
- package/packages/core/src/cli/dashboard/frame-image.ts +0 -373
- package/packages/core/src/cli/store-opening.contract.ts +0 -289
- package/packages/core/src/cli/testkit.ts +0 -194
- package/packages/core/src/markdown.ts +0 -67
- package/packages/core/src/operator/client.ts +0 -311
- package/packages/core/src/operator/server.ts +0 -616
- package/packages/core/src/operator/sessions.ts +0 -223
- package/packages/core/src/ports/runner/live-contract-fixture.ts +0 -176
- package/packages/core/src/ports/tickets/remote-server.ts +0 -185
- package/packages/core/src/store/remote/server.ts +0 -923
- 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
|
-
}
|