@defrex/autobuild 0.8.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 (251) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +192 -0
  3. package/SPEC.md +2269 -0
  4. package/bin/ab-build-runner.ts +47 -0
  5. package/bin/ab-dev.ts +27 -0
  6. package/bin/ab-dispatch-kernel.ts +92 -0
  7. package/bin/ab-harvest-runner.ts +29 -0
  8. package/bin/ab-init-probe.ts +19 -0
  9. package/bin/ab-process-group-reaper.ts +32 -0
  10. package/bin/ab.ts +6 -0
  11. package/bin/agent/ab +3 -0
  12. package/docs/answering-blocked-builds.md +89 -0
  13. package/docs/architecture.md +726 -0
  14. package/docs/assets/headline-wide.png +0 -0
  15. package/docs/configuration.md +1623 -0
  16. package/docs/hosted-dispatcher-cutover-validation.md +178 -0
  17. package/docs/hosted-dispatcher.md +204 -0
  18. package/docs/mcp.md +119 -0
  19. package/docs/operator-api.md +186 -0
  20. package/docs/remote-store-protocol.md +972 -0
  21. package/docs/remote-ticket-protocol.md +79 -0
  22. package/docs/setup.md +289 -0
  23. package/docs/spec-standard.md +63 -0
  24. package/docs/ticket-dependencies.md +59 -0
  25. package/docs/vercel-sandbox-rollout-probe.md +7 -0
  26. package/package.json +135 -0
  27. package/packages/core/src/cli/args.ts +74 -0
  28. package/packages/core/src/cli/artifact.ts +182 -0
  29. package/packages/core/src/cli/binary.ts +198 -0
  30. package/packages/core/src/cli/build-control.ts +789 -0
  31. package/packages/core/src/cli/build-progress.ts +55 -0
  32. package/packages/core/src/cli/bulk-control.ts +385 -0
  33. package/packages/core/src/cli/context.ts +470 -0
  34. package/packages/core/src/cli/dashboard/actions.ts +59 -0
  35. package/packages/core/src/cli/dashboard/cells.ts +92 -0
  36. package/packages/core/src/cli/dashboard/composer.ts +230 -0
  37. package/packages/core/src/cli/dashboard/detail.ts +100 -0
  38. package/packages/core/src/cli/dashboard/frame-image.ts +373 -0
  39. package/packages/core/src/cli/dashboard/live.ts +152 -0
  40. package/packages/core/src/cli/dashboard/model.ts +1173 -0
  41. package/packages/core/src/cli/dashboard/poll.ts +196 -0
  42. package/packages/core/src/cli/dashboard/render.ts +1568 -0
  43. package/packages/core/src/cli/dashboard/selection.ts +66 -0
  44. package/packages/core/src/cli/dashboard/session-feed.ts +244 -0
  45. package/packages/core/src/cli/dashboard/session-view.ts +506 -0
  46. package/packages/core/src/cli/dashboard/transcript.ts +108 -0
  47. package/packages/core/src/cli/dev.ts +46 -0
  48. package/packages/core/src/cli/dispatch-frontend.ts +1045 -0
  49. package/packages/core/src/cli/dispatch-process.ts +259 -0
  50. package/packages/core/src/cli/dispatch.ts +3692 -0
  51. package/packages/core/src/cli/dotenv.ts +48 -0
  52. package/packages/core/src/cli/env.ts +206 -0
  53. package/packages/core/src/cli/harvest.ts +698 -0
  54. package/packages/core/src/cli/help.ts +713 -0
  55. package/packages/core/src/cli/init-agent.ts +106 -0
  56. package/packages/core/src/cli/init-validation.ts +583 -0
  57. package/packages/core/src/cli/init.ts +756 -0
  58. package/packages/core/src/cli/installation.ts +333 -0
  59. package/packages/core/src/cli/keyboard.ts +261 -0
  60. package/packages/core/src/cli/main.ts +1399 -0
  61. package/packages/core/src/cli/mcp.ts +279 -0
  62. package/packages/core/src/cli/models.ts +65 -0
  63. package/packages/core/src/cli/observe.ts +55 -0
  64. package/packages/core/src/cli/plugin.ts +227 -0
  65. package/packages/core/src/cli/pr-attachments.ts +138 -0
  66. package/packages/core/src/cli/pr-summary.ts +124 -0
  67. package/packages/core/src/cli/publication-settlement.ts +188 -0
  68. package/packages/core/src/cli/repo-state.ts +213 -0
  69. package/packages/core/src/cli/repository-status.ts +80 -0
  70. package/packages/core/src/cli/self-update.ts +443 -0
  71. package/packages/core/src/cli/status.ts +895 -0
  72. package/packages/core/src/cli/store-opening.contract.ts +289 -0
  73. package/packages/core/src/cli/store-opening.ts +167 -0
  74. package/packages/core/src/cli/store-ref.ts +33 -0
  75. package/packages/core/src/cli/terminal-restore.ts +174 -0
  76. package/packages/core/src/cli/terminal.ts +361 -0
  77. package/packages/core/src/cli/terminals.ts +995 -0
  78. package/packages/core/src/cli/testkit.ts +194 -0
  79. package/packages/core/src/cli/ticket.ts +484 -0
  80. package/packages/core/src/cli/upgrade-agent.ts +180 -0
  81. package/packages/core/src/cli/upgrade-commit.ts +468 -0
  82. package/packages/core/src/cli/upgrade-notice.ts +91 -0
  83. package/packages/core/src/cli/upgrade-progress.ts +110 -0
  84. package/packages/core/src/cli/upgrade.ts +888 -0
  85. package/packages/core/src/cli/wait.ts +587 -0
  86. package/packages/core/src/cli/watch.ts +932 -0
  87. package/packages/core/src/config/live.ts +255 -0
  88. package/packages/core/src/config/load.ts +98 -0
  89. package/packages/core/src/config/roles.ts +438 -0
  90. package/packages/core/src/config/schema.ts +774 -0
  91. package/packages/core/src/distribution.ts +24 -0
  92. package/packages/core/src/events/catalog.ts +201 -0
  93. package/packages/core/src/events/envelope.ts +59 -0
  94. package/packages/core/src/events/payloads.ts +618 -0
  95. package/packages/core/src/events/repository.ts +559 -0
  96. package/packages/core/src/events/sessions.ts +180 -0
  97. package/packages/core/src/harvest/schema.ts +147 -0
  98. package/packages/core/src/ids.ts +45 -0
  99. package/packages/core/src/integration/harness.ts +681 -0
  100. package/packages/core/src/kernel/auto-merge.ts +259 -0
  101. package/packages/core/src/kernel/converge.ts +117 -0
  102. package/packages/core/src/kernel/dispatch-settings.ts +60 -0
  103. package/packages/core/src/kernel/dispatch-status.ts +141 -0
  104. package/packages/core/src/kernel/engine.ts +1047 -0
  105. package/packages/core/src/kernel/harvest.ts +823 -0
  106. package/packages/core/src/kernel/origin.ts +69 -0
  107. package/packages/core/src/kernel/phases.ts +196 -0
  108. package/packages/core/src/kernel/plan-verify-selection.ts +105 -0
  109. package/packages/core/src/kernel/pr-attachments.ts +82 -0
  110. package/packages/core/src/kernel/reducer.ts +752 -0
  111. package/packages/core/src/kernel/stall.ts +155 -0
  112. package/packages/core/src/kernel/verify-applicability.ts +52 -0
  113. package/packages/core/src/markdown.ts +67 -0
  114. package/packages/core/src/ontology.ts +288 -0
  115. package/packages/core/src/open-map.ts +203 -0
  116. package/packages/core/src/operator/annotations.ts +187 -0
  117. package/packages/core/src/operator/client.ts +311 -0
  118. package/packages/core/src/operator/control.ts +117 -0
  119. package/packages/core/src/operator/index.ts +62 -0
  120. package/packages/core/src/operator/presentation.ts +22 -0
  121. package/packages/core/src/operator/protocol.ts +98 -0
  122. package/packages/core/src/operator/query.ts +201 -0
  123. package/packages/core/src/operator/registry.ts +1043 -0
  124. package/packages/core/src/operator/requests.ts +138 -0
  125. package/packages/core/src/operator/sandbox.ts +458 -0
  126. package/packages/core/src/operator/server.ts +616 -0
  127. package/packages/core/src/operator/sessions.ts +223 -0
  128. package/packages/core/src/operator/tickets.ts +177 -0
  129. package/packages/core/src/plugin-sdk/index.ts +174 -0
  130. package/packages/core/src/plugins/contract-entry.ts +74 -0
  131. package/packages/core/src/plugins/contract-env.ts +3 -0
  132. package/packages/core/src/plugins/load.ts +259 -0
  133. package/packages/core/src/plugins/manifest.ts +236 -0
  134. package/packages/core/src/plugins/registry.ts +274 -0
  135. package/packages/core/src/plugins/runtimes.ts +75 -0
  136. package/packages/core/src/ports/forge/contract.ts +258 -0
  137. package/packages/core/src/ports/forge/create.ts +74 -0
  138. package/packages/core/src/ports/forge/fake.ts +488 -0
  139. package/packages/core/src/ports/forge/github-pr-attachments.ts +351 -0
  140. package/packages/core/src/ports/forge/github-transport.ts +352 -0
  141. package/packages/core/src/ports/forge/github.ts +1162 -0
  142. package/packages/core/src/ports/forge/local-git.ts +568 -0
  143. package/packages/core/src/ports/runner/claude.ts +873 -0
  144. package/packages/core/src/ports/runner/cli-stream.ts +90 -0
  145. package/packages/core/src/ports/runner/codex.ts +873 -0
  146. package/packages/core/src/ports/runner/contract.ts +492 -0
  147. package/packages/core/src/ports/runner/fake.ts +213 -0
  148. package/packages/core/src/ports/runner/json-record.ts +2 -0
  149. package/packages/core/src/ports/runner/live-contract-fixture.ts +176 -0
  150. package/packages/core/src/ports/runner/one-shot.ts +29 -0
  151. package/packages/core/src/ports/runner/pi-bridge-core.ts +136 -0
  152. package/packages/core/src/ports/runner/pi-bridge.js +6 -0
  153. package/packages/core/src/ports/runner/pi-rpc.ts +454 -0
  154. package/packages/core/src/ports/runner/pi.ts +617 -0
  155. package/packages/core/src/ports/runner/production.ts +74 -0
  156. package/packages/core/src/ports/runner/provider-error.ts +121 -0
  157. package/packages/core/src/ports/runner/routing.ts +234 -0
  158. package/packages/core/src/ports/runner/runtime.ts +214 -0
  159. package/packages/core/src/ports/runner/session-env.ts +34 -0
  160. package/packages/core/src/ports/runner/stream-parts.ts +103 -0
  161. package/packages/core/src/ports/tickets/contract.ts +570 -0
  162. package/packages/core/src/ports/tickets/create.ts +135 -0
  163. package/packages/core/src/ports/tickets/fake.ts +215 -0
  164. package/packages/core/src/ports/tickets/file.ts +559 -0
  165. package/packages/core/src/ports/tickets/linear.ts +811 -0
  166. package/packages/core/src/ports/tickets/operations.ts +161 -0
  167. package/packages/core/src/ports/tickets/remote-index.ts +13 -0
  168. package/packages/core/src/ports/tickets/remote-protocol.ts +80 -0
  169. package/packages/core/src/ports/tickets/remote-server.ts +185 -0
  170. package/packages/core/src/ports/tickets/remote.ts +130 -0
  171. package/packages/core/src/ports/tickets/update.ts +51 -0
  172. package/packages/core/src/ports/types.ts +528 -0
  173. package/packages/core/src/ports/workspace/build-execution.ts +123 -0
  174. package/packages/core/src/ports/workspace/contract.ts +136 -0
  175. package/packages/core/src/ports/workspace/create.ts +132 -0
  176. package/packages/core/src/ports/workspace/distribution-archive.ts +317 -0
  177. package/packages/core/src/ports/workspace/fake.ts +432 -0
  178. package/packages/core/src/ports/workspace/git-worktree.ts +848 -0
  179. package/packages/core/src/ports/workspace/harvest-execution.ts +111 -0
  180. package/packages/core/src/ports/workspace/in-process-build-execution.ts +30 -0
  181. package/packages/core/src/ports/workspace/local-build-execution.ts +88 -0
  182. package/packages/core/src/ports/workspace/operator-sandbox.ts +142 -0
  183. package/packages/core/src/ports/workspace/phase-scratch.ts +156 -0
  184. package/packages/core/src/ports/workspace/vercel-sandbox.ts +2541 -0
  185. package/packages/core/src/processes/build-child-exit.ts +78 -0
  186. package/packages/core/src/processes/build-child.ts +119 -0
  187. package/packages/core/src/processes/build-execution-state.ts +95 -0
  188. package/packages/core/src/processes/build-parent-watch.ts +35 -0
  189. package/packages/core/src/processes/build-runner.ts +2614 -0
  190. package/packages/core/src/processes/dispatcher.ts +2825 -0
  191. package/packages/core/src/processes/execution-settlement.ts +171 -0
  192. package/packages/core/src/processes/harvest-child.ts +115 -0
  193. package/packages/core/src/processes/harvest-execution-state.ts +121 -0
  194. package/packages/core/src/processes/harvest-runner.ts +1656 -0
  195. package/packages/core/src/processes/harvest.ts +595 -0
  196. package/packages/core/src/processes/infrastructure-failure-budget.ts +146 -0
  197. package/packages/core/src/processes/phase-failure-budget.ts +52 -0
  198. package/packages/core/src/processes/process-group-reaper.ts +35 -0
  199. package/packages/core/src/processes/process-group.ts +65 -0
  200. package/packages/core/src/processes/publication-loss.ts +62 -0
  201. package/packages/core/src/processes/publication-state.ts +93 -0
  202. package/packages/core/src/processes/sandbox-state.ts +101 -0
  203. package/packages/core/src/registry.ts +24 -0
  204. package/packages/core/src/sessions/reducer.ts +185 -0
  205. package/packages/core/src/skills.ts +7 -0
  206. package/packages/core/src/spec-standard.ts +47 -0
  207. package/packages/core/src/store/adapter.ts +80 -0
  208. package/packages/core/src/store/build-scope.ts +272 -0
  209. package/packages/core/src/store/contract.ts +2581 -0
  210. package/packages/core/src/store/local/blobs.ts +38 -0
  211. package/packages/core/src/store/local/schema.ts +180 -0
  212. package/packages/core/src/store/local/store.ts +1713 -0
  213. package/packages/core/src/store/memory.ts +1138 -0
  214. package/packages/core/src/store/phase-session.ts +425 -0
  215. package/packages/core/src/store/remote/client.ts +780 -0
  216. package/packages/core/src/store/remote/index.ts +12 -0
  217. package/packages/core/src/store/remote/protocol.ts +348 -0
  218. package/packages/core/src/store/remote/server.ts +923 -0
  219. package/packages/core/src/store/remote/token.ts +137 -0
  220. package/packages/core/src/store/remote/version.ts +10 -0
  221. package/packages/core/src/store/retention.ts +91 -0
  222. package/packages/core/src/store/session-handle.ts +265 -0
  223. package/packages/core/src/store/streams/assemble.ts +79 -0
  224. package/packages/core/src/store/streams/lock.ts +42 -0
  225. package/packages/core/src/store/streams/session-writer.ts +306 -0
  226. package/packages/core/src/store/streams/types.ts +171 -0
  227. package/packages/core/src/store/streams/wait.ts +127 -0
  228. package/packages/core/src/store/subscribe.ts +105 -0
  229. package/packages/core/src/store/types.ts +466 -0
  230. package/packages/core/src/testing/fixed.ts +44 -0
  231. package/packages/core/src/testing/store-failures.ts +32 -0
  232. package/packages/core/src/zod-issues.ts +91 -0
  233. package/skills/code-review/SKILL.md +100 -0
  234. package/skills/finalize/SKILL.md +44 -0
  235. package/skills/guide/SKILL.md +1781 -0
  236. package/skills/guide/references/operator-api.md +164 -0
  237. package/skills/guide/references/plugin-authoring.md +354 -0
  238. package/skills/guide/references/remote-store-protocol.md +972 -0
  239. package/skills/guide/references/remote-ticket-protocol.md +79 -0
  240. package/skills/guide/references/setup.md +289 -0
  241. package/skills/guide/references/spec-standard.md +63 -0
  242. package/skills/guide/references/ticket-dependencies.md +59 -0
  243. package/skills/harvest/SKILL.md +66 -0
  244. package/skills/harvest-review/SKILL.md +63 -0
  245. package/skills/implement/SKILL.md +82 -0
  246. package/skills/plan/SKILL.md +123 -0
  247. package/skills/plan-review/SKILL.md +121 -0
  248. package/skills/reconcile/SKILL.md +65 -0
  249. package/skills/spec/SKILL.md +132 -0
  250. package/skills/tickets/SKILL.md +139 -0
  251. package/templates/autobuild.toml +36 -0
