@defrex/autobuild 0.8.0 → 0.10.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 (182) hide show
  1. package/README.md +1 -1
  2. package/SPEC.md +323 -26
  3. package/bin/ab-build-runner.ts +8 -2
  4. package/docs/architecture.md +166 -14
  5. package/docs/assets/headline-wide.png +0 -0
  6. package/docs/configuration.md +175 -28
  7. package/docs/hosted-dispatcher.md +69 -13
  8. package/docs/operator-api.md +2 -2
  9. package/docs/remote-store-protocol.md +204 -16
  10. package/docs/remote-ticket-protocol.md +1 -1
  11. package/docs/setup.md +12 -9
  12. package/docs/website.md +14 -0
  13. package/package.json +45 -23
  14. package/packages/core/src/cli/artifact.ts +130 -6
  15. package/packages/core/src/cli/binary.ts +9 -1
  16. package/packages/core/src/cli/build-control.ts +110 -13
  17. package/packages/core/src/cli/context.ts +46 -0
  18. package/packages/core/src/cli/dashboard/facts.ts +215 -0
  19. package/packages/core/src/cli/dashboard/frame-parse.ts +223 -0
  20. package/packages/core/src/cli/dashboard/model.ts +141 -153
  21. package/packages/core/src/cli/dashboard/poll.ts +76 -12
  22. package/packages/core/src/cli/dashboard/render.ts +34 -5
  23. package/packages/core/src/cli/dispatch-frontend.ts +54 -5
  24. package/packages/core/src/cli/dispatch-process.ts +1 -13
  25. package/packages/core/src/cli/dispatch.ts +392 -75
  26. package/packages/core/src/cli/harvest.ts +122 -8
  27. package/packages/core/src/cli/help.ts +62 -9
  28. package/packages/core/src/cli/init-readiness-remote.ts +110 -0
  29. package/packages/core/src/cli/init-readiness-shared.ts +72 -0
  30. package/packages/core/src/cli/init-validation.ts +266 -222
  31. package/packages/core/src/cli/installation.ts +50 -8
  32. package/packages/core/src/cli/main.ts +70 -11
  33. package/packages/core/src/cli/mcp.ts +23 -1
  34. package/packages/core/src/cli/plugin.ts +7 -4
  35. package/packages/core/src/cli/remote-poll.ts +277 -0
  36. package/packages/core/src/cli/repo-state.ts +22 -0
  37. package/packages/core/src/cli/repository-status.ts +39 -1
  38. package/packages/core/src/cli/self-update.ts +77 -2
  39. package/packages/core/src/cli/status.ts +150 -28
  40. package/packages/core/src/cli/terminals.ts +2 -1
  41. package/packages/core/src/cli/ticket-assets.ts +247 -0
  42. package/packages/core/src/cli/ticket.ts +453 -8
  43. package/packages/core/src/cli/wait.ts +157 -41
  44. package/packages/core/src/cli/watch.ts +138 -168
  45. package/packages/core/src/config/live.ts +23 -0
  46. package/packages/core/src/config/pipeline-source.ts +388 -0
  47. package/packages/core/src/config/schema.ts +251 -19
  48. package/packages/core/src/events/catalog.ts +1 -0
  49. package/packages/core/src/events/globs.ts +47 -0
  50. package/packages/core/src/events/payloads.ts +52 -2
  51. package/packages/core/src/events/repository.ts +52 -1
  52. package/packages/core/src/events/sessions.ts +69 -10
  53. package/packages/core/src/events/workspace-remote.ts +12 -0
  54. package/packages/core/src/integration/harness.ts +115 -2
  55. package/packages/core/src/kernel/__snapshots__/reducer-registry.test.ts.snap +1369 -0
  56. package/packages/core/src/kernel/auto-merge-default.ts +157 -0
  57. package/packages/core/src/kernel/auto-merge.ts +99 -22
  58. package/packages/core/src/kernel/dispatch-settings.ts +42 -17
  59. package/packages/core/src/kernel/dispatch-status.ts +23 -0
  60. package/packages/core/src/kernel/engine.ts +9 -438
  61. package/packages/core/src/kernel/generators/build-log.ts +259 -0
  62. package/packages/core/src/kernel/generators/repository-journals.ts +206 -0
  63. package/packages/core/src/kernel/generators/session-log.ts +72 -0
  64. package/packages/core/src/kernel/harvest.ts +62 -18
  65. package/packages/core/src/kernel/incremental-contract.ts +75 -0
  66. package/packages/core/src/kernel/incremental-fixtures.ts +211 -0
  67. package/packages/core/src/kernel/incremental.ts +53 -0
  68. package/packages/core/src/kernel/log-index.ts +620 -0
  69. package/packages/core/src/kernel/phases.ts +16 -2
  70. package/packages/core/src/kernel/pr-attachments.ts +164 -46
  71. package/packages/core/src/kernel/reducer-registry.ts +164 -0
  72. package/packages/core/src/kernel/reducer.ts +254 -73
  73. package/packages/core/src/operator/annotations.ts +6 -0
  74. package/packages/core/src/operator/control.ts +19 -5
  75. package/packages/core/src/operator/index.ts +71 -6
  76. package/packages/core/src/operator/query.ts +253 -24
  77. package/packages/core/src/operator/read-view.ts +98 -0
  78. package/packages/core/src/operator/registry.ts +55 -10
  79. package/packages/core/src/operator/requests.ts +9 -6
  80. package/packages/core/src/operator/sandbox-host.ts +140 -0
  81. package/packages/core/src/operator/sandbox.ts +247 -14
  82. package/packages/core/src/operator/ticket-source-backend.ts +47 -0
  83. package/packages/core/src/orchestrator/approvals.ts +115 -0
  84. package/packages/core/src/orchestrator/conversation.ts +167 -0
  85. package/packages/core/src/orchestrator/model.ts +86 -0
  86. package/packages/core/src/orchestrator/prompt.ts +61 -0
  87. package/packages/core/src/orchestrator/tools.ts +117 -0
  88. package/packages/core/src/orchestrator/turn-runner.ts +457 -0
  89. package/packages/core/src/plugin-sdk/index.ts +51 -1
  90. package/packages/core/src/plugins/load.ts +135 -7
  91. package/packages/core/src/plugins/manifest.ts +183 -10
  92. package/packages/core/src/plugins/registry.ts +73 -4
  93. package/packages/core/src/ports/forge/create.ts +1 -1
  94. package/packages/core/src/ports/forge/fake.ts +31 -0
  95. package/packages/core/src/ports/forge/github-transport.ts +13 -2
  96. package/packages/core/src/ports/forge/github.ts +13 -2
  97. package/packages/core/src/ports/forge/local-git.ts +31 -1
  98. package/packages/core/src/ports/runner/claude.ts +2 -2
  99. package/packages/core/src/ports/runner/codex.ts +6 -2
  100. package/packages/core/src/ports/runner/fake.ts +7 -1
  101. package/packages/core/src/ports/runner/pi.ts +8 -6
  102. package/packages/core/src/ports/runner/provider-error.ts +13 -3
  103. package/packages/core/src/ports/tickets/fake.ts +5 -1
  104. package/packages/core/src/ports/tickets/file.ts +169 -48
  105. package/packages/core/src/ports/tickets/{remote-index.ts → hosted-index.ts} +3 -2
  106. package/packages/core/src/ports/tickets/linear.ts +33 -1
  107. package/packages/core/src/ports/tickets/operations.ts +1 -1
  108. package/packages/core/src/ports/tickets/remote.ts +8 -1
  109. package/packages/core/src/ports/types.ts +49 -3
  110. package/packages/core/src/ports/workspace/builtin-capabilities.ts +114 -0
  111. package/packages/core/src/ports/workspace/create.ts +74 -39
  112. package/packages/core/src/ports/workspace/distribution-archive.ts +171 -12
  113. package/packages/core/src/ports/workspace/fake.ts +169 -4
  114. package/packages/core/src/ports/workspace/git-worktree.ts +246 -26
  115. package/packages/core/src/ports/workspace/github-origin.ts +36 -0
  116. package/packages/core/src/ports/workspace/operator-sandbox.ts +7 -1
  117. package/packages/core/src/ports/workspace/provider-capabilities.ts +171 -0
  118. package/packages/core/src/ports/workspace/vercel-capabilities.ts +110 -0
  119. package/packages/core/src/ports/workspace/vercel-sandbox.ts +142 -114
  120. package/packages/core/src/processes/build-child.ts +17 -2
  121. package/packages/core/src/processes/build-execution-state.ts +68 -15
  122. package/packages/core/src/processes/build-runner.ts +238 -93
  123. package/packages/core/src/processes/dispatcher-selectors.ts +123 -0
  124. package/packages/core/src/processes/dispatcher.ts +501 -201
  125. package/packages/core/src/processes/execution-settlement.ts +72 -39
  126. package/packages/core/src/processes/harvest-child.ts +5 -0
  127. package/packages/core/src/processes/harvest-execution-state.ts +41 -29
  128. package/packages/core/src/processes/harvest-runner.ts +66 -19
  129. package/packages/core/src/processes/harvest.ts +196 -24
  130. package/packages/core/src/processes/infrastructure-failure-budget.ts +31 -18
  131. package/packages/core/src/processes/orchestrator-tick.ts +360 -0
  132. package/packages/core/src/processes/publication-state.ts +164 -52
  133. package/packages/core/src/processes/repo-view.ts +1436 -0
  134. package/packages/core/src/processes/sandbox-state.ts +36 -2
  135. package/packages/core/src/processes/tick-state.ts +33 -0
  136. package/packages/core/src/store/adapter.ts +41 -0
  137. package/packages/core/src/store/build-scope.ts +90 -0
  138. package/packages/core/src/store/contract.ts +1205 -0
  139. package/packages/core/src/store/digest.ts +102 -0
  140. package/packages/core/src/store/local/schema.ts +46 -2
  141. package/packages/core/src/store/local/store.ts +445 -4
  142. package/packages/core/src/store/memory.ts +246 -0
  143. package/packages/core/src/store/new-build.ts +17 -0
  144. package/packages/core/src/store/phase-session.ts +94 -0
  145. package/packages/core/src/store/remote/client.ts +310 -2
  146. package/packages/core/src/store/remote/index.ts +14 -2
  147. package/packages/core/src/store/remote/protocol.ts +115 -12
  148. package/packages/core/src/store/remote/version.ts +6 -2
  149. package/packages/core/src/store/repo-state-events.ts +205 -0
  150. package/packages/core/src/store/retention.ts +7 -0
  151. package/packages/core/src/store/session-handle.ts +94 -0
  152. package/packages/core/src/{sessions/reducer.ts → store/session-reducer.ts} +79 -19
  153. package/packages/core/src/store/snapshots.ts +88 -0
  154. package/packages/core/src/store/streams/session-writer.ts +42 -2
  155. package/packages/core/src/store/streams/wait.ts +4 -5
  156. package/packages/core/src/store/ticket-assets.ts +458 -0
  157. package/packages/core/src/store/types.ts +160 -1
  158. package/packages/core/src/testing/index.ts +59 -0
  159. package/skills/code-review/SKILL.md +5 -1
  160. package/skills/guide/SKILL.md +152 -40
  161. package/skills/guide/references/operator-api.md +2 -2
  162. package/skills/guide/references/plugin-authoring.md +17 -0
  163. package/skills/guide/references/remote-store-protocol.md +204 -16
  164. package/skills/guide/references/remote-ticket-protocol.md +1 -1
  165. package/skills/guide/references/setup.md +12 -9
  166. package/skills/implement/SKILL.md +3 -0
  167. package/skills/operate/SKILL.md +126 -0
  168. package/skills/plan/SKILL.md +4 -0
  169. package/skills/plan-review/SKILL.md +5 -1
  170. package/skills/spec/SKILL.md +46 -3
  171. package/skills/tickets/SKILL.md +57 -5
  172. package/packages/core/src/cli/dashboard/frame-image.ts +0 -373
  173. package/packages/core/src/cli/store-opening.contract.ts +0 -289
  174. package/packages/core/src/cli/testkit.ts +0 -194
  175. package/packages/core/src/markdown.ts +0 -67
  176. package/packages/core/src/operator/client.ts +0 -311
  177. package/packages/core/src/operator/server.ts +0 -616
  178. package/packages/core/src/operator/sessions.ts +0 -223
  179. package/packages/core/src/ports/runner/live-contract-fixture.ts +0 -176
  180. package/packages/core/src/ports/tickets/remote-server.ts +0 -185
  181. package/packages/core/src/store/remote/server.ts +0 -923
  182. package/packages/core/src/testing/store-failures.ts +0 -32
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@defrex/autobuild",
3
- "version": "0.8.0",
3
+ "version": "0.10.0",
4
4
  "description": "Agent-driven software delivery from groomed ticket to merged PR",
