@defrex/autobuild 0.8.0 → 0.9.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 (147) hide show
  1. package/README.md +1 -1
  2. package/SPEC.md +313 -26
  3. package/bin/ab-build-runner.ts +8 -2
  4. package/docs/architecture.md +57 -14
  5. package/docs/assets/headline-wide.png +0 -0
  6. package/docs/configuration.md +151 -25
  7. package/docs/hosted-dispatcher.md +69 -13
  8. package/docs/operator-api.md +2 -2
  9. package/docs/remote-store-protocol.md +169 -16
  10. package/docs/remote-ticket-protocol.md +1 -1
  11. package/docs/setup.md +12 -9
  12. package/docs/website.md +10 -0
  13. package/package.json +43 -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/model.ts +57 -7
  19. package/packages/core/src/cli/dashboard/poll.ts +69 -10
  20. package/packages/core/src/cli/dashboard/render.ts +34 -5
  21. package/packages/core/src/cli/dispatch-frontend.ts +54 -5
  22. package/packages/core/src/cli/dispatch.ts +320 -46
  23. package/packages/core/src/cli/harvest.ts +122 -8
  24. package/packages/core/src/cli/help.ts +47 -8
  25. package/packages/core/src/cli/init-readiness-remote.ts +110 -0
  26. package/packages/core/src/cli/init-readiness-shared.ts +72 -0
  27. package/packages/core/src/cli/init-validation.ts +248 -222
  28. package/packages/core/src/cli/main.ts +70 -11
  29. package/packages/core/src/cli/mcp.ts +23 -1
  30. package/packages/core/src/cli/plugin.ts +7 -4
  31. package/packages/core/src/cli/remote-poll.ts +277 -0
  32. package/packages/core/src/cli/repo-state.ts +22 -0
  33. package/packages/core/src/cli/repository-status.ts +39 -1
  34. package/packages/core/src/cli/status.ts +149 -27
  35. package/packages/core/src/cli/terminals.ts +2 -1
  36. package/packages/core/src/cli/ticket-assets.ts +247 -0
  37. package/packages/core/src/cli/ticket.ts +307 -7
  38. package/packages/core/src/cli/wait.ts +147 -39
  39. package/packages/core/src/cli/watch.ts +128 -166
  40. package/packages/core/src/config/live.ts +22 -0
  41. package/packages/core/src/config/pipeline-source.ts +376 -0
  42. package/packages/core/src/config/schema.ts +230 -19
  43. package/packages/core/src/events/catalog.ts +1 -0
  44. package/packages/core/src/events/globs.ts +47 -0
  45. package/packages/core/src/events/payloads.ts +52 -2
  46. package/packages/core/src/events/repository.ts +52 -1
  47. package/packages/core/src/events/sessions.ts +69 -10
  48. package/packages/core/src/events/workspace-remote.ts +12 -0
  49. package/packages/core/src/integration/harness.ts +2 -1
  50. package/packages/core/src/kernel/auto-merge-default.ts +123 -0
  51. package/packages/core/src/kernel/phases.ts +16 -2
  52. package/packages/core/src/kernel/reducer.ts +40 -1
  53. package/packages/core/src/operator/annotations.ts +6 -0
  54. package/packages/core/src/operator/control.ts +19 -5
  55. package/packages/core/src/operator/index.ts +71 -6
  56. package/packages/core/src/operator/query.ts +208 -19
  57. package/packages/core/src/operator/registry.ts +55 -10
  58. package/packages/core/src/operator/requests.ts +9 -6
  59. package/packages/core/src/operator/sandbox-host.ts +140 -0
  60. package/packages/core/src/operator/sandbox.ts +247 -14
  61. package/packages/core/src/operator/ticket-source-backend.ts +47 -0
  62. package/packages/core/src/orchestrator/approvals.ts +115 -0
  63. package/packages/core/src/orchestrator/conversation.ts +167 -0
  64. package/packages/core/src/orchestrator/model.ts +86 -0
  65. package/packages/core/src/orchestrator/prompt.ts +61 -0
  66. package/packages/core/src/orchestrator/tools.ts +117 -0
  67. package/packages/core/src/orchestrator/turn-runner.ts +457 -0
  68. package/packages/core/src/plugin-sdk/index.ts +51 -1
  69. package/packages/core/src/plugins/load.ts +135 -7
  70. package/packages/core/src/plugins/manifest.ts +183 -10
  71. package/packages/core/src/plugins/registry.ts +73 -4
  72. package/packages/core/src/ports/forge/create.ts +1 -1
  73. package/packages/core/src/ports/forge/fake.ts +31 -0
  74. package/packages/core/src/ports/forge/github-transport.ts +13 -2
  75. package/packages/core/src/ports/forge/github.ts +13 -2
  76. package/packages/core/src/ports/forge/local-git.ts +31 -1
  77. package/packages/core/src/ports/runner/claude.ts +2 -2
  78. package/packages/core/src/ports/runner/codex.ts +6 -2
  79. package/packages/core/src/ports/runner/fake.ts +7 -1
  80. package/packages/core/src/ports/runner/pi.ts +8 -6
  81. package/packages/core/src/ports/runner/provider-error.ts +13 -3
  82. package/packages/core/src/ports/tickets/{remote-index.ts → hosted-index.ts} +3 -2
  83. package/packages/core/src/ports/types.ts +17 -2
  84. package/packages/core/src/ports/workspace/builtin-capabilities.ts +114 -0
  85. package/packages/core/src/ports/workspace/create.ts +74 -39
  86. package/packages/core/src/ports/workspace/distribution-archive.ts +171 -12
  87. package/packages/core/src/ports/workspace/fake.ts +169 -4
  88. package/packages/core/src/ports/workspace/git-worktree.ts +246 -26
  89. package/packages/core/src/ports/workspace/github-origin.ts +36 -0
  90. package/packages/core/src/ports/workspace/operator-sandbox.ts +7 -1
  91. package/packages/core/src/ports/workspace/provider-capabilities.ts +171 -0
  92. package/packages/core/src/ports/workspace/vercel-capabilities.ts +110 -0
  93. package/packages/core/src/ports/workspace/vercel-sandbox.ts +142 -114
  94. package/packages/core/src/processes/build-child.ts +17 -2
  95. package/packages/core/src/processes/build-execution-state.ts +37 -0
  96. package/packages/core/src/processes/build-runner.ts +54 -3
  97. package/packages/core/src/processes/dispatcher.ts +247 -41
  98. package/packages/core/src/processes/harvest-child.ts +5 -0
  99. package/packages/core/src/processes/harvest-runner.ts +66 -19
  100. package/packages/core/src/processes/harvest.ts +196 -24
  101. package/packages/core/src/processes/orchestrator-tick.ts +319 -0
  102. package/packages/core/src/processes/sandbox-state.ts +10 -0
  103. package/packages/core/src/store/adapter.ts +32 -0
  104. package/packages/core/src/store/build-scope.ts +64 -0
  105. package/packages/core/src/store/contract.ts +868 -0
  106. package/packages/core/src/store/digest.ts +49 -0
  107. package/packages/core/src/store/local/schema.ts +29 -2
  108. package/packages/core/src/store/local/store.ts +338 -4
  109. package/packages/core/src/store/memory.ts +179 -0
  110. package/packages/core/src/store/new-build.ts +17 -0
  111. package/packages/core/src/store/phase-session.ts +71 -0
  112. package/packages/core/src/store/remote/client.ts +260 -1
  113. package/packages/core/src/store/remote/index.ts +12 -2
  114. package/packages/core/src/store/remote/protocol.ts +100 -12
  115. package/packages/core/src/store/remote/version.ts +6 -2
  116. package/packages/core/src/store/repo-state-events.ts +140 -0
  117. package/packages/core/src/store/retention.ts +7 -0
  118. package/packages/core/src/store/session-handle.ts +64 -0
  119. package/packages/core/src/{sessions/reducer.ts → store/session-reducer.ts} +28 -9
  120. package/packages/core/src/store/streams/session-writer.ts +42 -2
  121. package/packages/core/src/store/streams/wait.ts +4 -5
  122. package/packages/core/src/store/ticket-assets.ts +425 -0
  123. package/packages/core/src/store/types.ts +122 -1
  124. package/packages/core/src/testing/index.ts +59 -0
  125. package/skills/code-review/SKILL.md +5 -1
  126. package/skills/guide/SKILL.md +126 -33
  127. package/skills/guide/references/operator-api.md +2 -2
  128. package/skills/guide/references/plugin-authoring.md +17 -0
  129. package/skills/guide/references/remote-store-protocol.md +169 -16
  130. package/skills/guide/references/remote-ticket-protocol.md +1 -1
  131. package/skills/guide/references/setup.md +12 -9
  132. package/skills/implement/SKILL.md +3 -0
  133. package/skills/operate/SKILL.md +126 -0
  134. package/skills/plan/SKILL.md +4 -0
  135. package/skills/plan-review/SKILL.md +5 -1
  136. package/skills/spec/SKILL.md +46 -3
  137. package/packages/core/src/cli/dashboard/frame-image.ts +0 -373
  138. package/packages/core/src/cli/store-opening.contract.ts +0 -289
  139. package/packages/core/src/cli/testkit.ts +0 -194
  140. package/packages/core/src/markdown.ts +0 -67
  141. package/packages/core/src/operator/client.ts +0 -311
  142. package/packages/core/src/operator/server.ts +0 -616
  143. package/packages/core/src/operator/sessions.ts +0 -223
  144. package/packages/core/src/ports/runner/live-contract-fixture.ts +0 -176
  145. package/packages/core/src/ports/tickets/remote-server.ts +0 -185
  146. package/packages/core/src/store/remote/server.ts +0 -923
  147. package/packages/core/src/testing/store-failures.ts +0 -32
