@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
@@ -26,12 +26,34 @@ import {
26
26
  } from '../events/sessions'
27
27
  import { humanActor } from '../events/envelope'
28
28
  import { createBuildScopedStore } from './build-scope'
29
+ import {
30
+ type ReducerSnapshot,
31
+ type SnapshotScope,
32
+ snapshotScopeKey,
33
+ snapshotSupersedes,
34
+ } from './snapshots'
35
+ import { validateCreatedEvent } from './new-build'
29
36
  import { createSessionScopedStore } from './session-handle'
37
+ import { reduceBuildDigest } from './digest'
38
+ import { projectRepositoryStateEvents } from './repo-state-events'
30
39
  import {
31
40
  DEFAULT_ARTIFACT_RETENTION_MAX_REVISIONS,
32
41
  isRetentionManagedKind,
33
42
  revisionsToPrune,
34
43
  } from './retention'
44
+ import {
45
+ DEFAULT_TICKET_ASSET_LIMITS,
46
+ loadTicketAsset,
47
+ resolvePinnedAsset,
48
+ storeTicketAssetBlobs,
49
+ summarizeTicketAsset,
50
+ validateTicketAssetInput,
51
+ type TicketAsset,
52
+ type TicketAssetInput,
53
+ type TicketAssetLimits,
54
+ type TicketAssetMeta,
55
+ type TicketAssetSummary,
56
+ } from './ticket-assets'
35
57
  import { pollingSubscribe } from './subscribe'
36
58
  import { StreamLocks } from './streams/lock'
37
59
  import { assembleUIMessageDocument } from './streams/assemble'
