@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
|
@@ -37,15 +37,23 @@
|
|
|
37
37
|
* execution lease before launch; the child renews that same-holder lease.
|
|
38
38
|
*/
|
|
39
39
|
import type { Config } from '../config/schema'
|
|
40
|
+
import type { LanguageModel } from 'ai'
|
|
41
|
+
import { runOrchestratorTickStep } from './orchestrator-tick'
|
|
40
42
|
import { DISPATCHER, agentActor, humanActor } from '../events/envelope'
|
|
43
|
+
import { isRemoteWorkspace } from '../events/workspace-remote'
|
|
41
44
|
import type { AbEvent, EventWrite } from '../events/catalog'
|
|
42
|
-
import type { EventPayload } from '../events/payloads'
|
|
45
|
+
import type { EventPayload, PinnedAsset } from '../events/payloads'
|
|
43
46
|
import type { IdSource } from '../ids'
|
|
44
47
|
import {
|
|
45
48
|
autoMergeApplicationType,
|
|
46
49
|
pendingAutoMerge,
|
|
47
50
|
recordAutoMergeDeferralObservation,
|
|
48
51
|
} from '../kernel/auto-merge'
|
|
52
|
+
import {
|
|
53
|
+
autoMergeDefaultTarget,
|
|
54
|
+
latestAutoMergeDefault,
|
|
55
|
+
type AutoMergeDefaultFact,
|
|
56
|
+
} from '../kernel/auto-merge-default'
|
|
49
57
|
import { reduceDispatchSettings } from '../kernel/dispatch-settings'
|
|
50
58
|
import { decideNext } from '../kernel/engine'
|
|
51
59
|
import {
|
|
@@ -56,7 +64,8 @@ import {
|
|
|
56
64
|
} from '../kernel/harvest'
|
|
57
65
|
import { pendingPrAttachmentReclaims } from '../kernel/pr-attachments'
|
|
58
66
|
import { reduceBuild, type BuildState } from '../kernel/reducer'
|
|
59
|
-
import type
|
|
67
|
+
import { createOperatorSandboxService, type OperatorSandboxService } from '../operator/sandbox'
|
|
68
|
+
import type { ArtifactRef, TicketRef } from '../ontology'
|
|
60
69
|
import { BUILD_EXECUTION_LEASE_TTL_MS } from '../ports/workspace/build-execution'
|
|
61
70
|
import type {
|
|
62
71
|
DependencyState,
|
|
@@ -67,12 +76,14 @@ import type {
|
|
|
67
76
|
} from '../ports/types'
|
|
68
77
|
import type { Exec } from '../ports/workspace/git-worktree'
|
|
69
78
|
import type { ArtifactMeta, BuildRecord, BuildStore, Clock } from '../store/types'
|
|
79
|
+
import { samplePinnedAssets } from '../store/ticket-assets'
|
|
70
80
|
import { resolveRepoOrigin } from '../cli/repo-state'
|
|
71
81
|
import { specConformance } from '../spec-standard'
|
|
72
82
|
export { specConformance, type SpecConformance } from '../spec-standard'
|
|
73
83
|
import { recordInfrastructureFailure as appendInfrastructureFailure } from './infrastructure-failure-budget'
|
|
74
84
|
import { lastExecutionOutcome, openExecution, settleExecution } from './execution-settlement'
|
|
75
85
|
import { openHarvestExecutions } from './harvest-execution-state'
|
|
86
|
+
import { readRepoEventsIfRecorded } from './harvest'
|
|
76
87
|
import type { RepositoryEvent } from '../events/repository'
|
|
77
88
|
import { sandboxStates } from './sandbox-state'
|
|
78
89
|
import { abandonedPublicationPending, publicationPending } from './publication-state'
|
|
@@ -349,6 +360,11 @@ export interface TickReport {
|
|
|
349
360
|
sandboxIdleStops: number
|
|
350
361
|
/** Contained idle-settlement failures this tick; retried next tick. */
|
|
351
362
|
sandboxSettleFailures: number
|
|
363
|
+
/** Contained sandbox-backend construction failures this tick (AUT-584):
|
|
364
|
+
* deterministic and config-shaped, so it re-reports every tick until the
|
|
365
|
+
* config is repaired. The affected tick runs with sandbox tools filtered
|
|
366
|
+
* exactly as when no backend exists. */
|
|
367
|
+
orchestratorSandboxFailures: number
|
|
352
368
|
}
|
|
353
369
|
|
|
354
370
|
export function emptyTickReport(): TickReport {
|
|
@@ -385,6 +401,7 @@ export function emptyTickReport(): TickReport {
|
|
|
385
401
|
harvestFailed: 0,
|
|
386
402
|
sandboxIdleStops: 0,
|
|
387
403
|
sandboxSettleFailures: 0,
|
|
404
|
+
orchestratorSandboxFailures: 0,
|
|
388
405
|
}
|
|
389
406
|
}
|
|
390
407
|
|
|
@@ -534,6 +551,11 @@ export interface DispatcherDeps {
|
|
|
534
551
|
* supplies cancellation and treats every absence/failure/invalid result as a
|
|
535
552
|
* local deterministic fallback, so naming can never prevent build creation. */
|
|
536
553
|
nameSlug?: (spec: string, signal: AbortSignal) => Promise<string | null>
|
|
554
|
+
/** The orchestrator turn runner's language model, injected for tests. When
|
|
555
|
+
* absent the configured gateway model is resolved in process (AUT-342);
|
|
556
|
+
* the step itself only ever constructs in origin mode with the orchestrator
|
|
557
|
+
* enabled, so a local dispatcher never resolves a model. */
|
|
558
|
+
orchestratorModel?: LanguageModel
|
|
537
559
|
/** Settle pending publication for `slug` after durable proof that the
|
|
538
560
|
* guest execution ended. Supplied by the DispatchLoop; the settlement stage
|
|
539
561
|
* invokes it only on a recorded completion. */
|
|
@@ -672,11 +694,23 @@ export class Dispatcher {
|
|
|
672
694
|
/** Builds already launched this tick — a janitor/startup re-attach must
|
|
673
695
|
* not be doubled by the sweep, nor a fresh dispatch by anything. */
|
|
674
696
|
const launched = new Set<string>()
|
|
675
|
-
//
|
|
676
|
-
//
|
|
677
|
-
//
|
|
678
|
-
//
|
|
679
|
-
|
|
697
|
+
// One journal read for both repository-wide controls: a pause that lands
|
|
698
|
+
// mid-tick takes effect on the next tick, matching intake (which the CLI
|
|
699
|
+
// likewise samples before calling `tick`); the auto-merge default fact is
|
|
700
|
+
// the durable driver of this tick's fan-out below. Neither control
|
|
701
|
+
// pretends to a serialization the store does not offer.
|
|
702
|
+
const repoEvents = await readRepoEventsIfRecorded(this.deps.store, this.deps.repo)
|
|
703
|
+
const paused = reduceDispatchSettings(repoEvents).paused
|
|
704
|
+
const autoMergeDefault = latestAutoMergeDefault(repoEvents)
|
|
705
|
+
// The claim-time seed cites the newest fact whose enabled state matches
|
|
706
|
+
// the launch sample, not blindly the newest fact: the CLI samples
|
|
707
|
+
// `defaultAutoMerge` from its own earlier read, so an interleaved toggle
|
|
708
|
+
// can make this tick's read newer-and-opposite. Citing the matching fact
|
|
709
|
+
// keeps the newer opposite fact strictly ahead of the seed's provenance,
|
|
710
|
+
// so the fan-out reconciles the build on the next tick instead of the
|
|
711
|
+
// stale sample out-voting the durable default forever (f_b851c0e8).
|
|
712
|
+
const autoMergeSeedFact =
|
|
713
|
+
opts.defaultAutoMerge === true ? latestAutoMergeDefault(repoEvents, true) : undefined
|
|
680
714
|
// Stage 0 — SETTLEMENT: observe foreign executions from durable facts plus
|
|
681
715
|
// provider liveness and settle their completion, lease, and publication.
|
|
682
716
|
// Runs before the janitor so its executionLeaseLive checks see freshly
|
|
@@ -691,6 +725,13 @@ export class Dispatcher {
|
|
|
691
725
|
if (this.outOfBudget(opts)) return report
|
|
692
726
|
await this.settleOrchestratorSandboxes(report, opts)
|
|
693
727
|
if (this.outOfBudget(opts)) return report
|
|
728
|
+
// The default fan-out runs before the janitor so one tick both records the
|
|
729
|
+
// consent and lets the janitor act on it (the janitor reduces a fresh read
|
|
730
|
+
// inside `checkPr`). Independent of intake and `acceptNewWork` — the
|
|
731
|
+
// durable fact drives it, so a second dispatcher polling the same
|
|
732
|
+
// repository converges the same state.
|
|
733
|
+
await this.applyAutoMergeDefault(autoMergeDefault)
|
|
734
|
+
if (this.outOfBudget(opts)) return report
|
|
694
735
|
await this.janitor(report, launched, opts)
|
|
695
736
|
if (this.outOfBudget(opts)) return report
|
|
696
737
|
await this.recoverDispatches(report, launched, paused, opts)
|
|
@@ -700,7 +741,7 @@ export class Dispatcher {
|
|
|
700
741
|
await this.leaseSweep(report, launched, paused, opts)
|
|
701
742
|
if (this.outOfBudget(opts)) return report
|
|
702
743
|
if (opts.acceptNewWork !== false) {
|
|
703
|
-
await this.dispatch(report, launched, autoMergeUser, paused, opts)
|
|
744
|
+
await this.dispatch(report, launched, autoMergeUser, paused, opts, autoMergeSeedFact)
|
|
704
745
|
}
|
|
705
746
|
// Fire-and-forget by contract: long synthesize/review sessions must not
|
|
706
747
|
// stop janitor, lease sweep, ticket dispatch, or signal handling on later
|
|
@@ -708,6 +749,60 @@ export class Dispatcher {
|
|
|
708
749
|
// launch. Pending control commands still launch the runner so the kernel
|
|
709
750
|
// can settle them under the repository lease.
|
|
710
751
|
await this.triggerHarvest()
|
|
752
|
+
// The orchestrator step (AUT-342) — LAST, after every build stage, so it
|
|
753
|
+
// can only consume slack: resumes, crash reaping, and wake turns run
|
|
754
|
+
// against min(configured, remaining tick budget) with a 30 s floor, and
|
|
755
|
+
// a busy repository simply defers its sessions to later ticks (durable
|
|
756
|
+
// state makes that safe). Two independent gates keep it inert where it
|
|
757
|
+
// does not belong: the step is constructed ONLY in origin mode (a local
|
|
758
|
+
// `ab dispatch` never constructs it — enabled = true in a local
|
|
759
|
+
// repository's autobuild.toml lists no sessions, builds no registry, and
|
|
760
|
+
// calls no model), and in origin mode only when the effective config
|
|
761
|
+
// enables the orchestrator.
|
|
762
|
+
if (this.deps.repoOrigin !== undefined && this.deps.config.orchestrator.enabled) {
|
|
763
|
+
if (!this.outOfBudget(opts)) {
|
|
764
|
+
const remainingSeconds =
|
|
765
|
+
opts.deadlineAt === undefined
|
|
766
|
+
? undefined
|
|
767
|
+
: Math.max(0, Math.floor((opts.deadlineAt - this.deps.clock().getTime()) / 1000))
|
|
768
|
+
// The origin-mode tick's sandbox backend (AUT-584), built from the
|
|
769
|
+
// dispatcher's own deps — the same wired provider and forge the
|
|
770
|
+
// settlement stage already consumes. Construction is cheap and
|
|
771
|
+
// network-free (one idempotent ensureRepo), and building per tick
|
|
772
|
+
// picks up hot config reloads the way the rest of the tick does.
|
|
773
|
+
// A construction failure is contained: the step runs with sandbox
|
|
774
|
+
// tools filtered exactly as today, and the standing counter names
|
|
775
|
+
// the condition (the same discipline as sandboxSettleFailures).
|
|
776
|
+
let sandbox: OperatorSandboxService | undefined
|
|
777
|
+
try {
|
|
778
|
+
sandbox = await createOperatorSandboxService({
|
|
779
|
+
store: this.deps.store,
|
|
780
|
+
repo: this.deps.repo,
|
|
781
|
+
provider: this.deps.workspaces,
|
|
782
|
+
sandbox: this.deps.config.orchestrator.sandbox,
|
|
783
|
+
baseBranch: this.deps.config.baseBranch,
|
|
784
|
+
forge: this.deps.forge,
|
|
785
|
+
clock: this.deps.clock,
|
|
786
|
+
})
|
|
787
|
+
} catch {
|
|
788
|
+
report.orchestratorSandboxFailures += 1
|
|
789
|
+
sandbox = undefined
|
|
790
|
+
}
|
|
791
|
+
await runOrchestratorTickStep({
|
|
792
|
+
store: this.deps.store,
|
|
793
|
+
repo: this.deps.repo,
|
|
794
|
+
config: this.deps.config,
|
|
795
|
+
clock: this.deps.clock,
|
|
796
|
+
ids: this.deps.ids,
|
|
797
|
+
tickets: this.deps.tickets,
|
|
798
|
+
...(sandbox !== undefined ? { sandbox } : {}),
|
|
799
|
+
...(this.deps.orchestratorModel !== undefined
|
|
800
|
+
? { model: this.deps.orchestratorModel }
|
|
801
|
+
: {}),
|
|
802
|
+
...(remainingSeconds !== undefined ? { remainingBudgetSeconds: remainingSeconds } : {}),
|
|
803
|
+
})
|
|
804
|
+
}
|
|
805
|
+
}
|
|
711
806
|
return report
|
|
712
807
|
}
|
|
713
808
|
|
|
@@ -745,13 +840,51 @@ export class Dispatcher {
|
|
|
745
840
|
}
|
|
746
841
|
}
|
|
747
842
|
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
843
|
+
/** Apply the newest durable `dispatcher.auto-merge-default-set` fact to every
|
|
844
|
+
* current non-terminal build of this repository: on — request consent; off —
|
|
845
|
+
* cancel it. Attribution is the toggling human's own actor, exactly as if
|
|
846
|
+
* they had pressed `m` on each row; each command carries the fact's seq so
|
|
847
|
+
* the fan-out is idempotent (a settled build fails `autoMergeDefaultTarget`)
|
|
848
|
+
* and a per-build override stands until the default is toggled again. A fact
|
|
849
|
+
* whose value already matches the build's state writes a provenance-only
|
|
850
|
+
* `build.auto-merge-default-observed` marker instead of a duplicate command,
|
|
851
|
+
* so the matching fact is recorded as answered (f_28b3fba1).
|
|
852
|
+
*
|
|
853
|
+
* Per build, a compare-and-set loop (the `requestOne` shape from
|
|
854
|
+
* `bulk-control.ts`): the read/reduce/evaluate lives inside the loop, so a
|
|
855
|
+
* competing write loses at most one attempt and the next re-reads fresh
|
|
856
|
+
* state. No TickReport counter: the evidence is the build events, and the
|
|
857
|
+
* report shape stays compatible. */
|
|
858
|
+
private async applyAutoMergeDefault(fact: AutoMergeDefaultFact | undefined): Promise<void> {
|
|
859
|
+
if (fact === undefined) return
|
|
860
|
+
const { store } = this.deps
|
|
861
|
+
for (const record of (await store.listBuilds())
|
|
862
|
+
.filter((candidate) => candidate.repo === this.deps.repo)
|
|
863
|
+
.sort((a, b) => a.slug.localeCompare(b.slug))) {
|
|
864
|
+
while (true) {
|
|
865
|
+
const events = await store.getEvents(record.slug)
|
|
866
|
+
const state = reduceBuild(events)
|
|
867
|
+
const target = autoMergeDefaultTarget(state, fact)
|
|
868
|
+
if (target === undefined) break
|
|
869
|
+
if (target === 'observed') {
|
|
870
|
+
const observed = await store.appendIfCurrent(record.slug, state.lastSeq, {
|
|
871
|
+
actor: DISPATCHER,
|
|
872
|
+
type: 'build.auto-merge-default-observed',
|
|
873
|
+
payload: { defaultSeq: fact.seq },
|
|
874
|
+
})
|
|
875
|
+
if (observed !== null) break
|
|
876
|
+
continue
|
|
877
|
+
}
|
|
878
|
+
const type =
|
|
879
|
+
target === 'request' ? 'build.auto-merge-requested' : 'build.auto-merge-cancelled'
|
|
880
|
+
const appended = await store.appendIfCurrent(record.slug, state.lastSeq, {
|
|
881
|
+
actor: fact.actor,
|
|
882
|
+
type,
|
|
883
|
+
payload: { defaultSeq: fact.seq },
|
|
884
|
+
})
|
|
885
|
+
if (appended !== null) break
|
|
886
|
+
}
|
|
887
|
+
}
|
|
755
888
|
}
|
|
756
889
|
|
|
757
890
|
private async triggerHarvest(): Promise<void> {
|
|
@@ -762,7 +895,8 @@ export class Dispatcher {
|
|
|
762
895
|
start()
|
|
763
896
|
return
|
|
764
897
|
}
|
|
765
|
-
|
|
898
|
+
// Bounded read (AUT-489): the harvest reducer consumes durable types only.
|
|
899
|
+
const state = reduceHarvest(await this.deps.store.getRepoStateEvents(this.deps.repo))
|
|
766
900
|
if (decideHarvestControl(state, this.maxHarvestRecoveryAttempts).kind !== 'park') {
|
|
767
901
|
start()
|
|
768
902
|
}
|
|
@@ -837,9 +971,12 @@ export class Dispatcher {
|
|
|
837
971
|
const active = this.deps.activeHarvestExecutions?.() ?? new Set<string>()
|
|
838
972
|
let events: RepositoryEvent[]
|
|
839
973
|
try {
|
|
840
|
-
events = await this.
|
|
974
|
+
events = await readRepoEventsIfRecorded(this.deps.store, this.deps.repo)
|
|
841
975
|
} catch {
|
|
842
|
-
//
|
|
976
|
+
// The read itself failed (a transient store error — a missing record
|
|
977
|
+
// answers `[]` inside the helper, so it never reaches this arm):
|
|
978
|
+
// skipping this settlement stage for this tick while the rest of the
|
|
979
|
+
// tick proceeds, and the next tick retries what remains.
|
|
843
980
|
return
|
|
844
981
|
}
|
|
845
982
|
for (const execution of openHarvestExecutions(events)) {
|
|
@@ -933,9 +1070,12 @@ export class Dispatcher {
|
|
|
933
1070
|
const providerName = this.deps.workspaces.name
|
|
934
1071
|
let events: RepositoryEvent[]
|
|
935
1072
|
try {
|
|
936
|
-
events = await this.
|
|
1073
|
+
events = await readRepoEventsIfRecorded(this.deps.store, this.deps.repo)
|
|
937
1074
|
} catch {
|
|
938
|
-
//
|
|
1075
|
+
// The read itself failed (a transient store error — a missing record
|
|
1076
|
+
// answers `[]` inside the helper, so it never reaches this arm):
|
|
1077
|
+
// skipping this settlement stage for this tick while the rest of the
|
|
1078
|
+
// tick proceeds, and the next tick retries what remains.
|
|
939
1079
|
return
|
|
940
1080
|
}
|
|
941
1081
|
const idleMs = config.orchestrator.sandbox.idleMinutes * 60_000
|
|
@@ -1065,6 +1205,8 @@ export class Dispatcher {
|
|
|
1065
1205
|
body: string
|
|
1066
1206
|
authoredSession?: string
|
|
1067
1207
|
autoMergeUser?: string
|
|
1208
|
+
autoMergeDefaultSeq?: number
|
|
1209
|
+
assets?: PinnedAsset[]
|
|
1068
1210
|
},
|
|
1069
1211
|
): Promise<'kicked' | 'in-flight' | 'foreign-live'> {
|
|
1070
1212
|
const slug = record.slug
|
|
@@ -1134,6 +1276,8 @@ export class Dispatcher {
|
|
|
1134
1276
|
body: string
|
|
1135
1277
|
authoredSession?: string
|
|
1136
1278
|
autoMergeUser?: string
|
|
1279
|
+
autoMergeDefaultSeq?: number
|
|
1280
|
+
assets?: PinnedAsset[]
|
|
1137
1281
|
}
|
|
1138
1282
|
| undefined
|
|
1139
1283
|
tail: 'dispatch' | 'launch'
|
|
@@ -1192,6 +1336,7 @@ export class Dispatcher {
|
|
|
1192
1336
|
ref: handle.ref,
|
|
1193
1337
|
path: handle.path,
|
|
1194
1338
|
...(handle.localPath !== undefined ? { localPath: handle.localPath } : {}),
|
|
1339
|
+
remote: workspaces.publication !== undefined,
|
|
1195
1340
|
branch: handle.branch,
|
|
1196
1341
|
base: handle.base,
|
|
1197
1342
|
},
|
|
@@ -1379,7 +1524,7 @@ export class Dispatcher {
|
|
|
1379
1524
|
|
|
1380
1525
|
private forgeWorkspacePath(events: AbEvent[]): string {
|
|
1381
1526
|
const open = openWorkspace(events)
|
|
1382
|
-
if (open
|
|
1527
|
+
if (open !== null && isRemoteWorkspace(open)) return this.deps.repo
|
|
1383
1528
|
// Local forges run git with cwd = workspacePath: it must be the physical
|
|
1384
1529
|
// checkout, never the store identity (an origin URL since the identity
|
|
1385
1530
|
// change; a path only for origin-less fixtures).
|
|
@@ -1407,13 +1552,12 @@ export class Dispatcher {
|
|
|
1407
1552
|
// provisioned facts (not the open one, which is null when the build never
|
|
1408
1553
|
// provisioned or already released — only the former must skip the
|
|
1409
1554
|
// local-branch git step; a released local workspace may still have left a
|
|
1410
|
-
// branch in the checkout). Remote workspaces
|
|
1411
|
-
//
|
|
1412
|
-
//
|
|
1413
|
-
//
|
|
1555
|
+
// branch in the checkout). Remote workspaces never create a local branch
|
|
1556
|
+
// in the main checkout, and origin mode has no checkout at all: the
|
|
1557
|
+
// local-branch git step is skipped, and the saga's later steps tolerate
|
|
1558
|
+
// its absence.
|
|
1414
1559
|
const hadLocalWorkspace = events.some(
|
|
1415
|
-
(event) =>
|
|
1416
|
-
event.type === 'workspace.provisioned' && event.payload.provider !== 'vercel-sandbox',
|
|
1560
|
+
(event) => event.type === 'workspace.provisioned' && !isRemoteWorkspace(event.payload),
|
|
1417
1561
|
)
|
|
1418
1562
|
const has = (type: AbEvent['type']): boolean => events.some((event) => event.type === type)
|
|
1419
1563
|
const append = async <T extends EventWrite['type']>(write: EventWrite<T>): Promise<void> => {
|
|
@@ -2046,7 +2190,15 @@ export class Dispatcher {
|
|
|
2046
2190
|
private async baseSha(baseBranch: string): Promise<string> {
|
|
2047
2191
|
const remoteBranchSha = this.deps.forge.remoteBranchSha
|
|
2048
2192
|
if (remoteBranchSha !== undefined) {
|
|
2049
|
-
|
|
2193
|
+
try {
|
|
2194
|
+
return await remoteBranchSha.call(this.deps.forge, baseBranch)
|
|
2195
|
+
} catch {
|
|
2196
|
+
// A forge that advertises the capability but cannot resolve this branch
|
|
2197
|
+
// (a checkout-mode adapter, an unseeded test fake, or a transient
|
|
2198
|
+
// remote failure) falls through to the physical checkout's git read.
|
|
2199
|
+
// In origin mode there is no checkout, so the git read's failure is the
|
|
2200
|
+
// actionable error and the tick still fails closed.
|
|
2201
|
+
}
|
|
2050
2202
|
}
|
|
2051
2203
|
const args = [
|
|
2052
2204
|
'git',
|
|
@@ -2068,6 +2220,29 @@ export class Dispatcher {
|
|
|
2068
2220
|
|
|
2069
2221
|
// ── b. Interrupted dispatch recovery ───────────────────────────────────────
|
|
2070
2222
|
|
|
2223
|
+
/** The claim-time `build.created` payload. Assets are omitted when the ticket
|
|
2224
|
+
* had none, so a no-asset build's log is exactly what it was before. */
|
|
2225
|
+
private buildCreatedPayload(
|
|
2226
|
+
ticket: TicketRef,
|
|
2227
|
+
repo: string,
|
|
2228
|
+
seed:
|
|
2229
|
+
| { autoMergeUser?: string; autoMergeDefaultSeq?: number; assets?: PinnedAsset[] }
|
|
2230
|
+
| undefined,
|
|
2231
|
+
): EventPayload<'build.created'> {
|
|
2232
|
+
const { config } = this.deps
|
|
2233
|
+
return {
|
|
2234
|
+
ticket,
|
|
2235
|
+
repo,
|
|
2236
|
+
baseBranch: config.baseBranch,
|
|
2237
|
+
...(seed?.autoMergeUser !== undefined ? { autoMergeRequestedBy: seed.autoMergeUser } : {}),
|
|
2238
|
+
...(seed?.autoMergeDefaultSeq !== undefined
|
|
2239
|
+
? { autoMergeDefaultSeq: seed.autoMergeDefaultSeq }
|
|
2240
|
+
: {}),
|
|
2241
|
+
...(config.pr !== undefined ? { pr: config.pr } : {}),
|
|
2242
|
+
...(seed?.assets !== undefined && seed.assets.length > 0 ? { assets: seed.assets } : {}),
|
|
2243
|
+
}
|
|
2244
|
+
}
|
|
2245
|
+
|
|
2071
2246
|
/** Every boundary before runner attachment is derived from durable facts.
|
|
2072
2247
|
* Missing facts are the todo list; successful provider calls are followed
|
|
2073
2248
|
* immediately by their facts, so an ordinary later tick can continue. */
|
|
@@ -2091,6 +2266,8 @@ export class Dispatcher {
|
|
|
2091
2266
|
body: string
|
|
2092
2267
|
authoredSession?: string
|
|
2093
2268
|
autoMergeUser?: string
|
|
2269
|
+
autoMergeDefaultSeq?: number
|
|
2270
|
+
assets?: PinnedAsset[]
|
|
2094
2271
|
},
|
|
2095
2272
|
): Promise<DispatchOutcome> {
|
|
2096
2273
|
const { store, config } = this.deps
|
|
@@ -2103,18 +2280,16 @@ export class Dispatcher {
|
|
|
2103
2280
|
'build record lacks the immutable ticket facts needed to reconstruct build.created',
|
|
2104
2281
|
)
|
|
2105
2282
|
}
|
|
2283
|
+
// Legacy path: a record an older dispatcher left with an empty stream.
|
|
2284
|
+
// New claims write `build.created` atomically with the record, so this
|
|
2285
|
+
// can only sample the ticket's current assets (SPEC §6.3).
|
|
2286
|
+
const assets =
|
|
2287
|
+
seed?.assets ??
|
|
2288
|
+
(await samplePinnedAssets(store, record.repo, record.ticket.id).catch(() => []))
|
|
2106
2289
|
await store.appendIfCurrent(record.slug, 0, {
|
|
2107
2290
|
actor: DISPATCHER,
|
|
2108
2291
|
type: 'build.created',
|
|
2109
|
-
payload: {
|
|
2110
|
-
ticket: record.ticket,
|
|
2111
|
-
repo: record.repo,
|
|
2112
|
-
baseBranch: config.baseBranch,
|
|
2113
|
-
...(seed?.autoMergeUser !== undefined
|
|
2114
|
-
? { autoMergeRequestedBy: seed.autoMergeUser }
|
|
2115
|
-
: {}),
|
|
2116
|
-
...(config.pr !== undefined ? { pr: config.pr } : {}),
|
|
2117
|
-
},
|
|
2292
|
+
payload: this.buildCreatedPayload(record.ticket, record.repo, { ...seed, assets }),
|
|
2118
2293
|
})
|
|
2119
2294
|
events = await store.getEvents(record.slug)
|
|
2120
2295
|
if (!events.some((event) => event.type === 'build.created')) {
|
|
@@ -2124,6 +2299,10 @@ export class Dispatcher {
|
|
|
2124
2299
|
|
|
2125
2300
|
const created = events.find((event) => event.type === 'build.created')
|
|
2126
2301
|
const autoMergeUser = seed?.autoMergeUser ?? created?.payload.autoMergeRequestedBy
|
|
2302
|
+
// The claim's sampled default fact, for the request's fan-out provenance:
|
|
2303
|
+
// the seed's own value first (this dispatch created the build), else the
|
|
2304
|
+
// durable `build.created` fact a recovery pass resumes from.
|
|
2305
|
+
const autoMergeDefaultSeq = seed?.autoMergeDefaultSeq ?? created?.payload.autoMergeDefaultSeq
|
|
2127
2306
|
if (
|
|
2128
2307
|
autoMergeUser !== undefined &&
|
|
2129
2308
|
!events.some((event) => event.type === 'build.auto-merge-requested')
|
|
@@ -2131,7 +2310,9 @@ export class Dispatcher {
|
|
|
2131
2310
|
await store.append(record.slug, {
|
|
2132
2311
|
actor: humanActor(autoMergeUser),
|
|
2133
2312
|
type: 'build.auto-merge-requested',
|
|
2134
|
-
payload: {
|
|
2313
|
+
payload: {
|
|
2314
|
+
...(autoMergeDefaultSeq !== undefined ? { defaultSeq: autoMergeDefaultSeq } : {}),
|
|
2315
|
+
},
|
|
2135
2316
|
})
|
|
2136
2317
|
events = await store.getEvents(record.slug)
|
|
2137
2318
|
}
|
|
@@ -2172,6 +2353,7 @@ export class Dispatcher {
|
|
|
2172
2353
|
ref: handle.ref,
|
|
2173
2354
|
path: handle.path,
|
|
2174
2355
|
...(handle.localPath !== undefined ? { localPath: handle.localPath } : {}),
|
|
2356
|
+
remote: this.deps.workspaces.publication !== undefined,
|
|
2175
2357
|
branch: handle.branch,
|
|
2176
2358
|
base: handle.base,
|
|
2177
2359
|
},
|
|
@@ -2218,6 +2400,8 @@ export class Dispatcher {
|
|
|
2218
2400
|
body: string
|
|
2219
2401
|
authoredSession?: string
|
|
2220
2402
|
autoMergeUser?: string
|
|
2403
|
+
autoMergeDefaultSeq?: number
|
|
2404
|
+
assets?: PinnedAsset[]
|
|
2221
2405
|
},
|
|
2222
2406
|
report?: TickReport,
|
|
2223
2407
|
launched?: Set<string>,
|
|
@@ -2578,6 +2762,7 @@ export class Dispatcher {
|
|
|
2578
2762
|
autoMergeUser: string | undefined,
|
|
2579
2763
|
paused: boolean,
|
|
2580
2764
|
opts: TickOpts = {},
|
|
2765
|
+
autoMergeSeedFact: AutoMergeDefaultFact | undefined = undefined,
|
|
2581
2766
|
): Promise<void> {
|
|
2582
2767
|
const { store, tickets, config } = this.deps
|
|
2583
2768
|
// Blocked and paused builds still occupy a slot: their workspaces and
|
|
@@ -2599,9 +2784,9 @@ export class Dispatcher {
|
|
|
2599
2784
|
// standing queue-depth report and must stay honest exactly when the
|
|
2600
2785
|
// dispatcher is saturated. Sources that keep a claimed ticket in the
|
|
2601
2786
|
// ready state are deduped against the active builds embodying them.
|
|
2602
|
-
const journalBeforeReady = await this.
|
|
2787
|
+
const journalBeforeReady = await readRepoEventsIfRecorded(this.deps.store, this.deps.repo)
|
|
2603
2788
|
const listing = await tickets.listReady(readyCriteria(config))
|
|
2604
|
-
const journalAfterReady = await this.
|
|
2789
|
+
const journalAfterReady = await readRepoEventsIfRecorded(this.deps.store, this.deps.repo)
|
|
2605
2790
|
const creationHolds = new Map(
|
|
2606
2791
|
harvestCreationsDuringReadyScan(journalBeforeReady, journalAfterReady).map((creation) => [
|
|
2607
2792
|
creation.creationKey.toLowerCase(),
|
|
@@ -2691,6 +2876,10 @@ export class Dispatcher {
|
|
|
2691
2876
|
continue
|
|
2692
2877
|
}
|
|
2693
2878
|
|
|
2879
|
+
// Freeze the ticket's assets at the claim (SPEC §6.3): sampled before the
|
|
2880
|
+
// spec-authoring interval so edits made during it are never pinned.
|
|
2881
|
+
const assets = await samplePinnedAssets(store, this.deps.repo, ticket.ref.id)
|
|
2882
|
+
|
|
2694
2883
|
// Quality gate (§6.3): import a conforming ticket body as the spec;
|
|
2695
2884
|
// author one for thin-but-groomed tickets; otherwise bounce.
|
|
2696
2885
|
let body = ticket.body
|
|
@@ -2717,12 +2906,29 @@ export class Dispatcher {
|
|
|
2717
2906
|
const slug = await this.uniqueSlug(baseSlug)
|
|
2718
2907
|
const branch = `ab/${slug}`
|
|
2719
2908
|
const repoOrigin = await this.resolveRepoOriginOnce()
|
|
2909
|
+
const createdSeed = {
|
|
2910
|
+
...(autoMergeUser !== undefined ? { autoMergeUser } : {}),
|
|
2911
|
+
// Claim-time provenance: the newest durable default fact that agrees
|
|
2912
|
+
// with the launch sample, recorded on `build.created` so the fan-out's
|
|
2913
|
+
// seq comparison sees the claim as already answering that fact — and
|
|
2914
|
+
// so a newer opposite fact (an interleaved toggle) stays strictly
|
|
2915
|
+
// ahead of it and reconciles this build on the next tick (f_b851c0e8).
|
|
2916
|
+
...(autoMergeSeedFact !== undefined ? { autoMergeDefaultSeq: autoMergeSeedFact.seq } : {}),
|
|
2917
|
+
assets,
|
|
2918
|
+
}
|
|
2919
|
+
// `build.created` rides with the record so the pinned assets and the
|
|
2920
|
+
// record exist together or not at all.
|
|
2720
2921
|
const record = await store.createBuild({
|
|
2721
2922
|
slug,
|
|
2722
2923
|
repo: this.deps.repo,
|
|
2723
2924
|
ticket: ticket.ref,
|
|
2724
2925
|
branch,
|
|
2725
2926
|
...(repoOrigin !== undefined ? { repoOrigin } : {}),
|
|
2927
|
+
created: {
|
|
2928
|
+
actor: DISPATCHER,
|
|
2929
|
+
type: 'build.created',
|
|
2930
|
+
payload: this.buildCreatedPayload(ticket.ref, this.deps.repo, createdSeed),
|
|
2931
|
+
},
|
|
2726
2932
|
})
|
|
2727
2933
|
report.queued -= 1
|
|
2728
2934
|
capacity -= 1
|
|
@@ -2730,7 +2936,7 @@ export class Dispatcher {
|
|
|
2730
2936
|
ticket,
|
|
2731
2937
|
body,
|
|
2732
2938
|
...(authoredSession !== undefined ? { authoredSession } : {}),
|
|
2733
|
-
...
|
|
2939
|
+
...createdSeed,
|
|
2734
2940
|
})
|
|
2735
2941
|
// Accepted dispatches: durable boundaries recorded. A deferred outcome
|
|
2736
2942
|
// means the remote provisioning continuation owns the rest.
|
|
@@ -61,8 +61,13 @@ export async function runHarvestChild(
|
|
|
61
61
|
// Full build-child composition parity, in the same order: plugins load
|
|
62
62
|
// first or materializePluginRuntimes silently no-ops and leaves any
|
|
63
63
|
// plugin-provided runtime unrouted in the guest.
|
|
64
|
+
// Guest tolerance (AUT-517): a harvest runner launch is always
|
|
65
|
+
// environment-supervised — the process is structurally a guest and never
|
|
66
|
+
// constructs a workspace provider — so a configured provider plugin the
|
|
67
|
+
// guest cannot resolve is skipped with a notice instead of failing.
|
|
64
68
|
const plugins = await loadPlugins(config.plugins, workspacePath, {
|
|
65
69
|
packageRoot: input.repo,
|
|
70
|
+
...(input.supervision.kind === 'environment' ? { guest: true } : {}),
|
|
66
71
|
})
|
|
67
72
|
const { runtimes: builtins } = createProductionRuntimes()
|
|
68
73
|
const runtimes = await materializePluginRuntimes(builtins, plugins, {
|