@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
package/SPEC.md ADDED
@@ -0,0 +1,2269 @@
1
+ # Autobuild v2 — Specification
2
+
3
+ An agent-driven software development lifecycle system: it takes work from
4
+ *"something should be done"* to a merged PR, with a human in the loop only
5
+ where judgment matters. This is a ground-up rebuild of v1 (preserved in git
6
+ history at `6d4dce3`), keeping v1's validated ideas and replacing what it
7
+ hard-coded with real interfaces.
8
+
9
+ **What this document is.** SPEC.md holds the durable design: principles,
10
+ boundaries between components, invariants, and decisions with their rationale
11
+ (the **[D1]–[D11]** series). It is deliberately not three other things:
12
+
13
+ - **Not a behavior reference.** Exact schemas, flag grammars, validation
14
+ rules, and edge-case handling are specified by the code and its tests —
15
+ event payloads are frozen in `packages/core/src/events/`, and the store contract in
16
+ `packages/core/src/store/contract.ts`. Where this document and the code disagree on
17
+ behavior detail, the code is authoritative.
18
+ - **Not an operating manual.** How to install, configure, and drive the
19
+ system lives in `README.md` and `docs/`.
20
+ - **Not a change log.** Each resolved design question lives in its ticket,
21
+ build, and PR; the paper trail is queryable in the BuildStore. A merged
22
+ change earns a paragraph here only if it altered a principle, boundary, or
23
+ invariant.
24
+
25
+ Status legend: sections describe **decided** design unless marked **[OPEN]**.
26
+
27
+ ---
28
+
29
+ ## 1. Why a rebuild
30
+
31
+ v1 proved the core loop works. Its structural limits, which motivate v2:
32
+
33
+ - **Repo as database.** Build state and artifacts lived in `build/<feature>/`
34
+ dirs committed to the repo — accumulating noise, bloating the repo, with no
35
+ clean archival story.
36
+ - **No observability seam.** State in local files meant no way to monitor
37
+ builds, especially remote ones. The "UI" was tailing `build.log`.
38
+ - **Hard-coded everything.** The phase list, the verdict vocabulary, Linear,
39
+ Sentry, and the review loop (implemented twice, differently) were all baked
40
+ in.
41
+ - **Sentinel parsing.** Verdicts rode on stdout and were regex-parsed —
42
+ fragile by construction.
43
+ - **Opaque agent sessions.** Near-zero insight into transcripts; no corpus
44
+ for improving prompts or writing evals.
45
+
46
+ ## 2. Constitution
47
+
48
+ Principles — the first four carried from v1 — that are non-negotiable and that
49
+ every design decision below answers to:
50
+
51
+ 1. **Judgment in skills, determinism in code.** Agents never decide phase
52
+ transitions, signal identity, or state. Agent surfaces own the fuzzy parts
53
+ (primarily skills, plus narrow pre-build seams such as naming); plain tested
54
+ code owns the state machine, validation, fallback, dedup, gating, and
55
+ plumbing.
56
+ 2. **Resumability is not a feature.** Re-running a build resumes it. There is
57
+ no separate resume path; every phase is a function of durable state.
58
+ 3. **Ingesters propose, humans dispatch.** Nothing auto-generated moves past
59
+ Triage without a human grooming it to Ready. That single gate is where
60
+ taste and prioritization live. A repository may waive the gate for its own
61
+ harvest by naming a filing state (`[tickets].proposalState`, §12); the
62
+ waiver is explicit, repository-wide, and configured, and no code path
63
+ reaches Ready without one.
64
+ 4. **Every step leaves a paper trail** — queryable, not carried in the repo.
65
+ 5. **Processes communicate only through durable state.** There is no private
66
+ channel between Autobuild processes — not kernel to operator UI, not parent
67
+ to child, not kernel to sandbox. A process's liveness may be observed; what
68
+ it has *done* is known only by reading the log. This is what keeps every
69
+ component relocatable, and why a new process boundary never needs a new
70
+ protocol.
71
+
72
+ ## 3. System decomposition
73
+
74
+ Four kinds of things with sharp boundaries:
75
+
76
+ ### 3.1 The kernel
77
+
78
+ A headless library: pipeline engine, the `converge` review-loop primitive,
79
+ event log handling, state reducer. No I/O opinions — no ticket tracker, no
80
+ telemetry vendor, no terminal.
81
+
82
+ ### 3.2 Ports
83
+
84
+ Interfaces to the world, each with swappable adapters:
85
+
86
+ | Port | Duty | Initial adapters |
87
+ |---|---|---|
88
+ | `TicketSource` | list/claim/comment/transition/create/update tickets; add, remove, and resolve declared dependencies | file-based (default directory); Linear; third-party in-process registrations; later GitHub Issues |
89
+ | `AgentRunner` | run agent sessions (see §9) | Claude Code CLI (headless); Codex CLI (`exec --json`); local pi CLI (`--mode rpc`, minimum 0.84.3); third-party in-process registrations |
90
+ | `Workspace` | provision isolated working copies | git worktree; third-party in-process registrations; later remote sandbox |
91
+ | `Forge` | git + PR plumbing | GitHub; local Git; third-party in-process registrations |
92
+ | `TelemetrySource` | production signals | Sentry; later log streams |
93
+ | `BuildStore` | per-build streams plus repository journals: events, artifacts, transcripts, leases (see §7) | local; remote HTTP |
94
+
95
+ #### 3.2.1 In-process adapter plugins
96
+
97
+ A repository may list trusted Bun modules in `autobuild.toml`. Each module
98
+ default-exports a strict manifest with a diagnostic `name`, an `apiVersion`
99
+ semver range, and optional name-to-factory maps for `ticketSources`,
100
+ `agentRuntimes`, `workspaceProviders`, and `forges`. One manifest may register
101
+ adapters for several ports. A ticket-source entry is either its legacy bare
102
+ factory or `{ factory, requiredEnv?, contract? }`; `requiredEnv` lets the host
103
+ reject unset or empty credential variables before construction with a
104
+ diagnostic that names both source and variables. Plugin API 1.1 introduces the
105
+ descriptor metadata while remaining compatible with manifests accepting
106
+ `^1.0.0`. Factories receive
107
+ adapter-specific config, the process environment, and the absolute repository
108
+ root, and remain lazy during startup registration. Runtime registrations reuse
109
+ §9's capability-bearing `RuntimeRegistration`; the frozen `AgentRunner`
110
+ interface is not widened.
111
+
112
+ The host exposes one versioned authoring surface, `@defrex/autobuild/plugin-sdk`: port
113
+ and manifest types, the reusable TicketSource/AgentRunner/WorkspaceProvider/
114
+ Forge/BuildStore/BlobStore contract suites, and fake/reference adapters. Plugin
115
+ production code can use erased type-only imports, with Autobuild present only
116
+ as a development or peer dependency; a consuming repository needs no bridge
117
+ module.
118
+
119
+ Plugin resolution has two repository-owned roots and one installation root.
120
+ Relative, absolute, and `file:` specifiers resolve from the repository root
121
+ whose config is being read; in a scoped build CLI process, that is the
122
+ immutable build worktree. Bare package specifiers and package export maps
123
+ resolve first from the consuming repository's main checkout, so they use that
124
+ repository's installed dependencies and remain available when a local store
125
+ places linked worktrees outside the checkout's package ancestry; a package the
126
+ repository lacks then resolves from the Autobuild installation the CLI runs
127
+ from, so an extension installed next to the CLI (`bun add -g
128
+ @defrex/autobuild-<extension>`) loads without being added to the repository. A
129
+ repository copy always wins over an installed one. Dispatch and sessionless
130
+ commands naturally use the main checkout for both repository roots. Autobuild
131
+ does not install a missing package.
132
+
133
+ Configured specifier strings must be unique: an exact repeated value fails
134
+ config validation before module resolution or evaluation, identifies the
135
+ duplicate and its first declaration, and directs the operator to remove or
136
+ deduplicate it. Distinct specifier strings remain separate declarations.
137
+ Modules load in declaration order during `ab dispatch`, the sessionless
138
+ `ab ticket` commands, and scoped build CLI composition. Dispatch loads them after
139
+ strict config parsing and before stores, production adapters, ticket claims, or
140
+ build launch; ticket commands load them before ticket access; scoped processes
141
+ load worktree config and repository-path modules, but use the main checkout only
142
+ for package lookup, before opening their store or executing terminal plumbing.
143
+ Resolution/evaluation errors, malformed or missing default manifests, and
144
+ plugin-API incompatibility fail startup with both the configured module and
145
+ available compatibility details.
146
+ Builtin registration names and names registered by an earlier plugin are
147
+ reserved per port; collisions fail atomically, identify the conflicting adapter
148
+ and owners, and shadow nothing. The same name may exist on different ports.
149
+ `[workspace].provider` selects from the workspace catalog; omission selects
150
+ `git-worktree`, while `vercel-sandbox` is the shipped opt-in remote execution
151
+ provider. Plugin factories are invoked lazily with `[workspace.config]`,
152
+ environment, and repository root. The Vercel builtin instead consumes its
153
+ strict typed config and requires an HTTPS Store/scoped token and HTTPS GitHub
154
+ origin before any ticket claim.
155
+
156
+ Each adapter map value may remain a bare factory or may be an object containing
157
+ that factory plus an optional `contract: { factory, live? }` descriptor; ticket
158
+ sources may carry `requiredEnv` in the same object. The contract factory
159
+ receives the same repository context and returns the fixture
160
+ factory required by that port's unchanged shared suite. `ab plugin list`
161
+ projects builtin and configured registrations with provenance, resolution kind,
162
+ API compatibility, and contract availability. `ab plugin doctor` exhaustively
163
+ attempts every configured module and exits nonzero if any fail; this diagnostic
164
+ collection does not weaken `ab dispatch`, which remains fail-fast. `ab plugin
165
+ test <ticket-source|agent-runtime|workspace-provider|forge> <adapter>` delegates
166
+ one selected suite to Bun's test runner and preserves its per-test output and
167
+ exit status. A descriptor marked `live` is never launched unless
168
+ `AB_RUN_LIVE_PORT_CONTRACTS=1` is explicitly present.
169
+
170
+ Plugins execute in-process and are Bun-only. They have the same repository
171
+ trust boundary as declarative shell commands: no sandbox is promised.
172
+ `[tickets].source` may name a loaded plugin registration; unknown names fail
173
+ with all available builtin and plugin names. Dispatch routes one selected
174
+ instance through readiness, dependencies, claim, projection, harvest creation,
175
+ and janitor completion; the same registration backs every source-agnostic
176
+ `ab ticket` operation. Plugin sources receive the existing `[tickets]`
177
+ lifecycle fields unchanged and own any further validation; credentials remain
178
+ environment-only.
179
+
180
+ Forge selection is open through the root `forge` scalar (`github` by default).
181
+ Two adapters ship: `github` publishes through GitHub/`gh`; `local-git` uses only
182
+ the repository's shared local Git database and requires no remote, network, or
183
+ forge credentials. A selected plugin factory receives an empty adapter config, process
184
+ environment, and absolute repository root, and is invoked before store opening.
185
+ Unknown names list the complete available forge catalog. Dispatch and scoped
186
+ build CLI processes resolve the same configured name independently, and all
187
+ forge plumbing receives the selected adapter unchanged. `PrRef.url` is the
188
+ provider's nonblank locator: an HTTP(S) URL when a web UI exists, or a literal
189
+ provider-native identifier such as `refs/heads/ab/<slug>` for local Git. Workspace selection is
190
+ open through `[workspace].provider` as described above.
191
+
192
+ Agent runtime selection is open through every `[roles.*].runtime`: dispatch
193
+ materializes registered runtime factories before eager role validation, and the
194
+ lazy upgrade conflict resolver does the same only when its first conflict needs
195
+ judgment. Plugin registrations use the same exact-name routing, model-family and
196
+ default-model validation, event attribution, and optional one-shot capability
197
+ selection as builtins.
198
+
199
+ `TelemetrySource` remains deferred, and BuildStore's third-party extension
200
+ surface remains the remote HTTP protocol rather than in-process registration.
201
+
202
+ ### 3.3 Processes
203
+
204
+ Small, independently runnable, crash-safe:
205
+
206
+ - **build-runner** — one operating-system process per build; owns one pipeline
207
+ execution end to end. The local implementation starts the process beside its
208
+ Git worktree in its own POSIX session/process group and owns teardown of that
209
+ complete group, including agent-tool grandchildren. Workspace execution is a
210
+ substitutable capability, so a future sandbox provider can place both checkout
211
+ and process elsewhere without the dispatcher spawning a local proxy; that
212
+ provider owns equivalent complete-environment teardown. Per-build processes are deliberate: crash
213
+ isolation, and the natural shape once builds run in remote sandboxes.
214
+ - **dispatcher** — watches the TicketSource for tickets passing the configured
215
+ ready gate, claims, establishes the final conforming spec, chooses a short
216
+ immutable build slug, provisions a workspace, and launches build-runners up
217
+ to capacity. On process startup it attempts every current build for its
218
+ repo, so re-running `ab dispatch` resumes durable work rather than only
219
+ looking for new tickets. Every ordinary tick also completes queued dispatch
220
+ records whose pre-run sequence was interrupted: it reconstructs a missing
221
+ `build.created` from the immutable record, executes only missing workspace,
222
+ spec, and ticket-notification boundaries, and records each failed attempt as
223
+ `dispatch.failed`. `build.created` retains any claim-time auto-merge
224
+ attribution until its human-authored command fact is materialized, while
225
+ `dispatch.comment-posted` prevents retries from duplicating the ticket notice.
226
+ It also owns observation back-pressure: settling
227
+ outstanding recoverable harvest runs takes priority over starting new scans
228
+ (§12). Cron-friendly.
229
+ - **harvest-runner** — one staged repository workflow (`scan → synthesize ⇄
230
+ review → file`) under a repository lease; not a build and not a phase.
231
+ - **ingesters** — other outer-loop processes turning signals into proposals (§12).
232
+ - **operator** — UI process(es); see §14.
233
+
234
+ ### 3.4 The event log spine
235
+
236
+ Build processes append typed events to per-build logs; repository-scoped outer
237
+ workflows and operator settings append to a separate repository journal in the
238
+ same BuildStore. Consequences, by design:
239
+
240
+ - **State is a reduction of events.** Any state snapshot is a cache, never
241
+ the source of truth. Resumability falls out.
242
+ - **The UI layer is a subscriber** plus a command channel back. TUI, web —
243
+ same adapter pattern. The shipped interactive terminal owns a supervised
244
+ kernel child, and the BuildStore is their only changing-state boundary.
245
+ - **The audit trail is the log**, serialized.
246
+
247
+ The event vocabulary (§15) is deliberately designed early and carefully: it
248
+ is simultaneously the store schema, the kernel's I/O, the UI API, and the
249
+ resume format. A bad event schema calcifies.
250
+
251
+ ## 4. Ontology
252
+
253
+ One name, used everywhere. Every noun lives in exactly one layer.
254
+
255
+ | Term | Meaning |
256
+ |---|---|
257
+ | **Signal** | Raw input from the world — a Sentry issue, an observation |
258
+ | **Proposal** | A synthesized candidate ticket sitting in Triage, awaiting grooming |
259
+ | **Ticket** | A groomed, dispatchable unit of work in the TicketSource |
260
+ | **Build** | One pipeline execution for one ticket; has a slug; the unit the operator sees |
261
+ | **Harvest run** | One repository-scoped claimed observation snapshot and staged proposal workflow; never a build |
262
+ | **Phase** | A named stage of the build pipeline grammar (§5) |
263
+ | **Round** | One iteration inside a review loop |
264
+ | **Artifact** | A durable, versioned output: spec, plan, review, report, transcript |
265
+ | **Verdict** | Structured terminal outcome: reviews use `approve` \| `revise(findings)` \| `escalate(reason)`; agent verification uses `pass` \| `fail(report)` \| `skip(reason)` |
266
+ | **Finding** | One structured item inside a `revise` verdict (file refs, description, severity) |
267
+ | **Observation** | An out-of-scope discovery emitted mid-build: `followup` \| `refactor` \| `latent-bug` |
268
+ | **Escalation** | A parked request for human input, answerable from any UI |
269
+ | **Event** | An append-only log record; everything above leaves one |
270
+
271
+ Reserved-word discipline: **build** names the whole pipeline execution and
272
+ nothing else; the coding phase is **implement**.
273
+
274
+ ### Naming propagation
275
+
276
+ A phase's name derives its skill, its events, and its artifact kind —
277
+ mechanically:
278
+
279
+ | Phase | Skill | Events | Artifact |
280
+ |---|---|---|---|
281
+ | `plan` | `/plan <build>` | `plan.started`, `plan.completed` | `plan` (rev N) |
282
+ | `plan-review` | `/plan-review <build>` | `plan-review.verdict` | `plan-review` (rev N) |
283
+ | `implement` | `/implement <build>` | `implement.completed` | diff + `implement-notes` |
284
+ | `code-review` | `/code-review <build>` | `code-review.verdict` | `code-review` (rev N) |
285
+ | `verify:e2e` | `/verify-app-e2e <build>` (repository-authored) | `verify.completed {step, outcome}` | `verify-report:e2e` on pass/fail when present |
286
+ | `finalize` | `/finalize <build>` | `finalize.completed` | PR ref, summary |
287
+ | `reconcile` | `/reconcile <build>` | `reconcile.started`, `reconcile.completed` | `reconcile-notes` + merge commit |
288
+
289
+ Every phase skill takes **only the build slug**; everything else it needs
290
+ comes from the store via the `ab` CLI (§8).
291
+
292
+ Canonical skills installed into a repo carry the `ab-` namespace prefix
293
+ (§16.3), for example `/ab-plan` and `/ab-code-review`. Repository-authored
294
+ extension skills use their own names, such as `/verify-app-e2e`. This spec uses
295
+ the bare phase names throughout.
296
+
297
+ ## 5. Pipeline grammar
298
+
299
+ ```
300
+ spec → plan ⇄ plan-review → implement ⇄ code-review → verify:* → finalize (+ finalize:*)
301
+ ```
302
+
303
+ The grammar is fixed — an opinionated skeleton, not a generic workflow
304
+ engine. Exactly two extension points:
305
+
306
+ - **`verify:*`** — an ordered, configurable list of verifiers, declared in
307
+ per-repo config. Two subtypes: *check* (deterministic command; pass/fail is
308
+ its exit code) and *agent-verify* (an agent run with a `pass|fail|skip`
309
+ verdict schema). The durable outcomes are `pass`, `fail(report)`, and
310
+ `skipped(reason)`. A failure routes back to `implement` with the report,
311
+ re-entering the code loop. A skip requires a human-readable reason,
312
+ satisfies only that step for the current cycle, and consumes no failure
313
+ budget — it is neither passing evidence nor a failure, and it never hides
314
+ another step's failure. A skip is produced either by an agent verifier's
315
+ explicit verdict or by the kernel, when the approved plan did not select an
316
+ optional step or a configured path-applicability rule excludes it (§16.1).
317
+ - **`finalize:*`** — optional ordered post-steps (release notes, changelog,
318
+ publishing, ticket linking). Each configured step is either a deterministic
319
+ command check with no agent session or an agent running an exact skill.
320
+ Independent and failure-tolerant: a failed post-step files an observation;
321
+ it never kills a green build. A step that produces repository content
322
+ selects and commits its files locally and must finish with a clean worktree.
323
+ The runner verifies that the last durably published head remains an ancestor,
324
+ then regular-pushes the new head and checkpoints it on
325
+ `finalize.step-completed`. An unchanged head creates and pushes no commit;
326
+ Git or publication failure is the ordinary failed-step observation, never a
327
+ reason to rewrite history or fail the green build.
328
+
329
+ Everything else — phase order, the two review loops, escalation semantics —
330
+ is kernel-hard-coded. That is what keeps the system inspectable.
331
+
332
+ `spec` is *not* a phase; it is satisfied at or before dispatch (§6). After
333
+ `finalize`, the build enters a post-PR epilogue owned by the dispatcher's
334
+ janitor duty (§15.7): a conflicted PR triggers the `reconcile` phase —
335
+ agent-resolved merge of base into branch, full `verify:*` re-run — repeating
336
+ until merge or close.
337
+
338
+ ## 6. The spec
339
+
340
+ Two things wear the name:
341
+
342
+ ### 6.1 The spec standard
343
+
344
+ A reference document defining "buildable": what/why not how, acceptance
345
+ criteria, explicit out-of-scope, evidence links. It is a shared resource
346
+ cited by **every ticket-producing surface** — `/spec`, `harvest`, every
347
+ `ingest:*`, and `dispatch`. An ingester's proposal is a spec written to the
348
+ same standard with weaker evidence.
349
+
350
+ ### 6.2 The /spec skill
351
+
352
+ The human-interactive, conversational surface over the standard. It takes
353
+ a ticket rather than a build slug, because it runs before a build exists:
354
+
355
+ - `/spec` (no args) — design a feature through conversation, spec-first; on
356
+ completion it creates the ticket via the TicketSource.
357
+ - `/spec <ticket>` — flesh out a thin existing ticket into conforming shape,
358
+ then sync the body back through the TicketSource so unrelated metadata
359
+ remains untouched.
360
+
361
+ ### 6.3 The spec artifact
362
+
363
+ The **contract artifact of a build**: kind `spec`, revision 0, sole input to
364
+ `plan`. Pre-build, the spec lives in the ticket body; **dispatch imports it**
365
+ (`spec.imported`) or, for thin-but-groomed tickets, authors one via a
366
+ non-interactive agent pass (`spec.authored`). Three production paths, one
367
+ artifact: human-led via `/spec`, dispatch-authored, ingester-shaped.
368
+
369
+ **Dispatch quality gate:** if a Ready ticket cannot be expanded into a
370
+ conforming spec, dispatch bounces it back to Triage with a comment citing
371
+ the standard — moving failure to the cheapest point instead of launching a
372
+ build that will thrash and escalate.
373
+
374
+ **Build identity at dispatch:** once the final conforming spec is in hand,
375
+ the selected runtime proposes a short kebab-case slug from its substance —
376
+ judgment proposes; the dispatcher validates, deduplicates against the store,
377
+ and falls back to a deterministic title-derived name on any absence, invalid
378
+ output, error, or timeout. Naming failure never prevents build creation. The
379
+ slug and its `ab/<slug>` branch are recorded once and never renamed.
380
+
381
+ **Immutability:** the spec cannot change during a build except through the
382
+ explicit escalation revision protocol. Every downstream reviewer approves
383
+ conformance *to it*; a drifting spec silently converts approvals into
384
+ approvals-of-something-else. A phase discovering the spec itself is wrong
385
+ raises an `escalation`; a human uses `ab answer --revise-spec` (or
386
+ `--revise-spec-from-ticket`), the spec gets rev N+1, and the build restarts
387
+ from `plan` (cheap — downstream was invalidated anyway).
388
+
389
+ ## 7. The build store
390
+
391
+ The v1 repo-bloat and no-observability problems share one root cause: the
392
+ repo did double duty as workspace and database. v2 splits the roles:
393
+
394
+ - **Workspace** — the sandbox/worktree. Scratch by construction: the working
395
+ tree plus the gitignored `.ab/` dir agents use as working memory during a
396
+ phase (§8.3). Disposable.
397
+ - **Build store** — the durable home of everything else, one logical place
398
+ whether builds run locally or in ten remote sandboxes.
399
+
400
+ ### 7.1 Data model
401
+
402
+ ```
403
+ builds id/slug, ticket ref, repo, branch, status (derived), created/updated,
404
+ lease + heartbeat (mutable liveness columns — §15.2.6, never events)
405
+ events build_id, seq, timestamp, actor, type, payload (JSON) — append-only
406
+ artifacts build_id, kind, revision, blobRef, metadata
407
+ repo_streams repo, created/updated, lease + heartbeat
408
+ repo_events repo, seq, timestamp, actor, type, payload (JSON) — append-only
409
+ repo_artifacts repo, kind, revision, blobRef, metadata
410
+ ```
411
+
412
+ #### 7.1.1 Operator sessions (the third resource kind)
413
+
414
+ An operator session is durable orchestrator-conversation state — a record keyed
415
+ by repository and operator, hosted-only. It is neither a build nor a
416
+ repository fact: folding it into the build streams would leak conversation
417
+ facts into the build reducer, and folding it into the repository journal would
418
+ mix one operator's chat with dispatcher and harvest history that every process
419
+ reads. The store's data model therefore gains a third resource family with the
420
+ same shape as the other two:
421
+
422
+ ```
423
+ sessions id (os_…), repo, operator, title?, created/updated
424
+ session_events session_id, seq, timestamp, actor, type, payload (JSON) — append-only
425
+ session_artifacts session_id, kind, revision, blobRef, metadata
426
+ ```
427
+
428
+ The record carries only identity and timestamps; everything else — status
429
+ (`idle | running | suspended | awaiting-approval | archived`), the open turn,
430
+ the pending approval, wake settings, wake cursors, the turn list — is a
431
+ reduction of the session's own event log (a pure reducer, never a column).
432
+ The session event catalog is closed and validated separately from the build
433
+ and repository catalogs, so no build reducer can interpret session facts, and
434
+ per-session sequencing is independent. The stream primitive's scope vocabulary
435
+ gains `{kind: "session"}` with the session id as its reference, so every
436
+ turn's content is one session-scoped stream. Local installs never create
437
+ sessions: local adapters implement the contract uniformly, and the operator
438
+ API (§14) is the only creation surface.
439
+
440
+ Schema requirements (the exact DDL is not design-critical): simple,
441
+ normalized, defined once, with a local embedded target and a remote
442
+ server-database target. Blobs are content-addressed (sha256) behind a narrow
443
+ `BlobStore` adapter (`put(hash, bytes)`, `get(hash)`): a plain directory
444
+ locally; remotely any object store — always an adapter, never an assumption.
445
+ The database stores refs, never bulk content. The design lives in the data
446
+ model and event vocabulary (§15); the schema should stay boring.
447
+
448
+ Transcripts are an artifact kind (`transcript`) with metadata: phase, round,
449
+ role, runner, model, token counts. This one decision produces the analysis
450
+ corpus — prompt improvement, evals against the build skills, replay — as a
451
+ query rather than a project.
452
+
453
+ Reading session events uses the same contract shape as stream reads (§7.6):
454
+ cursor-based, returning events with `seq` strictly greater than `sinceSeq`
455
+ (default 0) in increasing sequence order, and an optional bounded wait —
456
+ `waitSeconds` in whole seconds — that returns as soon as an event lands and
457
+ no later than the §7.6 bound, with a `waitSeconds` above 30 clamping to 30
458
+ exactly as stream reads do. One asymmetry is deliberate: an event log has no
459
+ closed state, so a session-event read wakes only on an appended event or the
460
+ clamped deadline, never on a close. Every store adapter enforces the same
461
+ rules, so a poll loop cannot drift between stream and session-event reads;
462
+ over the remote protocol the read is
463
+ `GET /sessions/{id}/events?since={n}&wait={n}`, parsed exactly like the
464
+ stream read's ([docs/remote-store-protocol.md](docs/remote-store-protocol.md),
465
+ §5).
466
+
467
+ ### 7.2 Interface and adapters
468
+
469
+ Deliberately narrow: build runners need `append(event)`, `putArtifact`,
470
+ `getArtifact`, `getEvents(since)`; operator UIs add `listBuilds`, `subscribe`.
471
+ A build stream also exposes `appendIfCurrent(expectedSeq, event)`: one atomic
472
+ compare-and-append that returns no event when the stream has advanced. It is a
473
+ narrow coordination primitive for deterministic event-log deduplication under
474
+ concurrent processors; the event stream remains authoritative, with no
475
+ snapshot or side ledger. Sequence 0 names an empty stream, candidates receive
476
+ ordinary event validation, unknown builds reject, and a comparison miss leaves
477
+ the log and build timestamps untouched.
478
+ The same contract has repository-scoped `ensureRepo`, event/artifact deposit
479
+ and read methods, plus a repository lease. Before execution, a runner obtains
480
+ an interface-enforced handle scoped to exactly its build. Own-build records,
481
+ stream, artifacts, lease, and subscription remain available; another build,
482
+ collection/admin operations, nested foreign scope, and every repository-journal
483
+ operation reject. This guard applies identically to every adapter; remote tokens
484
+ carry the same authority over the wire as defense in depth rather than creating
485
+ it. Two implementations of one contract (`packages/core/src/store/contract.ts` is the shared
486
+ conformance suite):
487
+
488
+ 1. **Local** — one self-contained state tree at `<main-repo>/.autobuild/` by
489
+ default: database, content-addressed blobs, Git worktrees, and the file
490
+ ticket source's default directory. The main checkout is derived from Git's
491
+ repository/worktree topology, so a command run inside a linked worktree
492
+ resolves the same state. There is no home-directory fallback or
493
+ machine-global state. Store selection is uniform: explicit `--store` >
494
+ nonempty `AB_STORE` > repository default. When a phase or Harvest CLI
495
+ process reopens this store, a handle wrapper derives exact build-or-repository
496
+ authority and agent-event session attribution from its validated ambient
497
+ identity. The same wrapper applies when `ab builds`, `ab build status`, or
498
+ `ab artifact download` is invoked with a complete ambient phase identity;
499
+ without identity those read forms retain unscoped operator access. This
500
+ preserves the remote adapter's observable resource boundary without making
501
+ the SQLite adapter itself ambient-aware.
502
+ 2. **Remote** — the same store interface behind the independently deployed
503
+ hosted HTTP service, selected by an `http(s)://` reference. Kernels,
504
+ sandboxes, terminal dashboards, and later human-facing pages share this
505
+ service tier; it owns token authorization, protocol write validation,
506
+ artifact policy, and exact package/protocol version-skew enforcement. The
507
+ later human routes share its deployment so a browser or second service never
508
+ receives the machine signing secret. Artifacts remain content-by-value. The
509
+ documented [remote store protocol](docs/remote-store-protocol.md) is the
510
+ public BuildStore extension surface for independently implemented servers;
511
+ `packages/core/src/store/contract.ts`, driven through the shipped remote
512
+ client, is the conformance bar. Autobuild does not load in-process BuildStore
513
+ plugins. Git worktrees and default file tickets necessarily remain local.
514
+
515
+ `subscribe` is specced in the interface; the v2.0 implementation is polling
516
+ `getEvents(since)`. True push comes later.
517
+
518
+ ### 7.3 Persistence granularity
519
+
520
+ Required phase outputs persist at **phase/round boundaries**; a killed phase
521
+ re-runs from its start. An agent may also explicitly deposit a review artifact
522
+ mid-session, including an atomic PR-attachment designation (§7.5, §8.2); these
523
+ immutable revisions are harmless if a killed phase later retries. The
524
+ streaming exception reserved here is now the stream primitive (§7.6): live
525
+ output persists continuously while an agent runs, so a session that dies
526
+ mid-turn leaves its transcript behind instead of nothing.
527
+
528
+ ### 7.4 Resumption across sandboxes
529
+
530
+ Because state is the event log and artifacts live in the store, a *new*
531
+ sandbox can resume a build a dead sandbox started: pull events, rehydrate
532
+ scratch from latest artifact revisions, continue. v1 structurally could not
533
+ do this.
534
+
535
+ Four base-selection invariants make this safe with Git:
536
+
537
+ - **The base is chosen once, at first branch creation**, from the freshly
538
+ fetched origin tip of the configured base branch (fetched into a
539
+ build-scoped internal ref so concurrent dispatches cannot clobber each
540
+ other or the operator's refs). If the remote is unavailable, creation falls
541
+ back to the local base and records the diagnostic.
542
+ - **Re-provisioning never re-cuts.** An existing build branch is resumed at
543
+ its current tip — never rewound, rebased, or re-created from a newer base.
544
+ The first `workspace.provisioned` base remains immutable branch-cut
545
+ provenance; later `existing` facts are resume evidence, not new cuts.
546
+ - **Each implementation completion records the branch's effective target
547
+ divergence.** `ab done` asks the selected Forge to snapshot its authoritative
548
+ base when that capability exists; otherwise it fetches the frozen target
549
+ branch's current remote tip into a build-scoped private ref. It requires
550
+ exactly one merge-base with
551
+ the implementation `HEAD`. That merge-base and head become the durable
552
+ `implement.completed {commits}` range, so target history already absorbed by
553
+ the build is excluded while every branch-unique commit remains reviewable.
554
+ Resolution is refreshed across review rounds and sandbox resumption. Fetch,
555
+ ref, missing-ancestor, malformed-output, or ambiguous-base failure rejects
556
+ the terminal before push, artifact deposit, or completion event; there is no
557
+ stale fallback. This interrogation never moves the build branch, rewrites
558
+ history, writes `FETCH_HEAD`, or updates operator/remote-tracking refs.
559
+ - **Conditional verify keeps its own durable diff base.** Path applicability
560
+ still uses the initial branch-cut SHA until a completed reconcile promotes
561
+ that attempt's refreshed target. It does not consume the focused review base.
562
+
563
+ ### 7.5 What the PR gets
564
+
565
+ A summary comment — verdict history, verification results, links into the
566
+ store. The full audit trail is queryable, not committed to the branch.
567
+
568
+ Any agent session can explicitly designate an exact deposited artifact as a PR
569
+ attachment through `ab artifact put <kind> <file> --attach`. Designation is a
570
+ typed, atomic event on the ordinary artifact channel — never an artifact-name,
571
+ verify-step, or report-format convention. The current post-spec-restart
572
+ designation for each artifact kind is projected as text with an exact pinned
573
+ retrieval command; distinct kinds remain distinct. The BuildStore copy is
574
+ always authoritative, and non-image artifacts use this same path.
575
+
576
+ Optionally, `[pr.imageHost]` may name a **public** GitHub release so designated
577
+ `image/*` media render inline during review. The selected Forge must expose the
578
+ optional `PrAttachmentHosting` capability to serve upload and reclamation;
579
+ without it the supported path remains the complete text-only projection.
580
+ Non-images never cross that host boundary. Enabling it is an explicit
581
+ public-disclosure choice made in config, and hosted copies are temporary — the
582
+ dispatcher reclaims them after the build reaches a terminal outcome, while
583
+ store artifacts remain under the store's retention policy. Upload/validation/timeout failures record follow-up
584
+ observations and preserve the complete text projection; they never fail
585
+ verification or block finalize. Agents receive no forge credentials. A
586
+ designation after the PR exists publishes a new complete summary, so finalize
587
+ post-steps and post-reconcile verifiers need no custom phase.
588
+
589
+ ### 7.6 Streams (the third primitive)
590
+
591
+ The store's content model is three primitives, each doing one job:
592
+
593
+ - **Events** carry facts — small, typed, append-only, the routing spine.
594
+ - **Artifacts** carry bulk — content-addressed bytes deposited at boundaries.
595
+ - **Streams** carry the agent's running output — token deltas, tool calls and
596
+ results, reasoning — which is too bulky for the event log and arrives too
597
+ continuously to be an artifact until a session ends.
598
+
599
+ A stream is an append-only, per-stream sequenced log of **chunks** (a chunk is
600
+ a nonempty batch of protocol parts) with an open-then-closed lifecycle. The
601
+ chunk vocabulary is the Vercel AI SDK UI Message Stream protocol, version 1 of
602
+ the current major (AI SDK 7): the store holds the protocol's parts rather than
603
+ a private shape, so a read route can re-emit them as Server-Sent Events that
604
+ the AI SDK's own client hooks and any customer's tooling consume unchanged.
605
+ The store performs no protocol validation on append; each part only needs a
606
+ nonempty-string `type`, and the store assigns each batch its per-stream
607
+ sequence (from 1) and timestamp, so producers cannot fake ordering.
608
+
609
+ Closing a stream assembles its chunks into the protocol's `UIMessage[]`
610
+ document — dropping parts of undefined types while counting them — and
611
+ deposits that document as an artifact (`stream:<streamId>`, revision 0) on the
612
+ owning scope in the same atomic operation that marks the stream closed. A
613
+ stream that dies mid-turn stays open and readable, so its output survives.
614
+
615
+ Close serializes against append, per stream: an append issued while a stream
616
+ is being closed — or after it has closed — either lands in the finalized
617
+ artifact or is rejected with `StreamClosedError`; an accepted append is never
618
+ silently omitted from the artifact the close deposits. Appends to other
619
+ streams are unaffected.
620
+
621
+ Closing an already-closed stream is a no-op that returns the closed record.
622
+ The repeated close's outcome argument is ignored; the artifact the first
623
+ close deposited is left untouched.
624
+
625
+ **Rejections write nothing.** Append validation and every rejection below
626
+ happen before any mutation: a rejected operation leaves no chunk, no sequence
627
+ number, and no artifact. An append batch must be a nonempty array of JSON
628
+ objects, each carrying a nonempty-string `type`; a batch that fails this
629
+ shape is rejected with a plain error, and a batch whose serialized JSON
630
+ exceeds 1,048,576 bytes is rejected with `StreamBatchTooLargeError`.
631
+ Operations that address an unknown stream id — append, read, and close — are
632
+ rejected with a plain error; `getStream` alone answers `null` for an unknown
633
+ id, never an error. The contract is common to every shipped store — the
634
+ in-memory reference, the local SQLite adapter, and the remote Postgres-backed
635
+ service. Over the remote protocol the typed rejections cross the wire as
636
+ status codes — the ceiling as 413, the closed-append rejection above as 409,
637
+ the unknown stream as 404 — and the shipped client rehydrates the same error
638
+ types; `docs/remote-store-protocol.md` specifies the wire surface.
639
+
640
+ **Rejection precedence is transport-dependent, deliberately.** The store-side
641
+ adapters reject an append in the order part validation, batch-size ceiling,
642
+ unknown stream, closed stream: a malformed or oversized batch addressed to an
643
+ unknown stream id reports the validation error or `StreamBatchTooLargeError`,
644
+ never unknown-stream — the batch is validated and size-checked before the
645
+ per-stream critical section, and the unknown-stream lookup inside it precedes
646
+ the closed check. Over the remote protocol the server resolves and authorizes
647
+ the stream before parsing the request body, so the same request is rejected
648
+ `404 not-found` (`unknown stream "…"`) before the body schema or the ceiling
649
+ is evaluated. Both orders are deliberate: the 404-first order is the server's
650
+ no-existence-leak shape (an unknown stream and a foreign-scoped stream are the
651
+ same 404), while the local order keeps malformed input failing before any
652
+ per-stream synchronization. Writer-facing clients must therefore not assume a
653
+ single precedence across transports — a malformed batch on an unknown stream
654
+ legitimately rejects with different errors by transport — and both orders
655
+ reject before any mutation: no chunk, no sequence number, no artifact.
656
+
657
+ Chunk retention is deposit-path and count-based, like artifact retention: at
658
+ the next stream create in a scope, every previously closed stream's chunks
659
+ except the most recently closed are deleted; finalized artifacts are never
660
+ touched.
661
+
662
+ Streams are scoped to a build, to a repository, or to an operator session
663
+ (§7.1.1) — the scope vocabulary is closed. Reads are cursor-based
664
+ (`since` sequence) with an optional bounded wait (`waitSeconds`, whole
665
+ seconds) that returns as soon as a chunk lands or the stream closes — the
666
+ resumable live channel every frontend would otherwise invent for itself.
667
+ The wait is bounded at `MAX_STREAM_WAIT_SECONDS` = 30: a `waitSeconds` above
668
+ 30 clamps to 30, so a request to wait 61 seconds returns no later than 30.
669
+ Every store — the in-memory reference, the SQLite adapter, and the remote
670
+ client/server path — enforces the same clamp, and session-event reads
671
+ (§7.1.1) honor the identical rule so a poll loop cannot drift between the
672
+ two.
673
+
674
+ **Presentation, never routing.** No kernel, engine, reducer, or dispatcher
675
+ decision reads stream content; outcomes travel only the typed CLI. Writers
676
+ (agent runners emitting parts) and readers (operator UIs) are separate
677
+ tickets; this section defines only the primitive.
678
+
679
+ ## 8. The `ab` CLI
680
+
681
+ The **only** channel between agents and the store — and the enforcement
682
+ point of the entire ontology. Designed by walking every phase's session from
683
+ first command to terminal command, plus the failure paths (§8.7). Decisions
684
+ forced by the walkthrough are marked **[D5]–[D8]**, continuing §15's series.
685
+
686
+ It buys three properties at once:
687
+
688
+ 1. **Storage-agnostic skills** — local or remote store, same commands.
689
+ 2. **An enforced ontology** — the CLI validates schemas at deposit; verdicts
690
+ travel a typed channel, never parsed from stdout (the root-cause fix for
691
+ v1 sentinel parsing).
692
+ 3. **Structured observations at the point of capture**, not prose mined from
693
+ `observations.md` after the fact.
694
+
695
+ The kernel's job then reduces to: read the event log, decide the next phase
696
+ per the transition table, ask the AgentRunner to invoke the named skill with
697
+ its runtime-native syntax (`/{skill} {build}` for Claude/Pi, `$<skill> {build}`
698
+ for Codex), wait for the terminal event, repeat.
699
+
700
+ ### 8.1 Invocation model and ambient auth [D8]
701
+
702
+ The runner launches every session with environment set:
703
+
704
+ ```
705
+ AB_STORE # store URL or local path
706
+ AB_BUILD # build slug
707
+ AB_PHASE # current phase (+ round)
708
+ AB_SESSION # session id
709
+ AB_TOKEN # transports the scoped handle's authority to a remote store
710
+ ```
711
+
712
+ A harvest session instead carries `AB_REPO`, `AB_HARVEST`, and an `AB_PHASE`
713
+ of `synthesize@N` or `review@N`. The AgentRunner invocation argument is
714
+ opaque: a build skill receives its slug; a harvest skill receives its run id.
715
+ The CLI resolves identity from ambient auth. Every build process holds an
716
+ interface-enforced build-scoped Store handle. At the phase CLI opening boundary,
717
+ a remote handle forwards the runner-minted token unchanged, while a local handle
718
+ is wrapped with authority over exactly `AB_BUILD` or `AB_REPO`. Build and
719
+ repository resources cannot cross, and naming another resource to a Store
720
+ operation cannot widen either handle. Event-bearing writes attributed to an
721
+ agent must name the ambient `AB_SESSION`; non-agent writes remain allowed so the
722
+ CLI can perform its existing trusted kernel plumbing, including finalize.
723
+ Operator and kernel processes with no agent identity markers retain unscoped
724
+ Store access. Three read-only forms are also valid inside a phase: `ab build
725
+ status` and `ab artifact download` may read only `AB_BUILD`, while `ab builds`
726
+ is denied because collection access cannot represent one build. A complete
727
+ Harvest identity has repository scope and cannot use those build/admin reads.
728
+ If any build or Harvest identity marker is present, the corresponding
729
+ `AB_STORE`/resource/phase/session tuple must be complete, valid, and unambiguous;
730
+ partial, empty, malformed, shared-only, or mixed identity fails before Store
731
+ access rather than falling back to operator authority. `AB_STORE` and
732
+ `AB_TOKEN` alone remain operator selection and credentials, not identity.
733
+ Remote tokens transport the corresponding resource and session authority over
734
+ the wire; build-process resource scope is already enforced by the Store
735
+ interface. Least privilege comes from the Store and runner, not prompt
736
+ instructions.
737
+
738
+ Every phase and harvest turn also receives a runner-controlled `PATH` prefix
739
+ containing a private `ab` launcher from the same Autobuild distribution that
740
+ started the session, applied after ambient and scoped environment merging —
741
+ so a host executable named `ab` can never shadow the typed CLI, and agent
742
+ sessions need no separate global installation.
743
+
744
+ ### 8.2 Command surface
745
+
746
+ | Command | Purpose | Terminal? |
747
+ |---|---|---|
748
+ | `ab context` | hydrate `.ab/` with the phase's inputs; print the manifest | no |
749
+ | `ab artifact put <kind> <file> [--attach]` | deposit a versioned artifact → returns rev; optionally designate that exact revision for the PR | no |
750
+ | `ab artifact get <kind>[@rev]` | fetch an artifact within own build | no |
751
+ | `ab artifact download …` | read-only exact-byte retrieval; operator-wide without identity, own-build only with ambient phase identity; works after build termination | no |
752
+ | `ab observe --kind <followup\|refactor\|latent-bug> …` | structured observation, any phase, any time | no |
753
+ | `ab done` | complete a producer phase (validates, then runs phase plumbing) | **yes** |
754
+ | `ab verdict <approve\|revise\|escalate\|pass\|fail\|skip> …` | complete a review/verify phase | **yes** |
755
+ | `ab escalate <question>` | park the build for human input | **yes** |
756
+ | `ab mcp [--store <ref>] [--repo <id>]` | serve the agent tool registry as an MCP server over stdio; fails closed inside a phase | no |
757
+
758
+ The read-only `ab builds`, `ab build status`, and `ab artifact download` forms
759
+ require no session identity for operator use and preserve their repository-wide
760
+ results and store-selection precedence in that case. When a complete ambient
761
+ phase tuple is present, they retain its local or remote Store authority:
762
+ own-build status/download succeed, foreign and repository-scoped targets fail,
763
+ and `ab builds` fails at repository-wide `listBuilds` access rather than
764
+ filtering. Malformed ambient identity fails closed as defined in §8.1.
765
+
766
+ The verdict vocabulary is phase-dependent and the CLI enforces it: review
767
+ phases accept `approve|revise|escalate`; agent-verify steps accept
768
+ `pass|fail|skip` (`fail` requires a report; `skip` requires a reason).
769
+ Deterministic checks never touch the CLI — the kernel runs them directly.
770
+ Exact flags and validation live with the CLI implementation and its tests.
771
+
772
+ ### 8.3 What `ab context` materializes
773
+
774
+ A phase-scoped hydration into the gitignored scratch dir:
775
+
776
+ ```
777
+ .ab/
778
+ context.json # manifest: build, phase, round, artifact revs,
779
+ # required deposits, allowed terminal commands
780
+ spec.md
781
+ plan.md # per phase needs (see table)
782
+ findings.json # current feedback, when round > 1
783
+ history/ # prior-round artifacts where the phase needs them
784
+ verify/ # failure reports routed back to implement
785
+ ```
786
+
787
+ The manifest tells the agent its contract — `required` deposits and
788
+ `allowedTerminals` — so skills are self-checking against the same data the
789
+ CLI validates with. Per-phase inputs and terminals:
790
+
791
+ | Phase | Materialized inputs | Terminal |
792
+ |---|---|---|
793
+ | `plan` | ticket, spec; prior plan rev + findings (round > 1) | `done` (requires `plan` artifact) |
794
+ | `plan-review` | spec, plan@latest, all prior rounds' findings (for `persists` marking) | `verdict` (requires notes artifact) |
795
+ | `implement` | spec, approved plan, feedback (findings **or** verify report), own prior notes | `done` (requires clean worktree + notes) |
796
+ | `code-review` | spec, plan, commit range `{base, head}`, prior findings, implement-notes | `verdict` |
797
+ | `verify:<step>` (agent) | spec (acceptance criteria), step config, commit range | `verdict pass` \| `verdict fail --report` \| `verdict skip --reason` |
798
+ | `finalize` | spec, plan, verify reports, PR template config | `done` (requires `pr-description` artifact) |
799
+ | `reconcile` | spec, plan, implement-notes, conflict `{baseSha}` from this attempt's `reconcile.started` | `done` (requires merge commit present) |
800
+
801
+ Scoping is deliberate: the planner never sees code-review rounds; the
802
+ reviewer sees prior findings but not the producer's session. What a phase
803
+ *can't* see is part of its design.
804
+
805
+ ### 8.4 Terminal discipline [D5]
806
+
807
+ **Every phase ends with exactly one terminal command** — `done`, `verdict`,
808
+ or `escalate`. The CLI rejects a second terminal call, and each terminal
809
+ validates its preconditions before emitting the phase event (no `done`
810
+ without the required artifacts; no `done` on a dirty worktree in
811
+ `implement`). A normally completed turn that ends **without** any terminal
812
+ call is an infra failure: the runner emits
813
+ `phase.failed {error: "no-terminal"}` and applies retry policy.
814
+
815
+ Provider/runtime-declared turn failures are a separate `AgentRunner` result
816
+ (§9), never conflated with agent silence. When the selected role has declared
817
+ alternates, availability failures (overload, rate limit, 5xx, timeout,
818
+ transport, and unclassified provider failure) and provider exhaustion (quota,
819
+ usage limit, and billing) start the next target inside the same phase attempt.
820
+ Authentication, permission, and local runtime-configuration failures do not.
821
+ Each failed target's transcript is deposited. Only a stopped or exhausted chain
822
+ emits one `phase.failed` with the final provider message verbatim; that final
823
+ failure controls retry policy. Provider exhaustion skips the retry budget and
824
+ raises a policy escalation, while availability uses the existing bounded
825
+ budget. Every build-agent session also has a kernel-owned wall-clock budget,
826
+ resolved from its logical role and then `[policy].sessionBudgetSeconds`. Expiry
827
+ aborts the turn, ends any available handle, drops producer continuation state,
828
+ and emits retryable `phase.failed` with `phase session budget expired after
829
+ <seconds> seconds`; it is kernel policy and therefore does not select a provider
830
+ alternate. The existing phase-attempt guard retries and then raises an
831
+ answerable policy escalation. Operator cancellation and the captured deadline
832
+ remain independent safeguards: an operator request aborts the adapter promptly,
833
+ but the deadline still releases the wait if the adapter ignores cancellation.
834
+ When the operator request arrived first, that release remains an abort control
835
+ outcome and appends no phase failure or provider substitution. Agent finalize
836
+ post-step expiry remains failure-tolerant. Harvest sessions and direct check
837
+ commands are outside this budget. If the turn already wrote a valid typed
838
+ terminal, that terminal remains authoritative and no contradictory failure is
839
+ appended.
840
+
841
+ This completes the sentinel-parsing replacement: success is only expressible
842
+ through the typed channel, so "the agent rambled and exited" can never be
843
+ misread as completion, while a rejected turn cannot be misreported as agent
844
+ silence.
845
+
846
+ ### 8.5 Atomic deposits, validation as feedback [D6]
847
+
848
+ Terminal commands are **atomic bundles**: `ab verdict revise --findings
849
+ f.json --notes review.md` stores the notes artifact, validates and
850
+ id-stamps the findings, and appends the `*.verdict` event in one operation —
851
+ there is no state where an artifact exists without its event or vice versa.
852
+ Validation failures (malformed findings JSON, missing required artifact)
853
+ return the schema and a precise error to the agent *in-session*, so the
854
+ correction loop is immediate and cheap — schema errors are agent feedback,
855
+ not build failures.
856
+
857
+ ### 8.6 Agents never touch the remote [D7]
858
+
859
+ All `git push`, PR creation, and forge API calls happen kernel-side. `ab done`
860
+ in `implement` first establishes the focused target merge-base, then pushes
861
+ and records `{base, head}`; `ab done` in `finalize` has the kernel open the PR
862
+ using the deposited `pr-description` artifact. After a successful
863
+ content-producing `finalize:*` step, the runner
864
+ requires a clean worktree and a descendant local `HEAD`, then regular-pushes
865
+ the configured build branch and records that head before completing the step.
866
+ A no-op step has no push. Agents only ever commit locally. Consequences: forge
867
+ credentials **never enter the sandbox** (load-bearing once builds run on
868
+ remote sandboxes), history is extended rather than force-pushed, and the
869
+ push-at-boundary rule from [D3] is enforced by construction rather than
870
+ convention.
871
+
872
+ ### 8.7 Walkthroughs
873
+
874
+ **Happy path, one code-loop round-trip:**
875
+
876
+ ```
877
+ implementer: ab context → (work, commit) → ab observe --kind refactor "…"
878
+ → ab done --notes .ab/implement-notes.md
879
+ ⇒ validates clean worktree and focused review boundary,
880
+ pushes branch, emits implement.completed {commits, artifact}
881
+ reviewer: ab context (gets spec, plan, {base,head}, prior findings)
882
+ → ab verdict revise --findings f.json --notes review.md
883
+ ⇒ stamps finding ids, stores artifact,
884
+ emits code-review.verdict
885
+ implementer: ab context (findings.json now materialized) → …
886
+ ```
887
+
888
+ **Failure paths:**
889
+
890
+ - *Completed turn, no terminal* → `phase.failed {no-terminal}`, retry per
891
+ policy [D5].
892
+ - *Session budget expiry* → abort and best-effort `end`, then retryable
893
+ `phase.failed {error: "phase session budget expired after … seconds"}`; retry
894
+ from the primary and escalate under the existing attempt cap. If operator
895
+ abort already owns cancellation, the same deadline only bounds the wait and
896
+ the runner acknowledges abort without a phase failure [D5].
897
+ - *Provider rejection* → transcript deposited; eligible failures walk the
898
+ role's declared alternates within one attempt. A stopped/exhausted chain
899
+ emits one `phase.failed` with the final verbatim error and every tried target;
900
+ credentials/configuration stop immediately, final exhaustion escalates
901
+ instead of retrying, and final availability uses bounded retry [D5].
902
+ - *Malformed deposit* → rejected in-session with schema + error; agent
903
+ corrects and retries [D6].
904
+ - *Crash after deposits, before terminal* → artifacts are revisioned; the
905
+ re-run phase deposits fresh revs; orphaned revs are harmless history.
906
+ - *Wrong-resource access or wrong-session agent write* → the applicable
907
+ build-scoped or ambient-session Store handle rejects it at the interface;
908
+ a remote token enforces the same scope over the wire [D8].
909
+ - *Store unreachable* → CLI retries with backoff; a phase that cannot
910
+ deposit cannot complete → `phase.failed`, runner-level policy takes over.
911
+
912
+ ### 8.8 Outer-loop namespace
913
+
914
+ Human/pre-build ticket grooming uses one configured-source namespace
915
+ (`ab ticket create|update|block|unblock|list|show|move`). These commands are
916
+ sessionless, source-agnostic, and never available as a mid-build spec
917
+ mutation path. Every subcommand accepts `--json` and then writes exactly one
918
+ bare value: `list` emits `Ticket[]`; every other form emits the complete
919
+ created, read, or post-mutation `Ticket`. Human-readable confirmations are not
920
+ a data format and callers must not parse ids from them.
921
+
922
+ `block` and `unblock` accept a comma-separated blocker-id list. The CLI
923
+ first deduplicates and validates the complete list, including blocker existence
924
+ for both operations and direct self-block rejection when adding, then applies
925
+ the existing idempotent one-edge port methods; invalid input writes no edge.
926
+ Dependencies gate only the dispatcher's claim. Changing blockers after a ticket
927
+ has already been claimed does not stop its active build. A repository that
928
+ creates tickets directly in its ready state must therefore create a dependency
929
+ chain in dependency order, carrying each predecessor id into the next create,
930
+ before a later ticket can be claimed.
931
+
932
+ Observation harvest uses a separate typed, repository-scoped namespace
933
+ (`ab harvest context|submit|verdict|status`), mirroring the build session
934
+ commands: context hydration, a producer terminal, a reviewer terminal, and a
935
+ sessionless read-only status projection.
936
+
937
+ `ab repository status [--json] [--store <ref>]` is the sessionless read-only
938
+ projection of the repository journal's dispatcher controls: ticket intake, the
939
+ repository-wide pause, and the claim-time auto-merge default. It uses the same
940
+ dispatch-settings reducer as dispatcher decisions and the dashboard. An absent
941
+ repository stream reduces as an empty journal and reports intake on, repository
942
+ pause off, and auto-merge default off; the query does not create that stream,
943
+ append an event, claim a ticket, attach a runner, or start dispatcher work.
944
+
945
+ Agents never receive TicketSource credentials. Only the deterministic file
946
+ step creates/adopts approved proposals and commits ledger facts.
947
+
948
+ ### 8.9 The agent tool registry
949
+
950
+ `packages/core/src/operator/registry.ts` is the agent tool surface: one typed,
951
+ closed table of tools — each naming the tool, describing it for a model,
952
+ declaring its input schema and MCP annotations, carrying an approval class, and
953
+ binding a handler that calls the same operator services the operator API routes
954
+ call. It is the single source of truth for the agent-facing operator surface,
955
+ and every binding is generated from it; the table never widens beyond the
956
+ operator services, and a contract suite proves, tool by tool, that each
957
+ handler's result equals the corresponding route's.
958
+
959
+ The shipped binding is `ab mcp`, a stdio MCP server over the registry: the CLI
960
+ remains the primary local interface for agents that have a shell, while the
961
+ binding exists so the registry can be exercised end to end from a local
962
+ install, for dogfooding, and for local agents that cannot run commands. It
963
+ fails closed inside a phase session — repository-wide operator authority cannot
964
+ be narrowed to the ambient build — and `--repo` constrains every call to one
965
+ repository. Mutating tools attribute their writes to the sessionless operator
966
+ identity, exactly as the other operator commands do.
967
+
968
+ ## 9. AgentRunner
969
+
970
+ Session-based, because review loops need memory:
971
+
972
+ ```ts
973
+ type Result =
974
+ | { kind: 'completed', text, usage }
975
+ | { kind: 'failed', text, usage,
976
+ failure: { message, permanent, cause?: 'availability' | 'exhaustion' |
977
+ 'credentials' | 'configuration' } }
978
+
979
+ interface AgentRunner {
980
+ start(opts: { skill, invocation, workspace, model, … }): { session, result }
981
+ continue(session, message): Result // review-loop rounds
982
+ end(session): Transcript // → store, always
983
+ }
984
+ ```
985
+
986
+ The discriminator is a port-level requirement: an SDK/provider-declared error
987
+ must never be returned as `completed`, so every adapter inherits the
988
+ distinction from `no-terminal`. `failure.message` preserves provider text and
989
+ `cause` separates availability, provider exhaustion, credential/permission,
990
+ and local runtime configuration. `permanent` remains the retry-policy and
991
+ legacy-plugin compatibility bit: exhaustion and credential/configuration
992
+ failures are permanent, while availability uses existing bounded retry. A
993
+ legacy plugin may omit `cause`; `permanent: false` remains alternate-eligible
994
+ and `permanent: true` remains a stopping failure. A failed `start` still returns
995
+ an endable session handle, guaranteeing transcript deposition.
996
+
997
+ Narrow non-phase judgments do not widen this contract. A runtime may
998
+ separately register an optional one-shot completion capability (tool-free,
999
+ non-resumable), used for slug naming (§6.3) and vendored-skill conflict
1000
+ resolution (§16.3). A runtime without it is valid; each caller owns its
1001
+ deterministic fail-safe.
1002
+
1003
+ - **Adapters:** three builtins sit behind the interface: the locally installed
1004
+ Claude Code CLI in headless mode for Claude models; the locally installed
1005
+ Codex CLI for unqualified `gpt-*` models; and the locally installed pi CLI
1006
+ (minimum version 0.84.3), driven headlessly through `--mode rpc`, for every
1007
+ provider-qualified model that installation resolves. Each adapter uses its
1008
+ CLI's local authenticated login; pi also owns its local model catalog and
1009
+ keeps one ephemeral RPC process alive across review-loop continuations. All
1010
+ three provide tool-free one-shot completion. Plugins may register additional
1011
+ adapters, each under a *distinct runtime name*, never
1012
+ as a mode flag on an existing one. Plugin adapters must pass the exported
1013
+ AgentRunner contract suite.
1014
+ - **Routing — explicit role inheritance (§16.1):** runtime, model, extra CLI
1015
+ arguments (`args`), and build-session budget live in one open `[roles]` map whose
1016
+ reserved `default` entry is the inheritance base and must explicitly name a
1017
+ runtime. The budget inherits independently, then falls back to
1018
+ `[policy].sessionBudgetSeconds`; it belongs to the logical role, so provider
1019
+ alternates cannot override it.
1020
+
1021
+ **Which key a session selects.** One rule for both kinds of agent step: an
1022
+ agent verify step and an agent finalize step each select `[roles.<step>]` by
1023
+ their *logical step name*; core phases select `[roles.<phase>]`.
1024
+
1025
+ ```toml
1026
+ [verify.e2e]
1027
+ kind = "agent"
1028
+ skill = "verify-app-e2e" # repository-authored skill
1029
+
1030
+ [roles.e2e] # the STEP name — not "verify-app-e2e"
1031
+ runtime = "pi"
1032
+ ```
1033
+
1034
+ The step's configured skill name remains a deprecated alias for existing
1035
+ configurations and will be removed in a future release. It is consulted only
1036
+ when `[roles.<step>]` is undeclared, so the step name always wins.
1037
+
1038
+ **What is and is not a fallback**, stated separately so the mechanisms are
1039
+ not conflated:
1040
+
1041
+ 1. Primary field resolution never hunts for a compatible substitute. Every
1042
+ concrete role merges runtime, model, and `args` over `default`
1043
+ independently; an incompatible merged pair is an eager error. A declared
1044
+ `args` list, including `[]`, replaces the inherited list wholesale.
1045
+ 2. A **requested** role key that no `[roles.<key>]` declares resolves to
1046
+ `[roles.default]` wholesale. That is undeclared-key fallback, not outage
1047
+ routing.
1048
+ 3. A role may declare `alternates = [{ runtime?, model?, args? }, …]`.
1049
+ The list is independently inherited: a role's own list,
1050
+ including `[]`, replaces the default list wholesale. Each entry overlays
1051
+ that concrete role's effective primary axes and is resolved and validated
1052
+ eagerly with indexed problems in the aggregate startup error.
1053
+ 4. A **declared** key that nothing ever requests is resolved, validated, and
1054
+ then never used. `ab dispatch` reports it at startup as a warning.
1055
+
1056
+ Each `args` string is one argv token, preserved in order and appended to
1057
+ every phase and tool-free one-shot invocation for that role. Adapters reject
1058
+ only model selection and the machine-readable mode they emit and parse:
1059
+ Claude owns `-p`/`--print`, `--output-format`, and `--model`; Codex owns
1060
+ `--json` and `--model`/`-m`; Pi owns `--mode` and `--model`. An effective
1061
+ primary or alternate list containing one (including `--option=value` or a
1062
+ documented compact short alias) joins the aggregate eager startup error.
1063
+ Every other option passes through without Autobuild validation, even when it
1064
+ changes sandbox, approval, session, tool, skill, thinking, extension, or
1065
+ lifecycle behavior or supplements another adapter-emitted option. Pi always
1066
+ disables ambient extension discovery and explicitly loads the Autobuild
1067
+ bridge; a role can add repeatable `--extension <path>` tokens through `args`,
1068
+ and all tools from that explicitly loaded extension are available alongside
1069
+ the bridge. With no such args, Pi loads no operator-installed extensions.
1070
+ The deprecated `extensions` field remains schema-valid but has no effect and
1071
+ produces a startup warning directing the operator to `args`.
1072
+
1073
+ All roles
1074
+ resolve **eagerly, before any session launches**, with problems aggregated
1075
+ into one error. A missing default diagnostic includes a copyable table and
1076
+ every materialized runtime name. Builtin and plugin registrations use the same model-family,
1077
+ default-model, session, and optional one-shot capability path; adding a
1078
+ runtime touches only the adapter registry, never the kernel. Mixing models
1079
+ across roles is intentional — a different reviewer catches more. The
1080
+ Within one agent-session attempt, eligible failures try the primary then each
1081
+ alternate in order without spending another phase attempt. Each target has a
1082
+ fresh session bracket. A failed producer continuation may therefore continue
1083
+ on a different runtime only by rehydrating durable context, not by inheriting
1084
+ the failed conversation. Selection is non-sticky: every later phase, loop
1085
+ continuation, and retry starts from the primary. The final failure controls
1086
+ retry/permanent behavior. This applies to core phases, agent verify/finalize,
1087
+ and Harvest, but not tool-free one-shots. Every selected runtime/model/args
1088
+ tuple is recorded on `session.started`; fallback starts cite the preceding target and
1089
+ verbatim error, while exhausted `phase.failed`/`harvest.failed` facts carry
1090
+ the complete ordered attempt list.
1091
+ - **Transcripts come back through the interface**, not scraped from disk, so
1092
+ every adapter must produce one whenever its owning runner reaches the session
1093
+ end boundary, including turns rejected by a provider after a handle exists.
1094
+ A runner process that dies before deposition is the explicit exception: a
1095
+ takeover records transcriptless reclamation rather than fabricating corpus
1096
+ bytes it cannot recover.
1097
+ - **Session streams (the live view).** While a session runs, its runner
1098
+ translates the harness's native output into AI SDK UI Message Stream parts
1099
+ (`ai-ui-message-stream/v1`) and appends them to a session stream (§7.6) as
1100
+ they happen — the live counterpart of the transcript guarantee above. The
1101
+ stream sink is a builtin-runner responsibility expressed as an optional
1102
+ `openSessionStream` capability on the runtime registration (beside the
1103
+ optional one-shot capability; the frozen `AgentRunner` port is untouched):
1104
+ the build-runner creates a store-backed sink per session bracket, opens the
1105
+ stream before the session's first turn with label `session:<sessionId>`
1106
+ keyed to the Autobuild session id (never a harness-native session, thread,
1107
+ or runtime id), appends `session.started` carrying the returned stream id,
1108
+ and closes the stream at the session end boundary — before the
1109
+ `session.ended` event lands — with outcome `completed` when the final turn
1110
+ completed, `aborted` when it failed or was cancelled. A session that ends on
1111
+ a substituted runtime keeps the stream opened for its own bracket. The
1112
+ harvest runner does the same for its synthesize and review session brackets
1113
+ with the identical protocol and part vocabulary: streams are scoped
1114
+ `{ kind: 'repo', repo }` instead of build-scoped, `harvest.session.started`
1115
+ carries the returned stream id, `phase` is spelled `harvest:<step>`, each
1116
+ revise round is its own bracket and its own stream, and a closed stream
1117
+ finalizes onto the repository scope. Harvest journal events
1118
+ (`harvest.step.*`, `harvest.proposals.submitted`, `harvest.review.verdict`,
1119
+ and their kin) are deliberately not translated into parts — they are typed
1120
+ journal facts, observable via `ab watch --repository`, not harness output.
1121
+
1122
+ The part vocabulary: the stream's first part is always a `data-ab-session`
1123
+ part naming the session id, role, runner, model, phase, and round; each
1124
+ turn's prompt (the skill invocation on turn 1, the continuation message on
1125
+ later rounds) is a `data-ab-prompt` part before that turn's output; while a
1126
+ turn runs the runner emits `start-step`/`finish-step` around each harness
1127
+ step, assistant text as `text-start`/`text-delta`/`text-end` at the finest
1128
+ granularity the harness exposes (partial-message deltas for Claude Code and
1129
+ Pi, whole messages where the harness only reports completed messages),
1130
+ reasoning parts when the harness exposes reasoning,
1131
+ `tool-input-available` and `tool-output-available` per tool call, and
1132
+ `finish` on completed turns. A turn that ends in a provider or runtime
1133
+ failure emits an `error` part before the stream closes; a turn cancelled by
1134
+ the caller emits an `abort` part. Text, reasoning, prompt, and tool
1135
+ payloads serialized above 65,536 UTF-8 bytes are truncated on a UTF-8
1136
+ boundary and followed by a `data-ab-truncation` part naming the omitted
1137
+ byte count. Parts reach the store within one second of the harness emitting
1138
+ them under normal conditions: the runner batches appends on a short cadence
1139
+ rather than one request per delta.
1140
+
1141
+ Streams are presentation, never routing: no kernel, engine, reducer, or
1142
+ dispatcher decision reads stream content or presence, and the phase outcome
1143
+ still travels only the typed CLI. A failing stream path never fails a turn:
1144
+ an append error retries on the next batch, a writer that keeps failing
1145
+ stops streaming the session, closes the stream `aborted` when it can,
1146
+ reports once on the build runner's diagnostics, and the phase proceeds
1147
+ exactly as it would without streams; the transcript artifact is deposited
1148
+ regardless. A plugin runtime that declares no capability yields sessions
1149
+ with no stream, and every consumer treats those sessions exactly as before;
1150
+ tool-free one-shot completions produce no stream. A closed session's
1151
+ finalized document is the `stream:<id>` artifact (§7.6).
1152
+ - Adapters without native session resumption implement `continue` as
1153
+ start-with-rehydrate-from-store — which must exist anyway per §7.4.
1154
+
1155
+ ## 10. The review loop (`converge`)
1156
+
1157
+ One generic primitive, used for the plan loop, code loop, and harvest's
1158
+ synthesize/review loop:
1159
+
1160
+ ```ts
1161
+ converge<A>(
1162
+ produce: (feedback: Feedback | null) => Promise<A>, // planner | implementer
1163
+ review: (artifact: A) => Promise<Verdict>, // plan-reviewer | code-reviewer
1164
+ policy: { maxRounds, escalateOn, reviewerRunner, … }
1165
+ // Feedback = findings from a verdict, or a verify failure report (§5)
1166
+ ): Approved<A> | Escalated
1167
+ ```
1168
+
1169
+ - **Structured verdicts** (`approve | revise(findings) | escalate(reason)`),
1170
+ deposited via `ab verdict`. Findings are structured (file refs,
1171
+ description, severity) so round N+1's producer prompt is assembled
1172
+ deterministically, not "here's what the reviewer said."
1173
+ - **Memory model:** producer *continues* its session across rounds; reviewer
1174
+ gets a *fresh* session each round (a fresh skeptic catches more). Both are
1175
+ policy knobs.
1176
+ - **Anti-stall rule:** new findings round-over-round is normal; the *same*
1177
+ finding surviving N rounds is a disagreement between two agents and
1178
+ auto-escalates rather than burning rounds (mechanics in §15.4: the
1179
+ reviewer marks persistence — judgment; the kernel applies the threshold —
1180
+ determinism).
1181
+ - **Round ceiling:** `policy.maxReviewRounds` is the default absolute cap for
1182
+ each loop. Once its `review-round-limit` escalation is open, a human may
1183
+ answer it with an optional positive `reviewRoundCeiling`. That value replaces
1184
+ the configured cap for only the escalation's plan or code loop and current
1185
+ spec revision. It is absolute, durable, and cleared by `spec.revised`; the
1186
+ stall check retains precedence.
1187
+
1188
+ ## 11. Escalation
1189
+
1190
+ An escalation is an **event**, answerable from any UI — not a file to go
1191
+ find. When a build parks, the operator surface shows it among the blocked
1192
+ builds with the question and an answer channel (an `escalation.answered`
1193
+ event — commands are events, §15.2.7). The durable record is in the store
1194
+ like everything else. An answer carries either bare `retry` or free-text
1195
+ `guidance` that feeds the parked phase's next run; answering is an attempt,
1196
+ not a forced success — an unresolved condition may escalate again. For an
1197
+ agent verifier's own `ab escalate`, that next run is the same `verify:<step>`:
1198
+ `verify.started.feedback` cites the answer and `ab context` materializes it as
1199
+ `.ab/guidance.json`. The citation remains the durable carrier across any crash
1200
+ before launch; only a later `session.started` with the same phase and attempt
1201
+ consumes it. Producer guidance uses the same phase-and-round launch boundary. A
1202
+ bare retry reruns the step without guidance. The intentionally different policy
1203
+ escalation after an exhausted failed verify report feeds `implement`, where its
1204
+ guidance outranks
1205
+ the pending report.
1206
+
1207
+ Only an operator answering an open `review-round-limit` policy escalation may
1208
+ set `escalation.answered.reviewRoundCeiling`. The same event both clears the
1209
+ human gate and replaces the cap; no standalone or pre-emptive form exists. A
1210
+ ceiling cannot accompany a spec revision because `spec.revised` resets both
1211
+ loops to the configured default. The value is attached only to the matching
1212
+ review-limit answer, so unrelated blockers answered in the same command do not
1213
+ acquire it and agents cannot widen their own build's loop.
1214
+
1215
+ Every open escalation remains a human judgment gate until an operator answers
1216
+ it. This includes all policy and setup causes. Starting either `ab dispatch` or
1217
+ `ab dispatch --once` never appends `escalation.answered` and never re-arms a
1218
+ budget. Startup still recovers every actionable current build and delivers
1219
+ pending operator commands, whose decisions take precedence over a blocked wait;
1220
+ an otherwise blocked build is reported as parked and left unchanged.
1221
+
1222
+ Crash-gap and exhaustion deduplication is exact to the escalation's source/class,
1223
+ target, and any durable scope that distinguishes policy conditions. Every new
1224
+ policy-authored `escalation.raised` names a closed `policyCause`; `round` scopes
1225
+ the occurrence but does not identify the policy class. A triggering event is
1226
+ considered acknowledged only by a later raise matching those dimensions;
1227
+ another cause, target, or required scope cannot suppress it. The triggering
1228
+ event's sequence is the boundary, so newer qualifying failure, verdict, or
1229
+ conflict evidence re-arms that exact condition after an answer.
1230
+
1231
+ Occurrence scope and phase-runner failure-budget reset scope are separate.
1232
+ Answering `phase-attempt-limit` or `non-retryable-phase-failure` re-arms only
1233
+ the matching phase and `round`; a missing round cannot broaden either cause.
1234
+ `review-round-limit` retains the same matching-round effect.
1235
+ `verify-failure-limit` is the explicit phase-level case and re-arms every
1236
+ phase-runner failure round for its verify target. `reconcile-no-progress` and
1237
+ `setup-failure-limit` have no phase-runner failure-budget effect (setup's own
1238
+ answer/retry accounting remains separate). Agent and stall raises, and
1239
+ cause-less historical policy raises, retain the legacy shape rule: a matching
1240
+ round resets that round and an absent round resets all rounds for the target.
1241
+ The closed `policyCause` union has an exhaustive reset-scope mapping, so adding
1242
+ a policy cause requires choosing matching-round, all-round, or no-reset
1243
+ semantics rather than acquiring behavior by omitting `round`.
1244
+
1245
+ Reconcile has two distinct policy scopes. Runner retry exhaustion and
1246
+ non-retryable failures at progress-check or base-refresh boundaries are scoped
1247
+ to a concrete reconcile `round`. Unchanged-base no-progress exhaustion is
1248
+ scoped to the current conflict, omits `round`, and has `policyCause =
1249
+ "reconcile-no-progress"`. Only a later open or answered raise with that cause
1250
+ acknowledges the no-progress guard; an explicitly different cause does not,
1251
+ even if it is also roundless. Cause-less historical roundless
1252
+ `policy`/`reconcile` raises retain their former no-progress meaning on replay,
1253
+ without migration. Routing uses these typed event fields and never parses the
1254
+ human-facing `question`.
1255
+
1256
+ ## 12. The outer loop
1257
+
1258
+ ```
1259
+ signals (telemetry, observations)
1260
+ → ingest:* / harvest (signal source → dedup ledger → cluster/synthesize → propose)
1261
+ → Triage (proposals)
1262
+ → groom (the human gate — named in the ontology, no skill)
1263
+ → Ready
1264
+ → dispatch (claim → spec import/author → name → workspace → launch build-runner)
1265
+ → build → PR → merge
1266
+ ```
1267
+
1268
+ Additional scheduled ingesters such as `ingest:sentry` remain an open design
1269
+ thread and have no shipped config surface. Observation harvest is
1270
+ pressure-driven: on the existing one-pass scan of repository build streams, the
1271
+ dispatcher measures both unclaimed observation count and repository drift. A run
1272
+ starts when count reaches `[policy].harvestThreshold` **or** when the number of
1273
+ same-repository `pr.merged` facts strictly after the oldest unclaimed
1274
+ observation reaches `[policy].harvestMaxDrift`, whichever happens first. Drift
1275
+ counts distinct other builds only: the build that recorded the oldest
1276
+ observation is excluded even if it later merges, and aborted or closed-unmerged
1277
+ builds contribute no `pr.merged` fact. The drift limit defaults to `3`; zero
1278
+ disables that condition and restores count-only gating. Empty accumulations
1279
+ never trigger. Either condition claims the whole current accumulation as one
1280
+ immutable snapshot, including newer observations, and `harvest.started` records
1281
+ `count`, `drift`, or `both` as durable trigger provenance. Occurrence identity
1282
+ is `{build slug, event seq}` — never payload id or a scalar high-water mark,
1283
+ because event sequences are per build. Harvest state lives in the repository
1284
+ journal, separate from build streams; the repository lease is the cross-process
1285
+ exclusivity gate, and harvest runs fire-and-forget so dispatch ticks stay
1286
+ responsive.
1287
+
1288
+ The fixed workflow is:
1289
+
1290
+ 1. **scan (deterministic)** — subtract all claimed occurrences, reconcile
1291
+ prior proposal tickets through TicketSource lifecycle facts, expose each
1292
+ distinct observation-origin ticket's source match, existence, and resolution
1293
+ state, and atomically store the scan packet with the run's claim.
1294
+ 2. **synthesize ⇄ review (judgment through `converge`)** — the continuing
1295
+ producer clusters same-problem records and authors typed
1296
+ create/join/suppress proposals. A create may carry source-local `blockedBy`
1297
+ ids when its evidence establishes a hard prerequisite other than the
1298
+ automatic observation-origin relationship; contextual references and
1299
+ nonbinding ordering do not become blockers. A fresh reviewer checks
1300
+ coverage, semantic dedup, spec quality, evidence, and both directions of
1301
+ that prerequisite rule. Only approval advances.
1302
+ 3. **file (deterministic)** — refresh the selected TicketSource lifecycle for
1303
+ every declared blocker and same-source ticket that originated a create's
1304
+ clustered observations. Unknown agent-declared ids fail filing; missing,
1305
+ resolved, foreign-source, and absent origins contribute nothing. The created
1306
+ ticket receives the first-seen deduplicated union of declared blockers and
1307
+ still-existing unresolved origins. The filing fact and report preserve those
1308
+ two provenance sets separately. Creates are rendered to the spec standard
1309
+ and filed into `[tickets].proposalState`, Triage by default, with the reserved
1310
+ `autobuild:proposal` provenance label.
1311
+ Filing is crash-safe by construction: an idempotency ID is durably reserved
1312
+ *before* each external create, so a restart adopts the already-created
1313
+ ticket instead of duplicating it, and a partially filed approved set creates
1314
+ only its missing tickets. Ready projections expose that stable external
1315
+ creation key separately from the human-facing ticket id. Dispatch brackets
1316
+ each ready listing with repository-journal reads and withholds a ticket when
1317
+ its key matches an unfinished reservation, including a reservation first
1318
+ observed during the listing interval. The ticket remains counted as queued,
1319
+ consumes no capacity, and receives a distinct durable standing diagnostic.
1320
+ A later tick releases it after filing settles or the owning run completes,
1321
+ escalates, fails non-retryingly, or exhausts recovery; tickets without a
1322
+ matching reservation are unchanged. The label is informational: Autobuild never reads
1323
+ it as readiness policy and does not remove it during grooming.
1324
+
1325
+ Beyond the workflow, harvest is governed by a small set of invariants (their
1326
+ event-level mechanics live in the repository catalog and reducer tests):
1327
+
1328
+ - **Pause is a repository-wide durable gate**, not a run status — requested
1329
+ by a human, acknowledged by the kernel at a safe boundary, and never
1330
+ destructive to the open run or its claim.
1331
+ - **A failed run parks; settling parked runs outranks new scans.** The
1332
+ dispatcher settles the oldest outstanding recoverable run before starting
1333
+ any new scan, so later runs cannot shadow an older stop.
1334
+ - **Automatic recovery is bounded per run** (two reopens, no config surface),
1335
+ separate from within-step retry policy. Completed steps never re-run; an
1336
+ approved set goes straight to filing.
1337
+ - **Give-up never silently destroys the snapshot.** Exhaustion atomically
1338
+ commits the provable partial dispositions to the ledger, releases only
1339
+ genuinely pending work, and raises a durable human-attention barrier so
1340
+ released work cannot be immediately reclaimed into another hot loop.
1341
+ - **Human resume is repository-wide**: one acknowledgement reopens every
1342
+ ordinary parked run and clears every exhaustion barrier. It never
1343
+ resurrects a terminal run. Completed and escalated runs are irrevocable; a
1344
+ deliberate escalation consumes its snapshot and is never auto-recovered.
1345
+ - **The harvester only proposes.** It never claims, grooms, or dispatches a
1346
+ proposal, and it files every one into the same configured state rather than
1347
+ ranking them. Humans own Triage → Ready by default. A repository that points
1348
+ `[tickets].proposalState` at its ready state has waived that gate for itself:
1349
+ every harvested proposal enters the ordinary dispatch eligibility checks
1350
+ unread, on the strength of the synthesize ⇄ review loop and the spec gate.
1351
+ An unresolved harvested `blockedBy` relationship still prevents claim until
1352
+ the source reports completion or the relationship is deliberately removed.
1353
+ The waiver is a field of
1354
+ its own rather than a reuse of `triageState`, because bounces, aborts, and
1355
+ closed-unmerged PRs must still land where the next tick will not reclaim
1356
+ them — a bounce filed into Ready is claimed and bounced again forever.
1357
+
1358
+ ## 13. Ticket source policy
1359
+
1360
+ The TicketSource **initiates and receives projections; it is never consulted
1361
+ mid-build and never used as artifact storage.** Dispatch reads the ticket
1362
+ (including the spec) as part of initiation — at claim time, or on a later tick
1363
+ while an interrupted dispatch still lacks its imported spec. After import, the
1364
+ build never reads the tracker again. Human-legibility projections (spec
1365
+ posted as a comment, final summary, status transitions) flow outward only.
1366
+ This keeps the abstraction honest: a file-based TicketSource with nowhere to
1367
+ put blobs must be fully workable.
1368
+
1369
+ **Claim lifecycle.** Claim acquires nonterminal work: it returns `true` and
1370
+ moves a claimable ticket to the source's claimed state. It returns `false` for
1371
+ an unknown ticket, an already-claimed or contested ticket, and a ticket that
1372
+ the source considers terminal. Refusing a terminal ticket performs no state
1373
+ transition and never implicitly reopens completed work. An explicit transition
1374
+ back to a nonterminal state makes a later claim possible again.
1375
+
1376
+ **Partial listings and source invariants.** A listing returns both valid
1377
+ tickets and diagnostics for individually malformed records, which are
1378
+ excluded but left byte-untouched — one broken ticket never blocks unrelated
1379
+ dispatch. Tracker-wide safety violations (duplicate ids across states,
1380
+ stateless records) remain fatal: continuing could permit double dispatch.
1381
+
1382
+ **Pre-build edits.** Update is partial and strict: it replaces only the named
1383
+ editable fields, and state is never an update field — transitions are a
1384
+ separate operation with their own validation. Labels are values at the port
1385
+ boundary: a label name need not already exist for either create or update.
1386
+ Adapters own reconciliation with provider-side label registries, including
1387
+ concurrent attempts to create the same name; callers never pre-register labels.
1388
+
1389
+ **Ticket dependencies.** A ticket may declare blockers within its source, at
1390
+ creation or later. The source owns representation (how a blocker is stored)
1391
+ and completion semantics (what "done" means); the dispatcher owns the
1392
+ decision — an unresolved blocker means the ticket is not claimed and creates
1393
+ no build. Dependencies are written during grooming or by an approved harvest
1394
+ create, then read at dispatch time. Harvest validates agent-declared source-local ids
1395
+ and automatically derives unresolved same-source observation origins through
1396
+ fresh configured-source lifecycle reads before creating the proposal ticket.
1397
+ The scan packet exposes the earlier informational lifecycle view to producer
1398
+ and reviewer, while the filing read is authoritative. Both paths remain
1399
+ initiation, so the never-consulted-mid-build rule is untouched. A
1400
+ dependency-blocked ticket stays
1401
+ queued source work rather than becoming a blocked build: the runtime `blocked`
1402
+ status is for builds awaiting a human.
1403
+
1404
+ **Crash-safe filing.** Creation supports a state override (harvest targets
1405
+ Triage explicitly) and an idempotency key that must adopt the same ticket on
1406
+ retry across process restarts. Ticket projections may carry this stable
1407
+ external creation key separately from their source-facing identifier; adapters
1408
+ that support idempotent creation preserve it through create, adoption, get, and
1409
+ ready listing. The reservation fact precedes the external side effect — that
1410
+ ordering, not provider behavior, is what makes filing crash-safe. Dispatch
1411
+ uses the key only to withhold unfinished Autobuild creates; its absence fails
1412
+ open for legacy/plugin tickets and introduces no new readiness gate.
1413
+
1414
+ ## 14. Operator UI
1415
+
1416
+ The UI layer is defined by the seam, not any implementation: **subscribe to
1417
+ events, render, send commands** — commands being events appended to the
1418
+ applicable build or repository log (§15.2.7). The event vocabulary *is* the
1419
+ UI API; forge mutation remains kernel/dispatcher plumbing. Anything a UI
1420
+ displays is a reduction of the logs, and anything it does is an event — so
1421
+ every frontend (terminal today, web later) is the same adapter pattern
1422
+ against the same store, and a dead runner still receives commands on resume.
1423
+
1424
+ Interactive `ab dispatch` owns the terminal in a frontend process and supervises
1425
+ one private child containing the dispatcher, build runners, Harvest, config and
1426
+ plugin loading, and every ticket/forge/workspace/runtime adapter. The frontend
1427
+ has no handle on those adapters: it reads build and repository streams plus
1428
+ referenced artifacts, writes operator commands, and keeps only presentation
1429
+ state (selection, scroll, composer text, confirmation) in memory. The child
1430
+ publishes a run-correlated effective-config artifact before its first tick and
1431
+ records tick/queue diagnostics, reload outcomes, runner coordination, and
1432
+ normal/abnormal lifecycle as repository facts. Thus a rejected on-disk reload
1433
+ cannot change the displayed effective capacity, and a frontend restart can
1434
+ reconstruct every operational notice from the Store. Observation pressure is a
1435
+ separate display-only reduction: the interactive frontend scans the shared
1436
+ BuildStore directly, pairs the current unclaimed count with the effective
1437
+ config's `policy.harvestThreshold`, and retains the last successful count while
1438
+ showing a refresh diagnostic after a failed read. It appends no event and does
1439
+ not use `dispatcher.tick-completed` to transport the sample; the headless child
1440
+ therefore runs ticks without a dashboard, terminal, or frontend connection.
1441
+ Before the first successful sample the frontend renders only the diagnostic,
1442
+ never a fabricated zero. Non-interactive dispatch performs no frontend-only
1443
+ sampling. Repository polling advances from the last observed sequence rather
1444
+ than replaying the growing journal on every frame. SIGINT, SIGHUP, SIGQUIT, and
1445
+ SIGTERM restore terminal modes immediately and ask the child to stop; repeated
1446
+ signals remain graceful while a dispatcher tick is crossing the ticket-claim
1447
+ boundary. The frontend reaps the child before finishing, and for SIGHUP,
1448
+ SIGQUIT, or SIGTERM only then restores the default disposition by re-signalling
1449
+ itself. A child also stops when its terminal-owning parent dies. A timed-out
1450
+ child is terminated unless such a tick is open, in which case that tick first
1451
+ reaches a recoverable durable boundary. The run-stopped fact and frontend result
1452
+ distinguish a graceful drain, an actual forced termination requested by the
1453
+ operator, and an unsolicited abnormal exit, retaining available exit-code,
1454
+ signal, and error evidence. `--plain`, non-TTY,
1455
+ and `--once` kernel semantics remain the line-oriented/direct compatibility
1456
+ path; `--once` still performs one tick and drains its in-flight work.
1457
+
1458
+ Durable operator settings (intake, the repository-wide pause, the claim-time
1459
+ auto-merge default, the harvest gate) are repository-journal facts: they
1460
+ survive restarts, propagate between dispatchers by ordinary polling, and are
1461
+ never optimistically rendered — the UI shows acknowledged state. The
1462
+ repository-wide pause holds every queued build: while it is set, no dispatcher
1463
+ tick may attach a runner to a build that does not have one yet.
1464
+
1465
+ The **operator sandbox** is the third execution kind beside build and harvest
1466
+ executions: a persistent, credential-free, per-operator-per-repository
1467
+ environment behind registry tools (`sandbox.*`, AUT-340), provisioned like a
1468
+ build workspace and exposed to operator agents only through those tools. The
1469
+ typed tools are the only route to state from inside it — the environment
1470
+ receives no store, forge, ticket-provider, or model credential, and
1471
+ `[orchestrator].sandbox.environmentVariables` may never name one. Its lifecycle
1472
+ is repository-journal fact: provisioned/resumed/activity/stopped/released facts
1473
+ bracket one deterministic environment per operator × repository, the
1474
+ dispatcher tick's idle settlement stops an environment whose evidence is
1475
+ stale (settling an orphan exactly as harvest executions are settled), and
1476
+ `ab repository status` reports each operator's sandbox.
1477
+
1478
+ The operator's job across many concurrent builds: see status at a glance,
1479
+ act on a selected build, find blocked builds, answer escalations, and inspect
1480
+ any build's trail. Each dashboard build row spends its header on identity and
1481
+ lifecycle status, with the pipeline and its per-step elapsed values below it;
1482
+ controls and diagnostic lines expose actions, pending dispatch, abort cleanup,
1483
+ setup failures, and blockers. Dashboard rows derive from build events and
1484
+ effective configuration only, so heartbeat or lease renewal without an event
1485
+ does not alter them. Every nonterminal build, including `queued`, has a
1486
+ dashboard row; a pre-run row names its pending dispatch boundary or latest
1487
+ durable failure. A human may discard only such a queued build. Discard is
1488
+ dispatcher cleanup, returns the ticket to its
1489
+ configured Ready state, and completes with
1490
+ `discarded`; it is deliberately distinct from abort, which returns work to
1491
+ Triage for human judgment. Any selected nonterminal build can be aborted from
1492
+ the list or detail view with `a`; Enter confirms and Escape cancels, so the first
1493
+ keypress never writes destructive intent. CLI and dashboard both append the same
1494
+ `build.abort-requested` fact through the shared control service. The concrete
1495
+ presentation — layout, key bindings, colors — is owned by the dashboard
1496
+ implementation and its tests.
1497
+
1498
+ ## 15. Event vocabulary
1499
+
1500
+ Drafted by walking one build's happy path end to end, then three unhappy
1501
+ paths (verify failure, review stall, sandbox death). Four decisions this
1502
+ exercise forced are marked **[D1]–[D4]**; all four are **confirmed**, with
1503
+ [D1] extended to cover merge standardization and conflict resolution (§15.7).
1504
+
1505
+ The complete vocabulary is frozen in code: build payloads in
1506
+ `packages/core/src/events/payloads.ts`, repository workflow and control payloads in
1507
+ `packages/core/src/events/repository.ts`. Every adapter validates before append. The
1508
+ sections below define the envelope, the conventions that govern every event,
1509
+ and the walkthroughs that motivated the design — not a field-by-field
1510
+ catalog.
1511
+
1512
+ ### 15.1 Envelope
1513
+
1514
+ Every event shares:
1515
+
1516
+ ```jsonc
1517
+ {
1518
+ "build": "auth-rate-limit", // build slug
1519
+ "seq": 42, // per-build, monotonic, assigned by the store on append
1520
+ "ts": "2026-07-15T14:03:22Z", // assigned by the store
1521
+ "actor": { "kind": "agent", "role": "code-review", "session": "s_9f2" },
1522
+ "type": "code-review.verdict",
1523
+ "payload": { /* per-type, frozen in packages/core/src/events/ */ }
1524
+ }
1525
+ ```
1526
+
1527
+ `actor.kind ∈ kernel | agent | human | dispatcher | ingester`. Agents carry
1528
+ `role` and `session`; humans carry `user`. The store assigns `seq` and `ts`
1529
+ so producers can't fake ordering. Repository-journal events use the same
1530
+ shape with `repo` in place of `build` and their own per-repository sequence,
1531
+ validated by the separate repository catalog so build reducers cannot
1532
+ accidentally interpret repository state.
1533
+
1534
+ A human actor may carry an optional `via` marker for delegated writes —
1535
+ `{kind: "session", id}` (the operator's orchestrator session) or
1536
+ `{kind: "mcp", client}` (an MCP client) — so a team's audit trail can
1537
+ distinguish "the operator did this" from "the operator's orchestrator session
1538
+ did this" or "an MCP client did this" forever. `via` is valid only on human
1539
+ actors and only on build and repository events (session events are the
1540
+ delegate's own log); validation rejects it anywhere else. The hosted service
1541
+ stamps `via` from the authorizing token and rejects a write claiming a `via`
1542
+ the token does not carry; events without it replay unchanged. Over a
1543
+ transport with token authority (the hosted HTTP store) that rejection is an
1544
+ authority failure — 403 `auth` (`AuthError` on the remote client), applied
1545
+ after token verification and before catalog validation, so "not your
1546
+ delegate" (403) stays distinguishable from "malformed event" (422
1547
+ `validation`). A token without a `via` claim may not write delegated events
1548
+ at all: any human write claiming a `via`, valid or malformed, rejects the
1549
+ same way. Local adapters and the open (no-secret) remote server have no
1550
+ token authority; there the backing catalog is the only `via` gate and every
1551
+ rejection is `EventValidationError`.
1552
+
1553
+ ### 15.2 Conventions
1554
+
1555
+ 1. **Closed vocabularies live in type names; open ones live in payloads.**
1556
+ Phases are a closed set → `plan.completed`. Verify and finalize steps are
1557
+ config-defined (open) → `verify.completed {step: "e2e"}`.
1558
+ 2. **Events carry facts, never derived state.** Build status is a reduction
1559
+ (§15.5); no event ever says "status is now X".
1560
+ 3. **Blobs live in artifacts; events carry refs** `{kind, rev}`.
1561
+ 4. **[D3] Code travels through the Forge, never the store.**
1562
+ `implement.completed` pushes the branch; events carry commit SHAs only.
1563
+ This is what makes cross-sandbox resume work (§15.6-C) and keeps the
1564
+ store lean.
1565
+ 5. **Append-only; corrections are new events.**
1566
+ 6. **Liveness is not history.** Heartbeats and runner leases are mutable
1567
+ columns on the `builds` table, never events — they would drown the log.
1568
+ 7. **[D2] Operator commands are events in the same log.** Humans append
1569
+ `*-requested`/`*-cancelled` events (including queued-only discard), `escalation.answered`, and dispatcher
1570
+ setting facts; kernel or dispatcher plumbing acknowledges effects that
1571
+ require a boundary. The store is the *only* coordination surface — no side
1572
+ channel — and polling covers commands exactly the way it covers
1573
+ `subscribe`. A runner that is dead still receives pause/resume/abort
1574
+ commands and escalation answers on resume. While a turn is live, the runner
1575
+ subscribes from its session boundary and forwards an abort as caller-owned
1576
+ cancellation to the AgentRunner; it closes the session transcript without
1577
+ recording `phase.failed` and acknowledges abort. The supervising execution
1578
+ retains the lease until the build's complete process tree has been reaped.
1579
+
1580
+ ### 15.3 Catalog
1581
+
1582
+ Authoritative in code (`packages/core/src/events/payloads.ts`,
1583
+ `packages/core/src/events/repository.ts`, and the session catalog in
1584
+ `packages/core/src/events/sessions.ts` — the third catalog, validated
1585
+ separately like the repository journal's). The build and repository families,
1586
+ with illustrative members:
1587
+
1588
+ | Family | Examples |
1589
+ |---|---|
1590
+ | Build lifecycle | `build.created`, `workspace.provisioned`, `workspace.released`, `execution.started`, `execution.ended`, `infrastructure.failed`, `infrastructure.cleanup-attempted`, `dispatch.comment-posted`, `dispatch.failed`, `runner.attached`, `runner.setup-failed`, `abort.remote-branch-deleted`, `abort.local-branch-deleted`, `abort.ticket-returned`, `build.completed` |
1591
+ | Operator commands [D2] | `build.pause-requested` → `build.paused`; `build.discard-requested`; `build.auto-merge-requested`; `escalation.answered` (optionally carrying a validated `reviewRoundCeiling`) |
1592
+ | Spec | `spec.imported`, `spec.authored`, `spec.revised` |
1593
+ | Sessions | `session.started`; `session.ended` with transcript ref and usage (ordinary completion — the analysis corpus), or `session.ended {outcome: reclaimed, reclaimedBy: {instance, resumedFromSeq}}` (explicit transcriptless takeover) |
1594
+ | Plan/code loops | `plan.started` … `plan-review.verdict`; `implement.started` … `code-review.verdict` |
1595
+ | Verify/finalize | `verify.started {step, attempt, feedback?}`, `verify.completed {step, outcome}`, `finalize.completed {pr}`, `finalize.step-completed {step, ok, headSha?}` |
1596
+ | PR attachments | `pr-attachment.designated`, `pr-attachment.hosted`, `pr-attachment.reclaimed`, `pr-attachment.reclaim-failed` |
1597
+ | Post-PR [D1] | `pr.merged`, `pr.conflicted`, `reconcile.progress-checked`, `reconcile.started`, `reconcile.completed` |
1598
+ | Cross-cutting | `observation.recorded`, `escalation.raised`, `phase.failed` |
1599
+ | Repository journal | dispatcher setting facts; run lifecycle, effective config, tick/queue diagnostics, reload and runner outcomes; the `harvest.*` workflow, recovery, and ledger facts |
1600
+
1601
+ Abort cleanup is a dispatcher-owned ordered saga. While an execution lease is
1602
+ live, the janitor defers the saga so it cannot race process-tree teardown. Once
1603
+ that supervisor releases the lease, or an abruptly orphaned lease expires, cleanup
1604
+ closes an open unmerged PR without overriding a racing merge, releases the workspace,
1605
+ deletes the exact published remote branch and exact local branch, unions the
1606
+ `autobuild:aborted` label into the current ticket labels, returns the ticket to
1607
+ configured Triage, and only then completes as `abandoned`. External effects are
1608
+ idempotent and the three `abort.*` facts checkpoint projections across crashes;
1609
+ missing Forge capabilities or outages leave the remainder due on the next tick.
1610
+
1611
+ One deliberate subtlety worth recording: `pr.conflicted.baseSha` is
1612
+ detection-time evidence, while `reconcile.progress-checked.baseSha` is the
1613
+ kernel's authoritative observation for deciding whether a completed attempt
1614
+ made progress and `reconcile.started.baseSha` is the separately refreshed merge
1615
+ target for the next attempt. Agent context uses only the started fact, so a
1616
+ reconcile never runs against a persisted or known-stale observation.
1617
+
1618
+ ### 15.4 Finding schema and stall mechanics [D4]
1619
+
1620
+ ```jsonc
1621
+ Finding {
1622
+ "id": "f_3a91", // kernel-assigned at deposit, stable for the build
1623
+ "severity": "blocking" | "important" | "minor",
1624
+ "file": "packages/core/src/auth.ts", // optional
1625
+ "lines": [40, 62], // optional
1626
+ "summary": "…",
1627
+ "detail": "…", // optional
1628
+ "persists": ["f_1c22"] // reviewer-marked: earlier findings this one continues
1629
+ }
1630
+ ```
1631
+
1632
+ Stall detection splits along the constitution's line: deciding whether a new
1633
+ finding is *the same disagreement* as an earlier one is fuzzy → **judgment**
1634
+ → the reviewer (fresh each round) receives the prior rounds' findings and
1635
+ marks `persists`. Applying the threshold is mechanical → **determinism** →
1636
+ the kernel raises `escalation.raised {source: "stall"}` when any persistence
1637
+ chain survives `policy.stallRounds` rounds.
1638
+
1639
+ ### 15.5 Derived state (the reducer)
1640
+
1641
+ `status ∈ queued | running | paused | blocked | done | aborted`, plus
1642
+ `{phase, round, openEscalations[], reviewRoundCeilings?, pr?, autoMerge,
1643
+ lastEvent}`. `blocked` ≡ an `escalation.raised` without a matching
1644
+ `escalation.answered`, matched by id. `reviewRoundCeilings` projects valid
1645
+ review-limit answers independently for the plan and code loops after the latest
1646
+ `spec.revised`; status and dashboard surfaces expose any projected values.
1647
+ `paused` ≡ a `build.paused` without a later `build.resumed`, and takes
1648
+ reducer precedence over blocked. Auto-merge state tracks the latest human
1649
+ *desired* value separately from the latest *applied* fact, settled only when
1650
+ both match — a stale acknowledgement can never erase newer intent.
1651
+
1652
+ Queued state additionally retains ordered `dispatch.failed` diagnostics and an
1653
+ outstanding `build.discard-requested` fact. `build.completed {outcome:
1654
+ "discarded"}` settles that intent without changing the status vocabulary.
1655
+ Runner state retains ordered `runner.setup-failed {command,attempt,exitStatus,
1656
+ output}` facts and projects the latest one until a later `runner.attached`
1657
+ proves successful setup recovery. A legacy `session.started` with no ending
1658
+ continues to project as open. On takeover, the recovering runner explicitly
1659
+ appends a reclaimed `session.ended` for every session open at its captured
1660
+ resume boundary before it appends `runner.attached`; only a subsequently
1661
+ started rerun session can then be open.
1662
+
1663
+ Every projection — operator UI, CLI status, dispatcher decisions — is a
1664
+ reduction of the logs. `ab builds` and `ab build status` additionally present
1665
+ the reduced last event as durable progress beside mutable heartbeat and lease
1666
+ health. On those diagnostic CLI surfaces, a nonterminal build with a live lease
1667
+ whose heartbeat is at least one hour newer than its last event is
1668
+ presentation-marked `diverged`; terminal, no-lease, and expired-lease builds
1669
+ are never marked. Progress age and that marker never feed a reducer or engine
1670
+ decision. Caches serving those liveness-aware consumers may key a reduction by
1671
+ last event sequence, but record-only heartbeat renewal must still refresh their
1672
+ presentation inputs. No decision ever consults a snapshot in place of the
1673
+ append-only log. Separate reducers
1674
+ derive dispatcher settings and harvest state from the repository journal; each
1675
+ ignores the other's facts.
1676
+
1677
+ ### 15.6 Walkthroughs
1678
+
1679
+ **Happy path** (elided: `session.started/ended` brackets around every agent
1680
+ run):
1681
+
1682
+ ```
1683
+ build.created → workspace.provisioned{base:{source:remote,sha}} → spec.imported → dispatch.comment-posted → runner.attached
1684
+ plan.started{r1} → plan.completed{plan@1, verifySteps}
1685
+ plan-review.started{r1} → plan-review.verdict{approve}
1686
+ implement.started{r1} → implement.completed{commits, notes@1}
1687
+ code-review.started{r1} → code-review.verdict{approve}
1688
+ verify.started{types} → verify.completed{outcome:pass} → …unit
1689
+ verify.started{e2e} → pr-attachment.designated{artifact,filename,mediaType} → verify.completed{outcome:pass}
1690
+ finalize.started → pr-attachment.hosted{designationSeq,asset} → finalize.completed{pr} → finalize.step-completed{release-notes}
1691
+ (later, janitor:) pr.merged → workspace.released → build.completed{merged} → pr-attachment.reclaimed{hostedSeq}
1692
+ ```
1693
+
1694
+ **Interrupted dispatch:** if the log stops after `build.created`, a later
1695
+ ordinary dispatcher tick provisions the workspace, atomically imports the spec,
1696
+ posts the ticket notice once, and launches the runner. Claim-time auto-merge
1697
+ attribution retained by `build.created` is materialized as the ordinary
1698
+ human-authored request before launch. A failed boundary appends `dispatch.failed
1699
+ {stage,attempt,error}` and stays queued for another tick. A discard request is
1700
+ honored only while the build remains queued; one that races with runner
1701
+ attachment is inert and cannot tear down live work. A human discard waits out
1702
+ any unexpired execution lease, then releases a partial workspace and stale
1703
+ lease, returns the ticket to Ready, and appends `build.completed {outcome:
1704
+ discarded}` last. No runner is required.
1705
+
1706
+ **A — verify failure:** `verify.completed {step: e2e, outcome: fail,
1707
+ report}` → kernel routes back into the code loop: `implement.started
1708
+ {round: 2, feedback: {verify: {step, report}}}` → fix → `code-review` round
1709
+ 2 → approve → verify re-runs **from the first step** (implement changed the
1710
+ code; cheap checks first), `attempt: 2`. `policy.maxVerifyAttempts`
1711
+ exhausted → `escalation.raised {source: "policy"}`. Guidance answering that
1712
+ policy escalation routes to `implement` and outranks the pending report. By
1713
+ contrast, when the agent verifier itself uses `ab escalate`, guidance answering
1714
+ it reruns that same step with `verify.started {feedback: {guidance}}`; `ab context`
1715
+ writes `.ab/guidance.json`. The start citation remains pending through repeated
1716
+ pre-launch recovery. A later `session.started` for that same verifier and
1717
+ attempt consumes the answer once, so a later failure routes its report to
1718
+ `implement` without stale guidance. Producer starts use the same exact-phase,
1719
+ round-matched rule. A bare retry reruns the verifier with no feedback.
1720
+
1721
+ **B — review stall:** round 1 `code-review.verdict {revise, [f1]}` → round 2
1722
+ verdict's finding marks `persists: [f1]` → round 3 again → kernel:
1723
+ `escalation.raised {source: "stall", refs: [chain]}`; status → `blocked`.
1724
+ `escalation.answered` uses the resolution vocabulary `guidance`,
1725
+ `dismiss-finding`, `revise-spec`, `abort`, or `retry`. Operators produce
1726
+ `dismiss-finding` and `revise-spec` through `ab answer`; a `revise-spec` answer
1727
+ names the exact replacement artifact it authorizes. `guidance` feeds the answer
1728
+ into the next producer round as authoritative feedback; `dismiss-finding`
1729
+ marks the chain human-resolved and the next reviewer round is told so. Engine-routed
1730
+ guidance is latest-only per destination (`plan`, `code`, or one exact agent
1731
+ verifier): a newer answer durably supersedes every older answer for that
1732
+ destination before delivery. A guidance-bearing producer or verifier start is
1733
+ the durable carrier, and only its later matching `session.started` launch
1734
+ consumes the winner; until then the winner remains eligible across recovery.
1735
+ After that delivery, no shadowed answer can surface on a later round, while an
1736
+ answer appended after delivery is a new eligible winner. Destinations remain
1737
+ independent, and the kernel derives both the winner and its delivery from the
1738
+ event history so replay and restart preserve the result.
1739
+
1740
+ **C — setup failure:** the first attach retains the normal `runner.attached`
1741
+ fact, then a failed setup appends `runner.setup-failed
1742
+ {command,attempt,exitStatus,output}` and starts no phase or session. A retry runs
1743
+ setup under the claimed lease before announcing another attachment. Another
1744
+ failure appends only a new failure fact; success appends `runner.attached
1745
+ {resumedFromSeq}` and clears the current error projection. After
1746
+ `policy.maxSetupAttempts`, the kernel raises one policy escalation targeted at
1747
+ `setup`; lease sweeps and fresh dispatcher startup leave it parked until a human
1748
+ answer re-arms the setup budget. Its exhaustion guard considers only setup-targeted
1749
+ policy raises, independently of phase-policy exhaustion. After claiming the lease,
1750
+ however, a runner honors an engine-selected pause or abort acknowledgement before
1751
+ setup-failure gating, even while that escalation is open. This control-only path
1752
+ executes no setup, appends no `runner.attached`, and starts no phase or session.
1753
+ Resume and all decisions that use the workspace still require successful setup.
1754
+ The setup target belongs only to escalation metadata and is not a pipeline `Phase`.
1755
+
1756
+ **D — sandbox death:** log ends at `session.started {session: old}` after
1757
+ `implement.started {round: 2}`; heartbeat goes stale → dispatcher expires the
1758
+ lease, records the old execution identity, stops/deletes its exact deterministic
1759
+ sandbox name, and confirms absence before recording `workspace.released` → it
1760
+ provisions the next generation from the published build head (or the original
1761
+ recorded branch-cut SHA when no checkpoint exists) → `workspace.provisioned
1762
+ {base: {source: existing, sha}}` → the workspace execution capability starts a
1763
+ fresh process and records the replacement sandbox/session identity. The
1764
+ supervising kernel first claims the lease for that execution
1765
+ instance; the process reads its workspace location from the durable event,
1766
+ renews the same-holder lease, captures the current resume boundary, and appends
1767
+ `session.ended {session: old, outcome: reclaimed, reclaimedBy: {instance,
1768
+ resumedFromSeq}}` before `runner.attached {resumedFromSeq}`. The reducer now
1769
+ shows no pre-boundary session as open and says implement r2
1770
+ started-not-completed, so the process re-runs the phase from its start and opens
1771
+ one fresh session. For a `publication.requested` fact issued before the old
1772
+ `workspace.released`, the supervisor first compares its exact SHA with the
1773
+ durable remote branch: a match repairs the missing completion fact before the
1774
+ replacement starts, covering a lost post-push acknowledgement; otherwise the
1775
+ commit existed only in the deleted sandbox, the request is abandoned, and the
1776
+ phase rerun must issue a new request. A crash while closing several sessions
1777
+ lands no attachment;
1778
+ the next recovery closes only those still open, so repeated takeover converges.
1779
+ Historical orphan-only logs remain valid and require no repair.
1780
+
1781
+ The re-run retains another `implement.started {round: 2}` as evidence of the
1782
+ execution attempt. Duplicate starts for one logical round or attempt are not
1783
+ policy units: review budget follows verdict rounds, phase retry budget follows
1784
+ `phase.failed`, verify budget follows failed completions while a crashed step
1785
+ reuses its attempt, and reconcile no-progress budget follows distinct completed
1786
+ attempts. Recovery alone therefore consumes none of those budgets. The provider
1787
+ restores the already-created branch at round 1's pushed head [D3]; the Git
1788
+ adapter never re-cuts it from a newer base (§7.4). `ab context` rehydrates
1789
+ scratch from the store into a fresh session. Uncommitted round-2 work is lost by
1790
+ design (§7.3 — phase boundaries are the resume points).
1791
+
1792
+ Two liveness rules complete the picture. Within one dispatcher process,
1793
+ build-runner launches are single-flighted by slug through supervised process
1794
+ liveness — execution completion means the complete environment has been reaped,
1795
+ but remains only a liveness signal, never a pipeline outcome. Build progress,
1796
+ config, diagnostics, and outcomes cross the process boundary only through
1797
+ build-owned Store state. The kernel claims each execution's durable lease before
1798
+ launch and releases that exact holder only after completion; the child renews it.
1799
+ The lease remains the cross-process recovery and workspace-teardown gate (the
1800
+ in-memory guard is deliberately not durable: a dead process's memory disappears
1801
+ with it). Ordinary dispatcher shutdown gracefully signals each local process
1802
+ group, escalates the whole group after the bounded stop delay, and reaps it.
1803
+ Before every build-child terminal path exits — natural success or failure,
1804
+ SIGINT/SIGTERM, or immutable-parent watchdog death — it launches one detached
1805
+ group reaper while the leader still pins the group's identity. That owner
1806
+ applies the same bounded TERM-then-KILL policy and survives abrupt dispatcher
1807
+ death. The live local supervisor independently awaits full-group teardown before
1808
+ publishing execution completion and releasing the lease; lease expiry remains
1809
+ the durable recovery fallback when the dispatcher dies. A new `ab dispatch`
1810
+ process attempts
1811
+ every actionable build on its first tick rather than waiting for the sweep;
1812
+ lease claiming stays the exclusivity gate, so a genuinely live old runner
1813
+ wins harmlessly.
1814
+
1815
+ ### 15.7 Post-PR lifecycle [D1 — confirmed]
1816
+
1817
+ Walking the happy path exposed a gap in the grammar: `finalize` creates the
1818
+ PR, but *something* must watch it to merge/close, release the workspace, and
1819
+ emit `build.completed`. v2 makes it a deterministic **janitor duty of the
1820
+ dispatcher** (which already polls on cron): it checks open PRs for its
1821
+ builds, emits `pr.merged`/`pr.closed`/`pr.conflicted`, releases workspaces,
1822
+ and completes builds. Destructive discard, abort, and terminal-PR cleanup defer
1823
+ while an unexpired execution lease exists, so no workspace is released under a
1824
+ live process tree. After any terminal outcome it also reclaims every
1825
+ pending hosted PR-attachment copy. Reclamation success and failure are durable
1826
+ correlated facts; a failed delete remains pending and retries on later ticks,
1827
+ including after the build is already done. A merged-PR fixup request is a *new
1828
+ ticket*, never a reopened build.
1829
+
1830
+ **Merge standard: one rule per direction, never rebase.**
1831
+
1832
+ - **PR → main: squash merge.** Main stays linear, one commit per build —
1833
+ which keeps reverts (one commit → one new ticket), release notes, and
1834
+ history archaeology clean.
1835
+ - **main → feature branch: merge commit.** A stale branch is refreshed by
1836
+ merging base *into* it, resolving conflicts once against current main.
1837
+ - **Rebase is banned**, for two reasons. Operationally: at this system's
1838
+ merge velocity, a rebase re-resolves conflicts commit-by-commit against a
1839
+ target that keeps moving — agents can be stuck in
1840
+ rebase-conflict-after-rebase-conflict nearly indefinitely (observed in
1841
+ practice). Structurally: rebase rewrites the branch and severs the SHAs
1842
+ recorded in `implement.completed` events [D3]. Squash-at-merge is safe on
1843
+ both counts.
1844
+
1845
+ **Merge gates are never bypassed.** The operator's auto-merge command is
1846
+ durable consent to merge, not a bypass. Whenever the base branch has any real
1847
+ merge-blocking gate, consent is applied as GitHub-native auto-merge, so the
1848
+ forge's own checks decide when the PR lands. Only when the forge
1849
+ authoritatively reports no gate may the janitor perform a normal,
1850
+ head-guarded squash itself — never admin, force, or rebase — and only after
1851
+ all verification and finalize work is complete. GitHub's exact documented
1852
+ account-plan refusal for the branch-rulesets endpoint proves that no ruleset
1853
+ can exist only when the independent classic-protection probe also
1854
+ successfully reports no protection. Every near miss, generic authorization
1855
+ failure, malformed or unknown response, and tooling failure remains unknown
1856
+ and fails closed. Gate presence is decided by the mechanism actually in
1857
+ effect: a ruleset probe that proves merge-blocking rules settles presence even
1858
+ when the classic protection response is incomplete, while an incomplete classic
1859
+ response with no proven ruleset gate still fails closed with a reason naming
1860
+ the missing subsection.
1861
+
1862
+ Inability to prove or apply auto-merge is nonfatal pipeline plumbing, not a PR
1863
+ creation failure: finalize still records the open PR and completes, and consent
1864
+ stays pending for later janitor polls, which re-examine it. Deferrals divide by
1865
+ who resolves them. Merge conflicts and not-yet-computed mergeability are
1866
+ pipeline-owned: a conflict re-enters the pipeline through `pr.conflicted` and
1867
+ reconcile (below), and uncomputed mergeability resolves on a later poll —
1868
+ neither records anything, and the reconcile step on the build row is the only
1869
+ indication. A kernel-authored follow-up is recorded only for a deferral a
1870
+ person must fix: the first such refusal for that PR and consent command records
1871
+ one follow-up while the PR is open, and a later `reconcile.completed` or
1872
+ `finalize.completed` supersedes it — the projection shows nothing until the
1873
+ gate re-examines consent and records a fresh one. A repository
1874
+ with native auto-merge disabled is left open for a human; Autobuild never
1875
+ changes the setting. The local-git forge has no external gate or native
1876
+ setting: consent produces the same inspected-head guarded squash candidate,
1877
+ mergeability is recomputed against the current local base, and the exact
1878
+ `pr-description` becomes the single-parent squash commit message. Its author and
1879
+ committer are Git's ordinary resolved repository identity, exactly as for a plain
1880
+ local commit; commit signing remains disabled. Before offering the candidate,
1881
+ the adapter resolves both identities through Git. If either is unavailable,
1882
+ consent remains pending and one reason-bearing follow-up names the missing
1883
+ `user.name` / `user.email` configuration; later polls retry after the operator
1884
+ configures it. When the base branch is checked out, Git's two-tree
1885
+ `read-tree -m -u` transition preflights and lands the old-to-squash tree:
1886
+ tracked/index changes on untouched paths and
1887
+ untracked non-colliding files survive, while a tracked or untracked overwrite
1888
+ leaves consent pending and records one path-bearing follow-up instead of failing
1889
+ the dispatcher. Its durable pending-landing record is written before moving the
1890
+ base ref. A poll after a crash retries that same old-to-landed transition without
1891
+ rolling back the ref or overwriting operator work; the PR remains open and
1892
+ retryable until checkout repair succeeds. No merge is ever assumed: a build
1893
+ reaches `merged` only when a later poll observes the landing and any required
1894
+ checkout synchronization. Without consent, the local branch and PR record
1895
+ remain open for inspection with ordinary Git.
1896
+
1897
+ **Conflicts re-enter the pipeline via `reconcile`.** When the janitor's
1898
+ mergeability check fails it emits `pr.conflicted` and re-attaches a
1899
+ build-runner (the dispatcher itself never runs agents). Immediately before
1900
+ each attempt, the runner asks the Forge for its current authoritative base
1901
+ snapshot when supported, otherwise fetching the build's frozen base branch
1902
+ fresh from `origin`, and records the resolved SHA on `reconcile.started` — known-stale input is never
1903
+ used (§15.3). The agent merges that base into the branch guided by the spec,
1904
+ plan, and implement-notes, with the explicit charge to regress against
1905
+ neither; the resolution lands as a merge commit, and because reconciliation
1906
+ changed code, **`verify:*` re-runs in full**. A resolution the agent judges
1907
+ risky — semantic conflicts, spec-relevant choices — escalates rather than
1908
+ guesses. Reconcile skips `code-review` by default (escalation covers the
1909
+ judgment cases; policy can force it).
1910
+
1911
+ On each repeat conflict, the runner records a fresh authoritative base snapshot
1912
+ on `reconcile.progress-checked`, tied to that conflict and the most recently
1913
+ completed attempt. The kernel compares it with that attempt's matching latest
1914
+ `reconcile.started.baseSha`. A different SHA proves that the reconcile lost a
1915
+ race against a moving base, so another monotonic attempt runs regardless of the
1916
+ configured limit. An equal SHA consumes `policy.maxReconcileAttempts`; reaching
1917
+ the limit escalates because reconciliation made no progress against an unchanged
1918
+ base. This no-progress escalation is conflict-scoped, roundless, and identified
1919
+ by `policyCause = "reconcile-no-progress"`. By contrast, a runner retry
1920
+ exhaustion or non-retryable failure while obtaining the progress check or
1921
+ refreshing the base is scoped to the concrete next reconcile round and carries
1922
+ its own cause. Answering that runner condition re-arms its phase-round failure
1923
+ budget so the authoritative decision can complete, but it does not acknowledge
1924
+ no-progress: if the unchanged-base budget is already exhausted, the separate
1925
+ no-progress escalation is raised before another reconcile session starts.
1926
+ Answering the matching no-progress escalation continues to authorize one
1927
+ subsequent reconcile for the same conflict without resetting runner failures
1928
+ already counted for any reconcile round. An explicitly different cause cannot
1929
+ acknowledge that guard even when roundless. Cause-less historical roundless
1930
+ `policy`/`reconcile` raises retain the former no-progress and phase-level
1931
+ failure-reset interpretations on replay without migration. Routing uses cause
1932
+ for class identity and reset semantics, `round` for occurrence scope, and never
1933
+ `question` text.
1934
+
1935
+ The classification is reduced from the durable log, with the next attempt's
1936
+ authoritative `reconcile.started` serving as the observation for historical logs
1937
+ that predate the progress-check event. A started but incomplete attempt re-runs
1938
+ at the same number and consumes nothing. Every completed reconcile still starts
1939
+ a fresh, fully bounded `verify:*` cycle.
1940
+
1941
+ The grammar's tail is thus an epilogue loop, outside the mainline:
1942
+
1943
+ ```
1944
+ finalize → ( pr.conflicted → reconcile → verify:* )* → merged | closed
1945
+ ```
1946
+
1947
+ ## 16. Per-repo configuration and installation
1948
+
1949
+ Decisions here continue the series: **[D9]** declarative repo config and
1950
+ **[D11]** vendored editable skills.
1951
+
1952
+ ### 16.1 `autobuild.toml` [D9]
1953
+
1954
+ One declarative file at the repo root. A running dispatcher owns an accepted
1955
+ snapshot read from the **main checkout** and refreshes it before each dispatch
1956
+ tick. Before launch and after each accepted revision, the dispatcher deposits
1957
+ the composed snapshot into each affected build's artifact namespace. The child
1958
+ samples that durable artifact at setup and pipeline-step boundaries, so a valid
1959
+ change may affect the next action of an in-flight build but never interrupts an agent turn
1960
+ or deterministic command already running. Scoped phase CLI processes separately
1961
+ read the build worktree's branch-owned file. Because the file is repo-versioned,
1962
+ changes still flow through the pipeline itself: once merged, the system can
1963
+ adopt a retuned configuration without restarting the dispatcher.
1964
+
1965
+ A missing or unreadable file and a malformed or routing-invalid candidate are
1966
+ rejected atomically: the last valid snapshot, resolver, and revision remain in
1967
+ force, and no action starts under missing, partial, or defaulted configuration.
1968
+ The dispatcher emits an actionable operator notice without repeating it for the
1969
+ same failure. Restoring a valid `autobuild.toml` clears the rejection and
1970
+ ordinary boundary reload resumes.
1971
+
1972
+ ```toml
1973
+ baseBranch = "main"
1974
+ capacity = 3 # concurrent builds for this repo
1975
+ forge = "github" # builtin: github | local-git; or a plugin name
1976
+ plugins = ["./plugins/local.ts", "@acme/autobuild-plugin"]
1977
+
1978
+ #[workspace] # optional; default provider = "git-worktree"
1979
+ #provider = "vercel-sandbox" # shipped remote provider, or plugin name
1980
+ #[workspace.config]
1981
+ #image = "vercel/sandbox/universal:latest"
1982
+ #vcpus = 4
1983
+ #timeoutSeconds = 2700
1984
+ #operationTimeoutMs = 30000 # provider calls, not VM/session lifetime
1985
+ #environmentVariables = ["ANTHROPIC_API_KEY"]
1986
+
1987
+ #[pr.imageHost] # optional public inline rendering for attached images
1988
+ #provider = "github-release"
1989
+ #repository = "owner/public-review-assets"
1990
+ #releaseId = 123456
1991
+
1992
+ [commands] # deterministic verbs the kernel may run
1993
+ setup = "bun install" # after provision / sandbox rehydrate (§15.6-C)
1994
+ typecheck = "bun run type-check"
1995
+ test = "bun run test"
1996
+ publish = "bun run publish"
1997
+
1998
+ [verify]
1999
+ steps = ["types", "unit", "e2e"]
2000
+ [verify.types]
2001
+ kind = "check" # deterministic: command + pass/fail
2002
+ command = "typecheck" # ref into [commands]
2003
+ [verify.unit]
2004
+ kind = "check"
2005
+ command = "test"
2006
+ [verify.e2e]
2007
+ kind = "agent" # agent-verify: skill + pass|fail|skip verdict
2008
+ skill = "verify-app-e2e" # repository-authored agent verifier
2009
+ paths = ["web/**"] # optional changed-path applicability
2010
+
2011
+ [finalize]
2012
+ steps = ["publish", "release-notes"] # ordered, failure-tolerant (§5)
2013
+ [finalize.publish]
2014
+ kind = "check"
2015
+ command = "publish"
2016
+ [finalize.release-notes]
2017
+ kind = "agent"
2018
+ skill = "ab-release-notes"
2019
+
2020
+ [roles.default] # reserved inheritance base, never a phase (§9)
2021
+ runtime = "claude"
2022
+
2023
+ [roles.code-review] # fields override default independently
2024
+ runtime = "pi"
2025
+ model = "moonshotai/kimi-k3"
2026
+ sessionBudgetSeconds = 7200 # optional role override
2027
+
2028
+ [policy]
2029
+ sessionBudgetSeconds = 3600 # non-infinite default for build agent sessions
2030
+ stallRounds = 3
2031
+ maxVerifyAttempts = 3
2032
+ maxSetupAttempts = 3
2033
+ maxInfrastructureAttempts = 3
2034
+ maxReconcileAttempts = 3
2035
+ maxReviewRounds = 6
2036
+ harvestThreshold = 5 # observation-count pressure in dispatch
2037
+ harvestMaxDrift = 3 # merged builds since oldest observation; 0 disables
2038
+
2039
+ [tickets]
2040
+ source = "file"
2041
+ readyState = "ready" # required: the one state a ticket must sit in to dispatch
2042
+ ```
2043
+
2044
+ The root scalars must appear before the first table header (TOML otherwise
2045
+ nests them in that table). `forge` defaults to `"github"` and `plugins` defaults
2046
+ to `[]`, preserving repositories with no plugin configuration. Every nonblank
2047
+ `plugins` specifier must be unique by exact string; a repeated entry fails
2048
+ config validation with the duplicate position, first position, and
2049
+ remove/deduplicate guidance. `[workspace]` defaults to
2050
+ `provider = "git-worktree"` and empty config; the strict selector
2051
+ envelope permits plugin-owned values under `[workspace.config]`; selecting the
2052
+ Vercel builtin applies its closed image/vCPU/timeout/region/environment-name
2053
+ schema. Unknown providers fail with the complete available-name list. A
2054
+ workspace records provider `ref`, absolute execution-environment `path`, and an
2055
+ optional dispatcher-reachable `localPath`. Vercel runs setup, all agent/check
2056
+ phases, review/reconciliation, and finalize post-steps in its guest checkout.
2057
+ Its child uses environment-owned supervision rather than a host-PID watchdog.
2058
+ Remote publication is a durable request settled by the local supervisor only
2059
+ after command exit, VM stop, and lease release; credentials are transformed by
2060
+ the firewall only for an exact fixed push, and Forge/PR calls remain local.
2061
+ Declarative (TOML), not executable config: the
2062
+ kernel, dispatcher, CLI, and any future tooling parse it without evaluating
2063
+ anything; commands are plain shell strings. Parsing is strict — an unknown table or key is an error, so a
2064
+ typo cannot silently disable a verifier. The full config surface, field
2065
+ semantics, and validation rules live with the config code and
2066
+ `docs/configuration.md`. The removed `[dashboardFrames]`, `[project]`,
2067
+ `[dispatcher]`, `[harvest]`, and `[outer]` tables have no aliases or migration
2068
+ shims. `[tickets].source` is a nonblank builtin or plugin registration name.
2069
+ The builtin `linear` and `file` branches retain their exact field restrictions
2070
+ and defaults; any other name is resolved after plugin loading, receives the
2071
+ existing ticket table as factory config, and fails with the complete available
2072
+ name set when unregistered. Plugin-declared `requiredEnv` values are never TOML
2073
+ fields: secrets stay in the process environment or local `.env`.
2074
+
2075
+ Two configurable narrowing mechanisms govern which verify steps run, both
2076
+ resolving to the ordinary `skipped` outcome so exclusions stay queryable:
2077
+
2078
+ - **Plan selection.** An approved plan may declare the complete subset of
2079
+ optional verify steps warranted by the spec, in strict front matter
2080
+ validated at deposit. The selection paired with the approving review
2081
+ verdict is authoritative; `always = true` steps can never be deselected.
2082
+ Missing metadata means all configured steps, so historical builds keep
2083
+ their behavior.
2084
+ - **Path applicability.** A step may declare positive changed-path selectors,
2085
+ evaluated by the kernel immediately before the step on every verify cycle,
2086
+ diffing `HEAD` against the build's durable base (promoted by a completed
2087
+ reconcile, so upstream-merged work is not attributed to the build). No
2088
+ match skips without launching anything; a Git failure is infrastructure and
2089
+ fails closed, never a synthetic skip.
2090
+
2091
+ ### 16.3 Skill installation: vendored, namespaced, editable [D11]
2092
+
2093
+ This project ships 11 canonical default skills for the fixed phases, the
2094
+ non-phase surfaces (`spec`, `tickets`, and `guide`), and the outer-loop skills.
2095
+ Repository setup guidance ships as `guide/references/setup.md`, not as a skill;
2096
+ agent verifier skills are authored by each repository when needed. `ab init`
2097
+ installs into a repo:
2098
+
2099
+ - **A stack-neutral `autobuild.toml` skeleton**, rendered only when absent. It
2100
+ declares no setup command or verify step and inspects no language or package
2101
+ manifest. Its explicit `[roles.default].runtime` is only a schema placeholder.
2102
+ Existing config is never overwritten, even with `--force`.
2103
+ - **An agent-driven setup handoff.** Init probes every registered runtime and
2104
+ reports each usable/unusable result, then chooses an interactive setup agent
2105
+ using the product-fixed `claude`, `codex`, `pi` preference order. The launcher
2106
+ choice does not constrain the pipeline's final roles or models. On an
2107
+ interactive terminal, init starts that coding-agent CLI directly in the
2108
+ target repository with inherited terminal I/O, no `AB_*` session identity,
2109
+ and a short prompt pointing to the installed editable
2110
+ `.agents/skills/ab-guide/references/setup.md`. The prompt carries the
2111
+ new-config or existing-config preface but does not embed the reference body.
2112
+ Its exit status is init's. This direct process creates no build, session,
2113
+ transcript, event, or BuildStore record. Without a usable shipped runtime or
2114
+ an interactive terminal, installation still succeeds and init prints the
2115
+ exact same pointer prompt for the user to run in a coding agent. A missing
2116
+ setup reference in an older or partial distribution does not fail init. On
2117
+ rerun, the prompt asks the agent to review and improve the preserved existing
2118
+ config.
2119
+ - **Copies** of the 11 default skills into the project skills directory,
2120
+ namespaced `ab-*`. Copies, not references — per-repo customization is the
2121
+ point: this repo's code-review standards and setup reference live in the
2122
+ vendored trees. Harness-specific discovery paths are symlinks to the one
2123
+ canonical editable copy. Setup directs the repository to author and configure
2124
+ its own agent verifier when judgment-driven end-to-end coverage is warranted.
2125
+ - **Model-invocation discipline.** Phase skills are installed
2126
+ non-agent-invocable: they are invoked explicitly by the runner or a human,
2127
+ never auto-triggered by a model pattern-matching a description — a model
2128
+ must not start a pipeline phase by accident. The model-invocable exceptions
2129
+ (`ab-spec`, `ab-tickets`, `ab-guide`) are exactly the three skills that
2130
+ **drive no phase**; membership is decided by that criterion, not taste.
2131
+
2132
+ When `.agents/skills` and `.claude/skills` resolve to the same filesystem
2133
+ entry, that repository-level alias already satisfies harness discovery. Init
2134
+ and upgrade create no per-skill links through it and do not reinterpret its
2135
+ live or pristine files as legacy `.claude` copies. A real
2136
+ `.claude/skills/ab-*` directory that is a distinct filesystem entry remains a
2137
+ conflict: all other skills are still processed, every such conflict is reported
2138
+ with move/remove guidance, setup is not launched by init, and the command exits
2139
+ nonzero.
2140
+
2141
+ **Installation identity** is local to the running distribution. `ab --version`
2142
+ reads its package version, Bun-recorded forge commit when present, and the
2143
+ independent plugin API version; it needs no repository, config, store, or
2144
+ network. A source checkout is identified by its `.git` marker. A managed Bun
2145
+ install has one of two channels, both derived from the owning `package.json`
2146
+ dependency and `bun.lock` record, never hardcoded. An npm registry install is
2147
+ identified when the direct dependency is a semver range the installed version
2148
+ satisfies and the lock resolves that name to the same registry version. A
2149
+ movable Bun forge install is identified only when the distribution's
2150
+ `.bun-tag`, its owning direct `github:owner/repository` dependency, and
2151
+ `bun.lock` record agree; a fork install therefore follows its fork. Unknown or
2152
+ contradictory provenance is a named refusal, never a guessed install command.
2153
+
2154
+ **Upgrades** run only on explicit `ab upgrade`. By default the command resolves
2155
+ the latest published version on the installation's channel: the registry's
2156
+ version document for an npm install, or the latest full GitHub Release of the
2157
+ installation repository for a forge install. If newer, it
2158
+ uses the matching Bun local or global operation to install the release, then
2159
+ hands off to a fresh process from the replaced distribution before touching
2160
+ skills. Thus both defaults and merge logic come from the new version. A local
2161
+ install updates the owning project's `package.json` dependency and `bun.lock`;
2162
+ a global install updates Bun's global package-manager state. An already-current
2163
+ (or locally newer) install proceeds directly to skill merge. A source checkout
2164
+ is never mutated and still merges installed skills, including when an exact
2165
+ release was named. For the default latest operation, an indeterminate mechanism
2166
+ or lookup/install failure warns and also merges installed skills. For an
2167
+ operator-selected `--version <semver>`, indeterminate mechanism, resolution, or
2168
+ install failure is fatal and does not merge against the wrong defaults.
2169
+ `--no-self-update` always selects merge-only
2170
+ behavior. An exact version may be older than the installed version.
2171
+
2172
+ Upgrade records its successful work in one local commit on the target's current
2173
+ HEAD by default. The exact ownership boundary is the reported skills' canonical
2174
+ `.agents` trees, pristine records, and `.claude` discovery paths, plus
2175
+ `package.json` and `bun.lock` only when this run's successful local Bun update
2176
+ wrote those files inside the target repository. A global update contributes no
2177
+ repository path. Additions, modifications, symlinks, and deletions all
2178
+ participate; a run with no owned Git change creates no commit. The message
2179
+ identifies `ab upgrade` and lists every skill with an owned byte change together
2180
+ with its reported outcome. It supplies no authorship or attribution trailers.
2181
+ Unrelated staged, unstaged, and untracked work is neither committed nor
2182
+ unstaged.
2183
+
2184
+ The baseline is captured before self-update and carried to the replacement
2185
+ binary. `--no-commit` also survives that handoff and leaves the merge exactly as
2186
+ written. A replacement child marked as a handoff but missing that pre-update
2187
+ baseline — as when an older parent launches a newer child without the newer
2188
+ context — suppresses the entire automatic commit, names the cross-version
2189
+ compatibility reason, and leaves all upgrade-owned changes uncommitted for the
2190
+ operator. The skill-upgrade results and merge-derived exit status are unchanged.
2191
+ Any content conflict, Claude discovery conflict, pre-existing dirt in an owned
2192
+ path, non-Git target, changed HEAD/worktree identity, or in-progress merge,
2193
+ rebase, or cherry-pick likewise suppresses the whole commit and prints the
2194
+ reason. If upgrade cannot snapshot the worktree's Git index, it warns and
2195
+ declines to stage. A staging or commit failure restores that exact pre-attempt
2196
+ index without touching the merged worktree files, and warns with the original
2197
+ Git failure.
2198
+ Commit suppression or failure never changes the merge-derived exit status:
2199
+ content conflicts remain zero and discovery conflicts remain nonzero. Upgrade
2200
+ never pushes or rewrites existing history.
2201
+
2202
+ Skill handling remains the classic vendoring problem: `ab init` records the
2203
+ pristine version of each installed skill; upgrade three-way merges (pristine
2204
+ base × local edits × new default). Two fixed former defaults, `ab-setup` and
2205
+ `ab-verify-e2e`, additionally have one-time retirement handling when absent
2206
+ from the incoming distribution. A pristine record is required to prove
2207
+ Autobuild provenance; without it, a same-named repository-authored skill is
2208
+ untouched and unreported. Upgrade reports `removed` when the complete live and
2209
+ pristine trees match byte-for-byte, no configured agent verify or finalize step
2210
+ names the skill, and no user-owned Claude discovery entry remains; it also
2211
+ reports `removed` when the canonical live tree is already missing. In the latter
2212
+ state it clears obsolete provenance and removes only an Autobuild-owned dangling
2213
+ discovery link. Any customization, config reference, or inability to inspect
2214
+ config safely preserves the live tree and reports `kept`. When an otherwise
2215
+ removable canonical tree is shadowed by a user-owned
2216
+ `.claude/skills/<name>` discovery entry — either a distinct real directory or a
2217
+ foreign symlink — upgrade reports `kept`, removes only the canonical/pristine
2218
+ Autobuild-owned trees, preserves the entry byte-for-byte (including a symlink's
2219
+ link text and target), and includes it in the existing structured
2220
+ discovery-conflict report and nonzero exit behavior. Every terminal
2221
+ classification clears obsolete pristine ownership, so a second upgrade neither
2222
+ recreates a dangling link nor resurrects or re-reports the retirement. All
2223
+ other installed skills absent from the distribution retain the `unknown`
2224
+ local-addition classification.
2225
+
2226
+ A conflict may be resolved by the optional
2227
+ tool-free `upgrade` one-shot with a standing bias: **prefer the local
2228
+ customization**. The agent output is only an untrusted proposal — deterministic
2229
+ validation verifies skill identity and exact preservation of every already-clean
2230
+ merge region before anything is written. Each per-file judgment has a fixed
2231
+ generous deadline of at least ten minutes. While it runs on an interactive
2232
+ stdout, upgrade redraws a live indicator naming the skill and path, elapsed
2233
+ time, and Ctrl-C cancellation. Ctrl-C there aborts only that invocation: the
2234
+ file becomes an ordinary byte-untouched content conflict and upgrade continues.
2235
+ Outside an active resolution Ctrl-C retains ordinary process termination.
2236
+ Non-TTY output stays byte-identical and line-oriented. Failed, timed-out,
2237
+ cancelled, or unavailable judgment leaves both live and pristine byte-untouched
2238
+ and names the manual merge path. These content conflicts remain exit zero;
2239
+ discovery conflicts retain their nonzero contract. Local customization survives
2240
+ upgrades; divergence is visible instead of silent. No other command checks for
2241
+ or installs releases in the foreground or background.
2242
+
2243
+ ## 17. Out of scope for v2.0 (explicitly)
2244
+
2245
+ - True push `subscribe` (interface reserved; polling implementation).
2246
+ - Live transcript streaming for writers and readers (the stream primitive —
2247
+ SPEC §7.6 — has shipped; agent runners emitting parts and operator UIs
2248
+ rendering them remain future work).
2249
+ - Web UI (seam designed; terminal first).
2250
+ - Generic workflow DAGs (the grammar is fixed; extension via `verify:*` and
2251
+ `finalize:*` only).
2252
+
2253
+ ## 18. Open threads
2254
+
2255
+ 1. **[OPEN] Other-ingester detail** — observation harvest is decided (§8.8,
2256
+ §12); per-source filter design for scheduled `ingest:*` sources remains
2257
+ open.
2258
+ 2. **[OPEN] Retention/archival policy** — the v1 archival gap, now a store
2259
+ config concern rather than a repo problem. Needs a default (e.g. prune
2260
+ blobs for merged builds after N months, keep events).
2261
+ 3. **[OPEN] Global capacity** — per-repo capacity is the top-level `capacity`
2262
+ scalar (§16.1); whether a cross-repo global cap is needed, and where it
2263
+ lives, is unresolved.
2264
+ 4. **[DECIDED] Hosted, multi-host deployment** — independently deployed
2265
+ kernels, build sessions, and operator clients reach the hosted HTTP service
2266
+ tier described in §7.2. The tier owns authentication, write-rule and version
2267
+ enforcement and will colocate later session-authenticated human routes.
2268
+ Artifacts remain content-by-value for now; direct-to-blob references are a
2269
+ possible later protocol revision, not a second current deployment path.