5
5
  "repository": {
6
6
  "type": "git",
@@ -37,10 +37,15 @@
37
37
  "import": "./packages/core/src/store/remote/index.ts",
38
38
  "default": "./packages/core/src/store/remote/index.ts"
39
39
  },
40
- "./operator-api": {
41
- "types": "./packages/core/src/operator/index.ts",
42
- "import": "./packages/core/src/operator/index.ts",
43
- "default": "./packages/core/src/operator/index.ts"
40
+ "./hosted-tickets": {
41
+ "types": "./packages/core/src/ports/tickets/hosted-index.ts",
42
+ "import": "./packages/core/src/ports/tickets/hosted-index.ts",
43
+ "default": "./packages/core/src/ports/tickets/hosted-index.ts"
44
+ },
45
+ "./testing": {
46
+ "types": "./packages/core/src/testing/index.ts",
47
+ "import": "./packages/core/src/testing/index.ts",
48
+ "default": "./packages/core/src/testing/index.ts"
44
49
  },
45
50
  "./operator-presentation": {
46
51
  "types": "./packages/core/src/operator/presentation.ts",
@@ -52,11 +57,6 @@
52
57
  "import": "./packages/core/src/ports/tickets/update.ts",
53
58
  "default": "./packages/core/src/ports/tickets/update.ts"
54
59
  },
55
- "./remote-tickets": {
56
- "types": "./packages/core/src/ports/tickets/remote-index.ts",
57
- "import": "./packages/core/src/ports/tickets/remote-index.ts",
58
- "default": "./packages/core/src/ports/tickets/remote-index.ts"
59
- },
60
60
  "./origin": {
61
61
  "types": "./packages/core/src/kernel/origin.ts",
62
62
  "import": "./packages/core/src/kernel/origin.ts",
@@ -76,12 +76,32 @@
76
76
  "types": "./packages/core/src/ports/workspace/distribution-archive.ts",
77
77
  "import": "./packages/core/src/ports/workspace/distribution-archive.ts",
78
78
  "default": "./packages/core/src/ports/workspace/distribution-archive.ts"
79
+ },
80
+ "./operator": {
81
+ "types": "./packages/core/src/operator/index.ts",
82
+ "import": "./packages/core/src/operator/index.ts",
83
+ "default": "./packages/core/src/operator/index.ts"
84
+ },
85
+ "./operator-sandbox-host": {
86
+ "types": "./packages/core/src/operator/sandbox-host.ts",
87
+ "import": "./packages/core/src/operator/sandbox-host.ts",
88
+ "default": "./packages/core/src/operator/sandbox-host.ts"
79
89
  }
80
90
  },
