@defrex/autobuild 0.8.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/LICENSE +201 -0
- package/README.md +192 -0
- package/SPEC.md +2269 -0
- package/bin/ab-build-runner.ts +47 -0
- package/bin/ab-dev.ts +27 -0
- package/bin/ab-dispatch-kernel.ts +92 -0
- package/bin/ab-harvest-runner.ts +29 -0
- package/bin/ab-init-probe.ts +19 -0
- package/bin/ab-process-group-reaper.ts +32 -0
- package/bin/ab.ts +6 -0
- package/bin/agent/ab +3 -0
- package/docs/answering-blocked-builds.md +89 -0
- package/docs/architecture.md +726 -0
- package/docs/assets/headline-wide.png +0 -0
- package/docs/configuration.md +1623 -0
- package/docs/hosted-dispatcher-cutover-validation.md +178 -0
- package/docs/hosted-dispatcher.md +204 -0
- package/docs/mcp.md +119 -0
- package/docs/operator-api.md +186 -0
- package/docs/remote-store-protocol.md +972 -0
- package/docs/remote-ticket-protocol.md +79 -0
- package/docs/setup.md +289 -0
- package/docs/spec-standard.md +63 -0
- package/docs/ticket-dependencies.md +59 -0
- package/docs/vercel-sandbox-rollout-probe.md +7 -0
- package/package.json +135 -0
- package/packages/core/src/cli/args.ts +74 -0
- package/packages/core/src/cli/artifact.ts +182 -0
- package/packages/core/src/cli/binary.ts +198 -0
- package/packages/core/src/cli/build-control.ts +789 -0
- package/packages/core/src/cli/build-progress.ts +55 -0
- package/packages/core/src/cli/bulk-control.ts +385 -0
- package/packages/core/src/cli/context.ts +470 -0
- package/packages/core/src/cli/dashboard/actions.ts +59 -0
- package/packages/core/src/cli/dashboard/cells.ts +92 -0
- package/packages/core/src/cli/dashboard/composer.ts +230 -0
- package/packages/core/src/cli/dashboard/detail.ts +100 -0
- package/packages/core/src/cli/dashboard/frame-image.ts +373 -0
- package/packages/core/src/cli/dashboard/live.ts +152 -0
- package/packages/core/src/cli/dashboard/model.ts +1173 -0
- package/packages/core/src/cli/dashboard/poll.ts +196 -0
- package/packages/core/src/cli/dashboard/render.ts +1568 -0
- package/packages/core/src/cli/dashboard/selection.ts +66 -0
- package/packages/core/src/cli/dashboard/session-feed.ts +244 -0
- package/packages/core/src/cli/dashboard/session-view.ts +506 -0
- package/packages/core/src/cli/dashboard/transcript.ts +108 -0
- package/packages/core/src/cli/dev.ts +46 -0
- package/packages/core/src/cli/dispatch-frontend.ts +1045 -0
- package/packages/core/src/cli/dispatch-process.ts +259 -0
- package/packages/core/src/cli/dispatch.ts +3692 -0
- package/packages/core/src/cli/dotenv.ts +48 -0
- package/packages/core/src/cli/env.ts +206 -0
- package/packages/core/src/cli/harvest.ts +698 -0
- package/packages/core/src/cli/help.ts +713 -0
- package/packages/core/src/cli/init-agent.ts +106 -0
- package/packages/core/src/cli/init-validation.ts +583 -0
- package/packages/core/src/cli/init.ts +756 -0
- package/packages/core/src/cli/installation.ts +333 -0
- package/packages/core/src/cli/keyboard.ts +261 -0
- package/packages/core/src/cli/main.ts +1399 -0
- package/packages/core/src/cli/mcp.ts +279 -0
- package/packages/core/src/cli/models.ts +65 -0
- package/packages/core/src/cli/observe.ts +55 -0
- package/packages/core/src/cli/plugin.ts +227 -0
- package/packages/core/src/cli/pr-attachments.ts +138 -0
- package/packages/core/src/cli/pr-summary.ts +124 -0
- package/packages/core/src/cli/publication-settlement.ts +188 -0
- package/packages/core/src/cli/repo-state.ts +213 -0
- package/packages/core/src/cli/repository-status.ts +80 -0
- package/packages/core/src/cli/self-update.ts +443 -0
- package/packages/core/src/cli/status.ts +895 -0
- package/packages/core/src/cli/store-opening.contract.ts +289 -0
- package/packages/core/src/cli/store-opening.ts +167 -0
- package/packages/core/src/cli/store-ref.ts +33 -0
- package/packages/core/src/cli/terminal-restore.ts +174 -0
- package/packages/core/src/cli/terminal.ts +361 -0
- package/packages/core/src/cli/terminals.ts +995 -0
- package/packages/core/src/cli/testkit.ts +194 -0
- package/packages/core/src/cli/ticket.ts +484 -0
- package/packages/core/src/cli/upgrade-agent.ts +180 -0
- package/packages/core/src/cli/upgrade-commit.ts +468 -0
- package/packages/core/src/cli/upgrade-notice.ts +91 -0
- package/packages/core/src/cli/upgrade-progress.ts +110 -0
- package/packages/core/src/cli/upgrade.ts +888 -0
- package/packages/core/src/cli/wait.ts +587 -0
- package/packages/core/src/cli/watch.ts +932 -0
- package/packages/core/src/config/live.ts +255 -0
- package/packages/core/src/config/load.ts +98 -0
- package/packages/core/src/config/roles.ts +438 -0
- package/packages/core/src/config/schema.ts +774 -0
- package/packages/core/src/distribution.ts +24 -0
- package/packages/core/src/events/catalog.ts +201 -0
- package/packages/core/src/events/envelope.ts +59 -0
- package/packages/core/src/events/payloads.ts +618 -0
- package/packages/core/src/events/repository.ts +559 -0
- package/packages/core/src/events/sessions.ts +180 -0
- package/packages/core/src/harvest/schema.ts +147 -0
- package/packages/core/src/ids.ts +45 -0
- package/packages/core/src/integration/harness.ts +681 -0
- package/packages/core/src/kernel/auto-merge.ts +259 -0
- package/packages/core/src/kernel/converge.ts +117 -0
- package/packages/core/src/kernel/dispatch-settings.ts +60 -0
- package/packages/core/src/kernel/dispatch-status.ts +141 -0
- package/packages/core/src/kernel/engine.ts +1047 -0
- package/packages/core/src/kernel/harvest.ts +823 -0
- package/packages/core/src/kernel/origin.ts +69 -0
- package/packages/core/src/kernel/phases.ts +196 -0
- package/packages/core/src/kernel/plan-verify-selection.ts +105 -0
- package/packages/core/src/kernel/pr-attachments.ts +82 -0
- package/packages/core/src/kernel/reducer.ts +752 -0
- package/packages/core/src/kernel/stall.ts +155 -0
- package/packages/core/src/kernel/verify-applicability.ts +52 -0
- package/packages/core/src/markdown.ts +67 -0
- package/packages/core/src/ontology.ts +288 -0
- package/packages/core/src/open-map.ts +203 -0
- package/packages/core/src/operator/annotations.ts +187 -0
- package/packages/core/src/operator/client.ts +311 -0
- package/packages/core/src/operator/control.ts +117 -0
- package/packages/core/src/operator/index.ts +62 -0
- package/packages/core/src/operator/presentation.ts +22 -0
- package/packages/core/src/operator/protocol.ts +98 -0
- package/packages/core/src/operator/query.ts +201 -0
- package/packages/core/src/operator/registry.ts +1043 -0
- package/packages/core/src/operator/requests.ts +138 -0
- package/packages/core/src/operator/sandbox.ts +458 -0
- package/packages/core/src/operator/server.ts +616 -0
- package/packages/core/src/operator/sessions.ts +223 -0
- package/packages/core/src/operator/tickets.ts +177 -0
- package/packages/core/src/plugin-sdk/index.ts +174 -0
- package/packages/core/src/plugins/contract-entry.ts +74 -0
- package/packages/core/src/plugins/contract-env.ts +3 -0
- package/packages/core/src/plugins/load.ts +259 -0
- package/packages/core/src/plugins/manifest.ts +236 -0
- package/packages/core/src/plugins/registry.ts +274 -0
- package/packages/core/src/plugins/runtimes.ts +75 -0
- package/packages/core/src/ports/forge/contract.ts +258 -0
- package/packages/core/src/ports/forge/create.ts +74 -0
- package/packages/core/src/ports/forge/fake.ts +488 -0
- package/packages/core/src/ports/forge/github-pr-attachments.ts +351 -0
- package/packages/core/src/ports/forge/github-transport.ts +352 -0
- package/packages/core/src/ports/forge/github.ts +1162 -0
- package/packages/core/src/ports/forge/local-git.ts +568 -0
- package/packages/core/src/ports/runner/claude.ts +873 -0
- package/packages/core/src/ports/runner/cli-stream.ts +90 -0
- package/packages/core/src/ports/runner/codex.ts +873 -0
- package/packages/core/src/ports/runner/contract.ts +492 -0
- package/packages/core/src/ports/runner/fake.ts +213 -0
- package/packages/core/src/ports/runner/json-record.ts +2 -0
- package/packages/core/src/ports/runner/live-contract-fixture.ts +176 -0
- package/packages/core/src/ports/runner/one-shot.ts +29 -0
- package/packages/core/src/ports/runner/pi-bridge-core.ts +136 -0
- package/packages/core/src/ports/runner/pi-bridge.js +6 -0
- package/packages/core/src/ports/runner/pi-rpc.ts +454 -0
- package/packages/core/src/ports/runner/pi.ts +617 -0
- package/packages/core/src/ports/runner/production.ts +74 -0
- package/packages/core/src/ports/runner/provider-error.ts +121 -0
- package/packages/core/src/ports/runner/routing.ts +234 -0
- package/packages/core/src/ports/runner/runtime.ts +214 -0
- package/packages/core/src/ports/runner/session-env.ts +34 -0
- package/packages/core/src/ports/runner/stream-parts.ts +103 -0
- package/packages/core/src/ports/tickets/contract.ts +570 -0
- package/packages/core/src/ports/tickets/create.ts +135 -0
- package/packages/core/src/ports/tickets/fake.ts +215 -0
- package/packages/core/src/ports/tickets/file.ts +559 -0
- package/packages/core/src/ports/tickets/linear.ts +811 -0
- package/packages/core/src/ports/tickets/operations.ts +161 -0
- package/packages/core/src/ports/tickets/remote-index.ts +13 -0
- package/packages/core/src/ports/tickets/remote-protocol.ts +80 -0
- package/packages/core/src/ports/tickets/remote-server.ts +185 -0
- package/packages/core/src/ports/tickets/remote.ts +130 -0
- package/packages/core/src/ports/tickets/update.ts +51 -0
- package/packages/core/src/ports/types.ts +528 -0
- package/packages/core/src/ports/workspace/build-execution.ts +123 -0
- package/packages/core/src/ports/workspace/contract.ts +136 -0
- package/packages/core/src/ports/workspace/create.ts +132 -0
- package/packages/core/src/ports/workspace/distribution-archive.ts +317 -0
- package/packages/core/src/ports/workspace/fake.ts +432 -0
- package/packages/core/src/ports/workspace/git-worktree.ts +848 -0
- package/packages/core/src/ports/workspace/harvest-execution.ts +111 -0
- package/packages/core/src/ports/workspace/in-process-build-execution.ts +30 -0
- package/packages/core/src/ports/workspace/local-build-execution.ts +88 -0
- package/packages/core/src/ports/workspace/operator-sandbox.ts +142 -0
- package/packages/core/src/ports/workspace/phase-scratch.ts +156 -0
- package/packages/core/src/ports/workspace/vercel-sandbox.ts +2541 -0
- package/packages/core/src/processes/build-child-exit.ts +78 -0
- package/packages/core/src/processes/build-child.ts +119 -0
- package/packages/core/src/processes/build-execution-state.ts +95 -0
- package/packages/core/src/processes/build-parent-watch.ts +35 -0
- package/packages/core/src/processes/build-runner.ts +2614 -0
- package/packages/core/src/processes/dispatcher.ts +2825 -0
- package/packages/core/src/processes/execution-settlement.ts +171 -0
- package/packages/core/src/processes/harvest-child.ts +115 -0
- package/packages/core/src/processes/harvest-execution-state.ts +121 -0
- package/packages/core/src/processes/harvest-runner.ts +1656 -0
- package/packages/core/src/processes/harvest.ts +595 -0
- package/packages/core/src/processes/infrastructure-failure-budget.ts +146 -0
- package/packages/core/src/processes/phase-failure-budget.ts +52 -0
- package/packages/core/src/processes/process-group-reaper.ts +35 -0
- package/packages/core/src/processes/process-group.ts +65 -0
- package/packages/core/src/processes/publication-loss.ts +62 -0
- package/packages/core/src/processes/publication-state.ts +93 -0
- package/packages/core/src/processes/sandbox-state.ts +101 -0
- package/packages/core/src/registry.ts +24 -0
- package/packages/core/src/sessions/reducer.ts +185 -0
- package/packages/core/src/skills.ts +7 -0
- package/packages/core/src/spec-standard.ts +47 -0
- package/packages/core/src/store/adapter.ts +80 -0
- package/packages/core/src/store/build-scope.ts +272 -0
- package/packages/core/src/store/contract.ts +2581 -0
- package/packages/core/src/store/local/blobs.ts +38 -0
- package/packages/core/src/store/local/schema.ts +180 -0
- package/packages/core/src/store/local/store.ts +1713 -0
- package/packages/core/src/store/memory.ts +1138 -0
- package/packages/core/src/store/phase-session.ts +425 -0
- package/packages/core/src/store/remote/client.ts +780 -0
- package/packages/core/src/store/remote/index.ts +12 -0
- package/packages/core/src/store/remote/protocol.ts +348 -0
- package/packages/core/src/store/remote/server.ts +923 -0
- package/packages/core/src/store/remote/token.ts +137 -0
- package/packages/core/src/store/remote/version.ts +10 -0
- package/packages/core/src/store/retention.ts +91 -0
- package/packages/core/src/store/session-handle.ts +265 -0
- package/packages/core/src/store/streams/assemble.ts +79 -0
- package/packages/core/src/store/streams/lock.ts +42 -0
- package/packages/core/src/store/streams/session-writer.ts +306 -0
- package/packages/core/src/store/streams/types.ts +171 -0
- package/packages/core/src/store/streams/wait.ts +127 -0
- package/packages/core/src/store/subscribe.ts +105 -0
- package/packages/core/src/store/types.ts +466 -0
- package/packages/core/src/testing/fixed.ts +44 -0
- package/packages/core/src/testing/store-failures.ts +32 -0
- package/packages/core/src/zod-issues.ts +91 -0
- package/skills/code-review/SKILL.md +100 -0
- package/skills/finalize/SKILL.md +44 -0
- package/skills/guide/SKILL.md +1781 -0
- package/skills/guide/references/operator-api.md +164 -0
- package/skills/guide/references/plugin-authoring.md +354 -0
- package/skills/guide/references/remote-store-protocol.md +972 -0
- package/skills/guide/references/remote-ticket-protocol.md +79 -0
- package/skills/guide/references/setup.md +289 -0
- package/skills/guide/references/spec-standard.md +63 -0
- package/skills/guide/references/ticket-dependencies.md +59 -0
- package/skills/harvest/SKILL.md +66 -0
- package/skills/harvest-review/SKILL.md +63 -0
- package/skills/implement/SKILL.md +82 -0
- package/skills/plan/SKILL.md +123 -0
- package/skills/plan-review/SKILL.md +121 -0
- package/skills/reconcile/SKILL.md +65 -0
- package/skills/spec/SKILL.md +132 -0
- package/skills/tickets/SKILL.md +139 -0
- package/templates/autobuild.toml +36 -0
|
@@ -0,0 +1,752 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The state reducer (SPEC §15.5, §3.4): state is a reduction of events — any
|
|
3
|
+
* snapshot is a cache, never the source of truth, and resumability falls out.
|
|
4
|
+
* `reduceBuild` is pure, total (any prefix of a valid log reduces), and O(n).
|
|
5
|
+
*
|
|
6
|
+
* This projection is the single read model: the engine picks the next phase
|
|
7
|
+
* from it, the operator UI's build list is exactly this reduction over every
|
|
8
|
+
* build (§15.5), and the dispatcher's janitor reads `pr`/`prState` (§15.7).
|
|
9
|
+
*/
|
|
10
|
+
import type { Actor } from '../events/envelope'
|
|
11
|
+
import type { AbEvent } from '../events/catalog'
|
|
12
|
+
import { normalizeVerifyCompletion, type EventPayload } from '../events/payloads'
|
|
13
|
+
import type {
|
|
14
|
+
ArtifactRef,
|
|
15
|
+
BuildOutcome,
|
|
16
|
+
BuildStatus,
|
|
17
|
+
CommitRange,
|
|
18
|
+
EscalationResolution,
|
|
19
|
+
EscalationSource,
|
|
20
|
+
EscalationTarget,
|
|
21
|
+
Finding,
|
|
22
|
+
Phase,
|
|
23
|
+
PolicyEscalationCause,
|
|
24
|
+
VerifyOutcome,
|
|
25
|
+
} from '../ontology'
|
|
26
|
+
import { verifyPhase } from '../ontology'
|
|
27
|
+
|
|
28
|
+
/** An `escalation.raised` without a matching `escalation.answered` — the
|
|
29
|
+
* definition of `blocked` (§15.5). Matched by id, so answers may arrive in
|
|
30
|
+
* any order. */
|
|
31
|
+
export interface OpenEscalation {
|
|
32
|
+
id: string
|
|
33
|
+
phase: EscalationTarget
|
|
34
|
+
round?: number
|
|
35
|
+
source: EscalationSource
|
|
36
|
+
/** Policy guard identity. Absent only on historical policy raises. */
|
|
37
|
+
policyCause?: PolicyEscalationCause
|
|
38
|
+
question: string
|
|
39
|
+
refs?: string[]
|
|
40
|
+
/** seq of the `escalation.raised` event — what `spec.revised` cites (§15.3). */
|
|
41
|
+
seq: number
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Answered escalations keep their resolution because the engine routes on it
|
|
45
|
+
* (§15.6-B, §6.3): `guidance` feeds the next producer round as authoritative
|
|
46
|
+
* feedback, `dismiss-finding` marks the chain human-resolved for the next
|
|
47
|
+
* reviewer, `revise-spec` restarts the build from plan, `abort` ends it, and
|
|
48
|
+
* bare `retry` re-arms the applicable policy-exhausted budget without
|
|
49
|
+
* inventing guidance. */
|
|
50
|
+
export interface AnsweredEscalation extends OpenEscalation {
|
|
51
|
+
answer: string
|
|
52
|
+
resolution: EscalationResolution
|
|
53
|
+
/** Exact replacement spec revision authorized by a `revise-spec` answer. */
|
|
54
|
+
artifact?: ArtifactRef
|
|
55
|
+
/** Absolute review budget accepted with a review-round-limit answer. */
|
|
56
|
+
reviewRoundCeiling?: number
|
|
57
|
+
/** seq of the `escalation.answered` event. */
|
|
58
|
+
answeredSeq: number
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** One phase occurrence. Loop phases carry `round` (§15.3); verify and
|
|
62
|
+
* reconcile carry `attempt`; finalize carries neither. */
|
|
63
|
+
export interface PhaseContext {
|
|
64
|
+
phase: Phase
|
|
65
|
+
round?: number
|
|
66
|
+
attempt?: number
|
|
67
|
+
/** seq of the `*.started` event (currentPhase) or the terminal event
|
|
68
|
+
* (lastCompletedPhase) — the resume anchor (§15.6-C). */
|
|
69
|
+
seq: number
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** An accepted operator command the kernel has not yet acknowledged with its
|
|
73
|
+
* fact event (D2, §15.2.7). Most come from `*-requested`; an escalation answer
|
|
74
|
+
* with the abort resolution is the equivalent accepted abort intent. A dead
|
|
75
|
+
* runner receives these on resume. Commands are ordered events (§15.2.7): a
|
|
76
|
+
* request supersedes any earlier pending request of the OPPOSING kind (pause
|
|
77
|
+
* vs resume) — the operator's latest command wins, and a countermanded request
|
|
78
|
+
* is never delivered. */
|
|
79
|
+
export interface PendingCommand {
|
|
80
|
+
command: 'pause' | 'resume' | 'abort'
|
|
81
|
+
/** seq of the event that accepted the command. */
|
|
82
|
+
seq: number
|
|
83
|
+
reason?: string
|
|
84
|
+
actor: Actor
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** A `session.started` without its `session.ended`. Historical orphan-only
|
|
88
|
+
* logs remain open and readable; a recovering runner explicitly ends every
|
|
89
|
+
* session open at its resume boundary before announcing attachment (§15.6-C). */
|
|
90
|
+
export interface OpenSession {
|
|
91
|
+
session: string
|
|
92
|
+
role: string
|
|
93
|
+
runner: string
|
|
94
|
+
model?: string
|
|
95
|
+
phase: Phase
|
|
96
|
+
round?: number
|
|
97
|
+
/** Live-view stream id (SPEC §9); absent on historical events. */
|
|
98
|
+
stream?: string
|
|
99
|
+
/** seq of the `session.started` event. */
|
|
100
|
+
seq: number
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** One `verify.completed` fact. Results accumulate across attempts; membership
|
|
104
|
+
* in the current cycle is solely
|
|
105
|
+
* `results.filter(r => r.seq > verify.cycleSince)`. `maxAttemptSeen` is only
|
|
106
|
+
* the aggregate attempt high-water and cannot identify the current cycle in
|
|
107
|
+
* the window between a verify failure and the next `verify.started`. */
|
|
108
|
+
export interface VerifyResult {
|
|
109
|
+
step: string
|
|
110
|
+
attempt: number
|
|
111
|
+
outcome: VerifyOutcome
|
|
112
|
+
report?: ArtifactRef
|
|
113
|
+
/** Present exactly when `outcome === "skipped"`. */
|
|
114
|
+
reason?: string
|
|
115
|
+
/** seq of the `verify.completed` event — what `cycleSince` is compared to. */
|
|
116
|
+
seq: number
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** PR lifecycle (§15.7): 'open' once `finalize.completed` records the PR;
|
|
120
|
+
* then per `pr.*` janitor events. 'conflicted' holds until a
|
|
121
|
+
* `reconcile.completed` appears after the `pr.conflicted`, which returns the
|
|
122
|
+
* PR to 'open' while verify re-runs in full. */
|
|
123
|
+
export type PrLifecycle = 'open' | 'merged' | 'closed' | 'conflicted'
|
|
124
|
+
|
|
125
|
+
/** Durable desired state for GitHub native auto-merge plus the latest forge
|
|
126
|
+
* application fact. The command seq correlates the two sides of the
|
|
127
|
+
* non-transactional forge/event boundary: consumers treat the command as
|
|
128
|
+
* settled only when both `enabled` and `commandSeq` match. */
|
|
129
|
+
export interface AutoMergeProjection {
|
|
130
|
+
/** Latest human command. False with no commandSeq means the default: off. */
|
|
131
|
+
requested: boolean
|
|
132
|
+
commandSeq?: number
|
|
133
|
+
/** Latest recorded external application. It may acknowledge an older
|
|
134
|
+
* command; such a stale fact never changes `requested`/`commandSeq`. */
|
|
135
|
+
applied?: { enabled: boolean; commandSeq: number }
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export type ObservationRecord = EventPayload<'observation.recorded'>
|
|
139
|
+
|
|
140
|
+
export interface BuildState {
|
|
141
|
+
/**
|
|
142
|
+
* §15.5: 'queued' until `runner.attached`, 'running' after;
|
|
143
|
+
* 'blocked' ≡ open escalation; 'paused' ≡ `build.paused` without a later
|
|
144
|
+
* `build.resumed`; 'done' after `build.completed`; 'aborted' after
|
|
145
|
+
* `build.aborted`. Precedence: aborted/done (terminal, latest wins) >
|
|
146
|
+
* paused > blocked > running > queued.
|
|
147
|
+
*
|
|
148
|
+
* Paused + blocked overlap: both can hold at once (an escalation is open
|
|
149
|
+
* when the operator pauses). Paused wins — it is the operator's explicit
|
|
150
|
+
* instruction — but the escalation stays in `openEscalations`, so resuming
|
|
151
|
+
* a still-blocked build reports 'blocked' again. No information is lost.
|
|
152
|
+
*/
|
|
153
|
+
status: BuildStatus
|
|
154
|
+
/** From `build.completed` — present only once status is 'done'. */
|
|
155
|
+
outcome?: BuildOutcome
|
|
156
|
+
/** currentPhase if one is started-but-not-completed, else the latest
|
|
157
|
+
* completed phase. A started-without-terminal phase is exactly what
|
|
158
|
+
* §15.6-C re-runs from its start. */
|
|
159
|
+
phase?: Phase
|
|
160
|
+
/** The current loop round: the round carried by the most recent
|
|
161
|
+
* plan/plan-review/implement/code-review event. Verify, finalize, and
|
|
162
|
+
* reconcile events do not reset it (verify attempt 2 still belongs to the
|
|
163
|
+
* loop round that produced the code — §15.6-A). 0 before `plan.started`. */
|
|
164
|
+
round: number
|
|
165
|
+
/** Started, not completed — set by `*.started`, cleared by the phase's
|
|
166
|
+
* terminal event (§8.4). `escalation.raised` and `phase.failed` do NOT
|
|
167
|
+
* clear it: the phase still needs re-running (§15.6-B/C). */
|
|
168
|
+
currentPhase?: PhaseContext
|
|
169
|
+
lastCompletedPhase?: PhaseContext
|
|
170
|
+
/** In raise order. Non-empty ≡ blocked (§15.5). */
|
|
171
|
+
openEscalations: OpenEscalation[]
|
|
172
|
+
/** In answer order, with resolutions for engine routing (§15.6-B). */
|
|
173
|
+
answeredEscalations: AnsweredEscalation[]
|
|
174
|
+
/** Human-selected absolute caps for the current spec's independent loops. */
|
|
175
|
+
reviewRoundCeilings: { plan?: number; code?: number }
|
|
176
|
+
/** From `finalize.completed` (§15.3) — the kernel opened the PR (D7). */
|
|
177
|
+
pr?: { number: number; url: string; headSha: string }
|
|
178
|
+
prState?: PrLifecycle
|
|
179
|
+
/** Human auto-merge intent and its correlated forge application fact. */
|
|
180
|
+
autoMerge: AutoMergeProjection
|
|
181
|
+
/** seq of the latest `finalize.completed`, else 0 — "has finalize run for
|
|
182
|
+
* the CURRENT spec". `prState` cannot answer that: it is the full-log PR
|
|
183
|
+
* fact and must stay so, because the janitor (dispatcher.ts:342,:367,:396)
|
|
184
|
+
* and the deliberately restart-orthogonal epilogue (engine.ts:161-162,:402)
|
|
185
|
+
* read it. Compare this against `restartSince`, exactly as engine.ts:707-708
|
|
186
|
+
* post-filters its own `finalizeCompleted`. */
|
|
187
|
+
finalizeCompletedSeq: number
|
|
188
|
+
/** Full-log `finalize.step-completed` facts, in event order (§5).
|
|
189
|
+
* Post-steps are failure-tolerant, so a completion counts whether `ok` is
|
|
190
|
+
* true or false. Current-spec membership is determined by
|
|
191
|
+
* `finalizeSteps.filter(f => f.seq > restartBoundary)`, matching
|
|
192
|
+
* engine.ts's post-restart `finalizeStepsDone` scope (engine.ts:710-712). */
|
|
193
|
+
finalizeSteps: { step: string; ok: boolean; seq: number }[]
|
|
194
|
+
lastEvent?: AbEvent
|
|
195
|
+
/** 0 for an empty log; `runner.attached {resumedFromSeq}` cites this. */
|
|
196
|
+
lastSeq: number
|
|
197
|
+
/** Latest spec artifact rev — `spec.imported`/`spec.authored` set it,
|
|
198
|
+
* `spec.revised` bumps it (§6.3: rev N+1 restarts the build from plan). */
|
|
199
|
+
specRev?: number
|
|
200
|
+
/** seq of the latest `spec.revised`, else 0 — the restart boundary (§6.3).
|
|
201
|
+
* Rev N+1 restarts the build from plan, so every approval and result at or
|
|
202
|
+
* before this seq describes a spec that no longer exists. Mirrors
|
|
203
|
+
* engine.ts's `restartSeq` (engine.ts:126). */
|
|
204
|
+
restartSince: number
|
|
205
|
+
/** `approved` ≡ the latest `plan-review.verdict` is 'approve' (§10);
|
|
206
|
+
* `artifactRev` is the latest deposited plan rev.
|
|
207
|
+
*
|
|
208
|
+
* `approval` is the `plan-review.verdict` approve that currently STANDS —
|
|
209
|
+
* its event seq and round. Set only on an approve; cleared by any other
|
|
210
|
+
* verdict, so `approval !== undefined` ⇔ `approved`. A consumer asking "is
|
|
211
|
+
* the plan loop settled?" must check this against `restartSince` and
|
|
212
|
+
* `plan.round`, not `approved` alone — `approved` spans the full log and
|
|
213
|
+
* survives a restart, which re-runs the whole loop (engine.ts:466). */
|
|
214
|
+
plan: {
|
|
215
|
+
round: number
|
|
216
|
+
approved: boolean
|
|
217
|
+
artifactRev?: number
|
|
218
|
+
approval?: { seq: number; round: number }
|
|
219
|
+
}
|
|
220
|
+
/** `round` tracks the latest `implement.started`/`.completed`; `commits`
|
|
221
|
+
* and `artifactRev` come from the latest `implement.completed` only — so
|
|
222
|
+
* mid-round they still point at the last pushed head, the cross-sandbox
|
|
223
|
+
* resume anchor (D3, §15.6-C). */
|
|
224
|
+
implement: { round: number; commits?: CommitRange; artifactRev?: number }
|
|
225
|
+
/** ≡ the latest `code-review.verdict` is 'approve'. */
|
|
226
|
+
codeReviewApproved: boolean
|
|
227
|
+
/** The `code-review.verdict` approve that currently stands — its event seq
|
|
228
|
+
* and round. Same discipline as `plan.approval`: check it against
|
|
229
|
+
* `restartSince` and `implement.round`, AND against the verify cycle, which
|
|
230
|
+
* a failure reopens (§15.6-A) before any new implement round lands. */
|
|
231
|
+
codeReviewApproval?: { seq: number; round: number }
|
|
232
|
+
/** Findings per round, in round order (index round-1); rounds that never
|
|
233
|
+
* produced a verdict are empty. Stall detection walks `persists` chains
|
|
234
|
+
* across these (§15.4); reviewers get all prior rounds as context (§8.3). */
|
|
235
|
+
reviewFindings: { planReview: Finding[][]; codeReview: Finding[][] }
|
|
236
|
+
/** `maxAttemptSeen` is the highest verify attempt seen across the full log;
|
|
237
|
+
* it is a high-water mark, not a current-cycle identifier. `results`
|
|
238
|
+
* accumulate across attempts (filter by `cycleSince` for the current cycle
|
|
239
|
+
* — see VerifyResult); `currentStep` is a `verify.started` without its
|
|
240
|
+
* `verify.completed`.
|
|
241
|
+
*
|
|
242
|
+
* `cycleSince` is the seq after which results describe the CURRENT code
|
|
243
|
+
* (§15.6-A): max(restartSince, latest code-review approve, latest
|
|
244
|
+
* `reconcile.completed`). Results at or before it describe code that no
|
|
245
|
+
* longer exists — implement or reconcile changed it, and a new cycle re-runs
|
|
246
|
+
* from the FIRST step. 0 before any boundary lands. This is the same filter
|
|
247
|
+
* engine.ts:275-276 applies over its post-restart index. */
|
|
248
|
+
verify: {
|
|
249
|
+
maxAttemptSeen: number
|
|
250
|
+
results: VerifyResult[]
|
|
251
|
+
currentStep?: string
|
|
252
|
+
cycleSince: number
|
|
253
|
+
}
|
|
254
|
+
/** Highest `reconcile.started.attempt` — the kernel's own occurrence
|
|
255
|
+
* counter, so a re-run of the same attempt after sandbox death does not
|
|
256
|
+
* double-count. Progress-budget accounting is projected separately from
|
|
257
|
+
* authoritative repeat-conflict evidence (§15.7). */
|
|
258
|
+
reconcileAttempts: number
|
|
259
|
+
/** `observation.recorded` payloads in order — harvest input (§12). */
|
|
260
|
+
observations: ObservationRecord[]
|
|
261
|
+
/** Every setup failure remains historical evidence. `setupFailure` is the
|
|
262
|
+
* latest one not superseded by a later successful runner attachment. */
|
|
263
|
+
setupFailures: Array<EventPayload<'runner.setup-failed'> & { seq: number }>
|
|
264
|
+
setupFailure?: EventPayload<'runner.setup-failed'> & { seq: number }
|
|
265
|
+
/** Pre-run dispatch attempts, retained as durable operator diagnostics. */
|
|
266
|
+
dispatchFailures: Array<EventPayload<'dispatch.failed'> & { seq: number }>
|
|
267
|
+
/** Provider identities and failures are durable even after recovery clears the current error. */
|
|
268
|
+
executions: Array<EventPayload<'execution.started'> & { seq: number }>
|
|
269
|
+
infrastructureFailures: Array<EventPayload<'infrastructure.failed'> & { seq: number }>
|
|
270
|
+
infrastructureFailure?: EventPayload<'infrastructure.failed'> & { seq: number }
|
|
271
|
+
cleanupAttempts: Array<EventPayload<'infrastructure.cleanup-attempted'> & { seq: number }>
|
|
272
|
+
/** Outstanding human discard intent. It is settled only by terminal completion. */
|
|
273
|
+
discardRequest?: { seq: number; actor: Actor }
|
|
274
|
+
/** Unacknowledged runner-owned operator commands in request order (D2). */
|
|
275
|
+
pendingCommands: PendingCommand[]
|
|
276
|
+
sessions: { open: OpenSession[] }
|
|
277
|
+
/** `phase.failed` tally per phase (verify steps key as `verify:<step>`) —
|
|
278
|
+
* retry policy input (§8.4). */
|
|
279
|
+
failures: Record<string, number>
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export function reduceBuild(events: AbEvent[]): BuildState {
|
|
283
|
+
let attached = false
|
|
284
|
+
let pausedFlag = false
|
|
285
|
+
let terminal: 'done' | 'aborted' | undefined
|
|
286
|
+
let outcome: BuildOutcome | undefined
|
|
287
|
+
let round = 0
|
|
288
|
+
let currentPhase: PhaseContext | undefined
|
|
289
|
+
let lastCompletedPhase: PhaseContext | undefined
|
|
290
|
+
const openEscalations = new Map<string, OpenEscalation>()
|
|
291
|
+
const answeredEscalations: AnsweredEscalation[] = []
|
|
292
|
+
const reviewRoundCeilings: BuildState['reviewRoundCeilings'] = {}
|
|
293
|
+
let pr: BuildState['pr']
|
|
294
|
+
let prState: PrLifecycle | undefined
|
|
295
|
+
const autoMerge: AutoMergeProjection = { requested: false }
|
|
296
|
+
let specRev: number | undefined
|
|
297
|
+
let restartSince = 0
|
|
298
|
+
let finalizeCompletedSeq = 0
|
|
299
|
+
const finalizeSteps: BuildState['finalizeSteps'] = []
|
|
300
|
+
const plan: BuildState['plan'] = { round: 0, approved: false }
|
|
301
|
+
const implement: BuildState['implement'] = { round: 0 }
|
|
302
|
+
let codeReviewApproved = false
|
|
303
|
+
let codeReviewApproval: BuildState['codeReviewApproval']
|
|
304
|
+
const planReviewFindings: Finding[][] = []
|
|
305
|
+
const codeReviewFindings: Finding[][] = []
|
|
306
|
+
const verify: BuildState['verify'] = { maxAttemptSeen: 0, results: [], cycleSince: 0 }
|
|
307
|
+
let reconcileAttempts = 0
|
|
308
|
+
const observations: ObservationRecord[] = []
|
|
309
|
+
const setupFailures: BuildState['setupFailures'] = []
|
|
310
|
+
let setupFailure: BuildState['setupFailure']
|
|
311
|
+
const dispatchFailures: BuildState['dispatchFailures'] = []
|
|
312
|
+
const executions: BuildState['executions'] = []
|
|
313
|
+
const infrastructureFailures: BuildState['infrastructureFailures'] = []
|
|
314
|
+
let infrastructureFailure: BuildState['infrastructureFailure']
|
|
315
|
+
const cleanupAttempts: BuildState['cleanupAttempts'] = []
|
|
316
|
+
let discardRequest: BuildState['discardRequest']
|
|
317
|
+
const pending: Record<PendingCommand['command'], PendingCommand[]> = {
|
|
318
|
+
pause: [],
|
|
319
|
+
resume: [],
|
|
320
|
+
abort: [],
|
|
321
|
+
}
|
|
322
|
+
const openSessions = new Map<string, OpenSession>()
|
|
323
|
+
const failures: Record<string, number> = {}
|
|
324
|
+
|
|
325
|
+
const start = (ctx: PhaseContext): void => {
|
|
326
|
+
currentPhase = ctx
|
|
327
|
+
}
|
|
328
|
+
/** Terminal event for `phase` (§8.4): record completion, close the current
|
|
329
|
+
* context if it is this phase. A terminal with no matching start still
|
|
330
|
+
* records — the reducer is total over any log. */
|
|
331
|
+
const complete = (
|
|
332
|
+
phase: Phase,
|
|
333
|
+
seq: number,
|
|
334
|
+
ctx?: { round?: number; attempt?: number },
|
|
335
|
+
): void => {
|
|
336
|
+
lastCompletedPhase = { phase, seq, ...ctx }
|
|
337
|
+
if (currentPhase?.phase === phase) currentPhase = undefined
|
|
338
|
+
}
|
|
339
|
+
const setFindings = (per: Finding[][], r: number, findings: Finding[]): void => {
|
|
340
|
+
while (per.length < r) per.push([])
|
|
341
|
+
per[r - 1] = findings
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
let lastEvent: AbEvent | undefined
|
|
345
|
+
for (const event of events) {
|
|
346
|
+
switch (event.type) {
|
|
347
|
+
// Facts the projection does not need (workspace liveness is the
|
|
348
|
+
// dispatcher's concern; finalize post-steps are failure-tolerant §5).
|
|
349
|
+
case 'build.created':
|
|
350
|
+
case 'workspace.provisioned':
|
|
351
|
+
case 'workspace.released':
|
|
352
|
+
case 'publication.requested':
|
|
353
|
+
// Inert loss fact (AUT-328): the dispatcher released a workspace with a
|
|
354
|
+
// pending publication. Recovery runs through the existing
|
|
355
|
+
// abandoned-publication flow (abandonedPublicationPending), which re-runs
|
|
356
|
+
// the phase — the reducer needs no state from it.
|
|
357
|
+
case 'publication.lost':
|
|
358
|
+
case 'dispatch.comment-posted':
|
|
359
|
+
case 'pr-attachment.designated':
|
|
360
|
+
case 'pr-attachment.hosted':
|
|
361
|
+
case 'pr-attachment.reclaimed':
|
|
362
|
+
case 'pr-attachment.reclaim-failed':
|
|
363
|
+
break
|
|
364
|
+
|
|
365
|
+
case 'build.completed':
|
|
366
|
+
terminal = 'done' // terminal, latest wins (§15.5) — in-order overwrite
|
|
367
|
+
outcome = event.payload.outcome
|
|
368
|
+
break
|
|
369
|
+
case 'runner.attached':
|
|
370
|
+
attached = true
|
|
371
|
+
// On a recovery attempt attachment is delayed until setup succeeds,
|
|
372
|
+
// so this fact is also the durable proof that the condition cleared.
|
|
373
|
+
setupFailure = undefined
|
|
374
|
+
break
|
|
375
|
+
case 'runner.setup-failed': {
|
|
376
|
+
const failure = { ...event.payload, seq: event.seq }
|
|
377
|
+
setupFailures.push(failure)
|
|
378
|
+
setupFailure = failure
|
|
379
|
+
break
|
|
380
|
+
}
|
|
381
|
+
case 'dispatch.failed':
|
|
382
|
+
dispatchFailures.push({ ...event.payload, seq: event.seq })
|
|
383
|
+
break
|
|
384
|
+
case 'execution.started':
|
|
385
|
+
executions.push({ ...event.payload, seq: event.seq })
|
|
386
|
+
infrastructureFailure = undefined
|
|
387
|
+
break
|
|
388
|
+
case 'execution.ended':
|
|
389
|
+
infrastructureFailure = undefined
|
|
390
|
+
break
|
|
391
|
+
case 'infrastructure.failed': {
|
|
392
|
+
const failure = { ...event.payload, seq: event.seq }
|
|
393
|
+
infrastructureFailures.push(failure)
|
|
394
|
+
infrastructureFailure = failure
|
|
395
|
+
break
|
|
396
|
+
}
|
|
397
|
+
case 'infrastructure.cleanup-attempted':
|
|
398
|
+
cleanupAttempts.push({ ...event.payload, seq: event.seq })
|
|
399
|
+
if (
|
|
400
|
+
event.payload.outcome !== 'unknown' &&
|
|
401
|
+
infrastructureFailure?.workspaceRef === event.payload.workspaceRef
|
|
402
|
+
) {
|
|
403
|
+
infrastructureFailure = undefined
|
|
404
|
+
}
|
|
405
|
+
break
|
|
406
|
+
|
|
407
|
+
// Operator commands (D2): requests queue until the kernel's fact event
|
|
408
|
+
// acknowledges them; one ack clears every earlier request of its kind.
|
|
409
|
+
// Commands are ORDERED events (§15.2.7), so a request also expires every
|
|
410
|
+
// earlier pending request of the opposing kind: a pause countermands a
|
|
411
|
+
// still-pending resume and vice versa. Otherwise a stale retained
|
|
412
|
+
// request would later be acknowledged against the operator's newest
|
|
413
|
+
// command — an old resume un-pausing a later pause.
|
|
414
|
+
case 'build.pause-requested':
|
|
415
|
+
pending.resume = []
|
|
416
|
+
pending.pause.push({
|
|
417
|
+
command: 'pause',
|
|
418
|
+
seq: event.seq,
|
|
419
|
+
reason: event.payload.reason,
|
|
420
|
+
actor: event.actor,
|
|
421
|
+
})
|
|
422
|
+
break
|
|
423
|
+
case 'build.resume-requested':
|
|
424
|
+
pending.pause = []
|
|
425
|
+
pending.resume.push({
|
|
426
|
+
command: 'resume',
|
|
427
|
+
seq: event.seq,
|
|
428
|
+
reason: event.payload.reason,
|
|
429
|
+
actor: event.actor,
|
|
430
|
+
})
|
|
431
|
+
break
|
|
432
|
+
case 'build.abort-requested':
|
|
433
|
+
pending.abort.push({
|
|
434
|
+
command: 'abort',
|
|
435
|
+
seq: event.seq,
|
|
436
|
+
reason: event.payload.reason,
|
|
437
|
+
actor: event.actor,
|
|
438
|
+
})
|
|
439
|
+
break
|
|
440
|
+
case 'build.discard-requested':
|
|
441
|
+
discardRequest = { seq: event.seq, actor: event.actor }
|
|
442
|
+
break
|
|
443
|
+
case 'build.auto-merge-requested':
|
|
444
|
+
autoMerge.requested = true
|
|
445
|
+
autoMerge.commandSeq = event.seq
|
|
446
|
+
break
|
|
447
|
+
case 'build.auto-merge-cancelled':
|
|
448
|
+
autoMerge.requested = false
|
|
449
|
+
autoMerge.commandSeq = event.seq
|
|
450
|
+
break
|
|
451
|
+
case 'build.paused':
|
|
452
|
+
pausedFlag = true
|
|
453
|
+
pending.pause = []
|
|
454
|
+
break
|
|
455
|
+
case 'build.resumed':
|
|
456
|
+
pausedFlag = false
|
|
457
|
+
pending.resume = []
|
|
458
|
+
break
|
|
459
|
+
case 'build.aborted':
|
|
460
|
+
terminal = 'aborted'
|
|
461
|
+
pending.abort = []
|
|
462
|
+
break
|
|
463
|
+
|
|
464
|
+
case 'spec.imported':
|
|
465
|
+
case 'spec.authored':
|
|
466
|
+
specRev = event.payload.artifact.rev
|
|
467
|
+
break
|
|
468
|
+
case 'spec.revised':
|
|
469
|
+
specRev = event.payload.artifact.rev
|
|
470
|
+
// §6.3: rev N+1 restarts the build from plan. The restart boundary
|
|
471
|
+
// invalidates every approval and result at or before it, so it is also
|
|
472
|
+
// a verify cycle boundary. Finalize post-steps remain in full-log
|
|
473
|
+
// history; consumers use this boundary to select the current spec,
|
|
474
|
+
// matching engine.ts:710-712.
|
|
475
|
+
restartSince = event.seq
|
|
476
|
+
verify.cycleSince = Math.max(verify.cycleSince, event.seq)
|
|
477
|
+
delete reviewRoundCeilings.plan
|
|
478
|
+
delete reviewRoundCeilings.code
|
|
479
|
+
break
|
|
480
|
+
|
|
481
|
+
case 'session.started':
|
|
482
|
+
openSessions.set(event.payload.session, {
|
|
483
|
+
session: event.payload.session,
|
|
484
|
+
role: event.payload.role,
|
|
485
|
+
runner: event.payload.runner,
|
|
486
|
+
model: event.payload.model,
|
|
487
|
+
phase: event.payload.phase,
|
|
488
|
+
round: event.payload.round,
|
|
489
|
+
stream: event.payload.stream,
|
|
490
|
+
seq: event.seq,
|
|
491
|
+
})
|
|
492
|
+
break
|
|
493
|
+
case 'session.ended':
|
|
494
|
+
openSessions.delete(event.payload.session)
|
|
495
|
+
break
|
|
496
|
+
|
|
497
|
+
case 'plan.started':
|
|
498
|
+
round = event.payload.round
|
|
499
|
+
plan.round = round
|
|
500
|
+
start({ phase: 'plan', round, seq: event.seq })
|
|
501
|
+
break
|
|
502
|
+
case 'plan.completed':
|
|
503
|
+
round = event.payload.round
|
|
504
|
+
plan.round = round
|
|
505
|
+
plan.artifactRev = event.payload.artifact.rev
|
|
506
|
+
complete('plan', event.seq, { round })
|
|
507
|
+
break
|
|
508
|
+
case 'plan-review.started':
|
|
509
|
+
round = event.payload.round
|
|
510
|
+
start({ phase: 'plan-review', round, seq: event.seq })
|
|
511
|
+
break
|
|
512
|
+
case 'plan-review.verdict':
|
|
513
|
+
round = event.payload.round
|
|
514
|
+
plan.approved = event.payload.verdict === 'approve'
|
|
515
|
+
// Only an approve stands; any other verdict clears it, so
|
|
516
|
+
// `approval !== undefined` ⇔ `approved`.
|
|
517
|
+
if (plan.approved) plan.approval = { seq: event.seq, round }
|
|
518
|
+
else delete plan.approval
|
|
519
|
+
setFindings(planReviewFindings, round, event.payload.findings)
|
|
520
|
+
complete('plan-review', event.seq, { round })
|
|
521
|
+
break
|
|
522
|
+
case 'implement.started':
|
|
523
|
+
round = event.payload.round
|
|
524
|
+
implement.round = round
|
|
525
|
+
start({ phase: 'implement', round, seq: event.seq })
|
|
526
|
+
break
|
|
527
|
+
case 'implement.completed':
|
|
528
|
+
round = event.payload.round
|
|
529
|
+
implement.round = round
|
|
530
|
+
implement.commits = event.payload.commits
|
|
531
|
+
implement.artifactRev = event.payload.artifact.rev
|
|
532
|
+
complete('implement', event.seq, { round })
|
|
533
|
+
break
|
|
534
|
+
case 'code-review.started':
|
|
535
|
+
round = event.payload.round
|
|
536
|
+
start({ phase: 'code-review', round, seq: event.seq })
|
|
537
|
+
break
|
|
538
|
+
case 'code-review.verdict':
|
|
539
|
+
round = event.payload.round
|
|
540
|
+
codeReviewApproved = event.payload.verdict === 'approve'
|
|
541
|
+
codeReviewApproval = codeReviewApproved ? { seq: event.seq, round } : undefined
|
|
542
|
+
// §15.6-A: an approve means the code is settled and verifiable, so it
|
|
543
|
+
// opens a new verify cycle. This tracker is deliberately SEPARATE from
|
|
544
|
+
// `codeReviewApproval` — a later revise clears the approval but must
|
|
545
|
+
// NOT move the cycle boundary backwards (mirrors engine.ts:637, which
|
|
546
|
+
// never resets `latestApproveSeq`).
|
|
547
|
+
if (codeReviewApproved) verify.cycleSince = Math.max(verify.cycleSince, event.seq)
|
|
548
|
+
setFindings(codeReviewFindings, round, event.payload.findings)
|
|
549
|
+
complete('code-review', event.seq, { round })
|
|
550
|
+
break
|
|
551
|
+
|
|
552
|
+
case 'verify.started':
|
|
553
|
+
verify.maxAttemptSeen = Math.max(verify.maxAttemptSeen, event.payload.attempt)
|
|
554
|
+
verify.currentStep = event.payload.step
|
|
555
|
+
start({
|
|
556
|
+
phase: verifyPhase(event.payload.step),
|
|
557
|
+
attempt: event.payload.attempt,
|
|
558
|
+
seq: event.seq,
|
|
559
|
+
})
|
|
560
|
+
break
|
|
561
|
+
case 'verify.completed': {
|
|
562
|
+
const result = normalizeVerifyCompletion(event.payload)
|
|
563
|
+
verify.maxAttemptSeen = Math.max(verify.maxAttemptSeen, result.attempt)
|
|
564
|
+
verify.results.push({
|
|
565
|
+
step: result.step,
|
|
566
|
+
attempt: result.attempt,
|
|
567
|
+
outcome: result.outcome,
|
|
568
|
+
report: result.report,
|
|
569
|
+
...(result.reason !== undefined ? { reason: result.reason } : {}),
|
|
570
|
+
seq: event.seq,
|
|
571
|
+
})
|
|
572
|
+
if (verify.currentStep === result.step) {
|
|
573
|
+
verify.currentStep = undefined
|
|
574
|
+
}
|
|
575
|
+
complete(verifyPhase(result.step), event.seq, {
|
|
576
|
+
attempt: result.attempt,
|
|
577
|
+
})
|
|
578
|
+
break
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
case 'finalize.started':
|
|
582
|
+
start({ phase: 'finalize', seq: event.seq })
|
|
583
|
+
break
|
|
584
|
+
case 'finalize.completed':
|
|
585
|
+
pr = event.payload.pr
|
|
586
|
+
prState = 'open'
|
|
587
|
+
finalizeCompletedSeq = event.seq
|
|
588
|
+
complete('finalize', event.seq)
|
|
589
|
+
break
|
|
590
|
+
case 'finalize.step-completed':
|
|
591
|
+
// §5: post-steps are independent and failure-tolerant — a completion
|
|
592
|
+
// counts whether `ok` is true or false. Retain full-log history with
|
|
593
|
+
// occurrence identity; readers scope it with `seq > restartBoundary`.
|
|
594
|
+
finalizeSteps.push({ step: event.payload.step, ok: event.payload.ok, seq: event.seq })
|
|
595
|
+
break
|
|
596
|
+
|
|
597
|
+
case 'pr.auto-merge-enabled':
|
|
598
|
+
autoMerge.applied = { enabled: true, commandSeq: event.payload.commandSeq }
|
|
599
|
+
break
|
|
600
|
+
case 'pr.auto-merge-disabled':
|
|
601
|
+
autoMerge.applied = { enabled: false, commandSeq: event.payload.commandSeq }
|
|
602
|
+
break
|
|
603
|
+
case 'pr.merged':
|
|
604
|
+
prState = 'merged'
|
|
605
|
+
break
|
|
606
|
+
case 'pr.closed':
|
|
607
|
+
prState = 'closed'
|
|
608
|
+
break
|
|
609
|
+
case 'pr.conflicted':
|
|
610
|
+
prState = 'conflicted'
|
|
611
|
+
break
|
|
612
|
+
case 'reconcile.started':
|
|
613
|
+
reconcileAttempts = Math.max(reconcileAttempts, event.payload.attempt)
|
|
614
|
+
start({
|
|
615
|
+
phase: 'reconcile',
|
|
616
|
+
attempt: event.payload.attempt,
|
|
617
|
+
seq: event.seq,
|
|
618
|
+
})
|
|
619
|
+
break
|
|
620
|
+
case 'reconcile.completed': {
|
|
621
|
+
// The terminal payload has no attempt, so preserve it from the
|
|
622
|
+
// matching in-flight occurrence before complete() clears that context.
|
|
623
|
+
// An unmatched completion remains a bare context rather than borrowing
|
|
624
|
+
// the aggregate reconcileAttempts high-water.
|
|
625
|
+
const attempt = currentPhase?.phase === 'reconcile' ? currentPhase.attempt : undefined
|
|
626
|
+
// §15.7: conflicted until a reconcile.completed appears after the
|
|
627
|
+
// pr.conflicted — the PR is open again while verify:* re-runs.
|
|
628
|
+
if (prState === 'conflicted') prState = 'open'
|
|
629
|
+
// Reconciliation changed the code, so verify re-runs in full — a new
|
|
630
|
+
// cycle (§15.7, engine.ts:275).
|
|
631
|
+
verify.cycleSince = Math.max(verify.cycleSince, event.seq)
|
|
632
|
+
complete('reconcile', event.seq, attempt !== undefined ? { attempt } : undefined)
|
|
633
|
+
break
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
case 'observation.recorded':
|
|
637
|
+
observations.push(event.payload)
|
|
638
|
+
break
|
|
639
|
+
case 'escalation.raised':
|
|
640
|
+
openEscalations.set(event.payload.id, {
|
|
641
|
+
id: event.payload.id,
|
|
642
|
+
phase: event.payload.phase,
|
|
643
|
+
round: event.payload.round,
|
|
644
|
+
source: event.payload.source,
|
|
645
|
+
...(event.payload.policyCause !== undefined
|
|
646
|
+
? { policyCause: event.payload.policyCause }
|
|
647
|
+
: {}),
|
|
648
|
+
question: event.payload.question,
|
|
649
|
+
refs: event.payload.refs,
|
|
650
|
+
seq: event.seq,
|
|
651
|
+
})
|
|
652
|
+
break
|
|
653
|
+
case 'escalation.answered': {
|
|
654
|
+
// Matched by id — answers may arrive out of raise order. An answer
|
|
655
|
+
// with no open raise is ignored (total over any prefix).
|
|
656
|
+
const open = openEscalations.get(event.payload.id)
|
|
657
|
+
if (open) {
|
|
658
|
+
openEscalations.delete(event.payload.id)
|
|
659
|
+
answeredEscalations.push({
|
|
660
|
+
...open,
|
|
661
|
+
answer: event.payload.answer,
|
|
662
|
+
resolution: event.payload.resolution,
|
|
663
|
+
...(event.payload.artifact !== undefined ? { artifact: event.payload.artifact } : {}),
|
|
664
|
+
...(event.payload.reviewRoundCeiling !== undefined
|
|
665
|
+
? { reviewRoundCeiling: event.payload.reviewRoundCeiling }
|
|
666
|
+
: {}),
|
|
667
|
+
answeredSeq: event.seq,
|
|
668
|
+
})
|
|
669
|
+
if (
|
|
670
|
+
event.payload.reviewRoundCeiling !== undefined &&
|
|
671
|
+
open.source === 'policy' &&
|
|
672
|
+
open.policyCause === 'review-round-limit'
|
|
673
|
+
) {
|
|
674
|
+
if (open.phase === 'plan-review') {
|
|
675
|
+
reviewRoundCeilings.plan = event.payload.reviewRoundCeiling
|
|
676
|
+
} else if (open.phase === 'code-review') {
|
|
677
|
+
reviewRoundCeilings.code = event.payload.reviewRoundCeiling
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
// Abort is accepted through either supported operator surface. Keep
|
|
681
|
+
// the answer as the audit reason and project its event identity as
|
|
682
|
+
// the same pending command an explicit abort request creates; the
|
|
683
|
+
// kernel's one build.aborted fact acknowledges both pathways.
|
|
684
|
+
if (event.payload.resolution === 'abort') {
|
|
685
|
+
pending.abort.push({
|
|
686
|
+
command: 'abort',
|
|
687
|
+
seq: event.seq,
|
|
688
|
+
reason: event.payload.answer,
|
|
689
|
+
actor: event.actor,
|
|
690
|
+
})
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
break
|
|
694
|
+
}
|
|
695
|
+
case 'phase.failed':
|
|
696
|
+
failures[event.payload.phase] = (failures[event.payload.phase] ?? 0) + 1
|
|
697
|
+
break
|
|
698
|
+
}
|
|
699
|
+
lastEvent = event
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
// §15.5 precedence: aborted/done (terminal, latest wins) > paused >
|
|
703
|
+
// blocked > running > queued. See BuildState.status for the paused+blocked
|
|
704
|
+
// overlap rule.
|
|
705
|
+
const status: BuildStatus =
|
|
706
|
+
terminal ??
|
|
707
|
+
(pausedFlag ? 'paused' : openEscalations.size > 0 ? 'blocked' : attached ? 'running' : 'queued')
|
|
708
|
+
|
|
709
|
+
const active = currentPhase ?? lastCompletedPhase
|
|
710
|
+
|
|
711
|
+
return {
|
|
712
|
+
status,
|
|
713
|
+
outcome,
|
|
714
|
+
phase: active?.phase,
|
|
715
|
+
round,
|
|
716
|
+
currentPhase,
|
|
717
|
+
lastCompletedPhase,
|
|
718
|
+
openEscalations: [...openEscalations.values()],
|
|
719
|
+
answeredEscalations,
|
|
720
|
+
reviewRoundCeilings,
|
|
721
|
+
pr,
|
|
722
|
+
prState,
|
|
723
|
+
autoMerge,
|
|
724
|
+
finalizeCompletedSeq,
|
|
725
|
+
finalizeSteps,
|
|
726
|
+
lastEvent,
|
|
727
|
+
lastSeq: lastEvent?.seq ?? 0,
|
|
728
|
+
specRev,
|
|
729
|
+
restartSince,
|
|
730
|
+
plan,
|
|
731
|
+
implement,
|
|
732
|
+
codeReviewApproved,
|
|
733
|
+
codeReviewApproval,
|
|
734
|
+
reviewFindings: { planReview: planReviewFindings, codeReview: codeReviewFindings },
|
|
735
|
+
verify,
|
|
736
|
+
reconcileAttempts,
|
|
737
|
+
observations,
|
|
738
|
+
setupFailures,
|
|
739
|
+
setupFailure,
|
|
740
|
+
dispatchFailures,
|
|
741
|
+
executions,
|
|
742
|
+
infrastructureFailures,
|
|
743
|
+
infrastructureFailure,
|
|
744
|
+
cleanupAttempts,
|
|
745
|
+
...(terminal === undefined && discardRequest !== undefined ? { discardRequest } : {}),
|
|
746
|
+
pendingCommands: [...pending.pause, ...pending.resume, ...pending.abort].sort(
|
|
747
|
+
(a, b) => a.seq - b.seq,
|
|
748
|
+
),
|
|
749
|
+
sessions: { open: [...openSessions.values()] },
|
|
750
|
+
failures,
|
|
751
|
+
}
|
|
752
|
+
}
|