@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 { isAbsolute } from 'node:path'
2
+ import { pathToFileURL } from 'node:url'
3
+ import { ZodError } from 'zod'
4
+ import { distributionRoot } from '../distribution'
5
+ import { expandIssues } from '../zod-issues'
6
+ import {
7
+ parsePluginManifest,
8
+ pluginApiCompatibility,
9
+ PluginApiCompatibilityError,
10
+ type PluginApiCompatibility,
11
+ } from './manifest'
12
+ import { createPluginRegistry, type PluginRegistry, type PluginResolutionKind } from './registry'
13
+
14
+ function reason(error: unknown): string {
15
+ return error instanceof Error ? error.message : String(error)
16
+ }
17
+
18
+ function importUrl(resolved: string): string {
19
+ return resolved.startsWith('file:') ? resolved : pathToFileURL(resolved).href
20
+ }
21
+
22
+ export type PluginLoadStage = 'resolution' | 'evaluation' | 'manifest' | 'registration'
23
+
24
+ /** Which root satisfied a bare package specifier: the consuming repository's
25
+ * installed dependencies, or the Autobuild installation the CLI runs from. */
26
+ export type PluginResolutionSource = 'repository' | 'installation'
27
+
28
+ export interface PluginModuleReport {
29
+ module: string
30
+ resolutionKind: PluginResolutionKind
31
+ resolved?: string
32
+ resolvedFrom?: PluginResolutionSource
33
+ status: 'loaded' | 'failed'
34
+ stage: PluginLoadStage | 'loaded'
35
+ pluginName?: string
36
+ api?: PluginApiCompatibility
37
+ error?: string
38
+ cause?: unknown
39
+ }
40
+
41
+ export interface PluginDiagnosis {
42
+ registry: PluginRegistry
43
+ reports: PluginModuleReport[]
44
+ healthy: boolean
45
+ }
46
+
47
+ export interface PluginLoadOptions {
48
+ /** Root whose installed dependencies satisfy bare package specifiers first. */
49
+ packageRoot?: string
50
+ /** Autobuild installation root tried after `packageRoot`, so an extension
51
+ * installed next to the CLI (`bun add -g @defrex/autobuild-<extension>`)
52
+ * loads without being added to the repository. Defaults to the running
53
+ * distribution's root. */
54
+ installationRoot?: string
55
+ }
56
+
57
+ export function pluginResolutionKind(moduleSpecifier: string): PluginResolutionKind {
58
+ return moduleSpecifier.startsWith('./') ||
59
+ moduleSpecifier.startsWith('../') ||
60
+ moduleSpecifier.startsWith('file:') ||
61
+ isAbsolute(moduleSpecifier) ||
62
+ /^[A-Za-z]:[\\/]/.test(moduleSpecifier)
63
+ ? 'repo-path'
64
+ : 'package'
65
+ }
66
+
67
+ function failed(
68
+ base: Omit<PluginModuleReport, 'status' | 'stage' | 'error'>,
69
+ stage: PluginLoadStage,
70
+ message: string,
71
+ cause?: unknown,
72
+ ): PluginModuleReport {
73
+ return {
74
+ ...base,
75
+ status: 'failed',
76
+ stage,
77
+ error: message,
78
+ ...(cause !== undefined ? { cause } : {}),
79
+ }
80
+ }
81
+
82
+ function manifestIdentity(value: unknown): {
83
+ pluginName?: string
84
+ api?: PluginApiCompatibility
85
+ } {
86
+ if (typeof value !== 'object' || value === null) return {}
87
+ const record = value as Record<string, unknown>
88
+ return {
89
+ ...(typeof record.name === 'string' ? { pluginName: record.name } : {}),
90
+ ...(typeof record.apiVersion === 'string'
91
+ ? { api: pluginApiCompatibility(record.apiVersion) }
92
+ : {}),
93
+ }
94
+ }
95
+
96
+ /** One structured, atomic module attempt shared by fail-fast dispatch loading
97
+ * and exhaustive operator diagnostics. */
98
+ export async function attemptPlugin(
99
+ moduleSpecifier: string,
100
+ repoRoot: string,
101
+ registry: PluginRegistry,
102
+ options: PluginLoadOptions = {},
103
+ ): Promise<PluginModuleReport> {
104
+ const resolutionKind = pluginResolutionKind(moduleSpecifier)
105
+ const initial = { module: moduleSpecifier, resolutionKind }
106
+ let resolved: string
107
+ let resolvedFrom: PluginResolutionSource | undefined
108
+ if (resolutionKind === 'repo-path') {
109
+ try {
110
+ resolved = Bun.resolveSync(moduleSpecifier, repoRoot)
111
+ } catch (error) {
112
+ return failed(
113
+ initial,
114
+ 'resolution',
115
+ `plugin module "${moduleSpecifier}" could not be resolved from repository "${repoRoot}": ${reason(error)}`,
116
+ error,
117
+ )
118
+ }
119
+ } else {
120
+ // A repository copy wins over an installed one: the consuming checkout's
121
+ // dependencies are tried first, then the Autobuild installation itself.
122
+ const candidates: Array<{ source: PluginResolutionSource; root: string }> = [
123
+ { source: 'repository', root: options.packageRoot ?? repoRoot },
124
+ ]
125
+ const installationRoot = options.installationRoot ?? distributionRoot()
126
+ if (installationRoot !== candidates[0]?.root) {
127
+ candidates.push({ source: 'installation', root: installationRoot })
128
+ }
129
+ const failures: string[] = []
130
+ let found: { path: string; source: PluginResolutionSource } | undefined
131
+ let lastError: unknown
132
+ for (const candidate of candidates) {
133
+ try {
134
+ found = { path: Bun.resolveSync(moduleSpecifier, candidate.root), source: candidate.source }
135
+ break
136
+ } catch (error) {
137
+ lastError = error
138
+ failures.push(`${candidate.source} "${candidate.root}"`)
139
+ }
140
+ }
141
+ if (found === undefined) {
142
+ return failed(
143
+ initial,
144
+ 'resolution',
145
+ `plugin module "${moduleSpecifier}" could not be resolved from ${failures.join(' or ')}: ${reason(lastError)}`,
146
+ lastError,
147
+ )
148
+ }
149
+ resolved = found.path
150
+ resolvedFrom = found.source
151
+ }
152
+
153
+ const located = { ...initial, resolved, ...(resolvedFrom !== undefined ? { resolvedFrom } : {}) }
154
+ let namespace: Record<string, unknown>
155
+ try {
156
+ namespace = (await import(importUrl(resolved))) as Record<string, unknown>
157
+ } catch (error) {
158
+ return failed(
159
+ located,
160
+ 'evaluation',
161
+ `plugin module "${moduleSpecifier}" failed while evaluating: ${reason(error)}`,
162
+ error,
163
+ )
164
+ }
165
+
166
+ if (!Object.hasOwn(namespace, 'default')) {
167
+ return failed(
168
+ located,
169
+ 'manifest',
170
+ `plugin module "${moduleSpecifier}" has no default export; default-export an AutobuildPluginManifest`,
171
+ )
172
+ }
173
+
174
+ const identity = manifestIdentity(namespace.default)
175
+ let manifest: ReturnType<typeof parsePluginManifest>
176
+ try {
177
+ manifest = parsePluginManifest(namespace.default)
178
+ } catch (error) {
179
+ const detail =
180
+ error instanceof ZodError
181
+ ? expandIssues(error.issues)
182
+ .map((issue) => `${issue.path.join('.') || '(manifest)'}: ${issue.message}`)
183
+ .join('; ')
184
+ : reason(error)
185
+ const api = error instanceof PluginApiCompatibilityError ? error.compatibility : identity.api
186
+ return failed(
187
+ {
188
+ ...located,
189
+ ...(identity.pluginName !== undefined ? { pluginName: identity.pluginName } : {}),
190
+ ...(api !== undefined ? { api } : {}),
191
+ },
192
+ 'manifest',
193
+ `plugin module "${moduleSpecifier}" has an invalid manifest: ${detail}`,
194
+ error,
195
+ )
196
+ }
197
+
198
+ const api = pluginApiCompatibility(manifest.apiVersion)
199
+ const identified = {
200
+ ...located,
201
+ pluginName: manifest.name,
202
+ api,
203
+ }
204
+ try {
205
+ registry.register(manifest, {
206
+ module: moduleSpecifier,
207
+ resolved,
208
+ resolutionKind,
209
+ })
210
+ } catch (error) {
211
+ return failed(
212
+ identified,
213
+ 'registration',
214
+ `plugin module "${moduleSpecifier}" (plugin "${manifest.name}") could not register: ${reason(error)}`,
215
+ error,
216
+ )
217
+ }
218
+
219
+ return {
220
+ ...identified,
221
+ status: 'loaded',
222
+ stage: 'loaded',
223
+ }
224
+ }
225
+
226
+ /** Exhaustively attempt configured modules in declaration order. Failed
227
+ * modules leave no registrations; later healthy modules still load. */
228
+ export async function diagnosePlugins(
229
+ modules: readonly string[],
230
+ repoRoot: string,
231
+ options: PluginLoadOptions = {},
232
+ registry: PluginRegistry = createPluginRegistry(),
233
+ ): Promise<PluginDiagnosis> {
234
+ const reports: PluginModuleReport[] = []
235
+ for (const moduleSpecifier of modules) {
236
+ reports.push(await attemptPlugin(moduleSpecifier, repoRoot, registry, options))
237
+ }
238
+ return {
239
+ registry,
240
+ reports,
241
+ healthy: reports.every((report) => report.status === 'loaded'),
242
+ }
243
+ }
244
+
245
+ /** Dispatch compatibility wrapper: preserve first-failure, fail-closed startup. */
246
+ export async function loadPlugins(
247
+ modules: readonly string[],
248
+ repoRoot: string,
249
+ options: PluginLoadOptions = {},
250
+ registry: PluginRegistry = createPluginRegistry(),
251
+ ): Promise<PluginRegistry> {
252
+ for (const moduleSpecifier of modules) {
253
+ const report = await attemptPlugin(moduleSpecifier, repoRoot, registry, options)
254
+ if (report.status === 'failed') {
255
+ throw new Error(report.error, { cause: report.cause })
256
+ }
257
+ }
258
+ return registry
259
+ }
@@ -0,0 +1,236 @@
1
+ import semver from 'semver'
2
+ import { z } from 'zod'
3
+ import { openMap } from '../open-map'
4
+ import { forwardIssues } from '../zod-issues'
5
+ import type { Forge, TicketSource, WorkspaceProvider } from '../ports/types'
6
+ import type { AgentRunnerContractFactory } from '../ports/runner/contract'
7
+ import type { RuntimeRegistration } from '../ports/runner/runtime'
8
+ import type { TicketSourceContractFactory } from '../ports/tickets/contract'
9
+ import type { ForgeContractFactory } from '../ports/forge/contract'
10
+ import type { WorkspaceProviderContractFactory } from '../ports/workspace/contract'
11
+
12
+ /** Version of the in-process plugin contract exposed by `@defrex/autobuild/plugin-sdk`. */
13
+ export const PLUGIN_API_VERSION = '1.5.0' as const
14
+
15
+ /** Context supplied when a registered adapter or contract fixture is selected. */
16
+ export interface PluginFactoryContext<Config = Record<string, unknown>> {
17
+ /** Adapter-specific declarative configuration. */
18
+ config: Readonly<Config>
19
+ /** The dispatch process environment; secrets stay out of autobuild.toml. */
20
+ env: Readonly<Record<string, string | undefined>>
21
+ /** Absolute root of the repository that enabled the plugin. */
22
+ repoRoot: string
23
+ }
24
+
25
+ export type PluginFactory<Adapter, Config = Record<string, unknown>> = {
26
+ /** The method-index form intentionally makes this callback bivariant, so a
27
+ * plugin can retain its concrete config type across the erased manifest. */
28
+ invoke(context: PluginFactoryContext<Config>): Adapter | Promise<Adapter>
29
+ }['invoke']
30
+
31
+ export type TicketSourcePluginFactory<Config = Record<string, unknown>> = PluginFactory<
32
+ TicketSource,
33
+ Config
34
+ >
35
+
36
+ /** Optional host-enforced metadata for a ticket source. Bare factories remain
37
+ * valid for plugin API 1.0 compatibility. */
38
+ export interface TicketSourcePluginDescriptor<Config = Record<string, unknown>> {
39
+ factory: TicketSourcePluginFactory<Config>
40
+ /** Environment variables that must be nonempty before the factory runs. */
41
+ requiredEnv?: readonly string[]
42
+ /** Optional shared-suite certification fixture. */
43
+ contract?: PluginContractDescriptor<TicketSourceContractFactory>
44
+ }
45
+
46
+ export type TicketSourcePluginRegistration<Config = Record<string, unknown>> =
47
+ | TicketSourcePluginFactory<Config>
48
+ | TicketSourcePluginDescriptor<Config>
49
+
50
+ export type AgentRuntimePluginFactory<Config = Record<string, unknown>> = PluginFactory<
51
+ RuntimeRegistration,
52
+ Config
53
+ >
54
+ export type WorkspaceProviderPluginFactory<Config = Record<string, unknown>> = PluginFactory<
55
+ WorkspaceProvider,
56
+ Config
57
+ >
58
+ export type ForgePluginFactory<Config = Record<string, unknown>> = PluginFactory<Forge, Config>
59
+
60
+ export interface PluginContractDescriptor<ContractFactory, Config = Record<string, unknown>> {
61
+ /** Creates the unchanged shared-suite harness factory when the test verb runs. */
62
+ factory: PluginFactory<ContractFactory, Config>
63
+ /** True when creating/running the fixture touches a live external system. */
64
+ live?: boolean
65
+ }
66
+
67
+ export type PluginAdapterRegistration<AdapterFactory, ContractFactory> =
68
+ | AdapterFactory
69
+ | {
70
+ factory: AdapterFactory
71
+ contract?: PluginContractDescriptor<ContractFactory>
72
+ }
73
+
74
+ export type AgentRuntimePluginRegistration = PluginAdapterRegistration<
75
+ AgentRuntimePluginFactory,
76
+ AgentRunnerContractFactory
77
+ >
78
+ export type WorkspaceProviderPluginRegistration = PluginAdapterRegistration<
79
+ WorkspaceProviderPluginFactory,
80
+ WorkspaceProviderContractFactory
81
+ >
82
+ export type ForgePluginRegistration = PluginAdapterRegistration<
83
+ ForgePluginFactory,
84
+ ForgeContractFactory
85
+ >
86
+
87
+ export interface AutobuildPluginManifest {
88
+ /** Diagnostic/ownership identity. It need not equal the npm package name. */
89
+ name: string
90
+ /** Semver range of plugin API versions accepted by this plugin. */
91
+ apiVersion: string
92
+ ticketSources?: Record<string, TicketSourcePluginRegistration>
93
+ agentRuntimes?: Record<string, AgentRuntimePluginRegistration>
94
+ workspaceProviders?: Record<string, WorkspaceProviderPluginRegistration>
95
+ forges?: Record<string, ForgePluginRegistration>
96
+ }
97
+
98
+ export interface PluginApiCompatibility {
99
+ declaredRange: string
100
+ hostVersion: typeof PLUGIN_API_VERSION
101
+ status: 'compatible' | 'incompatible' | 'invalid'
102
+ }
103
+
104
+ export class PluginApiCompatibilityError extends Error {
105
+ constructor(
106
+ readonly pluginName: string,
107
+ readonly compatibility: PluginApiCompatibility,
108
+ ) {
109
+ const { declaredRange, hostVersion, status } = compatibility
110
+ super(
111
+ status === 'invalid'
112
+ ? `plugin "${pluginName}" declares invalid plugin API range "${declaredRange}"; host provides ${hostVersion}`
113
+ : `plugin "${pluginName}" requires plugin API "${declaredRange}", but host provides ${hostVersion}`,
114
+ )
115
+ this.name = 'PluginApiCompatibilityError'
116
+ }
117
+ }
118
+
119
+ export function pluginApiCompatibility(declaredRange: string): PluginApiCompatibility {
120
+ const valid = semver.validRange(declaredRange)
121
+ return {
122
+ declaredRange,
123
+ hostVersion: PLUGIN_API_VERSION,
124
+ status:
125
+ valid === null
126
+ ? 'invalid'
127
+ : semver.satisfies(PLUGIN_API_VERSION, valid)
128
+ ? 'compatible'
129
+ : 'incompatible',
130
+ }
131
+ }
132
+
133
+ const nonblank = z.string().refine((value) => value.trim().length > 0, 'must be a nonblank string')
134
+
135
+ const factorySchema = z.custom<PluginFactory<unknown>>(
136
+ (value) => typeof value === 'function',
137
+ 'must be a factory function',
138
+ )
139
+
140
+ const contractSchema = z.strictObject({
141
+ factory: factorySchema,
142
+ live: z.boolean().optional(),
143
+ })
144
+ const registrationObjectSchema = z.strictObject({
145
+ factory: factorySchema,
146
+ contract: contractSchema.optional(),
147
+ })
148
+ const registrationSchema = z.unknown().transform((value, ctx) => {
149
+ if (typeof value === 'function') return value
150
+ const parsed = registrationObjectSchema.safeParse(value)
151
+ if (parsed.success) return parsed.data
152
+ // No prefix: this transform already sits at the value's own path.
153
+ forwardIssues(parsed.error.issues, ctx)
154
+ return z.NEVER
155
+ })
156
+
157
+ /**
158
+ * Adapter maps are keyed by names the PLUGIN AUTHOR chooses, so they go through
159
+ * the same key-preservation contract as autobuild.toml's open maps: a declared
160
+ * name reaches the registry verbatim, `__proto__` included, instead of being
161
+ * lost to `z.record`'s assignment-built result. A dropped registration here is
162
+ * pure misdirection — the operator selecting that adapter is told it is not
163
+ * registered, and the list of available adapters omits the name their manifest
164
+ * plainly declares.
165
+ *
166
+ * Names are `nonblank` rather than merely nonempty: an adapter is addressed from
167
+ * configuration by name, and a name made only of whitespace cannot be.
168
+ */
169
+ const ADAPTER_MAP = {
170
+ keys: 'nonblank',
171
+ shape: 'an object of named adapter registrations',
172
+ } as const
173
+ const registrationMap = (label: string) => openMap(label, registrationSchema, ADAPTER_MAP)
174
+
175
+ const requiredEnvSchema = z.array(nonblank).superRefine((names, ctx) => {
176
+ const seen = new Set<string>()
177
+ names.forEach((name, index) => {
178
+ if (seen.has(name)) {
179
+ ctx.addIssue({
180
+ code: 'custom',
181
+ path: [index],
182
+ message: `environment variable "${name}" is declared more than once`,
183
+ })
184
+ }
185
+ seen.add(name)
186
+ })
187
+ })
188
+
189
+ const ticketSourceDescriptorSchema = z.strictObject({
190
+ factory: factorySchema,
191
+ requiredEnv: requiredEnvSchema.optional(),
192
+ contract: contractSchema.optional(),
193
+ })
194
+
195
+ /**
196
+ * The ticket-source counterpart of `registrationSchema`. A `z.union` would be
197
+ * the obvious spelling, and the boundary would now carry its branch detail
198
+ * through — `openMap` forwards issues verbatim and the loader expands a union's
199
+ * branches, so nothing is dropped either way. This stays a transform for what it
200
+ * REPORTS, not for what a union would lose: once the value is known not to be a
201
+ * function, committing to the descriptor branch says
202
+ * `ticketSources.acme.factory: must be a factory function`, where a union offers
203
+ * an alternatives list that includes the already-rejected function branch. It
204
+ * also matches its three sibling maps. Do not copy this shape as a workaround —
205
+ * there is nothing left here to work around.
206
+ */
207
+ const ticketSourceRegistrationSchema = z.unknown().transform((value, ctx) => {
208
+ if (typeof value === 'function') return value
209
+ const parsed = ticketSourceDescriptorSchema.safeParse(value)
210
+ if (parsed.success) return parsed.data
211
+ forwardIssues(parsed.error.issues, ctx)
212
+ return z.NEVER
213
+ })
214
+
215
+ /** Strict runtime contract for a plugin module's default export.
216
+ *
217
+ * `.optional()` stays outermost, so an undeclared port remains `undefined`
218
+ * rather than becoming `openMap`'s prefaulted `{}`. */
219
+ export const pluginManifestSchema = z.strictObject({
220
+ name: nonblank,
221
+ apiVersion: nonblank,
222
+ ticketSources: openMap('ticketSources', ticketSourceRegistrationSchema, ADAPTER_MAP).optional(),
223
+ agentRuntimes: registrationMap('agentRuntimes').optional(),
224
+ workspaceProviders: registrationMap('workspaceProviders').optional(),
225
+ forges: registrationMap('forges').optional(),
226
+ })
227
+
228
+ /** Validate shape and API compatibility before any registration is committed. */
229
+ export function parsePluginManifest(value: unknown): AutobuildPluginManifest {
230
+ const parsed = pluginManifestSchema.parse(value) as AutobuildPluginManifest
231
+ const compatibility = pluginApiCompatibility(parsed.apiVersion)
232
+ if (compatibility.status !== 'compatible') {
233
+ throw new PluginApiCompatibilityError(parsed.name, compatibility)
234
+ }
235
+ return parsed
236
+ }