@defrex/autobuild 0.8.0 → 0.9.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 +313 -26
- package/bin/ab-build-runner.ts +8 -2
- package/docs/architecture.md +57 -14
- package/docs/assets/headline-wide.png +0 -0
- package/docs/configuration.md +151 -25
- package/docs/hosted-dispatcher.md +69 -13
- package/docs/operator-api.md +2 -2
- package/docs/remote-store-protocol.md +169 -16
- package/docs/remote-ticket-protocol.md +1 -1
- package/docs/setup.md +12 -9
- package/docs/website.md +10 -0
- package/package.json +43 -23
- package/packages/core/src/cli/artifact.ts +130 -6
- package/packages/core/src/cli/binary.ts +9 -1
- package/packages/core/src/cli/build-control.ts +110 -13
- package/packages/core/src/cli/context.ts +46 -0
- package/packages/core/src/cli/dashboard/model.ts +57 -7
- package/packages/core/src/cli/dashboard/poll.ts +69 -10
- package/packages/core/src/cli/dashboard/render.ts +34 -5
- package/packages/core/src/cli/dispatch-frontend.ts +54 -5
- package/packages/core/src/cli/dispatch.ts +320 -46
- package/packages/core/src/cli/harvest.ts +122 -8
- package/packages/core/src/cli/help.ts +47 -8
- package/packages/core/src/cli/init-readiness-remote.ts +110 -0
- package/packages/core/src/cli/init-readiness-shared.ts +72 -0
- package/packages/core/src/cli/init-validation.ts +248 -222
- package/packages/core/src/cli/main.ts +70 -11
- package/packages/core/src/cli/mcp.ts +23 -1
- package/packages/core/src/cli/plugin.ts +7 -4
- package/packages/core/src/cli/remote-poll.ts +277 -0
- package/packages/core/src/cli/repo-state.ts +22 -0
- package/packages/core/src/cli/repository-status.ts +39 -1
- package/packages/core/src/cli/status.ts +149 -27
- package/packages/core/src/cli/terminals.ts +2 -1
- package/packages/core/src/cli/ticket-assets.ts +247 -0
- package/packages/core/src/cli/ticket.ts +307 -7
- package/packages/core/src/cli/wait.ts +147 -39
- package/packages/core/src/cli/watch.ts +128 -166
- package/packages/core/src/config/live.ts +22 -0
- package/packages/core/src/config/pipeline-source.ts +376 -0
- package/packages/core/src/config/schema.ts +230 -19
- package/packages/core/src/events/catalog.ts +1 -0
- package/packages/core/src/events/globs.ts +47 -0
- package/packages/core/src/events/payloads.ts +52 -2
- package/packages/core/src/events/repository.ts +52 -1
- package/packages/core/src/events/sessions.ts +69 -10
- package/packages/core/src/events/workspace-remote.ts +12 -0
- package/packages/core/src/integration/harness.ts +2 -1
- package/packages/core/src/kernel/auto-merge-default.ts +123 -0
- package/packages/core/src/kernel/phases.ts +16 -2
- package/packages/core/src/kernel/reducer.ts +40 -1
- package/packages/core/src/operator/annotations.ts +6 -0
- package/packages/core/src/operator/control.ts +19 -5
- package/packages/core/src/operator/index.ts +71 -6
- package/packages/core/src/operator/query.ts +208 -19
- package/packages/core/src/operator/registry.ts +55 -10
- package/packages/core/src/operator/requests.ts +9 -6
- package/packages/core/src/operator/sandbox-host.ts +140 -0
- package/packages/core/src/operator/sandbox.ts +247 -14
- package/packages/core/src/operator/ticket-source-backend.ts +47 -0
- package/packages/core/src/orchestrator/approvals.ts +115 -0
- package/packages/core/src/orchestrator/conversation.ts +167 -0
- package/packages/core/src/orchestrator/model.ts +86 -0
- package/packages/core/src/orchestrator/prompt.ts +61 -0
- package/packages/core/src/orchestrator/tools.ts +117 -0
- package/packages/core/src/orchestrator/turn-runner.ts +457 -0
- package/packages/core/src/plugin-sdk/index.ts +51 -1
- package/packages/core/src/plugins/load.ts +135 -7
- package/packages/core/src/plugins/manifest.ts +183 -10
- package/packages/core/src/plugins/registry.ts +73 -4
- package/packages/core/src/ports/forge/create.ts +1 -1
- package/packages/core/src/ports/forge/fake.ts +31 -0
- package/packages/core/src/ports/forge/github-transport.ts +13 -2
- package/packages/core/src/ports/forge/github.ts +13 -2
- package/packages/core/src/ports/forge/local-git.ts +31 -1
- package/packages/core/src/ports/runner/claude.ts +2 -2
- package/packages/core/src/ports/runner/codex.ts +6 -2
- package/packages/core/src/ports/runner/fake.ts +7 -1
- package/packages/core/src/ports/runner/pi.ts +8 -6
- package/packages/core/src/ports/runner/provider-error.ts +13 -3
- package/packages/core/src/ports/tickets/{remote-index.ts → hosted-index.ts} +3 -2
- package/packages/core/src/ports/types.ts +17 -2
- package/packages/core/src/ports/workspace/builtin-capabilities.ts +114 -0
- package/packages/core/src/ports/workspace/create.ts +74 -39
- package/packages/core/src/ports/workspace/distribution-archive.ts +171 -12
- package/packages/core/src/ports/workspace/fake.ts +169 -4
- package/packages/core/src/ports/workspace/git-worktree.ts +246 -26
- package/packages/core/src/ports/workspace/github-origin.ts +36 -0
- package/packages/core/src/ports/workspace/operator-sandbox.ts +7 -1
- package/packages/core/src/ports/workspace/provider-capabilities.ts +171 -0
- package/packages/core/src/ports/workspace/vercel-capabilities.ts +110 -0
- package/packages/core/src/ports/workspace/vercel-sandbox.ts +142 -114
- package/packages/core/src/processes/build-child.ts +17 -2
- package/packages/core/src/processes/build-execution-state.ts +37 -0
- package/packages/core/src/processes/build-runner.ts +54 -3
- package/packages/core/src/processes/dispatcher.ts +247 -41
- package/packages/core/src/processes/harvest-child.ts +5 -0
- package/packages/core/src/processes/harvest-runner.ts +66 -19
- package/packages/core/src/processes/harvest.ts +196 -24
- package/packages/core/src/processes/orchestrator-tick.ts +319 -0
- package/packages/core/src/processes/sandbox-state.ts +10 -0
- package/packages/core/src/store/adapter.ts +32 -0
- package/packages/core/src/store/build-scope.ts +64 -0
- package/packages/core/src/store/contract.ts +868 -0
- package/packages/core/src/store/digest.ts +49 -0
- package/packages/core/src/store/local/schema.ts +29 -2
- package/packages/core/src/store/local/store.ts +338 -4
- package/packages/core/src/store/memory.ts +179 -0
- package/packages/core/src/store/new-build.ts +17 -0
- package/packages/core/src/store/phase-session.ts +71 -0
- package/packages/core/src/store/remote/client.ts +260 -1
- package/packages/core/src/store/remote/index.ts +12 -2
- package/packages/core/src/store/remote/protocol.ts +100 -12
- package/packages/core/src/store/remote/version.ts +6 -2
- package/packages/core/src/store/repo-state-events.ts +140 -0
- package/packages/core/src/store/retention.ts +7 -0
- package/packages/core/src/store/session-handle.ts +64 -0
- package/packages/core/src/{sessions/reducer.ts → store/session-reducer.ts} +28 -9
- package/packages/core/src/store/streams/session-writer.ts +42 -2
- package/packages/core/src/store/streams/wait.ts +4 -5
- package/packages/core/src/store/ticket-assets.ts +425 -0
- package/packages/core/src/store/types.ts +122 -1
- package/packages/core/src/testing/index.ts +59 -0
- package/skills/code-review/SKILL.md +5 -1
- package/skills/guide/SKILL.md +126 -33
- package/skills/guide/references/operator-api.md +2 -2
- package/skills/guide/references/plugin-authoring.md +17 -0
- package/skills/guide/references/remote-store-protocol.md +169 -16
- package/skills/guide/references/remote-ticket-protocol.md +1 -1
- package/skills/guide/references/setup.md +12 -9
- package/skills/implement/SKILL.md +3 -0
- package/skills/operate/SKILL.md +126 -0
- package/skills/plan/SKILL.md +4 -0
- package/skills/plan-review/SKILL.md +5 -1
- package/skills/spec/SKILL.md +46 -3
- package/packages/core/src/cli/dashboard/frame-image.ts +0 -373
- package/packages/core/src/cli/store-opening.contract.ts +0 -289
- package/packages/core/src/cli/testkit.ts +0 -194
- package/packages/core/src/markdown.ts +0 -67
- package/packages/core/src/operator/client.ts +0 -311
- package/packages/core/src/operator/server.ts +0 -616
- package/packages/core/src/operator/sessions.ts +0 -223
- package/packages/core/src/ports/runner/live-contract-fixture.ts +0 -176
- package/packages/core/src/ports/tickets/remote-server.ts +0 -185
- package/packages/core/src/store/remote/server.ts +0 -923
- package/packages/core/src/testing/store-failures.ts +0 -32
|
@@ -67,6 +67,7 @@ export const allowedActorKinds: Record<EventType, readonly ActorKind[]> = {
|
|
|
67
67
|
'build.discard-requested': ['human'],
|
|
68
68
|
'build.auto-merge-requested': ['human'],
|
|
69
69
|
'build.auto-merge-cancelled': ['human'],
|
|
70
|
+
'build.auto-merge-default-observed': ['dispatcher'],
|
|
70
71
|
'build.paused': ['kernel'],
|
|
71
72
|
'build.resumed': ['kernel'],
|
|
72
73
|
'build.aborted': ['kernel', 'dispatcher'],
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared event-glob compilation (§15.3, AUT-342): the anchored glob matcher
|
|
3
|
+
* `ab watch`, `ab wait`, and the `[orchestrator].wake` configuration use.
|
|
4
|
+
* `*` matches any run of characters, `?` exactly one, everything else
|
|
5
|
+
* verbatim (so the dots inside event type names are escaped). Every glob
|
|
6
|
+
* must match at least one event type in one of the given catalogs, else a
|
|
7
|
+
* validation error naming the glob is thrown.
|
|
8
|
+
*/
|
|
9
|
+
import { EVENT_TYPES } from './payloads'
|
|
10
|
+
import { REPOSITORY_EVENT_TYPES } from './repository'
|
|
11
|
+
|
|
12
|
+
export interface CompileEventGlobsOptions {
|
|
13
|
+
/** Also accept matches in the repository-journal catalog (the `--repository`
|
|
14
|
+
* behavior of `ab watch`/`ab wait`). */
|
|
15
|
+
repository?: boolean
|
|
16
|
+
usage: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function compileEventGlobs(
|
|
20
|
+
globs: readonly string[],
|
|
21
|
+
opts: CompileEventGlobsOptions,
|
|
22
|
+
): RegExp[] {
|
|
23
|
+
return globs.map((glob) => {
|
|
24
|
+
const source =
|
|
25
|
+
'^' +
|
|
26
|
+
[...glob]
|
|
27
|
+
.map((char) => {
|
|
28
|
+
if (char === '*') return '.*'
|
|
29
|
+
if (char === '?') return '.'
|
|
30
|
+
return char.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
31
|
+
})
|
|
32
|
+
.join('') +
|
|
33
|
+
'$'
|
|
34
|
+
const regex = new RegExp(source)
|
|
35
|
+
const inBuildCatalog = EVENT_TYPES.some((type) => regex.test(type))
|
|
36
|
+
const inRepositoryCatalog =
|
|
37
|
+
opts.repository === true && REPOSITORY_EVENT_TYPES.some((type) => regex.test(type))
|
|
38
|
+
if (!inBuildCatalog && !inRepositoryCatalog) {
|
|
39
|
+
throw new Error(
|
|
40
|
+
`--event "${glob}" matches no known event type${
|
|
41
|
+
opts.repository ? ' in the build or repository catalogs' : ' in the build event catalog'
|
|
42
|
+
} — ${opts.usage}`,
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
return regex
|
|
46
|
+
})
|
|
47
|
+
}
|
|
@@ -36,6 +36,23 @@ import {
|
|
|
36
36
|
|
|
37
37
|
const empty = z.strictObject({})
|
|
38
38
|
const reasonOnly = z.strictObject({ reason: z.string().optional() })
|
|
39
|
+
/** Fan-out provenance shared by the auto-merge command facts: the repository
|
|
40
|
+
* seq of the `dispatcher.auto-merge-default-set` fact that caused the command,
|
|
41
|
+
* when one did. Per-build commands omit it. `{}` stays valid, so existing
|
|
42
|
+
* producers and logs replay unchanged. */
|
|
43
|
+
const autoMergeCommand = z.strictObject({
|
|
44
|
+
defaultSeq: z.number().int().positive().optional(),
|
|
45
|
+
})
|
|
46
|
+
/** A fan-out reconciliation marker: the repository seq of the newest
|
|
47
|
+
* `dispatcher.auto-merge-default-set` fact a build has observed when that fact
|
|
48
|
+
* already matched the build's requested state. It advances the build's
|
|
49
|
+
* provenance without appending a duplicate request/cancel command, so a later
|
|
50
|
+
* per-build toggle stands until a strictly newer default fact arrives
|
|
51
|
+
* (f_28b3fba1). Dispatcher-authored: it records a reconciliation, not a
|
|
52
|
+
* human command. */
|
|
53
|
+
const autoMergeDefaultObserved = z.strictObject({
|
|
54
|
+
defaultSeq: z.number().int().positive(),
|
|
55
|
+
})
|
|
39
56
|
const round = z.number().int().positive()
|
|
40
57
|
const attempt = z.number().int().positive()
|
|
41
58
|
const dispatchStage = z.enum(['create', 'workspace', 'spec', 'comment', 'launch'])
|
|
@@ -175,6 +192,20 @@ const finalizeStepCompletedPayloadSchema = z.discriminatedUnion('ok', [
|
|
|
175
192
|
}),
|
|
176
193
|
])
|
|
177
194
|
|
|
195
|
+
/** One ticket asset revision a build froze (SPEC §6.3): the identity and
|
|
196
|
+
* shape of what `listTicketAssets` returned at the pinning moment. */
|
|
197
|
+
export const pinnedAssetSchema = z.strictObject({
|
|
198
|
+
kind: z.string().min(1),
|
|
199
|
+
name: z.string().min(1),
|
|
200
|
+
revision: z.number().int().nonnegative(),
|
|
201
|
+
layout: z.enum(['file', 'tree']),
|
|
202
|
+
size: z.number().int().nonnegative(),
|
|
203
|
+
fileCount: z.number().int().nonnegative(),
|
|
204
|
+
dirCount: z.number().int().nonnegative(),
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
export type PinnedAsset = z.infer<typeof pinnedAssetSchema>
|
|
208
|
+
|
|
178
209
|
/** Shared by `plan-review.verdict` and `code-review.verdict` (symmetric by design). */
|
|
179
210
|
const reviewVerdictPayload = z.strictObject({
|
|
180
211
|
round,
|
|
@@ -194,12 +225,20 @@ export const eventPayloadSchemas = {
|
|
|
194
225
|
/** Claim-time auto-merge intent retained across dispatch recovery until the
|
|
195
226
|
* ordinary human-authored command fact can be materialized. */
|
|
196
227
|
autoMergeRequestedBy: z.string().min(1).optional(),
|
|
228
|
+
/** Claim-time auto-merge intent provenance: the repository seq of the
|
|
229
|
+
* `dispatcher.auto-merge-default-set` fact the claim sampled. Retained for
|
|
230
|
+
* the same recovery window as `autoMergeRequestedBy`; omitted when the
|
|
231
|
+
* claim sampled no default fact. */
|
|
232
|
+
autoMergeDefaultSeq: z.number().int().positive().optional(),
|
|
197
233
|
/** Frozen at claim time. Historical logs and disabled installs omit it. */
|
|
198
234
|
pr: z
|
|
199
235
|
.strictObject({
|
|
200
236
|
imageHost: prImageHostSchema.optional(),
|
|
201
237
|
})
|
|
202
238
|
.optional(),
|
|
239
|
+
/** Ticket assets frozen at claim time. Omitted when the ticket had none
|
|
240
|
+
* and in historical logs. */
|
|
241
|
+
assets: z.array(pinnedAssetSchema).optional(),
|
|
203
242
|
}),
|
|
204
243
|
'build.completed': z.strictObject({ outcome: buildOutcomeSchema }),
|
|
205
244
|
'runner.attached': z.strictObject({
|
|
@@ -223,6 +262,10 @@ export const eventPayloadSchemas = {
|
|
|
223
262
|
path: z.string().min(1).optional(),
|
|
224
263
|
/** Dispatcher-reachable path. Remote providers intentionally omit it. */
|
|
225
264
|
localPath: z.string().min(1).optional(),
|
|
265
|
+
/** True when the provider reported a remote publication capability at
|
|
266
|
+
* provision time. Absent ⇒ local (historical journals). AUT-505 removes
|
|
267
|
+
* the legacy fallback that reads absent as the provider name. */
|
|
268
|
+
remote: z.boolean().optional(),
|
|
226
269
|
branch: z.string().min(1),
|
|
227
270
|
base: workspaceBaseSchema,
|
|
228
271
|
}),
|
|
@@ -363,8 +406,9 @@ export const eventPayloadSchemas = {
|
|
|
363
406
|
'build.resume-requested': reasonOnly,
|
|
364
407
|
'build.abort-requested': reasonOnly,
|
|
365
408
|
'build.discard-requested': empty,
|
|
366
|
-
'build.auto-merge-requested':
|
|
367
|
-
'build.auto-merge-cancelled':
|
|
409
|
+
'build.auto-merge-requested': autoMergeCommand,
|
|
410
|
+
'build.auto-merge-cancelled': autoMergeCommand,
|
|
411
|
+
'build.auto-merge-default-observed': autoMergeDefaultObserved,
|
|
368
412
|
'build.paused': empty,
|
|
369
413
|
'build.resumed': empty,
|
|
370
414
|
'build.aborted': empty,
|
|
@@ -382,6 +426,9 @@ export const eventPayloadSchemas = {
|
|
|
382
426
|
artifact: artifactRefSchema,
|
|
383
427
|
/** seq of the `escalation.raised` event that forced the revision. */
|
|
384
428
|
escalation: z.number().int().positive(),
|
|
429
|
+
/** Present (possibly empty) when the revision re-pins the build to the
|
|
430
|
+
* ticket's then-current assets; absent leaves the pinned set unchanged. */
|
|
431
|
+
assets: z.array(pinnedAssetSchema).optional(),
|
|
385
432
|
}),
|
|
386
433
|
|
|
387
434
|
// ── Sessions (every agent run is bracketed by these) ──────────────────────
|
|
@@ -552,6 +599,9 @@ export const eventPayloadSchemas = {
|
|
|
552
599
|
resolution: escalationResolutionSchema,
|
|
553
600
|
/** Exact replacement spec authorized by a `revise-spec` answer. */
|
|
554
601
|
artifact: artifactRefSchema.optional(),
|
|
602
|
+
/** The ticket's assets sampled with a revise-from-ticket authorization, so a
|
|
603
|
+
* retry re-pins the set that was authorized. Absent otherwise. */
|
|
604
|
+
assets: z.array(pinnedAssetSchema).optional(),
|
|
555
605
|
/** Absolute per-loop/current-spec review budget selected by the operator. */
|
|
556
606
|
reviewRoundCeiling: z.number().int().positive().optional(),
|
|
557
607
|
}),
|
|
@@ -76,6 +76,10 @@ const tickCountersSchema = z.strictObject({
|
|
|
76
76
|
sandboxIdleStops: z.number().int().nonnegative().optional(),
|
|
77
77
|
/** Contained idle-settlement failures this tick; retried next tick. */
|
|
78
78
|
sandboxSettleFailures: z.number().int().nonnegative().optional(),
|
|
79
|
+
/** Contained sandbox-backend construction failures this tick; re-reported
|
|
80
|
+
* every tick until the config is repaired. Optional so historical tick
|
|
81
|
+
* facts replay. */
|
|
82
|
+
orchestratorSandboxFailures: z.number().int().nonnegative().optional(),
|
|
79
83
|
harvestStarted: z.number().int().nonnegative(),
|
|
80
84
|
harvestResumed: z.number().int().nonnegative(),
|
|
81
85
|
harvestCompleted: z.number().int().nonnegative(),
|
|
@@ -264,13 +268,20 @@ export const harvestEventPayloadSchemas = {
|
|
|
264
268
|
* `processes/sandbox-state.ts`, mirroring harvest execution settlement. */
|
|
265
269
|
export const orchestratorSandboxEventPayloadSchemas = {
|
|
266
270
|
/** First use of one operator's environment for this repository, or a
|
|
267
|
-
* post-reset reprovision. Carries the full environment identity
|
|
271
|
+
* post-reset reprovision. Carries the full environment identity, plus the
|
|
272
|
+
* base branch head the fresh provision selected — the publication
|
|
273
|
+
* precondition's reference point (a publish must be a descendant of it).
|
|
274
|
+
* Optional so historical journals replay unchanged. */
|
|
268
275
|
'orchestrator.sandbox.provisioned': z.strictObject({
|
|
269
276
|
operator: z.string().min(1),
|
|
270
277
|
environmentId: z.string().min(1),
|
|
271
278
|
provider: z.string().min(1),
|
|
272
279
|
sessionId: z.string().min(1).optional(),
|
|
273
280
|
workspacePath: z.string().min(1),
|
|
281
|
+
baseSha: z
|
|
282
|
+
.string()
|
|
283
|
+
.regex(/^[0-9a-f]{40,64}$/i)
|
|
284
|
+
.optional(),
|
|
274
285
|
}),
|
|
275
286
|
/** A stopped environment resumed for a later tool call. */
|
|
276
287
|
'orchestrator.sandbox.resumed': z.strictObject({
|
|
@@ -311,6 +322,34 @@ export const orchestratorSandboxEventPayloadSchemas = {
|
|
|
311
322
|
operator: z.string().min(1),
|
|
312
323
|
environmentId: z.string().min(1),
|
|
313
324
|
}),
|
|
325
|
+
/** One PR-only publication from an operator sandbox: the operator, the
|
|
326
|
+
* delegated orchestrator session when one triggered the tool, the
|
|
327
|
+
* deterministic branch the commit was pushed to, the published commit, and
|
|
328
|
+
* the opened-or-adopted PR. Written after both the push and the forge call
|
|
329
|
+
* succeed; the push-before-fact discipline makes a retry complete the
|
|
330
|
+
* journal rather than re-push. */
|
|
331
|
+
'orchestrator.sandbox.published': z.strictObject({
|
|
332
|
+
operator: z.string().min(1),
|
|
333
|
+
environmentId: z.string().min(1),
|
|
334
|
+
branch: z.string().min(1),
|
|
335
|
+
sha: z.string().regex(/^[0-9a-f]{40,64}$/i),
|
|
336
|
+
session: z.string().min(1).optional(),
|
|
337
|
+
pr: z.strictObject({
|
|
338
|
+
number: z.number().int().positive(),
|
|
339
|
+
url: z.string().min(1),
|
|
340
|
+
headSha: z.string().min(1),
|
|
341
|
+
}),
|
|
342
|
+
}),
|
|
343
|
+
/** A refused or failed publication attempt. `stage` names where it stopped
|
|
344
|
+
* (the precondition checks, the push, or the forge PR call); `message` is
|
|
345
|
+
* the provider's or forge's text, redacted of credential values by the
|
|
346
|
+
* service. */
|
|
347
|
+
'orchestrator.sandbox.publish-failed': z.strictObject({
|
|
348
|
+
operator: z.string().min(1),
|
|
349
|
+
environmentId: z.string().min(1),
|
|
350
|
+
stage: z.enum(['checks', 'push', 'pr']),
|
|
351
|
+
message: z.string().min(1),
|
|
352
|
+
}),
|
|
314
353
|
} as const
|
|
315
354
|
|
|
316
355
|
export const dispatcherStatusEventPayloadSchemas = {
|
|
@@ -421,6 +460,16 @@ export type RepositoryEventType = keyof typeof repositoryEventPayloadSchemas
|
|
|
421
460
|
export const REPOSITORY_EVENT_TYPES = Object.keys(
|
|
422
461
|
repositoryEventPayloadSchemas,
|
|
423
462
|
) as RepositoryEventType[]
|
|
463
|
+
|
|
464
|
+
/** The repository-journal attention set, followed only under `--repository`
|
|
465
|
+
* by `ab watch` and scanned by the orchestrator wake pass. */
|
|
466
|
+
export const REPOSITORY_ATTENTION_EVENTS = [
|
|
467
|
+
'harvest.escalated',
|
|
468
|
+
'harvest.failed',
|
|
469
|
+
'dispatcher.tick-failed',
|
|
470
|
+
'dispatcher.config-rejected',
|
|
471
|
+
'dispatcher.harvest-runner-failed',
|
|
472
|
+
] as const satisfies readonly RepositoryEventType[]
|
|
424
473
|
export type RepositoryEventPayload<T extends RepositoryEventType> = z.infer<
|
|
425
474
|
(typeof repositoryEventPayloadSchemas)[T]
|
|
426
475
|
>
|
|
@@ -487,6 +536,8 @@ const allowedActorKinds: Record<RepositoryEventType, readonly ActorKind[]> = {
|
|
|
487
536
|
// `harvest.execution.released` is dispatcher-authored.
|
|
488
537
|
'orchestrator.sandbox.released': ['human', 'dispatcher'],
|
|
489
538
|
'orchestrator.sandbox.reset': ['human'],
|
|
539
|
+
'orchestrator.sandbox.published': ['human'],
|
|
540
|
+
'orchestrator.sandbox.publish-failed': ['human'],
|
|
490
541
|
'dispatcher.run-started': ['dispatcher'],
|
|
491
542
|
'dispatcher.run-stopped': ['dispatcher'],
|
|
492
543
|
'dispatcher.tick-yielded': ['dispatcher'],
|
|
@@ -24,24 +24,62 @@ const positiveInt = z.number().int().positive()
|
|
|
24
24
|
|
|
25
25
|
const empty = z.strictObject({})
|
|
26
26
|
|
|
27
|
-
/** The trigger that started a turn: a human message or a wake event.
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
27
|
+
/** The trigger that started a turn: a human message or a wake event. A wake
|
|
28
|
+
* trigger names exactly one source — a build (the build-log attention scan)
|
|
29
|
+
* or the repository journal — enforced by the schema's `.check()` below even
|
|
30
|
+
* though the static type (one object with optional fields) cannot express it:
|
|
31
|
+
* a two-member union or a nested discriminated union is not a valid
|
|
32
|
+
* discriminated-union option on zod 4 (both constructions throw at first
|
|
33
|
+
* parse), so the wake variant stays a single object and call sites branch on
|
|
34
|
+
* `journal === true`. */
|
|
35
|
+
const turnWakeTriggerSchema = z
|
|
36
|
+
.strictObject({
|
|
31
37
|
kind: z.literal('wake'),
|
|
32
|
-
build: z.string().min(1),
|
|
38
|
+
build: z.string().min(1).optional(),
|
|
39
|
+
journal: z.literal(true).optional(),
|
|
33
40
|
seq: positiveInt,
|
|
34
41
|
type: z.string().min(1),
|
|
35
|
-
})
|
|
42
|
+
})
|
|
43
|
+
.check((ctx) => {
|
|
44
|
+
const hasBuild = ctx.value.build !== undefined
|
|
45
|
+
const hasJournal = ctx.value.journal === true
|
|
46
|
+
if (hasBuild === hasJournal) {
|
|
47
|
+
ctx.issues.push({
|
|
48
|
+
code: 'custom',
|
|
49
|
+
input: ctx.value,
|
|
50
|
+
message: 'a wake trigger names exactly one of build or journal',
|
|
51
|
+
})
|
|
52
|
+
}
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
const turnTriggerSchema = z.discriminatedUnion('kind', [
|
|
56
|
+
z.strictObject({ kind: z.literal('message'), messageSeq: positiveInt }),
|
|
57
|
+
turnWakeTriggerSchema,
|
|
36
58
|
])
|
|
37
59
|
|
|
38
|
-
/** Matches the transcript usage shape (harvest.session.ended, §15.3).
|
|
60
|
+
/** Matches the transcript usage shape (harvest.session.ended, §15.3). The
|
|
61
|
+
* turn runner records the AI SDK's totalUsage token counts plus the step
|
|
62
|
+
* count. Old `turn.completed` events without `steps` still reduce. */
|
|
39
63
|
const usageSchema = z.strictObject({
|
|
40
64
|
inputTokens: z.number().int().nonnegative(),
|
|
41
65
|
outputTokens: z.number().int().nonnegative(),
|
|
66
|
+
steps: z.number().int().nonnegative(),
|
|
42
67
|
turns: z.number().int().nonnegative().optional(),
|
|
43
68
|
})
|
|
44
69
|
|
|
70
|
+
/** The typed model-failure vocabulary (AUT-342): provider availability,
|
|
71
|
+
* exhaustion, credentials, and configuration are distinguished so a failed
|
|
72
|
+
* turn's class is visible in the session's reduced state and nothing is ever
|
|
73
|
+
* retried unboundedly. `internal` covers non-model failures — the dispatcher
|
|
74
|
+
* tick's crash reaper among them. */
|
|
75
|
+
const turnFailureKindSchema = z.enum([
|
|
76
|
+
'provider-unavailable',
|
|
77
|
+
'exhausted',
|
|
78
|
+
'credentials',
|
|
79
|
+
'configuration',
|
|
80
|
+
'internal',
|
|
81
|
+
])
|
|
82
|
+
|
|
45
83
|
export const sessionEventPayloadSchemas = {
|
|
46
84
|
/** The fact of creation; the session record carries the same title. */
|
|
47
85
|
'session.created': z.strictObject({ title: z.string().min(1).optional() }),
|
|
@@ -52,11 +90,27 @@ export const sessionEventPayloadSchemas = {
|
|
|
52
90
|
'session.wake-set': z.strictObject({
|
|
53
91
|
globs: z.array(z.string().min(1)).max(100),
|
|
54
92
|
}),
|
|
55
|
-
/** The (later
|
|
93
|
+
/** The (later turn runner's) start of one turn on its stream. A wake
|
|
94
|
+
* turn additionally carries the delivered input — the attention event
|
|
95
|
+
* record and, for a build wake, the build's reduced state, frozen at wake
|
|
96
|
+
* time — so a later invocation reconstructs the identical user message
|
|
97
|
+
* from durable state alone (live build state keeps moving; the frozen
|
|
98
|
+
* snapshot does not). A journal wake delivers the event record only. */
|
|
56
99
|
'turn.started': z.strictObject({
|
|
57
100
|
turn: z.string().min(1),
|
|
58
101
|
stream: z.string().min(1),
|
|
59
102
|
trigger: turnTriggerSchema,
|
|
103
|
+
wake: z
|
|
104
|
+
.strictObject({
|
|
105
|
+
event: z.strictObject({
|
|
106
|
+
seq: positiveInt,
|
|
107
|
+
ts: z.string().min(1),
|
|
108
|
+
type: z.string().min(1),
|
|
109
|
+
payload: z.record(z.string(), z.unknown()),
|
|
110
|
+
}),
|
|
111
|
+
buildState: z.record(z.string(), z.unknown()).optional(),
|
|
112
|
+
})
|
|
113
|
+
.optional(),
|
|
60
114
|
}),
|
|
61
115
|
/** A turn was suspended for budget or for an approval. */
|
|
62
116
|
'turn.suspended': z.strictObject({
|
|
@@ -80,8 +134,13 @@ export const sessionEventPayloadSchemas = {
|
|
|
80
134
|
}),
|
|
81
135
|
/** One turn finished. */
|
|
82
136
|
'turn.completed': z.strictObject({ turn: z.string().min(1), usage: usageSchema }),
|
|
83
|
-
/** One turn failed
|
|
84
|
-
|
|
137
|
+
/** One turn failed: `kind` is the typed failure class, `error` the
|
|
138
|
+
* human-readable message surfaced in the session's reduced state. */
|
|
139
|
+
'turn.failed': z.strictObject({
|
|
140
|
+
turn: z.string().min(1),
|
|
141
|
+
kind: turnFailureKindSchema,
|
|
142
|
+
error: z.string().min(1),
|
|
143
|
+
}),
|
|
85
144
|
/** Terminal. The session is read-only afterwards. */
|
|
86
145
|
'session.archived': empty,
|
|
87
146
|
} as const
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { EventPayload } from './payloads'
|
|
2
|
+
|
|
3
|
+
/** Whether the workspace this payload records publishes remotely, per the
|
|
4
|
+
* marker recorded at provision. Journals written before the marker existed
|
|
5
|
+
* fall back to the legacy provider-name reading — the only name comparison
|
|
6
|
+
* among the remote/local decisions (removed in AUT-505). */
|
|
7
|
+
export function isRemoteWorkspace(
|
|
8
|
+
payload: Pick<EventPayload<'workspace.provisioned'>, 'provider' | 'remote'>,
|
|
9
|
+
): boolean {
|
|
10
|
+
if (payload.remote !== undefined) return payload.remote
|
|
11
|
+
return payload.provider === 'vercel-sandbox'
|
|
12
|
+
}
|
|
@@ -347,7 +347,8 @@ export async function makeHarness(opts: {
|
|
|
347
347
|
gatePresence: opts.gatePresence ?? 'present',
|
|
348
348
|
...(opts.pluginForge?.prAttachments === true ? { prAttachments: true } : {}),
|
|
349
349
|
})
|
|
350
|
-
let selectedForge: Forge =
|
|
350
|
+
let selectedForge: Forge =
|
|
351
|
+
opts.localGitForge === true ? new LocalGitForge({ repoRoot: origin }) : forge
|
|
351
352
|
let plugins: PluginRegistry | undefined
|
|
352
353
|
if (opts.pluginForge !== undefined) {
|
|
353
354
|
plugins = createPluginRegistry()
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The repository auto-merge default as a bulk action over in-flight builds.
|
|
3
|
+
*
|
|
4
|
+
* The durable `dispatcher.auto-merge-default-set` fact has always been the
|
|
5
|
+
* claim-time seed; it now also fans out onto every current non-terminal build
|
|
6
|
+
* of the repository, attributed to the operator who toggled it. The fan-out is
|
|
7
|
+
* a dispatcher-tick reconciliation driven by the durable fact — never a
|
|
8
|
+
* write-time walk — so a build claimed concurrently with the toggle and a
|
|
9
|
+
* build observed by a second dispatcher polling the same repository converge
|
|
10
|
+
* on the same state.
|
|
11
|
+
*
|
|
12
|
+
* Idempotency vs. per-build overrides: each answering command records the
|
|
13
|
+
* repository `seq` of the default fact that caused it (`defaultSeq`). A tick
|
|
14
|
+
* applies the default to a build only when the newest fact's seq is strictly
|
|
15
|
+
* newer than the build's recorded `defaultSeq`. When the build's requested
|
|
16
|
+
* state already matches the fact, the tick records a provenance-only
|
|
17
|
+
* `build.auto-merge-default-observed` marker instead of appending a duplicate
|
|
18
|
+
* command, so a later per-build toggle is not mistaken for staleness and
|
|
19
|
+
* reverted. A global toggle therefore re-applies over a per-build choice, a
|
|
20
|
+
* per-build toggle in between is stable, and pressing the toggle twice appends
|
|
21
|
+
* no duplicate request.
|
|
22
|
+
*
|
|
23
|
+
* Eligibility is expressed against `BuildState`, not `effectiveStatus`
|
|
24
|
+
* (dashboard/model.ts), whose contract is "DISPLAY-ONLY — nothing consults
|
|
25
|
+
* this"; a durable write path must not become its first consumer — the same
|
|
26
|
+
* precedent `bulk-control.ts` set for the pause/resume walk.
|
|
27
|
+
*/
|
|
28
|
+
import type { RepositoryEvent } from '../events/repository'
|
|
29
|
+
import { discardInFlight, type BuildState } from './reducer'
|
|
30
|
+
|
|
31
|
+
/** One `dispatcher.auto-merge-default-set` fact, resolved. `actor` is the
|
|
32
|
+
* event's actor — the human who toggled, `via` marker included — and is the
|
|
33
|
+
* attribution every fan-out command carries. */
|
|
34
|
+
export interface AutoMergeDefaultFact {
|
|
35
|
+
enabled: boolean
|
|
36
|
+
/** The repository event's seq — the fan-out's comparison marker. */
|
|
37
|
+
seq: number
|
|
38
|
+
actor: RepositoryEvent['actor']
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** The newest `dispatcher.auto-merge-default-set` fact in the repository
|
|
42
|
+
* journal, or undefined when no default was ever set (the scan runs from the
|
|
43
|
+
* tail because seqs are monotonic, so the first hit is the newest). With
|
|
44
|
+
* `enabled`, the newest fact *matching that value* — the fact a claim-time
|
|
45
|
+
* seed built on a stale pre-tick sample must cite, so a newer opposite fact
|
|
46
|
+
* stays strictly newer than the seed's provenance and the next tick's
|
|
47
|
+
* fan-out can reconcile it (f_b851c0e8). */
|
|
48
|
+
export function latestAutoMergeDefault(
|
|
49
|
+
events: RepositoryEvent[],
|
|
50
|
+
enabled?: boolean,
|
|
51
|
+
): AutoMergeDefaultFact | undefined {
|
|
52
|
+
for (let index = events.length - 1; index >= 0; index -= 1) {
|
|
53
|
+
const event = events[index]
|
|
54
|
+
if (
|
|
55
|
+
event?.type === 'dispatcher.auto-merge-default-set' &&
|
|
56
|
+
(enabled === undefined || event.payload.enabled === enabled)
|
|
57
|
+
) {
|
|
58
|
+
return { enabled: event.payload.enabled, seq: event.seq, actor: event.actor }
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return undefined
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* A build the default may still act on: not terminal, and not on its way out.
|
|
66
|
+
* A pending abort (an explicit request or an accepted escalation answer) must
|
|
67
|
+
* never receive consent — a merge landing mid-abort is exactly the race this
|
|
68
|
+
* clause exists to close. The same holds for an outstanding discard request:
|
|
69
|
+
* `discardRequest` is present exactly while the build is non-terminal and its
|
|
70
|
+
* discard is unsettled, and the reducer settles it only by terminal completion
|
|
71
|
+
* — consent recorded inside that window can merge a build the operator already
|
|
72
|
+
* asked to discard (in the raced-runner-attachment case the discard is inert
|
|
73
|
+
* for a running build, and the recorded consent later drives the merge in
|
|
74
|
+
* `checkPr`). Direction-blind like the abort clause: the build terminalizes
|
|
75
|
+
* imminently either way. If discard ever grows a mid-flight settlement event,
|
|
76
|
+
* this predicate must be revisited.
|
|
77
|
+
*/
|
|
78
|
+
export function autoMergeDefaultEligible(state: BuildState): boolean {
|
|
79
|
+
if (state.status === 'done' || state.status === 'aborted') return false
|
|
80
|
+
// An empty log is a record whose `build.created` has not landed yet (the
|
|
81
|
+
// crash or one-await window between `createBuild` and the first append):
|
|
82
|
+
// writing an auto-merge command ahead of the immutable facts would leave the
|
|
83
|
+
// log unrecoverable — dispatch recovery rejects a log that does not start
|
|
84
|
+
// with `build.created` (f_647d40be). `lastSeq === 0` is exactly that log.
|
|
85
|
+
if (state.lastSeq === 0) return false
|
|
86
|
+
// An outstanding discard request is the same "on its way out" shape as a
|
|
87
|
+
// pending abort: it settles only by terminal completion (reducer.ts), so
|
|
88
|
+
// consent recorded inside that window can merge a build the operator
|
|
89
|
+
// already asked to discard. Direction-blind, like the abort clause — an
|
|
90
|
+
// OFF fan-out skipping it withdraws nothing that survives the settlement.
|
|
91
|
+
// Shared predicate: the per-build consent guard (build-control.ts) applies
|
|
92
|
+
// the same exclusion to consent-recording writes. A per-build OFF withdrawal
|
|
93
|
+
// stays available there — revocation only shrinks the merge set — which is a
|
|
94
|
+
// stated, directional divergence, not a silent one.
|
|
95
|
+
if (discardInFlight(state)) return false
|
|
96
|
+
return !state.pendingCommands.some((command) => command.command === 'abort')
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** What one default fact asks of one build: a real command, a pure provenance
|
|
100
|
+
* advance, or `undefined` for "nothing". */
|
|
101
|
+
export type AutoMergeDefaultTarget = 'request' | 'cancel' | 'observed'
|
|
102
|
+
|
|
103
|
+
/** What one default fact asks of one build, or undefined for "nothing".
|
|
104
|
+
*
|
|
105
|
+
* Seq-first: a build whose recorded provenance is already this fact (or newer)
|
|
106
|
+
* has had its day in court — a per-build toggle made after the fan-out stands
|
|
107
|
+
* until the default moves again. A build that sampled the default at claim time
|
|
108
|
+
* carries the fact's seq too, so a fresh claim is not re-fanned on the next
|
|
109
|
+
* tick.
|
|
110
|
+
*
|
|
111
|
+
* When the requested state already matches the fact, the fact still has to be
|
|
112
|
+
* *recorded* as observed (`'observed'`) so the next tick does not mistake a
|
|
113
|
+
* later per-build command for staleness: the marker advances `defaultSeq`
|
|
114
|
+
* without a duplicate request/cancel (f_28b3fba1). */
|
|
115
|
+
export function autoMergeDefaultTarget(
|
|
116
|
+
state: BuildState,
|
|
117
|
+
fact: AutoMergeDefaultFact,
|
|
118
|
+
): AutoMergeDefaultTarget | undefined {
|
|
119
|
+
if (!autoMergeDefaultEligible(state)) return undefined
|
|
120
|
+
if ((state.autoMerge.defaultSeq ?? 0) >= fact.seq) return undefined
|
|
121
|
+
if (state.autoMerge.requested === fact.enabled) return 'observed'
|
|
122
|
+
return fact.enabled ? 'request' : 'cancel'
|
|
123
|
+
}
|
|
@@ -52,6 +52,9 @@ export interface ContextInputs {
|
|
|
52
52
|
* loop routing (loopOfPhase → 'other') relies on this input existing.
|
|
53
53
|
*/
|
|
54
54
|
answeredGuidance?: boolean
|
|
55
|
+
/** The ticket assets the build froze at claim (SPEC §6.3), written under
|
|
56
|
+
* `.ab/assets/`. Every phase receives them. */
|
|
57
|
+
assets?: boolean
|
|
55
58
|
}
|
|
56
59
|
|
|
57
60
|
export interface PhaseSpec {
|
|
@@ -91,6 +94,7 @@ export const PHASE_SPECS: Record<CorePhase | 'verify', PhaseSpec> = {
|
|
|
91
94
|
spec: true,
|
|
92
95
|
priorOwnArtifacts: true,
|
|
93
96
|
findings: 'current',
|
|
97
|
+
assets: true,
|
|
94
98
|
},
|
|
95
99
|
},
|
|
96
100
|
'plan-review': {
|
|
@@ -102,7 +106,7 @@ export const PHASE_SPECS: Record<CorePhase | 'verify', PhaseSpec> = {
|
|
|
102
106
|
terminalEvent: 'plan-review.verdict',
|
|
103
107
|
requiredArtifact: 'plan-review',
|
|
104
108
|
verdictVocabulary: ['approve', 'revise', 'escalate'],
|
|
105
|
-
inputs: { spec: true, plan: 'latest', findings: 'all-rounds' },
|
|
109
|
+
inputs: { spec: true, plan: 'latest', findings: 'all-rounds', assets: true },
|
|
106
110
|
},
|
|
107
111
|
implement: {
|
|
108
112
|
name: 'implement',
|
|
@@ -120,6 +124,7 @@ export const PHASE_SPECS: Record<CorePhase | 'verify', PhaseSpec> = {
|
|
|
120
124
|
findings: 'current',
|
|
121
125
|
verifyReports: true,
|
|
122
126
|
priorOwnArtifacts: true,
|
|
127
|
+
assets: true,
|
|
123
128
|
},
|
|
124
129
|
},
|
|
125
130
|
'code-review': {
|
|
@@ -137,6 +142,7 @@ export const PHASE_SPECS: Record<CorePhase | 'verify', PhaseSpec> = {
|
|
|
137
142
|
commitRange: true,
|
|
138
143
|
findings: 'all-rounds',
|
|
139
144
|
implementNotes: true,
|
|
145
|
+
assets: true,
|
|
140
146
|
},
|
|
141
147
|
},
|
|
142
148
|
verify: {
|
|
@@ -147,7 +153,13 @@ export const PHASE_SPECS: Record<CorePhase | 'verify', PhaseSpec> = {
|
|
|
147
153
|
terminal: 'verdict',
|
|
148
154
|
terminalEvent: 'verify.completed',
|
|
149
155
|
verdictVocabulary: ['pass', 'fail', 'skip'],
|
|
150
|
-
inputs: {
|
|
156
|
+
inputs: {
|
|
157
|
+
spec: true,
|
|
158
|
+
stepConfig: true,
|
|
159
|
+
commitRange: true,
|
|
160
|
+
currentFeedback: true,
|
|
161
|
+
assets: true,
|
|
162
|
+
},
|
|
151
163
|
},
|
|
152
164
|
finalize: {
|
|
153
165
|
name: 'finalize',
|
|
@@ -164,6 +176,7 @@ export const PHASE_SPECS: Record<CorePhase | 'verify', PhaseSpec> = {
|
|
|
164
176
|
verifyReports: true,
|
|
165
177
|
prTemplate: true,
|
|
166
178
|
answeredGuidance: true,
|
|
179
|
+
assets: true,
|
|
167
180
|
},
|
|
168
181
|
},
|
|
169
182
|
reconcile: {
|
|
@@ -181,6 +194,7 @@ export const PHASE_SPECS: Record<CorePhase | 'verify', PhaseSpec> = {
|
|
|
181
194
|
implementNotes: true,
|
|
182
195
|
conflict: true,
|
|
183
196
|
answeredGuidance: true,
|
|
197
|
+
assets: true,
|
|
184
198
|
},
|
|
185
199
|
},
|
|
186
200
|
}
|