@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,106 @@
1
+ import type { RuntimeRegistry, RuntimeUsabilityResult } from '../ports/runner/runtime'
2
+
3
+ export const SETUP_RUNTIME_PREFERENCE = ['claude', 'codex', 'pi'] as const
4
+
5
+ export interface RuntimeProbeReport {
6
+ runtime: string
7
+ usable: boolean
8
+ reason: string
9
+ }
10
+
11
+ export interface SetupAgentInvocation {
12
+ runtime: string
13
+ prompt: string
14
+ cwd: string
15
+ env: Readonly<Record<string, string | undefined>>
16
+ signal?: AbortSignal
17
+ }
18
+
19
+ export type SetupAgentLauncher = (invocation: SetupAgentInvocation) => Promise<number>
20
+
21
+ function message(error: unknown): string {
22
+ return error instanceof Error ? error.message : String(error)
23
+ }
24
+
25
+ function normalizeProbeResult(result: RuntimeUsabilityResult): { usable: boolean; reason: string } {
26
+ if (typeof result === 'boolean') {
27
+ return {
28
+ usable: result,
29
+ reason: result ? 'usable' : 'runtime reported unavailable',
30
+ }
31
+ }
32
+ return {
33
+ usable: result.usable,
34
+ reason: result.reason.trim() || (result.usable ? 'usable' : 'runtime reported unavailable'),
35
+ }
36
+ }
37
+
38
+ /** Probe every registration in deterministic diagnostic order. */
39
+ export async function probeInitRuntimes(
40
+ runtimes: RuntimeRegistry,
41
+ cwd: string,
42
+ env: Readonly<Record<string, string | undefined>>,
43
+ ): Promise<RuntimeProbeReport[]> {
44
+ const preferred = SETUP_RUNTIME_PREFERENCE.filter((name) => Object.hasOwn(runtimes, name))
45
+ const remaining = Object.keys(runtimes)
46
+ .filter((name) => !preferred.includes(name as (typeof SETUP_RUNTIME_PREFERENCE)[number]))
47
+ .sort()
48
+ const names = [...preferred, ...remaining]
49
+ return Promise.all(
50
+ names.map(async (runtime): Promise<RuntimeProbeReport> => {
51
+ const registration = runtimes[runtime]!
52
+ if (registration.initUsable === undefined) {
53
+ return { runtime, usable: false, reason: 'no init usability probe is registered' }
54
+ }
55
+ try {
56
+ const result = await registration.initUsable({
57
+ cwd,
58
+ env,
59
+ models: registration.defaultModel === undefined ? [] : [registration.defaultModel],
60
+ })
61
+ return { runtime, ...normalizeProbeResult(result) }
62
+ } catch (error) {
63
+ return { runtime, usable: false, reason: `probe failed: ${message(error)}` }
64
+ }
65
+ }),
66
+ )
67
+ }
68
+
69
+ /** Setup launch order is product-owned and never depends on repository contents. */
70
+ export function selectSetupRuntime(reports: readonly RuntimeProbeReport[]): string | undefined {
71
+ for (const preferred of SETUP_RUNTIME_PREFERENCE) {
72
+ if (reports.some((report) => report.runtime === preferred && report.usable)) return preferred
73
+ }
74
+ return undefined
75
+ }
76
+
77
+ /** Remove ambient Autobuild identities before starting a non-phase setup agent. */
78
+ export function sanitizeSetupEnvironment(
79
+ env: Readonly<Record<string, string | undefined>>,
80
+ ): Record<string, string> {
81
+ return Object.fromEntries(
82
+ Object.entries(env).filter(
83
+ (entry): entry is [string, string] => entry[1] !== undefined && !entry[0].startsWith('AB_'),
84
+ ),
85
+ )
86
+ }
87
+
88
+ export function setupAgentCommand(runtime: string, prompt: string): string[] {
89
+ if (!SETUP_RUNTIME_PREFERENCE.some((supported) => supported === runtime)) {
90
+ throw new Error(`runtime "${runtime}" has no interactive setup launcher`)
91
+ }
92
+ return [runtime, prompt]
93
+ }
94
+
95
+ /** Launch a shipped coding-agent CLI with inherited terminal streams. */
96
+ export const launchSetupAgent: SetupAgentLauncher = async (invocation) => {
97
+ const proc = Bun.spawn(setupAgentCommand(invocation.runtime, invocation.prompt), {
98
+ cwd: invocation.cwd,
99
+ env: sanitizeSetupEnvironment(invocation.env),
100
+ stdin: 'inherit',
101
+ stdout: 'inherit',
102
+ stderr: 'inherit',
103
+ ...(invocation.signal === undefined ? {} : { signal: invocation.signal }),
104
+ })
105
+ return proc.exited
106
+ }
@@ -0,0 +1,583 @@
1
+ import { mkdtemp, readFile, rm } from 'node:fs/promises'
2
+ import { tmpdir } from 'node:os'
3
+ import { join, resolve } from 'node:path'
4
+ import { effectiveRuntimeReferences } from '../config/roles'
5
+ import type { Config } from '../config/schema'
6
+ import { vercelSandboxConfigSchema } from '../config/schema'
7
+ import { loadConfig } from '../config/load'
8
+ import { createProductionRuntimes } from '../ports/runner/production'
9
+ import { createRuntimeResolver } from '../ports/runner/routing'
10
+ import type { RuntimeRegistry } from '../ports/runner/runtime'
11
+ import type { Exec } from '../ports/workspace/git-worktree'
12
+ import { spawnExec } from '../ports/workspace/git-worktree'
13
+ import { type VercelSandboxFacade, validateVercelSandbox } from '../ports/workspace/vercel-sandbox'
14
+ import { loadPlugins } from '../plugins/load'
15
+ import { materializePluginRuntimes } from '../plugins/runtimes'
16
+ import { createTicketSource } from '../ports/tickets/create'
17
+ import { inspectLocalStoreSnapshot } from '../store/local/store'
18
+ import type { StoreOpener } from './store-opening'
19
+ import { openProductionStore } from './store-opening'
20
+ import { isRemoteStoreRef, resolveMainRepo, resolveRepoStatePaths } from './repo-state'
21
+
22
+ export const INIT_PROBE_MARKER = 'AB_INIT_READINESS_V1='
23
+
24
+ export interface ReadinessCheck {
25
+ name: string
26
+ status: 'pass' | 'fail' | 'absent'
27
+ detail: string
28
+ }
29
+
30
+ export interface InitValidationReport {
31
+ provider: string
32
+ context: 'local worktree' | 'Vercel Sandbox'
33
+ workspace?: string
34
+ revision?: string
35
+ checks: ReadinessCheck[]
36
+ exitCode: number
37
+ /** Vercel only: automatic snapshots deleted while releasing the disposable
38
+ * environment; zero proves no snapshot storage was left behind. */
39
+ snapshotsDeleted?: number
40
+ }
41
+
42
+ export interface GuestProbeReport {
43
+ checks: ReadinessCheck[]
44
+ }
45
+
46
+ function message(error: unknown): string {
47
+ if (error instanceof AggregateError) {
48
+ return [error.message, ...error.errors.map(message)].filter(Boolean).join('; ')
49
+ }
50
+ if (error instanceof Error)
51
+ return `${error.message}${error.cause === undefined ? '' : `; ${message(error.cause)}`}`
52
+ return String(error)
53
+ }
54
+
55
+ /** Replace every supplied nonempty value, longest first, in all diagnostics. */
56
+ export function createReadinessRedactor(
57
+ env: Readonly<Record<string, string | undefined>>,
58
+ explicitSecretNames: readonly string[] = [],
59
+ ): (value: unknown) => string {
60
+ const namedSecrets = new Set(explicitSecretNames)
61
+ const secrets = [
62
+ ...new Set(
63
+ Object.entries(env)
64
+ .filter(
65
+ ([name, value]) =>
66
+ value !== undefined &&
67
+ value !== '' &&
68
+ (namedSecrets.has(name) || /(?:TOKEN|KEY|SECRET|PASSWORD|CREDENTIAL|AUTH)/i.test(name)),
69
+ )
70
+ .map(([, value]) => value as string),
71
+ ),
72
+ ].sort((left, right) => right.length - left.length)
73
+ return (value) => {
74
+ let text = message(value)
75
+ for (const secret of secrets) text = text.split(secret).join('[REDACTED]')
76
+ return text
77
+ }
78
+ }
79
+
80
+ async function shell(
81
+ exec: Exec,
82
+ cwd: string,
83
+ command: string,
84
+ signal?: AbortSignal,
85
+ ): Promise<void> {
86
+ const result = await exec(['sh', '-c', command], {
87
+ cwd,
88
+ ...(signal === undefined ? {} : { signal }),
89
+ })
90
+ if (result.exitCode !== 0) {
91
+ throw new Error(
92
+ `setup command exited ${result.exitCode}: ${result.stderr.trim() || result.stdout.trim() || '(no output)'}`,
93
+ )
94
+ }
95
+ }
96
+
97
+ function effectiveTargets(
98
+ config: Config,
99
+ runtimes: RuntimeRegistry,
100
+ ): Array<{ runtime: string; models: string[] }> {
101
+ // Preserve eager registry/model/argument validation, but probe only routes
102
+ // the pipeline can consume rather than every declared role table.
103
+ createRuntimeResolver(runtimes, config.roles, config.policy.sessionBudgetSeconds)
104
+ return effectiveRuntimeReferences(config).map((group) => {
105
+ const defaultModel = runtimes[group.runtime]?.defaultModel
106
+ return {
107
+ runtime: group.runtime,
108
+ models: [
109
+ ...new Set([
110
+ ...group.models,
111
+ ...(group.usesRuntimeDefaultModel && defaultModel !== undefined ? [defaultModel] : []),
112
+ ]),
113
+ ].sort(),
114
+ }
115
+ })
116
+ }
117
+
118
+ /** Probe code shared by the detached local worktree and the private sandbox entry. */
119
+ export async function runGuestReadinessProbe(opts: {
120
+ repo: string
121
+ env: Record<string, string | undefined>
122
+ runtimes?: RuntimeRegistry
123
+ openStore?: StoreOpener
124
+ exec?: Exec
125
+ signal?: AbortSignal
126
+ }): Promise<GuestProbeReport> {
127
+ const checks: ReadinessCheck[] = []
128
+ const config = await loadConfig(join(opts.repo, 'autobuild.toml'))
129
+ const vercelConfig =
130
+ config.workspace.provider === 'vercel-sandbox'
131
+ ? vercelSandboxConfigSchema.parse(config.workspace.config)
132
+ : undefined
133
+ const redact = createReadinessRedactor(opts.env, vercelConfig?.environmentVariables)
134
+ try {
135
+ const setup = config.commands.setup
136
+ if (opts.signal?.aborted) throw opts.signal.reason ?? new Error('validation cancelled')
137
+ if (setup !== undefined) await shell(opts.exec ?? spawnExec, opts.repo, setup, opts.signal)
138
+ checks.push({
139
+ name: 'repository setup',
140
+ status: 'pass',
141
+ detail: setup === undefined ? 'no commands.setup configured' : 'commands.setup completed',
142
+ })
143
+ } catch (error) {
144
+ if (opts.signal?.aborted) throw opts.signal.reason ?? error
145
+ return {
146
+ checks: [
147
+ {
148
+ name: 'repository setup',
149
+ status: 'fail',
150
+ detail: `${redact(error)}; make commands.setup idempotent and runnable in a fresh environment`,
151
+ },
152
+ ],
153
+ }
154
+ }
155
+
156
+ let runtimes: RuntimeRegistry
157
+ try {
158
+ const packageRoot = await resolveMainRepo(opts.repo, opts.exec ?? spawnExec)
159
+ const plugins = await loadPlugins(config.plugins, opts.repo, { packageRoot })
160
+ runtimes = await materializePluginRuntimes(
161
+ opts.runtimes ?? createProductionRuntimes().runtimes,
162
+ plugins,
163
+ { repoRoot: opts.repo, env: opts.env },
164
+ )
165
+ checks.push({
166
+ name: 'configuration and plugins',
167
+ status: 'pass',
168
+ detail: `${config.plugins.length} configured plugin(s) loaded`,
169
+ })
170
+ } catch (error) {
171
+ return {
172
+ checks: [
173
+ ...checks,
174
+ {
175
+ name: 'configuration and plugins',
176
+ status: 'fail',
177
+ detail: `${redact(error)}; install or correct the configured plugin in this environment`,
178
+ },
179
+ ],
180
+ }
181
+ }
182
+
183
+ const runtimeRemediation = (runtime: string, preflightOnly: boolean): string =>
184
+ vercelConfig === undefined
185
+ ? 'install/authenticate this runtime in the local validation environment'
186
+ : `fix workspace.config.runtimeProvisioning.${runtime}${preflightOnly ? '.preflight' : ''} and expose API credential names in workspace.config.environmentVariables`
187
+
188
+ try {
189
+ for (const target of effectiveTargets(config, runtimes)) {
190
+ if (opts.signal?.aborted) throw opts.signal.reason ?? new Error('validation cancelled')
191
+ const probe = runtimes[target.runtime]?.initUsable
192
+ if (probe === undefined) {
193
+ checks.push({
194
+ name: `runtime ${target.runtime}`,
195
+ status: 'fail',
196
+ detail:
197
+ 'selected runtime has no onboarding usability probe; add initUsable or select a supported runtime',
198
+ })
199
+ continue
200
+ }
201
+ try {
202
+ const outcome = await probe({ cwd: opts.repo, env: opts.env, models: target.models })
203
+ const usable = typeof outcome === 'boolean' ? outcome : outcome.usable
204
+ const reason =
205
+ typeof outcome === 'boolean' ? (outcome ? 'usable' : 'unusable') : outcome.reason
206
+ checks.push({
207
+ name: `runtime ${target.runtime}${target.models.length === 0 ? '' : ` (${target.models.join(', ')})`}`,
208
+ status: usable ? 'pass' : 'fail',
209
+ detail: usable
210
+ ? redact(reason)
211
+ : `${redact(reason)}; ${runtimeRemediation(target.runtime, true)}`,
212
+ })
213
+ } catch (error) {
214
+ if (opts.signal?.aborted) throw opts.signal.reason ?? error
215
+ checks.push({
216
+ name: `runtime ${target.runtime}`,
217
+ status: 'fail',
218
+ detail: `${redact(error)}; ${runtimeRemediation(target.runtime, false)}`,
219
+ })
220
+ }
221
+ }
222
+ } catch (error) {
223
+ if (opts.signal?.aborted) throw opts.signal.reason ?? error
224
+ checks.push({ name: 'runtime routing', status: 'fail', detail: redact(error) })
225
+ }
226
+
227
+ if (opts.signal?.aborted) throw opts.signal.reason ?? new Error('validation cancelled')
228
+ const storeRef = opts.env.AB_STORE
229
+ if (storeRef === undefined || storeRef === '') {
230
+ checks.push({
231
+ name: 'BuildStore',
232
+ status: 'fail',
233
+ detail: 'AB_STORE is missing; configure the Store used by builds',
234
+ })
235
+ } else if (opts.openStore === undefined && !isRemoteStoreRef(storeRef)) {
236
+ const localRoot = resolve(opts.repo, storeRef)
237
+ try {
238
+ const inspection = await inspectLocalStoreSnapshot(localRoot)
239
+ if (inspection.status === 'absent') {
240
+ checks.push({
241
+ name: 'BuildStore',
242
+ status: 'absent',
243
+ detail: `${inspection.databasePath} does not exist; no repository history was available to inspect and no Store was created`,
244
+ })
245
+ } else {
246
+ checks.push({
247
+ name: 'BuildStore',
248
+ status: 'pass',
249
+ detail: `${inspection.buildCount} readable build record(s) in a disposable snapshot of ${inspection.databasePath}`,
250
+ })
251
+ }
252
+ } catch (error) {
253
+ checks.push({
254
+ name: 'BuildStore',
255
+ status: 'fail',
256
+ detail: `${redact(error)}; verify the local Store database permissions and integrity`,
257
+ })
258
+ }
259
+ } else {
260
+ let store: ReturnType<StoreOpener> | undefined
261
+ try {
262
+ store = (opts.openStore ?? openProductionStore)(storeRef, opts.env.AB_TOKEN)
263
+ await store.listBuilds()
264
+ checks.push({ name: 'BuildStore', status: 'pass', detail: 'read-only request succeeded' })
265
+ } catch (error) {
266
+ checks.push({
267
+ name: 'BuildStore',
268
+ status: 'fail',
269
+ detail: `${redact(error)}; verify the Store URL, AB_TOKEN scope, and network access`,
270
+ })
271
+ } finally {
272
+ await store?.close().catch((error) => {
273
+ checks.push({ name: 'BuildStore close', status: 'fail', detail: redact(error) })
274
+ })
275
+ }
276
+ }
277
+ return { checks }
278
+ }
279
+
280
+ function parseGuestOutput(output: string): GuestProbeReport {
281
+ const line = output.split(/\r?\n/).find((candidate) => candidate.startsWith(INIT_PROBE_MARKER))
282
+ if (line === undefined)
283
+ throw new Error('remote readiness probe returned malformed output (result marker absent)')
284
+ const value = JSON.parse(line.slice(INIT_PROBE_MARKER.length)) as GuestProbeReport
285
+ if (!Array.isArray(value.checks))
286
+ throw new Error('remote readiness probe returned malformed checks')
287
+ return value
288
+ }
289
+
290
+ async function gitText(
291
+ exec: Exec,
292
+ repo: string,
293
+ args: string[],
294
+ signal?: AbortSignal,
295
+ ): Promise<string> {
296
+ const result = await exec(['git', ...args], {
297
+ cwd: repo,
298
+ ...(signal === undefined ? {} : { signal }),
299
+ })
300
+ if (result.exitCode !== 0)
301
+ throw new Error(
302
+ `git ${args.join(' ')} exited ${result.exitCode}: ${result.stderr.trim() || result.stdout.trim()}`,
303
+ )
304
+ return result.stdout.trim()
305
+ }
306
+
307
+ function hostPreflight(config: Config, env: Record<string, string | undefined>): void {
308
+ if (config.workspace.provider !== 'vercel-sandbox') return
309
+ if (config.forge !== 'github')
310
+ throw new Error(
311
+ 'vercel-sandbox supports forge = "github" only; configure GitHub publication before validating',
312
+ )
313
+ if (!env.GITHUB_TOKEN && !env.GH_TOKEN)
314
+ throw new Error('vercel-sandbox publication requires push-capable GITHUB_TOKEN or GH_TOKEN')
315
+ if (
316
+ !env.VERCEL_OIDC_TOKEN &&
317
+ (!env.VERCEL_TOKEN || !env.VERCEL_TEAM_ID || !env.VERCEL_PROJECT_ID)
318
+ ) {
319
+ throw new Error(
320
+ 'Vercel authentication requires VERCEL_OIDC_TOKEN or the durable VERCEL_TOKEN, VERCEL_TEAM_ID, and VERCEL_PROJECT_ID set',
321
+ )
322
+ }
323
+ if (config.tickets.source === 'linear' && !env.LINEAR_API_KEY)
324
+ throw new Error('ticket source "linear" requires LINEAR_API_KEY')
325
+ }
326
+
327
+ export async function validateInitReadiness(opts: {
328
+ targetRepo: string
329
+ env: Record<string, string | undefined>
330
+ stdout?: (line: string) => void
331
+ exec?: Exec
332
+ openStore?: StoreOpener
333
+ runtimes?: RuntimeRegistry
334
+ vercelFacade?: VercelSandboxFacade
335
+ packageArchive?: () => Promise<Uint8Array>
336
+ signal?: AbortSignal
337
+ }): Promise<InitValidationReport> {
338
+ const exec = opts.exec ?? spawnExec
339
+ if (opts.signal?.aborted) throw opts.signal.reason ?? new Error('validation cancelled')
340
+ const repo = await resolveMainRepo(opts.targetRepo, exec)
341
+ const configPath = join(repo, 'autobuild.toml')
342
+ const configBytes = await readFile(configPath, 'utf8')
343
+ const config = await loadConfig(configPath)
344
+ const state = resolveRepoStatePaths({ repo, envStore: opts.env.AB_STORE })
345
+ const vercelConfig =
346
+ config.workspace.provider === 'vercel-sandbox'
347
+ ? vercelSandboxConfigSchema.parse(config.workspace.config)
348
+ : undefined
349
+ const redact = createReadinessRedactor(opts.env, [
350
+ ...(vercelConfig?.environmentVariables ?? []),
351
+ ...(vercelConfig?.gitUsernameEnv === undefined ? [] : [vercelConfig.gitUsernameEnv]),
352
+ ...(vercelConfig?.gitPasswordEnv === undefined ? [] : [vercelConfig.gitPasswordEnv]),
353
+ ])
354
+ try {
355
+ hostPreflight(config, opts.env)
356
+ // Ticket acquisition is a host responsibility for both workspace providers.
357
+ // Exercise its read surface before allocating disposable infrastructure.
358
+ const hostPlugins = await loadPlugins(config.plugins, repo, { packageRoot: repo })
359
+ if (config.tickets.source !== 'file') {
360
+ const ticketSource = await createTicketSource(
361
+ config.tickets,
362
+ { ...opts.env, AB_STORE: state.storeRef },
363
+ repo,
364
+ state.localStateRoot,
365
+ hostPlugins,
366
+ )
367
+ await ticketSource.listReady({
368
+ ...(config.tickets.readyLabels !== undefined ? { labels: config.tickets.readyLabels } : {}),
369
+ state: config.tickets.readyState,
370
+ })
371
+ }
372
+ } catch (error) {
373
+ throw new Error(redact(error))
374
+ }
375
+ let report: InitValidationReport | undefined
376
+
377
+ if (config.workspace.provider === 'git-worktree') {
378
+ const root = await mkdtemp(join(tmpdir(), 'ab-init-validation-'))
379
+ const workspace = join(root, 'worktree')
380
+ let failure: unknown
381
+ let cleanupFailure: unknown
382
+ try {
383
+ const revision = await gitText(
384
+ exec,
385
+ repo,
386
+ ['rev-parse', '--verify', `${config.baseBranch}^{commit}`],
387
+ opts.signal,
388
+ )
389
+ const add = await exec(['git', 'worktree', 'add', '--detach', workspace, revision], {
390
+ cwd: repo,
391
+ ...(opts.signal === undefined ? {} : { signal: opts.signal }),
392
+ })
393
+ if (add.exitCode !== 0)
394
+ throw new Error(
395
+ `could not create validation worktree: ${add.stderr.trim() || add.stdout.trim()}`,
396
+ )
397
+ const candidateConfig = await readFile(join(workspace, 'autobuild.toml'), 'utf8')
398
+ if (candidateConfig !== configBytes)
399
+ throw new Error(
400
+ `autobuild.toml differs from committed ${config.baseBranch}; commit setup changes before validating`,
401
+ )
402
+ const guest = await runGuestReadinessProbe({
403
+ repo: workspace,
404
+ env: { ...opts.env, AB_STORE: state.storeRef },
405
+ ...(opts.openStore !== undefined ? { openStore: opts.openStore } : {}),
406
+ ...(opts.runtimes !== undefined ? { runtimes: opts.runtimes } : {}),
407
+ exec,
408
+ ...(opts.signal === undefined ? {} : { signal: opts.signal }),
409
+ })
410
+ report = {
411
+ provider: 'git-worktree',
412
+ context: 'local worktree',
413
+ workspace,
414
+ revision,
415
+ checks: [
416
+ {
417
+ name: 'repository acquisition',
418
+ status: 'pass',
419
+ detail: `detached committed ${config.baseBranch} at ${revision}`,
420
+ },
421
+ ...guest.checks,
422
+ ],
423
+ exitCode: guest.checks.some((check) => check.status === 'fail') ? 1 : 0,
424
+ }
425
+ } catch (error) {
426
+ failure = error
427
+ } finally {
428
+ const remove = await exec(['git', 'worktree', 'remove', '--force', workspace], { cwd: repo })
429
+ const prune = await exec(['git', 'worktree', 'prune'], { cwd: repo })
430
+ await rm(root, { recursive: true, force: true })
431
+ if (remove.exitCode !== 0 && !/not a working tree/i.test(remove.stderr)) {
432
+ cleanupFailure = new Error(
433
+ `validation worktree ${workspace} could not be removed: ${remove.stderr.trim()}`,
434
+ )
435
+ } else if (prune.exitCode !== 0) {
436
+ cleanupFailure = new Error(
437
+ `validation worktree registry could not be pruned: ${prune.stderr.trim()}`,
438
+ )
439
+ }
440
+ }
441
+ if (failure !== undefined && cleanupFailure !== undefined) {
442
+ throw new AggregateError(
443
+ [failure, cleanupFailure],
444
+ `validation failed and disposable worktree ${workspace} cleanup also failed`,
445
+ )
446
+ }
447
+ if (failure !== undefined) throw failure
448
+ if (cleanupFailure !== undefined) throw cleanupFailure
449
+ } else if (config.workspace.provider === 'vercel-sandbox') {
450
+ const vercel = vercelConfig!
451
+ const storeRef = state.storeRef
452
+ if (!/^https:\/\//i.test(storeRef))
453
+ throw new Error('vercel-sandbox requires AB_STORE to be an HTTPS URL reachable from Vercel')
454
+ const token = opts.env.AB_TOKEN
455
+ if (!token) throw new Error('vercel-sandbox requires nonempty AB_TOKEN for the hosted Store')
456
+ const remoteLine = await gitText(
457
+ exec,
458
+ repo,
459
+ ['ls-remote', '--heads', 'origin', `refs/heads/${config.baseBranch}`],
460
+ opts.signal,
461
+ )
462
+ const remoteRevision = remoteLine.split(/\s+/)[0]
463
+ if (!/^[0-9a-f]{40,64}$/i.test(remoteRevision ?? '')) {
464
+ throw new Error(
465
+ `remote base ${config.baseBranch} does not exist; commit and push setup changes before validating`,
466
+ )
467
+ }
468
+ await gitText(
469
+ exec,
470
+ repo,
471
+ [
472
+ 'fetch',
473
+ '--no-tags',
474
+ '--no-write-fetch-head',
475
+ '--refmap=',
476
+ 'origin',
477
+ `refs/heads/${config.baseBranch}`,
478
+ ],
479
+ opts.signal,
480
+ )
481
+ const shownConfig = await exec(['git', 'show', `${remoteRevision}:autobuild.toml`], {
482
+ cwd: repo,
483
+ ...(opts.signal === undefined ? {} : { signal: opts.signal }),
484
+ })
485
+ if (shownConfig.exitCode !== 0) {
486
+ throw new Error(
487
+ `remote ${config.baseBranch} does not contain autobuild.toml; commit and push setup changes before validating`,
488
+ )
489
+ }
490
+ if (shownConfig.stdout !== configBytes) {
491
+ throw new Error(
492
+ `remote ${config.baseBranch} autobuild.toml differs from this checkout; commit and push setup changes before validating`,
493
+ )
494
+ }
495
+ let remote: Awaited<ReturnType<typeof validateVercelSandbox>>
496
+ try {
497
+ remote = await validateVercelSandbox({
498
+ config: vercel,
499
+ env: opts.env,
500
+ storeRef,
501
+ storeToken: token,
502
+ repo,
503
+ baseBranch: config.baseBranch,
504
+ ...(opts.vercelFacade !== undefined ? { facade: opts.vercelFacade } : {}),
505
+ exec,
506
+ ...(opts.packageArchive !== undefined ? { packageArchive: opts.packageArchive } : {}),
507
+ runtimeReferences: effectiveRuntimeReferences(config),
508
+ ...(opts.signal !== undefined ? { signal: opts.signal } : {}),
509
+ onSandbox: (name) => opts.stdout?.(`Disposable Vercel Sandbox: ${name} (active)`),
510
+ })
511
+ } catch (error) {
512
+ throw new Error(redact(error))
513
+ }
514
+ let guest: GuestProbeReport
515
+ try {
516
+ guest = parseGuestOutput(remote.output)
517
+ } catch (error) {
518
+ throw new Error(
519
+ `disposable sandbox ${remote.sandbox} was deleted, but its readiness output was invalid: ${redact(error)}`,
520
+ )
521
+ }
522
+ report = {
523
+ provider: 'vercel-sandbox',
524
+ context: 'Vercel Sandbox',
525
+ workspace: remote.sandbox,
526
+ revision: remote.revision,
527
+ snapshotsDeleted: remote.snapshotsDeleted,
528
+ checks: [
529
+ {
530
+ name: 'repository acquisition',
531
+ status: 'pass',
532
+ detail: `${remote.origin} ${remote.revision}`,
533
+ },
534
+ {
535
+ name: 'system provisioning',
536
+ status: 'pass',
537
+ detail:
538
+ remote.provisioning.length === 0
539
+ ? 'no workspace.config.provisioning steps declared'
540
+ : `completed: ${remote.provisioning.join(', ')}`,
541
+ },
542
+ ...guest.checks,
543
+ ],
544
+ exitCode: guest.checks.some((check) => check.status === 'fail') ? 1 : 0,
545
+ }
546
+ } else {
547
+ throw new Error(
548
+ `workspace provider "${config.workspace.provider}" does not support init readiness validation`,
549
+ )
550
+ }
551
+
552
+ if (report === undefined) throw new Error('readiness validation produced no report')
553
+ const stdout = opts.stdout ?? (() => {})
554
+ stdout(`Readiness: ${report.context} (${report.provider})`)
555
+ stdout(`Store: ${state.storeRef}`)
556
+ stdout(`Forge: ${config.forge}`)
557
+ if (vercelConfig !== undefined) {
558
+ stdout(
559
+ `Vercel auth: ${opts.env.VERCEL_OIDC_TOKEN ? 'OIDC' : 'access token'}; team=${opts.env.VERCEL_TEAM_ID ?? '(linked)'}; project=${opts.env.VERCEL_PROJECT_ID ?? '(linked)'}`,
560
+ )
561
+ stdout(
562
+ `Private clone variables: ${vercelConfig.gitUsernameEnv === undefined ? '(public repository)' : `${vercelConfig.gitUsernameEnv}, ${vercelConfig.gitPasswordEnv}`}`,
563
+ )
564
+ stdout(
565
+ `Guest environment variable names: ${vercelConfig.environmentVariables.join(', ') || '(none)'}`,
566
+ )
567
+ stdout(
568
+ `Runtime provisioning names: ${Object.keys(vercelConfig.runtimeProvisioning).sort().join(', ') || '(none)'}`,
569
+ )
570
+ }
571
+ if (report.workspace !== undefined)
572
+ stdout(
573
+ `Disposable environment: ${report.workspace} (released${report.snapshotsDeleted === undefined ? '' : `; ${report.snapshotsDeleted} snapshot(s) deleted`})`,
574
+ )
575
+ for (const check of report.checks) {
576
+ const label = check.status === 'pass' ? 'PASS' : check.status === 'absent' ? 'ABSENT' : 'FAIL'
577
+ stdout(` ${label} ${check.name}: ${redact(check.detail)}`)
578
+ }
579
+ return {
580
+ ...report,
581
+ checks: report.checks.map((check) => ({ ...check, detail: redact(check.detail) })),
582
+ }
583
+ }