@defrex/autobuild 0.8.0 → 0.10.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.
- package/README.md +1 -1
- package/SPEC.md +323 -26
- package/bin/ab-build-runner.ts +8 -2
- package/docs/architecture.md +166 -14
- package/docs/assets/headline-wide.png +0 -0
- package/docs/configuration.md +175 -28
- package/docs/hosted-dispatcher.md +69 -13
- package/docs/operator-api.md +2 -2
- package/docs/remote-store-protocol.md +204 -16
- package/docs/remote-ticket-protocol.md +1 -1
- package/docs/setup.md +12 -9
- package/docs/website.md +14 -0
- package/package.json +45 -23
- package/packages/core/src/cli/artifact.ts +130 -6
- package/packages/core/src/cli/binary.ts +9 -1
- package/packages/core/src/cli/build-control.ts +110 -13
- package/packages/core/src/cli/context.ts +46 -0
- package/packages/core/src/cli/dashboard/facts.ts +215 -0
- package/packages/core/src/cli/dashboard/frame-parse.ts +223 -0
- package/packages/core/src/cli/dashboard/model.ts +141 -153
- package/packages/core/src/cli/dashboard/poll.ts +76 -12
- package/packages/core/src/cli/dashboard/render.ts +34 -5
- package/packages/core/src/cli/dispatch-frontend.ts +54 -5
- package/packages/core/src/cli/dispatch-process.ts +1 -13
- package/packages/core/src/cli/dispatch.ts +392 -75
- package/packages/core/src/cli/harvest.ts +122 -8
- package/packages/core/src/cli/help.ts +62 -9
- package/packages/core/src/cli/init-readiness-remote.ts +110 -0
- package/packages/core/src/cli/init-readiness-shared.ts +72 -0
- package/packages/core/src/cli/init-validation.ts +266 -222
- package/packages/core/src/cli/installation.ts +50 -8
- package/packages/core/src/cli/main.ts +70 -11
- package/packages/core/src/cli/mcp.ts +23 -1
- package/packages/core/src/cli/plugin.ts +7 -4
- package/packages/core/src/cli/remote-poll.ts +277 -0
- package/packages/core/src/cli/repo-state.ts +22 -0
- package/packages/core/src/cli/repository-status.ts +39 -1
- package/packages/core/src/cli/self-update.ts +77 -2
- package/packages/core/src/cli/status.ts +150 -28
- package/packages/core/src/cli/terminals.ts +2 -1
- package/packages/core/src/cli/ticket-assets.ts +247 -0
- package/packages/core/src/cli/ticket.ts +453 -8
- package/packages/core/src/cli/wait.ts +157 -41
- package/packages/core/src/cli/watch.ts +138 -168
- package/packages/core/src/config/live.ts +23 -0
- package/packages/core/src/config/pipeline-source.ts +388 -0
- package/packages/core/src/config/schema.ts +251 -19
- package/packages/core/src/events/catalog.ts +1 -0
- package/packages/core/src/events/globs.ts +47 -0
- package/packages/core/src/events/payloads.ts +52 -2
- package/packages/core/src/events/repository.ts +52 -1
- package/packages/core/src/events/sessions.ts +69 -10
- package/packages/core/src/events/workspace-remote.ts +12 -0
- package/packages/core/src/integration/harness.ts +115 -2
- package/packages/core/src/kernel/__snapshots__/reducer-registry.test.ts.snap +1369 -0
- package/packages/core/src/kernel/auto-merge-default.ts +157 -0
- package/packages/core/src/kernel/auto-merge.ts +99 -22
- package/packages/core/src/kernel/dispatch-settings.ts +42 -17
- package/packages/core/src/kernel/dispatch-status.ts +23 -0
- package/packages/core/src/kernel/engine.ts +9 -438
- package/packages/core/src/kernel/generators/build-log.ts +259 -0
- package/packages/core/src/kernel/generators/repository-journals.ts +206 -0
- package/packages/core/src/kernel/generators/session-log.ts +72 -0
- package/packages/core/src/kernel/harvest.ts +62 -18
- package/packages/core/src/kernel/incremental-contract.ts +75 -0
- package/packages/core/src/kernel/incremental-fixtures.ts +211 -0
- package/packages/core/src/kernel/incremental.ts +53 -0
- package/packages/core/src/kernel/log-index.ts +620 -0
- package/packages/core/src/kernel/phases.ts +16 -2
- package/packages/core/src/kernel/pr-attachments.ts +164 -46
- package/packages/core/src/kernel/reducer-registry.ts +164 -0
- package/packages/core/src/kernel/reducer.ts +254 -73
- package/packages/core/src/operator/annotations.ts +6 -0
- package/packages/core/src/operator/control.ts +19 -5
- package/packages/core/src/operator/index.ts +71 -6
- package/packages/core/src/operator/query.ts +253 -24
- package/packages/core/src/operator/read-view.ts +98 -0
- package/packages/core/src/operator/registry.ts +55 -10
- package/packages/core/src/operator/requests.ts +9 -6
- package/packages/core/src/operator/sandbox-host.ts +140 -0
- package/packages/core/src/operator/sandbox.ts +247 -14
- package/packages/core/src/operator/ticket-source-backend.ts +47 -0
- package/packages/core/src/orchestrator/approvals.ts +115 -0
- package/packages/core/src/orchestrator/conversation.ts +167 -0
- package/packages/core/src/orchestrator/model.ts +86 -0
- package/packages/core/src/orchestrator/prompt.ts +61 -0
- package/packages/core/src/orchestrator/tools.ts +117 -0
- package/packages/core/src/orchestrator/turn-runner.ts +457 -0
- package/packages/core/src/plugin-sdk/index.ts +51 -1
- package/packages/core/src/plugins/load.ts +135 -7
- package/packages/core/src/plugins/manifest.ts +183 -10
- package/packages/core/src/plugins/registry.ts +73 -4
- package/packages/core/src/ports/forge/create.ts +1 -1
- package/packages/core/src/ports/forge/fake.ts +31 -0
- package/packages/core/src/ports/forge/github-transport.ts +13 -2
- package/packages/core/src/ports/forge/github.ts +13 -2
- package/packages/core/src/ports/forge/local-git.ts +31 -1
- package/packages/core/src/ports/runner/claude.ts +2 -2
- package/packages/core/src/ports/runner/codex.ts +6 -2
- package/packages/core/src/ports/runner/fake.ts +7 -1
- package/packages/core/src/ports/runner/pi.ts +8 -6
- package/packages/core/src/ports/runner/provider-error.ts +13 -3
- package/packages/core/src/ports/tickets/fake.ts +5 -1
- package/packages/core/src/ports/tickets/file.ts +169 -48
- package/packages/core/src/ports/tickets/{remote-index.ts → hosted-index.ts} +3 -2
- package/packages/core/src/ports/tickets/linear.ts +33 -1
- package/packages/core/src/ports/tickets/operations.ts +1 -1
- package/packages/core/src/ports/tickets/remote.ts +8 -1
- package/packages/core/src/ports/types.ts +49 -3
- package/packages/core/src/ports/workspace/builtin-capabilities.ts +114 -0
- package/packages/core/src/ports/workspace/create.ts +74 -39
- package/packages/core/src/ports/workspace/distribution-archive.ts +171 -12
- package/packages/core/src/ports/workspace/fake.ts +169 -4
- package/packages/core/src/ports/workspace/git-worktree.ts +246 -26
- package/packages/core/src/ports/workspace/github-origin.ts +36 -0
- package/packages/core/src/ports/workspace/operator-sandbox.ts +7 -1
- package/packages/core/src/ports/workspace/provider-capabilities.ts +171 -0
- package/packages/core/src/ports/workspace/vercel-capabilities.ts +110 -0
- package/packages/core/src/ports/workspace/vercel-sandbox.ts +142 -114
- package/packages/core/src/processes/build-child.ts +17 -2
- package/packages/core/src/processes/build-execution-state.ts +68 -15
- package/packages/core/src/processes/build-runner.ts +238 -93
- package/packages/core/src/processes/dispatcher-selectors.ts +123 -0
- package/packages/core/src/processes/dispatcher.ts +501 -201
- package/packages/core/src/processes/execution-settlement.ts +72 -39
- package/packages/core/src/processes/harvest-child.ts +5 -0
- package/packages/core/src/processes/harvest-execution-state.ts +41 -29
- package/packages/core/src/processes/harvest-runner.ts +66 -19
- package/packages/core/src/processes/harvest.ts +196 -24
- package/packages/core/src/processes/infrastructure-failure-budget.ts +31 -18
- package/packages/core/src/processes/orchestrator-tick.ts +360 -0
- package/packages/core/src/processes/publication-state.ts +164 -52
- package/packages/core/src/processes/repo-view.ts +1436 -0
- package/packages/core/src/processes/sandbox-state.ts +36 -2
- package/packages/core/src/processes/tick-state.ts +33 -0
- package/packages/core/src/store/adapter.ts +41 -0
- package/packages/core/src/store/build-scope.ts +90 -0
- package/packages/core/src/store/contract.ts +1205 -0
- package/packages/core/src/store/digest.ts +102 -0
- package/packages/core/src/store/local/schema.ts +46 -2
- package/packages/core/src/store/local/store.ts +445 -4
- package/packages/core/src/store/memory.ts +246 -0
- package/packages/core/src/store/new-build.ts +17 -0
- package/packages/core/src/store/phase-session.ts +94 -0
- package/packages/core/src/store/remote/client.ts +310 -2
- package/packages/core/src/store/remote/index.ts +14 -2
- package/packages/core/src/store/remote/protocol.ts +115 -12
- package/packages/core/src/store/remote/version.ts +6 -2
- package/packages/core/src/store/repo-state-events.ts +205 -0
- package/packages/core/src/store/retention.ts +7 -0
- package/packages/core/src/store/session-handle.ts +94 -0
- package/packages/core/src/{sessions/reducer.ts → store/session-reducer.ts} +79 -19
- package/packages/core/src/store/snapshots.ts +88 -0
- package/packages/core/src/store/streams/session-writer.ts +42 -2
- package/packages/core/src/store/streams/wait.ts +4 -5
- package/packages/core/src/store/ticket-assets.ts +458 -0
- package/packages/core/src/store/types.ts +160 -1
- package/packages/core/src/testing/index.ts +59 -0
- package/skills/code-review/SKILL.md +5 -1
- package/skills/guide/SKILL.md +152 -40
- package/skills/guide/references/operator-api.md +2 -2
- package/skills/guide/references/plugin-authoring.md +17 -0
- package/skills/guide/references/remote-store-protocol.md +204 -16
- package/skills/guide/references/remote-ticket-protocol.md +1 -1
- package/skills/guide/references/setup.md +12 -9
- package/skills/implement/SKILL.md +3 -0
- package/skills/operate/SKILL.md +126 -0
- package/skills/plan/SKILL.md +4 -0
- package/skills/plan-review/SKILL.md +5 -1
- package/skills/spec/SKILL.md +46 -3
- package/skills/tickets/SKILL.md +57 -5
- package/packages/core/src/cli/dashboard/frame-image.ts +0 -373
- package/packages/core/src/cli/store-opening.contract.ts +0 -289
- package/packages/core/src/cli/testkit.ts +0 -194
- package/packages/core/src/markdown.ts +0 -67
- package/packages/core/src/operator/client.ts +0 -311
- package/packages/core/src/operator/server.ts +0 -616
- package/packages/core/src/operator/sessions.ts +0 -223
- package/packages/core/src/ports/runner/live-contract-fixture.ts +0 -176
- package/packages/core/src/ports/tickets/remote-server.ts +0 -185
- package/packages/core/src/store/remote/server.ts +0 -923
- package/packages/core/src/testing/store-failures.ts +0 -32
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { existsSync } from 'node:fs'
|
|
2
|
+
import { dirname, isAbsolute, join, resolve } from 'node:path'
|
|
2
3
|
import { pathToFileURL } from 'node:url'
|
|
3
4
|
import { ZodError } from 'zod'
|
|
4
5
|
import { distributionRoot } from '../distribution'
|
|
@@ -7,6 +8,7 @@ import {
|
|
|
7
8
|
parsePluginManifest,
|
|
8
9
|
pluginApiCompatibility,
|
|
9
10
|
PluginApiCompatibilityError,
|
|
11
|
+
type AutobuildPluginManifest,
|
|
10
12
|
type PluginApiCompatibility,
|
|
11
13
|
} from './manifest'
|
|
12
14
|
import { createPluginRegistry, type PluginRegistry, type PluginResolutionKind } from './registry'
|
|
@@ -15,6 +17,49 @@ function reason(error: unknown): string {
|
|
|
15
17
|
return error instanceof Error ? error.message : String(error)
|
|
16
18
|
}
|
|
17
19
|
|
|
20
|
+
/** The package directory a bare specifier names: `@scope/name/sub` names
|
|
21
|
+
* `@scope/name`, `name/sub` names `name`. Subpath exports resolve inside the
|
|
22
|
+
* package, so the on-disk gate below must check the package, not the
|
|
23
|
+
* subpath. */
|
|
24
|
+
function packageNameOf(specifier: string): string {
|
|
25
|
+
const segments = specifier.split('/')
|
|
26
|
+
return specifier.startsWith('@') ? segments.slice(0, 2).join('/') : (segments[0] ?? specifier)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** First directory of `root`'s ancestor chain (inclusive) under which
|
|
30
|
+
* `node_modules/<specifier>` exists on disk, or undefined. Pure filesystem
|
|
31
|
+
* syscalls — deliberately no Bun APIs: `Bun.resolveSync` on a bare specifier
|
|
32
|
+
* that no `node_modules` lookup satisfies hands the specifier to Bun's
|
|
33
|
+
* runtime auto-installer, which fetches from the network and writes a cache;
|
|
34
|
+
* on a read-only filesystem (the hosted dispatcher function) that is a
|
|
35
|
+
* fatal, uncatchable process exit (AUT-587). Gating on a disk hit keeps the
|
|
36
|
+
* installer unreachable: per the planner-verified Bun 1.4 behavior, a
|
|
37
|
+
* resolver call whose base chain holds the package on disk resolves from
|
|
38
|
+
* disk and never touches the installer.
|
|
39
|
+
*
|
|
40
|
+
* Termination (AUT-597): `root` is resolved to an absolute path before the
|
|
41
|
+
* walk begins, so a relative candidate root cannot make `dirname` a fixed
|
|
42
|
+
* point and spin the loop forever. The `parent === current` guard backstops
|
|
43
|
+
* the fixed point (`dirname('/') === '/'`) should the loop's start point
|
|
44
|
+
* ever change.
|
|
45
|
+
*
|
|
46
|
+
* Candidate-root provenance (AUT-604): production callers never supply a
|
|
47
|
+
* non-filesystem token here. Origin mode does not pass
|
|
48
|
+
* `'<hosted-dispatcher>'` — `abDispatch` normalizes `targetRepo` to the
|
|
49
|
+
* per-origin scratch root before `loadPlugins` runs — so candidate roots
|
|
50
|
+
* are always real filesystem roots; a relative one reaching this walk is a
|
|
51
|
+
* caller bug with a fail-closed outcome, not a supported input. */
|
|
52
|
+
function diskNodeModulesDirectory(specifier: string, root: string): string | undefined {
|
|
53
|
+
const pkg = packageNameOf(specifier)
|
|
54
|
+
let current = resolve(root)
|
|
55
|
+
while (true) {
|
|
56
|
+
if (existsSync(join(current, 'node_modules', pkg))) return current
|
|
57
|
+
const parent = dirname(current)
|
|
58
|
+
if (parent === current) return undefined
|
|
59
|
+
current = parent
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
18
63
|
function importUrl(resolved: string): string {
|
|
19
64
|
return resolved.startsWith('file:') ? resolved : pathToFileURL(resolved).href
|
|
20
65
|
}
|
|
@@ -30,12 +75,15 @@ export interface PluginModuleReport {
|
|
|
30
75
|
resolutionKind: PluginResolutionKind
|
|
31
76
|
resolved?: string
|
|
32
77
|
resolvedFrom?: PluginResolutionSource
|
|
33
|
-
status: 'loaded' | 'failed'
|
|
78
|
+
status: 'loaded' | 'failed' | 'skipped'
|
|
34
79
|
stage: PluginLoadStage | 'loaded'
|
|
35
80
|
pluginName?: string
|
|
36
81
|
api?: PluginApiCompatibility
|
|
37
82
|
error?: string
|
|
38
83
|
cause?: unknown
|
|
84
|
+
/** One-line explanation for a `skipped` report, emitted through the
|
|
85
|
+
* loader's notice channel. Undefined for `loaded` and `failed`. */
|
|
86
|
+
notice?: string
|
|
39
87
|
}
|
|
40
88
|
|
|
41
89
|
export interface PluginDiagnosis {
|
|
@@ -52,6 +100,14 @@ export interface PluginLoadOptions {
|
|
|
52
100
|
* loads without being added to the repository. Defaults to the running
|
|
53
101
|
* distribution's root. */
|
|
54
102
|
installationRoot?: string
|
|
103
|
+
/** Guest context (AUT-517): a package-kind specifier that cannot be
|
|
104
|
+
* resolved from either candidate is skipped with a notice instead of
|
|
105
|
+
* failing the process, because guests never construct workspace providers
|
|
106
|
+
* and may legitimately be handed a provider plugin they need not load.
|
|
107
|
+
* Repo-path specifiers and every post-resolution failure stay fail-closed. */
|
|
108
|
+
guest?: boolean
|
|
109
|
+
/** Notice channel for skipped loads. Defaults to one line on stderr. */
|
|
110
|
+
onNotice?: (line: string) => void
|
|
55
111
|
}
|
|
56
112
|
|
|
57
113
|
export function pluginResolutionKind(moduleSpecifier: string): PluginResolutionKind {
|
|
@@ -79,6 +135,14 @@ function failed(
|
|
|
79
135
|
}
|
|
80
136
|
}
|
|
81
137
|
|
|
138
|
+
/** True when the manifest declares no registrations outside the
|
|
139
|
+
* workspace-provider map. */
|
|
140
|
+
function declaresOnlyWorkspaceProviderRegistrations(manifest: AutobuildPluginManifest): boolean {
|
|
141
|
+
const empty = (registrations: Record<string, unknown> | undefined): boolean =>
|
|
142
|
+
registrations === undefined || Object.keys(registrations).length === 0
|
|
143
|
+
return empty(manifest.ticketSources) && empty(manifest.agentRuntimes) && empty(manifest.forges)
|
|
144
|
+
}
|
|
145
|
+
|
|
82
146
|
function manifestIdentity(value: unknown): {
|
|
83
147
|
pluginName?: string
|
|
84
148
|
api?: PluginApiCompatibility
|
|
@@ -130,8 +194,17 @@ export async function attemptPlugin(
|
|
|
130
194
|
let found: { path: string; source: PluginResolutionSource } | undefined
|
|
131
195
|
let lastError: unknown
|
|
132
196
|
for (const candidate of candidates) {
|
|
197
|
+
// Disk-first gate (AUT-587): only a candidate whose `node_modules`
|
|
198
|
+
// chain holds the package on disk reaches the resolver. Bun's runtime
|
|
199
|
+
// auto-installer fires exactly when disk resolution fails, so a miss
|
|
200
|
+
// here must be recorded without calling `Bun.resolveSync` at all.
|
|
201
|
+
const diskRoot = diskNodeModulesDirectory(moduleSpecifier, candidate.root)
|
|
202
|
+
if (diskRoot === undefined) {
|
|
203
|
+
failures.push(`${candidate.source} "${candidate.root}"`)
|
|
204
|
+
continue
|
|
205
|
+
}
|
|
133
206
|
try {
|
|
134
|
-
found = { path: Bun.resolveSync(moduleSpecifier,
|
|
207
|
+
found = { path: Bun.resolveSync(moduleSpecifier, diskRoot), source: candidate.source }
|
|
135
208
|
break
|
|
136
209
|
} catch (error) {
|
|
137
210
|
lastError = error
|
|
@@ -139,10 +212,26 @@ export async function attemptPlugin(
|
|
|
139
212
|
}
|
|
140
213
|
}
|
|
141
214
|
if (found === undefined) {
|
|
215
|
+
if (options.guest === true) {
|
|
216
|
+
return {
|
|
217
|
+
...initial,
|
|
218
|
+
status: 'skipped',
|
|
219
|
+
stage: 'resolution',
|
|
220
|
+
notice:
|
|
221
|
+
`plugin module "${moduleSpecifier}" could not be resolved from ${failures.join(' or ')}; ` +
|
|
222
|
+
'guests never construct workspace providers, so the provider plugin is skipped here',
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
// A disk miss never reached the resolver, so there is no Bun error to
|
|
226
|
+
// quote — say directly what the gate established instead.
|
|
227
|
+
const detail =
|
|
228
|
+
lastError !== undefined
|
|
229
|
+
? reason(lastError)
|
|
230
|
+
: `no "node_modules/${packageNameOf(moduleSpecifier)}" entry exists on disk under any candidate root`
|
|
142
231
|
return failed(
|
|
143
232
|
initial,
|
|
144
233
|
'resolution',
|
|
145
|
-
`plugin module "${moduleSpecifier}" could not be resolved from ${failures.join(' or ')}: ${
|
|
234
|
+
`plugin module "${moduleSpecifier}" could not be resolved from ${failures.join(' or ')}: ${detail}`,
|
|
146
235
|
lastError,
|
|
147
236
|
)
|
|
148
237
|
}
|
|
@@ -201,6 +290,33 @@ export async function attemptPlugin(
|
|
|
201
290
|
pluginName: manifest.name,
|
|
202
291
|
api,
|
|
203
292
|
}
|
|
293
|
+
// Transitional duplicate-skip (AUT-517): while the builtin hosts the
|
|
294
|
+
// vercel-sandbox implementation, a configured plugin that re-registers the
|
|
295
|
+
// same workspace-provider name must be accepted and skipped, not thrown
|
|
296
|
+
// out of startup. Skip the whole module IFF it declares at least one
|
|
297
|
+
// registration, only workspace-provider registrations, and EVERY declared
|
|
298
|
+
// name collides with a builtin workspace-provider registration. Any other
|
|
299
|
+
// collision (other ports, plugin-vs-plugin, mixed fresh/colliding) throws
|
|
300
|
+
// exactly as before via registry.register below. Keyed on builtin
|
|
301
|
+
// ownership, so the rule retires itself when the builtin is removed.
|
|
302
|
+
const collisions = registry.builtinWorkspaceProviderCollisions(manifest)
|
|
303
|
+
if (
|
|
304
|
+
collisions.length > 0 &&
|
|
305
|
+
declaresOnlyWorkspaceProviderRegistrations(manifest) &&
|
|
306
|
+
collisions.length === Object.keys(manifest.workspaceProviders ?? {}).length
|
|
307
|
+
) {
|
|
308
|
+
const names = Object.keys(manifest.workspaceProviders ?? {})
|
|
309
|
+
.map((name) => JSON.stringify(name))
|
|
310
|
+
.join(', ')
|
|
311
|
+
return {
|
|
312
|
+
...identified,
|
|
313
|
+
status: 'skipped',
|
|
314
|
+
stage: 'registration',
|
|
315
|
+
notice:
|
|
316
|
+
`plugin "${manifest.name}" declares only builtin workspace-provider registration(s) ${names}; ` +
|
|
317
|
+
'skipping it — the builtin keeps serving the provider',
|
|
318
|
+
}
|
|
319
|
+
}
|
|
204
320
|
try {
|
|
205
321
|
registry.register(manifest, {
|
|
206
322
|
module: moduleSpecifier,
|
|
@@ -224,7 +340,9 @@ export async function attemptPlugin(
|
|
|
224
340
|
}
|
|
225
341
|
|
|
226
342
|
/** Exhaustively attempt configured modules in declaration order. Failed
|
|
227
|
-
* modules leave no registrations; later healthy modules still load.
|
|
343
|
+
* modules leave no registrations; later healthy modules still load. Skipped
|
|
344
|
+
* modules (builtin duplicate-skip, guest tolerance) register nothing and
|
|
345
|
+
* count as healthy. */
|
|
228
346
|
export async function diagnosePlugins(
|
|
229
347
|
modules: readonly string[],
|
|
230
348
|
repoRoot: string,
|
|
@@ -238,22 +356,32 @@ export async function diagnosePlugins(
|
|
|
238
356
|
return {
|
|
239
357
|
registry,
|
|
240
358
|
reports,
|
|
241
|
-
healthy: reports.every((report) => report.status
|
|
359
|
+
healthy: reports.every((report) => report.status !== 'failed'),
|
|
242
360
|
}
|
|
243
361
|
}
|
|
244
362
|
|
|
245
|
-
|
|
363
|
+
function defaultPluginNotice(line: string): void {
|
|
364
|
+
console.error(line)
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/** Dispatch compatibility wrapper: preserve first-failure, fail-closed startup.
|
|
368
|
+
* Skipped modules are announced through the notice channel and load
|
|
369
|
+
* continues. */
|
|
246
370
|
export async function loadPlugins(
|
|
247
371
|
modules: readonly string[],
|
|
248
372
|
repoRoot: string,
|
|
249
373
|
options: PluginLoadOptions = {},
|
|
250
374
|
registry: PluginRegistry = createPluginRegistry(),
|
|
251
375
|
): Promise<PluginRegistry> {
|
|
376
|
+
const onNotice = options.onNotice ?? defaultPluginNotice
|
|
252
377
|
for (const moduleSpecifier of modules) {
|
|
253
378
|
const report = await attemptPlugin(moduleSpecifier, repoRoot, registry, options)
|
|
254
379
|
if (report.status === 'failed') {
|
|
255
380
|
throw new Error(report.error, { cause: report.cause })
|
|
256
381
|
}
|
|
382
|
+
if (report.status === 'skipped' && report.notice !== undefined) {
|
|
383
|
+
onNotice(report.notice)
|
|
384
|
+
}
|
|
257
385
|
}
|
|
258
386
|
return registry
|
|
259
387
|
}
|
|
@@ -8,9 +8,13 @@ import type { RuntimeRegistration } from '../ports/runner/runtime'
|
|
|
8
8
|
import type { TicketSourceContractFactory } from '../ports/tickets/contract'
|
|
9
9
|
import type { ForgeContractFactory } from '../ports/forge/contract'
|
|
10
10
|
import type { WorkspaceProviderContractFactory } from '../ports/workspace/contract'
|
|
11
|
+
import type {
|
|
12
|
+
RuntimeReferencesSource,
|
|
13
|
+
WorkspaceProviderCapabilities,
|
|
14
|
+
} from '../ports/workspace/provider-capabilities'
|
|
11
15
|
|
|
12
16
|
/** Version of the in-process plugin contract exposed by `@defrex/autobuild/plugin-sdk`. */
|
|
13
|
-
export const PLUGIN_API_VERSION = '1.
|
|
17
|
+
export const PLUGIN_API_VERSION = '1.7.0' as const
|
|
14
18
|
|
|
15
19
|
/** Context supplied when a registered adapter or contract fixture is selected. */
|
|
16
20
|
export interface PluginFactoryContext<Config = Record<string, unknown>> {
|
|
@@ -22,12 +26,48 @@ export interface PluginFactoryContext<Config = Record<string, unknown>> {
|
|
|
22
26
|
repoRoot: string
|
|
23
27
|
}
|
|
24
28
|
|
|
25
|
-
export type PluginFactory<
|
|
29
|
+
export type PluginFactory<
|
|
30
|
+
Adapter,
|
|
31
|
+
Config = Record<string, unknown>,
|
|
32
|
+
Context = PluginFactoryContext<Config>,
|
|
33
|
+
> = {
|
|
26
34
|
/** The method-index form intentionally makes this callback bivariant, so a
|
|
27
|
-
* plugin can retain its concrete config type across the erased manifest
|
|
28
|
-
|
|
35
|
+
* plugin can retain its concrete config type across the erased manifest —
|
|
36
|
+
* and a port that widens the context (workspace providers receive the
|
|
37
|
+
* host-derived seams, AUT-560) keeps every base-context factory assignable. */
|
|
38
|
+
invoke(context: Context): Adapter | Promise<Adapter>
|
|
29
39
|
}['invoke']
|
|
30
40
|
|
|
41
|
+
/** Context supplied to a workspace-provider factory: the shared
|
|
42
|
+
* `{ config, env, repoRoot }` context plus the host-derived seams a
|
|
43
|
+
* store-requiring remote provider needs (AUT-560). Every added field is
|
|
44
|
+
* optional — the shared call sites that carry no seam construct the plain
|
|
45
|
+
* shared context, and a factory that ignores the new fields changes nothing.
|
|
46
|
+
* Plugin API 1.7.0. */
|
|
47
|
+
export interface WorkspaceProviderPluginFactoryContext<Config = Record<string, unknown>>
|
|
48
|
+
extends PluginFactoryContext<Config> {
|
|
49
|
+
/** Git store URL the dispatcher selected (`AB_STORE`). Guaranteed present
|
|
50
|
+
* when the registration declares `capabilities.storeRequirements`:
|
|
51
|
+
* `createWorkspaceProvider` refuses construction before the factory runs
|
|
52
|
+
* when either store seam is missing. */
|
|
53
|
+
storeRef?: string
|
|
54
|
+
/** Scoped store token (`AB_TOKEN`). Guaranteed present under
|
|
55
|
+
* `capabilities.storeRequirements` for the same reason. */
|
|
56
|
+
storeToken?: string
|
|
57
|
+
/** Host-derived effective runtime routes (the referenced runtimes and their
|
|
58
|
+
* selection provenance). Absent at call sites that reference no runtimes
|
|
59
|
+
* (for example the MCP server), which never construct a workspace. */
|
|
60
|
+
runtimeReferences?: RuntimeReferencesSource
|
|
61
|
+
/** Checkout-less seam: the repository's HTTPS origin. When absent the
|
|
62
|
+
* provider falls back to host `git` from `repoRoot` — the same default the
|
|
63
|
+
* builtin `VercelSandboxProvider` applies. */
|
|
64
|
+
origin?: () => Promise<string>
|
|
65
|
+
/** Checkout-less seam: the remote branch's current head, `undefined` when
|
|
66
|
+
* the branch does not exist. When absent the provider falls back to host
|
|
67
|
+
* `git` from `repoRoot`. */
|
|
68
|
+
remoteBranchHead?: (branch: string) => Promise<string | undefined>
|
|
69
|
+
}
|
|
70
|
+
|
|
31
71
|
export type TicketSourcePluginFactory<Config = Record<string, unknown>> = PluginFactory<
|
|
32
72
|
TicketSource,
|
|
33
73
|
Config
|
|
@@ -53,10 +93,29 @@ export type AgentRuntimePluginFactory<Config = Record<string, unknown>> = Plugin
|
|
|
53
93
|
>
|
|
54
94
|
export type WorkspaceProviderPluginFactory<Config = Record<string, unknown>> = PluginFactory<
|
|
55
95
|
WorkspaceProvider,
|
|
56
|
-
Config
|
|
96
|
+
Config,
|
|
97
|
+
WorkspaceProviderPluginFactoryContext<Config>
|
|
57
98
|
>
|
|
58
99
|
export type ForgePluginFactory<Config = Record<string, unknown>> = PluginFactory<Forge, Config>
|
|
59
100
|
|
|
101
|
+
/** Optional host-enforced metadata for a workspace provider. Bare factories
|
|
102
|
+
* remain valid for plugin API 1.0 compatibility. Required environment lives
|
|
103
|
+
* on `capabilities.requiredEnv` — a descriptor-level `requiredEnv` is
|
|
104
|
+
* rejected at manifest parse (see `workspaceProviderRegistrationSchema`). */
|
|
105
|
+
export interface WorkspaceProviderPluginDescriptor<Config = Record<string, unknown>> {
|
|
106
|
+
factory: WorkspaceProviderPluginFactory<Config>
|
|
107
|
+
/** Optional shared-suite certification fixture. */
|
|
108
|
+
contract?: PluginContractDescriptor<WorkspaceProviderContractFactory>
|
|
109
|
+
/** Behavior declarations the host enforces without naming the provider
|
|
110
|
+
* (AUT-516): config schema, forges, environment groups, store requirements,
|
|
111
|
+
* origin validation, and remote readiness. */
|
|
112
|
+
capabilities?: WorkspaceProviderCapabilities
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export type WorkspaceProviderPluginRegistration<Config = Record<string, unknown>> =
|
|
116
|
+
| WorkspaceProviderPluginFactory<Config>
|
|
117
|
+
| WorkspaceProviderPluginDescriptor<Config>
|
|
118
|
+
|
|
60
119
|
export interface PluginContractDescriptor<ContractFactory, Config = Record<string, unknown>> {
|
|
61
120
|
/** Creates the unchanged shared-suite harness factory when the test verb runs. */
|
|
62
121
|
factory: PluginFactory<ContractFactory, Config>
|
|
@@ -75,10 +134,6 @@ export type AgentRuntimePluginRegistration = PluginAdapterRegistration<
|
|
|
75
134
|
AgentRuntimePluginFactory,
|
|
76
135
|
AgentRunnerContractFactory
|
|
77
136
|
>
|
|
78
|
-
export type WorkspaceProviderPluginRegistration = PluginAdapterRegistration<
|
|
79
|
-
WorkspaceProviderPluginFactory,
|
|
80
|
-
WorkspaceProviderContractFactory
|
|
81
|
-
>
|
|
82
137
|
export type ForgePluginRegistration = PluginAdapterRegistration<
|
|
83
138
|
ForgePluginFactory,
|
|
84
139
|
ForgeContractFactory
|
|
@@ -212,6 +267,120 @@ const ticketSourceRegistrationSchema = z.unknown().transform((value, ctx) => {
|
|
|
212
267
|
return z.NEVER
|
|
213
268
|
})
|
|
214
269
|
|
|
270
|
+
/** Function-valued capability fields (origin validation, redaction, summary
|
|
271
|
+
* lines, remote readiness). */
|
|
272
|
+
const capabilityFunctionSchema = z.custom<(...args: never[]) => unknown>(
|
|
273
|
+
(value) => typeof value === 'function',
|
|
274
|
+
'must be a function',
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
const workspaceProviderCapabilitiesSchema = z.strictObject({
|
|
278
|
+
// A Zod schema carries both `parse` and `safeParse`; the consumers use each
|
|
279
|
+
// (`create.ts` parses with `safeParse`, `init-validation.ts` with `parse`),
|
|
280
|
+
// so requiring only one would let a half-shaped object past manifest parsing
|
|
281
|
+
// and crash the other seam with a TypeError instead of the config
|
|
282
|
+
// diagnostic (f_69fc887c).
|
|
283
|
+
configSchema: z
|
|
284
|
+
.custom<z.ZodType>((value) => {
|
|
285
|
+
if (value === null || typeof value !== 'object') return false
|
|
286
|
+
const shape = value as { parse?: unknown; safeParse?: unknown }
|
|
287
|
+
return typeof shape.parse === 'function' && typeof shape.safeParse === 'function'
|
|
288
|
+
}, 'must be a Zod schema')
|
|
289
|
+
.optional(),
|
|
290
|
+
configRefusal: z.string().optional(),
|
|
291
|
+
requireRuntimeProvisioning: z.boolean().optional(),
|
|
292
|
+
sandboxForbiddenEnv: z.array(z.string()).optional(),
|
|
293
|
+
supportedForges: z.array(z.string()).optional(),
|
|
294
|
+
forgeDispatchMessage: z.string().optional(),
|
|
295
|
+
forgeValidationMessage: z.string().optional(),
|
|
296
|
+
requiredEnv: z
|
|
297
|
+
.array(
|
|
298
|
+
z.strictObject({
|
|
299
|
+
alternatives: z.array(z.array(z.string().min(1)).min(1)).min(1),
|
|
300
|
+
dispatchMessage: z.string().optional(),
|
|
301
|
+
validationMessage: z.string().optional(),
|
|
302
|
+
}),
|
|
303
|
+
)
|
|
304
|
+
// AUT-554: a group carrying neither message is skipped at both
|
|
305
|
+
// enforcement sites (dispatch.ts omits an undefined dispatchMessage;
|
|
306
|
+
// init-validation.ts's declaredForgeEnvChecks omits an undefined
|
|
307
|
+
// validationMessage), so accepting it here would license a declaration
|
|
308
|
+
// the host never enforces. The issue anchors at the group's index so the
|
|
309
|
+
// rendered path names it by location and the message names it by its
|
|
310
|
+
// alternatives. The shape guard keeps the check quiet when the group's
|
|
311
|
+
// structural diagnostics already speak: zod v4 runs refinements even
|
|
312
|
+
// after a check-stage failure inside the group (an empty alternatives
|
|
313
|
+
// array still parses through), and a malformed group must not grow a
|
|
314
|
+
// second, degraded copy of this issue.
|
|
315
|
+
.superRefine((groups, ctx) => {
|
|
316
|
+
groups.forEach((group, index) => {
|
|
317
|
+
if (group.dispatchMessage !== undefined || group.validationMessage !== undefined) return
|
|
318
|
+
// Skip a structurally malformed group: its own diagnostics speak.
|
|
319
|
+
if (
|
|
320
|
+
group.alternatives.length === 0 ||
|
|
321
|
+
group.alternatives.some((names) => names.length === 0)
|
|
322
|
+
)
|
|
323
|
+
return
|
|
324
|
+
const alternatives = group.alternatives.map((names) => names.join(', ')).join(' | ')
|
|
325
|
+
ctx.addIssue({
|
|
326
|
+
code: 'custom',
|
|
327
|
+
path: [index],
|
|
328
|
+
message: `requiredEnv group (alternatives: ${alternatives}) declares neither dispatchMessage nor validationMessage; declare at least one so the host enforces this group — a group with neither is silently skipped at dispatch and init validation`,
|
|
329
|
+
})
|
|
330
|
+
})
|
|
331
|
+
})
|
|
332
|
+
.optional(),
|
|
333
|
+
processEnvOnly: z
|
|
334
|
+
.array(z.strictObject({ name: z.string().min(1), message: z.string() }))
|
|
335
|
+
.optional(),
|
|
336
|
+
storeRequirements: z
|
|
337
|
+
.strictObject({
|
|
338
|
+
constructionMessage: z.string(),
|
|
339
|
+
storeRefMessage: z.string(),
|
|
340
|
+
storeTokenMessage: z.string(),
|
|
341
|
+
})
|
|
342
|
+
.optional(),
|
|
343
|
+
validateOrigin: capabilityFunctionSchema.optional(),
|
|
344
|
+
originReadFailureMessage: z.string().optional(),
|
|
345
|
+
guestEnvNames: capabilityFunctionSchema.optional(),
|
|
346
|
+
describeEnvironment: capabilityFunctionSchema.optional(),
|
|
347
|
+
validateReadiness: capabilityFunctionSchema.optional(),
|
|
348
|
+
})
|
|
349
|
+
|
|
350
|
+
const workspaceProviderDescriptorSchema = z.strictObject({
|
|
351
|
+
factory: factorySchema,
|
|
352
|
+
contract: contractSchema.optional(),
|
|
353
|
+
capabilities: workspaceProviderCapabilitiesSchema.optional(),
|
|
354
|
+
})
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* The workspace-provider counterpart of `ticketSourceRegistrationSchema`, with
|
|
358
|
+
* one deliberate tightening (AUT-516): a top-level `requiredEnv` key is
|
|
359
|
+
* rejected with a bespoke remediation message instead of the generic
|
|
360
|
+
* unrecognized-key complaint. The generic registration schema accepts the key
|
|
361
|
+
* today, but its only consumer is ticket-source construction — routing
|
|
362
|
+
* workspace-provider env requirements there would leave the declaration
|
|
363
|
+
* parsed and never checked, exactly the ignored-declaration failure mode this
|
|
364
|
+
* port's capabilities exist to eliminate. Required environment belongs on
|
|
365
|
+
* `capabilities.requiredEnv`, which carries the per-site messages a bare name
|
|
366
|
+
* list cannot.
|
|
367
|
+
*/
|
|
368
|
+
const workspaceProviderRegistrationSchema = z.unknown().transform((value, ctx) => {
|
|
369
|
+
if (typeof value === 'function') return value
|
|
370
|
+
if (value !== null && typeof value === 'object' && 'requiredEnv' in value) {
|
|
371
|
+
ctx.addIssue({
|
|
372
|
+
code: 'custom',
|
|
373
|
+
message:
|
|
374
|
+
'workspaceProviders.<name>.requiredEnv is not supported; declare required environment as workspaceProviders.<name>.capabilities.requiredEnv',
|
|
375
|
+
})
|
|
376
|
+
return z.NEVER
|
|
377
|
+
}
|
|
378
|
+
const parsed = workspaceProviderDescriptorSchema.safeParse(value)
|
|
379
|
+
if (parsed.success) return parsed.data
|
|
380
|
+
forwardIssues(parsed.error.issues, ctx)
|
|
381
|
+
return z.NEVER
|
|
382
|
+
})
|
|
383
|
+
|
|
215
384
|
/** Strict runtime contract for a plugin module's default export.
|
|
216
385
|
*
|
|
217
386
|
* `.optional()` stays outermost, so an undeclared port remains `undefined`
|
|
@@ -221,7 +390,11 @@ export const pluginManifestSchema = z.strictObject({
|
|
|
221
390
|
apiVersion: nonblank,
|
|
222
391
|
ticketSources: openMap('ticketSources', ticketSourceRegistrationSchema, ADAPTER_MAP).optional(),
|
|
223
392
|
agentRuntimes: registrationMap('agentRuntimes').optional(),
|
|
224
|
-
workspaceProviders:
|
|
393
|
+
workspaceProviders: openMap(
|
|
394
|
+
'workspaceProviders',
|
|
395
|
+
workspaceProviderRegistrationSchema,
|
|
396
|
+
ADAPTER_MAP,
|
|
397
|
+
).optional(),
|
|
225
398
|
forges: registrationMap('forges').optional(),
|
|
226
399
|
})
|
|
227
400
|
|
|
@@ -12,6 +12,23 @@ import type { AgentRunnerContractFactory } from '../ports/runner/contract'
|
|
|
12
12
|
import type { TicketSourceContractFactory } from '../ports/tickets/contract'
|
|
13
13
|
import type { WorkspaceProviderContractFactory } from '../ports/workspace/contract'
|
|
14
14
|
import type { ForgeContractFactory } from '../ports/forge/contract'
|
|
15
|
+
import type { WorkspaceConfig } from '../config/schema'
|
|
16
|
+
import type { CreateWorkspaceProviderOptions } from '../ports/workspace/create'
|
|
17
|
+
import type { WorkspaceProviderCapabilities } from '../ports/workspace/provider-capabilities'
|
|
18
|
+
import type { WorkspaceProvider } from '../ports/types'
|
|
19
|
+
import {
|
|
20
|
+
builtinWorkspaceProviderNames,
|
|
21
|
+
builtinWorkspaceProviderRegistration,
|
|
22
|
+
} from '../ports/workspace/builtin-capabilities'
|
|
23
|
+
|
|
24
|
+
/** Host-owned construction closure for a builtin workspace provider. `parsed`
|
|
25
|
+
* is the declared `configSchema`'s parse output, or the raw
|
|
26
|
+
* `[workspace.config]` when no schema is declared. */
|
|
27
|
+
export type BuiltinWorkspaceProviderFactory = (
|
|
28
|
+
config: WorkspaceConfig,
|
|
29
|
+
opts: CreateWorkspaceProviderOptions,
|
|
30
|
+
parsed: unknown,
|
|
31
|
+
) => WorkspaceProvider | Promise<WorkspaceProvider>
|
|
15
32
|
|
|
16
33
|
export type PluginPort = 'ticket-source' | 'agent-runtime' | 'workspace-provider' | 'forge'
|
|
17
34
|
|
|
@@ -49,6 +66,14 @@ export interface AdapterRegistration<Factory, ContractFactory = unknown> {
|
|
|
49
66
|
* builtins and every other plugin port. */
|
|
50
67
|
requiredEnv?: readonly string[]
|
|
51
68
|
contract?: PluginContractDescriptor<ContractFactory>
|
|
69
|
+
/** Workspace-provider capability declarations (AUT-516). Attached to the
|
|
70
|
+
* builtin registrations and normalized from plugin descriptors; every other
|
|
71
|
+
* port leaves it undefined. */
|
|
72
|
+
capabilities?: WorkspaceProviderCapabilities
|
|
73
|
+
/** Builtin workspace providers construct their host-owned adapters through
|
|
74
|
+
* this closure (`createWorkspaceProvider` calls it after the uniform
|
|
75
|
+
* capability checks); plugins keep `factory`. */
|
|
76
|
+
builtinFactory?: BuiltinWorkspaceProviderFactory
|
|
52
77
|
source: RegistrationSource
|
|
53
78
|
}
|
|
54
79
|
|
|
@@ -92,6 +117,7 @@ interface PendingRegistration {
|
|
|
92
117
|
factory: unknown
|
|
93
118
|
requiredEnv?: readonly string[]
|
|
94
119
|
contract?: PluginContractDescriptor<unknown>
|
|
120
|
+
capabilities?: WorkspaceProviderCapabilities
|
|
95
121
|
target: Map<string, AdapterRegistration<unknown, unknown>>
|
|
96
122
|
}
|
|
97
123
|
|
|
@@ -101,6 +127,7 @@ type InternalRegistration<AdapterFactory, ContractFactory> =
|
|
|
101
127
|
factory: AdapterFactory
|
|
102
128
|
requiredEnv?: readonly string[]
|
|
103
129
|
contract?: PluginContractDescriptor<ContractFactory>
|
|
130
|
+
capabilities?: WorkspaceProviderCapabilities
|
|
104
131
|
}
|
|
105
132
|
|
|
106
133
|
function normalize<AdapterFactory, ContractFactory>(
|
|
@@ -109,6 +136,7 @@ function normalize<AdapterFactory, ContractFactory>(
|
|
|
109
136
|
factory: AdapterFactory
|
|
110
137
|
requiredEnv?: readonly string[]
|
|
111
138
|
contract?: PluginContractDescriptor<ContractFactory>
|
|
139
|
+
capabilities?: WorkspaceProviderCapabilities
|
|
112
140
|
} {
|
|
113
141
|
if (typeof registration === 'function') {
|
|
114
142
|
return { factory: registration as AdapterFactory }
|
|
@@ -117,11 +145,13 @@ function normalize<AdapterFactory, ContractFactory>(
|
|
|
117
145
|
factory: AdapterFactory
|
|
118
146
|
requiredEnv?: readonly string[]
|
|
119
147
|
contract?: PluginContractDescriptor<ContractFactory>
|
|
148
|
+
capabilities?: WorkspaceProviderCapabilities
|
|
120
149
|
}
|
|
121
150
|
return {
|
|
122
151
|
factory: object.factory,
|
|
123
152
|
...(object.requiredEnv !== undefined ? { requiredEnv: [...object.requiredEnv] } : {}),
|
|
124
153
|
...(object.contract !== undefined ? { contract: object.contract } : {}),
|
|
154
|
+
...(object.capabilities !== undefined ? { capabilities: object.capabilities } : {}),
|
|
125
155
|
}
|
|
126
156
|
}
|
|
127
157
|
|
|
@@ -141,10 +171,29 @@ export class PluginRegistry {
|
|
|
141
171
|
// profile value, so a runtime with the same name could never be selected.
|
|
142
172
|
'split',
|
|
143
173
|
])
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
174
|
+
// Builtin workspace-provider names are derived from the canonical builtin
|
|
175
|
+
// list in builtin-capabilities.ts (AUT-579), so a builtin added there
|
|
176
|
+
// registers here automatically — no registry edit is needed.
|
|
177
|
+
readonly workspaceProviders = new Map<
|
|
178
|
+
string,
|
|
179
|
+
AdapterRegistration<WorkspaceProviderPluginFactory, WorkspaceProviderContractFactory>
|
|
180
|
+
>(
|
|
181
|
+
builtinWorkspaceProviderNames().map((name) => {
|
|
182
|
+
const builtin = builtinWorkspaceProviderRegistration(name)
|
|
183
|
+
if (builtin === undefined) {
|
|
184
|
+
throw new Error(`builtin workspace provider "${name}" has no capability declaration`)
|
|
185
|
+
}
|
|
186
|
+
return [
|
|
187
|
+
name,
|
|
188
|
+
{
|
|
189
|
+
owner: BUILTIN,
|
|
190
|
+
source: { kind: 'builtin' } as const,
|
|
191
|
+
capabilities: builtin.capabilities,
|
|
192
|
+
builtinFactory: builtin.builtinFactory,
|
|
193
|
+
},
|
|
194
|
+
]
|
|
195
|
+
}),
|
|
196
|
+
)
|
|
148
197
|
readonly forges = reserved<ForgePluginFactory, ForgeContractFactory>(['github', 'local-git'])
|
|
149
198
|
|
|
150
199
|
register(
|
|
@@ -184,6 +233,9 @@ export class PluginRegistry {
|
|
|
184
233
|
...(normalized.contract !== undefined
|
|
185
234
|
? { contract: normalized.contract as PluginContractDescriptor<unknown> }
|
|
186
235
|
: {}),
|
|
236
|
+
...(normalized.capabilities !== undefined
|
|
237
|
+
? { capabilities: normalized.capabilities }
|
|
238
|
+
: {}),
|
|
187
239
|
target: target as Map<string, AdapterRegistration<unknown, unknown>>,
|
|
188
240
|
})
|
|
189
241
|
}
|
|
@@ -223,6 +275,9 @@ export class PluginRegistry {
|
|
|
223
275
|
? { requiredEnv: registration.requiredEnv }
|
|
224
276
|
: {}),
|
|
225
277
|
...(registration.contract !== undefined ? { contract: registration.contract } : {}),
|
|
278
|
+
...(registration.capabilities !== undefined
|
|
279
|
+
? { capabilities: registration.capabilities }
|
|
280
|
+
: {}),
|
|
226
281
|
source,
|
|
227
282
|
})
|
|
228
283
|
}
|
|
@@ -267,6 +322,20 @@ export class PluginRegistry {
|
|
|
267
322
|
return this.forges.get(name)
|
|
268
323
|
}
|
|
269
324
|
}
|
|
325
|
+
|
|
326
|
+
/** Workspace-provider names a manifest declares that currently belong to a
|
|
327
|
+
* BUILTIN registration (AUT-517). Names owned by another plugin are not
|
|
328
|
+
* reported — those collisions throw as always. The loader uses this for
|
|
329
|
+
* the transitional duplicate-skip: while the builtin hosts the
|
|
330
|
+
* vercel-sandbox implementation, a configured plugin re-registering the
|
|
331
|
+
* name is skipped instead of failing startup. Keyed on builtin ownership,
|
|
332
|
+
* so the rule retires itself when the builtin is removed (AUT-505). */
|
|
333
|
+
builtinWorkspaceProviderCollisions(manifest: AutobuildPluginManifest): string[] {
|
|
334
|
+
return Object.keys(manifest.workspaceProviders ?? {}).filter((name) => {
|
|
335
|
+
const existing = this.workspaceProviders.get(name)
|
|
336
|
+
return existing !== undefined && existing.owner.kind === 'builtin'
|
|
337
|
+
})
|
|
338
|
+
}
|
|
270
339
|
}
|
|
271
340
|
|
|
272
341
|
export function createPluginRegistry(): PluginRegistry {
|
|
@@ -47,7 +47,7 @@ export async function createForge(opts: {
|
|
|
47
47
|
...(opts.repository !== undefined ? { repository: opts.repository } : {}),
|
|
48
48
|
})
|
|
49
49
|
}
|
|
50
|
-
if (opts.name === 'local-git') return new LocalGitForge()
|
|
50
|
+
if (opts.name === 'local-git') return new LocalGitForge({ repoRoot: opts.repoRoot })
|
|
51
51
|
throw new Error(`builtin forge adapter ${JSON.stringify(opts.name)} has no constructor`)
|
|
52
52
|
}
|
|
53
53
|
|
|
@@ -83,6 +83,13 @@ export class FakeForge implements Forge {
|
|
|
83
83
|
readonly name = 'fake'
|
|
84
84
|
readonly prAttachments?: PrAttachmentHosting
|
|
85
85
|
|
|
86
|
+
/** Optional checkout-less read seam (SPEC §16.1 pipeline pinning): seeded
|
|
87
|
+
* remote branches (`branch` → sha) and file contents keyed `ref\0path`.
|
|
88
|
+
* `remoteBranchSha` throws when the branch was never seeded — the same
|
|
89
|
+
* not-found contract as the production adapters. */
|
|
90
|
+
readonly remoteBranches = new Map<string, string>()
|
|
91
|
+
readonly files = new Map<string, string>()
|
|
92
|
+
|
|
86
93
|
/** Journals — public so tests assert directly on call order and args. */
|
|
87
94
|
readonly pushes: PushRecord[] = []
|
|
88
95
|
readonly opened: OpenPrRecord[] = []
|
|
@@ -330,6 +337,30 @@ export class FakeForge implements Forge {
|
|
|
330
337
|
return this.autoMerge.get(number) ?? false
|
|
331
338
|
}
|
|
332
339
|
|
|
340
|
+
/** Seed a remote branch head for the optional checkout-less read seam. */
|
|
341
|
+
seedBranch(branch: string, sha: string): void {
|
|
342
|
+
this.remoteBranches.set(branch, sha)
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/** Seed file content readable at a given ref (sha or branch name). */
|
|
346
|
+
seedFile(ref: string, path: string, content: string): void {
|
|
347
|
+
this.files.set(`${ref}\0${path}`, content)
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
async remoteBranchSha(branch: string): Promise<string> {
|
|
351
|
+
const sha = this.remoteBranches.get(branch)
|
|
352
|
+
if (sha === undefined) throw new Error(`FakeForge: unknown remote branch ${branch}`)
|
|
353
|
+
return sha
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
async readFile(path: string, ref?: string): Promise<string> {
|
|
357
|
+
const key = `${ref ?? 'default'}\0${path}`
|
|
358
|
+
const content = this.files.get(key)
|
|
359
|
+
if (content === undefined)
|
|
360
|
+
throw new Error(`FakeForge: no file ${path} at ref ${ref ?? 'default'}`)
|
|
361
|
+
return content
|
|
362
|
+
}
|
|
363
|
+
|
|
333
364
|
async pushBranch(workspacePath: string, branch: string): Promise<void> {
|
|
334
365
|
this.pushes.push({ workspacePath, branch })
|
|
335
366
|
this.publishedBranches.add(branch)
|