@defrex/autobuild 0.12.0 → 0.14.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 +138 -16
- package/bin/ab-dispatch-kernel.ts +4 -6
- package/docs/architecture.md +155 -73
- package/docs/configuration.md +41 -9
- package/docs/hosted-service.md +31 -5
- package/docs/operator-api.md +8 -1
- package/docs/remote-store-protocol.md +94 -2
- package/docs/remote-ticket-protocol.md +151 -9
- package/docs/setup.md +33 -7
- package/docs/ticket-migration.md +64 -0
- package/package.json +4 -3
- package/packages/core/src/cli/attention.ts +18 -164
- package/packages/core/src/cli/build-control.ts +23 -16
- package/packages/core/src/cli/bulk-control.ts +3 -2
- package/packages/core/src/cli/connect.ts +141 -38
- package/packages/core/src/cli/context.ts +23 -79
- package/packages/core/src/cli/dashboard/model.ts +36 -22
- package/packages/core/src/cli/dashboard/poll.ts +1 -1
- package/packages/core/src/cli/dispatch-frontend.ts +24 -67
- package/packages/core/src/cli/dispatch-process.ts +14 -7
- package/packages/core/src/cli/dispatch-repository-feed.ts +196 -0
- package/packages/core/src/cli/dispatch.ts +304 -82
- package/packages/core/src/cli/dispatcher-liveness.ts +26 -19
- package/packages/core/src/cli/harvest.ts +84 -42
- package/packages/core/src/cli/help.ts +14 -8
- package/packages/core/src/cli/historical-build-state.ts +27 -0
- package/packages/core/src/cli/installation.ts +67 -13
- package/packages/core/src/cli/main.ts +23 -5
- package/packages/core/src/cli/mcp.ts +2 -1
- package/packages/core/src/cli/pr-attachments.ts +10 -13
- package/packages/core/src/cli/pr-summary-publication.ts +27 -60
- package/packages/core/src/cli/pr-summary.ts +43 -13
- package/packages/core/src/cli/publication-settlement.ts +10 -20
- package/packages/core/src/cli/repository-status.ts +38 -2
- package/packages/core/src/cli/self-update.ts +5 -1
- package/packages/core/src/cli/status.ts +62 -32
- package/packages/core/src/cli/terminals.ts +25 -142
- package/packages/core/src/cli/ticket-assets.ts +7 -2
- package/packages/core/src/cli/ticket-editor.ts +82 -0
- package/packages/core/src/cli/ticket.ts +166 -36
- package/packages/core/src/cli/wait.ts +91 -84
- package/packages/core/src/cli/watch.ts +60 -97
- package/packages/core/src/config/pipeline-source.ts +4 -3
- package/packages/core/src/config/schema.ts +35 -3
- package/packages/core/src/connection.ts +3 -3
- package/packages/core/src/events/catalog.ts +3 -2
- package/packages/core/src/events/payloads.ts +6 -0
- package/packages/core/src/events/repository.ts +7 -3
- package/packages/core/src/integration/harness.ts +38 -4
- package/packages/core/src/kernel/__snapshots__/reducer-registry.test.ts.snap +2444 -9
- package/packages/core/src/kernel/auto-merge.ts +19 -3
- package/packages/core/src/kernel/dispatcher-liveness.ts +10 -4
- package/packages/core/src/kernel/generators/build-log.ts +1 -1
- package/packages/core/src/kernel/generators/repository-journals.ts +14 -3
- package/packages/core/src/kernel/harvest.ts +25 -23
- package/packages/core/src/kernel/incremental-fixtures.ts +13 -2
- package/packages/core/src/kernel/log-index-value.ts +38 -0
- package/packages/core/src/kernel/read-facts.ts +637 -0
- package/packages/core/src/kernel/reducer-registry.ts +139 -3
- package/packages/core/src/kernel/repository-attention.ts +179 -0
- package/packages/core/src/kernel/service-facts.ts +135 -0
- package/packages/core/src/kernel/terminal-guard.ts +133 -0
- package/packages/core/src/ontology.ts +1 -0
- package/packages/core/src/operator/annotations.ts +12 -0
- package/packages/core/src/operator/control.ts +5 -6
- package/packages/core/src/operator/index.ts +7 -0
- package/packages/core/src/operator/protocol.ts +19 -2
- package/packages/core/src/operator/query.ts +37 -20
- package/packages/core/src/operator/read-view.ts +4 -13
- package/packages/core/src/operator/registry.ts +61 -8
- package/packages/core/src/operator/requests.ts +2 -14
- package/packages/core/src/operator/sandbox.ts +10 -9
- package/packages/core/src/operator/ticket-source-backend.ts +17 -21
- package/packages/core/src/operator/tickets.ts +132 -22
- package/packages/core/src/plugin-sdk/index.ts +19 -0
- package/packages/core/src/plugins/runtimes.ts +5 -1
- package/packages/core/src/ports/runner/session-env.ts +2 -0
- package/packages/core/src/ports/tickets/contract.ts +66 -1
- package/packages/core/src/ports/tickets/create.ts +48 -12
- package/packages/core/src/ports/tickets/fake.ts +30 -1
- package/packages/core/src/ports/tickets/file-assets.ts +159 -0
- package/packages/core/src/ports/tickets/file-codec.ts +90 -0
- package/packages/core/src/ports/tickets/file.ts +114 -120
- package/packages/core/src/ports/tickets/grants.ts +47 -0
- package/packages/core/src/ports/tickets/hosted-index.ts +17 -0
- package/packages/core/src/ports/tickets/identity.ts +20 -0
- package/packages/core/src/ports/tickets/lifecycle.ts +18 -0
- package/packages/core/src/ports/tickets/linear.ts +74 -9
- package/packages/core/src/ports/tickets/migration.ts +336 -0
- package/packages/core/src/ports/tickets/operations.ts +38 -7
- package/packages/core/src/ports/tickets/portable-assets.ts +54 -0
- package/packages/core/src/ports/tickets/remote-protocol.ts +95 -0
- package/packages/core/src/ports/tickets/remote.ts +60 -3
- package/packages/core/src/ports/tickets/requested-id.ts +46 -0
- package/packages/core/src/ports/tickets/revision.ts +62 -0
- package/packages/core/src/ports/tickets/states.ts +61 -0
- package/packages/core/src/ports/types.ts +40 -3
- package/packages/core/src/ports/workspace/build-execution.ts +1 -0
- package/packages/core/src/ports/workspace/create.ts +5 -1
- package/packages/core/src/ports/workspace/operator-sandbox.ts +2 -0
- package/packages/core/src/ports/workspace/vercel-sandbox.ts +8 -4
- package/packages/core/src/processes/build-child.ts +6 -6
- package/packages/core/src/processes/build-runner.ts +62 -70
- package/packages/core/src/processes/dispatch-failure-budget.ts +5 -4
- package/packages/core/src/processes/dispatcher.ts +223 -205
- package/packages/core/src/processes/execution-no-progress.ts +176 -0
- package/packages/core/src/processes/harvest-child.ts +30 -4
- package/packages/core/src/processes/harvest-execution-state.ts +15 -9
- package/packages/core/src/processes/harvest-runner.ts +58 -74
- package/packages/core/src/processes/harvest.ts +72 -29
- package/packages/core/src/processes/infrastructure-failure-budget.ts +13 -15
- package/packages/core/src/processes/orchestrator-tick.ts +2 -2
- package/packages/core/src/processes/phase-failure-budget.ts +1 -0
- package/packages/core/src/processes/publication-loss.ts +35 -21
- package/packages/core/src/processes/repo-view.ts +229 -1099
- package/packages/core/src/processes/tick-publication.ts +110 -0
- package/packages/core/src/store/adapter.ts +23 -0
- package/packages/core/src/store/build-scope.ts +17 -0
- package/packages/core/src/store/contract.ts +283 -1
- package/packages/core/src/store/local/schema.ts +1 -0
- package/packages/core/src/store/local/store.ts +180 -55
- package/packages/core/src/store/memory.ts +119 -29
- package/packages/core/src/store/phase-session.ts +17 -0
- package/packages/core/src/store/reduced-reads.ts +239 -0
- package/packages/core/src/store/reducer-value-shapes.json +14056 -0
- package/packages/core/src/store/reducer-value-validation.ts +96 -0
- package/packages/core/src/store/reducer-values.ts +145 -0
- package/packages/core/src/store/remote/client.ts +41 -0
- package/packages/core/src/store/remote/compatibility.ts +2 -0
- package/packages/core/src/store/remote/index.ts +19 -0
- package/packages/core/src/store/remote/protocol.ts +16 -0
- package/packages/core/src/store/remote/version.ts +3 -1
- package/packages/core/src/store/repo-state-events.ts +1 -1
- package/packages/core/src/store/repo-tick.ts +72 -0
- package/packages/core/src/store/session-handle.ts +19 -0
- package/packages/core/src/store/snapshots.ts +10 -0
- package/packages/core/src/store/ticket-asset-snapshots.ts +192 -0
- package/packages/core/src/store/ticket-assets.ts +86 -8
- package/packages/core/src/store/types.ts +40 -3
- package/packages/core/src/testing/dispatch-read-budget.ts +215 -0
- package/packages/core/src/testing/index.ts +8 -0
- package/packages/core/src/testing/reduced-read-budget.ts +462 -0
- package/skills/guide/SKILL.md +97 -12
- package/skills/guide/references/operator-api.md +8 -1
- package/skills/guide/references/remote-store-protocol.md +94 -2
- package/skills/guide/references/remote-ticket-protocol.md +151 -9
- package/skills/guide/references/setup.md +33 -7
- package/skills/guide/references/ticket-migration.md +64 -0
- package/skills/tickets/SKILL.md +71 -0
- package/templates/hosted-service/package.json +2 -2
|
@@ -1,11 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readBuildFacts, type CurrentBuildFacts } from '../store/reduced-reads'
|
|
2
2
|
export { summaryBoundary, pendingSummaryRequests } from '../kernel/pr-summary'
|
|
3
3
|
import type { AbEvent } from '../events/catalog'
|
|
4
4
|
import { KERNEL } from '../events/envelope'
|
|
5
5
|
import { isRemoteWorkspace } from '../events/workspace-remote'
|
|
6
|
-
import { reduceBuild } from '../kernel/reducer'
|
|
7
6
|
import { currentPrAttachments } from '../kernel/pr-attachments'
|
|
8
|
-
import { openExecution } from '../processes/execution-settlement'
|
|
9
7
|
import type { Forge } from '../ports/types'
|
|
10
8
|
import { githubRateLimitUntil } from '../ports/forge/github-transport'
|
|
11
9
|
import { summaryMarker } from '../ports/forge/summary-marker'
|
|
@@ -27,7 +25,7 @@ interface SummaryDeps {
|
|
|
27
25
|
* is deliberately not designated, avoiding recursive growth. */
|
|
28
26
|
export async function boundedSummary(
|
|
29
27
|
deps: SummaryDeps,
|
|
30
|
-
events: readonly AbEvent[],
|
|
28
|
+
events: readonly AbEvent[] | CurrentBuildFacts,
|
|
31
29
|
): Promise<string> {
|
|
32
30
|
const env = {
|
|
33
31
|
build: deps.slug,
|
|
@@ -48,7 +46,7 @@ export async function boundedSummary(
|
|
|
48
46
|
summaryMarker(deps.slug),
|
|
49
47
|
`## Autobuild: ${html(deps.slug)}`,
|
|
50
48
|
'',
|
|
51
|
-
`The full summary exceeds the comment limit. It includes ${currentPrAttachments(events).length} PR attachments.`,
|
|
49
|
+
`The full summary exceeds the comment limit. It includes ${(Array.isArray(events) ? currentPrAttachments(events) : (events as CurrentBuildFacts).designations).length} PR attachments.`,
|
|
52
50
|
`Store: <code>${html(deps.storeRef)}</code>`,
|
|
53
51
|
'',
|
|
54
52
|
'Download the complete summary, including all pinned artifact commands and hosted links:',
|
|
@@ -65,26 +63,16 @@ export async function publishSummary(
|
|
|
65
63
|
boundary: { occurrence: number; boundary: number },
|
|
66
64
|
): Promise<'attempted' | 'skipped' | 'deferred'> {
|
|
67
65
|
try {
|
|
68
|
-
const events = await deps.store
|
|
69
|
-
const pr =
|
|
66
|
+
const events = (await readBuildFacts(deps.store, deps.slug)).value
|
|
67
|
+
const pr = events.state.pr
|
|
70
68
|
if (pr === undefined) return 'skipped'
|
|
69
|
+
if (events.summaryAttempts.includes(boundary.occurrence)) return 'attempted'
|
|
71
70
|
if (
|
|
72
|
-
events.
|
|
73
|
-
|
|
74
|
-
event.type === 'pr-summary.attempted' && event.payload.occurrence === boundary.occurrence,
|
|
75
|
-
)
|
|
76
|
-
)
|
|
77
|
-
return 'attempted'
|
|
78
|
-
if (
|
|
79
|
-
events.some(
|
|
80
|
-
(event) =>
|
|
81
|
-
event.type === 'pr-summary.result' &&
|
|
82
|
-
event.payload.eligibleAfter !== undefined &&
|
|
83
|
-
Date.parse(event.payload.eligibleAfter) > deps.clock().getTime(),
|
|
84
|
-
)
|
|
71
|
+
events.summaryEligibleAfter !== undefined &&
|
|
72
|
+
Date.parse(events.summaryEligibleAfter) > deps.clock().getTime()
|
|
85
73
|
)
|
|
86
74
|
return 'deferred'
|
|
87
|
-
const claim = await deps.store.appendIfCurrent(deps.slug, events.
|
|
75
|
+
const claim = await deps.store.appendIfCurrent(deps.slug, events.state.lastSeq, {
|
|
88
76
|
actor: KERNEL,
|
|
89
77
|
type: 'pr-summary.attempted',
|
|
90
78
|
payload: { ...boundary, pr: pr.number },
|
|
@@ -93,7 +81,7 @@ export async function publishSummary(
|
|
|
93
81
|
let outcome: 'success' | 'failure' | 'rate-limited' = 'success'
|
|
94
82
|
let eligibleAfter: string | undefined
|
|
95
83
|
try {
|
|
96
|
-
const body = await boundedSummary(deps, await deps.store
|
|
84
|
+
const body = await boundedSummary(deps, (await readBuildFacts(deps.store, deps.slug)).value)
|
|
97
85
|
await deps.forge.commentOnPr(deps.workspacePath, pr.number, body)
|
|
98
86
|
} catch (error) {
|
|
99
87
|
eligibleAfter = githubRateLimitUntil(error, deps.clock().getTime())
|
|
@@ -118,45 +106,24 @@ export async function publishSummary(
|
|
|
118
106
|
* prevent pipeline progress, including a failed request deposit. */
|
|
119
107
|
export async function refreshSummaryBoundary(deps: SummaryDeps): Promise<boolean> {
|
|
120
108
|
try {
|
|
121
|
-
const events = await deps.store
|
|
122
|
-
const boundary = summaryBoundary
|
|
109
|
+
const events = (await readBuildFacts(deps.store, deps.slug)).value
|
|
110
|
+
const boundary = events.summaryBoundary
|
|
123
111
|
if (boundary === undefined) return false
|
|
124
|
-
const workspace = events.
|
|
112
|
+
const workspace = events.latestWorkspace
|
|
125
113
|
if (workspace?.type === 'workspace.provisioned' && isRemoteWorkspace(workspace.payload)) {
|
|
126
|
-
const request = events.
|
|
127
|
-
(event) =>
|
|
128
|
-
event.type === 'pr-summary.requested' && event.payload.occurrence === boundary.occurrence,
|
|
129
|
-
)
|
|
114
|
+
const request = events.summaryRequested[String(boundary.occurrence)]
|
|
130
115
|
if (request !== undefined) {
|
|
131
116
|
// A lost result/disposition append must not repeatedly park a guest.
|
|
117
|
+
if (events.summaryAttempts.includes(boundary.occurrence)) return false
|
|
132
118
|
if (
|
|
133
|
-
events.
|
|
134
|
-
|
|
135
|
-
event.type === 'pr-summary.attempted' &&
|
|
136
|
-
event.payload.occurrence === boundary.occurrence,
|
|
137
|
-
)
|
|
138
|
-
)
|
|
139
|
-
return false
|
|
140
|
-
if (
|
|
141
|
-
events.some(
|
|
142
|
-
(event) =>
|
|
143
|
-
event.type === 'pr-summary.result' &&
|
|
144
|
-
event.payload.eligibleAfter !== undefined &&
|
|
145
|
-
Date.parse(event.payload.eligibleAfter) > deps.clock().getTime(),
|
|
146
|
-
)
|
|
119
|
+
events.summaryEligibleAfter !== undefined &&
|
|
120
|
+
Date.parse(events.summaryEligibleAfter) > deps.clock().getTime()
|
|
147
121
|
)
|
|
148
122
|
return false
|
|
149
|
-
return
|
|
123
|
+
return events.summaryRequests.some((event) => event.seq === request.seq)
|
|
150
124
|
}
|
|
151
|
-
if (
|
|
152
|
-
|
|
153
|
-
!events.some(
|
|
154
|
-
(event) =>
|
|
155
|
-
event.type === 'publication.requested' && event.payload.operation === 'finalize',
|
|
156
|
-
)
|
|
157
|
-
)
|
|
158
|
-
return false
|
|
159
|
-
const written = await deps.store.appendIfCurrent(deps.slug, events.at(-1)?.seq ?? 0, {
|
|
125
|
+
if (events.state.pr === undefined && !events.finalizePublicationRequested) return false
|
|
126
|
+
const written = await deps.store.appendIfCurrent(deps.slug, events.state.lastSeq, {
|
|
160
127
|
actor: KERNEL,
|
|
161
128
|
type: 'pr-summary.requested',
|
|
162
129
|
payload: boundary,
|
|
@@ -178,10 +145,10 @@ export async function settleSummaryRequests(
|
|
|
178
145
|
let acquired = false
|
|
179
146
|
let heartbeat: ReturnType<typeof setInterval> | undefined
|
|
180
147
|
try {
|
|
181
|
-
let events = await deps.store
|
|
182
|
-
if (
|
|
148
|
+
let events = (await readBuildFacts(deps.store, deps.slug)).value
|
|
149
|
+
if (events.open !== null) return
|
|
183
150
|
// Repair a crash after terminal/session completion but before guest handoff.
|
|
184
|
-
const workspace = events.
|
|
151
|
+
const workspace = events.latestWorkspace
|
|
185
152
|
if (workspace?.type !== 'workspace.provisioned' || !isRemoteWorkspace(workspace.payload)) return
|
|
186
153
|
const holder = deps.reservation ?? deps.holder
|
|
187
154
|
acquired = await deps.store.claimLease(deps.slug, holder, 60_000)
|
|
@@ -189,11 +156,11 @@ export async function settleSummaryRequests(
|
|
|
189
156
|
heartbeat = setInterval(() => {
|
|
190
157
|
void deps.store.heartbeat(deps.slug, holder).catch(() => {})
|
|
191
158
|
}, 15_000)
|
|
192
|
-
events = await deps.store
|
|
193
|
-
if (
|
|
159
|
+
events = (await readBuildFacts(deps.store, deps.slug)).value
|
|
160
|
+
if (events.open !== null) return
|
|
194
161
|
await refreshSummaryBoundary(deps)
|
|
195
|
-
events = await deps.store
|
|
196
|
-
const requests =
|
|
162
|
+
events = (await readBuildFacts(deps.store, deps.slug)).value
|
|
163
|
+
const requests = events.summaryRequests
|
|
197
164
|
const latest = requests.at(-1)
|
|
198
165
|
for (const request of requests) {
|
|
199
166
|
if (request.type !== 'pr-summary.requested') continue
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { CurrentBuildFacts } from '../store/reduced-reads'
|
|
1
2
|
import { summaryMarker } from '../ports/forge/summary-marker'
|
|
2
3
|
import type { AbEvent } from '../events/catalog'
|
|
3
4
|
import {
|
|
@@ -33,12 +34,21 @@ export function shellQuote(value: string): string {
|
|
|
33
34
|
return `'${value.replaceAll("'", `'"'"'`)}'`
|
|
34
35
|
}
|
|
35
36
|
|
|
36
|
-
export function renderPrAttachmentSection(
|
|
37
|
-
|
|
37
|
+
export function renderPrAttachmentSection(
|
|
38
|
+
env: CliEnv,
|
|
39
|
+
events: readonly AbEvent[] | CurrentBuildFacts,
|
|
40
|
+
): string[] {
|
|
41
|
+
const designations = Array.isArray(events)
|
|
42
|
+
? currentPrAttachments(events)
|
|
43
|
+
: (events as CurrentBuildFacts).designations
|
|
38
44
|
if (designations.length === 0) return []
|
|
39
45
|
|
|
40
|
-
const target =
|
|
41
|
-
|
|
46
|
+
const target = Array.isArray(events)
|
|
47
|
+
? frozenPrImageHost(events)
|
|
48
|
+
: (events as CurrentBuildFacts).imageHost
|
|
49
|
+
const hosted = Array.isArray(events)
|
|
50
|
+
? hostedPrAttachments(events, designations)
|
|
51
|
+
: new Map((events as CurrentBuildFacts).hostedImages)
|
|
42
52
|
const lines = [
|
|
43
53
|
'',
|
|
44
54
|
'### PR attachments',
|
|
@@ -81,19 +91,39 @@ export function renderPrAttachmentSection(env: CliEnv, events: readonly AbEvent[
|
|
|
81
91
|
}
|
|
82
92
|
|
|
83
93
|
/** The complete PR summary projection, rendered only from durable facts. */
|
|
84
|
-
export function renderPrSummary(
|
|
94
|
+
export function renderPrSummary(
|
|
95
|
+
env: CliEnv,
|
|
96
|
+
events: readonly AbEvent[] | CurrentBuildFacts,
|
|
97
|
+
): string {
|
|
85
98
|
const verdicts: string[] = []
|
|
86
99
|
const verifies: string[] = []
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
100
|
+
const history = Array.isArray(events)
|
|
101
|
+
? events.flatMap((event): CurrentBuildFacts['summaryHistory'] =>
|
|
102
|
+
event.type === 'plan-review.verdict' || event.type === 'code-review.verdict'
|
|
103
|
+
? [
|
|
104
|
+
{
|
|
105
|
+
kind: 'review',
|
|
106
|
+
phase: event.type === 'plan-review.verdict' ? 'plan-review' : 'code-review',
|
|
107
|
+
round: event.payload.round,
|
|
108
|
+
verdict: event.payload.verdict,
|
|
109
|
+
count: event.payload.findings.length,
|
|
110
|
+
},
|
|
111
|
+
]
|
|
112
|
+
: event.type === 'verify.completed'
|
|
113
|
+
? [{ kind: 'verify', result: event.payload }]
|
|
114
|
+
: [],
|
|
115
|
+
)
|
|
116
|
+
: (events as CurrentBuildFacts).summaryHistory
|
|
117
|
+
for (const entry of history) {
|
|
118
|
+
if (entry.kind === 'review') {
|
|
119
|
+
const phase = entry.phase
|
|
120
|
+
const count = entry.count
|
|
91
121
|
const detail =
|
|
92
|
-
|
|
93
|
-
verdicts.push(`- ${phase} r${
|
|
122
|
+
entry.verdict === 'revise' ? ` (${count} finding${count === 1 ? '' : 's'})` : ''
|
|
123
|
+
verdicts.push(`- ${phase} r${entry.round}: ${entry.verdict}${detail}`)
|
|
94
124
|
}
|
|
95
|
-
if (
|
|
96
|
-
const result = normalizeVerifyCompletion(
|
|
125
|
+
if (entry.kind === 'verify' && entry.result) {
|
|
126
|
+
const result = normalizeVerifyCompletion(entry.result)
|
|
97
127
|
const detail =
|
|
98
128
|
result.outcome === 'skipped'
|
|
99
129
|
? ` — ${html(result.reason ?? '')}`
|
|
@@ -1,14 +1,8 @@
|
|
|
1
|
+
import { readBuildFacts } from '../store/reduced-reads'
|
|
1
2
|
import type { IdSource } from '../ids'
|
|
2
3
|
import { DISPATCHER, KERNEL } from '../events/envelope'
|
|
3
4
|
import type { Forge, WorkspacePublication } from '../ports/types'
|
|
4
5
|
import type { Exec } from '../ports/workspace/git-worktree'
|
|
5
|
-
import { openExecution } from '../processes/execution-settlement'
|
|
6
|
-
import {
|
|
7
|
-
latestUncompletedPublicationRequest,
|
|
8
|
-
publicationLostRecorded,
|
|
9
|
-
publicationRequestCompleted,
|
|
10
|
-
publicationRequestSettled,
|
|
11
|
-
} from '../processes/publication-state'
|
|
12
6
|
import { systemClock, type BuildStore, type Clock } from '../store/types'
|
|
13
7
|
import { completeFinalizePr } from './terminals'
|
|
14
8
|
|
|
@@ -41,11 +35,11 @@ export async function settlePendingPublication(
|
|
|
41
35
|
): Promise<void> {
|
|
42
36
|
const publication = deps.publication
|
|
43
37
|
if (publication === undefined) return
|
|
44
|
-
let events = await deps.store
|
|
45
|
-
const request =
|
|
38
|
+
let events = (await readBuildFacts(deps.store, slug)).value
|
|
39
|
+
const request = events.publication.latestUncompletedRequest()
|
|
46
40
|
if (request === undefined) return
|
|
47
41
|
|
|
48
|
-
const abandoned =
|
|
42
|
+
const abandoned = events.publication.requestSettled(request)
|
|
49
43
|
// Consulted for every uncompleted request, not only abandoned ones: a guest
|
|
50
44
|
// that pushed its branch before dying is owed its completion fact, not a
|
|
51
45
|
// re-push or a loss record.
|
|
@@ -59,7 +53,7 @@ export async function settlePendingPublication(
|
|
|
59
53
|
// returning silently — the exact drop that lost the validate-hosted-
|
|
60
54
|
// dispatcher generation-1 work. The finalize-step publish-failure path
|
|
61
55
|
// below is unchanged: it already settles loudly via its failed outcome.
|
|
62
|
-
if (!
|
|
56
|
+
if (!events.publication.lostRecorded(request)) {
|
|
63
57
|
await deps.store.append(slug, {
|
|
64
58
|
actor: DISPATCHER,
|
|
65
59
|
type: 'publication.lost',
|
|
@@ -85,12 +79,8 @@ export async function settlePendingPublication(
|
|
|
85
79
|
const leaseLive =
|
|
86
80
|
record?.lease !== undefined &&
|
|
87
81
|
new Date(record.lease.expiresAt).getTime() > clock().getTime()
|
|
88
|
-
if (
|
|
89
|
-
|
|
90
|
-
for (const event of events) {
|
|
91
|
-
if (event.type === 'workspace.provisioned') ref = event.payload.ref
|
|
92
|
-
else if (event.type === 'workspace.released') ref = undefined
|
|
93
|
-
}
|
|
82
|
+
if (events.open !== null && leaseLive) return
|
|
83
|
+
const ref = events.workspace?.ref
|
|
94
84
|
if (ref === undefined)
|
|
95
85
|
throw new Error(`build ${slug} has a publication request but no open workspace`)
|
|
96
86
|
await publication.publish({ ref, sha: request.payload.sha, branch: request.payload.branch })
|
|
@@ -115,8 +105,8 @@ export async function settlePendingPublication(
|
|
|
115
105
|
})
|
|
116
106
|
return
|
|
117
107
|
}
|
|
118
|
-
events = await deps.store
|
|
119
|
-
if (
|
|
108
|
+
events = (await readBuildFacts(deps.store, slug)).value
|
|
109
|
+
if (events.publication.requestCompleted(request)) return
|
|
120
110
|
|
|
121
111
|
if (request.payload.operation === 'implement') {
|
|
122
112
|
await deps.store.append(slug, {
|
|
@@ -157,7 +147,7 @@ export async function settlePendingPublication(
|
|
|
157
147
|
const newline = text.indexOf('\n')
|
|
158
148
|
const title = (newline === -1 ? text : text.slice(0, newline)).replace(/^#+\s*/, '').trim()
|
|
159
149
|
if (title === '') throw new Error(`empty PR title for ${slug}`)
|
|
160
|
-
const created = events.
|
|
150
|
+
const created = events.lastCreated
|
|
161
151
|
if (created?.type !== 'build.created') throw new Error(`missing build.created for ${slug}`)
|
|
162
152
|
|
|
163
153
|
await completeFinalizePr(
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { readRepositoryFacts, type RepositoryReadFacts } from '../store/reduced-reads'
|
|
1
2
|
import {
|
|
2
3
|
formatDispatcherIdentity,
|
|
3
4
|
projectDispatcherLiveness,
|
|
@@ -99,6 +100,37 @@ export function projectRepositoryStatus(
|
|
|
99
100
|
}
|
|
100
101
|
}
|
|
101
102
|
|
|
103
|
+
export function repositoryStatusFromFacts(
|
|
104
|
+
repo: string,
|
|
105
|
+
facts: RepositoryReadFacts,
|
|
106
|
+
dispatcher: DispatcherLiveness,
|
|
107
|
+
): RepositoryStatus {
|
|
108
|
+
return {
|
|
109
|
+
repo,
|
|
110
|
+
dispatcher,
|
|
111
|
+
...facts.settings,
|
|
112
|
+
...(facts.health === undefined ? {} : { ticketSourceDegradation: facts.health }),
|
|
113
|
+
sandboxes: facts.sandboxes
|
|
114
|
+
.filter((state) => state.state !== 'released')
|
|
115
|
+
.map((state) => ({
|
|
116
|
+
operator: state.operator,
|
|
117
|
+
environmentId: state.environmentId,
|
|
118
|
+
provider: state.provider,
|
|
119
|
+
state: state.state === 'stopped' ? ('stopped' as const) : ('live' as const),
|
|
120
|
+
lastEvidenceAt: state.lastEvidenceTs,
|
|
121
|
+
})),
|
|
122
|
+
publications: [...facts.publications].reverse().map((event) => ({
|
|
123
|
+
operator: event.payload.operator,
|
|
124
|
+
branch: event.payload.branch,
|
|
125
|
+
sha: event.payload.sha,
|
|
126
|
+
prNumber: event.payload.pr.number,
|
|
127
|
+
prUrl: event.payload.pr.url,
|
|
128
|
+
...(event.payload.session !== undefined ? { session: event.payload.session } : {}),
|
|
129
|
+
at: event.ts,
|
|
130
|
+
})),
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
102
134
|
export function renderRepositoryStatus(status: RepositoryStatus): string[] {
|
|
103
135
|
const setting = (enabled: boolean): string => (enabled ? 'ON' : 'OFF')
|
|
104
136
|
const active = status.ticketSourceDegradation
|
|
@@ -147,8 +179,12 @@ export async function abRepositoryStatus(opts: RepositoryStatusOpts): Promise<vo
|
|
|
147
179
|
await withSessionlessStore(opts, async ({ store, repo }) => {
|
|
148
180
|
// A fresh store has no repository row. Do not call ensureRepo: this query's
|
|
149
181
|
// empty-stream defaults must remain a genuinely read-only operation.
|
|
150
|
-
const {
|
|
151
|
-
const status =
|
|
182
|
+
const { dispatcher } = await readDispatcherLiveness(store, repo, opts.now?.())
|
|
183
|
+
const status = repositoryStatusFromFacts(
|
|
184
|
+
repo,
|
|
185
|
+
(await readRepositoryFacts(store, repo)).value,
|
|
186
|
+
dispatcher,
|
|
187
|
+
)
|
|
152
188
|
if (opts.json === true) {
|
|
153
189
|
opts.stdout(JSON.stringify(status, null, 2))
|
|
154
190
|
return
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { join } from 'node:path'
|
|
1
2
|
import { checkUpgradeCompatibility, type CompatibilityFetch } from './upgrade-compatibility'
|
|
2
3
|
import { readFile, writeFile } from 'node:fs/promises'
|
|
3
4
|
import semver from 'semver'
|
|
@@ -363,6 +364,7 @@ export async function selfUpdate(options: SelfUpdateOptions): Promise<SelfUpdate
|
|
|
363
364
|
const inspection = await inspectInstallation({
|
|
364
365
|
distRoot,
|
|
365
366
|
globalBin: globalBinResult.stdout.trim(),
|
|
367
|
+
cwd: options.targetRepo,
|
|
366
368
|
})
|
|
367
369
|
if (inspection.kind !== 'bun-forge' && inspection.kind !== 'npm-registry') {
|
|
368
370
|
return explicit ? fail(options, inspection.reason) : warn(options, inspection.reason)
|
|
@@ -469,7 +471,9 @@ export async function selfUpdate(options: SelfUpdateOptions): Promise<SelfUpdate
|
|
|
469
471
|
|
|
470
472
|
let updated: DistributionIdentity
|
|
471
473
|
try {
|
|
472
|
-
updated = await readDistributionIdentity(
|
|
474
|
+
updated = await readDistributionIdentity(
|
|
475
|
+
join(install.ownerRoot, 'node_modules', ...install.packageName.split('/')),
|
|
476
|
+
)
|
|
473
477
|
// Injected command fixtures do not rewrite disk; a real successful Bun
|
|
474
478
|
// operation must. Keep the check injectable by accepting the old identity
|
|
475
479
|
// only when the command seam itself was supplied.
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { readBuildDigests } from '../store/reduced-reads'
|
|
2
|
+
import { readBuildFacts } from '../store/reduced-reads'
|
|
3
|
+
import type { BuildFacts } from '../processes/repo-view'
|
|
1
4
|
/**
|
|
2
5
|
* `ab builds` and `ab build status <slug>` — the read-only query surface on
|
|
3
6
|
* build state (SPEC §8.2, §15.5). Their invocation forms require no session,
|
|
@@ -44,9 +47,9 @@ import {
|
|
|
44
47
|
} from '../processes/build-execution-state'
|
|
45
48
|
import type { PipelineSourceMeta } from '../config/pipeline-source'
|
|
46
49
|
import { openExecution } from '../processes/execution-settlement'
|
|
47
|
-
import {
|
|
50
|
+
import { currentAutoMergeDeferralFrom } from '../kernel/auto-merge'
|
|
48
51
|
import { autoMergeDisplay, type AutoMergeDisplay } from './dashboard/model'
|
|
49
|
-
import {
|
|
52
|
+
import { decideNextFromFacts } from '../kernel/engine'
|
|
50
53
|
import {
|
|
51
54
|
reduceBuild,
|
|
52
55
|
type BuildState,
|
|
@@ -271,7 +274,31 @@ export function detail(
|
|
|
271
274
|
decision?: BuildDecisionProjection,
|
|
272
275
|
streams?: readonly StreamRecord[],
|
|
273
276
|
): BuildDetail {
|
|
274
|
-
|
|
277
|
+
return detailFromFacts(
|
|
278
|
+
record,
|
|
279
|
+
{
|
|
280
|
+
state: reduceBuild(events),
|
|
281
|
+
observations: events
|
|
282
|
+
.filter((event) => event.type === 'observation.recorded')
|
|
283
|
+
.map((event) => event.payload),
|
|
284
|
+
open: openExecution(events),
|
|
285
|
+
},
|
|
286
|
+
now,
|
|
287
|
+
eventCount === undefined ? undefined : events.slice(-eventCount),
|
|
288
|
+
decision,
|
|
289
|
+
streams,
|
|
290
|
+
)
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export function detailFromFacts(
|
|
294
|
+
record: BuildRecord,
|
|
295
|
+
facts: Pick<BuildFacts, 'state' | 'open'> & { observations: BuildState['observations'] },
|
|
296
|
+
now: Date,
|
|
297
|
+
events?: AbEvent[],
|
|
298
|
+
decision?: BuildDecisionProjection,
|
|
299
|
+
streams?: readonly StreamRecord[],
|
|
300
|
+
): BuildDetail {
|
|
301
|
+
const state = facts.state
|
|
275
302
|
const streamStatus = new Map<string, { stream?: string; status: 'open' | 'closed' }>()
|
|
276
303
|
if (streams !== undefined) {
|
|
277
304
|
for (const record of streams) {
|
|
@@ -316,15 +343,13 @@ export function detail(
|
|
|
316
343
|
}),
|
|
317
344
|
// Observations are historical facts rather than a derived current-state
|
|
318
345
|
// opinion. Preserve event order and exact optional evidence fields.
|
|
319
|
-
observations:
|
|
320
|
-
|
|
321
|
-
.
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
...(event.payload.refs !== undefined ? { refs: event.payload.refs } : {}),
|
|
327
|
-
})),
|
|
346
|
+
observations: facts.observations.map((observation) => ({
|
|
347
|
+
id: observation.id,
|
|
348
|
+
kind: observation.kind,
|
|
349
|
+
summary: observation.summary,
|
|
350
|
+
...(observation.files !== undefined ? { files: observation.files } : {}),
|
|
351
|
+
...(observation.refs !== undefined ? { refs: observation.refs } : {}),
|
|
352
|
+
})),
|
|
328
353
|
...(state.setupFailure !== undefined
|
|
329
354
|
? {
|
|
330
355
|
setupFailure: {
|
|
@@ -338,7 +363,7 @@ export function detail(
|
|
|
338
363
|
...(state.executions.at(-1) !== undefined ? { execution: state.executions.at(-1)! } : {}),
|
|
339
364
|
// Durable supervision: a recorded execution with no following end is the
|
|
340
365
|
// source of truth for whether a guest may still be running.
|
|
341
|
-
executionOpen:
|
|
366
|
+
executionOpen: facts.open !== null,
|
|
342
367
|
...(state.infrastructureFailure !== undefined
|
|
343
368
|
? { infrastructureFailure: state.infrastructureFailure }
|
|
344
369
|
: {}),
|
|
@@ -364,7 +389,7 @@ export function detail(
|
|
|
364
389
|
: {}),
|
|
365
390
|
// getEvents returns seq-ordered events, so the newest n slice out already
|
|
366
391
|
// chronological — which is what the AC asks for.
|
|
367
|
-
...(
|
|
392
|
+
...(events !== undefined ? { events } : {}),
|
|
368
393
|
...(state.outcome !== undefined ? { outcome: state.outcome } : {}),
|
|
369
394
|
}
|
|
370
395
|
}
|
|
@@ -742,7 +767,7 @@ export async function abBuilds(opts: AbBuildsOpts): Promise<void> {
|
|
|
742
767
|
// round trip and the listing's store cost stays flat as finished builds
|
|
743
768
|
// accumulate. The `--all` scope includes terminal statuses, so it takes no
|
|
744
769
|
// digest read and skips nothing — byte-for-byte the old loop.
|
|
745
|
-
const digests = opts.all === true ? undefined : await store
|
|
770
|
+
const digests = opts.all === true ? undefined : await readBuildDigests(store, identity)
|
|
746
771
|
const summaries: BuildSummary[] = []
|
|
747
772
|
for (const record of records) {
|
|
748
773
|
if (digests !== undefined && record.repo === identity) {
|
|
@@ -766,7 +791,11 @@ export async function abBuilds(opts: AbBuildsOpts): Promise<void> {
|
|
|
766
791
|
// entry under `identity` — `getRepoBuildDigests` groups by `record.repo`
|
|
767
792
|
// exactly — so it keeps its per-build history read (correct, just not
|
|
768
793
|
// accelerated).
|
|
769
|
-
const summary =
|
|
794
|
+
const summary = summarizeFrom(
|
|
795
|
+
record,
|
|
796
|
+
(await readBuildFacts(store, record.slug)).value.state,
|
|
797
|
+
now,
|
|
798
|
+
)
|
|
770
799
|
if (wanted.has(summary.status)) summaries.push(summary)
|
|
771
800
|
}
|
|
772
801
|
summaries.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt))
|
|
@@ -797,7 +826,7 @@ export interface AbBuildStatusOpts extends StatusOpts {
|
|
|
797
826
|
events?: number
|
|
798
827
|
}
|
|
799
828
|
|
|
800
|
-
function
|
|
829
|
+
function _openWorkspacePath(events: AbEvent[]): string | undefined {
|
|
801
830
|
let path: string | undefined
|
|
802
831
|
for (const event of events) {
|
|
803
832
|
if (event.type === 'workspace.provisioned') {
|
|
@@ -812,15 +841,11 @@ function openWorkspacePath(events: AbEvent[]): string | undefined {
|
|
|
812
841
|
/** The kernel tail both config sources share, so the deposited artifact and
|
|
813
842
|
* the workspace file cannot drift in what they compute — the sources differ
|
|
814
843
|
* only in where the config comes from, never in what is decided with it. */
|
|
815
|
-
function decideFromConfig(
|
|
816
|
-
|
|
817
|
-
state: BuildState,
|
|
818
|
-
config: Config,
|
|
819
|
-
): BuildDecisionProjection {
|
|
820
|
-
const decision = decideNext(events, config)
|
|
844
|
+
function decideFromConfig(facts: BuildFacts, config: Config): BuildDecisionProjection {
|
|
845
|
+
const decision = decideNextFromFacts(facts.state, facts.log, config)
|
|
821
846
|
if (decision.kind !== 'wait') return { kind: 'runner-work' }
|
|
822
847
|
if (decision.reason !== 'awaiting-pr') return { kind: 'parked', reason: decision.reason }
|
|
823
|
-
const reason =
|
|
848
|
+
const reason = currentAutoMergeDeferralFrom(facts.deferral, facts.state)
|
|
824
849
|
return { kind: 'awaiting-pr', ...(reason !== undefined ? { reason } : {}) }
|
|
825
850
|
}
|
|
826
851
|
|
|
@@ -840,10 +865,10 @@ function decideFromConfig(
|
|
|
840
865
|
* the read-only command fail. */
|
|
841
866
|
async function projectBuildDecision(
|
|
842
867
|
slug: string,
|
|
843
|
-
|
|
868
|
+
facts: BuildFacts,
|
|
844
869
|
store: Pick<BuildStore, 'getArtifact'>,
|
|
845
870
|
): Promise<BuildDecisionProjection | undefined> {
|
|
846
|
-
const state =
|
|
871
|
+
const state = facts.state
|
|
847
872
|
const active =
|
|
848
873
|
state.status === 'running' || state.status === 'paused' || state.status === 'blocked'
|
|
849
874
|
if (!active) return undefined
|
|
@@ -858,7 +883,7 @@ async function projectBuildDecision(
|
|
|
858
883
|
const artifact = await store.getArtifact(slug, BUILD_EFFECTIVE_CONFIG_ARTIFACT)
|
|
859
884
|
if (artifact !== null) {
|
|
860
885
|
try {
|
|
861
|
-
return decideFromConfig(
|
|
886
|
+
return decideFromConfig(facts, parseEffectiveBuildConfig(artifact))
|
|
862
887
|
} catch (error) {
|
|
863
888
|
const message = error instanceof Error ? error.message : String(error)
|
|
864
889
|
return {
|
|
@@ -870,7 +895,7 @@ async function projectBuildDecision(
|
|
|
870
895
|
|
|
871
896
|
// Source 2 — the local workspace, exactly as before, only when no artifact
|
|
872
897
|
// is deposited.
|
|
873
|
-
const workspace =
|
|
898
|
+
const workspace = facts.workspace?.path ?? facts.workspace?.ref
|
|
874
899
|
if (workspace === undefined) {
|
|
875
900
|
return {
|
|
876
901
|
kind: 'unavailable',
|
|
@@ -879,7 +904,7 @@ async function projectBuildDecision(
|
|
|
879
904
|
}
|
|
880
905
|
|
|
881
906
|
try {
|
|
882
|
-
return decideFromConfig(
|
|
907
|
+
return decideFromConfig(facts, await loadConfig(join(workspace, 'autobuild.toml')))
|
|
883
908
|
} catch {
|
|
884
909
|
// The raw error deliberately stays here: ENOENT/ENOTDIR messages embed the
|
|
885
910
|
// recorded workspace path, which for a remote build is a guest location.
|
|
@@ -944,8 +969,13 @@ export async function abBuildStatus(opts: AbBuildStatusOpts): Promise<void> {
|
|
|
944
969
|
const now = (opts.now ?? (() => new Date()))()
|
|
945
970
|
await withAmbientReadStore(opts, async (context) => {
|
|
946
971
|
const record = await requireReadableBuild(opts, context)
|
|
947
|
-
const
|
|
948
|
-
const
|
|
972
|
+
const current = await readBuildFacts(context.store, opts.slug)
|
|
973
|
+
const facts = current.value
|
|
974
|
+
const events =
|
|
975
|
+
opts.events === undefined
|
|
976
|
+
? undefined
|
|
977
|
+
: await context.store.getEvents(opts.slug, Math.max(0, current.cursor - opts.events))
|
|
978
|
+
const decision = await projectBuildDecision(opts.slug, facts, context.store)
|
|
949
979
|
// Stream enrichment is presentation (SPEC §9): an unavailable read
|
|
950
980
|
// degrades to payload-carried ids without a status, never a failure.
|
|
951
981
|
let streams: StreamRecord[] | undefined
|
|
@@ -957,7 +987,7 @@ export async function abBuildStatus(opts: AbBuildStatusOpts): Promise<void> {
|
|
|
957
987
|
} catch {
|
|
958
988
|
streams = undefined
|
|
959
989
|
}
|
|
960
|
-
const projected =
|
|
990
|
+
const projected = detailFromFacts(record, facts, now, events, decision, streams)
|
|
961
991
|
// Operator visibility (SPEC §16.1): which autobuild.toml the build's
|
|
962
992
|
// pinned pipeline came from, alongside the effective-config revision.
|
|
963
993
|
const configArtifact = await context.store.getArtifact(
|