@defrex/autobuild 0.11.0 → 0.13.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 +3 -1
- package/SPEC.md +335 -31
- package/docs/architecture.md +125 -7
- package/docs/configuration.md +142 -21
- package/docs/hosted-dispatcher.md +17 -2
- package/docs/hosted-service.md +240 -0
- package/docs/mcp.md +8 -2
- package/docs/operator-api.md +97 -7
- package/docs/remote-store-protocol.md +199 -144
- package/docs/remote-ticket-protocol.md +249 -19
- package/docs/setup.md +199 -16
- package/docs/ticket-dependencies.md +1 -1
- package/docs/ticket-migration.md +64 -0
- package/package.json +7 -6
- package/packages/core/src/cli/attention.ts +209 -0
- package/packages/core/src/cli/build-control.ts +12 -1
- package/packages/core/src/cli/configured-store.ts +17 -0
- package/packages/core/src/cli/connect.ts +364 -0
- package/packages/core/src/cli/context.ts +7 -1
- package/packages/core/src/cli/dashboard/model.ts +4 -5
- package/packages/core/src/cli/dashboard/poll.ts +1 -1
- package/packages/core/src/cli/dispatch.ts +336 -41
- package/packages/core/src/cli/harvest.ts +13 -3
- package/packages/core/src/cli/help.ts +51 -16
- package/packages/core/src/cli/init-validation.ts +27 -2
- package/packages/core/src/cli/installation.ts +67 -13
- package/packages/core/src/cli/main.ts +60 -16
- package/packages/core/src/cli/mcp.ts +9 -2
- package/packages/core/src/cli/pr-summary-publication.ts +220 -0
- package/packages/core/src/cli/pr-summary.ts +2 -0
- package/packages/core/src/cli/project-credentials.ts +132 -0
- package/packages/core/src/cli/repo-state.ts +15 -2
- package/packages/core/src/cli/self-update.ts +15 -1
- package/packages/core/src/cli/store-opening.ts +22 -14
- package/packages/core/src/cli/terminals.ts +0 -10
- 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 +172 -35
- package/packages/core/src/cli/upgrade-compatibility.ts +80 -0
- package/packages/core/src/cli/wait.ts +84 -17
- package/packages/core/src/cli/watch.ts +197 -60
- package/packages/core/src/config/live.ts +3 -0
- package/packages/core/src/config/schema.ts +90 -11
- package/packages/core/src/connection.ts +86 -0
- package/packages/core/src/events/catalog.ts +11 -2
- package/packages/core/src/events/payloads.ts +50 -0
- package/packages/core/src/events/repository.ts +13 -0
- package/packages/core/src/integration/harness.ts +37 -2
- package/packages/core/src/kernel/__snapshots__/reducer-registry.test.ts.snap +145 -83
- package/packages/core/src/kernel/engine.ts +24 -0
- package/packages/core/src/kernel/generators/build-log.ts +11 -1
- package/packages/core/src/kernel/incremental-fixtures.ts +13 -2
- package/packages/core/src/kernel/log-index.ts +17 -0
- package/packages/core/src/kernel/pr-summary.ts +89 -0
- package/packages/core/src/kernel/reducer-registry.ts +10 -0
- package/packages/core/src/kernel/reducer.ts +8 -0
- package/packages/core/src/ontology.ts +2 -0
- package/packages/core/src/operator/annotations.ts +12 -0
- package/packages/core/src/operator/index.ts +12 -0
- package/packages/core/src/operator/protocol.ts +19 -2
- package/packages/core/src/operator/query.ts +38 -16
- package/packages/core/src/operator/registry.ts +61 -8
- package/packages/core/src/operator/ticket-source-backend.ts +17 -20
- package/packages/core/src/operator/tickets.ts +136 -28
- package/packages/core/src/plugin-sdk/index.ts +7 -0
- package/packages/core/src/plugins/manifest.ts +5 -3
- package/packages/core/src/plugins/runtimes.ts +7 -1
- package/packages/core/src/ports/forge/fake.ts +13 -1
- package/packages/core/src/ports/forge/github-transport.ts +36 -1
- package/packages/core/src/ports/forge/github.ts +29 -5
- package/packages/core/src/ports/forge/local-git.ts +12 -4
- package/packages/core/src/ports/forge/summary-marker.ts +8 -0
- package/packages/core/src/ports/runner/session-env.ts +3 -0
- package/packages/core/src/ports/tickets/contract.ts +66 -1
- package/packages/core/src/ports/tickets/create.ts +54 -10
- 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 -3
- 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 +96 -0
- package/packages/core/src/ports/tickets/remote.ts +61 -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 +6 -1
- package/packages/core/src/ports/workspace/create.ts +15 -2
- package/packages/core/src/ports/workspace/distribution-archive.ts +3 -5
- package/packages/core/src/ports/workspace/harvest-execution.ts +3 -1
- package/packages/core/src/ports/workspace/local-build-execution.ts +8 -1
- package/packages/core/src/ports/workspace/operator-sandbox.ts +2 -0
- package/packages/core/src/ports/workspace/vercel-sandbox.ts +51 -19
- package/packages/core/src/processes/build-execution-state.ts +1 -1
- package/packages/core/src/processes/build-runner.ts +87 -26
- package/packages/core/src/processes/dispatch-failure-budget.ts +83 -0
- package/packages/core/src/processes/dispatcher.ts +168 -41
- package/packages/core/src/processes/execution-no-progress.ts +176 -0
- package/packages/core/src/processes/harvest-child.ts +31 -4
- package/packages/core/src/processes/harvest-execution-state.ts +19 -2
- package/packages/core/src/processes/harvest-runner.ts +166 -104
- package/packages/core/src/processes/harvest.ts +34 -14
- package/packages/core/src/processes/phase-failure-budget.ts +2 -0
- package/packages/core/src/processes/publication-loss.ts +18 -1
- package/packages/core/src/processes/repo-view.ts +35 -0
- package/packages/core/src/store/contract.ts +32 -1
- package/packages/core/src/store/local/store.ts +1 -1
- package/packages/core/src/store/remote/client.ts +1 -1
- package/packages/core/src/store/remote/compatibility.ts +90 -0
- package/packages/core/src/store/remote/credentials.ts +28 -0
- package/packages/core/src/store/remote/index.ts +14 -1
- package/packages/core/src/store/remote/protocol.ts +25 -0
- package/packages/core/src/store/remote/token.ts +74 -13
- 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 +1 -1
- package/packages/core/src/testing/index.ts +7 -0
- package/skills/guide/SKILL.md +232 -34
- package/skills/guide/references/operator-api.md +142 -13
- package/skills/guide/references/plugin-authoring.md +10 -2
- package/skills/guide/references/remote-store-protocol.md +199 -144
- package/skills/guide/references/remote-ticket-protocol.md +253 -18
- package/skills/guide/references/setup.md +199 -16
- package/skills/guide/references/ticket-dependencies.md +1 -1
- package/skills/guide/references/ticket-migration.md +64 -0
- package/skills/operate/SKILL.md +126 -36
- package/skills/tickets/SKILL.md +71 -0
- package/templates/hosted-service/package.json +13 -0
- package/templates/hosted-service/vercel.json +7 -0
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ticketGrantRequest } from '../ports/tickets/grants'
|
|
1
2
|
/**
|
|
2
3
|
* Crash-safe repository-scoped harvest workflow: scan -> synthesize <-> review
|
|
3
4
|
* -> file. It reuses `converge` for adversarial review semantics while every
|
|
@@ -56,6 +57,20 @@ export async function runHarvestChild(
|
|
|
56
57
|
workspacePath: string = process.cwd(),
|
|
57
58
|
): Promise<void> {
|
|
58
59
|
const fullStore: BuildStore = openStore(input.storeRef, env.AB_TOKEN)
|
|
60
|
+
const ticketAuthority =
|
|
61
|
+
env.AB_TICKET_URL && env.AB_TICKET_TOKEN
|
|
62
|
+
? { url: env.AB_TICKET_URL, token: env.AB_TICKET_TOKEN }
|
|
63
|
+
: undefined
|
|
64
|
+
const renewTickets = ticketAuthority
|
|
65
|
+
? setInterval(() => {
|
|
66
|
+
void ticketGrantRequest(ticketAuthority.url, ticketAuthority.token, {
|
|
67
|
+
action: 'renew',
|
|
68
|
+
repo: input.repo,
|
|
69
|
+
}).catch(() => {
|
|
70
|
+
// Expiry denies subsequent ticket operations; never regain project authority.
|
|
71
|
+
})
|
|
72
|
+
}, 60_000)
|
|
73
|
+
: undefined
|
|
59
74
|
try {
|
|
60
75
|
const config = await loadConfig(join(workspacePath, 'autobuild.toml'))
|
|
61
76
|
// Full build-child composition parity, in the same order: plugins load
|
|
@@ -80,7 +95,7 @@ export async function runHarvestChild(
|
|
|
80
95
|
const { targetRepo, localStateRoot } = guestTicketSourceArgs(input, workspacePath)
|
|
81
96
|
const tickets = await createTicketSource(
|
|
82
97
|
config.tickets,
|
|
83
|
-
env,
|
|
98
|
+
{ ...env, AB_HARVEST: input.preparedRun?.run ?? input.instance },
|
|
84
99
|
targetRepo,
|
|
85
100
|
localStateRoot,
|
|
86
101
|
plugins,
|
|
@@ -97,11 +112,14 @@ export async function runHarvestChild(
|
|
|
97
112
|
uuids: randomUuids(),
|
|
98
113
|
clock: systemClock,
|
|
99
114
|
instance: input.instance,
|
|
115
|
+
...(input.preparedRun ? { preparedRun: input.preparedRun } : {}),
|
|
100
116
|
// Adopted holder: the guest skips its own claim, heartbeats the owning
|
|
101
117
|
// dispatch loop's repository lease, and never releases it.
|
|
102
118
|
...(input.leaseHolder !== undefined ? { leaseHolder: input.leaseHolder } : {}),
|
|
103
119
|
...(input.environment !== undefined ? { environment: input.environment } : {}),
|
|
104
120
|
sessionEnv: {
|
|
121
|
+
...(env.AB_TICKET_URL ? { AB_TICKET_URL: env.AB_TICKET_URL } : {}),
|
|
122
|
+
...(env.AB_TICKET_TOKEN ? { AB_TICKET_TOKEN: env.AB_TICKET_TOKEN } : {}),
|
|
105
123
|
AB_STORE: input.storeRef,
|
|
106
124
|
...(env.AB_TOKEN !== undefined && env.AB_TOKEN !== '' ? { AB_TOKEN: env.AB_TOKEN } : {}),
|
|
107
125
|
},
|
|
@@ -111,10 +129,19 @@ export async function runHarvestChild(
|
|
|
111
129
|
})
|
|
112
130
|
await runner.run()
|
|
113
131
|
} finally {
|
|
132
|
+
if (renewTickets) clearInterval(renewTickets)
|
|
114
133
|
try {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
134
|
+
if (ticketAuthority)
|
|
135
|
+
await ticketGrantRequest(ticketAuthority.url, ticketAuthority.token, {
|
|
136
|
+
action: 'revoke',
|
|
137
|
+
repo: input.repo,
|
|
138
|
+
})
|
|
139
|
+
} finally {
|
|
140
|
+
try {
|
|
141
|
+
await fullStore.close()
|
|
142
|
+
} catch {
|
|
143
|
+
// Process teardown cannot change the already-durable runner outcome.
|
|
144
|
+
}
|
|
118
145
|
}
|
|
119
146
|
}
|
|
120
147
|
}
|
|
@@ -92,9 +92,26 @@ export function classifyHarvestOutcome(
|
|
|
92
92
|
input: HarvestExecutionClassificationInput,
|
|
93
93
|
): HarvestRunnerResult {
|
|
94
94
|
const state = reduceHarvest(events)
|
|
95
|
-
const
|
|
95
|
+
const started = events.find(
|
|
96
|
+
(event) =>
|
|
97
|
+
event.type === 'harvest.execution.started' && event.seq === input.executionStartedSeq,
|
|
98
|
+
)
|
|
99
|
+
const authorization =
|
|
100
|
+
started?.type === 'harvest.execution.started'
|
|
101
|
+
? events.find(
|
|
102
|
+
(event) =>
|
|
103
|
+
event.type === 'harvest.execution.authorized' &&
|
|
104
|
+
event.payload.execution === started.payload.execution,
|
|
105
|
+
)
|
|
106
|
+
: undefined
|
|
107
|
+
const selected =
|
|
108
|
+
authorization?.type === 'harvest.execution.authorized' ? authorization.payload : undefined
|
|
109
|
+
const latestRun = selected
|
|
110
|
+
? state.runs.find((run) => run.run === selected.run)
|
|
111
|
+
: state.runs.at(-1)
|
|
96
112
|
if (latestRun !== undefined) {
|
|
97
|
-
const launch =
|
|
113
|
+
const launch =
|
|
114
|
+
selected?.launch ?? (latestRun.startedSeq > input.executionStartedSeq ? 'started' : 'resumed')
|
|
98
115
|
// Recency gate: a terminal outcome counts only if its fact — the run's
|
|
99
116
|
// terminal fact, or for an unresolved failure the failure fact itself
|
|
100
117
|
// (`failureSeq`), which also covers exhaustion via `terminalSeq` —
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { ticketIdentity } from '../ports/tickets/identity'
|
|
2
|
+
import { issueTicketGrant } from '../ports/tickets/grants'
|
|
3
|
+
import { delegateCredential } from '../store/remote/credentials'
|
|
1
4
|
/**
|
|
2
5
|
* Crash-safe repository-scoped harvest workflow:
|
|
3
6
|
* scan -> synthesize <-> review -> file. It reuses `converge` for adversarial
|
|
@@ -100,11 +103,13 @@ export interface HarvestRunnerDeps {
|
|
|
100
103
|
* executes in (hosted harvest). Stamped into the run's harvest.started
|
|
101
104
|
* payload so the journal records where it ran; absent on local runs. */
|
|
102
105
|
environment?: { provider: string; environmentId: string; sessionId?: string }
|
|
106
|
+
preparedRun?: { run: string }
|
|
103
107
|
sessionEnv?: Record<string, string>
|
|
104
108
|
opts?: HarvestRunnerOpts
|
|
105
109
|
}
|
|
106
110
|
|
|
107
111
|
export type HarvestRunnerResult =
|
|
112
|
+
| { outcome: 'prepared'; run: string; launch: 'started' | 'resumed' }
|
|
108
113
|
| { outcome: 'idle' }
|
|
109
114
|
| { outcome: 'held' }
|
|
110
115
|
| { outcome: 'parked'; run?: string }
|
|
@@ -217,9 +222,9 @@ export class HarvestRunner {
|
|
|
217
222
|
)
|
|
218
223
|
}
|
|
219
224
|
|
|
220
|
-
async run(): Promise<HarvestRunnerResult> {
|
|
225
|
+
async run(preparationOnly = false): Promise<HarvestRunnerResult> {
|
|
221
226
|
const { store, repo, instance } = this.deps
|
|
222
|
-
await store.ensureRepo(repo)
|
|
227
|
+
if (!this.deps.preparedRun) await store.ensureRepo(repo)
|
|
223
228
|
if (
|
|
224
229
|
this.deps.leaseHolder === undefined &&
|
|
225
230
|
!(await store.claimRepoLease(repo, instance, this.leaseTtlMs))
|
|
@@ -233,101 +238,116 @@ export class HarvestRunner {
|
|
|
233
238
|
await this.ensureLease()
|
|
234
239
|
// Bounded read (AUT-489): reduceHarvest and the verdict/failure filters
|
|
235
240
|
// below consume durable types only, so the subset is replay-equivalent.
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
await
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
//
|
|
254
|
-
//
|
|
255
|
-
//
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
const pressure = evaluateHarvestPressureFromDigests({
|
|
263
|
-
digests,
|
|
264
|
-
harvestEvents: gateEvents,
|
|
265
|
-
policy: this.currentConfig().policy,
|
|
266
|
-
})
|
|
267
|
-
if (pressure.trigger === undefined) {
|
|
268
|
-
// Treat the read's completion as a boundary even when the gate did
|
|
269
|
-
// not trip, so a request arriving during the read is acknowledged
|
|
270
|
-
// before this runner returns idle.
|
|
241
|
+
if (this.deps.preparedRun) {
|
|
242
|
+
const events = await store.getRepoStateEvents(repo)
|
|
243
|
+
const authorization = events.find(
|
|
244
|
+
(event) =>
|
|
245
|
+
event.type === 'harvest.execution.authorized' &&
|
|
246
|
+
event.payload.execution === instance &&
|
|
247
|
+
event.payload.run === this.deps.preparedRun!.run,
|
|
248
|
+
)
|
|
249
|
+
if (authorization?.type !== 'harvest.execution.authorized')
|
|
250
|
+
throw new Error('prepared harvest execution unavailable')
|
|
251
|
+
run = reduceHarvest(events).runs.find((run) => run.run === this.deps.preparedRun!.run)
|
|
252
|
+
initial = authorization.payload.launch
|
|
253
|
+
} else {
|
|
254
|
+
let events = await store.getRepoStateEvents(repo)
|
|
255
|
+
let state = reduceHarvest(events)
|
|
256
|
+
run = actionableHarvestRun(state)
|
|
257
|
+
|
|
258
|
+
// Settle durable operator control before scanning or resuming work. The
|
|
259
|
+
// selected run comes from the full journal, so a shadowed failure or
|
|
260
|
+
// exhaustion barrier is recovered/reported before any later open run.
|
|
261
|
+
await this.controlBoundary(run?.run)
|
|
262
|
+
events = await store.getRepoStateEvents(repo)
|
|
263
|
+
state = reduceHarvest(events)
|
|
264
|
+
run = openHarvestRun(state)
|
|
265
|
+
|
|
266
|
+
if (!run) {
|
|
271
267
|
await this.controlBoundary()
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
268
|
+
// Flat-cost gate (AUT-521): the pressure trigger is evaluated from
|
|
269
|
+
// the journal and one repo-scoped digest batch read — no per-build
|
|
270
|
+
// history reads — so the idle evaluation no longer grows with
|
|
271
|
+
// accumulated finished builds. Only a tripped gate pays for the full
|
|
272
|
+
// unclaimed-observation scan, because the packet artifact needs the
|
|
273
|
+
// observation payloads no bounded source carries.
|
|
274
|
+
// Bounded read (AUT-489): the gate's journal input is consumed only
|
|
275
|
+
// through reduceHarvest, so the subset is replay-equivalent here too.
|
|
276
|
+
const [gateEvents, digests] = await Promise.all([
|
|
277
|
+
store.getRepoStateEvents(repo),
|
|
278
|
+
store.getRepoBuildDigests(repo),
|
|
279
|
+
])
|
|
280
|
+
const pressure = evaluateHarvestPressureFromDigests({
|
|
281
|
+
digests,
|
|
282
|
+
harvestEvents: gateEvents,
|
|
283
|
+
policy: this.currentConfig().policy,
|
|
284
|
+
})
|
|
285
|
+
if (pressure.trigger === undefined) {
|
|
286
|
+
// Treat the read's completion as a boundary even when the gate did
|
|
287
|
+
// not trip, so a request arriving during the read is acknowledged
|
|
288
|
+
// before this runner returns idle.
|
|
289
|
+
await this.controlBoundary()
|
|
290
|
+
return { outcome: 'idle' }
|
|
291
|
+
}
|
|
292
|
+
await this.controlBoundary()
|
|
293
|
+
const scan = await scanUnclaimedObservations(store, repo)
|
|
294
|
+
// The trigger came from an earlier read than this scan, and a claim
|
|
295
|
+
// may have landed in the window. Re-confirm the gate against the
|
|
296
|
+
// fresh scan (pure, free): an emptied window returns idle exactly as
|
|
297
|
+
// if the gate had not tripped, so a packet is only ever built from a
|
|
298
|
+
// scan that itself triggered.
|
|
299
|
+
const confirmed = evaluateHarvestPressure(scan, this.currentConfig().policy)
|
|
300
|
+
if (confirmed.trigger === undefined) {
|
|
301
|
+
await this.controlBoundary()
|
|
302
|
+
return { outcome: 'idle' }
|
|
303
|
+
}
|
|
304
|
+
const runId = this.deps.ids('harvest')
|
|
305
|
+
const packet = await makeHarvestScanPacket({
|
|
306
|
+
store,
|
|
307
|
+
tickets: this.deps.tickets,
|
|
308
|
+
repo,
|
|
309
|
+
run: runId,
|
|
310
|
+
observations: scan.observations,
|
|
311
|
+
state: scan.state,
|
|
312
|
+
})
|
|
313
|
+
// Scan preparation is not a claim. Re-read operator control immediately
|
|
314
|
+
// before atomically committing the immutable snapshot.
|
|
283
315
|
await this.controlBoundary()
|
|
284
|
-
|
|
316
|
+
initial = 'started'
|
|
317
|
+
await store.appendRepoWithArtifacts(
|
|
318
|
+
repo,
|
|
319
|
+
[
|
|
320
|
+
{
|
|
321
|
+
kind: HARVEST_SCAN_ARTIFACT,
|
|
322
|
+
content: `${JSON.stringify(packet, null, 2)}\n`,
|
|
323
|
+
metadata: { run: runId, observations: packet.observations.length },
|
|
324
|
+
},
|
|
325
|
+
],
|
|
326
|
+
(deposited) => ({
|
|
327
|
+
actor: KERNEL,
|
|
328
|
+
type: 'harvest.started',
|
|
329
|
+
payload: {
|
|
330
|
+
run: runId,
|
|
331
|
+
observations: packet.observations.map((item) => item.occurrence),
|
|
332
|
+
scan: artifactRef(deposited[0]!),
|
|
333
|
+
trigger: confirmed.trigger,
|
|
334
|
+
...(this.deps.environment !== undefined
|
|
335
|
+
? { environment: this.deps.environment }
|
|
336
|
+
: {}),
|
|
337
|
+
},
|
|
338
|
+
}),
|
|
339
|
+
)
|
|
340
|
+
// Bounded read (AUT-489): reduceHarvest consumes durable types only.
|
|
341
|
+
events = await store.getRepoStateEvents(repo)
|
|
342
|
+
run = reduceHarvest(events).runs.find((candidate) => candidate.run === runId)
|
|
343
|
+
if (!run) throw new Error(`harvest.started did not reduce run "${runId}"`)
|
|
344
|
+
// A request racing the claim parks this exact run; its snapshot stays
|
|
345
|
+
// open and is resumed rather than scanned again.
|
|
346
|
+
await this.controlBoundary(run.run)
|
|
285
347
|
}
|
|
286
|
-
const runId = this.deps.ids('harvest')
|
|
287
|
-
const packet = await makeHarvestScanPacket({
|
|
288
|
-
store,
|
|
289
|
-
tickets: this.deps.tickets,
|
|
290
|
-
repo,
|
|
291
|
-
run: runId,
|
|
292
|
-
observations: scan.observations,
|
|
293
|
-
state: scan.state,
|
|
294
|
-
})
|
|
295
|
-
// Scan preparation is not a claim. Re-read operator control immediately
|
|
296
|
-
// before atomically committing the immutable snapshot.
|
|
297
|
-
await this.controlBoundary()
|
|
298
|
-
initial = 'started'
|
|
299
|
-
await store.appendRepoWithArtifacts(
|
|
300
|
-
repo,
|
|
301
|
-
[
|
|
302
|
-
{
|
|
303
|
-
kind: HARVEST_SCAN_ARTIFACT,
|
|
304
|
-
content: `${JSON.stringify(packet, null, 2)}\n`,
|
|
305
|
-
metadata: { run: runId, observations: packet.observations.length },
|
|
306
|
-
},
|
|
307
|
-
],
|
|
308
|
-
(deposited) => ({
|
|
309
|
-
actor: KERNEL,
|
|
310
|
-
type: 'harvest.started',
|
|
311
|
-
payload: {
|
|
312
|
-
run: runId,
|
|
313
|
-
observations: packet.observations.map((item) => item.occurrence),
|
|
314
|
-
scan: artifactRef(deposited[0]!),
|
|
315
|
-
trigger: confirmed.trigger,
|
|
316
|
-
...(this.deps.environment !== undefined
|
|
317
|
-
? { environment: this.deps.environment }
|
|
318
|
-
: {}),
|
|
319
|
-
},
|
|
320
|
-
}),
|
|
321
|
-
)
|
|
322
|
-
// Bounded read (AUT-489): reduceHarvest consumes durable types only.
|
|
323
|
-
events = await store.getRepoStateEvents(repo)
|
|
324
|
-
run = reduceHarvest(events).runs.find((candidate) => candidate.run === runId)
|
|
325
|
-
if (!run) throw new Error(`harvest.started did not reduce run "${runId}"`)
|
|
326
|
-
// A request racing the claim parks this exact run; its snapshot stays
|
|
327
|
-
// open and is resumed rather than scanned again.
|
|
328
|
-
await this.controlBoundary(run.run)
|
|
329
348
|
}
|
|
330
|
-
|
|
349
|
+
if (!run) throw new Error('prepared harvest run unavailable')
|
|
350
|
+
if (preparationOnly) return { outcome: 'prepared', run: run.run, launch: initial }
|
|
331
351
|
try {
|
|
332
352
|
await this.ensureScanStep(run)
|
|
333
353
|
await this.controlBoundary(run.run)
|
|
@@ -691,7 +711,7 @@ export class HarvestRunner {
|
|
|
691
711
|
live.handle,
|
|
692
712
|
`Revise harvest proposals for round ${spec.round}: run ab harvest context, address .ab/findings.json, then submit.`,
|
|
693
713
|
{
|
|
694
|
-
env: this.sessionEnv(spec.run, spec.step, spec.round, session),
|
|
714
|
+
env: await this.sessionEnv(spec.run, spec.step, spec.round, session),
|
|
695
715
|
...(stream !== undefined ? { stream } : {}),
|
|
696
716
|
},
|
|
697
717
|
)
|
|
@@ -702,7 +722,7 @@ export class HarvestRunner {
|
|
|
702
722
|
workspacePath,
|
|
703
723
|
...(target.model !== undefined ? { model: target.model } : {}),
|
|
704
724
|
...(target.args !== undefined ? { args: target.args } : {}),
|
|
705
|
-
env: this.sessionEnv(spec.run, spec.step, spec.round, session),
|
|
725
|
+
env: await this.sessionEnv(spec.run, spec.step, spec.round, session),
|
|
706
726
|
...(stream !== undefined ? { stream } : {}),
|
|
707
727
|
})
|
|
708
728
|
handle = turn.session
|
|
@@ -941,7 +961,7 @@ export class HarvestRunner {
|
|
|
941
961
|
live.handle,
|
|
942
962
|
`Revise harvest proposals for round ${spec.round}: run ab harvest context, address .ab/findings.json, then submit.`,
|
|
943
963
|
{
|
|
944
|
-
env: this.sessionEnv(spec.run, spec.step, spec.round, session),
|
|
964
|
+
env: await this.sessionEnv(spec.run, spec.step, spec.round, session),
|
|
945
965
|
...(stream !== undefined ? { stream } : {}),
|
|
946
966
|
},
|
|
947
967
|
)
|
|
@@ -952,7 +972,7 @@ export class HarvestRunner {
|
|
|
952
972
|
workspacePath,
|
|
953
973
|
...(resolved.model !== undefined ? { model: resolved.model } : {}),
|
|
954
974
|
...(resolved.args !== undefined ? { args: resolved.args } : {}),
|
|
955
|
-
env: this.sessionEnv(spec.run, spec.step, spec.round, session),
|
|
975
|
+
env: await this.sessionEnv(spec.run, spec.step, spec.round, session),
|
|
956
976
|
...(stream !== undefined ? { stream } : {}),
|
|
957
977
|
})
|
|
958
978
|
handle = turn.session
|
|
@@ -1158,9 +1178,16 @@ export class HarvestRunner {
|
|
|
1158
1178
|
)
|
|
1159
1179
|
const packet = await loadScanPacket(store, repo, current.scan)
|
|
1160
1180
|
const observations = packet.observations
|
|
1161
|
-
const
|
|
1162
|
-
|
|
1163
|
-
)
|
|
1181
|
+
const identity = ticketIdentity(tickets)
|
|
1182
|
+
const knownLedger = new Map<string, (typeof packet.ledger)[number]>()
|
|
1183
|
+
for (const entry of packet.ledger) {
|
|
1184
|
+
const current =
|
|
1185
|
+
entry.ticket.source === tickets.name ? await tickets.get(entry.ticket.id) : null
|
|
1186
|
+
knownLedger.set(await identity(entry.ticket), {
|
|
1187
|
+
...entry,
|
|
1188
|
+
ticket: current?.ref ?? entry.ticket,
|
|
1189
|
+
})
|
|
1190
|
+
}
|
|
1164
1191
|
const alreadyFiled = new Map(current.filed.map((entry) => [entry.proposalKey, entry]))
|
|
1165
1192
|
const reservations = new Map(current.reservations.map((entry) => [entry.proposalKey, entry.id]))
|
|
1166
1193
|
const dispositions: HarvestDisposition[] = []
|
|
@@ -1240,7 +1267,7 @@ export class HarvestRunner {
|
|
|
1240
1267
|
...(filed.blockers !== undefined ? { blockers: filed.blockers } : {}),
|
|
1241
1268
|
})
|
|
1242
1269
|
} else if (proposal.action === 'join') {
|
|
1243
|
-
const known = knownLedger.get(
|
|
1270
|
+
const known = knownLedger.get(await identity(proposal.ticket))
|
|
1244
1271
|
if (!known) {
|
|
1245
1272
|
throw new Error(
|
|
1246
1273
|
`approved join target ${proposal.ticket.source}:${proposal.ticket.id} is not in the ledger context`,
|
|
@@ -1391,6 +1418,7 @@ export class HarvestRunner {
|
|
|
1391
1418
|
* give-up before returning. Initial/within-step failures do not spend this
|
|
1392
1419
|
* outer budget and therefore remain recoverable. */
|
|
1393
1420
|
private async settleRecoveryExhaustion(run: string): Promise<void> {
|
|
1421
|
+
if (this.deps.preparedRun) return
|
|
1394
1422
|
await this.ensureLease()
|
|
1395
1423
|
// Bounded read (AUT-489): reduceHarvest consumes durable types only.
|
|
1396
1424
|
const state = reduceHarvest(await this.deps.store.getRepoStateEvents(this.deps.repo))
|
|
@@ -1459,6 +1487,7 @@ export class HarvestRunner {
|
|
|
1459
1487
|
const state = reduceHarvest(await this.deps.store.getRepoStateEvents(this.deps.repo))
|
|
1460
1488
|
const decision = decideHarvestControl(state, this.maxRecoveryAttempts)
|
|
1461
1489
|
if (decision.kind === 'proceed') return
|
|
1490
|
+
if (this.deps.preparedRun) throw new HarvestParkedSignal(run)
|
|
1462
1491
|
if (decision.kind === 'park') {
|
|
1463
1492
|
throw new HarvestParkedSignal(actionableHarvestRun(state)?.run ?? run)
|
|
1464
1493
|
}
|
|
@@ -1602,15 +1631,37 @@ export class HarvestRunner {
|
|
|
1602
1631
|
}
|
|
1603
1632
|
}
|
|
1604
1633
|
|
|
1605
|
-
private sessionEnv(
|
|
1634
|
+
private async sessionEnv(
|
|
1606
1635
|
run: string,
|
|
1607
1636
|
step: 'synthesize' | 'review',
|
|
1608
1637
|
round: number,
|
|
1609
1638
|
session: string,
|
|
1610
|
-
): Record<string, string
|
|
1639
|
+
): Promise<Record<string, string>> {
|
|
1640
|
+
const parent = this.deps.sessionEnv
|
|
1641
|
+
const token =
|
|
1642
|
+
parent?.AB_STORE && /^https?:/i.test(parent.AB_STORE)
|
|
1643
|
+
? await delegateCredential(parent.AB_STORE, parent.AB_TOKEN, {
|
|
1644
|
+
kind: 'harvest-phase',
|
|
1645
|
+
repo: this.deps.repo,
|
|
1646
|
+
id: run,
|
|
1647
|
+
session,
|
|
1648
|
+
})
|
|
1649
|
+
: undefined
|
|
1650
|
+
const ticketGrant =
|
|
1651
|
+
parent?.AB_TICKET_URL && parent.AB_TICKET_TOKEN
|
|
1652
|
+
? await issueTicketGrant(
|
|
1653
|
+
parent.AB_TICKET_URL,
|
|
1654
|
+
parent.AB_TICKET_TOKEN,
|
|
1655
|
+
this.deps.repo,
|
|
1656
|
+
this.deps.instance,
|
|
1657
|
+
'phase',
|
|
1658
|
+
)
|
|
1659
|
+
: undefined
|
|
1611
1660
|
return {
|
|
1612
1661
|
AB_STORE: 'local',
|
|
1613
1662
|
...this.deps.sessionEnv,
|
|
1663
|
+
...(ticketGrant ? { AB_TICKET_TOKEN: ticketGrant.token } : {}),
|
|
1664
|
+
...(token ? { AB_TOKEN: token } : {}),
|
|
1614
1665
|
AB_REPO: this.deps.repo,
|
|
1615
1666
|
AB_HARVEST: run,
|
|
1616
1667
|
AB_PHASE: `${step}@${round}`,
|
|
@@ -1638,7 +1689,18 @@ export class HarvestRunner {
|
|
|
1638
1689
|
const open = target.openSessionStream
|
|
1639
1690
|
if (open === undefined) return undefined
|
|
1640
1691
|
const sink = createSessionStreamSink({
|
|
1641
|
-
store:
|
|
1692
|
+
store: {
|
|
1693
|
+
...this.deps.store,
|
|
1694
|
+
createStream: (scope, label) =>
|
|
1695
|
+
this.deps.store.createStream(
|
|
1696
|
+
scope,
|
|
1697
|
+
this.deps.preparedRun
|
|
1698
|
+
? `harvest:${this.deps.preparedRun.run}:${this.deps.instance}:${label}`
|
|
1699
|
+
: label,
|
|
1700
|
+
),
|
|
1701
|
+
appendStreamParts: (id, parts) => this.deps.store.appendStreamParts(id, parts),
|
|
1702
|
+
closeStream: (id, outcome) => this.deps.store.closeStream(id, outcome),
|
|
1703
|
+
},
|
|
1642
1704
|
scope: { kind: 'repo', repo: this.deps.repo },
|
|
1643
1705
|
onDiagnostic: this.onDiagnostic,
|
|
1644
1706
|
})
|
|
@@ -327,12 +327,17 @@ export async function reconcileHarvestLedger(
|
|
|
327
327
|
)
|
|
328
328
|
if (entries.length === 0) return []
|
|
329
329
|
|
|
330
|
-
const ids = entries
|
|
330
|
+
const ids = entries
|
|
331
|
+
.filter((entry) => entry.ticket.source === tickets.name)
|
|
332
|
+
.map((entry) => entry.ticket.id)
|
|
331
333
|
const states = await tickets.dependencyStates(ids)
|
|
334
|
+
const byId = new Map(states.map((state) => [state.id, state]))
|
|
332
335
|
const out: HarvestLedgerTicket[] = []
|
|
333
336
|
for (let index = 0; index < entries.length; index += 1) {
|
|
334
337
|
const entry = entries[index]!
|
|
335
|
-
const dependency =
|
|
338
|
+
const dependency = (entry.ticket.source === tickets.name
|
|
339
|
+
? byId.get(entry.ticket.id)
|
|
340
|
+
: undefined) ?? {
|
|
336
341
|
id: entry.ticket.id,
|
|
337
342
|
exists: false,
|
|
338
343
|
resolved: false,
|
|
@@ -341,7 +346,7 @@ export async function reconcileHarvestLedger(
|
|
|
341
346
|
const current = dependency.exists ? await tickets.get(entry.ticket.id) : null
|
|
342
347
|
out.push({
|
|
343
348
|
proposalKey: entry.proposalKey,
|
|
344
|
-
ticket: structuredClone(entry.ticket),
|
|
349
|
+
ticket: structuredClone(current?.ref ?? entry.ticket),
|
|
345
350
|
exists: dependency.exists,
|
|
346
351
|
resolved: dependency.resolved,
|
|
347
352
|
...(current?.title !== undefined ? { title: current.title } : {}),
|
|
@@ -371,15 +376,25 @@ export async function reconcileOriginatingTickets(
|
|
|
371
376
|
const states = matchingIds.length > 0 ? await tickets.dependencyStates(matchingIds) : []
|
|
372
377
|
const byId = new Map(states.map((state) => [state.id, state]))
|
|
373
378
|
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
ticket
|
|
379
|
-
sourceMatches
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
379
|
+
const identity = new Set<string>()
|
|
380
|
+
const projected = await Promise.all(
|
|
381
|
+
refs.map(async (ticket) => {
|
|
382
|
+
const sourceMatches = ticket.source === tickets.name
|
|
383
|
+
const state = sourceMatches ? byId.get(ticket.id) : undefined
|
|
384
|
+
const current = sourceMatches && state?.exists ? await tickets.get(ticket.id) : null
|
|
385
|
+
return {
|
|
386
|
+
ticket: current?.ref ?? ticket,
|
|
387
|
+
sourceMatches,
|
|
388
|
+
exists: state?.exists ?? false,
|
|
389
|
+
resolved: state?.resolved ?? false,
|
|
390
|
+
}
|
|
391
|
+
}),
|
|
392
|
+
)
|
|
393
|
+
return projected.filter((entry) => {
|
|
394
|
+
const key = `${entry.ticket.source}:${entry.ticket.id}`
|
|
395
|
+
if (identity.has(key)) return false
|
|
396
|
+
identity.add(key)
|
|
397
|
+
return true
|
|
383
398
|
})
|
|
384
399
|
}
|
|
385
400
|
|
|
@@ -536,9 +551,14 @@ export async function resolveHarvestCreateBlockers(
|
|
|
536
551
|
const state = byId.get(id)
|
|
537
552
|
return state?.exists === true && !state.resolved
|
|
538
553
|
})
|
|
554
|
+
const canonical = new Map<string, string>()
|
|
555
|
+
for (const id of candidates) {
|
|
556
|
+
if (byId.get(id)?.exists) canonical.set(id, (await tickets.get(id))?.ref.id ?? id)
|
|
557
|
+
}
|
|
558
|
+
const project = (ids: string[]) => [...new Set(ids.map((id) => canonical.get(id) ?? id))]
|
|
539
559
|
return {
|
|
540
|
-
blockedBy:
|
|
541
|
-
provenance: { declared, derived },
|
|
560
|
+
blockedBy: project([...declared, ...derived]),
|
|
561
|
+
provenance: { declared: project(declared), derived: project(derived) },
|
|
542
562
|
}
|
|
543
563
|
}
|
|
544
564
|
|
|
@@ -11,6 +11,8 @@ export const POLICY_PHASE_FAILURE_RESET_SCOPES = {
|
|
|
11
11
|
'review-round-limit': 'matching-round',
|
|
12
12
|
'verify-failure-limit': 'all-phase-rounds',
|
|
13
13
|
'reconcile-no-progress': 'none',
|
|
14
|
+
'dispatch-failure-limit': 'none',
|
|
15
|
+
'execution-no-progress': 'none',
|
|
14
16
|
'setup-failure-limit': 'none',
|
|
15
17
|
'infrastructure-failure-limit': 'none',
|
|
16
18
|
'phase-attempt-limit': 'matching-round',
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { summaryBoundary } from '../kernel/pr-summary'
|
|
2
|
+
import { isRemoteWorkspace } from '../events/workspace-remote'
|
|
1
3
|
/**
|
|
2
4
|
* Last-chance publication settlement at the workspace-release boundary
|
|
3
5
|
* (AUT-328). A pending `publication.requested` whose workspace is destroyed
|
|
@@ -37,7 +39,22 @@ export async function settlePublicationBeforeRelease(
|
|
|
37
39
|
reason: string,
|
|
38
40
|
): Promise<AbEvent[]> {
|
|
39
41
|
const request = latestUncompletedPublicationRequest(events)
|
|
40
|
-
if (request === undefined)
|
|
42
|
+
if (request === undefined) {
|
|
43
|
+
const workspace = events.findLast((event) => event.type === 'workspace.provisioned')
|
|
44
|
+
if (
|
|
45
|
+
workspace?.type === 'workspace.provisioned' &&
|
|
46
|
+
isRemoteWorkspace(workspace.payload) &&
|
|
47
|
+
summaryBoundary(events) !== undefined
|
|
48
|
+
) {
|
|
49
|
+
try {
|
|
50
|
+
await deps.settlePublication?.(slug)
|
|
51
|
+
} catch {
|
|
52
|
+
/* Summary is best effort. */
|
|
53
|
+
}
|
|
54
|
+
return deps.store.getEvents(slug)
|
|
55
|
+
}
|
|
56
|
+
return events
|
|
57
|
+
}
|
|
41
58
|
try {
|
|
42
59
|
await deps.settlePublication?.(slug)
|
|
43
60
|
} catch {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { executionNoProgressReducer } from './execution-no-progress'
|
|
1
2
|
/**
|
|
2
3
|
* The dispatcher's in-process repository view (AUT-647): a `BuildStore`
|
|
3
4
|
* decorator that builds the state a tick needs once, at the first refresh, and
|
|
@@ -154,6 +155,7 @@ const EXTRA_REDUCERS = {
|
|
|
154
155
|
finish: buildDigestReducer.finish,
|
|
155
156
|
},
|
|
156
157
|
logIndex: logIndexReducer,
|
|
158
|
+
executionNoProgress: executionNoProgressReducer,
|
|
157
159
|
openExecution: openExecutionReducer,
|
|
158
160
|
openBuildWorkspace: openBuildWorkspaceReducer,
|
|
159
161
|
publicationState: publicationStateReducer,
|
|
@@ -186,6 +188,7 @@ const WORK_BUNDLE_REDUCER = { version: WORK_BUNDLE_VERSION }
|
|
|
186
188
|
/** The facts a quiet-path stage needs about one build, without its event array. */
|
|
187
189
|
export interface BuildFacts {
|
|
188
190
|
state: BuildState
|
|
191
|
+
executionNoProgress: ReturnType<typeof executionNoProgressReducer.finish>
|
|
189
192
|
log: LogIndex
|
|
190
193
|
open: OpenExecution | null
|
|
191
194
|
/** The open workspace (the latest provisioned, not yet released). */
|
|
@@ -279,6 +282,37 @@ function retainedShape(retained: unknown[], cursor: number): boolean {
|
|
|
279
282
|
/** Structural checks on the small accumulators a later fold dereferences. */
|
|
280
283
|
function extraAccsShape(extra: ExtraAccs): boolean {
|
|
281
284
|
const field = (acc: unknown, key: string): unknown => (isObject(acc) ? acc[key] : undefined)
|
|
285
|
+
const emptyCount = field(extra.executionNoProgress, 'count')
|
|
286
|
+
const emptyOpen = field(extra.executionNoProgress, 'open')
|
|
287
|
+
if (
|
|
288
|
+
typeof emptyCount !== 'number' ||
|
|
289
|
+
!Number.isSafeInteger(emptyCount) ||
|
|
290
|
+
emptyCount < 0 ||
|
|
291
|
+
!Array.isArray(emptyOpen) ||
|
|
292
|
+
!emptyOpen.every((id) => typeof id === 'string')
|
|
293
|
+
)
|
|
294
|
+
return false
|
|
295
|
+
const emptyLatest = field(extra.executionNoProgress, 'latest')
|
|
296
|
+
if (
|
|
297
|
+
emptyLatest !== null &&
|
|
298
|
+
!(
|
|
299
|
+
isObject(emptyLatest) &&
|
|
300
|
+
typeof emptyLatest.instance === 'string' &&
|
|
301
|
+
typeof emptyLatest.seq === 'number'
|
|
302
|
+
)
|
|
303
|
+
)
|
|
304
|
+
return false
|
|
305
|
+
const active = field(extra.executionNoProgress, 'active')
|
|
306
|
+
if (
|
|
307
|
+
active !== null &&
|
|
308
|
+
!(
|
|
309
|
+
isObject(active) &&
|
|
310
|
+
typeof active.instance === 'string' &&
|
|
311
|
+
typeof active.seq === 'number' &&
|
|
312
|
+
typeof active.eligible === 'boolean'
|
|
313
|
+
)
|
|
314
|
+
)
|
|
315
|
+
return false
|
|
282
316
|
const open = field(extra.openExecution, 'open')
|
|
283
317
|
if (open !== null && !(isObject(open) && typeof open.instance === 'string')) return false
|
|
284
318
|
const workspace = field(extra.openBuildWorkspace, 'open')
|
|
@@ -914,6 +948,7 @@ export class RepoViewStore implements BuildStore {
|
|
|
914
948
|
const extra = log.extra!
|
|
915
949
|
return {
|
|
916
950
|
state: buildReducer.finish(log.acc!),
|
|
951
|
+
executionNoProgress: executionNoProgressReducer.finish(extra.executionNoProgress as never),
|
|
917
952
|
log: logIndexReducer.finish(extra.logIndex as never),
|
|
918
953
|
open: openExecutionReducer.finish(extra.openExecution as never),
|
|
919
954
|
workspace: openBuildWorkspaceReducer.finish(extra.openBuildWorkspace as never),
|