@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,259 @@
|
|
|
1
|
+
import type { AbEvent, EventEnvelope, EventWrite } from '../events/catalog'
|
|
2
|
+
import { KERNEL } from '../events/envelope'
|
|
3
|
+
import { autoMergeDeferralClasses, type AutoMergeDeferralReason } from '../ports/types'
|
|
4
|
+
import type { BuildStore } from '../store/types'
|
|
5
|
+
import type { BuildState } from './reducer'
|
|
6
|
+
|
|
7
|
+
/** The complete GitHub `mergeStateStatus` enum. Keeping this list closed is a
|
|
8
|
+
* safety property: a future forge state cannot silently become direct-merge
|
|
9
|
+
* eligible. */
|
|
10
|
+
export const mergeStateStatuses = [
|
|
11
|
+
'BEHIND',
|
|
12
|
+
'BLOCKED',
|
|
13
|
+
'CLEAN',
|
|
14
|
+
'DIRTY',
|
|
15
|
+
'DRAFT',
|
|
16
|
+
'HAS_HOOKS',
|
|
17
|
+
'UNKNOWN',
|
|
18
|
+
'UNSTABLE',
|
|
19
|
+
] as const
|
|
20
|
+
|
|
21
|
+
export type MergeStateStatus = (typeof mergeStateStatuses)[number]
|
|
22
|
+
export type MergeGatePresence = 'present' | 'absent'
|
|
23
|
+
|
|
24
|
+
export type AutoMergeEnableDisposition =
|
|
25
|
+
| { kind: 'native' }
|
|
26
|
+
| { kind: 'direct' }
|
|
27
|
+
| { kind: 'deferred' }
|
|
28
|
+
| { kind: 'error'; reason: string }
|
|
29
|
+
|
|
30
|
+
const NATIVE = { kind: 'native' } as const
|
|
31
|
+
const DIRECT = { kind: 'direct' } as const
|
|
32
|
+
const DEFERRED = { kind: 'deferred' } as const
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Route from two independent facts: whether a real merge gate exists and the
|
|
36
|
+
* PR's current GitHub merge state. In particular, CLEAN never proves that a
|
|
37
|
+
* branch is ungated: a gated branch whose requirements passed is CLEAN too.
|
|
38
|
+
*/
|
|
39
|
+
const ENABLE_DISPOSITIONS = {
|
|
40
|
+
BEHIND: { present: NATIVE, absent: DIRECT },
|
|
41
|
+
BLOCKED: {
|
|
42
|
+
present: NATIVE,
|
|
43
|
+
absent: {
|
|
44
|
+
kind: 'error',
|
|
45
|
+
reason: 'GitHub reports the PR BLOCKED despite no discovered merge-blocking gate',
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
CLEAN: { present: NATIVE, absent: DIRECT },
|
|
49
|
+
DIRTY: { present: DEFERRED, absent: DEFERRED },
|
|
50
|
+
DRAFT: {
|
|
51
|
+
present: {
|
|
52
|
+
kind: 'error',
|
|
53
|
+
reason: 'GitHub reports the PR as DRAFT; Autobuild only finalizes ready PRs',
|
|
54
|
+
},
|
|
55
|
+
absent: {
|
|
56
|
+
kind: 'error',
|
|
57
|
+
reason: 'GitHub reports the PR as DRAFT; Autobuild only finalizes ready PRs',
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
HAS_HOOKS: { present: NATIVE, absent: NATIVE },
|
|
61
|
+
UNKNOWN: { present: NATIVE, absent: DEFERRED },
|
|
62
|
+
UNSTABLE: { present: NATIVE, absent: DIRECT },
|
|
63
|
+
} as const satisfies Record<MergeStateStatus, Record<MergeGatePresence, AutoMergeEnableDisposition>>
|
|
64
|
+
|
|
65
|
+
export function classifyAutoMergeEnable(
|
|
66
|
+
mergeState: MergeStateStatus,
|
|
67
|
+
gate: MergeGatePresence,
|
|
68
|
+
): AutoMergeEnableDisposition {
|
|
69
|
+
return ENABLE_DISPOSITIONS[mergeState][gate]
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** A human auto-merge command whose desired forge state has not yet been
|
|
73
|
+
* acknowledged by a matching application fact. */
|
|
74
|
+
export interface PendingAutoMerge {
|
|
75
|
+
enabled: boolean
|
|
76
|
+
commandSeq: number
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Compare durable desired state with the correlated forge application fact.
|
|
81
|
+
* A value mismatch OR a command-seq mismatch is pending. This is the recovery
|
|
82
|
+
* predicate at both plumbing seams: retries are safe because Forge.setAutoMerge
|
|
83
|
+
* is idempotent.
|
|
84
|
+
*/
|
|
85
|
+
export function pendingAutoMerge(
|
|
86
|
+
state: Pick<BuildState, 'autoMerge'>,
|
|
87
|
+
): PendingAutoMerge | undefined {
|
|
88
|
+
const { requested, commandSeq, applied } = state.autoMerge
|
|
89
|
+
if (commandSeq === undefined) return undefined
|
|
90
|
+
if (applied?.commandSeq === commandSeq && applied.enabled === requested) {
|
|
91
|
+
return undefined
|
|
92
|
+
}
|
|
93
|
+
return { enabled: requested, commandSeq }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function autoMergeApplicationType(
|
|
97
|
+
enabled: boolean,
|
|
98
|
+
): 'pr.auto-merge-enabled' | 'pr.auto-merge-disabled' {
|
|
99
|
+
return enabled ? 'pr.auto-merge-enabled' : 'pr.auto-merge-disabled'
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Stable correlation key for the one follow-up allowed per PR/consent command. */
|
|
103
|
+
export function autoMergeDeferralRef(prNumber: number, commandSeq: number): string {
|
|
104
|
+
return `auto-merge-gate:pr:${prNumber}:command:${commandSeq}`
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* The one live deferral observation for a PR/consent command, or undefined.
|
|
109
|
+
* An observation is current only while the PR is open — any `pr.merged` or
|
|
110
|
+
* `pr.closed` fact in the log ends it — and until later evidence proves the
|
|
111
|
+
* PR head changed, superseding the recorded condition until the gate
|
|
112
|
+
* re-examines consent and records a fresh observation. Head-changing evidence
|
|
113
|
+
* is a `reconcile.completed` (the resolution is a merge commit on the branch)
|
|
114
|
+
* or a `finalize.completed` whose head differs from the head the observation
|
|
115
|
+
* was recorded under. The dedupe and the projection share this predicate, so
|
|
116
|
+
* a stale observation stops suppressing and re-recording after reconcile
|
|
117
|
+
* works.
|
|
118
|
+
*
|
|
119
|
+
* The head at observation time is the newest `finalize.completed` before it:
|
|
120
|
+
* the record seam always runs right after that event is appended. An
|
|
121
|
+
* observation with no preceding `finalize.completed` (janitor-recorded seeds,
|
|
122
|
+
* synthetic logs) takes its head from the FIRST following `finalize.completed`
|
|
123
|
+
* instead of being superseded by it — the §8.7 retry that adopts the same PR
|
|
124
|
+
* at the same head must not invalidate the marker it honors. A
|
|
125
|
+
* `finalize.completed` naming a different head is a rebuilt pipeline
|
|
126
|
+
* re-finalizing after new commits, and it does make the observation stale.
|
|
127
|
+
*
|
|
128
|
+
* Ending currency on any terminal PR fact in the log is safe only because a
|
|
129
|
+
* build finalizes one PR; if a future feature ever re-finalizes onto a second
|
|
130
|
+
* PR, revisit.
|
|
131
|
+
*/
|
|
132
|
+
export function currentDeferralObservation(
|
|
133
|
+
events: AbEvent[],
|
|
134
|
+
prNumber: number,
|
|
135
|
+
commandSeq: number,
|
|
136
|
+
): Extract<AbEvent, { type: 'observation.recorded' }> | undefined {
|
|
137
|
+
const marker = autoMergeDeferralRef(prNumber, commandSeq)
|
|
138
|
+
let observation: Extract<AbEvent, { type: 'observation.recorded' }> | undefined
|
|
139
|
+
for (let index = events.length - 1; index >= 0; index -= 1) {
|
|
140
|
+
const event = events[index]
|
|
141
|
+
if (
|
|
142
|
+
event !== undefined &&
|
|
143
|
+
event.type === 'observation.recorded' &&
|
|
144
|
+
event.payload.refs?.includes(marker) === true
|
|
145
|
+
) {
|
|
146
|
+
observation = event
|
|
147
|
+
break
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (observation === undefined) return undefined
|
|
151
|
+
|
|
152
|
+
let headAtObservation: string | undefined
|
|
153
|
+
for (const event of events) {
|
|
154
|
+
if (event.seq >= observation.seq) break
|
|
155
|
+
if (event.type === 'finalize.completed') headAtObservation = event.payload.pr.headSha
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
for (const event of events) {
|
|
159
|
+
if (event.seq <= observation.seq) continue
|
|
160
|
+
if (event.type === 'pr.merged' || event.type === 'pr.closed') return undefined
|
|
161
|
+
if (event.type === 'reconcile.completed') return undefined
|
|
162
|
+
if (event.type === 'finalize.completed') {
|
|
163
|
+
if (headAtObservation === undefined) headAtObservation = event.payload.pr.headSha
|
|
164
|
+
else if (event.payload.pr.headSha !== headAtObservation) return undefined
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return observation
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function hasAutoMergeDeferralObservation(
|
|
171
|
+
events: AbEvent[],
|
|
172
|
+
prNumber: number,
|
|
173
|
+
commandSeq: number,
|
|
174
|
+
): boolean {
|
|
175
|
+
return currentDeferralObservation(events, prNumber, commandSeq) !== undefined
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* The exact provider-bearing summary for the current, unapplied enable
|
|
180
|
+
* command. Deferral observations are durable history, so correlation to both
|
|
181
|
+
* the current PR and command is what prevents an old reason from looking live
|
|
182
|
+
* after consent is applied, cancelled, or replaced — and the shared currency
|
|
183
|
+
* predicate is what keeps a superseded or terminal-PR observation from
|
|
184
|
+
* looking live at all.
|
|
185
|
+
*/
|
|
186
|
+
export function currentAutoMergeDeferral(
|
|
187
|
+
events: AbEvent[],
|
|
188
|
+
state: Pick<BuildState, 'autoMerge' | 'pr'>,
|
|
189
|
+
): string | undefined {
|
|
190
|
+
const pending = pendingAutoMerge(state)
|
|
191
|
+
if (pending?.enabled !== true || state.pr === undefined) return undefined
|
|
192
|
+
return currentDeferralObservation(events, state.pr.number, pending.commandSeq)?.payload.summary
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const DEFERRAL_SUMMARIES = {
|
|
196
|
+
'github-plan-limitation':
|
|
197
|
+
'GitHub rulesets are unavailable because of the repository account plan',
|
|
198
|
+
'repository-auto-merge-disabled': 'repository-level auto-merge is disabled',
|
|
199
|
+
'unproven-gate-state': 'merge-gate state or native auto-merge application could not be proven',
|
|
200
|
+
'merge-conflicts': 'the PR has merge conflicts with its base branch',
|
|
201
|
+
'mergeability-uncomputed':
|
|
202
|
+
'GitHub has not finished computing mergeability (transient; expected to resolve on retry)',
|
|
203
|
+
'local-base-checkout-dirty': 'local merge is blocked by uncommitted work in the base checkout',
|
|
204
|
+
'local-git-identity-missing':
|
|
205
|
+
'local squash requires a configured Git author and committer identity',
|
|
206
|
+
} as const satisfies Record<AutoMergeDeferralReason['code'], string>
|
|
207
|
+
|
|
208
|
+
/** Kernel-authored durable diagnostic for a human-actionable declined consent. */
|
|
209
|
+
export function autoMergeDeferralObservation(
|
|
210
|
+
reason: AutoMergeDeferralReason,
|
|
211
|
+
prNumber: number,
|
|
212
|
+
commandSeq: number,
|
|
213
|
+
id: string,
|
|
214
|
+
): EventWrite<'observation.recorded'> {
|
|
215
|
+
return {
|
|
216
|
+
actor: KERNEL,
|
|
217
|
+
type: 'observation.recorded',
|
|
218
|
+
payload: {
|
|
219
|
+
id,
|
|
220
|
+
kind: 'followup',
|
|
221
|
+
summary:
|
|
222
|
+
`Auto-merge gate could not apply consent for PR #${prNumber}: ` +
|
|
223
|
+
`${DEFERRAL_SUMMARIES[reason.code]} — ${reason.detail}`,
|
|
224
|
+
refs: [autoMergeDeferralRef(prNumber, commandSeq)],
|
|
225
|
+
},
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Record the one durable diagnostic allowed for a PR/auto-merge command —
|
|
231
|
+
* only for a deferral a person must fix. Pipeline-owned and transient codes
|
|
232
|
+
* (`merge-conflicts`, `mergeability-uncomputed`) resolve through reconcile or
|
|
233
|
+
* a later poll, so they record nothing: consent stays pending and later ticks
|
|
234
|
+
* re-examine it. Every comparison is against the authoritative stream tail. A
|
|
235
|
+
* concurrent unrelated append merely causes a retry; a concurrent matching
|
|
236
|
+
* append makes the next read return without writing a duplicate.
|
|
237
|
+
*/
|
|
238
|
+
export async function recordAutoMergeDeferralObservation(
|
|
239
|
+
store: BuildStore,
|
|
240
|
+
slug: string,
|
|
241
|
+
reason: AutoMergeDeferralReason,
|
|
242
|
+
prNumber: number,
|
|
243
|
+
commandSeq: number,
|
|
244
|
+
id: string,
|
|
245
|
+
): Promise<EventEnvelope<'observation.recorded'> | null> {
|
|
246
|
+
if (autoMergeDeferralClasses[reason.code] === 'pipeline-resolved') return null
|
|
247
|
+
while (true) {
|
|
248
|
+
const events = await store.getEvents(slug)
|
|
249
|
+
if (hasAutoMergeDeferralObservation(events, prNumber, commandSeq)) return null
|
|
250
|
+
|
|
251
|
+
const expectedSeq = events.at(-1)?.seq ?? 0
|
|
252
|
+
const appended = await store.appendIfCurrent(
|
|
253
|
+
slug,
|
|
254
|
+
expectedSeq,
|
|
255
|
+
autoMergeDeferralObservation(reason, prNumber, commandSeq, id),
|
|
256
|
+
)
|
|
257
|
+
if (appended !== null) return appended
|
|
258
|
+
}
|
|
259
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The generic review-loop primitive (SPEC §10): one tested state machine for
|
|
3
|
+
* both the plan loop and the code loop. It owns rounds, feedback threading,
|
|
4
|
+
* verdict routing, and the stall/policy thresholds — and nothing else: the
|
|
5
|
+
* callbacks own all I/O (sessions, store deposits, plumbing), so this module
|
|
6
|
+
* never touches a port or the store.
|
|
7
|
+
*
|
|
8
|
+
* Memory model (§10): the producer continues its session across rounds — its
|
|
9
|
+
* feedback is only the latest verdict's finding ids, assembled
|
|
10
|
+
* deterministically. The reviewer is a fresh skeptic each round and receives
|
|
11
|
+
* ALL prior rounds' findings so it can mark `persists` (§15.4).
|
|
12
|
+
*
|
|
13
|
+
* Findings are opaque here: ids are stamped at deposit (D6) before a verdict
|
|
14
|
+
* ever reaches this function, and converge never copies or mutates them —
|
|
15
|
+
* the same objects flow into `stalledChains` and back out in the outcome.
|
|
16
|
+
*
|
|
17
|
+
* Relationship to the engine: the MAINLINE loop is `decideLoop` inside
|
|
18
|
+
* `decideNext` (src/kernel/engine.ts) — the event-sourced re-statement of
|
|
19
|
+
* this primitive, because the kernel's resume path must re-decide from the
|
|
20
|
+
* log (§2.2) rather than hold a loop on the stack. converge remains the §10
|
|
21
|
+
* reference semantics for in-process callers, and the two implementations
|
|
22
|
+
* are pinned together by the differential suite in converge.test.ts
|
|
23
|
+
* ('converge ⇄ decideNext differential'): the same verdict scripts must
|
|
24
|
+
* produce the same rounds, feedback, and outcome in both. Edit loop
|
|
25
|
+
* semantics (thresholds, stall ordering, feedback assembly) in BOTH places
|
|
26
|
+
* or the differential fails.
|
|
27
|
+
*/
|
|
28
|
+
import type { Feedback, Finding, Verdict } from '../ontology'
|
|
29
|
+
import { stalledChains, type FindingChain } from './stall'
|
|
30
|
+
|
|
31
|
+
export interface ConvergePolicy {
|
|
32
|
+
/** Producer/review round pairs allowed before escalating (source: policy). */
|
|
33
|
+
maxRounds: number
|
|
34
|
+
/** Persistence-chain streak that auto-escalates (source: stall — §15.4). */
|
|
35
|
+
stallRounds: number
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export type ConvergeOutcome<A> =
|
|
39
|
+
| { outcome: 'approved'; artifact: A; rounds: number }
|
|
40
|
+
| {
|
|
41
|
+
outcome: 'escalated'
|
|
42
|
+
source: 'agent' | 'stall' | 'policy'
|
|
43
|
+
reason: string
|
|
44
|
+
rounds: number
|
|
45
|
+
chain?: FindingChain
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function converge<A>(opts: {
|
|
49
|
+
produce: (feedback: Feedback | null, round: number) => Promise<A>
|
|
50
|
+
review: (artifact: A, round: number, priorRounds: Finding[][]) => Promise<Verdict>
|
|
51
|
+
/** Static policy for ordinary callers, or a boundary accessor for a
|
|
52
|
+
* long-running repository workflow that hot-reloads policy between rounds. */
|
|
53
|
+
policy: ConvergePolicy | (() => ConvergePolicy)
|
|
54
|
+
/** Chains a human already resolved via dismiss-finding (§15.6-B). */
|
|
55
|
+
dismissedIds?: ReadonlySet<string>
|
|
56
|
+
/** Durable callers may resume after prior revise rounds. Defaults preserve
|
|
57
|
+
* the ordinary in-memory loop (round 1, no history). */
|
|
58
|
+
startRound?: number
|
|
59
|
+
priorRounds?: Finding[][]
|
|
60
|
+
initialFeedback?: Feedback | null
|
|
61
|
+
}): Promise<ConvergeOutcome<A>> {
|
|
62
|
+
const { produce, review, dismissedIds } = opts
|
|
63
|
+
const currentPolicy = (): ConvergePolicy =>
|
|
64
|
+
typeof opts.policy === 'function' ? opts.policy() : opts.policy
|
|
65
|
+
const roundFindings: Finding[][] = (opts.priorRounds ?? []).map((round) => [...round])
|
|
66
|
+
let feedback: Feedback | null = opts.initialFeedback ?? null
|
|
67
|
+
|
|
68
|
+
for (let round = opts.startRound ?? 1; ; round += 1) {
|
|
69
|
+
const openingPolicy = currentPolicy()
|
|
70
|
+
if (round > openingPolicy.maxRounds) {
|
|
71
|
+
return {
|
|
72
|
+
outcome: 'escalated',
|
|
73
|
+
source: 'policy',
|
|
74
|
+
reason: `maxRounds (${openingPolicy.maxRounds}) exhausted without approval`,
|
|
75
|
+
rounds: round - 1,
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const artifact = await produce(feedback, round)
|
|
80
|
+
const verdict = await review(artifact, round, roundFindings.slice())
|
|
81
|
+
|
|
82
|
+
if (verdict.verdict === 'approve') {
|
|
83
|
+
return { outcome: 'approved', artifact, rounds: round }
|
|
84
|
+
}
|
|
85
|
+
if (verdict.verdict === 'escalate') {
|
|
86
|
+
return {
|
|
87
|
+
outcome: 'escalated',
|
|
88
|
+
source: 'agent',
|
|
89
|
+
reason: verdict.reason,
|
|
90
|
+
rounds: round,
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
roundFindings.push(verdict.findings)
|
|
95
|
+
|
|
96
|
+
// The kernel applies the stall threshold after every revise, BEFORE
|
|
97
|
+
// burning another produce round (§15.4). Deepest chain reported; first
|
|
98
|
+
// in root order on ties.
|
|
99
|
+
const stalled = stalledChains(roundFindings, currentPolicy().stallRounds, dismissedIds)
|
|
100
|
+
if (stalled.length > 0) {
|
|
101
|
+
const chain = stalled.reduce((deepest, candidate) =>
|
|
102
|
+
candidate.rounds > deepest.rounds ? candidate : deepest,
|
|
103
|
+
)
|
|
104
|
+
return {
|
|
105
|
+
outcome: 'escalated',
|
|
106
|
+
source: 'stall',
|
|
107
|
+
reason: `finding chain persisted ${chain.rounds} rounds: ${chain.ids.join(' -> ')}`,
|
|
108
|
+
rounds: round,
|
|
109
|
+
chain,
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Round N+1's producer prompt is assembled deterministically from finding
|
|
114
|
+
// ids (§10) — payloads carry refs, never blobs (§15.2.3).
|
|
115
|
+
feedback = { findings: verdict.findings.map((finding) => finding.id) }
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure projection of repository-scoped dispatcher controls — the intake gate,
|
|
3
|
+
* the repository-wide pause, and the claim-time auto-merge default. The journal
|
|
4
|
+
* is authoritative; fresh repositories retain the historical process defaults.
|
|
5
|
+
*/
|
|
6
|
+
import type { RepositoryEvent } from '../events/repository'
|
|
7
|
+
|
|
8
|
+
export const DEFAULT_DISPATCH_INTAKE = true
|
|
9
|
+
export const DEFAULT_DISPATCH_PAUSED = false
|
|
10
|
+
export const DEFAULT_DISPATCH_AUTO_MERGE = false
|
|
11
|
+
|
|
12
|
+
export interface DispatchSettings {
|
|
13
|
+
intake: boolean
|
|
14
|
+
/**
|
|
15
|
+
* Repository-wide quiescence: while true, no queued build may be given a
|
|
16
|
+
* runner. Distinct from the per-build pause (a build-log fact with its own
|
|
17
|
+
* reducer precedence) and from `HarvestState.paused` (the observation
|
|
18
|
+
* workflow's own gate, which this does not govern).
|
|
19
|
+
*/
|
|
20
|
+
paused: boolean
|
|
21
|
+
defaultAutoMerge: boolean
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function reduceDispatchSettings(events: RepositoryEvent[]): DispatchSettings {
|
|
25
|
+
let intake = DEFAULT_DISPATCH_INTAKE
|
|
26
|
+
let paused = DEFAULT_DISPATCH_PAUSED
|
|
27
|
+
let defaultAutoMerge = DEFAULT_DISPATCH_AUTO_MERGE
|
|
28
|
+
let intakeSeq = 0
|
|
29
|
+
let pausedSeq = 0
|
|
30
|
+
let autoMergeSeq = 0
|
|
31
|
+
|
|
32
|
+
for (const event of events) {
|
|
33
|
+
switch (event.type) {
|
|
34
|
+
case 'dispatcher.intake-set':
|
|
35
|
+
if (event.seq > intakeSeq) {
|
|
36
|
+
intake = event.payload.enabled
|
|
37
|
+
intakeSeq = event.seq
|
|
38
|
+
}
|
|
39
|
+
break
|
|
40
|
+
case 'dispatcher.pause-set':
|
|
41
|
+
if (event.seq > pausedSeq) {
|
|
42
|
+
paused = event.payload.enabled
|
|
43
|
+
pausedSeq = event.seq
|
|
44
|
+
}
|
|
45
|
+
break
|
|
46
|
+
case 'dispatcher.auto-merge-default-set':
|
|
47
|
+
if (event.seq > autoMergeSeq) {
|
|
48
|
+
defaultAutoMerge = event.payload.enabled
|
|
49
|
+
autoMergeSeq = event.seq
|
|
50
|
+
}
|
|
51
|
+
break
|
|
52
|
+
default:
|
|
53
|
+
// Harvest facts share this journal and have no dispatcher-setting
|
|
54
|
+
// meaning. Each setting is reduced independently.
|
|
55
|
+
break
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return { intake, paused, defaultAutoMerge }
|
|
60
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import type { RepositoryEvent } from '../events/repository'
|
|
2
|
+
import type { ArtifactRef } from '../ontology'
|
|
3
|
+
|
|
4
|
+
export type DispatchHealth = 'starting' | 'running' | 'stopped' | 'failed'
|
|
5
|
+
|
|
6
|
+
/** Store-only projection consumed by terminal (and future) frontends. */
|
|
7
|
+
export interface DispatchStatus {
|
|
8
|
+
run: string
|
|
9
|
+
health: DispatchHealth
|
|
10
|
+
effectiveConfig?: ArtifactRef
|
|
11
|
+
queued?: number
|
|
12
|
+
/** Standing count from newer tick facts; absent for historical events. */
|
|
13
|
+
creationWithheld?: number
|
|
14
|
+
availableUpgrade?: string
|
|
15
|
+
roleWarnings: string[]
|
|
16
|
+
diagnostics: string[]
|
|
17
|
+
/** Latest notice of warning/failure severity, retained independently from routine notices. */
|
|
18
|
+
warningNotice?: string
|
|
19
|
+
notice?: string
|
|
20
|
+
lastSeq: number
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function errorNotice(prefix: string, error: string): string {
|
|
24
|
+
return `${prefix}: ${error}`
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Reduce one correlated dispatch run without consulting process-local state.
|
|
28
|
+
* An optional prior projection permits strictly newer repository deltas.
|
|
29
|
+
* Successful ticks supersede standing diagnostics; failures retain the last
|
|
30
|
+
* known queue/config values instead of fabricating replacements. */
|
|
31
|
+
export function reduceDispatchStatus(
|
|
32
|
+
events: readonly RepositoryEvent[],
|
|
33
|
+
run: string,
|
|
34
|
+
previous?: Readonly<DispatchStatus>,
|
|
35
|
+
): DispatchStatus {
|
|
36
|
+
if (previous !== undefined && previous.run !== run) {
|
|
37
|
+
throw new Error(`cannot continue dispatch run "${run}" from "${previous.run}" status`)
|
|
38
|
+
}
|
|
39
|
+
const state: DispatchStatus =
|
|
40
|
+
previous === undefined
|
|
41
|
+
? {
|
|
42
|
+
run,
|
|
43
|
+
health: 'starting',
|
|
44
|
+
roleWarnings: [],
|
|
45
|
+
diagnostics: [],
|
|
46
|
+
lastSeq: 0,
|
|
47
|
+
}
|
|
48
|
+
: {
|
|
49
|
+
...previous,
|
|
50
|
+
roleWarnings: [...previous.roleWarnings],
|
|
51
|
+
diagnostics: [...previous.diagnostics],
|
|
52
|
+
}
|
|
53
|
+
for (const event of events) {
|
|
54
|
+
if (event.seq <= state.lastSeq) continue
|
|
55
|
+
const payload = event.payload as { run?: string }
|
|
56
|
+
if (payload.run !== run) continue
|
|
57
|
+
state.lastSeq = event.seq
|
|
58
|
+
switch (event.type) {
|
|
59
|
+
case 'dispatcher.run-started':
|
|
60
|
+
state.health = 'running'
|
|
61
|
+
state.effectiveConfig = event.payload.effectiveConfig
|
|
62
|
+
state.roleWarnings = [...event.payload.roleWarnings]
|
|
63
|
+
state.notice = undefined
|
|
64
|
+
state.warningNotice = undefined
|
|
65
|
+
break
|
|
66
|
+
case 'dispatcher.config-reloaded':
|
|
67
|
+
if (event.payload.effectiveConfig !== undefined) {
|
|
68
|
+
state.effectiveConfig = event.payload.effectiveConfig
|
|
69
|
+
}
|
|
70
|
+
if (event.payload.roleWarnings !== undefined) {
|
|
71
|
+
state.roleWarnings = [...event.payload.roleWarnings]
|
|
72
|
+
}
|
|
73
|
+
state.notice =
|
|
74
|
+
event.payload.restartRequired.length > 0
|
|
75
|
+
? `autobuild.toml reload requires dispatch restart for: ${event.payload.restartRequired.join(', ')}`
|
|
76
|
+
: 'autobuild.toml reloaded'
|
|
77
|
+
if (event.payload.restartRequired.length > 0) state.warningNotice = state.notice
|
|
78
|
+
break
|
|
79
|
+
case 'dispatcher.config-rejected':
|
|
80
|
+
state.notice = errorNotice('config reload rejected', event.payload.error)
|
|
81
|
+
state.warningNotice = state.notice
|
|
82
|
+
break
|
|
83
|
+
case 'dispatcher.config-publication-failed':
|
|
84
|
+
state.notice = errorNotice(
|
|
85
|
+
'config reload not applied because its durable trace failed',
|
|
86
|
+
event.payload.error,
|
|
87
|
+
)
|
|
88
|
+
state.warningNotice = state.notice
|
|
89
|
+
break
|
|
90
|
+
case 'dispatcher.tick-completed':
|
|
91
|
+
state.queued = event.payload.queued
|
|
92
|
+
state.creationWithheld = event.payload.counters.creationWithheld
|
|
93
|
+
state.diagnostics = [
|
|
94
|
+
...event.payload.janitorDiagnostics,
|
|
95
|
+
...event.payload.ticketDiagnostics,
|
|
96
|
+
...(event.payload.creationDiagnostics ?? []),
|
|
97
|
+
...event.payload.dependencyDiagnostics,
|
|
98
|
+
]
|
|
99
|
+
break
|
|
100
|
+
case 'dispatcher.tick-failed':
|
|
101
|
+
state.notice = errorNotice('tick failed', event.payload.error)
|
|
102
|
+
state.warningNotice = state.notice
|
|
103
|
+
break
|
|
104
|
+
case 'dispatcher.runner-settled':
|
|
105
|
+
if (event.payload.outcome === 'parked') {
|
|
106
|
+
state.notice = `build ${event.payload.slug} parked (${event.payload.status ?? 'unknown'})`
|
|
107
|
+
} else if (event.payload.outcome === 'lease-held') {
|
|
108
|
+
state.notice = `build ${event.payload.slug} already held by another runner — skipped`
|
|
109
|
+
} else {
|
|
110
|
+
state.notice = `build ${event.payload.slug} runner failed: ${event.payload.error ?? event.payload.outcome}`
|
|
111
|
+
}
|
|
112
|
+
state.warningNotice = state.notice
|
|
113
|
+
break
|
|
114
|
+
case 'dispatcher.harvest-runner-failed':
|
|
115
|
+
state.notice = errorNotice('harvest runner failed', event.payload.error)
|
|
116
|
+
state.warningNotice = state.notice
|
|
117
|
+
break
|
|
118
|
+
case 'dispatcher.upgrade-available':
|
|
119
|
+
state.availableUpgrade = event.payload.version
|
|
120
|
+
break
|
|
121
|
+
case 'dispatcher.operator-reported':
|
|
122
|
+
state.notice = event.payload.message
|
|
123
|
+
if (event.payload.level === 'warning') state.warningNotice = state.notice
|
|
124
|
+
break
|
|
125
|
+
case 'dispatcher.run-stopped':
|
|
126
|
+
state.health = event.payload.outcome === 'normal' ? 'stopped' : 'failed'
|
|
127
|
+
if (event.payload.outcome !== 'normal') {
|
|
128
|
+
state.notice = `dispatcher stopped unexpectedly${
|
|
129
|
+
event.payload.error !== undefined ? `: ${event.payload.error}` : ''
|
|
130
|
+
}`
|
|
131
|
+
state.warningNotice = state.notice
|
|
132
|
+
} else if (event.payload.reason === 'superseded') {
|
|
133
|
+
state.notice = 'dispatcher superseded by another invocation'
|
|
134
|
+
}
|
|
135
|
+
break
|
|
136
|
+
default:
|
|
137
|
+
break
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return state
|
|
141
|
+
}
|