@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,171 @@
1
+ /**
2
+ * Settlement of foreign build executions from durable facts plus provider
3
+ * liveness (AUT-301). Today's completion handling hangs off the in-memory
4
+ * `handle.completion` promise of the process that launched the runner; when
5
+ * that process dies, nothing writes `execution.ended`, releases the lease, or
6
+ * settles publication. These helpers let any dispatcher tick re-derive the
7
+ * same decisions from the event log and one bounded provider observation —
8
+ * mirroring the engine, which is already a pure function over the log.
9
+ *
10
+ * Conservatism rule: an observation that cannot be resolved (provider error)
11
+ * is treated as `running`. Supervision decisions reap or settle only on
12
+ * provider proof, never on an unknown.
13
+ */
14
+ import { DISPATCHER } from '../events/envelope'
15
+ import type { AbEvent, EventWrite } from '../events/catalog'
16
+ import type {
17
+ BuildExecution,
18
+ BuildExecutionIdentity,
19
+ ExecutionObservation,
20
+ } from '../ports/workspace/build-execution'
21
+ import type { BuildStore } from '../store/types'
22
+
23
+ /** The latest `execution.started` not followed by an instance-matched
24
+ * `execution.ended`, or null when no execution is open. */
25
+ export function openExecution(events: AbEvent[]): {
26
+ instance: string
27
+ workspaceRef: string
28
+ provider: string
29
+ environmentId?: string
30
+ sessionId?: string
31
+ commandId?: string
32
+ } | null {
33
+ let open: {
34
+ instance: string
35
+ workspaceRef: string
36
+ provider: string
37
+ environmentId?: string
38
+ sessionId?: string
39
+ commandId?: string
40
+ } | null = null
41
+ for (const event of events) {
42
+ if (event.type === 'execution.started') {
43
+ const payload = event.payload
44
+ open = {
45
+ instance: payload.instance,
46
+ workspaceRef: payload.workspaceRef,
47
+ provider: payload.provider,
48
+ ...(payload.environmentId !== undefined ? { environmentId: payload.environmentId } : {}),
49
+ ...(payload.sessionId !== undefined ? { sessionId: payload.sessionId } : {}),
50
+ ...(payload.commandId !== undefined ? { commandId: payload.commandId } : {}),
51
+ }
52
+ } else if (event.type === 'execution.ended' && open !== null) {
53
+ if (event.payload.instance === open.instance) open = null
54
+ }
55
+ }
56
+ return open
57
+ }
58
+
59
+ /** The lifecycle state of the latest recorded execution: `open` (no matching
60
+ * end yet), `completed`/`stopped`/`lost` (its recorded end), or `none`. */
61
+ export function lastExecutionOutcome(
62
+ events: AbEvent[],
63
+ ): 'open' | 'completed' | 'stopped' | 'lost' | 'none' {
64
+ let state: 'open' | 'completed' | 'stopped' | 'lost' | 'none' = 'none'
65
+ for (const event of events) {
66
+ if (event.type === 'execution.started') {
67
+ state = 'open'
68
+ } else if (event.type === 'execution.ended' && state === 'open') {
69
+ state = event.payload.outcome
70
+ }
71
+ }
72
+ return state
73
+ }
74
+
75
+ function observationIdentity(open: {
76
+ instance: string
77
+ workspaceRef: string
78
+ provider: string
79
+ environmentId?: string
80
+ sessionId?: string
81
+ commandId?: string
82
+ }): BuildExecutionIdentity {
83
+ return {
84
+ provider: open.provider,
85
+ workspaceRef: open.workspaceRef,
86
+ ...(open.environmentId !== undefined ? { environmentId: open.environmentId } : {}),
87
+ ...(open.sessionId !== undefined ? { sessionId: open.sessionId } : {}),
88
+ ...(open.commandId !== undefined ? { commandId: open.commandId } : {}),
89
+ }
90
+ }
91
+
92
+ /** Durable settlement outcome for one foreign open execution. `settled` means
93
+ * a recorded completion was written and publication was settled; `lost` means
94
+ * the provider proved the execution gone without an exit code (publication is
95
+ * deliberately left to the existing abandonment flow); `running` means the
96
+ * guest is alive or unknowable — no writes. */
97
+ export type ExecutionSettlement = 'running' | 'settled' | 'lost'
98
+
99
+ export interface ExecutionSettlementDeps {
100
+ store: BuildStore
101
+ /** The observing provider's executor. Settlement requires the `observe`
102
+ * capability; without it the caller supervises as before. */
103
+ execution: BuildExecution
104
+ /** Trusted publication settlement, invoked only after a recorded completion. */
105
+ settlePublication?: (slug: string) => Promise<void>
106
+ }
107
+
108
+ /** Release the open execution's exact lease, but only while that instance
109
+ * still holds it — a renewed runner lease under a different instance is never
110
+ * touched. */
111
+ async function releaseSettledLease(
112
+ store: BuildStore,
113
+ slug: string,
114
+ instance: string,
115
+ ): Promise<void> {
116
+ const record = await store.getBuild(slug)
117
+ if (record?.lease !== undefined && record.lease.holder === instance) {
118
+ await store.releaseLease(slug, instance)
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Observe one foreign open execution and settle it from the observation.
124
+ * Observation errors are contained and read as `running` (conservative: never
125
+ * reap on an unknown); the next tick retries.
126
+ */
127
+ export async function settleExecution(
128
+ deps: ExecutionSettlementDeps,
129
+ slug: string,
130
+ events: AbEvent[],
131
+ ): Promise<ExecutionSettlement> {
132
+ const open = openExecution(events)
133
+ if (open === null) return 'settled'
134
+ if (open.commandId === undefined || deps.execution.observe === undefined) return 'running'
135
+ let observation: ExecutionObservation
136
+ try {
137
+ observation = await deps.execution.observe(observationIdentity(open))
138
+ } catch {
139
+ return 'running'
140
+ }
141
+ if (observation.state === 'running') return 'running'
142
+
143
+ const ended: EventWrite<'execution.ended'> =
144
+ observation.state === 'ended'
145
+ ? {
146
+ actor: DISPATCHER,
147
+ type: 'execution.ended',
148
+ payload: {
149
+ instance: open.instance,
150
+ workspaceRef: open.workspaceRef,
151
+ outcome: 'completed',
152
+ exitCode: observation.exitCode ?? null,
153
+ },
154
+ }
155
+ : {
156
+ actor: DISPATCHER,
157
+ type: 'execution.ended',
158
+ payload: {
159
+ instance: open.instance,
160
+ workspaceRef: open.workspaceRef,
161
+ outcome: 'lost',
162
+ },
163
+ }
164
+ await deps.store.append(slug, ended)
165
+ await releaseSettledLease(deps.store, slug, open.instance)
166
+ if (observation.state === 'ended') {
167
+ await deps.settlePublication?.(slug)
168
+ return 'settled'
169
+ }
170
+ return 'lost'
171
+ }
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Crash-safe repository-scoped harvest workflow: scan -> synthesize <-> review
3
+ * -> file. It reuses `converge` for adversarial review semantics while every
4
+ * durable boundary lives in the repository journal, so a replacement process
5
+ * resumes the claimed snapshot rather than starting a duplicate run.
6
+ */
7
+ import { join } from 'node:path'
8
+ import { loadConfig } from '../config/load'
9
+ import type { HarvestExecutionEnvironment } from '../ports/workspace/harvest-execution'
10
+ import type { HarvestRunnerLaunch } from '../ports/workspace/harvest-execution'
11
+ import { loadPlugins } from '../plugins/load'
12
+ import { materializePluginRuntimes } from '../plugins/runtimes'
13
+ import { randomIds, randomUuids } from '../ids'
14
+ import { createTicketSource } from '../ports/tickets/create'
15
+ import { createProductionRuntimes } from '../ports/runner/production'
16
+ import type { BuildStore } from '../store/types'
17
+ import { systemClock } from '../store/types'
18
+ import { openProductionStore, type StoreOpener } from '../cli/store-opening'
19
+ import { resolveRepoStatePaths } from '../cli/repo-state'
20
+ import { DEFAULT_MAX_HARVEST_RECOVERY_ATTEMPTS } from '../kernel/harvest'
21
+ import { HarvestRunner } from './harvest-runner'
22
+
23
+ export type { HarvestExecutionEnvironment }
24
+
25
+ /** Guest-side ticket-source arguments, identical to what a local harvest in
26
+ * this checkout would pass: targetRepo is the physical guest checkout, and
27
+ * localStateRoot follows the same store-selection precedence rule the host's
28
+ * `resolveRepoStatePaths` applies (remote store → checkout-local `.autobuild`,
29
+ * local store → the resolved store ref). A relative or defaulted
30
+ * `[tickets].dir` therefore resolves identically on host and guest. */
31
+ export function guestTicketSourceArgs(
32
+ input: HarvestRunnerLaunch,
33
+ workspacePath: string,
34
+ ): { targetRepo: string; localStateRoot: string } {
35
+ const paths = resolveRepoStatePaths({
36
+ repo: input.repo,
37
+ checkout: workspacePath,
38
+ storeRef: input.storeRef,
39
+ })
40
+ return { targetRepo: paths.checkout, localStateRoot: paths.localStateRoot }
41
+ }
42
+
43
+ /** Guest-side composition. The launch envelope supplies identity, lease
44
+ * adoption, and environment provenance only; configuration comes from the
45
+ * guest checkout and the hosted Store/ticket authority from the forwarded
46
+ * environment — the same kernel class a local harvest runs, so filed
47
+ * proposals, creation keys, and idempotency are byte-identical. Ticket-source
48
+ * composition likewise matches a local harvest in this checkout: a relative
49
+ * or defaulted `[tickets].dir` resolves against the guest checkout and the
50
+ * selected local state root (see `guestTicketSourceArgs`). */
51
+ export async function runHarvestChild(
52
+ input: HarvestRunnerLaunch,
53
+ env: Record<string, string | undefined> = process.env,
54
+ openStore: StoreOpener = openProductionStore,
55
+ /** Guest checkout root. The provider launches the runner with this as cwd. */
56
+ workspacePath: string = process.cwd(),
57
+ ): Promise<void> {
58
+ const fullStore: BuildStore = openStore(input.storeRef, env.AB_TOKEN)
59
+ try {
60
+ const config = await loadConfig(join(workspacePath, 'autobuild.toml'))
61
+ // Full build-child composition parity, in the same order: plugins load
62
+ // first or materializePluginRuntimes silently no-ops and leaves any
63
+ // plugin-provided runtime unrouted in the guest.
64
+ const plugins = await loadPlugins(config.plugins, workspacePath, {
65
+ packageRoot: input.repo,
66
+ })
67
+ const { runtimes: builtins } = createProductionRuntimes()
68
+ const runtimes = await materializePluginRuntimes(builtins, plugins, {
69
+ repoRoot: workspacePath,
70
+ env,
71
+ })
72
+ // Ticket-source composition matches a local harvest in this checkout: a
73
+ // relative or defaulted [tickets].dir resolves against the guest checkout
74
+ // and the selected local state root, never the host-side repo identity.
75
+ const { targetRepo, localStateRoot } = guestTicketSourceArgs(input, workspacePath)
76
+ const tickets = await createTicketSource(
77
+ config.tickets,
78
+ env,
79
+ targetRepo,
80
+ localStateRoot,
81
+ plugins,
82
+ )
83
+
84
+ const runner = new HarvestRunner({
85
+ store: fullStore,
86
+ tickets,
87
+ config,
88
+ runtimes,
89
+ repo: input.repo,
90
+ workspacePath,
91
+ ids: randomIds(),
92
+ uuids: randomUuids(),
93
+ clock: systemClock,
94
+ instance: input.instance,
95
+ // Adopted holder: the guest skips its own claim, heartbeats the owning
96
+ // dispatch loop's repository lease, and never releases it.
97
+ ...(input.leaseHolder !== undefined ? { leaseHolder: input.leaseHolder } : {}),
98
+ ...(input.environment !== undefined ? { environment: input.environment } : {}),
99
+ sessionEnv: {
100
+ AB_STORE: input.storeRef,
101
+ ...(env.AB_TOKEN !== undefined && env.AB_TOKEN !== '' ? { AB_TOKEN: env.AB_TOKEN } : {}),
102
+ },
103
+ opts: {
104
+ maxRecoveryAttempts: DEFAULT_MAX_HARVEST_RECOVERY_ATTEMPTS,
105
+ },
106
+ })
107
+ await runner.run()
108
+ } finally {
109
+ try {
110
+ await fullStore.close()
111
+ } catch {
112
+ // Process teardown cannot change the already-durable runner outcome.
113
+ }
114
+ }
115
+ }
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Durable harvest execution state: the repository-journal projection that
3
+ * hosted-harvest supervision (AUT-305) reads instead of process memory. The
4
+ * launch/close facts (`harvest.execution.started`/`.released`) bracket one
5
+ * disposable environment; the run's own facts decide the outcome. Every
6
+ * helper here is a pure function over the journal, mirroring the build
7
+ * execution settlement, so any dispatcher invocation can settle a run whose
8
+ * supervisor died.
9
+ */
10
+ import type { RepositoryEvent } from '../events/repository'
11
+ import {
12
+ DEFAULT_MAX_HARVEST_RECOVERY_ATTEMPTS,
13
+ actionableHarvestRun,
14
+ decideHarvestControl,
15
+ reduceHarvest,
16
+ } from '../kernel/harvest'
17
+ import type { HarvestRunnerResult } from './harvest-runner'
18
+
19
+ /** Open harvest executions: every `harvest.execution.started` not followed by
20
+ * a matching `harvest.execution.released`, in journal order. */
21
+ export function openHarvestExecutions(events: readonly RepositoryEvent[]): Array<{
22
+ execution: string
23
+ provider: string
24
+ environmentId: string
25
+ sessionId?: string
26
+ commandId?: string
27
+ seq: number
28
+ }> {
29
+ const open = new Map<
30
+ string,
31
+ {
32
+ execution: string
33
+ provider: string
34
+ environmentId: string
35
+ sessionId?: string
36
+ commandId?: string
37
+ seq: number
38
+ }
39
+ >()
40
+ for (const event of events) {
41
+ if (event.type === 'harvest.execution.started') {
42
+ open.set(event.payload.execution, {
43
+ execution: event.payload.execution,
44
+ provider: event.payload.provider,
45
+ environmentId: event.payload.environmentId,
46
+ ...(event.payload.sessionId !== undefined ? { sessionId: event.payload.sessionId } : {}),
47
+ ...(event.payload.commandId !== undefined ? { commandId: event.payload.commandId } : {}),
48
+ seq: event.seq,
49
+ })
50
+ } else if (event.type === 'harvest.execution.released') {
51
+ open.delete(event.payload.execution)
52
+ }
53
+ }
54
+ return [...open.values()]
55
+ }
56
+
57
+ export interface HarvestExecutionClassificationInput {
58
+ /** Repo-journal seq of this execution's `harvest.execution.started` fact. */
59
+ executionStartedSeq: number
60
+ /** The repository-lease holder this execution adopted from the dispatch
61
+ * loop. Absent (local runner shape) makes the held check inapplicable. */
62
+ adoptedHolder?: string
63
+ /** The repository lease holder observed after the guest exited. */
64
+ leaseHolder?: string
65
+ maxRecoveryAttempts?: number
66
+ }
67
+
68
+ /** Classify one finished hosted harvest execution from the repository journal
69
+ * alone — the guest's exit code carries no pipeline outcome. The latest run's
70
+ * terminal fact wins (`completed`/`escalated`/unresolved `failed`) — but only
71
+ * when that terminal fact postdates this execution's start: a stale terminal
72
+ * fact was already counted and announced by the execution that produced it,
73
+ * and re-reporting it would double-count. Launch attribution is by whether
74
+ * the run started inside this execution; a run still open under a
75
+ * parked/paused control decision is `parked`; a lease held by someone else is
76
+ * `held`; anything else is `idle`. The result feeds the same counters a
77
+ * locally run harvest reports. */
78
+ export function classifyHarvestOutcome(
79
+ events: readonly RepositoryEvent[],
80
+ input: HarvestExecutionClassificationInput,
81
+ ): HarvestRunnerResult {
82
+ const state = reduceHarvest(events)
83
+ const latestRun = state.runs.at(-1)
84
+ if (latestRun !== undefined) {
85
+ const launch = latestRun.startedSeq > input.executionStartedSeq ? 'started' : 'resumed'
86
+ // Recency gate: a terminal outcome counts only if its fact — the run's
87
+ // terminal fact, or for an unresolved failure the failure fact itself
88
+ // (`failureSeq`), which also covers exhaustion via `terminalSeq` —
89
+ // postdates this execution's `harvest.execution.started`.
90
+ const outcomeSeq = Math.max(latestRun.terminalSeq ?? 0, latestRun.failureSeq ?? 0)
91
+ if (outcomeSeq > input.executionStartedSeq) {
92
+ if (latestRun.status === 'completed') {
93
+ return { outcome: 'completed', launch, run: latestRun.run }
94
+ }
95
+ if (latestRun.status === 'escalated') {
96
+ return { outcome: 'escalated', launch, run: latestRun.run }
97
+ }
98
+ // An unresolved harvest.failed — including recovery exhaustion — is a
99
+ // durable stop, not an idle exit.
100
+ if (latestRun.failure !== undefined) {
101
+ return { outcome: 'failed', launch, run: latestRun.run }
102
+ }
103
+ }
104
+ }
105
+ const decision = decideHarvestControl(
106
+ state,
107
+ input.maxRecoveryAttempts ?? DEFAULT_MAX_HARVEST_RECOVERY_ATTEMPTS,
108
+ )
109
+ if (decision.kind === 'park') {
110
+ const run = actionableHarvestRun(state)?.run
111
+ return run === undefined ? { outcome: 'parked' } : { outcome: 'parked', run }
112
+ }
113
+ if (
114
+ input.adoptedHolder !== undefined &&
115
+ input.leaseHolder !== undefined &&
116
+ input.leaseHolder !== input.adoptedHolder
117
+ ) {
118
+ return { outcome: 'held' }
119
+ }
120
+ return { outcome: 'idle' }
121
+ }