@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
@@ -1,19 +1,24 @@
1
1
  # The hosted dispatcher
2
2
 
3
- The optional [hosted service](../packages/hosted-store-service/README.md) can
4
- own the dispatch kernel. One authenticated HTTP endpoint — `GET /api/dispatch`
5
- — runs, for each configured repository, one bounded origin-mode dispatcher
3
+ The optional [`@defrex/autobuild-hosted-dispatcher`](../packages/hosted-dispatcher/README.md)
4
+ package lets a deployment own the dispatch kernel. Installing it and mounting
5
+ its route — `GET /api/dispatch`, backed by the package's
6
+ `dispatcherEndpoint()` singleton — makes the hosted deployment the kernel
7
+ owner. One authenticated HTTP invocation runs, for each configured
8
+ repository, one bounded origin-mode dispatcher
6
9
  tick: the kernel claims ready tickets, provisions and launches sandboxes,
7
10
  observes running builds, settles publications, opens and merges PRs, and runs
8
11
  the janitor, exactly as a local `ab dispatch --once --repository <origin>`
9
12
  pass does. The schedule is what you attach to that endpoint (Vercel Cron calls
10
13
  it once a minute by default), so builds no longer depend on a maintainer's
11
- machine.
14
+ machine. A deployment that does not install the package serves the hosted
15
+ store, ticket, operator, and web endpoints and never attempts a dispatch tick.
12
16
 
13
17
  ## Enabling the schedule
14
18
 