@@ -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,7 +290,7 @@ 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 |
@@ -357,7 +357,8 @@ requires. A held request's authentication, token scope, and version-skew rules
357
357
  are identical to the immediate form: a `401`, `403`, or `409` identity failure
358
358
  is answered immediately, never held.
359
359
 
360
- The parameter is purely additive, so the protocol version stays `2`: a server
360
+ The parameter is purely additive in the version it landed in (protocol `2`,
361
+ where the event-wait rules were introduced): a server
361
362
  that ignores `wait` remains conforming — its immediate empty answers only raise
362
363
  the client's request rate — and a client that sends `wait` to such a server
363
364
  observes no other difference. This is the same reasoning the stream and session
@@ -368,7 +369,11 @@ waits apply. There is no push transport; a held request is the whole mechanism.
368
369
  Repository journals are separate resources with independent event sequences,
369
370
  artifacts, and leases. `{repo}` is one percent-encoded repository id; encode
370
371
  embedded `/` characters. All resource routes authorize and require the
371
- repository to exist. An unknown repository returns `404 not-found`.
372
+ repository to exist. An unknown repository returns `404 not-found` — with one
373
+ carve-out: the build-digest batch read of this section is keyed to build
374
+ records, not the repository journal record, so it authorizes the repo token
375
+ but answers `200` (possibly empty) even for an unknown repository, a repository
376
+ without a journal record, or a repository without builds.
372
377
 
