@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
|
@@ -3,7 +3,7 @@ import { humanActor } from '../events/envelope'
|
|
|
3
3
|
import type { RepositoryEvent } from '../events/repository'
|
|
4
4
|
import { reduceDispatchStatus, type DispatchStatus } from '../kernel/dispatch-status'
|
|
5
5
|
import type { BuildState } from '../kernel/reducer'
|
|
6
|
-
import {
|
|
6
|
+
import { sampleUnclaimedObservationCount } from '../processes/harvest'
|
|
7
7
|
import {
|
|
8
8
|
controlHarvestRun,
|
|
9
9
|
OperatorControlError,
|
|
@@ -120,6 +120,9 @@ export class DispatchFrontend {
|
|
|
120
120
|
private readonly keyboard
|
|
121
121
|
private child: DispatchChildHandle | undefined
|
|
122
122
|
private repositorySeq = 0
|
|
123
|
+
/** Set only after a successful initial fold; every later `events()` call
|
|
124
|
+
* takes the delta path. */
|
|
125
|
+
private seeded = false
|
|
123
126
|
private repositoryEvents: RepositoryEvent[] = []
|
|
124
127
|
private dispatchStatus: DispatchStatus = reduceDispatchStatus([], this.runId)
|
|
125
128
|
private eventRefreshTail: Promise<void> = Promise.resolve()
|
|
@@ -164,6 +167,36 @@ export class DispatchFrontend {
|
|
|
164
167
|
* are retained for their existing replay reducers. */
|
|
165
168
|
private async events(): Promise<RepositoryEvent[]> {
|
|
166
169
|
const refresh = this.eventRefreshTail.then(async () => {
|
|
170
|
+
if (!this.seeded) {
|
|
171
|
+
// Initial fold (AUT-534): the whole-journal read the first poll used
|
|
172
|
+
// to pay is replaced by AUT-489's bounded read where that is provably
|
|
173
|
+
// equivalent. The bounded attempt is confined to the pre-launch
|
|
174
|
+
// window (`this.child === undefined`): before `launchChild` no
|
|
175
|
+
// run-scoped journal fact can carry `this.runId` (only the supervised
|
|
176
|
+
// child appends run-scoped facts with it), so every run-scoped event
|
|
177
|
+
// in the bounded subset is skipped by `reduceDispatchStatus` exactly
|
|
178
|
+
// as it would be in a full replay, and the dashboard filter keeps
|
|
179
|
+
// only durable types, which the subset carries in full. After launch
|
|
180
|
+
// a bounded subset cannot guarantee the child's `run-started` under a
|
|
181
|
+
// competing dispatcher's later anchor — dropping it would leave
|
|
182
|
+
// `effectiveConfig` unset and the first frame would never render —
|
|
183
|
+
// so a post-launch seed always takes the full read (the pre-AUT-534
|
|
184
|
+
// behavior). `once` mode's teardown fold runs after the child exits,
|
|
185
|
+
// so the same guard routes it to the full read.
|
|
186
|
+
const seed =
|
|
187
|
+
this.child === undefined
|
|
188
|
+
? await this.opts.store.getRepoStateEvents(this.opts.repo).catch(() =>
|
|
189
|
+
// Bounded-read failure falls back to the exact pre-AUT-534
|
|
190
|
+
// full read within this same seed attempt.
|
|
191
|
+
this.opts.store.getRepoEvents(this.opts.repo, 0),
|
|
192
|
+
)
|
|
193
|
+
: await this.opts.store.getRepoEvents(this.opts.repo, 0)
|
|
194
|
+
this.repositorySeq = seed.at(-1)?.seq ?? 0
|
|
195
|
+
this.dispatchStatus = reduceDispatchStatus(seed, this.runId)
|
|
196
|
+
this.repositoryEvents.push(...seed.filter(isDashboardRepositoryEvent))
|
|
197
|
+
this.seeded = true
|
|
198
|
+
return
|
|
199
|
+
}
|
|
167
200
|
const delta = await this.opts.store.getRepoEvents(this.opts.repo, this.repositorySeq)
|
|
168
201
|
if (delta.length === 0) return
|
|
169
202
|
this.repositorySeq = delta.at(-1)!.seq
|
|
@@ -295,8 +328,14 @@ export class DispatchFrontend {
|
|
|
295
328
|
}
|
|
296
329
|
const config = await this.loadConfig(ref.kind, ref.rev)
|
|
297
330
|
try {
|
|
298
|
-
|
|
299
|
-
this
|
|
331
|
+
// Journal + digests (AUT-487): one journal read for the claimed set
|
|
332
|
+
// (this class's cached `repositoryEvents` is dashboard-filtered, so the
|
|
333
|
+
// claimed set needs the full journal, exactly as the old per-build scan
|
|
334
|
+
// read it) plus one repo-scoped digest read — the sample's store
|
|
335
|
+
// traffic stays flat as finished builds accumulate. A journal record
|
|
336
|
+
// that does not yet exist reads as an empty journal (AUT-524); the
|
|
337
|
+
// sample writes nothing.
|
|
338
|
+
this.observationCount = await sampleUnclaimedObservationCount(this.opts.store, this.opts.repo)
|
|
300
339
|
} catch {
|
|
301
340
|
// Before the first successful sample there is no factual zero to place in
|
|
302
341
|
// a complete frame. Leave the prior frame untouched and retry on the next
|
|
@@ -677,6 +716,7 @@ export class DispatchFrontend {
|
|
|
677
716
|
repo: this.opts.repo,
|
|
678
717
|
user: buildControlUser(this.opts.env),
|
|
679
718
|
setting: 'intake',
|
|
719
|
+
run: this.runId,
|
|
680
720
|
})
|
|
681
721
|
await this.report(`dispatcher intake ${event.enabled ? 'ON' : 'OFF'}`)
|
|
682
722
|
await this.renderOnce()
|
|
@@ -688,6 +728,7 @@ export class DispatchFrontend {
|
|
|
688
728
|
repo: this.opts.repo,
|
|
689
729
|
user: buildControlUser(this.opts.env),
|
|
690
730
|
setting: 'auto-merge-default',
|
|
731
|
+
run: this.runId,
|
|
691
732
|
})
|
|
692
733
|
await this.report(`dispatcher auto-merge default ${event.enabled ? 'ON' : 'OFF'}`)
|
|
693
734
|
await this.renderOnce()
|
|
@@ -990,17 +1031,25 @@ export class DispatchFrontend {
|
|
|
990
1031
|
await this.opts.store.appendRepo(this.opts.repo, {
|
|
991
1032
|
actor,
|
|
992
1033
|
type: 'dispatcher.intake-set',
|
|
993
|
-
payload: { enabled: this.opts.intake },
|
|
1034
|
+
payload: { enabled: this.opts.intake, run: this.runId },
|
|
994
1035
|
})
|
|
995
1036
|
}
|
|
996
1037
|
if (this.opts.defaultAutoMerge !== undefined) {
|
|
997
1038
|
await this.opts.store.appendRepo(this.opts.repo, {
|
|
998
1039
|
actor,
|
|
999
1040
|
type: 'dispatcher.auto-merge-default-set',
|
|
1000
|
-
payload: { enabled: this.opts.defaultAutoMerge },
|
|
1041
|
+
payload: { enabled: this.opts.defaultAutoMerge, run: this.runId },
|
|
1001
1042
|
})
|
|
1002
1043
|
}
|
|
1003
1044
|
|
|
1045
|
+
// Warm the initial fold before `launchChild` (AUT-534): the bounded seed
|
|
1046
|
+
// is only safe while `this.child === undefined`, so the one opportunity
|
|
1047
|
+
// to take it is here. A failed warm-up leaves the seed unset; the poll
|
|
1048
|
+
// loop's existing failure reporting covers the retry, which then takes
|
|
1049
|
+
// the full-read path post-launch. `once` mode gains no read — its single
|
|
1050
|
+
// fold runs from `finishPresentation()` after the child exits.
|
|
1051
|
+
if (!this.opts.once) await this.events().catch(() => {})
|
|
1052
|
+
|
|
1004
1053
|
const options: DispatchChildOptions = {
|
|
1005
1054
|
targetRepo: this.opts.checkout ?? this.opts.repo,
|
|
1006
1055
|
storeRef: this.opts.storeRef,
|
|
@@ -28,13 +28,25 @@ import { tmpdir } from 'node:os'
|
|
|
28
28
|
import { join, resolve } from 'node:path'
|
|
29
29
|
import semver from 'semver'
|
|
30
30
|
import { parseConfig } from '../config/load'
|
|
31
|
-
import {
|
|
31
|
+
import {
|
|
32
|
+
composeBuildConfig,
|
|
33
|
+
DISPATCHER_CONFIG_ARTIFACT,
|
|
34
|
+
LiveConfig,
|
|
35
|
+
type ConfigSnapshot,
|
|
36
|
+
} from '../config/live'
|
|
37
|
+
import {
|
|
38
|
+
isActionablePipelineFailure,
|
|
39
|
+
resolvePipelineSource,
|
|
40
|
+
type PipelineSourceFailure,
|
|
41
|
+
type PipelineSourceMeta,
|
|
42
|
+
} from '../config/pipeline-source'
|
|
32
43
|
import { effectiveRuntimeReferences, roleKeyWarnings, SLUG_ROLE } from '../config/roles'
|
|
33
44
|
import type { Config } from '../config/schema'
|
|
34
|
-
import { loadPlugins } from '../plugins/load'
|
|
45
|
+
import { loadPlugins, pluginResolutionKind } from '../plugins/load'
|
|
35
46
|
import type { PluginRegistry } from '../plugins/registry'
|
|
36
47
|
import { materializePluginRuntimes } from '../plugins/runtimes'
|
|
37
|
-
import { DISPATCHER, humanActor } from '../events/envelope'
|
|
48
|
+
import { DISPATCHER, KERNEL, humanActor } from '../events/envelope'
|
|
49
|
+
import type { AbEvent } from '../events/catalog'
|
|
38
50
|
import type { RepositoryEventWrite } from '../events/repository'
|
|
39
51
|
import { randomIds, randomUuids, type IdSource, type UuidSource } from '../ids'
|
|
40
52
|
import { reduceDispatchSettings } from '../kernel/dispatch-settings'
|
|
@@ -100,15 +112,21 @@ import {
|
|
|
100
112
|
type BuildExecutionHandle,
|
|
101
113
|
} from '../ports/workspace/build-execution'
|
|
102
114
|
import type { Exec } from '../ports/workspace/git-worktree'
|
|
103
|
-
import { validateVercelGithubOrigin } from '../ports/workspace/vercel-sandbox'
|
|
104
115
|
import {
|
|
105
116
|
BUILD_EFFECTIVE_CONFIG_ARTIFACT,
|
|
106
117
|
BUILD_RUNNER_DIAGNOSTIC_ARTIFACT,
|
|
107
118
|
effectiveBuildConfigContent,
|
|
119
|
+
parseBuildConfigMetadata,
|
|
108
120
|
parseDiagnostic,
|
|
121
|
+
parseEffectiveBuildConfig,
|
|
122
|
+
selectOpenWorkspace,
|
|
109
123
|
} from '../processes/build-execution-state'
|
|
110
124
|
import { HarvestRunner, type HarvestRunnerResult } from '../processes/harvest-runner'
|
|
111
|
-
import {
|
|
125
|
+
import {
|
|
126
|
+
sampleUnclaimedObservationCount,
|
|
127
|
+
readRepoEventsIfRecorded,
|
|
128
|
+
evaluateHarvestPressureFromStore,
|
|
129
|
+
} from '../processes/harvest'
|
|
112
130
|
import { classifyHarvestOutcome } from '../processes/harvest-execution-state'
|
|
113
131
|
import type { HarvestExecution } from '../ports/workspace/harvest-execution'
|
|
114
132
|
import {
|
|
@@ -154,8 +172,12 @@ const DEFAULT_INTERVAL_MS = 10_000
|
|
|
154
172
|
const REPO_LEASE_TTL_MS = 60_000
|
|
155
173
|
const REPO_LEASE_HEARTBEAT_MS = 20_000
|
|
156
174
|
/** Repository artifact containing the schema-validated composed Config used by
|
|
157
|
-
* one dispatch run. It is the frontend's only config source.
|
|
158
|
-
|
|
175
|
+
* one dispatch run. It is the frontend's only config source. Defined in
|
|
176
|
+
* `store/retention.ts` beside the other kind constants and re-exported here
|
|
177
|
+
* (the long-standing import path). */
|
|
178
|
+
export { DISPATCHER_EFFECTIVE_CONFIG_ARTIFACT } from '../store/retention'
|
|
179
|
+
// Local uses within this module.
|
|
180
|
+
import { DISPATCHER_EFFECTIVE_CONFIG_ARTIFACT as _effectiveConfigKind } from '../store/retention'
|
|
159
181
|
|
|
160
182
|
/** JSON encoding in the config schema's declarative input shape. Parsed Config
|
|
161
183
|
* has normalized `{steps, stepConfigs}` sections; flattening named step tables
|
|
@@ -196,8 +218,9 @@ function definedEnv(env: Record<string, string | undefined>): Record<string, str
|
|
|
196
218
|
* the process-local display cache then polls dashboard-visible streams with
|
|
197
219
|
* `getEvents(lastSeq)`, retaining abort cleanup until final completion and reusing unchanged
|
|
198
220
|
* reductions/timing projections. Repository controls and Harvest are still
|
|
199
|
-
* read fresh
|
|
200
|
-
*
|
|
221
|
+
* read fresh through the bounded journal read (AUT-489 subset; AUT-545). The
|
|
222
|
+
* identical-frame check in `live.ts` makes an unchanged paint cost zero
|
|
223
|
+
* terminal writes. */
|
|
201
224
|
const DASHBOARD_POLL_MS = 500
|
|
202
225
|
|
|
203
226
|
/** Dashboard repaint (not re-read) cadence in watch mode. A running step's
|
|
@@ -295,7 +318,13 @@ const DEFAULT_TIMERS: TimerScheduler = {
|
|
|
295
318
|
|
|
296
319
|
export interface DispatchOpts {
|
|
297
320
|
/** Repo the dispatcher serves (§12: one dispatcher per repo) — the cwd, or
|
|
298
|
-
* in origin mode a private scratch root. Filesystem consumers only.
|
|
321
|
+
* in origin mode a private scratch root. Filesystem consumers only.
|
|
322
|
+
* Callers without a checkout may pass a non-filesystem identity token
|
|
323
|
+
* instead (e.g. hosted-dispatcher's `'<hosted-dispatcher>'`): it is a
|
|
324
|
+
* caller-facing label, not a path, and origin-mode state resolution
|
|
325
|
+
* replaces it with the per-origin scratch root before any filesystem
|
|
326
|
+
* consumer — plugin loading, worktrees, exec cwd — runs (AUT-604). The
|
|
327
|
+
* token therefore never participates in cwd-relative disk resolution. */
|
|
299
328
|
targetRepo: string
|
|
300
329
|
/** Checkout-less origin mode (AUT-302): serve the repository at this origin
|
|
301
330
|
* with no local checkout. CLI `--repository <origin>`, env `AB_REPOSITORY`.
|
|
@@ -433,23 +462,37 @@ async function defaultWire(
|
|
|
433
462
|
// Origin mode: no checkout exists, so the provider's host git seams are
|
|
434
463
|
// replaced by the injected origin and the forge's remote reads.
|
|
435
464
|
const originMode = opts.repository !== undefined
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
})
|
|
444
|
-
if (origin.exitCode !== 0) throw new Error('vercel-sandbox requires a readable Git origin')
|
|
445
|
-
validateVercelGithubOrigin(origin.stdout.trim())
|
|
446
|
-
}
|
|
447
|
-
if (githubTokenFromEnv(opts.env) === undefined) {
|
|
465
|
+
// Dispatch preflight is capability-driven (AUT-516): forge support, origin
|
|
466
|
+
// validation, and required environment all read the selected provider's
|
|
467
|
+
// registration declarations — builtin and plugin alike — instead of naming
|
|
468
|
+
// a provider.
|
|
469
|
+
const workspaceCaps = plugins.workspaceProviders.get(config.workspace.provider)?.capabilities
|
|
470
|
+
if (workspaceCaps?.supportedForges !== undefined) {
|
|
471
|
+
if (!workspaceCaps.supportedForges.includes(config.forge)) {
|
|
448
472
|
throw new Error(
|
|
449
|
-
|
|
473
|
+
workspaceCaps.forgeDispatchMessage ??
|
|
474
|
+
`workspace provider "${config.workspace.provider}" does not support forge "${config.forge}"`,
|
|
450
475
|
)
|
|
451
476
|
}
|
|
452
477
|
}
|
|
478
|
+
if (workspaceCaps?.validateOrigin !== undefined && !originMode) {
|
|
479
|
+
const origin = await opts.exec(['git', 'remote', 'get-url', 'origin'], {
|
|
480
|
+
cwd: opts.targetRepo,
|
|
481
|
+
})
|
|
482
|
+
if (origin.exitCode !== 0)
|
|
483
|
+
throw new Error(
|
|
484
|
+
workspaceCaps.originReadFailureMessage ??
|
|
485
|
+
`workspace provider "${config.workspace.provider}" requires a readable Git origin`,
|
|
486
|
+
)
|
|
487
|
+
workspaceCaps.validateOrigin(origin.stdout.trim())
|
|
488
|
+
}
|
|
489
|
+
for (const group of workspaceCaps?.requiredEnv ?? []) {
|
|
490
|
+
if (group.dispatchMessage === undefined) continue
|
|
491
|
+
const satisfied = group.alternatives.some((names) =>
|
|
492
|
+
names.every((name) => opts.env[name] !== undefined && opts.env[name] !== ''),
|
|
493
|
+
)
|
|
494
|
+
if (!satisfied) throw new Error(group.dispatchMessage)
|
|
495
|
+
}
|
|
453
496
|
// The builtin GitHub forge resolves its credential lazily on its first
|
|
454
497
|
// request (GITHUB_TOKEN, GH_TOKEN, then the gh CLI login) and names the
|
|
455
498
|
// miss reason on any request that had to go out anonymously; nothing is
|
|
@@ -505,6 +548,7 @@ async function defaultWire(
|
|
|
505
548
|
sandboxSetupCommand: config.commands.setup,
|
|
506
549
|
sandboxRoot: resolve(join(opened.localStateRoot, 'orchestrator-sandboxes')),
|
|
507
550
|
sandboxEnvironmentVariables: config.orchestrator.sandbox.environmentVariables,
|
|
551
|
+
orchestratorSandboxEnabled: config.orchestrator.enabled,
|
|
508
552
|
...providerSeams,
|
|
509
553
|
...(opened.token !== undefined ? { storeToken: opened.token } : {}),
|
|
510
554
|
})
|
|
@@ -779,22 +823,198 @@ class DispatchLoop {
|
|
|
779
823
|
return this.liveConfig.current()
|
|
780
824
|
}
|
|
781
825
|
|
|
826
|
+
/** Resolve this build's pinned pipeline source (SPEC §16.1). Never throws:
|
|
827
|
+
* every failure — missing build record, absent forge capability, unreadable
|
|
828
|
+
* branch/base, malformed TOML, store read failure — degrades to `source:
|
|
829
|
+
* undefined` with the failed attempts classified, never failing the
|
|
830
|
+
* deposit. The failures feed the caller's fallback diagnostic. */
|
|
831
|
+
private async resolveBuildPipeline(slug: string): Promise<{
|
|
832
|
+
source: Awaited<ReturnType<typeof resolvePipelineSource>>
|
|
833
|
+
failures: PipelineSourceFailure[]
|
|
834
|
+
events: AbEvent[]
|
|
835
|
+
}> {
|
|
836
|
+
const failures: PipelineSourceFailure[] = []
|
|
837
|
+
try {
|
|
838
|
+
// The workspace event carries the branch the build was provisioned on;
|
|
839
|
+
// the record's `branch` (when present) is the same fact. Using the event
|
|
840
|
+
// keeps this read to the events the caller already needs and avoids an
|
|
841
|
+
// extra build-record lookup on the hot launch path.
|
|
842
|
+
const events = await this.wiring.store.getEvents(slug)
|
|
843
|
+
const workspace = selectOpenWorkspace(events)
|
|
844
|
+
const source = await resolvePipelineSource({
|
|
845
|
+
slug,
|
|
846
|
+
record: { ...(workspace !== null ? { branch: workspace.branch } : {}) },
|
|
847
|
+
events,
|
|
848
|
+
mode: this.opts.repository !== undefined ? 'origin' : 'checkout',
|
|
849
|
+
forge: this.wiring.forge,
|
|
850
|
+
checkout: this.opts.targetRepo,
|
|
851
|
+
exec: this.opts.exec,
|
|
852
|
+
...(workspace !== null ? { workspacePath: workspace.path } : {}),
|
|
853
|
+
onFailure: (failure) => failures.push(failure),
|
|
854
|
+
})
|
|
855
|
+
return { source, failures, events }
|
|
856
|
+
} catch (error) {
|
|
857
|
+
failures.push({
|
|
858
|
+
kind: 'store',
|
|
859
|
+
detail: `build-event read failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
860
|
+
})
|
|
861
|
+
return { source: undefined, failures, events: [] }
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
|
|
782
865
|
/** Publish the exact composed snapshot into one build-owned namespace. The
|
|
783
|
-
* child samples only this artifact and never receives config over IPC.
|
|
866
|
+
* child samples only this artifact and never receives config over IPC.
|
|
867
|
+
*
|
|
868
|
+
* Build-owned sections (verify/finalize/commands/workspace) come from the
|
|
869
|
+
* build's own branch at its recorded pipeline-source commit; every other
|
|
870
|
+
* section comes from the live dispatcher snapshot. The source commit travels
|
|
871
|
+
* in the artifact metadata so status can report which autobuild.toml the
|
|
872
|
+
* build runs under (SPEC §16.1).
|
|
873
|
+
*
|
|
874
|
+
* A failed resolution never rewrites a pinned pipeline: when the build
|
|
875
|
+
* already has a deposit, its build-owned sections are reused verbatim (the
|
|
876
|
+
* reload path's rule), because depositing the live base-branch snapshot on a
|
|
877
|
+
* transient forge/store/git failure is exactly the AUT-366 poisoning. Only
|
|
878
|
+
* a build with no pinned artifact at all (legacy build, or a first deposit
|
|
879
|
+
* during an outage) takes the live snapshot. An actionable degradation — a
|
|
880
|
+
* missing forge capability, a build-branch source that resolved but cannot
|
|
881
|
+
* be read or parsed, a store failure — files a build-scoped observation
|
|
882
|
+
* naming the failed attempts; the documented fallbacks (an unpublished
|
|
883
|
+
* branch, an unreadable legacy base) stay quiet. */
|
|
784
884
|
private async publishBuildConfig(slug: string, snapshot = this.currentConfig()): Promise<void> {
|
|
885
|
+
const { source, failures, events } = await this.resolveBuildPipeline(slug)
|
|
886
|
+
if (source !== undefined) {
|
|
887
|
+
await this.depositBuildConfig(
|
|
888
|
+
slug,
|
|
889
|
+
snapshot,
|
|
890
|
+
composeBuildConfig(source.config, snapshot.config),
|
|
891
|
+
source.meta,
|
|
892
|
+
)
|
|
893
|
+
return
|
|
894
|
+
}
|
|
895
|
+
let reused: PipelineSourceMeta | undefined
|
|
896
|
+
try {
|
|
897
|
+
const artifact = await this.wiring.store.getArtifact(slug, BUILD_EFFECTIVE_CONFIG_ARTIFACT)
|
|
898
|
+
if (artifact !== null) {
|
|
899
|
+
const pinned = parseEffectiveBuildConfig(artifact)
|
|
900
|
+
reused = parseBuildConfigMetadata(artifact).pipelineSource
|
|
901
|
+
await this.depositBuildConfig(
|
|
902
|
+
slug,
|
|
903
|
+
snapshot,
|
|
904
|
+
composeBuildConfig(pinned, snapshot.config),
|
|
905
|
+
reused,
|
|
906
|
+
)
|
|
907
|
+
} else {
|
|
908
|
+
await this.depositBuildConfig(slug, snapshot, snapshot.config)
|
|
909
|
+
}
|
|
910
|
+
} catch {
|
|
911
|
+
// Even the artifact read failed; the live snapshot deposit is the only
|
|
912
|
+
// option left that keeps the launch alive.
|
|
913
|
+
await this.depositBuildConfig(slug, snapshot, snapshot.config)
|
|
914
|
+
}
|
|
915
|
+
await this.filePipelineFallbackObservation(slug, reused, failures, events)
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
/** File a build-scoped observation when a deposit hit an actionable
|
|
919
|
+
* pipeline-source failure (SPEC §16.1): the operator must know the deposit
|
|
920
|
+
* degraded to the last good pipeline or the live snapshot, and why. The
|
|
921
|
+
* documented fallbacks (unpublished branch, unreadable legacy base) are
|
|
922
|
+
* normal resolution order and stay quiet. A diagnostic, never a launch
|
|
923
|
+
* dependency. */
|
|
924
|
+
private async filePipelineFallbackObservation(
|
|
925
|
+
slug: string,
|
|
926
|
+
reused: PipelineSourceMeta | undefined,
|
|
927
|
+
failures: PipelineSourceFailure[],
|
|
928
|
+
events: readonly AbEvent[],
|
|
929
|
+
): Promise<void> {
|
|
930
|
+
const actionable = failures.filter(isActionablePipelineFailure)
|
|
931
|
+
if (actionable.length === 0) return
|
|
932
|
+
const degradedTo =
|
|
933
|
+
reused !== undefined
|
|
934
|
+
? 'kept the last pinned pipeline' +
|
|
935
|
+
(reused.commit !== undefined ? ` at ${reused.ref}@${reused.commit}` : ` (${reused.ref})`)
|
|
936
|
+
: 'deposited the live dispatcher snapshot (legacy fallback)'
|
|
937
|
+
const summary =
|
|
938
|
+
`pipeline-source resolution failed; ${degradedTo}. ` +
|
|
939
|
+
`Failed attempts: ${
|
|
940
|
+
actionable.map((failure) => `${failure.kind}: ${failure.detail}`).join('; ') ||
|
|
941
|
+
'none recorded'
|
|
942
|
+
}`
|
|
943
|
+
// Deduplicated against the build's log: a parked build re-attaches every
|
|
944
|
+
// sweep, and an outage must not harvest one identical diagnostic per tick.
|
|
945
|
+
if (
|
|
946
|
+
events.some(
|
|
947
|
+
(event) => event.type === 'observation.recorded' && event.payload.summary === summary,
|
|
948
|
+
)
|
|
949
|
+
) {
|
|
950
|
+
return
|
|
951
|
+
}
|
|
952
|
+
try {
|
|
953
|
+
await this.wiring.store.append(slug, {
|
|
954
|
+
// The catalog admits kernel/agent actors for observations (§8.2); the
|
|
955
|
+
// dispatcher's system-side diagnostic rides the kernel actor, like the
|
|
956
|
+
// finalize-failure and auto-merge-deferral observations.
|
|
957
|
+
actor: KERNEL,
|
|
958
|
+
type: 'observation.recorded',
|
|
959
|
+
payload: {
|
|
960
|
+
id: this.wiring.ids('obs'),
|
|
961
|
+
kind: 'followup',
|
|
962
|
+
summary,
|
|
963
|
+
files: ['autobuild.toml'],
|
|
964
|
+
},
|
|
965
|
+
})
|
|
966
|
+
} catch {
|
|
967
|
+
// The build's pipeline is already deposited; the observation is best-effort.
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
private async depositBuildConfig(
|
|
972
|
+
slug: string,
|
|
973
|
+
snapshot: ConfigSnapshot,
|
|
974
|
+
config: Config,
|
|
975
|
+
pipelineSource?: PipelineSourceMeta,
|
|
976
|
+
): Promise<void> {
|
|
785
977
|
await this.wiring.store.putArtifact(slug, {
|
|
786
978
|
kind: BUILD_EFFECTIVE_CONFIG_ARTIFACT,
|
|
787
|
-
content: effectiveBuildConfigContent(
|
|
979
|
+
content: effectiveBuildConfigContent(config),
|
|
788
980
|
metadata: {
|
|
789
981
|
revision: snapshot.revision,
|
|
790
982
|
run: this.runId,
|
|
983
|
+
...(pipelineSource !== undefined ? { pipelineSource } : {}),
|
|
791
984
|
},
|
|
792
985
|
})
|
|
793
986
|
}
|
|
794
987
|
|
|
988
|
+
/** Re-deposit deployment-owned sections to active builds on a reload. The
|
|
989
|
+
* build-owned sections are reused verbatim from the build's last deposit, so
|
|
990
|
+
* a base-branch reload can never retarget an in-flight build's pipeline; a
|
|
991
|
+
* build picks up pipeline changes only from its own branch at its next
|
|
992
|
+
* launch (SPEC §16.1). */
|
|
795
993
|
private async publishActiveBuildConfigs(snapshot: ConfigSnapshot): Promise<void> {
|
|
796
994
|
await Promise.all(
|
|
797
|
-
[...this.activeBuildRuns.keys()].map((slug) =>
|
|
995
|
+
[...this.activeBuildRuns.keys()].map(async (slug) => {
|
|
996
|
+
const artifact = await this.wiring.store.getArtifact(slug, BUILD_EFFECTIVE_CONFIG_ARTIFACT)
|
|
997
|
+
if (artifact === null) {
|
|
998
|
+
await this.publishBuildConfig(slug, snapshot)
|
|
999
|
+
return
|
|
1000
|
+
}
|
|
1001
|
+
let existing: Config
|
|
1002
|
+
try {
|
|
1003
|
+
existing = parseEffectiveBuildConfig(artifact)
|
|
1004
|
+
} catch {
|
|
1005
|
+
// A corrupt or pre-pin artifact cannot pin a pipeline; fall back to
|
|
1006
|
+
// the launch-path resolution instead of failing the reload.
|
|
1007
|
+
await this.publishBuildConfig(slug, snapshot)
|
|
1008
|
+
return
|
|
1009
|
+
}
|
|
1010
|
+
const meta = parseBuildConfigMetadata(artifact)
|
|
1011
|
+
await this.depositBuildConfig(
|
|
1012
|
+
slug,
|
|
1013
|
+
snapshot,
|
|
1014
|
+
composeBuildConfig(existing, snapshot.config),
|
|
1015
|
+
meta.pipelineSource,
|
|
1016
|
+
)
|
|
1017
|
+
}),
|
|
798
1018
|
)
|
|
799
1019
|
}
|
|
800
1020
|
|
|
@@ -805,7 +1025,10 @@ class DispatchLoop {
|
|
|
805
1025
|
private async refreshConfig(): Promise<void> {
|
|
806
1026
|
if (this.opts.once === true) return
|
|
807
1027
|
// Origin mode reloads from the forge: autobuild.toml at the CURRENT
|
|
808
|
-
// effective base branch (hot — a rename converges within two ticks).
|
|
1028
|
+
// effective base branch (hot — a rename converges within two ticks). This
|
|
1029
|
+
// read feeds the dispatcher's own snapshot; a reload re-deposits only the
|
|
1030
|
+
// deployment-owned sections to active builds (build-owned pipeline
|
|
1031
|
+
// sections stay pinned to each build's own branch, SPEC §16.1).
|
|
809
1032
|
// Restart-required changes keep startup-built adapters and file the
|
|
810
1033
|
// existing restart-required observation (LiveConfig semantics).
|
|
811
1034
|
const outcome =
|
|
@@ -861,7 +1084,8 @@ class DispatchLoop {
|
|
|
861
1084
|
}
|
|
862
1085
|
|
|
863
1086
|
private async readDispatchSettings(): Promise<ReturnType<typeof reduceDispatchSettings>> {
|
|
864
|
-
|
|
1087
|
+
// Bounded read (AUT-489): the settings reducer consumes durable types only.
|
|
1088
|
+
const events = await this.wiring.store.getRepoStateEvents(this.repoIdentity)
|
|
865
1089
|
return reduceDispatchSettings(events)
|
|
866
1090
|
}
|
|
867
1091
|
|
|
@@ -884,12 +1108,17 @@ class DispatchLoop {
|
|
|
884
1108
|
})
|
|
885
1109
|
|
|
886
1110
|
// Unclaimed observations are display-only and sampled once per interactive
|
|
887
|
-
// dispatcher tick
|
|
888
|
-
// the
|
|
1111
|
+
// dispatcher tick from the journal and one repo-scoped digest read
|
|
1112
|
+
// (AUT-487) — flat in the finished-build count. A journal record that
|
|
1113
|
+
// does not yet exist reads as an empty journal (AUT-524); the sample
|
|
1114
|
+
// writes nothing. Any other failed sample must neither fail dispatch nor
|
|
1115
|
+
// replace the last complete measurement with a fabricated zero.
|
|
889
1116
|
if (this.dashboard) {
|
|
890
1117
|
try {
|
|
891
|
-
|
|
892
|
-
|
|
1118
|
+
this.observationCount = await sampleUnclaimedObservationCount(
|
|
1119
|
+
this.wiring.store,
|
|
1120
|
+
this.repoIdentity,
|
|
1121
|
+
)
|
|
893
1122
|
} catch {
|
|
894
1123
|
// Display-only sampling failures retain the last factual count and
|
|
895
1124
|
// retry on the next tick; they are not dashboard failures.
|
|
@@ -1175,6 +1404,7 @@ class DispatchLoop {
|
|
|
1175
1404
|
repo: this.repoIdentity,
|
|
1176
1405
|
user: buildControlUser(this.opts.env),
|
|
1177
1406
|
setting: 'intake',
|
|
1407
|
+
run: this.runId,
|
|
1178
1408
|
})
|
|
1179
1409
|
this.say(`dispatcher intake ${event.enabled ? 'ON' : 'OFF'}`)
|
|
1180
1410
|
await this.renderOnce()
|
|
@@ -1439,6 +1669,7 @@ class DispatchLoop {
|
|
|
1439
1669
|
repo: this.repoIdentity,
|
|
1440
1670
|
user: buildControlUser(this.opts.env),
|
|
1441
1671
|
setting: 'auto-merge-default',
|
|
1672
|
+
run: this.runId,
|
|
1442
1673
|
})
|
|
1443
1674
|
this.say(`dispatcher auto-merge default ${event.enabled ? 'ON' : 'OFF'}`)
|
|
1444
1675
|
await this.renderOnce()
|
|
@@ -2056,7 +2287,9 @@ class DispatchLoop {
|
|
|
2056
2287
|
// once provisioned, so a threshold crossing between gate and guest scan
|
|
2057
2288
|
// is never suppressed by this check.
|
|
2058
2289
|
const record = await store.getRepo(repo)
|
|
2059
|
-
|
|
2290
|
+
// Bounded read (AUT-489): the harvest control decision reduces durable
|
|
2291
|
+
// types only.
|
|
2292
|
+
const events = record === null ? [] : await store.getRepoStateEvents(repo)
|
|
2060
2293
|
const state = reduceHarvest(events)
|
|
2061
2294
|
const control = decideHarvestControl(state, this.maxHarvestRecoveryAttempts)
|
|
2062
2295
|
const resumePending =
|
|
@@ -2066,8 +2299,16 @@ class DispatchLoop {
|
|
|
2066
2299
|
control.kind === 'request-recovery' ||
|
|
2067
2300
|
control.kind === 'exhaust-recovery'
|
|
2068
2301
|
if (!resumePending) {
|
|
2069
|
-
|
|
2070
|
-
|
|
2302
|
+
// Flat-cost gate (AUT-521): the journal read above plus one
|
|
2303
|
+
// repo-scoped digest batch read — no per-build history reads, so the
|
|
2304
|
+
// per-evaluation cost no longer grows with accumulated finished
|
|
2305
|
+
// builds. The guest rescans authoritatively once provisioned.
|
|
2306
|
+
const pressure = await evaluateHarvestPressureFromStore({
|
|
2307
|
+
store,
|
|
2308
|
+
repo,
|
|
2309
|
+
harvestEvents: events,
|
|
2310
|
+
policy: this.currentConfig().config.policy,
|
|
2311
|
+
})
|
|
2071
2312
|
if (pressure.trigger === undefined) return
|
|
2072
2313
|
}
|
|
2073
2314
|
|
|
@@ -2165,7 +2406,8 @@ class DispatchLoop {
|
|
|
2165
2406
|
startedSeq: number,
|
|
2166
2407
|
): Promise<HarvestRunnerResult> {
|
|
2167
2408
|
const { store } = this.wiring
|
|
2168
|
-
|
|
2409
|
+
// Bounded read (AUT-489): classifyHarvestOutcome reduces harvest facts only.
|
|
2410
|
+
const events = await store.getRepoStateEvents(repo)
|
|
2169
2411
|
const record = await store.getRepo(repo)
|
|
2170
2412
|
return classifyHarvestOutcome(events, {
|
|
2171
2413
|
executionStartedSeq: startedSeq,
|
|
@@ -2839,9 +3081,14 @@ class DispatchLoop {
|
|
|
2839
3081
|
configSnapshot.config,
|
|
2840
3082
|
configSnapshot.revision,
|
|
2841
3083
|
)
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
3084
|
+
// Bounded read (AUT-489, AUT-545): every repository consumer of this frame —
|
|
3085
|
+
// projectRepositoryHarvest (reduceHarvest plus the harvest-only raw scans in
|
|
3086
|
+
// projectHarvestRun/escalationAttentionDismissed) and reduceDispatchSettings —
|
|
3087
|
+
// reduces durable types only, so the AUT-489 subset is replay-equivalent with
|
|
3088
|
+
// no anchor dependence (anchor analysis recorded; the frontend seed needed one
|
|
3089
|
+
// only because reduceDispatchStatus folds here too — it does not here).
|
|
3090
|
+
// Missing journal record stays a read-only empty journal (AUT-524).
|
|
3091
|
+
const repositoryEvents = await readRepoEventsIfRecorded(this.wiring.store, this.repoIdentity)
|
|
2845
3092
|
|
|
2846
3093
|
// Action-triggered and timer refreshes share the cache but may finish their
|
|
2847
3094
|
// repository reads out of order. Never let an older build snapshot replace
|
|
@@ -3502,10 +3749,23 @@ export async function abDispatch(opts: DispatchOpts): Promise<void> {
|
|
|
3502
3749
|
`origin-mode dispatch requires the builtin github forge, but the fetched autobuild.toml selects ${JSON.stringify(config.forge)}`,
|
|
3503
3750
|
)
|
|
3504
3751
|
}
|
|
3505
|
-
|
|
3752
|
+
// Origin mode has no checkout, so repository-path plugin specifiers
|
|
3753
|
+
// (relative, absolute, file:) cannot resolve — reject them with the
|
|
3754
|
+
// existing remediation tone before any wiring. Bare package specifiers
|
|
3755
|
+
// are accepted: the shared loadPlugins below resolves them from the
|
|
3756
|
+
// dispatcher's own installation when the scratch root lacks them
|
|
3757
|
+
// (AUT-517), which is exactly "resolvable from the dispatcher's own
|
|
3758
|
+
// installation". The caller-supplied `targetRepo` is an identity token,
|
|
3759
|
+
// not a path, and never reaches plugin resolution: the candidate roots
|
|
3760
|
+
// are the scratch checkout and the installation, per the loadPlugins
|
|
3761
|
+
// call below (AUT-604).
|
|
3762
|
+
const repoPathPlugin = (config.plugins ?? []).find(
|
|
3763
|
+
(moduleSpecifier) => pluginResolutionKind(moduleSpecifier) === 'repo-path',
|
|
3764
|
+
)
|
|
3765
|
+
if (repoPathPlugin !== undefined) {
|
|
3506
3766
|
throw new Error(
|
|
3507
|
-
|
|
3508
|
-
'and there is no checkout. Remove [plugins]
|
|
3767
|
+
`origin-mode dispatch cannot load the repository-path plugin ${JSON.stringify(repoPathPlugin)}: ` +
|
|
3768
|
+
'plugin code is checkout-relative and there is no checkout. Remove repository-path entries from [plugins] in the base branch autobuild.toml or run from a checkout',
|
|
3509
3769
|
)
|
|
3510
3770
|
}
|
|
3511
3771
|
} else {
|
|
@@ -3524,8 +3784,22 @@ export async function abDispatch(opts: DispatchOpts): Promise<void> {
|
|
|
3524
3784
|
}
|
|
3525
3785
|
// Configured plugin code is trusted like configured shell commands, but it
|
|
3526
3786
|
// must resolve, evaluate, validate, and register before production wiring
|
|
3527
|
-
// opens a store, claims a ticket, or launches a runner.
|
|
3528
|
-
|
|
3787
|
+
// opens a store, claims a ticket, or launches a runner. Skipped-module
|
|
3788
|
+
// notices (builtin duplicate-skip, guest tolerance) surface on dispatch's
|
|
3789
|
+
// own stderr channel.
|
|
3790
|
+
// AUT-604: origin mode's repository candidate root for plugin loading is
|
|
3791
|
+
// the per-origin scratch checkout, never the caller-supplied `targetRepo`
|
|
3792
|
+
// token (e.g. hosted-dispatcher's `'<hosted-dispatcher>'`) — that token is
|
|
3793
|
+
// a caller-facing identity label which `resolveOriginModeState` replaced
|
|
3794
|
+
// with `state.checkout` above, so it never participates in cwd-relative
|
|
3795
|
+
// disk resolution. `state.checkout` equals `resolvedOpts.targetRepo` in
|
|
3796
|
+
// both modes, so passing it as `packageRoot` is behavior-preserving; the
|
|
3797
|
+
// installation candidate (AUT-517) and the disk-first EROFS gate (AUT-587)
|
|
3798
|
+
// are unchanged.
|
|
3799
|
+
const plugins = await loadPlugins(config.plugins, resolvedOpts.targetRepo, {
|
|
3800
|
+
packageRoot: state.checkout,
|
|
3801
|
+
onNotice: (line) => resolvedOpts.stderr(line),
|
|
3802
|
+
})
|
|
3529
3803
|
// Validate the selector against the complete catalog before either custom
|
|
3530
3804
|
// wiring or production wiring can open state or perform side effects.
|
|
3531
3805
|
resolveForgeRegistration(config.forge, plugins)
|
|
@@ -3580,7 +3854,7 @@ export async function abDispatch(opts: DispatchOpts): Promise<void> {
|
|
|
3580
3854
|
metadata: { restartRequired: [...restartRequired], effectiveChanged },
|
|
3581
3855
|
},
|
|
3582
3856
|
{
|
|
3583
|
-
kind:
|
|
3857
|
+
kind: _effectiveConfigKind,
|
|
3584
3858
|
content: effectiveConfigContent(effectiveConfig),
|
|
3585
3859
|
metadata: { run: runId, effectiveChanged },
|
|
3586
3860
|
},
|
|
@@ -3636,7 +3910,7 @@ export async function abDispatch(opts: DispatchOpts): Promise<void> {
|
|
|
3636
3910
|
state.repo,
|
|
3637
3911
|
[
|
|
3638
3912
|
{
|
|
3639
|
-
kind:
|
|
3913
|
+
kind: _effectiveConfigKind,
|
|
3640
3914
|
content: effectiveConfigContent(config),
|
|
3641
3915
|
metadata: { run: runId, revision: 0 },
|
|
3642
3916
|
},
|