@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,259 @@
1
+ import { distributionPath } from '../distribution'
2
+ import { DISPATCHER } from '../events/envelope'
3
+ import type { RepositoryEvent } from '../events/repository'
4
+ import type { BuildStore } from '../store/types'
5
+
6
+ export const DISPATCH_CHILD_OPTIONS_ENV = 'AB_DISPATCH_CHILD_OPTIONS'
7
+ const DEFAULT_STOP_TIMEOUT_MS = 5_000
8
+
9
+ export interface DispatchChildOptions {
10
+ targetRepo: string
11
+ storeRef: string
12
+ run: string
13
+ once: boolean
14
+ intervalMs?: number
15
+ }
16
+
17
+ export type DispatchChildOutcome = 'normal' | 'abnormal' | 'forced'
18
+
19
+ export interface DispatchChildResult {
20
+ outcome: DispatchChildOutcome
21
+ exitCode: number
22
+ signal?: string
23
+ error?: string
24
+ }
25
+
26
+ export interface DispatchChildHandle {
27
+ completed: Promise<DispatchChildResult>
28
+ stop(): Promise<void>
29
+ }
30
+
31
+ export interface DispatchSubprocess {
32
+ exited: Promise<number>
33
+ exitCode: number | null
34
+ signalCode: string | number | null
35
+ kill(signal: 'SIGINT' | 'SIGKILL'): void
36
+ }
37
+
38
+ export interface DispatchProcessHook {
39
+ close(): void
40
+ }
41
+
42
+ export const DISPATCH_KERNEL_SIGNALS = [
43
+ 'SIGINT',
44
+ 'SIGHUP',
45
+ 'SIGQUIT',
46
+ 'SIGTERM',
47
+ ] as const satisfies readonly NodeJS.Signals[]
48
+
49
+ type DispatchKernelSignal = (typeof DISPATCH_KERNEL_SIGNALS)[number]
50
+
51
+ export interface DispatchKernelSignalBoundary {
52
+ on(signal: DispatchKernelSignal, listener: () => void): unknown
53
+ removeListener(signal: DispatchKernelSignal, listener: () => void): unknown
54
+ }
55
+
56
+ /** Keep graceful handlers active until the kernel has actually drained. Using
57
+ * one-shot handlers would expose the child to the default disposition on a
58
+ * repeated Ctrl-C while it is still crossing an unsafe claim boundary. */
59
+ export function installDispatchKernelSignalHandlers(
60
+ onStop: () => void,
61
+ boundary: DispatchKernelSignalBoundary = process,
62
+ ): DispatchProcessHook {
63
+ for (const signal of DISPATCH_KERNEL_SIGNALS) boundary.on(signal, onStop)
64
+ let closed = false
65
+ return {
66
+ close(): void {
67
+ if (closed) return
68
+ closed = true
69
+ for (const signal of DISPATCH_KERNEL_SIGNALS) boundary.removeListener(signal, onStop)
70
+ },
71
+ }
72
+ }
73
+
74
+ export interface DispatchParentWatchDeps {
75
+ intervalMs?: number
76
+ currentParentPid?: () => number
77
+ isAlive?: (pid: number) => boolean
78
+ }
79
+
80
+ /** A private kernel must never become a detached daemon if its terminal-owning
81
+ * parent is killed directly rather than through the foreground process group.
82
+ * Polling is deliberately local process liveness, not an operational IPC
83
+ * channel; shutdown still converges through the normal AbortSignal boundary. */
84
+ export function watchDispatchParent(
85
+ parentPid: number,
86
+ onParentExit: () => void,
87
+ deps: DispatchParentWatchDeps = {},
88
+ ): DispatchProcessHook {
89
+ const currentParentPid = deps.currentParentPid ?? (() => process.ppid)
90
+ const isAlive =
91
+ deps.isAlive ??
92
+ ((pid: number): boolean => {
93
+ try {
94
+ process.kill(pid, 0)
95
+ return true
96
+ } catch {
97
+ return false
98
+ }
99
+ })
100
+ let closed = false
101
+ let timer: ReturnType<typeof setInterval>
102
+ const close = (): void => {
103
+ if (closed) return
104
+ closed = true
105
+ clearInterval(timer)
106
+ }
107
+ timer = setInterval(() => {
108
+ if (currentParentPid() === parentPid && isAlive(parentPid)) return
109
+ close()
110
+ onParentExit()
111
+ }, deps.intervalMs ?? 100)
112
+ timer.unref?.()
113
+ return { close }
114
+ }
115
+
116
+ export interface DispatchChildSupervisorDeps {
117
+ store: BuildStore
118
+ /** Repository identity (§12) — the Store journal key. */
119
+ repo: string
120
+ /** Process cwd for the spawned kernel; defaults to `repo` (checkout mode). */
121
+ cwd?: string
122
+ run: string
123
+ env: Record<string, string | undefined>
124
+ options: DispatchChildOptions
125
+ stopTimeoutMs?: number
126
+ entrypoint?: string
127
+ /** Injectable process seam for lifecycle and timeout tests. */
128
+ spawn?: (input: {
129
+ entrypoint: string
130
+ cwd: string
131
+ env: Record<string, string>
132
+ }) => DispatchSubprocess
133
+ }
134
+
135
+ function hasOpenTick(events: readonly RepositoryEvent[], run: string): boolean {
136
+ const boundary = events.findLast(
137
+ (event) =>
138
+ 'run' in event.payload &&
139
+ event.payload.run === run &&
140
+ (event.type === 'dispatcher.tick-started' ||
141
+ event.type === 'dispatcher.tick-completed' ||
142
+ event.type === 'dispatcher.tick-failed'),
143
+ )
144
+ return boundary?.type === 'dispatcher.tick-started'
145
+ }
146
+
147
+ function defaultSpawn(input: {
148
+ entrypoint: string
149
+ cwd: string
150
+ env: Record<string, string>
151
+ }): DispatchSubprocess {
152
+ return Bun.spawn([process.execPath, input.entrypoint], {
153
+ cwd: input.cwd,
154
+ env: input.env,
155
+ stdin: 'ignore',
156
+ stdout: 'ignore',
157
+ stderr: 'ignore',
158
+ })
159
+ }
160
+
161
+ /** Spawn and reap the private kernel. Changing operational state never crosses
162
+ * this boundary: options are immutable launch identity and all live facts and
163
+ * commands use BuildStore. */
164
+ export function superviseDispatchChild(deps: DispatchChildSupervisorDeps): DispatchChildHandle {
165
+ const entrypoint = deps.entrypoint ?? distributionPath('bin', 'ab-dispatch-kernel.ts')
166
+ const env: Record<string, string> = {}
167
+ for (const [key, value] of Object.entries(deps.env)) if (value !== undefined) env[key] = value
168
+ env[DISPATCH_CHILD_OPTIONS_ENV] = JSON.stringify(deps.options)
169
+ const child = (deps.spawn ?? defaultSpawn)({ entrypoint, cwd: deps.cwd ?? deps.repo, env })
170
+
171
+ let stopping = false
172
+ let forced = false
173
+ let stopPromise: Promise<void> | undefined
174
+ // This is the sole supervisor writer of a missing stop fact. `stop()` only
175
+ // chooses/awaits process termination, avoiding check-then-append races.
176
+ const completed = child.exited.then(async (exitCode): Promise<DispatchChildResult> => {
177
+ const processSignal = child.signalCode === null ? undefined : String(child.signalCode)
178
+ const events = await deps.store.getRepoEvents(deps.repo)
179
+ const recorded = events.findLast(
180
+ (event) => event.type === 'dispatcher.run-stopped' && event.payload.run === deps.run,
181
+ )
182
+ if (recorded?.type === 'dispatcher.run-stopped') {
183
+ const signal = processSignal ?? recorded.payload.signal
184
+ return {
185
+ outcome: recorded.payload.outcome,
186
+ exitCode,
187
+ ...(signal !== undefined ? { signal } : {}),
188
+ ...(recorded.payload.error !== undefined ? { error: recorded.payload.error } : {}),
189
+ }
190
+ }
191
+
192
+ const outcome: DispatchChildOutcome = forced
193
+ ? 'forced'
194
+ : stopping && exitCode === 0 && processSignal === undefined
195
+ ? 'normal'
196
+ : 'abnormal'
197
+ const error =
198
+ outcome === 'abnormal'
199
+ ? `kernel process exited with status ${exitCode}${
200
+ processSignal !== undefined ? ` (signal ${processSignal})` : ''
201
+ }`
202
+ : undefined
203
+ await deps.store.appendRepo(deps.repo, {
204
+ actor: DISPATCHER,
205
+ type: 'dispatcher.run-stopped',
206
+ payload: {
207
+ run: deps.run,
208
+ outcome,
209
+ exitCode,
210
+ ...(processSignal !== undefined ? { signal: processSignal } : {}),
211
+ ...(error !== undefined ? { error } : {}),
212
+ },
213
+ })
214
+ return {
215
+ outcome,
216
+ exitCode,
217
+ ...(processSignal !== undefined ? { signal: processSignal } : {}),
218
+ ...(error !== undefined ? { error } : {}),
219
+ }
220
+ })
221
+
222
+ return {
223
+ completed,
224
+ stop(): Promise<void> {
225
+ if (stopPromise !== undefined) return stopPromise
226
+ stopPromise = (async () => {
227
+ if (child.exitCode !== null) return
228
+ stopping = true
229
+ child.kill('SIGINT')
230
+ const timeout = deps.stopTimeoutMs ?? DEFAULT_STOP_TIMEOUT_MS
231
+ const stopped = await Promise.race([
232
+ child.exited.then(() => true),
233
+ new Promise<false>((resolve) => setTimeout(() => resolve(false), timeout)),
234
+ ])
235
+ if (stopped || child.exitCode !== null) return
236
+
237
+ // A dispatcher tick may hold an external ticket claim before its build
238
+ // record exists. Killing that turn can make the ticket disappear from
239
+ // Ready with no durable build to recover. Let an open tick reach its
240
+ // normal boundary; all non-tick work is lease-backed/recoverable and a
241
+ // child stuck there is safe to terminate.
242
+ const events = await deps.store.getRepoEvents(deps.repo)
243
+ // The child may have exited while the asynchronous journal read was in
244
+ // flight. Re-check at the force boundary so evidence never claims a
245
+ // SIGKILL that was not actually sent.
246
+ if (child.exitCode !== null) return
247
+ if (hasOpenTick(events, deps.run)) {
248
+ await child.exited
249
+ return
250
+ }
251
+
252
+ forced = true
253
+ child.kill('SIGKILL')
254
+ await child.exited
255
+ })()
256
+ return stopPromise
257
+ },
258
+ }
259
+ }