373
378
  | BuildStore operation | HTTP route | Request | Success |
374
379
  |---|---|---|---|
@@ -376,10 +381,12 @@ repository to exist. An unknown repository returns `404 not-found`.
376
381
  | `getRepo` | `GET /repos/{repo}` | none | `200` + `RepositoryRecord`; absent is `404` (the shipped client maps this to `null`) |
377
382
  | `appendRepo` | `POST /repos/{repo}/events` | event write | `201` + repository event envelope |
378
383
  | `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 |
384
+ | `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` |
379
385
  | `appendRepoWithArtifacts` | `POST /repos/{repo}/deposits` | atomic deposit request | `201` + `{event, artifacts}` using repository shapes |
380
386
  | `putRepoArtifact` | `POST /repos/{repo}/artifacts` | artifact input | `201` + repository artifact metadata |
381
387
  | `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
388
  | `listRepoArtifacts` | `GET /repos/{repo}/artifact-list?kind={kind}` | optional `kind`; absence means all kinds | `200` + metadata ordered by kind and then increasing revision |
389
+ | `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
390
  | `claimRepoLease` | `POST /repos/{repo}/lease/claim` | claim request | `200 {"ok": boolean}` |
384
391
  | `heartbeatRepo` | `POST /repos/{repo}/lease/heartbeat` | holder request | `200 {"ok": boolean}` |
385
392
  | `releaseRepoLease` | `POST /repos/{repo}/lease/release` | holder request | `200 {"ok": true}`; a wrong holder is a successful no-op |
@@ -389,10 +396,153 @@ creation operation and must not reset timestamps, events, artifacts, or lease
389
396
  state when the repository already exists. Event sequence numbering starts at 1
390
397
  for each repository independently of every build and other repository.
391
398
 
399
+ A `BuildDigest` is the projection of one build's log onto its terminal fact
400
+ and its `observation.recorded` occurrences:
401
+
402
+ ```jsonc
403
+ // BuildDigest
404
+ {
405
+ "slug": "ab-fix-login-3",
406
+ "terminal": "done", // optional: "done" | "aborted", omitted while the log has neither
407
+ "observations": [ // ascending-seq pairs for the log's observation.recorded events
408
+ { "seq": 1, "ts": "2026-07-15T12:00:01.000Z" },
409
+ { "seq": 4, "ts": "2026-07-15T12:00:04.000Z" }
410
+ ],
411
+ "merged": "2026-07-15T12:00:06.000Z" // optional: ts of the log's latest pr.merged, omitted when none
412
+ }
413
+ ```
414
+
415
+ The digest is derived from the event log on every call — the server persists
416
+ nothing and there is no refresh step. Protocol version `3` reshaped the
417
+ digest's `observations` from bare sequence numbers to `{seq, ts}` pairs and
418
+ added the optional `merged` fact, so the harvest pressure gate can evaluate
419
+ launch decisions from digests and the repository journal alone instead of
420
+ reading every build's full history per evaluation. That is a breaking wire
421
+ change, not an additive one: a server that predates it returns the old shape
422
+ and would fail the client's schema parse mid-request, which is why the version
423
+ moved — a stale peer is now rejected at the handshake (`409 conflict`) instead.
424
+ This is the same discipline the transport's exact-match version rules promise:
425
+ wire shapes that independently implemented servers must reproduce update this
426
+ document and the protocol version together.
427
+
392
428
  The build-stream query, artifact, ordering, timestamp, and validation rules
393
429
  apply symmetrically to repository journals, including the event-read bounded
394
430
  wait of section 3.
395
431
 
432
+ ### Ticket assets
433
+
434
+ A ticket asset is a named, versioned bundle of files attached to
435
+ a ticket, keyed by `(repo, ticketId, kind, name)`. These routes live under the
436
+ repository but, unlike the journal routes, **do not require a repository
437
+ record**: assets can precede any journal row, so the repository-existence gate
438
+ does not apply. `{repo}` and `{id}` are percent-encoded path segments. Token
439
+ authorization follows the one-matching-repo column of the matrix (a repo token
440
+ for its own repository, or admin); build and session tokens receive `403 auth`.
441
+ The family is additive, so `REMOTE_STORE_PROTOCOL_VERSION` did not change.
442
+
443
+ A build also reads the assets it froze at its claim through its own build-scoped
444
+ routes, which a build token may use (no ticket-asset authority needed). The
445
+ server reduces the build's events to find the pinned revision, then returns that
446
+ exact revision, so a later replace or removal never changes the answer and a
447
+ finished build still answers:
448
+
449
+ | BuildStore operation | HTTP route | Request | Success |
450
+ |---|---|---|---|
451
+ | `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 |
452
+ | `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 |
453
+
454
+ | BuildStore operation | HTTP route | Request | Success |
455
+ |---|---|---|---|
456
+ | `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 |
457
+ | `putTicketAsset` | `POST /repos/{repo}/tickets/{id}/assets` | `{kind, name, layout, entries}` | `201` + `TicketAssetMeta` (with its manifest) |
458
+ | `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 |
459
+ | `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 |
460
+ | `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 |
461
+ | `removeTicketAsset` | `POST /repos/{repo}/tickets/{id}/assets/remove` | `{kind, name}` | `200` + the removed `TicketAssetMeta`, or `200 null` when no live asset exists |
462
+
463
+ Wire types:
464
+
465
+ ```jsonc
466
+ // put request: entries are files (content as base64) or explicit directories
467
+ { "kind": "design", "name": "home", "layout": "tree", // layout: "file" | "tree"
468
+ "entries": [
469
+ { "type": "file", "path": "index.html", "contentBase64": "PGgxPg==" },
470
+ { "type": "dir", "path": "img/empty" }
471
+ ] }
472
+
473
+ // TicketAssetMeta: dir entries carry no size or blobRef
474
+ { "repo": "…", "ticketId": "AUT-1", "kind": "design", "name": "home",
475
+ "revision": 0, "layout": "tree", "size": 1234, // size = total file bytes
476
+ "entries": [
477
+ { "type": "file", "path": "index.html", "size": 1234, "blobRef": "<sha256 hex>" },
478
+ { "type": "dir", "path": "img/empty" }
479
+ ],
480
+ "createdAt": "2026-10-01T12:00:00.000Z" }
481
+
482
+ // TicketAssetSummary (list): the same without entries
483
+ { "repo": "…", "ticketId": "AUT-1", "kind": "design", "name": "home",
484
+ "revision": 0, "layout": "tree", "size": 1234,
485
+ "fileCount": 1, "dirCount": 1, "createdAt": "…" }
486
+ ```
487
+
488
+ Semantics every server must reproduce:
489
+
490
+ - **Revisions** are 0-based per `(repo, ticketId, kind, name)`. Putting the same
491
+ kind and name again creates the next revision; earlier revisions stay readable
492
+ by explicit `rev`.
493
+ - **Removal is a tombstone** that takes the next revision number with an empty
494
+ manifest. The latest row decides liveness: a read without `rev` and the
495
+ default list skip a removed asset, while an explicit earlier `rev` keeps
496
+ working (a build that pinned that revision still retrieves it). Re-attaching
497
+ takes the next revision, so numbers may skip the tombstone. `revisions=1`
498
+ lists real revisions only.
499
+ - **Validation** (`422 validation`, nothing stored). `kind` and `name` match
500
+ `[A-Za-z0-9._-]+`, at most 64 characters. Paths are relative and
501
+ `/`-separated, with no empty, `.`, or `..` segment and no NUL, at most 256
502
+ UTF-8 bytes (128 per segment); paths are unique across files and directories
503
+ and no file is the ancestor of another entry. `layout: "file"` has exactly one
504
+ top-level `file` entry and no directory; `layout: "tree"` may hold any number
505
+ of entries, including zero (an empty folder; its root is implicit). A revision
506
+ holds at most 1000 entries (files plus directories) and **26214400 bytes (25
507
+ MiB)** of file bytes; the error names the limit, for example `ticket asset
508
+ exceeds the 26214400-byte (25 MiB) limit`.
509
+ - File bytes are content-addressed (`blobRef` is the sha256 hex) in the same
510
+ blob store as artifacts; the database holds only the manifest.
511
+
512
+ **Deployment ceilings.** A platform may reject oversized request or response
513
+ bodies before any application code runs (Vercel's functions cap them at 4.5
514
+ MB), so validating on the server alone cannot enforce a limit. A server that
515
+ has such a ceiling advertises it as `maxRequestBytes` on the limits route
516
+ together with the effective `maxBytes`, computed from it as follows. Paths and
517
+ entry counts are bounded, so the worst-case wire overhead is a constant:
518
+ `overhead = 1024 + 1000 × 320 = 321024` bytes, and
519
+ `maxBytes = min(26214400, max(0, floor((maxRequestBytes − overhead) × 3 / 4)))`
520
+ (base64 inflates content by 4/3); `maxEntries` stays 1000. The hosted service
521
+ sets `maxRequestBytes` to 4 MiB, giving an effective per-asset limit of 2904960
522
+ bytes (about 2.8 MiB). The formula is a safe lower bound, not the final
523
+ authority:
524
+
525
+ - The shipped **client** reads the limits once per store handle and, before any
526
+ upload, validates against `maxBytes` and `maxEntries` and then compares the
527
+ exact UTF-8 length of the serialized request with `maxRequestBytes`. A bundle
528
+ that does not fit is refused locally with `ticket asset exceeds this store's
529
+ N-byte limit (deployment request ceiling M bytes)` and **no upload request is
530
+ made**.
531
+ - The **server** re-validates every put against its effective limits and checks
532
+ every ticket-asset body against `maxRequestBytes` itself, answering `413` (a
533
+ request) or `422` with a limit-naming message where the platform lets the
534
+ request through.
535
+ - **Downloads never need a bundle-sized response**: the client reads the
536
+ manifest and then each file by its own request. The manifest, file, and list
537
+ routes check the exact serialized length of their response against
538
+ `maxRequestBytes` and fail with a `413 validation` error naming the asset (or
539
+ the file, its size, and the ceiling) rather than letting the platform reject
540
+ it. This matters for a revision stored under an older or higher ceiling:
541
+ its manifest and smaller files remain retrievable; a file that cannot fit can
542
+ only be fetched from a store with a higher ceiling. Chunked or ranged transfer
543
+ is not part of the protocol, so a deployment's effective per-asset limit is
544
+ its advertised `maxBytes`.
545
+
396
546
  ## 5. Operator-session operations
397
547
 
398
548
  Operator sessions are a third resource kind: durable orchestrator-conversation
@@ -430,6 +580,7 @@ identity is already the actor.
430
580
  | `listSessions` | `GET /repos/{repo}/sessions` | none | `200` + `SessionRecord[]`, creation order (`createdAt`, then the store's creation counter for same-millisecond ties) |
431
581
  | `getSession` | `GET /sessions/{id}` | none | `200` + `SessionRecord`; absent is `404` (the shipped client maps this to `null`) |
432
582
  | `appendSessionEvent` | `POST /sessions/{id}/events` | event write | `201` + session event envelope (same envelope shape with `"session"` in place of `"build"`) |
583
+ | `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
584
  | `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
585
  | `appendSessionWithArtifacts` | `POST /sessions/{id}/deposits` | atomic deposit request | `201` + `{event, artifacts}` using session shapes; the substitution algorithm of section 8 applies unchanged |
435
586
  | `putSessionArtifact` | `POST /sessions/{id}/artifacts` | artifact input | `201` + session artifact metadata |
@@ -599,7 +750,8 @@ by this rule (their retention belongs to the open archival thread), and open
599
750
  streams are never pruned. A read of a stream whose chunks were pruned returns
600
751
  no chunks, `status: "closed"`, and the artifact reference.
601
752
 
602
- These additions are purely additive: the protocol version stays `2`, and a
753
+ These additions were purely additive in the version they landed in (protocol
754
+ `2`, where the stream routes were introduced), and a
603
755
  conforming server implements the stream routes together with this document's
604
756
  rules exactly as it does for the event and artifact routes.
605
757
 
@@ -675,12 +827,12 @@ resource access; an explicit resource whose id is `"*"` is not admin.
675
827
 
676
828
  ### Resource authorization matrix
677
829
 
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 |
830
+ | 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 |
831
+ |---|---:|---:|---:|---:|---:|---:|---:|---:|---:|
832
+ | admin (`build: "*"`) | yes | any | yes | any | yes | any | any | any | any |
833
+ | build | no | exact id only | no | no | no | no | no | no | no |
834
+ | repo | no | no | no | exact id only | yes | exact id only | exact id only | exact repo only | no |
835
+ | session | no | no | no | no | no | no | no | no | exact id only |
684
836
 
685
837
  A valid token used for the wrong resource receives `403 auth`. Resource scope
686
838
  gates all operations, including reads, artifact operations, and leases. A
@@ -793,8 +945,8 @@ The shipped server maps failures as follows:
793
945
  | `403` | `auth` | Valid token with the wrong resource scope or event-session attribution |
794
946
  | `404` | `not-found` | Unknown route, unsupported method, unknown build, unknown repository, unknown session, or an unknown or foreign-scoped stream |
795
947
  | `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 |
