@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
|
@@ -38,20 +38,23 @@
|
|
|
38
38
|
* Output carries no ANSI, ever, in either form (§16).
|
|
39
39
|
*/
|
|
40
40
|
import type { AbEvent } from '../events/catalog'
|
|
41
|
-
import {
|
|
42
|
-
import {
|
|
43
|
-
|
|
44
|
-
type RepositoryEvent,
|
|
45
|
-
type RepositoryEventType,
|
|
46
|
-
} from '../events/repository'
|
|
41
|
+
import type { EventType } from '../events/payloads'
|
|
42
|
+
import type { RepositoryEvent, RepositoryEventType } from '../events/repository'
|
|
43
|
+
import { REPOSITORY_ATTENTION_EVENTS } from '../events/repository'
|
|
47
44
|
import type { BuildState, PrLifecycle } from '../kernel/reducer'
|
|
48
45
|
import { reduceBuild } from '../kernel/reducer'
|
|
49
46
|
import type { BuildOutcome, BuildStatus, Phase } from '../ontology'
|
|
50
47
|
import type { Exec } from '../ports/workspace/git-worktree'
|
|
51
48
|
import { PhaseSessionError } from '../store/phase-session'
|
|
52
|
-
import { REMOTE_EVENT_WAIT_SECONDS } from '../store/remote/client'
|
|
53
49
|
import type { BuildRecord } from '../store/types'
|
|
54
50
|
import { resolveAmbientReadSession } from './env'
|
|
51
|
+
import {
|
|
52
|
+
createRemotePollRunner,
|
|
53
|
+
defaultDelay,
|
|
54
|
+
makeFailureStreak,
|
|
55
|
+
type RemotePollReadOpts,
|
|
56
|
+
type RemotePollRunner,
|
|
57
|
+
} from './remote-poll'
|
|
55
58
|
import { buildInRepository, isRemoteStoreRef, normalizeGitRemoteUrl } from './repo-state'
|
|
56
59
|
import { withAmbientReadStore, type StoreOpener } from './store-opening'
|
|
57
60
|
|
|
@@ -93,15 +96,6 @@ export const BUILD_ATTENTION_EVENTS = [
|
|
|
93
96
|
'build.aborted',
|
|
94
97
|
] as const satisfies readonly EventType[]
|
|
95
98
|
|
|
96
|
-
/** The repository-journal attention set, followed only under `--repository`. */
|
|
97
|
-
export const REPOSITORY_ATTENTION_EVENTS = [
|
|
98
|
-
'harvest.escalated',
|
|
99
|
-
'harvest.failed',
|
|
100
|
-
'dispatcher.tick-failed',
|
|
101
|
-
'dispatcher.config-rejected',
|
|
102
|
-
'dispatcher.harvest-runner-failed',
|
|
103
|
-
] as const satisfies readonly RepositoryEventType[]
|
|
104
|
-
|
|
105
99
|
// ── Glob compilation and matching ────────────────────────────────────────────
|
|
106
100
|
|
|
107
101
|
/**
|
|
@@ -111,36 +105,12 @@ export const REPOSITORY_ATTENTION_EVENTS = [
|
|
|
111
105
|
* the build catalog — or, when `--repository` is given, in the repository
|
|
112
106
|
* catalog — otherwise a usage error naming the glob is thrown before any
|
|
113
107
|
* store read. Supplying any glob replaces the attention set entirely.
|
|
108
|
+
*
|
|
109
|
+
* The compiler itself lives in `events/globs.ts`, shared with the
|
|
110
|
+
* `[orchestrator].wake` configuration validation (AUT-342).
|
|
114
111
|
*/
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
opts: { repository: boolean; usage: string },
|
|
118
|
-
): RegExp[] {
|
|
119
|
-
return globs.map((glob) => {
|
|
120
|
-
const source =
|
|
121
|
-
'^' +
|
|
122
|
-
[...glob]
|
|
123
|
-
.map((char) => {
|
|
124
|
-
if (char === '*') return '.*'
|
|
125
|
-
if (char === '?') return '.'
|
|
126
|
-
return char.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
127
|
-
})
|
|
128
|
-
.join('') +
|
|
129
|
-
'$'
|
|
130
|
-
const regex = new RegExp(source)
|
|
131
|
-
const inBuildCatalog = EVENT_TYPES.some((type) => regex.test(type))
|
|
132
|
-
const inRepositoryCatalog =
|
|
133
|
-
opts.repository && REPOSITORY_EVENT_TYPES.some((type) => regex.test(type))
|
|
134
|
-
if (!inBuildCatalog && !inRepositoryCatalog) {
|
|
135
|
-
throw new Error(
|
|
136
|
-
`--event "${glob}" matches no known event type${
|
|
137
|
-
opts.repository ? ' in the build or repository catalogs' : ' in the build event catalog'
|
|
138
|
-
} — ${opts.usage}`,
|
|
139
|
-
)
|
|
140
|
-
}
|
|
141
|
-
return regex
|
|
142
|
-
})
|
|
143
|
-
}
|
|
112
|
+
import { compileEventGlobs } from '../events/globs'
|
|
113
|
+
export { compileEventGlobs } from '../events/globs'
|
|
144
114
|
|
|
145
115
|
function matchesBuildFilter(type: EventType, filters: readonly RegExp[]): boolean {
|
|
146
116
|
return filters.length > 0
|
|
@@ -470,23 +440,6 @@ interface RepositoryStream {
|
|
|
470
440
|
|
|
471
441
|
type Stream = BuildStream | RepositoryStream
|
|
472
442
|
|
|
473
|
-
function defaultDelay(ms: number, signal?: AbortSignal): Promise<void> {
|
|
474
|
-
return new Promise((resolve) => {
|
|
475
|
-
if (signal?.aborted === true) {
|
|
476
|
-
resolve()
|
|
477
|
-
return
|
|
478
|
-
}
|
|
479
|
-
const finish = (): void => {
|
|
480
|
-
clearTimeout(timer)
|
|
481
|
-
signal?.removeEventListener('abort', onAbort)
|
|
482
|
-
resolve()
|
|
483
|
-
}
|
|
484
|
-
const timer = setTimeout(finish, ms)
|
|
485
|
-
const onAbort = (): void => finish()
|
|
486
|
-
signal?.addEventListener('abort', onAbort, { once: true })
|
|
487
|
-
})
|
|
488
|
-
}
|
|
489
|
-
|
|
490
443
|
/**
|
|
491
444
|
* `ab watch` — stream matching build and repository events until a timeout, a
|
|
492
445
|
* record count, or (only when slugs were named) terminal status ends the
|
|
@@ -531,6 +484,14 @@ export async function abWatch(opts: AbWatchOpts): Promise<void> {
|
|
|
531
484
|
const sinceStreams =
|
|
532
485
|
opts.since !== undefined ? decodeCursor(opts.since, { store: storeRef, repo }).streams : {}
|
|
533
486
|
|
|
487
|
+
// Verified (AUT-574): this is the full normalized-identity rule of
|
|
488
|
+
// AUT-567 — the same `mine` predicate as status.ts and wait.ts, with
|
|
489
|
+
// buildInRepository's semantics. `identity` is resolveRepoState's
|
|
490
|
+
// `origin ?? resolved checkout`, already normalized (re-normalizing is
|
|
491
|
+
// a no-op); the first arm is the whole test for new records, whose
|
|
492
|
+
// `record.repo` IS the normalized origin, and the second arm forgives
|
|
493
|
+
// legacy path-keyed records via their normalized `repoOrigin`. A
|
|
494
|
+
// foreign origin, or an origin-less legacy record, is excluded.
|
|
534
495
|
const identity = normalizeGitRemoteUrl(repo)
|
|
535
496
|
const mine = (record: BuildRecord): boolean =>
|
|
536
497
|
record.repo === identity ||
|
|
@@ -543,17 +504,13 @@ export async function abWatch(opts: AbWatchOpts): Promise<void> {
|
|
|
543
504
|
let emitted = 0
|
|
544
505
|
let countCursor: string | undefined
|
|
545
506
|
let stop = false
|
|
546
|
-
//
|
|
547
|
-
//
|
|
548
|
-
//
|
|
549
|
-
|
|
550
|
-
const inFlightReads = new Set<AbortController>()
|
|
551
|
-
// Wakes stop-aware gap sleeps the moment the watch stops.
|
|
552
|
-
const stopController = new AbortController()
|
|
507
|
+
// On the remote path this is the shared long-poll runner (the held
|
|
508
|
+
// reads, their cancellation, and the gap sleeps it owns); on the local
|
|
509
|
+
// path it stays undefined and a stop trips the `stop` flag alone.
|
|
510
|
+
let runner: RemotePollRunner | undefined
|
|
553
511
|
const requestStop = (): void => {
|
|
554
512
|
stop = true
|
|
555
|
-
|
|
556
|
-
for (const controller of inFlightReads) controller.abort()
|
|
513
|
+
runner?.requestStop()
|
|
557
514
|
}
|
|
558
515
|
|
|
559
516
|
const encodeCurrent = (): string =>
|
|
@@ -607,6 +564,12 @@ export async function abWatch(opts: AbWatchOpts): Promise<void> {
|
|
|
607
564
|
if (streams.has(slug)) return true
|
|
608
565
|
const record = await store.getBuild(slug)
|
|
609
566
|
if (record === null) return false
|
|
567
|
+
// AUT-545: this full-log read is deliberate, accepted growth — the
|
|
568
|
+
// retained prefix is consumed at full replay depth by every later
|
|
569
|
+
// reduction (approvals, findings, answered escalations, PR facts,
|
|
570
|
+
// observations), so bounding it would need reducer-partition store
|
|
571
|
+
// machinery out of AUT-545's scope. Required for `--since` replay and
|
|
572
|
+
// the baselined stream prefix. Mirrors cli/wait.ts trackBuild.
|
|
610
573
|
const events = await store.getEvents(slug)
|
|
611
574
|
const stream: BuildStream = {
|
|
612
575
|
kind: 'build',
|
|
@@ -646,8 +609,24 @@ export async function abWatch(opts: AbWatchOpts): Promise<void> {
|
|
|
646
609
|
* the throw stay tracked, so the retry neither duplicates nor skips.
|
|
647
610
|
*/
|
|
648
611
|
const discoverBuilds = async (): Promise<void> => {
|
|
612
|
+
// AUT-545: the terminal filter is the AUT-487 digest, not a full-log read —
|
|
613
|
+
// one flat batch read replaces a full getEvents per candidate build, per
|
|
614
|
+
// discovery pass; `terminal` is reduceBuild's exact terminal rule
|
|
615
|
+
// (store/digest's reduceBuildDigest, pinned by contract). The per-build
|
|
616
|
+
// full read below remains deliberate for builds the digest cannot retire:
|
|
617
|
+
// its retained prefix is consumed at full replay depth by every later
|
|
618
|
+
// reduction (approvals, findings, answered escalations, PR facts,
|
|
619
|
+
// observations), so bounding it would need reducer-partition store
|
|
620
|
+
// machinery out of AUT-545's scope. The reduceBuild re-check stays: it is
|
|
621
|
+
// the legacy-record fallback (a record whose `repo` predates origin
|
|
622
|
+
// normalization is in `mine` via `repoOrigin` but absent from the digest
|
|
623
|
+
// map, which keys on `record.repo`) and it keeps this filter unable to
|
|
624
|
+
// diverge from reduceBuild. Mirrors cli/wait.ts discoverBuilds.
|
|
625
|
+
const digests = await store.getRepoBuildDigests(repo)
|
|
649
626
|
for (const record of (await store.listBuilds()).filter(mine)) {
|
|
650
627
|
if (streams.has(record.slug)) continue
|
|
628
|
+
const digest = digests.get(record.slug)
|
|
629
|
+
if (digest !== undefined && digest.terminal !== undefined) continue
|
|
651
630
|
const events = await store.getEvents(record.slug)
|
|
652
631
|
const status = reduceBuild(events).status
|
|
653
632
|
if (!NONTERMINAL_STATUSES.includes(status)) continue
|
|
@@ -672,9 +651,27 @@ export async function abWatch(opts: AbWatchOpts): Promise<void> {
|
|
|
672
651
|
try {
|
|
673
652
|
// A fresh store has no repository row; do not create one.
|
|
674
653
|
if (resumeSeq === undefined && (await store.getRepo(repo)) !== null) {
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
654
|
+
// Bounded baseline (AUT-534): the baseline needs only the
|
|
655
|
+
// journal's end position, and AUT-489's bounded read answers that
|
|
656
|
+
// whenever the journal contains a `dispatcher.run-started` — the
|
|
657
|
+
// tail from the latest anchor runs to the journal end, so the
|
|
658
|
+
// subset's max seq equals the journal's max seq.
|
|
659
|
+
const subset = await store.getRepoStateEvents(repo)
|
|
660
|
+
let last = subset.at(-1)?.seq ?? 0
|
|
661
|
+
if (!subset.some((event) => event.type === 'dispatcher.run-started')) {
|
|
662
|
+
// Anchor-less journal: the subset may have dropped run-scoped
|
|
663
|
+
// facts appended before any anchor, so one bounded delta read
|
|
664
|
+
// recovers the true end; its events are baselined silently,
|
|
665
|
+
// exactly as the full read baselines them. The skip condition
|
|
666
|
+
// is exact: a run-started is its own anchor or lies in a later
|
|
667
|
+
// anchor's tail, so "no run-started in the subset" means "no
|
|
668
|
+
// run-started in the journal" — the only case where the subset
|
|
669
|
+
// can trail.
|
|
670
|
+
const tail = await store.getRepoEvents(repo, last)
|
|
671
|
+
last = tail.at(-1)?.seq ?? last
|
|
672
|
+
}
|
|
673
|
+
stream.lastSeq = last
|
|
674
|
+
positions[REPO_STREAM_KEY] = last
|
|
678
675
|
}
|
|
679
676
|
return true
|
|
680
677
|
} catch {
|
|
@@ -684,7 +681,7 @@ export async function abWatch(opts: AbWatchOpts): Promise<void> {
|
|
|
684
681
|
|
|
685
682
|
const pollBuild = async (
|
|
686
683
|
stream: BuildStream,
|
|
687
|
-
readOpts?:
|
|
684
|
+
readOpts?: RemotePollReadOpts,
|
|
688
685
|
): Promise<boolean> => {
|
|
689
686
|
const fresh = await store.getEvents(stream.slug, stream.lastSeq, readOpts)
|
|
690
687
|
for (const event of fresh) {
|
|
@@ -700,7 +697,7 @@ export async function abWatch(opts: AbWatchOpts): Promise<void> {
|
|
|
700
697
|
|
|
701
698
|
const pollRepository = async (
|
|
702
699
|
stream: RepositoryStream,
|
|
703
|
-
readOpts?:
|
|
700
|
+
readOpts?: RemotePollReadOpts,
|
|
704
701
|
): Promise<boolean> => {
|
|
705
702
|
// A repository row may be created mid-watch by the first dispatch.
|
|
706
703
|
if ((await store.getRepo(repo)) === null) return true
|
|
@@ -722,34 +719,10 @@ export async function abWatch(opts: AbWatchOpts): Promise<void> {
|
|
|
722
719
|
return true
|
|
723
720
|
}
|
|
724
721
|
|
|
725
|
-
/** A failed read is reported once per failure streak, advances nothing,
|
|
726
|
-
* and is retried at the next interval (local) or next request (remote);
|
|
727
|
-
* a success on the same source re-arms the report. Each read source —
|
|
728
|
-
* per stream task, plus discovery — carries its own streak, so a
|
|
729
|
-
* persistent failure on one stream interleaved with successes elsewhere
|
|
730
|
-
* is still reported only once. */
|
|
731
|
-
const makeFailureStreak = (): {
|
|
732
|
-
onFailure: (error: unknown) => void
|
|
733
|
-
onSuccess: () => void
|
|
734
|
-
} => {
|
|
735
|
-
let reported = false
|
|
736
|
-
return {
|
|
737
|
-
onFailure: (error: unknown): void => {
|
|
738
|
-
if (reported) return
|
|
739
|
-
reported = true
|
|
740
|
-
const message = error instanceof Error ? error.message : String(error)
|
|
741
|
-
opts.stderr(`ab watch: a store read failed (${message}); retrying at the next interval`)
|
|
742
|
-
},
|
|
743
|
-
onSuccess: (): void => {
|
|
744
|
-
reported = false
|
|
745
|
-
},
|
|
746
|
-
}
|
|
747
|
-
}
|
|
748
|
-
|
|
749
722
|
// One streak for the whole local loop — the same lifetime the shared
|
|
750
723
|
// reported flag had before per-source streaks: a persistent read
|
|
751
724
|
// failure is reported once, and a fully-successful cycle re-arms it.
|
|
752
|
-
const tickStreak = makeFailureStreak()
|
|
725
|
+
const tickStreak = makeFailureStreak('watch', opts.stderr)
|
|
753
726
|
|
|
754
727
|
const tick = async (): Promise<void> => {
|
|
755
728
|
let allReadsOk = true
|
|
@@ -799,7 +772,7 @@ export async function abWatch(opts: AbWatchOpts): Promise<void> {
|
|
|
799
772
|
}
|
|
800
773
|
if (repository) {
|
|
801
774
|
if (!(await trackRepository())) {
|
|
802
|
-
makeFailureStreak().onFailure(
|
|
775
|
+
makeFailureStreak('watch', opts.stderr).onFailure(
|
|
803
776
|
new Error(`could not read the repository journal for "${repo}"`),
|
|
804
777
|
)
|
|
805
778
|
}
|
|
@@ -840,57 +813,51 @@ export async function abWatch(opts: AbWatchOpts): Promise<void> {
|
|
|
840
813
|
// Every stop is prompt: an external abort, a tripped --count, an
|
|
841
814
|
// all-terminal named set, or an elapsed --timeout cancels the
|
|
842
815
|
// in-flight held reads (via `requestStop`) and wakes the gap sleeps
|
|
843
|
-
// (via
|
|
844
|
-
// held read's own bound is additionally capped at the
|
|
845
|
-
// remaining time budget, so a hold can never outlive the
|
|
816
|
+
// (via the runner's stop controller) instead of waiting out a full
|
|
817
|
+
// hold. The held read's own bound is additionally capped at the
|
|
818
|
+
// watch's remaining time budget, so a hold can never outlive the
|
|
819
|
+
// deadline.
|
|
846
820
|
const shouldStop = stop || aborted() || now().getTime() >= deadline || namedAllTerminal()
|
|
847
|
-
const
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
} finally {
|
|
878
|
-
inFlightReads.delete(controller)
|
|
879
|
-
}
|
|
880
|
-
if (stop || aborted()) return
|
|
881
|
-
const elapsed = now().getTime() - started
|
|
882
|
-
if (elapsed < interval) await sleep(interval - elapsed, stopController.signal)
|
|
883
|
-
}
|
|
884
|
-
}
|
|
821
|
+
const remoteRunner = createRemotePollRunner({
|
|
822
|
+
command: 'watch',
|
|
823
|
+
stderr: opts.stderr,
|
|
824
|
+
now,
|
|
825
|
+
sleep,
|
|
826
|
+
intervalMs: interval,
|
|
827
|
+
deadlineMs: deadline,
|
|
828
|
+
aborted,
|
|
829
|
+
shouldStop: () => aborted() || now().getTime() >= deadline || namedAllTerminal(),
|
|
830
|
+
// A task's terminal endCheck stops the runner from the inside; the
|
|
831
|
+
// command's `stop` flag must trip with it — pollBuild and
|
|
832
|
+
// pollRepository gate their batch loops on that flag
|
|
833
|
+
// (`if (stop) break`), so a sibling task whose held read has
|
|
834
|
+
// already resolved stops delivering its batch instead of draining
|
|
835
|
+
// it, exactly as the pre-extraction command-scoped `requestStop`
|
|
836
|
+
// (which set `stop` on every path) made it do (f_42263f38).
|
|
837
|
+
onStop: (): void => {
|
|
838
|
+
stop = true
|
|
839
|
+
},
|
|
840
|
+
// Quiesce, not the runner's one-shot snapshot: a stream the
|
|
841
|
+
// discovery task registers while drain is awaiting must have its
|
|
842
|
+
// first held read awaited before the final cursor is encoded, or a
|
|
843
|
+
// mid-discovery registration's emission lands after the cursor and
|
|
844
|
+
// the next run replays it (the gap widened from a scheduling race
|
|
845
|
+
// to a deterministic miss when discovery's digest read added one
|
|
846
|
+
// await before the per-build reads — AUT-545). The snapshot
|
|
847
|
+
// semantics the runner documents remain covered by its own tests.
|
|
848
|
+
drain: 'quiesce',
|
|
849
|
+
})
|
|
850
|
+
runner = remoteRunner
|
|
885
851
|
|
|
886
852
|
const launch = (stream: Stream): void => {
|
|
887
853
|
const key = stream.kind === 'build' ? stream.slug : REPO_STREAM_KEY
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
854
|
+
remoteRunner.launch(
|
|
855
|
+
key,
|
|
856
|
+
(readOpts) =>
|
|
857
|
+
stream.kind === 'build'
|
|
858
|
+
? pollBuild(stream, readOpts)
|
|
859
|
+
: pollRepository(stream, readOpts),
|
|
860
|
+
() => namedAllTerminal(),
|
|
894
861
|
)
|
|
895
862
|
}
|
|
896
863
|
|
|
@@ -901,25 +868,20 @@ export async function abWatch(opts: AbWatchOpts): Promise<void> {
|
|
|
901
868
|
for (const stream of streams.values()) if (!shouldStop) launch(stream)
|
|
902
869
|
|
|
903
870
|
if (slugs.length === 0 && !shouldStop) {
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
if (stop || aborted()) return
|
|
916
|
-
await sleep(interval, stopController.signal)
|
|
917
|
-
}
|
|
918
|
-
})(),
|
|
919
|
-
)
|
|
871
|
+
void remoteRunner.runDiscovery({
|
|
872
|
+
step: async (): Promise<void> => {
|
|
873
|
+
await discoverBuilds()
|
|
874
|
+
},
|
|
875
|
+
// Unconditional, after the catch — exactly where the inline
|
|
876
|
+
// launch loop ran: streams registered before a mid-discovery
|
|
877
|
+
// throw still get tasks whose polls then run independently.
|
|
878
|
+
launchPending: (): void => {
|
|
879
|
+
for (const stream of streams.values()) launch(stream)
|
|
880
|
+
},
|
|
881
|
+
})
|
|
920
882
|
}
|
|
921
883
|
|
|
922
|
-
await
|
|
884
|
+
await remoteRunner.drain()
|
|
923
885
|
opts.signal?.removeEventListener('abort', requestStop)
|
|
924
886
|
}
|
|
925
887
|
|
|
@@ -90,6 +90,28 @@ export function restartRequiredChanges(
|
|
|
90
90
|
return changed
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
+
/** Config sections that define the pipeline a build runs under (SPEC §16.1):
|
|
94
|
+
* the verify universe, finalize steps, deterministic commands, and the
|
|
95
|
+
* workspace provisioning definition. Build-owned sections are composed from
|
|
96
|
+
* the build's own branch at a recorded pipeline-source commit and never
|
|
97
|
+
* replaced by a later base-branch change. */
|
|
98
|
+
export const BUILD_OWNED_CONFIG_PATHS = ['verify', 'finalize', 'commands', 'workspace'] as const
|
|
99
|
+
|
|
100
|
+
/** Compose one build's effective config: build-owned tables come from the
|
|
101
|
+
* pipeline source (the build's own branch), everything else from the live
|
|
102
|
+
* dispatcher snapshot (deployment). Dispatcher-owned sections (baseBranch,
|
|
103
|
+
* capacity, forge, plugins, pr, tickets, orchestrator) always travel with the
|
|
104
|
+
* deployment side — the runner never interprets them. */
|
|
105
|
+
export function composeBuildConfig(pipeline: Config, deployment: Config): Config {
|
|
106
|
+
return {
|
|
107
|
+
...deployment,
|
|
108
|
+
verify: pipeline.verify,
|
|
109
|
+
finalize: pipeline.finalize,
|
|
110
|
+
commands: pipeline.commands,
|
|
111
|
+
workspace: pipeline.workspace,
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
93
115
|
/** Overlay all hot fields while retaining every startup-built adapter field. */
|
|
94
116
|
export function composeReloadedConfig(startup: Config, candidate: Config): Config {
|
|
95
117
|
return {
|