@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,137 @@
1
+ /**
2
+ * HMAC-scoped store tokens (SPEC §8.1 [D8]): "the token is scoped to this
3
+ * build and session — an agent physically cannot append to another build's
4
+ * log or read another build's artifacts". Least privilege comes from the
5
+ * runner minting a narrow token, not from prompt instructions.
6
+ *
7
+ * A token is `base64url(JSON scope) + '.' + base64url(hmac-sha256(secret,
8
+ * payload))`. The scope IS the token — the server keeps no session state;
9
+ * it verifies the signature (timing-safe) and the expiry, then trusts the
10
+ * embedded scope.
11
+ */
12
+ import { createHmac, timingSafeEqual } from 'node:crypto'
13
+ import { z } from 'zod'
14
+ import { viaSchema, type Via } from '../../events/envelope'
15
+
16
+ export interface LegacyBuildTokenScope {
17
+ /** Retained wire compatibility for all existing build-scoped tokens. */
18
+ build: string
19
+ session: string
20
+ exp: number
21
+ }
22
+
23
+ export interface ResourceTokenScope {
24
+ resource: {
25
+ kind: 'build' | 'repo' | 'session'
26
+ id: string
27
+ }
28
+ session: string
29
+ exp: number
30
+ /** Delegated-write attribution (§15.1): when present, the hosted service
31
+ * stamps this via onto human-actor event writes and rejects writes claiming
32
+ * a via the token does not carry. A session resource token's authority is
33
+ * exactly its one session. */
34
+ via?: Via
35
+ }
36
+
37
+ /** Human operator authority is deliberately disjoint from raw store authority.
38
+ * The signed nonblank user claim attributes every operator API event. An
39
+ * unscoped operator token carries no via claim, so any write claiming a via
40
+ * under it is rejected. */
41
+ export interface OperatorTokenScope {
42
+ operator: { user: string }
43
+ exp: number
44
+ via?: Via
45
+ }
46
+
47
+ /** Deployment authority used by dispatchers for raw store and hosted ticket
48
+ * routes. It intentionally does not grant access to attributed operator APIs. */
49
+ export interface DeploymentTokenScope {
50
+ operator: true
51
+ session: string
52
+ exp: number
53
+ }
54
+
55
+ /** Legacy scopes remain valid on their existing routes. */
56
+ export type TokenScope =
57
+ | LegacyBuildTokenScope
58
+ | ResourceTokenScope
59
+ | OperatorTokenScope
60
+ | DeploymentTokenScope
61
+
62
+ const commonScope = {
63
+ session: z.string().min(1),
64
+ exp: z.number().int(),
65
+ }
66
+ const tokenScopeSchema = z.union([
67
+ z.strictObject({ build: z.string().min(1), ...commonScope }),
68
+ z.strictObject({
69
+ resource: z.strictObject({
70
+ kind: z.enum(['build', 'repo', 'session']),
71
+ id: z.string().min(1),
72
+ }),
73
+ ...commonScope,
74
+ via: viaSchema.optional(),
75
+ }),
76
+ z.strictObject({
77
+ operator: z.strictObject({ user: z.string().trim().min(1) }),
78
+ exp: z.number().int(),
79
+ via: viaSchema.optional(),
80
+ }),
81
+ z.strictObject({ operator: z.literal(true), ...commonScope }),
82
+ ])
83
+
84
+ export function tokenResource(scope: TokenScope): {
85
+ kind: 'build' | 'repo' | 'session' | 'admin' | 'operator' | 'deployment'
86
+ id: string
87
+ } {
88
+ if ('operator' in scope) {
89
+ return scope.operator === true
90
+ ? { kind: 'deployment', id: '*' }
91
+ : { kind: 'operator', id: scope.operator.user }
92
+ }
93
+ if ('build' in scope) {
94
+ return scope.build === '*' ? { kind: 'admin', id: '*' } : { kind: 'build', id: scope.build }
95
+ }
96
+ return scope.resource
97
+ }
98
+
99
+ function sign(secret: string, payload: string): Buffer {
100
+ return createHmac('sha256', secret).update(payload).digest()
101
+ }
102
+
103
+ export function mintToken(secret: string, scope: TokenScope): string {
104
+ const payload = Buffer.from(JSON.stringify(scope), 'utf8').toString('base64url')
105
+ return `${payload}.${sign(secret, payload).toString('base64url')}`
106
+ }
107
+
108
+ /**
109
+ * Bad signature, malformed token, unparseable scope, or expired → null —
110
+ * verification never explains itself to the caller (the server maps null to
111
+ * 401). The signature compare is timing-safe via `crypto.timingSafeEqual`.
112
+ */
113
+ export function verifyToken(secret: string, token: string, now: Date): TokenScope | null {
114
+ const parts = token.split('.')
115
+ if (parts.length !== 2) return null
116
+ const [payload, signature] = parts as [string, string]
117
+ const expected = sign(secret, payload)
118
+ let provided: Buffer
119
+ try {
120
+ provided = Buffer.from(signature, 'base64url')
121
+ } catch {
122
+ return null
123
+ }
124
+ if (provided.length !== expected.length || !timingSafeEqual(provided, expected)) {
125
+ return null
126
+ }
127
+ let raw: unknown
128
+ try {
129
+ raw = JSON.parse(Buffer.from(payload, 'base64url').toString('utf8'))
130
+ } catch {
131
+ return null
132
+ }
133
+ const scope = tokenScopeSchema.safeParse(raw)
134
+ if (!scope.success) return null
135
+ if (scope.data.exp <= now.getTime()) return null
136
+ return scope.data
137
+ }
@@ -0,0 +1,10 @@
1
+ import manifest from '../../../../../package.json'
2
+
3
+ /** Release-synchronized identity sent by every remote-store client. */
4
+ export const AUTOBUILD_VERSION = manifest.version
5
+
6
+ /** Wire compatibility version for the remote BuildStore protocol. */
7
+ export const REMOTE_STORE_PROTOCOL_VERSION = '2'
8
+
9
+ export const AUTOBUILD_VERSION_HEADER = 'x-autobuild-version'
10
+ export const REMOTE_STORE_PROTOCOL_VERSION_HEADER = 'x-autobuild-protocol-version'
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Artifact retention policy for the dispatcher run/config artifact family.
3
+ *
4
+ * Every dispatcher run start and config reload deposits a
5
+ * `dispatcher-effective-config` (and on reload a `dispatcher-config`)
6
+ * repository artifact; each build launch deposits a
7
+ * `build-runner-effective-config` build artifact. Without a bound these rows
8
+ * grow without limit (~1440/day/repo for a per-minute hosted dispatcher cron).
9
+ *
10
+ * The store contract (`store/types.ts`) has no delete, and the remote-store
11
+ * protocol deliberately exposes no deletion endpoint — the artifact-store
12
+ * schema and transport are out of bounds for retention. The policy is
13
+ * therefore enforced **inside each store adapter at the deposit path**: when a
14
+ * new revision of a retention-managed kind is deposited, the adapter keeps the
15
+ * latest `maxRevisions` revisions of that `(scope, kind)` and deletes the
16
+ * older ones in the same transaction, under the same serialization as the
17
+ * deposit itself.
18
+ *
19
+ * The rule is count-based, not age-based: revision sequences already order by
20
+ * deposit recency, so recency is preserved without a clock in the decision,
21
+ * the boundary is deterministic, and an age rule would still only ever fire on
22
+ * a deposit (there is no sweeper). Events and journal entries are never
23
+ * pruned — historical payloads may reference a revision that no longer
24
+ * resolves; the current run's snapshot is always among the newest deposits.
25
+ */
26
+
27
+ /**
28
+ * Operator notes artifact kind (`operator/registry.ts`). The agent-facing
29
+ * notes tools read and write this repository artifact kind; it is
30
+ * retention-managed so an operator agent that revises its notes on every
31
+ * round cannot grow the repository artifact table without limit. The bound
32
+ * is the store's configured `maxRevisions` — the same deposit-time prune
33
+ * the dispatcher config family uses — whose default is
34
+ * `DEFAULT_ARTIFACT_RETENTION_MAX_REVISIONS` (the newest 200 revisions
35
+ * survive). Defined here rather than in the registry module for the same
36
+ * reason the dispatcher kinds are: store adapters must not import from the
37
+ * operator surface (store → operator would be an import cycle).
38
+ */
39
+ export const OPERATOR_NOTES_REPO_KIND = 'operator-notes'
40
+
41
+ /**
42
+ * Repository-scoped artifact kinds subject to retention. Values mirror the
43
+ * kind constants in `cli/dispatch.ts` (DISPATCHER_EFFECTIVE_CONFIG_ARTIFACT)
44
+ * and `config/live.ts` (DISPATCHER_CONFIG_ARTIFACT); they are restated as
45
+ * literals here because the store adapters must not import from the CLI
46
+ * (store → cli would be an import cycle).
47
+ */
48
+ export const DISPATCHER_RETENTION_REPO_KINDS = [
49
+ 'dispatcher-effective-config',
50
+ 'dispatcher-config',
51
+ ] as const
52
+
53
+ /** Repository-scoped kinds retained for the operator surface (see above). */
54
+ export const OPERATOR_RETENTION_REPO_KINDS = [OPERATOR_NOTES_REPO_KIND] as const
55
+
56
+ /**
57
+ * Build-scoped artifact kinds subject to retention. Value mirrors
58
+ * `BUILD_EFFECTIVE_CONFIG_ARTIFACT` in `processes/build-execution-state.ts`.
59
+ */
60
+ export const DISPATCHER_RETENTION_BUILD_KINDS = ['build-runner-effective-config'] as const
61
+
62
+ /** Default bound: the newest 200 revisions of each retention-managed kind survive. */
63
+ export const DEFAULT_ARTIFACT_RETENTION_MAX_REVISIONS = 200
64
+
65
+ /** True when deposits of `kind` are retention-managed (pruned past the bound). */
66
+ export function isRetentionManagedKind(kind: string): boolean {
67
+ return (
68
+ (DISPATCHER_RETENTION_REPO_KINDS as readonly string[]).includes(kind) ||
69
+ (OPERATOR_RETENTION_REPO_KINDS as readonly string[]).includes(kind) ||
70
+ (DISPATCHER_RETENTION_BUILD_KINDS as readonly string[]).includes(kind)
71
+ )
72
+ }
73
+
74
+ /**
75
+ * The pure pruning decision every adapter (and every test) shares: given the
76
+ * revision numbers that currently exist for one `(scope, kind)`, return the
77
+ * ones beyond the newest `maxRevisions` — exactly the revisions to delete.
78
+ * Pure and side-effect-free; returns [] when nothing is past the bound.
79
+ */
80
+ export function revisionsToPrune(revisions: readonly number[], maxRevisions: number): number[] {
81
+ if (!Number.isInteger(maxRevisions) || maxRevisions < 1) {
82
+ throw new Error(`maxRevisions must be a positive integer, got ${maxRevisions}`)
83
+ }
84
+ for (const revision of revisions) {
85
+ if (!Number.isInteger(revision) || revision < 0) {
86
+ throw new Error(`revisions must be nonnegative integers, got ${revision}`)
87
+ }
88
+ }
89
+ const newestFirst = [...revisions].sort((a, b) => b - a)
90
+ return newestFirst.slice(maxRevisions).sort((a, b) => a - b)
91
+ }
@@ -0,0 +1,265 @@
1
+ import type { EventType } from '../events/payloads'
2
+ import type { EventWrite } from '../events/catalog'
3
+ import type { RepositoryEventType, RepositoryEventWrite } from '../events/repository'
4
+ import type { AbEvent, EventEnvelope } from '../events/catalog'
5
+ import type { RepositoryEvent, RepositoryEventEnvelope } from '../events/repository'
6
+ import type {
7
+ SessionEvent,
8
+ SessionEventEnvelope,
9
+ SessionEventType,
10
+ SessionEventWrite,
11
+ } from '../events/sessions'
12
+ import type {
13
+ Artifact,
14
+ ArtifactInput,
15
+ ArtifactMeta,
16
+ BuildRecord,
17
+ BuildStore,
18
+ NewBuildInput,
19
+ RepositoryArtifact,
20
+ RepositoryArtifactMeta,
21
+ RepositoryRecord,
22
+ SessionArtifact,
23
+ SessionArtifactMeta,
24
+ SessionRecord,
25
+ SessionScopedStore,
26
+ SubscribeOptions,
27
+ Unsubscribe,
28
+ } from './types'
29
+ import type {
30
+ StreamChunk,
31
+ StreamOutcome,
32
+ StreamPart,
33
+ StreamRead,
34
+ StreamRecord,
35
+ StreamScope,
36
+ } from './streams/types'
37
+
38
+ /** Interface-level authority failure for a session-scoped handle — the
39
+ * session counterpart of `BuildScopeError`. */
40
+ export class SessionScopeError extends Error {
41
+ constructor(
42
+ readonly scope: string,
43
+ readonly operation: string,
44
+ readonly target?: string,
45
+ ) {
46
+ super(
47
+ target === undefined
48
+ ? `session-scoped store for ${JSON.stringify(scope)} forbids ${operation}`
49
+ : `session-scoped store for ${JSON.stringify(scope)} forbids ${operation} targeting ${JSON.stringify(target)}`,
50
+ )
51
+ this.name = 'SessionScopeError'
52
+ }
53
+ }
54
+
55
+ /** Wrap a full store with one-session authority. The wrapper keeps the
56
+ * complete BuildStore shape so accidental session create/list, collection,
57
+ * build, or repository calls fail loudly instead of disappearing behind a
58
+ * type assertion. A session-scoped handle may touch only its own session's
59
+ * record, events, artifacts, and session-scoped streams. */
60
+ export function createSessionScopedStore(store: BuildStore, scope: string): SessionScopedStore {
61
+ const own = (operation: string, id: string): void => {
62
+ if (id !== scope) throw new SessionScopeError(scope, operation, id)
63
+ }
64
+ const ownStreamScope = (record: StreamRecord): void => {
65
+ if (record.scope.kind !== 'session' || record.scope.session !== scope) {
66
+ throw new SessionScopeError(scope, 'stream', record.id)
67
+ }
68
+ }
69
+ const ownStream = async (streamId: string): Promise<void> => {
70
+ const record = await store.getStream(streamId)
71
+ if (record) ownStreamScope(record)
72
+ }
73
+ const sessionScope = (operation: string, candidate: StreamScope): void => {
74
+ if (candidate.kind !== 'session' || candidate.session !== scope) {
75
+ throw new SessionScopeError(scope, operation, JSON.stringify(candidate))
76
+ }
77
+ }
78
+ return {
79
+ sessionScope: scope,
80
+ scopeSession(id: string): SessionScopedStore {
81
+ own('scopeSession', id)
82
+ return this
83
+ },
84
+ scopeBuild(_slug: string): never {
85
+ throw new SessionScopeError(scope, 'scopeBuild')
86
+ },
87
+ createBuild(_input: NewBuildInput): Promise<BuildRecord> {
88
+ return Promise.reject(new SessionScopeError(scope, 'createBuild'))
89
+ },
90
+ getBuild(slug: string): Promise<BuildRecord | null> {
91
+ return Promise.reject(new SessionScopeError(scope, 'getBuild', slug))
92
+ },
93
+ listBuilds(): Promise<BuildRecord[]> {
94
+ return Promise.reject(new SessionScopeError(scope, 'listBuilds'))
95
+ },
96
+ append<T extends EventType>(slug: string, _event: EventWrite<T>): Promise<EventEnvelope<T>> {
97
+ return Promise.reject(new SessionScopeError(scope, 'append', slug))
98
+ },
99
+ appendIfCurrent<T extends EventType>(
100
+ slug: string,
101
+ _expectedSeq: number,
102
+ _event: EventWrite<T>,
103
+ ): Promise<EventEnvelope<T> | null> {
104
+ return Promise.reject(new SessionScopeError(scope, 'appendIfCurrent', slug))
105
+ },
106
+ appendWithArtifacts<T extends EventType>(
107
+ slug: string,
108
+ _artifacts: ArtifactInput[],
109
+ _makeEvent: (deposited: ArtifactMeta[]) => EventWrite<T>,
110
+ ): Promise<{ event: EventEnvelope<T>; artifacts: ArtifactMeta[] }> {
111
+ return Promise.reject(new SessionScopeError(scope, 'appendWithArtifacts', slug))
112
+ },
113
+ getEvents(slug: string, _sinceSeq?: number): Promise<AbEvent[]> {
114
+ return Promise.reject(new SessionScopeError(scope, 'getEvents', slug))
115
+ },
116
+ putArtifact(slug: string, _artifact: ArtifactInput): Promise<ArtifactMeta> {
117
+ return Promise.reject(new SessionScopeError(scope, 'putArtifact', slug))
118
+ },
119
+ getArtifact(slug: string, _kind: string, _rev?: number): Promise<Artifact | null> {
120
+ return Promise.reject(new SessionScopeError(scope, 'getArtifact', slug))
121
+ },
122
+ listArtifacts(slug: string, _kind?: string): Promise<ArtifactMeta[]> {
123
+ return Promise.reject(new SessionScopeError(scope, 'listArtifacts', slug))
124
+ },
125
+ claimLease(slug: string, _holder: string, _ttlMs: number): Promise<boolean> {
126
+ return Promise.reject(new SessionScopeError(scope, 'claimLease', slug))
127
+ },
128
+ heartbeat(slug: string, _holder: string): Promise<boolean> {
129
+ return Promise.reject(new SessionScopeError(scope, 'heartbeat', slug))
130
+ },
131
+ releaseLease(slug: string, _holder: string): Promise<void> {
132
+ return Promise.reject(new SessionScopeError(scope, 'releaseLease', slug))
133
+ },
134
+ subscribe(
135
+ slug: string,
136
+ _opts: SubscribeOptions,
137
+ _onEvent: (event: AbEvent) => void,
138
+ ): Unsubscribe {
139
+ throw new SessionScopeError(scope, 'subscribe', slug)
140
+ },
141
+ ensureRepo(repo: string): Promise<RepositoryRecord> {
142
+ return Promise.reject(new SessionScopeError(scope, 'ensureRepo', repo))
143
+ },
144
+ getRepo(repo: string): Promise<RepositoryRecord | null> {
145
+ return Promise.reject(new SessionScopeError(scope, 'getRepo', repo))
146
+ },
147
+ appendRepo<T extends RepositoryEventType>(
148
+ repo: string,
149
+ _event: RepositoryEventWrite<T>,
150
+ ): Promise<RepositoryEventEnvelope<T>> {
151
+ return Promise.reject(new SessionScopeError(scope, 'appendRepo', repo))
152
+ },
153
+ appendRepoWithArtifacts<T extends RepositoryEventType>(
154
+ repo: string,
155
+ _artifacts: ArtifactInput[],
156
+ _makeEvent: (deposited: RepositoryArtifactMeta[]) => RepositoryEventWrite<T>,
157
+ ): Promise<{ event: RepositoryEventEnvelope<T>; artifacts: RepositoryArtifactMeta[] }> {
158
+ return Promise.reject(new SessionScopeError(scope, 'appendRepoWithArtifacts', repo))
159
+ },
160
+ getRepoEvents(repo: string, _sinceSeq?: number): Promise<RepositoryEvent[]> {
161
+ return Promise.reject(new SessionScopeError(scope, 'getRepoEvents', repo))
162
+ },
163
+ putRepoArtifact(repo: string, _artifact: ArtifactInput): Promise<RepositoryArtifactMeta> {
164
+ return Promise.reject(new SessionScopeError(scope, 'putRepoArtifact', repo))
165
+ },
166
+ getRepoArtifact(
167
+ repo: string,
168
+ _kind: string,
169
+ _rev?: number,
170
+ ): Promise<RepositoryArtifact | null> {
171
+ return Promise.reject(new SessionScopeError(scope, 'getRepoArtifact', repo))
172
+ },
173
+ listRepoArtifacts(repo: string, _kind?: string): Promise<RepositoryArtifactMeta[]> {
174
+ return Promise.reject(new SessionScopeError(scope, 'listRepoArtifacts', repo))
175
+ },
176
+ claimRepoLease(repo: string, _holder: string, _ttlMs: number): Promise<boolean> {
177
+ return Promise.reject(new SessionScopeError(scope, 'claimRepoLease', repo))
178
+ },
179
+ heartbeatRepo(repo: string, _holder: string): Promise<boolean> {
180
+ return Promise.reject(new SessionScopeError(scope, 'heartbeatRepo', repo))
181
+ },
182
+ releaseRepoLease(repo: string, _holder: string): Promise<void> {
183
+ return Promise.reject(new SessionScopeError(scope, 'releaseRepoLease', repo))
184
+ },
185
+ async createSession(input): Promise<SessionRecord> {
186
+ throw new SessionScopeError(scope, 'createSession', input.repo)
187
+ },
188
+ async getSession(id: string): Promise<SessionRecord | null> {
189
+ own('getSession', id)
190
+ return store.getSession(id)
191
+ },
192
+ listSessions(repo: string): Promise<SessionRecord[]> {
193
+ return Promise.reject(new SessionScopeError(scope, 'listSessions', repo))
194
+ },
195
+ async appendSessionEvent<T extends SessionEventType>(
196
+ id: string,
197
+ event: SessionEventWrite<T>,
198
+ ): Promise<SessionEventEnvelope<T>> {
199
+ own('appendSessionEvent', id)
200
+ return store.appendSessionEvent(id, event)
201
+ },
202
+ async getSessionEvents(
203
+ id: string,
204
+ sinceSeq?: number,
205
+ opts?: { waitSeconds?: number; signal?: AbortSignal },
206
+ ): Promise<SessionEvent[]> {
207
+ own('getSessionEvents', id)
208
+ return store.getSessionEvents(id, sinceSeq, opts)
209
+ },
210
+ async appendSessionWithArtifacts<T extends SessionEventType>(
211
+ id: string,
212
+ artifacts: ArtifactInput[],
213
+ makeEvent: (deposited: SessionArtifactMeta[]) => SessionEventWrite<T>,
214
+ ): Promise<{ event: SessionEventEnvelope<T>; artifacts: SessionArtifactMeta[] }> {
215
+ own('appendSessionWithArtifacts', id)
216
+ return store.appendSessionWithArtifacts(id, artifacts, makeEvent)
217
+ },
218
+ async putSessionArtifact(id: string, artifact: ArtifactInput): Promise<SessionArtifactMeta> {
219
+ own('putSessionArtifact', id)
220
+ return store.putSessionArtifact(id, artifact)
221
+ },
222
+ async getSessionArtifact(
223
+ id: string,
224
+ kind: string,
225
+ rev?: number,
226
+ ): Promise<SessionArtifact | null> {
227
+ own('getSessionArtifact', id)
228
+ return store.getSessionArtifact(id, kind, rev)
229
+ },
230
+ async listSessionArtifacts(id: string, kind?: string): Promise<SessionArtifactMeta[]> {
231
+ own('listSessionArtifacts', id)
232
+ return store.listSessionArtifacts(id, kind)
233
+ },
234
+ async createStream(candidate: StreamScope, label: string): Promise<StreamRecord> {
235
+ sessionScope('createStream', candidate)
236
+ return store.createStream(candidate, label)
237
+ },
238
+ async appendStreamParts(streamId: string, parts: StreamPart[]): Promise<StreamChunk> {
239
+ await ownStream(streamId)
240
+ return store.appendStreamParts(streamId, parts)
241
+ },
242
+ async readStream(
243
+ streamId: string,
244
+ opts?: { since?: number; waitSeconds?: number; signal?: AbortSignal },
245
+ ): Promise<StreamRead> {
246
+ await ownStream(streamId)
247
+ return store.readStream(streamId, opts)
248
+ },
249
+ async closeStream(streamId: string, outcome: StreamOutcome): Promise<StreamRecord> {
250
+ await ownStream(streamId)
251
+ return store.closeStream(streamId, outcome)
252
+ },
253
+ async getStream(streamId: string): Promise<StreamRecord | null> {
254
+ await ownStream(streamId)
255
+ return store.getStream(streamId)
256
+ },
257
+ async listStreams(candidate: StreamScope): Promise<StreamRecord[]> {
258
+ sessionScope('listStreams', candidate)
259
+ return store.listStreams(candidate)
260
+ },
261
+ close(): Promise<void> {
262
+ return Promise.reject(new SessionScopeError(scope, 'close'))
263
+ },
264
+ }
265
+ }
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Close-time assembly (SPEC §7.6): the one piece of shared core code every
3
+ * adapter uses to turn a stream's part sequence into the protocol's
4
+ * `UIMessage[]` document.
5
+ *
6
+ * The SDK's own processor is the protocol: classification reuses
7
+ * `uiMessageChunkSchema` (the executable definition of "defined part type")
8
+ * and assembly reuses `readUIMessageStream`. Parts of undefined types — and
9
+ * malformed defined-type parts — are dropped and counted, never fatal.
10
+ *
11
+ * The SDK's stream state machine models exactly one message per stream (a
12
+ * `start` chunk updates the message id/metadata without resetting accumulated
13
+ * parts), while a stream here is a whole session. The part sequence is
14
+ * therefore split into segments at each `start` chunk, and one `UIMessage`
15
+ * per segment is collected, in order. Verified against ai@7 behavior by
16
+ * `assemble.test.ts`, which is the tripwire if an SDK upgrade changes it.
17
+ */
18
+ import { readUIMessageStream, uiMessageChunkSchema } from 'ai'
19
+ import type { UIMessage } from 'ai'
20
+ import type { StreamPart } from './types'
21
+
22
+ const chunkSchema = uiMessageChunkSchema() as unknown as {
23
+ validate: (value: unknown) => Promise<{ success: boolean }>
24
+ }
25
+
26
+ /** Classify one part against the protocol's defined chunk union. */
27
+ async function isDefinedPart(part: StreamPart): Promise<boolean> {
28
+ return (await chunkSchema.validate(part)).success
29
+ }
30
+
31
+ /**
32
+ * Assemble the ordered part sequence of a whole stream into the document.
33
+ * Returns the `UIMessage[]` (empty when nothing assembled, e.g. zero chunks)
34
+ * and the count of dropped parts (undefined types and malformed defined-type
35
+ * parts). Per-chunk protocol errors surface through the SDK's `onError` and
36
+ * are swallowed — an `error` chunk is content, not an assembly failure.
37
+ */
38
+ export async function assembleUIMessageDocument(
39
+ parts: StreamPart[],
40
+ ): Promise<{ document: UIMessage[]; droppedPartCount: number }> {
41
+ const defined: StreamPart[] = []
42
+ let droppedPartCount = 0
43
+ for (const part of parts) {
44
+ if (await isDefinedPart(part)) {
45
+ defined.push(part)
46
+ } else {
47
+ droppedPartCount++
48
+ }
49
+ }
50
+
51
+ // Split into segments at each `start` chunk (a segment begins with its
52
+ // `start`); parts before the first `start` form a leading segment.
53
+ const segments: StreamPart[][] = []
54
+ let current: StreamPart[] = []
55
+ for (const part of defined) {
56
+ if (part.type === 'start' && current.length > 0) {
57
+ segments.push(current)
58
+ current = []
59
+ }
60
+ current.push(part)
61
+ }
62
+ if (current.length > 0) segments.push(current)
63
+
64
+ const document: UIMessage[] = []
65
+ for (const segment of segments) {
66
+ const stream = new ReadableStream<StreamPart>({
67
+ start(controller) {
68
+ for (const part of segment) controller.enqueue(part)
69
+ controller.close()
70
+ },
71
+ })
72
+ let last: UIMessage | undefined
73
+ for await (const message of readUIMessageStream({ stream: stream as never })) {
74
+ last = message
75
+ }
76
+ if (last) document.push(last)
77
+ }
78
+ return { document, droppedPartCount }
79
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Per-stream, in-process async mutex (AUT-348): a promise-chain per key that
3
+ * serializes a store's close against its appends inside one process, so an
4
+ * append issued while a close is in flight either lands before the close's
5
+ * prepare snapshot or blocks until the close commits and then fails the
6
+ * closed re-check with an explicit error — never a silently omitted chunk.
7
+ *
8
+ * This is coordination *within one process only*. Cross-process and
9
+ * cross-connection writers are invisible to it; serializing those remains the
10
+ * store adapter's transaction discipline (BEGIN IMMEDIATE re-verification in
11
+ * SQLite, the `FOR UPDATE` row lock in Postgres).
12
+ */
13
+ export class StreamLocks {
14
+ /** Queue tail per key: settles only when the current holder finishes. */
15
+ private readonly tails = new Map<string, Promise<unknown>>()
16
+
17
+ /** Run `fn` while holding the lock for `key`. Holders run in FIFO launch
18
+ * order; a holder that throws releases the key and propagates its error to
19
+ * its own caller without rejecting (poisoning) later holders. */
20
+ run<T>(key: string, fn: () => T | Promise<T>): Promise<T> {
21
+ const tail = this.tails.get(key) ?? Promise.resolve()
22
+ // A failed holder must release the key, so the wait chain never rejects.
23
+ const turn = tail.then(
24
+ () => undefined,
25
+ () => undefined,
26
+ )
27
+ const run = turn.then(fn)
28
+ // Keep the chain alive regardless of this holder's fate, and drop the
29
+ // entry once this holder is the last one (the tail) so idle keys do not
30
+ // accumulate. A caller that already chained onto `tail` re-sets the entry
31
+ // with its own, which makes the equality check a no-op for it.
32
+ const released = run.then(
33
+ () => undefined,
34
+ () => undefined,
35
+ )
36
+ this.tails.set(key, released)
37
+ released.then(() => {
38
+ if (this.tails.get(key) === released) this.tails.delete(key)
39
+ })
40
+ return run
41
+ }
42
+ }