@defrex/autobuild 0.8.0 → 0.9.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 +313 -26
- package/bin/ab-build-runner.ts +8 -2
- package/docs/architecture.md +57 -14
- package/docs/assets/headline-wide.png +0 -0
- package/docs/configuration.md +151 -25
- package/docs/hosted-dispatcher.md +69 -13
- package/docs/operator-api.md +2 -2
- package/docs/remote-store-protocol.md +169 -16
- package/docs/remote-ticket-protocol.md +1 -1
- package/docs/setup.md +12 -9
- package/docs/website.md +10 -0
- package/package.json +43 -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/model.ts +57 -7
- package/packages/core/src/cli/dashboard/poll.ts +69 -10
- 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.ts +320 -46
- package/packages/core/src/cli/harvest.ts +122 -8
- package/packages/core/src/cli/help.ts +47 -8
- 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 +248 -222
- 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/status.ts +149 -27
- 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 +307 -7
- package/packages/core/src/cli/wait.ts +147 -39
- package/packages/core/src/cli/watch.ts +128 -166
- package/packages/core/src/config/live.ts +22 -0
- package/packages/core/src/config/pipeline-source.ts +376 -0
- package/packages/core/src/config/schema.ts +230 -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 +2 -1
- package/packages/core/src/kernel/auto-merge-default.ts +123 -0
- package/packages/core/src/kernel/phases.ts +16 -2
- package/packages/core/src/kernel/reducer.ts +40 -1
- 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 +208 -19
- 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/{remote-index.ts → hosted-index.ts} +3 -2
- package/packages/core/src/ports/types.ts +17 -2
- 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 +37 -0
- package/packages/core/src/processes/build-runner.ts +54 -3
- package/packages/core/src/processes/dispatcher.ts +247 -41
- package/packages/core/src/processes/harvest-child.ts +5 -0
- package/packages/core/src/processes/harvest-runner.ts +66 -19
- package/packages/core/src/processes/harvest.ts +196 -24
- package/packages/core/src/processes/orchestrator-tick.ts +319 -0
- package/packages/core/src/processes/sandbox-state.ts +10 -0
- package/packages/core/src/store/adapter.ts +32 -0
- package/packages/core/src/store/build-scope.ts +64 -0
- package/packages/core/src/store/contract.ts +868 -0
- package/packages/core/src/store/digest.ts +49 -0
- package/packages/core/src/store/local/schema.ts +29 -2
- package/packages/core/src/store/local/store.ts +338 -4
- package/packages/core/src/store/memory.ts +179 -0
- package/packages/core/src/store/new-build.ts +17 -0
- package/packages/core/src/store/phase-session.ts +71 -0
- package/packages/core/src/store/remote/client.ts +260 -1
- package/packages/core/src/store/remote/index.ts +12 -2
- package/packages/core/src/store/remote/protocol.ts +100 -12
- package/packages/core/src/store/remote/version.ts +6 -2
- package/packages/core/src/store/repo-state-events.ts +140 -0
- package/packages/core/src/store/retention.ts +7 -0
- package/packages/core/src/store/session-handle.ts +64 -0
- package/packages/core/src/{sessions/reducer.ts → store/session-reducer.ts} +28 -9
- 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 +425 -0
- package/packages/core/src/store/types.ts +122 -1
- package/packages/core/src/testing/index.ts +59 -0
- package/skills/code-review/SKILL.md +5 -1
- package/skills/guide/SKILL.md +126 -33
- 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 +169 -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/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
|
@@ -213,8 +213,14 @@ export function createGitHubFetchTransport(opts: {
|
|
|
213
213
|
// a private repository is otherwise indistinguishable from a bad path,
|
|
214
214
|
// and the operator's fix (export a token, or `gh auth login`) is the
|
|
215
215
|
// point of the message.
|
|
216
|
+
// `in` rather than relying on the `token === undefined` discriminant:
|
|
217
|
+
// the plugin-sdk typecheck fixture compiles this chain without
|
|
218
|
+
// strictNullChecks, where `undefined` is assignable to `string` and
|
|
219
|
+
// the union stops discriminating (AUT-517).
|
|
216
220
|
const anonymous =
|
|
217
|
-
credential.token === undefined
|
|
221
|
+
credential.token === undefined && 'reason' in credential
|
|
222
|
+
? ` (request was unauthenticated: ${credential.reason})`
|
|
223
|
+
: ''
|
|
218
224
|
throw new GitHubApiError(
|
|
219
225
|
response.status,
|
|
220
226
|
`${githubErrorMessage(response.status, json, text)}${anonymous}`,
|
|
@@ -335,9 +341,14 @@ export async function resolveGitHubToken(
|
|
|
335
341
|
if (fromEnv !== undefined) return { token: fromEnv }
|
|
336
342
|
const probe = await githubTokenFromGhCli(exec)
|
|
337
343
|
if (probe.token !== undefined) return probe
|
|
344
|
+
// `in` rather than relying on the `token !== undefined` discriminant: the
|
|
345
|
+
// plugin-sdk typecheck fixture compiles this chain without
|
|
346
|
+
// strictNullChecks, where `undefined` is assignable to `string` and the
|
|
347
|
+
// union stops discriminating (AUT-517). A miss always carries its reason.
|
|
348
|
+
const missReason = 'reason' in probe ? probe.reason : 'no credential source succeeded'
|
|
338
349
|
return {
|
|
339
350
|
token: undefined,
|
|
340
|
-
reason: `GITHUB_TOKEN and GH_TOKEN are unset and ${
|
|
351
|
+
reason: `GITHUB_TOKEN and GH_TOKEN are unset and ${missReason}`,
|
|
341
352
|
}
|
|
342
353
|
}
|
|
343
354
|
|
|
@@ -470,6 +470,16 @@ function restMergeState(raw: string): MergeStateStatus {
|
|
|
470
470
|
|
|
471
471
|
const sleep = (ms: number): Promise<void> => new Promise((resolve) => setTimeout(resolve, ms))
|
|
472
472
|
|
|
473
|
+
/** Bounded UNKNOWN re-query schedule: 7 re-queries, ~30 s of nondecreasing
|
|
474
|
+
* backoff capped at 6 s steps. Sized after AUT-392 — the prior
|
|
475
|
+
* two-re-query/2 s budget (`[500, 1500]`) was exhausted by a live occurrence
|
|
476
|
+
* (build `give-repo-scoped`, PR #352) while GitHub was still computing
|
|
477
|
+
* mergeability after required checks completed. Single-occurrence evidence,
|
|
478
|
+
* so this is a deliberate sizing bet, not a measured latency distribution;
|
|
479
|
+
* the unchanged pendingAutoMerge tick backstop remains behind it for waits a
|
|
480
|
+
* single tick cannot absorb. */
|
|
481
|
+
export const MERGEABILITY_RETRY_DELAYS_MS = [1000, 2000, 4000, 5000, 6000, 6000, 6000]
|
|
482
|
+
|
|
473
483
|
/** Hard cap on the janitor poll's per-PR ETag cache. One small entry per PR
|
|
474
484
|
* ever polled in a dispatcher process lifetime makes hitting this in practice
|
|
475
485
|
* implausible; the cap is hygiene, not a load-bearing bound. */
|
|
@@ -514,7 +524,8 @@ export class GitHubForge implements Forge {
|
|
|
514
524
|
env?: Readonly<Record<string, string | undefined>>
|
|
515
525
|
/** Bounded waits between mergeability re-queries when GitHub reports
|
|
516
526
|
* `mergeable_state: 'unknown'` (mergeability still being computed).
|
|
517
|
-
*
|
|
527
|
+
* Defaults to `MERGEABILITY_RETRY_DELAYS_MS` (7 re-queries, ~30 s);
|
|
528
|
+
* tests inject `[0, 0]` or all-zero schedules so no test sleeps. */
|
|
518
529
|
mergeabilityRetryDelaysMs?: number[]
|
|
519
530
|
} = {},
|
|
520
531
|
) {
|
|
@@ -535,7 +546,7 @@ export class GitHubForge implements Forge {
|
|
|
535
546
|
this.explicitRepository = this.env.AB_REPOSITORY
|
|
536
547
|
}
|
|
537
548
|
this.repoRoot = opts.repoRoot
|
|
538
|
-
this.mergeabilityRetryDelaysMs = opts.mergeabilityRetryDelaysMs ??
|
|
549
|
+
this.mergeabilityRetryDelaysMs = opts.mergeabilityRetryDelaysMs ?? MERGEABILITY_RETRY_DELAYS_MS
|
|
539
550
|
this.prAttachments = new GitHubPrAttachmentHosting({ transport: this.transport })
|
|
540
551
|
}
|
|
541
552
|
|
|
@@ -40,9 +40,39 @@ function detail(error: unknown): string {
|
|
|
40
40
|
export class LocalGitForge implements Forge {
|
|
41
41
|
readonly name = 'local-git'
|
|
42
42
|
private readonly exec: Exec
|
|
43
|
+
/** The repository the optional checkout-less read seam reads from. The
|
|
44
|
+
* factory passes the served checkout; without it the capability is absent
|
|
45
|
+
* and callers retain their checkout/git fallbacks. */
|
|
46
|
+
private readonly repoRoot?: string
|
|
43
47
|
|
|
44
|
-
constructor(opts: { exec?: Exec } = {}) {
|
|
48
|
+
constructor(opts: { exec?: Exec; repoRoot?: string } = {}) {
|
|
45
49
|
this.exec = opts.exec ?? bunExec
|
|
50
|
+
this.repoRoot = opts.repoRoot
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Current tip of a local branch in the served repository. Throws when the
|
|
54
|
+
* branch does not exist — the same not-found contract as remote adapters. */
|
|
55
|
+
async remoteBranchSha(branch: string): Promise<string> {
|
|
56
|
+
if (this.repoRoot === undefined) {
|
|
57
|
+
throw new Error('local-git forge has no repository root for remote branch reads')
|
|
58
|
+
}
|
|
59
|
+
const result = await this.command(
|
|
60
|
+
['rev-parse', '--verify', `refs/heads/${branch}`],
|
|
61
|
+
this.repoRoot,
|
|
62
|
+
true,
|
|
63
|
+
)
|
|
64
|
+
if (result.exitCode !== 0) throw new Error(`local-git: unknown remote branch ${branch}`)
|
|
65
|
+
return result.stdout.trim()
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** File bytes from the served repository. Ref omitted → the checkout's HEAD.
|
|
69
|
+
* Throws when the path does not exist at that ref. */
|
|
70
|
+
async readFile(path: string, ref?: string): Promise<string> {
|
|
71
|
+
if (this.repoRoot === undefined) {
|
|
72
|
+
throw new Error('local-git forge has no repository root for file reads')
|
|
73
|
+
}
|
|
74
|
+
const result = await this.command(['show', `${ref ?? 'HEAD'}:${path}`], this.repoRoot)
|
|
75
|
+
return result.stdout
|
|
46
76
|
}
|
|
47
77
|
|
|
48
78
|
private async command(args: string[], cwd: string, allowFailure = false) {
|
|
@@ -38,7 +38,7 @@ import {
|
|
|
38
38
|
toolInputPart,
|
|
39
39
|
toolOutputPart,
|
|
40
40
|
} from './stream-parts'
|
|
41
|
-
import { classifyProviderError,
|
|
41
|
+
import { classifyProviderError, missingExecutableFailure } from './provider-error'
|
|
42
42
|
import { sessionEnv } from './session-env'
|
|
43
43
|
import type { OneShotCompletion, OneShotCompletionInput, OneShotCompletionResult } from './one-shot'
|
|
44
44
|
import type { RuntimeUsabilityInput, RuntimeUsabilityResult } from './runtime'
|
|
@@ -652,7 +652,7 @@ function launchFailure(error: unknown): ClaudeTurn {
|
|
|
652
652
|
return {
|
|
653
653
|
text: '',
|
|
654
654
|
usage: { inputTokens: 0, outputTokens: 0 },
|
|
655
|
-
failure: missing ?
|
|
655
|
+
failure: missing ? missingExecutableFailure(message) : classifyProviderError(message),
|
|
656
656
|
cli: {
|
|
657
657
|
stdout: '',
|
|
658
658
|
stderr: errorText(error),
|
|
@@ -17,7 +17,11 @@ import {
|
|
|
17
17
|
type Transcript,
|
|
18
18
|
} from '../types'
|
|
19
19
|
import type { OneShotCompletion, OneShotCompletionInput, OneShotCompletionResult } from './one-shot'
|
|
20
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
classifyProviderError,
|
|
22
|
+
credentialFailure,
|
|
23
|
+
missingExecutableFailure,
|
|
24
|
+
} from './provider-error'
|
|
21
25
|
import {
|
|
22
26
|
abortPart,
|
|
23
27
|
errorPart,
|
|
@@ -694,7 +698,7 @@ function launchFailure(error: unknown): CodexTurn {
|
|
|
694
698
|
return {
|
|
695
699
|
text: '',
|
|
696
700
|
usage: { inputTokens: 0, outputTokens: 0 },
|
|
697
|
-
failure: missing ?
|
|
701
|
+
failure: missing ? missingExecutableFailure(message) : classifyProviderError(message),
|
|
698
702
|
cli: { stdout: '', stderr: errorText(error), exitCode: -1 },
|
|
699
703
|
events: [],
|
|
700
704
|
malformedLines: [],
|
|
@@ -49,12 +49,18 @@ export function failedTurnResult(
|
|
|
49
49
|
permanent: boolean,
|
|
50
50
|
text = '',
|
|
51
51
|
cause?: AgentFailureCause,
|
|
52
|
+
missingExecutable?: true,
|
|
52
53
|
): AgentTurnResult {
|
|
53
54
|
return {
|
|
54
55
|
kind: 'failed',
|
|
55
56
|
text,
|
|
56
57
|
usage: { inputTokens: 0, outputTokens: 0, turns: 1 },
|
|
57
|
-
failure: {
|
|
58
|
+
failure: {
|
|
59
|
+
message,
|
|
60
|
+
permanent,
|
|
61
|
+
...(cause !== undefined ? { cause } : {}),
|
|
62
|
+
...(missingExecutable === true ? { missingExecutable } : {}),
|
|
63
|
+
},
|
|
58
64
|
}
|
|
59
65
|
}
|
|
60
66
|
|
|
@@ -13,7 +13,12 @@ import {
|
|
|
13
13
|
type Transcript,
|
|
14
14
|
} from '../types'
|
|
15
15
|
import type { OneShotCompletion, OneShotCompletionInput, OneShotCompletionResult } from './one-shot'
|
|
16
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
classifyProviderError,
|
|
18
|
+
configurationFailure,
|
|
19
|
+
credentialFailure,
|
|
20
|
+
missingExecutableFailure,
|
|
21
|
+
} from './provider-error'
|
|
17
22
|
import { createPiRpcSession, PI_MODEL_ARG, PI_RPC_MODE_ARG } from './pi-rpc'
|
|
18
23
|
import {
|
|
19
24
|
abortPart,
|
|
@@ -586,11 +591,8 @@ export class PiAgentRunner implements AgentRunner, OneShotCompletion {
|
|
|
586
591
|
|
|
587
592
|
function localPiFailure(error: unknown): AgentTurnFailure {
|
|
588
593
|
const message = errorText(error)
|
|
589
|
-
if (
|
|
590
|
-
|
|
591
|
-
message,
|
|
592
|
-
)
|
|
593
|
-
) {
|
|
594
|
+
if (/executable "pi" was not found/.test(message)) return missingExecutableFailure(message)
|
|
595
|
+
if (/requires Pi .* or newer|could not parse detected Pi version/.test(message)) {
|
|
594
596
|
return configurationFailure(message)
|
|
595
597
|
}
|
|
596
598
|
if (/not ready|authentication|logged in|credentials/i.test(message))
|
|
@@ -77,9 +77,12 @@ export function classifyProviderError(
|
|
|
77
77
|
/** Whether another declared target may be tried inside this phase attempt.
|
|
78
78
|
* Legacy plugins have no cause: preserve their old safe split. */
|
|
79
79
|
export function isAlternateEligible(failure: AgentTurnFailure): boolean {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
80
|
+
if (failure.cause === undefined) return !failure.permanent
|
|
81
|
+
return (
|
|
82
|
+
failure.cause === 'availability' ||
|
|
83
|
+
failure.cause === 'exhaustion' ||
|
|
84
|
+
(failure.cause === 'configuration' && failure.missingExecutable === true)
|
|
85
|
+
)
|
|
83
86
|
}
|
|
84
87
|
|
|
85
88
|
/** Final-failure retry behavior remains governed by the compatibility bit. */
|
|
@@ -92,6 +95,13 @@ export function configurationFailure(message: string): AgentTurnFailure {
|
|
|
92
95
|
return { message, permanent: true, cause: 'configuration' }
|
|
93
96
|
}
|
|
94
97
|
|
|
98
|
+
/** The runtime's CLI executable is not installed. Still a permanent
|
|
99
|
+
* `configuration` failure (never retried), but a later declared target whose
|
|
100
|
+
* CLI is installed may serve the phase. */
|
|
101
|
+
export function missingExecutableFailure(message: string): AgentTurnFailure {
|
|
102
|
+
return { message, permanent: true, cause: 'configuration', missingExecutable: true }
|
|
103
|
+
}
|
|
104
|
+
|
|
95
105
|
/** Explicit credential failures bypass both alternates and phase retries. */
|
|
96
106
|
export function credentialFailure(message: string): AgentTurnFailure {
|
|
97
107
|
return { message, permanent: true, cause: 'credentials' }
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
// Client half of the hosted ticket source (SPEC §13): the TicketSource a
|
|
2
|
+
// local CLI drives against a hosted deployment. The HTTP server half ships
|
|
3
|
+
// in `@defrex/autobuild-hosted-store-service`.
|
|
1
4
|
export { HostedTicketSource } from './remote'
|
|
2
5
|
export type { HostedTicketFetch, HostedTicketSourceOptions } from './remote'
|
|
3
|
-
export { createTicketServer } from './remote-server'
|
|
4
|
-
export type { TicketServerOptions } from './remote-server'
|
|
5
6
|
export {
|
|
6
7
|
HOSTED_TICKET_OPERATIONS,
|
|
7
8
|
hostedTicketContextSchema,
|
|
@@ -177,8 +177,11 @@ export interface WorkspaceProvisionResult extends WorkspaceHandle {
|
|
|
177
177
|
|
|
178
178
|
export interface WorkspacePublication {
|
|
179
179
|
/** Observe whether the exact requested commit already reached the durable
|
|
180
|
-
* remote branch after an interrupted acknowledgement.
|
|
181
|
-
|
|
180
|
+
* remote branch after an interrupted acknowledgement. `ref` optionally
|
|
181
|
+
* names the workspace whose git database the probe runs against — used by
|
|
182
|
+
* the operator-sandbox publication path, whose provider probes from the
|
|
183
|
+
* sandbox checkout; the build-path callers omit it. */
|
|
184
|
+
isPublished?(input: { ref?: string; sha: string; branch: string }): Promise<boolean>
|
|
182
185
|
/** Publish exactly one commit to exactly one branch; no arbitrary command
|
|
183
186
|
* surface is exposed to the dispatcher or build child. */
|
|
184
187
|
publish(input: { ref: string; sha: string; branch: string }): Promise<void>
|
|
@@ -221,6 +224,14 @@ export interface WorkspaceProvider {
|
|
|
221
224
|
readonly orchestratorSandbox?: OperatorSandboxExecution
|
|
222
225
|
/** Trusted dispatcher-only publication capability for remote workspaces. */
|
|
223
226
|
readonly publication?: WorkspacePublication
|
|
227
|
+
/** Publication capability for the operator sandbox (PR-only publication):
|
|
228
|
+
* deliberately distinct from `publication`, which is the dispatcher's
|
|
229
|
+
* remote-workspace discriminator — its presence reclassifies every build
|
|
230
|
+
* workspace as remote and reroutes build publication. `sandboxPublication`
|
|
231
|
+
* is never consulted by the dispatcher; it is consumed only by the
|
|
232
|
+
* operator-sandbox publish service, and present only on providers that can
|
|
233
|
+
* host operator sandboxes. */
|
|
234
|
+
readonly sandboxPublication?: WorkspacePublication
|
|
224
235
|
/** Optional only for remote/disposable providers. */
|
|
225
236
|
readonly recovery?: WorkspaceRecovery
|
|
226
237
|
provision(opts: {
|
|
@@ -424,6 +435,10 @@ export interface AgentTurnFailure {
|
|
|
424
435
|
/** Additive structured routing cause. Legacy plugin runners may omit it;
|
|
425
436
|
* their existing permanent bit then supplies the conservative behavior. */
|
|
426
437
|
cause?: AgentFailureCause
|
|
438
|
+
/** Set with cause 'configuration' when the runtime's CLI executable is not
|
|
439
|
+
* installed. This is the one configuration failure a configured alternate
|
|
440
|
+
* may serve; every other configuration failure still stops the chain. */
|
|
441
|
+
missingExecutable?: true
|
|
427
442
|
}
|
|
428
443
|
|
|
429
444
|
/** A turn that reached a provider/runtime-declared failure while retaining an
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The full capability objects for the builtin workspace providers (AUT-516).
|
|
3
|
+
* The parse-time subset lives in `config/schema.ts`'s
|
|
4
|
+
* `BUILTIN_WORKSPACE_PROVIDER_CONFIG` because config validation must be
|
|
5
|
+
* statically reachable without the registry; this module assembles the FULL
|
|
6
|
+
* declarations — including the registry-seam behaviors — and is attached to
|
|
7
|
+
* the builtin registrations by `plugins/registry.ts`.
|
|
8
|
+
*
|
|
9
|
+
* Import graph discipline: this module imports `config/schema.ts`,
|
|
10
|
+
* `git-worktree.ts`, `vercel-sandbox.ts`, and the shared vercel capability
|
|
11
|
+
* module; nothing imports it from `schema.ts`, so no cycle exists. The
|
|
12
|
+
* `vercel-sandbox` entry's capabilities are the shared
|
|
13
|
+
* `VERCEL_SANDBOX_CAPABILITIES` object (AUT-517) — the plugin package
|
|
14
|
+
* references the same object, so the declarations cannot drift. The
|
|
15
|
+
* construction-site `configRefusal` for git-worktree is set EXPLICITLY here,
|
|
16
|
+
* not copied from the table's parse-site `configRefusalMessage` — the two
|
|
17
|
+
* strings differ by design (the parse-site message adds the remediation
|
|
18
|
+
* clause; the construction-site one does not).
|
|
19
|
+
*
|
|
20
|
+
* This `BUILTINS` record is the single source of truth for which builtins
|
|
21
|
+
* exist: `plugins/registry.ts` derives its builtin workspace-provider set
|
|
22
|
+
* from `builtinWorkspaceProviderNames()` (AUT-579), so adding an entry here
|
|
23
|
+
* (plus the config-table half that AUT-573's parity tests pin) completes
|
|
24
|
+
* the registration step — no registry edit is required.
|
|
25
|
+
*/
|
|
26
|
+
import { join, resolve } from 'node:path'
|
|
27
|
+
import type { VercelSandboxConfig, WorkspaceConfig } from '../../config/schema'
|
|
28
|
+
import type { WorkspaceProviderCapabilities } from './provider-capabilities'
|
|
29
|
+
import type { CreateWorkspaceProviderOptions } from './create'
|
|
30
|
+
import { GitWorktreeProvider } from './git-worktree'
|
|
31
|
+
import { VercelSandboxProvider } from './vercel-sandbox'
|
|
32
|
+
import { VERCEL_SANDBOX_CAPABILITIES } from './vercel-capabilities'
|
|
33
|
+
import type { WorkspaceProvider } from '../types'
|
|
34
|
+
|
|
35
|
+
/** The git-worktree construction-site refusal: no remediation clause, unlike
|
|
36
|
+
* the parse-site message in the config table. */
|
|
37
|
+
const GIT_WORKTREE_CONSTRUCTION_REFUSAL =
|
|
38
|
+
'[workspace.config] is not supported by the builtin "git-worktree" provider'
|
|
39
|
+
|
|
40
|
+
interface BuiltinWorkspaceProviderDeclaration {
|
|
41
|
+
capabilities: WorkspaceProviderCapabilities
|
|
42
|
+
builtinFactory: (
|
|
43
|
+
config: WorkspaceConfig,
|
|
44
|
+
opts: CreateWorkspaceProviderOptions,
|
|
45
|
+
parsed: unknown,
|
|
46
|
+
) => WorkspaceProvider | Promise<WorkspaceProvider>
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const BUILTINS: Record<string, BuiltinWorkspaceProviderDeclaration> = {
|
|
50
|
+
'git-worktree': {
|
|
51
|
+
capabilities: {
|
|
52
|
+
configRefusal: GIT_WORKTREE_CONSTRUCTION_REFUSAL,
|
|
53
|
+
},
|
|
54
|
+
builtinFactory(_config, opts) {
|
|
55
|
+
// Its provider config is always `{}`: the refusal (parse site and
|
|
56
|
+
// construction site alike) has already rejected anything else.
|
|
57
|
+
return new GitWorktreeProvider({
|
|
58
|
+
root: resolve(opts.worktreeRoot),
|
|
59
|
+
sandboxRoot:
|
|
60
|
+
opts.sandboxRoot ?? resolve(join(opts.worktreeRoot, '..', 'orchestrator-sandboxes')),
|
|
61
|
+
setupCommand: opts.sandboxSetupCommand,
|
|
62
|
+
sandboxEnvironmentVariables: opts.sandboxEnvironmentVariables,
|
|
63
|
+
envSource: opts.env,
|
|
64
|
+
})
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
'vercel-sandbox': {
|
|
68
|
+
capabilities: VERCEL_SANDBOX_CAPABILITIES,
|
|
69
|
+
builtinFactory(_config, opts, parsed) {
|
|
70
|
+
// storeRef/storeToken are guaranteed present: the pre-split
|
|
71
|
+
// storeRequirements check in createWorkspaceProvider ran before
|
|
72
|
+
// construction.
|
|
73
|
+
return new VercelSandboxProvider({
|
|
74
|
+
config: parsed as VercelSandboxConfig,
|
|
75
|
+
env: opts.env,
|
|
76
|
+
storeRef: opts.storeRef!,
|
|
77
|
+
storeToken: opts.storeToken!,
|
|
78
|
+
repo: resolve(opts.repoRoot),
|
|
79
|
+
runtimeReferences: opts.runtimeReferences ?? [],
|
|
80
|
+
setupCommand: opts.sandboxSetupCommand,
|
|
81
|
+
sandboxEnvironmentVariables: opts.sandboxEnvironmentVariables,
|
|
82
|
+
...(opts.origin !== undefined ? { origin: opts.origin } : {}),
|
|
83
|
+
...(opts.remoteBranchHead !== undefined ? { remoteBranchHead: opts.remoteBranchHead } : {}),
|
|
84
|
+
})
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Every builtin provider name, for parity checks over the declaration
|
|
90
|
+
* tables (AUT-573): the parse-time table in `config/schema.ts` and the FULL
|
|
91
|
+
* declarations here must cover the same provider set. */
|
|
92
|
+
export function builtinWorkspaceProviderNames(): readonly string[] {
|
|
93
|
+
return Object.keys(BUILTINS)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** The FULL capability object of a builtin workspace provider, `undefined`
|
|
97
|
+
* for any other name. Used by pre-registry seams (guest probe, init
|
|
98
|
+
* preflight) that cannot consult the plugin registry. */
|
|
99
|
+
export function builtinWorkspaceProviderCapabilities(
|
|
100
|
+
name: string,
|
|
101
|
+
): WorkspaceProviderCapabilities | undefined {
|
|
102
|
+
return BUILTINS[name]?.capabilities
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** The builtin registration extras for `plugins/registry.ts`: the capability
|
|
106
|
+
* object plus the host-owned construction closure. */
|
|
107
|
+
export function builtinWorkspaceProviderRegistration(
|
|
108
|
+
name: string,
|
|
109
|
+
): BuiltinWorkspaceProviderDeclaration | undefined {
|
|
110
|
+
const builtin = BUILTINS[name]
|
|
111
|
+
return builtin === undefined
|
|
112
|
+
? undefined
|
|
113
|
+
: { capabilities: builtin.capabilities, builtinFactory: builtin.builtinFactory }
|
|
114
|
+
}
|
|
@@ -1,11 +1,17 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { resolve } from 'node:path'
|
|
2
|
+
import { SANDBOX_FORBIDDEN_ENV } from './operator-sandbox'
|
|
3
|
+
import type { WorkspaceConfig } from '../../config/schema'
|
|
3
4
|
import type { PluginRegistry } from '../../plugins/registry'
|
|
4
5
|
import type { WorkspaceProvider } from '../types'
|
|
5
6
|
import type { BuildExecution } from './build-execution'
|
|
6
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
currentRuntimeReferences,
|
|
9
|
+
runtimeProvisioningMap,
|
|
10
|
+
runtimeProvisioningMissingMessage,
|
|
11
|
+
sandboxForbiddenEnvMessage,
|
|
12
|
+
} from './provider-capabilities'
|
|
7
13
|
import { LocalBuildExecution } from './local-build-execution'
|
|
8
|
-
import {
|
|
14
|
+
import type { RuntimeReferencesSource } from './vercel-sandbox'
|
|
9
15
|
|
|
10
16
|
export interface CreateWorkspaceProviderOptions {
|
|
11
17
|
registry: PluginRegistry
|
|
@@ -31,6 +37,11 @@ export interface CreateWorkspaceProviderOptions {
|
|
|
31
37
|
sandboxSetupCommand?: string
|
|
32
38
|
sandboxRoot?: string
|
|
33
39
|
sandboxEnvironmentVariables?: readonly string[]
|
|
40
|
+
/** Whether `[orchestrator].enabled` is set at the call site. Gates the
|
|
41
|
+
* construction-site `sandboxForbiddenEnv` check so the parse-time rule is
|
|
42
|
+
* reproduced exactly (the config validation only runs when the orchestrator
|
|
43
|
+
* is enabled). */
|
|
44
|
+
orchestratorSandboxEnabled?: boolean
|
|
34
45
|
}
|
|
35
46
|
|
|
36
47
|
export interface WorkspaceRuntime {
|
|
@@ -53,44 +64,56 @@ export async function createWorkspaceProvider(
|
|
|
53
64
|
)
|
|
54
65
|
}
|
|
55
66
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
67
|
+
// Uniform capability enforcement (AUT-516): every declaration that needs
|
|
68
|
+
// the registry is honoured here, before the builtin/plugin split, where
|
|
69
|
+
// `registration.capabilities`, `opts.storeRef`, and `opts.storeToken` are
|
|
70
|
+
// already in hand — a plugin declaring any of them is enforced too, never
|
|
71
|
+
// accepted by manifest parsing and silently ignored. Order preserves the
|
|
72
|
+
// builtin failure precedence.
|
|
73
|
+
const capabilities = registration.capabilities
|
|
74
|
+
const hasConfig = Object.keys(config.config).length > 0
|
|
75
|
+
if (capabilities?.configRefusal !== undefined && hasConfig) {
|
|
76
|
+
throw new Error(capabilities.configRefusal)
|
|
77
|
+
}
|
|
78
|
+
let parsed: { data: unknown } | undefined
|
|
79
|
+
if (capabilities?.configSchema !== undefined) {
|
|
80
|
+
const result = capabilities.configSchema.safeParse(config.config)
|
|
81
|
+
if (!result.success) {
|
|
82
|
+
throw new Error(`invalid ${config.provider} config: ${result.error.message}`)
|
|
71
83
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
84
|
+
parsed = result as { data: unknown }
|
|
85
|
+
}
|
|
86
|
+
if (capabilities?.requireRuntimeProvisioning === true && opts.runtimeReferences !== undefined) {
|
|
87
|
+
// `mcp.ts` passes no `runtimeReferences` because it references no runtimes;
|
|
88
|
+
// dispatch and init validation always do.
|
|
89
|
+
const provisioning = runtimeProvisioningMap(parsed !== undefined ? parsed.data : config.config)
|
|
90
|
+
for (const group of currentRuntimeReferences(opts.runtimeReferences)) {
|
|
91
|
+
if (Object.hasOwn(provisioning, group.runtime)) continue
|
|
92
|
+
throw new Error(runtimeProvisioningMissingMessage(group))
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (opts.orchestratorSandboxEnabled === true && capabilities?.sandboxForbiddenEnv !== undefined) {
|
|
96
|
+
for (const name of opts.sandboxEnvironmentVariables ?? []) {
|
|
97
|
+
if (SANDBOX_FORBIDDEN_ENV.includes(name) || capabilities.sandboxForbiddenEnv.includes(name)) {
|
|
98
|
+
throw new Error(sandboxForbiddenEnvMessage(name))
|
|
77
99
|
}
|
|
78
|
-
return new VercelSandboxProvider({
|
|
79
|
-
config: parsed.data,
|
|
80
|
-
env: opts.env,
|
|
81
|
-
storeRef: opts.storeRef,
|
|
82
|
-
storeToken: opts.storeToken,
|
|
83
|
-
repo: resolve(opts.repoRoot),
|
|
84
|
-
runtimeReferences: opts.runtimeReferences ?? [],
|
|
85
|
-
setupCommand: opts.sandboxSetupCommand,
|
|
86
|
-
sandboxEnvironmentVariables: opts.sandboxEnvironmentVariables,
|
|
87
|
-
...(opts.origin !== undefined ? { origin: opts.origin } : {}),
|
|
88
|
-
...(opts.remoteBranchHead !== undefined ? { remoteBranchHead: opts.remoteBranchHead } : {}),
|
|
89
|
-
})
|
|
90
100
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
101
|
+
}
|
|
102
|
+
if (
|
|
103
|
+
capabilities?.storeRequirements !== undefined &&
|
|
104
|
+
(opts.storeRef === undefined || opts.storeToken === undefined)
|
|
105
|
+
) {
|
|
106
|
+
throw new Error(capabilities.storeRequirements.constructionMessage)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (registration.owner.kind === 'builtin') {
|
|
110
|
+
const builtinFactory = registration.builtinFactory
|
|
111
|
+
if (builtinFactory === undefined) {
|
|
112
|
+
throw new Error(
|
|
113
|
+
`workspace provider "${config.provider}" is registered as a builtin but has no constructor`,
|
|
114
|
+
)
|
|
115
|
+
}
|
|
116
|
+
return builtinFactory(config, opts, parsed !== undefined ? parsed.data : config.config)
|
|
94
117
|
}
|
|
95
118
|
|
|
96
119
|
const factory = registration.factory
|
|
@@ -101,10 +124,22 @@ export async function createWorkspaceProvider(
|
|
|
101
124
|
}
|
|
102
125
|
|
|
103
126
|
try {
|
|
127
|
+
// Conditional spreads keep the existing exactness guarantee: a call site
|
|
128
|
+
// that supplies no seams constructs exactly `{ config, env, repoRoot }`,
|
|
129
|
+
// so plugin factories see no `undefined`-valued additions. With seams,
|
|
130
|
+
// the context widens in place (AUT-560) — the seams the builtin factory
|
|
131
|
+
// receives through `opts` reach the plugin factory through the context.
|
|
104
132
|
return await factory({
|
|
105
133
|
config: config.config,
|
|
106
134
|
env: opts.env,
|
|
107
135
|
repoRoot: resolve(opts.repoRoot),
|
|
136
|
+
...(opts.storeRef !== undefined ? { storeRef: opts.storeRef } : {}),
|
|
137
|
+
...(opts.storeToken !== undefined ? { storeToken: opts.storeToken } : {}),
|
|
138
|
+
...(opts.runtimeReferences !== undefined
|
|
139
|
+
? { runtimeReferences: opts.runtimeReferences }
|
|
140
|
+
: {}),
|
|
141
|
+
...(opts.origin !== undefined ? { origin: opts.origin } : {}),
|
|
142
|
+
...(opts.remoteBranchHead !== undefined ? { remoteBranchHead: opts.remoteBranchHead } : {}),
|
|
108
143
|
})
|
|
109
144
|
} catch (error) {
|
|
110
145
|
throw new Error(
|