@defrex/autobuild 0.9.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/SPEC.md +10 -0
- package/docs/architecture.md +109 -0
- package/docs/configuration.md +24 -3
- package/docs/remote-store-protocol.md +35 -0
- package/docs/website.md +4 -0
- package/package.json +6 -4
- package/packages/core/src/cli/dashboard/facts.ts +215 -0
- package/packages/core/src/cli/dashboard/frame-parse.ts +223 -0
- package/packages/core/src/cli/dashboard/model.ts +85 -147
- package/packages/core/src/cli/dashboard/poll.ts +7 -2
- package/packages/core/src/cli/dispatch-process.ts +1 -13
- package/packages/core/src/cli/dispatch.ts +80 -37
- package/packages/core/src/cli/help.ts +15 -1
- package/packages/core/src/cli/init-validation.ts +18 -0
- package/packages/core/src/cli/installation.ts +50 -8
- package/packages/core/src/cli/self-update.ts +77 -2
- package/packages/core/src/cli/status.ts +1 -1
- package/packages/core/src/cli/ticket.ts +146 -1
- package/packages/core/src/cli/wait.ts +10 -2
- package/packages/core/src/cli/watch.ts +10 -2
- package/packages/core/src/config/live.ts +1 -0
- package/packages/core/src/config/pipeline-source.ts +17 -5
- package/packages/core/src/config/schema.ts +21 -0
- package/packages/core/src/integration/harness.ts +113 -1
- package/packages/core/src/kernel/__snapshots__/reducer-registry.test.ts.snap +1369 -0
- package/packages/core/src/kernel/auto-merge-default.ts +43 -9
- package/packages/core/src/kernel/auto-merge.ts +99 -22
- package/packages/core/src/kernel/dispatch-settings.ts +42 -17
- package/packages/core/src/kernel/dispatch-status.ts +23 -0
- package/packages/core/src/kernel/engine.ts +9 -438
- package/packages/core/src/kernel/generators/build-log.ts +259 -0
- package/packages/core/src/kernel/generators/repository-journals.ts +206 -0
- package/packages/core/src/kernel/generators/session-log.ts +72 -0
- package/packages/core/src/kernel/harvest.ts +62 -18
- package/packages/core/src/kernel/incremental-contract.ts +75 -0
- package/packages/core/src/kernel/incremental-fixtures.ts +211 -0
- package/packages/core/src/kernel/incremental.ts +53 -0
- package/packages/core/src/kernel/log-index.ts +620 -0
- package/packages/core/src/kernel/pr-attachments.ts +164 -46
- package/packages/core/src/kernel/reducer-registry.ts +164 -0
- package/packages/core/src/kernel/reducer.ts +216 -74
- package/packages/core/src/operator/query.ts +68 -28
- package/packages/core/src/operator/read-view.ts +98 -0
- package/packages/core/src/ports/tickets/fake.ts +5 -1
- package/packages/core/src/ports/tickets/file.ts +169 -48
- package/packages/core/src/ports/tickets/linear.ts +33 -1
- package/packages/core/src/ports/tickets/operations.ts +1 -1
- package/packages/core/src/ports/tickets/remote.ts +8 -1
- package/packages/core/src/ports/types.ts +32 -1
- package/packages/core/src/processes/build-execution-state.ts +31 -15
- package/packages/core/src/processes/build-runner.ts +184 -90
- package/packages/core/src/processes/dispatcher-selectors.ts +123 -0
- package/packages/core/src/processes/dispatcher.ts +270 -176
- package/packages/core/src/processes/execution-settlement.ts +72 -39
- package/packages/core/src/processes/harvest-execution-state.ts +41 -29
- package/packages/core/src/processes/infrastructure-failure-budget.ts +31 -18
- package/packages/core/src/processes/orchestrator-tick.ts +44 -3
- package/packages/core/src/processes/publication-state.ts +164 -52
- package/packages/core/src/processes/repo-view.ts +1436 -0
- package/packages/core/src/processes/sandbox-state.ts +26 -2
- package/packages/core/src/processes/tick-state.ts +33 -0
- package/packages/core/src/store/adapter.ts +10 -1
- package/packages/core/src/store/build-scope.ts +26 -0
- package/packages/core/src/store/contract.ts +337 -0
- package/packages/core/src/store/digest.ts +71 -18
- package/packages/core/src/store/local/schema.ts +17 -0
- package/packages/core/src/store/local/store.ts +111 -4
- package/packages/core/src/store/memory.ts +68 -1
- package/packages/core/src/store/phase-session.ts +23 -0
- package/packages/core/src/store/remote/client.ts +50 -1
- package/packages/core/src/store/remote/index.ts +2 -0
- package/packages/core/src/store/remote/protocol.ts +15 -0
- package/packages/core/src/store/repo-state-events.ts +72 -7
- package/packages/core/src/store/session-handle.ts +30 -0
- package/packages/core/src/store/session-reducer.ts +52 -11
- package/packages/core/src/store/snapshots.ts +88 -0
- package/packages/core/src/store/ticket-assets.ts +41 -8
- package/packages/core/src/store/types.ts +38 -0
- package/skills/guide/SKILL.md +26 -7
- package/skills/guide/references/remote-store-protocol.md +35 -0
- package/skills/tickets/SKILL.md +57 -5
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
/** Seeded generated build logs for the incremental reducer property tests. */
|
|
2
|
+
import { DISPATCHER, KERNEL, humanActor, type Actor } from '../../events/envelope'
|
|
3
|
+
import {
|
|
4
|
+
validateEventWrite,
|
|
5
|
+
allowedActorKinds,
|
|
6
|
+
type AbEvent,
|
|
7
|
+
type EventWrite,
|
|
8
|
+
} from '../../events/catalog'
|
|
9
|
+
import type { EventType } from '../../events/payloads'
|
|
10
|
+
import { steppingClock } from '../../testing/fixed'
|
|
11
|
+
import { pick, seededRandom } from '../incremental-contract'
|
|
12
|
+
|
|
13
|
+
function actorFor(type: EventType): Actor {
|
|
14
|
+
switch (allowedActorKinds[type][0]) {
|
|
15
|
+
case 'kernel':
|
|
16
|
+
return KERNEL
|
|
17
|
+
case 'dispatcher':
|
|
18
|
+
return DISPATCHER
|
|
19
|
+
case 'human':
|
|
20
|
+
return humanActor('aron')
|
|
21
|
+
case 'agent':
|
|
22
|
+
return { kind: 'agent', role: 'test-role', session: 's_test' }
|
|
23
|
+
default:
|
|
24
|
+
return { kind: 'ingester', source: 'test' }
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const w = (type: EventType, payload: Record<string, unknown>): EventWrite =>
|
|
29
|
+
validateEventWrite({ actor: actorFor(type), type, payload } as EventWrite)
|
|
30
|
+
|
|
31
|
+
const SHA = 'a'.repeat(40)
|
|
32
|
+
const ART = (kind: string, rev: number) => ({ kind, rev })
|
|
33
|
+
|
|
34
|
+
/** Plausible payload templates drawn from the catalog; ids and rounds come
|
|
35
|
+
* from small pools so correlations (escalations, sessions, rounds) happen. */
|
|
36
|
+
function randomWrite(rand: () => number): EventWrite {
|
|
37
|
+
const round = 1 + Math.floor(rand() * 3)
|
|
38
|
+
const attempt = 1 + Math.floor(rand() * 3)
|
|
39
|
+
const id = pick(rand, ['e_1', 'e_2', 'e_3'])
|
|
40
|
+
const session = pick(rand, ['s_a', 's_b'])
|
|
41
|
+
const step = pick(rand, ['types', 'unit'])
|
|
42
|
+
const templates: Array<() => EventWrite> = [
|
|
43
|
+
() => w('runner.attached', { instance: 'runner-1', host: 'local' }),
|
|
44
|
+
() => w('spec.imported', { artifact: ART('spec', 0), ticket: { source: 'linear', id: 'E-1' } }),
|
|
45
|
+
() => w('spec.revised', { artifact: ART('spec', round), escalation: 1 }),
|
|
46
|
+
() => w('plan.started', { round }),
|
|
47
|
+
() => w('plan.completed', { round, artifact: ART('plan', round) }),
|
|
48
|
+
() => w('plan-review.started', { round }),
|
|
49
|
+
() =>
|
|
50
|
+
w('plan-review.verdict', {
|
|
51
|
+
round,
|
|
52
|
+
verdict: pick(rand, ['approve', 'revise'] as const),
|
|
53
|
+
findings: [],
|
|
54
|
+
artifact: ART('plan-review', round),
|
|
55
|
+
}),
|
|
56
|
+
() => w('implement.started', { round }),
|
|
57
|
+
() =>
|
|
58
|
+
w('implement.completed', {
|
|
59
|
+
round,
|
|
60
|
+
commits: { base: 'b', head: `h${round}` },
|
|
61
|
+
artifact: ART('implement-notes', round),
|
|
62
|
+
}),
|
|
63
|
+
() => w('code-review.started', { round }),
|
|
64
|
+
() =>
|
|
65
|
+
w('code-review.verdict', {
|
|
66
|
+
round,
|
|
67
|
+
verdict: pick(rand, ['approve', 'revise'] as const),
|
|
68
|
+
findings: [],
|
|
69
|
+
artifact: ART('code-review', round),
|
|
70
|
+
}),
|
|
71
|
+
() => w('verify.started', { step, attempt }),
|
|
72
|
+
() => w('verify.completed', { step, attempt, pass: rand() < 0.5 }),
|
|
73
|
+
() => w('finalize.started', {}),
|
|
74
|
+
() =>
|
|
75
|
+
w('finalize.completed', {
|
|
76
|
+
pr: { number: 7, url: 'https://github.com/o/r/pull/7', headSha: 'h' },
|
|
77
|
+
}),
|
|
78
|
+
() => w('finalize.step-completed', { step: 'notes', ok: rand() < 0.5 }),
|
|
79
|
+
() => w('pr.conflicted', { baseSha: 'm' }),
|
|
80
|
+
() => w('reconcile.started', { attempt, baseSha: 'm' }),
|
|
81
|
+
() =>
|
|
82
|
+
w('reconcile.completed', { mergeCommit: 'mc', artifact: ART('reconcile-notes', attempt) }),
|
|
83
|
+
() => w('pr.merged', { sha: 's' }),
|
|
84
|
+
() => w('build.completed', { outcome: 'merged' }),
|
|
85
|
+
() => w('build.pause-requested', {}),
|
|
86
|
+
() => w('build.resume-requested', {}),
|
|
87
|
+
() => w('build.paused', {}),
|
|
88
|
+
() => w('build.resumed', {}),
|
|
89
|
+
() => w('build.abort-requested', {}),
|
|
90
|
+
() => w('build.aborted', {}),
|
|
91
|
+
() => w('build.auto-merge-requested', {}),
|
|
92
|
+
() => w('build.auto-merge-cancelled', {}),
|
|
93
|
+
() => w('pr.auto-merge-enabled', { commandSeq: 1 }),
|
|
94
|
+
() =>
|
|
95
|
+
w('escalation.raised', {
|
|
96
|
+
id,
|
|
97
|
+
phase: pick(rand, ['plan', 'implement', 'plan-review'] as const),
|
|
98
|
+
source: 'agent',
|
|
99
|
+
question: 'q',
|
|
100
|
+
}),
|
|
101
|
+
() =>
|
|
102
|
+
w('escalation.answered', {
|
|
103
|
+
id,
|
|
104
|
+
answer: 'a',
|
|
105
|
+
resolution: pick(rand, ['guidance', 'abort', 'retry'] as const),
|
|
106
|
+
}),
|
|
107
|
+
() =>
|
|
108
|
+
w('session.started', {
|
|
109
|
+
session,
|
|
110
|
+
role: 'plan',
|
|
111
|
+
runner: 'claude',
|
|
112
|
+
phase: 'plan',
|
|
113
|
+
round,
|
|
114
|
+
}),
|
|
115
|
+
() =>
|
|
116
|
+
w('session.ended', {
|
|
117
|
+
session,
|
|
118
|
+
transcript: ART('transcript', 0),
|
|
119
|
+
usage: { inputTokens: 1, outputTokens: 1, turns: 1 },
|
|
120
|
+
}),
|
|
121
|
+
() =>
|
|
122
|
+
w('observation.recorded', { id: `o_${round}`, kind: 'refactor', summary: 's', files: [] }),
|
|
123
|
+
() => w('phase.failed', { phase: 'plan', round, attempt, error: 'x', willRetry: true }),
|
|
124
|
+
() => w('dispatch.failed', { stage: 'workspace', attempt, error: 'offline' }),
|
|
125
|
+
() => w('execution.started', { provider: 'p', workspaceRef: 'w', instance: 'i' }),
|
|
126
|
+
() => w('execution.ended', { instance: 'i', workspaceRef: 'w', outcome: 'completed' }),
|
|
127
|
+
() =>
|
|
128
|
+
w('infrastructure.failed', {
|
|
129
|
+
provider: 'p',
|
|
130
|
+
workspaceRef: 'w',
|
|
131
|
+
instance: 'i',
|
|
132
|
+
operation: 'wait',
|
|
133
|
+
cause: 'timeout',
|
|
134
|
+
attempt,
|
|
135
|
+
retryable: true,
|
|
136
|
+
cleanupPending: false,
|
|
137
|
+
error: 'x',
|
|
138
|
+
}),
|
|
139
|
+
() =>
|
|
140
|
+
w('infrastructure.cleanup-attempted', {
|
|
141
|
+
provider: 'p',
|
|
142
|
+
workspaceRef: 'w',
|
|
143
|
+
operation: 'stop',
|
|
144
|
+
attempt,
|
|
145
|
+
outcome: 'confirmed',
|
|
146
|
+
}),
|
|
147
|
+
() => w('runner.setup-failed', { command: 'bun install', attempt, exitStatus: 1, output: 'x' }),
|
|
148
|
+
() => w('build.discard-requested', {}),
|
|
149
|
+
() =>
|
|
150
|
+
w('escalation.raised', {
|
|
151
|
+
id,
|
|
152
|
+
phase: 'plan',
|
|
153
|
+
source: 'policy',
|
|
154
|
+
policyCause: 'infrastructure-failure-limit',
|
|
155
|
+
question: 'q',
|
|
156
|
+
}),
|
|
157
|
+
() =>
|
|
158
|
+
w('workspace.provision-started', { provider: 'worktree', branch: 'ab/x', generation: round }),
|
|
159
|
+
() =>
|
|
160
|
+
w('workspace.provisioned', {
|
|
161
|
+
provider: 'worktree',
|
|
162
|
+
ref: `/ws/${round}`,
|
|
163
|
+
branch: 'ab/x',
|
|
164
|
+
base: { source: 'remote', sha: `base${round}` },
|
|
165
|
+
...(rand() < 0.5 ? { remote: true } : {}),
|
|
166
|
+
}),
|
|
167
|
+
() => w('workspace.released', rand() < 0.5 ? {} : { ref: '/ws/1', reason: 'completion' }),
|
|
168
|
+
() =>
|
|
169
|
+
w('publication.requested', {
|
|
170
|
+
operation: 'implement',
|
|
171
|
+
branch: 'ab/x',
|
|
172
|
+
sha: SHA,
|
|
173
|
+
round,
|
|
174
|
+
base: SHA,
|
|
175
|
+
artifact: ART('implement-notes', round),
|
|
176
|
+
}),
|
|
177
|
+
() =>
|
|
178
|
+
w('publication.requested', {
|
|
179
|
+
operation: 'reconcile',
|
|
180
|
+
branch: 'ab/x',
|
|
181
|
+
sha: SHA,
|
|
182
|
+
artifact: ART('reconcile-notes', round),
|
|
183
|
+
}),
|
|
184
|
+
() =>
|
|
185
|
+
w('publication.lost', {
|
|
186
|
+
request: round,
|
|
187
|
+
operation: 'implement',
|
|
188
|
+
branch: 'ab/x',
|
|
189
|
+
sha: SHA,
|
|
190
|
+
reason: 'released',
|
|
191
|
+
}),
|
|
192
|
+
() =>
|
|
193
|
+
w('pr-attachment.designated', {
|
|
194
|
+
artifact: ART('screenshot', round),
|
|
195
|
+
filename: `shot${round}.png`,
|
|
196
|
+
mediaType: 'image/png',
|
|
197
|
+
}),
|
|
198
|
+
() =>
|
|
199
|
+
w('pr-attachment.hosted', {
|
|
200
|
+
designationSeq: 1 + Math.floor(rand() * 20),
|
|
201
|
+
asset: {
|
|
202
|
+
provider: 'github-release',
|
|
203
|
+
repository: 'o/r',
|
|
204
|
+
releaseId: round,
|
|
205
|
+
assetId: attempt,
|
|
206
|
+
url: 'https://example.com/a.png',
|
|
207
|
+
},
|
|
208
|
+
}),
|
|
209
|
+
() => w('pr-attachment.reclaimed', { hostedSeq: 1 + Math.floor(rand() * 20) }),
|
|
210
|
+
() => w('build.auto-merge-default-observed', { defaultSeq: round }),
|
|
211
|
+
() =>
|
|
212
|
+
w('observation.recorded', {
|
|
213
|
+
id: `o_d${round}`,
|
|
214
|
+
kind: 'followup',
|
|
215
|
+
summary: 'deferred',
|
|
216
|
+
files: [],
|
|
217
|
+
refs: [`auto-merge-gate:pr:7:${round}`],
|
|
218
|
+
}),
|
|
219
|
+
() =>
|
|
220
|
+
w('spec.revised', {
|
|
221
|
+
artifact: ART('spec', round),
|
|
222
|
+
escalation: 1,
|
|
223
|
+
assets: [
|
|
224
|
+
{
|
|
225
|
+
kind: 'design',
|
|
226
|
+
name: 'a',
|
|
227
|
+
revision: round,
|
|
228
|
+
layout: 'file',
|
|
229
|
+
size: 1,
|
|
230
|
+
fileCount: 1,
|
|
231
|
+
dirCount: 0,
|
|
232
|
+
},
|
|
233
|
+
],
|
|
234
|
+
}),
|
|
235
|
+
]
|
|
236
|
+
return pick(rand, templates)()
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export function randomBuildLog(seed: number, length: number): AbEvent[] {
|
|
240
|
+
const rand = seededRandom(seed)
|
|
241
|
+
const clock = steppingClock()
|
|
242
|
+
const created = w('build.created', {
|
|
243
|
+
ticket: { source: 'linear', id: 'E-1', title: 't' },
|
|
244
|
+
repo: 'o/r',
|
|
245
|
+
baseBranch: 'main',
|
|
246
|
+
})
|
|
247
|
+
const writes = [created, ...Array.from({ length }, () => randomWrite(rand))]
|
|
248
|
+
return writes.map(
|
|
249
|
+
(write, index) =>
|
|
250
|
+
({
|
|
251
|
+
build: 'gen',
|
|
252
|
+
seq: index + 1,
|
|
253
|
+
ts: clock().toISOString(),
|
|
254
|
+
actor: write.actor,
|
|
255
|
+
type: write.type,
|
|
256
|
+
payload: write.payload,
|
|
257
|
+
}) as AbEvent,
|
|
258
|
+
)
|
|
259
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/** Seeded generated repository journals for the incremental reducer property tests. */
|
|
2
|
+
import { DISPATCHER, KERNEL, humanActor } from '../../events/envelope'
|
|
3
|
+
import type { RepositoryEvent } from '../../events/repository'
|
|
4
|
+
import { pick, seededRandom } from '../incremental-contract'
|
|
5
|
+
|
|
6
|
+
type Spec = { type: string; payload: Record<string, unknown>; actor?: RepositoryEvent['actor'] }
|
|
7
|
+
|
|
8
|
+
function toJournal(specs: Spec[], shuffleTs = false): RepositoryEvent[] {
|
|
9
|
+
return specs.map(
|
|
10
|
+
(spec, index) =>
|
|
11
|
+
({
|
|
12
|
+
repo: 'acme/repo',
|
|
13
|
+
seq: index + 1,
|
|
14
|
+
ts: new Date(
|
|
15
|
+
Date.UTC(2026, 0, 1, 0, 0, shuffleTs ? (index * 7) % 13 : index),
|
|
16
|
+
).toISOString(),
|
|
17
|
+
actor: spec.actor ?? KERNEL,
|
|
18
|
+
type: spec.type,
|
|
19
|
+
payload: spec.payload,
|
|
20
|
+
}) as RepositoryEvent,
|
|
21
|
+
)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Valid harvest lifecycles only (the reducer throws on dangling references):
|
|
25
|
+
* runs start one at a time with fresh observations; pause/resume facts and
|
|
26
|
+
* failures interleave while a run is open. */
|
|
27
|
+
export function randomHarvestJournal(seed: number, length: number): RepositoryEvent[] {
|
|
28
|
+
const rand = seededRandom(seed)
|
|
29
|
+
const specs: Spec[] = []
|
|
30
|
+
let runNo = 0
|
|
31
|
+
let open: { run: string; obs: Array<{ build: string; seq: number }> } | undefined
|
|
32
|
+
let parked = false
|
|
33
|
+
for (let i = 0; i < length; i++) {
|
|
34
|
+
const r = rand()
|
|
35
|
+
if (open === undefined) {
|
|
36
|
+
runNo += 1
|
|
37
|
+
const obs = [{ build: `b${runNo}`, seq: 1 }]
|
|
38
|
+
open = { run: `h_${runNo}`, obs }
|
|
39
|
+
parked = false
|
|
40
|
+
specs.push({
|
|
41
|
+
type: 'harvest.started',
|
|
42
|
+
payload: { run: open.run, observations: obs, scan: { kind: 'harvest-scan', rev: runNo } },
|
|
43
|
+
})
|
|
44
|
+
continue
|
|
45
|
+
}
|
|
46
|
+
if (r < 0.15) {
|
|
47
|
+
specs.push({ type: 'harvest.pause-requested', payload: {}, actor: humanActor('aron') })
|
|
48
|
+
} else if (r < 0.25) {
|
|
49
|
+
specs.push({ type: 'harvest.resume-requested', payload: {}, actor: humanActor('aron') })
|
|
50
|
+
} else if (r < 0.35) {
|
|
51
|
+
specs.push({ type: 'harvest.paused', payload: {} })
|
|
52
|
+
} else if (r < 0.5) {
|
|
53
|
+
specs.push({ type: 'harvest.resumed', payload: {} })
|
|
54
|
+
parked = false
|
|
55
|
+
} else if (r < 0.7 && !parked) {
|
|
56
|
+
const willRetry = rand() < 0.5
|
|
57
|
+
specs.push({
|
|
58
|
+
type: 'harvest.failed',
|
|
59
|
+
payload: { run: open.run, step: 'file', attempt: 1, error: 'boom', willRetry },
|
|
60
|
+
})
|
|
61
|
+
if (!willRetry) parked = true
|
|
62
|
+
} else if (r < 0.8 && !parked) {
|
|
63
|
+
specs.push({
|
|
64
|
+
type: 'harvest.completed',
|
|
65
|
+
payload: {
|
|
66
|
+
run: open.run,
|
|
67
|
+
dispositions: [
|
|
68
|
+
{ occurrence: open.obs[0], action: 'suppressed', proposalKey: `k${runNo}` },
|
|
69
|
+
],
|
|
70
|
+
report: { kind: 'harvest-report', rev: runNo },
|
|
71
|
+
},
|
|
72
|
+
})
|
|
73
|
+
open = undefined
|
|
74
|
+
} else if (r < 0.9 && !parked) {
|
|
75
|
+
specs.push({
|
|
76
|
+
type: 'harvest.escalated',
|
|
77
|
+
payload: { run: open.run, source: 'agent', reason: 'judgment', observations: open.obs },
|
|
78
|
+
})
|
|
79
|
+
open = undefined
|
|
80
|
+
} else if (r < 0.95) {
|
|
81
|
+
const execution = `x${i % 3}`
|
|
82
|
+
specs.push(
|
|
83
|
+
rand() < 0.6
|
|
84
|
+
? {
|
|
85
|
+
type: 'harvest.execution.started',
|
|
86
|
+
payload: { execution, provider: 'vercel', environmentId: 'env', sessionId: 's' },
|
|
87
|
+
actor: DISPATCHER,
|
|
88
|
+
}
|
|
89
|
+
: {
|
|
90
|
+
type: 'harvest.execution.released',
|
|
91
|
+
payload: { execution, environmentId: 'env', snapshots: { outcome: 'confirmed' } },
|
|
92
|
+
actor: DISPATCHER,
|
|
93
|
+
},
|
|
94
|
+
)
|
|
95
|
+
} else {
|
|
96
|
+
specs.push({
|
|
97
|
+
type: 'dispatcher.intake-set',
|
|
98
|
+
payload: { enabled: rand() < 0.5 },
|
|
99
|
+
actor: DISPATCHER,
|
|
100
|
+
})
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return toJournal(specs)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function randomSettingsJournal(seed: number, length: number): RepositoryEvent[] {
|
|
107
|
+
const rand = seededRandom(seed)
|
|
108
|
+
const specs: Spec[] = Array.from({ length }, () => ({
|
|
109
|
+
type: pick(rand, [
|
|
110
|
+
'dispatcher.intake-set',
|
|
111
|
+
'dispatcher.pause-set',
|
|
112
|
+
'dispatcher.auto-merge-default-set',
|
|
113
|
+
'harvest.paused',
|
|
114
|
+
]),
|
|
115
|
+
payload: { enabled: rand() < 0.5 },
|
|
116
|
+
actor: humanActor('aron'),
|
|
117
|
+
}))
|
|
118
|
+
return toJournal(specs)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const counters = {
|
|
122
|
+
claimed: 0,
|
|
123
|
+
launched: 0,
|
|
124
|
+
merged: 0,
|
|
125
|
+
closed: 0,
|
|
126
|
+
conflicted: 0,
|
|
127
|
+
reconciled: 0,
|
|
128
|
+
harvestStarted: 0,
|
|
129
|
+
harvestCompleted: 0,
|
|
130
|
+
harvestEscalated: 0,
|
|
131
|
+
harvestFailed: 0,
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function randomStatusJournal(seed: number, length: number): RepositoryEvent[] {
|
|
135
|
+
const rand = seededRandom(seed)
|
|
136
|
+
const specs: Spec[] = []
|
|
137
|
+
for (let i = 0; i < length; i++) {
|
|
138
|
+
const run = pick(rand, ['run-a', 'run-b'])
|
|
139
|
+
const templates: Array<() => Spec> = [
|
|
140
|
+
() => ({
|
|
141
|
+
type: 'dispatcher.run-started',
|
|
142
|
+
payload: {
|
|
143
|
+
run,
|
|
144
|
+
pid: 100,
|
|
145
|
+
effectiveConfig: { kind: 'dispatcher-effective-config', rev: i },
|
|
146
|
+
roleWarnings: ['w'],
|
|
147
|
+
},
|
|
148
|
+
}),
|
|
149
|
+
() => ({
|
|
150
|
+
type: 'dispatcher.tick-completed',
|
|
151
|
+
payload: {
|
|
152
|
+
run,
|
|
153
|
+
queued: i,
|
|
154
|
+
counters: { ...counters, creationWithheld: 1 },
|
|
155
|
+
janitorDiagnostics: ['j'],
|
|
156
|
+
ticketDiagnostics: [],
|
|
157
|
+
dependencyDiagnostics: [],
|
|
158
|
+
},
|
|
159
|
+
}),
|
|
160
|
+
() => ({ type: 'dispatcher.tick-failed', payload: { run, error: 'e' } }),
|
|
161
|
+
() => ({ type: 'dispatcher.tick-started', payload: { run } }),
|
|
162
|
+
() => ({ type: 'dispatcher.config-rejected', payload: { run, error: 'bad' } }),
|
|
163
|
+
() => ({
|
|
164
|
+
type: 'dispatcher.operator-reported',
|
|
165
|
+
payload: { run, level: pick(rand, ['info', 'warning']), message: 'm' },
|
|
166
|
+
}),
|
|
167
|
+
() => ({ type: 'dispatcher.upgrade-available', payload: { run, version: '1.0.0' } }),
|
|
168
|
+
() => ({
|
|
169
|
+
type: 'dispatcher.run-stopped',
|
|
170
|
+
payload: { run, outcome: pick(rand, ['normal', 'abnormal']) },
|
|
171
|
+
}),
|
|
172
|
+
]
|
|
173
|
+
specs.push({ ...pick(rand, templates)(), actor: DISPATCHER })
|
|
174
|
+
}
|
|
175
|
+
return toJournal(specs)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function randomSandboxJournal(seed: number, length: number): RepositoryEvent[] {
|
|
179
|
+
const rand = seededRandom(seed)
|
|
180
|
+
const specs: Spec[] = []
|
|
181
|
+
for (let i = 0; i < length; i++) {
|
|
182
|
+
const base = { operator: 'op', environmentId: pick(rand, ['env_1', 'env_2']) }
|
|
183
|
+
const templates: Array<() => Spec> = [
|
|
184
|
+
() => ({
|
|
185
|
+
type: 'orchestrator.sandbox.provisioned',
|
|
186
|
+
payload: {
|
|
187
|
+
...base,
|
|
188
|
+
provider: 'vercel',
|
|
189
|
+
workspacePath: '/w',
|
|
190
|
+
...(rand() < 0.5 ? { sessionId: 's' } : {}),
|
|
191
|
+
},
|
|
192
|
+
}),
|
|
193
|
+
() => ({ type: 'orchestrator.sandbox.resumed', payload: { ...base, provider: 'vercel' } }),
|
|
194
|
+
() => ({ type: 'orchestrator.sandbox.activity', payload: base }),
|
|
195
|
+
() => ({ type: 'orchestrator.sandbox.stopped', payload: { ...base, reason: 'idle' } }),
|
|
196
|
+
() => ({ type: 'orchestrator.sandbox.reset', payload: base }),
|
|
197
|
+
() => ({
|
|
198
|
+
type: 'orchestrator.sandbox.released',
|
|
199
|
+
payload: { ...base, snapshots: { outcome: 'confirmed' } },
|
|
200
|
+
}),
|
|
201
|
+
]
|
|
202
|
+
specs.push({ ...pick(rand, templates)(), actor: DISPATCHER })
|
|
203
|
+
}
|
|
204
|
+
// Non-monotonic timestamps exercise the `lastEvidenceTs <= ts` guards.
|
|
205
|
+
return toJournal(specs, true)
|
|
206
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/** Seeded generated session logs, with `session.archived` optionally mid-log. */
|
|
2
|
+
import { agentActor, humanActor } from '../../events/envelope'
|
|
3
|
+
import type { SessionEvent } from '../../events/sessions'
|
|
4
|
+
import { pick, seededRandom } from '../incremental-contract'
|
|
5
|
+
|
|
6
|
+
type Spec = { type: SessionEvent['type']; payload: Record<string, unknown>; human?: boolean }
|
|
7
|
+
|
|
8
|
+
export function randomSessionLog(seed: number, length: number, archive: boolean): SessionEvent[] {
|
|
9
|
+
const rand = seededRandom(seed)
|
|
10
|
+
const specs: Spec[] = [{ type: 'session.created', payload: {}, human: true }]
|
|
11
|
+
for (let i = 0; i < length; i++) {
|
|
12
|
+
const turn = pick(rand, ['t1', 't2', 't3'])
|
|
13
|
+
const toolCallId = pick(rand, ['c1', 'c2'])
|
|
14
|
+
const templates: Spec[] = [
|
|
15
|
+
{ type: 'message.posted', payload: { text: 'hi' }, human: true },
|
|
16
|
+
{
|
|
17
|
+
type: 'session.wake-set',
|
|
18
|
+
payload: { globs: [pick(rand, ['a*', 'b*', 'c*'])] },
|
|
19
|
+
human: true,
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
type: 'turn.started',
|
|
23
|
+
payload: {
|
|
24
|
+
turn,
|
|
25
|
+
stream: `st_${turn}`,
|
|
26
|
+
trigger: pick(rand, [
|
|
27
|
+
{ kind: 'message', messageSeq: 1 },
|
|
28
|
+
{
|
|
29
|
+
kind: 'wake',
|
|
30
|
+
build: pick(rand, ['b1', 'b2']),
|
|
31
|
+
seq: 1 + Math.floor(rand() * 9),
|
|
32
|
+
type: 'x',
|
|
33
|
+
},
|
|
34
|
+
{ kind: 'wake', journal: true, seq: 1 + Math.floor(rand() * 9), type: 'x' },
|
|
35
|
+
]),
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
{ type: 'turn.suspended', payload: { turn, cause: pick(rand, ['budget', 'approval']) } },
|
|
39
|
+
{ type: 'turn.resumed', payload: { turn } },
|
|
40
|
+
{
|
|
41
|
+
type: 'approval.requested',
|
|
42
|
+
payload: { turn, toolCallId, toolName: 'tool', input: { a: 1 } },
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
type: 'approval.answered',
|
|
46
|
+
payload: { turn, toolCallId, decision: 'approve' },
|
|
47
|
+
human: true,
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
type: 'turn.completed',
|
|
51
|
+
payload: { turn, usage: { inputTokens: 1, outputTokens: 2, steps: 3 } },
|
|
52
|
+
},
|
|
53
|
+
{ type: 'turn.failed', payload: { turn, kind: 'internal', error: 'boom' } },
|
|
54
|
+
]
|
|
55
|
+
specs.push(pick(rand, templates))
|
|
56
|
+
}
|
|
57
|
+
if (archive) {
|
|
58
|
+
const at = 1 + Math.floor(rand() * specs.length)
|
|
59
|
+
specs.splice(at, 0, { type: 'session.archived', payload: {}, human: true })
|
|
60
|
+
}
|
|
61
|
+
return specs.map(
|
|
62
|
+
(spec, index) =>
|
|
63
|
+
({
|
|
64
|
+
session: 's_1',
|
|
65
|
+
seq: index + 1,
|
|
66
|
+
ts: new Date(Date.UTC(2026, 0, 1, 0, 0, index)).toISOString(),
|
|
67
|
+
actor: spec.human ? humanActor('aron') : agentActor('orchestrator', 's_a'),
|
|
68
|
+
type: spec.type,
|
|
69
|
+
payload: spec.payload,
|
|
70
|
+
}) as unknown as SessionEvent,
|
|
71
|
+
)
|
|
72
|
+
}
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
type HarvestTrigger,
|
|
16
16
|
type OccurrenceKey,
|
|
17
17
|
} from '../harvest/schema'
|
|
18
|
+
import { defineReducer, type IncrementalReducer } from './incremental'
|
|
18
19
|
|
|
19
20
|
export interface HarvestStepOccurrence {
|
|
20
21
|
step: HarvestStep
|
|
@@ -184,21 +185,35 @@ function requireRun(
|
|
|
184
185
|
return state
|
|
185
186
|
}
|
|
186
187
|
|
|
187
|
-
/**
|
|
188
|
-
*
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
188
|
+
/** The carried accumulator for `reduceHarvest`: `runs` in start order, and
|
|
189
|
+
* `claimed` as `Map`-insertion-ordered entries keyed by occurrence id. The
|
|
190
|
+
* public `latest` aliases the last run and is rebuilt by `finish`. */
|
|
191
|
+
export interface HarvestAcc {
|
|
192
|
+
runs: HarvestRunState[]
|
|
193
|
+
claimed: Array<{ key: string; occurrence: OccurrenceKey; run: string }>
|
|
194
|
+
ledger: HarvestLedgerEntry[]
|
|
195
|
+
pending: Record<HarvestPendingCommand['command'], HarvestPendingCommand[]>
|
|
196
|
+
paused: boolean
|
|
197
|
+
pausedSeq?: number
|
|
198
|
+
pausedAt?: string
|
|
199
|
+
lastSeq: number
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** Bump when `HarvestAcc`'s shape or fold semantics change. */
|
|
203
|
+
export const HARVEST_REDUCER_VERSION = 1
|
|
204
|
+
|
|
205
|
+
function foldHarvest(acc: HarvestAcc, events: readonly RepositoryEvent[]): void {
|
|
206
|
+
const order = acc.runs
|
|
207
|
+
const runs = new Map(order.map((run) => [run.run, run]))
|
|
208
|
+
const claimed = new Map(
|
|
209
|
+
acc.claimed.map((entry) => [entry.key, { occurrence: entry.occurrence, run: entry.run }]),
|
|
210
|
+
)
|
|
211
|
+
const ledger = acc.ledger
|
|
212
|
+
const pending = acc.pending
|
|
213
|
+
let paused = acc.paused
|
|
214
|
+
let pausedSeq = acc.pausedSeq
|
|
215
|
+
let pausedAt = acc.pausedAt
|
|
216
|
+
let lastSeq = acc.lastSeq
|
|
202
217
|
|
|
203
218
|
for (const event of events) {
|
|
204
219
|
lastSeq = Math.max(lastSeq, event.seq)
|
|
@@ -638,9 +653,17 @@ export function reduceHarvest(events: readonly RepositoryEvent[]): HarvestState
|
|
|
638
653
|
break
|
|
639
654
|
}
|
|
640
655
|
}
|
|
656
|
+
acc.claimed = [...claimed].map(([key, { occurrence, run }]) => ({ key, occurrence, run }))
|
|
657
|
+
acc.paused = paused
|
|
658
|
+
acc.pausedSeq = pausedSeq
|
|
659
|
+
acc.pausedAt = pausedAt
|
|
660
|
+
acc.lastSeq = lastSeq
|
|
661
|
+
}
|
|
641
662
|
|
|
663
|
+
function finishHarvest(acc: HarvestAcc): HarvestState {
|
|
664
|
+
const { runs: order, paused, pausedSeq, pausedAt, pending } = acc
|
|
642
665
|
const state: HarvestState = {
|
|
643
|
-
lastSeq,
|
|
666
|
+
lastSeq: acc.lastSeq,
|
|
644
667
|
runs: order,
|
|
645
668
|
paused,
|
|
646
669
|
...(pausedSeq !== undefined ? { pausedSeq } : {}),
|
|
@@ -648,14 +671,35 @@ export function reduceHarvest(events: readonly RepositoryEvent[]): HarvestState
|
|
|
648
671
|
pendingCommands: [...pending.pause, ...pending.resume].sort(
|
|
649
672
|
(left, right) => left.seq - right.seq,
|
|
650
673
|
),
|
|
651
|
-
claimed:
|
|
652
|
-
ledger,
|
|
674
|
+
claimed: acc.claimed.map(({ occurrence }) => occurrence),
|
|
675
|
+
ledger: acc.ledger,
|
|
653
676
|
}
|
|
654
677
|
const latest = order.at(-1)
|
|
655
678
|
if (latest !== undefined) state.latest = latest
|
|
656
679
|
return state
|
|
657
680
|
}
|
|
658
681
|
|
|
682
|
+
export const harvestReducer: IncrementalReducer<HarvestAcc, RepositoryEvent, HarvestState> =
|
|
683
|
+
defineReducer({
|
|
684
|
+
version: HARVEST_REDUCER_VERSION,
|
|
685
|
+
initial: (): HarvestAcc => ({
|
|
686
|
+
runs: [],
|
|
687
|
+
claimed: [],
|
|
688
|
+
ledger: [],
|
|
689
|
+
pending: { pause: [], resume: [] },
|
|
690
|
+
paused: false,
|
|
691
|
+
lastSeq: 0,
|
|
692
|
+
}),
|
|
693
|
+
fold: foldHarvest,
|
|
694
|
+
finish: finishHarvest,
|
|
695
|
+
})
|
|
696
|
+
|
|
697
|
+
/** Total for valid journals; malformed cross-event references throw loudly so
|
|
698
|
+
* storage corruption cannot masquerade as an idle harvester. */
|
|
699
|
+
export function reduceHarvest(events: readonly RepositoryEvent[]): HarvestState {
|
|
700
|
+
return harvestReducer.reduce(events)
|
|
701
|
+
}
|
|
702
|
+
|
|
659
703
|
export function claimedOccurrenceKeys(state: HarvestState): Set<string> {
|
|
660
704
|
return new Set(state.claimed.map(occurrenceKey))
|
|
661
705
|
}
|