@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,78 @@
1
+ import type { ProcessGroupReaperOptions } from './process-group-reaper'
2
+ import { launchProcessGroupReaper, processGroupReaperOptions } from './process-group-reaper'
3
+
4
+ interface ParentWatch {
5
+ close(): void
6
+ }
7
+
8
+ export interface BuildChildExitCoordinatorOptions {
9
+ groupId: number
10
+ stopTimeoutMs?: number
11
+ launchReaper?: (options: ProcessGroupReaperOptions) => number
12
+ exit?: (code: number) => void
13
+ retryDelayMs?: number
14
+ }
15
+
16
+ /**
17
+ * Transfers teardown ownership before a build-child leader exits. Every
18
+ * terminal path shares this coordinator so signal/watchdog races preserve the
19
+ * first exit intent and create exactly one detached group reaper.
20
+ */
21
+ export class BuildChildExitCoordinator {
22
+ private readonly reaperOptions: ProcessGroupReaperOptions
23
+ private readonly launchReaper: (options: ProcessGroupReaperOptions) => number
24
+ private readonly exit: (code: number) => void
25
+ private readonly retryDelayMs: number
26
+ private parentWatch: ParentWatch | undefined
27
+ private parentWatchClosed = false
28
+ private intendedExitCode: number | undefined
29
+ private retry: ReturnType<typeof setTimeout> | undefined
30
+ private handedOff = false
31
+
32
+ constructor(options: BuildChildExitCoordinatorOptions) {
33
+ this.reaperOptions =
34
+ options.stopTimeoutMs === undefined
35
+ ? processGroupReaperOptions(options.groupId)
36
+ : { groupId: options.groupId, stopTimeoutMs: options.stopTimeoutMs }
37
+ this.launchReaper = options.launchReaper ?? launchProcessGroupReaper
38
+ this.exit = options.exit ?? process.exit
39
+ this.retryDelayMs = options.retryDelayMs ?? 10
40
+ }
41
+
42
+ setParentWatch(parentWatch: ParentWatch): void {
43
+ this.parentWatch = parentWatch
44
+ if (this.intendedExitCode !== undefined) this.closeParentWatch()
45
+ }
46
+
47
+ terminate(exitCode: number): void {
48
+ if (this.intendedExitCode === undefined) this.intendedExitCode = exitCode
49
+ if (this.handedOff) return
50
+
51
+ this.closeParentWatch()
52
+ this.tryHandoff()
53
+ }
54
+
55
+ private closeParentWatch(): void {
56
+ if (this.parentWatch === undefined || this.parentWatchClosed) return
57
+ this.parentWatchClosed = true
58
+ this.parentWatch.close()
59
+ }
60
+
61
+ private tryHandoff(): void {
62
+ if (this.handedOff || this.retry !== undefined) return
63
+ try {
64
+ this.launchReaper(this.reaperOptions)
65
+ } catch {
66
+ // Do not let the leader disappear while detached ownership is absent.
67
+ // A referenced retry keeps natural completion alive until spawn succeeds.
68
+ this.retry = setTimeout(() => {
69
+ this.retry = undefined
70
+ this.tryHandoff()
71
+ }, this.retryDelayMs)
72
+ return
73
+ }
74
+
75
+ this.handedOff = true
76
+ this.exit(this.intendedExitCode!)
77
+ }
78
+ }
@@ -0,0 +1,119 @@
1
+ import { hostname } from 'node:os'
2
+ import { createForge } from '../ports/forge/create'
3
+ import { createProductionRuntimes } from '../ports/runner/production'
4
+ import { spawnExec } from '../ports/workspace/git-worktree'
5
+ import { loadPlugins } from '../plugins/load'
6
+ import { materializePluginRuntimes } from '../plugins/runtimes'
7
+ import { randomIds } from '../ids'
8
+ import { systemClock, type BuildStore } from '../store/types'
9
+ import { openProductionStore } from '../cli/store-opening'
10
+ import { BuildRunner, LeaseHeldError, SetupFailureError } from './build-runner'
11
+ import {
12
+ BUILD_EFFECTIVE_CONFIG_ARTIFACT,
13
+ diagnosticArtifact,
14
+ parseEffectiveBuildConfig,
15
+ selectOpenWorkspace,
16
+ type BuildRunnerDiagnosticOutcome,
17
+ } from './build-execution-state'
18
+ import type { BuildExecutionStart } from '../ports/workspace/build-execution'
19
+
20
+ function message(error: unknown): string {
21
+ return error instanceof Error ? error.message : String(error)
22
+ }
23
+
24
+ /** Child-side composition. The launch envelope supplies identity only; all
25
+ * execution location/configuration is discovered through the scoped Store. */
26
+ export async function runBuildChild(
27
+ input: BuildExecutionStart,
28
+ env: Record<string, string | undefined> = process.env,
29
+ openStore: (ref: string, token?: string) => BuildStore = openProductionStore,
30
+ ): Promise<void> {
31
+ const fullStore = openStore(input.storeRef, env.AB_TOKEN)
32
+ const store = fullStore.scopeBuild(input.slug)
33
+ try {
34
+ const record = await store.getBuild(input.slug)
35
+ if (record === null) throw new Error(`unknown build ${JSON.stringify(input.slug)}`)
36
+ const workspace = selectOpenWorkspace(await store.getEvents(input.slug))
37
+ if (workspace === null) {
38
+ throw new Error(`build ${JSON.stringify(input.slug)} has no open durable workspace`)
39
+ }
40
+ if (workspace.ref !== input.workspaceRef) {
41
+ throw new Error(
42
+ `build ${JSON.stringify(input.slug)} workspace changed from launch ref ` +
43
+ `${JSON.stringify(input.workspaceRef)} to ${JSON.stringify(workspace.ref)}`,
44
+ )
45
+ }
46
+ const configArtifact = await store.getArtifact(input.slug, BUILD_EFFECTIVE_CONFIG_ARTIFACT)
47
+ if (configArtifact === null) {
48
+ throw new Error(`build ${JSON.stringify(input.slug)} has no effective config artifact`)
49
+ }
50
+ let currentConfig = parseEffectiveBuildConfig(configArtifact)
51
+ const plugins = await loadPlugins(currentConfig.plugins, workspace.path, {
52
+ packageRoot: record.repo,
53
+ })
54
+ const { runtimes: builtins } = createProductionRuntimes()
55
+ const runtimes = await materializePluginRuntimes(builtins, plugins, {
56
+ repoRoot: workspace.path,
57
+ env,
58
+ })
59
+ const forge = await createForge({
60
+ name: currentConfig.forge,
61
+ registry: plugins,
62
+ env,
63
+ repoRoot: workspace.path,
64
+ })
65
+ const getConfig = async () => {
66
+ const latest = await store.getArtifact(input.slug, BUILD_EFFECTIVE_CONFIG_ARTIFACT)
67
+ if (latest === null) return currentConfig
68
+ try {
69
+ currentConfig = parseEffectiveBuildConfig(latest)
70
+ } catch {
71
+ // The dispatcher publishes only validated snapshots. If storage is
72
+ // externally corrupted, preserve the child's last valid boundary.
73
+ }
74
+ return currentConfig
75
+ }
76
+
77
+ const runner = new BuildRunner({
78
+ store,
79
+ config: currentConfig,
80
+ getConfig,
81
+ runtimes,
82
+ workspacePath: workspace.path,
83
+ branch: workspace.branch,
84
+ slug: input.slug,
85
+ exec: spawnExec,
86
+ forge,
87
+ ids: randomIds(),
88
+ clock: systemClock,
89
+ instance: input.instance,
90
+ host: hostname(),
91
+ sessionEnv: {
92
+ AB_STORE: input.storeRef,
93
+ ...(env.AB_TOKEN !== undefined && env.AB_TOKEN !== '' ? { AB_TOKEN: env.AB_TOKEN } : {}),
94
+ },
95
+ })
96
+ await runner.run()
97
+ } catch (error) {
98
+ let outcome: BuildRunnerDiagnosticOutcome = 'failed'
99
+ if (error instanceof LeaseHeldError) outcome = 'lease-held'
100
+ else if (error instanceof SetupFailureError) outcome = 'setup-failed'
101
+ try {
102
+ await store.putArtifact(
103
+ input.slug,
104
+ diagnosticArtifact({ instance: input.instance, outcome, error: message(error) }),
105
+ )
106
+ } catch {
107
+ // Store reachability failures cannot be diagnosed through that Store.
108
+ }
109
+ throw error
110
+ } finally {
111
+ try {
112
+ await fullStore.close()
113
+ } catch {
114
+ // Process teardown cannot change the already-durable runner outcome. A
115
+ // close failure after a clean park must not turn truthful Store state
116
+ // into a synthetic failed child settlement.
117
+ }
118
+ }
119
+ }
@@ -0,0 +1,95 @@
1
+ import type { Config } from '../config/schema'
2
+ import { configSchema } from '../config/schema'
3
+ import type { AbEvent } from '../events/catalog'
4
+ import type { Artifact, ArtifactInput } from '../store/types'
5
+
6
+ export const BUILD_EFFECTIVE_CONFIG_ARTIFACT = 'build-runner-effective-config'
7
+ export const BUILD_RUNNER_DIAGNOSTIC_ARTIFACT = 'build-runner-diagnostic'
8
+
9
+ export type BuildRunnerDiagnosticOutcome = 'lease-held' | 'setup-failed' | 'failed'
10
+
11
+ export interface BuildRunnerDiagnostic {
12
+ instance: string
13
+ outcome: BuildRunnerDiagnosticOutcome
14
+ error: string
15
+ }
16
+
17
+ /** JSON form of the normalized config's strict declarative input shape. */
18
+ export function effectiveBuildConfigContent(config: Config): string {
19
+ const { verify, finalize, ...root } = config
20
+ return JSON.stringify({
21
+ ...root,
22
+ verify: { steps: verify.steps, ...verify.stepConfigs },
23
+ finalize: { steps: finalize.steps, ...finalize.stepConfigs },
24
+ })
25
+ }
26
+
27
+ export function parseEffectiveBuildConfig(artifact: Artifact): Config {
28
+ let value: unknown
29
+ try {
30
+ value = JSON.parse(new TextDecoder().decode(artifact.content))
31
+ } catch (error) {
32
+ throw new Error(
33
+ `invalid ${BUILD_EFFECTIVE_CONFIG_ARTIFACT} JSON: ${
34
+ error instanceof Error ? error.message : String(error)
35
+ }`,
36
+ )
37
+ }
38
+ const result = configSchema.safeParse(value)
39
+ if (!result.success) {
40
+ throw new Error(
41
+ `invalid ${BUILD_EFFECTIVE_CONFIG_ARTIFACT}: ${result.error.issues
42
+ .map((issue) => `${issue.path.join('.') || '(root)'}: ${issue.message}`)
43
+ .join('; ')}`,
44
+ )
45
+ }
46
+ return result.data
47
+ }
48
+
49
+ /** Latest workspace location not followed by release. Historical events use
50
+ * the provider ref as the path compatibility fallback. */
51
+ export function selectOpenWorkspace(events: readonly AbEvent[]): {
52
+ ref: string
53
+ path: string
54
+ branch: string
55
+ } | null {
56
+ let open: { ref: string; path: string; branch: string } | null = null
57
+ for (const event of events) {
58
+ if (event.type === 'workspace.provisioned') {
59
+ open = {
60
+ ref: event.payload.ref,
61
+ path: event.payload.path ?? event.payload.ref,
62
+ branch: event.payload.branch,
63
+ }
64
+ } else if (event.type === 'workspace.released') {
65
+ open = null
66
+ }
67
+ }
68
+ return open
69
+ }
70
+
71
+ export function diagnosticArtifact(diagnostic: BuildRunnerDiagnostic): ArtifactInput {
72
+ return {
73
+ kind: BUILD_RUNNER_DIAGNOSTIC_ARTIFACT,
74
+ content: JSON.stringify(diagnostic),
75
+ metadata: { instance: diagnostic.instance, outcome: diagnostic.outcome },
76
+ }
77
+ }
78
+
79
+ export function parseDiagnostic(artifact: Artifact): BuildRunnerDiagnostic | null {
80
+ try {
81
+ const value = JSON.parse(
82
+ new TextDecoder().decode(artifact.content),
83
+ ) as Partial<BuildRunnerDiagnostic>
84
+ if (
85
+ typeof value.instance !== 'string' ||
86
+ typeof value.error !== 'string' ||
87
+ !['lease-held', 'setup-failed', 'failed'].includes(value.outcome ?? '')
88
+ ) {
89
+ return null
90
+ }
91
+ return value as BuildRunnerDiagnostic
92
+ } catch {
93
+ return null
94
+ }
95
+ }
@@ -0,0 +1,35 @@
1
+ /** Parent-liveness watchdog for a local build child. `expectedParentPid` comes
2
+ * from the immutable launch envelope rather than a possibly already-reparented
3
+ * `process.ppid`, closing the module-startup death window. The child
4
+ * deliberately terminates without releasing its lease; ordinary expiry is the
5
+ * durable recovery mechanism after an abrupt kernel death. */
6
+ export function watchBuildParent(
7
+ expectedParentPid: number,
8
+ onDeath: () => never | void,
9
+ intervalMs = 500,
10
+ ): { close(): void } {
11
+ let closed = false
12
+ const check = (): void => {
13
+ if (closed) return
14
+ if (process.ppid !== expectedParentPid || process.ppid === 1) {
15
+ closed = true
16
+ onDeath()
17
+ return
18
+ }
19
+ try {
20
+ process.kill(expectedParentPid, 0)
21
+ } catch {
22
+ closed = true
23
+ onDeath()
24
+ }
25
+ }
26
+ const timer = setInterval(check, intervalMs)
27
+ timer.unref()
28
+ return {
29
+ close() {
30
+ if (closed) return
31
+ closed = true
32
+ clearInterval(timer)
33
+ },
34
+ }
35
+ }