@defrex/autobuild 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (251) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +192 -0
  3. package/SPEC.md +2269 -0
  4. package/bin/ab-build-runner.ts +47 -0
  5. package/bin/ab-dev.ts +27 -0
  6. package/bin/ab-dispatch-kernel.ts +92 -0
  7. package/bin/ab-harvest-runner.ts +29 -0
  8. package/bin/ab-init-probe.ts +19 -0
  9. package/bin/ab-process-group-reaper.ts +32 -0
  10. package/bin/ab.ts +6 -0
  11. package/bin/agent/ab +3 -0
  12. package/docs/answering-blocked-builds.md +89 -0
  13. package/docs/architecture.md +726 -0
  14. package/docs/assets/headline-wide.png +0 -0
  15. package/docs/configuration.md +1623 -0
  16. package/docs/hosted-dispatcher-cutover-validation.md +178 -0
  17. package/docs/hosted-dispatcher.md +204 -0
  18. package/docs/mcp.md +119 -0
  19. package/docs/operator-api.md +186 -0
  20. package/docs/remote-store-protocol.md +972 -0
  21. package/docs/remote-ticket-protocol.md +79 -0
  22. package/docs/setup.md +289 -0
  23. package/docs/spec-standard.md +63 -0
  24. package/docs/ticket-dependencies.md +59 -0
  25. package/docs/vercel-sandbox-rollout-probe.md +7 -0
  26. package/package.json +135 -0
  27. package/packages/core/src/cli/args.ts +74 -0
  28. package/packages/core/src/cli/artifact.ts +182 -0
  29. package/packages/core/src/cli/binary.ts +198 -0
  30. package/packages/core/src/cli/build-control.ts +789 -0
  31. package/packages/core/src/cli/build-progress.ts +55 -0
  32. package/packages/core/src/cli/bulk-control.ts +385 -0
  33. package/packages/core/src/cli/context.ts +470 -0
  34. package/packages/core/src/cli/dashboard/actions.ts +59 -0
  35. package/packages/core/src/cli/dashboard/cells.ts +92 -0
  36. package/packages/core/src/cli/dashboard/composer.ts +230 -0
  37. package/packages/core/src/cli/dashboard/detail.ts +100 -0
  38. package/packages/core/src/cli/dashboard/frame-image.ts +373 -0
  39. package/packages/core/src/cli/dashboard/live.ts +152 -0
  40. package/packages/core/src/cli/dashboard/model.ts +1173 -0
  41. package/packages/core/src/cli/dashboard/poll.ts +196 -0
  42. package/packages/core/src/cli/dashboard/render.ts +1568 -0
  43. package/packages/core/src/cli/dashboard/selection.ts +66 -0
  44. package/packages/core/src/cli/dashboard/session-feed.ts +244 -0
  45. package/packages/core/src/cli/dashboard/session-view.ts +506 -0
  46. package/packages/core/src/cli/dashboard/transcript.ts +108 -0
  47. package/packages/core/src/cli/dev.ts +46 -0
  48. package/packages/core/src/cli/dispatch-frontend.ts +1045 -0
  49. package/packages/core/src/cli/dispatch-process.ts +259 -0
  50. package/packages/core/src/cli/dispatch.ts +3692 -0
  51. package/packages/core/src/cli/dotenv.ts +48 -0
  52. package/packages/core/src/cli/env.ts +206 -0
  53. package/packages/core/src/cli/harvest.ts +698 -0
  54. package/packages/core/src/cli/help.ts +713 -0
  55. package/packages/core/src/cli/init-agent.ts +106 -0
  56. package/packages/core/src/cli/init-validation.ts +583 -0
  57. package/packages/core/src/cli/init.ts +756 -0
  58. package/packages/core/src/cli/installation.ts +333 -0
  59. package/packages/core/src/cli/keyboard.ts +261 -0
  60. package/packages/core/src/cli/main.ts +1399 -0
  61. package/packages/core/src/cli/mcp.ts +279 -0
  62. package/packages/core/src/cli/models.ts +65 -0
  63. package/packages/core/src/cli/observe.ts +55 -0
  64. package/packages/core/src/cli/plugin.ts +227 -0
  65. package/packages/core/src/cli/pr-attachments.ts +138 -0
  66. package/packages/core/src/cli/pr-summary.ts +124 -0
  67. package/packages/core/src/cli/publication-settlement.ts +188 -0
  68. package/packages/core/src/cli/repo-state.ts +213 -0
  69. package/packages/core/src/cli/repository-status.ts +80 -0
  70. package/packages/core/src/cli/self-update.ts +443 -0
  71. package/packages/core/src/cli/status.ts +895 -0
  72. package/packages/core/src/cli/store-opening.contract.ts +289 -0
  73. package/packages/core/src/cli/store-opening.ts +167 -0
  74. package/packages/core/src/cli/store-ref.ts +33 -0
  75. package/packages/core/src/cli/terminal-restore.ts +174 -0
  76. package/packages/core/src/cli/terminal.ts +361 -0
  77. package/packages/core/src/cli/terminals.ts +995 -0
  78. package/packages/core/src/cli/testkit.ts +194 -0
  79. package/packages/core/src/cli/ticket.ts +484 -0
  80. package/packages/core/src/cli/upgrade-agent.ts +180 -0
  81. package/packages/core/src/cli/upgrade-commit.ts +468 -0
  82. package/packages/core/src/cli/upgrade-notice.ts +91 -0
  83. package/packages/core/src/cli/upgrade-progress.ts +110 -0
  84. package/packages/core/src/cli/upgrade.ts +888 -0
  85. package/packages/core/src/cli/wait.ts +587 -0
  86. package/packages/core/src/cli/watch.ts +932 -0
  87. package/packages/core/src/config/live.ts +255 -0
  88. package/packages/core/src/config/load.ts +98 -0
  89. package/packages/core/src/config/roles.ts +438 -0
  90. package/packages/core/src/config/schema.ts +774 -0
  91. package/packages/core/src/distribution.ts +24 -0
  92. package/packages/core/src/events/catalog.ts +201 -0
  93. package/packages/core/src/events/envelope.ts +59 -0
  94. package/packages/core/src/events/payloads.ts +618 -0
  95. package/packages/core/src/events/repository.ts +559 -0
  96. package/packages/core/src/events/sessions.ts +180 -0
  97. package/packages/core/src/harvest/schema.ts +147 -0
  98. package/packages/core/src/ids.ts +45 -0
  99. package/packages/core/src/integration/harness.ts +681 -0
  100. package/packages/core/src/kernel/auto-merge.ts +259 -0
  101. package/packages/core/src/kernel/converge.ts +117 -0
  102. package/packages/core/src/kernel/dispatch-settings.ts +60 -0
  103. package/packages/core/src/kernel/dispatch-status.ts +141 -0
  104. package/packages/core/src/kernel/engine.ts +1047 -0
  105. package/packages/core/src/kernel/harvest.ts +823 -0
  106. package/packages/core/src/kernel/origin.ts +69 -0
  107. package/packages/core/src/kernel/phases.ts +196 -0
  108. package/packages/core/src/kernel/plan-verify-selection.ts +105 -0
  109. package/packages/core/src/kernel/pr-attachments.ts +82 -0
  110. package/packages/core/src/kernel/reducer.ts +752 -0
  111. package/packages/core/src/kernel/stall.ts +155 -0
  112. package/packages/core/src/kernel/verify-applicability.ts +52 -0
  113. package/packages/core/src/markdown.ts +67 -0
  114. package/packages/core/src/ontology.ts +288 -0
  115. package/packages/core/src/open-map.ts +203 -0
  116. package/packages/core/src/operator/annotations.ts +187 -0
  117. package/packages/core/src/operator/client.ts +311 -0
  118. package/packages/core/src/operator/control.ts +117 -0
  119. package/packages/core/src/operator/index.ts +62 -0
  120. package/packages/core/src/operator/presentation.ts +22 -0
  121. package/packages/core/src/operator/protocol.ts +98 -0
  122. package/packages/core/src/operator/query.ts +201 -0
  123. package/packages/core/src/operator/registry.ts +1043 -0
  124. package/packages/core/src/operator/requests.ts +138 -0
  125. package/packages/core/src/operator/sandbox.ts +458 -0
  126. package/packages/core/src/operator/server.ts +616 -0
  127. package/packages/core/src/operator/sessions.ts +223 -0
  128. package/packages/core/src/operator/tickets.ts +177 -0
  129. package/packages/core/src/plugin-sdk/index.ts +174 -0
  130. package/packages/core/src/plugins/contract-entry.ts +74 -0
  131. package/packages/core/src/plugins/contract-env.ts +3 -0
  132. package/packages/core/src/plugins/load.ts +259 -0
  133. package/packages/core/src/plugins/manifest.ts +236 -0
  134. package/packages/core/src/plugins/registry.ts +274 -0
  135. package/packages/core/src/plugins/runtimes.ts +75 -0
  136. package/packages/core/src/ports/forge/contract.ts +258 -0
  137. package/packages/core/src/ports/forge/create.ts +74 -0
  138. package/packages/core/src/ports/forge/fake.ts +488 -0
  139. package/packages/core/src/ports/forge/github-pr-attachments.ts +351 -0
  140. package/packages/core/src/ports/forge/github-transport.ts +352 -0
  141. package/packages/core/src/ports/forge/github.ts +1162 -0
  142. package/packages/core/src/ports/forge/local-git.ts +568 -0
  143. package/packages/core/src/ports/runner/claude.ts +873 -0
  144. package/packages/core/src/ports/runner/cli-stream.ts +90 -0
  145. package/packages/core/src/ports/runner/codex.ts +873 -0
  146. package/packages/core/src/ports/runner/contract.ts +492 -0
  147. package/packages/core/src/ports/runner/fake.ts +213 -0
  148. package/packages/core/src/ports/runner/json-record.ts +2 -0
  149. package/packages/core/src/ports/runner/live-contract-fixture.ts +176 -0
  150. package/packages/core/src/ports/runner/one-shot.ts +29 -0
  151. package/packages/core/src/ports/runner/pi-bridge-core.ts +136 -0
  152. package/packages/core/src/ports/runner/pi-bridge.js +6 -0
  153. package/packages/core/src/ports/runner/pi-rpc.ts +454 -0
  154. package/packages/core/src/ports/runner/pi.ts +617 -0
  155. package/packages/core/src/ports/runner/production.ts +74 -0
  156. package/packages/core/src/ports/runner/provider-error.ts +121 -0
  157. package/packages/core/src/ports/runner/routing.ts +234 -0
  158. package/packages/core/src/ports/runner/runtime.ts +214 -0
  159. package/packages/core/src/ports/runner/session-env.ts +34 -0
  160. package/packages/core/src/ports/runner/stream-parts.ts +103 -0
  161. package/packages/core/src/ports/tickets/contract.ts +570 -0
  162. package/packages/core/src/ports/tickets/create.ts +135 -0
  163. package/packages/core/src/ports/tickets/fake.ts +215 -0
  164. package/packages/core/src/ports/tickets/file.ts +559 -0
  165. package/packages/core/src/ports/tickets/linear.ts +811 -0
  166. package/packages/core/src/ports/tickets/operations.ts +161 -0
  167. package/packages/core/src/ports/tickets/remote-index.ts +13 -0
  168. package/packages/core/src/ports/tickets/remote-protocol.ts +80 -0
  169. package/packages/core/src/ports/tickets/remote-server.ts +185 -0
  170. package/packages/core/src/ports/tickets/remote.ts +130 -0
  171. package/packages/core/src/ports/tickets/update.ts +51 -0
  172. package/packages/core/src/ports/types.ts +528 -0
  173. package/packages/core/src/ports/workspace/build-execution.ts +123 -0
  174. package/packages/core/src/ports/workspace/contract.ts +136 -0
  175. package/packages/core/src/ports/workspace/create.ts +132 -0
  176. package/packages/core/src/ports/workspace/distribution-archive.ts +317 -0
  177. package/packages/core/src/ports/workspace/fake.ts +432 -0
  178. package/packages/core/src/ports/workspace/git-worktree.ts +848 -0
  179. package/packages/core/src/ports/workspace/harvest-execution.ts +111 -0
  180. package/packages/core/src/ports/workspace/in-process-build-execution.ts +30 -0
  181. package/packages/core/src/ports/workspace/local-build-execution.ts +88 -0
  182. package/packages/core/src/ports/workspace/operator-sandbox.ts +142 -0
  183. package/packages/core/src/ports/workspace/phase-scratch.ts +156 -0
  184. package/packages/core/src/ports/workspace/vercel-sandbox.ts +2541 -0
  185. package/packages/core/src/processes/build-child-exit.ts +78 -0
  186. package/packages/core/src/processes/build-child.ts +119 -0
  187. package/packages/core/src/processes/build-execution-state.ts +95 -0
  188. package/packages/core/src/processes/build-parent-watch.ts +35 -0
  189. package/packages/core/src/processes/build-runner.ts +2614 -0
  190. package/packages/core/src/processes/dispatcher.ts +2825 -0
  191. package/packages/core/src/processes/execution-settlement.ts +171 -0
  192. package/packages/core/src/processes/harvest-child.ts +115 -0
  193. package/packages/core/src/processes/harvest-execution-state.ts +121 -0
  194. package/packages/core/src/processes/harvest-runner.ts +1656 -0
  195. package/packages/core/src/processes/harvest.ts +595 -0
  196. package/packages/core/src/processes/infrastructure-failure-budget.ts +146 -0
  197. package/packages/core/src/processes/phase-failure-budget.ts +52 -0
  198. package/packages/core/src/processes/process-group-reaper.ts +35 -0
  199. package/packages/core/src/processes/process-group.ts +65 -0
  200. package/packages/core/src/processes/publication-loss.ts +62 -0
  201. package/packages/core/src/processes/publication-state.ts +93 -0
  202. package/packages/core/src/processes/sandbox-state.ts +101 -0
  203. package/packages/core/src/registry.ts +24 -0
  204. package/packages/core/src/sessions/reducer.ts +185 -0
  205. package/packages/core/src/skills.ts +7 -0
  206. package/packages/core/src/spec-standard.ts +47 -0
  207. package/packages/core/src/store/adapter.ts +80 -0
  208. package/packages/core/src/store/build-scope.ts +272 -0
  209. package/packages/core/src/store/contract.ts +2581 -0
  210. package/packages/core/src/store/local/blobs.ts +38 -0
  211. package/packages/core/src/store/local/schema.ts +180 -0
  212. package/packages/core/src/store/local/store.ts +1713 -0
  213. package/packages/core/src/store/memory.ts +1138 -0
  214. package/packages/core/src/store/phase-session.ts +425 -0
  215. package/packages/core/src/store/remote/client.ts +780 -0
  216. package/packages/core/src/store/remote/index.ts +12 -0
  217. package/packages/core/src/store/remote/protocol.ts +348 -0
  218. package/packages/core/src/store/remote/server.ts +923 -0
  219. package/packages/core/src/store/remote/token.ts +137 -0
  220. package/packages/core/src/store/remote/version.ts +10 -0
  221. package/packages/core/src/store/retention.ts +91 -0
  222. package/packages/core/src/store/session-handle.ts +265 -0
  223. package/packages/core/src/store/streams/assemble.ts +79 -0
  224. package/packages/core/src/store/streams/lock.ts +42 -0
  225. package/packages/core/src/store/streams/session-writer.ts +306 -0
  226. package/packages/core/src/store/streams/types.ts +171 -0
  227. package/packages/core/src/store/streams/wait.ts +127 -0
  228. package/packages/core/src/store/subscribe.ts +105 -0
  229. package/packages/core/src/store/types.ts +466 -0
  230. package/packages/core/src/testing/fixed.ts +44 -0
  231. package/packages/core/src/testing/store-failures.ts +32 -0
  232. package/packages/core/src/zod-issues.ts +91 -0
  233. package/skills/code-review/SKILL.md +100 -0
  234. package/skills/finalize/SKILL.md +44 -0
  235. package/skills/guide/SKILL.md +1781 -0
  236. package/skills/guide/references/operator-api.md +164 -0
  237. package/skills/guide/references/plugin-authoring.md +354 -0
  238. package/skills/guide/references/remote-store-protocol.md +972 -0
  239. package/skills/guide/references/remote-ticket-protocol.md +79 -0
  240. package/skills/guide/references/setup.md +289 -0
  241. package/skills/guide/references/spec-standard.md +63 -0
  242. package/skills/guide/references/ticket-dependencies.md +59 -0
  243. package/skills/harvest/SKILL.md +66 -0
  244. package/skills/harvest-review/SKILL.md +63 -0
  245. package/skills/implement/SKILL.md +82 -0
  246. package/skills/plan/SKILL.md +123 -0
  247. package/skills/plan-review/SKILL.md +121 -0
  248. package/skills/reconcile/SKILL.md +65 -0
  249. package/skills/spec/SKILL.md +132 -0
  250. package/skills/tickets/SKILL.md +139 -0
  251. package/templates/autobuild.toml +36 -0
