@defrex/autobuild 0.8.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/SPEC.md +323 -26
- package/bin/ab-build-runner.ts +8 -2
- package/docs/architecture.md +166 -14
- package/docs/assets/headline-wide.png +0 -0
- package/docs/configuration.md +175 -28
- package/docs/hosted-dispatcher.md +69 -13
- package/docs/operator-api.md +2 -2
- package/docs/remote-store-protocol.md +204 -16
- package/docs/remote-ticket-protocol.md +1 -1
- package/docs/setup.md +12 -9
- package/docs/website.md +14 -0
- package/package.json +45 -23
- package/packages/core/src/cli/artifact.ts +130 -6
- package/packages/core/src/cli/binary.ts +9 -1
- package/packages/core/src/cli/build-control.ts +110 -13
- package/packages/core/src/cli/context.ts +46 -0
- package/packages/core/src/cli/dashboard/facts.ts +215 -0
- package/packages/core/src/cli/dashboard/frame-parse.ts +223 -0
- package/packages/core/src/cli/dashboard/model.ts +141 -153
- package/packages/core/src/cli/dashboard/poll.ts +76 -12
- package/packages/core/src/cli/dashboard/render.ts +34 -5
- package/packages/core/src/cli/dispatch-frontend.ts +54 -5
- package/packages/core/src/cli/dispatch-process.ts +1 -13
- package/packages/core/src/cli/dispatch.ts +392 -75
- package/packages/core/src/cli/harvest.ts +122 -8
- package/packages/core/src/cli/help.ts +62 -9
- package/packages/core/src/cli/init-readiness-remote.ts +110 -0
- package/packages/core/src/cli/init-readiness-shared.ts +72 -0
- package/packages/core/src/cli/init-validation.ts +266 -222
- package/packages/core/src/cli/installation.ts +50 -8
- package/packages/core/src/cli/main.ts +70 -11
- package/packages/core/src/cli/mcp.ts +23 -1
- package/packages/core/src/cli/plugin.ts +7 -4
- package/packages/core/src/cli/remote-poll.ts +277 -0
- package/packages/core/src/cli/repo-state.ts +22 -0
- package/packages/core/src/cli/repository-status.ts +39 -1
- package/packages/core/src/cli/self-update.ts +77 -2
- package/packages/core/src/cli/status.ts +150 -28
- package/packages/core/src/cli/terminals.ts +2 -1
- package/packages/core/src/cli/ticket-assets.ts +247 -0
- package/packages/core/src/cli/ticket.ts +453 -8
- package/packages/core/src/cli/wait.ts +157 -41
- package/packages/core/src/cli/watch.ts +138 -168
- package/packages/core/src/config/live.ts +23 -0
- package/packages/core/src/config/pipeline-source.ts +388 -0
- package/packages/core/src/config/schema.ts +251 -19
- package/packages/core/src/events/catalog.ts +1 -0
- package/packages/core/src/events/globs.ts +47 -0
- package/packages/core/src/events/payloads.ts +52 -2
- package/packages/core/src/events/repository.ts +52 -1
- package/packages/core/src/events/sessions.ts +69 -10
- package/packages/core/src/events/workspace-remote.ts +12 -0
- package/packages/core/src/integration/harness.ts +115 -2
- package/packages/core/src/kernel/__snapshots__/reducer-registry.test.ts.snap +1369 -0
- package/packages/core/src/kernel/auto-merge-default.ts +157 -0
- package/packages/core/src/kernel/auto-merge.ts +99 -22
- package/packages/core/src/kernel/dispatch-settings.ts +42 -17
- package/packages/core/src/kernel/dispatch-status.ts +23 -0
- package/packages/core/src/kernel/engine.ts +9 -438
- package/packages/core/src/kernel/generators/build-log.ts +259 -0
- package/packages/core/src/kernel/generators/repository-journals.ts +206 -0
- package/packages/core/src/kernel/generators/session-log.ts +72 -0
- package/packages/core/src/kernel/harvest.ts +62 -18
- package/packages/core/src/kernel/incremental-contract.ts +75 -0
- package/packages/core/src/kernel/incremental-fixtures.ts +211 -0
- package/packages/core/src/kernel/incremental.ts +53 -0
- package/packages/core/src/kernel/log-index.ts +620 -0
- package/packages/core/src/kernel/phases.ts +16 -2
- package/packages/core/src/kernel/pr-attachments.ts +164 -46
- package/packages/core/src/kernel/reducer-registry.ts +164 -0
- package/packages/core/src/kernel/reducer.ts +254 -73
- package/packages/core/src/operator/annotations.ts +6 -0
- package/packages/core/src/operator/control.ts +19 -5
- package/packages/core/src/operator/index.ts +71 -6
- package/packages/core/src/operator/query.ts +253 -24
- package/packages/core/src/operator/read-view.ts +98 -0
- package/packages/core/src/operator/registry.ts +55 -10
- package/packages/core/src/operator/requests.ts +9 -6
- package/packages/core/src/operator/sandbox-host.ts +140 -0
- package/packages/core/src/operator/sandbox.ts +247 -14
- package/packages/core/src/operator/ticket-source-backend.ts +47 -0
- package/packages/core/src/orchestrator/approvals.ts +115 -0
- package/packages/core/src/orchestrator/conversation.ts +167 -0
- package/packages/core/src/orchestrator/model.ts +86 -0
- package/packages/core/src/orchestrator/prompt.ts +61 -0
- package/packages/core/src/orchestrator/tools.ts +117 -0
- package/packages/core/src/orchestrator/turn-runner.ts +457 -0
- package/packages/core/src/plugin-sdk/index.ts +51 -1
- package/packages/core/src/plugins/load.ts +135 -7
- package/packages/core/src/plugins/manifest.ts +183 -10
- package/packages/core/src/plugins/registry.ts +73 -4
- package/packages/core/src/ports/forge/create.ts +1 -1
- package/packages/core/src/ports/forge/fake.ts +31 -0
- package/packages/core/src/ports/forge/github-transport.ts +13 -2
- package/packages/core/src/ports/forge/github.ts +13 -2
- package/packages/core/src/ports/forge/local-git.ts +31 -1
- package/packages/core/src/ports/runner/claude.ts +2 -2
- package/packages/core/src/ports/runner/codex.ts +6 -2
- package/packages/core/src/ports/runner/fake.ts +7 -1
- package/packages/core/src/ports/runner/pi.ts +8 -6
- package/packages/core/src/ports/runner/provider-error.ts +13 -3
- package/packages/core/src/ports/tickets/fake.ts +5 -1
- package/packages/core/src/ports/tickets/file.ts +169 -48
- package/packages/core/src/ports/tickets/{remote-index.ts → hosted-index.ts} +3 -2
- package/packages/core/src/ports/tickets/linear.ts +33 -1
- package/packages/core/src/ports/tickets/operations.ts +1 -1
- package/packages/core/src/ports/tickets/remote.ts +8 -1
- package/packages/core/src/ports/types.ts +49 -3
- package/packages/core/src/ports/workspace/builtin-capabilities.ts +114 -0
- package/packages/core/src/ports/workspace/create.ts +74 -39
- package/packages/core/src/ports/workspace/distribution-archive.ts +171 -12
- package/packages/core/src/ports/workspace/fake.ts +169 -4
- package/packages/core/src/ports/workspace/git-worktree.ts +246 -26
- package/packages/core/src/ports/workspace/github-origin.ts +36 -0
- package/packages/core/src/ports/workspace/operator-sandbox.ts +7 -1
- package/packages/core/src/ports/workspace/provider-capabilities.ts +171 -0
- package/packages/core/src/ports/workspace/vercel-capabilities.ts +110 -0
- package/packages/core/src/ports/workspace/vercel-sandbox.ts +142 -114
- package/packages/core/src/processes/build-child.ts +17 -2
- package/packages/core/src/processes/build-execution-state.ts +68 -15
- package/packages/core/src/processes/build-runner.ts +238 -93
- package/packages/core/src/processes/dispatcher-selectors.ts +123 -0
- package/packages/core/src/processes/dispatcher.ts +501 -201
- package/packages/core/src/processes/execution-settlement.ts +72 -39
- package/packages/core/src/processes/harvest-child.ts +5 -0
- package/packages/core/src/processes/harvest-execution-state.ts +41 -29
- package/packages/core/src/processes/harvest-runner.ts +66 -19
- package/packages/core/src/processes/harvest.ts +196 -24
- package/packages/core/src/processes/infrastructure-failure-budget.ts +31 -18
- package/packages/core/src/processes/orchestrator-tick.ts +360 -0
- package/packages/core/src/processes/publication-state.ts +164 -52
- package/packages/core/src/processes/repo-view.ts +1436 -0
- package/packages/core/src/processes/sandbox-state.ts +36 -2
- package/packages/core/src/processes/tick-state.ts +33 -0
- package/packages/core/src/store/adapter.ts +41 -0
- package/packages/core/src/store/build-scope.ts +90 -0
- package/packages/core/src/store/contract.ts +1205 -0
- package/packages/core/src/store/digest.ts +102 -0
- package/packages/core/src/store/local/schema.ts +46 -2
- package/packages/core/src/store/local/store.ts +445 -4
- package/packages/core/src/store/memory.ts +246 -0
- package/packages/core/src/store/new-build.ts +17 -0
- package/packages/core/src/store/phase-session.ts +94 -0
- package/packages/core/src/store/remote/client.ts +310 -2
- package/packages/core/src/store/remote/index.ts +14 -2
- package/packages/core/src/store/remote/protocol.ts +115 -12
- package/packages/core/src/store/remote/version.ts +6 -2
- package/packages/core/src/store/repo-state-events.ts +205 -0
- package/packages/core/src/store/retention.ts +7 -0
- package/packages/core/src/store/session-handle.ts +94 -0
- package/packages/core/src/{sessions/reducer.ts → store/session-reducer.ts} +79 -19
- package/packages/core/src/store/snapshots.ts +88 -0
- package/packages/core/src/store/streams/session-writer.ts +42 -2
- package/packages/core/src/store/streams/wait.ts +4 -5
- package/packages/core/src/store/ticket-assets.ts +458 -0
- package/packages/core/src/store/types.ts +160 -1
- package/packages/core/src/testing/index.ts +59 -0
- package/skills/code-review/SKILL.md +5 -1
- package/skills/guide/SKILL.md +152 -40
- package/skills/guide/references/operator-api.md +2 -2
- package/skills/guide/references/plugin-authoring.md +17 -0
- package/skills/guide/references/remote-store-protocol.md +204 -16
- package/skills/guide/references/remote-ticket-protocol.md +1 -1
- package/skills/guide/references/setup.md +12 -9
- package/skills/implement/SKILL.md +3 -0
- package/skills/operate/SKILL.md +126 -0
- package/skills/plan/SKILL.md +4 -0
- package/skills/plan-review/SKILL.md +5 -1
- package/skills/spec/SKILL.md +46 -3
- package/skills/tickets/SKILL.md +57 -5
- package/packages/core/src/cli/dashboard/frame-image.ts +0 -373
- package/packages/core/src/cli/store-opening.contract.ts +0 -289
- package/packages/core/src/cli/testkit.ts +0 -194
- package/packages/core/src/markdown.ts +0 -67
- package/packages/core/src/operator/client.ts +0 -311
- package/packages/core/src/operator/server.ts +0 -616
- package/packages/core/src/operator/sessions.ts +0 -223
- package/packages/core/src/ports/runner/live-contract-fixture.ts +0 -176
- package/packages/core/src/ports/tickets/remote-server.ts +0 -185
- package/packages/core/src/store/remote/server.ts +0 -923
- package/packages/core/src/testing/store-failures.ts +0 -32
|
@@ -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 {
|
|
@@ -122,6 +140,7 @@ import {
|
|
|
122
140
|
type LaunchRunnerResult,
|
|
123
141
|
type TickReport,
|
|
124
142
|
} from '../processes/dispatcher'
|
|
143
|
+
import { RepoViewStore } from '../processes/repo-view'
|
|
125
144
|
import {
|
|
126
145
|
BuildControlError,
|
|
127
146
|
buildControlUser,
|
|
@@ -154,8 +173,12 @@ const DEFAULT_INTERVAL_MS = 10_000
|
|
|
154
173
|
const REPO_LEASE_TTL_MS = 60_000
|
|
155
174
|
const REPO_LEASE_HEARTBEAT_MS = 20_000
|
|
156
175
|
/** Repository artifact containing the schema-validated composed Config used by
|
|
157
|
-
* one dispatch run. It is the frontend's only config source.
|
|
158
|
-
|
|
176
|
+
* one dispatch run. It is the frontend's only config source. Defined in
|
|
177
|
+
* `store/retention.ts` beside the other kind constants and re-exported here
|
|
178
|
+
* (the long-standing import path). */
|
|
179
|
+
export { DISPATCHER_EFFECTIVE_CONFIG_ARTIFACT } from '../store/retention'
|
|
180
|
+
// Local uses within this module.
|
|
181
|
+
import { DISPATCHER_EFFECTIVE_CONFIG_ARTIFACT as _effectiveConfigKind } from '../store/retention'
|
|
159
182
|
|
|
160
183
|
/** JSON encoding in the config schema's declarative input shape. Parsed Config
|
|
161
184
|
* has normalized `{steps, stepConfigs}` sections; flattening named step tables
|
|
@@ -196,8 +219,9 @@ function definedEnv(env: Record<string, string | undefined>): Record<string, str
|
|
|
196
219
|
* the process-local display cache then polls dashboard-visible streams with
|
|
197
220
|
* `getEvents(lastSeq)`, retaining abort cleanup until final completion and reusing unchanged
|
|
198
221
|
* reductions/timing projections. Repository controls and Harvest are still
|
|
199
|
-
* read fresh
|
|
200
|
-
*
|
|
222
|
+
* read fresh through the bounded journal read (AUT-489 subset; AUT-545). The
|
|
223
|
+
* identical-frame check in `live.ts` makes an unchanged paint cost zero
|
|
224
|
+
* terminal writes. */
|
|
201
225
|
const DASHBOARD_POLL_MS = 500
|
|
202
226
|
|
|
203
227
|
/** Dashboard repaint (not re-read) cadence in watch mode. A running step's
|
|
@@ -295,7 +319,13 @@ const DEFAULT_TIMERS: TimerScheduler = {
|
|
|
295
319
|
|
|
296
320
|
export interface DispatchOpts {
|
|
297
321
|
/** Repo the dispatcher serves (§12: one dispatcher per repo) — the cwd, or
|
|
298
|
-
* in origin mode a private scratch root. Filesystem consumers only.
|
|
322
|
+
* in origin mode a private scratch root. Filesystem consumers only.
|
|
323
|
+
* Callers without a checkout may pass a non-filesystem identity token
|
|
324
|
+
* instead (e.g. hosted-dispatcher's `'<hosted-dispatcher>'`): it is a
|
|
325
|
+
* caller-facing label, not a path, and origin-mode state resolution
|
|
326
|
+
* replaces it with the per-origin scratch root before any filesystem
|
|
327
|
+
* consumer — plugin loading, worktrees, exec cwd — runs (AUT-604). The
|
|
328
|
+
* token therefore never participates in cwd-relative disk resolution. */
|
|
299
329
|
targetRepo: string
|
|
300
330
|
/** Checkout-less origin mode (AUT-302): serve the repository at this origin
|
|
301
331
|
* with no local checkout. CLI `--repository <origin>`, env `AB_REPOSITORY`.
|
|
@@ -433,23 +463,37 @@ async function defaultWire(
|
|
|
433
463
|
// Origin mode: no checkout exists, so the provider's host git seams are
|
|
434
464
|
// replaced by the injected origin and the forge's remote reads.
|
|
435
465
|
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) {
|
|
466
|
+
// Dispatch preflight is capability-driven (AUT-516): forge support, origin
|
|
467
|
+
// validation, and required environment all read the selected provider's
|
|
468
|
+
// registration declarations — builtin and plugin alike — instead of naming
|
|
469
|
+
// a provider.
|
|
470
|
+
const workspaceCaps = plugins.workspaceProviders.get(config.workspace.provider)?.capabilities
|
|
471
|
+
if (workspaceCaps?.supportedForges !== undefined) {
|
|
472
|
+
if (!workspaceCaps.supportedForges.includes(config.forge)) {
|
|
448
473
|
throw new Error(
|
|
449
|
-
|
|
474
|
+
workspaceCaps.forgeDispatchMessage ??
|
|
475
|
+
`workspace provider "${config.workspace.provider}" does not support forge "${config.forge}"`,
|
|
450
476
|
)
|
|
451
477
|
}
|
|
452
478
|
}
|
|
479
|
+
if (workspaceCaps?.validateOrigin !== undefined && !originMode) {
|
|
480
|
+
const origin = await opts.exec(['git', 'remote', 'get-url', 'origin'], {
|
|
481
|
+
cwd: opts.targetRepo,
|
|
482
|
+
})
|
|
483
|
+
if (origin.exitCode !== 0)
|
|
484
|
+
throw new Error(
|
|
485
|
+
workspaceCaps.originReadFailureMessage ??
|
|
486
|
+
`workspace provider "${config.workspace.provider}" requires a readable Git origin`,
|
|
487
|
+
)
|
|
488
|
+
workspaceCaps.validateOrigin(origin.stdout.trim())
|
|
489
|
+
}
|
|
490
|
+
for (const group of workspaceCaps?.requiredEnv ?? []) {
|
|
491
|
+
if (group.dispatchMessage === undefined) continue
|
|
492
|
+
const satisfied = group.alternatives.some((names) =>
|
|
493
|
+
names.every((name) => opts.env[name] !== undefined && opts.env[name] !== ''),
|
|
494
|
+
)
|
|
495
|
+
if (!satisfied) throw new Error(group.dispatchMessage)
|
|
496
|
+
}
|
|
453
497
|
// The builtin GitHub forge resolves its credential lazily on its first
|
|
454
498
|
// request (GITHUB_TOKEN, GH_TOKEN, then the gh CLI login) and names the
|
|
455
499
|
// miss reason on any request that had to go out anonymously; nothing is
|
|
@@ -505,6 +549,7 @@ async function defaultWire(
|
|
|
505
549
|
sandboxSetupCommand: config.commands.setup,
|
|
506
550
|
sandboxRoot: resolve(join(opened.localStateRoot, 'orchestrator-sandboxes')),
|
|
507
551
|
sandboxEnvironmentVariables: config.orchestrator.sandbox.environmentVariables,
|
|
552
|
+
orchestratorSandboxEnabled: config.orchestrator.enabled,
|
|
508
553
|
...providerSeams,
|
|
509
554
|
...(opened.token !== undefined ? { storeToken: opened.token } : {}),
|
|
510
555
|
})
|
|
@@ -553,6 +598,8 @@ interface ActiveBuildExecution {
|
|
|
553
598
|
* running, so the completion chain must append no `execution.ended`, release
|
|
554
599
|
* no lease, settle no publication, and record no wait failure. */
|
|
555
600
|
detaching?: boolean
|
|
601
|
+
/** A wait failure being recorded; teardown lets it finish rather than drop it. */
|
|
602
|
+
recording?: Promise<void>
|
|
556
603
|
}
|
|
557
604
|
|
|
558
605
|
/** The dispatch loop owns deterministic decisions and supervises one
|
|
@@ -560,6 +607,10 @@ interface ActiveBuildExecution {
|
|
|
560
607
|
* Store; child completion is liveness evidence used for reaping/single-flight. */
|
|
561
608
|
class DispatchLoop {
|
|
562
609
|
private readonly dispatcher: Dispatcher
|
|
610
|
+
/** The incremental repository view (AUT-647) the dispatcher and this
|
|
611
|
+
* frontend's tick-path journal reads and writes share. Resident for a
|
|
612
|
+
* watch loop; `--once` builds it cold for its single tick. */
|
|
613
|
+
private readonly repoView: RepoViewStore
|
|
563
614
|
private readonly host = hostname()
|
|
564
615
|
private readonly maxHarvestRecoveryAttempts = DEFAULT_MAX_HARVEST_RECOVERY_ATTEMPTS
|
|
565
616
|
/** In-flight build and harvest runs (fire-and-forget) — awaited before a
|
|
@@ -705,6 +756,8 @@ class DispatchLoop {
|
|
|
705
756
|
private readonly liveConfig: LiveConfig,
|
|
706
757
|
private readonly wiring: DispatchWiring,
|
|
707
758
|
private readonly opts: DispatchOpts,
|
|
759
|
+
/** The view `abDispatch` already used for its lifecycle appends. */
|
|
760
|
+
view?: RepoViewStore,
|
|
708
761
|
) {
|
|
709
762
|
const config = liveConfig.current().config
|
|
710
763
|
// Minted exactly once per invocation: `wiring.ids` is `randomIds()` and a
|
|
@@ -741,8 +794,16 @@ class DispatchLoop {
|
|
|
741
794
|
return result.text
|
|
742
795
|
}
|
|
743
796
|
|
|
797
|
+
// `abDispatch` hands over a view it already used for the lifecycle appends,
|
|
798
|
+
// so those rows are folded, not re-read, and one flush covers them all.
|
|
799
|
+
this.repoView =
|
|
800
|
+
view ??
|
|
801
|
+
new RepoViewStore(wiring.store, {
|
|
802
|
+
repo: this.repoIdentity,
|
|
803
|
+
resident: opts.once !== true,
|
|
804
|
+
})
|
|
744
805
|
this.dispatcher = new Dispatcher({
|
|
745
|
-
store:
|
|
806
|
+
store: this.repoView,
|
|
746
807
|
tickets: wiring.tickets,
|
|
747
808
|
workspaces: wiring.workspaces,
|
|
748
809
|
...(wiring.retiredWorkspaces === undefined
|
|
@@ -779,33 +840,212 @@ class DispatchLoop {
|
|
|
779
840
|
return this.liveConfig.current()
|
|
780
841
|
}
|
|
781
842
|
|
|
843
|
+
/** Resolve this build's pinned pipeline source (SPEC §16.1). Never throws:
|
|
844
|
+
* every failure — missing build record, absent forge capability, unreadable
|
|
845
|
+
* branch/base, malformed TOML, store read failure — degrades to `source:
|
|
846
|
+
* undefined` with the failed attempts classified, never failing the
|
|
847
|
+
* deposit. The failures feed the caller's fallback diagnostic. */
|
|
848
|
+
private async resolveBuildPipeline(slug: string): Promise<{
|
|
849
|
+
source: Awaited<ReturnType<typeof resolvePipelineSource>>
|
|
850
|
+
failures: PipelineSourceFailure[]
|
|
851
|
+
events: AbEvent[]
|
|
852
|
+
}> {
|
|
853
|
+
const failures: PipelineSourceFailure[] = []
|
|
854
|
+
try {
|
|
855
|
+
// The workspace event carries the branch the build was provisioned on;
|
|
856
|
+
// the record's `branch` (when present) is the same fact. Using the event
|
|
857
|
+
// keeps this read to the events the caller already needs and avoids an
|
|
858
|
+
// extra build-record lookup on the hot launch path.
|
|
859
|
+
const events = await this.repoView.getEvents(slug)
|
|
860
|
+
const workspace = selectOpenWorkspace(events)
|
|
861
|
+
const source = await resolvePipelineSource({
|
|
862
|
+
slug,
|
|
863
|
+
record: { ...(workspace !== null ? { branch: workspace.branch } : {}) },
|
|
864
|
+
events,
|
|
865
|
+
mode: this.opts.repository !== undefined ? 'origin' : 'checkout',
|
|
866
|
+
forge: this.wiring.forge,
|
|
867
|
+
checkout: this.opts.targetRepo,
|
|
868
|
+
exec: this.opts.exec,
|
|
869
|
+
...(workspace !== null ? { workspacePath: workspace.path } : {}),
|
|
870
|
+
onFailure: (failure) => failures.push(failure),
|
|
871
|
+
})
|
|
872
|
+
return { source, failures, events }
|
|
873
|
+
} catch (error) {
|
|
874
|
+
failures.push({
|
|
875
|
+
kind: 'store',
|
|
876
|
+
detail: `build-event read failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
877
|
+
})
|
|
878
|
+
return { source: undefined, failures, events: [] }
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
|
|
782
882
|
/** Publish the exact composed snapshot into one build-owned namespace. The
|
|
783
|
-
* child samples only this artifact and never receives config over IPC.
|
|
883
|
+
* child samples only this artifact and never receives config over IPC.
|
|
884
|
+
*
|
|
885
|
+
* Build-owned sections (verify/finalize/commands/workspace) come from the
|
|
886
|
+
* build's own branch at its recorded pipeline-source commit; every other
|
|
887
|
+
* section comes from the live dispatcher snapshot. The source commit travels
|
|
888
|
+
* in the artifact metadata so status can report which autobuild.toml the
|
|
889
|
+
* build runs under (SPEC §16.1).
|
|
890
|
+
*
|
|
891
|
+
* A failed resolution never rewrites a pinned pipeline: when the build
|
|
892
|
+
* already has a deposit, its build-owned sections are reused verbatim (the
|
|
893
|
+
* reload path's rule), because depositing the live base-branch snapshot on a
|
|
894
|
+
* transient forge/store/git failure is exactly the AUT-366 poisoning. Only
|
|
895
|
+
* a build with no pinned artifact at all (legacy build, or a first deposit
|
|
896
|
+
* during an outage) takes the live snapshot. An actionable degradation — a
|
|
897
|
+
* missing forge capability, a build-branch source that resolved but cannot
|
|
898
|
+
* be read or parsed, a store failure — files a build-scoped observation
|
|
899
|
+
* naming the failed attempts; the documented fallbacks (an unpublished
|
|
900
|
+
* branch, an unreadable legacy base) stay quiet. */
|
|
784
901
|
private async publishBuildConfig(slug: string, snapshot = this.currentConfig()): Promise<void> {
|
|
902
|
+
const { source, failures, events } = await this.resolveBuildPipeline(slug)
|
|
903
|
+
if (source !== undefined) {
|
|
904
|
+
await this.depositBuildConfig(
|
|
905
|
+
slug,
|
|
906
|
+
snapshot,
|
|
907
|
+
composeBuildConfig(source.config, snapshot.config),
|
|
908
|
+
source.meta,
|
|
909
|
+
)
|
|
910
|
+
return
|
|
911
|
+
}
|
|
912
|
+
let reused: PipelineSourceMeta | undefined
|
|
913
|
+
try {
|
|
914
|
+
const artifact = await this.wiring.store.getArtifact(slug, BUILD_EFFECTIVE_CONFIG_ARTIFACT)
|
|
915
|
+
if (artifact !== null) {
|
|
916
|
+
const pinned = parseEffectiveBuildConfig(artifact)
|
|
917
|
+
reused = parseBuildConfigMetadata(artifact).pipelineSource
|
|
918
|
+
await this.depositBuildConfig(
|
|
919
|
+
slug,
|
|
920
|
+
snapshot,
|
|
921
|
+
composeBuildConfig(pinned, snapshot.config),
|
|
922
|
+
reused,
|
|
923
|
+
)
|
|
924
|
+
} else {
|
|
925
|
+
await this.depositBuildConfig(slug, snapshot, snapshot.config)
|
|
926
|
+
}
|
|
927
|
+
} catch {
|
|
928
|
+
// Even the artifact read failed; the live snapshot deposit is the only
|
|
929
|
+
// option left that keeps the launch alive.
|
|
930
|
+
await this.depositBuildConfig(slug, snapshot, snapshot.config)
|
|
931
|
+
}
|
|
932
|
+
await this.filePipelineFallbackObservation(slug, reused, failures, events)
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
/** File a build-scoped observation when a deposit hit an actionable
|
|
936
|
+
* pipeline-source failure (SPEC §16.1): the operator must know the deposit
|
|
937
|
+
* degraded to the last good pipeline or the live snapshot, and why. The
|
|
938
|
+
* documented fallbacks (unpublished branch, unreadable legacy base) are
|
|
939
|
+
* normal resolution order and stay quiet. A diagnostic, never a launch
|
|
940
|
+
* dependency. */
|
|
941
|
+
private async filePipelineFallbackObservation(
|
|
942
|
+
slug: string,
|
|
943
|
+
reused: PipelineSourceMeta | undefined,
|
|
944
|
+
failures: PipelineSourceFailure[],
|
|
945
|
+
events: readonly AbEvent[],
|
|
946
|
+
): Promise<void> {
|
|
947
|
+
const actionable = failures.filter(isActionablePipelineFailure)
|
|
948
|
+
if (actionable.length === 0) return
|
|
949
|
+
const degradedTo =
|
|
950
|
+
reused !== undefined
|
|
951
|
+
? 'kept the last pinned pipeline' +
|
|
952
|
+
(reused.commit !== undefined ? ` at ${reused.ref}@${reused.commit}` : ` (${reused.ref})`)
|
|
953
|
+
: 'deposited the live dispatcher snapshot (legacy fallback)'
|
|
954
|
+
const summary =
|
|
955
|
+
`pipeline-source resolution failed; ${degradedTo}. ` +
|
|
956
|
+
`Failed attempts: ${
|
|
957
|
+
actionable.map((failure) => `${failure.kind}: ${failure.detail}`).join('; ') ||
|
|
958
|
+
'none recorded'
|
|
959
|
+
}`
|
|
960
|
+
// Deduplicated against the build's log: a parked build re-attaches every
|
|
961
|
+
// sweep, and an outage must not harvest one identical diagnostic per tick.
|
|
962
|
+
if (
|
|
963
|
+
events.some(
|
|
964
|
+
(event) => event.type === 'observation.recorded' && event.payload.summary === summary,
|
|
965
|
+
)
|
|
966
|
+
) {
|
|
967
|
+
return
|
|
968
|
+
}
|
|
969
|
+
try {
|
|
970
|
+
await this.wiring.store.append(slug, {
|
|
971
|
+
// The catalog admits kernel/agent actors for observations (§8.2); the
|
|
972
|
+
// dispatcher's system-side diagnostic rides the kernel actor, like the
|
|
973
|
+
// finalize-failure and auto-merge-deferral observations.
|
|
974
|
+
actor: KERNEL,
|
|
975
|
+
type: 'observation.recorded',
|
|
976
|
+
payload: {
|
|
977
|
+
id: this.wiring.ids('obs'),
|
|
978
|
+
kind: 'followup',
|
|
979
|
+
summary,
|
|
980
|
+
files: ['autobuild.toml'],
|
|
981
|
+
},
|
|
982
|
+
})
|
|
983
|
+
} catch {
|
|
984
|
+
// The build's pipeline is already deposited; the observation is best-effort.
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
private async depositBuildConfig(
|
|
989
|
+
slug: string,
|
|
990
|
+
snapshot: ConfigSnapshot,
|
|
991
|
+
config: Config,
|
|
992
|
+
pipelineSource?: PipelineSourceMeta,
|
|
993
|
+
): Promise<void> {
|
|
785
994
|
await this.wiring.store.putArtifact(slug, {
|
|
786
995
|
kind: BUILD_EFFECTIVE_CONFIG_ARTIFACT,
|
|
787
|
-
content: effectiveBuildConfigContent(
|
|
996
|
+
content: effectiveBuildConfigContent(config),
|
|
788
997
|
metadata: {
|
|
789
998
|
revision: snapshot.revision,
|
|
790
999
|
run: this.runId,
|
|
1000
|
+
...(pipelineSource !== undefined ? { pipelineSource } : {}),
|
|
791
1001
|
},
|
|
792
1002
|
})
|
|
793
1003
|
}
|
|
794
1004
|
|
|
1005
|
+
/** Re-deposit deployment-owned sections to active builds on a reload. The
|
|
1006
|
+
* build-owned sections are reused verbatim from the build's last deposit, so
|
|
1007
|
+
* a base-branch reload can never retarget an in-flight build's pipeline; a
|
|
1008
|
+
* build picks up pipeline changes only from its own branch at its next
|
|
1009
|
+
* launch (SPEC §16.1). */
|
|
795
1010
|
private async publishActiveBuildConfigs(snapshot: ConfigSnapshot): Promise<void> {
|
|
796
1011
|
await Promise.all(
|
|
797
|
-
[...this.activeBuildRuns.keys()].map((slug) =>
|
|
1012
|
+
[...this.activeBuildRuns.keys()].map(async (slug) => {
|
|
1013
|
+
const artifact = await this.wiring.store.getArtifact(slug, BUILD_EFFECTIVE_CONFIG_ARTIFACT)
|
|
1014
|
+
if (artifact === null) {
|
|
1015
|
+
await this.publishBuildConfig(slug, snapshot)
|
|
1016
|
+
return
|
|
1017
|
+
}
|
|
1018
|
+
let existing: Config
|
|
1019
|
+
try {
|
|
1020
|
+
existing = parseEffectiveBuildConfig(artifact)
|
|
1021
|
+
} catch {
|
|
1022
|
+
// A corrupt or pre-pin artifact cannot pin a pipeline; fall back to
|
|
1023
|
+
// the launch-path resolution instead of failing the reload.
|
|
1024
|
+
await this.publishBuildConfig(slug, snapshot)
|
|
1025
|
+
return
|
|
1026
|
+
}
|
|
1027
|
+
const meta = parseBuildConfigMetadata(artifact)
|
|
1028
|
+
await this.depositBuildConfig(
|
|
1029
|
+
slug,
|
|
1030
|
+
snapshot,
|
|
1031
|
+
composeBuildConfig(existing, snapshot.config),
|
|
1032
|
+
meta.pipelineSource,
|
|
1033
|
+
)
|
|
1034
|
+
}),
|
|
798
1035
|
)
|
|
799
1036
|
}
|
|
800
1037
|
|
|
801
1038
|
private async appendStatus(event: RepositoryEventWrite): Promise<void> {
|
|
802
|
-
await this.
|
|
1039
|
+
await this.repoView.appendRepo(this.repoIdentity, event)
|
|
803
1040
|
}
|
|
804
1041
|
|
|
805
1042
|
private async refreshConfig(): Promise<void> {
|
|
806
1043
|
if (this.opts.once === true) return
|
|
807
1044
|
// Origin mode reloads from the forge: autobuild.toml at the CURRENT
|
|
808
|
-
// effective base branch (hot — a rename converges within two ticks).
|
|
1045
|
+
// effective base branch (hot — a rename converges within two ticks). This
|
|
1046
|
+
// read feeds the dispatcher's own snapshot; a reload re-deposits only the
|
|
1047
|
+
// deployment-owned sections to active builds (build-owned pipeline
|
|
1048
|
+
// sections stay pinned to each build's own branch, SPEC §16.1).
|
|
809
1049
|
// Restart-required changes keep startup-built adapters and file the
|
|
810
1050
|
// existing restart-required observation (LiveConfig semantics).
|
|
811
1051
|
const outcome =
|
|
@@ -861,8 +1101,10 @@ class DispatchLoop {
|
|
|
861
1101
|
}
|
|
862
1102
|
|
|
863
1103
|
private async readDispatchSettings(): Promise<ReturnType<typeof reduceDispatchSettings>> {
|
|
864
|
-
|
|
865
|
-
|
|
1104
|
+
// The view's bounded subset (AUT-489): the settings reducer consumes
|
|
1105
|
+
// durable types only. `refresh()` ran first in the tick, so this is the
|
|
1106
|
+
// delta-current journal, never a store read.
|
|
1107
|
+
return reduceDispatchSettings(this.repoView.recordedJournal())
|
|
866
1108
|
}
|
|
867
1109
|
|
|
868
1110
|
private dispatcherTick(resumeCurrent: boolean): Promise<Awaited<ReturnType<Dispatcher['tick']>>> {
|
|
@@ -884,12 +1126,17 @@ class DispatchLoop {
|
|
|
884
1126
|
})
|
|
885
1127
|
|
|
886
1128
|
// Unclaimed observations are display-only and sampled once per interactive
|
|
887
|
-
// dispatcher tick
|
|
888
|
-
// the
|
|
1129
|
+
// dispatcher tick from the journal and one repo-scoped digest read
|
|
1130
|
+
// (AUT-487) — flat in the finished-build count. A journal record that
|
|
1131
|
+
// does not yet exist reads as an empty journal (AUT-524); the sample
|
|
1132
|
+
// writes nothing. Any other failed sample must neither fail dispatch nor
|
|
1133
|
+
// replace the last complete measurement with a fabricated zero.
|
|
889
1134
|
if (this.dashboard) {
|
|
890
1135
|
try {
|
|
891
|
-
|
|
892
|
-
|
|
1136
|
+
this.observationCount = await sampleUnclaimedObservationCount(
|
|
1137
|
+
this.wiring.store,
|
|
1138
|
+
this.repoIdentity,
|
|
1139
|
+
)
|
|
893
1140
|
} catch {
|
|
894
1141
|
// Display-only sampling failures retain the last factual count and
|
|
895
1142
|
// retry on the next tick; they are not dashboard failures.
|
|
@@ -898,6 +1145,7 @@ class DispatchLoop {
|
|
|
898
1145
|
|
|
899
1146
|
// Sample inside the serialized tick, not at process startup. Every
|
|
900
1147
|
// dispatcher therefore gates claims from the latest repository facts.
|
|
1148
|
+
await this.repoView.refresh()
|
|
901
1149
|
const settings = await this.readDispatchSettings()
|
|
902
1150
|
const readyObservation =
|
|
903
1151
|
settings.intake || (!this.dashboard && this.opts.kernelRunId === undefined)
|
|
@@ -905,6 +1153,7 @@ class DispatchLoop {
|
|
|
905
1153
|
: await this.dispatcher.observeReady()
|
|
906
1154
|
const report = await this.dispatcher.tick({
|
|
907
1155
|
resumeCurrent,
|
|
1156
|
+
viewRefreshed: true,
|
|
908
1157
|
acceptNewWork: settings.intake,
|
|
909
1158
|
defaultAutoMerge: settings.defaultAutoMerge,
|
|
910
1159
|
autoMergeUser: buildControlUser(this.opts.env),
|
|
@@ -1175,6 +1424,7 @@ class DispatchLoop {
|
|
|
1175
1424
|
repo: this.repoIdentity,
|
|
1176
1425
|
user: buildControlUser(this.opts.env),
|
|
1177
1426
|
setting: 'intake',
|
|
1427
|
+
run: this.runId,
|
|
1178
1428
|
})
|
|
1179
1429
|
this.say(`dispatcher intake ${event.enabled ? 'ON' : 'OFF'}`)
|
|
1180
1430
|
await this.renderOnce()
|
|
@@ -1439,6 +1689,7 @@ class DispatchLoop {
|
|
|
1439
1689
|
repo: this.repoIdentity,
|
|
1440
1690
|
user: buildControlUser(this.opts.env),
|
|
1441
1691
|
setting: 'auto-merge-default',
|
|
1692
|
+
run: this.runId,
|
|
1442
1693
|
})
|
|
1443
1694
|
this.say(`dispatcher auto-merge default ${event.enabled ? 'ON' : 'OFF'}`)
|
|
1444
1695
|
await this.renderOnce()
|
|
@@ -2055,8 +2306,11 @@ class DispatchLoop {
|
|
|
2055
2306
|
// the pure Store-read pressure check; the guest rescans authoritatively
|
|
2056
2307
|
// once provisioned, so a threshold crossing between gate and guest scan
|
|
2057
2308
|
// is never suppressed by this check.
|
|
2058
|
-
|
|
2059
|
-
|
|
2309
|
+
// The view's bounded subset (AUT-489): the harvest control decision
|
|
2310
|
+
// reduces durable types only. Take the journal delta first — guests and
|
|
2311
|
+
// other dispatchers write harvest facts between ticks.
|
|
2312
|
+
await this.repoView.refreshJournal()
|
|
2313
|
+
const events = this.repoView.recordedJournal()
|
|
2060
2314
|
const state = reduceHarvest(events)
|
|
2061
2315
|
const control = decideHarvestControl(state, this.maxHarvestRecoveryAttempts)
|
|
2062
2316
|
const resumePending =
|
|
@@ -2066,8 +2320,16 @@ class DispatchLoop {
|
|
|
2066
2320
|
control.kind === 'request-recovery' ||
|
|
2067
2321
|
control.kind === 'exhaust-recovery'
|
|
2068
2322
|
if (!resumePending) {
|
|
2069
|
-
|
|
2070
|
-
|
|
2323
|
+
// Flat-cost gate (AUT-521): the journal read above plus one
|
|
2324
|
+
// repo-scoped digest batch read — no per-build history reads, so the
|
|
2325
|
+
// per-evaluation cost no longer grows with accumulated finished
|
|
2326
|
+
// builds. The guest rescans authoritatively once provisioned.
|
|
2327
|
+
const pressure = await evaluateHarvestPressureFromStore({
|
|
2328
|
+
store,
|
|
2329
|
+
repo,
|
|
2330
|
+
harvestEvents: events,
|
|
2331
|
+
policy: this.currentConfig().config.policy,
|
|
2332
|
+
})
|
|
2071
2333
|
if (pressure.trigger === undefined) return
|
|
2072
2334
|
}
|
|
2073
2335
|
|
|
@@ -2090,7 +2352,7 @@ class DispatchLoop {
|
|
|
2090
2352
|
)
|
|
2091
2353
|
}
|
|
2092
2354
|
this.hostedHarvest = { execution, handle, detached }
|
|
2093
|
-
const started = await
|
|
2355
|
+
const started = await this.repoView.appendRepo(repo, {
|
|
2094
2356
|
actor: DISPATCHER,
|
|
2095
2357
|
type: 'harvest.execution.started',
|
|
2096
2358
|
payload: {
|
|
@@ -2165,7 +2427,10 @@ class DispatchLoop {
|
|
|
2165
2427
|
startedSeq: number,
|
|
2166
2428
|
): Promise<HarvestRunnerResult> {
|
|
2167
2429
|
const { store } = this.wiring
|
|
2168
|
-
|
|
2430
|
+
// Bounded subset (AUT-489): classifyHarvestOutcome reduces harvest facts
|
|
2431
|
+
// only. A guest wrote the facts through its own handle, so read the delta.
|
|
2432
|
+
await this.repoView.refreshJournal()
|
|
2433
|
+
const events = this.repoView.recordedJournal()
|
|
2169
2434
|
const record = await store.getRepo(repo)
|
|
2170
2435
|
return classifyHarvestOutcome(events, {
|
|
2171
2436
|
executionStartedSeq: startedSeq,
|
|
@@ -2191,7 +2456,7 @@ class DispatchLoop {
|
|
|
2191
2456
|
branch: '',
|
|
2192
2457
|
}
|
|
2193
2458
|
const reap = await this.reapHostedWorkspace(workspaceHandle)
|
|
2194
|
-
await this.
|
|
2459
|
+
await this.repoView.appendRepo(repo, {
|
|
2195
2460
|
actor: DISPATCHER,
|
|
2196
2461
|
type: 'harvest.execution.released',
|
|
2197
2462
|
payload: {
|
|
@@ -2348,9 +2613,11 @@ class DispatchLoop {
|
|
|
2348
2613
|
}
|
|
2349
2614
|
|
|
2350
2615
|
private async settlePendingPublication(slug: string): Promise<void> {
|
|
2616
|
+
// The guest appended through its own handle; settle against fresh state.
|
|
2617
|
+
await this.repoView.refreshBuild(slug)
|
|
2351
2618
|
await settleWorkspacePublication(
|
|
2352
2619
|
{
|
|
2353
|
-
store: this.
|
|
2620
|
+
store: this.repoView,
|
|
2354
2621
|
storeRef: this.wiring.storeRef,
|
|
2355
2622
|
publication: this.wiring.workspaces.publication,
|
|
2356
2623
|
forge: this.wiring.forge,
|
|
@@ -2373,11 +2640,13 @@ class DispatchLoop {
|
|
|
2373
2640
|
cleanupPending: boolean
|
|
2374
2641
|
identity?: BuildExecutionHandle['identity']
|
|
2375
2642
|
}): Promise<void> {
|
|
2643
|
+
// The failed executor may have appended through its own handle.
|
|
2644
|
+
await this.repoView.refreshBuild(input.slug)
|
|
2376
2645
|
await appendInfrastructureFailure(
|
|
2377
|
-
{ store: this.
|
|
2646
|
+
{ store: this.repoView, ids: this.wiring.ids },
|
|
2378
2647
|
{
|
|
2379
2648
|
slug: input.slug,
|
|
2380
|
-
events: await this.
|
|
2649
|
+
events: await this.repoView.getEvents(input.slug),
|
|
2381
2650
|
maxAttempts: this.currentConfig().config.policy.maxInfrastructureAttempts,
|
|
2382
2651
|
provider: input.identity?.provider ?? this.wiring.workspaces.name,
|
|
2383
2652
|
workspaceRef: input.workspaceRef,
|
|
@@ -2408,11 +2677,7 @@ class DispatchLoop {
|
|
|
2408
2677
|
|
|
2409
2678
|
try {
|
|
2410
2679
|
await this.publishBuildConfig(slug)
|
|
2411
|
-
leaseClaimed = await this.
|
|
2412
|
-
slug,
|
|
2413
|
-
instance,
|
|
2414
|
-
BUILD_EXECUTION_LEASE_TTL_MS,
|
|
2415
|
-
)
|
|
2680
|
+
leaseClaimed = await this.repoView.claimLease(slug, instance, BUILD_EXECUTION_LEASE_TTL_MS)
|
|
2416
2681
|
if (!leaseClaimed) {
|
|
2417
2682
|
this.activeBuildRuns.delete(slug)
|
|
2418
2683
|
await this.appendStatus({
|
|
@@ -2423,7 +2688,7 @@ class DispatchLoop {
|
|
|
2423
2688
|
this.failureNotice(`build ${slug} already held by another runner — skipped`)
|
|
2424
2689
|
return 'already-active'
|
|
2425
2690
|
}
|
|
2426
|
-
const launchEvents = await this.
|
|
2691
|
+
const launchEvents = await this.repoView.getEvents(slug)
|
|
2427
2692
|
let workspaceRef: string | undefined
|
|
2428
2693
|
let workspaceProvider: string | undefined
|
|
2429
2694
|
for (const event of launchEvents) {
|
|
@@ -2467,7 +2732,7 @@ class DispatchLoop {
|
|
|
2467
2732
|
provider: owning.name,
|
|
2468
2733
|
workspaceRef,
|
|
2469
2734
|
}
|
|
2470
|
-
await this.
|
|
2735
|
+
await this.repoView.append(slug, {
|
|
2471
2736
|
actor: DISPATCHER,
|
|
2472
2737
|
type: 'execution.started',
|
|
2473
2738
|
payload: { ...identity, instance },
|
|
@@ -2482,7 +2747,7 @@ class DispatchLoop {
|
|
|
2482
2747
|
// supervisor settles the execution from the Store.
|
|
2483
2748
|
if (active.detaching) return
|
|
2484
2749
|
try {
|
|
2485
|
-
await this.
|
|
2750
|
+
await this.repoView.append(slug, {
|
|
2486
2751
|
actor: DISPATCHER,
|
|
2487
2752
|
type: 'execution.ended',
|
|
2488
2753
|
payload: {
|
|
@@ -2504,7 +2769,8 @@ class DispatchLoop {
|
|
|
2504
2769
|
return
|
|
2505
2770
|
}
|
|
2506
2771
|
|
|
2507
|
-
|
|
2772
|
+
await this.repoView.refreshBuild(slug)
|
|
2773
|
+
const state = reduceBuild(await this.repoView.getEvents(slug))
|
|
2508
2774
|
if (diagnostic === null && exit.exitCode === 0) {
|
|
2509
2775
|
await this.appendStatus({
|
|
2510
2776
|
actor: DISPATCHER,
|
|
@@ -2530,7 +2796,7 @@ class DispatchLoop {
|
|
|
2530
2796
|
})
|
|
2531
2797
|
this.warn(`build ${slug} runner failed: ${detail}`)
|
|
2532
2798
|
} finally {
|
|
2533
|
-
await this.
|
|
2799
|
+
await this.repoView.releaseLease(slug, instance)
|
|
2534
2800
|
await this.settlePendingPublication(slug)
|
|
2535
2801
|
}
|
|
2536
2802
|
},
|
|
@@ -2541,7 +2807,7 @@ class DispatchLoop {
|
|
|
2541
2807
|
// A rejected executor completion cannot prove the remote VM was
|
|
2542
2808
|
// stopped. Keep the lease until expiry: recovery fences and reaps
|
|
2543
2809
|
// the exact recorded identity before authorizing a replacement.
|
|
2544
|
-
|
|
2810
|
+
active.recording = this.recordInfrastructureFailure({
|
|
2545
2811
|
slug,
|
|
2546
2812
|
instance,
|
|
2547
2813
|
workspaceRef,
|
|
@@ -2550,6 +2816,7 @@ class DispatchLoop {
|
|
|
2550
2816
|
cleanupPending: true,
|
|
2551
2817
|
identity,
|
|
2552
2818
|
})
|
|
2819
|
+
await active.recording
|
|
2553
2820
|
},
|
|
2554
2821
|
)
|
|
2555
2822
|
.finally(() => {
|
|
@@ -2568,7 +2835,9 @@ class DispatchLoop {
|
|
|
2568
2835
|
this.activeBuildRuns.delete(slug)
|
|
2569
2836
|
}
|
|
2570
2837
|
if (leaseClaimed) {
|
|
2571
|
-
|
|
2838
|
+
// The runner appended through its own handle: take the delta.
|
|
2839
|
+
await this.repoView.refreshBuild(slug)
|
|
2840
|
+
const events = await this.repoView.getEvents(slug)
|
|
2572
2841
|
let workspaceRef = ''
|
|
2573
2842
|
for (const event of events) {
|
|
2574
2843
|
if (event.type === 'workspace.provisioned') workspaceRef = event.payload.ref
|
|
@@ -2587,7 +2856,7 @@ class DispatchLoop {
|
|
|
2587
2856
|
// Local start failures have no possible remote holder. Remote failures
|
|
2588
2857
|
// retain the lease until its TTL fences an ambiguous start.
|
|
2589
2858
|
if (this.wiring.workspaces.recovery === undefined) {
|
|
2590
|
-
await this.
|
|
2859
|
+
await this.repoView.releaseLease(slug, instance)
|
|
2591
2860
|
}
|
|
2592
2861
|
}
|
|
2593
2862
|
await this.appendStatus({
|
|
@@ -2683,6 +2952,9 @@ class DispatchLoop {
|
|
|
2683
2952
|
} catch {
|
|
2684
2953
|
// Detach is best-effort: an unresolvable wait still exits.
|
|
2685
2954
|
}
|
|
2955
|
+
// A wait failure already being recorded is durable work, not
|
|
2956
|
+
// supervision: let it land before the process exits.
|
|
2957
|
+
await entry.recording?.catch(() => {})
|
|
2686
2958
|
this.activeBuildRuns.delete(slug)
|
|
2687
2959
|
if (entry.settled !== undefined) {
|
|
2688
2960
|
this.inFlight.delete(entry.settled)
|
|
@@ -2839,9 +3111,14 @@ class DispatchLoop {
|
|
|
2839
3111
|
configSnapshot.config,
|
|
2840
3112
|
configSnapshot.revision,
|
|
2841
3113
|
)
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
3114
|
+
// Bounded read (AUT-489, AUT-545): every repository consumer of this frame —
|
|
3115
|
+
// projectRepositoryHarvest (reduceHarvest plus the harvest-only raw scans in
|
|
3116
|
+
// projectHarvestRun/escalationAttentionDismissed) and reduceDispatchSettings —
|
|
3117
|
+
// reduces durable types only, so the AUT-489 subset is replay-equivalent with
|
|
3118
|
+
// no anchor dependence (anchor analysis recorded; the frontend seed needed one
|
|
3119
|
+
// only because reduceDispatchStatus folds here too — it does not here).
|
|
3120
|
+
// Missing journal record stays a read-only empty journal (AUT-524).
|
|
3121
|
+
const repositoryEvents = await readRepoEventsIfRecorded(this.wiring.store, this.repoIdentity)
|
|
2845
3122
|
|
|
2846
3123
|
// Action-triggered and timer refreshes share the cache but may finish their
|
|
2847
3124
|
// repository reads out of order. Never let an older build snapshot replace
|
|
@@ -3162,7 +3439,7 @@ class DispatchLoop {
|
|
|
3162
3439
|
const holder = record?.lease?.holder
|
|
3163
3440
|
if (holder === undefined || this.yieldedTo === holder) return
|
|
3164
3441
|
this.yieldedTo = holder
|
|
3165
|
-
await this.
|
|
3442
|
+
await this.repoView.appendRepo(this.repoIdentity, {
|
|
3166
3443
|
actor: DISPATCHER,
|
|
3167
3444
|
type: 'dispatcher.tick-yielded',
|
|
3168
3445
|
payload: {
|
|
@@ -3502,10 +3779,23 @@ export async function abDispatch(opts: DispatchOpts): Promise<void> {
|
|
|
3502
3779
|
`origin-mode dispatch requires the builtin github forge, but the fetched autobuild.toml selects ${JSON.stringify(config.forge)}`,
|
|
3503
3780
|
)
|
|
3504
3781
|
}
|
|
3505
|
-
|
|
3782
|
+
// Origin mode has no checkout, so repository-path plugin specifiers
|
|
3783
|
+
// (relative, absolute, file:) cannot resolve — reject them with the
|
|
3784
|
+
// existing remediation tone before any wiring. Bare package specifiers
|
|
3785
|
+
// are accepted: the shared loadPlugins below resolves them from the
|
|
3786
|
+
// dispatcher's own installation when the scratch root lacks them
|
|
3787
|
+
// (AUT-517), which is exactly "resolvable from the dispatcher's own
|
|
3788
|
+
// installation". The caller-supplied `targetRepo` is an identity token,
|
|
3789
|
+
// not a path, and never reaches plugin resolution: the candidate roots
|
|
3790
|
+
// are the scratch checkout and the installation, per the loadPlugins
|
|
3791
|
+
// call below (AUT-604).
|
|
3792
|
+
const repoPathPlugin = (config.plugins ?? []).find(
|
|
3793
|
+
(moduleSpecifier) => pluginResolutionKind(moduleSpecifier) === 'repo-path',
|
|
3794
|
+
)
|
|
3795
|
+
if (repoPathPlugin !== undefined) {
|
|
3506
3796
|
throw new Error(
|
|
3507
|
-
|
|
3508
|
-
'and there is no checkout. Remove [plugins]
|
|
3797
|
+
`origin-mode dispatch cannot load the repository-path plugin ${JSON.stringify(repoPathPlugin)}: ` +
|
|
3798
|
+
'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
3799
|
)
|
|
3510
3800
|
}
|
|
3511
3801
|
} else {
|
|
@@ -3524,8 +3814,22 @@ export async function abDispatch(opts: DispatchOpts): Promise<void> {
|
|
|
3524
3814
|
}
|
|
3525
3815
|
// Configured plugin code is trusted like configured shell commands, but it
|
|
3526
3816
|
// must resolve, evaluate, validate, and register before production wiring
|
|
3527
|
-
// opens a store, claims a ticket, or launches a runner.
|
|
3528
|
-
|
|
3817
|
+
// opens a store, claims a ticket, or launches a runner. Skipped-module
|
|
3818
|
+
// notices (builtin duplicate-skip, guest tolerance) surface on dispatch's
|
|
3819
|
+
// own stderr channel.
|
|
3820
|
+
// AUT-604: origin mode's repository candidate root for plugin loading is
|
|
3821
|
+
// the per-origin scratch checkout, never the caller-supplied `targetRepo`
|
|
3822
|
+
// token (e.g. hosted-dispatcher's `'<hosted-dispatcher>'`) — that token is
|
|
3823
|
+
// a caller-facing identity label which `resolveOriginModeState` replaced
|
|
3824
|
+
// with `state.checkout` above, so it never participates in cwd-relative
|
|
3825
|
+
// disk resolution. `state.checkout` equals `resolvedOpts.targetRepo` in
|
|
3826
|
+
// both modes, so passing it as `packageRoot` is behavior-preserving; the
|
|
3827
|
+
// installation candidate (AUT-517) and the disk-first EROFS gate (AUT-587)
|
|
3828
|
+
// are unchanged.
|
|
3829
|
+
const plugins = await loadPlugins(config.plugins, resolvedOpts.targetRepo, {
|
|
3830
|
+
packageRoot: state.checkout,
|
|
3831
|
+
onNotice: (line) => resolvedOpts.stderr(line),
|
|
3832
|
+
})
|
|
3529
3833
|
// Validate the selector against the complete catalog before either custom
|
|
3530
3834
|
// wiring or production wiring can open state or perform side effects.
|
|
3531
3835
|
resolveForgeRegistration(config.forge, plugins)
|
|
@@ -3580,7 +3884,7 @@ export async function abDispatch(opts: DispatchOpts): Promise<void> {
|
|
|
3580
3884
|
metadata: { restartRequired: [...restartRequired], effectiveChanged },
|
|
3581
3885
|
},
|
|
3582
3886
|
{
|
|
3583
|
-
kind:
|
|
3887
|
+
kind: _effectiveConfigKind,
|
|
3584
3888
|
content: effectiveConfigContent(effectiveConfig),
|
|
3585
3889
|
metadata: { run: runId, effectiveChanged },
|
|
3586
3890
|
},
|
|
@@ -3616,27 +3920,36 @@ export async function abDispatch(opts: DispatchOpts): Promise<void> {
|
|
|
3616
3920
|
// another dispatcher cannot clobber the latest operator choice with a value
|
|
3617
3921
|
// it inferred at startup. Fresh-repository fallbacks live in the reducer.
|
|
3618
3922
|
await wiring.store.ensureRepo(state.repo)
|
|
3923
|
+
// Every own lifecycle append goes through one view, restored from snapshots
|
|
3924
|
+
// before the first of them, so a quiet invocation reads none of its own
|
|
3925
|
+
// rows back and the final flush covers `run-stopped` too.
|
|
3926
|
+
const view = new RepoViewStore(wiring.store, {
|
|
3927
|
+
// The loop's store-keyed identity (an explicit `repo` pins it).
|
|
3928
|
+
repo: resolvedOpts.repo ?? resolvedOpts.targetRepo,
|
|
3929
|
+
resident: resolvedOpts.once !== true,
|
|
3930
|
+
})
|
|
3931
|
+
await view.startJournal()
|
|
3619
3932
|
const actor = humanActor(buildControlUser(resolvedOpts.env))
|
|
3620
3933
|
if (resolvedOpts.intake !== undefined) {
|
|
3621
|
-
await
|
|
3934
|
+
await view.appendRepo(state.repo, {
|
|
3622
3935
|
actor,
|
|
3623
3936
|
type: 'dispatcher.intake-set',
|
|
3624
3937
|
payload: { enabled: resolvedOpts.intake, run: runId },
|
|
3625
3938
|
})
|
|
3626
3939
|
}
|
|
3627
3940
|
if (resolvedOpts.defaultAutoMerge !== undefined) {
|
|
3628
|
-
await
|
|
3941
|
+
await view.appendRepo(state.repo, {
|
|
3629
3942
|
actor,
|
|
3630
3943
|
type: 'dispatcher.auto-merge-default-set',
|
|
3631
3944
|
payload: { enabled: resolvedOpts.defaultAutoMerge, run: runId },
|
|
3632
3945
|
})
|
|
3633
3946
|
}
|
|
3634
3947
|
|
|
3635
|
-
await
|
|
3948
|
+
await view.appendRepoWithArtifacts(
|
|
3636
3949
|
state.repo,
|
|
3637
3950
|
[
|
|
3638
3951
|
{
|
|
3639
|
-
kind:
|
|
3952
|
+
kind: _effectiveConfigKind,
|
|
3640
3953
|
content: effectiveConfigContent(config),
|
|
3641
3954
|
metadata: { run: runId, revision: 0 },
|
|
3642
3955
|
},
|
|
@@ -3657,10 +3970,10 @@ export async function abDispatch(opts: DispatchOpts): Promise<void> {
|
|
|
3657
3970
|
},
|
|
3658
3971
|
)
|
|
3659
3972
|
|
|
3660
|
-
const loop = new DispatchLoop(liveConfig, wiring, resolvedOpts)
|
|
3973
|
+
const loop = new DispatchLoop(liveConfig, wiring, resolvedOpts, view)
|
|
3661
3974
|
try {
|
|
3662
3975
|
await loop.run()
|
|
3663
|
-
await
|
|
3976
|
+
await view.appendRepo(state.repo, {
|
|
3664
3977
|
actor: DISPATCHER,
|
|
3665
3978
|
type: 'dispatcher.run-stopped',
|
|
3666
3979
|
payload: {
|
|
@@ -3672,7 +3985,7 @@ export async function abDispatch(opts: DispatchOpts): Promise<void> {
|
|
|
3672
3985
|
})
|
|
3673
3986
|
} catch (error) {
|
|
3674
3987
|
try {
|
|
3675
|
-
await
|
|
3988
|
+
await view.appendRepo(state.repo, {
|
|
3676
3989
|
actor: DISPATCHER,
|
|
3677
3990
|
type: 'dispatcher.run-stopped',
|
|
3678
3991
|
payload: {
|
|
@@ -3687,6 +4000,10 @@ export async function abDispatch(opts: DispatchOpts): Promise<void> {
|
|
|
3687
4000
|
}
|
|
3688
4001
|
throw error
|
|
3689
4002
|
} finally {
|
|
4003
|
+
// Every append this invocation made, `run-stopped` included, is in the
|
|
4004
|
+
// view; leave the reduced state behind so the next cold process reads
|
|
4005
|
+
// only what is newer than it.
|
|
4006
|
+
await view.persistSnapshots({ force: true })
|
|
3690
4007
|
if (resolvedOpts.wire === undefined) await wiring.store.close()
|
|
3691
4008
|
}
|
|
3692
4009
|
}
|