@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,1781 @@
1
+ ---
2
+ name: guide
3
+ description: Authoritative reference for the autobuild system as installed in this repository - the build lifecycle (grooming, dispatch, plan, plan-review, implement, code-review, verify, finalize, reconcile, merge), the complete autobuild.toml configuration surface, how `ab init` and `ab upgrade` treat config and vendored skills, and what each installed ab-* skill is for. Use when asked about how autobuild works or why a build did what it did; when editing autobuild.toml; when adding or changing a verify or finalize step; when configuring roles, runners, models, policy limits, dispatch, or ticket sources; when reading, editing, or upgrading the installed ab-* skills; when a question mentions autobuild, autobuild.toml, or the `ab` CLI; or when writing the product's own name in prose, documentation, or a UI string.
4
+ ---
5
+
6
+ # Autobuild system guide
7
+
8
+ Reference material for an agent working on a repository that uses autobuild.
9
+ This skill describes the *system*. It drives no phase and changes no files.
10
+
11
+ ## How to use this skill
12
+
13
+ For first-time or existing-repository installation, including the local versus
14
+ Vercel decision and readiness check, follow the canonical
15
+ [repository setup procedure](references/setup.md).
16
+
17
+ Two rules come first, because they govern everything below.
18
+
19
+ **The repository decides its own configuration; this guide only describes the
20
+ system.** For anything repository-specific — what verify steps run here, which
21
+ runner a role uses, what `test` actually invokes — read the repo, not this
22
+ document:
23
+
24
+ - `autobuild.toml` at the repo root — the live configuration.
25
+ - `.agents/skills/ab-*/SKILL.md` — the installed skills *as this repo edited
26
+ them*. They are editable by design, so the vendored copy may deliberately
27
+ differ from the distribution's default described here.
28
+ - The project's own `CLAUDE.md`, `README.md`, and docs.
29
+
30
+ When this guide and the repository disagree about the repository, the
31
+ repository wins. Quote its files, not this one, when answering "what does this
32
+ repo do?"
33
+
34
+ **Supply context for what the user asked; never prescribe what they didn't.**
35
+ This skill is background knowledge for the request in the current session. It
36
+ is not a mandate to audit or improve anything. If you notice a config you find
37
+ suboptimal — a missing verify step, a policy limit you'd set differently — that
38
+ is not a finding to act on. Make the change the user asked for, and nothing
39
+ else. Mentioning an unrelated observation at most belongs in a closing
40
+ sentence, never in a diff.
41
+
42
+ ## The product's name
43
+
44
+ Autobuild is one word. Write it `Autobuild` in prose and as a proper noun, and
45
+ `autobuild` in identifiers, commands, paths, config keys, and other technical
46
+ contexts. Never split or hyphenate it — not in UI strings, documentation,
47
+ comments, commit messages, or tests. The one exception is a verbatim quotation
48
+ of a name owned by an external system: that system's spelling is its own to
49
+ choose, not this project's to correct.
50
+
51
+ ## The lifecycle
52
+
53
+ A groomed ticket becomes a build, and the build walks a **fixed pipeline**:
54
+
55
+ ```text
56
+ spec → plan ⇄ plan-review → implement ⇄ code-review → verify:* → finalize
57
+ epilogue: (pr.conflicted → reconcile → verify:*)* → merged or closed
58
+ ```
59
+
60
+ - **Grooming and dispatch.** Work enters at Triage. Ingesters *propose*
61
+ tickets; a human grooms them to the complete
62
+ [spec standard](references/spec-standard.md) — what and why but never how,
63
+ verifiable acceptance criteria, explicit out-of-scope, and evidence — and
64
+ dispatches. Generated work cannot leave Triage un-groomed. The dispatcher
65
+ claims tickets that pass the `[tickets]` ready
66
+ gate, chooses a short immutable slug from the final conforming spec, and
67
+ starts builds up to the top-level `capacity` bound.
68
+ - **`spec`** — the ticket's spec becomes the build's contract. The `ab-spec`
69
+ skill is the human-interactive surface for producing it, and it runs *before*
70
+ a build exists.
71
+ - **`plan ⇄ plan-review`** — the planner turns the spec into a plan and may
72
+ select configured optional verify steps in its opening front matter; a fresh
73
+ reviewer (no memory of prior rounds, by design) reviews both together and
74
+ approves, asks for a revision, or escalates. The loop repeats until approval
75
+ or a policy limit.
76
+ - **`implement ⇄ code-review`** — the implementer executes the approved plan,
77
+ committing locally; a fresh reviewer reads the diff against spec and plan.
78
+ Same loop shape.
79
+ - **`verify:*`** — each step in `[verify].steps` runs as its own phase, in
80
+ order. A failing step sends the build back to `implement` with the report;
81
+ an explicit skip records why the step did not apply and advances.
82
+ - **`finalize`** — the agent writes the PR description; the kernel opens the
83
+ PR. `[finalize].steps` run afterward and are failure-tolerant. A
84
+ content-producing step commits selected files locally and finishes clean;
85
+ the runner regular-pushes a descendant head to extend the open PR branch.
86
+ - **Epilogue.** If main moves and the PR conflicts, `pr.conflicted` sends the
87
+ build to `reconcile` (merge base *into* the branch — never a rebase), then
88
+ back through `verify:*`. This can repeat. The build terminates **merged** or
89
+ **closed**.
90
+
91
+ **Review severity is proportion to the spec.** Both review loops rate a
92
+ finding by what the defect costs against the spec's acceptance criteria and
93
+ the realistic operating conditions of the work under review, not by how
94
+ certainly it is a defect: `blocking` names an acceptance criterion the defect
95
+ defeats, `important` names a criterion or stated invariant it puts at material
96
+ risk. A true defect that puts no acceptance criterion at risk, breaks no
97
+ stated invariant, and is unreachable under realistic input is `ab observe`,
98
+ not a finding, and work carrying such observations is approvable. A reviewer
99
+ does not raise a bar the spec set: where the spec bounds a failure model or an
100
+ operating condition, a stricter model is not a defect, and hostile or
101
+ pathological input that the surface's contract does not promise to handle is
102
+ `minor` or an observation, unless a security boundary, an acceptance
103
+ criterion, or a stated invariant makes it material.
104
+
105
+ **The grammar is fixed.** `verify:*` and `finalize:*` are the *only* extension
106
+ points. There are no custom phases, no DAGs, no reordering — a repo extends
107
+ autobuild by configuring verify and finalize steps, never by inventing stages.
108
+ If a request seems to need a new phase, say so rather than improvising one.
109
+
110
+ **Observation harvest is not a build phase.** On each dispatch tick, dispatch
111
+ measures both unclaimed observation count and drift: distinct other builds
112
+ merged after the oldest unclaimed observation. It starts when count reaches
113
+ `[policy].harvestThreshold` **or** drift reaches
114
+ `[policy].harvestMaxDrift`; zero disables drift. The originating build is
115
+ excluded even if it later merges, and only durable same-repository `pr.merged`
116
+ facts count. Either trigger claims the whole current accumulation and records
117
+ `count`, `drift`, or `both` on `harvest.started`, then dispatch runs one
118
+ repository-scoped workflow: deterministic `scan`, agent `synthesize`
119
+ ⇄ fresh adversarial `review`, then deterministic `file`. Only approved
120
+ spec-standard proposals are created, in `[tickets].proposalState` — the triage
121
+ state unless this repository has named another one. The scan context exposes
122
+ existence and resolution for each distinct observation-origin ticket. At filing,
123
+ matching-source origins are refreshed and every still-existing unresolved one
124
+ is unioned with the agent-declared `blockedBy` ids; missing, resolved, foreign,
125
+ and absent origins are dropped. Filing records declared and origin-derived
126
+ blockers separately, while duplicate ids appear only once on the created ticket.
127
+ If the ticket is already visible as ready while that create is still unfinished,
128
+ dispatch correlates its source creation key with the durable reservation and
129
+ withholds it without consuming capacity; it remains in queue depth with a
130
+ distinct durable diagnostic, then proceeds normally after filing settles or the
131
+ owning workflow stops. A repository journal, artifact stream, dedup ledger, and lease make every step
132
+ queryable and crash-safe without polluting `ab builds` or the fixed phase grammar. Claims
133
+ exclude observations until they are dispositioned or selectively released;
134
+ idle ticks launch no harvest agent. A non-retrying infrastructure failure parks
135
+ its run at the durable boundary. Before any new scan, dispatch recovers every
136
+ ordinary parked run in start order, even when later runs shadow it, with a
137
+ fixed two-attempt budget applied independently per run. Automatic and human
138
+ reopening share `harvest.resumed`; one human acknowledgement reopens every
139
+ ordinary parked run and acknowledges every exhaustion barrier. Claims,
140
+ artifacts, attempts, reservations, and filing facts stay intact, so completed
141
+ work is not repeated. After a run's bound, one atomic fact commits only
142
+ classifiable filed creates, still-valid frozen joins, and suppressions; missing
143
+ creates, tombstone/unknown joins, and otherwise unclassifiable members are
144
+ released as pending. A rejected store read remains retryable infrastructure,
145
+ while successfully read malformed content fails safe toward release. The same
146
+ fact raises a human-attention barrier. Acknowledging it permits a future scan
147
+ but never resurrects an exhausted run. Existing journals gain these semantics
148
+ on replay without migration; completed and deliberate escalated runs remain
149
+ terminal.
150
+
151
+ **Slug naming is not a phase.** For each new build, a tool-free one-shot call
152
+ proposes a lowercase kebab base of at most three meaningful spec-derived words.
153
+ The dispatcher owns a hard deadline, strict validation, and store-wide `-2`,
154
+ `-3`, … collision suffixes. Absence or any failure falls back to the first
155
+ three words of the kebab-cased title, so naming never blocks dispatch. The slug
156
+ and `ab/<slug>` branch are chosen once; existing builds are never renamed.
157
+ Naming inherits `[roles.default]` and can be overridden by `[roles.slug]`.
158
+
159
+ ## Who does what
160
+
161
+ The distinctions that change an administrator's answer:
162
+
163
+ - **Agents supply judgment** — planning, reviewing, implementing, verifying,
164
+ and narrow non-phase proposals such as slug naming or skill-conflict text. An
165
+ agent never decides a transition or whether any proposal is valid enough to
166
+ apply.
167
+ - **The kernel owns determinism** — phase transitions, gating, deduplication,
168
+ convergence and stall detection. Outcomes come from the typed `ab` CLI, never
169
+ from parsing an agent's stdout.
170
+ - **The BuildStore is append-only event logs.** The kernel reduces build status
171
+ from each build stream; dispatcher settings and harvest state are reduced
172
+ independently from the repository journal. Snapshots are never authoritative.
173
+ Events record facts, never derived state.
174
+ - **The interactive dashboard is a Store-only frontend.** It owns the terminal
175
+ and supervises a private child that owns dispatcher ticks, build runners,
176
+ Harvest, config/plugins, and ticket/forge/workspace/runtime adapters. Effective
177
+ config, standing queue depth, diagnostics, reload outcomes, runner outcomes,
178
+ and child health are run-correlated repository facts. Controls append directly
179
+ to the Store, so a slow claim or agent turn cannot delay a keypress. Repository
180
+ reads advance incrementally. SIGINT, SIGHUP, SIGQUIT, and SIGTERM restore the
181
+ terminal immediately, drain and reap the child, and keep repeated signals
182
+ graceful until an open claim tick reaches a recoverable boundary; non-SIGINT
183
+ termination is replayed only after that drain. Durable normal/forced/abnormal
184
+ evidence retains available process detail, and parent-death detection prevents
185
+ a detached kernel. `--plain`/non-TTY stay on the direct path, and `--once`
186
+ remains one tick plus drain.
187
+ - **Live configuration** has two explicit contexts. A running dispatcher owns
188
+ the last valid snapshot from the **main checkout**; dispatch, setup, and each
189
+ pipeline step capture it at their action boundary, without interrupting an
190
+ agent turn or command already running. Missing, unreadable, malformed, and
191
+ routing-invalid candidates retain that snapshot with deduplicated operator
192
+ notice, and restoring a valid file resumes reload. Scoped phase CLI processes
193
+ still read the build worktree's branch-owned file.
194
+ - **Workspaces** are provisioned per build and remain the locally reachable
195
+ execution context for that build.
196
+ - **Ticket providers** (`linear`, `file`) sit behind one port; the dispatcher
197
+ does not know which is configured.
198
+ - **Forge operations and pushes are kernel-side plumbing.** Agents commit
199
+ locally and never push, never touch a remote, never open the PR. A push
200
+ happens at the implement terminal boundary or after the runner validates a
201
+ successful content-producing finalize post-step; neither path force-pushes.
202
+ `github` publishes through GitHub. `local-git` performs no remote/network/`gh`
203
+ operation: durable PR records live under private Git refs, the review locator
204
+ is `refs/heads/ab/<slug>`, and consent lands a local guarded, unsigned squash
205
+ whose message is the deposited PR description and whose author and committer
206
+ come from the repository's ordinary Git identity. If Git cannot resolve that
207
+ identity, the PR remains open with actionable `user.name` / `user.email`
208
+ guidance and later dispatcher ticks retry. Without consent it remains open for
209
+ ordinary `git diff` inspection; it has no review UI or image host.
210
+
211
+ ## `autobuild.toml` reference
212
+
213
+ One declarative file at the repo root. **Declarative, not executable** —
214
+ commands are plain shell strings that the kernel runs; nothing in this file is
215
+ evaluated as config logic.
216
+
217
+ **Strictness:** unknown top-level keys/tables and unknown keys inside known
218
+ tables are **errors**, not warnings — a typo must not silently disable a
219
+ verifier. The open maps are `[commands]`, `[roles]`, `[workspace.config]`,
220
+ `[workspace.config.runtimeProvisioning]`, `[verify.<step>]`, and
221
+ `[finalize.<step>]`. Autobuild strictly validates the
222
+ repository-defined command, role, and step entries. `[workspace.config]` is
223
+ plugin-owned and passed through unchanged for plugins; the builtin `git-worktree` provider requires it to be
224
+ empty, while `vercel-sandbox`
225
+ interprets it as a closed typed table.
226
+ Every other known table is closed to unknown keys. The removed `[dashboardFrames]`, `[project]`,
227
+ `[dispatcher]`, `[harvest]`, and `[outer]` tables have no aliases or migration
228
+ shims; they fail as ordinary unknown top-level keys.
229
+
230
+ ### Root scalars
231
+
232
+ Root scalars must appear before the first TOML table; TOML otherwise nests them
233
+ under the preceding table.
234
+
235
+ | Field | Default | Allowed / constraints | Effect |
236
+ |---|---|---|---|
237
+ | `baseBranch` | `"main"` | nonempty string | The branch builds branch from and target with their PR; what `reconcile` merges into the build branch. |
238
+ | `capacity` | `1` | positive integer | Maximum concurrent builds for this repository. |
239
+ | `forge` | `"github"` | nonblank string | Selects shipped `github` or `local-git`, or a plugin-registered Forge adapter. `github` authenticates with `GITHUB_TOKEN`, then `GH_TOKEN`, then the gh CLI's stored login. |
240
+ | `plugins` | `[]` | array of unique nonblank module specifiers | Trusted Bun plugin modules loaded before dispatch, `ab ticket`, and scoped phase wiring. |
241
+
242
+ Repository-path specifiers resolve from the config-bearing root, which is the
243
+ immutable build worktree in scoped phases. npm package specifiers resolve from
244
+ the consuming main checkout's installed dependencies first, then from the
245
+ Autobuild installation the CLI runs from, so an extension installed next to
246
+ the CLI (`bun add -g @defrex/autobuild-<extension>`) loads without being added
247
+ to the repository; a repository copy wins over an installed one, and missing
248
+ packages fail and are never installed automatically. Specifier strings must be
249
+ unique: an exact repeat fails config
250
+ validation before resolution or evaluation, identifies the repeated value and
251
+ both positions, and tells the operator to remove or deduplicate it. Distinct
252
+ specifiers remain separate declarations; if they register the same adapter
253
+ name, the collision error still identifies that adapter and both plugin owners.
254
+ Modules are trusted, in-process Bun code and cannot shadow builtin or previously
255
+ registered names. All four registration maps have production selectors:
256
+ `[tickets].source`, the root `forge` scalar, `[workspace].provider`,
257
+ and `[roles.*].runtime`. BuildStore uses the remote HTTP protocol, not this
258
+ manifest, and TelemetrySource has no registration map.
259
+
260
+ For the complete author workflow—from choosing one of the four open ports,
261
+ through the strict versioned manifest and environment-only secrets, to the
262
+ unchanged shared contract suite and optional npm publication—read
263
+ [`references/plugin-authoring.md`](references/plugin-authoring.md).
264
+
265
+ ### `[pr]`
266
+
267
+ Optional PR-projection settings. Omission leaves artifact attachments text-only.
268
+
269
+ | Field | Default | Allowed / constraints | Effect |
270
+ |---|---|---|---|
271
+ | `imageHost` | — | optional `[pr.imageHost]` table | Opts attached images into temporary public hosting and inline PR rendering. |
272
+
273
+ ### `[pr.imageHost]`
274
+
275
+ Optional and **off by default**. Any agent session may explicitly designate an
276
+ exact deposited artifact with `ab artifact put <kind> <file> --attach`. Every
277
+ attachment receives a pinned BuildStore download command in the PR summary.
278
+ When this table is configured, only attachments whose normalized media type is
279
+ `image/*` are copied to an existing public GitHub release and rendered inline;
280
+ non-images remain text-download-only. The release must already be published and
281
+ mutable; Autobuild creates no release or tag. A private source repository must
282
+ name a separate public asset repository because GitHub's image proxy cannot
283
+ fetch authenticated assets. This is an explicit temporary-public-disclosure
284
+ opt-in.
285
+
286
+ | Field | Default | Allowed / constraints | Effect |
287
+ |---|---|---|---|
288
+ | `provider` | — | required literal `"github-release"` | Selects the only image host implemented in this release. |
289
+ | `repository` | — | required nonblank `"owner/repo"` pair | Public repository that owns the pre-existing release. |
290
+ | `releaseId` | — | required positive integer | Numeric GitHub release id used for upload and deletion. |
291
+
292
+ The `gh` identity running Autobuild needs Contents write permission on the host
293
+ repository. Obtain an existing release's numeric id with, for example,
294
+ `gh api repos/owner/repo/releases/tags/<tag> --jq .id`. Upload, validation, or
295
+ timeout failure records a follow-up observation and preserves every exact text
296
+ retrieval command; it never changes verification or blocks finalize. Hosted
297
+ copies are deleted after `build.completed` (404 is already clean), and failed
298
+ deletions remain durable and retry on later dispatcher ticks. Their inline URLs
299
+ therefore intentionally stop working after the review window. The authoritative
300
+ BuildStore artifacts remain queryable under the store's separate retention
301
+ policy.
302
+
303
+ ### `[workspace]`
304
+
305
+ Optional and backward-compatible: omission selects the builtin `git-worktree`
306
+ provider. The selector envelope is strict; only `[workspace.config]` is an open,
307
+ plugin-owned table.
308
+
309
+ | Field | Default | Allowed / constraints | Effect |
310
+ |---|---|---|---|
311
+ | `provider` | `"git-worktree"` | nonblank builtin or plugin-registered name | Selects the one provider used by dispatch, recovery, PR polling, and cleanup. |
312
+ | `config` | `{}` | open nested table; must be empty for `git-worktree` | Passed unchanged to the selected plugin factory. |
313
+
314
+ A selected plugin factory receives the nested config, process environment, and
315
+ absolute repository root. Unknown names fail before claims and list all
316
+ available providers. A handle's `path` is absolute in its execution environment,
317
+ `ref` is provider-scoped, and optional `localPath` alone promises dispatcher
318
+ reachability.
319
+
320
+ `provider = "vercel-sandbox"` runs the complete build in a persistent Vercel VM
321
+ and requires an HTTPS hosted Store/scoped token, GitHub HTTPS origin, GitHub
322
+ forge, a dispatcher-side push-capable `GITHUB_TOKEN` or `GH_TOKEN`, and either
323
+ `VERCEL_OIDC_TOKEN` or the `VERCEL_TOKEN`/`VERCEL_TEAM_ID`/
324
+ `VERCEL_PROJECT_ID` tuple. These credentials are validated before claims:
325
+
326
+ ```toml
327
+ [workspace]
328
+ provider = "vercel-sandbox"
329
+ [workspace.config]
330
+ timeoutSeconds = 2700
331
+ image = "vercel/sandbox/universal:latest"
332
+ vcpus = 4
333
+ environmentVariables = ["AI_GATEWAY_API_KEY"]
334
+ # private repository only; dedicated contents-read/no-write identity:
335
+ gitUsernameEnv = "AB_GIT_READ_USER"
336
+ gitPasswordEnv = "AB_GIT_READ_TOKEN"
337
+
338
+ [workspace.config.runtimeProvisioning.pi]
339
+ install = "npm install --global --ignore-scripts @earendil-works/pi-coding-agent@0.84.4"
340
+ preflight = "test \"$(pi --version)\" = \"0.84.4\""
341
+ ```
342
+
343
+ `timeoutSeconds` is required (60–86400; Hobby currently caps at 2700), `vcpus`
344
+ is 1–32, and optional `region`/unique `failoverRegions` select placement. Only
345
+ the named runtime variables and scoped `AB_STORE`/`AB_TOKEN` enter guest
346
+ commands. Never list Store, Forge, Vercel, or private-clone credentials there.
347
+ Every effective primary/alternate runtime needs a strict
348
+ `runtimeProvisioning` entry with pinned `install` and exact-version `preflight`
349
+ commands; runtime names remain open for plugins. Install/preflight runs before
350
+ the marker in every fresh/replacement/readiness sandbox, and preflight reruns
351
+ before every child. Failures name the runtime, role/alternate, and field and
352
+ start no agent. Keep runtime installation out of `commands.setup`. Pi over
353
+ `vercel-ai-gateway/...` uses `AI_GATEWAY_API_KEY`; never copy OAuth tokens,
354
+ `~/.pi`, or interactive login state. The clone identity is upload-pack-only and
355
+ scrubbed from Git config before an agent starts. Agents cannot push. Remote phase terminals deposit a durable
356
+ publication request and park; that successful `ab done` is the one terminal and
357
+ must not be repeated. After VM stop and lease release, the local kernel
358
+ publishes the exact SHA/branch through a temporary narrow firewall transform,
359
+ verifies it, and performs PR work locally. Ordinary completion deletes the VM.
360
+ Branch config, installed skills, relative/package plugins, setup, and phase CLI
361
+ commands resolve from the guest checkout, never dispatcher paths.
362
+
363
+ ### `[commands]`
364
+
365
+ An **open map** of name → shell string. Both the key and the value must be
366
+ nonempty strings. Keys are user-chosen: `setup`, `lint`, `type-check`, and
367
+ `test` are *conventions*, not required keys, and a repo may define any verb it
368
+ likes.
369
+
370
+ | Field | Default | Allowed / constraints | Effect |
371
+ |---|---|---|---|
372
+ | `<name>` | — | nonempty string → nonempty shell string | Names a deterministic verb the kernel may run. Referenced by name from check steps in `[verify.<step>]` or `[finalize.<step>]`. |
373
+
374
+ `setup` is special by convention: it runs after workspace provision and after a
375
+ sandbox rehydrate. Failures are durable, remain visible in build status and the
376
+ dashboard, and retry up to `[policy].maxSetupAttempts`; exhaustion raises a
377
+ setup-targeted escalation without manufacturing a pipeline phase or verdict. A
378
+ human answer re-arms the budget, and a later successful `runner.attached` clears
379
+ the current error. Values are never evaluated as config — they are handed to a
380
+ shell as written.
381
+
382
+ Fresh init config deliberately leaves this map empty. The setup agent follows
383
+ the installed [repository setup reference](references/setup.md), derives commands
384
+ from the repository's actual toolchain, and asks the user about choices source
385
+ cannot answer; deterministic init code never guesses a language or package
386
+ manager.
387
+
388
+ ### `[verify]`
389
+
390
+ `steps` orders the verify phases; each entry `"<step>"` becomes a
391
+ `verify:<step>` phase and needs its own `[verify.<step>]` subtable. Those
392
+ subtables are part of this section — their fields are listed here.
393
+
394
+ | Field | Default | Allowed / constraints | Effect |
395
+ |---|---|---|---|
396
+ | `steps` | `[]` | array of nonempty strings | Ordered configured universe of verify phases. Each name must have a matching `[verify.<step>]` table. |
397
+ | `kind` | — | **required**, `"check"` \| `"agent"` | Discriminator. `check` is deterministic (command + pass/fail, never an agent); `agent` runs a skill that returns `pass`, `fail`, or `skip`. |
398
+ | `command` | — | **required when `kind = "check"`**, nonempty string | Ref into `[commands]` — the key, not a shell string. Pass/fail is the command's exit status. |
399
+ | `skill` | — | **required when `kind = "agent"`**, nonempty string | Installed skill name to run (e.g. a repository-authored `"verify-app-e2e"`). |
400
+ | `paths` | — | optional nonempty array of positive repository-relative globs | Makes either step kind apply when any changed path matches any selector. Omitted means unconditional. |
401
+ | `always` | — | optional boolean | `true` makes the step unconditional and mandatory (a plan cannot deselect it); `false` is equivalent to omission. |
402
+
403
+ A plan may begin with strict TOML front matter selecting the complete set of
404
+ optional verification it warrants:
405
+
406
+ ```toml
407
+ +++
408
+ verifySteps = ["types", "e2e"]
409
+ +++
410
+ ```
411
+
412
+ Names must already exist in `[verify].steps` with matching tables; duplicates,
413
+ blanks, unknown names, malformed metadata, and omission of an `always = true`
414
+ step make the planner's `ab done` fail before `plan.completed` is appended.
415
+
416
+ An agent verify step selects `[roles.<step>]` by its **logical step name** —
417
+ the name in `[verify].steps`, not the skill it runs. An agent finalize
418
+ post-step follows the same rule.
419
+
420
+ ```toml
421
+ [verify]
422
+ steps = ["e2e"]
423
+
424
+ [verify.e2e]
425
+ kind = "agent"
426
+ skill = "verify-app-e2e" # repository-authored skill
427
+
428
+ [roles.e2e] # the STEP name — not "verify-app-e2e"
429
+ runtime = "pi"
430
+ model = "openai-codex/gpt-5.6-sol"
431
+ ```
432
+
433
+ The step's configured skill name remains a deprecated alias for existing
434
+ configurations and will be removed in a future release. It applies only when
435
+ `[roles.<step>]` is undeclared, so the step name always wins when both are
436
+ present, and `ab dispatch` reports the alias with the step name to rename it to.
437
+ Written order never reorders execution: the event records the canonical config
438
+ order. No front matter means all configured steps, preserving old plans and
439
+ logs; an explicit empty list is valid only when all steps are optional. The
440
+ selection from the exact plan completion approved by `plan-review` is
441
+ authoritative. A spec restart replaces it with the newly approved plan; a
442
+ reconcile reuses it.
443
+
444
+ Cross-field rules the validator actually enforces — each is an **error**:
445
+
446
+ - A step listed in `steps` with no `[verify.<step>]` table.
447
+ - A `[verify.<step>]` table whose step is not listed in `steps` (add it to
448
+ `steps` or remove the table — a defined-but-unlisted step never runs, so this
449
+ is never silently tolerated).
450
+ - `command` naming a key that does not exist in `[commands]`.
451
+ - An empty or malformed `paths` list. Matching is case-sensitive over Git's
452
+ `/`-separated paths; supported syntax is literals, `*`, `?`, and `**` only as
453
+ a whole segment. Absolute/traversing/empty segments, negation, escapes,
454
+ character classes, brace expansion, extglobs, and malformed `**` are errors.
455
+ `always = true` does not hide selector errors.
456
+
457
+ For each step, the kernel evaluates the approved plan selection first, then
458
+ `paths`: both must include the step for it to run. A selection exclusion records
459
+ `excluded by approved plan selection (plan@<rev>): verify step "<step>" was not selected`.
460
+ A selected step's `paths` are evaluated immediately before the step against
461
+ current `HEAD`, relative to the initial branch-cut SHA or the refreshed base
462
+ from the latest completed reconcile. It uses a NUL-delimited, no-rename Git diff, so
463
+ adds/modifications/deletions and both rename sides participate. A reconcile
464
+ starts a fresh cycle and evaluation repeats; upstream-only merged paths are
465
+ excluded while build-owned resolutions remain visible. A miss starts no
466
+ command, agent, or session and records `skipped` with
467
+ `excluded by [verify.<step>].paths: no changed path matched <JSON paths>`.
468
+ Git/base failures fail closed as infrastructure rather than becoming skips.
469
+
470
+ Agent verifiers may also use `ab verdict pass`,
471
+ `ab verdict fail --report <file>`, or `ab verdict skip --reason <text>`. The
472
+ skip reason is required and
473
+ must be non-blank; no failure report is required. Either kind of skip satisfies
474
+ that step for the current cycle and advances, but it is not a pass and never
475
+ masks another step's failure. Only failures return the build to `implement` and
476
+ consume `[policy].maxVerifyAttempts`; skips retain the cycle's attempt number
477
+ without consuming that budget.
478
+
479
+ ### `[finalize]`
480
+
481
+ `steps` orders the post-PR actions; every name requires a matching
482
+ `[finalize.<step>]` table. Finalize has its own narrow union: verify-only
483
+ `paths` and `always` fields are errors.
484
+
485
+ | Field | Default | Allowed / constraints | Effect |
486
+ |---|---|---|---|
487
+ | `steps` | `[]` | array of nonempty step names | Ordered post-steps that run after the PR is opened. Each name must have a matching table. |
488
+ | `kind` | — | **required**, `"check"` \| `"agent"` | Selects deterministic command execution or an agent skill. |
489
+ | `command` | — | **required when `kind = "check"`**, nonempty string | Ref into `[commands]`. The kernel runs the resolved shell command in the workspace with no agent session. |
490
+ | `skill` | — | **required when `kind = "agent"`**, nonempty string | Exact installed skill name passed to the runtime; no implicit `ab-` prefix is added. |
491
+
492
+ ```toml
493
+ [finalize]
494
+ steps = ["publish", "release-notes"]
495
+
496
+ [finalize.publish]
497
+ kind = "check"
498
+ command = "publish"
499
+
500
+ [finalize.release-notes]
501
+ kind = "agent"
502
+ skill = "ab-release-notes"
503
+ ```
504
+
505
+ The logical step name selects `[roles.<step>]` for agent routing — the same
506
+ rule an agent verify step follows, so one convention covers both:
507
+
508
+ ```toml
509
+ [verify]
510
+ steps = ["e2e"]
511
+
512
+ [verify.e2e]
513
+ kind = "agent"
514
+ skill = "verify-app-e2e" # repository-authored skill
515
+
516
+ [finalize]
517
+ steps = ["release-notes"]
518
+
519
+ [finalize.release-notes]
520
+ kind = "agent"
521
+ skill = "ab-release-notes"
522
+
523
+ [roles.e2e] # verify STEP name — not "verify-app-e2e"
524
+ runtime = "pi"
525
+
526
+ [roles.release-notes] # finalize step name
527
+ runtime = "pi"
528
+ ```
529
+
530
+ For an agent *verify* step, the step's configured skill name remains a
531
+ deprecated alias for existing configurations and will be removed in a future
532
+ release. Finalize has never had such an alias.
533
+
534
+ Checks
535
+ write their completion and any follow-up with kernel attribution; agents retain
536
+ their session actor and transcript. Nonzero command exits, execution/launch
537
+ exceptions, and structured agent failures all produce `ok = false` plus a
538
+ follow-up observation. Either `ok = true` or `ok = false` drains the step, so
539
+ neither kind can turn an otherwise green build red.
540
+
541
+ Validation rejects a listed name with no table, a table whose name is not in
542
+ `steps`, a check command absent from `[commands]`, an unknown kind, or any
543
+ field outside the selected strict variant. Diagnostics name the logical step.
544
+
545
+ A post-step that produces repository content must select and commit its files
546
+ locally and leave a clean worktree. The runner proves the last published head
547
+ is an ancestor and performs the regular, non-force push through the Forge port;
548
+ the agent never pushes. An unchanged `HEAD` creates and pushes no commit. Dirty
549
+ output, rewritten history, Git errors, and publication failures record the
550
+ step as failed and file an observation while the green build continues.
551
+
552
+ ### `[roles]`
553
+
554
+ An **open map** of role name → `{ runtime?, model?, args?, extensions?, sessionBudgetSeconds?, alternates? }`.
555
+ The three primary axes, session budget, and ordered alternate list inherit independently. The reserved `default` role must explicitly name a
556
+ runtime, is the raw inheritance base for every other role, and is **never
557
+ dispatched as a phase**. Its absence fails eager resolution before a session,
558
+ with a copyable fix and all registered runtime names. Three runtimes ship:
559
+ **`claude`** (local Claude Code CLI), **`codex`** (local Codex CLI; unqualified
560
+ `gpt-*` ids), and **`pi`** (local Pi CLI 0.84.3 or newer; provider-qualified ids
561
+ such as the independent `openai-codex/gpt-5.6-sol` route). Pi runs headlessly
562
+ through RPC and uses that local installation's login and model catalog;
563
+ Autobuild does not install it. `ab models [query]` lists the local catalog, and
564
+ `ab models --available` filters it to configured credentials. Claude and Codex
565
+ delegate an omitted model to their CLI default; Pi declares `kimi-coding/k3`.
566
+ Trusted plugins may register additional runtime names;
567
+ they use the same role inheritance, default-model compatibility validation,
568
+ session event attribution, and optional one-shot capability path as builtins.
569
+
570
+ The pipeline resolves `plan`, `plan-review`, `implement`, `code-review`,
571
+ `finalize`, and `reconcile`, plus each **agent** verify and finalize step by
572
+ its logical step name. A verify step's configured skill name remains a
573
+ deprecated alias for existing configurations and will be removed in a future
574
+ release. The repository workflow resolves `harvest` and `harvest-review`. Two
575
+ non-phase judgments use the same runtime/model/args resolver: `slug` optionally
576
+ proposes pre-build naming, and `upgrade` proposes a resolution only when a
577
+ vendored skill merge conflicts. Both are tool-free one-shots and receive the
578
+ role's effective `args`. Missing slug capability uses the deterministic title fallback;
579
+ unavailable upgrade judgment retains the conflict for manual resolution.
580
+
581
+ A declared key that none of those routes requests is resolved and validated
582
+ like any other, and then never used — `ab dispatch` reports it at startup,
583
+ naming the key and the keys valid for this configuration, and reports a
584
+ deprecated skill-name key with the step name to rename it to. Both stay
585
+ warnings; neither blocks a session or changes which runtime and model run. A
586
+ `kind = "check"` step starts no session and so is not a route: naming a role
587
+ after one does not make it consumed. It is reported only when that check step is
588
+ its sole apparent route — a check step named `plan` leaves `[roles.plan]`
589
+ consumed by the core `plan` phase, and nothing is reported.
590
+
591
+ | Field | Default | Allowed / constraints | Effect |
592
+ |---|---|---|---|
593
+ | `runtime` | — | required on `default`; otherwise optional, nonempty string | Runtime for this role. A child that omits it inherits `[roles.default].runtime`. Must name a registered runtime. |
594
+ | `model` | — | optional, nonempty string | Model for this role. A role that omits it inherits `[roles.default].model`; only when neither names a model does the merged runtime supply its own default. |
595
+ | `args` | `[]` | optional, array of nonempty strings | Ordered extra CLI argv tokens for every phase and tool-free one-shot invocation. Omitted ⇒ inherit `[roles.default].args`; a set list, including `[]`, replaces it wholesale. Each string is one token, without shell parsing. Only model and parsed protocol/output-mode spellings are rejected eagerly. |
596
+ | `extensions` | no effect | deprecated optional array of nonempty strings | Compatibility-only field. Dispatch warns for each declaring role and directs migration to `args`. |
597
+ | `sessionBudgetSeconds` | policy fallback | optional positive integer | Wall-clock budget for each build phase session on this logical role. Omitted ⇒ inherit `[roles.default].sessionBudgetSeconds`; absent there too ⇒ `[policy].sessionBudgetSeconds`. One value covers the primary and every alternate. |
598
+ | `alternates` | `[]` | optional ordered array of strict `{ runtime?, model?, args?, extensions? }` entries | Failure-triggered targets. Omitted ⇒ inherit `[roles.default].alternates`; a role list, including `[]`, replaces it wholesale. Each entry overlays that role's effective primary axes and is eagerly validated. `extensions` remains only the deprecated no-op. |
599
+
600
+ Inheritance is mechanical and **independent per field**: merge each configured
601
+ role over the raw `default` entry, then validate the exact runtime/model pair.
602
+ The named runtime must serve the named model. A model-only role never searches
603
+ for another runtime, and an incompatible inherited model is never replaced by
604
+ the selected runtime's default. The one implicit fill-in is benign: if neither
605
+ the phase role nor `default` names a model, the merged runtime uses its own
606
+ default model. Compatibility failures name the role, runtime, model, and served
607
+ model families; all problems in `default` and every declared role are
608
+ aggregated by one eager resolver construction before judgment runs. Runtime
609
+ adapters reject only the model and parsed protocol/output-mode options they
610
+ emit: Claude rejects `-p`/`--print`, `--output-format`, and `--model`; Codex
611
+ rejects `--json` and `--model`/`-m`; Pi rejects `--mode` and `--model`. Exact
612
+ spellings, `--option=value` forms, and documented compact short aliases join
613
+ that same role-naming aggregate. Every other option passes through without
614
+ Autobuild validation, including sandbox, approval, session, tool, skill,
615
+ thinking, extension, lifecycle, and unknown options, even when it supplements
616
+ another adapter-emitted occurrence. Pi always
617
+ disables ambient extension discovery and explicitly loads the Autobuild bridge;
618
+ `--extension <path>` in `args` loads an explicit extension alongside it and
619
+ activates all tools that extension registers. With no such args, Pi loads no
620
+ operator-installed extensions. Effective args are recorded on session-start
621
+ facts. Dispatch
622
+ constructs it before any build launches; upgrade constructs it lazily on the
623
+ first conflict and converts a construction failure to the safe `conflicted`
624
+ outcome.
625
+
626
+ Each attempt starts from the primary. Overload, rate-limit, 5xx, timeout,
627
+ transport, unknown provider failures, and quota/usage/billing exhaustion try
628
+ alternates in order without consuming extra phase attempts. Authentication,
629
+ permission, and local runtime-configuration failures do not. Every target has
630
+ its own session/transcript; a continuation fallback starts fresh from durable
631
+ context. Selection is not sticky, so the next phase, continuation, or retry
632
+ starts from the primary. Only chain exhaustion writes the phase failure, and
633
+ the final failure controls retry behavior. Substitution starts and exhausted
634
+ failures retain the targets and verbatim errors durably. Core phases, agent
635
+ verify/finalize, and Harvest use the chain; tool-free one-shots do not.
636
+
637
+ A build phase session that reaches its captured budget is aborted and ended
638
+ best-effort. The kernel records retryable `phase.failed` with `phase session
639
+ budget expired after <seconds> seconds`, retries from the primary under the
640
+ existing phase-attempt cap, and raises an answerable policy escalation when
641
+ exhausted. Kernel expiry never selects an alternate. Operator cancellation does
642
+ not disable the captured deadline: cooperative adapters return promptly, while
643
+ the deadline releases a turn that ignores cancellation. If operator abort
644
+ arrived first, that release remains an abort control outcome with no alternate
645
+ or `phase.failed`. A typed terminal racing either boundary remains authoritative.
646
+ Agent finalize post-step expiry is failure-tolerant; Harvest and direct check
647
+ commands are outside this budget.
648
+
649
+ Mixing models across roles is **intentional**, not an inconsistency to clean
650
+ up: a reviewer that differs from the implementer catches more. The removed
651
+ legacy `[agent]` table is not an alias: config loading rejects it and directs
652
+ users to move its fields to `[roles.default]`.
653
+
654
+ ### `[policy]`
655
+
656
+ Every field is a **positive integer** except `harvestMaxDrift`, which is a
657
+ nonnegative integer so zero can disable that trigger.
658
+
659
+ | Field | Default | Allowed / constraints | Effect |
660
+ |---|---|---|---|
661
+ | `sessionBudgetSeconds` | `3600` | positive integer | Wall-clock bound for each build agent session unless its logical role overrides it. |
662
+ | `stallRounds` | `3` | positive integer | The same finding surviving this many review rounds auto-escalates to a human — the anti-loop guard. |
663
+ | `maxVerifyAttempts` | `3` | positive integer | Caps the `verify → implement → verify` cycle before escalation. |
664
+ | `maxSetupAttempts` | `3` | positive integer | Caps consecutive `[commands].setup` failures before a setup-targeted human escalation. |
665
+ | `maxInfrastructureAttempts` | `3` | positive integer | Caps consecutive remote provider lifecycle failures before a setup-targeted human escalation; a `retry` answer re-arms the budget. |
666
+ | `maxReconcileAttempts` | `3` | positive integer | Caps completed reconciles that leave the PR conflicted against an unchanged authoritative base. Moving-base races do not consume the budget. |
667
+ | `maxReviewRounds` | `6` | positive integer | Default `maxRounds` for the `plan ⇄ plan-review` and `implement ⇄ code-review` convergence loops. When that cap parks a build, `ab answer <slug> --review-round-ceiling <n>` may replace it for only the escalation's loop and current spec revision. |
668
+ | `harvestThreshold` | `5` | positive integer | Newly unclaimed `observation.recorded` occurrences required to start one repository harvest run. |
669
+ | `harvestMaxDrift` | `3` | nonnegative integer | Other builds merged after the oldest unclaimed observation required to start a run; `0` disables drift. |
670
+
671
+ For each repeat conflict, Autobuild compares the base merged by the latest
672
+ completed reconcile with a fresh authoritative base snapshot. An advanced base
673
+ proves the attempt lost a race and permits another reconcile regardless of the
674
+ attempt high-water. An unchanged base consumes `maxReconcileAttempts` and
675
+ escalates when the budget is exhausted. Every current policy escalation carries
676
+ a closed `policyCause`; reconcile no-progress uses `reconcile-no-progress`,
677
+ while `round` remains occurrence scope. Answering a runner-failure cause re-arms
678
+ only its matching phase and round. The phase-level `verify-failure-limit` cause
679
+ explicitly re-arms all rounds for its verify target, while
680
+ `reconcile-no-progress` and the setup-only cause reset no phase-runner failures.
681
+ The closed cause map forces every future policy condition to choose one of these
682
+ semantics instead of inheriting behavior from a missing `round`. Runner failures
683
+ while checking progress or refreshing the base therefore remain counted after a
684
+ roundless no-progress answer. An already exhausted no-progress budget raises its
685
+ matching cause separately before another reconcile. Cause-less historical
686
+ raises retain their former round-shaped reset behavior, and roundless
687
+ policy/reconcile raises retain their former no-progress meaning, on replay
688
+ without migration. Post-reconcile verification still runs in full and remains
689
+ independently bounded by `maxVerifyAttempts`.
690
+
691
+ `stallRounds` counts *persistence chains*, which reviewers mark and the kernel
692
+ only follows. A finding's `persists` ids name prior-round findings whose defect
693
+ is still present in the work under review; a new instance of a defect class
694
+ whose reported instance was fixed is fresh work and starts its own chain,
695
+ however closely it resembles its predecessor. So the counter measures a
696
+ producer/reviewer stalemate rather than a defect category the loop keeps
697
+ converging on, and the kernel decides only whether a marked chain has survived
698
+ the threshold — never whether two findings are the same disagreement.
699
+
700
+ Harvest is driven by pressure inside `ab dispatch`, not by a wall clock, and
701
+ remains independent of build `capacity` and repository intake. Count reaching
702
+ `harvestThreshold` or drift reaching `harvestMaxDrift` starts a run. Drift is
703
+ measured from the oldest unclaimed observation and counts distinct later
704
+ same-repository merges from other builds; aborted and closed-unmerged builds do
705
+ not count. Once either limit is reached, the run claims the whole current
706
+ accumulation. Dispatch
707
+ tracks it without blocking watch ticks; a repository lease provides
708
+ cross-process single-flight. Recovery has its fixed per-run two-attempt budget,
709
+ separate from the configurable policy fields above.
710
+
711
+ ### `[tickets]`
712
+
713
+ Names the TicketSource and owns its readiness/lifecycle state vocabulary.
714
+ Declarative only. The table is required: `readyState` has no default, and an
715
+ absent table fails clearly at `tickets.readyState` rather than making every
716
+ state eligible.
717
+
718
+ | Field | Default | Allowed / constraints | Effect |
719
+ |---|---|---|---|
720
+ | `source` | — | **required**, nonblank builtin or plugin registration name | Which provider backs all ticket operations. Unknown names report every available builtin and plugin source. |
721
+ | `readyLabels` | — (source-aware) | optional; array of nonempty strings | A ticket must carry **every** listed label to be dispatchable. `[]` = **no label gate**. Absent uses the source default below. |
722
+ | `readyState` | — | **required**, non-blank string | The one workflow state a ticket must sit in to be dispatchable. Linear matches exactly and case-sensitively; file canonicalizes it to a state directory (`ready` → `ready/`). There is no any-state mode. |
723
+ | `teamKey` | — | required by `linear`, forbidden by `file`, allowed for plugins | The Linear team key (e.g. `"ENG"`) or an existing plugin configuration field. |
724
+ | `claimedState` | — | optional nonempty string; forbidden by `file`, allowed for plugins | Workflow state `claim()` moves a ticket to when a build starts. |
725
+ | `createState` | — | optional, nonempty string | Default state for new tickets when a create does not name one. Absent = the provider's default (Linear: the team's default, e.g. Backlog; file: Triage). |
726
+ | `triageState` | — | optional, nonempty string | State the dispatcher hands tickets back to for human triage — spec-gate bounces, aborted builds, closed-unmerged PRs. Absent = Linear: Backlog; file/plugin: Triage. Must name a state the tracker actually has — a Linear team only has "Triage" when its triage feature is enabled. |
727
+ | `proposalState` | — | optional, nonempty string | State observation harvest files approved proposals into. Absent = the resolved `triageState`, which keeps the human grooming gate. Setting it to `readyState` waives that gate: harvested proposals dispatch without being read. Keep it distinct from `triageState` — see below. |
728
+ | `dir` | file: `.autobuild/tickets`; plugin: — | optional nonempty string; forbidden by `linear`, allowed for plugins | Root holding file state directories, or an existing plugin configuration field. |
729
+
730
+ `readyLabels` is the only source-aware readiness default. Dispatch resolves it
731
+ as follows:
732
+
733
+ | `[tickets].source` | `readyLabels` absent |
734
+ |---|---|
735
+ | `"linear"` | `["autobuild"]` — the historical label narrowing |
736
+ | `"file"` | `[]` — no label narrowing; `readyState` selects the directory |
737
+ | plugin | `[]` — no host-imposed label convention |
738
+
739
+ An explicit `readyLabels` value always wins for every source.
740
+
741
+ `proposalState` is the one supported way to waive the grooming gate, and it is
742
+ deliberately not `triageState`. Do not suggest pointing `triageState` at the
743
+ ready state to get auto-dispatched proposals: `triageState` is also where a
744
+ spec-gate bounce lands, so a nonconforming ticket would be claimed, bounced,
745
+ and reclaimed on every tick. `proposalState` moves only what harvest files.
746
+
747
+ Cross-field rules, each an **error**:
748
+
749
+ - `source = "linear"` without `teamKey`; or with `dir` set.
750
+ - `source = "file"` with `teamKey` or `claimedState` set. `dir` is **optional**
751
+ here — absent means the default above, which is why the schema leaves it
752
+ optional rather than giving it a `.default()`: that is what keeps the
753
+ linear-only rule above meaningful and lets the factory tell a defaulted `dir`
754
+ from an explicit one.
755
+ - Every other name is plugin-selected after loading. The factory receives the
756
+ existing ticket table, environment, and absolute repository root; no untyped
757
+ plugin-options table is added. The selected dispatch instance serves
758
+ readiness, dependencies, claim, comments/transitions, harvest creation, and
759
+ janitor completion. Every source-agnostic `ab ticket` command loads and uses
760
+ the same configured registration.
761
+
762
+ The file tracker is **directory-per-state**: `<dir>/<state>/<id>.md` over
763
+ `triage/ ready/ doing/ done/`. The directory *is* the state, so a transition or
764
+ claim is a rename — frontmatter carries no `state`/`claimedBy`, and a ticket
765
+ body survives byte-exactly because a move never rewrites the file. A malformed
766
+ record in any lifecycle directory is diagnosed with its path and validation
767
+ problem, left untouched, and excluded from listing/claim so unrelated valid
768
+ ready work can continue. `ab ticket list` sends those diagnostics to stderr and
769
+ keeps JSON stdout as one bare valid-ticket array. Tracker-wide safety checks
770
+ remain fatal: the same id in two state dirs is a loud error naming both paths,
771
+ as are root-level ticket files with no state and filesystem failures. When
772
+ `dir` is defaulted, the backlog writes its own `.gitignore` of `*`, so git never sees it; an
773
+ explicit `dir` is the user's and is left alone. Agents and operators drive it
774
+ through the source-agnostic `ab ticket` commands rather than running `mv` by
775
+ hand.
776
+
777
+ **Secrets never live in this file.** `LINEAR_API_KEY` and plugin-declared
778
+ `requiredEnv` credentials are environment variables (a local `.env` works).
779
+ Missing plugin credentials name both the selected source and every missing
780
+ variable. If a user asks you to put an API key in `autobuild.toml`, use the
781
+ environment variable instead and say why.
782
+
783
+ ### `[orchestrator]`
784
+
785
+ Gates the operator-sandbox feature (AUT-340): a persistent, **credential-free**
786
+ environment per operator × repository that an operator agent drives through the
787
+ `sandbox.*` registry tools (`ab mcp`, and every later binding). The whole table
788
+ is **closed to unknown keys** and restart-classified: changing it requires a
789
+ dispatcher restart.
790
+
791
+ | Field | Default | Allowed / constraints | Effect |
792
+ |---|---|---|---|
793
+ | `enabled` | `false` | boolean | Master gate. With the table absent or `enabled = false`, the sandbox tools are absent from every binding and no environment is ever provisioned. |
794
+ | `sandbox` | — | strict subtable | The sandbox behavior knobs; absence reads as the defaults below. |
795
+
796
+ `[orchestrator.sandbox]` fields:
797
+
798
+ | Field | Default | Allowed / constraints | Effect |
799
+ |---|---|---|---|
800
+ | `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. |
801
+ | `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. |
802
+
803
+ The credential-free rule is structural, not advisory: the environment receives
804
+ no store, forge, ticket-provider, or model credential, and naming any of
805
+ `AB_STORE`, `AB_TOKEN`, `GITHUB_TOKEN`, `GH_TOKEN`, `LINEAR_API_KEY`,
806
+ `VERCEL_OIDC_TOKEN`, `VERCEL_TOKEN`, `VERCEL_TEAM_ID`, `VERCEL_PROJECT_ID`, or
807
+ `AI_GATEWAY_API_KEY` in `environmentVariables` is a config error. The typed
808
+ operator tools remain the only route to build state from inside the sandbox.
809
+
810
+ Tool bounds an operator agent should know: `sandbox.exec` waits at most **300
811
+ seconds**; stdout and stderr are each truncated at **65,536 bytes** with a
812
+ `[truncated by autobuild: output exceeded 65536 bytes]` marker; `sandbox.reset`
813
+ and archiving an operator's last session are **destructive** (reset re-provisions
814
+ from the current base head); there is at most **one** environment per operator
815
+ and repository.
816
+
817
+ ## Setup and upgrades
818
+
819
+ **`ab init <target> [--force]`** runs *outside* build sessions — it takes a
820
+ repo path, needs no `AB_*` environment, and is safe to re-run. It:
821
+
822
+ - Probes every registered runtime and reports whether it is usable, including a
823
+ diagnostic reason. Setup launch selection uses the fixed product order
824
+ `claude`, `codex`, then `pi`; it never depends on repository content or a user
825
+ questionnaire.
826
+ - If config is absent, writes a valid stack-neutral skeleton with empty
827
+ `[commands]`, no verify or finalize steps, a valid local ticket gate, and one
828
+ explicit runtime solely as a schema placeholder. It reads no package or
829
+ language manifest and invents no command.
830
+ - Vendors 11 skills before handoff, including the locally editable
831
+ [repository setup reference](references/setup.md) in the installed `ab-guide`
832
+ tree. On an interactive terminal it starts the selected coding-agent CLI in
833
+ the target repository with a short prompt to read that reference and
834
+ propagates that process's exit status. The direct child inherits terminal I/O
835
+ but no ambient `AB_*` identity, and creates no build, session, event,
836
+ transcript, or BuildStore record.
837
+ - Without an interactive terminal or usable shipped runtime, deterministic
838
+ installation still succeeds. Init prints every probe reason and the exact
839
+ setup prompt for the user to run in a coding agent.
840
+ - The setup agent inspects the repository, configures real commands and verify
841
+ steps, independently chooses pipeline role runtimes/models, configures the
842
+ ticket source and environment-only credentials, arranges repository-specific
843
+ end-to-end verification (authoring a repository-owned agent verifier when
844
+ judgment is warranted), and files one groomed dispatchable ticket.
845
+ - It never overwrites config once the file exists, even with `--force`; a rerun
846
+ launches or prints a prompt that asks the agent to review and improve it.
847
+ - Idempotently adds the exact `.autobuild/` rule to the target's `.gitignore`,
848
+ preserving every existing byte/rule and handling a missing trailing newline.
849
+ - **Copies each complete canonical skill directory** to
850
+ `.agents/skills/ab-<name>/` — `SKILL.md` plus references/supporting files,
851
+ not links. Every file is **editable**: per-repo customization is the point.
852
+ Unknown repository-local support files are left alone.
853
+ - Links `.claude/skills/ab-<name>` → the `.agents` directory, so Claude, Codex,
854
+ and Pi discover **one** editable tree rather than diverging copies. If the
855
+ `.claude/skills` and `.agents/skills` roots already resolve to the same
856
+ directory, that repository-level alias satisfies discovery: no per-skill
857
+ link or legacy `.claude` pristine migration is attempted.
858
+ - Records the **pristine** installed tree at
859
+ `.agents/skills/.ab-pristine/ab-<name>/` — repo-versioned, and the per-file
860
+ base for `ab upgrade`'s three-way merges.
861
+ - Rewrites frontmatter on install: `name` → `ab-<name>`, and
862
+ `disable-model-invocation: true` on every skill outside the model-invocable
863
+ set (`ab-spec`, `ab-tickets`, `ab-guide`).
864
+
865
+ The final report states `autobuild.toml: written|skipped`, aggregates all skill
866
+ counts, names only attention-worthy `kept` and `overwritten` skills, and reports
867
+ every runtime probe result. A distinct real `.claude/skills/ab-*` directory is
868
+ not mistaken for an alias: init processes the remaining skills, accumulates all
869
+ such conflicts with move/remove guidance, skips setup, and exits nonzero. The
870
+ interactive path otherwise names the selected setup runtime; the fallback
871
+ prints its manual-run instruction and exact prompt. Non-TTY output is plain
872
+ text.
873
+
874
+ **`ab --version`** reports the installed package version, Bun-recorded commit
875
+ when available, and plugin API version using local distribution metadata only.
876
+ It works outside a repository, needs no config or `AB_*` session, and performs
877
+ no network request.
878
+
879
+ **`ab upgrade <target> [--no-self-update | --version <semver>]`** updates only
880
+ when explicitly invoked. By default it resolves the latest published version on
881
+ the installation's channel — the npm registry for a registry install, or the
882
+ latest full GitHub Release from the repository recorded by a Bun forge install
883
+ (including a fork) — installs it with the matching local/global Bun mechanism,
884
+ then hands off to that replacement binary. The fresh process therefore supplies both skill
885
+ defaults and merge logic. `--version` selects an exact release, including an
886
+ older one; `--no-self-update` merges against the installed distribution. Source
887
+ checkouts are never mutated and still merge installed skills, even with an
888
+ exact selector. For latest, unknown/contradictory provenance and lookup/install
889
+ failures warn and continue. For an exact release, mechanism, resolution, or
890
+ install failure stops before skill merge. No other command checks for or
891
+ installs a release.
892
+
893
+ A local Bun update changes the dependency in its owning project's
894
+ `package.json` and the matching `bun.lock` resolution. An existing global
895
+ install changes Bun's global package-manager state instead. Before either can
896
+ write, upgrade captures the target repository's Git baseline and carries it to
897
+ the replacement binary.
898
+
899
+ After that distribution decision, upgrade three-way merges every distributed
900
+ skill file as *pristine base × local edits × new default*, with a standing bias
901
+ toward **the local customization** — upstream is adopted only where it doesn't
902
+ collide with what the repo deliberately changed. New upstream files are
903
+ delivered; an upstream-removed unedited file is removed, while a customized
904
+ copy becomes a preserved repository-local support file.
905
+ Outcomes:
906
+
907
+ | Outcome | Meaning for the repo's files |
908
+ |---|---|
909
+ | `current` | Local already matches the new default, or the repo's edit stands as-is. Nothing written. |
910
+ | `adopted` | Upstream's version taken; pristine advanced. |
911
+ | `merged` | Clean three-way merge; live file and pristine both advanced. |
912
+ | `resolved` | Merge conflicted; a local-biased agent proposal passed deterministic validation, so that live file was resolved and its pristine base advanced to incoming. |
913
+ | `conflicted` | Resolution was unavailable, failed, declined as ambiguous, or failed validation. Both sides of that file stay **byte-untouched** for a human — **conflict markers are never written into a live skill**. |
914
+ | `installed` | In the distribution but not yet in the repo — installed fresh, like init. |
915
+ | `removed` | A fixed retired distribution skill had pristine provenance and either its unreferenced live tree matched pristine and was removed with its owned discovery link, or its canonical live tree was already missing and obsolete provenance plus any owned dangling link were removed. |
916
+ | `kept` | A fixed retired skill was customized, still configured, or unsafe to remove, so its live copy remains local; or an otherwise removable canonical copy was deleted while a user-owned Claude discovery entry was preserved and remains discoverable. Obsolete pristine ownership is cleared. |
917
+ | `unknown` | An installed `ab-*` skill absent from the distribution. **Left alone** — local skill additions are legitimate. |
918
+
919
+ The `removed`/`kept` classifications apply only to the fixed retirements
920
+ `ab-setup` and `ab-verify-e2e`. A pristine record proves Autobuild provenance;
921
+ a same-named repository-authored skill without one is untouched. Upgrade keeps
922
+ a retired skill named by an agent verify or finalize step, and parses config
923
+ conservatively so an inspection failure also keeps it. A user-owned
924
+ `.claude/skills/<name>` discovery entry — a distinct real directory or foreign
925
+ symlink — is preserved byte-for-byte, enters the structured discovery-conflict
926
+ report, and makes upgrade exit nonzero even when the corresponding
927
+ canonical/pristine trees are retired. A preserved symlink keeps its exact link
928
+ text and target. Every terminal classification clears obsolete pristine
929
+ ownership, making the retirement report one-time and preventing later link
930
+ recreation, resurrection, or re-reporting.
931
+
932
+ After a conflict-free merge, upgrade makes one local commit by default. It
933
+ includes only each reported skill's canonical tree, pristine record, and Claude
934
+ discovery path, plus `package.json` and `bun.lock` when this run's local Bun
935
+ update wrote those files inside the target repository. A global update adds no
936
+ path, and no owned change means no commit. Additions, modifications, links, and
937
+ deletions are included; unrelated staged, unstaged, and untracked work remains
938
+ untouched. The message identifies `ab upgrade` and lists every byte-changing
939
+ skill with its reported outcome. `--no-commit` disables this behavior and is
940
+ preserved through self-update handoff. If a replacement child receives the
941
+ handoff marker without the pre-self-update baseline — possible when an older
942
+ parent launches a newer child — it suppresses the entire automatic commit,
943
+ names the cross-version compatibility reason, and leaves all upgrade-owned
944
+ changes uncommitted for the operator. Skill-upgrade results and the report's
945
+ existing exit code remain unchanged.
946
+
947
+ Any content conflict, discovery conflict, pre-existing dirt in an owned path,
948
+ non-Git target, changed HEAD/worktree identity, or in-progress merge, rebase, or
949
+ cherry-pick likewise suppresses the whole commit with a named warning. If upgrade
950
+ cannot snapshot the worktree's Git index, it warns and declines to stage. A
951
+ staging or commit failure restores that exact pre-attempt index and reports the
952
+ original Git failure without touching merged worktree files. The report's
953
+ existing exit code is unchanged; upgrade never pushes or rewrites history.
954
+
955
+ The agent gets a fixed per-file deadline of at least ten minutes. While it is
956
+ resolving and stdout is interactive, `ab upgrade` continuously redraws one line
957
+ with the skill and file path, elapsed time, and `Ctrl-C to cancel`. Ctrl-C then
958
+ aborts only that agent invocation, records the file as the usual byte-preserving
959
+ `conflicted` outcome, and continues through later files and skills. Ctrl-C when
960
+ no resolution is active retains ordinary process termination. Piped and other
961
+ non-TTY output has no indicator, cursor sequences, or changed bytes. Timeouts
962
+ and cancellations remain content conflicts with exit zero; only Claude
963
+ discovery conflicts make upgrade exit nonzero.
964
+
965
+ The agent output is only an untrusted proposal. Each merge uses unguessable
966
+ labels so marker-looking skill content cannot impersonate that merge's
967
+ structure. `ab upgrade` requires a
968
+ complete file with every region outside the uniquely labelled conflict hunks
969
+ unchanged and in order; `SKILL.md` additionally requires the same namespaced
970
+ frontmatter identity. Standard marker lines are rejected in agent-authored hunk
971
+ gaps but allowed when already protected as exact clean content. Validation
972
+ finishes before either file is written. A rejected proposal remains report-only alongside the marked merge diagnostic;
973
+ the CLI prints the exact pristine path to merge by hand. Distinct real Claude
974
+ skill directories are a separate discovery-conflict class: upgrade processes
975
+ all skills, reports every collision with move/remove guidance, and exits
976
+ nonzero, while content-level `conflicted` outcomes retain their exit-zero
977
+ contract. Local customization survives upgrades, and divergence is visible
978
+ instead of silent.
979
+
980
+ ## Local state and store selection
981
+
982
+ By default, each Git repository owns one self-contained state tree at
983
+ `<main-repo>/.autobuild/`:
984
+
985
+ - `autobuild.sqlite` — build records, event logs, and repository journals.
986
+ - `blobs/` — content-addressed artifacts.
987
+ - `worktrees/` — autobuild-created Git worktrees.
988
+ - `tickets/` — the default location for the `file` ticket source.
989
+
990
+ The main checkout comes from Git's absolute repository/worktree topology, not
991
+ blindly from the current worktree's top level or the parent of its Git common
992
+ directory. Commands run in an autobuild-created linked worktree therefore read
993
+ the main checkout's state and file tickets, while submodules and
994
+ separate-Git-dir checkouts keep distinct roots beneath their own working trees.
995
+ There is no home-directory fallback: a repository with no state tree starts
996
+ empty.
997
+
998
+ Every command uses the same precedence: explicit `--store <ref>` > nonempty
999
+ `AB_STORE` > `<main-repo>/.autobuild`. Relative local overrides are normalized
1000
+ against the main checkout. A local override relocates the whole state tree,
1001
+ including `worktrees/` and default file tickets; an explicit `[tickets].dir`
1002
+ remains repo-relative. An HTTP(S) URL still selects the remote store unchanged,
1003
+ while worktrees and default file tickets remain under the repository-default
1004
+ `.autobuild/` directory. The dispatcher passes its normalized selection to
1005
+ every agent session as `AB_STORE`.
1006
+
1007
+ ## Source-agnostic ticket operations
1008
+
1009
+ The sessionless `ab ticket` namespace constructs whichever TicketSource the
1010
+ repository's `[tickets]` table selects. These forms therefore work unchanged
1011
+ with Linear and the file tracker, without provider-specific API/MCP calls:
1012
+
1013
+ - `ab ticket create <title> --body <file> [--state <state>] [--labels a,b] [--blocked-by id,id] [--json]`
1014
+ files a ticket. Without `--state`, the source uses `[tickets].createState` or
1015
+ its own default. A named state is passed through unchanged and the selected
1016
+ source validates it before creating anything. Blocker ids belong to the same
1017
+ source and are also checked before the single create call.
1018
+ - `ab ticket update <id> [--title <title>] [--body <file>] [--labels a,b] [--json]`
1019
+ partially replaces editable fields. At least one editable flag is required;
1020
+ `--json` alone is not an update.
1021
+ - `ab ticket block <id> <blocker-id[,blocker-id...]> [--json]` adds one or more
1022
+ blocking relationships.
1023
+ - `ab ticket unblock <id> <blocker-id[,blocker-id...]> [--json]` removes one or
1024
+ more blocking relationships.
1025
+ - `ab ticket list [--state <state>] [--labels a,b] [--json]` lists tickets. With
1026
+ no filters it uses exactly dispatch's configured ready state and source-aware
1027
+ default labels. If either filter is present, only explicitly supplied
1028
+ criteria apply; every requested label must match.
1029
+ - `ab ticket show <id> [--json]` reads one complete ticket. Human output includes
1030
+ labeled metadata and the body verbatim, so it can read a stored spec back.
1031
+ - `ab ticket move <id> <state> [--json]` transitions one ticket and reports its
1032
+ post-transition value.
1033
+
1034
+ State names and ids are source-local. For block and unblock, the first id is the
1035
+ ticket being changed and the comma-separated second operand names its blockers.
1036
+ Create validates every `--blocked-by` id before filing. The relationship forms
1037
+ deduplicate their list and validate the target plus every blocker id before
1038
+ writing any edge; block also rejects a direct self-block. An unknown id fails
1039
+ the whole invocation without a partial write. Adding an existing relationship
1040
+ and removing an absent relationship to an existing ticket both succeed as
1041
+ no-ops, so either command is safe to retry.
1042
+
1043
+ Blockers gate a ticket when the dispatcher tries to claim it. Adding a blocker
1044
+ to a ticket that has already been claimed into a build does not stop that build.
1045
+ For dependent tickets, follow the
1046
+ [safe blocker-filing workflow](references/ticket-dependencies.md): determine the
1047
+ intended final destination, stage outside ready, verify all blocker relationships,
1048
+ and move to ready last. Direct and hosted Linear create the issue and its blockers
1049
+ in separate requests; one `create --blocked-by` call is not atomic publication.
1050
+ Harvest's creation reservations cover Harvest only, not interactive CLI creates.
1051
+ Dependency order and JSON id extraction are still required, but do not alone
1052
+ prevent an active dispatcher from claiming a partially created ready ticket.
1053
+
1054
+ Update is partial: omitted fields remain untouched, including labels, assignee,
1055
+ and provider metadata. `--labels` is a complete replacement and an explicitly
1056
+ empty value (`--labels ''`) clears the list. Supplied title/body values must be
1057
+ nonblank. State is intentionally not an update field — `transition()` remains its sole owner —
1058
+ and every validation or unknown-ticket error occurs before
1059
+ mutation and names the offending field or id. `--body` always names a file; a
1060
+ missing file fails before a source write.
1061
+
1062
+ Quote multiword Linear states, for example `"In Progress"`; file states are
1063
+ case-insensitive on input and canonical on output. The adapters validate
1064
+ transitions, so an invalid state fails with the
1065
+ source's known states. A missing id fails nonzero with an error naming both the
1066
+ id and configured source.
1067
+
1068
+ Human-readable output is the default and must not be parsed for ids. Every
1069
+ subcommand accepts `--json`, which emits one bare JSON value and no prose: a
1070
+ `Ticket[]` for `list`, and the complete resulting `Ticket` for `create`,
1071
+ `update`, `block`, `unblock`, `show`, or `move`.
1072
+
1073
+ ## Retrieving build artifacts
1074
+
1075
+ `ab artifact download <build> <kind>[@rev] --output <file> [--store <ref>]`
1076
+ is the read-only binary retrieval form. Operators need no session identity: it
1077
+ works after a build is terminal, writes exact bytes (creating parent
1078
+ directories), verifies that the build belongs to this repository, and uses the
1079
+ standard explicit `--store` > nonblank `AB_STORE` > repository-local selection.
1080
+ Inside a phase, a complete ambient tuple permits only the ambient build; a
1081
+ foreign build or malformed/partial identity is rejected. Use the exact pinned
1082
+ `@rev` from a PR attachment command.
1083
+
1084
+ In a build session, `ab artifact put <kind> <file> --attach` atomically deposits
1085
+ the exact bytes and designates that revision for the PR. A later designation of
1086
+ the same kind replaces it; distinct kinds remain distinct attachments. The
1087
+ ordinary form without `--attach` remains a private BuildStore deposit, and
1088
+ in-session `put|get` still require ambient phase auth.
1089
+
1090
+ ## Dispatch dashboard
1091
+
1092
+ On a TTY, `ab dispatch` renders one fixed interactive frame anchored at the
1093
+ terminal's first row. Its header remains on that top row as frame height changes
1094
+ and after a resize; unused rows remain below. On exit, the final frame is copied
1095
+ to the normal screen and remains in scrollback. Its always-present two-line
1096
+ process-global header has a selectable `Autobuild` summary with the repository
1097
+ basename followed by the compact counters
1098
+ `queue <depth> | active <current>/<limit> | observations <current>/<limit>`.
1099
+ `queue` is the ready-ticket queue depth; `active` is the current
1100
+ nonterminal-build count against root `capacity`; and `observations` is the count
1101
+ of recorded observation occurrences not yet claimed by a Harvest snapshot
1102
+ against `policy.harvestThreshold`. The Store-only interactive frontend refreshes
1103
+ that display value directly from the BuildStore; a failed refresh retains the
1104
+ last successful count, shows a diagnostic, and appends no transport event.
1105
+ Non-interactive dispatch performs no frontend-only sampling, and the separate
1106
+ drift trigger is not displayed. An indented controls line follows for
1107
+ `intake ON`/`intake OFF`, `auto merge ON`/`auto merge OFF`, and `harvest
1108
+ ON`/`harvest OFF`, plus a conditional yellow `repository PAUSED` segment while
1109
+ the durable repository-wide hold is set. The controls start in the title
1110
+ column. From the top of the frame through the body, the two-column marker lane
1111
+ stays empty except for the selected row's `> ` marker. These controls and the
1112
+ pause segment are durable repository projections and converge across
1113
+ dispatchers on the existing poll; harvest specifically reflects its
1114
+ acknowledged gate, not pending intent. Routine tick
1115
+ counts, dependency diagnostics, parked-build notices, harvest outcomes, and
1116
+ action confirmations are suppressed in the interactive frame. The latest true
1117
+ warning or error instead appears on a conditional row below the header, aligned
1118
+ to the title; when none exists, that row consumes no space. A blank line
1119
+ separates the global section from the first body row, and another separates the
1120
+ body from the legend or feedback controls. The duplicate startup banner is
1121
+ suppressed; `--plain` and non-TTY output remain line-oriented and unchanged. A
1122
+ satisfied verify skip carries the literal `skipped` qualifier, so it remains
1123
+ distinct from a pass without color. Every nonterminal build has a row,
1124
+ including `queued` builds that have not attached a runner. A queued row shows
1125
+ its ticket, slug, literal `QUEUED` status, and either the pending dispatch
1126
+ boundary or the latest durable `dispatch.failed` stage, attempt, and error.
1127
+ While the repository hold is set, only queued rows gain a yellow `(held)`
1128
+ modifier beside `QUEUED`; intake being off without that hold shows neither
1129
+ `repository PAUSED` nor `(held)`.
1130
+
1131
+ A build row previews each unresolved blocker and setup error as at most three
1132
+ rendered rows, preserving authored newlines and one blank row between
1133
+ paragraphs. Longer messages add the withheld-row count and name `Enter details`;
1134
+ that existing detail view shows the setup error and every blocker in full.
1135
+ Up/Down scrolls detail content, Left/Right selects a session, and Enter opens the
1136
+ selected transcript. Harvest failure detail and each process warning use the
1137
+ same three-row cap and count but advertise no unavailable expansion action.
1138
+ These are display-only transformations: stored event and status text is
1139
+ unchanged.
1140
+
1141
+ When the selected session carries a stream, Enter instead opens a read-only
1142
+ session view, whether that session is still open or has already ended. For an
1143
+ open session the view follows the stream live: the turn's prompt, the agent's
1144
+ reasoning and answer text, each tool call with a one-line input summary, tool
1145
+ output capped at eight rows (longer output names the withheld-row count), and
1146
+ step separators appear as they land, within one dashboard poll interval, while
1147
+ the view tracks the tail. For an ended session the view renders the session's
1148
+ finalized content, including error and truncation markers where the runner
1149
+ emitted them, and a final `Stream closed` line names the outcome. Up/Down
1150
+ scrolls; scrolling up from the bottom pauses tail-following — the legend says
1151
+ `paused` — and scrolling back to the bottom resumes it. The view is strictly
1152
+ read-only: no key inside it answers, pauses, resumes, or aborts anything, the
1153
+ abort confirmation cannot be opened from it, and its legend offers only
1154
+ Up/Down, Esc, and quit. Escape returns to build detail with the same session
1155
+ selected. Sessions without a stream keep the existing messages: the
1156
+ open-session unavailable note, the reclaimed note, the no-deposit note, and the
1157
+ existing transcript rendering.
1158
+
1159
+ Up/Down moves without wrapping through global first, optional `Harvest` second,
1160
+ then slug-sorted builds. Stable discriminated identity preserves selection
1161
+ through repaint, re-sort, and row appearance/disappearance. The legend is
1162
+ contextual: every row offers navigation and quit; global offers `h harvest`,
1163
+ `m auto-merge`, `i intake`, `p pause all`, and `r resume all`; `Harvest` offers `p resume` for ordinary
1164
+ failure or `p acknowledge` for exhaustion/escalation only when that action is
1165
+ available; runner-attached builds offer `m auto-merge` plus the state-specific
1166
+ `p pause`, `p cancel pause`, or `r resume` action, while queued builds offer
1167
+ `d discard`. `m` on `Harvest` is an explanatory build-only no-op.
1168
+
1169
+ A running build reads `PAUSING` after `p` appends its pause request and before
1170
+ the kernel acknowledges it at a safe boundary. During that window `p` appends
1171
+ the opposing resume request, cancels the stale pause, and projects the build
1172
+ back to `RUNNING`. An acknowledged paused build offers only `r resume` and reads
1173
+ `RESUMING` until the kernel acknowledges that request; there is no gesture to
1174
+ cancel a pending resume. Blocked takes visual precedence, including when the
1175
+ build is also operator-paused, and `r` opens the existing feedback field. Keys
1176
+ that are not offered for the current build state are no-ops and append no
1177
+ control event.
1178
+
1179
+ Global-row `p` is the repository-wide version — the same walk `ab pause --all`
1180
+ runs sessionless: it durably holds every `QUEUED` build, turns intake off, and
1181
+ appends `build.pause-requested` to every `RUNNING` build, so the repository
1182
+ comes to rest before a machine, terminal, or dispatcher restart. The hold is the `dispatcher.pause-set` repository fact, written before
1183
+ the intake fact; while it is set no dispatcher tick attaches a runner to a
1184
+ queued build, whether through interrupted-dispatch recovery, startup resume, or
1185
+ the lease sweep. The per-build walk skips `PAUSING` — the bulk control never
1186
+ cancels a pending pause, unlike build-row `p` — and skips `QUEUED`, `PAUSED`,
1187
+ `RESUMING`, and `BLOCKED` without error; queued builds are covered by the hold
1188
+ instead. Global-row `r` reverses all of it: the hold released,
1189
+ `build.resume-requested` on every `PAUSED` build (including builds paused by
1190
+ hand beforehand), and intake back on. A released queued build becomes
1191
+ launchable again on the next ordinary tick, with no further operator action. `r`
1192
+ does not touch `BLOCKED` builds, whose blockers still need the per-build `r`
1193
+ feedback field. Both report the affected build count, the hold, and the new
1194
+ intake value on the notice row. One carve-out remains: the harvest gate is
1195
+ independent, so a harvest session can still start after a bulk pause (`h` stops
1196
+ it). The janitor is likewise ungated, so an abort or discard on a held build is
1197
+ still settled. Build work in this repository is otherwise genuinely at rest.
1198
+
1199
+ `--intake` and `--no-intake` are mutually exclusive durable repository setters.
1200
+ Omitting both reuses stored state, falling back to ON only when no intake fact
1201
+ exists. Global-row `i` re-reads current state and appends the opposite value.
1202
+ Intake off skips only new ticket claims while janitor, stale-runner, harvest,
1203
+ and in-flight work continue — it deliberately does **not** set the repository
1204
+ hold, so a build the repository has already accepted still starts and finishes.
1205
+ Every dispatcher samples the current value on each tick, so a change in one
1206
+ process gates all dispatchers for that repository.
1207
+
1208
+ The repository hold has no launch flag and no setter of its own: `p` and `r` on
1209
+ the dashboard's global row and the `ab pause --all` / `ab resume --all` walk
1210
+ they share are the only ways to set or clear it, and each of those moves intake
1211
+ with it. Each dispatcher frame reads the hold from the repository journal
1212
+ itself, so an already-paused repository shows `repository PAUSED` and `(held)`
1213
+ on its first paint; clearing the hold removes both on the next poll. The hold
1214
+ survives a restart and is not defeated by re-running `ab dispatch`.
1215
+
1216
+ `--auto-merge` and `--no-auto-merge` similarly set the durable repository
1217
+ claim-time default; omission reuses stored state, falling back to OFF only when
1218
+ no fact exists. The forms are mutually exclusive independently of the intake
1219
+ pair. Global-row `m` re-reads current state, appends the opposite value, and
1220
+ posts a dispatcher notice. When on, each fresh dispatcher claim records the
1221
+ existing human-authored `build.auto-merge-requested` fact immediately after
1222
+ `build.created` and before runner launch. The first visible build frame therefore
1223
+ carries `auto merge`. This is a creation-time seed, not policy: changes never
1224
+ touch existing builds, resumed/adopted logs or other creation paths never sample
1225
+ it, and build-row `m` remains independent (a seeded build can be cancelled while
1226
+ the global default stays on). Both repository settings are independently
1227
+ last-write-wins by event sequence. They are stored in the BuildStore, not in
1228
+ `autobuild.toml`; propagation uses polling, not a push channel.
1229
+
1230
+ Global-row `h` re-reads the repository journal and appends the existing human
1231
+ pause/resume request. The newest pending command determines the next target, so
1232
+ rapid presses issue opposing requests; the token changes only after the kernel's
1233
+ `harvest.paused`/`harvest.resumed` acknowledgement.
1234
+
1235
+ `Harvest` uses the same marker, right-aligned status column, and status colors
1236
+ as builds; its internal run id is not shown. One row selects the oldest
1237
+ unresolved ordinary failure, exhaustion, or escalation attention before the
1238
+ oldest open run, so later running/completed work cannot hide it. Completed runs,
1239
+ an idle paused repository, acknowledged recovery exhaustion, and dismissed
1240
+ escalation have no row. A paused open run freezes and reads `PAUSED`. Ordinary
1241
+ failure reads red `FAILED`, marks the stopped step, shows automatic progress,
1242
+ and offers `p resume`. Exhaustion remains red, says `recovery exhausted — human
1243
+ attention required`, shows stopped step/pending count, and offers `p
1244
+ acknowledge`; escalation also offers `p acknowledge`. A request awaiting
1245
+ acknowledgement has no duplicate action. Row `p` never pauses the gate. When the
1246
+ gate is off it writes nothing and directs the operator to global `h`; the
1247
+ eventual shared human acknowledgement reopens every ordinary parked run and
1248
+ settles every exhaustion barrier. Exhaustion and escalation remain terminal.
1249
+ Escalation's row is dismissed only by a post-terminal human resume request
1250
+ followed by its later kernel acknowledgement, never by the request alone.
1251
+
1252
+ A build with auto-merge off has no auto-merge token. Requested, enabled, and
1253
+ cancelling states all read `auto merge`: cyan means requested locally but not
1254
+ yet applied on GitHub, green means native auto-merge is enabled, and yellow
1255
+ means cancellation is in flight. The token disappears when cancellation lands.
1256
+ Merge conflicts recover automatically: the gate defers, the pipeline re-enters
1257
+ reconcile, and verify re-runs before consent is re-examined — the reconcile
1258
+ step on the build row is the only indication, and uncomputed mergeability
1259
+ resolves the same way on a later poll. The auto-merge gate records a durable
1260
+ observation only for a deferral a person must fix (disabled repository
1261
+ auto-merge, a dirty local checkout, a missing Git identity, an unproven gate
1262
+ state, a plan limitation) — never for a conflict the pipeline resolves itself.
1263
+ A recorded deferral stops being shown once the PR merges or closes, or once a
1264
+ reconcile or re-finalize supersedes it, until the gate re-examines consent.
1265
+
1266
+ ### Durable build controls: CLI and dashboard
1267
+
1268
+ The CLI and dashboard are two surfaces over the same store-backed control
1269
+ operations. Use the CLI from a non-TTY, a script, or an assistant; use the
1270
+ keys while watching the interactive dashboard. Both append human-authored facts
1271
+ to the build's event log and apply the same write-time checks.
1272
+
1273
+ | Intent | CLI | Dashboard | Durable event(s) |
1274
+ |---|---|---|---|
1275
+ | Discard interrupted dispatch | — | Select a queued build and press `d`. | `build.discard-requested` |
1276
+ | Pause | `ab pause <slug> [--store <ref>]` | Select a `RUNNING` build and press `p`; press `p` again while `PAUSING` to cancel the pending pause. | `build.pause-requested`; cancellation reuses `build.resume-requested` |
1277
+ | Resume | `ab resume <slug> [--store <ref>]` | Select a `PAUSED` build and press `r`. | `build.resume-requested` |
1278
+ | Pause all | `ab pause --all [--store <ref>] [--json]` | Select the global row and press `p`. | `dispatcher.pause-set` (first), `dispatcher.intake-set`, then one `build.pause-requested` per `RUNNING` build |
1279
+ | Resume all | `ab resume --all [--store <ref>] [--json]` | Select the global row and press `r`. | `dispatcher.pause-set` (first), `dispatcher.intake-set`, then one `build.resume-requested` per `PAUSED` build |
1280
+ | Enable/disable auto-merge | `ab auto-merge <slug> on\|off [--store <ref>]` | Select the build and press `m` to toggle. | `build.auto-merge-requested` / `build.auto-merge-cancelled` |
1281
+ | Answer blockers with guidance | `ab answer <slug> <text> [--store <ref>]` | Select a blocked build, press `r`, enter text, then Enter. | One `escalation.answered` with `resolution: guidance` per applicable blocker. |
1282
+ | Retry blockers without guidance | `ab answer <slug> [--store <ref>]` | Open the same `r` field and press Enter empty or whitespace-only. | One `escalation.answered` with `resolution: retry` per applicable blocker. |
1283
+ | Dismiss cited finding chains | `ab answer <slug> --dismiss [--store <ref>]` | CLI only; build detail names the command. | One `escalation.answered` with `resolution: dismiss-finding` per blocker that cites a real review finding. |
1284
+ | Revise spec from a file | `ab answer <slug> --revise-spec <file> [--store <ref>]` | CLI only; build detail names the command. | Human `escalation.answered` events naming the exact replacement revision, then kernel `spec.revised`. |
1285
+ | Re-import revised ticket spec | `ab answer <slug> --revise-spec-from-ticket [--store <ref>]` | CLI only; build detail names the command. | The same `revise-spec` answer and `spec.revised` sequence. |
1286
+ | Abort | `ab abort <slug> [--store <ref>]` | Select any non-terminal build and press `a`, then Enter to confirm (Escape cancels). | `build.abort-requested` |
1287
+
1288
+ The two `--all` forms and the global-row keys are one walk behind two surfaces.
1289
+ The repository pause is written first and intake second, both absolutely —
1290
+ setters, not toggles, so the values the command names are the values the
1291
+ repository holds afterwards — and each per-build request is a compare-and-set
1292
+ against a freshly reduced log, so a rerun adds no second pending pause and
1293
+ never cancels one already in flight. Pause first is what makes every surviving
1294
+ prefix of a failed walk at least as quiesced as asked: once it lands, no queued
1295
+ build is launchable, whatever else fails after it. Output names every build that
1296
+ received a request, the hold, and the intake value written; `--json` emits that
1297
+ same summary for a script. A walk that fails partway exits nonzero and reports
1298
+ which of the two repository facts landed, which builds were requested, which one
1299
+ failed, and which were never attempted. Neither form starts a dispatcher, so
1300
+ reaching for one to quiesce the repository does not launch the work it is trying
1301
+ to park.
1302
+
1303
+ Discard is dashboard-only and queued-only. After any launching execution has
1304
+ released its process-tree lease, the dispatcher releases a partial workspace,
1305
+ returns the ticket to `[tickets].readyState`, and completes the old build as
1306
+ `discarded`; a later tick may claim the ticket into a fresh build. This is not
1307
+ abort: abort returns the ticket to Triage.
1308
+
1309
+ Abort accepts queued, running, paused, or blocked builds. A live turn receives a
1310
+ cancellation signal and the runner acknowledges the request. Its execution
1311
+ supervisor gracefully stops and reaps the complete process tree, force-terminating
1312
+ it after the bounded stop delay when necessary, and only then releases the lease.
1313
+ The dispatcher defers cleanup while that lease is live, then performs the
1314
+ checkpointed, retry-safe cleanup: close an open unmerged PR, release the workspace,
1315
+ delete the exact remote and local build branches, preserve existing ticket labels
1316
+ while adding `autobuild:aborted`, and return the ticket to configured Triage before
1317
+ completing as `abandoned`. Missing
1318
+ Forge cleanup capabilities or provider outages leave cleanup pending with an
1319
+ actionable diagnostic for the next tick.
1320
+
1321
+ On the dashboard, `r` on a blocked build replaces the bottom legend with a
1322
+ multi-line composer panel: the build being resumed, a note that guidance is
1323
+ optional, the build's unresolved blocker questions, the field itself, and its
1324
+ key bindings. All printable keys edit the field instead of triggering dashboard
1325
+ actions. `Enter` submits (an empty field resumes without guidance), `Ctrl-J`
1326
+ inserts a line break — as does `Shift+Enter` in terminals that report it
1327
+ distinctly — `Backspace` deletes at the caret, `Left`/`Right`/`Up`/`Down` and
1328
+ `Home`/`End` move the caret rather than the dashboard's row selection, and
1329
+ `Escape` cancels without writing. Line breaks survive submission, so the
1330
+ guidance the agent receives keeps the operator's line structure. Pasting
1331
+ multi-line text inserts all of it in one step in terminals that support
1332
+ bracketed paste; where they do not, a paste still submits at its first line
1333
+ break.
1334
+
1335
+ `ab answer` answers every escalation that is open when the command runs,
1336
+ regardless of `agent`, `stall`, or `policy` source. Its text is joined, trimmed,
1337
+ and delivered as authoritative guidance. With no text (or only whitespace), it
1338
+ requests a bare retry and supplies no agent guidance. Verify has two explicit
1339
+ answer routes: guidance answering an agent verifier's own `ab escalate` returns
1340
+ to that same `verify:<step>` on `verify.started.feedback` and materializes as
1341
+ `.ab/guidance.json`; the cited start carries it durably across any pre-launch
1342
+ recovery, and the matching `session.started` consumes it once after launch.
1343
+ Guidance answering the policy escalation after exhausted failed verify reports
1344
+ instead goes to `implement` and outranks the pending report. A bare retry on either path carries
1345
+ no guidance. The dashboard captures the blocker ids when its field opens, then
1346
+ answers only those still open at submission; Escape cancels without writing. If
1347
+ the build is also paused, both surfaces append all answers first and
1348
+ `build.resume-requested` last. A plain
1349
+ `ab resume` does not answer blockers; use `ab answer` for a blocked build.
1350
+
1351
+ An answer is not only recorded — it is delivered to the agent that can act on
1352
+ it. An escalation from `plan` or `plan-review` feeds the next `plan` round.
1353
+ An escalation from `implement` or `code-review` feeds the next `implement`
1354
+ round. An agent verifier's own escalation feeds the next run of that same
1355
+ `verify:<step>`. When a failed verify report exhausts policy, guidance answering
1356
+ that policy escalation instead feeds the next `implement` round and takes
1357
+ precedence over the failed report. When several answers are waiting for one
1358
+ engine-routed destination (`plan`, `code`, or the same exact verifier), only the
1359
+ newest is eligible: it durably supersedes older answers, which do not resurface
1360
+ after the winner is delivered. Destinations remain independent, and a new
1361
+ answer recorded after an earlier delivery is eligible for the next appropriate
1362
+ run. `finalize` and `reconcile` receive their own answers on their next attempt.
1363
+ In each of these routed cases, `ab context`
1364
+ writes the answer to `.ab/guidance.json` in the build's workspace, and the
1365
+ receiving skill treats it as authoritative feedback for that round or attempt.
1366
+ A producer round's feedback is exclusive: on a guidance round
1367
+ `.ab/guidance.json` replaces the reviewer's findings or a routed-back verify
1368
+ failure, so the answer has to carry what the agent needs. A bare retry writes no
1369
+ `.ab/guidance.json` and displaces nothing: the resumed round still gets whatever
1370
+ the loop was already routing, such as the last review round's findings.
1371
+
1372
+ `--dismiss` is per escalation. It answers only blockers that cite at least one
1373
+ real plan-review or code-review finding; blockers with no such citation remain
1374
+ open. A dismissed finding id means a human retired that disagreement. The next
1375
+ reviewer receives `.ab/dismissed-findings.json`, must not re-raise the finding,
1376
+ and must use a fresh id without `persists` if a genuinely different problem
1377
+ touches the same code. Dismissal settles a chain, not a file or the reviewer's
1378
+ ability to report new problems.
1379
+
1380
+ Spec revision requires an open escalation. `--revise-spec` reads a conforming
1381
+ replacement from a file; `--revise-spec-from-ticket` re-imports the build's
1382
+ recorded ticket after an operator amends it. The human `escalation.answered`
1383
+ fact names the exact deposited revision it authorizes, then kernel
1384
+ `spec.revised` publishes it. If interrupted between those facts, a retry
1385
+ completes the recorded body and does not use a newly supplied one. A
1386
+ non-conforming body records nothing. The build restarts from `plan` on the new
1387
+ revision and keeps any open PR. A build's spec can therefore change under it:
1388
+ phase runs after the revision read the new revision, while historical runs
1389
+ remain readable at the revision they actually saw.
1390
+
1391
+ Every command requires the target to exist in this repository and be active
1392
+ (`running`, `paused`, or `blocked`), except abort also accepts `queued`;
1393
+ `ab answer` additionally requires an open escalation. A stale, missing, done,
1394
+ aborted, or otherwise inapplicable target is an error and gets no new event. Attribution uses `USER`, then `USERNAME`, then the
1395
+ same stable fallback on both surfaces. All five commands run sessionless and
1396
+ accept `--store <ref>` with the usual explicit flag > `AB_STORE` > repository
1397
+ local precedence. If nonblank `AB_SESSION` and matching `AB_BUILD` identify the
1398
+ caller's own phase build, the command refuses it; a phase cannot pause, resume,
1399
+ auto-merge, answer, or abort itself. `ab pause --all` and `ab resume --all`
1400
+ accept `--store` on the same precedence and are refused for the same reason
1401
+ whenever `AB_SESSION` and `AB_BUILD` are both nonblank — the caller's own build
1402
+ is inside the walk.
1403
+
1404
+ These commands request normal kernel work; they do not wake a runner, operate
1405
+ the forge, or bypass the lease sweep. Resume is therefore an attempt, not a
1406
+ guarantee: if the condition still fails, a phase may raise a new escalation and
1407
+ block again. Every open escalation, including all policy and setup causes,
1408
+ survives `ab dispatch` and `ab dispatch --once` startup until a human answers
1409
+ it. Startup still recovers actionable builds and delivers pending operator
1410
+ commands, but it never records an answer or re-arms a budget.
1411
+
1412
+ Durable repository intake and the claim-time auto-merge default have launch-flag
1413
+ setters and global-row toggles but no standalone sessionless control commands.
1414
+ Abort is available through both its CLI command and the confirmed build-row/detail
1415
+ `a` key. Global-row `h` owns the durable harvest gate. Global-row `p`/`r` and
1416
+ `ab pause --all` / `ab resume --all` are two surfaces over the one walk described
1417
+ above: from the same repository state they write the same events. Neither the
1418
+ repository hold nor intake has a standalone sessionless setter of its own, but
1419
+ the bulk commands write both as part of quiescing — the hold first. On the
1420
+ optional repository-scoped
1421
+ `Harvest` run row, `p` only resumes or acknowledges the represented run; `i`
1422
+ and `h` are no-ops, and `m` remains an explanatory build-only no-op. On build
1423
+ rows, `i` and `h` are also no-ops.
1424
+
1425
+ ## Checking build status
1426
+
1427
+ The read-only status commands answer "what is happening?" without inspecting
1428
+ SQLite, worktrees, or OS processes. Operators need no phase-session `AB_*`
1429
+ identity; those invocations remain repository-wide. Inside a phase, a complete
1430
+ ambient tuple permits `ab build status` only for the ambient build and denies
1431
+ `ab builds` because it requires repository-wide collection access. Any partial,
1432
+ malformed, shared-only, or mixed build/Harvest identity is rejected rather than
1433
+ falling back to operator access. The commands append no events, take no leases,
1434
+ and are safe to run at any time.
1435
+
1436
+ **`ab repository status [--json] [--store <ref>]`** reports the three durable
1437
+ dispatcher settings in one answer: ticket intake (`intake`, default `true`), the
1438
+ repository-wide pause (`paused`, default `false`), and the claim-time auto-merge
1439
+ default (`defaultAutoMerge`, default `false`). Those defaults also describe a
1440
+ repository with no journal row; the read does not create one. Human output is
1441
+ the default. `--json` emits one bare object with those exact fields plus the
1442
+ canonical `repo`, and `--store` follows the usual explicit flag > `AB_STORE` >
1443
+ repository-local precedence. The command uses the same reducer as the
1444
+ dispatcher and dashboard, writes no state, claims no ticket, attaches no runner,
1445
+ and starts no dispatcher work.
1446
+
1447
+ **`ab builds`** summarizes this repository's builds, one row each. It reports
1448
+ **active** builds by default — `running`, `paused`, `blocked` — because those
1449
+ are the ones something can still be done about. `HEARTBEAT` is mutable runner
1450
+ liveness; `PROGRESS` is the age of the latest durable event. A literal
1451
+ `diverged` suffix means a nonterminal build still holds a live lease but its
1452
+ heartbeat is at least one hour newer than its latest event. It is a visibility
1453
+ signal, not a stalled/dead verdict.
1454
+
1455
+ | Flag | Effect |
1456
+ |---|---|
1457
+ | `--queued` | Also include `queued` builds — dispatched but not yet attached to a runner. |
1458
+ | `--all` | Every status, including `queued`, `done`, and `aborted`. Subsumes `--queued`. |
1459
+ | `--json` | The projection as JSON: no ANSI, no prose. Use this when scripting or parsing. |
1460
+ | `--store <ref>` | Read a different store — a path or an `http(s)` URL, same reference behavior `ab dispatch` takes. |
1461
+
1462
+ An empty result names the filter in effect, so "no active builds" means the
1463
+ filter matched nothing, not that the command failed. Widen it with `--queued`
1464
+ or `--all` before concluding a build doesn't exist.
1465
+
1466
+ **`ab build status <slug>`** details one build: unresolved escalations, open
1467
+ sessions, chronological durable observations, verify progress for the current
1468
+ cycle, PR lifecycle, latest event, durable progress age, heartbeat, and lease.
1469
+ For an open PR, the current work-owner decision reads the build-owned
1470
+ configuration from the store's deposited `build-runner-effective-config`
1471
+ artifact — deposited at every runner launch and refreshed on effective-config
1472
+ changes — falling back to the local workspace's `autobuild.toml` only when no
1473
+ deposit exists; the decision is reported unavailable only when neither source
1474
+ can supply a configuration, and that message names the missing artifact kind
1475
+ rather than a workspace path.
1476
+ Observations are shown without `--events`; for `forge = "local-git"`, this is where a deferred landing
1477
+ names uncommitted work that collides with the squash. Autobuild leaves that work
1478
+ untouched and later dispatcher ticks retry automatically after the operator
1479
+ resolves it. `--events <n>` appends the newest `n` event envelopes in
1480
+ chronological order — the fastest way to see what a build actually just did.
1481
+
1482
+ Verify progress covers the **current cycle** — the results since the latest
1483
+ code-review approve or reconcile. Implement and reconcile change the code, so a
1484
+ new cycle re-runs from the first step and earlier results describe code that no
1485
+ longer exists. An empty step list next to `attempt 1` therefore means the cycle
1486
+ restarted, not that verify never ran. A skip renders as `SKIP` with its reason;
1487
+ JSON exposes `outcome: "skipped"` and `reason`, never a synthetic pass.
1488
+ `--json` and `--store <ref>` work the same here.
1489
+
1490
+ Use `ab builds` to find the build; use `ab build status` to understand it.
1491
+
1492
+ The open-session list is literal durable state. Historical logs with a
1493
+ `session.started` but no ending remain readable and show that session as open;
1494
+ Autobuild does not reinterpret or repair them. When a runner actually takes a
1495
+ build over, it first records each session open at the resume boundary as
1496
+ `reclaimed`, without inventing a transcript, and then records its attachment.
1497
+ The rerun opens one fresh session, so after attachment only work that could
1498
+ still be alive appears in `ab build status`. Dashboard session history keeps the
1499
+ reclaimed entry visibly distinct and reports that its transcript is unavailable.
1500
+
1501
+ Each session row also shows its live-view stream: the stream id and
1502
+ whether that stream is `open` or `closed` — for example `stream st_1f2e (open)`.
1503
+ A session that never streamed (a plugin runtime without the streaming
1504
+ capability, or a historical log) shows no stream segment. While a session
1505
+ runs, its stream is the live view of the turn; once the session ends the
1506
+ stream finalizes into an artifact, and `ab artifact download <slug>
1507
+ stream:<id>` retrieves the closed session's finalized document.
1508
+
1509
+ Harvest sessions stream the same protocol into *repository-scoped* streams:
1510
+ the harvest runner's synthesize and review brackets each get one stream
1511
+ (`phase` spelled `harvest:<step>`), with `harvest.session.started` carrying
1512
+ the stream id. Find them via that `stream` field or the harvest journal
1513
+ (`ab watch --repository`); a closed stream finalizes to a repository-scope
1514
+ `stream:<id>` artifact. The harvest run id is not on the stream — recover it
1515
+ from the journal event that names the stream.
1516
+
1517
+ **`ab watch [<slug>...] [--repository] [--event <glob>]... [--since <cursor>]
1518
+ [--timeout <dur>] [--interval <dur>] [--count <n>] [--json] [--store <ref>]`**
1519
+ streams matching events as they land instead of answering once. With no slug it
1520
+ follows every nonterminal build in the repository — a build created while the
1521
+ watch runs joins it without a restart — and naming one or more slugs watches
1522
+ exactly those builds, ending the watch when all of them reach a terminal status.
1523
+ `--repository` additionally follows the repository journal; without it,
1524
+ repository events are never emitted.
1525
+
1526
+ The default filter is the attention set — the events something must wake up
1527
+ for. For builds that is exactly: `escalation.raised`, `phase.failed`,
1528
+ `infrastructure.failed`, `runner.setup-failed`, `dispatch.failed`,
1529
+ `publication.lost`, `finalize.completed`, `pr.conflicted`, `pr.merged`,
1530
+ `pr.closed`, `build.completed`, `build.aborted`. For the repository journal:
1531
+ `harvest.escalated`, `harvest.failed`, `dispatcher.tick-failed`,
1532
+ `dispatcher.config-rejected`, `dispatcher.harvest-runner-failed`. A repeatable
1533
+ `--event <glob>` (`escalation.*`, `pr.*`, `*` — `*` matches any run of
1534
+ characters, `?` one) replaces that filter, and every glob must match at least
1535
+ one known event type or the command fails before reading the store.
1536
+
1537
+ With `--json`, stdout is newline-delimited JSON — exactly one object per
1538
+ matching event, flushed as soon as it is written — with fields `build` (slug,
1539
+ or null for a repository event), `repo` (present only for repository events),
1540
+ `event` (the complete durable envelope: build, seq, ts, actor, type, payload),
1541
+ `state` (for build events, the reduction after that event: `status`, `phase`,
1542
+ `round`, `openEscalations` with their ids and questions, `pr`, `prState`,
1543
+ `outcome`; null for repository events), and `cursor`. Without `--json` each
1544
+ record is one plain human-readable line. On every exit the last `--json` line
1545
+ is a bare `{"cursor": …}` object reflecting the final position even when no
1546
+ record matched. Cursors are opaque; resume with `--since <cursor>` and every
1547
+ matching event after that cursor is delivered exactly once, in per-stream
1548
+ sequence order — including events that landed while no watch was running. A
1549
+ cursor from a different repository or store is rejected.
1550
+
1551
+ The watch ends with exit 0 when `--timeout` elapses (30 minutes by default;
1552
+ `0` unbounded), after `--count <n>` records, or when named builds have all
1553
+ gone terminal; with no slugs named, an empty set of active builds does not end
1554
+ it. Usage errors, scope violations, unknown slugs, and cursor rejection exit 1
1555
+ before any record. A store read that fails after the watch has started is
1556
+ reported once on stderr and retried without duplicating or skipping any event.
1557
+ Against a local store the default cadence polls every 1 second. Against an
1558
+ `http(s)` store each tracked stream long-polls instead — one held request per
1559
+ stream waits up to 25 seconds for the next event, requests run concurrently,
1560
+ and an appended matching event is delivered within about a second of its
1561
+ append rather than at the next tick — while `--interval` (default 5 seconds)
1562
+ still bounds the spacing between a stream's request starts, so a quiet stream
1563
+ costs roughly one request per 25 seconds and a chatty one no more than one
1564
+ request per interval. The command is read-only — it appends no
1565
+ event, takes no lease, creates no record, and starts no work — and deciding
1566
+ what to do about a blocker or a merge stays with the caller, not the command.
1567
+ Inside a phase, only the ambient build may be watched.
1568
+
1569
+ Two recipes cover the common harnesses. First, a line-per-notification tool
1570
+ that re-arms periodically (Claude Code's Monitor tool expires after at most 30
1571
+ minutes): the cursor is what makes each re-arming lossless — persist the final
1572
+ `{"cursor": …}` line and pass it back through `--since` next time:
1573
+
1574
+ ```sh
1575
+ last=$(cat .ab-watch.cursor 2>/dev/null || true)
1576
+ ab watch --json --timeout 30m ${last:+--since "$last"} \
1577
+ | awk '/^\{"cursor":/ { cursor = $0; next } { print } \
1578
+ END { if (cursor != "") print cursor > ".ab-watch.cursor" }'
1579
+ ```
1580
+
1581
+ Second, a plain shell loop — the same resume discipline wrapped in `while`:
1582
+
1583
+ ```sh
1584
+ while :; do
1585
+ last=$(cat .ab-watch.cursor 2>/dev/null || true)
1586
+ ab watch --json --timeout 30m ${last:+--since "$last"} \
1587
+ | while IFS= read -r line; do
1588
+ case $line in
1589
+ '{"cursor":'* ) printf '%s\n' "$line" > .ab-watch.cursor ;;
1590
+ * ) echo "$line" ;; # one JSON record per matching event
1591
+ esac
1592
+ done
1593
+ sleep 1
1594
+ done
1595
+ ```
1596
+
1597
+ **`ab wait [<slug>...] (--for <condition> | --event <glob>)... [--since <cursor>]
1598
+ [--timeout <dur>] [--interval <dur>] [--json] [--store <ref>]`** is the blocking
1599
+ sibling of `ab watch`: it blocks until a targeted build satisfies a condition,
1600
+ prints exactly one record, and exits with a code that tells the caller what
1601
+ happened. Target selection matches `ab watch` — no slug targets every
1602
+ nonterminal build (builds created while waiting join; an empty or entirely
1603
+ terminal set keeps waiting for a build to appear), named slugs target exactly
1604
+ those builds, and an unknown slug is an error before waiting begins. Inside a
1605
+ phase, only the ambient build may be waited on.
1606
+
1607
+ At least one `--for <condition>` or `--event <glob>` is required. The condition
1608
+ vocabulary is closed: `blocked`, `paused`, `running`, `done`, `aborted`,
1609
+ `terminal` (done or aborted), `pr=open`, `pr=merged`, `pr=closed`,
1610
+ `pr=conflicted`, `phase=<phase>` (a core phase — `plan`, `plan-review`,
1611
+ `implement`, `code-review`, `finalize`, `reconcile` — or `verify:<step>`), and
1612
+ `attention` (any event in the `ab watch` attention set). Repeatable `--event`
1613
+ globs follow the `ab watch` rules. Multiple values are alternatives: the first
1614
+ one satisfied ends the wait — all `--for` values in supply order, then all
1615
+ globs in supply order — and the record names what matched.
1616
+
1617
+ A status, PR, or phase condition is satisfied when a targeted build's reduction
1618
+ has that state — the same projections `ab build status` reports, so `blocked`
1619
+ means an open escalation exactly as everywhere else. In the manner of `kubectl
1620
+ wait`, a condition that already holds when the command starts returns
1621
+ immediately with that build's latest event (`--since` does not suppress this);
1622
+ otherwise the first later event whose reduction produces it ends the wait.
1623
+ `attention` and `--event` conditions fire only on events appended after the
1624
+ command started, or on a cursor-resumed backlog after `--since`.
1625
+
1626
+ The record is the `ab watch` record plus a `condition` field naming the
1627
+ satisfied `--for` value or `--event` glob. Unlike `ab watch`, there is no
1628
+ trailing cursor object: the record's own `cursor` is the final position.
1629
+
1630
+ Exit codes: 0 — satisfied, exactly one record printed; 1 — usage errors, scope
1631
+ violations, unknown slugs, or a store that cannot be opened at start; 2 —
1632
+ slugs were named and every named build went terminal without satisfying any
1633
+ condition (one record with `condition` null, showing how the build ended); 3 —
1634
+ `--timeout` elapsed (30 minutes by default; `0` unbounded; with `--json`,
1635
+ stdout is a bare `{"cursor": …}` object for the final position, plus one line
1636
+ on stderr); 4 — interrupted by SIGINT before any condition was satisfied (same
1637
+ cursor behavior as 3). The default poll interval and `--interval` match
1638
+ `ab watch`. A store read that fails while waiting is reported once on stderr
1639
+ and retried without missing or duplicating a condition. The command is
1640
+ read-only — it appends no event, takes no lease, and creates no record — and
1641
+ deciding what to do about a match stays with the caller's skill.
1642
+
1643
+ Two recipes cover the common harnesses. First, a single-wake-up harness
1644
+ (Claude Code's background Bash mode re-invokes the agent exactly once, when the
1645
+ command exits): run `ab wait` as a background command and let the exit code
1646
+ plus the one record be the whole wake-up payload —
1647
+
1648
+ ```sh
1649
+ ab wait --for blocked --json my-build # exit 0: record says why; 2: it ended; 3: re-arm
1650
+ ```
1651
+
1652
+ Second, a plain blocking call — Codex, Pi, and the shell only expose "run a
1653
+ command and wait": run it in the turn and read the record when it returns.
1654
+
1655
+ The loop pattern ties the two together: wait, act on the record (answer the
1656
+ escalation, inspect the build, merge the PR), then wait again, passing the
1657
+ record's `cursor` back through `--since` so no event is ever examined twice:
1658
+
1659
+ ```sh
1660
+ last=$(cat .ab-wait.cursor 2>/dev/null || true)
1661
+ record=$(ab wait --for attention --json --timeout 30m ${last:+--since "$last"} my-build)
1662
+ code=$?
1663
+ cursor=$(printf '%s' "$record" | grep -o '"cursor":"[^"]*"' | cut -d'"' -f4)
1664
+ [ -n "$cursor" ] && printf '%s' "$cursor" > .ab-wait.cursor
1665
+ # exit 0: $record names the condition in its `condition` field — act on it.
1666
+ # exit 2: the build ended without the condition; exit 3: deadline, re-arm.
1667
+ ```
1668
+
1669
+ **`ab harvest status [--events N] [--json] [--store <ref>]`** projects the
1670
+ **`ab harvest status [--events N] [--json] [--store <ref>]`** projects the
1671
+ durable repository gate and an ordered collection of every unresolved failed
1672
+ run plus relevant open/latest context from the same journal the runner resumes.
1673
+ Each section distinguishes recoverable from terminal and shows that run's
1674
+ automatic attempts/limit, stopped step/round, attention, exact retained snapshot
1675
+ before exhaustion or released observation/proposal keys afterward, workflow
1676
+ occurrences, review rounds, and filed ticket refs. Repository event history is
1677
+ shown once. It is read-only and also reports an idle or paused repository with
1678
+ no run. The dispatch header always shows its acknowledged `harvest ON/OFF` gate
1679
+ and global `h` controls it. The optional, non-color-only `Harvest` row omits the
1680
+ internal run id and represents one deterministically selected open run or
1681
+ unresolved attention; its contextual `p` issues the repository-wide resume or
1682
+ acknowledgement, and `m` remains the build-only no-op.
1683
+
1684
+ ### Status, durable progress, and lease health are separate
1685
+
1686
+ These are **three independent axes**, and reading one as another is the mistake
1687
+ worth avoiding.
1688
+
1689
+ **Status** is reduced from the event log (§15.5) — authoritative, and the same
1690
+ projection the engine itself routes on. **Durable progress** is the timestamp of
1691
+ the latest reduced event; its age says how long the log has been silent.
1692
+ **Lease health** comes from the mutable lease columns (§15.2.6), because
1693
+ liveness is not an event: nothing appends when a sandbox dies, so a build whose
1694
+ runner is gone still reduces to `running` forever. That gap is exactly why all
1695
+ three are reported separately.
1696
+
1697
+ `diverged` is shown only when a nonterminal build has an unexpired lease and its
1698
+ heartbeat is at least one hour newer than its latest event. Finished, aborted,
1699
+ no-lease, and expired-lease builds are never marked. The marker does not alter
1700
+ status, lease health, routing, or recovery.
1701
+
1702
+ | Lease | Meaning |
1703
+ |---|---|
1704
+ | `held` | A runner holds an unexpired lease. The runner is live; inspect `PROGRESS` to see when it last wrote a durable fact. |
1705
+ | `expired` | The lease ran out — the runner is gone. `running` + `expired` is the **stale** case: the status is not lying, it simply has no "runner died" fact to record. Re-attachment depends on the current engine decision: the lease sweep re-attaches actionable runner work, while a merged or closed PR awaits repository-level completion with no runner re-attachment pending. |
1706
+ | `no-lease` | **Not necessarily dead.** A build that has not yet claimed its first lease reads this way, and the lease sweep deliberately grants an absent lease a first-claim grace window before acting. A freshly launched build is the common case — read it together with progress, not alone. |
1707
+
1708
+ So `running` + `held` confirms liveness, while its progress age says whether
1709
+ the durable log is moving. For `running` + `expired`, inspect the build detail
1710
+ to distinguish actionable work that will return through the lease sweep from
1711
+ an ended PR awaiting repository-level completion. `no-lease` with recent
1712
+ progress is usually a runner still starting up.
1713
+
1714
+ ## Serving the operator tools over MCP
1715
+
1716
+ `ab mcp [--store <ref>] [--repo <id>]` serves Autobuild's operator tool
1717
+ registry as an MCP (Model Context Protocol) server over stdio. Every tool it
1718
+ advertises mirrors an operator CLI command or an operator API route — same
1719
+ input schemas, same annotations, same results — and mutating tools attribute
1720
+ their writes to the sessionless operator identity (USER/USERNAME, else
1721
+ "dashboard"), exactly like the other operator commands. The server fails
1722
+ closed inside a phase session: repository-wide operator authority cannot be
1723
+ narrowed to the ambient build, so a complete or malformed `AB_*` phase tuple
1724
+ refuses to start it.
1725
+
1726
+ **The `ab` CLI stays the primary local interface** for agents that have a
1727
+ shell. Use `ab mcp` when a local agent cannot run commands, for dogfooding the
1728
+ operator tools, or for parity testing of the tool registry itself — the
1729
+ served tools are generated from the same closed table the CLI and operator API
1730
+ are measured against.
1731
+
1732
+ | Flag | Effect |
1733
+ |---|---|
1734
+ | `--store <ref>` | Override the BuildStore path or URL, same precedence as the other sessionless commands. |
1735
+ | `--repo <id>` | Constrain the server to one repository; calls naming any other repository are refused. |
1736
+
1737
+ Register it with Claude Code (from an Autobuild installation root):
1738
+
1739
+ ```sh
1740
+ claude mcp add autobuild -- bun <install-root>/bin/ab.ts mcp
1741
+ ```
1742
+
1743
+ Register it with Codex by adding a server entry to `~/.codex/config.toml`:
1744
+
1745
+ ```toml
1746
+ [mcp_servers.autobuild]
1747
+ command = "bun"
1748
+ args = ["<install-root>/bin/ab.ts", "mcp"]
1749
+ ```
1750
+
1751
+ The server runs until its client disconnects (stdio EOF) and needs no
1752
+ daemon: each client session starts one short-lived `ab mcp` process.
1753
+
1754
+ ## The installed skills
1755
+
1756
+ Each is invoked as `ab-<name>`, with its editable copy at
1757
+ `.agents/skills/ab-<name>/SKILL.md` (read that copy, not the distribution
1758
+ default, when you need to know what this repo's version says).
1759
+
1760
+ | Skill | Place in the lifecycle | Purpose |
1761
+ |---|---|---|
1762
+ | `ab-spec` | Before a build exists | Design a feature spec-first through conversation, or flesh out a ticket to the spec standard. The human-interactive surface; takes a ticket, not a build slug. **Model-invocable.** |
1763
+ | `ab-tickets` | Before a build exists | Drive this repo's local file tracker: create a ticket, report the backlog, groom or move one between `triage/ ready/ doing/ done/`. The agent-facing surface on the tracker — use it instead of `mv`. **Model-invocable.** |
1764
+ | `ab-guide` | Outside the pipeline | This skill: reference for the lifecycle, config surface, setup/upgrade behavior, and the installed skills. Its editable `references/setup.md` guides the init handoff. **Model-invocable.** |
1765
+ | `ab-harvest` | harvest `synthesize` step | Continue the producer across review rounds: cluster the claimed structured observations and author typed spec-standard create/join/suppress proposals. Runner-only. |
1766
+ | `ab-harvest-review` | harvest `review` step | Fresh adversarial reviewer for proposal coverage, semantic dedup, spec quality, and evidence; returns `approve`/`revise`/`escalate`. Runner-only. |
1767
+ | `ab-plan` | `plan` phase | Turn the spec into a plan another agent can implement without re-deriving the reasoning. Writes no product code. |
1768
+ | `ab-plan-review` | `plan-review` phase | Fresh skeptic: review the plan against the spec, verdict `approve`/`revise`/`escalate`. |
1769
+ | `ab-implement` | `implement` phase | Execute the approved plan as local commits plus deposited notes. Never pushes. |
1770
+ | `ab-code-review` | `code-review` phase | Fresh skeptic: review the implementation diff against spec and plan, same verdict vocabulary. |
1771
+ | `ab-reconcile` | `reconcile` phase (epilogue) | Resolve a conflicted PR with one merge commit, base merged *into* the build branch. Never rebases. |
1772
+ | `ab-finalize` | `finalize` phase | Write the PR description for a green build; the kernel opens the PR. |
1773
+
1774
+ Autobuild installs 11 skills. Everything except `ab-spec`, `ab-tickets`, and
1775
+ `ab-guide` is **runner-invoked** by the kernel and carries
1776
+ `disable-model-invocation: true` — do not invoke a phase skill yourself, and do
1777
+ not remove that key to make one convenient to call. A model starting a pipeline
1778
+ phase by pattern-matching a description is exactly what the flag prevents. The
1779
+ three exceptions drive no phase, which is the criterion for membership (§16.3):
1780
+ `ab-spec` and `ab-tickets` are human/agent-facing pre-build surfaces, and
1781
+ `ab-guide` is read-only reference including the setup handoff guidance.