@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
|
@@ -15,6 +15,17 @@ export interface RepositoryStatus extends DispatchSettings {
|
|
|
15
15
|
state: 'live' | 'stopped'
|
|
16
16
|
lastEvidenceAt: string
|
|
17
17
|
}>
|
|
18
|
+
/** PR-only publications from operator sandboxes (AUT-343), newest first,
|
|
19
|
+
* latest per (operator, branch). */
|
|
20
|
+
publications: Array<{
|
|
21
|
+
operator: string
|
|
22
|
+
branch: string
|
|
23
|
+
sha: string
|
|
24
|
+
prNumber: number
|
|
25
|
+
prUrl: string
|
|
26
|
+
session?: string
|
|
27
|
+
at: string
|
|
28
|
+
}>
|
|
18
29
|
}
|
|
19
30
|
|
|
20
31
|
export interface RepositoryStatusOpts {
|
|
@@ -31,6 +42,27 @@ export interface RepositoryStatusOpts {
|
|
|
31
42
|
}
|
|
32
43
|
|
|
33
44
|
export function projectRepositoryStatus(repo: string, events: RepositoryEvent[]): RepositoryStatus {
|
|
45
|
+
const publications: RepositoryStatus['publications'] = []
|
|
46
|
+
for (const event of events) {
|
|
47
|
+
if (event.type !== 'orchestrator.sandbox.published') continue
|
|
48
|
+
const payload = event.payload
|
|
49
|
+
// Latest per (operator, branch): a later fact for the same pair
|
|
50
|
+
// supersedes the earlier one.
|
|
51
|
+
const index = publications.findIndex(
|
|
52
|
+
(entry) => entry.operator === payload.operator && entry.branch === payload.branch,
|
|
53
|
+
)
|
|
54
|
+
if (index !== -1) publications.splice(index, 1)
|
|
55
|
+
publications.push({
|
|
56
|
+
operator: payload.operator,
|
|
57
|
+
branch: payload.branch,
|
|
58
|
+
sha: payload.sha,
|
|
59
|
+
prNumber: payload.pr.number,
|
|
60
|
+
prUrl: payload.pr.url,
|
|
61
|
+
...(payload.session !== undefined ? { session: payload.session } : {}),
|
|
62
|
+
at: event.ts,
|
|
63
|
+
})
|
|
64
|
+
}
|
|
65
|
+
publications.reverse()
|
|
34
66
|
return {
|
|
35
67
|
repo,
|
|
36
68
|
...reduceDispatchSettings(events),
|
|
@@ -43,6 +75,7 @@ export function projectRepositoryStatus(repo: string, events: RepositoryEvent[])
|
|
|
43
75
|
state: state.state === 'stopped' ? ('stopped' as const) : ('live' as const),
|
|
44
76
|
lastEvidenceAt: state.lastEvidenceTs,
|
|
45
77
|
})),
|
|
78
|
+
publications,
|
|
46
79
|
}
|
|
47
80
|
}
|
|
48
81
|
|
|
@@ -57,6 +90,10 @@ export function renderRepositoryStatus(status: RepositoryStatus): string[] {
|
|
|
57
90
|
(sandbox) =>
|
|
58
91
|
`operator sandbox ${sandbox.environmentId} (${sandbox.provider}) — ${sandbox.state}, idle since ${sandbox.lastEvidenceAt}`,
|
|
59
92
|
),
|
|
93
|
+
...status.publications.map(
|
|
94
|
+
(publication) =>
|
|
95
|
+
`publication by ${publication.operator} — ${publication.branch} at ${publication.sha.slice(0, 8)} → PR #${publication.prNumber} (${publication.prUrl})`,
|
|
96
|
+
),
|
|
60
97
|
]
|
|
61
98
|
}
|
|
62
99
|
|
|
@@ -69,7 +106,8 @@ export async function abRepositoryStatus(opts: RepositoryStatusOpts): Promise<vo
|
|
|
69
106
|
// A fresh store has no repository row. Do not call ensureRepo: this query's
|
|
70
107
|
// empty-stream defaults must remain a genuinely read-only operation.
|
|
71
108
|
const record = await store.getRepo(repo)
|
|
72
|
-
|
|
109
|
+
// Bounded read (AUT-489): projectRepositoryStatus reduces durable types only.
|
|
110
|
+
const events = record === null ? [] : await store.getRepoStateEvents(repo)
|
|
73
111
|
const status = projectRepositoryStatus(repo, events)
|
|
74
112
|
if (opts.json === true) {
|
|
75
113
|
opts.stdout(JSON.stringify(status, null, 2))
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { readFile, writeFile } from 'node:fs/promises'
|
|
1
2
|
import semver from 'semver'
|
|
2
3
|
import type { ExecResult } from '../ports/workspace/git-worktree'
|
|
3
4
|
import { registryBaseUrl, registryVersionUrl } from '../registry'
|
|
@@ -230,6 +231,36 @@ export function updateDependency(install: ManagedInstallation, version: string):
|
|
|
230
231
|
: `${install.packageName}@${version}`
|
|
231
232
|
}
|
|
232
233
|
|
|
234
|
+
/** The specifier written into package.json for a resolved version: what
|
|
235
|
+
* `bun add <pkg>@<version>` writes (the exact version) for the registry, the tag for a forge. */
|
|
236
|
+
function manifestSpecifier(install: ManagedInstallation, version: string): string {
|
|
237
|
+
return install.channel === 'github'
|
|
238
|
+
? `github:${install.owner}/${install.repository}#v${version}`
|
|
239
|
+
: version
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** Set the package's specifier in every section that declares it, preserving
|
|
243
|
+
* key order, indentation, and the trailing newline. Returns the new bytes. */
|
|
244
|
+
export function rewriteDeclaredSpecifiers(
|
|
245
|
+
manifestText: string,
|
|
246
|
+
packageName: string,
|
|
247
|
+
specifier: string,
|
|
248
|
+
): string {
|
|
249
|
+
const manifest = JSON.parse(manifestText) as Record<string, unknown>
|
|
250
|
+
for (const section of ['dependencies', 'devDependencies']) {
|
|
251
|
+
const entries = manifest[section]
|
|
252
|
+
if (
|
|
253
|
+
typeof entries === 'object' &&
|
|
254
|
+
entries !== null &&
|
|
255
|
+
typeof (entries as Record<string, unknown>)[packageName] === 'string'
|
|
256
|
+
) {
|
|
257
|
+
;(entries as Record<string, unknown>)[packageName] = specifier
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
const indent = /^([ \t]+)"/m.exec(manifestText)?.[1] ?? 2
|
|
261
|
+
return JSON.stringify(manifest, null, indent) + (manifestText.endsWith('\n') ? '\n' : '')
|
|
262
|
+
}
|
|
263
|
+
|
|
233
264
|
/** Silently determine whether this installation has a newer published release.
|
|
234
265
|
* This read-only courtesy intentionally collapses every unsupported install,
|
|
235
266
|
* command failure, malformed response, and cancellation to no result. */
|
|
@@ -244,7 +275,9 @@ export async function availableRelease(
|
|
|
244
275
|
const identity = await readDistributionIdentity(distRoot)
|
|
245
276
|
if (identity.sourceCheckout || signalAborted(options.signal)) return undefined
|
|
246
277
|
|
|
247
|
-
const globalBin = await command(['bun', 'pm', 'bin', '-g'], {
|
|
278
|
+
const globalBin = await command(['bun', 'pm', 'bin', '-g'], {
|
|
279
|
+
signal: options.signal,
|
|
280
|
+
})
|
|
248
281
|
if (
|
|
249
282
|
globalBin.exitCode !== 0 ||
|
|
250
283
|
globalBin.stdout.trim() === '' ||
|
|
@@ -369,11 +402,53 @@ export async function selfUpdate(options: SelfUpdateOptions): Promise<SelfUpdate
|
|
|
369
402
|
options.stdout(
|
|
370
403
|
`Updating Autobuild to v${resolved} in ${install.ownerRoot}; Bun will rewrite ${install.ownerManifest} and ${install.ownerLock}.`,
|
|
371
404
|
)
|
|
372
|
-
|
|
405
|
+
if (install.dual) {
|
|
406
|
+
// Bun cannot reconcile two differing specifiers through `bun add`: it
|
|
407
|
+
// exits 0 yet keeps the old version. Bring both entries to the new
|
|
408
|
+
// specifier and let `bun install` apply it.
|
|
409
|
+
options.stdout(
|
|
410
|
+
`${install.packageName} is declared in both dependencies and devDependencies; updating both entries.`,
|
|
411
|
+
)
|
|
412
|
+
updateCommand = ['bun', 'install', '--cwd', install.ownerRoot]
|
|
413
|
+
} else {
|
|
414
|
+
updateCommand = [
|
|
415
|
+
'bun',
|
|
416
|
+
'add',
|
|
417
|
+
'--cwd',
|
|
418
|
+
install.ownerRoot,
|
|
419
|
+
...(install.section === 'devDependencies' ? ['-d'] : []),
|
|
420
|
+
dependency,
|
|
421
|
+
]
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
let originalManifest: string | undefined
|
|
426
|
+
if (install.scope === 'local' && install.dual) {
|
|
427
|
+
try {
|
|
428
|
+
originalManifest = await readFile(install.ownerManifest, 'utf8')
|
|
429
|
+
await writeFile(
|
|
430
|
+
install.ownerManifest,
|
|
431
|
+
rewriteDeclaredSpecifiers(
|
|
432
|
+
originalManifest,
|
|
433
|
+
install.packageName,
|
|
434
|
+
manifestSpecifier(install, resolved),
|
|
435
|
+
),
|
|
436
|
+
)
|
|
437
|
+
} catch (error) {
|
|
438
|
+
const reason = `rewriting ${install.ownerManifest}: ${error instanceof Error ? error.message : String(error)}`
|
|
439
|
+
if (explicit) {
|
|
440
|
+
options.stderr(`self-update failed: ${reason}`)
|
|
441
|
+
return { kind: 'failed' }
|
|
442
|
+
}
|
|
443
|
+
return warn(options, reason)
|
|
444
|
+
}
|
|
373
445
|
}
|
|
374
446
|
|
|
375
447
|
const update = await invoke(updateCommand, {})
|
|
376
448
|
if (update.exitCode !== 0) {
|
|
449
|
+
if (originalManifest !== undefined) {
|
|
450
|
+
await writeFile(install.ownerManifest, originalManifest).catch(() => undefined)
|
|
451
|
+
}
|
|
377
452
|
const reason = commandFailure(`installing v${resolved}`, update)
|
|
378
453
|
if (explicit) {
|
|
379
454
|
options.stderr(`self-update failed: ${reason}`)
|
|
@@ -39,10 +39,13 @@ import { loadConfig } from '../config/load'
|
|
|
39
39
|
import type { Config } from '../config/schema'
|
|
40
40
|
import {
|
|
41
41
|
BUILD_EFFECTIVE_CONFIG_ARTIFACT,
|
|
42
|
+
parseBuildConfigMetadata,
|
|
42
43
|
parseEffectiveBuildConfig,
|
|
43
44
|
} from '../processes/build-execution-state'
|
|
45
|
+
import type { PipelineSourceMeta } from '../config/pipeline-source'
|
|
44
46
|
import { openExecution } from '../processes/execution-settlement'
|
|
45
47
|
import { currentAutoMergeDeferral } from '../kernel/auto-merge'
|
|
48
|
+
import { autoMergeDisplay, type AutoMergeDisplay } from './dashboard/model'
|
|
46
49
|
import { decideNext } from '../kernel/engine'
|
|
47
50
|
import {
|
|
48
51
|
reduceBuild,
|
|
@@ -64,6 +67,7 @@ import type { BuildRecord, BuildStore, StreamRecord, StreamScope } from '../stor
|
|
|
64
67
|
import { buildProgress, isDiverged, type BuildProgress } from './build-progress'
|
|
65
68
|
import { normalizeGitRemoteUrl } from '../kernel/origin'
|
|
66
69
|
import { buildInRepository, resolveMainRepo } from './repo-state'
|
|
70
|
+
import { writeAssetTo } from './ticket-assets'
|
|
67
71
|
import { withAmbientReadStore, type StoreOpener } from './store-opening'
|
|
68
72
|
|
|
69
73
|
/** Backward-compatible name for callers/tests; repository resolution is shared. */
|
|
@@ -135,6 +139,10 @@ export interface BuildDetail extends BuildSummary {
|
|
|
135
139
|
/** Current work-owner decision. Open PRs use the build-owned config; ended
|
|
136
140
|
* PRs project the repository completion path directly from durable state. */
|
|
137
141
|
decision?: BuildDecisionProjection
|
|
142
|
+
/** The build's effective auto-merge display state (off, requested, enabled,
|
|
143
|
+
* cancelling) — the same projection the dashboards render, so `--json` and
|
|
144
|
+
* the text agree and `waiting: on PR` can say why it is waiting. */
|
|
145
|
+
autoMerge: AutoMergeDisplay
|
|
138
146
|
openEscalations: OpenEscalation[]
|
|
139
147
|
/** Per-loop review overrides for the current spec; omitted when none are set. */
|
|
140
148
|
reviewRoundCeilings?: { plan?: number; code?: number }
|
|
@@ -155,6 +163,11 @@ export interface BuildDetail extends BuildSummary {
|
|
|
155
163
|
lastEvent?: { type: string; seq: number; ts: string; actor: Actor }
|
|
156
164
|
/** Present only with `--events <n>`: the newest n, chronological. */
|
|
157
165
|
events?: AbEvent[]
|
|
166
|
+
/** Which `autobuild.toml` the build's pinned pipeline was read from, and the
|
|
167
|
+
* effective-config artifact revision it was deposited under (SPEC §16.1).
|
|
168
|
+
* Absent when no effective-config artifact has been deposited. */
|
|
169
|
+
pipelineSource?: PipelineSourceMeta
|
|
170
|
+
effectiveConfigRev?: number
|
|
158
171
|
outcome?: BuildOutcome
|
|
159
172
|
}
|
|
160
173
|
|
|
@@ -192,7 +205,7 @@ function leaseInfo(record: BuildRecord, now: Date): LeaseInfo {
|
|
|
192
205
|
* The summary over an ALREADY-reduced state — so `detail` reduces the log
|
|
193
206
|
* once and builds both projections from it, rather than reducing twice.
|
|
194
207
|
*/
|
|
195
|
-
function summarizeFrom(record: BuildRecord, state: BuildState, now: Date): BuildSummary {
|
|
208
|
+
export function summarizeFrom(record: BuildRecord, state: BuildState, now: Date): BuildSummary {
|
|
196
209
|
// `phase`, `round`, and `attempt` describe ONE phase occurrence, so they come
|
|
197
210
|
// from ONE source: the in-flight phase, else the last completed one — the
|
|
198
211
|
// same `currentPhase ?? lastCompletedPhase` the reducer uses to derive
|
|
@@ -287,6 +300,7 @@ export function detail(
|
|
|
287
300
|
}))
|
|
288
301
|
return {
|
|
289
302
|
...summary,
|
|
303
|
+
autoMerge: autoMergeDisplay(state),
|
|
290
304
|
...(decision !== undefined ? { decision } : {}),
|
|
291
305
|
openEscalations: state.openEscalations,
|
|
292
306
|
...(state.reviewRoundCeilings.plan !== undefined || state.reviewRoundCeilings.code !== undefined
|
|
@@ -493,6 +507,16 @@ export function renderDetail(d: BuildDetail, now: Date): string[] {
|
|
|
493
507
|
lines.push(` review round ceiling: ${ceilings.join(', ')}`)
|
|
494
508
|
}
|
|
495
509
|
lines.push(` updated: ${d.updatedAt} (${relativeTime(d.updatedAt, now)})`)
|
|
510
|
+
if (d.pipelineSource !== undefined || d.effectiveConfigRev !== undefined) {
|
|
511
|
+
const source =
|
|
512
|
+
d.pipelineSource !== undefined
|
|
513
|
+
? `autobuild.toml@${
|
|
514
|
+
d.pipelineSource.commit !== undefined ? d.pipelineSource.commit.slice(0, 7) : 'unknown'
|
|
515
|
+
} (${d.pipelineSource.ref})`
|
|
516
|
+
: 'autobuild.toml@unknown (pre-pin)'
|
|
517
|
+
const rev = d.effectiveConfigRev !== undefined ? ` config rev ${d.effectiveConfigRev}` : ''
|
|
518
|
+
lines.push(` pipeline: ${source}${rev}`)
|
|
519
|
+
}
|
|
496
520
|
|
|
497
521
|
// Its own line, never folded into status: a running build with an expired
|
|
498
522
|
// lease is the case this exists to make visible.
|
|
@@ -548,9 +572,11 @@ export function renderDetail(d: BuildDetail, now: Date): string[] {
|
|
|
548
572
|
)
|
|
549
573
|
}
|
|
550
574
|
if (d.decision?.kind === 'awaiting-pr') {
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
575
|
+
// The deferral reason (a durable observation about WHY consent could not
|
|
576
|
+
// be applied) takes precedence; otherwise the auto-merge state itself is
|
|
577
|
+
// what the parked operator needs — off names the missing consent.
|
|
578
|
+
const consent = d.decision.reason ?? `auto merge ${d.autoMerge}`
|
|
579
|
+
lines.push(` waiting: on PR — ${consent}`)
|
|
554
580
|
} else if (d.decision?.kind === 'repository-completion') {
|
|
555
581
|
lines.push(
|
|
556
582
|
` waiting: repository-level completion after the PR was ${d.decision.prState}; no runner re-attachment is pending`,
|
|
@@ -703,9 +729,43 @@ export async function abBuilds(opts: AbBuildsOpts): Promise<void> {
|
|
|
703
729
|
const wanted = new Set(statusFilter(opts.all, opts.queued))
|
|
704
730
|
// Cross-repo aggregation is out of scope (§12: one dispatcher per repo,
|
|
705
731
|
// one repo's builds per answer).
|
|
732
|
+
// Records are read and held before the digest read: builds are never
|
|
733
|
+
// deleted, so every identity-keyed record iterated below is guaranteed an
|
|
734
|
+
// entry in the digest map fetched after it — a build created between the
|
|
735
|
+
// two reads appears only in the digest map, which is harmless — and the
|
|
736
|
+
// completeness check can only ever catch a genuine adapter bug, never a
|
|
737
|
+
// concurrent dispatch (AUT-488 finding f_3cb67aba).
|
|
706
738
|
const records = (await store.listBuilds()).filter(mine)
|
|
739
|
+
// One digest read gates the per-build history reads (AUT-488): the active
|
|
740
|
+
// and queued scopes never show a terminal build, so an identity-keyed
|
|
741
|
+
// build whose digest already carries a terminal fact skips its `getEvents`
|
|
742
|
+
// round trip and the listing's store cost stays flat as finished builds
|
|
743
|
+
// accumulate. The `--all` scope includes terminal statuses, so it takes no
|
|
744
|
+
// digest read and skips nothing — byte-for-byte the old loop.
|
|
745
|
+
const digests = opts.all === true ? undefined : await store.getRepoBuildDigests(identity)
|
|
707
746
|
const summaries: BuildSummary[] = []
|
|
708
747
|
for (const record of records) {
|
|
748
|
+
if (digests !== undefined && record.repo === identity) {
|
|
749
|
+
// Completeness is contractual (one entry per repo build); a missing
|
|
750
|
+
// entry is an adapter bug and must fail loudly rather than silently
|
|
751
|
+
// drop the build from the listing.
|
|
752
|
+
const digest = digests.get(record.slug)
|
|
753
|
+
if (digest === undefined) {
|
|
754
|
+
throw new Error(`getRepoBuildDigests is missing an entry for build "${record.slug}"`)
|
|
755
|
+
}
|
|
756
|
+
// The digest's `terminal` follows `reduceBuild`'s terminal rule
|
|
757
|
+
// exactly (in-order overwrite of `build.completed`/`build.aborted`,
|
|
758
|
+
// never cleared — pinned against `reduceBuild` in
|
|
759
|
+
// store/digest.test.ts), so `terminal !== undefined` is exactly
|
|
760
|
+
// reduced status `done`/`aborted`, which the active and queued status
|
|
761
|
+
// sets never include. Skipping the history read therefore cannot
|
|
762
|
+
// change the emitted summaries.
|
|
763
|
+
if (digest.terminal !== undefined) continue
|
|
764
|
+
}
|
|
765
|
+
// A record matched only through the legacy `repoOrigin` path has no
|
|
766
|
+
// entry under `identity` — `getRepoBuildDigests` groups by `record.repo`
|
|
767
|
+
// exactly — so it keeps its per-build history read (correct, just not
|
|
768
|
+
// accelerated).
|
|
709
769
|
const summary = summarize(record, await store.getEvents(record.slug), now)
|
|
710
770
|
if (wanted.has(summary.status)) summaries.push(summary)
|
|
711
771
|
}
|
|
@@ -830,6 +890,40 @@ async function projectBuildDecision(
|
|
|
830
890
|
}
|
|
831
891
|
}
|
|
832
892
|
|
|
893
|
+
type AmbientReadContext = Parameters<Parameters<typeof withAmbientReadStore>[1]>[0]
|
|
894
|
+
|
|
895
|
+
/** The build record, after the repository gate every single-build read shares
|
|
896
|
+
* (see `abBuildStatus`). */
|
|
897
|
+
async function requireReadableBuild(
|
|
898
|
+
opts: AbBuildStatusOpts,
|
|
899
|
+
context: AmbientReadContext,
|
|
900
|
+
): Promise<BuildRecord> {
|
|
901
|
+
const record = await context.store.getBuild(opts.slug)
|
|
902
|
+
if (record === null) {
|
|
903
|
+
throw new Error(
|
|
904
|
+
`no build "${opts.slug}" in this store — run 'ab builds --all' to list ` +
|
|
905
|
+
"this repo's builds, or pass --store <ref> if it lives in another store",
|
|
906
|
+
)
|
|
907
|
+
}
|
|
908
|
+
// Own-session authority: slug equality bounds the read to the session's
|
|
909
|
+
// own build, and an explicit --store must name the session's own store for
|
|
910
|
+
// the bypass to apply. Every other combination falls through to the
|
|
911
|
+
// ordinary repository gate, unchanged — which now keys on the checkout's
|
|
912
|
+
// normalized origin identity (`context.checkout` is the physical path;
|
|
913
|
+
// `buildInRepository` derives the identity from it).
|
|
914
|
+
const ownSession =
|
|
915
|
+
context.ambient !== undefined &&
|
|
916
|
+
'build' in context.ambient &&
|
|
917
|
+
context.ambient.build === opts.slug &&
|
|
918
|
+
(opts.storeRef === undefined || opts.storeRef === context.ambient.store)
|
|
919
|
+
if (!(ownSession || (await buildInRepository(record, context.checkout, opts.exec)))) {
|
|
920
|
+
throw new Error(
|
|
921
|
+
`build "${opts.slug}" belongs to repository "${record.repo}", not "${context.repo}"`,
|
|
922
|
+
)
|
|
923
|
+
}
|
|
924
|
+
return record
|
|
925
|
+
}
|
|
926
|
+
|
|
833
927
|
/** `ab build status <slug>` — one build in detail. Read-only.
|
|
834
928
|
*
|
|
835
929
|
* Repository authority mirrors the other ambient reads: an operator invocation
|
|
@@ -849,29 +943,7 @@ async function projectBuildDecision(
|
|
|
849
943
|
export async function abBuildStatus(opts: AbBuildStatusOpts): Promise<void> {
|
|
850
944
|
const now = (opts.now ?? (() => new Date()))()
|
|
851
945
|
await withAmbientReadStore(opts, async (context) => {
|
|
852
|
-
const record = await
|
|
853
|
-
if (record === null) {
|
|
854
|
-
throw new Error(
|
|
855
|
-
`no build "${opts.slug}" in this store — run 'ab builds --all' to list ` +
|
|
856
|
-
"this repo's builds, or pass --store <ref> if it lives in another store",
|
|
857
|
-
)
|
|
858
|
-
}
|
|
859
|
-
// Own-session authority: slug equality bounds the read to the session's
|
|
860
|
-
// own build, and an explicit --store must name the session's own store for
|
|
861
|
-
// the bypass to apply. Every other combination falls through to the
|
|
862
|
-
// ordinary repository gate, unchanged — which now keys on the checkout's
|
|
863
|
-
// normalized origin identity (`context.checkout` is the physical path;
|
|
864
|
-
// `buildInRepository` derives the identity from it).
|
|
865
|
-
const ownSession =
|
|
866
|
-
context.ambient !== undefined &&
|
|
867
|
-
'build' in context.ambient &&
|
|
868
|
-
context.ambient.build === opts.slug &&
|
|
869
|
-
(opts.storeRef === undefined || opts.storeRef === context.ambient.store)
|
|
870
|
-
if (!(ownSession || (await buildInRepository(record, context.checkout, opts.exec)))) {
|
|
871
|
-
throw new Error(
|
|
872
|
-
`build "${opts.slug}" belongs to repository "${record.repo}", not "${context.repo}"`,
|
|
873
|
-
)
|
|
874
|
-
}
|
|
946
|
+
const record = await requireReadableBuild(opts, context)
|
|
875
947
|
const events = await context.store.getEvents(opts.slug)
|
|
876
948
|
const decision = await projectBuildDecision(opts.slug, events, context.store)
|
|
877
949
|
// Stream enrichment is presentation (SPEC §9): an unavailable read
|
|
@@ -885,7 +957,22 @@ export async function abBuildStatus(opts: AbBuildStatusOpts): Promise<void> {
|
|
|
885
957
|
} catch {
|
|
886
958
|
streams = undefined
|
|
887
959
|
}
|
|
888
|
-
const
|
|
960
|
+
const projected = detail(record, events, now, opts.events, decision, streams)
|
|
961
|
+
// Operator visibility (SPEC §16.1): which autobuild.toml the build's
|
|
962
|
+
// pinned pipeline came from, alongside the effective-config revision.
|
|
963
|
+
const configArtifact = await context.store.getArtifact(
|
|
964
|
+
opts.slug,
|
|
965
|
+
BUILD_EFFECTIVE_CONFIG_ARTIFACT,
|
|
966
|
+
)
|
|
967
|
+
const configMeta =
|
|
968
|
+
configArtifact !== null ? parseBuildConfigMetadata(configArtifact) : undefined
|
|
969
|
+
const d: BuildDetail = {
|
|
970
|
+
...projected,
|
|
971
|
+
...(configMeta?.revision !== undefined ? { effectiveConfigRev: configMeta.revision } : {}),
|
|
972
|
+
...(configMeta?.pipelineSource !== undefined
|
|
973
|
+
? { pipelineSource: configMeta.pipelineSource }
|
|
974
|
+
: {}),
|
|
975
|
+
}
|
|
889
976
|
if (opts.json === true) {
|
|
890
977
|
opts.stdout(JSON.stringify(d, null, 2))
|
|
891
978
|
return
|
|
@@ -893,3 +980,38 @@ export async function abBuildStatus(opts: AbBuildStatusOpts): Promise<void> {
|
|
|
893
980
|
for (const line of renderDetail(d, now)) opts.stdout(line)
|
|
894
981
|
})
|
|
895
982
|
}
|
|
983
|
+
|
|
984
|
+
export interface AbBuildAssetGetOpts extends AbBuildStatusOpts {
|
|
985
|
+
kind: string
|
|
986
|
+
name: string
|
|
987
|
+
dest: string
|
|
988
|
+
rev?: number
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
/** `ab build asset get <slug> <kind> <name> <dest>` — the exact bytes a build
|
|
992
|
+
* froze at its claim (SPEC §6.3), available after the build has finished. */
|
|
993
|
+
export async function abBuildAssetGet(opts: AbBuildAssetGetOpts): Promise<void> {
|
|
994
|
+
await withAmbientReadStore(opts, async (context) => {
|
|
995
|
+
await requireReadableBuild(opts, context)
|
|
996
|
+
const asset = await context.store.getPinnedTicketAsset(
|
|
997
|
+
opts.slug,
|
|
998
|
+
opts.kind,
|
|
999
|
+
opts.name,
|
|
1000
|
+
opts.rev,
|
|
1001
|
+
)
|
|
1002
|
+
if (asset === null) {
|
|
1003
|
+
const revision = opts.rev === undefined ? '' : ` revision ${opts.rev}`
|
|
1004
|
+
throw new Error(
|
|
1005
|
+
`build "${opts.slug}" pinned no ticket asset ${opts.kind}/${opts.name}${revision}`,
|
|
1006
|
+
)
|
|
1007
|
+
}
|
|
1008
|
+
const written = await writeAssetTo(asset, opts.dest)
|
|
1009
|
+
if (opts.json === true) {
|
|
1010
|
+
opts.stdout(JSON.stringify({ ...asset.meta, path: written }, null, 2))
|
|
1011
|
+
return
|
|
1012
|
+
}
|
|
1013
|
+
opts.stdout(
|
|
1014
|
+
`build asset downloaded: ${opts.slug} ${opts.kind}/${opts.name} revision ${asset.meta.revision} → ${written}`,
|
|
1015
|
+
)
|
|
1016
|
+
})
|
|
1017
|
+
}
|
|
@@ -20,6 +20,7 @@ import { z } from 'zod'
|
|
|
20
20
|
import { loadConfig } from '../config/load'
|
|
21
21
|
import type { AbEvent, EventEnvelope } from '../events/catalog'
|
|
22
22
|
import { agentActor, KERNEL } from '../events/envelope'
|
|
23
|
+
import { isRemoteWorkspace } from '../events/workspace-remote'
|
|
23
24
|
import type { IdSource } from '../ids'
|
|
24
25
|
import {
|
|
25
26
|
autoMergeApplicationType,
|
|
@@ -258,7 +259,7 @@ function requireImplementationProvisioning(events: AbEvent[]): void {
|
|
|
258
259
|
function remotePublication(events: readonly AbEvent[]): boolean {
|
|
259
260
|
let remote = false
|
|
260
261
|
for (const event of events) {
|
|
261
|
-
if (event.type === 'workspace.provisioned') remote = event.payload
|
|
262
|
+
if (event.type === 'workspace.provisioned') remote = isRemoteWorkspace(event.payload)
|
|
262
263
|
else if (event.type === 'workspace.released') remote = false
|
|
263
264
|
}
|
|
264
265
|
return remote
|