@defrex/autobuild 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (251) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +192 -0
  3. package/SPEC.md +2269 -0
  4. package/bin/ab-build-runner.ts +47 -0
  5. package/bin/ab-dev.ts +27 -0
  6. package/bin/ab-dispatch-kernel.ts +92 -0
  7. package/bin/ab-harvest-runner.ts +29 -0
  8. package/bin/ab-init-probe.ts +19 -0
  9. package/bin/ab-process-group-reaper.ts +32 -0
  10. package/bin/ab.ts +6 -0
  11. package/bin/agent/ab +3 -0
  12. package/docs/answering-blocked-builds.md +89 -0
  13. package/docs/architecture.md +726 -0
  14. package/docs/assets/headline-wide.png +0 -0
  15. package/docs/configuration.md +1623 -0
  16. package/docs/hosted-dispatcher-cutover-validation.md +178 -0
  17. package/docs/hosted-dispatcher.md +204 -0
  18. package/docs/mcp.md +119 -0
  19. package/docs/operator-api.md +186 -0
  20. package/docs/remote-store-protocol.md +972 -0
  21. package/docs/remote-ticket-protocol.md +79 -0
  22. package/docs/setup.md +289 -0
  23. package/docs/spec-standard.md +63 -0
  24. package/docs/ticket-dependencies.md +59 -0
  25. package/docs/vercel-sandbox-rollout-probe.md +7 -0
  26. package/package.json +135 -0
  27. package/packages/core/src/cli/args.ts +74 -0
  28. package/packages/core/src/cli/artifact.ts +182 -0
  29. package/packages/core/src/cli/binary.ts +198 -0
  30. package/packages/core/src/cli/build-control.ts +789 -0
  31. package/packages/core/src/cli/build-progress.ts +55 -0
  32. package/packages/core/src/cli/bulk-control.ts +385 -0
  33. package/packages/core/src/cli/context.ts +470 -0
  34. package/packages/core/src/cli/dashboard/actions.ts +59 -0
  35. package/packages/core/src/cli/dashboard/cells.ts +92 -0
  36. package/packages/core/src/cli/dashboard/composer.ts +230 -0
  37. package/packages/core/src/cli/dashboard/detail.ts +100 -0
  38. package/packages/core/src/cli/dashboard/frame-image.ts +373 -0
  39. package/packages/core/src/cli/dashboard/live.ts +152 -0
  40. package/packages/core/src/cli/dashboard/model.ts +1173 -0
  41. package/packages/core/src/cli/dashboard/poll.ts +196 -0
  42. package/packages/core/src/cli/dashboard/render.ts +1568 -0
  43. package/packages/core/src/cli/dashboard/selection.ts +66 -0
  44. package/packages/core/src/cli/dashboard/session-feed.ts +244 -0
  45. package/packages/core/src/cli/dashboard/session-view.ts +506 -0
  46. package/packages/core/src/cli/dashboard/transcript.ts +108 -0
  47. package/packages/core/src/cli/dev.ts +46 -0
  48. package/packages/core/src/cli/dispatch-frontend.ts +1045 -0
  49. package/packages/core/src/cli/dispatch-process.ts +259 -0
  50. package/packages/core/src/cli/dispatch.ts +3692 -0
  51. package/packages/core/src/cli/dotenv.ts +48 -0
  52. package/packages/core/src/cli/env.ts +206 -0
  53. package/packages/core/src/cli/harvest.ts +698 -0
  54. package/packages/core/src/cli/help.ts +713 -0
  55. package/packages/core/src/cli/init-agent.ts +106 -0
  56. package/packages/core/src/cli/init-validation.ts +583 -0
  57. package/packages/core/src/cli/init.ts +756 -0
  58. package/packages/core/src/cli/installation.ts +333 -0
  59. package/packages/core/src/cli/keyboard.ts +261 -0
  60. package/packages/core/src/cli/main.ts +1399 -0
  61. package/packages/core/src/cli/mcp.ts +279 -0
  62. package/packages/core/src/cli/models.ts +65 -0
  63. package/packages/core/src/cli/observe.ts +55 -0
  64. package/packages/core/src/cli/plugin.ts +227 -0
  65. package/packages/core/src/cli/pr-attachments.ts +138 -0
  66. package/packages/core/src/cli/pr-summary.ts +124 -0
  67. package/packages/core/src/cli/publication-settlement.ts +188 -0
  68. package/packages/core/src/cli/repo-state.ts +213 -0
  69. package/packages/core/src/cli/repository-status.ts +80 -0
  70. package/packages/core/src/cli/self-update.ts +443 -0
  71. package/packages/core/src/cli/status.ts +895 -0
  72. package/packages/core/src/cli/store-opening.contract.ts +289 -0
  73. package/packages/core/src/cli/store-opening.ts +167 -0
  74. package/packages/core/src/cli/store-ref.ts +33 -0
  75. package/packages/core/src/cli/terminal-restore.ts +174 -0
  76. package/packages/core/src/cli/terminal.ts +361 -0
  77. package/packages/core/src/cli/terminals.ts +995 -0
  78. package/packages/core/src/cli/testkit.ts +194 -0
  79. package/packages/core/src/cli/ticket.ts +484 -0
  80. package/packages/core/src/cli/upgrade-agent.ts +180 -0
  81. package/packages/core/src/cli/upgrade-commit.ts +468 -0
  82. package/packages/core/src/cli/upgrade-notice.ts +91 -0
  83. package/packages/core/src/cli/upgrade-progress.ts +110 -0
  84. package/packages/core/src/cli/upgrade.ts +888 -0
  85. package/packages/core/src/cli/wait.ts +587 -0
  86. package/packages/core/src/cli/watch.ts +932 -0
  87. package/packages/core/src/config/live.ts +255 -0
  88. package/packages/core/src/config/load.ts +98 -0
  89. package/packages/core/src/config/roles.ts +438 -0
  90. package/packages/core/src/config/schema.ts +774 -0
  91. package/packages/core/src/distribution.ts +24 -0
  92. package/packages/core/src/events/catalog.ts +201 -0
  93. package/packages/core/src/events/envelope.ts +59 -0
  94. package/packages/core/src/events/payloads.ts +618 -0
  95. package/packages/core/src/events/repository.ts +559 -0
  96. package/packages/core/src/events/sessions.ts +180 -0
  97. package/packages/core/src/harvest/schema.ts +147 -0
  98. package/packages/core/src/ids.ts +45 -0
  99. package/packages/core/src/integration/harness.ts +681 -0
  100. package/packages/core/src/kernel/auto-merge.ts +259 -0
  101. package/packages/core/src/kernel/converge.ts +117 -0
  102. package/packages/core/src/kernel/dispatch-settings.ts +60 -0
  103. package/packages/core/src/kernel/dispatch-status.ts +141 -0
  104. package/packages/core/src/kernel/engine.ts +1047 -0
  105. package/packages/core/src/kernel/harvest.ts +823 -0
  106. package/packages/core/src/kernel/origin.ts +69 -0
  107. package/packages/core/src/kernel/phases.ts +196 -0
  108. package/packages/core/src/kernel/plan-verify-selection.ts +105 -0
  109. package/packages/core/src/kernel/pr-attachments.ts +82 -0
  110. package/packages/core/src/kernel/reducer.ts +752 -0
  111. package/packages/core/src/kernel/stall.ts +155 -0
  112. package/packages/core/src/kernel/verify-applicability.ts +52 -0
  113. package/packages/core/src/markdown.ts +67 -0
  114. package/packages/core/src/ontology.ts +288 -0
  115. package/packages/core/src/open-map.ts +203 -0
  116. package/packages/core/src/operator/annotations.ts +187 -0
  117. package/packages/core/src/operator/client.ts +311 -0
  118. package/packages/core/src/operator/control.ts +117 -0
  119. package/packages/core/src/operator/index.ts +62 -0
  120. package/packages/core/src/operator/presentation.ts +22 -0
  121. package/packages/core/src/operator/protocol.ts +98 -0
  122. package/packages/core/src/operator/query.ts +201 -0
  123. package/packages/core/src/operator/registry.ts +1043 -0
  124. package/packages/core/src/operator/requests.ts +138 -0
  125. package/packages/core/src/operator/sandbox.ts +458 -0
  126. package/packages/core/src/operator/server.ts +616 -0
  127. package/packages/core/src/operator/sessions.ts +223 -0
  128. package/packages/core/src/operator/tickets.ts +177 -0
  129. package/packages/core/src/plugin-sdk/index.ts +174 -0
  130. package/packages/core/src/plugins/contract-entry.ts +74 -0
  131. package/packages/core/src/plugins/contract-env.ts +3 -0
  132. package/packages/core/src/plugins/load.ts +259 -0
  133. package/packages/core/src/plugins/manifest.ts +236 -0
  134. package/packages/core/src/plugins/registry.ts +274 -0
  135. package/packages/core/src/plugins/runtimes.ts +75 -0
  136. package/packages/core/src/ports/forge/contract.ts +258 -0
  137. package/packages/core/src/ports/forge/create.ts +74 -0
  138. package/packages/core/src/ports/forge/fake.ts +488 -0
  139. package/packages/core/src/ports/forge/github-pr-attachments.ts +351 -0
  140. package/packages/core/src/ports/forge/github-transport.ts +352 -0
  141. package/packages/core/src/ports/forge/github.ts +1162 -0
  142. package/packages/core/src/ports/forge/local-git.ts +568 -0
  143. package/packages/core/src/ports/runner/claude.ts +873 -0
  144. package/packages/core/src/ports/runner/cli-stream.ts +90 -0
  145. package/packages/core/src/ports/runner/codex.ts +873 -0
  146. package/packages/core/src/ports/runner/contract.ts +492 -0
  147. package/packages/core/src/ports/runner/fake.ts +213 -0
  148. package/packages/core/src/ports/runner/json-record.ts +2 -0
  149. package/packages/core/src/ports/runner/live-contract-fixture.ts +176 -0
  150. package/packages/core/src/ports/runner/one-shot.ts +29 -0
  151. package/packages/core/src/ports/runner/pi-bridge-core.ts +136 -0
  152. package/packages/core/src/ports/runner/pi-bridge.js +6 -0
  153. package/packages/core/src/ports/runner/pi-rpc.ts +454 -0
  154. package/packages/core/src/ports/runner/pi.ts +617 -0
  155. package/packages/core/src/ports/runner/production.ts +74 -0
  156. package/packages/core/src/ports/runner/provider-error.ts +121 -0
  157. package/packages/core/src/ports/runner/routing.ts +234 -0
  158. package/packages/core/src/ports/runner/runtime.ts +214 -0
  159. package/packages/core/src/ports/runner/session-env.ts +34 -0
  160. package/packages/core/src/ports/runner/stream-parts.ts +103 -0
  161. package/packages/core/src/ports/tickets/contract.ts +570 -0
  162. package/packages/core/src/ports/tickets/create.ts +135 -0
  163. package/packages/core/src/ports/tickets/fake.ts +215 -0
  164. package/packages/core/src/ports/tickets/file.ts +559 -0
  165. package/packages/core/src/ports/tickets/linear.ts +811 -0
  166. package/packages/core/src/ports/tickets/operations.ts +161 -0
  167. package/packages/core/src/ports/tickets/remote-index.ts +13 -0
  168. package/packages/core/src/ports/tickets/remote-protocol.ts +80 -0
  169. package/packages/core/src/ports/tickets/remote-server.ts +185 -0
  170. package/packages/core/src/ports/tickets/remote.ts +130 -0
  171. package/packages/core/src/ports/tickets/update.ts +51 -0
  172. package/packages/core/src/ports/types.ts +528 -0
  173. package/packages/core/src/ports/workspace/build-execution.ts +123 -0
  174. package/packages/core/src/ports/workspace/contract.ts +136 -0
  175. package/packages/core/src/ports/workspace/create.ts +132 -0
  176. package/packages/core/src/ports/workspace/distribution-archive.ts +317 -0
  177. package/packages/core/src/ports/workspace/fake.ts +432 -0
  178. package/packages/core/src/ports/workspace/git-worktree.ts +848 -0
  179. package/packages/core/src/ports/workspace/harvest-execution.ts +111 -0
  180. package/packages/core/src/ports/workspace/in-process-build-execution.ts +30 -0
  181. package/packages/core/src/ports/workspace/local-build-execution.ts +88 -0
  182. package/packages/core/src/ports/workspace/operator-sandbox.ts +142 -0
  183. package/packages/core/src/ports/workspace/phase-scratch.ts +156 -0
  184. package/packages/core/src/ports/workspace/vercel-sandbox.ts +2541 -0
  185. package/packages/core/src/processes/build-child-exit.ts +78 -0
  186. package/packages/core/src/processes/build-child.ts +119 -0
  187. package/packages/core/src/processes/build-execution-state.ts +95 -0
  188. package/packages/core/src/processes/build-parent-watch.ts +35 -0
  189. package/packages/core/src/processes/build-runner.ts +2614 -0
  190. package/packages/core/src/processes/dispatcher.ts +2825 -0
  191. package/packages/core/src/processes/execution-settlement.ts +171 -0
  192. package/packages/core/src/processes/harvest-child.ts +115 -0
  193. package/packages/core/src/processes/harvest-execution-state.ts +121 -0
  194. package/packages/core/src/processes/harvest-runner.ts +1656 -0
  195. package/packages/core/src/processes/harvest.ts +595 -0
  196. package/packages/core/src/processes/infrastructure-failure-budget.ts +146 -0
  197. package/packages/core/src/processes/phase-failure-budget.ts +52 -0
  198. package/packages/core/src/processes/process-group-reaper.ts +35 -0
  199. package/packages/core/src/processes/process-group.ts +65 -0
  200. package/packages/core/src/processes/publication-loss.ts +62 -0
  201. package/packages/core/src/processes/publication-state.ts +93 -0
  202. package/packages/core/src/processes/sandbox-state.ts +101 -0
  203. package/packages/core/src/registry.ts +24 -0
  204. package/packages/core/src/sessions/reducer.ts +185 -0
  205. package/packages/core/src/skills.ts +7 -0
  206. package/packages/core/src/spec-standard.ts +47 -0
  207. package/packages/core/src/store/adapter.ts +80 -0
  208. package/packages/core/src/store/build-scope.ts +272 -0
  209. package/packages/core/src/store/contract.ts +2581 -0
  210. package/packages/core/src/store/local/blobs.ts +38 -0
  211. package/packages/core/src/store/local/schema.ts +180 -0
  212. package/packages/core/src/store/local/store.ts +1713 -0
  213. package/packages/core/src/store/memory.ts +1138 -0
  214. package/packages/core/src/store/phase-session.ts +425 -0
  215. package/packages/core/src/store/remote/client.ts +780 -0
  216. package/packages/core/src/store/remote/index.ts +12 -0
  217. package/packages/core/src/store/remote/protocol.ts +348 -0
  218. package/packages/core/src/store/remote/server.ts +923 -0
  219. package/packages/core/src/store/remote/token.ts +137 -0
  220. package/packages/core/src/store/remote/version.ts +10 -0
  221. package/packages/core/src/store/retention.ts +91 -0
  222. package/packages/core/src/store/session-handle.ts +265 -0
  223. package/packages/core/src/store/streams/assemble.ts +79 -0
  224. package/packages/core/src/store/streams/lock.ts +42 -0
  225. package/packages/core/src/store/streams/session-writer.ts +306 -0
  226. package/packages/core/src/store/streams/types.ts +171 -0
  227. package/packages/core/src/store/streams/wait.ts +127 -0
  228. package/packages/core/src/store/subscribe.ts +105 -0
  229. package/packages/core/src/store/types.ts +466 -0
  230. package/packages/core/src/testing/fixed.ts +44 -0
  231. package/packages/core/src/testing/store-failures.ts +32 -0
  232. package/packages/core/src/zod-issues.ts +91 -0
  233. package/skills/code-review/SKILL.md +100 -0
  234. package/skills/finalize/SKILL.md +44 -0
  235. package/skills/guide/SKILL.md +1781 -0
  236. package/skills/guide/references/operator-api.md +164 -0
  237. package/skills/guide/references/plugin-authoring.md +354 -0
  238. package/skills/guide/references/remote-store-protocol.md +972 -0
  239. package/skills/guide/references/remote-ticket-protocol.md +79 -0
  240. package/skills/guide/references/setup.md +289 -0
  241. package/skills/guide/references/spec-standard.md +63 -0
  242. package/skills/guide/references/ticket-dependencies.md +59 -0
  243. package/skills/harvest/SKILL.md +66 -0
  244. package/skills/harvest-review/SKILL.md +63 -0
  245. package/skills/implement/SKILL.md +82 -0
  246. package/skills/plan/SKILL.md +123 -0
  247. package/skills/plan-review/SKILL.md +121 -0
  248. package/skills/reconcile/SKILL.md +65 -0
  249. package/skills/spec/SKILL.md +132 -0
  250. package/skills/tickets/SKILL.md +139 -0
  251. package/templates/autobuild.toml +36 -0