81
91
  "files": [
82
92
  "bin",
83
93
  "packages/core/src",
84
94
  "!packages/core/src/**/*.test.ts",
95
+ "!packages/core/src/**/*.test.tsx",
96
+ "!packages/core/src/**/*.spec.ts",
97
+ "!packages/core/src/**/*.spec.tsx",
98
+ "!packages/core/src/testing/store-failures.ts",
99
+ "!packages/core/src/testing/packed-install.ts",
100
+ "!packages/core/src/cli/testkit.ts",
101
+ "!packages/core/src/cli/store-opening.contract.ts",
102
+ "!packages/core/src/ports/runner/live-contract-fixture.ts",
103
+ "!packages/core/src/cli/dashboard/frame-image.ts",
104
+ "!packages/core/src/markdown.ts",
85
105
  "skills",
86
106
  "templates",
87
107
  "LICENSE",
@@ -90,24 +110,25 @@
90
110
  "docs"
91
111
  ],
92
112
  "scripts": {
93
- "dev": "bun run --bun next dev",
94
113
  "dev:cli": "bun --hot --no-clear-screen bin/ab-dev.ts",
95
- "build": "bun run --bun next build",
96
- "deploy:build": "bun packages/hosted-store-service/src/bin.ts pack-distribution && bun run postgres:migrate && bun run build && bun tools/ship-packed-distribution.ts",
97
- "start": "bun run --bun next start",
98
- "hosted-store": "bun server.ts",
99
114
  "capture:dashboard": "bun tools/dashboard-capture.ts",
100
- "capture:web-dashboard": "bun tools/web-dashboard-capture.tsx",
115
+ "capture:web-dashboard": "bun packages/web-dashboard-capture/src/web-dashboard-capture.tsx",
116
+ "capture:website": "bun tools/website-capture.ts",
117
+ "website:dev": "bun packages/website/src/dev.ts",
118
+ "website:build": "bun packages/website/src/build.ts",
101
119
  "capture:readme-headline": "bun tools/readme-headline.ts",
120
+ "capture:website-hero": "bun tools/website-hero.ts",
121
+ "capture:website-og": "bun tools/website-og.ts",
102
122
  "release": "bun tools/release.ts",
103
123
  "postgres:migrate": "bun packages/postgres-store/src/bin.ts migrate",
104
- "check": "bun tools/biome-check.ts && bun tools/product-name-check.ts && bun tools/docs-asset-check.ts && bun tools/readme-headline.ts --check && bun tools/workspace-manifest-check.ts",
124
+ "check": "bun tools/biome-check.ts && bun tools/product-name-check.ts && bun tools/docs-asset-check.ts && bun tools/skill-docs-asset-check.ts && bun tools/readme-headline.ts --check && bun tools/website-hero.ts --check && bun tools/workspace-manifest-check.ts && bun tools/package-boundary-check.ts && bun tools/bin-mode-check.ts && bun tools/publish-contents-check.ts && bun tools/publish-imports-check.ts",
105
125
  "format:staged": "git-format-staged --formatter './node_modules/.bin/biome format --stdin-file-path {}' '*.js' '*.jsx' '*.ts' '*.tsx' '*.json' '*.jsonc' '*.css' '*.graphql' '*.gql'",
106
126
  "prepare": "husky",
107
- "test": "bun test packages/*/src tools bin app",
127
+ "test": "bun test packages/*/src tools bin packages/hosted-store-service/app",
108
128
  "typecheck": "tsc --noEmit"
109
129
  },
