@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
|
@@ -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',
|
|
@@ -634,6 +597,9 @@ export async function abWatch(opts: AbWatchOpts): Promise<void> {
|
|
|
634
597
|
return true
|
|
635
598
|
}
|
|
636
599
|
|
|
600
|
+
/** Builds discovery read in full and found terminal; never re-read. */
|
|
601
|
+
const retired = new Set<string>()
|
|
602
|
+
|
|
637
603
|
/**
|
|
638
604
|
* No-slug membership: every nonterminal build of this repository joins
|
|
639
605
|
* the watch, baselined at its current maximum so no history replays. A
|
|
@@ -646,11 +612,32 @@ export async function abWatch(opts: AbWatchOpts): Promise<void> {
|
|
|
646
612
|
* the throw stay tracked, so the retry neither duplicates nor skips.
|
|
647
613
|
*/
|
|
648
614
|
const discoverBuilds = async (): Promise<void> => {
|
|
615
|
+
// AUT-545: the terminal filter is the AUT-487 digest, not a full-log read —
|
|
616
|
+
// one flat batch read replaces a full getEvents per candidate build, per
|
|
617
|
+
// discovery pass; `terminal` is reduceBuild's exact terminal rule
|
|
618
|
+
// (store/digest's reduceBuildDigest, pinned by contract). The per-build
|
|
619
|
+
// full read below remains deliberate for builds the digest cannot retire:
|
|
620
|
+
// its retained prefix is consumed at full replay depth by every later
|
|
621
|
+
// reduction (approvals, findings, answered escalations, PR facts,
|
|
622
|
+
// observations), so bounding it would need reducer-partition store
|
|
623
|
+
// machinery out of AUT-545's scope. The reduceBuild re-check stays: it is
|
|
624
|
+
// the legacy-record fallback (a record whose `repo` predates origin
|
|
625
|
+
// normalization is in `mine` via `repoOrigin` but absent from the digest
|
|
626
|
+
// map, which keys on `record.repo`) and it keeps this filter unable to
|
|
627
|
+
// diverge from reduceBuild. Mirrors cli/wait.ts discoverBuilds.
|
|
628
|
+
const digests = await store.getRepoBuildDigests(repo)
|
|
649
629
|
for (const record of (await store.listBuilds()).filter(mine)) {
|
|
650
|
-
if (streams.has(record.slug)) continue
|
|
630
|
+
if (streams.has(record.slug) || retired.has(record.slug)) continue
|
|
631
|
+
const digest = digests.get(record.slug)
|
|
632
|
+
if (digest !== undefined && digest.terminal !== undefined) continue
|
|
651
633
|
const events = await store.getEvents(record.slug)
|
|
652
634
|
const status = reduceBuild(events).status
|
|
653
|
-
if (!NONTERMINAL_STATUSES.includes(status))
|
|
635
|
+
if (!NONTERMINAL_STATUSES.includes(status)) {
|
|
636
|
+
// Terminal is permanent (reduceBuild never clears it): a legacy
|
|
637
|
+
// record the digest cannot retire is read once, not every pass.
|
|
638
|
+
retired.add(record.slug)
|
|
639
|
+
continue
|
|
640
|
+
}
|
|
654
641
|
const stream: BuildStream = {
|
|
655
642
|
kind: 'build',
|
|
656
643
|
slug: record.slug,
|
|
@@ -672,9 +659,27 @@ export async function abWatch(opts: AbWatchOpts): Promise<void> {
|
|
|
672
659
|
try {
|
|
673
660
|
// A fresh store has no repository row; do not create one.
|
|
674
661
|
if (resumeSeq === undefined && (await store.getRepo(repo)) !== null) {
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
662
|
+
// Bounded baseline (AUT-534): the baseline needs only the
|
|
663
|
+
// journal's end position, and AUT-489's bounded read answers that
|
|
664
|
+
// whenever the journal contains a `dispatcher.run-started` — the
|
|
665
|
+
// tail from the latest anchor runs to the journal end, so the
|
|
666
|
+
// subset's max seq equals the journal's max seq.
|
|
667
|
+
const subset = await store.getRepoStateEvents(repo)
|
|
668
|
+
let last = subset.at(-1)?.seq ?? 0
|
|
669
|
+
if (!subset.some((event) => event.type === 'dispatcher.run-started')) {
|
|
670
|
+
// Anchor-less journal: the subset may have dropped run-scoped
|
|
671
|
+
// facts appended before any anchor, so one bounded delta read
|
|
672
|
+
// recovers the true end; its events are baselined silently,
|
|
673
|
+
// exactly as the full read baselines them. The skip condition
|
|
674
|
+
// is exact: a run-started is its own anchor or lies in a later
|
|
675
|
+
// anchor's tail, so "no run-started in the subset" means "no
|
|
676
|
+
// run-started in the journal" — the only case where the subset
|
|
677
|
+
// can trail.
|
|
678
|
+
const tail = await store.getRepoEvents(repo, last)
|
|
679
|
+
last = tail.at(-1)?.seq ?? last
|
|
680
|
+
}
|
|
681
|
+
stream.lastSeq = last
|
|
682
|
+
positions[REPO_STREAM_KEY] = last
|
|
678
683
|
}
|
|
679
684
|
return true
|
|
680
685
|
} catch {
|
|
@@ -684,7 +689,7 @@ export async function abWatch(opts: AbWatchOpts): Promise<void> {
|
|
|
684
689
|
|
|
685
690
|
const pollBuild = async (
|
|
686
691
|
stream: BuildStream,
|
|
687
|
-
readOpts?:
|
|
692
|
+
readOpts?: RemotePollReadOpts,
|
|
688
693
|
): Promise<boolean> => {
|
|
689
694
|
const fresh = await store.getEvents(stream.slug, stream.lastSeq, readOpts)
|
|
690
695
|
for (const event of fresh) {
|
|
@@ -700,7 +705,7 @@ export async function abWatch(opts: AbWatchOpts): Promise<void> {
|
|
|
700
705
|
|
|
701
706
|
const pollRepository = async (
|
|
702
707
|
stream: RepositoryStream,
|
|
703
|
-
readOpts?:
|
|
708
|
+
readOpts?: RemotePollReadOpts,
|
|
704
709
|
): Promise<boolean> => {
|
|
705
710
|
// A repository row may be created mid-watch by the first dispatch.
|
|
706
711
|
if ((await store.getRepo(repo)) === null) return true
|
|
@@ -722,34 +727,10 @@ export async function abWatch(opts: AbWatchOpts): Promise<void> {
|
|
|
722
727
|
return true
|
|
723
728
|
}
|
|
724
729
|
|
|
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
730
|
// One streak for the whole local loop — the same lifetime the shared
|
|
750
731
|
// reported flag had before per-source streaks: a persistent read
|
|
751
732
|
// failure is reported once, and a fully-successful cycle re-arms it.
|
|
752
|
-
const tickStreak = makeFailureStreak()
|
|
733
|
+
const tickStreak = makeFailureStreak('watch', opts.stderr)
|
|
753
734
|
|
|
754
735
|
const tick = async (): Promise<void> => {
|
|
755
736
|
let allReadsOk = true
|
|
@@ -799,7 +780,7 @@ export async function abWatch(opts: AbWatchOpts): Promise<void> {
|
|
|
799
780
|
}
|
|
800
781
|
if (repository) {
|
|
801
782
|
if (!(await trackRepository())) {
|
|
802
|
-
makeFailureStreak().onFailure(
|
|
783
|
+
makeFailureStreak('watch', opts.stderr).onFailure(
|
|
803
784
|
new Error(`could not read the repository journal for "${repo}"`),
|
|
804
785
|
)
|
|
805
786
|
}
|
|
@@ -840,57 +821,51 @@ export async function abWatch(opts: AbWatchOpts): Promise<void> {
|
|
|
840
821
|
// Every stop is prompt: an external abort, a tripped --count, an
|
|
841
822
|
// all-terminal named set, or an elapsed --timeout cancels the
|
|
842
823
|
// 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
|
|
824
|
+
// (via the runner's stop controller) instead of waiting out a full
|
|
825
|
+
// hold. The held read's own bound is additionally capped at the
|
|
826
|
+
// watch's remaining time budget, so a hold can never outlive the
|
|
827
|
+
// deadline.
|
|
846
828
|
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
|
-
}
|
|
829
|
+
const remoteRunner = createRemotePollRunner({
|
|
830
|
+
command: 'watch',
|
|
831
|
+
stderr: opts.stderr,
|
|
832
|
+
now,
|
|
833
|
+
sleep,
|
|
834
|
+
intervalMs: interval,
|
|
835
|
+
deadlineMs: deadline,
|
|
836
|
+
aborted,
|
|
837
|
+
shouldStop: () => aborted() || now().getTime() >= deadline || namedAllTerminal(),
|
|
838
|
+
// A task's terminal endCheck stops the runner from the inside; the
|
|
839
|
+
// command's `stop` flag must trip with it — pollBuild and
|
|
840
|
+
// pollRepository gate their batch loops on that flag
|
|
841
|
+
// (`if (stop) break`), so a sibling task whose held read has
|
|
842
|
+
// already resolved stops delivering its batch instead of draining
|
|
843
|
+
// it, exactly as the pre-extraction command-scoped `requestStop`
|
|
844
|
+
// (which set `stop` on every path) made it do (f_42263f38).
|
|
845
|
+
onStop: (): void => {
|
|
846
|
+
stop = true
|
|
847
|
+
},
|
|
848
|
+
// Quiesce, not the runner's one-shot snapshot: a stream the
|
|
849
|
+
// discovery task registers while drain is awaiting must have its
|
|
850
|
+
// first held read awaited before the final cursor is encoded, or a
|
|
851
|
+
// mid-discovery registration's emission lands after the cursor and
|
|
852
|
+
// the next run replays it (the gap widened from a scheduling race
|
|
853
|
+
// to a deterministic miss when discovery's digest read added one
|
|
854
|
+
// await before the per-build reads — AUT-545). The snapshot
|
|
855
|
+
// semantics the runner documents remain covered by its own tests.
|
|
856
|
+
drain: 'quiesce',
|
|
857
|
+
})
|
|
858
|
+
runner = remoteRunner
|
|
885
859
|
|
|
886
860
|
const launch = (stream: Stream): void => {
|
|
887
861
|
const key = stream.kind === 'build' ? stream.slug : REPO_STREAM_KEY
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
862
|
+
remoteRunner.launch(
|
|
863
|
+
key,
|
|
864
|
+
(readOpts) =>
|
|
865
|
+
stream.kind === 'build'
|
|
866
|
+
? pollBuild(stream, readOpts)
|
|
867
|
+
: pollRepository(stream, readOpts),
|
|
868
|
+
() => namedAllTerminal(),
|
|
894
869
|
)
|
|
895
870
|
}
|
|
896
871
|
|
|
@@ -901,25 +876,20 @@ export async function abWatch(opts: AbWatchOpts): Promise<void> {
|
|
|
901
876
|
for (const stream of streams.values()) if (!shouldStop) launch(stream)
|
|
902
877
|
|
|
903
878
|
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
|
-
)
|
|
879
|
+
void remoteRunner.runDiscovery({
|
|
880
|
+
step: async (): Promise<void> => {
|
|
881
|
+
await discoverBuilds()
|
|
882
|
+
},
|
|
883
|
+
// Unconditional, after the catch — exactly where the inline
|
|
884
|
+
// launch loop ran: streams registered before a mid-discovery
|
|
885
|
+
// throw still get tasks whose polls then run independently.
|
|
886
|
+
launchPending: (): void => {
|
|
887
|
+
for (const stream of streams.values()) launch(stream)
|
|
888
|
+
},
|
|
889
|
+
})
|
|
920
890
|
}
|
|
921
891
|
|
|
922
|
-
await
|
|
892
|
+
await remoteRunner.drain()
|
|
923
893
|
opts.signal?.removeEventListener('abort', requestStop)
|
|
924
894
|
}
|
|
925
895
|
|
|
@@ -31,6 +31,7 @@ export const CONFIG_RELOAD_CLASSIFICATION = {
|
|
|
31
31
|
source: 'restart',
|
|
32
32
|
readyLabels: 'hot',
|
|
33
33
|
readyState: 'hot',
|
|
34
|
+
readyAssignee: 'hot',
|
|
34
35
|
teamKey: 'restart',
|
|
35
36
|
claimedState: 'restart',
|
|
36
37
|
createState: 'restart',
|
|
@@ -90,6 +91,28 @@ export function restartRequiredChanges(
|
|
|
90
91
|
return changed
|
|
91
92
|
}
|
|
92
93
|
|
|
94
|
+
/** Config sections that define the pipeline a build runs under (SPEC §16.1):
|
|
95
|
+
* the verify universe, finalize steps, deterministic commands, and the
|
|
96
|
+
* workspace provisioning definition. Build-owned sections are composed from
|
|
97
|
+
* the build's own branch at a recorded pipeline-source commit and never
|
|
98
|
+
* replaced by a later base-branch change. */
|
|
99
|
+
export const BUILD_OWNED_CONFIG_PATHS = ['verify', 'finalize', 'commands', 'workspace'] as const
|
|
100
|
+
|
|
101
|
+
/** Compose one build's effective config: build-owned tables come from the
|
|
102
|
+
* pipeline source (the build's own branch), everything else from the live
|
|
103
|
+
* dispatcher snapshot (deployment). Dispatcher-owned sections (baseBranch,
|
|
104
|
+
* capacity, forge, plugins, pr, tickets, orchestrator) always travel with the
|
|
105
|
+
* deployment side — the runner never interprets them. */
|
|
106
|
+
export function composeBuildConfig(pipeline: Config, deployment: Config): Config {
|
|
107
|
+
return {
|
|
108
|
+
...deployment,
|
|
109
|
+
verify: pipeline.verify,
|
|
110
|
+
finalize: pipeline.finalize,
|
|
111
|
+
commands: pipeline.commands,
|
|
112
|
+
workspace: pipeline.workspace,
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
93
116
|
/** Overlay all hot fields while retaining every startup-built adapter field. */
|
|
94
117
|
export function composeReloadedConfig(startup: Config, candidate: Config): Config {
|
|
95
118
|
return {
|