@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,279 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ab mcp` — the first binding of the agent tool registry (AUT-338): the
|
|
3
|
+
* registry served as an MCP server over stdio, using the official MCP
|
|
4
|
+
* TypeScript SDK's current major.
|
|
5
|
+
*
|
|
6
|
+
* The CLI remains the primary local interface for agents that have a shell.
|
|
7
|
+
* The stdio binding exists so the registry can be exercised end to end from a
|
|
8
|
+
* local install, for dogfooding, and for local agents that cannot run
|
|
9
|
+
* commands. Every tool is registered from the registry's closed table — name,
|
|
10
|
+
* description, JSON-schema input, annotations, handler — so this surface can
|
|
11
|
+
* never drift from the in-process registry the contract suite proves.
|
|
12
|
+
*
|
|
13
|
+
* Identity: the sessionless operator identity the other operator commands use
|
|
14
|
+
* (`buildControlUser` — USER/USERNAME, else "dashboard"), with a via marker
|
|
15
|
+
* naming the connected MCP client (learned lazily at the initialize handshake).
|
|
16
|
+
* The server fails closed inside a phase: repository-wide operator authority
|
|
17
|
+
* cannot be narrowed to the ambient build (the §8.2 rule that makes
|
|
18
|
+
* repository-wide `ab builds` fail inside a phase, moved to process start
|
|
19
|
+
* because a server cannot scope per request).
|
|
20
|
+
*/
|
|
21
|
+
import { join } from 'node:path'
|
|
22
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
|
|
23
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
24
|
+
import { loadConfig } from '../config/load'
|
|
25
|
+
import type { Config } from '../config/schema'
|
|
26
|
+
import type { Via } from '../events/envelope'
|
|
27
|
+
import { defaultTriageState } from '../processes/dispatcher'
|
|
28
|
+
import { createWorkspaceProvider } from '../ports/workspace/create'
|
|
29
|
+
import { createTicketSource } from '../ports/tickets/create'
|
|
30
|
+
import { loadPlugins } from '../plugins/load'
|
|
31
|
+
import { AUTOBUILD_VERSION } from '../store/remote/version'
|
|
32
|
+
import type { Clock } from '../store/types'
|
|
33
|
+
import { buildControlUser } from './build-control'
|
|
34
|
+
import type { Exec } from '../ports/workspace/git-worktree'
|
|
35
|
+
import { openSessionlessStore, type StoreOpener } from './store-opening'
|
|
36
|
+
import { InvalidAmbientContextError, resolveAmbientReadSession } from './env'
|
|
37
|
+
import { buildRegistry, RegistryError, type OperatorToolRegistry } from '../operator/registry'
|
|
38
|
+
import { createOperatorSandboxService, type OperatorSandboxService } from '../operator/sandbox'
|
|
39
|
+
import type { OperatorTicketBackend } from '../operator/tickets'
|
|
40
|
+
|
|
41
|
+
export const AB_MCP_USAGE = 'usage: ab mcp [--store <ref>] [--repo <id>] (§8.2)'
|
|
42
|
+
|
|
43
|
+
export const AB_MCP_PHASE_REFUSAL =
|
|
44
|
+
'ab mcp serves repository-wide operator authority and cannot run inside a phase session (§8.2)'
|
|
45
|
+
|
|
46
|
+
const SERVER_INSTRUCTIONS = [
|
|
47
|
+
'Autobuild operator tools for one repository: builds, repository and harvest controls, tickets, and operator notes.',
|
|
48
|
+
'The `ab` CLI is the primary local interface for agents that have a shell; these tools mirror its operator commands',
|
|
49
|
+
'and the operator API routes — every result equals the corresponding route. Prefer the CLI when a command exists;',
|
|
50
|
+
'use these tools when no shell is available, for dogfooding, and for parity testing of the tool registry.',
|
|
51
|
+
'Every tool names its repository explicitly via `repo`; mutating tools append durable, human-attributed events.',
|
|
52
|
+
].join(' ')
|
|
53
|
+
|
|
54
|
+
export interface AbMcpOpts {
|
|
55
|
+
targetRepo: string
|
|
56
|
+
env: Record<string, string | undefined>
|
|
57
|
+
exec: Exec
|
|
58
|
+
stdout: (line: string) => void
|
|
59
|
+
stderr: (line: string) => void
|
|
60
|
+
/** Explicit --store; selection remains --store > AB_STORE > repo-local. */
|
|
61
|
+
storeRef?: string
|
|
62
|
+
/** Explicit --repo: refuse calls targeting any other repository. */
|
|
63
|
+
repo?: string
|
|
64
|
+
/** Injectable adapter seam for tests; production composes the real adapter. */
|
|
65
|
+
openStore?: StoreOpener
|
|
66
|
+
/** Injectable ticket-source factory for tests. */
|
|
67
|
+
ticketSourceFactory?: typeof createTicketSource
|
|
68
|
+
clock?: Clock
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* The operator ticket backend for the stdio binding: one TicketSource built
|
|
73
|
+
* once from the checkout's own `[tickets]` config. `statesFor` returns the
|
|
74
|
+
* configured lifecycle names; `sourceFor` cross-checks the effective
|
|
75
|
+
* config's teamKey against this startup config and refuses on drift.
|
|
76
|
+
*/
|
|
77
|
+
export async function buildMcpTicketBackend(
|
|
78
|
+
opts: AbMcpOpts & { checkout: string; localStateRoot: string },
|
|
79
|
+
): Promise<OperatorTicketBackend> {
|
|
80
|
+
const configPath = join(opts.checkout, 'autobuild.toml')
|
|
81
|
+
let config: Config
|
|
82
|
+
try {
|
|
83
|
+
config = await loadConfig(configPath)
|
|
84
|
+
} catch (error) {
|
|
85
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
|
|
86
|
+
throw new Error(
|
|
87
|
+
`${configPath}: not found — 'ab mcp' reads autobuild.toml from the resolved Git main checkout (SPEC §8.8)`,
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
throw error
|
|
91
|
+
}
|
|
92
|
+
const plugins = await loadPlugins(config.plugins, opts.checkout)
|
|
93
|
+
const factory = opts.ticketSourceFactory ?? createTicketSource
|
|
94
|
+
const source = await factory(
|
|
95
|
+
config.tickets,
|
|
96
|
+
opts.env,
|
|
97
|
+
opts.checkout,
|
|
98
|
+
opts.localStateRoot,
|
|
99
|
+
plugins,
|
|
100
|
+
)
|
|
101
|
+
const states = [
|
|
102
|
+
...new Set(
|
|
103
|
+
[
|
|
104
|
+
defaultTriageState(config),
|
|
105
|
+
config.tickets.readyState,
|
|
106
|
+
config.tickets.claimedState,
|
|
107
|
+
config.tickets.createState,
|
|
108
|
+
].filter((state): state is string => state !== undefined),
|
|
109
|
+
),
|
|
110
|
+
]
|
|
111
|
+
return {
|
|
112
|
+
sourceFor: async (context) => {
|
|
113
|
+
if (config.tickets.teamKey !== context.teamKey) {
|
|
114
|
+
throw new Error(
|
|
115
|
+
`the repository's effective ticket config names team "${context.teamKey}", but this checkout's autobuild.toml names team "${String(config.tickets.teamKey)}"; refusing to serve tickets across that drift — run ab mcp from the checkout dispatch uses`,
|
|
116
|
+
)
|
|
117
|
+
}
|
|
118
|
+
return source
|
|
119
|
+
},
|
|
120
|
+
statesFor: async () => states,
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Map every registry entry onto the MCP server: name, description (with the
|
|
126
|
+
* output description appended), input schema, annotations, and a handler that
|
|
127
|
+
* routes through registry.call so validation, identity enforcement, and
|
|
128
|
+
* failure-body mapping stay identical across bindings. Successes and failures
|
|
129
|
+
* both surface as JSON text content; failures additionally carry isError.
|
|
130
|
+
*/
|
|
131
|
+
export function serveRegistry(
|
|
132
|
+
registry: OperatorToolRegistry,
|
|
133
|
+
server: McpServer,
|
|
134
|
+
ctx: { identity?: string; via?: Via | (() => Via | undefined) },
|
|
135
|
+
): void {
|
|
136
|
+
for (const entry of registry.entries) {
|
|
137
|
+
server.registerTool(
|
|
138
|
+
entry.name,
|
|
139
|
+
{
|
|
140
|
+
description: `${entry.description}\n\nReturns: ${entry.outputDescription}`,
|
|
141
|
+
inputSchema: entry.inputSchema,
|
|
142
|
+
annotations: entry.annotations,
|
|
143
|
+
},
|
|
144
|
+
// The SDK validates args against the entry's schema before this runs;
|
|
145
|
+
// registry.call re-validates so every binding shares one enforcement
|
|
146
|
+
// path.
|
|
147
|
+
async (args: unknown) => {
|
|
148
|
+
try {
|
|
149
|
+
// A lazy via resolves per call — the stdio server is long-lived and
|
|
150
|
+
// only learns the client's identity at the initialize handshake.
|
|
151
|
+
const via = typeof ctx.via === 'function' ? ctx.via() : ctx.via
|
|
152
|
+
const value = await registry.call(entry.name, args, {
|
|
153
|
+
identity: ctx.identity,
|
|
154
|
+
...(via !== undefined ? { via } : {}),
|
|
155
|
+
})
|
|
156
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify(value) }] }
|
|
157
|
+
} catch (error) {
|
|
158
|
+
const body =
|
|
159
|
+
error instanceof RegistryError
|
|
160
|
+
? error.body
|
|
161
|
+
: { kind: 'internal' as const, error: 'operator tool is unavailable' }
|
|
162
|
+
return {
|
|
163
|
+
content: [{ type: 'text' as const, text: JSON.stringify(body) }],
|
|
164
|
+
isError: true,
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
)
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** The `ab mcp` command core. Returns the process exit code. */
|
|
173
|
+
export async function abMcp(opts: AbMcpOpts): Promise<number> {
|
|
174
|
+
// Fail closed before any store open: a complete ambient tuple (or a
|
|
175
|
+
// malformed one) must not start a repository-wide server.
|
|
176
|
+
let ambient: unknown
|
|
177
|
+
try {
|
|
178
|
+
ambient = resolveAmbientReadSession(opts.env)
|
|
179
|
+
} catch (error) {
|
|
180
|
+
if (error instanceof InvalidAmbientContextError) {
|
|
181
|
+
opts.stderr(`${AB_MCP_PHASE_REFUSAL} ${error.message}`)
|
|
182
|
+
return 1
|
|
183
|
+
}
|
|
184
|
+
throw error
|
|
185
|
+
}
|
|
186
|
+
if (ambient !== undefined) {
|
|
187
|
+
opts.stderr(AB_MCP_PHASE_REFUSAL)
|
|
188
|
+
return 1
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const context = await openSessionlessStore({
|
|
192
|
+
targetRepo: opts.targetRepo,
|
|
193
|
+
env: opts.env,
|
|
194
|
+
exec: opts.exec,
|
|
195
|
+
...(opts.storeRef !== undefined ? { storeRef: opts.storeRef } : {}),
|
|
196
|
+
...(opts.openStore !== undefined ? { openStore: opts.openStore } : {}),
|
|
197
|
+
})
|
|
198
|
+
try {
|
|
199
|
+
const backend = await buildMcpTicketBackend({
|
|
200
|
+
...opts,
|
|
201
|
+
checkout: context.checkout,
|
|
202
|
+
localStateRoot: context.localStateRoot,
|
|
203
|
+
})
|
|
204
|
+
// The operator-sandbox backend (AUT-340) is built only when the
|
|
205
|
+
// repository enables it. A construction failure is contained: the
|
|
206
|
+
// server still serves every other tool, one diagnostic names the
|
|
207
|
+
// failure, and the sandbox tools are absent from advertisement AND
|
|
208
|
+
// dispatch (never broken).
|
|
209
|
+
let sandbox: OperatorSandboxService | undefined
|
|
210
|
+
try {
|
|
211
|
+
const configPath = join(context.checkout, 'autobuild.toml')
|
|
212
|
+
let mcpConfig: Config
|
|
213
|
+
try {
|
|
214
|
+
mcpConfig = await loadConfig(configPath)
|
|
215
|
+
} catch (error) {
|
|
216
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
|
|
217
|
+
throw new Error(
|
|
218
|
+
`${configPath}: not found — 'ab mcp' reads autobuild.toml from the resolved Git main checkout (SPEC §8.8)`,
|
|
219
|
+
)
|
|
220
|
+
}
|
|
221
|
+
throw error
|
|
222
|
+
}
|
|
223
|
+
if (mcpConfig.orchestrator.enabled) {
|
|
224
|
+
const provider = await createWorkspaceProvider(mcpConfig.workspace, {
|
|
225
|
+
registry: await loadPlugins(mcpConfig.plugins, context.checkout),
|
|
226
|
+
worktreeRoot: context.worktreeRoot,
|
|
227
|
+
repoRoot: context.checkout,
|
|
228
|
+
env: opts.env,
|
|
229
|
+
storeRef: context.storeRef,
|
|
230
|
+
...(context.token !== undefined ? { storeToken: context.token } : {}),
|
|
231
|
+
sandboxSetupCommand: mcpConfig.commands.setup,
|
|
232
|
+
sandboxRoot: join(context.localStateRoot, 'orchestrator-sandboxes'),
|
|
233
|
+
sandboxEnvironmentVariables: mcpConfig.orchestrator.sandbox.environmentVariables,
|
|
234
|
+
})
|
|
235
|
+
sandbox = await createOperatorSandboxService({
|
|
236
|
+
store: context.store,
|
|
237
|
+
repo: context.repo,
|
|
238
|
+
provider,
|
|
239
|
+
sandbox: mcpConfig.orchestrator.sandbox,
|
|
240
|
+
baseBranch: mcpConfig.baseBranch,
|
|
241
|
+
...(opts.clock !== undefined ? { clock: opts.clock } : {}),
|
|
242
|
+
})
|
|
243
|
+
}
|
|
244
|
+
} catch (error) {
|
|
245
|
+
opts.stderr(
|
|
246
|
+
`ab mcp: operator sandbox backend unavailable, sandbox tools omitted: ${error instanceof Error ? error.message : String(error)}`,
|
|
247
|
+
)
|
|
248
|
+
sandbox = undefined
|
|
249
|
+
}
|
|
250
|
+
const registry = buildRegistry({
|
|
251
|
+
store: context.store,
|
|
252
|
+
tickets: backend,
|
|
253
|
+
...(sandbox !== undefined ? { sandbox } : {}),
|
|
254
|
+
clock: opts.clock,
|
|
255
|
+
...(opts.repo !== undefined ? { allowedRepo: opts.repo } : {}),
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
const server = new McpServer(
|
|
259
|
+
{ name: 'autobuild', version: AUTOBUILD_VERSION },
|
|
260
|
+
{ instructions: SERVER_INSTRUCTIONS },
|
|
261
|
+
)
|
|
262
|
+
serveRegistry(registry, server, {
|
|
263
|
+
identity: buildControlUser(opts.env),
|
|
264
|
+
// Unlike the hosted stateless server, the stdio server is long-lived:
|
|
265
|
+
// the initialize handshake populates the client version before any tool
|
|
266
|
+
// call, so the via marker names the connected client.
|
|
267
|
+
via: () => ({ kind: 'mcp', client: server.server.getClientVersion()?.name ?? 'stdio' }),
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
const done = new Promise<void>((resolve) => {
|
|
271
|
+
server.server.onclose = () => resolve()
|
|
272
|
+
})
|
|
273
|
+
await server.connect(new StdioServerTransport())
|
|
274
|
+
await done
|
|
275
|
+
return 0
|
|
276
|
+
} finally {
|
|
277
|
+
await context.store.close()
|
|
278
|
+
}
|
|
279
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/** `ab models` lists the catalog resolved by the operator's local Pi CLI. */
|
|
2
|
+
import { checkLocalPi } from '../ports/runner/pi'
|
|
3
|
+
import { readLocalPiCatalog } from '../ports/runner/pi-rpc'
|
|
4
|
+
|
|
5
|
+
/** One catalog entry — a provider-qualified id, split for display. */
|
|
6
|
+
export interface PiCatalogEntry {
|
|
7
|
+
provider: string
|
|
8
|
+
id: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Fetch Pi's model catalog. `availableOnly` narrows to models whose provider
|
|
13
|
+
* credentials are configured (a network/credential check); otherwise the full
|
|
14
|
+
* offline catalog is returned.
|
|
15
|
+
*/
|
|
16
|
+
export type PiModelCatalogFn = (opts: { availableOnly: boolean }) => Promise<PiCatalogEntry[]>
|
|
17
|
+
|
|
18
|
+
const defaultCatalog: PiModelCatalogFn = async ({ availableOnly }) => {
|
|
19
|
+
const env = Object.fromEntries(
|
|
20
|
+
Object.entries(process.env).filter(
|
|
21
|
+
(entry): entry is [string, string] => entry[1] !== undefined,
|
|
22
|
+
),
|
|
23
|
+
)
|
|
24
|
+
await checkLocalPi(process.cwd(), env)
|
|
25
|
+
return readLocalPiCatalog({ cwd: process.cwd(), env, availableOnly })
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface AbModelsOptions {
|
|
29
|
+
/** Case-insensitive substring over `provider/id`; absent ⇒ list everything. */
|
|
30
|
+
query?: string
|
|
31
|
+
/** Restrict to models with configured credentials. */
|
|
32
|
+
availableOnly: boolean
|
|
33
|
+
stdout: (line: string) => void
|
|
34
|
+
/** Injectable for tests; defaults to the local Pi CLI catalog. */
|
|
35
|
+
catalog?: PiModelCatalogFn
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function abModels(opts: AbModelsOptions): Promise<void> {
|
|
39
|
+
const catalog = opts.catalog ?? defaultCatalog
|
|
40
|
+
const entries = await catalog({ availableOnly: opts.availableOnly })
|
|
41
|
+
|
|
42
|
+
const needle = opts.query?.toLowerCase()
|
|
43
|
+
const matched = (
|
|
44
|
+
needle === undefined
|
|
45
|
+
? entries
|
|
46
|
+
: entries.filter((e) => `${e.provider}/${e.id}`.toLowerCase().includes(needle))
|
|
47
|
+
).sort((a, b) => `${a.provider}/${a.id}`.localeCompare(`${b.provider}/${b.id}`))
|
|
48
|
+
|
|
49
|
+
if (matched.length === 0) {
|
|
50
|
+
opts.stdout(
|
|
51
|
+
opts.query !== undefined
|
|
52
|
+
? `no Pi models match "${opts.query}"${opts.availableOnly ? ' with configured credentials' : ''}`
|
|
53
|
+
: 'no Pi models found',
|
|
54
|
+
)
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
for (const e of matched) {
|
|
59
|
+
opts.stdout(`${e.provider}/${e.id}`)
|
|
60
|
+
}
|
|
61
|
+
opts.stdout('')
|
|
62
|
+
opts.stdout(
|
|
63
|
+
`${matched.length} model(s) — paste a provider-qualified id into autobuild.toml [roles.default] or a concrete [roles.<name>] entry.`,
|
|
64
|
+
)
|
|
65
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ab observe` (SPEC §8.2, §12): structured observations at the point of
|
|
3
|
+
* capture — followup | refactor | latent-bug, with file/ticket refs — so
|
|
4
|
+
* `harvest` clusters records, not prose mined from a markdown file.
|
|
5
|
+
*
|
|
6
|
+
* NOT a terminal: usable in any phase, at any time, any number of times.
|
|
7
|
+
* The id is kernel-assigned at deposit, like all ids (§15.4).
|
|
8
|
+
*/
|
|
9
|
+
import type { EventEnvelope } from '../events/catalog'
|
|
10
|
+
import { agentActor } from '../events/envelope'
|
|
11
|
+
import type { IdSource } from '../ids'
|
|
12
|
+
import { observationKindSchema } from '../ontology'
|
|
13
|
+
import type { BuildStore } from '../store/types'
|
|
14
|
+
import type { CliEnv } from './env'
|
|
15
|
+
|
|
16
|
+
export interface ObserveDeps {
|
|
17
|
+
store: BuildStore
|
|
18
|
+
env: CliEnv
|
|
19
|
+
ids: IdSource
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ObserveOpts {
|
|
23
|
+
kind: string
|
|
24
|
+
summary: string
|
|
25
|
+
files?: string[]
|
|
26
|
+
refs?: string[]
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function observe(
|
|
30
|
+
deps: ObserveDeps,
|
|
31
|
+
opts: ObserveOpts,
|
|
32
|
+
): Promise<EventEnvelope<'observation.recorded'>> {
|
|
33
|
+
const kind = observationKindSchema.safeParse(opts.kind)
|
|
34
|
+
if (!kind.success) {
|
|
35
|
+
throw new Error(
|
|
36
|
+
`--kind "${opts.kind}" is not an observation kind — expected ` +
|
|
37
|
+
'followup | refactor | latent-bug (§8.2)',
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
if (opts.summary.trim() === '') {
|
|
41
|
+
throw new Error("'ab observe' requires a non-empty <summary> (§8.2)")
|
|
42
|
+
}
|
|
43
|
+
const { env } = deps
|
|
44
|
+
return deps.store.append(env.build, {
|
|
45
|
+
actor: agentActor(env.phase, env.session),
|
|
46
|
+
type: 'observation.recorded',
|
|
47
|
+
payload: {
|
|
48
|
+
id: deps.ids('obs'),
|
|
49
|
+
kind: kind.data,
|
|
50
|
+
summary: opts.summary,
|
|
51
|
+
...(opts.files !== undefined && opts.files.length > 0 ? { files: opts.files } : {}),
|
|
52
|
+
...(opts.refs !== undefined && opts.refs.length > 0 ? { refs: opts.refs } : {}),
|
|
53
|
+
},
|
|
54
|
+
})
|
|
55
|
+
}
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { join } from 'node:path'
|
|
2
|
+
import { loadConfig } from '../config/load'
|
|
3
|
+
import { CONTRACT_ADAPTER_ENV, CONTRACT_PORT_ENV, CONTRACT_REPO_ENV } from '../plugins/contract-env'
|
|
4
|
+
import { diagnosePlugins, type PluginDiagnosis } from '../plugins/load'
|
|
5
|
+
import {
|
|
6
|
+
PLUGIN_PORTS,
|
|
7
|
+
pluginPortLabel,
|
|
8
|
+
type AdapterProjection,
|
|
9
|
+
type PluginPort,
|
|
10
|
+
} from '../plugins/registry'
|
|
11
|
+
import type { Exec } from '../ports/workspace/git-worktree'
|
|
12
|
+
import { resolveMainRepo } from './repo-state'
|
|
13
|
+
|
|
14
|
+
export interface PluginContractProcessInput {
|
|
15
|
+
repoRoot: string
|
|
16
|
+
port: PluginPort
|
|
17
|
+
adapter: string
|
|
18
|
+
env: Record<string, string | undefined>
|
|
19
|
+
stdout: (line: string) => void
|
|
20
|
+
stderr: (line: string) => void
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type PluginContractSubprocess = (input: PluginContractProcessInput) => Promise<number>
|
|
24
|
+
|
|
25
|
+
export interface PluginCliOpts {
|
|
26
|
+
targetRepo: string
|
|
27
|
+
env: Record<string, string | undefined>
|
|
28
|
+
exec: Exec
|
|
29
|
+
stdout: (line: string) => void
|
|
30
|
+
stderr: (line: string) => void
|
|
31
|
+
subprocess?: PluginContractSubprocess
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function definedEnv(env: Record<string, string | undefined>): Record<string, string> {
|
|
35
|
+
return Object.fromEntries(
|
|
36
|
+
Object.entries(env).filter((entry): entry is [string, string] => entry[1] !== undefined),
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function forwardText(text: string, sink: (line: string) => void): void {
|
|
41
|
+
const normalized = text.endsWith('\n') ? text.slice(0, -1) : text
|
|
42
|
+
if (normalized === '') return
|
|
43
|
+
for (const line of normalized.split('\n')) sink(line)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const spawnPluginContract: PluginContractSubprocess = async (input) => {
|
|
47
|
+
const entry = join(import.meta.dir, '..', 'plugins', 'contract-entry.ts')
|
|
48
|
+
// Use the current Bun executable by absolute path: the plugin's declared
|
|
49
|
+
// environment may intentionally omit PATH, but contract execution must not
|
|
50
|
+
// depend on executable lookup inside that replacement environment.
|
|
51
|
+
const child = Bun.spawn([process.execPath, 'test', entry], {
|
|
52
|
+
cwd: input.repoRoot,
|
|
53
|
+
env: {
|
|
54
|
+
...definedEnv(input.env),
|
|
55
|
+
[CONTRACT_REPO_ENV]: input.repoRoot,
|
|
56
|
+
[CONTRACT_PORT_ENV]: input.port,
|
|
57
|
+
[CONTRACT_ADAPTER_ENV]: input.adapter,
|
|
58
|
+
},
|
|
59
|
+
stdout: 'pipe',
|
|
60
|
+
stderr: 'pipe',
|
|
61
|
+
})
|
|
62
|
+
const [stdout, stderr, status] = await Promise.all([
|
|
63
|
+
new Response(child.stdout).text(),
|
|
64
|
+
new Response(child.stderr).text(),
|
|
65
|
+
child.exited,
|
|
66
|
+
])
|
|
67
|
+
forwardText(stdout, input.stdout)
|
|
68
|
+
forwardText(stderr, input.stderr)
|
|
69
|
+
return status
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function renderModuleReport(report: PluginDiagnosis['reports'][number]): string {
|
|
73
|
+
const location = report.resolved === undefined ? '' : ` resolved=${report.resolved}`
|
|
74
|
+
const source = report.resolvedFrom === undefined ? '' : ` from=${report.resolvedFrom}`
|
|
75
|
+
const plugin = report.pluginName === undefined ? '' : ` plugin=${report.pluginName}`
|
|
76
|
+
const api =
|
|
77
|
+
report.api === undefined
|
|
78
|
+
? ''
|
|
79
|
+
: ` api=${report.api.declaredRange} host=${report.api.hostVersion} ${report.api.status}`
|
|
80
|
+
return report.status === 'loaded'
|
|
81
|
+
? `OK ${report.module} kind=${report.resolutionKind}${location}${source}${plugin}${api}`
|
|
82
|
+
: `FAIL ${report.module} kind=${report.resolutionKind} stage=${report.stage}${location}${source}${plugin}${api}: ${report.error}`
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function renderAdapter(adapter: AdapterProjection): string {
|
|
86
|
+
const contract = adapter.hasContract
|
|
87
|
+
? adapter.live
|
|
88
|
+
? 'contract=live'
|
|
89
|
+
: 'contract=available'
|
|
90
|
+
: 'contract=missing'
|
|
91
|
+
if (adapter.source.kind === 'builtin') {
|
|
92
|
+
return ` ${adapter.name} owner=builtin module=(builtin) resolution=builtin api=host-compatible ${contract}`
|
|
93
|
+
}
|
|
94
|
+
const { source } = adapter
|
|
95
|
+
return (
|
|
96
|
+
` ${adapter.name} owner=plugin:${source.pluginName} module=${source.module} ` +
|
|
97
|
+
`resolution=${source.resolutionKind} resolved=${source.resolved} ` +
|
|
98
|
+
`api=${source.api.declaredRange} host=${source.api.hostVersion} ${source.api.status} ${contract}`
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function contractField(port: PluginPort, adapter: string): string {
|
|
103
|
+
const map: Record<PluginPort, string> = {
|
|
104
|
+
'ticket-source': 'ticketSources',
|
|
105
|
+
'agent-runtime': 'agentRuntimes',
|
|
106
|
+
'workspace-provider': 'workspaceProviders',
|
|
107
|
+
forge: 'forges',
|
|
108
|
+
}
|
|
109
|
+
return `${map[port]}.${adapter}.contract.factory`
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function loadDiagnosis(opts: PluginCliOpts): Promise<{
|
|
113
|
+
repoRoot: string
|
|
114
|
+
diagnosis: PluginDiagnosis
|
|
115
|
+
}> {
|
|
116
|
+
const repoRoot = await resolveMainRepo(opts.targetRepo, opts.exec)
|
|
117
|
+
const configPath = join(repoRoot, 'autobuild.toml')
|
|
118
|
+
let config: Awaited<ReturnType<typeof loadConfig>>
|
|
119
|
+
try {
|
|
120
|
+
config = await loadConfig(configPath)
|
|
121
|
+
} catch (error) {
|
|
122
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
|
|
123
|
+
throw new Error(
|
|
124
|
+
`${configPath}: not found — 'ab plugin' reads autobuild.toml from the resolved Git main checkout`,
|
|
125
|
+
)
|
|
126
|
+
}
|
|
127
|
+
throw error
|
|
128
|
+
}
|
|
129
|
+
return {
|
|
130
|
+
repoRoot,
|
|
131
|
+
diagnosis: await diagnosePlugins(config.plugins, repoRoot),
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function parsePort(value: string): PluginPort {
|
|
136
|
+
if (!PLUGIN_PORTS.includes(value as PluginPort)) {
|
|
137
|
+
throw new Error(`unknown plugin port "${value}" — expected one of: ${PLUGIN_PORTS.join(', ')}`)
|
|
138
|
+
}
|
|
139
|
+
return value as PluginPort
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Sessionless `ab plugin` operator/authoring namespace. */
|
|
143
|
+
export async function abPlugin(argv: readonly string[], opts: PluginCliOpts): Promise<number> {
|
|
144
|
+
const [command, ...rest] = argv
|
|
145
|
+
if (command === 'list') {
|
|
146
|
+
if (rest.length !== 0) throw new Error('usage: ab plugin list')
|
|
147
|
+
const { diagnosis } = await loadDiagnosis(opts)
|
|
148
|
+
opts.stdout('configured modules:')
|
|
149
|
+
if (diagnosis.reports.length === 0) opts.stdout(' (none)')
|
|
150
|
+
for (const report of diagnosis.reports) {
|
|
151
|
+
const line = ` ${renderModuleReport(report)}`
|
|
152
|
+
if (report.status === 'loaded') opts.stdout(line)
|
|
153
|
+
else opts.stderr(line)
|
|
154
|
+
}
|
|
155
|
+
opts.stdout('adapters:')
|
|
156
|
+
for (const port of PLUGIN_PORTS) {
|
|
157
|
+
opts.stdout(`${port}:`)
|
|
158
|
+
for (const adapter of diagnosis.registry.adapters(port)) {
|
|
159
|
+
opts.stdout(renderAdapter(adapter))
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return diagnosis.healthy ? 0 : 1
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (command === 'doctor') {
|
|
166
|
+
if (rest.length !== 0) throw new Error('usage: ab plugin doctor')
|
|
167
|
+
const { diagnosis } = await loadDiagnosis(opts)
|
|
168
|
+
if (diagnosis.reports.length === 0) {
|
|
169
|
+
opts.stdout('No configured plugin modules.')
|
|
170
|
+
}
|
|
171
|
+
for (const report of diagnosis.reports) {
|
|
172
|
+
const line = renderModuleReport(report)
|
|
173
|
+
if (report.status === 'loaded') opts.stdout(line)
|
|
174
|
+
else opts.stderr(line)
|
|
175
|
+
}
|
|
176
|
+
return diagnosis.healthy ? 0 : 1
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (command === 'test') {
|
|
180
|
+
if (rest.length !== 2) {
|
|
181
|
+
throw new Error(
|
|
182
|
+
'usage: ab plugin test <ticket-source|agent-runtime|workspace-provider|forge> <adapter>',
|
|
183
|
+
)
|
|
184
|
+
}
|
|
185
|
+
const port = parsePort(rest[0]!)
|
|
186
|
+
const adapter = rest[1]!
|
|
187
|
+
const { repoRoot, diagnosis } = await loadDiagnosis(opts)
|
|
188
|
+
if (!diagnosis.healthy) {
|
|
189
|
+
for (const report of diagnosis.reports) {
|
|
190
|
+
if (report.status === 'failed') opts.stderr(renderModuleReport(report))
|
|
191
|
+
}
|
|
192
|
+
return 1
|
|
193
|
+
}
|
|
194
|
+
const registration = diagnosis.registry.registration(port, adapter)
|
|
195
|
+
if (registration === undefined) {
|
|
196
|
+
const names = diagnosis.registry.adapters(port).map((entry) => entry.name)
|
|
197
|
+
throw new Error(
|
|
198
|
+
`unknown ${pluginPortLabel(port)} adapter "${adapter}" — configured adapters: ${names.join(', ') || '(none)'}`,
|
|
199
|
+
)
|
|
200
|
+
}
|
|
201
|
+
if (registration.contract === undefined) {
|
|
202
|
+
throw new Error(
|
|
203
|
+
`${pluginPortLabel(port)} adapter "${adapter}" cannot be contract-tested: ` +
|
|
204
|
+
`its manifest does not provide ${contractField(port, adapter)}. ` +
|
|
205
|
+
'Register the adapter as { factory, contract: { factory } } where the contract factory returns the shared suite harness factory.',
|
|
206
|
+
)
|
|
207
|
+
}
|
|
208
|
+
if (registration.contract.live === true && opts.env.AB_RUN_LIVE_PORT_CONTRACTS !== '1') {
|
|
209
|
+
throw new Error(
|
|
210
|
+
`${pluginPortLabel(port)} adapter "${adapter}" declares a live contract; ` +
|
|
211
|
+
'set AB_RUN_LIVE_PORT_CONTRACTS=1 to explicitly allow external side effects',
|
|
212
|
+
)
|
|
213
|
+
}
|
|
214
|
+
return await (opts.subprocess ?? spawnPluginContract)({
|
|
215
|
+
repoRoot,
|
|
216
|
+
port,
|
|
217
|
+
adapter,
|
|
218
|
+
env: opts.env,
|
|
219
|
+
stdout: opts.stdout,
|
|
220
|
+
stderr: opts.stderr,
|
|
221
|
+
})
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
throw new Error(
|
|
225
|
+
'usage: ab plugin <list|doctor|test> — test requires <ticket-source|agent-runtime|workspace-provider|forge> <adapter>',
|
|
226
|
+
)
|
|
227
|
+
}
|