110
130
  "dependencies": {
131
+ "@ai-sdk/gateway": "4.0.82",
111
132
  "@modelcontextprotocol/sdk": "^1.30.0",
112
133
  "@vercel/sandbox": "^3.1.0",
113
134
  "ai": "^7.0.101",
@@ -118,18 +139,19 @@
118
139
  },
119
140
  "devDependencies": {
120
141
  "@biomejs/biome": "2.5.5",
142
+ "@defrex/autobuild-postgres-store": "0.9.0",
143
+ "@defrex/autobuild-vercel-sandbox": "0.9.0",
121
144
  "@resvg/resvg-js": "2.6.2",
122
145
  "@types/bun": "^1.3.14",
123
146
  "@types/node": "^24.0.0",
124
- "@types/pg": "8.16.0",
125
- "@types/react": "19.2.14",
126
- "@types/react-dom": "19.2.3",
127
147
  "@types/semver": "^7.7.1",
128
148
  "dejavu-fonts-ttf": "2.37.0",
129
149
  "drizzle-kit": "^0.31.10",
130
150
  "git-format-staged": "4.0.1",
131
151
  "husky": "9.1.7",
132
- "typescript": "5.9.3",
133
- "vercel": "^59.11.2"
152
+ "typescript": "5.9.3"
153
+ },
154
+ "patchedDependencies": {
155
+ "better-auth@1.4.18": "patches/better-auth@1.4.18.patch"
134
156
  }
135
157
  }
@@ -9,10 +9,11 @@ import { basename, dirname, resolve } from 'node:path'
9
9
  import { agentActor } from '../events/envelope'
10
10
  import { prAttachmentSchema } from '../ontology'
11
11
  import type { Exec } from '../ports/workspace/git-worktree'
12
- import type { Artifact, ArtifactMeta, BuildStore } from '../store/types'
12
+ import type { Artifact, ArtifactMeta, BuildStore, RepositoryArtifact } from '../store/types'
13
13
  import type { CliEnv } from './env'
14
- import { buildInRepository } from './repo-state'
15
- import { withAmbientReadStore, type StoreOpener } from './store-opening'
14
+ import { resolveAmbientReadSession } from './env'
15
+ import { buildInRepository, streamInRepository } from './repo-state'
16
+ import { withAmbientReadStore, withSessionlessStore, type StoreOpener } from './store-opening'
16
17
 
17
18
  export interface ArtifactDeps {
18
19
  store: BuildStore
@@ -174,9 +175,132 @@ export async function artifactDownload(
174
175
  )
175
176
  }