@@ -0,0 +1,105 @@
1
+ /**
2
+ * The shared polling implementation of `subscribe` (SPEC §7.2): push is the
3
+ * specced interface, polling `getEvents(since)` is the v2.0 implementation.
4
+ * Adapters delegate here so delivery semantics (in-order, exactly once per
5
+ * subscription, no overlapping polls) are identical everywhere.
6
+ *
7
+ * Two cadences (AUT-334). Without `waitSeconds` this is the unchanged
8
+ * interval loop: one immediate read, then one read per `pollMs`. With
9
+ * `waitSeconds` the loop long-polls: each cycle issues one held request and
10
+ * then gap-fills so request *starts* stay at least `pollMs` apart (elapsed
11
+ * request time counts toward the gap). Against a server that honors the
12
+ * bound, a quiet stream costs one request per wait window and a new event is
13
+ * delivered within about a server-side poll of its append; against a server
14
+ * that answers immediately regardless of `wait`, the cadence degrades to
15
+ * exactly today's request rate and delivery is unchanged.
16
+ */
17
+ import type { AbEvent } from '../events/catalog'
18
+ import type { SubscribeOptions, Unsubscribe } from './types'
19
+
20
+ export const DEFAULT_POLL_MS = 250
21
+
22
+ export type SubscribeRead = (
23
+ sinceSeq: number,
24
+ opts?: { waitSeconds?: number; signal?: AbortSignal },
25
+ ) => Promise<AbEvent[]>
26
+
27
+ export function pollingSubscribe(
28
+ getEvents: SubscribeRead,
29
+ opts: SubscribeOptions,
30
+ onEvent: (event: AbEvent) => void,
31
+ ): Unsubscribe {
32
+ let lastSeq = opts.fromSeq ?? 0
33
+ let stopped = false
34
+ let inFlight = false
35
+
36
+ const deliver = (events: AbEvent[]): void => {
37
+ for (const event of events) {
38
+ if (stopped) break
39
+ if (event.seq <= lastSeq) continue
40
+ lastSeq = event.seq
41
+ onEvent(event)
42
+ }
43
+ }
44
+
45
+ if (opts.waitSeconds === undefined) {
46
+ const tick = async (): Promise<void> => {
47
+ if (stopped || inFlight) return
48
+ inFlight = true
49
+ try {
50
+ deliver(await getEvents(lastSeq))
51
+ } catch {
52
+ // Store unreachable — keep polling; the next tick retries (§8.7).
53
+ } finally {
54
+ inFlight = false
55
+ }
56
+ }
57
+
58
+ const timer = setInterval(() => void tick(), opts.pollMs ?? DEFAULT_POLL_MS)
59
+ void tick()
60
+
61
+ return () => {
62
+ stopped = true
63
+ clearInterval(timer)
64
+ }
65
+ }
66
+
67
+ // Bounded-wait mode: one held request in flight at a time (the loop is
68
+ // sequential, so polls never overlap); request starts spaced at least
69
+ // `pollMs` apart. A throw from the read (store unreachable) ends the cycle
70
+ // and is retried after the gap, like the interval loop. Each cycle carries
71
+ // an abort signal so unsubscribe also cancels a held request — otherwise a
72
+ // long-poll subscription could leave a request dangling for a whole wait
73
+ // window after the caller stopped listening.
74
+ const pollMs = opts.pollMs ?? DEFAULT_POLL_MS
75
+ let inFlightRead: AbortController | undefined
76
+ void (async (): Promise<void> => {
77
+ while (!stopped) {
78
+ inFlight = true
79
+ const started = Date.now()
80
+ const controller = new AbortController()
81
+ inFlightRead = controller
82
+ try {
83
+ deliver(
84
+ await getEvents(lastSeq, {
85
+ waitSeconds: opts.waitSeconds,
86
+ signal: controller.signal,
87
+ }),
88
+ )
89
+ } catch {
90
+ // Store unreachable — retry after the gap (§8.7).
91
+ } finally {
92
+ inFlight = false
93
+ inFlightRead = undefined
94
+ }
95
+ if (stopped) break
96
+ const elapsed = Date.now() - started
97
+ if (elapsed < pollMs) await Bun.sleep(pollMs - elapsed)
98
+ }
99
+ })()
100
+
101
+ return () => {
102
+ stopped = true
103
+ inFlightRead?.abort()
104
+ }
105
+ }
@@ -0,0 +1,466 @@
1
+ /**
2
+ * The BuildStore seam (SPEC §7): the durable home of everything a build
3
+ * produces — events, artifacts, transcripts — one logical place whether
4
+ * builds run locally or in ten remote sandboxes.
5
+ *
6
+ * Deliberately narrow (§7.2): runners need `append`, `putArtifact`,
7
+ * `getArtifact`, `getEvents(since)`; operator UIs add `listBuilds` and
8
+ * `subscribe`. Liveness (lease + heartbeat) is mutable columns, never events
9
+ * (§15.2.6).
10
+ *
11
+ * Every adapter must satisfy the contract suite in `store/contract.ts`.
12
+ */
13
+ import type { AbEvent, EventEnvelope, EventWrite } from '../events/catalog'
14
+ import type { EventType } from '../events/payloads'
15
+ import type {
16
+ RepositoryEvent,
17
+ RepositoryEventEnvelope,
18
+ RepositoryEventType,
19
+ RepositoryEventWrite,
20
+ } from '../events/repository'
21
+ import type {
22
+ SessionEvent,
23
+ SessionEventEnvelope,
24
+ SessionEventType,
25
+ SessionEventWrite,
26
+ } from '../events/sessions'
27
+ import type { TicketRef } from '../ontology'
28
+ import type {
29
+ StreamChunk,
30
+ StreamOutcome,
31
+ StreamPart,
32
+ StreamRead,
33
+ StreamRecord,
34
+ StreamScope,
35
+ } from './streams/types'
36
+ export type { StreamRecord, StreamScope }
37
+
38
+ /** Injectable time source — adapters take one so tests are deterministic. */
39
+ export type Clock = () => Date
40
+
41
+ export const systemClock: Clock = () => new Date()
42
+
43
+ export interface BuildRecord {
44
+ slug: string
45
+ repo: string
46
+ /** Normalized git origin URL of the build's repository, absent when the
47
+ * repo had no origin remote at dispatch time. A location-independent
48
+ * secondary identity: ambient reads may accept a differently located
49
+ * checkout of the same repository by origin equality (repo stays primary). */
50
+ repoOrigin?: string
51
+ ticket?: TicketRef
52
+ branch?: string
53
+ createdAt: string
54
+ updatedAt: string
55
+ /** Runner lease — mutable liveness, never events (§15.2.6). */
56
+ lease?: { holder: string; expiresAt: string }
57
+ heartbeatAt?: string
58
+ }
59
+
60
+ export interface NewBuildInput {
61
+ slug: string
62
+ repo: string
63
+ /** See BuildRecord.repoOrigin. */
64
+ repoOrigin?: string
65
+ ticket?: TicketRef
66
+ branch?: string
67
+ }
68
+
69
+ export interface ArtifactMeta {
70
+ build: string
71
+ kind: string
72
+ /** 0-based per kind: first deposit of a kind is rev 0 (SPEC §6.3). */
73
+ revision: number
74
+ /** Content address: sha256 hex of the blob (SPEC §7.1). */
75
+ blobRef: string
76
+ metadata: Record<string, unknown>
77
+ createdAt: string
78
+ }
79
+
80
+ export interface RepositoryRecord {
81
+ repo: string
82
+ createdAt: string
83
+ updatedAt: string
84
+ /** Repository workflow lease (harvest single-flight), separate from builds. */
85
+ lease?: { holder: string; expiresAt: string }
86
+ heartbeatAt?: string
87
+ }
88
+
89
+ export interface RepositoryArtifactMeta {
90
+ repo: string
91
+ kind: string
92
+ revision: number
93
+ blobRef: string
94
+ metadata: Record<string, unknown>
95
+ createdAt: string
96
+ }
97
+
98
+ export interface RepositoryArtifact {
99
+ meta: RepositoryArtifactMeta
100
+ content: Uint8Array
101
+ }
102
+
103
+ /** An operator session: hosted-only durable orchestrator-conversation state
104
+ * (SPEC §7.1.1). The record carries only identity and timestamps; everything
105
+ * else — status, open turn, pending approval, wake settings — is a reduction
106
+ * of the session's own event log (sessions/reducer.ts), never a column. */
107
+ export interface SessionRecord {
108
+ /** Store-assigned (`os_<uuid>`). */
109
+ id: string
110
+ repo: string
111
+ /** The owning operator's normalized identity (trimmed nonblank — the same
112
+ * normalization `OperatorTokenScope` applies). A session belongs to its
113
+ * creating operator. */
114
+ operator: string
115
+ title?: string
116
+ createdAt: string
117
+ updatedAt: string
118
+ }
119
+
120
+ export interface NewSessionInput {
121
+ repo: string
122
+ operator: string
123
+ title?: string
124
+ }
125
+
126
+ /** The operator identity's one normalization — trimmed, nonblank. The same
127
+ * normalization `OperatorTokenScope` applies to its `user` claim, so a token's
128
+ * user matches the session records it creates. */
129
+ export function normalizeOperator(operator: string): string {
130
+ const trimmed = operator.trim()
131
+ if (!trimmed) throw new Error('operator is required')
132
+ return trimmed
133
+ }
134
+
135
+ export interface SessionArtifactMeta {
136
+ session: string
137
+ kind: string
138
+ /** 0-based per kind, like build and repository artifacts (§6.3). */
139
+ revision: number
140
+ blobRef: string
141
+ metadata: Record<string, unknown>
142
+ createdAt: string
143
+ }
144
+
145
+ export interface SessionArtifact {
146
+ meta: SessionArtifactMeta
147
+ content: Uint8Array
148
+ }
149
+
150
+ export interface ArtifactInput {
151
+ kind: string
152
+ content: string | Uint8Array
153
+ metadata?: Record<string, unknown>
154
+ }
155
+
156
+ export interface Artifact {
157
+ meta: ArtifactMeta
158
+ content: Uint8Array
159
+ }
160
+
161
+ /**
162
+ * Content-addressed blob storage (SPEC §7.1) — a plain directory locally,
163
+ * any object store remotely. The database stores refs, never bulk content.
164
+ * The interface is deliberately this narrow; do not widen it.
165
+ */
166
+ export interface BlobStore {
167
+ put(hash: string, bytes: Uint8Array): Promise<void>
168
+ get(hash: string): Promise<Uint8Array | null>
169
+ }
170
+
171
+ export type Unsubscribe = () => void
172
+
173
+ export interface SubscribeOptions {
174
+ /** Deliver events with seq strictly greater than this (default 0 = all). */
175
+ fromSeq?: number
176
+ /** Poll interval for the v2.0 polling implementation (§7.2). In bounded-wait
177
+ * mode (`waitSeconds` set) it is the floor between request *starts* —
178
+ * elapsed request time counts toward the gap. */
179
+ pollMs?: number
180
+ /** Bounded-wait window in whole seconds. When set against a store whose
181
+ * event reads honor the bound (an `http(s)` store's server holds the
182
+ * request), each poll cycle issues one held request instead of polling
183
+ * every `pollMs`; delivery latency drops to about one server-side poll.
184
+ * The adapter clamps at its own ceiling; it never rejects. */
185
+ waitSeconds?: number
186
+ }
187
+
188
+ export interface BuildStore {
189
+ /** Return an interface-enforced handle with authority over exactly `slug`.
190
+ * The handle retains this shape so foreign/admin calls fail loudly. */
191
+ scopeBuild(slug: string): BuildScopedStore
192
+
193
+ createBuild(input: NewBuildInput): Promise<BuildRecord>
194
+ getBuild(slug: string): Promise<BuildRecord | null>
195
+ listBuilds(): Promise<BuildRecord[]>
196
+
197
+ /**
198
+ * Append one validated event; the store assigns `seq` (per-build,
199
+ * monotonic, starting at 1) and `ts` (§15.1). Writes must pass
200
+ * `validateEventWrite` — invalid events throw `EventValidationError`.
201
+ */
202
+ append<T extends EventType>(slug: string, event: EventWrite<T>): Promise<EventEnvelope<T>>
203
+
204
+ /**
205
+ * Atomically append one validated event only when the build stream's
206
+ * current sequence equals `expectedSeq` (0 for an empty stream). Returns
207
+ * null when the stream has advanced. A comparison miss must not mutate the
208
+ * stream or build timestamps. Invalid events and unknown builds reject just
209
+ * as they do for `append`.
210
+ */
211
+ appendIfCurrent<T extends EventType>(
212
+ slug: string,
213
+ expectedSeq: number,
214
+ event: EventWrite<T>,
215
+ ): Promise<EventEnvelope<T> | null>
216
+
217
+ /**
218
+ * Atomic deposit (D6): store artifacts, then append the event that
219
+ * references them — one operation, no state where an artifact exists
220
+ * without its event or vice versa. `makeEvent` receives the deposited
221
+ * metas (with assigned revisions) so the payload can carry `{kind, rev}`
222
+ * refs. If the event fails validation, the artifact deposit is rolled
223
+ * back (orphaned blobs are harmless — they are content-addressed).
224
+ *
225
+ * Ordering invariant (AUT-322): the adapter must run `validateEventWrite`
226
+ * on the batch event *before* any retention prune (store/retention.ts) of
227
+ * artifacts in the same deposit batch, so a same-kind batch larger than
228
+ * the retention bound never loses a sibling to a prune that outran
229
+ * validation; on validation failure the batch — deposits and prunes —
230
+ * must leave no trace. `revisionsToPrune` is a pure function of the full
231
+ * post-batch revision set, so pruning once per distinct batch kind after
232
+ * validation is equivalent to per-deposit pruning.
233
+ */
234
+ appendWithArtifacts<T extends EventType>(
235
+ slug: string,
236
+ artifacts: ArtifactInput[],
237
+ makeEvent: (deposited: ArtifactMeta[]) => EventWrite<T>,
238
+ ): Promise<{ event: EventEnvelope<T>; artifacts: ArtifactMeta[] }>
239
+
240
+ /** Events with seq strictly greater than `sinceSeq` (default 0), in order.
241
+ * When nothing newer exists and `opts.waitSeconds` is given (whole
242
+ * seconds; clamped at the adapter's ceiling, never rejected), the adapter
243
+ * honors the bound: it returns no later than the bound and as soon as an
244
+ * event with `seq > sinceSeq` is appended; when such events already exist
245
+ * it returns immediately. Omitting `waitSeconds` (or 0) is the immediate
246
+ * form.
247
+ *
248
+ * `opts.signal` lets a caller cancel the held read before its bound (a
249
+ * watcher or a disconnected peer torn down mid-hold). When the signal
250
+ * fires, the adapter resolves with its current result — the empty read it
251
+ * would otherwise keep polling for — and stops polling; it MUST NOT wait
252
+ * past the bound because of the signal, and a read that finds data before
253
+ * the abort still returns it. */
254
+ getEvents(
255
+ slug: string,
256
+ sinceSeq?: number,
257
+ opts?: { waitSeconds?: number; signal?: AbortSignal },
258
+ ): Promise<AbEvent[]>
259
+
260
+ putArtifact(slug: string, artifact: ArtifactInput): Promise<ArtifactMeta>
261
+ /** Latest revision when `rev` is omitted; null if kind (or rev) absent. */
262
+ getArtifact(slug: string, kind: string, rev?: number): Promise<Artifact | null>
263
+ listArtifacts(slug: string, kind?: string): Promise<ArtifactMeta[]>
264
+
265
+ /**
266
+ * Take or renew the runner lease. Succeeds when unheld, expired, or
267
+ * already held by `holder` (renewal). Claiming is how a new sandbox takes
268
+ * over a dead one's build (§7.4, §15.6-C).
269
+ */
270
+ claimLease(slug: string, holder: string, ttlMs: number): Promise<boolean>
271
+ /** Bump liveness; false (no-op) unless `holder` holds an unexpired lease. */
272
+ heartbeat(slug: string, holder: string): Promise<boolean>
273
+ releaseLease(slug: string, holder: string): Promise<void>
274
+
275
+ /**
276
+ * Push is the specced interface; the v2.0 implementation is polling
277
+ * `getEvents(since)` (§7.2). Events are delivered in seq order, each
278
+ * exactly once per subscription.
279
+ */
280
+ subscribe(slug: string, opts: SubscribeOptions, onEvent: (event: AbEvent) => void): Unsubscribe
281
+
282
+ // ── Repository journal (outer-loop workflows and controls) ───────────────
283
+ // Kept alongside, not inside, build streams: repository state is not a build.
284
+ ensureRepo(repo: string): Promise<RepositoryRecord>
285
+ getRepo(repo: string): Promise<RepositoryRecord | null>
286
+ appendRepo<T extends RepositoryEventType>(
287
+ repo: string,
288
+ event: RepositoryEventWrite<T>,
289
+ ): Promise<RepositoryEventEnvelope<T>>
290
+ appendRepoWithArtifacts<T extends RepositoryEventType>(
291
+ repo: string,
292
+ artifacts: ArtifactInput[],
293
+ makeEvent: (deposited: RepositoryArtifactMeta[]) => RepositoryEventWrite<T>,
294
+ ): Promise<{
295
+ event: RepositoryEventEnvelope<T>
296
+ artifacts: RepositoryArtifactMeta[]
297
+ }>
298
+ /** Same atomic-deposit and ordering contracts as `appendWithArtifacts`:
299
+ * the batch event is validated before any retention prune of artifacts in
300
+ * the same deposit batch (AUT-322), and an invalid event leaves no trace.
301
+ * The optional bounded wait mirrors `getEvents` exactly, including the
302
+ * resolve-with-current-result abort contract of `opts.signal`. */
303
+ getRepoEvents(
304
+ repo: string,
305
+ sinceSeq?: number,
306
+ opts?: { waitSeconds?: number; signal?: AbortSignal },
307
+ ): Promise<RepositoryEvent[]>
308
+ putRepoArtifact(repo: string, artifact: ArtifactInput): Promise<RepositoryArtifactMeta>
309
+ getRepoArtifact(repo: string, kind: string, rev?: number): Promise<RepositoryArtifact | null>
310
+ listRepoArtifacts(repo: string, kind?: string): Promise<RepositoryArtifactMeta[]>
311
+ claimRepoLease(repo: string, holder: string, ttlMs: number): Promise<boolean>
312
+ heartbeatRepo(repo: string, holder: string): Promise<boolean>
313
+ releaseRepoLease(repo: string, holder: string): Promise<void>
314
+
315
+ // ── Operator sessions (SPEC §7.1.1 — a third resource kind) ────────────
316
+ // Hosted-only durable orchestrator-conversation state: a record keyed by
317
+ // repository and operator, its own small typed event catalog
318
+ // (events/sessions.ts) with a per-session sequence, and session-scoped
319
+ // streams. The record and its first fact (`session.created`) land together;
320
+ // every later fact appends through the same validated gate as builds and
321
+ // the repository journal. Nothing local creates sessions; adapters merely
322
+ // implement the contract uniformly.
323
+ /** Create the record and atomically append `session.created` (seq 1,
324
+ * actor the operator). Unlike builds, nothing else ever creates sessions. */
325
+ createSession(input: NewSessionInput): Promise<SessionRecord>
326
+ getSession(id: string): Promise<SessionRecord | null>
327
+ /** Every session of `repo` in creation order: `createdAt` ascending, with
328
+ * same-millisecond ties broken by a store-assigned monotonic creation
329
+ * sequence — never by the random `os_<uuid>` id. The counter is assigned at
330
+ * `createSession`, is never reused (sessions are never deleted), and is not
331
+ * part of `SessionRecord`. This makes the returned order a function of the
332
+ * creation history alone, so every adapter (memory, SQLite, Postgres,
333
+ * remote) returns the same order for the same creation history. */
334
+ listSessions(repo: string): Promise<SessionRecord[]>
335
+ /** Append one validated session event; the store assigns the per-session
336
+ * `seq` (monotonic, from 1) and `ts`. Unknown sessions reject; invalid
337
+ * events throw `EventValidationError` and append nothing. */
338
+ appendSessionEvent<T extends SessionEventType>(
339
+ id: string,
340
+ event: SessionEventWrite<T>,
341
+ ): Promise<SessionEventEnvelope<T>>
342
+ /** Events with seq strictly greater than `sinceSeq` (default 0), in order.
343
+ * When nothing newer exists, an adapter honors `waitSeconds` with the
344
+ * same clamp/early-return rules as stream reads (§7.6): whole seconds,
345
+ * above 30 clamped to 30, returning as soon as an event lands.
346
+ * `opts.signal` carries the same resolve-with-current-result abort
347
+ * contract as `getEvents`: on abort the adapter stops polling and resolves
348
+ * with its current result, never waiting past the bound. */
349
+ getSessionEvents(
350
+ id: string,
351
+ sinceSeq?: number,
352
+ opts?: { waitSeconds?: number; signal?: AbortSignal },
353
+ ): Promise<SessionEvent[]>
354
+ /** Same atomic-deposit and validation-before-prune contracts as
355
+ * `appendWithArtifacts` (D6, AUT-322): the batch event is validated before
356
+ * any retention prune of artifacts in the same deposit batch, and an
357
+ * invalid event leaves no trace. */
358
+ appendSessionWithArtifacts<T extends SessionEventType>(
359
+ id: string,
360
+ artifacts: ArtifactInput[],
361
+ makeEvent: (deposited: SessionArtifactMeta[]) => SessionEventWrite<T>,
362
+ ): Promise<{ event: SessionEventEnvelope<T>; artifacts: SessionArtifactMeta[] }>
363
+ putSessionArtifact(id: string, artifact: ArtifactInput): Promise<SessionArtifactMeta>
364
+ /** Latest revision when `rev` is omitted; null if kind (or rev) absent. */
365
+ getSessionArtifact(id: string, kind: string, rev?: number): Promise<SessionArtifact | null>
366
+ listSessionArtifacts(id: string, kind?: string): Promise<SessionArtifactMeta[]>
367
+ /** Return an interface-enforced handle with authority over exactly one
368
+ * session: its record, events, artifacts, and session-scoped streams. */
369
+ scopeSession(id: string): SessionScopedStore
370
+
371
+ // ── Streams (SPEC §7.6 — the third primitive) ───────────────────────────
372
+ // An append-only, per-stream sequenced log of protocol parts with an
373
+ // open-then-closed lifecycle that finalizes into an artifact. Presentation,
374
+ // never routing: no kernel, engine, reducer, or dispatcher decision reads
375
+ // stream content; outcomes travel only the typed CLI. Chunk vocabulary:
376
+ // the AI SDK UI Message Stream protocol (`ai-ui-message-stream/v1`).
377
+ //
378
+ // Stream ids are store-assigned (`st_<uuid>`); the scope is fixed at
379
+ // create and every operation addresses the stream by id.
380
+
381
+ /** Create an open stream under `scope`. Store-assigned id and
382
+ * `createdAt`; `format` is the literal `ai-ui-message-stream/v1`. */
383
+ createStream(scope: StreamScope, label: string): Promise<StreamRecord>
384
+
385
+ /** Append one batch of parts. Each part must be a JSON object whose `type`
386
+ * is a nonempty string; the store performs no further protocol validation
387
+ * on append. The store assigns the per-stream sequence (from 1) and the
388
+ * timestamp. A batch whose serialized size exceeds 1,048,576 bytes rejects
389
+ * with `StreamBatchTooLargeError` without mutation; a closed or unknown
390
+ * stream rejects (`StreamClosedError` / `Error`) and writes nothing. */
391
+ appendStreamParts(streamId: string, parts: StreamPart[]): Promise<StreamChunk>
392
+
393
+ /** Chunks with sequence strictly greater than `since` (default 0), in
394
+ * order, plus the stream's current status and, when closed, its outcome
395
+ * and artifact reference. When no newer chunk exists and the stream is
396
+ * open, an adapter honors `waitSeconds` (whole seconds; above 30 clamped
397
+ * to 30): it returns no later than the bound and as soon as a chunk is
398
+ * appended or the stream closes. Closed streams never wait.
399
+ * `opts.signal` carries the same resolve-with-current-result abort
400
+ * contract as `getEvents`: on abort the adapter stops polling and resolves
401
+ * with its current result, never waiting past the bound. */
402
+ readStream(
403
+ streamId: string,
404
+ opts?: { since?: number; waitSeconds?: number; signal?: AbortSignal },
405
+ ): Promise<StreamRead>
406
+
407
+ /** Close with an outcome. One atomic operation: assemble the chunks into
408
+ * the protocol's `UIMessage[]` document, deposit it as an artifact
409
+ * (`stream:<streamId>`, revision 0) on the owning scope, and mark the
410
+ * stream closed. If the artifact deposit fails the stream stays open and
411
+ * nothing is written. Closing an already-closed stream is a no-op
412
+ * returning the record. */
413
+ closeStream(streamId: string, outcome: StreamOutcome): Promise<StreamRecord>
414
+
415
+ /** The stream record, or null when the id is unknown. */
416
+ getStream(streamId: string): Promise<StreamRecord | null>
417
+
418
+ /** Every stream in `scope` in creation order: `createdAt` ascending, with
419
+ * same-millisecond ties broken by a store-assigned monotonic creation
420
+ * sequence — never by the random `st_<uuid>` id. The counter is assigned at
421
+ * `createStream`, is never reused (streams are never deleted — retention
422
+ * prunes `stream_chunks` only), and is not part of `StreamRecord`. This
423
+ * makes the returned order a function of the creation history alone, so
424
+ * every adapter (memory, SQLite, Postgres, remote) returns the same order
425
+ * for the same creation history. */
426
+ listStreams(scope: StreamScope): Promise<StreamRecord[]>
427
+
428
+ close(): Promise<void>
429
+ }
430
+
431
+ /** A BuildStore constrained to one build stream. `buildScope` is immutable;
432
+ * attempting to re-scope to another slug is an authority error. */
433
+ export interface BuildScopedStore extends BuildStore {
434
+ readonly buildScope: string
435
+ scopeBuild(slug: string): BuildScopedStore
436
+ }
437
+
438
+ /** A BuildStore constrained to one operator session. `sessionScope` is
439
+ * immutable; the handle may touch only its own session's events, artifacts,
440
+ * and session-scoped streams, and rejects session create/list and every
441
+ * build/repository operation. This is the (later turn runner's) authority
442
+ * over exactly one session, mirroring `BuildScopedStore`. */
443
+ export interface SessionScopedStore extends BuildStore {
444
+ readonly sessionScope: string
445
+ scopeSession(id: string): SessionScopedStore
446
+ }
447
+
448
+ /** Shared runtime guard for in-process conditional append callers. */
449
+ export function validateExpectedSeq(expectedSeq: number): void {
450
+ if (!Number.isInteger(expectedSeq) || expectedSeq < 0) {
451
+ throw new Error(`expectedSeq must be a nonnegative integer, got ${expectedSeq}`)
452
+ }
453
+ }
454
+
455
+ /** sha256 hex — the content address for blobs (SPEC §7.1). */
456
+ export function contentHash(bytes: Uint8Array): string {
457
+ return new Bun.CryptoHasher('sha256').update(bytes).digest('hex')
458
+ }
459
+
460
+ export function toBytes(content: string | Uint8Array): Uint8Array {
461
+ return typeof content === 'string' ? new TextEncoder().encode(content) : content
462
+ }
463
+
464
+ export function textContent(artifact: Artifact): string {
465
+ return new TextDecoder().decode(artifact.content)
466
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Deterministic test doubles shared across the suite. Production code never
3
+ * imports from `src/testing/`.
4
+ */
5
+ import type { Clock } from '../store/types'
6
+
7
+ /**
8
+ * A clock that starts at `start` and advances `stepMs` per call — so every
9
+ * store-assigned `ts` in a test is distinct and predictable.
10
+ */
11
+ export function steppingClock(
12
+ start = '2026-07-15T12:00:00.000Z',
13
+ stepMs = 1000,
14
+ ): Clock & { advance(ms: number): void } {
15
+ let now = new Date(start).getTime()
16
+ const clock = (() => {
17
+ const current = new Date(now)
18
+ now += stepMs
19
+ return current
20
+ }) as Clock & { advance(ms: number): void }
21
+ clock.advance = (ms: number) => {
22
+ now += ms
23
+ }
24
+ return clock
25
+ }
26
+
27
+ /** A clock frozen at `at` until manually advanced. */
28
+ export function manualClock(at = '2026-07-15T12:00:00.000Z'): Clock & {
29
+ advance(ms: number): void
30
+ set(iso: string): void
31
+ } {
32
+ let now = new Date(at).getTime()
33
+ const clock = (() => new Date(now)) as Clock & {
34
+ advance(ms: number): void
35
+ set(iso: string): void
36
+ }
37
+ clock.advance = (ms: number) => {
38
+ now += ms
39
+ }
40
+ clock.set = (iso: string) => {
41
+ now = new Date(iso).getTime()
42
+ }
43
+ return clock
44
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Store decorators that inject deterministic failures. Nothing here involves
3
+ * timing: a decorator fails one named operation and delegates the rest, so a
4
+ * test can pin exactly what a surviving PREFIX of a multi-write action leaves
5
+ * behind — which is the whole reason write ORDER is load-bearing in actions
6
+ * the store cannot make atomic.
7
+ *
8
+ * Production code never imports from `src/testing/`.
9
+ */
10
+ import type {
11
+ RepositoryEventEnvelope,
12
+ RepositoryEventType,
13
+ RepositoryEventWrite,
14
+ } from '../events/repository'
15
+ import type { BuildStore } from '../store/types'
16
+
17
+ /**
18
+ * Fail every `appendRepo` of one repository event type; delegate everything
19
+ * else to `store`. The `Object.create` idiom keeps the decorator a real
20
+ * `BuildStore` without restating the port.
21
+ */
22
+ export function withFailingRepoAppend(store: BuildStore, failOn: RepositoryEventType): BuildStore {
23
+ const proxy = Object.create(store) as BuildStore
24
+ proxy.appendRepo = async <T extends RepositoryEventType>(
25
+ repo: string,
26
+ event: RepositoryEventWrite<T>,
27
+ ): Promise<RepositoryEventEnvelope<T>> => {
28
+ if (event.type === failOn) throw new Error(`store failure writing ${failOn}`)
29
+ return store.appendRepo(repo, event)
30
+ }
31
+ return proxy
32
+ }