948
+ | `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 |
949
+ | `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
950
  | `500` | `internal` | Any other unexpected backing or server failure; the thrown error message is returned |
799
951
 
800
952
  Authentication runs before resource lookup, and session authorization runs
@@ -916,11 +1068,12 @@ Four shipped behaviors do not add `BuildStore` routes:
916
1068
  store lifecycle; there is no close endpoint.
917
1069
  - `GET /health` is outside the `BuildStore` interface. It is always open and
918
1070
  returns exactly `200 {"ok":true,"autobuildVersion":"<package>",
919
- "protocolVersion":"2"}`. It reports HTTP-process availability and identity,
1071
+ "protocolVersion":"3"}`. It reports HTTP-process availability and identity,
920
1072
  not a deeper backing-store transaction or migration check.
921
1073
 
922
- There is no push/WebSocket subscription protocol, batch-read route,
923
- repository listing, artifact deletion, or server deployment API.
1074
+ There is no push/WebSocket subscription protocol, repository listing, artifact
1075
+ deletion, or server deployment API. The one repo-scoped batch read is the
1076
+ build-digest route of section 4; no other batch-read route exists.
924
1077
 
925
1078
  ## 12. Conformance
926
1079
 
@@ -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,10 @@
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
+ `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`.
9
+
10
+ The page loads one webfont from Google Fonts and makes no other third-party requests.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@defrex/autobuild",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "Agent-driven software delivery from groomed ticket to merged PR",
5
5
  "repository": {
6
6
  "type": "git",
@@ -37,10 +37,15 @@
37
37
  "import": "./packages/core/src/store/remote/index.ts",
38
38
  "default": "./packages/core/src/store/remote/index.ts"
39
39
  },
40
- "./operator-api": {
41
- "types": "./packages/core/src/operator/index.ts",
42
- "import": "./packages/core/src/operator/index.ts",
43
- "default": "./packages/core/src/operator/index.ts"
40
+ "./hosted-tickets": {
41
+ "types": "./packages/core/src/ports/tickets/hosted-index.ts",
42
+ "import": "./packages/core/src/ports/tickets/hosted-index.ts",
43
+ "default": "./packages/core/src/ports/tickets/hosted-index.ts"
44
+ },
45
+ "./testing": {
46
+ "types": "./packages/core/src/testing/index.ts",
47
+ "import": "./packages/core/src/testing/index.ts",
48
+ "default": "./packages/core/src/testing/index.ts"
44
49
  },
45
50
  "./operator-presentation": {
46
51
  "types": "./packages/core/src/operator/presentation.ts",
@@ -52,11 +57,6 @@
52
57
  "import": "./packages/core/src/ports/tickets/update.ts",
53
58
  "default": "./packages/core/src/ports/tickets/update.ts"
54
59
  },
55
- "./remote-tickets": {
56
- "types": "./packages/core/src/ports/tickets/remote-index.ts",
57
- "import": "./packages/core/src/ports/tickets/remote-index.ts",
58
- "default": "./packages/core/src/ports/tickets/remote-index.ts"
59
- },
60
60
  "./origin": {
61
61
  "types": "./packages/core/src/kernel/origin.ts",
62
62
  "import": "./packages/core/src/kernel/origin.ts",
@@ -76,12 +76,32 @@
76
76
  "types": "./packages/core/src/ports/workspace/distribution-archive.ts",
77
77
  "import": "./packages/core/src/ports/workspace/distribution-archive.ts",
78
78
  "default": "./packages/core/src/ports/workspace/distribution-archive.ts"
79
+ },
80
+ "./operator": {
81
+ "types": "./packages/core/src/operator/index.ts",
82
+ "import": "./packages/core/src/operator/index.ts",
83
+ "default": "./packages/core/src/operator/index.ts"
84
+ },
85
+ "./operator-sandbox-host": {
86
+ "types": "./packages/core/src/operator/sandbox-host.ts",
87
+ "import": "./packages/core/src/operator/sandbox-host.ts",
88
+ "default": "./packages/core/src/operator/sandbox-host.ts"
79
89
  }
80
90
  },
81
91
  "files": [
82
92
  "bin",
83
93
  "packages/core/src",
84
94
  "!packages/core/src/**/*.test.ts",
95
+ "!packages/core/src/**/*.test.tsx",
96
+ "!packages/core/src/**/*.spec.ts",
97
+ "!packages/core/src/**/*.spec.tsx",
98
+ "!packages/core/src/testing/store-failures.ts",
99
+ "!packages/core/src/testing/packed-install.ts",
100
+ "!packages/core/src/cli/testkit.ts",
101
+ "!packages/core/src/cli/store-opening.contract.ts",
102
+ "!packages/core/src/ports/runner/live-contract-fixture.ts",
103
+ "!packages/core/src/cli/dashboard/frame-image.ts",
104
+ "!packages/core/src/markdown.ts",
85
105
  "skills",
86
106
  "templates",
87
107
  "LICENSE",
@@ -90,24 +110,23 @@
90
110
  "docs"
91
111
  ],
92
112
  "scripts": {
93
- "dev": "bun run --bun next dev",
94
113
  "dev:cli": "bun --hot --no-clear-screen bin/ab-dev.ts",
95
- "build": "bun run --bun next build",
96
- "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",
97
- "start": "bun run --bun next start",
98
- "hosted-store": "bun server.ts",
99
114
  "capture:dashboard": "bun tools/dashboard-capture.ts",
100
- "capture:web-dashboard": "bun tools/web-dashboard-capture.tsx",
115
+ "capture:web-dashboard": "bun packages/web-dashboard-capture/src/web-dashboard-capture.tsx",
116
+ "capture:website": "bun tools/website-capture.ts",
117
+ "website:dev": "bun packages/website/src/dev.ts",
118
+ "website:build": "bun packages/website/src/build.ts",
101
119
  "capture:readme-headline": "bun tools/readme-headline.ts",
102
120
  "release": "bun tools/release.ts",
103
121
  "postgres:migrate": "bun packages/postgres-store/src/bin.ts migrate",
104
- "check": "bun tools/biome-check.ts && bun tools/product-name-check.ts && bun tools/docs-asset-check.ts && bun tools/readme-headline.ts --check && bun tools/workspace-manifest-check.ts",
122
+ "check": "bun tools/biome-check.ts && bun tools/product-name-check.ts && bun tools/docs-asset-check.ts && bun tools/skill-docs-asset-check.ts && bun tools/readme-headline.ts --check && bun tools/workspace-manifest-check.ts && bun tools/package-boundary-check.ts && bun tools/bin-mode-check.ts && bun tools/publish-contents-check.ts && bun tools/publish-imports-check.ts",
105
123
  "format:staged": "git-format-staged --formatter './node_modules/.bin/biome format --stdin-file-path {}' '*.js' '*.jsx' '*.ts' '*.tsx' '*.json' '*.jsonc' '*.css' '*.graphql' '*.gql'",
106
124
  "prepare": "husky",
107
- "test": "bun test packages/*/src tools bin app",
125
+ "test": "bun test packages/*/src tools bin packages/hosted-store-service/app",
108
126
  "typecheck": "tsc --noEmit"
109
127
  },
110
128
  "dependencies": {
129
+ "@ai-sdk/gateway": "4.0.82",
111
130
  "@modelcontextprotocol/sdk": "^1.30.0",
112
131
  "@vercel/sandbox": "^3.1.0",
113
132
  "ai": "^7.0.101",
@@ -118,18 +137,19 @@
118
137
  },
119
138
  "devDependencies": {
120
139
  "@biomejs/biome": "2.5.5",
140
+ "@defrex/autobuild-postgres-store": "0.8.0",
141
+ "@defrex/autobuild-vercel-sandbox": "0.8.0",
121
142
  "@resvg/resvg-js": "2.6.2",
122
143
  "@types/bun": "^1.3.14",
123
144
  "@types/node": "^24.0.0",
124
- "@types/pg": "8.16.0",
125
- "@types/react": "19.2.14",
126
- "@types/react-dom": "19.2.3",
127
145
  "@types/semver": "^7.7.1",
128
146
  "dejavu-fonts-ttf": "2.37.0",
129
147
  "drizzle-kit": "^0.31.10",
130
148
  "git-format-staged": "4.0.1",
131
149
  "husky": "9.1.7",
132
- "typescript": "5.9.3",
133
- "vercel": "^59.11.2"
150
+ "typescript": "5.9.3"
151
+ },
152
+ "patchedDependencies": {
153
+ "better-auth@1.4.18": "patches/better-auth@1.4.18.patch"
134
154
  }
135
155
  }
@@ -9,10 +9,11 @@ import { basename, dirname, resolve } from 'node:path'
9
9
  import { agentActor } from '../events/envelope'
10
10
  import { prAttachmentSchema } from '../ontology'
11
11
  import type { Exec } from '../ports/workspace/git-worktree'
12
- import type { Artifact, ArtifactMeta, BuildStore } from '../store/types'
12
+ import type { Artifact, ArtifactMeta, BuildStore, RepositoryArtifact } from '../store/types'
13
13
  import type { CliEnv } from './env'
14
- import { buildInRepository } from './repo-state'
15
- import { withAmbientReadStore, type StoreOpener } from './store-opening'
14
+ import { resolveAmbientReadSession } from './env'
15
+ import { buildInRepository, streamInRepository } from './repo-state'
16
+ import { withAmbientReadStore, withSessionlessStore, type StoreOpener } from './store-opening'
16
17
 
17
18
  export interface ArtifactDeps {
18
19
  store: BuildStore
@@ -174,9 +175,132 @@ export async function artifactDownload(
174
175
  )
175
176
  }
176
177
 
177
- const outputPath = resolve(opts.targetRepo, opts.outputPath)
178
- await mkdir(dirname(outputPath), { recursive: true })
179
- await writeFile(outputPath, artifact.content)
178
+ const outputPath = await writeArtifactBytes(opts.targetRepo, opts.outputPath, artifact.content)
180
179
  return { artifact, outputPath }
181
180
  })
182
181
  }
182
+
183
+ /** Shared download tail: exact bytes under the resolved output path (parent
184
+ * directories created), used by both the build-scoped and stream-addressed
185
+ * forms so neither can drift on byte handling. */
186
+ async function writeArtifactBytes(
187
+ targetRepo: string,
188
+ outputPath: string,
189
+ content: Uint8Array,
190
+ ): Promise<string> {
191
+ const resolved = resolve(targetRepo, outputPath)
192
+ await mkdir(dirname(resolved), { recursive: true })
193
+ await writeFile(resolved, content)
194
+ return resolved
195
+ }
196
+
197
+ export interface ArtifactDownloadStreamOpts {
198
+ /** Current checkout; repository identity resolves to its main worktree. */
199
+ targetRepo: string
200
+ env: Record<string, string | undefined>
201
+ exec: Exec
202
+ /** `stream:<id>` or `stream:<id>@<rev>`; rev pins the finalized artifact. */
203
+ spec: string
204
+ outputPath: string
205
+ /** Explicit --store; precedence remains flag > AB_STORE > local default. */
206
+ storeRef?: string
207
+ /** Adapter seam for local/remote selection tests. */
208
+ openStore?: StoreOpener
209
+ }
210
+
211
+ export interface ArtifactDownloadStreamResult {
212
+ artifact: RepositoryArtifact
213
+ outputPath: string
214
+ }
215
+
216
+ /** Operator retrieval of a finalized, repo-scoped stream artifact (the harvest
217
+ * session analog of the build-scoped form). A stream id is store-assigned and
218
+ * globally unique, so no build or repo argument is needed: the command reads
219
+ * the stream record, checks its scope, then fetches the close-deposited
220
+ * repository artifact.
221
+ *
222
+ * Authorization is the operator-wide read rule only (§8.2): this form requires
223
+ * a sessionless invocation and fails closed under any complete ambient phase
224
+ * identity — repository-scoped targets are never reachable by ambient
225
+ * own-build or own-harvest authority. The check runs here, before any store
226
+ * call, because local harvest-ambient handles would permit own-repo
227
+ * `getRepoArtifact` while remote handles rely on the core to enforce scope. */
228
+ export async function artifactDownloadStream(
229
+ opts: ArtifactDownloadStreamOpts,
230
+ ): Promise<ArtifactDownloadStreamResult> {
231
+ const ambient = resolveAmbientReadSession(opts.env)
232
+ if (ambient !== undefined) {
233
+ throw new Error(
234
+ 'repo-scoped stream downloads are operator reads (§8.2): ambient phase ' +
235
+ 'identity (build or Harvest) never extends to repository-scoped targets — ' +
236
+ 'run this command without agent identity markers',
237
+ )
238
+ }
239
+ const { kind, rev } = parseArtifactSpec(opts.spec)
240
+ const streamId = kind.startsWith('stream:') ? kind.slice('stream:'.length) : ''
241
+ if (streamId.trim() === '') {
242
+ throw new Error(
243
+ `invalid stream ref "${opts.spec}" — expected 'stream:<id>' or 'stream:<id>@<rev>'`,
244
+ )
245
+ }
246
+ return withSessionlessStore(
247
+ {
248
+ targetRepo: opts.targetRepo,
249
+ env: opts.env,
250
+ exec: opts.exec,
251
+ ...(opts.storeRef !== undefined ? { storeRef: opts.storeRef } : {}),
252
+ ...(opts.openStore !== undefined ? { openStore: opts.openStore } : {}),
253
+ },
254
+ async (context) => {
255
+ const record = await context.store.getStream(streamId)
256
+ if (record === null) {
257
+ throw new Error(
258
+ `no stream "${streamId}" in this store — run 'ab harvest status' to find ` +
259
+ 'session stream ids, or pass --store <ref>',
260
+ )
261
+ }
262
+ if (record.scope.kind === 'build') {
263
+ throw new Error(
264
+ `stream "${streamId}" is build-scoped — download it with ` +
265
+ `'ab artifact download ${record.scope.build} stream:${streamId}'`,
266
+ )
267
+ }
268
+ if (record.scope.kind === 'session') {
269
+ throw new Error(
270
+ `stream "${streamId}" is session-scoped — read it through the operator ` +
271
+ `session stream surface (GET /sessions/${record.scope.session}/streams/${streamId})`,
272
+ )
273
+ }
274
+ // Origin-aware membership, mirroring the build-scoped path above.
275
+ // Unlike a BuildRecord, StreamScope carries no `repoOrigin`, so a legacy
276
+ // physical-path scope.repo stays rejected here (see
277
+ // `streamInRepository` for the documented AUT-314-class divergence).
278
+ if (!streamInRepository(record.scope.repo, context.repo)) {
279
+ throw new Error(
280
+ `stream "${streamId}" belongs to repository "${record.scope.repo}", not "${context.repo}"`,
281
+ )
282
+ }
283
+ if (record.status === 'open') {
284
+ throw new Error(
285
+ `stream "${streamId}" is open — its finalized artifact exists only after ` +
286
+ 'the stream closes (§7.6)',
287
+ )
288
+ }
289
+ const artifact = await context.store.getRepoArtifact(record.scope.repo, kind, rev)
290
+ if (artifact === null) {
291
+ const available = await context.store.listRepoArtifacts(record.scope.repo, kind)
292
+ const refs = available.map((meta) => `${meta.kind}@${meta.revision}`)
293
+ throw new Error(
294
+ `no "${kind}" artifact${rev !== undefined ? ` at rev ${rev}` : ''} in ` +
295
+ `repository "${record.scope.repo}" — available refs: ${refs.join(', ') || '(none)'}`,
296
+ )
297
+ }
298
+ const outputPath = await writeArtifactBytes(
299
+ opts.targetRepo,
300
+ opts.outputPath,
301
+ artifact.content,
302
+ )
303
+ return { artifact, outputPath }
304
+ },
305
+ )
306
+ }
@@ -165,12 +165,20 @@ export async function runBinary(
165
165
  // A scoped phase process is composed from the build worktree's immutable
166
166
  // configuration just like dispatch. Load and select the forge before opening
167
167
  // the store so plugin failures cannot partially execute terminal plumbing.
168
+ // Guest tolerance (AUT-517), always on here: in checkout mode `ab dispatch`
169
+ // already failed startup if a configured plugin did not resolve, and
170
+ // origin-mode phase processes only run inside guests — so an unresolvable
171
+ // bare specifier here can only be the provider-plugin case, which a phase
172
+ // process (never constructing workspace providers) may skip.
168
173
  let forge: Awaited<ReturnType<typeof createForge>>
169
174
  try {
170
175
  const repoRoot = process.cwd()
171
176
  const packageRoot = await resolveMainRepo(repoRoot, spawnExec)
172
177
  const config = await loadConfig(join(repoRoot, 'autobuild.toml'))
173
- const plugins = await loadPlugins(config.plugins, repoRoot, { packageRoot })
178
+ const plugins = await loadPlugins(config.plugins, repoRoot, {
179
+ packageRoot,
180
+ guest: true,
181
+ })
174
182
  forge = await createForge({
175
183
  name: config.forge,
176
184
  registry: plugins,