@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.
- package/LICENSE +201 -0
- package/README.md +192 -0
- package/SPEC.md +2269 -0
- package/bin/ab-build-runner.ts +47 -0
- package/bin/ab-dev.ts +27 -0
- package/bin/ab-dispatch-kernel.ts +92 -0
- package/bin/ab-harvest-runner.ts +29 -0
- package/bin/ab-init-probe.ts +19 -0
- package/bin/ab-process-group-reaper.ts +32 -0
- package/bin/ab.ts +6 -0
- package/bin/agent/ab +3 -0
- package/docs/answering-blocked-builds.md +89 -0
- package/docs/architecture.md +726 -0
- package/docs/assets/headline-wide.png +0 -0
- package/docs/configuration.md +1623 -0
- package/docs/hosted-dispatcher-cutover-validation.md +178 -0
- package/docs/hosted-dispatcher.md +204 -0
- package/docs/mcp.md +119 -0
- package/docs/operator-api.md +186 -0
- package/docs/remote-store-protocol.md +972 -0
- package/docs/remote-ticket-protocol.md +79 -0
- package/docs/setup.md +289 -0
- package/docs/spec-standard.md +63 -0
- package/docs/ticket-dependencies.md +59 -0
- package/docs/vercel-sandbox-rollout-probe.md +7 -0
- package/package.json +135 -0
- package/packages/core/src/cli/args.ts +74 -0
- package/packages/core/src/cli/artifact.ts +182 -0
- package/packages/core/src/cli/binary.ts +198 -0
- package/packages/core/src/cli/build-control.ts +789 -0
- package/packages/core/src/cli/build-progress.ts +55 -0
- package/packages/core/src/cli/bulk-control.ts +385 -0
- package/packages/core/src/cli/context.ts +470 -0
- package/packages/core/src/cli/dashboard/actions.ts +59 -0
- package/packages/core/src/cli/dashboard/cells.ts +92 -0
- package/packages/core/src/cli/dashboard/composer.ts +230 -0
- package/packages/core/src/cli/dashboard/detail.ts +100 -0
- package/packages/core/src/cli/dashboard/frame-image.ts +373 -0
- package/packages/core/src/cli/dashboard/live.ts +152 -0
- package/packages/core/src/cli/dashboard/model.ts +1173 -0
- package/packages/core/src/cli/dashboard/poll.ts +196 -0
- package/packages/core/src/cli/dashboard/render.ts +1568 -0
- package/packages/core/src/cli/dashboard/selection.ts +66 -0
- package/packages/core/src/cli/dashboard/session-feed.ts +244 -0
- package/packages/core/src/cli/dashboard/session-view.ts +506 -0
- package/packages/core/src/cli/dashboard/transcript.ts +108 -0
- package/packages/core/src/cli/dev.ts +46 -0
- package/packages/core/src/cli/dispatch-frontend.ts +1045 -0
- package/packages/core/src/cli/dispatch-process.ts +259 -0
- package/packages/core/src/cli/dispatch.ts +3692 -0
- package/packages/core/src/cli/dotenv.ts +48 -0
- package/packages/core/src/cli/env.ts +206 -0
- package/packages/core/src/cli/harvest.ts +698 -0
- package/packages/core/src/cli/help.ts +713 -0
- package/packages/core/src/cli/init-agent.ts +106 -0
- package/packages/core/src/cli/init-validation.ts +583 -0
- package/packages/core/src/cli/init.ts +756 -0
- package/packages/core/src/cli/installation.ts +333 -0
- package/packages/core/src/cli/keyboard.ts +261 -0
- package/packages/core/src/cli/main.ts +1399 -0
- package/packages/core/src/cli/mcp.ts +279 -0
- package/packages/core/src/cli/models.ts +65 -0
- package/packages/core/src/cli/observe.ts +55 -0
- package/packages/core/src/cli/plugin.ts +227 -0
- package/packages/core/src/cli/pr-attachments.ts +138 -0
- package/packages/core/src/cli/pr-summary.ts +124 -0
- package/packages/core/src/cli/publication-settlement.ts +188 -0
- package/packages/core/src/cli/repo-state.ts +213 -0
- package/packages/core/src/cli/repository-status.ts +80 -0
- package/packages/core/src/cli/self-update.ts +443 -0
- package/packages/core/src/cli/status.ts +895 -0
- package/packages/core/src/cli/store-opening.contract.ts +289 -0
- package/packages/core/src/cli/store-opening.ts +167 -0
- package/packages/core/src/cli/store-ref.ts +33 -0
- package/packages/core/src/cli/terminal-restore.ts +174 -0
- package/packages/core/src/cli/terminal.ts +361 -0
- package/packages/core/src/cli/terminals.ts +995 -0
- package/packages/core/src/cli/testkit.ts +194 -0
- package/packages/core/src/cli/ticket.ts +484 -0
- package/packages/core/src/cli/upgrade-agent.ts +180 -0
- package/packages/core/src/cli/upgrade-commit.ts +468 -0
- package/packages/core/src/cli/upgrade-notice.ts +91 -0
- package/packages/core/src/cli/upgrade-progress.ts +110 -0
- package/packages/core/src/cli/upgrade.ts +888 -0
- package/packages/core/src/cli/wait.ts +587 -0
- package/packages/core/src/cli/watch.ts +932 -0
- package/packages/core/src/config/live.ts +255 -0
- package/packages/core/src/config/load.ts +98 -0
- package/packages/core/src/config/roles.ts +438 -0
- package/packages/core/src/config/schema.ts +774 -0
- package/packages/core/src/distribution.ts +24 -0
- package/packages/core/src/events/catalog.ts +201 -0
- package/packages/core/src/events/envelope.ts +59 -0
- package/packages/core/src/events/payloads.ts +618 -0
- package/packages/core/src/events/repository.ts +559 -0
- package/packages/core/src/events/sessions.ts +180 -0
- package/packages/core/src/harvest/schema.ts +147 -0
- package/packages/core/src/ids.ts +45 -0
- package/packages/core/src/integration/harness.ts +681 -0
- package/packages/core/src/kernel/auto-merge.ts +259 -0
- package/packages/core/src/kernel/converge.ts +117 -0
- package/packages/core/src/kernel/dispatch-settings.ts +60 -0
- package/packages/core/src/kernel/dispatch-status.ts +141 -0
- package/packages/core/src/kernel/engine.ts +1047 -0
- package/packages/core/src/kernel/harvest.ts +823 -0
- package/packages/core/src/kernel/origin.ts +69 -0
- package/packages/core/src/kernel/phases.ts +196 -0
- package/packages/core/src/kernel/plan-verify-selection.ts +105 -0
- package/packages/core/src/kernel/pr-attachments.ts +82 -0
- package/packages/core/src/kernel/reducer.ts +752 -0
- package/packages/core/src/kernel/stall.ts +155 -0
- package/packages/core/src/kernel/verify-applicability.ts +52 -0
- package/packages/core/src/markdown.ts +67 -0
- package/packages/core/src/ontology.ts +288 -0
- package/packages/core/src/open-map.ts +203 -0
- package/packages/core/src/operator/annotations.ts +187 -0
- package/packages/core/src/operator/client.ts +311 -0
- package/packages/core/src/operator/control.ts +117 -0
- package/packages/core/src/operator/index.ts +62 -0
- package/packages/core/src/operator/presentation.ts +22 -0
- package/packages/core/src/operator/protocol.ts +98 -0
- package/packages/core/src/operator/query.ts +201 -0
- package/packages/core/src/operator/registry.ts +1043 -0
- package/packages/core/src/operator/requests.ts +138 -0
- package/packages/core/src/operator/sandbox.ts +458 -0
- package/packages/core/src/operator/server.ts +616 -0
- package/packages/core/src/operator/sessions.ts +223 -0
- package/packages/core/src/operator/tickets.ts +177 -0
- package/packages/core/src/plugin-sdk/index.ts +174 -0
- package/packages/core/src/plugins/contract-entry.ts +74 -0
- package/packages/core/src/plugins/contract-env.ts +3 -0
- package/packages/core/src/plugins/load.ts +259 -0
- package/packages/core/src/plugins/manifest.ts +236 -0
- package/packages/core/src/plugins/registry.ts +274 -0
- package/packages/core/src/plugins/runtimes.ts +75 -0
- package/packages/core/src/ports/forge/contract.ts +258 -0
- package/packages/core/src/ports/forge/create.ts +74 -0
- package/packages/core/src/ports/forge/fake.ts +488 -0
- package/packages/core/src/ports/forge/github-pr-attachments.ts +351 -0
- package/packages/core/src/ports/forge/github-transport.ts +352 -0
- package/packages/core/src/ports/forge/github.ts +1162 -0
- package/packages/core/src/ports/forge/local-git.ts +568 -0
- package/packages/core/src/ports/runner/claude.ts +873 -0
- package/packages/core/src/ports/runner/cli-stream.ts +90 -0
- package/packages/core/src/ports/runner/codex.ts +873 -0
- package/packages/core/src/ports/runner/contract.ts +492 -0
- package/packages/core/src/ports/runner/fake.ts +213 -0
- package/packages/core/src/ports/runner/json-record.ts +2 -0
- package/packages/core/src/ports/runner/live-contract-fixture.ts +176 -0
- package/packages/core/src/ports/runner/one-shot.ts +29 -0
- package/packages/core/src/ports/runner/pi-bridge-core.ts +136 -0
- package/packages/core/src/ports/runner/pi-bridge.js +6 -0
- package/packages/core/src/ports/runner/pi-rpc.ts +454 -0
- package/packages/core/src/ports/runner/pi.ts +617 -0
- package/packages/core/src/ports/runner/production.ts +74 -0
- package/packages/core/src/ports/runner/provider-error.ts +121 -0
- package/packages/core/src/ports/runner/routing.ts +234 -0
- package/packages/core/src/ports/runner/runtime.ts +214 -0
- package/packages/core/src/ports/runner/session-env.ts +34 -0
- package/packages/core/src/ports/runner/stream-parts.ts +103 -0
- package/packages/core/src/ports/tickets/contract.ts +570 -0
- package/packages/core/src/ports/tickets/create.ts +135 -0
- package/packages/core/src/ports/tickets/fake.ts +215 -0
- package/packages/core/src/ports/tickets/file.ts +559 -0
- package/packages/core/src/ports/tickets/linear.ts +811 -0
- package/packages/core/src/ports/tickets/operations.ts +161 -0
- package/packages/core/src/ports/tickets/remote-index.ts +13 -0
- package/packages/core/src/ports/tickets/remote-protocol.ts +80 -0
- package/packages/core/src/ports/tickets/remote-server.ts +185 -0
- package/packages/core/src/ports/tickets/remote.ts +130 -0
- package/packages/core/src/ports/tickets/update.ts +51 -0
- package/packages/core/src/ports/types.ts +528 -0
- package/packages/core/src/ports/workspace/build-execution.ts +123 -0
- package/packages/core/src/ports/workspace/contract.ts +136 -0
- package/packages/core/src/ports/workspace/create.ts +132 -0
- package/packages/core/src/ports/workspace/distribution-archive.ts +317 -0
- package/packages/core/src/ports/workspace/fake.ts +432 -0
- package/packages/core/src/ports/workspace/git-worktree.ts +848 -0
- package/packages/core/src/ports/workspace/harvest-execution.ts +111 -0
- package/packages/core/src/ports/workspace/in-process-build-execution.ts +30 -0
- package/packages/core/src/ports/workspace/local-build-execution.ts +88 -0
- package/packages/core/src/ports/workspace/operator-sandbox.ts +142 -0
- package/packages/core/src/ports/workspace/phase-scratch.ts +156 -0
- package/packages/core/src/ports/workspace/vercel-sandbox.ts +2541 -0
- package/packages/core/src/processes/build-child-exit.ts +78 -0
- package/packages/core/src/processes/build-child.ts +119 -0
- package/packages/core/src/processes/build-execution-state.ts +95 -0
- package/packages/core/src/processes/build-parent-watch.ts +35 -0
- package/packages/core/src/processes/build-runner.ts +2614 -0
- package/packages/core/src/processes/dispatcher.ts +2825 -0
- package/packages/core/src/processes/execution-settlement.ts +171 -0
- package/packages/core/src/processes/harvest-child.ts +115 -0
- package/packages/core/src/processes/harvest-execution-state.ts +121 -0
- package/packages/core/src/processes/harvest-runner.ts +1656 -0
- package/packages/core/src/processes/harvest.ts +595 -0
- package/packages/core/src/processes/infrastructure-failure-budget.ts +146 -0
- package/packages/core/src/processes/phase-failure-budget.ts +52 -0
- package/packages/core/src/processes/process-group-reaper.ts +35 -0
- package/packages/core/src/processes/process-group.ts +65 -0
- package/packages/core/src/processes/publication-loss.ts +62 -0
- package/packages/core/src/processes/publication-state.ts +93 -0
- package/packages/core/src/processes/sandbox-state.ts +101 -0
- package/packages/core/src/registry.ts +24 -0
- package/packages/core/src/sessions/reducer.ts +185 -0
- package/packages/core/src/skills.ts +7 -0
- package/packages/core/src/spec-standard.ts +47 -0
- package/packages/core/src/store/adapter.ts +80 -0
- package/packages/core/src/store/build-scope.ts +272 -0
- package/packages/core/src/store/contract.ts +2581 -0
- package/packages/core/src/store/local/blobs.ts +38 -0
- package/packages/core/src/store/local/schema.ts +180 -0
- package/packages/core/src/store/local/store.ts +1713 -0
- package/packages/core/src/store/memory.ts +1138 -0
- package/packages/core/src/store/phase-session.ts +425 -0
- package/packages/core/src/store/remote/client.ts +780 -0
- package/packages/core/src/store/remote/index.ts +12 -0
- package/packages/core/src/store/remote/protocol.ts +348 -0
- package/packages/core/src/store/remote/server.ts +923 -0
- package/packages/core/src/store/remote/token.ts +137 -0
- package/packages/core/src/store/remote/version.ts +10 -0
- package/packages/core/src/store/retention.ts +91 -0
- package/packages/core/src/store/session-handle.ts +265 -0
- package/packages/core/src/store/streams/assemble.ts +79 -0
- package/packages/core/src/store/streams/lock.ts +42 -0
- package/packages/core/src/store/streams/session-writer.ts +306 -0
- package/packages/core/src/store/streams/types.ts +171 -0
- package/packages/core/src/store/streams/wait.ts +127 -0
- package/packages/core/src/store/subscribe.ts +105 -0
- package/packages/core/src/store/types.ts +466 -0
- package/packages/core/src/testing/fixed.ts +44 -0
- package/packages/core/src/testing/store-failures.ts +32 -0
- package/packages/core/src/zod-issues.ts +91 -0
- package/skills/code-review/SKILL.md +100 -0
- package/skills/finalize/SKILL.md +44 -0
- package/skills/guide/SKILL.md +1781 -0
- package/skills/guide/references/operator-api.md +164 -0
- package/skills/guide/references/plugin-authoring.md +354 -0
- package/skills/guide/references/remote-store-protocol.md +972 -0
- package/skills/guide/references/remote-ticket-protocol.md +79 -0
- package/skills/guide/references/setup.md +289 -0
- package/skills/guide/references/spec-standard.md +63 -0
- package/skills/guide/references/ticket-dependencies.md +59 -0
- package/skills/harvest/SKILL.md +66 -0
- package/skills/harvest-review/SKILL.md +63 -0
- package/skills/implement/SKILL.md +82 -0
- package/skills/plan/SKILL.md +123 -0
- package/skills/plan-review/SKILL.md +121 -0
- package/skills/reconcile/SKILL.md +65 -0
- package/skills/spec/SKILL.md +132 -0
- package/skills/tickets/SKILL.md +139 -0
- package/templates/autobuild.toml +36 -0
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AgentRuntimePluginFactory,
|
|
3
|
+
AutobuildPluginManifest,
|
|
4
|
+
ForgePluginFactory,
|
|
5
|
+
PluginApiCompatibility,
|
|
6
|
+
PluginContractDescriptor,
|
|
7
|
+
TicketSourcePluginFactory,
|
|
8
|
+
WorkspaceProviderPluginFactory,
|
|
9
|
+
} from './manifest'
|
|
10
|
+
import { pluginApiCompatibility } from './manifest'
|
|
11
|
+
import type { AgentRunnerContractFactory } from '../ports/runner/contract'
|
|
12
|
+
import type { TicketSourceContractFactory } from '../ports/tickets/contract'
|
|
13
|
+
import type { WorkspaceProviderContractFactory } from '../ports/workspace/contract'
|
|
14
|
+
import type { ForgeContractFactory } from '../ports/forge/contract'
|
|
15
|
+
|
|
16
|
+
export type PluginPort = 'ticket-source' | 'agent-runtime' | 'workspace-provider' | 'forge'
|
|
17
|
+
|
|
18
|
+
export const PLUGIN_PORTS: readonly PluginPort[] = [
|
|
19
|
+
'ticket-source',
|
|
20
|
+
'agent-runtime',
|
|
21
|
+
'workspace-provider',
|
|
22
|
+
'forge',
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
export type PluginResolutionKind = 'repo-path' | 'package'
|
|
26
|
+
|
|
27
|
+
export type RegistrationOwner =
|
|
28
|
+
| { kind: 'builtin'; name: 'autobuild' }
|
|
29
|
+
| { kind: 'plugin'; name: string }
|
|
30
|
+
|
|
31
|
+
export interface ConfiguredPluginSource {
|
|
32
|
+
kind: 'configured'
|
|
33
|
+
module: string
|
|
34
|
+
resolved: string
|
|
35
|
+
resolutionKind: PluginResolutionKind
|
|
36
|
+
pluginName: string
|
|
37
|
+
api: PluginApiCompatibility
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export type RegistrationSource = { kind: 'builtin' } | ConfiguredPluginSource
|
|
41
|
+
|
|
42
|
+
export interface AdapterRegistration<Factory, ContractFactory = unknown> {
|
|
43
|
+
owner: RegistrationOwner
|
|
44
|
+
/** Builtins reserve their names while host construction may remain outside
|
|
45
|
+
* the registry. Plugin registrations always carry a normalized factory for
|
|
46
|
+
* open selectors to consume lazily. */
|
|
47
|
+
factory?: Factory
|
|
48
|
+
/** Host-enforced ticket-source credential declarations. Undefined for
|
|
49
|
+
* builtins and every other plugin port. */
|
|
50
|
+
requiredEnv?: readonly string[]
|
|
51
|
+
contract?: PluginContractDescriptor<ContractFactory>
|
|
52
|
+
source: RegistrationSource
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface AdapterProjection {
|
|
56
|
+
port: PluginPort
|
|
57
|
+
name: string
|
|
58
|
+
owner: RegistrationOwner
|
|
59
|
+
source: RegistrationSource
|
|
60
|
+
hasContract: boolean
|
|
61
|
+
live: boolean
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const BUILTIN = { kind: 'builtin', name: 'autobuild' } as const
|
|
65
|
+
|
|
66
|
+
function reserved<Factory, ContractFactory>(
|
|
67
|
+
names: readonly string[],
|
|
68
|
+
): Map<string, AdapterRegistration<Factory, ContractFactory>> {
|
|
69
|
+
return new Map(names.map((name) => [name, { owner: BUILTIN, source: { kind: 'builtin' } }]))
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function pluginPortLabel(port: PluginPort): string {
|
|
73
|
+
switch (port) {
|
|
74
|
+
case 'ticket-source':
|
|
75
|
+
return 'ticket source'
|
|
76
|
+
case 'agent-runtime':
|
|
77
|
+
return 'agent runtime'
|
|
78
|
+
case 'workspace-provider':
|
|
79
|
+
return 'workspace provider'
|
|
80
|
+
case 'forge':
|
|
81
|
+
return 'forge'
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function ownerDescription(owner: RegistrationOwner): string {
|
|
86
|
+
return owner.kind === 'builtin' ? 'builtin adapter' : `plugin "${owner.name}"`
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
interface PendingRegistration {
|
|
90
|
+
port: PluginPort
|
|
91
|
+
name: string
|
|
92
|
+
factory: unknown
|
|
93
|
+
requiredEnv?: readonly string[]
|
|
94
|
+
contract?: PluginContractDescriptor<unknown>
|
|
95
|
+
target: Map<string, AdapterRegistration<unknown, unknown>>
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
type InternalRegistration<AdapterFactory, ContractFactory> =
|
|
99
|
+
| AdapterFactory
|
|
100
|
+
| {
|
|
101
|
+
factory: AdapterFactory
|
|
102
|
+
requiredEnv?: readonly string[]
|
|
103
|
+
contract?: PluginContractDescriptor<ContractFactory>
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function normalize<AdapterFactory, ContractFactory>(
|
|
107
|
+
registration: InternalRegistration<AdapterFactory, ContractFactory>,
|
|
108
|
+
): {
|
|
109
|
+
factory: AdapterFactory
|
|
110
|
+
requiredEnv?: readonly string[]
|
|
111
|
+
contract?: PluginContractDescriptor<ContractFactory>
|
|
112
|
+
} {
|
|
113
|
+
if (typeof registration === 'function') {
|
|
114
|
+
return { factory: registration as AdapterFactory }
|
|
115
|
+
}
|
|
116
|
+
const object = registration as {
|
|
117
|
+
factory: AdapterFactory
|
|
118
|
+
requiredEnv?: readonly string[]
|
|
119
|
+
contract?: PluginContractDescriptor<ContractFactory>
|
|
120
|
+
}
|
|
121
|
+
return {
|
|
122
|
+
factory: object.factory,
|
|
123
|
+
...(object.requiredEnv !== undefined ? { requiredEnv: [...object.requiredEnv] } : {}),
|
|
124
|
+
...(object.contract !== undefined ? { contract: object.contract } : {}),
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Owner-aware catalog populated at startup. Registration is atomic per
|
|
129
|
+
* plugin: every map and collision is checked before the first write. */
|
|
130
|
+
export class PluginRegistry {
|
|
131
|
+
readonly ticketSources = reserved<TicketSourcePluginFactory, TicketSourceContractFactory>([
|
|
132
|
+
'file',
|
|
133
|
+
'hosted',
|
|
134
|
+
'linear',
|
|
135
|
+
])
|
|
136
|
+
readonly agentRuntimes = reserved<AgentRuntimePluginFactory, AgentRunnerContractFactory>([
|
|
137
|
+
'claude',
|
|
138
|
+
'codex',
|
|
139
|
+
'pi',
|
|
140
|
+
// Init's independent author/reviewer arrangement is addressed by this
|
|
141
|
+
// profile value, so a runtime with the same name could never be selected.
|
|
142
|
+
'split',
|
|
143
|
+
])
|
|
144
|
+
readonly workspaceProviders = reserved<
|
|
145
|
+
WorkspaceProviderPluginFactory,
|
|
146
|
+
WorkspaceProviderContractFactory
|
|
147
|
+
>(['git-worktree', 'vercel-sandbox'])
|
|
148
|
+
readonly forges = reserved<ForgePluginFactory, ForgeContractFactory>(['github', 'local-git'])
|
|
149
|
+
|
|
150
|
+
register(
|
|
151
|
+
plugin: AutobuildPluginManifest,
|
|
152
|
+
configuredSource?: Omit<ConfiguredPluginSource, 'kind' | 'pluginName' | 'api'>,
|
|
153
|
+
): void {
|
|
154
|
+
const source: ConfiguredPluginSource = {
|
|
155
|
+
kind: 'configured',
|
|
156
|
+
module: configuredSource?.module ?? '<direct>',
|
|
157
|
+
resolved: configuredSource?.resolved ?? '<direct>',
|
|
158
|
+
resolutionKind: configuredSource?.resolutionKind ?? 'repo-path',
|
|
159
|
+
pluginName: plugin.name,
|
|
160
|
+
api: pluginApiCompatibility(plugin.apiVersion),
|
|
161
|
+
}
|
|
162
|
+
const pending: PendingRegistration[] = []
|
|
163
|
+
const collect = <AdapterFactory, ContractFactory>(
|
|
164
|
+
port: PluginPort,
|
|
165
|
+
registrations:
|
|
166
|
+
| Record<string, InternalRegistration<AdapterFactory, ContractFactory>>
|
|
167
|
+
| undefined,
|
|
168
|
+
target: Map<string, AdapterRegistration<AdapterFactory, ContractFactory>>,
|
|
169
|
+
): void => {
|
|
170
|
+
for (const [name, registration] of Object.entries(registrations ?? {})) {
|
|
171
|
+
const existing = target.get(name)
|
|
172
|
+
if (existing !== undefined) {
|
|
173
|
+
throw new Error(
|
|
174
|
+
`${pluginPortLabel(port)} adapter "${name}" from plugin "${plugin.name}" collides ` +
|
|
175
|
+
`with ${ownerDescription(existing.owner)}; choose a unique adapter name`,
|
|
176
|
+
)
|
|
177
|
+
}
|
|
178
|
+
const normalized = normalize(registration)
|
|
179
|
+
pending.push({
|
|
180
|
+
port,
|
|
181
|
+
name,
|
|
182
|
+
factory: normalized.factory,
|
|
183
|
+
...(normalized.requiredEnv !== undefined ? { requiredEnv: normalized.requiredEnv } : {}),
|
|
184
|
+
...(normalized.contract !== undefined
|
|
185
|
+
? { contract: normalized.contract as PluginContractDescriptor<unknown> }
|
|
186
|
+
: {}),
|
|
187
|
+
target: target as Map<string, AdapterRegistration<unknown, unknown>>,
|
|
188
|
+
})
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
collect(
|
|
193
|
+
'ticket-source',
|
|
194
|
+
plugin.ticketSources as
|
|
195
|
+
| Record<
|
|
196
|
+
string,
|
|
197
|
+
InternalRegistration<TicketSourcePluginFactory, TicketSourceContractFactory>
|
|
198
|
+
>
|
|
199
|
+
| undefined,
|
|
200
|
+
this.ticketSources,
|
|
201
|
+
)
|
|
202
|
+
collect('agent-runtime', plugin.agentRuntimes, this.agentRuntimes)
|
|
203
|
+
collect('workspace-provider', plugin.workspaceProviders, this.workspaceProviders)
|
|
204
|
+
collect('forge', plugin.forges, this.forges)
|
|
205
|
+
|
|
206
|
+
const seen = new Set<string>()
|
|
207
|
+
for (const registration of pending) {
|
|
208
|
+
const key = `${registration.port}\0${registration.name}`
|
|
209
|
+
if (seen.has(key)) {
|
|
210
|
+
throw new Error(
|
|
211
|
+
`${pluginPortLabel(registration.port)} adapter "${registration.name}" is registered ` +
|
|
212
|
+
`more than once by plugin "${plugin.name}"`,
|
|
213
|
+
)
|
|
214
|
+
}
|
|
215
|
+
seen.add(key)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
for (const registration of pending) {
|
|
219
|
+
registration.target.set(registration.name, {
|
|
220
|
+
owner: { kind: 'plugin', name: plugin.name },
|
|
221
|
+
factory: registration.factory,
|
|
222
|
+
...(registration.requiredEnv !== undefined
|
|
223
|
+
? { requiredEnv: registration.requiredEnv }
|
|
224
|
+
: {}),
|
|
225
|
+
...(registration.contract !== undefined ? { contract: registration.contract } : {}),
|
|
226
|
+
source,
|
|
227
|
+
})
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
adapters(port?: PluginPort): AdapterProjection[] {
|
|
232
|
+
const maps: Array<[PluginPort, Map<string, AdapterRegistration<unknown, unknown>>]> = [
|
|
233
|
+
['ticket-source', this.ticketSources as Map<string, AdapterRegistration<unknown, unknown>>],
|
|
234
|
+
['agent-runtime', this.agentRuntimes as Map<string, AdapterRegistration<unknown, unknown>>],
|
|
235
|
+
[
|
|
236
|
+
'workspace-provider',
|
|
237
|
+
this.workspaceProviders as Map<string, AdapterRegistration<unknown, unknown>>,
|
|
238
|
+
],
|
|
239
|
+
['forge', this.forges as Map<string, AdapterRegistration<unknown, unknown>>],
|
|
240
|
+
]
|
|
241
|
+
const result: AdapterProjection[] = []
|
|
242
|
+
for (const [candidate, registrations] of maps) {
|
|
243
|
+
if (port !== undefined && candidate !== port) continue
|
|
244
|
+
for (const [name, registration] of registrations) {
|
|
245
|
+
result.push({
|
|
246
|
+
port: candidate,
|
|
247
|
+
name,
|
|
248
|
+
owner: registration.owner,
|
|
249
|
+
source: registration.source,
|
|
250
|
+
hasContract: registration.contract !== undefined,
|
|
251
|
+
live: registration.contract?.live === true,
|
|
252
|
+
})
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return result
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
registration(port: PluginPort, name: string): AdapterRegistration<unknown, unknown> | undefined {
|
|
259
|
+
switch (port) {
|
|
260
|
+
case 'ticket-source':
|
|
261
|
+
return this.ticketSources.get(name)
|
|
262
|
+
case 'agent-runtime':
|
|
263
|
+
return this.agentRuntimes.get(name)
|
|
264
|
+
case 'workspace-provider':
|
|
265
|
+
return this.workspaceProviders.get(name)
|
|
266
|
+
case 'forge':
|
|
267
|
+
return this.forges.get(name)
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export function createPluginRegistry(): PluginRegistry {
|
|
273
|
+
return new PluginRegistry()
|
|
274
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { PluginFactoryContext } from './manifest'
|
|
2
|
+
import type { PluginRegistry } from './registry'
|
|
3
|
+
import { validateRuntimeRegistration, type RuntimeRegistry } from '../ports/runner/runtime'
|
|
4
|
+
|
|
5
|
+
export interface MaterializePluginRuntimesOpts {
|
|
6
|
+
repoRoot: string
|
|
7
|
+
env: Readonly<Record<string, string | undefined>>
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function reason(error: unknown): string {
|
|
11
|
+
return error instanceof Error ? error.message : String(error)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Materialize plugin runtime factories into a fresh registry. Factories run in
|
|
16
|
+
* plugin registration order and receive the same repository-scoped context as
|
|
17
|
+
* the other adapter selectors. The caller's registry is never mutated, and a
|
|
18
|
+
* failure never exposes a partially assembled registry.
|
|
19
|
+
*/
|
|
20
|
+
export async function materializePluginRuntimes(
|
|
21
|
+
builtins: RuntimeRegistry,
|
|
22
|
+
plugins: PluginRegistry,
|
|
23
|
+
opts: MaterializePluginRuntimesOpts,
|
|
24
|
+
): Promise<RuntimeRegistry> {
|
|
25
|
+
const pluginEntries = [...plugins.agentRuntimes.entries()].filter(
|
|
26
|
+
(entry) => entry[1].owner.kind === 'plugin',
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
// The normal PluginRegistry reserves shipped names. Keep this independent
|
|
30
|
+
// preflight for injected/test registries and future composition callers.
|
|
31
|
+
for (const [name, registration] of pluginEntries) {
|
|
32
|
+
if (Object.hasOwn(builtins, name)) {
|
|
33
|
+
throw new Error(
|
|
34
|
+
`agent runtime "${name}" from plugin "${registration.owner.name}" ` +
|
|
35
|
+
'collides with an existing runtime registration',
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const merged = Object.assign(Object.create(null) as RuntimeRegistry, builtins)
|
|
41
|
+
const config = Object.freeze({})
|
|
42
|
+
const env = Object.freeze({ ...opts.env })
|
|
43
|
+
const context: PluginFactoryContext = {
|
|
44
|
+
config,
|
|
45
|
+
env,
|
|
46
|
+
repoRoot: opts.repoRoot,
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
for (const [name, adapter] of pluginEntries) {
|
|
50
|
+
const owner = adapter.owner
|
|
51
|
+
if (owner.kind !== 'plugin' || adapter.factory === undefined) continue
|
|
52
|
+
|
|
53
|
+
let value: unknown
|
|
54
|
+
try {
|
|
55
|
+
value = await adapter.factory(context)
|
|
56
|
+
} catch (error) {
|
|
57
|
+
throw new Error(
|
|
58
|
+
`agent runtime "${name}" factory from plugin "${owner.name}" failed: ${reason(error)}`,
|
|
59
|
+
{ cause: error },
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
merged[name] = validateRuntimeRegistration(value)
|
|
65
|
+
} catch (error) {
|
|
66
|
+
throw new Error(
|
|
67
|
+
`agent runtime "${name}" from plugin "${owner.name}" returned an ` +
|
|
68
|
+
`invalid runtime registration: ${reason(error)}`,
|
|
69
|
+
{ cause: error },
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return merged
|
|
75
|
+
}
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import type { Forge, PrRef } from '../types'
|
|
3
|
+
|
|
4
|
+
export interface ForgeContractFactoryOptions {
|
|
5
|
+
/** Install/represent a real merge-blocking gate on the temporary base. */
|
|
6
|
+
gated?: boolean
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** Fixture controls arrange provider lifecycle that the Forge port does not
|
|
10
|
+
* expose. Probes are independent of the port call under assertion. */
|
|
11
|
+
export interface ForgeContractControls {
|
|
12
|
+
remoteHead(branch: string): Promise<string>
|
|
13
|
+
prepareMergeable(number: number): Promise<void>
|
|
14
|
+
closePr(number: number): Promise<void>
|
|
15
|
+
makeConflict(number: number): Promise<void>
|
|
16
|
+
advanceHead(number: number): Promise<string>
|
|
17
|
+
nativeAutoMergeEnabled(number: number): Promise<boolean>
|
|
18
|
+
commentExists(number: number, body: string): Promise<boolean>
|
|
19
|
+
mergeSha(number: number): Promise<string>
|
|
20
|
+
trackPr(number: number): Promise<void> | void
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface ForgeContractHarness {
|
|
24
|
+
forge: Forge
|
|
25
|
+
/** Whether this provider exposes a native gated auto-merge setting. Local
|
|
26
|
+
* forges may support only the universal ungated guarded-squash path. */
|
|
27
|
+
nativeAutoMerge?: boolean
|
|
28
|
+
workspacePath: string
|
|
29
|
+
head: string
|
|
30
|
+
base: string
|
|
31
|
+
title: string
|
|
32
|
+
body: string
|
|
33
|
+
controls: ForgeContractControls
|
|
34
|
+
cleanup?: () => Promise<void>
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type ForgeContractFactory = (
|
|
38
|
+
opts?: ForgeContractFactoryOptions,
|
|
39
|
+
) => Promise<ForgeContractHarness>
|
|
40
|
+
|
|
41
|
+
async function withForge(
|
|
42
|
+
factory: ForgeContractFactory,
|
|
43
|
+
opts: ForgeContractFactoryOptions | undefined,
|
|
44
|
+
run: (harness: ForgeContractHarness) => Promise<void>,
|
|
45
|
+
): Promise<void> {
|
|
46
|
+
const harness = await factory(opts)
|
|
47
|
+
let failure: unknown
|
|
48
|
+
try {
|
|
49
|
+
await run(harness)
|
|
50
|
+
} catch (error) {
|
|
51
|
+
failure = error
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
await harness.cleanup?.()
|
|
56
|
+
} catch (cleanupError) {
|
|
57
|
+
if (failure !== undefined) {
|
|
58
|
+
throw new AggregateError(
|
|
59
|
+
[failure, cleanupError],
|
|
60
|
+
'Forge contract assertion and cleanup both failed',
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
throw cleanupError
|
|
64
|
+
}
|
|
65
|
+
if (failure !== undefined) throw failure
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function pushAndOpen(harness: ForgeContractHarness): Promise<{
|
|
69
|
+
pr: PrRef
|
|
70
|
+
remoteHead: string
|
|
71
|
+
}> {
|
|
72
|
+
await harness.forge.pushBranch(harness.workspacePath, harness.head)
|
|
73
|
+
const remoteHead = await harness.controls.remoteHead(harness.head)
|
|
74
|
+
const pr = await harness.forge.openPr({
|
|
75
|
+
workspacePath: harness.workspacePath,
|
|
76
|
+
head: harness.head,
|
|
77
|
+
base: harness.base,
|
|
78
|
+
title: harness.title,
|
|
79
|
+
body: harness.body,
|
|
80
|
+
mergeMessage: `${harness.title}\n\n${harness.body}`,
|
|
81
|
+
})
|
|
82
|
+
await harness.controls.trackPr(pr.number)
|
|
83
|
+
return { pr, remoteHead }
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Reusable Forge semantics. Live fixtures manipulate only UUID-namespaced
|
|
88
|
+
* scratch branches/PRs; fake controls arrange the same states without changing
|
|
89
|
+
* the expected answers declared here.
|
|
90
|
+
*/
|
|
91
|
+
export function describeForgeContract(name: string, factory: ForgeContractFactory): void {
|
|
92
|
+
describe(`Forge contract: ${name}`, () => {
|
|
93
|
+
test('pushBranch publishes the head and openPr returns/adopts the actual PR', async () => {
|
|
94
|
+
await withForge(factory, undefined, async (harness) => {
|
|
95
|
+
const { pr, remoteHead } = await pushAndOpen(harness)
|
|
96
|
+
expect(pr.number).toBeGreaterThan(0)
|
|
97
|
+
expect(pr.url.trim()).not.toBe('')
|
|
98
|
+
expect(pr.headSha).toBe(remoteHead)
|
|
99
|
+
|
|
100
|
+
const retry = await harness.forge.openPr({
|
|
101
|
+
workspacePath: harness.workspacePath,
|
|
102
|
+
head: harness.head,
|
|
103
|
+
base: harness.base,
|
|
104
|
+
title: `${harness.title} retry must adopt`,
|
|
105
|
+
body: 'retry body must not create another PR',
|
|
106
|
+
mergeMessage: 'retry merge message must not replace the original',
|
|
107
|
+
})
|
|
108
|
+
expect(retry).toEqual(pr)
|
|
109
|
+
})
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
test('getPrState discriminates an open mergeable PR', async () => {
|
|
113
|
+
await withForge(factory, undefined, async (harness) => {
|
|
114
|
+
const { pr } = await pushAndOpen(harness)
|
|
115
|
+
await harness.controls.prepareMergeable(pr.number)
|
|
116
|
+
expect(await harness.forge.getPrState(harness.workspacePath, pr.number)).toEqual({
|
|
117
|
+
state: 'open',
|
|
118
|
+
mergeable: true,
|
|
119
|
+
})
|
|
120
|
+
})
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
test('getPrState discriminates an open conflict', async () => {
|
|
124
|
+
await withForge(factory, undefined, async (harness) => {
|
|
125
|
+
const { pr } = await pushAndOpen(harness)
|
|
126
|
+
await harness.controls.makeConflict(pr.number)
|
|
127
|
+
expect(await harness.forge.getPrState(harness.workspacePath, pr.number)).toEqual({
|
|
128
|
+
state: 'open',
|
|
129
|
+
mergeable: false,
|
|
130
|
+
})
|
|
131
|
+
})
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
test('getPrState discriminates a closed PR', async () => {
|
|
135
|
+
await withForge(factory, undefined, async (harness) => {
|
|
136
|
+
const { pr } = await pushAndOpen(harness)
|
|
137
|
+
await harness.controls.closePr(pr.number)
|
|
138
|
+
expect(await harness.forge.getPrState(harness.workspacePath, pr.number)).toEqual({
|
|
139
|
+
state: 'closed',
|
|
140
|
+
})
|
|
141
|
+
})
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
test('getPrState discriminates a merged PR with its independently observed landing SHA', async () => {
|
|
145
|
+
await withForge(factory, undefined, async (harness) => {
|
|
146
|
+
const { pr } = await pushAndOpen(harness)
|
|
147
|
+
await harness.controls.prepareMergeable(pr.number)
|
|
148
|
+
const candidate = await harness.forge.setAutoMerge(harness.workspacePath, pr.number, true)
|
|
149
|
+
expect(candidate).toEqual({ kind: 'ungated', headSha: pr.headSha })
|
|
150
|
+
await harness.forge.squashMerge(harness.workspacePath, pr.number, pr.headSha)
|
|
151
|
+
const landingSha = await harness.controls.mergeSha(pr.number)
|
|
152
|
+
expect(await harness.forge.getPrState(harness.workspacePath, pr.number)).toEqual({
|
|
153
|
+
state: 'merged',
|
|
154
|
+
sha: landingSha,
|
|
155
|
+
})
|
|
156
|
+
})
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
test('setAutoMerge acknowledges native state and is idempotent', async () => {
|
|
160
|
+
await withForge(factory, { gated: true }, async (harness) => {
|
|
161
|
+
if (harness.nativeAutoMerge === false) return
|
|
162
|
+
const { pr } = await pushAndOpen(harness)
|
|
163
|
+
await harness.controls.prepareMergeable(pr.number)
|
|
164
|
+
expect(await harness.controls.nativeAutoMergeEnabled(pr.number)).toBe(false)
|
|
165
|
+
|
|
166
|
+
expect(await harness.forge.setAutoMerge(harness.workspacePath, pr.number, true)).toEqual({
|
|
167
|
+
kind: 'applied',
|
|
168
|
+
})
|
|
169
|
+
expect(await harness.controls.nativeAutoMergeEnabled(pr.number)).toBe(true)
|
|
170
|
+
|
|
171
|
+
expect(await harness.forge.setAutoMerge(harness.workspacePath, pr.number, true)).toEqual({
|
|
172
|
+
kind: 'applied',
|
|
173
|
+
})
|
|
174
|
+
expect(await harness.controls.nativeAutoMergeEnabled(pr.number)).toBe(true)
|
|
175
|
+
|
|
176
|
+
expect(await harness.forge.setAutoMerge(harness.workspacePath, pr.number, false)).toEqual({
|
|
177
|
+
kind: 'applied',
|
|
178
|
+
})
|
|
179
|
+
expect(await harness.controls.nativeAutoMergeEnabled(pr.number)).toBe(false)
|
|
180
|
+
})
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
test('squashMerge rejects a moved head and merges only the matching head', async () => {
|
|
184
|
+
await withForge(factory, undefined, async (harness) => {
|
|
185
|
+
const { pr } = await pushAndOpen(harness)
|
|
186
|
+
await harness.controls.prepareMergeable(pr.number)
|
|
187
|
+
const inspected = await harness.forge.setAutoMerge(harness.workspacePath, pr.number, true)
|
|
188
|
+
expect(inspected).toEqual({ kind: 'ungated', headSha: pr.headSha })
|
|
189
|
+
|
|
190
|
+
const advancedHead = await harness.controls.advanceHead(pr.number)
|
|
191
|
+
expect(advancedHead).not.toBe(pr.headSha)
|
|
192
|
+
await expect(
|
|
193
|
+
harness.forge.squashMerge(harness.workspacePath, pr.number, pr.headSha),
|
|
194
|
+
).rejects.toThrow()
|
|
195
|
+
expect(await harness.forge.getPrState(harness.workspacePath, pr.number)).toEqual({
|
|
196
|
+
state: 'open',
|
|
197
|
+
mergeable: true,
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
const refreshed = await harness.forge.setAutoMerge(harness.workspacePath, pr.number, true)
|
|
201
|
+
expect(refreshed).toEqual({ kind: 'ungated', headSha: advancedHead })
|
|
202
|
+
await harness.forge.squashMerge(harness.workspacePath, pr.number, advancedHead)
|
|
203
|
+
const landingSha = await harness.controls.mergeSha(pr.number)
|
|
204
|
+
expect(await harness.forge.getPrState(harness.workspacePath, pr.number)).toEqual({
|
|
205
|
+
state: 'merged',
|
|
206
|
+
sha: landingSha,
|
|
207
|
+
})
|
|
208
|
+
})
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
test('closePr is idempotent and preserves merged PRs', async () => {
|
|
212
|
+
await withForge(factory, undefined, async (harness) => {
|
|
213
|
+
expect(harness.forge.closePr).toBeDefined()
|
|
214
|
+
const { pr } = await pushAndOpen(harness)
|
|
215
|
+
expect(await harness.forge.closePr!(harness.workspacePath, pr.number)).toEqual({
|
|
216
|
+
state: 'closed',
|
|
217
|
+
})
|
|
218
|
+
expect(await harness.forge.closePr!(harness.workspacePath, pr.number)).toEqual({
|
|
219
|
+
state: 'closed',
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
const merged = await pushAndOpen(harness)
|
|
223
|
+
await harness.controls.prepareMergeable(merged.pr.number)
|
|
224
|
+
const candidate = await harness.forge.setAutoMerge(
|
|
225
|
+
harness.workspacePath,
|
|
226
|
+
merged.pr.number,
|
|
227
|
+
true,
|
|
228
|
+
)
|
|
229
|
+
expect(candidate).toEqual({ kind: 'ungated', headSha: merged.pr.headSha })
|
|
230
|
+
await harness.forge.squashMerge(harness.workspacePath, merged.pr.number, merged.pr.headSha)
|
|
231
|
+
const sha = await harness.controls.mergeSha(merged.pr.number)
|
|
232
|
+
expect(await harness.forge.closePr!(harness.workspacePath, merged.pr.number)).toEqual({
|
|
233
|
+
state: 'merged',
|
|
234
|
+
sha,
|
|
235
|
+
})
|
|
236
|
+
})
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
test('deleteBranch removes the exact published branch and is idempotent', async () => {
|
|
240
|
+
await withForge(factory, undefined, async (harness) => {
|
|
241
|
+
expect(harness.forge.deleteBranch).toBeDefined()
|
|
242
|
+
await harness.forge.pushBranch(harness.workspacePath, harness.head)
|
|
243
|
+
await harness.forge.deleteBranch!(harness.workspacePath, harness.head)
|
|
244
|
+
await expect(harness.controls.remoteHead(harness.head)).rejects.toThrow()
|
|
245
|
+
await harness.forge.deleteBranch!(harness.workspacePath, harness.head)
|
|
246
|
+
})
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
test('commentOnPr delivers the exact unique body', async () => {
|
|
250
|
+
await withForge(factory, undefined, async (harness) => {
|
|
251
|
+
const { pr } = await pushAndOpen(harness)
|
|
252
|
+
const body = `Autobuild forge contract comment ${crypto.randomUUID()}`
|
|
253
|
+
await harness.forge.commentOnPr(harness.workspacePath, pr.number, body)
|
|
254
|
+
expect(await harness.controls.commentExists(pr.number, body)).toBe(true)
|
|
255
|
+
})
|
|
256
|
+
})
|
|
257
|
+
})
|
|
258
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { resolve } from 'node:path'
|
|
2
|
+
import type { PluginFactoryContext } from '../../plugins/manifest'
|
|
3
|
+
import type { AdapterRegistration, PluginRegistry } from '../../plugins/registry'
|
|
4
|
+
import type { Forge } from '../types'
|
|
5
|
+
import { GitHubForge } from './github'
|
|
6
|
+
import { LocalGitForge } from './local-git'
|
|
7
|
+
|
|
8
|
+
const EMPTY_CONFIG: Readonly<Record<string, unknown>> = Object.freeze({})
|
|
9
|
+
|
|
10
|
+
function reason(error: unknown): string {
|
|
11
|
+
return error instanceof Error ? error.message : String(error)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Resolve a forge selector without constructing the adapter. Composition
|
|
15
|
+
* boundaries use this before opening stores or invoking injectable wiring. */
|
|
16
|
+
export function resolveForgeRegistration(
|
|
17
|
+
name: string,
|
|
18
|
+
registry: PluginRegistry,
|
|
19
|
+
): AdapterRegistration<(context: PluginFactoryContext) => Forge | Promise<Forge>> {
|
|
20
|
+
const registration = registry.forges.get(name)
|
|
21
|
+
if (registration !== undefined) return registration
|
|
22
|
+
|
|
23
|
+
const available = [...registry.forges.keys()].sort()
|
|
24
|
+
throw new Error(
|
|
25
|
+
`unknown forge adapter ${JSON.stringify(name)}; available forges: ${available.join(', ')}`,
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Construct the configured Forge lazily from the validated startup catalog.
|
|
30
|
+
* The selected object is returned by identity so optional capabilities such as
|
|
31
|
+
* `prAttachments` remain authoritative. */
|
|
32
|
+
export async function createForge(opts: {
|
|
33
|
+
name: string
|
|
34
|
+
registry: PluginRegistry
|
|
35
|
+
env: Readonly<Record<string, string | undefined>>
|
|
36
|
+
repoRoot: string
|
|
37
|
+
/** Explicit repository identity (normalized origin). Origin-mode dispatch
|
|
38
|
+
* passes it so the builtin GitHub forge never probes a local checkout. */
|
|
39
|
+
repository?: string
|
|
40
|
+
}): Promise<Forge> {
|
|
41
|
+
const registration = resolveForgeRegistration(opts.name, opts.registry)
|
|
42
|
+
if (registration.owner.kind === 'builtin') {
|
|
43
|
+
if (opts.name === 'github') {
|
|
44
|
+
return new GitHubForge({
|
|
45
|
+
env: opts.env,
|
|
46
|
+
repoRoot: opts.repoRoot,
|
|
47
|
+
...(opts.repository !== undefined ? { repository: opts.repository } : {}),
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
if (opts.name === 'local-git') return new LocalGitForge()
|
|
51
|
+
throw new Error(`builtin forge adapter ${JSON.stringify(opts.name)} has no constructor`)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (registration.factory === undefined) {
|
|
55
|
+
throw new Error(
|
|
56
|
+
`forge adapter ${JSON.stringify(opts.name)} from plugin ` +
|
|
57
|
+
`${JSON.stringify(registration.owner.name)} has no factory`,
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
return await registration.factory({
|
|
63
|
+
config: EMPTY_CONFIG,
|
|
64
|
+
env: opts.env,
|
|
65
|
+
repoRoot: resolve(opts.repoRoot),
|
|
66
|
+
})
|
|
67
|
+
} catch (error) {
|
|
68
|
+
throw new Error(
|
|
69
|
+
`forge adapter ${JSON.stringify(opts.name)} from plugin ` +
|
|
70
|
+
`${JSON.stringify(registration.owner.name)} failed to initialize: ${reason(error)}`,
|
|
71
|
+
{ cause: error },
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
}
|