@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
@@ -65,6 +65,13 @@ spec → plan ⇄ plan-review → implement ⇄ code-review → verify:* → fin
65
65
  claims tickets that pass the `[tickets]` ready
66
66
  gate, chooses a short immutable slug from the final conforming spec, and
67
67
  starts builds up to the top-level `capacity` bound.
68
+ - **Ticket assets** — files or folders attached to a ticket (see
69
+ [Ticket assets](#ticket-assets)) are frozen into the build at claim and
70
+ appear under `.ab/assets/` in every phase, as inputs alongside the spec.
71
+ A `design` asset is part of the contract: `plan`, `plan-review`, `implement`,
72
+ `code-review`, and agent verify steps judge against it, as a contract on
73
+ observable visual and reference outcomes, not on implementation. Other kinds
74
+ are reference material.
68
75
  - **`spec`** — the ticket's spec becomes the build's contract. The `ab-spec`
69
76
  skill is the human-interactive surface for producing it, and it runs *before*
70
77
  a build exists.
@@ -337,7 +344,7 @@ gitPasswordEnv = "AB_GIT_READ_TOKEN"
337
344
 
338
345
  [workspace.config.runtimeProvisioning.pi]
339
346
  install = "npm install --global --ignore-scripts @earendil-works/pi-coding-agent@0.84.4"
340
- preflight = "test \"$(pi --version)\" = \"0.84.4\""
347
+ preflight = "test \"$(pi --version)\" = \"0.84.4\" && pi update --models"
341
348
  ```
342
349
 
343
350
  `timeoutSeconds` is required (60–86400; Hobby currently caps at 2700), `vcpus`
@@ -413,6 +420,12 @@ Names must already exist in `[verify].steps` with matching tables; duplicates,
413
420
  blanks, unknown names, malformed metadata, and omission of an `always = true`
414
421
  step make the planner's `ab done` fail before `plan.completed` is appended.
415
422
 
423
+ An agent verify step is handed the build's pinned ticket assets under
424
+ `.ab/assets/` (listed in `.ab/context.json`) as well as the spec. A verify skill
425
+ should treat a `design` asset as part of the contract, a contract on observable
426
+ visual and reference outcomes, not on implementation, and judge the result
427
+ against it.
428
+
416
429
  An agent verify step selects `[roles.<step>]` by its **logical step name** —
417
430
  the name in `[verify].steps`, not the skill it runs. An agent finalize
418
431
  post-step follows the same rule.
@@ -782,15 +795,21 @@ environment variable instead and say why.
782
795
 
783
796
  ### `[orchestrator]`
784
797
 
785
- Gates the operator-sandbox feature (AUT-340): a persistent, **credential-free**
786
- environment per operator × repository that an operator agent drives through the
787
- `sandbox.*` registry tools (`ab mcp`, and every later binding). The whole table
788
- is **closed to unknown keys** and restart-classified: changing it requires a
789
- dispatcher restart.
798
+ Gates the operator-agent feature (AUT-340, AUT-342): the persistent,
799
+ **credential-free** sandbox environment per operator × repository that an
800
+ operator agent drives through the `sandbox.*` registry tools (`ab mcp`, and
801
+ every later binding), and — when the embedded orchestrator is enabled — the
802
+ turn runner that answers operator sessions and wakes on build attention
803
+ events. The whole table is **closed to unknown keys** and restart-classified:
804
+ changing it requires a dispatcher restart.
790
805
 
791
806
  | Field | Default | Allowed / constraints | Effect |
792
807
  |---|---|---|---|
793
- | `enabled` | `false` | boolean | Master gate. With the table absent or `enabled = false`, the sandbox tools are absent from every binding and no environment is ever provisioned. |
808
+ | `enabled` | `false` | boolean | Master gate. With the table absent or `enabled = false`, the sandbox tools are absent from every binding, no environment is ever provisioned, and no dispatcher tick or message post ever runs orchestrator code — a local dispatcher is inert even when enabled, because the turn runner exists only in the hosted deployment. |
809
+ | `model` | — (required when `enabled = true`) | nonblank provider-qualified string, same vocabulary as `[roles]` | The turn runner's model, resolved through the deployment's gateway credential; a leading `vercel-ai-gateway/` prefix is accepted and stripped. |
810
+ | `invocationBudgetSeconds` | `240` | positive integer, clamped to 300 | Wall-clock budget for one turn invocation. A larger value is clamped — not rejected — to the 300-second hosted function limit the runner executes under. |
811
+ | `approvals` | `["builds.control:abort", "builds.control:discard", "builds.answer:revise-spec", "sandbox.publish", "tickets.move:ready"]` | array of `tool` or `tool:qualifier` strings | Registry tool calls that suspend a turn until the operator answers. Empty (`[]`) means none — autonomous operation. An entry naming an absent or unknown tool is **inert by design**, never an error. A qualified entry applies to one discriminator value only (`tickets.move:ready` approves moving a ticket to `ready`); for a tool without a discriminator, only the bare form matches. |
812
+ | `wake` | inherits the default | array of event-type globs | The attention events a **new** session wakes for. Absent inherits the default attention set (the events `ab watch` filters on by default); an explicit `[]` means never wake — message-only. Each glob must match at least one build event type. Existing sessions never retroactively inherit; an explicit wake update always wins. |
794
813
  | `sandbox` | — | strict subtable | The sandbox behavior knobs; absence reads as the defaults below. |
795
814
 
796
815
  `[orchestrator.sandbox]` fields:
@@ -814,6 +833,20 @@ and archiving an operator's last session are **destructive** (reset re-provision
814
833
  from the current base head); there is at most **one** environment per operator
815
834
  and repository.
816
835
 
836
+ Publication: `sandbox.publish` takes a title (1–200 characters), an optional
837
+ body (≤65,536 characters), and an optional explicit commit, and takes your
838
+ sandbox's current head commit to a pull request. It refuses when the checkout
839
+ has uncommitted changes — tracked or untracked (the sandbox's own provisioning
840
+ marker is excluded) — and when the commit is not a descendant of the base head
841
+ recorded when the sandbox was provisioned or last reset — a pre-existing environment without a recorded
842
+ base head asks you to run `sandbox.reset` first. The rule is PR only, never the
843
+ base branch: the sandbox itself never pushes and holds no credential — the push
844
+ and the PR creation are kernel plumbing through the workspace provider's
845
+ publication capability, to one deterministic operator branch per operator and
846
+ repository, so a later publish updates the same PR's head. A publication is a
847
+ repository-journal fact naming the operator, the session, the branch, the
848
+ commit, and the PR; a refused or failed attempt is journaled too.
849
+
817
850
  ## Setup and upgrades
818
851
 
819
852
  **`ab init <target> [--force]`** runs *outside* build sessions — it takes a
@@ -827,7 +860,7 @@ repo path, needs no `AB_*` environment, and is safe to re-run. It:
827
860
  `[commands]`, no verify or finalize steps, a valid local ticket gate, and one
828
861
  explicit runtime solely as a schema placeholder. It reads no package or
829
862
  language manifest and invents no command.
830
- - Vendors 11 skills before handoff, including the locally editable
863
+ - Vendors 12 skills before handoff, including the locally editable
831
864
  [repository setup reference](references/setup.md) in the installed `ab-guide`
832
865
  tree. On an interactive terminal it starts the selected coding-agent CLI in
833
866
  the target repository with a short prompt to read that reference and
@@ -1070,6 +1103,37 @@ subcommand accepts `--json`, which emits one bare JSON value and no prose: a
1070
1103
  `Ticket[]` for `list`, and the complete resulting `Ticket` for `create`,
1071
1104
  `update`, `block`, `unblock`, `show`, or `move`.
1072
1105
 
1106
+ ### Ticket assets
1107
+
1108
+ A ticket asset is a file or folder attached to a ticket and stored in the
1109
+ BuildStore, never in the ticket source: a design export, reference screenshots,
1110
+ a sample payload. Each has a kind and a name.
1111
+
1112
+ - `ab ticket attach <id> <kind> <path> [--name <name>] [--json]` stores `<path>`
1113
+ (default name: its basename). Attaching the same kind and name again makes a
1114
+ new revision; earlier ones stay retrievable. Over the store's size limit
1115
+ (25 MiB by default, lower on a hosted deployment) it is refused. Attach and
1116
+ remove leave a note on the ticket naming the asset.
1117
+ - `ab ticket show <id>` lists the current assets; with `--json` the ticket
1118
+ gains an `assets` array.
1119
+ - `ab ticket asset get <id> <kind> <name> <dest> [--rev <n>]` downloads the
1120
+ exact bytes of the latest (or given) revision; `ab ticket asset rm <id> <kind>
1121
+ <name>` drops it from the current assets.
1122
+ - **Frozen at claim.** A claim pins the current revisions into the build. A
1123
+ later attach or remove leaves a running build alone; a human answer with
1124
+ `--revise-spec-from-ticket` pins again. `ab build asset get <slug> <kind>
1125
+ <name> <dest> [--rev <n>]` downloads what the build froze, also after it ends.
1126
+ - **Where phases find them.** `ab context` writes each pinned asset to
1127
+ `.ab/assets/<kind>/<name>/` (a file asset is the single file inside that
1128
+ directory) and lists them under `assets` in `.ab/context.json` with kind,
1129
+ name, revision, layout, size, and path. The key is absent when the build has
1130
+ none.
1131
+
1132
+ By convention the `design` kind is a visual or reference contract, exported to
1133
+ static files when the ticket is groomed, so no build converts a link. The spec
1134
+ cites it by kind and name. See [the lifecycle](#the-lifecycle) for how phases
1135
+ judge against it.
1136
+
1073
1137
  ## Retrieving build artifacts
1074
1138
 
1075
1139
  `ab artifact download <build> <kind>[@rev] --output <file> [--store <ref>]`
@@ -1081,6 +1145,17 @@ Inside a phase, a complete ambient tuple permits only the ambient build; a
1081
1145
  foreign build or malformed/partial identity is rejected. Use the exact pinned
1082
1146
  `@rev` from a PR attachment command.
1083
1147
 
1148
+ Repository-scoped streams have their own form:
1149
+ `ab artifact download stream:<id>[@rev] --output <file> [--store <ref>]`. A
1150
+ stream id is store-assigned and globally unique, so no build argument is
1151
+ needed; the command resolves the stream, requires it to be repository-scoped
1152
+ and owned by this repository, and retrieves the artifact its close finalized
1153
+ (§7.6) — an open stream has no artifact yet. This form is strictly an operator
1154
+ read: under any complete ambient build or Harvest identity it fails closed
1155
+ (§8.2), since repository-scoped targets are never reachable by ambient
1156
+ own-build or own-harvest authority. Find stream ids in the session rows of
1157
+ `ab harvest status`.
1158
+
1084
1159
  In a build session, `ab artifact put <kind> <file> --attach` atomically deposits
1085
1160
  the exact bytes and designates that revision for the PR. A later designation of
1086
1161
  the same kind replaces it; distinct kinds remain distinct attachments. The
@@ -1214,16 +1289,19 @@ on its first paint; clearing the hold removes both on the next poll. The hold
1214
1289
  survives a restart and is not defeated by re-running `ab dispatch`.
1215
1290
 
1216
1291
  `--auto-merge` and `--no-auto-merge` similarly set the durable repository
1217
- claim-time default; omission reuses stored state, falling back to OFF only when
1292
+ auto-merge default; omission reuses stored state, falling back to OFF only when
1218
1293
  no fact exists. The forms are mutually exclusive independently of the intake
1219
1294
  pair. Global-row `m` re-reads current state, appends the opposite value, and
1220
1295
  posts a dispatcher notice. When on, each fresh dispatcher claim records the
1221
1296
  existing human-authored `build.auto-merge-requested` fact immediately after
1222
1297
  `build.created` and before runner launch. The first visible build frame therefore
1223
- carries `auto merge`. This is a creation-time seed, not policy: changes never
1224
- touch existing builds, resumed/adopted logs or other creation paths never sample
1225
- it, and build-row `m` remains independent (a seeded build can be cancelled while
1226
- the global default stays on). Both repository settings are independently
1298
+ carries `auto merge`. The default is also a bulk action over current builds:
1299
+ on each change the dispatcher reconciles every non-terminal build against the
1300
+ newest fact, requesting consent when it turns on and withdrawing it when it
1301
+ turns off, attributed to the toggling operator exactly as a row keypress would
1302
+ be. A per-build toggle overrides its build's state until the global row is
1303
+ toggled again, so a seeded or fanned build can still be cancelled while the
1304
+ global default stays on. Both repository settings are independently
1227
1305
  last-write-wins by event sequence. They are stored in the BuildStore, not in
1228
1306
  `autobuild.toml`; propagation uses polling, not a push channel.
1229
1307
 
@@ -1249,10 +1327,13 @@ settles every exhaustion barrier. Exhaustion and escalation remain terminal.
1249
1327
  Escalation's row is dismissed only by a post-terminal human resume request
1250
1328
  followed by its later kernel acknowledgement, never by the request alone.
1251
1329
 
1252
- A build with auto-merge off has no auto-merge token. Requested, enabled, and
1253
- cancelling states all read `auto merge`: cyan means requested locally but not
1254
- yet applied on GitHub, green means native auto-merge is enabled, and yellow
1255
- means cancellation is in flight. The token disappears when cancellation lands.
1330
+ Every build row renders an explicit `auto merge <state>` token, including off:
1331
+ cyan means requested locally but not yet applied on GitHub, green means native
1332
+ auto-merge is enabled, yellow means cancellation is in flight, and dim means
1333
+ off — turning red while the repository default reads ON, so the
1334
+ consent mismatch is visible without opening the detail view. A build parked on
1335
+ an open PR with no consent carries the reason on its merge step, naming the
1336
+ per-build control that requests it.
1256
1337
  Merge conflicts recover automatically: the gate defers, the pipeline re-enters
1257
1338
  reconcile, and verify re-runs before consent is re-examined — the reconcile
1258
1339
  step on the build row is the only indication, and uncomputed mergeability
@@ -1509,10 +1590,12 @@ stream:<id>` retrieves the closed session's finalized document.
1509
1590
  Harvest sessions stream the same protocol into *repository-scoped* streams:
1510
1591
  the harvest runner's synthesize and review brackets each get one stream
1511
1592
  (`phase` spelled `harvest:<step>`), with `harvest.session.started` carrying
1512
- the stream id. Find them via that `stream` field or the harvest journal
1513
- (`ab watch --repository`); a closed stream finalizes to a repository-scope
1514
- `stream:<id>` artifact. The harvest run id is not on the stream — recover it
1515
- from the journal event that names the stream.
1593
+ the stream id. `ab harvest status` shows each run's sessions the way build
1594
+ status does — one row per session with its stream id and open/closed status
1595
+ (`session hs_… (harvest, synthesize@1) stream st_… (open)`). Once a stream
1596
+ closes, its finalized document is a repository-scoped `stream:<id>` artifact,
1597
+ and `ab artifact download stream:<id>` retrieves it; the stream form is an
1598
+ operator-only read (ambient phase identity fails closed).
1516
1599
 
1517
1600
  **`ab watch [<slug>...] [--repository] [--event <glob>]... [--since <cursor>]
1518
1601
  [--timeout <dur>] [--interval <dur>] [--count <n>] [--json] [--store <ref>]`**
@@ -1553,7 +1636,10 @@ The watch ends with exit 0 when `--timeout` elapses (30 minutes by default;
1553
1636
  gone terminal; with no slugs named, an empty set of active builds does not end
1554
1637
  it. Usage errors, scope violations, unknown slugs, and cursor rejection exit 1
1555
1638
  before any record. A store read that fails after the watch has started is
1556
- reported once on stderr and retried without duplicating or skipping any event.
1639
+ reported on stderr once per failing read source — locally the poll cycle is
1640
+ one source, while against an `http(s)` store each tracked stream and the
1641
+ discovery pass carries its own failure streak — and retried without
1642
+ duplicating or skipping any event.
1557
1643
  Against a local store the default cadence polls every 1 second. Against an
1558
1644
  `http(s)` store each tracked stream long-polls instead — one held request per
1559
1645
  stream waits up to 25 seconds for the next event, requests run concurrently,
@@ -1635,8 +1721,11 @@ condition (one record with `condition` null, showing how the build ended); 3 —
1635
1721
  stdout is a bare `{"cursor": …}` object for the final position, plus one line
1636
1722
  on stderr); 4 — interrupted by SIGINT before any condition was satisfied (same
1637
1723
  cursor behavior as 3). The default poll interval and `--interval` match
1638
- `ab watch`. A store read that fails while waiting is reported once on stderr
1639
- and retried without missing or duplicating a condition. The command is
1724
+ `ab watch`. A store read that fails while waiting is reported on stderr once
1725
+ per failing read source — locally the poll cycle is one source, while
1726
+ against an `http(s)` store each tracked build and the discovery pass carries
1727
+ its own failure streak — and retried without missing or duplicating a
1728
+ condition. The command is
1640
1729
  read-only — it appends no event, takes no lease, and creates no record — and
1641
1730
  deciding what to do about a match stays with the caller's skill.
1642
1731
 
@@ -1770,12 +1859,16 @@ default, when you need to know what this repo's version says).
1770
1859
  | `ab-code-review` | `code-review` phase | Fresh skeptic: review the implementation diff against spec and plan, same verdict vocabulary. |
1771
1860
  | `ab-reconcile` | `reconcile` phase (epilogue) | Resolve a conflicted PR with one merge commit, base merged *into* the build branch. Never rebases. |
1772
1861
  | `ab-finalize` | `finalize` phase | Write the PR description for a green build; the kernel opens the PR. |
1773
-
1774
- Autobuild installs 11 skills. Everything except `ab-spec`, `ab-tickets`, and
1775
- `ab-guide` is **runner-invoked** by the kernel and carries
1776
- `disable-model-invocation: true` — do not invoke a phase skill yourself, and do
1777
- not remove that key to make one convenient to call. A model starting a pipeline
1778
- phase by pattern-matching a description is exactly what the flag prevents. The
1779
- three exceptions drive no phase, which is the criterion for membership (§16.3):
1780
- `ab-spec` and `ab-tickets` are human/agent-facing pre-build surfaces, and
1781
- `ab-guide` is read-only reference including the setup handoff guidance.
1862
+ | `ab-operate` | hosted only | The embedded orchestrator's own operating manual: its role, the registry tool surface with the approval semantics, the attention set, wake-trigger input, budget/approval suspension, and when to escalate to the operator rather than act. Invoked by the embedded orchestrator, never vendored into a build session. |
1863
+
1864
+ Autobuild installs 12 skills. Everything except `ab-spec`, `ab-tickets`, and
1865
+ `ab-guide` carries `disable-model-invocation: true` — do not invoke a phase
1866
+ skill yourself, and do not remove that key to make one convenient to call. A
1867
+ model starting a pipeline phase by pattern-matching a description is exactly
1868
+ what the flag prevents. Those three exceptions drive no build phase, which is
1869
+ the criterion for membership (§16.3): `ab-spec` and `ab-tickets` are
1870
+ human/agent-facing pre-build surfaces, and `ab-guide` is read-only reference
1871
+ including the setup handoff guidance. The fourth skill outside the
1872
+ model-invocable set, `ab-operate`, is not a build-phase skill either: it is the
1873
+ embedded orchestrator's own operating manual, read by the hosted turn runner as
1874
+ its system prompt and never invoked by a model in a build session.
@@ -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:
@@ -38,6 +38,17 @@ the durable provider locator as `BuildExecutionStart.workspaceRef`; local PID
38
38
  supervision remains host-owned. A workspace plugin relying on these fields
39
39
  should declare an API range containing `^1.5.0`.
40
40
 
41
+ Plugin API 1.7 extends the workspace-provider factory context with the
42
+ host-derived seams a store-requiring remote provider needs. Workspace-provider
43
+ factories additionally receive `storeRef` and `storeToken` (guaranteed present
44
+ when the registration declares `capabilities.storeRequirements`),
45
+ `runtimeReferences` (the host's effective runtime routes; absent at call sites
46
+ that reference no runtimes), and the checkout-less `origin` and
47
+ `remoteBranchHead` readers (absent means fall back to host `git` from
48
+ `repoRoot`). All five are optional, and a factory that ignores them changes
49
+ nothing. A plugin reading the new seams should declare an API range containing
50
+ `^1.7.0`.
51
+
41
52
  `BuildStore` and `BlobStore` contract types are exported for remote-server
42
53
  authors, but BuildStore is **not** an in-process manifest map. Implement the
43
54
  colocated language-neutral
@@ -113,6 +124,12 @@ Every module must default-export one strict `AutobuildPluginManifest`:
113
124
  absolute consuming-repository root. Ticket sources receive the existing
114
125
  `[tickets]` fields, workspace providers receive `[workspace.config]`, and
115
126
  runtime and forge factories currently receive an empty `config` object.
127
+ Workspace providers additionally receive the host-derived seams (API 1.7):
128
+ `storeRef` and `storeToken` — guaranteed strings when the registration
129
+ declares `capabilities.storeRequirements` — plus `runtimeReferences`,
130
+ `origin`, and `remoteBranchHead`. Each seam is optional and absent when the
131
+ host call site supplies no such value; absent `origin`/`remoteBranchHead`
132
+ fall back to host `git` from `repoRoot`.
116
133
 
117
134
  Add the module specifier at the TOML root, before any table. Repository-path
118
135
  specifiers resolve from the config-bearing root, which is the immutable build
@@ -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
 
@@ -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: