@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
|
@@ -48,8 +48,10 @@ import type { Config } from '../config/schema'
|
|
|
48
48
|
import type { AbEvent, EventWrite } from '../events/catalog'
|
|
49
49
|
import type { EventPayload } from '../events/payloads'
|
|
50
50
|
import { KERNEL, agentActor, type Actor } from '../events/envelope'
|
|
51
|
+
import { isRemoteWorkspace } from '../events/workspace-remote'
|
|
51
52
|
import type { IdSource } from '../ids'
|
|
52
53
|
import { decideNext, type Decision } from '../kernel/engine'
|
|
54
|
+
import { defineReducer } from '../kernel/incremental'
|
|
53
55
|
import { PHASE_SPECS } from '../kernel/phases'
|
|
54
56
|
import { reduceBuild, type BuildState } from '../kernel/reducer'
|
|
55
57
|
import { evaluateVerifyApplicability } from '../kernel/verify-applicability'
|
|
@@ -240,6 +242,12 @@ interface SessionSpec {
|
|
|
240
242
|
/** Longest tail of check output preserved in a verify report (§8.2). */
|
|
241
243
|
const REPORT_TAIL_CHARS = 10_000
|
|
242
244
|
|
|
245
|
+
/** Per-section cap for the finalize preflight's dirty-worktree diff capture:
|
|
246
|
+
* each diff view (working tree, staged) is truncated at this many characters
|
|
247
|
+
* so a large dirty diff cannot bloat the failure record unboundedly — worst
|
|
248
|
+
* case ≈ porcelain + 2 × (cap + header). */
|
|
249
|
+
const FINALIZE_DIFF_CAPTURE_CAP = 20_000
|
|
250
|
+
|
|
243
251
|
/**
|
|
244
252
|
* One session bracket's live stream (SPEC §9): the sink the build-runner
|
|
245
253
|
* opened through the resolved target's streaming capability, the store id it
|
|
@@ -260,6 +268,14 @@ function errorMessage(error: unknown): string {
|
|
|
260
268
|
return error instanceof Error ? error.message : String(error)
|
|
261
269
|
}
|
|
262
270
|
|
|
271
|
+
/** Bound one captured diff section: unchanged within the cap, otherwise the
|
|
272
|
+
* first FINALIZE_DIFF_CAPTURE_CAP characters plus an explicit truncation
|
|
273
|
+
* marker, so truncation is visible in the record itself. */
|
|
274
|
+
function capDiffOutput(output: string): string {
|
|
275
|
+
if (output.length <= FINALIZE_DIFF_CAPTURE_CAP) return output
|
|
276
|
+
return `${output.slice(0, FINALIZE_DIFF_CAPTURE_CAP)}\n... (truncated at ${FINALIZE_DIFF_CAPTURE_CAP} characters)`
|
|
277
|
+
}
|
|
278
|
+
|
|
263
279
|
function sessionBudgetError(seconds: number): string {
|
|
264
280
|
return `phase session budget expired after ${seconds} seconds`
|
|
265
281
|
}
|
|
@@ -299,22 +315,155 @@ function providerAttemptSummary(attempts: readonly ProviderAttempt[] | undefined
|
|
|
299
315
|
)
|
|
300
316
|
}
|
|
301
317
|
|
|
318
|
+
export interface PhaseFailureTally {
|
|
319
|
+
count: number
|
|
320
|
+
lastError?: string
|
|
321
|
+
lastWillRetry?: boolean
|
|
322
|
+
lastProviderAttempts?: readonly ProviderAttempt[]
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
export const PHASE_FAILURES_REDUCER_VERSION = 1
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* phase.failed tally for one phase+round — D5's retry-guard input. A factory
|
|
329
|
+
* because phase and round are parameters of the projection.
|
|
330
|
+
*
|
|
331
|
+
* An answered escalation re-arms this budget only when its semantic class
|
|
332
|
+
* acknowledges phase-runner failures (§15.6-B). Runner-failure causes reset
|
|
333
|
+
* their matching round, the phase-level verify cause resets all rounds, and
|
|
334
|
+
* unrelated policy causes reset nothing. Cause-less historical and
|
|
335
|
+
* non-policy raises retain the legacy round-shaped behavior. Failures after
|
|
336
|
+
* an applicable answer count again, so a still-failing phase re-escalates on
|
|
337
|
+
* new evidence instead of deadlocking before its recovery session starts.
|
|
338
|
+
*/
|
|
339
|
+
export function phaseFailuresReducer(phase: Phase, round: number) {
|
|
340
|
+
return defineReducer<
|
|
341
|
+
{
|
|
342
|
+
/** id → the raise's reset semantics — answers are matched by id (§15.3). */
|
|
343
|
+
raised: Record<string, PhaseFailureResetEscalation>
|
|
344
|
+
count: number
|
|
345
|
+
lastError?: string
|
|
346
|
+
lastWillRetry?: boolean
|
|
347
|
+
lastProviderAttempts?: readonly ProviderAttempt[]
|
|
348
|
+
},
|
|
349
|
+
AbEvent,
|
|
350
|
+
PhaseFailureTally
|
|
351
|
+
>({
|
|
352
|
+
version: PHASE_FAILURES_REDUCER_VERSION,
|
|
353
|
+
initial: () => ({ raised: {}, count: 0 }),
|
|
354
|
+
fold(acc, events) {
|
|
355
|
+
for (const event of events) {
|
|
356
|
+
switch (event.type) {
|
|
357
|
+
case 'escalation.raised':
|
|
358
|
+
acc.raised[event.payload.id] = {
|
|
359
|
+
phase: event.payload.phase,
|
|
360
|
+
source: event.payload.source,
|
|
361
|
+
...(event.payload.policyCause !== undefined
|
|
362
|
+
? { policyCause: event.payload.policyCause }
|
|
363
|
+
: {}),
|
|
364
|
+
...(event.payload.round !== undefined ? { round: event.payload.round } : {}),
|
|
365
|
+
}
|
|
366
|
+
break
|
|
367
|
+
case 'escalation.answered': {
|
|
368
|
+
const raise = acc.raised[event.payload.id]
|
|
369
|
+
if (raise !== undefined && resetsPhaseFailureBudget(raise, phase, round)) {
|
|
370
|
+
acc.count = 0
|
|
371
|
+
delete acc.lastError
|
|
372
|
+
delete acc.lastWillRetry
|
|
373
|
+
delete acc.lastProviderAttempts
|
|
374
|
+
}
|
|
375
|
+
break
|
|
376
|
+
}
|
|
377
|
+
case 'phase.failed':
|
|
378
|
+
if (event.payload.phase === phase && event.payload.round === round) {
|
|
379
|
+
acc.count += 1
|
|
380
|
+
acc.lastError = event.payload.error
|
|
381
|
+
acc.lastWillRetry = event.payload.willRetry
|
|
382
|
+
acc.lastProviderAttempts = event.payload.providerAttempts
|
|
383
|
+
}
|
|
384
|
+
break
|
|
385
|
+
default:
|
|
386
|
+
break
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
},
|
|
390
|
+
finish: (acc) => ({
|
|
391
|
+
count: acc.count,
|
|
392
|
+
...(acc.lastError !== undefined ? { lastError: acc.lastError } : {}),
|
|
393
|
+
...(acc.lastWillRetry !== undefined ? { lastWillRetry: acc.lastWillRetry } : {}),
|
|
394
|
+
...(acc.lastProviderAttempts !== undefined
|
|
395
|
+
? { lastProviderAttempts: acc.lastProviderAttempts }
|
|
396
|
+
: {}),
|
|
397
|
+
}),
|
|
398
|
+
})
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
export const SETUP_STREAK_REDUCER_VERSION = 1
|
|
402
|
+
|
|
403
|
+
/** Consecutive durable setup failures since the latest successful attachment
|
|
404
|
+
* or explicit human answer. The accumulator keeps the boundary seq and the
|
|
405
|
+
* failure seqs, so it is exact for any array order. */
|
|
406
|
+
export const setupStreakReducer = defineReducer<
|
|
407
|
+
{ escalations: string[]; boundary: number; failures: number[] },
|
|
408
|
+
AbEvent,
|
|
409
|
+
number
|
|
410
|
+
>({
|
|
411
|
+
version: SETUP_STREAK_REDUCER_VERSION,
|
|
412
|
+
initial: () => ({ escalations: [], boundary: 0, failures: [] }),
|
|
413
|
+
fold(acc, events) {
|
|
414
|
+
for (const event of events) {
|
|
415
|
+
if (event.type === 'runner.attached') acc.boundary = event.seq
|
|
416
|
+
else if (
|
|
417
|
+
event.type === 'escalation.raised' &&
|
|
418
|
+
event.payload.phase === 'setup' &&
|
|
419
|
+
event.payload.source === 'policy'
|
|
420
|
+
) {
|
|
421
|
+
if (!acc.escalations.includes(event.payload.id)) acc.escalations.push(event.payload.id)
|
|
422
|
+
} else if (
|
|
423
|
+
event.type === 'escalation.answered' &&
|
|
424
|
+
acc.escalations.includes(event.payload.id)
|
|
425
|
+
) {
|
|
426
|
+
acc.boundary = event.seq
|
|
427
|
+
} else if (event.type === 'runner.setup-failed') {
|
|
428
|
+
acc.failures.push(event.seq)
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
},
|
|
432
|
+
finish: (acc) => acc.failures.filter((seq) => seq > acc.boundary).length,
|
|
433
|
+
})
|
|
434
|
+
|
|
435
|
+
export const PUBLISHED_BRANCH_HEAD_REDUCER_VERSION = 1
|
|
436
|
+
|
|
437
|
+
/** Latest event-checkpointed branch head that kernel plumbing published;
|
|
438
|
+
* undefined while the build has none. */
|
|
439
|
+
export const publishedBranchHeadReducer = defineReducer<
|
|
440
|
+
{ head?: string },
|
|
441
|
+
AbEvent,
|
|
442
|
+
string | undefined
|
|
443
|
+
>({
|
|
444
|
+
version: PUBLISHED_BRANCH_HEAD_REDUCER_VERSION,
|
|
445
|
+
initial: () => ({}),
|
|
446
|
+
fold(acc, events) {
|
|
447
|
+
for (const event of events) {
|
|
448
|
+
if (event.type === 'implement.completed') {
|
|
449
|
+
acc.head = event.payload.commits.head
|
|
450
|
+
} else if (event.type === 'reconcile.completed') {
|
|
451
|
+
acc.head = event.payload.mergeCommit
|
|
452
|
+
} else if (
|
|
453
|
+
event.type === 'finalize.step-completed' &&
|
|
454
|
+
event.payload.ok &&
|
|
455
|
+
event.payload.headSha !== undefined
|
|
456
|
+
) {
|
|
457
|
+
acc.head = event.payload.headSha
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
},
|
|
461
|
+
finish: (acc) => acc.head,
|
|
462
|
+
})
|
|
463
|
+
|
|
302
464
|
/** Latest event-checkpointed branch head that kernel plumbing published. */
|
|
303
465
|
export function selectPublishedBranchHead(events: readonly AbEvent[]): string {
|
|
304
|
-
|
|
305
|
-
for (const event of events) {
|
|
306
|
-
if (event.type === 'implement.completed') {
|
|
307
|
-
head = event.payload.commits.head
|
|
308
|
-
} else if (event.type === 'reconcile.completed') {
|
|
309
|
-
head = event.payload.mergeCommit
|
|
310
|
-
} else if (
|
|
311
|
-
event.type === 'finalize.step-completed' &&
|
|
312
|
-
event.payload.ok &&
|
|
313
|
-
event.payload.headSha !== undefined
|
|
314
|
-
) {
|
|
315
|
-
head = event.payload.headSha
|
|
316
|
-
}
|
|
317
|
-
}
|
|
466
|
+
const head = publishedBranchHeadReducer.reduce(events)
|
|
318
467
|
if (head === undefined) {
|
|
319
468
|
throw new Error(
|
|
320
469
|
'finalize publication requires a prior implement.completed, reconcile.completed, ' +
|
|
@@ -324,6 +473,37 @@ export function selectPublishedBranchHead(events: readonly AbEvent[]): string {
|
|
|
324
473
|
return head
|
|
325
474
|
}
|
|
326
475
|
|
|
476
|
+
export const VERIFY_DIFF_BASE_REDUCER_VERSION = 1
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* The durable tree against which a conditional verifier evaluates the
|
|
480
|
+
* branch's current HEAD, or undefined until a `workspace.provisioned` exists.
|
|
481
|
+
* The accumulator keeps the first provisioned base and the latest promoted
|
|
482
|
+
* reconcile base separately, so the result is exact for any array order (a
|
|
483
|
+
* promotion that precedes the provisioned fact in the array still wins).
|
|
484
|
+
*/
|
|
485
|
+
export const verifyDiffBaseReducer = defineReducer<
|
|
486
|
+
{ provisioned?: string; promoted?: string; pendingReconcileBase?: string },
|
|
487
|
+
AbEvent,
|
|
488
|
+
string | undefined
|
|
489
|
+
>({
|
|
490
|
+
version: VERIFY_DIFF_BASE_REDUCER_VERSION,
|
|
491
|
+
initial: () => ({}),
|
|
492
|
+
fold(acc, events) {
|
|
493
|
+
for (const event of events) {
|
|
494
|
+
if (event.type === 'workspace.provisioned') {
|
|
495
|
+
if (acc.provisioned === undefined) acc.provisioned = event.payload.base.sha
|
|
496
|
+
} else if (event.type === 'reconcile.started') {
|
|
497
|
+
acc.pendingReconcileBase = event.payload.baseSha
|
|
498
|
+
} else if (event.type === 'reconcile.completed') {
|
|
499
|
+
if (acc.pendingReconcileBase !== undefined) acc.promoted = acc.pendingReconcileBase
|
|
500
|
+
delete acc.pendingReconcileBase
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
},
|
|
504
|
+
finish: (acc) => (acc.provisioned === undefined ? undefined : (acc.promoted ?? acc.provisioned)),
|
|
505
|
+
})
|
|
506
|
+
|
|
327
507
|
/**
|
|
328
508
|
* Select the durable tree against which a conditional verifier evaluates the
|
|
329
509
|
* branch's current HEAD. The initial branch-cut SHA is authoritative until a
|
|
@@ -331,24 +511,13 @@ export function selectPublishedBranchHead(events: readonly AbEvent[]): string {
|
|
|
331
511
|
* refreshed base SHA. A dangling start is intentionally ignored.
|
|
332
512
|
*/
|
|
333
513
|
export function selectVerifyDiffBase(events: readonly AbEvent[]): string {
|
|
334
|
-
const
|
|
335
|
-
if (
|
|
514
|
+
const base = verifyDiffBaseReducer.reduce(events)
|
|
515
|
+
if (base === undefined) {
|
|
336
516
|
throw new Error(
|
|
337
517
|
'conditional verify requires a workspace.provisioned base SHA, but this build has none',
|
|
338
518
|
)
|
|
339
519
|
}
|
|
340
|
-
|
|
341
|
-
let baseSha = provisioned.payload.base.sha
|
|
342
|
-
let pendingReconcileBase: string | undefined
|
|
343
|
-
for (const event of events) {
|
|
344
|
-
if (event.type === 'reconcile.started') {
|
|
345
|
-
pendingReconcileBase = event.payload.baseSha
|
|
346
|
-
} else if (event.type === 'reconcile.completed') {
|
|
347
|
-
if (pendingReconcileBase !== undefined) baseSha = pendingReconcileBase
|
|
348
|
-
pendingReconcileBase = undefined
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
return baseSha
|
|
520
|
+
return base
|
|
352
521
|
}
|
|
353
522
|
|
|
354
523
|
/** Parse `git diff --name-only -z`; malformed output fails closed. */
|
|
@@ -468,22 +637,7 @@ export class BuildRunner {
|
|
|
468
637
|
* or explicit human answer. The answer re-arms the budget but deliberately
|
|
469
638
|
* does not erase the current diagnostic while recovery is still running. */
|
|
470
639
|
private setupStreak(events: readonly AbEvent[]): number {
|
|
471
|
-
|
|
472
|
-
let boundary = 0
|
|
473
|
-
for (const event of events) {
|
|
474
|
-
if (event.type === 'runner.attached') boundary = event.seq
|
|
475
|
-
else if (
|
|
476
|
-
event.type === 'escalation.raised' &&
|
|
477
|
-
event.payload.phase === 'setup' &&
|
|
478
|
-
event.payload.source === 'policy'
|
|
479
|
-
) {
|
|
480
|
-
setupEscalations.add(event.payload.id)
|
|
481
|
-
} else if (event.type === 'escalation.answered' && setupEscalations.has(event.payload.id)) {
|
|
482
|
-
boundary = event.seq
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
return events.filter((event) => event.type === 'runner.setup-failed' && event.seq > boundary)
|
|
486
|
-
.length
|
|
640
|
+
return setupStreakReducer.reduce(events)
|
|
487
641
|
}
|
|
488
642
|
|
|
489
643
|
private setupQuestion(failure: EventPayload<'runner.setup-failed'>): string {
|
|
@@ -1137,8 +1291,7 @@ export class BuildRunner {
|
|
|
1137
1291
|
)
|
|
1138
1292
|
if (scratchPaths.length > 0) throw phaseScratchRejection(scratchPaths)
|
|
1139
1293
|
const remote = events.some(
|
|
1140
|
-
(event) =>
|
|
1141
|
-
event.type === 'workspace.provisioned' && event.payload.provider === 'vercel-sandbox',
|
|
1294
|
+
(event) => event.type === 'workspace.provisioned' && isRemoteWorkspace(event.payload),
|
|
1142
1295
|
)
|
|
1143
1296
|
if (remote) {
|
|
1144
1297
|
await this.deps.store.append(this.deps.slug, {
|
|
@@ -1490,13 +1643,50 @@ export class BuildRunner {
|
|
|
1490
1643
|
)
|
|
1491
1644
|
}
|
|
1492
1645
|
if (result.stdout.length > 0) {
|
|
1646
|
+
// The failure record must characterize the dirtiness on its own: the
|
|
1647
|
+
// build workspace is disposable, and porcelain lists paths and status
|
|
1648
|
+
// codes only — a mode-only flip (AUT-439) is indistinguishable from a
|
|
1649
|
+
// content modification once the workspace is gone. Append both diff
|
|
1650
|
+
// views so the record shows what actually changed.
|
|
1493
1651
|
throw new Error(
|
|
1494
1652
|
`workspace must be clean ${when}; commit intended files and remove ` +
|
|
1495
|
-
`temporary files before finishing:\n${result.stdout.trimEnd()}
|
|
1653
|
+
`temporary files before finishing:\n${result.stdout.trimEnd()}\n\n` +
|
|
1654
|
+
(await this.collectWorktreeDiffSections()),
|
|
1496
1655
|
)
|
|
1497
1656
|
}
|
|
1498
1657
|
}
|
|
1499
1658
|
|
|
1659
|
+
/** Labeled working-tree and staged diff sections for a dirty finalize
|
|
1660
|
+
* preflight record. Runs only on the dirty branch (a clean worktree or a
|
|
1661
|
+
* failed status command never executes a diff), caps each section at
|
|
1662
|
+
* FINALIZE_DIFF_CAPTURE_CAP characters, and is best-effort: a failing `git
|
|
1663
|
+
* diff` command annotates its own section instead of masking the
|
|
1664
|
+
* dirty-worktree failure it is annotating. */
|
|
1665
|
+
private async collectWorktreeDiffSections(): Promise<string> {
|
|
1666
|
+
const views = [
|
|
1667
|
+
{ args: ['git', 'diff'], header: '--- git diff (working tree) ---' },
|
|
1668
|
+
{ args: ['git', 'diff', '--cached'], header: '--- git diff --cached (staged) ---' },
|
|
1669
|
+
] as const
|
|
1670
|
+
const sections: string[] = []
|
|
1671
|
+
for (const { args, header } of views) {
|
|
1672
|
+
const command = args.join(' ')
|
|
1673
|
+
let body: string
|
|
1674
|
+
try {
|
|
1675
|
+
const result = await this.deps.exec([...args], { cwd: this.deps.workspacePath })
|
|
1676
|
+
if (result.exitCode !== 0) {
|
|
1677
|
+
const detail = result.stderr.trim() || result.stdout.trim() || '(no output)'
|
|
1678
|
+
body = `(${command} failed: exit ${result.exitCode}: ${capDiffOutput(detail)})`
|
|
1679
|
+
} else {
|
|
1680
|
+
body = capDiffOutput(result.stdout) || '(no output)'
|
|
1681
|
+
}
|
|
1682
|
+
} catch (error) {
|
|
1683
|
+
body = `(${command} failed: ${capDiffOutput(errorMessage(error))})`
|
|
1684
|
+
}
|
|
1685
|
+
sections.push(`${header}\n${body}`)
|
|
1686
|
+
}
|
|
1687
|
+
return sections.join('\n\n')
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1500
1690
|
private async resolveFinalizeHead(): Promise<string> {
|
|
1501
1691
|
const args = ['git', 'rev-parse', 'HEAD']
|
|
1502
1692
|
const result = await this.deps.exec(args, { cwd: this.deps.workspacePath })
|
|
@@ -2401,52 +2591,7 @@ export class BuildRunner {
|
|
|
2401
2591
|
lastWillRetry?: boolean
|
|
2402
2592
|
lastProviderAttempts?: readonly ProviderAttempt[]
|
|
2403
2593
|
} {
|
|
2404
|
-
|
|
2405
|
-
const raised = new Map<string, PhaseFailureResetEscalation>()
|
|
2406
|
-
let count = 0
|
|
2407
|
-
let lastError: string | undefined
|
|
2408
|
-
let lastWillRetry: boolean | undefined
|
|
2409
|
-
let lastProviderAttempts: readonly ProviderAttempt[] | undefined
|
|
2410
|
-
for (const event of events) {
|
|
2411
|
-
switch (event.type) {
|
|
2412
|
-
case 'escalation.raised':
|
|
2413
|
-
raised.set(event.payload.id, {
|
|
2414
|
-
phase: event.payload.phase,
|
|
2415
|
-
source: event.payload.source,
|
|
2416
|
-
...(event.payload.policyCause !== undefined
|
|
2417
|
-
? { policyCause: event.payload.policyCause }
|
|
2418
|
-
: {}),
|
|
2419
|
-
...(event.payload.round !== undefined ? { round: event.payload.round } : {}),
|
|
2420
|
-
})
|
|
2421
|
-
break
|
|
2422
|
-
case 'escalation.answered': {
|
|
2423
|
-
const raise = raised.get(event.payload.id)
|
|
2424
|
-
if (raise !== undefined && resetsPhaseFailureBudget(raise, phase, round)) {
|
|
2425
|
-
count = 0
|
|
2426
|
-
lastError = undefined
|
|
2427
|
-
lastWillRetry = undefined
|
|
2428
|
-
lastProviderAttempts = undefined
|
|
2429
|
-
}
|
|
2430
|
-
break
|
|
2431
|
-
}
|
|
2432
|
-
case 'phase.failed':
|
|
2433
|
-
if (event.payload.phase === phase && event.payload.round === round) {
|
|
2434
|
-
count += 1
|
|
2435
|
-
lastError = event.payload.error
|
|
2436
|
-
lastWillRetry = event.payload.willRetry
|
|
2437
|
-
lastProviderAttempts = event.payload.providerAttempts
|
|
2438
|
-
}
|
|
2439
|
-
break
|
|
2440
|
-
default:
|
|
2441
|
-
break
|
|
2442
|
-
}
|
|
2443
|
-
}
|
|
2444
|
-
return {
|
|
2445
|
-
count,
|
|
2446
|
-
...(lastError !== undefined ? { lastError } : {}),
|
|
2447
|
-
...(lastWillRetry !== undefined ? { lastWillRetry } : {}),
|
|
2448
|
-
...(lastProviderAttempts !== undefined ? { lastProviderAttempts } : {}),
|
|
2449
|
-
}
|
|
2594
|
+
return phaseFailuresReducer(phase, round).reduce(events)
|
|
2450
2595
|
}
|
|
2451
2596
|
|
|
2452
2597
|
private async failPhase(
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure event-log projections the dispatcher and janitor read, extracted from
|
|
3
|
+
* `dispatcher.ts` so each is an incremental reducer (see
|
|
4
|
+
* `kernel/incremental.ts`). The dispatcher's private helpers delegate here.
|
|
5
|
+
*/
|
|
6
|
+
import type { AbEvent } from '../events/catalog'
|
|
7
|
+
import type { EventPayload } from '../events/payloads'
|
|
8
|
+
import { defineReducer } from '../kernel/incremental'
|
|
9
|
+
|
|
10
|
+
export type OpenBuildWorkspace = EventPayload<'workspace.provisioned'>
|
|
11
|
+
|
|
12
|
+
export const OPEN_BUILD_WORKSPACE_REDUCER_VERSION = 1
|
|
13
|
+
|
|
14
|
+
/** Latest `workspace.provisioned` not followed by a `workspace.released` —
|
|
15
|
+
* the reducer deliberately ignores workspace events (liveness is the
|
|
16
|
+
* dispatcher's concern), so the janitor scans the raw log. */
|
|
17
|
+
export const openBuildWorkspaceReducer = defineReducer<
|
|
18
|
+
{ open: OpenBuildWorkspace | null },
|
|
19
|
+
AbEvent,
|
|
20
|
+
OpenBuildWorkspace | null
|
|
21
|
+
>({
|
|
22
|
+
version: OPEN_BUILD_WORKSPACE_REDUCER_VERSION,
|
|
23
|
+
initial: () => ({ open: null }),
|
|
24
|
+
fold(acc, events) {
|
|
25
|
+
for (const event of events) {
|
|
26
|
+
if (event.type === 'workspace.provisioned') acc.open = event.payload
|
|
27
|
+
else if (event.type === 'workspace.released') acc.open = null
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
finish: (acc) => (acc.open === null ? null : { ...acc.open }),
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
export function openBuildWorkspace(events: readonly AbEvent[]): OpenBuildWorkspace | null {
|
|
34
|
+
return openBuildWorkspaceReducer.reduce(events)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const BASE_BRANCH_REDUCER_VERSION = 1
|
|
38
|
+
|
|
39
|
+
/** The base branch recorded on the FIRST `build.created` in array order, or
|
|
40
|
+
* undefined for logs missing one (callers fall back to the repo config). */
|
|
41
|
+
export const baseBranchReducer = defineReducer<
|
|
42
|
+
{ baseBranch?: string },
|
|
43
|
+
AbEvent,
|
|
44
|
+
string | undefined
|
|
45
|
+
>({
|
|
46
|
+
version: BASE_BRANCH_REDUCER_VERSION,
|
|
47
|
+
initial: () => ({}),
|
|
48
|
+
fold(acc, events) {
|
|
49
|
+
if (acc.baseBranch !== undefined) return
|
|
50
|
+
for (const event of events) {
|
|
51
|
+
if (event.type === 'build.created') {
|
|
52
|
+
acc.baseBranch = event.payload.baseBranch
|
|
53
|
+
return
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
finish: (acc) => acc.baseBranch,
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
export interface ProvisionMarker {
|
|
61
|
+
provider: string
|
|
62
|
+
branch: string
|
|
63
|
+
generation: number
|
|
64
|
+
ts: string
|
|
65
|
+
seq: number
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export const PROVISION_MARKER_REDUCER_VERSION = 1
|
|
69
|
+
|
|
70
|
+
/** The latest `workspace.provision-started` not yet followed by a
|
|
71
|
+
* provisioned/released fact — an open provisioning marker. */
|
|
72
|
+
export const provisionMarkerReducer = defineReducer<
|
|
73
|
+
{ marker: ProvisionMarker | null },
|
|
74
|
+
AbEvent,
|
|
75
|
+
ProvisionMarker | undefined
|
|
76
|
+
>({
|
|
77
|
+
version: PROVISION_MARKER_REDUCER_VERSION,
|
|
78
|
+
initial: () => ({ marker: null }),
|
|
79
|
+
fold(acc, events) {
|
|
80
|
+
for (const event of events) {
|
|
81
|
+
if (event.type === 'workspace.provision-started') {
|
|
82
|
+
acc.marker = { ...event.payload, ts: event.ts, seq: event.seq }
|
|
83
|
+
} else if (
|
|
84
|
+
acc.marker !== null &&
|
|
85
|
+
(event.type === 'workspace.provisioned' || event.type === 'workspace.released')
|
|
86
|
+
) {
|
|
87
|
+
acc.marker = null
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
finish: (acc) => (acc.marker === null ? undefined : { ...acc.marker }),
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
export const RECOVERY_CHECKPOINT_REDUCER_VERSION = 1
|
|
95
|
+
|
|
96
|
+
/** The commit a recovered workspace is re-cut from: the latest settled
|
|
97
|
+
* publication head, else the first provisioned base. */
|
|
98
|
+
export const recoveryCheckpointReducer = defineReducer<
|
|
99
|
+
{ settled?: string; original?: string },
|
|
100
|
+
AbEvent,
|
|
101
|
+
string | undefined
|
|
102
|
+
>({
|
|
103
|
+
version: RECOVERY_CHECKPOINT_REDUCER_VERSION,
|
|
104
|
+
initial: () => ({}),
|
|
105
|
+
fold(acc, events) {
|
|
106
|
+
for (const event of events) {
|
|
107
|
+
if (event.type === 'workspace.provisioned' && acc.original === undefined) {
|
|
108
|
+
acc.original = event.payload.base.sha
|
|
109
|
+
} else if (event.type === 'implement.completed') {
|
|
110
|
+
acc.settled = event.payload.commits.head
|
|
111
|
+
} else if (event.type === 'reconcile.completed') {
|
|
112
|
+
acc.settled = event.payload.mergeCommit
|
|
113
|
+
} else if (
|
|
114
|
+
event.type === 'finalize.step-completed' &&
|
|
115
|
+
event.payload.ok &&
|
|
116
|
+
event.payload.headSha !== undefined
|
|
117
|
+
) {
|
|
118
|
+
acc.settled = event.payload.headSha
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
finish: (acc) => acc.settled ?? acc.original,
|
|
123
|
+
})
|