176
177
 
177
- const outputPath = resolve(opts.targetRepo, opts.outputPath)
178
- await mkdir(dirname(outputPath), { recursive: true })
179
- await writeFile(outputPath, artifact.content)
178
+ const outputPath = await writeArtifactBytes(opts.targetRepo, opts.outputPath, artifact.content)
180
179
  return { artifact, outputPath }
181
180
  })
182
181
  }
182
+
183
+ /** Shared download tail: exact bytes under the resolved output path (parent
184
+ * directories created), used by both the build-scoped and stream-addressed
185
+ * forms so neither can drift on byte handling. */
186
+ async function writeArtifactBytes(
187
+ targetRepo: string,
188
+ outputPath: string,
189
+ content: Uint8Array,
190
+ ): Promise<string> {
191
+ const resolved = resolve(targetRepo, outputPath)
192
+ await mkdir(dirname(resolved), { recursive: true })
193
+ await writeFile(resolved, content)
194
+ return resolved
195
+ }
196
+
197
+ export interface ArtifactDownloadStreamOpts {
198
+ /** Current checkout; repository identity resolves to its main worktree. */
199
+ targetRepo: string
200
+ env: Record<string, string | undefined>
201
+ exec: Exec
202
+ /** `stream:<id>` or `stream:<id>@<rev>`; rev pins the finalized artifact. */
203
+ spec: string
204
+ outputPath: string
205
+ /** Explicit --store; precedence remains flag > AB_STORE > local default. */
206
+ storeRef?: string
207
+ /** Adapter seam for local/remote selection tests. */
208
+ openStore?: StoreOpener
209
+ }
210
+
211
+ export interface ArtifactDownloadStreamResult {
212
+ artifact: RepositoryArtifact
213
+ outputPath: string
214
+ }
215
+
216
+ /** Operator retrieval of a finalized, repo-scoped stream artifact (the harvest
217
+ * session analog of the build-scoped form). A stream id is store-assigned and
218
+ * globally unique, so no build or repo argument is needed: the command reads
219
+ * the stream record, checks its scope, then fetches the close-deposited
220
+ * repository artifact.
221
+ *
222
+ * Authorization is the operator-wide read rule only (§8.2): this form requires
223
+ * a sessionless invocation and fails closed under any complete ambient phase
224
+ * identity — repository-scoped targets are never reachable by ambient
225
+ * own-build or own-harvest authority. The check runs here, before any store
226
+ * call, because local harvest-ambient handles would permit own-repo
227
+ * `getRepoArtifact` while remote handles rely on the core to enforce scope. */
228
+ export async function artifactDownloadStream(
229
+ opts: ArtifactDownloadStreamOpts,
230
+ ): Promise<ArtifactDownloadStreamResult> {
231
+ const ambient = resolveAmbientReadSession(opts.env)
232
+ if (ambient !== undefined) {
233
+ throw new Error(
234
+ 'repo-scoped stream downloads are operator reads (§8.2): ambient phase ' +
235
+ 'identity (build or Harvest) never extends to repository-scoped targets — ' +
236
+ 'run this command without agent identity markers',
237
+ )
238
+ }
239
+ const { kind, rev } = parseArtifactSpec(opts.spec)
240
+ const streamId = kind.startsWith('stream:') ? kind.slice('stream:'.length) : ''
241
+ if (streamId.trim() === '') {
242
+ throw new Error(
243
+ `invalid stream ref "${opts.spec}" — expected 'stream:<id>' or 'stream:<id>@<rev>'`,
244
+ )
245
+ }
246
+ return withSessionlessStore(
247
+ {
248
+ targetRepo: opts.targetRepo,
249
+ env: opts.env,
250
+ exec: opts.exec,
251
+ ...(opts.storeRef !== undefined ? { storeRef: opts.storeRef } : {}),
252
+ ...(opts.openStore !== undefined ? { openStore: opts.openStore } : {}),
253
+ },
254
+ async (context) => {
255
+ const record = await context.store.getStream(streamId)
256
+ if (record === null) {
257
+ throw new Error(
258
+ `no stream "${streamId}" in this store — run 'ab harvest status' to find ` +
259
+ 'session stream ids, or pass --store <ref>',
260
+ )
261
+ }
262
+ if (record.scope.kind === 'build') {
263
+ throw new Error(
264
+ `stream "${streamId}" is build-scoped — download it with ` +
265
+ `'ab artifact download ${record.scope.build} stream:${streamId}'`,
266
+ )
267
+ }
268
+ if (record.scope.kind === 'session') {
269
+ throw new Error(
270
+ `stream "${streamId}" is session-scoped — read it through the operator ` +
271
+ `session stream surface (GET /sessions/${record.scope.session}/streams/${streamId})`,
272
+ )
273
+ }
274
+ // Origin-aware membership, mirroring the build-scoped path above.
275
+ // Unlike a BuildRecord, StreamScope carries no `repoOrigin`, so a legacy
276
+ // physical-path scope.repo stays rejected here (see
277
+ // `streamInRepository` for the documented AUT-314-class divergence).
278
+ if (!streamInRepository(record.scope.repo, context.repo)) {
279
+ throw new Error(
280
+ `stream "${streamId}" belongs to repository "${record.scope.repo}", not "${context.repo}"`,
281
+ )
282
+ }
283
+ if (record.status === 'open') {
284
+ throw new Error(
285
+ `stream "${streamId}" is open — its finalized artifact exists only after ` +
286
+ 'the stream closes (§7.6)',
287
+ )
288
+ }
289
+ const artifact = await context.store.getRepoArtifact(record.scope.repo, kind, rev)
290
+ if (artifact === null) {
291
+ const available = await context.store.listRepoArtifacts(record.scope.repo, kind)
292
+ const refs = available.map((meta) => `${meta.kind}@${meta.revision}`)
293
+ throw new Error(
294
+ `no "${kind}" artifact${rev !== undefined ? ` at rev ${rev}` : ''} in ` +
295
+ `repository "${record.scope.repo}" — available refs: ${refs.join(', ') || '(none)'}`,
296
+ )
297
+ }
298
+ const outputPath = await writeArtifactBytes(
299
+ opts.targetRepo,
300
+ opts.outputPath,
301
+ artifact.content,
302
+ )
303
+ return { artifact, outputPath }
304
+ },
305
+ )
306
+ }
@@ -165,12 +165,20 @@ export async function runBinary(
165
165
  // A scoped phase process is composed from the build worktree's immutable
166
166
  // configuration just like dispatch. Load and select the forge before opening
167
167
  // the store so plugin failures cannot partially execute terminal plumbing.
168
+ // Guest tolerance (AUT-517), always on here: in checkout mode `ab dispatch`
169
+ // already failed startup if a configured plugin did not resolve, and
170
+ // origin-mode phase processes only run inside guests — so an unresolvable
171
+ // bare specifier here can only be the provider-plugin case, which a phase
172
+ // process (never constructing workspace providers) may skip.
168
173
  let forge: Awaited<ReturnType<typeof createForge>>
169
174
  try {
170
175
  const repoRoot = process.cwd()
171
176
  const packageRoot = await resolveMainRepo(repoRoot, spawnExec)
172
177
  const config = await loadConfig(join(repoRoot, 'autobuild.toml'))
173
- const plugins = await loadPlugins(config.plugins, repoRoot, { packageRoot })
178
+ const plugins = await loadPlugins(config.plugins, repoRoot, {
179
+ packageRoot,
180
+ guest: true,
181
+ })
174
182
  forge = await createForge({
175
183
  name: config.forge,
176
184
  registry: plugins,
@@ -7,11 +7,18 @@
7
7
  * normal runner and dispatcher consume them.
8
8
  */
9
9
  import type { AbEvent } from '../events/catalog'
10
+ import type { PinnedAsset } from '../events/payloads'
10
11
  import { humanActor, KERNEL, type Via } from '../events/envelope'
11
- import { reduceBuild, type BuildState, type OpenEscalation } from '../kernel/reducer'
12
+ import {
13
+ reduceBuild,
14
+ discardInFlight,
15
+ type BuildState,
16
+ type OpenEscalation,
17
+ } from '../kernel/reducer'
12
18
  import type { ArtifactRef, BuildOutcome, BuildStatus, TicketRef } from '../ontology'
13
19
  import type { Exec } from '../ports/workspace/git-worktree'
14
20
  import { specConformance } from '../spec-standard'
21
+ import { samplePinnedAssets } from '../store/ticket-assets'
15
22
  import type { BuildStore } from '../store/types'
16
23
  import { withSessionlessStore, type StoreOpener } from './store-opening'
17
24
 
@@ -107,6 +114,7 @@ export type BuildControlErrorCode =
107
114
  | 'missing-authorization'
108
115
  | 'no-longer-active'
109
116
  | 'abort-pending'
117
+ | 'discard-pending'
110
118
  | 'review-round-ceiling-unavailable'
111
119
  | 'incompatible-answer-options'
112
120
 
@@ -226,6 +234,78 @@ function hasPendingAbort(state: BuildState): boolean {
226
234
  return state.pendingCommands.some((command) => command.command === 'abort')
227
235
  }
228
236
 
237
+ function discardPending(slug: string): BuildControlError {
238
+ return new BuildControlError(
239
+ 'discard-pending',
240
+ `build "${slug}" has an outstanding discard request; auto-merge consent was not recorded. ` +
241
+ 'The discard settles only when the build terminalizes — nothing was appended. ' +
242
+ 'If the discard raced with runner attachment and you still want the change, ' +
243
+ 'merge the PR manually.',
244
+ )
245
+ }
246
+
247
+ /** Record or withdraw per-build auto-merge consent — the write path for
248
+ * `auto-merge-on`, `auto-merge-off`, and `toggle-auto-merge`.
249
+ *
250
+ * Ruling (AUT-445): recording consent on a build with an outstanding discard
251
+ * request is rejected — the fan-out's discard exclusion
252
+ * (`autoMergeDefaultEligible`'s discard clause, AUT-418) applied to explicit
253
+ * consent. Both controls call the same `discardInFlight` predicate from
254
+ * kernel/reducer.ts, so they cannot diverge silently. Withdrawing consent (OFF)
255
+ * is deliberately still allowed: `checkPr` honors revocation before anything
256
+ * else, so an OFF strictly shrinks the merge set, and it is the only way to
257
+ * withdraw consent recorded before the discard (a fan-out or claim-time seed) —
258
+ * refusing it would leave that discard-contradicting consent driving the merge
259
+ * anyway. The fan-out is direction-blind because it is a queue cleaner that
260
+ * withdraws nothing that survives the settlement; this guard is directional on
261
+ * purpose, and the divergence is stated here rather than left accidental.
262
+ *
263
+ * The guard runs inside a compare-and-append loop (the `appendIfCurrent` idiom
264
+ * the fan-out's dispatcher loop and `bulkControlRepository` already use), so a
265
+ * discard request that lands between the guard's read and this append cannot
266
+ * slip past it: the CAS misses, the loop re-reduces the log, and the guard
267
+ * fires on the fresh state. The toggle's direction is re-resolved from the
268
+ * re-read state each attempt. `activeState` runs before the guard on every
269
+ * attempt: a queued build (where the discard normally sits) still reports the
270
+ * existing `inactive` error. */
271
+ async function appendConsentCommand(
272
+ store: BuildStore,
273
+ slug: string,
274
+ user: string,
275
+ action: 'auto-merge-on' | 'auto-merge-off' | 'toggle-auto-merge',
276
+ via?: Via,
277
+ ): Promise<BuildControlResult> {
278
+ while (true) {
279
+ const state = reduceBuild(await store.getEvents(slug))
280
+ activeState(slug, state)
281
+ const command =
282
+ action === 'toggle-auto-merge'
283
+ ? state.autoMerge.requested
284
+ ? 'auto-merge-off'
285
+ : 'auto-merge-on'
286
+ : action
287
+ if (command === 'auto-merge-on' && discardInFlight(state)) throw discardPending(slug)
288
+ const actor = humanActor(user, via)
289
+ // Written out rather than selected into a variable so `appendIfCurrent`'s
290
+ // `T extends EventType` infers a concrete payload type in each branch.
291
+ const appended =
292
+ command === 'auto-merge-on'
293
+ ? await store.appendIfCurrent(slug, state.lastSeq, {
294
+ actor,
295
+ type: 'build.auto-merge-requested',
296
+ payload: {},
297
+ })
298
+ : await store.appendIfCurrent(slug, state.lastSeq, {
299
+ actor,
300
+ type: 'build.auto-merge-cancelled',
301
+ payload: {},
302
+ })
303
+ if (appended !== null) return { kind: 'command', slug, command, event: appended }
304
+ // The stream advanced under us (a racing discard, a fan-out write, a
305
+ // runner acknowledgement): loop to re-read and re-check before writing.
306
+ }
307
+ }
308
+
229
309
  function abortPending(slug: string): BuildControlError {
230
310
  return new BuildControlError(
231
311
  'abort-pending',
@@ -254,6 +334,9 @@ export type ReviseDecision =
254
334
  | {
255
335
  kind: 'complete'
256
336
  artifact: ArtifactRef
337
+ /** The assets sampled with the authorization; absent for a supplied body
338
+ * and for authorizations recorded before assets were pinned. */
339
+ assets?: PinnedAsset[]
257
340
  open: OpenEscalation[]
258
341
  escalationSeq: number
259
342
  }
@@ -289,7 +372,13 @@ export function decideRevise(
289
372
  }
290
373
  }
291
374
  const escalationSeq = Math.max(...[...open, ...pending].map((item) => item.seq))
292
- return { kind: 'complete', artifact: authorized.artifact, open, escalationSeq }
375
+ return {
376
+ kind: 'complete',
377
+ artifact: authorized.artifact,
378
+ ...(authorized.assets !== undefined ? { assets: authorized.assets } : {}),
379
+ open,
380
+ escalationSeq,
381
+ }
293
382
  }
294
383
  if (open.length === 0) {
295
384
  const paused =
@@ -425,9 +514,14 @@ export async function controlBuild(opts: ControlBuildOpts): Promise<BuildControl
425
514
  switch (opts.action.kind) {
426
515
  case 'pause':
427
516
  case 'resume':
517
+ return appendCommand(opts.store, opts.slug, user, opts.action.kind, opts.via)
518
+
428
519
  case 'auto-merge-on':
429
520
  case 'auto-merge-off':
430
- return appendCommand(opts.store, opts.slug, user, opts.action.kind, opts.via)
521
+ case 'toggle-auto-merge':
522
+ // Consent has its own guarded, compare-and-append write path; the
523
+ // outer `activeState` above only short-circuits the common inactive case.
524
+ return appendConsentCommand(opts.store, opts.slug, user, opts.action.kind, opts.via)
431
525
 
432
526
  case 'dashboard-pause': {
433
527
  const pendingPause = state.pendingCommands.some((command) => command.command === 'pause')
@@ -464,15 +558,6 @@ export async function controlBuild(opts: ControlBuildOpts): Promise<BuildControl
464
558
  return appendCommand(opts.store, opts.slug, user, 'resume', opts.via)
465
559
  }
466
560
 
467
- case 'toggle-auto-merge':
468
- return appendCommand(
469
- opts.store,
470
- opts.slug,
471
- user,
472
- state.autoMerge.requested ? 'auto-merge-off' : 'auto-merge-on',
473
- opts.via,
474
- )
475
-
476
561
  case 'answer': {
477
562
  const captured =
478
563
  opts.action.escalationIds === undefined ? undefined : new Set(opts.action.escalationIds)
@@ -510,11 +595,16 @@ export async function controlBuild(opts: ControlBuildOpts): Promise<BuildControl
510
595
  if (initial.kind === 'refuse') throwRefusal(initial)
511
596
 
512
597
  let artifact: ArtifactRef
598
+ // From-ticket revisions re-pin the ticket's assets; a supplied body
599
+ // leaves the pinned set alone (`undefined`). Sampled once, with the
600
+ // body, and persisted on the answers so a retry reuses it.
601
+ let assets: PinnedAsset[] | undefined
513
602
  let authorizedEarlier = initial.kind === 'complete'
514
603
  if (initial.kind === 'complete') {
515
604
  // Completion rule: recorded authorization wins before the caller's
516
605
  // lazy body source is opened, regardless of remaining blockers.
517
606
  artifact = initial.artifact
607
+ assets = initial.assets
518
608
  } else {
519
609
  let body: string
520
610
  const source = opts.action.resolve.body
@@ -534,6 +624,7 @@ export async function controlBuild(opts: ControlBuildOpts): Promise<BuildControl
534
624
  )
535
625
  }
536
626
  body = await opts.readTicketBody(record.ticket)
627
+ assets = await samplePinnedAssets(opts.store, record.repo, record.ticket.id)
537
628
  }
538
629
  const conformance = specConformance(body)
539
630
  if (!conformance.conforms) {
@@ -564,6 +655,7 @@ export async function controlBuild(opts: ControlBuildOpts): Promise<BuildControl
564
655
  if (decision.kind === 'refuse') throwRefusal(decision)
565
656
  if (decision.kind === 'complete') {
566
657
  artifact = decision.artifact
658
+ assets = decision.assets
567
659
  authorizedEarlier = true
568
660
  }
569
661
  const answer = guidance === '' ? SPEC_REVISION_ANSWER : guidance
@@ -580,13 +672,18 @@ export async function controlBuild(opts: ControlBuildOpts): Promise<BuildControl
580
672
  answer,
581
673
  resolution: 'revise-spec',
582
674
  artifact,
675
+ ...(assets !== undefined ? { assets } : {}),
583
676
  },
584
677
  })
585
678
  }
586
679
  await opts.store.append(opts.slug, {
587
680
  actor: KERNEL,
588
681
  type: 'spec.revised',
589
- payload: { artifact, escalation: decision.escalationSeq },
682
+ payload: {
683
+ artifact,
684
+ escalation: decision.escalationSeq,
685
+ ...(assets !== undefined ? { assets } : {}),
686
+ },
590
687
  })
591
688
  if (alsoPaused) {
592
689
  await opts.store.append(opts.slug, {
@@ -44,6 +44,7 @@ import {
44
44
  } from '../ports/workspace/phase-scratch'
45
45
  import type { Exec } from '../ports/workspace/git-worktree'
46
46
  import type { BuildStore } from '../store/types'
47
+ import { writeAssetTo } from './ticket-assets'
47
48
  import type { CliEnv } from './env'
48
49
 
49
50
  export interface ContextDeps {
@@ -81,6 +82,16 @@ export interface ContextManifest {
81
82
  notesPath?: string
82
83
  /** relPath (under .ab/) → source artifact ref, or 'derived'. */
83
84
  materialized: Record<string, MaterializedEntry>
85
+ /** The ticket assets this build froze (SPEC §6.3), each written to `path`
86
+ * (relative to the workspace). Omitted when the build pinned none. */
87
+ assets?: {
88
+ kind: string
89
+ name: string
90
+ revision: number
91
+ layout: 'file' | 'tree'
92
+ size: number
93
+ path: string
94
+ }[]
84
95
  /** From the latest `implement.completed` (§8.3 code-review/verify inputs). */
85
96
  commitRange?: CommitRange
86
97
  /** Reconcile: `{baseSha}` freshly recorded by this attempt's phase start (§15.7). */
@@ -276,6 +287,41 @@ export async function buildContext(deps: ContextDeps): Promise<ContextManifest>
276
287
  }
277
288
  }
278
289
 
290
+ if (inputs.assets === true && state.pinnedAssets.length > 0) {
291
+ // The pin, not the ticket's current state: the revisions `build.created`
292
+ // (or the latest re-pinning `spec.revised`) recorded, fetched by explicit
293
+ // revision so a later attach/replace/remove cannot change what a phase sees.
294
+ const root = locate('assets')
295
+ manifest.assets = []
296
+ for (const pinned of state.pinnedAssets) {
297
+ const asset = await store.getPinnedTicketAsset(
298
+ build,
299
+ pinned.kind,
300
+ pinned.name,
301
+ pinned.revision,
302
+ )
303
+ if (asset === null) {
304
+ throw new Error(
305
+ `pinned ticket asset ${pinned.kind}/${pinned.name} revision ${pinned.revision} is missing from the store`,
306
+ )
307
+ }
308
+ const relPath = `${root}/${pinned.kind}/${pinned.name}`
309
+ const target = join(abDir, relPath)
310
+ // A file asset lands inside its directory as `<filename>`; a tree asset
311
+ // keeps its relative tree, empty directories included.
312
+ await mkdir(target, { recursive: true })
313
+ await writeAssetTo(asset, target)
314
+ manifest.assets.push({
315
+ kind: pinned.kind,
316
+ name: pinned.name,
317
+ revision: pinned.revision,
318
+ layout: pinned.layout,
319
+ size: pinned.size,
320
+ path: `.ab/${relPath}`,
321
+ })
322
+ }
323
+ }
324
+
279
325
  if (inputs.spec === true) {
280
326
  // §6.3 immutability: the spec a phase sees is the rev the spec.* events
281
327
  // anchor (spec.imported/authored/revised → state.specRev), never the