@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
@@ -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
- ...(body !== undefined ? { body: JSON.stringify(body) } : {}),
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
- export const REMOTE_STORE_PROTOCOL_VERSION = '2'
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'