15
- 1. Deploy the hosted service with the dispatcher variables from
16
- [its README](../packages/hosted-store-service/README.md#hosted-dispatcher)
19
+ 1. Deploy the hosted service with the dispatcher package installed and the
20
+ dispatcher variables from
21
+ [its README](../packages/hosted-dispatcher/README.md)
17
22
  — `AB_DISPATCHER_ORIGIN`, the repository set, `CRON_SECRET`, and the forge
18
23
  credentials the kernel needs (`GITHUB_TOKEN` or `GH_TOKEN`, plus any
19
24
  guest-forwarded variables such as `AI_GATEWAY_API_KEY`). A deployment
@@ -44,26 +49,77 @@ Every sandbox installs the Autobuild distribution the launching dispatcher
44
49
  runs. A local dispatcher packs it from its source checkout with `bun pm pack`;
45
50
  a bundled deployment has neither `bun` nor a source tree at runtime, so it
46
51
  must pack the archive while both exist — in its build step — and carry it into
47
- the function bundle:
52
+ the function bundle. The script lives in the hosted store service package
53
+ (`packages/hosted-store-service/package.json`), whose directory is the Next.js
54
+ project directory and the deployment build's working directory:
48
55
 
49
56
  ```json
50
- { "scripts": { "deploy:build": "bun packages/hosted-store-service/src/bin.ts pack-distribution && bun run postgres:migrate && bun run build && bun tools/ship-packed-distribution.ts" } }
57
+ { "scripts": { "deploy:build": "bun ../../packages/hosted-dispatcher/src/bin.ts pack-distribution --root . && bun ../../packages/postgres-store/src/bin.ts migrate && bun run build && bun ../../packages/hosted-dispatcher/src/ship-packed-distribution.ts && bun ../../packages/hosted-dispatcher/src/ship-provider-plugin.ts" } }
51
58
  ```
52
59
 
53
- `pack-distribution` writes `.autobuild-dist/autobuild-<version>.tgz` under the
54
- distribution root (`--root DIR` overrides). The Next.js config's
60
+ `pack-distribution` (the `ab-hosted-dispatcher` bin) writes
61
+ `.autobuild-dist/autobuild-<version>.tgz` under the
62
+ distribution root (`--root DIR` overrides) — this deployment passes
63
+ `--root .`, because the packer's default root is the repository's distribution
64
+ root while the trace step below and the Next config's
55
65
  `outputFileTracingIncludes: { '/api/dispatch': ['./.autobuild-dist/**'] }`
56
- records the intent to include that directory in the cron route's bundle, but
66
+ resolve against the deployment build's working directory, where the archive
67
+ must land beside the built output. The Next.js config's
68
+ `outputFileTracingIncludes` records the intent to include that directory in the cron route's bundle, but
57
69
  Next 16's default Turbopack builds never apply `outputFileTracingIncludes`
58
70
  (only webpack builds do), so a deployment cannot rely on the config alone. The
59
71
  autobuild-api pipeline therefore ends `deploy:build` with a post-build trace
60
- step (`bun tools/ship-packed-distribution.ts`) that appends the archive to the
72
+ step (`bun ../../packages/hosted-dispatcher/src/ship-packed-distribution.ts`) that appends the archive to the
61
73
  dispatch route's `route.js.nft.json` — the trace file Vercel's Next builder
62
74
  consumes when assembling the function bundle — and fails the deploy loudly if
63
- nothing was packed or the trace file is missing. A deployment using this
75
+ nothing was packed or the trace file is missing. The same step appends the
76
+ distribution root's `package.json`, which provisioning reads for the running
77
+ version and which no import traces once the Next.js project directory sits
78
+ below the repository root. A deployment using this
64
79
  pipeline cannot ship without the archive; deployments that skip both packing
65
80
  and the trace step still fail at runtime.
66
81
 
82
+ A second post-build trace step ships the provider plugin package (AUT-517):
83
+ `bun ../../packages/hosted-dispatcher/src/ship-provider-plugin.ts` locates the
84
+ plugin's source through the repository root's `workspaces` tree (never through
85
+ `node_modules`, so a previous run's staged directory cannot shadow the source),
86
+ bundles its entrypoint with `bun build --target=bun --format=esm` into a
87
+ single self-contained module (the plugin-sdk closure inlines — the
88
+ deployment's repository-root `node_modules` carries no `@defrex/autobuild`
89
+ either — while node builtins stay external), stages it into
90
+ `<repoRoot>/node_modules/@defrex/autobuild-vercel-sandbox/` (replacing the
91
+ workspace symlink a local `bun install` leaves there — or a previous run's
92
+ staged directory, which `bun install` does not relink; the deployment never
93
+ runs `bun install` again), and appends the staged files to the dispatch
94
+ route's trace at the same repository-root depth as the existing
95
+ `node_modules/.bun/**` entries. A bare plugin specifier resolves only
96
+ through a `node_modules/` path, and the compiled dispatcher's
97
+ `distributionRoot()` is the repository root, so only this staging makes
98
+ `plugins = ["@defrex/autobuild-vercel-sandbox"]` loadable inside the
99
+ deployed function. The step is idempotent across runs — including in a
100
+ fresh process after an earlier run consumed the workspace link — and fails
101
+ the deploy loudly when the package, its entrypoint, or the trace file is
102
+ missing. The npm published package remains the real `src` — only the
103
+ deployment stages the bundle.
104
+
105
+ What the staged directory does afterwards depends on the build environment.
106
+ Inside a Vercel deployment build (which always sets `VERCEL`, including a
107
+ local `vercel build` run) the staged directory stays: the deployment never
108
+ runs `bun install` again and the function bundle's runtime resolution needs
109
+ it — a deployment building outside Vercel must set `VERCEL=1` in its build
110
+ environment to keep the staged bundle. A plain local `bun run deploy:build`
111
+ (no `VERCEL` environment) removes the staged directory afterwards and
112
+ restores the workspace symlink, so a local run leaves the checkout exactly
113
+ as `bun install` had it and local test runs keep resolving the workspace
114
+ source (the capability parity test's reference equality holds). On checkouts
115
+ of older versions the staged directory stayed behind and survived `bun
116
+ install` — it reports "no changes" because the staged package satisfies the
117
+ manifest — so later local runs resolved the plugin to the self-contained
118
+ bundle and the parity test failed on its reference-equality assertion; the
119
+ recovery is `rm -rf node_modules/@defrex/autobuild-vercel-sandbox && bun
120
+ install`, or simply re-run `deploy:build` locally on a version with this
121
+ fix, which restores the symlink itself.
122
+
67
123
  At runtime the kernel takes, in order: `AB_DISTRIBUTION_ARCHIVE` (an explicit
68
124
  archive path), the single archive under `.autobuild-dist/` of the
69
125
  distribution root or working directory, a source checkout packed on the spot,
@@ -1,9 +1,9 @@
1
1
  # Operator API v1
2
2
 
3
3
  The hosted service exposes the polling-oriented operator surface at
4
- `/operator/v1/repos/{repo}`. It is versioned with remote store protocol **2**.
4
+ `/operator/v1/repos/{repo}`. It is versioned with remote store protocol **3**.
5
5
  Every request must send `X-Autobuild-Version`,
6
- `X-Autobuild-Protocol-Version: 2`, and `Authorization: Bearer …`. Identity is
6
+ `X-Autobuild-Protocol-Version: 3`, and `Authorization: Bearer …`. Identity is
7
7
  checked before authentication or resource lookup.
8
8
 
9
9
  Mint a token offline; the signed user is used as the human actor on every write:
@@ -20,7 +20,7 @@ document and the package's executable contracts together.
20
20
  `AB_STORE`; the shipped client appends the routes below.
21
21
  - Every `/builds`, `/repos`, and `/sessions` request sends
22
22
  `X-Autobuild-Version` with the exact client package version and
23
- `X-Autobuild-Protocol-Version` with the remote protocol version (`2` in this
23
+ `X-Autobuild-Protocol-Version` with the remote protocol version (`3` in this
24
24
  distribution). Before authentication,
25
25
  body parsing, or resource lookup, the server compares both exact strings.
26
26
  Missing or different values receive `409 {"kind":"conflict","error":"…"}`;
@@ -290,13 +290,15 @@ it to exist. An unknown build returns `404 not-found`.
290
290
 
291
291
  | BuildStore operation | HTTP route | Request | Success |
292
292
  |---|---|---|---|
293
- | `createBuild` | `POST /builds` | `{slug: string, repo: string, repoOrigin?: string, ticket?: TicketRef, branch?: string}`; `slug`, `repo`, and a supplied `branch` are nonempty | `201` + `BuildRecord`; duplicate slug is `409 conflict` |
293
+ | `createBuild` | `POST /builds` | `{slug: string, repo: string, repoOrigin?: string, ticket?: TicketRef, branch?: string, created?: event write}`; `slug`, `repo`, and a supplied `branch` are nonempty. `created`, when present, must be a valid `build.created` write; the server appends it as seq 1 atomically with the record (both exist or neither), and an invalid one is `422` with nothing created | `201` + `BuildRecord`; duplicate slug is `409 conflict` |
294
294
  | `listBuilds` | `GET /builds` | none | `200` + `BuildRecord[]`; list order is unspecified |
295
295
  | `getBuild` | `GET /builds/{slug}` | none | `200` + `BuildRecord`; absent is `404` (the shipped client maps this to `null`) |
296
296
  | `append` | `POST /builds/{slug}/events` | event write | `201` + build event envelope |
297
297
  | `appendIfCurrent` | `POST /builds/{slug}/events/conditional` | `{"expectedSeq": nonnegative integer, "event": event write}` | appended: `201` + build event envelope; stream advanced: `200 null` |
298
298
  | `getEvents` | `GET /builds/{slug}/events?since={n}&wait={s}` | optional `since` query value, parsed below; absence defaults to `0`. Optional `wait` in whole seconds, parsed per the event-wait rules below | `200` + envelopes whose `seq` is strictly greater than parsed `since`, in increasing sequence order |
299
299
  | `appendWithArtifacts` | `POST /builds/{slug}/deposits` | atomic deposit request | `201` + `{event, artifacts}` |
300
+ | `getReducerSnapshot` | `GET /builds/{slug}/snapshots/{reducer}?version={n}` | required `version` query value, a positive integer; `reducer` is a registry name (`^[A-Za-z][A-Za-z0-9._-]{0,63}$`) | `200` + `{"version", "cursor", "state"}` or `200 null` when absent, written by another reducer version, or ahead of the build log's tail; an unknown build is `404`, which the shipped client also maps to `null` |
301
+ | `putReducerSnapshot` | `PUT /builds/{slug}/snapshots/{reducer}` | `{"version": int >= 1, "cursor": int >= 0, "state": JSON}`, at most 16 MiB (`413` above) | `200 {"written": boolean}`; the stored cursor never decreases, so a write at an equal or lower cursor (or an equal cursor at an equal or lower version) and one past the log's tail answer `written: false` |
300
302
  | `putArtifact` | `POST /builds/{slug}/artifacts` | artifact input | `201` + build artifact metadata |
301
303
  | `getArtifact` | `GET /builds/{slug}/artifacts?kind={kind}&rev={n}` | `kind` is required and nonempty; optional `rev` is parsed below | `200` + artifact read; an absent `rev` parameter selects the latest revision; a missing kind/revision is `200 null` |
302
304
  | `listArtifacts` | `GET /builds/{slug}/artifact-list?kind={kind}` | optional `kind`; absence means all kinds | `200` + metadata ordered by kind and then increasing revision |
@@ -336,6 +338,32 @@ part of this protocol. Retention is enforced server-side inside the store at
336
338
  deposit time (bounded pruning of dispatcher run/config artifact revisions),
337
339
  not through any endpoint.
338
340
 
341
+ ### Reducer snapshots
342
+
343
+ A reducer snapshot is one registered reducer's serialized state as of a log
344
+ cursor, kept per scope (one build log or one repository journal) and reducer
345
+ name. It is a cache the log regenerates, never an authority: a process with no
346
+ hot memory loads it, reads only the events newer than its cursor, advances the
347
+ reducer, and writes the advanced state back, and deleting every snapshot changes
348
+ no observable output. The routes above and in section 4 are additive and carry
349
+ no protocol-version bump. The store, not the client, enforces the invariants:
350
+
351
+ - the stored cursor never decreases, whatever the versions of the competing
352
+ writers; a write succeeds only when no row exists, when its cursor is above the
353
+ stored one, or when its cursor equals the stored one and its version is higher
354
+ (an upgraded reducer replacing the old shape at an unchanged tail);
355
+ - a read answers `null` for a row whose version differs from the requested one,
356
+ and for one whose cursor is beyond the scope's log tail; a write beyond the
357
+ tail is refused;
358
+ - an unknown build or repository answers `null` (read) or `written: false`
359
+ (write) and never creates the scope.
360
+
361
+ A client treats `404` and `405` from either route as "no snapshot", so a client
362
+ newer than its store replays the log with no error, and a client older than its
363
+ store never calls the routes. Authentication and transport failures still
364
+ surface normally. A build token may touch only its own build's snapshots and a
365
+ repository token only its own repository's, exactly as for events.
366
+
339
367
  ### The event-read bounded wait
340
368
 
341
369
  Both event reads (`getEvents` and, symmetrically, the `getRepoEvents` read of
@@ -357,7 +385,8 @@ requires. A held request's authentication, token scope, and version-skew rules
357
385
  are identical to the immediate form: a `401`, `403`, or `409` identity failure
358
386
  is answered immediately, never held.
359
387
 
360
- The parameter is purely additive, so the protocol version stays `2`: a server
388
+ The parameter is purely additive in the version it landed in (protocol `2`,
389
+ where the event-wait rules were introduced): a server
361
390
  that ignores `wait` remains conforming — its immediate empty answers only raise
362
391
  the client's request rate — and a client that sends `wait` to such a server
363
392
  observes no other difference. This is the same reasoning the stream and session
@@ -368,7 +397,11 @@ waits apply. There is no push transport; a held request is the whole mechanism.
368
397
  Repository journals are separate resources with independent event sequences,
369
398
  artifacts, and leases. `{repo}` is one percent-encoded repository id; encode
370
399
  embedded `/` characters. All resource routes authorize and require the
371
- repository to exist. An unknown repository returns `404 not-found`.
400
+ repository to exist. An unknown repository returns `404 not-found` — with one
401
+ carve-out: the build-digest batch read of this section is keyed to build
402
+ records, not the repository journal record, so it authorizes the repo token
403
+ but answers `200` (possibly empty) even for an unknown repository, a repository
404
+ without a journal record, or a repository without builds.
372
405
 
373
406
  | BuildStore operation | HTTP route | Request | Success |
374
407
  |---|---|---|---|
@@ -376,10 +409,14 @@ repository to exist. An unknown repository returns `404 not-found`.
376
409
  | `getRepo` | `GET /repos/{repo}` | none | `200` + `RepositoryRecord`; absent is `404` (the shipped client maps this to `null`) |
377
410
  | `appendRepo` | `POST /repos/{repo}/events` | event write | `201` + repository event envelope |
378
411
  | `getRepoEvents` | `GET /repos/{repo}/events?since={n}&wait={s}` | optional `since` query value, parsed as in section 3; absence defaults to `0`. Optional `wait` in whole seconds, per the event-wait rules of section 3 | `200` + envelopes with `seq >` parsed `since`, in increasing sequence order |
412
+ | `getRepoStateEvents` | `GET /repos/{repo}/state-events` | none | `200` + the bounded repository-journal subset (AUT-489): every durable event type (`harvest.*`, `orchestrator.sandbox.*`, the three dispatcher setting types) plus, when the journal has one, the tail from the latest `dispatcher.run-started`, in increasing sequence order; served inside the repository-existence gate like `GET events`, so an unknown repository is `404` |
413
+ | `getReducerSnapshot` (repo scope) | `GET /repos/{repo}/snapshots/{reducer}?version={n}` | required `version` query value, a positive integer; `reducer` is a registry name (`^[A-Za-z][A-Za-z0-9._-]{0,63}$`) | `200` + `{"version", "cursor", "state"}` or `200 null` when absent, written by another reducer version, ahead of the journal's tail, or the repository has no journal record; an unknown repository is `404`, which the shipped client also maps to `null` |
414
+ | `putReducerSnapshot` (repo scope) | `PUT /repos/{repo}/snapshots/{reducer}` | `{"version": int >= 1, "cursor": int >= 0, "state": JSON}`, at most 16 MiB (`413` above) | `200 {"written": boolean}`; the stored cursor never decreases, so a write at an equal or lower cursor (or an equal cursor at an equal or lower version) and one past the journal's tail answer `written: false` |
379
415
  | `appendRepoWithArtifacts` | `POST /repos/{repo}/deposits` | atomic deposit request | `201` + `{event, artifacts}` using repository shapes |
380
416
  | `putRepoArtifact` | `POST /repos/{repo}/artifacts` | artifact input | `201` + repository artifact metadata |
381
417
  | `getRepoArtifact` | `GET /repos/{repo}/artifacts?kind={kind}&rev={n}` | required nonempty `kind`; optional `rev` is parsed as in section 3 | `200` + artifact read; latest only when the `rev` parameter is absent; missing kind/revision is `200 null` |
382
418
  | `listRepoArtifacts` | `GET /repos/{repo}/artifact-list?kind={kind}` | optional `kind`; absence means all kinds | `200` + metadata ordered by kind and then increasing revision |
419
+ | `getRepoBuildDigests` | `GET /repos/{repo}/build-digests` | none | `200` + `BuildDigest[]`, one entry per build record of the repository (possibly empty), ordered by slug; handled before the repository-existence gate, so an unknown or journal-less repository answers `200 []` |
383
420
  | `claimRepoLease` | `POST /repos/{repo}/lease/claim` | claim request | `200 {"ok": boolean}` |
384
421
  | `heartbeatRepo` | `POST /repos/{repo}/lease/heartbeat` | holder request | `200 {"ok": boolean}` |
385
422
  | `releaseRepoLease` | `POST /repos/{repo}/lease/release` | holder request | `200 {"ok": true}`; a wrong holder is a successful no-op |
@@ -389,10 +426,153 @@ creation operation and must not reset timestamps, events, artifacts, or lease
389
426
  state when the repository already exists. Event sequence numbering starts at 1
390
427
  for each repository independently of every build and other repository.
391
428
 
429
+ A `BuildDigest` is the projection of one build's log onto its terminal fact
430
+ and its `observation.recorded` occurrences:
431
+
432
+ ```jsonc
433
+ // BuildDigest
434
+ {
435
+ "slug": "ab-fix-login-3",
436
+ "terminal": "done", // optional: "done" | "aborted", omitted while the log has neither
437
+ "observations": [ // ascending-seq pairs for the log's observation.recorded events
438
+ { "seq": 1, "ts": "2026-07-15T12:00:01.000Z" },
439
+ { "seq": 4, "ts": "2026-07-15T12:00:04.000Z" }
440
+ ],
441
+ "merged": "2026-07-15T12:00:06.000Z" // optional: ts of the log's latest pr.merged, omitted when none
442
+ }
443
+ ```
444
+
445
+ The digest is derived from the event log on every call — the server persists
446
+ nothing and there is no refresh step. Protocol version `3` reshaped the
447
+ digest's `observations` from bare sequence numbers to `{seq, ts}` pairs and
448
+ added the optional `merged` fact, so the harvest pressure gate can evaluate
449
+ launch decisions from digests and the repository journal alone instead of
450
+ reading every build's full history per evaluation. That is a breaking wire
451
+ change, not an additive one: a server that predates it returns the old shape
452
+ and would fail the client's schema parse mid-request, which is why the version
453
+ moved — a stale peer is now rejected at the handshake (`409 conflict`) instead.
454
+ This is the same discipline the transport's exact-match version rules promise:
455
+ wire shapes that independently implemented servers must reproduce update this
456
+ document and the protocol version together.
457
+
392
458
  The build-stream query, artifact, ordering, timestamp, and validation rules
393
459
  apply symmetrically to repository journals, including the event-read bounded
394
460
  wait of section 3.
395
461
 
462
+ ### Ticket assets
463
+
464
+ A ticket asset is a named, versioned bundle of files attached to
465
+ a ticket, keyed by `(repo, ticketId, kind, name)`. These routes live under the
466
+ repository but, unlike the journal routes, **do not require a repository
467
+ record**: assets can precede any journal row, so the repository-existence gate
468
+ does not apply. `{repo}` and `{id}` are percent-encoded path segments. Token
469
+ authorization follows the one-matching-repo column of the matrix (a repo token
470
+ for its own repository, or admin); build and session tokens receive `403 auth`.
471
+ The family is additive, so `REMOTE_STORE_PROTOCOL_VERSION` did not change.
472
+
473
+ A build also reads the assets it froze at its claim through its own build-scoped
474
+ routes, which a build token may use (no ticket-asset authority needed). The
475
+ server reduces the build's events to find the pinned revision, then returns that
476
+ exact revision, so a later replace or removal never changes the answer and a
477
+ finished build still answers:
478
+
479
+ | BuildStore operation | HTTP route | Request | Success |
480
+ |---|---|---|---|
481
+ | `getPinnedTicketAsset` (manifest) | `GET /builds/{slug}/ticket-assets?kind=&name=[&rev=]` | required `kind` and `name`; optional `rev` | `200` + `TicketAssetMeta` for the latest pinned revision (or the named pinned `rev`), or `200 null` when the build never pinned it |
482
+ | `getPinnedTicketAsset` (one file) | `GET /builds/{slug}/ticket-assets/file?kind=&name=&rev=&path=` | required `kind`, `name`, `path`; optional `rev` | `200` + `{"contentBase64": string}`, or `200 null` when not pinned; `404` when the revision has no such file |
483
+
484
+ | BuildStore operation | HTTP route | Request | Success |
485
+ |---|---|---|---|
486
+ | `ticketAssetLimits` | `GET /repos/{repo}/ticket-asset-limits` | none | `200` + `{maxBytes, maxEntries, maxRequestBytes?}` — the effective per-revision ceilings and, when the deployment has one, the largest request or response body it can carry. A server without this route answers `404`; clients treat that as the defaults |
487
+ | `putTicketAsset` | `POST /repos/{repo}/tickets/{id}/assets` | `{kind, name, layout, entries}` | `201` + `TicketAssetMeta` (with its manifest) |
488
+ | `getTicketAsset` (manifest) | `GET /repos/{repo}/tickets/{id}/assets?kind=&name=[&rev=]` | required `kind` and `name`; optional `rev` | `200` + `TicketAssetMeta` — **the manifest only, no bytes** — or `200 null` when the asset is absent or removed (latest) or the revision does not exist |
489
+ | `getTicketAsset` (one file) | `GET /repos/{repo}/tickets/{id}/assets/file?kind=&name=&rev=&path=` | required `kind`, `name`, `path`; optional `rev` | `200` + `{"contentBase64": string}`, or `200 null` when the asset is absent; `404` when the revision has no such file |
490
+ | `listTicketAssets` | `GET /repos/{repo}/tickets/{id}/assets/list[?revisions=1]` | optional `revisions=1` | `200` + `TicketAssetSummary[]` ordered by kind, name, revision: the latest live revision per kind and name, or with `revisions=1` every real revision (no tombstones). Summaries carry no manifest |
491
+ | `removeTicketAsset` | `POST /repos/{repo}/tickets/{id}/assets/remove` | `{kind, name}` | `200` + the removed `TicketAssetMeta`, or `200 null` when no live asset exists |
492
+
493
+ Wire types:
494
+
495
+ ```jsonc
496
+ // put request: entries are files (content as base64) or explicit directories
497
+ { "kind": "design", "name": "home", "layout": "tree", // layout: "file" | "tree"
498
+ "entries": [
499
+ { "type": "file", "path": "index.html", "contentBase64": "PGgxPg==" },
500
+ { "type": "dir", "path": "img/empty" }
501
+ ] }
502
+
503
+ // TicketAssetMeta: dir entries carry no size or blobRef
504
+ { "repo": "…", "ticketId": "AUT-1", "kind": "design", "name": "home",
505
+ "revision": 0, "layout": "tree", "size": 1234, // size = total file bytes
506
+ "entries": [
507
+ { "type": "file", "path": "index.html", "size": 1234, "blobRef": "<sha256 hex>" },
508
+ { "type": "dir", "path": "img/empty" }
509
+ ],
510
+ "createdAt": "2026-10-01T12:00:00.000Z" }
511
+
512
+ // TicketAssetSummary (list): the same without entries
513
+ { "repo": "…", "ticketId": "AUT-1", "kind": "design", "name": "home",
514
+ "revision": 0, "layout": "tree", "size": 1234,
515
+ "fileCount": 1, "dirCount": 1, "createdAt": "…" }
516
+ ```
517
+
518
+ Semantics every server must reproduce:
519
+
520
+ - **Revisions** are 0-based per `(repo, ticketId, kind, name)`. Putting the same
521
+ kind and name again creates the next revision; earlier revisions stay readable
522
+ by explicit `rev`.
523
+ - **Removal is a tombstone** that takes the next revision number with an empty
524
+ manifest. The latest row decides liveness: a read without `rev` and the
525
+ default list skip a removed asset, while an explicit earlier `rev` keeps
526
+ working (a build that pinned that revision still retrieves it). Re-attaching
527
+ takes the next revision, so numbers may skip the tombstone. `revisions=1`
528
+ lists real revisions only.
529
+ - **Validation** (`422 validation`, nothing stored). `kind` and `name` match
530
+ `[A-Za-z0-9._-]+`, at most 64 characters. Paths are relative and
531
+ `/`-separated, with no empty, `.`, or `..` segment and no NUL, at most 256
532
+ UTF-8 bytes (128 per segment); paths are unique across files and directories
533
+ and no file is the ancestor of another entry. `layout: "file"` has exactly one
534
+ top-level `file` entry and no directory; `layout: "tree"` may hold any number
535
+ of entries, including zero (an empty folder; its root is implicit). A revision
536
+ holds at most 1000 entries (files plus directories) and **26214400 bytes (25
537
+ MiB)** of file bytes; the error names the limit, for example `ticket asset
538
+ exceeds the 26214400-byte (25 MiB) limit`.
539
+ - File bytes are content-addressed (`blobRef` is the sha256 hex) in the same
540
+ blob store as artifacts; the database holds only the manifest.
541
+
542
+ **Deployment ceilings.** A platform may reject oversized request or response
543
+ bodies before any application code runs (Vercel's functions cap them at 4.5
544
+ MB), so validating on the server alone cannot enforce a limit. A server that
545
+ has such a ceiling advertises it as `maxRequestBytes` on the limits route
546
+ together with the effective `maxBytes`, computed from it as follows. Paths and
547
+ entry counts are bounded, so the worst-case wire overhead is a constant:
548
+ `overhead = 1024 + 1000 × 320 = 321024` bytes, and
549
+ `maxBytes = min(26214400, max(0, floor((maxRequestBytes − overhead) × 3 / 4)))`
550
+ (base64 inflates content by 4/3); `maxEntries` stays 1000. The hosted service
551
+ sets `maxRequestBytes` to 4 MiB, giving an effective per-asset limit of 2904960
552
+ bytes (about 2.8 MiB). The formula is a safe lower bound, not the final
553
+ authority:
554
+
555
+ - The shipped **client** reads the limits once per store handle and, before any
556
+ upload, validates against `maxBytes` and `maxEntries` and then compares the
557
+ exact UTF-8 length of the serialized request with `maxRequestBytes`. A bundle
558
+ that does not fit is refused locally with `ticket asset exceeds this store's
559
+ N-byte limit (deployment request ceiling M bytes)` and **no upload request is
560
+ made**.
561
+ - The **server** re-validates every put against its effective limits and checks
562
+ every ticket-asset body against `maxRequestBytes` itself, answering `413` (a
563
+ request) or `422` with a limit-naming message where the platform lets the
564
+ request through.
565
+ - **Downloads never need a bundle-sized response**: the client reads the
566
+ manifest and then each file by its own request. The manifest, file, and list
567
+ routes check the exact serialized length of their response against
568
+ `maxRequestBytes` and fail with a `413 validation` error naming the asset (or
569
+ the file, its size, and the ceiling) rather than letting the platform reject
570
+ it. This matters for a revision stored under an older or higher ceiling:
571
+ its manifest and smaller files remain retrievable; a file that cannot fit can
572
+ only be fetched from a store with a higher ceiling. Chunked or ranged transfer
573
+ is not part of the protocol, so a deployment's effective per-asset limit is
574
+ its advertised `maxBytes`.
575
+
396
576
  ## 5. Operator-session operations
397
577
 
398
578
  Operator sessions are a third resource kind: durable orchestrator-conversation
@@ -430,6 +610,7 @@ identity is already the actor.
430
610
  | `listSessions` | `GET /repos/{repo}/sessions` | none | `200` + `SessionRecord[]`, creation order (`createdAt`, then the store's creation counter for same-millisecond ties) |
431
611
  | `getSession` | `GET /sessions/{id}` | none | `200` + `SessionRecord`; absent is `404` (the shipped client maps this to `null`) |
432
612
  | `appendSessionEvent` | `POST /sessions/{id}/events` | event write | `201` + session event envelope (same envelope shape with `"session"` in place of `"build"`) |
613
+ | `appendSessionEventIfCurrent` | `POST /sessions/{id}/events/conditional` | `{"expectedSeq": nonnegative integer, "event": event write}` | appended: `201` + session event envelope; session advanced: `200 null` (mirroring section 3's build compare-and-append) |
433
614
  | `getSessionEvents` | `GET /sessions/{id}/events?since={n}&wait={s}` | optional `since` query value, parsed as in section 3; absence defaults to `0`. Optional `wait` in whole seconds, per the event-wait rules of section 3 | `200` + session event envelopes with `seq >` parsed `since`, in increasing sequence order |
434
615
  | `appendSessionWithArtifacts` | `POST /sessions/{id}/deposits` | atomic deposit request | `201` + `{event, artifacts}` using session shapes; the substitution algorithm of section 8 applies unchanged |
435
616
  | `putSessionArtifact` | `POST /sessions/{id}/artifacts` | artifact input | `201` + session artifact metadata |
@@ -599,7 +780,8 @@ by this rule (their retention belongs to the open archival thread), and open
599
780
  streams are never pruned. A read of a stream whose chunks were pruned returns
600
781
  no chunks, `status: "closed"`, and the artifact reference.
601
782
 
602
- These additions are purely additive: the protocol version stays `2`, and a
783
+ These additions were purely additive in the version they landed in (protocol
784
+ `2`, where the stream routes were introduced), and a
603
785
  conforming server implements the stream routes together with this document's
604
786
  rules exactly as it does for the event and artifact routes.
605
787
 
@@ -675,12 +857,17 @@ resource access; an explicit resource whose id is `"*"` is not admin.
675
857
 
676
858
  ### Resource authorization matrix
677
859
 
678
- | Token resource | `/builds` create/list | one matching build | `/repos` ensure | one matching repo | `/repos/{repo}/sessions` create/list | one matching session |
679
- |---|---:|---:|---:|---:|---:|---:|
680
- | admin (`build: "*"`) | yes | any | yes | any | yes | any |
681
- | build | no | exact id only | no | no | no | no |
682
- | repo | no | no | no | exact id only | yes | no |
683
- | session | no | no | no | no | no | exact id only |
860
+ | Token resource | `/builds` create/list | one matching build | `/repos` ensure | one matching repo | `/repos/{repo}/sessions` create/list | `/repos/{repo}/build-digests` | `/repos/{repo}/state-events` | `/repos/{repo}/tickets/{id}/assets` and `/repos/{repo}/ticket-asset-limits` | one matching session |
861
+ |---|---:|---:|---:|---:|---:|---:|---:|---:|---:|
862
+ | admin (`build: "*"`) | yes | any | yes | any | yes | any | any | any | any |
863
+ | build | no | exact id only | no | no | no | no | no | no | no |
864
+ | repo | no | no | no | exact id only | yes | exact id only | exact id only | exact repo only | no |
865
+ | session | no | no | no | no | no | no | no | no | exact id only |
866
+
867
+ The snapshot routes (`/builds/{slug}/snapshots/{reducer}` and
868
+ `/repos/{repo}/snapshots/{reducer}`) follow the one-matching-build and
869
+ one-matching-repo columns: a build token reads and writes only its own build's
870
+ snapshots, a repo token only its own repository's.
684
871
 
685
872
  A valid token used for the wrong resource receives `403 auth`. Resource scope
686
873
  gates all operations, including reads, artifact operations, and leases. A
@@ -793,8 +980,8 @@ The shipped server maps failures as follows:
793
980
  | `403` | `auth` | Valid token with the wrong resource scope or event-session attribution |
794
981
  | `404` | `not-found` | Unknown route, unsupported method, unknown build, unknown repository, unknown session, or an unknown or foreign-scoped stream |
795
982
  | `409` | `conflict` | Missing/mismatched package or protocol identity, duplicate build creation, appending to a closed stream (`stream "…" is closed`), or a backing conflict reported as already existing |
796
- | `413` | `validation` | Decoded artifact or serialized stream batch exceeds the configured ceiling; the message names the ceiling |
797
- | `422` | `validation` | `EventValidationError` from build, repository, or session catalog validation; its message is preserved verbatim |
983
+ | `413` | `validation` | Decoded artifact or serialized stream batch exceeds the configured ceiling; a ticket-asset request or response exceeds the deployment's request ceiling; the message names the ceiling |
984
+ | `422` | `validation` | `EventValidationError` from build, repository, or session catalog validation, or a ticket-asset validation failure (including the 25 MiB limit); its message is preserved verbatim |
798
985
  | `500` | `internal` | Any other unexpected backing or server failure; the thrown error message is returned |
799
986
 
800
987
  Authentication runs before resource lookup, and session authorization runs
@@ -916,11 +1103,12 @@ Four shipped behaviors do not add `BuildStore` routes:
916
1103
  store lifecycle; there is no close endpoint.
917
1104
  - `GET /health` is outside the `BuildStore` interface. It is always open and
918
1105
  returns exactly `200 {"ok":true,"autobuildVersion":"<package>",
919
- "protocolVersion":"2"}`. It reports HTTP-process availability and identity,
1106
+ "protocolVersion":"3"}`. It reports HTTP-process availability and identity,
920
1107
  not a deeper backing-store transaction or migration check.
921
1108
 
922
- There is no push/WebSocket subscription protocol, batch-read route,
923
- repository listing, artifact deletion, or server deployment API.
1109
+ There is no push/WebSocket subscription protocol, repository listing, artifact
1110
+ deletion, or server deployment API. The one repo-scoped batch read is the
1111
+ build-digest route of section 4; no other batch-read route exists.
924
1112
 
925
1113
  ## 12. Conformance
926
1114
 
@@ -3,7 +3,7 @@
3
3
  The hosted ticket protocol exposes the complete `TicketSource` port without
4
4
  exposing backend credentials. It shares the Autobuild and remote protocol
5
5
  version headers with the remote BuildStore. Both must exactly match the service;
6
- this release uses remote protocol version `2`.
6
+ this release uses remote protocol version `3`.
7
7
 
8
8
  ## Authentication and context
9
9
 
package/docs/setup.md CHANGED
@@ -59,7 +59,7 @@ DEBIAN_FRONTEND=noninteractive apt-get install -y chromium""" },
59
59
 
60
60
  [workspace.config.runtimeProvisioning.pi]
61
61
  install = "npm install --global --ignore-scripts @earendil-works/pi-coding-agent@0.84.4"
62
- preflight = "test \"$(pi --version)\" = \"0.84.4\""
62
+ preflight = "test \"$(pi --version)\" = \"0.84.4\" && pi update --models"
63
63
  ```
64
64
 
65
65
  Use a `vercel-ai-gateway/...` Pi model with `AI_GATEWAY_API_KEY` in the
@@ -220,14 +220,17 @@ same lockstep window. If the release is cut but the store upgrade lags, guests
220
220
  carrying the new distribution fail loudly against the old store (409) rather
221
221
  than running silently stale.
222
222
 
223
- Provisioning records the installed distribution's version in the guest's
224
- `/opt/autobuild/.distribution-version` marker. A reused persistent sandbox
225
- whose marker disagrees with the version the current system would deliver —
226
- which includes every guest provisioned before the marker existed — is
227
- refreshed in place (archive re-fetched, extracted, and production-installed)
228
- before being reused, so an upgraded dispatcher retrofits its existing guests
229
- instead of resuming a stale distribution. A refresh failure deletes the
230
- sandbox so the next provisioning pass rematerializes it from scratch.
223
+ Provisioning records the installed distribution's identity — package version
224
+ and remote-store protocol version — in the guest's
225
+ `/opt/autobuild/.distribution-version` marker. Every path that launches guest
226
+ code in an existing sandbox — provisioning reuse, harvest reuse, the operator
227
+ sandbox, and build execution restart after a lease sweep — compares the marker
228
+ against the identity the current system would deliver and, when they disagree
229
+ (which includes every guest provisioned before the marker existed), refreshes
230
+ it in place (archive re-fetched, extracted, and production-installed) first,
231
+ so an upgraded dispatcher retrofits its existing guests instead of resuming a
232
+ stale distribution the hosted store would reject. A refresh failure deletes
233
+ the sandbox so the next provisioning pass rematerializes it from scratch.
231
234
 
232
235
  After a lockstep upgrade, confirm a fix-bearing guest from the guest
233
236
  perspective:
@@ -0,0 +1,14 @@
1
+ # Website
2
+
3
+ The marketing site is one static page in `packages/website`, reproduced from the approved design in `design/website/`. The package is private and never published to npm.
4
+
5
+ - `bun run website:dev` serves it on port 3300 (`PORT` overrides), rebuilding on every request.
6
+ - `bun run website:build` writes `packages/website/dist/` (`index.html`, `site.css`, `site.js`).
7
+
8
+ The hero is the real `ab dispatch` dashboard: `packages/website/src/hero-frame.txt` holds the exact colored lines the terminal dashboard painted for the scripted `website-hero` capture, and the page renders them as preformatted HTML with each terminal color mapped to its design token. `bun run capture:website-hero` regenerates the file from the capture, and `bun run check` fails when it is stale.
9
+
10
+ Shared links preview through Open Graph and Twitter card tags in the page head, pointing at `og.png`, a 1200×630 card drawn from the same frame in the site's own styles. `bun run capture:website-og` screenshots it with a local Chromium into `packages/website/src/og.png`; rerun it after changing the headline, the hero frame, or the styles, and look at the result, since font rasterization differs between machines and nothing checks the bytes. The canonical origin every absolute URL is built from is `SITE_URL` in `packages/website/src/constants.ts`.
11
+
12
+ `dist/` is plain static files: any static host can serve it with no server runtime, credentials, or environment variables. On Vercel, set the root directory to `packages/website`, the build command to `bun run build`, and the output directory to `dist`.
13
+
14
+ The page loads one webfont from Google Fonts and makes no other third-party requests.