@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,255 @@
1
+ import { parseConfig } from './load'
2
+ import type { Config, TicketsConfig } from './schema'
3
+ import { createRuntimeResolver, type RuntimeResolver } from '../ports/runner/routing'
4
+ import type { RuntimeRegistry } from '../ports/runner/runtime'
5
+
6
+ /** Exact repository artifact kind used for accepted dispatcher config revisions. */
7
+ export const DISPATCHER_CONFIG_ARTIFACT = 'dispatcher-config'
8
+
9
+ /**
10
+ * Explicit hot/restart classification for every strict root field. Open maps
11
+ * are classified at their owning table: their entries all share that table's
12
+ * behavior.
13
+ */
14
+ export const CONFIG_RELOAD_CLASSIFICATION = {
15
+ baseBranch: 'hot',
16
+ capacity: 'hot',
17
+ forge: 'restart',
18
+ plugins: 'restart',
19
+ pr: 'hot',
20
+ workspace: {
21
+ provider: 'restart',
22
+ config: 'restart',
23
+ },
24
+ commands: 'hot',
25
+ verify: 'hot',
26
+ finalize: 'hot',
27
+ roles: 'hot',
28
+ policy: 'hot',
29
+ orchestrator: 'restart',
30
+ tickets: {
31
+ source: 'restart',
32
+ readyLabels: 'hot',
33
+ readyState: 'hot',
34
+ teamKey: 'restart',
35
+ claimedState: 'restart',
36
+ createState: 'restart',
37
+ triageState: 'hot',
38
+ proposalState: 'hot',
39
+ dir: 'restart',
40
+ },
41
+ } as const satisfies {
42
+ [K in keyof Config]: K extends 'workspace'
43
+ ? { [P in keyof Config['workspace']]: 'hot' | 'restart' }
44
+ : K extends 'tickets'
45
+ ? { [P in keyof TicketsConfig]: 'hot' | 'restart' }
46
+ : 'hot' | 'restart'
47
+ }
48
+
49
+ export const RESTART_REQUIRED_CONFIG_PATHS = [
50
+ 'forge',
51
+ 'plugins',
52
+ 'workspace.provider',
53
+ 'workspace.config',
54
+ 'orchestrator',
55
+ 'tickets.source',
56
+ 'tickets.teamKey',
57
+ 'tickets.claimedState',
58
+ 'tickets.createState',
59
+ 'tickets.dir',
60
+ ] as const
61
+ export type RestartRequiredConfigPath = (typeof RESTART_REQUIRED_CONFIG_PATHS)[number]
62
+
63
+ function same(left: unknown, right: unknown): boolean {
64
+ return JSON.stringify(left) === JSON.stringify(right)
65
+ }
66
+
67
+ /** Paths whose candidate values cannot replace startup-constructed adapters. */
68
+ export function restartRequiredChanges(
69
+ startup: Config,
70
+ candidate: Config,
71
+ ): RestartRequiredConfigPath[] {
72
+ const changed: RestartRequiredConfigPath[] = []
73
+ const compare = (
74
+ path: (typeof RESTART_REQUIRED_CONFIG_PATHS)[number],
75
+ a: unknown,
76
+ b: unknown,
77
+ ) => {
78
+ if (!same(a, b)) changed.push(path)
79
+ }
80
+ compare('forge', startup.forge, candidate.forge)
81
+ compare('plugins', startup.plugins, candidate.plugins)
82
+ compare('workspace.provider', startup.workspace.provider, candidate.workspace.provider)
83
+ compare('workspace.config', startup.workspace.config, candidate.workspace.config)
84
+ compare('tickets.source', startup.tickets.source, candidate.tickets.source)
85
+ compare('tickets.teamKey', startup.tickets.teamKey, candidate.tickets.teamKey)
86
+ compare('tickets.claimedState', startup.tickets.claimedState, candidate.tickets.claimedState)
87
+ compare('tickets.createState', startup.tickets.createState, candidate.tickets.createState)
88
+ compare('tickets.dir', startup.tickets.dir, candidate.tickets.dir)
89
+ compare('orchestrator', startup.orchestrator, candidate.orchestrator)
90
+ return changed
91
+ }
92
+
93
+ /** Overlay all hot fields while retaining every startup-built adapter field. */
94
+ export function composeReloadedConfig(startup: Config, candidate: Config): Config {
95
+ return {
96
+ ...candidate,
97
+ forge: startup.forge,
98
+ plugins: startup.plugins,
99
+ workspace: startup.workspace,
100
+ orchestrator: startup.orchestrator,
101
+ tickets: {
102
+ ...candidate.tickets,
103
+ source: startup.tickets.source,
104
+ teamKey: startup.tickets.teamKey,
105
+ claimedState: startup.tickets.claimedState,
106
+ createState: startup.tickets.createState,
107
+ dir: startup.tickets.dir,
108
+ },
109
+ }
110
+ }
111
+
112
+ export interface ConfigSnapshot {
113
+ readonly config: Config
114
+ readonly resolver: RuntimeResolver
115
+ /** Process-local revision. Zero is the startup file. */
116
+ readonly revision: number
117
+ }
118
+
119
+ export type ConfigReloadOutcome =
120
+ | { kind: 'unchanged' }
121
+ | { kind: 'rejected'; error: string; notify: boolean }
122
+ | { kind: 'publication-failed'; error: string }
123
+ | {
124
+ kind: 'adopted'
125
+ snapshot: ConfigSnapshot
126
+ restartRequired: RestartRequiredConfigPath[]
127
+ effectiveChanged: boolean
128
+ }
129
+
130
+ export type ConfigReloadPublisher = (input: {
131
+ content: string
132
+ /** Fully composed snapshot that will become current after publication. */
133
+ effectiveConfig: Config
134
+ restartRequired: readonly RestartRequiredConfigPath[]
135
+ effectiveChanged: boolean
136
+ }) => Promise<void>
137
+
138
+ /**
139
+ * Process-local owner of the dispatcher's effective config. A candidate is
140
+ * published only after parsing, eager route validation, and durable journal
141
+ * publication all succeed. Callers capture `current()` once per boundary.
142
+ */
143
+ export class LiveConfig {
144
+ private snapshot: ConfigSnapshot
145
+ private acceptedContent: string
146
+ private rejectedContent: string | undefined
147
+
148
+ constructor(
149
+ private readonly source: string,
150
+ private readonly startup: Config,
151
+ startupContent: string,
152
+ private readonly runtimes: RuntimeRegistry,
153
+ private readonly publish: ConfigReloadPublisher,
154
+ ) {
155
+ this.acceptedContent = startupContent
156
+ this.snapshot = {
157
+ config: startup,
158
+ resolver: createRuntimeResolver(runtimes, startup.roles, startup.policy.sessionBudgetSeconds),
159
+ revision: 0,
160
+ }
161
+ }
162
+
163
+ current(): ConfigSnapshot {
164
+ return this.snapshot
165
+ }
166
+
167
+ async refreshFromDisk(): Promise<ConfigReloadOutcome> {
168
+ return this.refreshFrom(() => Bun.file(this.source).text())
169
+ }
170
+
171
+ /** Reload from any content source — the startup file (refreshFromDisk) or,
172
+ * in checkout-less origin mode, the base branch's autobuild.toml fetched
173
+ * through the forge. Read failures take the same rejected-read path: the
174
+ * last valid snapshot remains active and the error is reported once per
175
+ * distinct failure. */
176
+ async refreshFrom(read: () => Promise<string>): Promise<ConfigReloadOutcome> {
177
+ let content: string
178
+ try {
179
+ content = await read()
180
+ } catch (error) {
181
+ const code = (error as NodeJS.ErrnoException).code
182
+ const detail =
183
+ code === 'ENOENT'
184
+ ? 'is missing during live reload'
185
+ : `could not be read during live reload: ${
186
+ error instanceof Error ? error.message : String(error)
187
+ }`
188
+ const message =
189
+ `${this.source} ${detail}; the last valid configuration snapshot remains active — ` +
190
+ 'restore a valid autobuild.toml to resume live reload'
191
+ const rejectedRead = `\0${message}`
192
+ const notify = this.rejectedContent !== rejectedRead
193
+ this.rejectedContent = rejectedRead
194
+ return { kind: 'rejected', error: message, notify }
195
+ }
196
+ return this.refresh(content)
197
+ }
198
+
199
+ async refresh(content: string): Promise<ConfigReloadOutcome> {
200
+ if (content === this.acceptedContent) {
201
+ this.rejectedContent = undefined
202
+ return { kind: 'unchanged' }
203
+ }
204
+
205
+ let candidate: Config
206
+ try {
207
+ candidate = parseConfig(content, this.source)
208
+ } catch (error) {
209
+ const message = error instanceof Error ? error.message : String(error)
210
+ const notify = this.rejectedContent !== content
211
+ this.rejectedContent = content
212
+ return { kind: 'rejected', error: message, notify }
213
+ }
214
+
215
+ const config = composeReloadedConfig(this.startup, candidate)
216
+ let resolver: RuntimeResolver
217
+ try {
218
+ resolver = createRuntimeResolver(
219
+ this.runtimes,
220
+ config.roles,
221
+ config.policy.sessionBudgetSeconds,
222
+ )
223
+ } catch (error) {
224
+ const message = error instanceof Error ? error.message : String(error)
225
+ const notify = this.rejectedContent !== content
226
+ this.rejectedContent = content
227
+ return { kind: 'rejected', error: message, notify }
228
+ }
229
+
230
+ const restartRequired = restartRequiredChanges(this.startup, candidate)
231
+ const effectiveChanged = !same(this.snapshot.config, config)
232
+ try {
233
+ await this.publish({ content, effectiveConfig: config, restartRequired, effectiveChanged })
234
+ } catch (error) {
235
+ return {
236
+ kind: 'publication-failed',
237
+ error: error instanceof Error ? error.message : String(error),
238
+ }
239
+ }
240
+
241
+ this.acceptedContent = content
242
+ this.rejectedContent = undefined
243
+ this.snapshot = {
244
+ config,
245
+ resolver,
246
+ revision: this.snapshot.revision + 1,
247
+ }
248
+ return {
249
+ kind: 'adopted',
250
+ snapshot: this.snapshot,
251
+ restartRequired,
252
+ effectiveChanged,
253
+ }
254
+ }
255
+ }
@@ -0,0 +1,98 @@
1
+ /**
2
+ * autobuild.toml loading (SPEC §16.1, D9).
3
+ *
4
+ * Branch-owned worktree config and supplied immutable text are parsed as
5
+ * one-shot inputs: `parseConfig(text)` is the primary API, while
6
+ * `loadConfig(path)` is a disk convenience for scoped CLI and local tooling.
7
+ * The running dispatcher has a separate live-reload context: `LiveConfig`
8
+ * reads the main checkout, retains its last valid snapshot across rejected
9
+ * candidates, and lets each action capture the current snapshot at its boundary.
10
+ */
11
+ import { parse as parseToml, TomlError } from 'smol-toml'
12
+ import type { z } from 'zod'
13
+ import { expandIssues } from '../zod-issues'
14
+ import { configSchema, TOP_LEVEL_SCALARS, TOP_LEVEL_TABLES, type Config } from './schema'
15
+
16
+ /**
17
+ * Config failures are feedback to whoever edits the file: the message carries
18
+ * the source name, the path of every problem, and what would be accepted.
19
+ */
20
+ export class ConfigError extends Error {
21
+ constructor(
22
+ message: string,
23
+ readonly issues?: unknown,
24
+ ) {
25
+ super(message)
26
+ this.name = 'ConfigError'
27
+ }
28
+ }
29
+
30
+ function formatPath(path: ReadonlyArray<PropertyKey>): string {
31
+ if (path.length === 0) return '(top level)'
32
+ return path
33
+ .map((segment, index) =>
34
+ typeof segment === 'number'
35
+ ? `[${segment}]`
36
+ : index === 0
37
+ ? String(segment)
38
+ : `.${String(segment)}`,
39
+ )
40
+ .join('')
41
+ }
42
+
43
+ function describeIssue(issue: z.core.$ZodIssue): string {
44
+ let message = issue.message
45
+ if (issue.code === 'unrecognized_keys' && issue.path.length === 0) {
46
+ if (issue.keys.includes('agent')) {
47
+ message +=
48
+ ' — [agent] was removed; move those fields to [roles.default] ' +
49
+ '(the default entry in [roles])'
50
+ }
51
+ if (issue.keys.includes('server')) {
52
+ message +=
53
+ ' — [server] was removed; arrange any application lifecycle inside the verify command or skill'
54
+ }
55
+ message +=
56
+ ` — known top-level keys: ${TOP_LEVEL_SCALARS.join(', ')}; ` +
57
+ `known tables: ${TOP_LEVEL_TABLES.join(', ')}`
58
+ }
59
+ const removedNeedsServer =
60
+ (issue.code === 'unrecognized_keys' && issue.keys.includes('needsServer')) ||
61
+ message.includes('"needsServer"')
62
+ if (removedNeedsServer) {
63
+ message +=
64
+ " — needsServer was removed; arrange any application lifecycle inside this step's command or skill"
65
+ }
66
+ const path =
67
+ removedNeedsServer && issue.path.at(-1) !== 'needsServer'
68
+ ? [...issue.path, 'needsServer']
69
+ : issue.path
70
+ return ` ${formatPath(path)}: ${message}`
71
+ }
72
+
73
+ export function parseConfig(tomlText: string, source = 'autobuild.toml'): Config {
74
+ let table: unknown
75
+ try {
76
+ table = parseToml(tomlText)
77
+ } catch (error) {
78
+ if (error instanceof TomlError) {
79
+ throw new ConfigError(`${source}: TOML syntax error: ${error.message}`)
80
+ }
81
+ throw error
82
+ }
83
+
84
+ const result = configSchema.safeParse(table)
85
+ if (!result.success) {
86
+ // Expanded for the operator, raw for the payload: the expanded copies carry
87
+ // synthesized `option N of M:` messages, while `ConfigError.issues` should
88
+ // stay what validation actually reported.
89
+ const details = expandIssues(result.error.issues).map(describeIssue).join('\n')
90
+ throw new ConfigError(`${source}: invalid config\n${details}`, result.error.issues)
91
+ }
92
+ return result.data
93
+ }
94
+
95
+ export async function loadConfig(path: string): Promise<Config> {
96
+ const text = await Bun.file(path).text()
97
+ return parseConfig(text, path)
98
+ }