@@ -62,6 +84,7 @@ import {
62
84
  type ArtifactMeta,
63
85
  type BlobStore,
64
86
  type BuildRecord,
87
+ type BuildDigest,
65
88
  type BuildScopedStore,
66
89
  type BuildStore,
67
90
  type Clock,
@@ -146,6 +169,11 @@ export class MemoryBuildStore implements BuildStore {
146
169
  * stream's close against its appends inside this process (AUT-348). */
147
170
  private readonly streamLocks = new StreamLocks()
148
171
  private readonly sessions = new Map<string, SessionState>()
172
+ /** Reducer snapshots (store/snapshots.ts), keyed by scope and reducer name. */
173
+ private readonly snapshots = new Map<string, ReducerSnapshot>()
174
+ /** `[repo, ticketId, kind, name]` → rows in revision order. A removal is a
175
+ * tombstone row (`removed`, empty manifest) taking the next revision. */
176
+ private readonly ticketAssets = new Map<string, { meta: TicketAssetMeta; removed: boolean }[]>()
149
177
  private readonly clock: Clock
150
178
  private readonly maxRevisions: number
151
179
  readonly blobs: BlobStore
@@ -244,6 +272,7 @@ export class MemoryBuildStore implements BuildStore {
244
272
  }
245
273
 
246
274
  async createBuild(input: NewBuildInput): Promise<BuildRecord> {
275
+ const created = validateCreatedEvent(input)
247
276
  if (this.builds.has(input.slug)) {
248
277
  throw new Error(`build "${input.slug}" already exists`)
249
278
  }
@@ -261,6 +290,18 @@ export class MemoryBuildStore implements BuildStore {
261
290
  events: [],
262
291
  artifacts: new Map(),
263
292
  }
293
+ if (created !== undefined) {
294
+ state.events.push(
295
+ structuredClone({
296
+ build: input.slug,
297
+ seq: 1,
298
+ ts,
299
+ actor: created.actor,
300
+ type: created.type,
301
+ payload: created.payload,
302
+ }) as AbEvent,
303
+ )
304
+ }
264
305
  this.builds.set(input.slug, state)
265
306
  return this.snapshot(state)
266
307
  }
@@ -274,6 +315,37 @@ export class MemoryBuildStore implements BuildStore {
274
315
  return [...this.builds.values()].map((state) => this.snapshot(state))
275
316
  }
276
317
 
318
+ async getRepoBuildDigests(repo: string): Promise<Map<string, BuildDigest>> {
319
+ // Insertion order is creation order (the listSessions tiebreak note); the
320
+ // digest map is returned in slug order so every adapter answers the same
321
+ // shape for the same store contents.
322
+ const slugs = [...this.builds.entries()]
323
+ .filter(([, state]) => state.record.repo === repo)
324
+ .map(([slug]) => slug)
325
+ .sort()
326
+ const digests = new Map<string, BuildDigest>()
327
+ for (const slug of slugs) {
328
+ const state = this.builds.get(slug)!
329
+ digests.set(slug, {
330
+ slug,
331
+ ...reduceBuildDigest(
332
+ state.events.map((event) => ({
333
+ type: event.type,
334
+ seq: event.seq,
335
+ ts: event.ts,
336
+ ...(event.type === 'pr-attachment.reclaimed'
337
+ ? { hostedSeq: event.payload.hostedSeq }
338
+ : {}),
339
+ ...(event.type === 'execution.started' || event.type === 'execution.ended'
340
+ ? { instance: event.payload.instance }
341
+ : {}),
342
+ })),
343
+ ),
344
+ })
345
+ }
346
+ return digests
347
+ }
348
+
277
349
  async append<T extends EventType>(slug: string, event: EventWrite<T>): Promise<EventEnvelope<T>> {
278
350
  const state = this.state(slug)
279
351
  const validated = validateEventWrite(event)
@@ -629,6 +701,58 @@ export class MemoryBuildStore implements BuildStore {
629
701
  })
630
702
  }
631
703
 
704
+ async getRepoHighWater(repo: string): Promise<number> {
705
+ return this.repoState(repo).events.at(-1)?.seq ?? 0
706
+ }
707
+
708
+ /** The scope's log tail, or `undefined` for a scope that does not exist. */
709
+ private snapshotTail(scope: SnapshotScope): number | undefined {
710
+ const events =
711
+ scope.kind === 'build'
712
+ ? this.builds.get(scope.slug)?.events
713
+ : this.repos.get(scope.repo)?.events
714
+ return events === undefined ? undefined : (events.at(-1)?.seq ?? 0)
715
+ }
716
+
717
+ async getReducerSnapshot(
718
+ scope: SnapshotScope,
719
+ reducer: string,
720
+ version: number,
721
+ ): Promise<ReducerSnapshot | null> {
722
+ const tail = this.snapshotTail(scope)
723
+ if (tail === undefined) return null
724
+ const stored = this.snapshots.get(`${snapshotScopeKey(scope)}\u0000${reducer}`)
725
+ if (stored === undefined || stored.version !== version || stored.cursor > tail) return null
726
+ return { version: stored.version, cursor: stored.cursor, state: structuredClone(stored.state) }
727
+ }
728
+
729
+ async putReducerSnapshot(
730
+ scope: SnapshotScope,
731
+ reducer: string,
732
+ snapshot: ReducerSnapshot,
733
+ ): Promise<boolean> {
734
+ const tail = this.snapshotTail(scope)
735
+ if (tail === undefined || snapshot.cursor > tail) return false
736
+ const key = `${snapshotScopeKey(scope)}\u0000${reducer}`
737
+ if (!snapshotSupersedes(snapshot, this.snapshots.get(key))) return false
738
+ // The JSON round trip is the stored form every adapter shares, so callers
739
+ // cannot alias what the store holds.
740
+ this.snapshots.set(key, {
741
+ version: snapshot.version,
742
+ cursor: snapshot.cursor,
743
+ state: JSON.parse(JSON.stringify(snapshot.state ?? null)),
744
+ })
745
+ return true
746
+ }
747
+
748
+ async getRepoStateEvents(repo: string): Promise<RepositoryEvent[]> {
749
+ // The oracle runs on the in-memory journal itself, which keeps the pure
750
+ // derivation honest against real adapter data.
751
+ return structuredClone(
752
+ projectRepositoryStateEvents(this.repoState(repo).events as RepositoryEvent[]),
753
+ )
754
+ }
755
+
632
756
  async putRepoArtifact(repo: string, artifact: ArtifactInput): Promise<RepositoryArtifactMeta> {
633
757
  const state = this.repoState(repo)
634
758
  if (!artifact.kind) throw new Error('artifact kind is required')
@@ -676,6 +800,102 @@ export class MemoryBuildStore implements BuildStore {
676
800
  )
677
801
  }
678
802
 
803
+ async ticketAssetLimits(_repo: string): Promise<TicketAssetLimits> {
804
+ return { ...DEFAULT_TICKET_ASSET_LIMITS }
805
+ }
806
+
807
+ async putTicketAsset(
808
+ repo: string,
809
+ ticketId: string,
810
+ asset: TicketAssetInput,
811
+ ): Promise<TicketAssetMeta> {
812
+ const size = validateTicketAssetInput(asset)
813
+ const entries = await storeTicketAssetBlobs(this.blobs, asset.entries)
814
+ const key = JSON.stringify([repo, ticketId, asset.kind, asset.name])
815
+ const rows = this.ticketAssets.get(key) ?? []
816
+ const meta: TicketAssetMeta = {
817
+ repo,
818
+ ticketId,
819
+ kind: asset.kind,
820
+ name: asset.name,
821
+ revision: MemoryBuildStore.nextRevision(rows.map((row) => row.meta)),
822
+ layout: asset.layout,
823
+ size,
824
+ entries,
825
+ createdAt: this.now(),
826
+ }
827
+ rows.push({ meta, removed: false })
828
+ this.ticketAssets.set(key, rows)
829
+ return structuredClone(meta)
830
+ }
831
+
832
+ async getTicketAsset(
833
+ repo: string,
834
+ ticketId: string,
835
+ kind: string,
836
+ name: string,
837
+ rev?: number,
838
+ ): Promise<TicketAsset | null> {
839
+ const rows = this.ticketAssets.get(JSON.stringify([repo, ticketId, kind, name]))
840
+ const row =
841
+ rev === undefined ? rows?.at(-1) : rows?.find((candidate) => candidate.meta.revision === rev)
842
+ if (!row || row.removed) return null
843
+ return loadTicketAsset(this.blobs, row.meta)
844
+ }
845
+
846
+ async getPinnedTicketAsset(
847
+ slug: string,
848
+ kind: string,
849
+ name: string,
850
+ rev?: number,
851
+ ): Promise<TicketAsset | null> {
852
+ return resolvePinnedAsset(this, slug, kind, name, rev)
853
+ }
854
+
855
+ async listTicketAssets(
856
+ repo: string,
857
+ ticketId: string,
858
+ opts?: { revisions?: boolean },
859
+ ): Promise<TicketAssetSummary[]> {
860
+ const out: TicketAssetSummary[] = []
861
+ for (const rows of this.ticketAssets.values()) {
862
+ const first = rows[0]
863
+ if (!first || first.meta.repo !== repo || first.meta.ticketId !== ticketId) continue
864
+ if (opts?.revisions) {
865
+ for (const row of rows) if (!row.removed) out.push(summarizeTicketAsset(row.meta))
866
+ } else {
867
+ const latest = rows.at(-1)
868
+ if (latest && !latest.removed) out.push(summarizeTicketAsset(latest.meta))
869
+ }
870
+ }
871
+ return out.sort(
872
+ (a, b) =>
873
+ a.kind.localeCompare(b.kind) || a.name.localeCompare(b.name) || a.revision - b.revision,
874
+ )
875
+ }
876
+
877
+ async removeTicketAsset(
878
+ repo: string,
879
+ ticketId: string,
880
+ kind: string,
881
+ name: string,
882
+ ): Promise<TicketAssetMeta | null> {
883
+ const rows = this.ticketAssets.get(JSON.stringify([repo, ticketId, kind, name]))
884
+ const latest = rows?.at(-1)
885
+ if (!rows || !latest || latest.removed) return null
886
+ rows.push({
887
+ meta: {
888
+ ...latest.meta,
889
+ revision: latest.meta.revision + 1,
890
+ size: 0,
891
+ entries: [],
892
+ createdAt: this.now(),
893
+ },
894
+ removed: true,
895
+ })
896
+ return structuredClone(latest.meta)
897
+ }
898
+
679
899
  async claimRepoLease(repo: string, holder: string, ttlMs: number): Promise<boolean> {
680
900
  const state = this.repoState(repo)
681
901
  const now = this.clock().getTime()
@@ -791,6 +1011,32 @@ export class MemoryBuildStore implements BuildStore {
791
1011
  return envelope
792
1012
  }
793
1013
 
1014
+ async appendSessionEventIfCurrent<T extends SessionEventType>(
1015
+ id: string,
1016
+ expectedSeq: number,
1017
+ event: SessionEventWrite<T>,
1018
+ ): Promise<SessionEventEnvelope<T> | null> {
1019
+ const state = this.sessionState(id)
1020
+ validateExpectedSeq(expectedSeq)
1021
+ const validated = validateSessionEventWrite(event)
1022
+
1023
+ // No await occurs between comparison and mutation. JavaScript's run-to-
1024
+ // completion semantics make this one atomic critical section even when
1025
+ // concurrent remote requests interleave elsewhere in the adapter.
1026
+ if (state.events.length !== expectedSeq) return null
1027
+ const envelope = {
1028
+ session: id,
1029
+ seq: expectedSeq + 1,
1030
+ ts: this.now(),
1031
+ actor: validated.actor,
1032
+ type: validated.type,
1033
+ payload: validated.payload,
1034
+ } as SessionEventEnvelope<T>
1035
+ state.events.push(structuredClone(envelope) as SessionEvent)
1036
+ state.record.updatedAt = envelope.ts
1037
+ return envelope
1038
+ }
1039
+
794
1040
  async getSessionEvents(
795
1041
  id: string,
796
1042
  sinceSeq = 0,
@@ -0,0 +1,17 @@
1
+ import { EventValidationError, validateEventWrite, type EventWrite } from '../events/catalog'
2
+ import type { NewBuildInput } from './types'
3
+
4
+ /** Validate `input.created` before an adapter opens its create transaction, so
5
+ * an invalid initial event creates nothing. Returns the validated write. */
6
+ export function validateCreatedEvent(
7
+ input: NewBuildInput,
8
+ ): EventWrite<'build.created'> | undefined {
9
+ if (input.created === undefined) return undefined
10
+ const validated = validateEventWrite(input.created)
11
+ if (validated.type !== 'build.created') {
12
+ throw new EventValidationError(
13
+ `createBuild's initial event must be "build.created", got "${validated.type}"`,
14
+ )
15
+ }
16
+ return validated as EventWrite<'build.created'>
17
+ }
@@ -8,6 +8,7 @@
8
8
  * scope handles in `session-handle.ts` (hosted-only, AUT-339), which scope a
9
9
  * store to a single operator session's record.
10
10
  */
11
+ import type { ReducerSnapshot, SnapshotScope } from './snapshots'
11
12
  import type { EventType } from '../events/payloads'
12
13
  import type { EventWrite } from '../events/catalog'
13
14
  import type { RepositoryEventType, RepositoryEventWrite } from '../events/repository'
@@ -23,6 +24,7 @@ import type {
23
24
  Artifact,
24
25
  ArtifactInput,
25
26
  ArtifactMeta,
27
+ BuildDigest,
26
28
  BuildRecord,
27
29
  BuildStore,
28
30
  NewBuildInput,
@@ -35,6 +37,13 @@ import type {
35
37
  SubscribeOptions,
36
38
  Unsubscribe,
37
39
  } from './types'
40
+ import type {
41
+ TicketAsset,
42
+ TicketAssetInput,
43
+ TicketAssetLimits,
44
+ TicketAssetMeta,
45
+ TicketAssetSummary,
46
+ } from './ticket-assets'
38
47
  import type {
39
48
  StreamChunk,
40
49
  StreamOutcome,
@@ -316,6 +325,36 @@ export function scopeLocalStoreToPhaseSession(
316
325
  own('getRepoEvents', 'repo', repo)
317
326
  return store.getRepoEvents(repo, sinceSeq, opts)
318
327
  },
328
+ async getRepoStateEvents(repo: string): Promise<RepositoryEvent[]> {
329
+ own('getRepoStateEvents', 'repo', repo)
330
+ return store.getRepoStateEvents(repo)
331
+ },
332
+ async getRepoHighWater(repo: string): Promise<number> {
333
+ own('getRepoHighWater', 'repo', repo)
334
+ return store.getRepoHighWater(repo)
335
+ },
336
+ async getReducerSnapshot(
337
+ target: SnapshotScope,
338
+ reducer: string,
339
+ version: number,
340
+ ): Promise<ReducerSnapshot | null> {
341
+ if (target.kind === 'build') own('getReducerSnapshot', 'build', target.slug)
342
+ else own('getReducerSnapshot', 'repo', target.repo)
343
+ return store.getReducerSnapshot(target, reducer, version)
344
+ },
345
+ async putReducerSnapshot(
346
+ target: SnapshotScope,
347
+ reducer: string,
348
+ snapshot: ReducerSnapshot,
349
+ ): Promise<boolean> {
350
+ if (target.kind === 'build') own('putReducerSnapshot', 'build', target.slug)
351
+ else own('putReducerSnapshot', 'repo', target.repo)
352
+ return store.putReducerSnapshot(target, reducer, snapshot)
353
+ },
354
+ async getRepoBuildDigests(repo: string): Promise<Map<string, BuildDigest>> {
355
+ own('getRepoBuildDigests', 'repo', repo)
356
+ return store.getRepoBuildDigests(repo)
357
+ },
319
358
  async putRepoArtifact(repo: string, artifact: ArtifactInput): Promise<RepositoryArtifactMeta> {
320
359
  own('putRepoArtifact', 'repo', repo)
321
360
  return store.putRepoArtifact(repo, artifact)
@@ -332,6 +371,54 @@ export function scopeLocalStoreToPhaseSession(
332
371
  own('listRepoArtifacts', 'repo', repo)
333
372
  return store.listRepoArtifacts(repo, kind)
334
373
  },
374
+ async ticketAssetLimits(repo: string): Promise<TicketAssetLimits> {
375
+ own('ticketAssetLimits', 'repo', repo)
376
+ return store.ticketAssetLimits(repo)
377
+ },
378
+ async putTicketAsset(
379
+ repo: string,
380
+ ticketId: string,
381
+ asset: TicketAssetInput,
382
+ ): Promise<TicketAssetMeta> {
383
+ own('putTicketAsset', 'repo', repo)
384
+ return store.putTicketAsset(repo, ticketId, asset)
385
+ },
386
+ async getTicketAsset(
387
+ repo: string,
388
+ ticketId: string,
389
+ kind: string,
390
+ name: string,
391
+ rev?: number,
392
+ ): Promise<TicketAsset | null> {
393
+ own('getTicketAsset', 'repo', repo)
394
+ return store.getTicketAsset(repo, ticketId, kind, name, rev)
395
+ },
396
+ async getPinnedTicketAsset(
397
+ slug: string,
398
+ kind: string,
399
+ name: string,
400
+ rev?: number,
401
+ ): Promise<TicketAsset | null> {
402
+ own('getPinnedTicketAsset', 'build', slug)
403
+ return store.getPinnedTicketAsset(slug, kind, name, rev)
404
+ },
405
+ async listTicketAssets(
406
+ repo: string,
407
+ ticketId: string,
408
+ opts?: { revisions?: boolean },
409
+ ): Promise<TicketAssetSummary[]> {
410
+ own('listTicketAssets', 'repo', repo)
411
+ return store.listTicketAssets(repo, ticketId, opts)
412
+ },
413
+ async removeTicketAsset(
414
+ repo: string,
415
+ ticketId: string,
416
+ kind: string,
417
+ name: string,
418
+ ): Promise<TicketAssetMeta | null> {
419
+ own('removeTicketAsset', 'repo', repo)
420
+ return store.removeTicketAsset(repo, ticketId, kind, name)
421
+ },
335
422
  async claimRepoLease(repo: string, holder: string, ttlMs: number): Promise<boolean> {
336
423
  own('claimRepoLease', 'repo', repo)
337
424
  return store.claimRepoLease(repo, holder, ttlMs)
@@ -390,6 +477,13 @@ export function scopeLocalStoreToPhaseSession(
390
477
  ): Promise<SessionEventEnvelope<T>> {
391
478
  return admin('appendSessionEvent')
392
479
  },
480
+ appendSessionEventIfCurrent<T extends SessionEventType>(
481
+ _id: string,
482
+ _expectedSeq: number,
483
+ _event: SessionEventWrite<T>,
484
+ ): Promise<SessionEventEnvelope<T> | null> {
485
+ return admin('appendSessionEventIfCurrent')
486
+ },
393
487
  getSessionEvents(_id: string, _sinceSeq?: number): Promise<SessionEvent[]> {
394
488
  return admin('getSessionEvents')
395
489
  },