@@ -0,0 +1,111 @@
1
+ import type {
2
+ BuildExecutionHandle,
3
+ BuildExecutionIdentity,
4
+ ExecutionObservation,
5
+ } from './build-execution'
6
+
7
+ /** Guest environment variable carrying the serialized harvest launch
8
+ * envelope, mirroring AB_BUILD_RUNNER_OPTIONS for build children. */
9
+ export const HARVEST_RUNNER_OPTIONS_ENV = 'AB_HARVEST_RUNNER_OPTIONS'
10
+
11
+ /** Where one hosted harvest run executed: the provider-native environment
12
+ * (and session) identity the provider supplies at launch time. */
13
+ export interface HarvestExecutionEnvironment {
14
+ provider: string
15
+ environmentId: string
16
+ sessionId?: string
17
+ }
18
+
19
+ /** Immutable identity passed when a workspace provider is asked to start one
20
+ * hosted harvest execution. Unlike builds there is no durable workspace fact,
21
+ * so the provider resolves the disposable environment itself and reports it
22
+ * on the returned handle's identity; `workspaceRef` is therefore optional and
23
+ * advisory. Run state, config, and outcomes are obtained only through the
24
+ * repository journal in the Store. */
25
+ export interface HarvestExecutionStart {
26
+ storeRef: string
27
+ /** Repository identity (normalized origin) the guest serves. */
28
+ repo: string
29
+ /** Supervisor instance id; doubles as the durable `execution` identity. */
30
+ instance: string
31
+ /** Remote base branch the disposable environment is provisioned from. */
32
+ baseBranch: string
33
+ workspaceRef?: string
34
+ /** Repository-lease holder the guest adopts from the owning dispatch loop:
35
+ * it skips its own claim, heartbeats this holder, and never releases. */
36
+ leaseHolder?: string
37
+ environment?: HarvestExecutionEnvironment
38
+ }
39
+
40
+ /** Serialized private-child envelope. A harvest guest is always owned by its
41
+ * environment — there is no local-parent mode (the guest outlives any host
42
+ * supervision and the environment owns teardown). */
43
+ export type HarvestRunnerLaunch = HarvestExecutionStart & {
44
+ supervision: { kind: 'environment' }
45
+ }
46
+
47
+ /** Validate one parsed launch envelope. Invalid/missing envelopes make the
48
+ * guest exit 2 rather than guessing at a Store or lease identity. */
49
+ export function parseHarvestRunnerLaunch(value: unknown): HarvestRunnerLaunch | undefined {
50
+ if (value === null || typeof value !== 'object') return undefined
51
+ const candidate = value as Partial<HarvestRunnerLaunch> & {
52
+ supervision?: Record<string, unknown>
53
+ }
54
+ if (
55
+ typeof candidate.storeRef !== 'string' ||
56
+ candidate.storeRef.length === 0 ||
57
+ typeof candidate.repo !== 'string' ||
58
+ candidate.repo.length === 0 ||
59
+ typeof candidate.instance !== 'string' ||
60
+ candidate.instance.length === 0 ||
61
+ typeof candidate.baseBranch !== 'string' ||
62
+ candidate.baseBranch.length === 0 ||
63
+ candidate.supervision === undefined
64
+ )
65
+ return undefined
66
+ if (candidate.supervision.kind !== 'environment') return undefined
67
+ if (Object.hasOwn(candidate.supervision, 'parentPid')) return undefined
68
+ if (
69
+ candidate.leaseHolder !== undefined &&
70
+ (typeof candidate.leaseHolder !== 'string' || candidate.leaseHolder.length === 0)
71
+ )
72
+ return undefined
73
+ if (
74
+ candidate.workspaceRef !== undefined &&
75
+ (typeof candidate.workspaceRef !== 'string' || candidate.workspaceRef.length === 0)
76
+ )
77
+ return undefined
78
+ const environment = candidate.environment
79
+ if (environment !== undefined) {
80
+ if (
81
+ typeof environment !== 'object' ||
82
+ typeof environment.provider !== 'string' ||
83
+ environment.provider.length === 0 ||
84
+ typeof environment.environmentId !== 'string' ||
85
+ environment.environmentId.length === 0
86
+ )
87
+ return undefined
88
+ if (
89
+ environment.sessionId !== undefined &&
90
+ (typeof environment.sessionId !== 'string' || environment.sessionId.length === 0)
91
+ )
92
+ return undefined
93
+ }
94
+ return candidate as HarvestRunnerLaunch
95
+ }
96
+
97
+ /** The substitutable seam at the workspace boundary for hosted harvest. A
98
+ * remote provider implements this by provisioning one disposable environment,
99
+ * launching the fixed harvest runner guest beside its checkout, and owning
100
+ * full teardown. Deliberately reuses the build execution handle/identity and
101
+ * observation types so supervision semantics (long-poll wait, teardown
102
+ * outcome, provider liveness) are identical to builds. */
103
+ export interface HarvestExecution {
104
+ start(input: HarvestExecutionStart): Promise<BuildExecutionHandle>
105
+ /** Optional liveness observation of a previously recorded execution, from
106
+ * provider state alone. Absent = this provider cannot observe; callers
107
+ * supervise exactly as before ("cannot observe" never means "act"). */
108
+ observe?(identity: BuildExecutionIdentity): Promise<ExecutionObservation>
109
+ }
110
+
111
+ export type { BuildExecutionHandle, ExecutionObservation }
@@ -0,0 +1,30 @@
1
+ import type { BuildExecution, BuildExecutionHandle, BuildExecutionStart } from './build-execution'
2
+
3
+ /** Deterministic test double. Production wiring never constructs this class;
4
+ * it keeps exhaustive scripted-agent scenarios in-process behind the same
5
+ * execution seam without weakening process isolation in the product path. */
6
+ export class InProcessBuildExecution implements BuildExecution {
7
+ constructor(private readonly run: (input: BuildExecutionStart) => Promise<void>) {}
8
+
9
+ async start(input: BuildExecutionStart): Promise<BuildExecutionHandle> {
10
+ const completion = Promise.resolve()
11
+ .then(() => this.run(input))
12
+ .then(
13
+ () => ({ exitCode: 0 }),
14
+ () => ({ exitCode: 1 }),
15
+ )
16
+ return {
17
+ identity: { provider: 'in-process', workspaceRef: input.workspaceRef },
18
+ supervision: 'local-parent',
19
+ completion,
20
+ async stop() {
21
+ // Scripted test turns are finite. Cancellation behavior belongs to the
22
+ // real local supervisor tests rather than this deterministic double.
23
+ return { outcome: 'confirmed' }
24
+ },
25
+ async detach() {
26
+ // Scripted test turns are finite and leave nothing to reap.
27
+ },
28
+ }
29
+ }
30
+ }
@@ -0,0 +1,88 @@
1
+ import { distributionPath } from '../../distribution'
2
+ import {
3
+ DEFAULT_PROCESS_GROUP_STOP_TIMEOUT_MS,
4
+ terminateProcessGroup,
5
+ } from '../../processes/process-group'
6
+ import type {
7
+ BuildExecution,
8
+ BuildExecutionExit,
9
+ BuildExecutionHandle,
10
+ BuildExecutionStart,
11
+ } from './build-execution'
12
+
13
+ export const BUILD_RUNNER_OPTIONS_ENV = 'AB_BUILD_RUNNER_OPTIONS'
14
+
15
+ export interface LocalBuildExecutionOptions {
16
+ entrypoint?: string
17
+ env?: Record<string, string | undefined>
18
+ stopTimeoutMs?: number
19
+ spawn?: typeof Bun.spawn
20
+ }
21
+
22
+ /** Shipped local workspace+process implementation. stdio is ignored on
23
+ * purpose: child output can never become a private progress/outcome channel. */
24
+ export class LocalBuildExecution implements BuildExecution {
25
+ private readonly entrypoint: string
26
+ private readonly env: Record<string, string | undefined>
27
+ private readonly stopTimeoutMs: number
28
+ private readonly spawn: typeof Bun.spawn
29
+
30
+ constructor(opts: LocalBuildExecutionOptions = {}) {
31
+ this.entrypoint = opts.entrypoint ?? distributionPath('bin', 'ab-build-runner.ts')
32
+ this.env = opts.env ?? process.env
33
+ this.stopTimeoutMs = opts.stopTimeoutMs ?? DEFAULT_PROCESS_GROUP_STOP_TIMEOUT_MS
34
+ this.spawn = opts.spawn ?? Bun.spawn
35
+ }
36
+
37
+ async start(input: BuildExecutionStart): Promise<BuildExecutionHandle> {
38
+ const child = this.spawn([process.execPath, this.entrypoint], {
39
+ env: {
40
+ ...this.env,
41
+ [BUILD_RUNNER_OPTIONS_ENV]: JSON.stringify({
42
+ ...input,
43
+ supervision: { kind: 'local-parent', parentPid: process.pid },
44
+ }),
45
+ },
46
+ stdin: 'ignore',
47
+ stdout: 'ignore',
48
+ stderr: 'ignore',
49
+ // On POSIX this makes the build child a new session and process-group
50
+ // leader. Every agent tool descendant inherits that group by default.
51
+ detached: true,
52
+ })
53
+ let reaping: Promise<void> | undefined
54
+ const reap = (): Promise<void> =>
55
+ (reaping ??= terminateProcessGroup(child.pid, this.stopTimeoutMs))
56
+ const leaderExit = child.exited.then(
57
+ (exitCode): BuildExecutionExit => ({
58
+ exitCode,
59
+ ...(child.signalCode !== null ? { signal: child.signalCode } : {}),
60
+ }),
61
+ )
62
+ const completion = leaderExit.then(async (exit) => {
63
+ // A naturally exited leader may leave inherited descendants behind.
64
+ await reap()
65
+ return exit
66
+ })
67
+
68
+ return {
69
+ pid: child.pid,
70
+ supervision: 'local-parent',
71
+ identity: {
72
+ provider: 'local-process',
73
+ workspaceRef: input.workspaceRef,
74
+ environmentId: String(child.pid),
75
+ },
76
+ completion,
77
+ stop: async () => {
78
+ await Promise.all([reap(), leaderExit])
79
+ return { outcome: 'confirmed' }
80
+ },
81
+ // A local execution cannot outlive its parent's teardown: detach is the
82
+ // same full reap as stop (the kernel reaps local children on teardown).
83
+ detach: async () => {
84
+ await Promise.all([reap(), leaderExit])
85
+ },
86
+ }
87
+ }
88
+ }
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Operator sandbox capability (AUT-340): one persistent, credential-free
3
+ * environment per operator × repository, provisioned like a build workspace
4
+ * and exposed to agent tools only through the registry. The provider owns
5
+ * environment mechanics; durable facts live in the repository journal; the
6
+ * caller supervises from the journal — the same split as the harvest
7
+ * execution capability.
8
+ *
9
+ * The credential-free rule (SPEC §14): the environment receives no store,
10
+ * forge, ticket-provider, or model credential, so the typed operator tools
11
+ * remain the only route to build state from inside a general-purpose agent
12
+ * shell. `SANDBOX_FORBIDDEN_ENV` is the one name set that rule is enforced
13
+ * from — config validation refuses forwarding any of them, and both adapters
14
+ * build guest command environments from an empty record (never the process
15
+ * environment) that can only ever contain forwarded names plus toolchain
16
+ * PATH. Lexical path validation in the service is not a sandbox boundary;
17
+ * the residual risk is bounded by this rule and by the guest network policy,
18
+ * which is the build workspace's upload-pack policy.
19
+ */
20
+
21
+ /** Environment variable names that may never be forwarded into an operator
22
+ * sandbox — store, forge, ticket-provider, model, and Vercel credentials.
23
+ * One set, two enforcers: `[orchestrator].sandbox.environmentVariables`
24
+ * config validation (refuses any of these names) and the guest command
25
+ * environment both adapters build (starts from an empty record and adds only
26
+ * forwarded names plus the toolchain PATH). */
27
+ export const SANDBOX_FORBIDDEN_ENV: readonly string[] = [
28
+ 'AB_STORE',
29
+ 'AB_TOKEN',
30
+ 'GITHUB_TOKEN',
31
+ 'GH_TOKEN',
32
+ 'LINEAR_API_KEY',
33
+ 'VERCEL_OIDC_TOKEN',
34
+ 'VERCEL_TOKEN',
35
+ 'VERCEL_TEAM_ID',
36
+ 'VERCEL_PROJECT_ID',
37
+ 'AI_GATEWAY_API_KEY',
38
+ ]
39
+
40
+ /** Durable identity of one operator sandbox environment: the provider-scoped
41
+ * name tools re-derive and journal facts carry. Deterministic per
42
+ * (repository origin, operator) — there is at most one live environment per
43
+ * operator and repository. */
44
+ export interface SandboxEnvironmentIdentity {
45
+ provider: string
46
+ /** Provider-scoped durable name (Vercel sandbox name / local worktree path). */
47
+ environmentId: string
48
+ /** Provider session, for journal facts. */
49
+ sessionId?: string
50
+ /** Checkout root inside the environment; tool file paths are rooted here. */
51
+ workspacePath: string
52
+ }
53
+
54
+ export interface SandboxCommandRequest {
55
+ command: string
56
+ cwd?: string
57
+ timeoutSeconds?: number
58
+ }
59
+
60
+ export interface SandboxCommandResult {
61
+ exitCode: number
62
+ stdout: string
63
+ stderr: string
64
+ }
65
+
66
+ /** State of one detached command. Vercel reports command output only after
67
+ * exit: while `running`, `stdout`/`stderr` stay undefined. */
68
+ export interface SandboxWaitResult {
69
+ state: 'running' | 'exited'
70
+ exitCode?: number
71
+ stdout?: string
72
+ stderr?: string
73
+ }
74
+
75
+ /** Typed failure of one operator-sandbox operation. `stage` names where it
76
+ * failed so a tool caller sees `sandbox-<stage>` codes; the message carries
77
+ * the provider's text, redacted of credential values by the service. */
78
+ export class SandboxOperationError extends Error {
79
+ override readonly name = 'SandboxOperationError'
80
+
81
+ constructor(
82
+ readonly stage:
83
+ | 'provision'
84
+ | 'resume'
85
+ | 'exec'
86
+ | 'exec-timeout'
87
+ | 'not-found'
88
+ | 'environment'
89
+ | 'reset'
90
+ | 'release',
91
+ message: string,
92
+ options?: { cause?: unknown },
93
+ ) {
94
+ super(message, options)
95
+ }
96
+ }
97
+
98
+ /** The substitutable seam at the workspace boundary for operator sandboxes.
99
+ * Every environment-deriving operation takes `{ repo, operator }` (plus
100
+ * `baseBranch` for `ensure`) so the one-environment-per-operator×repository
101
+ * invariant holds at the type level; per-environment operations take the
102
+ * resolved handle. Durable lifecycle facts are journaled by the caller
103
+ * (the sandbox service), never by the provider. */
104
+ export interface OperatorSandboxExecution {
105
+ /** Pure resolution of the deterministic identity — no provisioning, no
106
+ * provider traffic beyond origin discovery. Used by release paths that
107
+ * must name the environment without creating one. */
108
+ describe(input: { repo: string; operator: string }): Promise<SandboxEnvironmentIdentity>
109
+ /** Provision on first use, resume after a stop. `baseBranch` selects the
110
+ * fresh-provision source; reuse/resume never re-checks it. */
111
+ ensure(input: {
112
+ repo: string
113
+ operator: string
114
+ baseBranch: string
115
+ }): Promise<SandboxEnvironmentIdentity>
116
+ exec(
117
+ handle: SandboxEnvironmentIdentity,
118
+ request: SandboxCommandRequest,
119
+ ): Promise<SandboxCommandResult>
120
+ start(
121
+ handle: SandboxEnvironmentIdentity,
122
+ request: SandboxCommandRequest,
123
+ ): Promise<{ commandId: string }>
124
+ wait(
125
+ handle: SandboxEnvironmentIdentity,
126
+ input: { commandId: string; waitSeconds: number },
127
+ ): Promise<SandboxWaitResult>
128
+ readFile(handle: SandboxEnvironmentIdentity, path: string): Promise<Uint8Array>
129
+ writeFile(handle: SandboxEnvironmentIdentity, path: string, content: Uint8Array): Promise<void>
130
+ /** Idle stop: end the session, keep the snapshot. `unsupported` lets local
131
+ * providers decline honestly. Both fields come from journal facts, so the
132
+ * dispatcher can call this without provisioning anything. */
133
+ stop(input: { operator: string; environmentId: string }): Promise<{
134
+ outcome: 'stopped' | 'absent' | 'unsupported'
135
+ }>
136
+ /** Full teardown + snapshot purge, by exact environment identity. */
137
+ release(input: {
138
+ repo: string
139
+ operator: string
140
+ environmentId: string
141
+ }): Promise<{ snapshots: { outcome: 'confirmed' | 'unknown'; deleted?: number; error?: string } }>
142
+ }
@@ -0,0 +1,156 @@
1
+ import { mkdir, readFile, writeFile } from 'node:fs/promises'
2
+ import { dirname, isAbsolute, join } from 'node:path'
3
+ import type { Exec } from './git-worktree'
4
+
5
+ export const PHASE_SCRATCH_PREFIX = '.ab/'
6
+
7
+ async function git(exec: Exec, cwd: string, args: string[]): Promise<string> {
8
+ const result = await exec(['git', ...args], { cwd })
9
+ if (result.exitCode !== 0) {
10
+ throw new Error(
11
+ `git ${args.join(' ')} exited ${result.exitCode}: ${result.stderr.trim() || result.stdout.trim() || '(no output)'}`,
12
+ )
13
+ }
14
+ return result.stdout
15
+ }
16
+
17
+ /** Parse Git's -z path output without treating whitespace or quoting specially. */
18
+ export function parseNulPaths(output: string): string[] {
19
+ if (output === '') return []
20
+ if (!output.endsWith('\0')) throw new Error('git path output was not NUL-terminated')
21
+ const paths = output.slice(0, -1).split('\0')
22
+ if (paths.some((path) => path === '')) throw new Error('git path output contained an empty path')
23
+ return paths
24
+ }
25
+
26
+ export async function trackedScratchPaths(
27
+ exec: Exec,
28
+ workspacePath: string,
29
+ commit?: string,
30
+ ): Promise<string[]> {
31
+ const args = commit
32
+ ? ['ls-tree', '-r', '--name-only', '-z', commit, '--', '.ab']
33
+ : ['ls-files', '-z', '--', '.ab']
34
+ return [...new Set(parseNulPaths(await git(exec, workspacePath, args)))].sort()
35
+ }
36
+
37
+ /** Restore the exact index versions removed by a disposable scratch wipe. */
38
+ export async function restoreTrackedScratch(
39
+ exec: Exec,
40
+ workspacePath: string,
41
+ paths: readonly string[],
42
+ ): Promise<void> {
43
+ if (paths.length === 0) return
44
+ await git(exec, workspacePath, ['checkout-index', '--force', '--', ...paths])
45
+ }
46
+
47
+ /** Keep generated scratch ignored even when a legacy repo tracks .ab/.gitignore. */
48
+ export async function ensureScratchExcluded(exec: Exec, workspacePath: string): Promise<void> {
49
+ const raw = (await git(exec, workspacePath, ['rev-parse', '--git-path', 'info/exclude'])).trim()
50
+ if (raw === '') throw new Error('git rev-parse returned no exclude path')
51
+ const path = isAbsolute(raw) ? raw : join(workspacePath, raw)
52
+ let content = ''
53
+ try {
54
+ content = await readFile(path, 'utf8')
55
+ } catch (error) {
56
+ if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error
57
+ }
58
+ const lines = content.split(/\r?\n/)
59
+ if (lines.includes('.ab/')) return
60
+ await mkdir(dirname(path), { recursive: true })
61
+ const separator = content === '' || content.endsWith('\n') ? '' : '\n'
62
+ await writeFile(path, `${content}${separator}.ab/\n`)
63
+ }
64
+
65
+ function collides(path: string, unavailable: ReadonlySet<string>): boolean {
66
+ for (const other of unavailable) {
67
+ if (path === other || path.startsWith(`${other}/`) || other.startsWith(`${path}/`)) return true
68
+ }
69
+ return false
70
+ }
71
+
72
+ /** Allocate a stable path relative to .ab without overwriting tracked content. */
73
+ export function allocateScratchPath(
74
+ desired: string,
75
+ tracked: ReadonlySet<string>,
76
+ allocated: Set<string>,
77
+ ): string {
78
+ const unavailable = new Set([...tracked, ...allocated])
79
+ const conventional = `${PHASE_SCRATCH_PREFIX}${desired}`
80
+ if (!collides(conventional, unavailable)) {
81
+ allocated.add(conventional)
82
+ return desired
83
+ }
84
+ for (let index = 1; ; index += 1) {
85
+ const candidate = `generated/${index}/${desired}`
86
+ if (!collides(`${PHASE_SCRATCH_PREFIX}${candidate}`, unavailable)) {
87
+ allocated.add(`${PHASE_SCRATCH_PREFIX}${candidate}`)
88
+ return candidate
89
+ }
90
+ }
91
+ }
92
+
93
+ async function changedScratchPaths(
94
+ exec: Exec,
95
+ workspacePath: string,
96
+ from: string,
97
+ to: string,
98
+ ): Promise<Set<string>> {
99
+ return new Set(
100
+ parseNulPaths(
101
+ await git(exec, workspacePath, ['diff', '--name-only', '-z', from, to, '--', '.ab']),
102
+ ),
103
+ )
104
+ }
105
+
106
+ /** Every scratch path touched by any commit, including changes later reverted. */
107
+ export async function scratchPathsTouchedInRange(
108
+ exec: Exec,
109
+ workspacePath: string,
110
+ base: string,
111
+ head: string,
112
+ ): Promise<string[]> {
113
+ const commits = (await git(exec, workspacePath, ['rev-list', '--reverse', `${base}..${head}`]))
114
+ .trim()
115
+ .split('\n')
116
+ .filter(Boolean)
117
+ const touched = new Set<string>()
118
+ for (const commit of commits) {
119
+ const parents = (await git(exec, workspacePath, ['rev-list', '--parents', '-n', '1', commit]))
120
+ .trim()
121
+ .split(/\s+/)
122
+ .slice(1)
123
+ for (const parent of parents) {
124
+ for (const path of await changedScratchPaths(exec, workspacePath, parent, commit)) {
125
+ touched.add(path)
126
+ }
127
+ }
128
+ }
129
+ return [...touched].sort()
130
+ }
131
+
132
+ /** Merge-authored scratch differs from every parent; inherited bytes are safe. */
133
+ export async function reconcileScratchViolations(
134
+ exec: Exec,
135
+ workspacePath: string,
136
+ head: string,
137
+ parents: readonly string[],
138
+ ): Promise<string[]> {
139
+ let intersection: Set<string> | undefined
140
+ for (const parent of parents) {
141
+ const changed = await changedScratchPaths(exec, workspacePath, parent, head)
142
+ intersection =
143
+ intersection === undefined
144
+ ? changed
145
+ : new Set([...intersection].filter((path) => changed.has(path)))
146
+ }
147
+ return [...(intersection ?? [])].sort()
148
+ }
149
+
150
+ export function phaseScratchRejection(paths: readonly string[]): Error {
151
+ return new Error(
152
+ `publication rejected: commits must not add, modify, or delete paths under .ab/:\n` +
153
+ `${paths.map((path) => ` ${path}`).join('\n')}\n` +
154
+ 'Unstage or drop the .ab/ change, amend the commit if needed, then retry completion.',
155
+ )
156
+ }