@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
|
@@ -32,9 +32,22 @@ import type {
|
|
|
32
32
|
SessionEventType,
|
|
33
33
|
SessionEventWrite,
|
|
34
34
|
} from '../../events/sessions'
|
|
35
|
+
import {
|
|
36
|
+
DEFAULT_TICKET_ASSET_LIMITS,
|
|
37
|
+
TicketAssetValidationError,
|
|
38
|
+
validateTicketAssetInput,
|
|
39
|
+
type TicketAsset,
|
|
40
|
+
type TicketAssetContentEntry,
|
|
41
|
+
type TicketAssetInput,
|
|
42
|
+
type TicketAssetLimits,
|
|
43
|
+
type TicketAssetMeta,
|
|
44
|
+
type TicketAssetSummary,
|
|
45
|
+
} from '../ticket-assets'
|
|
35
46
|
import { createBuildScopedStore } from '../build-scope'
|
|
36
47
|
import { createSessionScopedStore } from '../session-handle'
|
|
37
48
|
import { pollingSubscribe } from '../subscribe'
|
|
49
|
+
import type { ReducerSnapshot, SnapshotScope } from '../snapshots'
|
|
50
|
+
import type { BuildDigest } from '../types'
|
|
38
51
|
import type {
|
|
39
52
|
StreamChunk,
|
|
40
53
|
StreamOutcome,
|
|
@@ -68,9 +81,11 @@ import {
|
|
|
68
81
|
artifactGetResponseSchema,
|
|
69
82
|
artifactMetaListSchema,
|
|
70
83
|
artifactMetaWireSchema,
|
|
84
|
+
buildDigestListSchema,
|
|
71
85
|
buildRecordListSchema,
|
|
72
86
|
buildRecordWireSchema,
|
|
73
87
|
conditionalEventResponseSchema,
|
|
88
|
+
conditionalSessionEventResponseSchema,
|
|
74
89
|
decodeBase64,
|
|
75
90
|
depositsResponseSchema,
|
|
76
91
|
encodeBase64,
|
|
@@ -78,8 +93,11 @@ import {
|
|
|
78
93
|
eventEnvelopeWireSchema,
|
|
79
94
|
eventListSchema,
|
|
80
95
|
repositoryEventEnvelopeWireSchema,
|
|
96
|
+
repoHighWaterSchema,
|
|
81
97
|
repositoryEventListSchema,
|
|
82
98
|
okResponseSchema,
|
|
99
|
+
reducerSnapshotPutResponseSchema,
|
|
100
|
+
reducerSnapshotResponseSchema,
|
|
83
101
|
placeholderRev,
|
|
84
102
|
repoDepositsResponseSchema,
|
|
85
103
|
repositoryArtifactGetResponseSchema,
|
|
@@ -96,6 +114,12 @@ import {
|
|
|
96
114
|
sessionRecordWireSchema,
|
|
97
115
|
streamChunkWireSchema,
|
|
98
116
|
streamReadWireSchema,
|
|
117
|
+
ticketAssetFileResponseSchema,
|
|
118
|
+
ticketAssetLimitsWireSchema,
|
|
119
|
+
ticketAssetManifestResponseSchema,
|
|
120
|
+
ticketAssetMetaWireSchema,
|
|
121
|
+
ticketAssetSummaryListSchema,
|
|
122
|
+
removeTicketAssetResponseSchema,
|
|
99
123
|
streamRecordListSchema,
|
|
100
124
|
streamRecordWireSchema,
|
|
101
125
|
} from './protocol'
|
|
@@ -173,7 +197,7 @@ export class RemoteBuildStore implements BuildStore {
|
|
|
173
197
|
}
|
|
174
198
|
|
|
175
199
|
private async raw(
|
|
176
|
-
method: 'GET' | 'POST',
|
|
200
|
+
method: 'GET' | 'POST' | 'PUT',
|
|
177
201
|
path: string,
|
|
178
202
|
body?: unknown,
|
|
179
203
|
signal?: AbortSignal,
|
|
@@ -187,7 +211,11 @@ export class RemoteBuildStore implements BuildStore {
|
|
|
187
211
|
return this.fetchFn(`${this.base}${path}`, {
|
|
188
212
|
method,
|
|
189
213
|
headers,
|
|
190
|
-
|
|
214
|
+
// A string body is already serialized JSON (the ticket-asset put sizes
|
|
215
|
+
// its exact bytes before sending).
|
|
216
|
+
...(body !== undefined
|
|
217
|
+
? { body: typeof body === 'string' ? body : JSON.stringify(body) }
|
|
218
|
+
: {}),
|
|
191
219
|
...(signal !== undefined ? { signal } : {}),
|
|
192
220
|
})
|
|
193
221
|
}
|
|
@@ -205,6 +233,12 @@ export class RemoteBuildStore implements BuildStore {
|
|
|
205
233
|
}
|
|
206
234
|
// D6: validation feedback crosses the wire as the same error type with
|
|
207
235
|
// the server's message intact.
|
|
236
|
+
if (
|
|
237
|
+
(response.status === 422 || response.status === 413) &&
|
|
238
|
+
message.startsWith('ticket asset')
|
|
239
|
+
) {
|
|
240
|
+
return new TicketAssetValidationError(message)
|
|
241
|
+
}
|
|
208
242
|
if (response.status === 422) return new EventValidationError(message)
|
|
209
243
|
// Typed stream rejections rehydrate by the server's message shape: the
|
|
210
244
|
// ceiling names its bytes, the closed-append names its stream (a 409 is
|
|
@@ -246,6 +280,19 @@ export class RemoteBuildStore implements BuildStore {
|
|
|
246
280
|
return this.requestJson('GET', '/builds', buildRecordListSchema)
|
|
247
281
|
}
|
|
248
282
|
|
|
283
|
+
async getRepoBuildDigests(repo: string): Promise<Map<string, BuildDigest>> {
|
|
284
|
+
// A repo-scoped batch read (AUT-487): one request regardless of build
|
|
285
|
+
// count. The route answers from build records, so an unknown repo or a
|
|
286
|
+
// repo without a journal record answers 200 with an empty array rather
|
|
287
|
+
// than 404 — the server composes it before the repository-existence gate.
|
|
288
|
+
const digests = await this.requestJson(
|
|
289
|
+
'GET',
|
|
290
|
+
`${this.repoPath(repo)}/build-digests`,
|
|
291
|
+
buildDigestListSchema,
|
|
292
|
+
)
|
|
293
|
+
return new Map(digests.map((digest) => [digest.slug, digest]))
|
|
294
|
+
}
|
|
295
|
+
|
|
249
296
|
async append<T extends EventType>(slug: string, event: EventWrite<T>): Promise<EventEnvelope<T>> {
|
|
250
297
|
const envelope = await this.requestJson(
|
|
251
298
|
'POST',
|
|
@@ -492,6 +539,64 @@ export class RemoteBuildStore implements BuildStore {
|
|
|
492
539
|
)
|
|
493
540
|
}
|
|
494
541
|
|
|
542
|
+
private snapshotPath(scope: SnapshotScope, reducer: string): string {
|
|
543
|
+
const root = scope.kind === 'build' ? this.buildPath(scope.slug) : this.repoPath(scope.repo)
|
|
544
|
+
return `${root}/snapshots/${encodeURIComponent(reducer)}`
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
async getReducerSnapshot(
|
|
548
|
+
scope: SnapshotScope,
|
|
549
|
+
reducer: string,
|
|
550
|
+
version: number,
|
|
551
|
+
): Promise<ReducerSnapshot | null> {
|
|
552
|
+
// Additive routes, no protocol bump. A store that predates them answers
|
|
553
|
+
// 404 or 405 for the route, which reads as "no snapshot": the caller
|
|
554
|
+
// replays the log. Auth and transport failures still surface.
|
|
555
|
+
const response = await this.raw(
|
|
556
|
+
'GET',
|
|
557
|
+
`${this.snapshotPath(scope, reducer)}?version=${version}`,
|
|
558
|
+
)
|
|
559
|
+
if (response.status === 404 || response.status === 405) return null
|
|
560
|
+
if (!response.ok) throw await this.toError(response)
|
|
561
|
+
return reducerSnapshotResponseSchema.parse(await response.json())
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
async putReducerSnapshot(
|
|
565
|
+
scope: SnapshotScope,
|
|
566
|
+
reducer: string,
|
|
567
|
+
snapshot: ReducerSnapshot,
|
|
568
|
+
): Promise<boolean> {
|
|
569
|
+
const response = await this.raw('PUT', this.snapshotPath(scope, reducer), {
|
|
570
|
+
version: snapshot.version,
|
|
571
|
+
cursor: snapshot.cursor,
|
|
572
|
+
state: snapshot.state ?? null,
|
|
573
|
+
})
|
|
574
|
+
if (response.status === 404 || response.status === 405) return false
|
|
575
|
+
if (!response.ok) throw await this.toError(response)
|
|
576
|
+
return reducerSnapshotPutResponseSchema.parse(await response.json()).written
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
async getRepoHighWater(repo: string): Promise<number> {
|
|
580
|
+
// Additive route beside `state-events`; the store service and client
|
|
581
|
+
// ship in version lockstep, so no protocol bump.
|
|
582
|
+
return this.requestJson('GET', `${this.repoPath(repo)}/high-water`, repoHighWaterSchema).then(
|
|
583
|
+
(body) => (body as { seq: number }).seq,
|
|
584
|
+
)
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
async getRepoStateEvents(repo: string): Promise<RepositoryEvent[]> {
|
|
588
|
+
// The bounded repository-journal read (AUT-489): one additive route next
|
|
589
|
+
// to `GET events`, reusing the same wire shape. No protocol-version bump
|
|
590
|
+
// and no change to existing routes — a client on the previous release
|
|
591
|
+
// never calls this route, so behavioral identity of the old surface is
|
|
592
|
+
// the whole cross-version contract.
|
|
593
|
+
return this.requestJson(
|
|
594
|
+
'GET',
|
|
595
|
+
`${this.repoPath(repo)}/state-events`,
|
|
596
|
+
repositoryEventListSchema,
|
|
597
|
+
) as Promise<RepositoryEvent[]>
|
|
598
|
+
}
|
|
599
|
+
|
|
495
600
|
async putRepoArtifact(repo: string, artifact: ArtifactInput): Promise<RepositoryArtifactMeta> {
|
|
496
601
|
return this.requestJson(
|
|
497
602
|
'POST',
|
|
@@ -531,6 +636,192 @@ export class RemoteBuildStore implements BuildStore {
|
|
|
531
636
|
)
|
|
532
637
|
}
|
|
533
638
|
|
|
639
|
+
/** The limits this store handle enforces locally, fetched once per repo
|
|
640
|
+
* and cached. A server that predates the route (404) gets the defaults. */
|
|
641
|
+
private readonly assetLimits = new Map<
|
|
642
|
+
string,
|
|
643
|
+
Promise<TicketAssetLimits & { maxRequestBytes?: number }>
|
|
644
|
+
>()
|
|
645
|
+
|
|
646
|
+
private assetLimitsFor(repo: string): Promise<TicketAssetLimits & { maxRequestBytes?: number }> {
|
|
647
|
+
let cached = this.assetLimits.get(repo)
|
|
648
|
+
if (cached === undefined) {
|
|
649
|
+
cached = (async () => {
|
|
650
|
+
const response = await this.raw('GET', `${this.repoPath(repo)}/ticket-asset-limits`)
|
|
651
|
+
if (response.status === 404) return { ...DEFAULT_TICKET_ASSET_LIMITS }
|
|
652
|
+
if (!response.ok) throw await this.toError(response)
|
|
653
|
+
return ticketAssetLimitsWireSchema.parse(await response.json())
|
|
654
|
+
})()
|
|
655
|
+
cached.catch(() => this.assetLimits.delete(repo))
|
|
656
|
+
this.assetLimits.set(repo, cached)
|
|
657
|
+
}
|
|
658
|
+
return cached
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
async ticketAssetLimits(repo: string): Promise<TicketAssetLimits> {
|
|
662
|
+
const { maxBytes, maxEntries } = await this.assetLimitsFor(repo)
|
|
663
|
+
return { maxBytes, maxEntries }
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
private assetPath(repo: string, ticketId: string): string {
|
|
667
|
+
return `${this.repoPath(repo)}/tickets/${encodeURIComponent(ticketId)}/assets`
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
async putTicketAsset(
|
|
671
|
+
repo: string,
|
|
672
|
+
ticketId: string,
|
|
673
|
+
asset: TicketAssetInput,
|
|
674
|
+
): Promise<TicketAssetMeta> {
|
|
675
|
+
const limits = await this.assetLimitsFor(repo)
|
|
676
|
+
try {
|
|
677
|
+
validateTicketAssetInput(asset, limits)
|
|
678
|
+
} catch (error) {
|
|
679
|
+
if (
|
|
680
|
+
error instanceof TicketAssetValidationError &&
|
|
681
|
+
limits.maxRequestBytes !== undefined &&
|
|
682
|
+
/^ticket asset exceeds the \d+-byte/.test(error.message)
|
|
683
|
+
) {
|
|
684
|
+
throw new TicketAssetValidationError(
|
|
685
|
+
`ticket asset exceeds this store's ${limits.maxBytes}-byte limit (deployment request ceiling ${limits.maxRequestBytes} bytes)`,
|
|
686
|
+
)
|
|
687
|
+
}
|
|
688
|
+
throw error
|
|
689
|
+
}
|
|
690
|
+
const text = JSON.stringify({
|
|
691
|
+
kind: asset.kind,
|
|
692
|
+
name: asset.name,
|
|
693
|
+
layout: asset.layout,
|
|
694
|
+
entries: asset.entries.map((entry) =>
|
|
695
|
+
entry.type === 'dir'
|
|
696
|
+
? { type: 'dir', path: entry.path }
|
|
697
|
+
: { type: 'file', path: entry.path, contentBase64: encodeBase64(entry.content) },
|
|
698
|
+
),
|
|
699
|
+
})
|
|
700
|
+
if (limits.maxRequestBytes !== undefined) {
|
|
701
|
+
const bytes = new TextEncoder().encode(text).length
|
|
702
|
+
if (bytes > limits.maxRequestBytes) {
|
|
703
|
+
throw new TicketAssetValidationError(
|
|
704
|
+
`ticket asset exceeds this store's ${limits.maxBytes}-byte limit (deployment request ceiling ${limits.maxRequestBytes} bytes): the encoded request is ${bytes} bytes`,
|
|
705
|
+
)
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
return this.requestJson('POST', this.assetPath(repo, ticketId), ticketAssetMetaWireSchema, text)
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
async getTicketAsset(
|
|
712
|
+
repo: string,
|
|
713
|
+
ticketId: string,
|
|
714
|
+
kind: string,
|
|
715
|
+
name: string,
|
|
716
|
+
rev?: number,
|
|
717
|
+
): Promise<TicketAsset | null> {
|
|
718
|
+
const params = new URLSearchParams({ kind, name })
|
|
719
|
+
if (rev !== undefined) params.set('rev', String(rev))
|
|
720
|
+
const meta = await this.requestJson(
|
|
721
|
+
'GET',
|
|
722
|
+
`${this.assetPath(repo, ticketId)}?${params}`,
|
|
723
|
+
ticketAssetManifestResponseSchema,
|
|
724
|
+
)
|
|
725
|
+
if (meta === null) return null
|
|
726
|
+
// One request per file, pinned to the manifest's revision, so no response
|
|
727
|
+
// carries more than a single file.
|
|
728
|
+
const entries: TicketAssetContentEntry[] = []
|
|
729
|
+
for (const entry of meta.entries) {
|
|
730
|
+
if (entry.type === 'dir') {
|
|
731
|
+
entries.push({ type: 'dir', path: entry.path })
|
|
732
|
+
continue
|
|
733
|
+
}
|
|
734
|
+
const fileParams = new URLSearchParams({
|
|
735
|
+
kind,
|
|
736
|
+
name,
|
|
737
|
+
rev: String(meta.revision),
|
|
738
|
+
path: entry.path,
|
|
739
|
+
})
|
|
740
|
+
const file = await this.requestJson(
|
|
741
|
+
'GET',
|
|
742
|
+
`${this.assetPath(repo, ticketId)}/file?${fileParams}`,
|
|
743
|
+
ticketAssetFileResponseSchema,
|
|
744
|
+
)
|
|
745
|
+
if (file === null) {
|
|
746
|
+
throw new Error(
|
|
747
|
+
`ticket asset ${kind}/${name} revision ${meta.revision} vanished mid-download`,
|
|
748
|
+
)
|
|
749
|
+
}
|
|
750
|
+
entries.push({ type: 'file', path: entry.path, content: decodeBase64(file.contentBase64) })
|
|
751
|
+
}
|
|
752
|
+
return { meta, entries }
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
async getPinnedTicketAsset(
|
|
756
|
+
slug: string,
|
|
757
|
+
kind: string,
|
|
758
|
+
name: string,
|
|
759
|
+
rev?: number,
|
|
760
|
+
): Promise<TicketAsset | null> {
|
|
761
|
+
const params = new URLSearchParams({ kind, name })
|
|
762
|
+
if (rev !== undefined) params.set('rev', String(rev))
|
|
763
|
+
const meta = await this.requestJson(
|
|
764
|
+
'GET',
|
|
765
|
+
`${this.buildPath(slug)}/ticket-assets?${params}`,
|
|
766
|
+
ticketAssetManifestResponseSchema,
|
|
767
|
+
)
|
|
768
|
+
if (meta === null) return null
|
|
769
|
+
// One request per file, pinned to the manifest's revision, so no response
|
|
770
|
+
// carries more than a single file.
|
|
771
|
+
const entries: TicketAssetContentEntry[] = []
|
|
772
|
+
for (const entry of meta.entries) {
|
|
773
|
+
if (entry.type === 'dir') {
|
|
774
|
+
entries.push({ type: 'dir', path: entry.path })
|
|
775
|
+
continue
|
|
776
|
+
}
|
|
777
|
+
const fileParams = new URLSearchParams({
|
|
778
|
+
kind,
|
|
779
|
+
name,
|
|
780
|
+
rev: String(meta.revision),
|
|
781
|
+
path: entry.path,
|
|
782
|
+
})
|
|
783
|
+
const file = await this.requestJson(
|
|
784
|
+
'GET',
|
|
785
|
+
`${this.buildPath(slug)}/ticket-assets/file?${fileParams}`,
|
|
786
|
+
ticketAssetFileResponseSchema,
|
|
787
|
+
)
|
|
788
|
+
if (file === null) {
|
|
789
|
+
throw new Error(
|
|
790
|
+
`ticket asset ${kind}/${name} revision ${meta.revision} vanished mid-download`,
|
|
791
|
+
)
|
|
792
|
+
}
|
|
793
|
+
entries.push({ type: 'file', path: entry.path, content: decodeBase64(file.contentBase64) })
|
|
794
|
+
}
|
|
795
|
+
return { meta, entries }
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
async listTicketAssets(
|
|
799
|
+
repo: string,
|
|
800
|
+
ticketId: string,
|
|
801
|
+
opts?: { revisions?: boolean },
|
|
802
|
+
): Promise<TicketAssetSummary[]> {
|
|
803
|
+
const query = opts?.revisions ? '?revisions=1' : ''
|
|
804
|
+
return this.requestJson(
|
|
805
|
+
'GET',
|
|
806
|
+
`${this.assetPath(repo, ticketId)}/list${query}`,
|
|
807
|
+
ticketAssetSummaryListSchema,
|
|
808
|
+
)
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
async removeTicketAsset(
|
|
812
|
+
repo: string,
|
|
813
|
+
ticketId: string,
|
|
814
|
+
kind: string,
|
|
815
|
+
name: string,
|
|
816
|
+
): Promise<TicketAssetMeta | null> {
|
|
817
|
+
return this.requestJson(
|
|
818
|
+
'POST',
|
|
819
|
+
`${this.assetPath(repo, ticketId)}/remove`,
|
|
820
|
+
removeTicketAssetResponseSchema,
|
|
821
|
+
{ kind, name },
|
|
822
|
+
)
|
|
823
|
+
}
|
|
824
|
+
|
|
534
825
|
async claimRepoLease(repo: string, holder: string, ttlMs: number): Promise<boolean> {
|
|
535
826
|
const result = await this.requestJson(
|
|
536
827
|
'POST',
|
|
@@ -595,6 +886,23 @@ export class RemoteBuildStore implements BuildStore {
|
|
|
595
886
|
return envelope as unknown as SessionEventEnvelope<T>
|
|
596
887
|
}
|
|
597
888
|
|
|
889
|
+
async appendSessionEventIfCurrent<T extends SessionEventType>(
|
|
890
|
+
id: string,
|
|
891
|
+
expectedSeq: number,
|
|
892
|
+
event: SessionEventWrite<T>,
|
|
893
|
+
): Promise<SessionEventEnvelope<T> | null> {
|
|
894
|
+
const envelope = await this.requestJson(
|
|
895
|
+
'POST',
|
|
896
|
+
`${this.sessionPath(id)}/events/conditional`,
|
|
897
|
+
conditionalSessionEventResponseSchema,
|
|
898
|
+
{
|
|
899
|
+
expectedSeq,
|
|
900
|
+
event: { actor: event.actor, type: event.type, payload: event.payload },
|
|
901
|
+
},
|
|
902
|
+
)
|
|
903
|
+
return envelope as SessionEventEnvelope<T> | null
|
|
904
|
+
}
|
|
905
|
+
|
|
598
906
|
async getSessionEvents(
|
|
599
907
|
id: string,
|
|
600
908
|
sinceSeq = 0,
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
// Client half + shared wire vocabulary for the remote BuildStore (SPEC §7.2).
|
|
2
|
+
// The HTTP server half ships in `@defrex/autobuild-hosted-store-service`;
|
|
3
|
+
// this barrel is what a local CLI and the hosted package both build against.
|
|
1
4
|
export { AuthError, RemoteBuildStore } from './client'
|
|
2
5
|
export type { RemoteBuildStoreOptions, RemoteStoreIdentity } from './client'
|
|
3
|
-
export { createStoreServer, startStoreServer } from './server'
|
|
4
|
-
export type { StartStoreServerOptions, StoreServer, StoreServerOptions } from './server'
|
|
5
6
|
export { mintToken, tokenResource, verifyToken } from './token'
|
|
6
7
|
export type { OperatorTokenScope, TokenScope } from './token'
|
|
7
8
|
export {
|
|
@@ -10,3 +11,14 @@ export {
|
|
|
10
11
|
REMOTE_STORE_PROTOCOL_VERSION,
|
|
11
12
|
REMOTE_STORE_PROTOCOL_VERSION_HEADER,
|
|
12
13
|
} from './version'
|
|
14
|
+
// The whole wire protocol: schemas, error vocabulary, and the placeholder-ref
|
|
15
|
+
// deposit convention. The protocol servers (hosted package) compose these;
|
|
16
|
+
// clients parse them for feedback.
|
|
17
|
+
export * from './protocol'
|
|
18
|
+
// Event vocabulary the wire envelopes and the server's D6 mapping rely on.
|
|
19
|
+
export { EventValidationError } from '../../events/catalog'
|
|
20
|
+
export type { Via } from '../../events/envelope'
|
|
21
|
+
export type { SessionEvent, SessionEventWrite } from '../../events/sessions'
|
|
22
|
+
export type { RepositoryEvent, RepositoryEventWrite } from '../../events/repository'
|
|
23
|
+
export { SNAPSHOT_REDUCER_NAME } from '../snapshots'
|
|
24
|
+
export type { ReducerSnapshot, SnapshotScope } from '../snapshots'
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
* Arbitrary computation over the assigned revisions inside `makeEvent`
|
|
25
25
|
* (e.g. `rev + 1`, string interpolation) is unsupported by design.
|
|
26
26
|
*/
|
|
27
|
+
import { reducerSnapshotWireSchema } from '../snapshots'
|
|
27
28
|
import { z } from 'zod'
|
|
28
29
|
import { actorSchema } from '../../events/envelope'
|
|
29
30
|
import { ticketRefSchema } from '../../ontology'
|
|
@@ -40,6 +41,18 @@ export const errorBodySchema = z.object({
|
|
|
40
41
|
})
|
|
41
42
|
export type ErrorBody = z.infer<typeof errorBodySchema>
|
|
42
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Deliberately loose: the ontology is enforced by `validateEventWrite` in
|
|
46
|
+
* the *backing store* (§8), so its precise EventValidationError message —
|
|
47
|
+
* the agent feedback D6 exists for — survives the wire instead of being
|
|
48
|
+
* shadowed by a duplicate wire-schema error.
|
|
49
|
+
*/
|
|
50
|
+
export const eventWriteWireSchema = z.object({
|
|
51
|
+
actor: z.unknown(),
|
|
52
|
+
type: z.string().min(1),
|
|
53
|
+
payload: z.unknown(),
|
|
54
|
+
})
|
|
55
|
+
|
|
43
56
|
// ── Builds ───────────────────────────────────────────────────────────────────
|
|
44
57
|
|
|
45
58
|
export const newBuildBodySchema = z.object({
|
|
@@ -51,6 +64,9 @@ export const newBuildBodySchema = z.object({
|
|
|
51
64
|
repoOrigin: z.string().optional(),
|
|
52
65
|
ticket: ticketRefSchema.optional(),
|
|
53
66
|
branch: z.string().min(1).optional(),
|
|
67
|
+
// Additive optional initial `build.created` event, appended atomically with
|
|
68
|
+
// the record. Loose like every event wire schema: the backing store validates.
|
|
69
|
+
created: eventWriteWireSchema.optional(),
|
|
54
70
|
})
|
|
55
71
|
|
|
56
72
|
export const buildRecordWireSchema = z.object({
|
|
@@ -68,18 +84,6 @@ export const buildRecordListSchema = z.array(buildRecordWireSchema)
|
|
|
68
84
|
|
|
69
85
|
// ── Events ───────────────────────────────────────────────────────────────────
|
|
70
86
|
|
|
71
|
-
/**
|
|
72
|
-
* Deliberately loose: the ontology is enforced by `validateEventWrite` in
|
|
73
|
-
* the *backing store* (§8), so its precise EventValidationError message —
|
|
74
|
-
* the agent feedback D6 exists for — survives the wire instead of being
|
|
75
|
-
* shadowed by a duplicate wire-schema error.
|
|
76
|
-
*/
|
|
77
|
-
export const eventWriteWireSchema = z.object({
|
|
78
|
-
actor: z.unknown(),
|
|
79
|
-
type: z.string().min(1),
|
|
80
|
-
payload: z.unknown(),
|
|
81
|
-
})
|
|
82
|
-
|
|
83
87
|
export const eventEnvelopeWireSchema = z.object({
|
|
84
88
|
build: z.string(),
|
|
85
89
|
seq: z.number().int().positive(),
|
|
@@ -117,6 +121,18 @@ export const repositoryEventEnvelopeWireSchema = z.object({
|
|
|
117
121
|
})
|
|
118
122
|
export const repositoryEventListSchema = z.array(repositoryEventEnvelopeWireSchema)
|
|
119
123
|
|
|
124
|
+
/** `GET repos/:repo/high-water`: the journal's greatest assigned seq. */
|
|
125
|
+
export const repoHighWaterSchema = z.object({ seq: z.number().int().nonnegative() })
|
|
126
|
+
|
|
127
|
+
/** `GET builds|repos/:id/snapshots/:reducer?version=N`: the snapshot or `null`. */
|
|
128
|
+
/** `PUT …/snapshots/:reducer` request body. */
|
|
129
|
+
export const reducerSnapshotPutBodySchema = reducerSnapshotWireSchema
|
|
130
|
+
|
|
131
|
+
export const reducerSnapshotResponseSchema = reducerSnapshotWireSchema.nullable()
|
|
132
|
+
|
|
133
|
+
/** `PUT builds|repos/:id/snapshots/:reducer`: whether the row was written. */
|
|
134
|
+
export const reducerSnapshotPutResponseSchema = z.object({ written: z.boolean() })
|
|
135
|
+
|
|
120
136
|
export const repositoryArtifactMetaWireSchema = z.object({
|
|
121
137
|
repo: z.string(),
|
|
122
138
|
kind: z.string(),
|
|
@@ -138,6 +154,68 @@ export const repoDepositsResponseSchema = z.object({
|
|
|
138
154
|
artifacts: repositoryArtifactMetaListSchema,
|
|
139
155
|
})
|
|
140
156
|
|
|
157
|
+
// ── Ticket assets (SPEC §7.1) ────────────────────────────────────────────────
|
|
158
|
+
|
|
159
|
+
export const ticketAssetLayoutSchema = z.enum(['file', 'tree'])
|
|
160
|
+
/** An entry on the put request: `{type:'file',path,contentBase64}` or `{type:'dir',path}`. */
|
|
161
|
+
export const ticketAssetEntryWireInputSchema = z.discriminatedUnion('type', [
|
|
162
|
+
z.object({ type: z.literal('file'), path: z.string(), contentBase64: z.string() }),
|
|
163
|
+
z.object({ type: z.literal('dir'), path: z.string() }),
|
|
164
|
+
])
|
|
165
|
+
export const putTicketAssetBodySchema = z.object({
|
|
166
|
+
kind: z.string(),
|
|
167
|
+
name: z.string(),
|
|
168
|
+
layout: ticketAssetLayoutSchema,
|
|
169
|
+
entries: z.array(ticketAssetEntryWireInputSchema),
|
|
170
|
+
})
|
|
171
|
+
/** A manifest entry: `dir` entries carry no size or blobRef. */
|
|
172
|
+
export const ticketAssetEntryWireSchema = z.discriminatedUnion('type', [
|
|
173
|
+
z.object({
|
|
174
|
+
type: z.literal('file'),
|
|
175
|
+
path: z.string(),
|
|
176
|
+
size: z.number().int().nonnegative(),
|
|
177
|
+
blobRef: z.string(),
|
|
178
|
+
}),
|
|
179
|
+
z.object({ type: z.literal('dir'), path: z.string() }),
|
|
180
|
+
])
|
|
181
|
+
export const ticketAssetMetaWireSchema = z.object({
|
|
182
|
+
repo: z.string(),
|
|
183
|
+
ticketId: z.string(),
|
|
184
|
+
kind: z.string(),
|
|
185
|
+
name: z.string(),
|
|
186
|
+
revision: z.number().int().nonnegative(),
|
|
187
|
+
layout: ticketAssetLayoutSchema,
|
|
188
|
+
size: z.number().int().nonnegative(),
|
|
189
|
+
entries: z.array(ticketAssetEntryWireSchema),
|
|
190
|
+
createdAt: z.string(),
|
|
191
|
+
})
|
|
192
|
+
export const ticketAssetSummaryWireSchema = z.object({
|
|
193
|
+
repo: z.string(),
|
|
194
|
+
ticketId: z.string(),
|
|
195
|
+
kind: z.string(),
|
|
196
|
+
name: z.string(),
|
|
197
|
+
revision: z.number().int().nonnegative(),
|
|
198
|
+
layout: ticketAssetLayoutSchema,
|
|
199
|
+
size: z.number().int().nonnegative(),
|
|
200
|
+
fileCount: z.number().int().nonnegative(),
|
|
201
|
+
dirCount: z.number().int().nonnegative(),
|
|
202
|
+
createdAt: z.string(),
|
|
203
|
+
})
|
|
204
|
+
export const ticketAssetSummaryListSchema = z.array(ticketAssetSummaryWireSchema)
|
|
205
|
+
/** `GET …/assets` answers the manifest only (no bytes); null when absent or removed. */
|
|
206
|
+
export const ticketAssetManifestResponseSchema = ticketAssetMetaWireSchema.nullable()
|
|
207
|
+
/** `GET …/assets/file` answers one file's bytes; null when absent. */
|
|
208
|
+
export const ticketAssetFileResponseSchema = z.object({ contentBase64: z.string() }).nullable()
|
|
209
|
+
export const removeTicketAssetBodySchema = z.object({ kind: z.string(), name: z.string() })
|
|
210
|
+
export const removeTicketAssetResponseSchema = ticketAssetMetaWireSchema.nullable()
|
|
211
|
+
export const ticketAssetLimitsWireSchema = z.object({
|
|
212
|
+
maxBytes: z.number().int().nonnegative(),
|
|
213
|
+
maxEntries: z.number().int().positive(),
|
|
214
|
+
/** Largest request or response body the deployment can carry. Absent: no ceiling. */
|
|
215
|
+
maxRequestBytes: z.number().int().positive().optional(),
|
|
216
|
+
})
|
|
217
|
+
export type TicketAssetLimitsWire = z.infer<typeof ticketAssetLimitsWireSchema>
|
|
218
|
+
|
|
141
219
|
// ── Operator sessions (SPEC §7.1.1) ─────────────────────────────────────────
|
|
142
220
|
|
|
143
221
|
export const newSessionBodySchema = z.object({
|
|
@@ -166,6 +244,12 @@ export const sessionEventEnvelopeWireSchema = z.object({
|
|
|
166
244
|
})
|
|
167
245
|
export const sessionEventListSchema = z.array(sessionEventEnvelopeWireSchema)
|
|
168
246
|
|
|
247
|
+
export const conditionalSessionEventBodySchema = z.object({
|
|
248
|
+
expectedSeq: z.number().int().nonnegative(),
|
|
249
|
+
event: eventWriteWireSchema,
|
|
250
|
+
})
|
|
251
|
+
export const conditionalSessionEventResponseSchema = sessionEventEnvelopeWireSchema.nullable()
|
|
252
|
+
|
|
169
253
|
export const sessionArtifactMetaWireSchema = z.object({
|
|
170
254
|
session: z.string(),
|
|
171
255
|
kind: z.string(),
|
|
@@ -187,6 +271,25 @@ export const sessionDepositsResponseSchema = z.object({
|
|
|
187
271
|
artifacts: sessionArtifactMetaListSchema,
|
|
188
272
|
})
|
|
189
273
|
|
|
274
|
+
// ── Build digests (repo-scoped batch read, AUT-487) ────────────────────────
|
|
275
|
+
// The wire form of `BuildDigest` (store/types.ts). `terminal` is omitted when
|
|
276
|
+
// the build's log carries no terminal fact, per the protocol's omission
|
|
277
|
+
// convention for optional members.
|
|
278
|
+
export const buildDigestWireSchema = z.object({
|
|
279
|
+
slug: z.string().min(1),
|
|
280
|
+
terminal: z.enum(['done', 'aborted']).optional(),
|
|
281
|
+
observations: z.array(
|
|
282
|
+
z.object({
|
|
283
|
+
seq: z.number().int().positive(),
|
|
284
|
+
ts: z.string().min(1),
|
|
285
|
+
}),
|
|
286
|
+
),
|
|
287
|
+
merged: z.string().min(1).optional(),
|
|
288
|
+
reclaimPending: z.literal(true).optional(),
|
|
289
|
+
executionOpen: z.literal(true).optional(),
|
|
290
|
+
})
|
|
291
|
+
export const buildDigestListSchema = z.array(buildDigestWireSchema)
|
|
292
|
+
|
|
190
293
|
// ── Artifacts ────────────────────────────────────────────────────────────────
|
|
191
294
|
|
|
192
295
|
export const artifactMetaWireSchema = z.object({
|
|
@@ -3,8 +3,12 @@ import manifest from '../../../../../package.json'
|
|
|
3
3
|
/** Release-synchronized identity sent by every remote-store client. */
|
|
4
4
|
export const AUTOBUILD_VERSION = manifest.version
|
|
5
5
|
|
|
6
|
-
/** Wire compatibility version for the remote BuildStore protocol.
|
|
7
|
-
|
|
6
|
+
/** Wire compatibility version for the remote BuildStore protocol.
|
|
7
|
+
* Version 3 (AUT-521): the build-digest wire shape gains per-occurrence
|
|
8
|
+
* observation timestamps and the `merged` fact — a breaking shape change,
|
|
9
|
+
* so stale peers are rejected at handshake instead of failing mid-request
|
|
10
|
+
* at schema parse. */
|
|
11
|
+
export const REMOTE_STORE_PROTOCOL_VERSION = '3'
|
|
8
12
|
|
|
9
13
|
export const AUTOBUILD_VERSION_HEADER = 'x-autobuild-version'
|
|
10
14
|
export const REMOTE_STORE_PROTOCOL_VERSION_HEADER = 'x-autobuild-protocol-version'
|