@@ -0,0 +1,1623 @@
1
+ # Configuration reference
2
+
3
+ Autobuild reads one declarative `autobuild.toml` from the repository root.
4
+ Commands in the file are shell strings; the file itself is never evaluated as
5
+ code. A running dispatcher owns an accepted snapshot from the main checkout,
6
+ while scoped phase commands read the build worktree's branch-owned file.
7
+ Configuration changes therefore move through review like any other change and,
8
+ once merged, can be adopted by the running dispatcher.
9
+
10
+ This document covers the complete accepted TOML surface. Follow the
11
+ [installation and repository setup procedure](setup.md) for the supported local
12
+ versus Vercel decision flow, credential checklist, reproducible provisioning,
13
+ and sessionless `ab init --validate` readiness check. Examples labelled as
14
+ fragments are intended to be added to an existing file; the [complete
15
+ example](#complete-example) is valid on its own.
16
+
17
+ ## Hosted service and PostgreSQL BuildStore environment
18
+
19
+ The optional `@defrex/autobuild-hosted-store-service` and its
20
+ `@defrex/autobuild-postgres-store` adapter are configured entirely through the
21
+ environment. They are published to npm as separate packages alongside the
22
+ `@defrex/autobuild` CLI, so a CLI install never acquires the service,
23
+ PostgreSQL, or blob-provider dependencies; a project that embeds the adapter
24
+ or the service adds the package it needs (`bun add
25
+ @defrex/autobuild-postgres-store` or `bun add
26
+ @defrex/autobuild-hosted-store-service`). The deployable web application still
27
+ runs from a release checkout: choose the compatible tag shown in
28
+ [GitHub Releases](https://github.com/defrex/autobuild/releases), clone that exact
29
+ revision into a dedicated checkout, and run the idempotent migration there:
30
+
31
+ ```sh
32
+ git clone --depth 1 --branch v0.6.0 --single-branch https://github.com/defrex/autobuild.git autobuild-v0.6.0
33
+ cd autobuild-v0.6.0
34
+ bun install --frozen-lockfile
35
+ DATABASE_URL=postgres://… bun run postgres:migrate
36
+ ```
37
+
38
+ Replace `v0.6.0` with the selected release tag. Schema diagnostics' root
39
+ `postgres:migrate` script is relative to that pinned checkout. A hosted build
40
+ can run the same step itself: `bun run deploy:build` migrates against the
41
+ deployment's database URL, logs the host it prepared, and then builds the web
42
+ application, failing the build when no database URL is configured or the
43
+ schema is incompatible. The database
44
+ identity needs permission to create tables during migration and to select,
45
+ insert, and update those tables at runtime. The complete run, deployment, and
46
+ token procedure is in the [hosted service
47
+ guide](../packages/hosted-store-service/README.md); operator clients use the
48
+ separate signed-user token described by the [operator API](operator-api.md).
49
+
50
+ | Variable | Required when | Values / purpose |
51
+ |---|---|---|
52
+ | `AB_STORE_SECRET` | Hosted service | Nonblank HMAC signing secret. Service-side only; never give it to clients. |
53
+ | `AB_HOST` | Optional, ordinary Bun host | Bind hostname; defaults to `0.0.0.0`. Vercel supplies its own listener. |
54
+ | `PORT` | Optional, ordinary Bun host | Integer 1–65535; defaults to `3000`. Vercel supplies its own listener. |
55
+ | `AB_STORE` | Remote client | Hosted service base URL used by the kernel, phase commands, and dashboard. |
56
+ | `AB_TOKEN` | Authenticated remote client | Offline-minted bearer token; this is not the signing secret. |
57
+ | `BETTER_AUTH_SECRET` | Web dashboard | Independent server-only session secret with at least 32 high-entropy characters. |
58
+ | `BETTER_AUTH_URL` | Web dashboard | Exact HTTP(S) origin, without credentials or a path; HTTPS is mandatory in production. |
59
+ | `GITHUB_CLIENT_ID` | GitHub sign-in | OAuth application client id. |
60
+ | `GITHUB_CLIENT_SECRET` | GitHub sign-in | Server-only OAuth application secret. |
61
+ | `AB_WEB_AUTH_PROVIDERS` | Web dashboard | Comma-separated provider set; currently exactly `github`. |
62
+ | `AB_WEB_ALLOWED_EMAILS` | Web dashboard | Nonempty comma-separated operator email allowlist; matching is case-insensitive. |
63
+ | `AB_WEB_REPOSITORIES` | Web dashboard | Nonempty comma-separated repository allowlist exposed to signed-in operators. Entries are repository identities — normalized `https://` origins (ssh-like spellings such as `git@github.com:owner/repo.git` are accepted and normalized); they must match the Store's identity for the repository. |
64
+
65
+ GitHub's callback is `{BETTER_AUTH_URL}/api/auth/callback/github`; the app needs
66
+ the `user:email` scope (or GitHub App read-only email permission). Sessions are
67
+ database-backed and represented in the browser only by an HTTP-only cookie.
68
+ Allowlist removal takes effect on the next web API request. Never expose these
69
+ secrets, `AB_STORE_SECRET`, database/blob credentials, machine tokens, or OAuth
70
+ account tokens through a `NEXT_PUBLIC_` variable.
71
+
72
+ The following variables configure the service-side durable adapter (or a direct
73
+ use of `@defrex/autobuild-postgres-store`).
74
+
75
+ | Variable | Required when | Values / purpose |
76
+ |---|---|---|
77
+ | `DATABASE_URL` | Always, unless `AB_POSTGRES_URL` is set | Nonblank PostgreSQL connection URL under its conventional name, as Vercel's Neon and Postgres storage integrations inject it. |
78
+ | `AB_POSTGRES_URL` | Optional override | Explicit Autobuild-specific connection URL; a nonblank value wins over `DATABASE_URL`. No other ambient variable is consulted. |
79
+ | `AB_BLOB_BACKEND` | Always | `s3` or `vercel`. |
80
+ | `AB_BLOB_PREFIX` | Optional | Object pathname prefix; redundant `/` characters are normalized. |
81
+ | `AB_S3_BUCKET` | S3 | Bucket name. |
82
+ | `AB_S3_REGION` | S3 | S3 signing region. |
83
+ | `AB_S3_ENDPOINT` | Optional, S3 | Endpoint URL for an S3-compatible service. |
84
+ | `AB_S3_ACCESS_KEY_ID` | S3 | Explicit access key; ambient AWS credential lookup is not used. |
85
+ | `AB_S3_SECRET_ACCESS_KEY` | S3 | Explicit secret key. |
86
+ | `AB_S3_SESSION_TOKEN` | Optional, S3 | Explicit temporary-credential session token. |
87
+ | `AB_S3_FORCE_PATH_STYLE` | Optional, S3 | Exact boolean `true` or `false`. |
88
+ | `AB_VERCEL_BLOB_ACCESS` | Vercel | `public` or `private`. |
89
+ | `BLOB_READ_WRITE_TOKEN` | Vercel auth option | Blob read-write token. |
90
+ | `VERCEL_OIDC_TOKEN` | Vercel OIDC option | Must be paired with `BLOB_STORE_ID` when no read-write token is supplied. |
91
+ | `BLOB_STORE_ID` | Vercel OIDC option | Must be paired with `VERCEL_OIDC_TOKEN`. |
92
+
93
+ S3 identities need `GetObject` and `PutObject` on the bucket/prefix. The adapter
94
+ maps only a provider 404 to absence and propagates authorization/service errors.
95
+ Vercel object names are deterministic and overwritten idempotently.
96
+
97
+ ## Strict parsing and validation
98
+
99
+ Parsing is strict. Unknown top-level keys or tables, unknown fields in a known
100
+ table, fields from the wrong step variant, malformed values, and dangling
101
+ command references are errors. The open maps are `[commands]`, `[roles]`,
102
+ `[workspace.config]`, `[workspace.config.runtimeProvisioning]`,
103
+ `[verify.<step>]`, and `[finalize.<step>]`. Autobuild
104
+ validates repository-defined command, role, and step entries.
105
+ `[workspace.config]` is plugin-owned and passed through unchanged for plugin
106
+ providers; the builtin `git-worktree` provider requires it to be empty, while
107
+ `vercel-sandbox` interprets it as a closed, typed table.
108
+ Every other known table is closed to unknown keys.
109
+
110
+ There are three validation layers:
111
+
112
+ 1. TOML syntax and schema/cross-field validation happen while
113
+ `autobuild.toml` is loaded. Errors start with the file path and either
114
+ `TOML syntax error` or `invalid config`; schema errors then identify paths
115
+ such as `verify.e2e.skill` or `tickets.teamKey`.
116
+ 2. Configured plugin modules are resolved from the repository, evaluated,
117
+ manifest-validated, checked for plugin-API compatibility, and registered
118
+ before production adapters, stores, claims, or runners are started.
119
+ 3. Runtime/model compatibility is checked eagerly after the configured
120
+ adapters are wired. Its separate `invalid runtime/model configuration`
121
+ error reports every incompatible declared role before a build launches.
122
+
123
+ Run `ab dispatch --once` from the repository to exercise production loading
124
+ and adapter validation. It is a real dispatcher pass, not a read-only linter,
125
+ so run it only when one intake/janitor tick is acceptable.
126
+
127
+ TOML does not return to the root after entering a table. Put root scalars before
128
+ the first table header; otherwise a value such as `capacity` becomes an unknown
129
+ field in whichever table precedes it.
130
+
131
+ ## Reloading a running dispatcher
132
+
133
+ A long-running `ab dispatch` watches the main checkout's `autobuild.toml` and
134
+ checks it before every dispatch tick. A valid save is adopted within that tick.
135
+ In origin mode (`ab dispatch --repository <origin>`, no checkout) the same
136
+ cadence reads `autobuild.toml` from the forge at the current `baseBranch`
137
+ instead: a push to the base branch is honored by a later tick without
138
+ restarting anything. Hot fields apply immediately; restart-classified fields
139
+ keep the startup-built adapters and file the existing restart-required notice
140
+ (a long-lived origin-mode watch picks them up on its next process restart;
141
+ the hosted cron entry point re-reads them on every invocation).
142
+ Each dispatch, build, check, or agent action captures the accepted configuration
143
+ snapshot at its start boundary: work already running is not interrupted, while
144
+ the next setup or pipeline step of the same in-flight build uses the new
145
+ snapshot. Raising capacity can fill the new slots immediately; lowering it
146
+ prevents new claims without terminating builds already above the limit. `ab
147
+ dispatch --once` performs one pass and does not watch or reload.
148
+
149
+ Every accepted revision is deposited verbatim as a `dispatcher-config`
150
+ repository artifact and referenced by a `dispatcher.config-reloaded` repository
151
+ event. The dashboard reports the reload and reprojects values such as capacity;
152
+ plain watch mode writes the reload notice to stdout. A missing or unreadable
153
+ file, invalid syntax or schema, or invalid runtime routing is a rejected
154
+ candidate: it cannot replace or partially modify the last valid snapshot. The
155
+ dispatcher keeps that snapshot active, reports an actionable notice in the
156
+ dashboard notice row or on plain-mode stderr, and suppresses repeats while the
157
+ same failure remains. Restoring `autobuild.toml` with valid configuration clears
158
+ the rejection and ordinary live reload resumes without a dispatcher restart.
159
+
160
+ These fields hot-reload:
161
+
162
+ | Field or table | Boundary where a new value applies |
163
+ |---|---|
164
+ | `baseBranch` | Next dispatch or base fallback decision; existing `build.created` facts remain immutable. |
165
+ | `capacity` | Next dispatcher tick. |
166
+ | `[pr]` | Next build creation. |
167
+ | `[commands]` | Next setup, verify check, or finalize check selected. |
168
+ | `[verify]` and `[finalize]` | Next engine step selection. An approved plan's stored verify selection remains authoritative. |
169
+ | `[roles]` | Next agent invocation for that role. Runtime, model, args, alternates, and the session budget are captured together. |
170
+ | `[policy]` | Next policy, convergence, or agent-session boundary. A running session keeps its captured budget. |
171
+ | `tickets.readyLabels`, `tickets.readyState` | Next ready-ticket scan. |
172
+ | `tickets.triageState`, `tickets.proposalState` | Next dispatcher handback or harvest filing boundary. |
173
+
174
+ Adapter selectors and factory inputs are constructed once and require a
175
+ restart. A valid save may still apply its hot fields, but these changed paths
176
+ remain pinned to their startup values and are reported explicitly:
177
+
178
+ | Restart-required field | Reason |
179
+ |---|---|
180
+ | `plugins` | Plugin modules and registration catalogs load before wiring. |
181
+ | `forge` | Selects the constructed Forge adapter. |
182
+ | `workspace.provider`, `workspace.config` | Select and configure the constructed workspace provider. |
183
+ | `tickets.source`, `tickets.teamKey`, `tickets.claimedState`, `tickets.createState`, `tickets.dir` | Select or configure the constructed TicketSource. |
184
+ | `orchestrator` | Tool availability and the dispatcher's sandbox idle settlement are startup-constructed. |
185
+
186
+ The dispatch process reads the main checkout because it owns repository intake.
187
+ Scoped phase commands still read the build worktree's `autobuild.toml`; this is
188
+ the existing build-context boundary and is not unified by hot reload.
189
+
190
+ ## Root scalars
191
+
192
+ All root scalars are optional and receive defaults. They must appear before
193
+ any table header.
194
+
195
+ | Field | Default | Constraints | Purpose |
196
+ |---|---:|---|---|
197
+ | `baseBranch` | `"main"` | nonempty string | Branch used to cut builds, target PRs, and merge during reconciliation. |
198
+ | `capacity` | `1` | positive integer | Maximum concurrent nonterminal builds for this repository. Paused and blocked builds still occupy capacity. |
199
+ | `forge` | `"github"` | nonblank string | Forge adapter name. Shipped values are `github` and `local-git`; configured plugins may register other names. |
200
+ | `plugins` | `[]` | array of unique nonblank module specifiers | Trusted Bun plugin modules loaded by dispatch, `ab ticket`, and scoped phase CLI processes. |
201
+
202
+ ### Plugin modules
203
+
204
+ <!-- config-fragment:plugins -->
205
+ ```toml
206
+ plugins = ["./plugins/company.ts", "@acme/autobuild-plugin"]
207
+ ```
208
+
209
+ Repository-path specifiers (relative, absolute, and `file:`) resolve from the
210
+ root whose config is being read. In a scoped phase process that root is the
211
+ immutable build worktree. Bare npm package specifiers resolve first from the
212
+ consuming repository's main checkout and therefore use its installed
213
+ dependencies; a package the repository lacks then resolves from Autobuild's own
214
+ installation, so an extension installed next to the CLI (`bun add -g
215
+ @defrex/autobuild-<extension>`) loads without being added to the repository. A
216
+ repository copy always wins over an installed one. This package lookup remains
217
+ stable when a relocated local store places a linked worktree outside the
218
+ checkout. Dispatch and sessionless commands use the main checkout for both
219
+ repository roots. `ab plugin list` reports which root satisfied each package
220
+ (`from=repository` or `from=installation`). Missing packages fail loading;
221
+ Autobuild does not install them.
222
+
223
+ Every configured specifier string must be unique. An exact repeat fails schema
224
+ validation before any plugin resolves or evaluates; the diagnostic identifies
225
+ the repeated value and both list positions and tells the operator to remove or
226
+ deduplicate the entry. Distinct specifier strings remain separate declarations.
227
+ Each module must default-export a strict manifest with a plugin name, a semver
228
+ range in `apiVersion`, and optional `ticketSources`, `agentRuntimes`,
229
+ `workspaceProviders`, and `forges` factory maps. One manifest may contribute to
230
+ several ports.
231
+
232
+ Plugin modules execute in-process during `ab dispatch`, sessionless `ab ticket`
233
+ commands, and configured scoped phase CLI composition. They have the same trust
234
+ as repository-supplied commands;
235
+ there is no sandbox. A missing module, module
236
+ that throws, malformed manifest, incompatible API range, or adapter-name
237
+ collision fails startup before a ticket claim. Builtin names and names from
238
+ earlier configured plugins are reserved, and declaration order never permits
239
+ shadowing. A collision between distinct plugin declarations continues to name
240
+ the conflicting adapter and both owners.
241
+
242
+ Plugin authors import the stable surface from `@defrex/autobuild/plugin-sdk`, normally
243
+ with `import type`, and can develop against Autobuild as a dev/peer dependency
244
+ without adding a runtime Autobuild dependency to the plugin. That entry point
245
+ exports the manifest/factory types, port types, fake adapters, and reusable
246
+ TicketSource, AgentRunner, WorkspaceProvider, Forge, BuildStore, and BlobStore
247
+ contract suites. Adapter values may use the backward-compatible bare factory or
248
+ carry a contract fixture descriptor. Ticket sources may also declare
249
+ `requiredEnv`; the host checks every declared variable for a nonempty value
250
+ before invoking the adapter factory. Plugins using Forge abort-cleanup and
251
+ AgentRunner turn-cancellation capabilities introduced in API 1.2 should require
252
+ `^1.2.0`. The structured failure `cause` and provider-exhaustion contract
253
+ scenario were added in API 1.3; a runtime plugin that relies on them should
254
+ require `^1.3.0`. API 1.4 adds the optional `Ticket.creationKey` projection: a
255
+ ticket-source plugin should return its stable external create/adoption key from
256
+ create, get, and ready listings so dispatch can correlate Autobuild's durable
257
+ in-flight creations. Legacy tickets may omit it and remain dispatchable.
258
+
259
+ ```ts
260
+ import type { AutobuildPluginManifest } from '@defrex/autobuild/plugin-sdk'
261
+
262
+ export default {
263
+ name: 'acme-integrations',
264
+ apiVersion: '^1.2.0',
265
+ ticketSources: {
266
+ jira: {
267
+ requiredEnv: ['JIRA_TOKEN'],
268
+ factory: ({ config, env, repoRoot }) => createJira(config, env, repoRoot),
269
+ contract: {
270
+ // Return the TicketSourceContractFactory consumed by the shared suite.
271
+ factory: ({ env }) => makeJiraContractFactory(env),
272
+ live: true,
273
+ },
274
+ },
275
+ },
276
+ } satisfies AutobuildPluginManifest
277
+ ```
278
+
279
+ Use the sessionless author/operator loop from the repository checkout:
280
+
281
+ ```sh
282
+ ab plugin list
283
+ ab plugin doctor
284
+ ab plugin test ticket-source jira
285
+ AB_RUN_LIVE_PORT_CONTRACTS=1 ab plugin test ticket-source jira
286
+ ```
287
+
288
+ The four test port tokens are `ticket-source`, `agent-runtime`,
289
+ `workspace-provider`, and `forge`. `list` includes builtins and successful
290
+ plugin registrations with module, resolved path/kind, owner, API status, and
291
+ contract availability. `doctor` attempts every configured module in declaration
292
+ order, reports each resolution/evaluation/manifest/registration result, and
293
+ exits nonzero if any fail. Dispatch intentionally differs: it stops on the
294
+ first plugin failure before opening stores or claiming work. `test` invokes one
295
+ unchanged shared suite under `bun test`; Bun's per-test output and exit status
296
+ are authoritative. A missing `contract.factory` is an actionable error. A
297
+ `live = true` descriptor cannot launch or create its harness unless
298
+ `AB_RUN_LIVE_PORT_CONTRACTS=1` is explicitly set. Plugin authors should run the
299
+ corresponding contract suite against every adapter, including
300
+ `describeAgentRunnerContract` for each runtime.
301
+
302
+ Ticket-source, forge, workspace, and agent-runtime selection are open. Set
303
+ `[tickets].source`, the root `forge` scalar, `[workspace].provider`, or any
304
+ `[roles.*].runtime` to a registered name; omission selects the builtin defaults
305
+ where applicable. A selected plugin forge factory receives an empty
306
+ adapter-specific `config` object, while a workspace factory receives
307
+ `[workspace.config]`; both receive the process environment and absolute
308
+ repository root and are invoked lazily after the complete plugin catalog loads.
309
+ Unknown names fail with the available names for that port, and factory failures
310
+ are contextualized with the adapter and plugin names. Scoped build-session CLI
311
+ processes repeat forge config/plugin loading from the build worktree, so phase
312
+ terminal plumbing uses the same configured forge. The returned forge is not
313
+ wrapped: an absent `prAttachments` capability intentionally selects text-only
314
+ attachment summaries, while a present capability serves upload and terminal
315
+ reclamation.
316
+
317
+ Plugin runtimes receive empty adapter config, the process environment, and the
318
+ absolute repository root. They participate in the same eager exact-name,
319
+ model-family, and default-model role validation as builtins, and their
320
+ registration key is used for session event and transcript attribution. A
321
+ runtime's optional one-shot capability serves slug naming and lazy upgrade
322
+ conflict resolution; when absent, each caller retains its existing safe
323
+ fallback.
324
+
325
+ ## `[pr]`
326
+
327
+ Optional. Omitting the table leaves attachment hosting off; attached artifacts
328
+ still receive exact BuildStore download commands in the PR summary.
329
+
330
+ | Field | Default | Constraints | Purpose |
331
+ |---|---:|---|---|
332
+ | `imageHost` | omitted (off) | optional `[pr.imageHost]` table | Copy attached images to a temporary public review location so they can render inline. |
333
+
334
+ ### `[pr.imageHost]`
335
+
336
+ This optional nested table selects the one shipped image-host adapter. If the
337
+ table exists, all three fields are required.
338
+
339
+ | Field | Default | Constraints | Purpose |
340
+ |---|---:|---|---|
341
+ | `provider` | — | required literal `"github-release"` | Select the GitHub release adapter. |
342
+ | `repository` | — | required exact nonblank `owner/repo` pair | Public repository containing the existing review release. |
343
+ | `releaseId` | — | required positive integer | Numeric id of an existing published, mutable release. |
344
+
345
+ <!-- config-fragment:image-host -->
346
+ ```toml
347
+ [pr.imageHost]
348
+ provider = "github-release"
349
+ repository = "acme/public-review-assets"
350
+ releaseId = 123456
351
+ ```
352
+
353
+ Agents opt in one artifact at a time with
354
+ `ab artifact put <kind> <file> --attach`. Every designation remains available
355
+ through a pinned `ab artifact download <build> <kind>@<rev> --output <file>`
356
+ command. With an image host configured, only normalized `image/*` attachments
357
+ are copied and rendered inline; non-images remain text-download-only.
358
+
359
+ The repository must be public because GitHub renders release assets without
360
+ authentication. A private source repository can name a separate public asset
361
+ repository. Autobuild creates no repository, release, or tag. The release must
362
+ already be published and mutable, and the `gh` identity running Autobuild needs
363
+ Contents write permission there. Obtain a release's numeric id with, for
364
+ example:
365
+
366
+ ```sh
367
+ gh api repos/acme/public-review-assets/releases/tags/review-window --jq .id
368
+ ```
369
+
370
+ Configuring this table is an explicit temporary-public-disclosure opt-in.
371
+ Hosted copies are deleted after `build.completed`; failed deletions remain
372
+ durable and retry on later dispatcher ticks. Upload, target-validation, and
373
+ timeout failures create follow-up observations but preserve every text download
374
+ command and do not fail verification or finalize. BuildStore artifacts remain
375
+ the authoritative copies under the store's own retention policy — dispatcher
376
+ run/config artifact kinds keep their latest 200 revisions
377
+ (`AB_ARTIFACT_RETENTION_MAX_REVISIONS` overrides the bound); other kinds are
378
+ never pruned.
379
+
380
+ ## `[workspace]`
381
+
382
+ Optional. Omission preserves the shipped git-worktree behavior. The surrounding
383
+ table is strict; only the explicit nested `config` table is plugin-owned.
384
+
385
+ | Field | Default | Constraints | Purpose |
386
+ |---|---:|---|---|
387
+ | `provider` | `"git-worktree"` | nonblank builtin or plugin-registered name | Select the provider used for provisioning, recovery, PR epilogue working-directory calls, and release. |
388
+ | `config` | `{}` | nested open table; must be empty for `git-worktree` | Adapter-specific declarative configuration passed unchanged to the selected plugin factory. |
389
+
390
+ <!-- config-fragment:workspace -->
391
+ ```toml
392
+ [workspace]
393
+ provider = "company-container"
394
+
395
+ [workspace.config]
396
+ image = "ghcr.io/acme/build:bun"
397
+ writableCache = true
398
+ ```
399
+
400
+ The builtin `git-worktree` provider needs no table and accepts no adapter
401
+ configuration. A plugin factory receives exactly `[workspace.config]`, the
402
+ process environment, and the absolute repository root. Every declared key
403
+ reaches the factory verbatim. Factory invocation is lazy; unknown names fail
404
+ before claims and list every available builtin and plugin provider.
405
+
406
+ ### Vercel Sandbox
407
+
408
+ Remote execution is explicit and requires the hosted HTTPS BuildStore plus its
409
+ scoped token, an HTTPS `github.com/owner/repository` origin, `forge = "github"`,
410
+ and Vercel authentication. Use either `VERCEL_OIDC_TOKEN`, or all of
411
+ `VERCEL_TOKEN`, `VERCEL_TEAM_ID`, and `VERCEL_PROJECT_ID`. The dispatcher also
412
+ requires a push-capable `GITHUB_TOKEN` or `GH_TOKEN`; this is validated before
413
+ ready tickets are listed or claimed. Keyring-only `gh auth login` is not enough
414
+ because publication injects the credential through Vercel's network transform.
415
+
416
+ <!-- config-fragment:workspace-vercel -->
417
+ ```toml
418
+ [workspace]
419
+ provider = "vercel-sandbox"
420
+
421
+ [workspace.config]
422
+ image = "vercel/sandbox/universal:latest"
423
+ vcpus = 4
424
+ timeoutSeconds = 2700
425
+ operationTimeoutMs = 30000
426
+ snapshotExpirationSeconds = 86400
427
+ region = "iad1"
428
+ failoverRegions = ["sfo1"]
429
+ environmentVariables = ["AI_GATEWAY_API_KEY"]
430
+ provisioning = [
431
+ { name = "system-install", command = """apt-get update
432
+ DEBIAN_FRONTEND=noninteractive apt-get install -y chromium""" },
433
+ { name = "browser-smoke", command = "CHROMIUM_BIN=/usr/bin/chromium ./scripts/browser-smoke.sh" },
434
+ ]
435
+ # Private repositories only:
436
+ gitUsernameEnv = "AB_GIT_READ_USER"
437
+ gitPasswordEnv = "AB_GIT_READ_TOKEN"
438
+
439
+ [workspace.config.runtimeProvisioning.pi]
440
+ install = "npm install --global --ignore-scripts @earendil-works/pi-coding-agent@0.84.4"
441
+ preflight = "test \"$(pi --version)\" = \"0.84.4\""
442
+ ```
443
+
444
+ | Vercel field | Default | Constraints |
445
+ |---|---:|---|
446
+ | `image` | `vercel/sandbox/universal:latest` | `vercel/sandbox/universal`, a tag such as `:latest`, or a `@sha256:<64 hex digits>` digest |
447
+ | `vcpus` | `4` | integer 1–32 (account limits may be lower) |
448
+ | `timeoutSeconds` | — | required, integer 60–86400; VM/session lifetime and abrupt-orphan bound; Hobby currently permits at most 2700 |
449
+ | `operationTimeoutMs` | `30000` | integer 1000–300000; deadline for each provider acknowledgement, not session lifetime |
450
+ | `snapshotExpirationSeconds` | — | optional integer 300–2592000; snapshots created for this environment expire after this duration instead of the provider default (30 days); recommended 86400 (24h) |
451
+ | `region` | Vercel default | nonempty region |
452
+ | `failoverRegions` | `[]` | unique and different from `region` |
453
+ | `environmentVariables` | `[]` | unique API-credential variable names copied into runtime and agent/check commands |
454
+ | `provisioning` | `[]` | ordered strict inline tables `{ name, command }`; names and commands are nonblank and names are unique |
455
+ | `runtimeProvisioning` | `{}` | open map keyed by every effective role/alternate runtime; each entry requires `install` and `preflight` |
456
+ | `gitUsernameEnv` | — | optional variable name for the dedicated read-only clone username; requires `gitPasswordEnv` |
457
+ | `gitPasswordEnv` | — | optional variable name for the dedicated read-only clone password; requires `gitUsernameEnv` |
458
+
459
+ `snapshotExpirationSeconds` is a billing safety net: snapshot cleanup normally
460
+ purges on release, but a crash that loses the workspace before cleanup can
461
+ strand snapshots that hold billed storage until Vercel's 30-day default expiry.
462
+ A configured bound ages those stranded snapshots out instead. Omitting the key
463
+ keeps today's behavior (the provider default), and `0` ("no expiration") is
464
+ rejected because never-expiring snapshots would contradict the setting's
465
+ purpose.
466
+
467
+ Each `[workspace.config.runtimeProvisioning.<runtime>]` entry is strict:
468
+
469
+ | Runtime provisioning field | Constraints |
470
+ |---|---|
471
+ | `install` | required nonblank shell command; use an immutable package version, digest, or commit |
472
+ | `preflight` | required nonblank shell command; verify the expected executable and exact version |
473
+
474
+ Runtime names are not a built-in enum. Plugin registrations work with the same
475
+ map, for example `[workspace.config.runtimeProvisioning.opencode]` with a pinned
476
+ plugin-owned install command and its executable/version preflight.
477
+
478
+ The built-in adapter supports only Vercel's
479
+ `vercel/sandbox/universal` managed image, selected by its bare/default name, a
480
+ tag, or a digest. Other managed images and arbitrary VCR images are rejected at
481
+ configuration load because their package tools, libc/CPU, and filesystem
482
+ contract have not been validated. The universal image must retain working
483
+ Node/npm, `sh`, and the ordinary writable filesystem layout. Autobuild does not
484
+ rely on image-preinstalled Bun: on each fresh sandbox it uses npm to install the
485
+ pinned `bun@1.4.0` package under `/opt/autobuild-runtime`, verifies that exact
486
+ executable with `--version`, and uses it for Autobuild and Bun-lockfile
487
+ dependency installation. Package-registry network access is therefore required
488
+ during provisioning. The child launch prepends the adapter-owned Bun directory
489
+ to `PATH`, so setup, checks, agents, and their descendants resolve the same Bun.
490
+
491
+ `provisioning` is repository-owned declarative data; Autobuild never evaluates
492
+ configuration as code. After pinned Bun is verified, each command runs serially
493
+ as `sh -c <command>` with provider `sudo: true` and
494
+ `/vercel/sandbox/workspace` as its working directory. Distribution installation,
495
+ lockfile dependency bootstrap, `[commands].setup`, and every agent session occur
496
+ after these system steps. Put system packages here, not in repeated setup.
497
+ Commands that need a browser should set the repository-controlled location,
498
+ for example `CHROMIUM_BIN=/usr/bin/chromium bun test`; the example's checked-in
499
+ `scripts/browser-smoke.sh` must start a local dev server, launch
500
+ `"$CHROMIUM_BIN" --headless` against it in the same guest, verify the rendered
501
+ page, and exit nonzero on failure.
502
+
503
+ The private-repository token must grant repository contents read and no
504
+ contents write. It must be distinct from Forge and Vercel credentials and may
505
+ not also appear in `environmentVariables`. Autobuild gives its firewall broker
506
+ only exact upload-pack GET/POST matchers, scrubs origin credentials, credential
507
+ helpers, and extra headers after clone, and gives normal sessions no Forge
508
+ credential. After Bun verification, every declared system step runs before the
509
+ Autobuild distribution and dependency bootstrap. Then runtime provisioning runs
510
+ each referenced runtime's `install` and `preflight` commands in sorted
511
+ runtime-name order from the checkout. Only `environmentVariables` values enter
512
+ runtime commands. The same sequence runs in fresh readiness sandboxes and every
513
+ replacement sandbox. Provisioning writes its readiness marker only after all
514
+ scrubbing, Bun verification, system steps, distribution installation,
515
+ dependency bootstrap, and runtime preflights complete; distribution
516
+ packing/install disables package lifecycle scripts so
517
+ checkout-only hooks such as Husky are not provisioning dependencies. A Bun
518
+ install or verification failure identifies the configured image and expected
519
+ universal-image capabilities, deletes the partial environment, and leaves no
520
+ readiness marker. A retry also deletes any named sandbox without that marker
521
+ instead of adopting partial setup. The installed distribution's version is
522
+ recorded in the guest's `/opt/autobuild/.distribution-version` marker; a
523
+ reused sandbox whose marker no longer matches the version the current system
524
+ would deliver is refreshed by reinstalling the archive before reuse, so an
525
+ upgraded dispatcher retrofits its persistent guests (see "Guest distribution
526
+ version selection and delivery" in docs/setup.md for the release-and-upgrade
527
+ delivery path and the hosted store's version-lockstep gate). Immediately
528
+ before every build-child launch, the adapter verifies the
529
+ provisioned absolute Bun executable and every referenced runtime again. A
530
+ failure names the runtime, selecting role/alternate, stage, and provisioning
531
+ field, launches no child, and instructs the operator to fix/reprovision. This
532
+ fails closed for stale marked snapshots. Runtime installation is deliberately
533
+ separate from `[commands].setup`, which starts inside the runner and cannot make
534
+ the runtime available before the first agent session. Branch publication is
535
+ requested durably, then performed by the
536
+ local supervisor only after the remote command exits, the VM is stopped, and
537
+ the execution lease is released. The supervisor uses a narrow credential
538
+ transform for a fixed non-force push and verifies the remote head before
539
+ recording completion; PR API work stays local. If that acknowledgement is lost
540
+ after the push lands, the next launch observes the exact remote branch head and
541
+ records the missing completion before allowing a replacement phase to run. If
542
+ the commit is absent, release of the old workspace abandons its request and the
543
+ replacement reruns the phase. Before every guest runner launch, the provider
544
+ reasserts the normal receive-pack-free policy, so a failed policy
545
+ restore cannot expose publication authority to later setup or plugin code.
546
+ Ordinary completion deletes the sandbox.
547
+
548
+ A Vercel VM is disposable, not durable build state. Every execution records its
549
+ Autobuild instance, deterministic sandbox name, and Vercel session id. After a
550
+ lease expires, recovery first stops/deletes the exact old name and confirms its
551
+ absence; only then does it provision the next generation. An interrupted create
552
+ is retried with the same deterministic generation/name, so an unknown response
553
+ cannot fan out duplicate VMs. Recovery uses the published build branch when it
554
+ exists and otherwise the original recorded branch-cut SHA, never a newly moved
555
+ base. Setup reruns and open sessions are reclaimed by the normal durable
556
+ protocol. Uncommitted files and `.ab` scratch may be lost; Store events/artifacts
557
+ and published Git checkpoints remain authoritative.
558
+
559
+ Paused and policy-blocked builds release their sandbox after the execution
560
+ lease expires. Resume creates a replacement and reruns setup. Graceful shutdown
561
+ bounds stop/delete calls by `operationTimeoutMs`; an unknown acknowledgement
562
+ keeps ownership fenced until lease expiry and is retried visibly. After abrupt
563
+ supervisor loss, Vercel's configured `timeoutSeconds` bounds orphan compute, and
564
+ the next dispatcher waits for lease expiry before exact-name cleanup. Provider
565
+ quota/vCPU/duration messages are retained verbatim in infrastructure events and
566
+ status output. A failed system step records its name, exact shell command, exit
567
+ status, labeled stdout and stderr, and `ab init --validate` remediation. It never
568
+ writes the marker or launches setup/an agent; the partial sandbox is deleted.
569
+ The existing `infrastructure.failed` stream exposes the diagnostic in `ab
570
+ status` and both dashboards, consumes `maxInfrastructureAttempts`, and raises
571
+ one setup-targeted escalation when exhausted. If cleanup also fails, both the
572
+ step output and cleanup uncertainty remain durable instead of hiding the root
573
+ cause. A marked sandbox does not repeat provisioning; every replacement/new
574
+ generation does.
575
+
576
+ The representative actual-provider check is opt-in and must point at an
577
+ independent consuming repository whose `autobuild.toml` selects the universal
578
+ image above, contains one ready ticket, and declares the `system-install` and
579
+ `browser-smoke` provisioning steps shown above. The checked-in browser smoke
580
+ must use `CHROMIUM_BIN`, serve a page, render it with headless Chromium in the
581
+ same guest, and fail if any stage fails. With hosted Store, Vercel, GitHub,
582
+ and configured agent-runtime credentials available, run:
583
+
584
+ ```sh
585
+ AB_RUN_VERCEL_SANDBOX_LIVE=1 \
586
+ AB_VERCEL_SANDBOX_LIVE_REPO=/absolute/path/to/consumer \
587
+ AB_STORE=https://store.example AB_TOKEN=… \
588
+ bun test packages/core/src/integration/vercel-sandbox.live.test.ts
589
+ ```
590
+
591
+ The suite first runs `ab init` readiness validation through the production
592
+ adapter: it creates an unnamed fresh sandbox, reports both named provisioning
593
+ steps, accepts the structured guest probe marker, and verifies deletion by
594
+ querying that exact sandbox identity. A separate
595
+ case runs a complete child-driven build to publish and finalize, then requests
596
+ cleanup and verifies sandbox deletion. It does not demonstrate support for other
597
+ Vercel images or package managers.
598
+
599
+ Vercel workspaces return an absolute guest `path` plus an opaque sandbox-name
600
+ `ref`; they omit dispatcher-local path evidence. Branch config, relative/package
601
+ plugins, installed skills, setup, runtimes, checks, and phase CLI commands all
602
+ resolve in the guest checkout. Only names in `environmentVariables` plus scoped `AB_STORE`/`AB_TOKEN` enter
603
+ commands—dispatcher environment variables are never copied wholesale. Pi in a
604
+ sandbox should use an API-key provider such as `vercel-ai-gateway/...` with
605
+ `AI_GATEWAY_API_KEY`; never copy dispatcher `~/.pi` state, OAuth refresh tokens,
606
+ or attempt an interactive login. The same API-key-only rule applies to Codex,
607
+ Claude Code, and plugin runtimes.
608
+
609
+ The opt-in live lifecycle suite is
610
+ `packages/core/src/integration/vercel-sandbox.live.test.ts`. Set
611
+ `AB_RUN_VERCEL_SANDBOX_LIVE=1`, `AB_VERCEL_SANDBOX_LIVE_REPO` to an independent
612
+ checkout with a ready file ticket and this provider configuration, plus the
613
+ hosted Store, Vercel, runtime, and GitHub credentials above. Its readiness case
614
+ creates and probes a fresh sandbox, parses the guest marker, and confirms
615
+ deletion by exact identity. Separately, its interruption case deletes the first
616
+ build sandbox while its durable execution identity is live, waits through the
617
+ lease fence, and asserts a distinct replacement sandbox/session identity,
618
+ successful continuation, and final provider cleanup. The suite is credentialed,
619
+ long-running, and mutates the configured test repository/Store; it is skipped
620
+ unless the opt-in flag is set.
621
+
622
+ ## `[commands]`
623
+
624
+ An optional open map of repository-defined names to shell strings. Its default
625
+ is `{}`. `setup`, `lint`, `type-check`, and `test` are conventions, not fixed
626
+ schema fields.
627
+
628
+ | Field | Default | Constraints | Purpose |
629
+ |---|---:|---|---|
630
+ | `<name>` | — | nonempty key and nonempty shell string | Name a deterministic verb for setup or a check step. |
631
+
632
+ <!-- config-fragment:commands -->
633
+ ```toml
634
+ [commands]
635
+ setup = "bun install"
636
+ typecheck = "bun tsc --noEmit"
637
+ test = "bun test"
638
+ publish = "bun run publish"
639
+ ```
640
+
641
+ `setup` is special by convention: the kernel runs it after workspace provision
642
+ and after sandbox rehydration. A failure is recorded durably with its command,
643
+ exit status, attempt, and output. Retries are bounded by
644
+ `policy.maxSetupAttempts`; exhaustion blocks on a human-answerable setup
645
+ escalation without creating a phase or verification result. A later successful
646
+ attachment clears the current error projection. Other names run only when
647
+ referenced by a configured check. A check's `command` value is the key in this
648
+ map, not an inline shell command. The kernel passes the mapped string to a shell
649
+ as written.
650
+
651
+ ## `[verify]` and `[verify.<step>]`
652
+
653
+ `[verify].steps` defines the configured universe and execution order. It is
654
+ optional and defaults to no verify phases. Every listed name needs one matching
655
+ subtable, and every named subtable must be listed; a defined-but-unlisted step
656
+ never silently disappears.
657
+
658
+ There are two strict step variants:
659
+
660
+ - `kind = "check"` runs a configured command directly and decides pass/fail
661
+ from its exit status. It starts no agent session.
662
+ - `kind = "agent"` runs the exact configured skill, which terminates with an
663
+ `ab verdict` of `pass`, `fail`, or `skip`.
664
+
665
+ | Field | Default | Constraints | Purpose |
666
+ |---|---:|---|---|
667
+ | `steps` | `[]` | array of nonempty step names | Canonical execution order. |
668
+ | `kind` | — | required `"check"` or `"agent"` | Select the strict step variant. |
669
+ | `command` | — | required for check; nonempty key in `[commands]` | Deterministic shell verb to run. Forbidden for agent steps. |
670
+ | `skill` | — | required for agent; nonempty string | Exact installed verifier skill. Forbidden for check steps. |
671
+ | `paths` | omitted (unconditional) | optional nonempty array of positive repository-relative globs | Apply only when at least one changed path matches. Available to both variants. |
672
+ | `always` | omitted (`false`) | optional boolean | `true` makes the step mandatory in plan selection and bypasses path gating. Available to both variants. |
673
+
674
+ <!-- config-fragment:verify -->
675
+ ```toml
676
+ [commands]
677
+ typecheck = "bun tsc --noEmit"
678
+
679
+ [verify]
680
+ steps = ["types", "e2e"]
681
+
682
+ [verify.types]
683
+ kind = "check"
684
+ command = "typecheck"
685
+ always = true
686
+
687
+ [verify.e2e]
688
+ kind = "agent"
689
+ skill = "verify-app-e2e" # repository-authored skill
690
+ paths = ["web/**", "src/routes/**"]
691
+ ```
692
+
693
+ Autobuild does not ship a generic agent verifier. `verify-app-e2e` above is a
694
+ skill authored by the repository for its own running application.
695
+
696
+ Cross-field validation rejects:
697
+
698
+ - a listed step without its `[verify.<step>]` table;
699
+ - a named table not present in `steps`;
700
+ - a check whose command is absent from `[commands]`;
701
+ - fields from the other kind, unknown fields, and malformed selectors.
702
+
703
+ `always = true` does not make malformed `paths` acceptable: all supplied
704
+ selectors are validated even though the mandatory step will not use them for
705
+ applicability.
706
+
707
+ ### Role routing for an agent verify step
708
+
709
+ An agent verify step selects `[roles.<step>]` by its **logical step name** —
710
+ the name in `[verify].steps`, not the skill it runs. This is the same rule an
711
+ agent finalize post-step follows.
712
+
713
+ <!-- config-fragment:verify-role -->
714
+ ```toml
715
+ [verify]
716
+ steps = ["e2e"]
717
+
718
+ [verify.e2e]
719
+ kind = "agent"
720
+ skill = "verify-app-e2e" # repository-authored skill
721
+
722
+ [roles.e2e] # the STEP name — not "verify-app-e2e"
723
+ runtime = "pi"
724
+ model = "openai-codex/gpt-5.6-sol"
725
+ ```
726
+
727
+ The step's configured skill name remains a deprecated alias for existing
728
+ configurations and will be removed in a future release. It is consulted only
729
+ when `[roles.<step>]` is undeclared, so the step name always wins when both are
730
+ present, and `ab dispatch` reports the alias with the step name to rename it to.
731
+
732
+ ### Plan-selected steps
733
+
734
+ A plan may open with strict TOML front matter naming the complete set of
735
+ optional verification warranted by that plan:
736
+
737
+ <!-- plan-front-matter -->
738
+ ```toml
739
+ +++
740
+ verifySteps = ["types", "e2e"]
741
+ +++
742
+ ```
743
+
744
+ No opening metadata selects every configured step, preserving older plans. An
745
+ explicit `verifySteps = []` selects none and is valid only when no configured
746
+ step has `always = true`. Names must be known, nonempty, unpadded, and unique.
747
+ Unknown names, duplicate names, malformed metadata, or omission of a mandatory
748
+ step make the planner's `ab done` fail before `plan.completed` is recorded.
749
+
750
+ The list is a set: its written order never reorders execution. The selected
751
+ steps are stored in `[verify].steps` order from the exact plan completion later
752
+ approved by plan review. A spec restart replaces the selection with the newly
753
+ approved plan; reconciliation reuses it.
754
+
755
+ For each step the kernel evaluates approved-plan selection first, then path
756
+ applicability. Exclusion by either mechanism records a queryable `skipped`
757
+ outcome. A selection exclusion or path miss starts no check or agent.
758
+ An `always = true` step cannot be excluded by the plan and runs regardless of
759
+ its `paths` value.
760
+
761
+ ### Path applicability
762
+
763
+ Selectors are case-sensitive over Git's `/`-separated repository-relative
764
+ paths. Supported syntax is literal characters, `*`, `?`, and `**` only as a
765
+ complete path segment. The following are rejected: absolute paths, `.` or `..`
766
+ traversal segments, empty segments, NUL bytes, negation,
767
+ backslashes/escapes, character classes, brace expansion, extglobs, and `**`
768
+ embedded in another segment. Selectors and changed paths use any-match
769
+ semantics.
770
+
771
+ Immediately before a selected conditional step, the runner performs a
772
+ NUL-delimited, no-rename Git name diff from the initial branch-cut SHA to the
773
+ current `HEAD`. Adds, modifications, deletions, and both sides of a rename can
774
+ therefore make a step apply. After a completed reconcile, the refreshed base
775
+ becomes the diff base: upstream-only paths merged from the base are excluded,
776
+ while build-owned changes and conflict resolutions remain visible. A Git or
777
+ base-resolution failure is an infrastructure failure, never a permissive skip.
778
+
779
+ ### Pass, fail, and skip
780
+
781
+ A passing check or agent advances to the next step. An agent failure requires
782
+ `ab verdict fail --report <file>`; the exact report is routed back to implement,
783
+ and the failure consumes the verify retry budget in
784
+ `policy.maxVerifyAttempts`. Check failures similarly retain command output as
785
+ the implementation feedback.
786
+
787
+ An agent may instead use `ab verdict skip --reason <text>` when the configured
788
+ judgment genuinely does not apply. The trimmed reason must be nonblank, and no
789
+ failure report is required. Plan and path exclusions are kernel-authored skips
790
+ with deterministic reasons. Every skip satisfies that one step and advances,
791
+ but remains distinct from a pass, does not mask another step's failure, and
792
+ does not consume the failure retry budget.
793
+
794
+ ## `[finalize]` and `[finalize.<step>]`
795
+
796
+ `[finalize].steps` is an optional ordered list of post-PR actions and defaults
797
+ to `[]`. These run after the finalize agent writes the PR description and the
798
+ kernel opens the PR. Like verify, every listed name requires one matching table
799
+ and every table must be listed.
800
+
801
+ Finalize has a smaller strict union. Verify-only `paths` and `always` fields
802
+ are errors.
803
+
804
+ | Field | Default | Constraints | Purpose |
805
+ |---|---:|---|---|
806
+ | `steps` | `[]` | array of nonempty step names | Ordered post-PR actions. |
807
+ | `kind` | — | required `"check"` or `"agent"` | Select deterministic command or agent action. |
808
+ | `command` | — | required for check; nonempty key in `[commands]` | Run the mapped command with no agent session. |
809
+ | `skill` | — | required for agent; nonempty string | Exact installed skill; no prefix is inferred. |
810
+
811
+ <!-- config-fragment:finalize -->
812
+ ```toml
813
+ [commands]
814
+ publish = "bun run publish"
815
+
816
+ [finalize]
817
+ steps = ["publish", "release-notes"]
818
+
819
+ [finalize.publish]
820
+ kind = "check"
821
+ command = "publish"
822
+
823
+ [finalize.release-notes]
824
+ kind = "agent"
825
+ skill = "ab-release-notes"
826
+ ```
827
+
828
+ The logical step name selects `[roles.<step>]` for an agent post-step — the
829
+ same rule an agent verify step follows, so one convention covers both:
830
+
831
+ <!-- config-fragment:finalize-role -->
832
+ ```toml
833
+ [verify]
834
+ steps = ["e2e"]
835
+
836
+ [verify.e2e]
837
+ kind = "agent"
838
+ skill = "verify-app-e2e" # repository-authored skill
839
+
840
+ [finalize]
841
+ steps = ["release-notes"]
842
+
843
+ [finalize.release-notes]
844
+ kind = "agent"
845
+ skill = "ab-release-notes"
846
+
847
+ [roles.e2e] # verify STEP name — not "verify-app-e2e"
848
+ runtime = "pi"
849
+
850
+ [roles.release-notes] # finalize step name
851
+ runtime = "pi"
852
+ ```
853
+
854
+ For an agent *verify* step, the step's configured skill name remains a
855
+ deprecated alias for existing configurations and will be removed in a future
856
+ release. Finalize has never had such an alias.
857
+
858
+ Both
859
+ kinds are failure-tolerant: nonzero commands, launch/execution errors, and
860
+ structured agent failures record `ok = false` plus a follow-up observation,
861
+ then the sequence continues. A post-step cannot turn an otherwise green build
862
+ red.
863
+
864
+ A content-producing step selects and commits only its intended files locally
865
+ and must leave a clean worktree. The runner proves the last published head is
866
+ an ancestor, then performs a regular non-force push through the Forge port to
867
+ extend the open PR branch. Agents never push. An unchanged `HEAD` creates and
868
+ pushes no commit. Dirty output, rewritten history, Git errors, and publication
869
+ failures become failure-tolerant follow-up observations.
870
+
871
+ ## `[roles]`
872
+
873
+ An open map from a nonempty role name to three primary agent axes, one
874
+ wall-clock session budget, and an ordered alternate list. All five live fields
875
+ inherit independently. The reserved
876
+ `default` entry is required to name a runtime, is the raw base for every other
877
+ role, and is never dispatched itself. Missing it fails eagerly before any
878
+ session starts, with a copyable table and all registered runtime names.
879
+
880
+ | Field | Default | Constraints | Purpose |
881
+ |---|---:|---|---|
882
+ | `runtime` | inherited from required `[roles.default].runtime` | required on `default`; optional nonempty registered runtime name on children | Select an agent adapter. |
883
+ | `model` | inherited; otherwise selected runtime's own default | optional nonempty model id compatible with the resolved runtime | Select the exact model. Codex uses unqualified `gpt-*`; Pi ids are provider-qualified. |
884
+ | `args` | inherited; otherwise `[]` | optional array of nonempty strings; `[]` allowed | Ordered extra CLI argv tokens applied to every phase and tool-free one-shot invocation. A supplied list replaces, rather than unions with, the inherited list. |
885
+ | `extensions` | no effect | deprecated optional array of nonempty strings | Compatibility-only field. Dispatch warns for each declaring role; migrate explicit runtime options to `args`. |
886
+ | `sessionBudgetSeconds` | inherited; otherwise `[policy].sessionBudgetSeconds` | optional positive integer | Wall-clock budget for each build phase session routed through this logical role. One budget covers the primary and every alternate target. |
887
+ | `alternates` | inherited; otherwise `[]` | optional ordered array of strict `{ runtime?, model?, args?, extensions? }` entries; `[]` allowed | Failure-triggered execution targets. A role's list replaces the inherited list wholesale; each entry overlays that role's effective primary axes. `extensions` is accepted there only as the same deprecated no-op. |
888
+
889
+ <!-- config-fragment:roles -->
890
+ ```toml
891
+ [roles.default]
892
+ runtime = "claude"
893
+ args = []
894
+ alternates = [
895
+ { runtime = "pi", model = "openai-codex/gpt-5.6-sol", args = ["--extension", "./tools/pi.js"] },
896
+ ]
897
+
898
+ [roles.plan]
899
+ args = ["--effort", "high"]
900
+ alternates = [] # replace the inherited list for this role
901
+
902
+ [roles.code-review]
903
+ runtime = "pi"
904
+ model = "kimi-coding/k3"
905
+ args = ["--extension", "./tools/web-access.js"]
906
+ sessionBudgetSeconds = 7200
907
+ ```
908
+
909
+ Inheritance is mechanical per field. For example, changing only a child
910
+ runtime does not discard a model inherited from `default`; the resulting exact
911
+ pair must be compatible. Autobuild never searches for a runtime that happens
912
+ to serve a configured model and never substitutes a different model to repair
913
+ an invalid pair. The only implicit fill is when neither the role nor `default`
914
+ names a model, in which case the selected runtime uses its own default. Every
915
+ alternate overlays that concrete role's effective primary runtime, model, and
916
+ args, then undergoes the same exact-pair validation. Each configured string is
917
+ one argv token; Autobuild does not shell-expand it. The narrow rejected set is
918
+ derived from the model and machine-readable protocol options each adapter emits:
919
+ Claude rejects `-p`/`--print`, `--output-format`, and `--model`; Codex rejects
920
+ `--json` and `--model`/`-m`; Pi rejects `--mode` and `--model`. Claude and Codex
921
+ also reject a standalone `--` because Autobuild owns and appends that structural
922
+ separator immediately before their positional prompt. Those option spellings,
923
+ `--option=value` forms, compact documented short aliases, and the exact affected
924
+ prompt separator join the eager aggregated startup error with a
925
+ role/runtime-specific diagnostic. Every other option passes through without
926
+ Autobuild validation, including sandbox, approval, session, tool, skill,
927
+ thinking, extension, lifecycle, unknown options, and arguments that merely
928
+ contain two hyphens, even when they supplement another occurrence emitted by the
929
+ adapter. Unknown fields,
930
+ runtimes, and incompatible models in any indexed entry join the aggregated
931
+ eager startup error; they never first surface during an outage.
932
+
933
+ Three runtimes ship: `claude`, `codex`, and `pi`; `ab plugin list` projects all
934
+ three as builtin agent runtimes, and trusted plugins may register additional
935
+ names. The `pi` runtime requires a locally installed Pi CLI version 0.84.3 or
936
+ newer and uses that installation's login and model catalog through headless RPC;
937
+ Autobuild does not install Pi. `ab init` reports a missing or older executable
938
+ as unusable, and `pi` must be logged in for every configured model. `ab models`
939
+ lists this local catalog, while `ab models --available` limits it to models with
940
+ configured credentials. Builtin and plugin runtimes use the same exact-pair
941
+ validation and event attribution. With no configured model, the selected runtime uses its declared
942
+ default when present: Claude and Codex delegate to their local CLI's selected
943
+ default, while Pi declares `kimi-coding/k3`. Codex accepts unqualified `gpt-*`
944
+ ids. Pi ids remain provider-qualified—including the independent
945
+ `openai-codex/*` route—and `ab models [query]` looks them up. Pi always passes
946
+ `--no-extensions` to disable ambient package discovery and loads the Autobuild
947
+ bridge explicitly. Repeatable `--extension <path>` tokens in `args` supplement
948
+ that setup and expose every tool registered by that explicitly loaded extension;
949
+ without them, no operator-installed extension loads. A plugin may declare
950
+ optional tool-free one-shot completion for `slug` and `upgrade`; absence keeps
951
+ each caller's existing fail-safe behavior. Role `args` apply to those one-shots
952
+ too. Effective arguments are recorded on session-start facts.
953
+
954
+ The injected Codex protocol/contract suite is deterministic and credential-free:
955
+ `bun test packages/core/src/ports/runner/codex.test.ts`. An authenticated local smoke run is
956
+ opt-in:
957
+ `AB_RUN_LIVE_PORT_CONTRACTS=1 AB_CODEX_CONTRACT_MODEL=gpt-… bun test packages/core/src/ports/runner/codex.live.test.ts`.
958
+
959
+ Core agent phases route by phase name (`plan`, `plan-review`, `implement`,
960
+ `code-review`, `finalize`, and `reconcile`). Agent verify steps and agent
961
+ finalize post-steps both route by their logical step name. The verify step's
962
+ configured skill name remains a deprecated alias for existing configurations
963
+ and will be removed in a future release. Repository judgments use `harvest` and
964
+ `harvest-review`; `slug` and `upgrade` configure tool-free one-shot judgments.
965
+ Arbitrary additional role keys are accepted, but only a name selected by one of
966
+ these routes affects a session.
967
+
968
+ A declared key that no route requests is resolved and validated like any other,
969
+ and then never used — `ab dispatch` reports it at startup, naming the key and
970
+ the keys valid for this configuration, and reports a deprecated skill-name key
971
+ with the step name to rename it to. Both stay warnings; neither blocks a
972
+ session or changes which runtime and model run. `kind = "check"` steps start no
973
+ session and so are not a route: naming a role after one does not make it
974
+ consumed. It is reported only when that check step is its sole apparent route —
975
+ a check step named `plan` leaves `[roles.plan]` consumed by the core `plan`
976
+ phase, and nothing is reported.
977
+
978
+ Resolver construction validates `default`, every declared role, and every
979
+ alternate eagerly and aggregates all unknown-runtime and incompatible-model
980
+ problems. Unknown-runtime diagnostics list every builtin and materialized
981
+ plugin runtime. A deliberately different reviewer model is valid and often
982
+ useful; mixed models are not a configuration inconsistency.
983
+
984
+ Each session attempt starts with its role's primary. Overload, rate limits, 5xx,
985
+ timeout, transport, unknown provider failures, and quota/usage/billing
986
+ exhaustion try alternates in declaration order inside that same phase attempt.
987
+ Authentication, permission, and local runtime-configuration failures do not.
988
+ Each target gets a separate session and transcript; a continuation that moves
989
+ to another target starts fresh from durable context and cannot inherit the
990
+ failed provider's conversation. Selection is not sticky: the next phase, next
991
+ review-loop continuation, and next attempt begin at the primary again.
992
+
993
+ Trying alternates does not consume additional phase attempts. Only an exhausted
994
+ or non-eligible chain writes `phase.failed`; its final failure controls retry
995
+ policy, so final quota/usage/billing exhaustion parks immediately while a final
996
+ availability failure consumes one existing bounded attempt. `session.started`
997
+ records each selected runtime/model and substitution cause, and an exhausted
998
+ failure records every tried target and verbatim error for policy escalation.
999
+ The chain applies to core phases, agent verify/finalize steps, and Harvest.
1000
+ Tool-free one-shot completions such as slug and upgrade use only the primary.
1001
+
1002
+ For build sessions, the kernel starts the captured role budget after the durable
1003
+ `session.started` event. Expiry aborts and ends the session best-effort, drops
1004
+ producer continuation state, and records `phase.failed` with
1005
+ `phase session budget expired after <seconds> seconds`. It does not select an
1006
+ alternate because it is kernel policy, not a provider failure. The ordinary
1007
+ phase-attempt guard retries from the primary and raises an answerable policy
1008
+ escalation when exhausted. Operator cancellation does not disable the captured
1009
+ deadline: a cooperative adapter still returns promptly, while an adapter that
1010
+ ignores cancellation is released when the deadline arrives. If operator abort
1011
+ arrived first, that release stays an abort control outcome, starts no alternate,
1012
+ and records no `phase.failed`. A typed terminal deposited before either boundary
1013
+ remains authoritative. Agent finalize post-step expiry instead records that
1014
+ failure-tolerant step as `ok = false` and files its follow-up observation.
1015
+ Harvest sessions and direct verify/finalize check commands are not covered by
1016
+ this setting.
1017
+
1018
+ ## `[policy]`
1019
+
1020
+ Optional. Every field receives its own default. All are positive integers except
1021
+ `harvestMaxDrift`, which is nonnegative so zero can disable that trigger.
1022
+
1023
+ | Field | Default | Constraints | Purpose |
1024
+ |---|---:|---|---|
1025
+ | `sessionBudgetSeconds` | `3600` | positive integer | Wall-clock bound for each build agent session unless its logical role overrides it. |
1026
+ | `stallRounds` | `3` | positive integer | Escalate when the same review finding survives this many rounds. |
1027
+ | `maxVerifyAttempts` | `3` | positive integer | Bound failure-driven verify → implement retry cycles. |
1028
+ | `maxSetupAttempts` | `3` | positive integer | Bound consecutive workspace setup failures before human escalation. |
1029
+ | `maxInfrastructureAttempts` | `3` | positive integer | Bound consecutive provision/start/wait/cleanup failures before a setup-targeted policy escalation; a human `retry` answer re-arms this budget. |
1030
+ | `maxReconcileAttempts` | `3` | positive integer | Bound completed reconciles that leave the PR conflicted against an unchanged authoritative base. Moving-base races do not consume the bound. |
1031
+ | `maxReviewRounds` | `6` | positive integer | Default bound for each plan/review and implement/review convergence loop. An operator may replace it for one parked build loop and current spec revision with `ab answer --review-round-ceiling`; other builds are unaffected. |
1032
+ | `harvestThreshold` | `5` | positive integer | New unclaimed observation occurrences needed to start one harvest run. |
1033
+ | `harvestMaxDrift` | `3` | nonnegative integer | Other builds merged after the oldest unclaimed observation needed to start a run; `0` disables drift. |
1034
+
1035
+ For each repeat conflict, Autobuild compares the base merged by the most recent
1036
+ completed reconcile with a fresh authoritative base snapshot. An advanced base
1037
+ proves the attempt lost a race and permits another reconcile regardless of the
1038
+ attempt high-water. An unchanged base consumes `maxReconcileAttempts` and
1039
+ escalates when the bound is exhausted. Every current policy escalation carries
1040
+ a closed `policyCause`; reconcile no-progress uses `reconcile-no-progress`,
1041
+ while `round` remains occurrence scope. Answering a runner-failure cause re-arms
1042
+ only its matching phase and round. The phase-level `verify-failure-limit` cause
1043
+ explicitly re-arms all rounds for its verify target, while
1044
+ `reconcile-no-progress` and the setup-only cause reset no phase-runner failures.
1045
+ The closed cause map forces every future policy condition to choose one of these
1046
+ semantics instead of inheriting behavior from a missing `round`. Runner failures
1047
+ while checking progress or refreshing the base therefore remain counted after a
1048
+ roundless no-progress answer. If the unchanged-base bound is already exhausted,
1049
+ Autobuild raises the matching no-progress escalation before starting another
1050
+ reconcile. Cause-less historical raises retain their former round-shaped reset
1051
+ behavior, and roundless policy/reconcile raises retain their former no-progress
1052
+ meaning, on replay without migration. Post-reconcile verification still runs in
1053
+ full and remains independently bounded by `maxVerifyAttempts`.
1054
+
1055
+ Harvest is driven by repository pressure during dispatcher ticks, not a wall
1056
+ clock, and is independent of build `capacity`. A run starts when observation
1057
+ count reaches `harvestThreshold` **or** drift reaches `harvestMaxDrift`. Drift
1058
+ uses the oldest unclaimed observation, counts only later same-repository
1059
+ `pr.merged` facts from other builds, and ignores aborted and closed-unmerged
1060
+ builds. Whichever trigger fires, Harvest claims the complete current
1061
+ accumulation and records `count`, `drift`, or `both` on the durable start fact.
1062
+ The dashboard header reports
1063
+ `queue <depth> | active <current>/<limit> | observations <current>/<limit>`.
1064
+ The observation current value is the unclaimed occurrence count and its limit is
1065
+ `policy.harvestThreshold`. The interactive frontend refreshes that display-only
1066
+ count directly from the BuildStore, retains the last successful value with a
1067
+ diagnostic after a failed refresh, and appends no transport event. The separate
1068
+ `policy.harvestMaxDrift` trigger is configured here but is not shown in the
1069
+ header. The repository lease and fixed per-run recovery budget are implementation
1070
+ invariants, not additional configuration fields.
1071
+
1072
+ ## `[tickets]`
1073
+
1074
+ Required in practice: `readyState` deliberately has no default, so omitting the
1075
+ table fails at `tickets.readyState` rather than allowing tickets from every
1076
+ state. Within a present table, `source` and `readyState` are required.
1077
+
1078
+ | Field | Default | Constraints | Purpose |
1079
+ |---|---:|---|---|
1080
+ | `source` | — | required nonblank builtin or plugin registration name | Select the ticket adapter after configured plugins load. |
1081
+ | `readyLabels` | source-aware | optional array of nonempty strings; `[]` allowed | Require every listed label in addition to the state gate. |
1082
+ | `readyState` | — | required nonblank string | The one workflow state eligible for dispatch. |
1083
+ | `teamKey` | — | required for Linear and hosted; forbidden for file; optional for plugins | Backend team key such as `"ENG"`, or an existing plugin config field. |
1084
+ | `claimedState` | `"In Progress"` for direct Linear; hosted backend default otherwise | optional nonempty string; forbidden for file; allowed for plugins | Workflow state entered when a ticket is claimed. |
1085
+ | `createState` | provider default | optional nonempty string | Default state for newly created tickets when `ab ticket create` omits `--state`. |
1086
+ | `triageState` | Linear: `"Backlog"`; file/plugin: `"Triage"` | optional nonempty string | State used for spec-gate bounces, aborts, and closed-unmerged PRs. |
1087
+ | `proposalState` | the resolved `triageState` | optional nonempty string | State harvest files its synthesized proposals into. Setting it to `readyState` waives the human grooming gate, not dependency eligibility. |
1088
+ | `dir` | file: selected local state root's `tickets/`; plugin: omitted | optional nonempty path; forbidden for Linear; allowed for plugins | Root containing file-source state directories, or an existing plugin config field. Relative file paths resolve from the repository. |
1089
+
1090
+ When `readyLabels` is absent, direct Linear uses `["autobuild"]`; hosted, file,
1091
+ and plugin sources use `[]`, meaning no host-imposed label gate. An explicit value always
1092
+ wins. A nonempty list is
1093
+ conjunctive: every configured label must be present. `readyState` remains
1094
+ mandatory regardless of labels. Linear compares state and label names exactly
1095
+ and case-sensitively. The file source accepts state names case-insensitively and
1096
+ canonicalizes them to `triage/`, `ready/`, `doing/`, or `done/`.
1097
+
1098
+ Source-specific validation is strict:
1099
+
1100
+ - Linear and hosted require `teamKey` and reject `dir`.
1101
+ - File rejects `teamKey` and `claimedState`; `dir` is optional.
1102
+ - `createState`, `triageState`, and `proposalState` are valid for every source,
1103
+ but the named state must exist in that provider when used.
1104
+ - Plugin sources receive the existing fields in this table unchanged and own
1105
+ any additional semantic validation. No untyped plugin-options table exists.
1106
+
1107
+ For Linear, omitting `createState` lets the team's default state apply. For the
1108
+ file source it defaults to `Triage`. A caller may override that default for one
1109
+ create with `ab ticket create --state <state>`; the value is passed unchanged to
1110
+ the selected source, which owns its workflow vocabulary and rejects unknown
1111
+ states before creating anything. An omitted Linear `triageState` uses `Backlog`,
1112
+ because every team has it while the optional Linear triage feature may be
1113
+ disabled. The file source uses `Triage`.
1114
+
1115
+ `proposalState` names the one state observation harvest files proposals into,
1116
+ and defaults to the resolved `triageState` — proposals wait for a human, which
1117
+ is the grooming gate the pipeline is built around. Naming `readyState` here
1118
+ waives that gate for this repository: every proposal the harvest loop approves
1119
+ enters the ordinary dispatch eligibility checks without being read, protected
1120
+ by that loop's own review and by the spec gate at dispatch. The producer and
1121
+ reviewer see scan-time existence and resolution for every distinct ticket that
1122
+ originated a claimed observation. Approved creates may carry evidence-backed
1123
+ source-local `blockedBy` ids for other prerequisites. Immediately before create,
1124
+ harvest refreshes declared ids and matching-source origins through the selected
1125
+ TicketSource. Unknown declared ids fail; missing or resolved origins are dropped;
1126
+ still-unresolved origins are deduplicated into `blockedBy` automatically.
1127
+ Harvest status and the filing report distinguish declared blockers from those
1128
+ derived from originating tickets. An unresolved harvested blocker still
1129
+ prevents claim until the source reports completion or the relationship is
1130
+ deliberately removed. It is a separate field precisely so
1131
+ the waiver stays narrow. Redirecting `triageState` instead would also send
1132
+ spec-gate bounces, aborts, and closed-unmerged PRs into the ready state, where
1133
+ a bounced ticket is reclaimed and bounced again on every tick.
1134
+
1135
+ The default file directory follows a selected local `AB_STORE` root and writes
1136
+ its own self-excluding `.gitignore`. An explicitly configured `dir` belongs to
1137
+ the repository owner and is not automatically ignored.
1138
+
1139
+ <!-- config-fragment:linear-tickets -->
1140
+ ```toml
1141
+ [tickets]
1142
+ source = "linear"
1143
+ teamKey = "ENG"
1144
+ readyState = "Todo"
1145
+ readyLabels = ["autobuild"]
1146
+ claimedState = "In Progress"
1147
+ createState = "Backlog"
1148
+ triageState = "Backlog"
1149
+ ```
1150
+
1151
+ Linear credentials do not belong in this table. Set `LINEAR_API_KEY` in the
1152
+ environment or local `.env` file described below.
1153
+
1154
+ For a hosted backend, repository policy remains local while credentials remain
1155
+ at the service:
1156
+
1157
+ <!-- config-fragment:hosted-tickets -->
1158
+ ```toml
1159
+ [tickets]
1160
+ source = "hosted"
1161
+ teamKey = "ENG"
1162
+ readyState = "Ready"
1163
+ readyLabels = []
1164
+ claimedState = "Doing"
1165
+ createState = "Triage"
1166
+ triageState = "Triage"
1167
+ ```
1168
+
1169
+ Set `AB_STORE` to the service's HTTP(S) URL and `AB_TOKEN` to a deployment
1170
+ operator token (`mint operator` without `--user`). The same source-agnostic wiring powers dispatch, harvest, ticket-aware
1171
+ skills, and all seven command families: `ab ticket create`, `update`, `block`,
1172
+ `unblock`, `list`, `show`, and `move`. See the
1173
+ [hosted ticket protocol](remote-ticket-protocol.md). In hosted Linear mode,
1174
+ `LINEAR_API_KEY` is configured only on the service; direct Linear remains
1175
+ supported.
1176
+
1177
+ When filing dependent tickets into the ready state, follow the
1178
+ [safe blocker-filing workflow](ticket-dependencies.md). Direct and hosted Linear
1179
+ create the issue before recording its blockers, so `create --blocked-by` is not
1180
+ an atomic publication guarantee. Stage outside ready, verify the relationships,
1181
+ then move to the intended ready state last.
1182
+
1183
+ A plugin registration is selected the same way. `ab dispatch` and every
1184
+ `ab ticket` subcommand load the plugin and route through that source. Within a
1185
+ dispatch process, one selected instance serves dependencies, claim/projections,
1186
+ harvest filing, and janitor completion. Unknown names fail with the available
1187
+ builtin and loaded plugin names. Missing descriptor credentials name both the
1188
+ source and each variable.
1189
+
1190
+ <!-- config-fragment:plugin-tickets -->
1191
+ ```toml
1192
+ plugins = ["./plugins/company.ts"]
1193
+
1194
+ [tickets]
1195
+ source = "company"
1196
+ readyState = "Ready"
1197
+ claimedState = "Doing"
1198
+ createState = "Triage"
1199
+ triageState = "Triage"
1200
+ ```
1201
+
1202
+ Set `COMPANY_TICKET_TOKEN` in the environment, never in this table.
1203
+
1204
+ ## `[orchestrator]`
1205
+
1206
+ Gates the operator-sandbox feature: a persistent, **credential-free**
1207
+ environment per operator × repository that an operator agent drives through the
1208
+ `sandbox.*` registry tools (served by `ab mcp` and every later binding). The
1209
+ table is closed to unknown keys and restart-classified as a whole.
1210
+
1211
+ | Field | Default | Constraints | Purpose |
1212
+ |---|---:|---|---|
1213
+ | `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. |
1214
+ | `sandbox` | — | strict subtable; absence reads as the defaults | Sandbox behavior knobs. |
1215
+
1216
+ `[orchestrator.sandbox]` fields:
1217
+
1218
+ | Field | Default | Constraints | Purpose |
1219
+ |---|---:|---|---|
1220
+ | `idleMinutes` | `30` | positive integer | Minutes without a sandbox tool call after which the dispatcher tick's janitor stops the environment, keeping its snapshot; the next tool call resumes it. |
1221
+ | `environmentVariables` | `[]` | array of nonblank names | Names of non-secret host variables forwarded into the sandbox — the only non-toolchain environment it ever sees, during provisioning/setup and tool exec/start alike. Unset means none. |
1222
+
1223
+ The credential-free rule is structural: the environment receives no store,
1224
+ forge, ticket-provider, or model credential, and naming any of `AB_STORE`,
1225
+ `AB_TOKEN`, `GITHUB_TOKEN`, `GH_TOKEN`, `LINEAR_API_KEY`, `VERCEL_OIDC_TOKEN`,
1226
+ `VERCEL_TOKEN`, `VERCEL_TEAM_ID`, `VERCEL_PROJECT_ID`, or `AI_GATEWAY_API_KEY`
1227
+ in `environmentVariables` is a config error. The typed operator tools remain
1228
+ the only route to build state from inside the sandbox. Tool bounds:
1229
+ `sandbox.exec` waits at most 300 seconds; stdout and stderr are each truncated
1230
+ at 65,536 bytes with a `[truncated by autobuild: output exceeded 65536 bytes]`
1231
+ marker; `sandbox.reset` and archiving an operator's last session are
1232
+ destructive (reset re-provisions from the current base head); there is at most
1233
+ one environment per operator and repository.
1234
+
1235
+ <!-- config-fragment:orchestrator -->
1236
+ ```toml
1237
+ [orchestrator]
1238
+ enabled = true
1239
+
1240
+ [orchestrator.sandbox]
1241
+ idleMinutes = 30
1242
+ environmentVariables = ["MY_TOOL_CONFIG"]
1243
+ ```
1244
+
1245
+ Set `MY_TOOL_CONFIG` in the host environment of the `ab mcp` process; a missing
1246
+ value fails the tool call that needs it, named as a typed `environment` error.
1247
+
1248
+ ## Complete example
1249
+
1250
+ The following deliberately exercises every fixed table and both verify/finalize
1251
+ step variants. Replace commands, paths, roles, and ticket states with values
1252
+ that exist in your repository and providers.
1253
+
1254
+ <!-- complete-config -->
1255
+ ```toml
1256
+ baseBranch = "main"
1257
+ capacity = 2
1258
+ forge = "github"
1259
+ plugins = ["./plugins/company.ts", "@acme/autobuild-plugin"]
1260
+
1261
+ [workspace]
1262
+ provider = "company-container"
1263
+
1264
+ [workspace.config]
1265
+ image = "ghcr.io/acme/build:bun"
1266
+
1267
+ [pr.imageHost]
1268
+ provider = "github-release"
1269
+ repository = "acme/public-review-assets"
1270
+ releaseId = 123456
1271
+
1272
+ [commands]
1273
+ setup = "bun install"
1274
+ typecheck = "bun tsc --noEmit"
1275
+ test = "bun test"
1276
+ publish = "bun run publish"
1277
+
1278
+ [verify]
1279
+ steps = ["types", "unit", "e2e"]
1280
+
1281
+ [verify.types]
1282
+ kind = "check"
1283
+ command = "typecheck"
1284
+ always = true
1285
+
1286
+ [verify.unit]
1287
+ kind = "check"
1288
+ command = "test"
1289
+
1290
+ [verify.e2e]
1291
+ kind = "agent"
1292
+ skill = "verify-app-e2e" # repository-authored skill
1293
+ paths = ["web/**", "src/routes/**"]
1294
+
1295
+ [finalize]
1296
+ steps = ["publish", "release-notes"]
1297
+
1298
+ [finalize.publish]
1299
+ kind = "check"
1300
+ command = "publish"
1301
+
1302
+ [finalize.release-notes]
1303
+ kind = "agent"
1304
+ skill = "ab-release-notes"
1305
+
1306
+ [roles.default]
1307
+ runtime = "claude"
1308
+ args = []
1309
+
1310
+ [roles.plan]
1311
+ args = ["--effort", "high"]
1312
+
1313
+ [roles.code-review]
1314
+ runtime = "pi"
1315
+ model = "kimi-coding/k3"
1316
+ args = ["--extension", "./tools/web-access.js"]
1317
+ sessionBudgetSeconds = 7200
1318
+
1319
+ [policy]
1320
+ sessionBudgetSeconds = 3600
1321
+ stallRounds = 3
1322
+ maxVerifyAttempts = 3
1323
+ maxSetupAttempts = 3
1324
+ maxInfrastructureAttempts = 3
1325
+ maxReconcileAttempts = 3
1326
+ maxReviewRounds = 6
1327
+ harvestThreshold = 5
1328
+ harvestMaxDrift = 3
1329
+
1330
+ [tickets]
1331
+ source = "file"
1332
+ readyState = "ready"
1333
+ readyLabels = []
1334
+ createState = "Triage"
1335
+ triageState = "Triage"
1336
+ proposalState = "Triage"
1337
+ dir = "tickets"
1338
+ ```
1339
+
1340
+ This repository's [`autobuild.toml`](../autobuild.toml) is another worked
1341
+ example.
1342
+
1343
+ ## What `ab init` generates
1344
+
1345
+ On the first `ab init [target]`, when `autobuild.toml` is absent, Autobuild
1346
+ writes a valid stack-neutral skeleton. Deterministic init code does not inspect
1347
+ package manifests, infer commands, or ask a fixed adapter survey. It vendors the
1348
+ editable skills, records their pristine bases, updates `.gitignore`, and probes
1349
+ every registered runtime for executable/authentication usability.
1350
+
1351
+ Init reports every probe result and chooses a setup launcher from the fixed
1352
+ product preference `claude`, then `codex`, then `pi`. That choice only starts
1353
+ setup; the temporary `[roles.default].runtime` skeleton value does not constrain
1354
+ the role runtimes or models the setup agent ultimately configures. When a
1355
+ shipped runtime is usable, no Claude discovery conflict remains, and both stdin
1356
+ and stdout are attached to a TTY, init starts the selected coding-agent CLI in
1357
+ the target repository with a short prompt pointing to the locally editable
1358
+ `.agents/skills/ab-guide/references/setup.md` guide reference. The user can
1359
+ answer its repository- and team-specific questions directly. When launched,
1360
+ the child exit status is init's exit status, and the direct handoff creates no
1361
+ build or BuildStore data.
1362
+
1363
+ When no discovery conflict exists but no shipped runtime is usable or either
1364
+ terminal stream is non-interactive, init still completes deterministic
1365
+ installation and exits successfully. It prints the same setup pointer prompt verbatim
1366
+ with instructions to run it in a coding agent; unusable runtime reports include
1367
+ their reasons. The setup agent reads the installed guide reference, derives
1368
+ commands and verification from the actual repository, chooses pipeline roles,
1369
+ configures ticket workflow and environment-only credentials, arranges suitable
1370
+ end-to-end verification (authoring a repository-owned agent verifier when
1371
+ needed), and leaves one groomed dispatchable ticket.
1372
+
1373
+ After installation, init reports `autobuild.toml: written|skipped`, counts all
1374
+ skill outcomes, names attention-worthy local edits, and prints runtime probe
1375
+ results. A distinct real `.claude/skills/ab-*` directory is an actionable
1376
+ discovery conflict rather than an alias: init processes every skill, summarizes
1377
+ all such conflicts with move/remove guidance, skips the setup handoff, and exits
1378
+ 1. Redirected/non-TTY output is plain text.
1379
+
1380
+ The generated file is an active-only, schema-valid skeleton. It writes
1381
+ `baseBranch = "main"`, `capacity = 4`, empty `[commands]`, empty verify and
1382
+ finalize step arrays, all six default policy values, a valid local file-ticket
1383
+ gate, and the selected setup runtime as a temporary explicit role default. It
1384
+ contains no setup command, package-manager command, language assumption, or
1385
+ repository-derived value. Therefore repositories containing only `package.json`
1386
+ or only `Cargo.toml` receive byte-identical config on the same machine.
1387
+
1388
+ The config rule is intentionally one-way: once `autobuild.toml` exists,
1389
+ `ab init` does not reconcile or overwrite the file. This remains true with
1390
+ `--force`; that flag can overwrite locally edited vendored skills, never
1391
+ configuration. The setup prompt instead asks the agent to review and improve
1392
+ the existing config.
1393
+
1394
+ Re-running init still maintains the `.autobuild/` ignore rule and skill
1395
+ installation. `ab upgrade` does not migrate or rewrite `autobuild.toml`; by
1396
+ default it first updates a recognized Bun installation — from the npm registry
1397
+ for a registry install, or from GitHub Releases for a `github:` install — and
1398
+ then merges vendored skills from that distribution. For a local install, Bun may update the
1399
+ *owning* project's Autobuild dependency in `package.json` and `bun.lock`; this
1400
+ package-manager side effect is separate from target-repository configuration.
1401
+ Use `ab upgrade --no-self-update` for merge-only behavior.
1402
+
1403
+ By default, upgrade makes one local commit on the target's current HEAD after a
1404
+ successful merge. It stages only paths it owns: each reported skill's canonical
1405
+ `.agents` tree, `.ab-pristine` record, and `.claude` discovery path, plus the
1406
+ target repository's `package.json` and `bun.lock` when this run's local Bun
1407
+ self-update wrote them. A global Bun update changes no target path, and a no-op
1408
+ skill merge creates no commit. Additions, modifications, links, and deletions
1409
+ are included. The message identifies `ab upgrade` and pairs every skill whose
1410
+ bytes changed with its reported outcome; unrelated staged, unstaged, and
1411
+ untracked work remains untouched.
1412
+
1413
+ Use `ab upgrade --no-commit` to leave all output uncommitted for manual review.
1414
+ The flag is forwarded when self-update hands off to the replacement binary, and
1415
+ the default path likewise carries its pre-update Git baseline through that
1416
+ handoff. If a replacement child receives the handoff marker without that
1417
+ baseline — possible when an older parent launches a newer child — it suppresses
1418
+ the entire automatic commit, names the cross-version compatibility reason, and
1419
+ leaves all upgrade-owned changes uncommitted for the operator. Skill-upgrade
1420
+ results and the merge-derived exit status remain unchanged. Upgrade likewise
1421
+ suppresses the whole commit and names why when any skill or Claude discovery
1422
+ path conflicts, an owned path was already dirty, the target is not a Git
1423
+ repository, HEAD/worktree identity changes, or Git is mid-merge, mid-rebase, or
1424
+ mid-cherry-pick. If upgrade cannot snapshot the worktree's Git index, it warns
1425
+ and declines to stage. A failed staging or commit attempt restores that exact
1426
+ pre-attempt index and reports the original Git failure; merged worktree files
1427
+ remain in place and the merge's exit status is unchanged. Upgrade never pushes
1428
+ or rewrites history.
1429
+
1430
+ Autobuild now installs 11 skills; only `ab-spec`, `ab-tickets`, and `ab-guide`
1431
+ are model-invocable. The setup reference is an ordinary support file in the
1432
+ editable/pristine `ab-guide` tree and participates in the same three-way
1433
+ upgrade merge as every other vendored file.
1434
+
1435
+ Upgrade has two one-time retirement classifications for the former
1436
+ `ab-setup` and `ab-verify-e2e` defaults. `removed` means pristine provenance
1437
+ existed and either the unreferenced live tree matched it and was removed with
1438
+ its owned discovery link, or the canonical live tree was already missing and
1439
+ upgrade cleared provenance plus any owned dangling link. `kept` normally means
1440
+ the tree was customized, still configured, or could not be proved safe to
1441
+ remove; upgrade preserves it and clears obsolete pristine ownership. It also
1442
+ means an otherwise removable canonical tree was deleted while a user-owned
1443
+ `.claude/skills/<name>` discovery entry — a distinct real directory or foreign
1444
+ symlink — was preserved byte-for-byte and remains discoverable. For a symlink,
1445
+ its link text and target are unchanged. That entry enters the ordinary
1446
+ structured discovery conflict report, so upgrade exits nonzero. A same-named
1447
+ repository-authored skill with no pristine provenance is never removed, and a
1448
+ second upgrade neither recreates a dangling link nor resurrects or re-reports
1449
+ either retirement.
1450
+
1451
+ ## Durable settings outside TOML
1452
+
1453
+ Four repository-wide operator choices intentionally live as facts in the
1454
+ BuildStore repository journal, not in `autobuild.toml`. They are latest-write
1455
+ wins, survive process restarts, and are sampled by every dispatcher on its
1456
+ poll. Editing TOML cannot change them.
1457
+
1458
+ | Setting | Fresh-repository default | Controls | Scope |
1459
+ |---|---:|---|---|
1460
+ | Ticket intake | on | `ab dispatch --intake` / `--no-intake`; `i` on the dashboard's global row, and `p` (pause all) / `r` (resume all) on that row or their sessionless equivalents `ab pause --all` / `ab resume --all`, which turn intake off and on as part of quiescing the repository | When off, skip only new ticket list/claim/dispatch work. Janitor work, lease recovery, in-flight builds, and harvesting continue. Turning intake off does not hold builds the repository has already accepted — that is the repository pause below. |
1461
+ | Repository pause | off | `p` (pause all) / `r` (resume all) on the dashboard's global row, or `ab pause --all` / `ab resume --all` | While on, the dashboard controls line shows `repository PAUSED`, and each held queued row shows `(held)` while retaining its literal `QUEUED` status. No dispatcher tick attaches a runner to such a build — recovery, startup resume, and the lease sweep all skip them. Running builds are parked by the per-build pauses the same command writes; the janitor still settles aborts and discards. |
1462
+ | Claim-time auto-merge default | off | `ab dispatch --auto-merge` / `--no-auto-merge`; `m` on the global row | Seeds durable auto-merge intent only on builds claimed after the setting is enabled. Existing builds never change with the default. |
1463
+ | Harvest gate | on | `h` on the dashboard's global row | Pauses or resumes repository observation harvesting. The header shows the kernel-acknowledged gate, not merely a pending keypress. |
1464
+
1465
+ The opposite flag forms for each dispatch setting are mutually exclusive.
1466
+ Omitting both writes nothing and reuses the durable value. Per-build
1467
+ pause/resume and auto-merge controls are separate facts and do not alter these
1468
+ repository defaults. Pause all and resume all write two facts each — the
1469
+ repository pause first, then intake — so a partial failure still leaves the
1470
+ repository at least as quiesced as asked; `ab pause --all` reports on stderr
1471
+ which of them landed when a walk stops partway. The current release has no TOML
1472
+ field for either the harvest gate or the repository pause, and no command that
1473
+ sets the repository pause on its own without also moving intake; inspect the
1474
+ harvest gate with `ab harvest status`.
1475
+
1476
+ ## Environment and credentials
1477
+
1478
+ Secrets and store selection accompany the file through environment variables:
1479
+
1480
+ | Variable | Used for | Notes |
1481
+ |---|---|---|
1482
+ | `LINEAR_API_KEY` | Direct Linear ticket source | Required and nonempty when `tickets.source = "linear"`; in hosted Linear mode it is service-only. |
1483
+ | `AB_STORE` | BuildStore and hosted ticket selection | A local path or HTTP(S) remote-store URL. `tickets.source = "hosted"` requires a nonblank HTTP(S) URL. A command's explicit `--store` wins for BuildStore selection, then nonblank `AB_STORE`, then the main checkout's `.autobuild/`. Relative local paths resolve from the main checkout. |
1484
+ | `AB_TOKEN` | Protected remote BuildStore and hosted tickets | Bearer credential forwarded to the service. Hosted tickets require deployment operator scope (`mint operator` without `--user`). Empty means no token; nonempty token bytes are treated as opaque. |
1485
+
1486
+ A local store selection relocates the state database, blobs, worktrees, and the
1487
+ default file-ticket directory together. With a remote store, Git worktrees and
1488
+ default file tickets remain under the repository's local `.autobuild/` root.
1489
+ An explicitly configured `tickets.dir` remains independent and resolves as
1490
+ described in its field row.
1491
+
1492
+ The `ab` binary loads exactly `<cwd>/.env` before routing a command. Its minimal
1493
+ parser accepts `KEY=VALUE`, an optional `export ` prefix, full-line `#`
1494
+ comments, and matching single or double quotes around the entire value. It
1495
+ trims surrounding whitespace, keeps additional `=` characters in values, does
1496
+ not perform interpolation or escape processing, and silently skips malformed
1497
+ lines. A missing or unreadable file is a no-op. Any key already present in the
1498
+ real process environment—even an empty string—wins over `.env`.
1499
+
1500
+ Variables such as `AB_BUILD`, `AB_PHASE`, `AB_SESSION`, and the harvest-session
1501
+ identity tuple are runner-owned ambient authorization. Operators should not
1502
+ set or copy them into `.env`; the runner stamps them for each session.
1503
+
1504
+ Forge and agent credentials remain adapter-owned. The `local-git` forge uses no
1505
+ credentials or network access: it keeps durable PR records under private Git
1506
+ refs, leaves each build at `refs/heads/ab/<slug>`, and squash-merges locally only
1507
+ after auto-merge consent. The squash author and committer come from the
1508
+ repository's ordinary Git identity, as resolved for a plain `git commit`; the
1509
+ landing is always unsigned even when `commit.gpgsign` is enabled. If Git cannot
1510
+ resolve that identity, the PR remains open and `ab build status <slug>` names
1511
+ `user.name` / `user.email` setup commands; dispatcher ticks retry automatically
1512
+ after the operator configures them. A dirty checked-out base does not by itself
1513
+ block the landing: tracked and untracked work on paths untouched by the squash
1514
+ survives.
1515
+ If the squash would overwrite operator work, the PR remains open, `ab build
1516
+ status <slug>` shows a path-bearing observation, and later dispatcher ticks retry
1517
+ automatically after the operator commits, stashes, or discards the collision;
1518
+ Autobuild never mutates that work. Inspect an open local change with `git diff
1519
+ main...ab/<slug>`. It has no review web UI and no image-hosting capability; attached artifacts use
1520
+ the existing text-download projection.
1521
+
1522
+ - for `forge = "github"`, either export a `GITHUB_TOKEN` or `GH_TOKEN` or run `gh auth login`:
1523
+ the kernel speaks the GitHub REST API directly, taking `GITHUB_TOKEN`, then `GH_TOKEN`, then the
1524
+ gh CLI's stored login (`gh auth token`) — so a local dispatcher needs only the gh login, while a
1525
+ host without gh (hosted or sandboxed dispatchers) must export a token. Separately make sure the
1526
+ Git remote can fetch/push with the process's Git credentials;
1527
+ - Claude sessions invoke the local `claude` CLI and use its configured login;
1528
+ install Claude Code, launch `claude`, and complete login before dispatching;
1529
+ - Codex sessions invoke the local `codex` CLI and use its configured login;
1530
+ install Codex, run `codex login`, and complete authentication before
1531
+ dispatching;
1532
+ - Pi sessions use Pi's provider authentication: start `pi` and run `/login`
1533
+ inside the interactive session, or, for non-interactive use, supply the
1534
+ provider credentials Pi supports (such as provider API keys in the environment).
1535
+
1536
+ No provider API key, GitHub token, or remote-store bearer token belongs in
1537
+ `autobuild.toml`.
1538
+
1539
+ ## Troubleshooting
1540
+
1541
+ ### Reading an invalid-config error
1542
+
1543
+ Start with `ab dispatch --once` when a real single tick is safe. A syntax error
1544
+ looks like:
1545
+
1546
+ ```text
1547
+ /path/to/autobuild.toml: TOML syntax error: ...
1548
+ ```
1549
+
1550
+ Fix TOML structure first. A schema failure starts with:
1551
+
1552
+ ```text
1553
+ /path/to/autobuild.toml: invalid config
1554
+ verify.e2e.skill: ...
1555
+ tickets.teamKey: ...
1556
+ ```
1557
+
1558
+ Each indented path is independently actionable. Common causes are a root scalar
1559
+ placed after a table, a misspelled strict field, a step listed without its
1560
+ subtable (or vice versa), a check naming no `[commands]` key, a field used on
1561
+ the wrong step kind, or source-specific ticket fields used together.
1562
+
1563
+ Runtime routing failures use a separate heading and list all bad roles. Check
1564
+ the merged `default` plus child values, not just the child table: each axis
1565
+ inherits independently. Confirm the runtime name is shipped or registered by a configured plugin and
1566
+ use `ab models [query]` (or the plugin's documentation) to choose a model family
1567
+ that runtime serves.
1568
+
1569
+ ### `tick: idle` with expected work
1570
+
1571
+ In plain/non-TTY mode, `tick: idle` means the pass recorded no dispatcher
1572
+ action. Check the gates in this order:
1573
+
1574
+ 1. **Durable intake:** the dashboard header must show intake on, or explicitly
1575
+ run a future dispatcher with `--intake`. Intake off skips the ready scan
1576
+ even when tickets exist.
1577
+ 2. **Repository pause:** a pause all holds every queued build, so the dashboard
1578
+ controls line shows `repository PAUSED` and an existing held build shows
1579
+ `(held)` beside its literal `QUEUED` status. Intake off alone shows neither
1580
+ pause indication. `r` on the dashboard's global row or `ab resume --all`
1581
+ releases the hold and restores intake.
1582
+ 3. **Ready state:** the ticket must be in exactly `tickets.readyState`. Linear
1583
+ is case-sensitive; file tickets must physically be in the corresponding
1584
+ state directory.
1585
+ 4. **Labels:** the ticket must carry every effective `readyLabels` value,
1586
+ including Linear's default `autobuild` label when the field is omitted.
1587
+ 5. **Dependencies:** every `blockedBy` ticket must exist and be complete in the
1588
+ same source. This includes harvest proposals filed directly into the ready
1589
+ state. Plain dispatch output reports unresolved ids and cycles.
1590
+ 6. **Capacity:** every nonterminal build for this repository—including paused
1591
+ and blocked builds—uses a slot. Inspect `ab builds --queued` and
1592
+ `ab build status <slug>` rather than looking only for a live process.
1593
+ 7. **Duplicate work:** a ready ticket already represented by an active build is
1594
+ deliberately excluded from the queue.
1595
+
1596
+ If the expected work is observation harvesting instead of a ticket, check the
1597
+ acknowledged harvest gate, the dashboard's unclaimed `observations` count, and
1598
+ the configured triggers. Harvest starts when `policy.harvestThreshold`
1599
+ unclaimed observations exist or when `policy.harvestMaxDrift` other builds have
1600
+ merged since the oldest one; a drift limit of zero disables the second
1601
+ condition. The header shows count pressure against `harvestThreshold`; drift
1602
+ progress is not shown. Harvest does not consume build capacity.
1603
+
1604
+ ### Authentication failures
1605
+
1606
+ - **Linear:** `LINEAR_API_KEY is not set` means the selected source was wired
1607
+ without a nonempty key. Set it in the real environment or `<cwd>/.env` and
1608
+ rerun. For API rejections, verify the personal key, `teamKey`, and that every
1609
+ configured workflow state exists in that team.
1610
+ - **GitHub and Git:** run `gh auth status` for PR, auto-merge, and release API
1611
+ calls; verify repository permissions and separately test the Git remote's
1612
+ fetch/push credentials. Image hosting additionally requires a public host
1613
+ repository, an existing published mutable release, the numeric release id,
1614
+ and Contents write permission.
1615
+ - **Agent runtime/provider:** authenticate the runtime selected by the merged
1616
+ role. A missing Codex adapter diagnostic names both `codex runtime` and the
1617
+ `codex` executable; install it and run `codex login`. A 401/402/403,
1618
+ permission, quota, or billing rejection is treated as a permanent provider
1619
+ failure rather than retried indefinitely. For Pi, confirm the
1620
+ provider-qualified model and provider login.
1621
+ - **Remote BuildStore:** confirm the effective `AB_STORE` URL and `AB_TOKEN`.
1622
+ A protected store reports 401 for a missing, invalid, or expired token and
1623
+ 403 when the token is valid but scoped to another build/session.