@defrex/autobuild 0.10.1 → 0.12.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 +3 -1
- package/SPEC.md +237 -23
- package/bin/ab-dispatch-kernel.ts +1 -1
- package/docs/architecture.md +66 -4
- package/docs/configuration.md +137 -19
- package/docs/hosted-dispatcher.md +17 -2
- package/docs/hosted-service.md +214 -0
- package/docs/mcp.md +8 -2
- package/docs/operator-api.md +107 -7
- package/docs/remote-store-protocol.md +189 -144
- package/docs/remote-ticket-protocol.md +111 -18
- package/docs/setup.md +173 -16
- package/docs/ticket-dependencies.md +1 -1
- package/package.json +7 -6
- package/packages/core/src/cli/attention.ts +209 -0
- package/packages/core/src/cli/binary.ts +3 -3
- package/packages/core/src/cli/configured-store.ts +17 -0
- package/packages/core/src/cli/connect.ts +261 -0
- package/packages/core/src/cli/dashboard/model.ts +8 -5
- package/packages/core/src/cli/dashboard/render.ts +37 -7
- package/packages/core/src/cli/dispatch-frontend.ts +16 -1
- package/packages/core/src/cli/dispatch-process.ts +25 -7
- package/packages/core/src/cli/dispatch.ts +247 -47
- package/packages/core/src/cli/dispatcher-liveness.ts +91 -0
- package/packages/core/src/cli/help.ts +45 -13
- package/packages/core/src/cli/init-validation.ts +27 -2
- package/packages/core/src/cli/main.ts +41 -15
- package/packages/core/src/cli/mcp.ts +7 -1
- package/packages/core/src/cli/pr-summary-publication.ts +220 -0
- package/packages/core/src/cli/pr-summary.ts +2 -0
- package/packages/core/src/cli/project-credentials.ts +132 -0
- package/packages/core/src/cli/repo-state.ts +15 -2
- package/packages/core/src/cli/repository-status.ts +45 -5
- package/packages/core/src/cli/self-update.ts +10 -0
- package/packages/core/src/cli/store-opening.ts +22 -14
- package/packages/core/src/cli/terminals.ts +0 -10
- package/packages/core/src/cli/ticket.ts +8 -1
- package/packages/core/src/cli/upgrade-compatibility.ts +80 -0
- package/packages/core/src/cli/wait.ts +84 -17
- package/packages/core/src/cli/watch.ts +350 -68
- package/packages/core/src/config/live.ts +4 -0
- package/packages/core/src/config/schema.ts +65 -11
- package/packages/core/src/connection.ts +86 -0
- package/packages/core/src/events/catalog.ts +10 -2
- package/packages/core/src/events/globs.ts +8 -3
- package/packages/core/src/events/payloads.ts +44 -0
- package/packages/core/src/events/repository.ts +64 -2
- package/packages/core/src/integration/harness.ts +2 -1
- package/packages/core/src/kernel/__snapshots__/reducer-registry.test.ts.snap +148 -83
- package/packages/core/src/kernel/dispatch-status.ts +9 -1
- package/packages/core/src/kernel/dispatcher-liveness.ts +70 -0
- package/packages/core/src/kernel/engine.ts +24 -0
- package/packages/core/src/kernel/generators/build-log.ts +10 -0
- package/packages/core/src/kernel/log-index.ts +17 -0
- package/packages/core/src/kernel/pr-summary.ts +89 -0
- package/packages/core/src/kernel/reducer-registry.ts +12 -0
- package/packages/core/src/kernel/reducer.ts +8 -0
- package/packages/core/src/kernel/ticket-source-health.ts +30 -0
- package/packages/core/src/ontology.ts +1 -0
- package/packages/core/src/operator/index.ts +7 -0
- package/packages/core/src/operator/query.ts +44 -19
- package/packages/core/src/operator/ticket-source-backend.ts +1 -0
- package/packages/core/src/operator/tickets.ts +6 -8
- package/packages/core/src/plugin-sdk/index.ts +1 -0
- package/packages/core/src/plugins/manifest.ts +5 -3
- package/packages/core/src/plugins/runtimes.ts +3 -1
- package/packages/core/src/ports/forge/fake.ts +13 -1
- package/packages/core/src/ports/forge/github-transport.ts +36 -1
- package/packages/core/src/ports/forge/github.ts +29 -5
- package/packages/core/src/ports/forge/local-git.ts +12 -4
- package/packages/core/src/ports/forge/summary-marker.ts +8 -0
- package/packages/core/src/ports/runner/session-env.ts +1 -0
- package/packages/core/src/ports/tickets/contract.ts +25 -0
- package/packages/core/src/ports/tickets/create.ts +11 -3
- package/packages/core/src/ports/tickets/fake.ts +10 -8
- package/packages/core/src/ports/tickets/file.ts +1 -0
- package/packages/core/src/ports/tickets/hosted-index.ts +0 -3
- package/packages/core/src/ports/tickets/linear.ts +241 -49
- package/packages/core/src/ports/tickets/remote-protocol.ts +3 -0
- package/packages/core/src/ports/tickets/remote.ts +1 -0
- package/packages/core/src/ports/types.ts +17 -0
- package/packages/core/src/ports/workspace/build-execution.ts +5 -1
- package/packages/core/src/ports/workspace/create.ts +11 -2
- package/packages/core/src/ports/workspace/distribution-archive.ts +3 -5
- package/packages/core/src/ports/workspace/harvest-execution.ts +3 -1
- package/packages/core/src/ports/workspace/local-build-execution.ts +8 -1
- package/packages/core/src/ports/workspace/vercel-sandbox.ts +47 -19
- package/packages/core/src/processes/build-execution-state.ts +1 -1
- package/packages/core/src/processes/build-runner.ts +87 -26
- package/packages/core/src/processes/dispatch-failure-budget.ts +83 -0
- package/packages/core/src/processes/dispatcher.ts +336 -83
- package/packages/core/src/processes/harvest-child.ts +1 -0
- package/packages/core/src/processes/harvest-execution-state.ts +19 -2
- package/packages/core/src/processes/harvest-runner.ts +142 -100
- package/packages/core/src/processes/phase-failure-budget.ts +1 -0
- package/packages/core/src/processes/publication-loss.ts +18 -1
- package/packages/core/src/processes/repo-view.ts +2 -1
- package/packages/core/src/store/adapter.ts +1 -0
- package/packages/core/src/store/contract.ts +109 -0
- package/packages/core/src/store/digest.ts +14 -2
- package/packages/core/src/store/local/store.ts +18 -5
- package/packages/core/src/store/remote/client.ts +1 -1
- package/packages/core/src/store/remote/compatibility.ts +90 -0
- package/packages/core/src/store/remote/credentials.ts +28 -0
- package/packages/core/src/store/remote/index.ts +14 -1
- package/packages/core/src/store/remote/protocol.ts +26 -0
- package/packages/core/src/store/remote/token.ts +74 -13
- package/packages/core/src/store/repo-state-events.ts +49 -59
- package/packages/core/src/store/types.ts +5 -1
- package/packages/core/src/testing/index.ts +7 -0
- package/skills/guide/SKILL.md +175 -26
- package/skills/guide/references/operator-api.md +135 -13
- package/skills/guide/references/plugin-authoring.md +10 -2
- package/skills/guide/references/remote-store-protocol.md +189 -144
- package/skills/guide/references/remote-ticket-protocol.md +111 -18
- package/skills/guide/references/setup.md +173 -16
- package/skills/guide/references/ticket-dependencies.md +1 -1
- package/skills/operate/SKILL.md +159 -34
- package/templates/hosted-service/package.json +13 -0
- package/templates/hosted-service/vercel.json +7 -0
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* validated.
|
|
13
13
|
*/
|
|
14
14
|
import { z } from 'zod'
|
|
15
|
+
import { normalizeServiceURL } from '../connection'
|
|
15
16
|
import { prImageHostSchema } from '../ontology'
|
|
16
17
|
import { SANDBOX_FORBIDDEN_ENV } from '../ports/workspace/operator-sandbox'
|
|
17
18
|
import {
|
|
@@ -60,14 +61,22 @@ function stepSection<T, C>(
|
|
|
60
61
|
label: string,
|
|
61
62
|
stepsSchema: z.ZodType<string[]>,
|
|
62
63
|
tableSchema: z.ZodType<T>,
|
|
63
|
-
compose: (steps: string[], stepConfigs: Record<string, T
|
|
64
|
+
compose: (steps: string[], stepConfigs: Record<string, T>, afterReconcile: boolean) => C,
|
|
65
|
+
allowAfterReconcile = false,
|
|
64
66
|
) {
|
|
65
67
|
return z
|
|
66
68
|
.unknown()
|
|
67
69
|
.transform((input, ctx): C => {
|
|
68
70
|
const stepConfigs: Record<string, T> = Object.create(null)
|
|
69
71
|
let steps: string[] = []
|
|
72
|
+
let afterReconcile = true
|
|
70
73
|
for (const [key, raw] of ownEntries(input, label, ctx)) {
|
|
74
|
+
if (key === 'afterReconcile' && allowAfterReconcile) {
|
|
75
|
+
const parsed = z.boolean().safeParse(raw)
|
|
76
|
+
if (parsed.success) afterReconcile = parsed.data
|
|
77
|
+
else forwardIssues(parsed.error.issues, ctx, [key])
|
|
78
|
+
continue
|
|
79
|
+
}
|
|
71
80
|
if (key === 'steps') {
|
|
72
81
|
const parsed = stepsSchema.safeParse(raw)
|
|
73
82
|
if (parsed.success) steps = parsed.data
|
|
@@ -77,7 +86,7 @@ function stepSection<T, C>(
|
|
|
77
86
|
const table = parseEntry(tableSchema, raw, key, ctx)
|
|
78
87
|
if (table.ok) defineEntry(stepConfigs, key, table.value)
|
|
79
88
|
}
|
|
80
|
-
return compose(steps, stepConfigs)
|
|
89
|
+
return compose(steps, stepConfigs, afterReconcile)
|
|
81
90
|
})
|
|
82
91
|
.prefault({})
|
|
83
92
|
}
|
|
@@ -367,6 +376,7 @@ export const verifyPathGlobSchema = z.string().superRefine((pattern, ctx) => {
|
|
|
367
376
|
})
|
|
368
377
|
|
|
369
378
|
const verifyApplicabilityShape = {
|
|
379
|
+
afterReconcile: z.boolean().optional(),
|
|
370
380
|
/** Any-match positive path selectors; absent means unconditional. */
|
|
371
381
|
paths: z
|
|
372
382
|
.array(verifyPathGlobSchema)
|
|
@@ -399,6 +409,7 @@ export const verifyStepConfigSchema = z.discriminatedUnion('kind', [
|
|
|
399
409
|
export type VerifyStepConfig = z.infer<typeof verifyStepConfigSchema>
|
|
400
410
|
|
|
401
411
|
export interface VerifyConfig {
|
|
412
|
+
afterReconcile: boolean
|
|
402
413
|
/** Step order — each entry names a `verify:<step>` phase (§15.2.1). */
|
|
403
414
|
steps: string[]
|
|
404
415
|
/** The [verify.<step>] tables, keyed by step name. */
|
|
@@ -406,8 +417,8 @@ export interface VerifyConfig {
|
|
|
406
417
|
}
|
|
407
418
|
|
|
408
419
|
/**
|
|
409
|
-
* [verify] mixes
|
|
410
|
-
* subtables, so it cannot be a strictObject;
|
|
420
|
+
* [verify] mixes known scalars (`steps`, `afterReconcile`) with per-step
|
|
421
|
+
* subtables, so it cannot be a strictObject; every other key is
|
|
411
422
|
* validated as a step table — nothing passes through loosely, and no step name
|
|
412
423
|
* is lost to the prototype chain (see `stepSection`).
|
|
413
424
|
*/
|
|
@@ -415,7 +426,8 @@ export const verifySectionSchema = stepSection(
|
|
|
415
426
|
'[verify]',
|
|
416
427
|
z.array(z.string().min(1)).default([]),
|
|
417
428
|
verifyStepConfigSchema,
|
|
418
|
-
(steps, stepConfigs): VerifyConfig => ({ steps, stepConfigs }),
|
|
429
|
+
(steps, stepConfigs, afterReconcile): VerifyConfig => ({ steps, stepConfigs, afterReconcile }),
|
|
430
|
+
true,
|
|
419
431
|
)
|
|
420
432
|
|
|
421
433
|
// ── [finalize.<step>] ────────────────────────────────────────────────────────
|
|
@@ -500,6 +512,8 @@ export const policySchema = z.strictObject({
|
|
|
500
512
|
/** Same-finding survival threshold before auto-escalate (§10, §15.4). */
|
|
501
513
|
stallRounds: z.number().int().positive().default(3),
|
|
502
514
|
maxVerifyAttempts: z.number().int().positive().default(3),
|
|
515
|
+
/** Consecutive dispatch failures before human intervention is required. */
|
|
516
|
+
maxDispatchAttempts: z.number().int().positive().default(3),
|
|
503
517
|
/** Consecutive workspace setup failures before human intervention is required. */
|
|
504
518
|
maxSetupAttempts: z.number().int().positive().default(3),
|
|
505
519
|
/** Consecutive provider lifecycle failures before recovery requires a human retry. */
|
|
@@ -573,15 +587,33 @@ export const ticketsSchema = z.strictObject({
|
|
|
573
587
|
/** Backend team key (e.g. "ENG") — required for linear and hosted. */
|
|
574
588
|
teamKey: z.string().min(1).optional(),
|
|
575
589
|
/** Workflow state claim() moves an issue to (§12); Linear and hosted. */
|
|
576
|
-
claimedState: z
|
|
590
|
+
claimedState: z
|
|
591
|
+
.string()
|
|
592
|
+
.min(1)
|
|
593
|
+
.refine((value) => value.trim().length > 0, 'state must be nonblank')
|
|
594
|
+
.optional(),
|
|
595
|
+
/** Hosted project completion state; defaults to Done. */
|
|
596
|
+
doneState: z
|
|
597
|
+
.string()
|
|
598
|
+
.min(1)
|
|
599
|
+
.refine((value) => value.trim().length > 0, 'state must be nonblank')
|
|
600
|
+
.optional(),
|
|
577
601
|
/** State create() files new tickets into. Absent = the provider's default
|
|
578
602
|
* (Linear: the team's default state, e.g. Backlog; file: Triage). */
|
|
579
|
-
createState: z
|
|
603
|
+
createState: z
|
|
604
|
+
.string()
|
|
605
|
+
.min(1)
|
|
606
|
+
.refine((value) => value.trim().length > 0, 'state must be nonblank')
|
|
607
|
+
.optional(),
|
|
580
608
|
/** State the dispatcher hands tickets back to for human triage — spec-gate
|
|
581
609
|
* bounces (§6.3), aborted builds, closed-unmerged PRs. Absent = the
|
|
582
610
|
* provider's default (Linear: Backlog; file: Triage) — resolved by
|
|
583
611
|
* defaultTriageState in src/processes/dispatcher.ts. */
|
|
584
|
-
triageState: z
|
|
612
|
+
triageState: z
|
|
613
|
+
.string()
|
|
614
|
+
.min(1)
|
|
615
|
+
.refine((value) => value.trim().length > 0, 'state must be nonblank')
|
|
616
|
+
.optional(),
|
|
585
617
|
/** State harvest files its synthesized proposals into (§12). Absent = the
|
|
586
618
|
* triage state, which keeps the constitutional grooming gate: a proposal
|
|
587
619
|
* waits for a human before it can be dispatched. Setting it to readyState
|
|
@@ -590,7 +622,11 @@ export const ticketsSchema = z.strictObject({
|
|
|
590
622
|
* does not also redirect spec-gate bounces, aborts, and closed-unmerged PRs
|
|
591
623
|
* into the ready state, where a bounce would re-dispatch itself. Resolved by
|
|
592
624
|
* defaultProposalState in src/processes/dispatcher.ts. */
|
|
593
|
-
proposalState: z
|
|
625
|
+
proposalState: z
|
|
626
|
+
.string()
|
|
627
|
+
.min(1)
|
|
628
|
+
.refine((value) => value.trim().length > 0, 'state must be nonblank')
|
|
629
|
+
.optional(),
|
|
594
630
|
/** Directory of state dirs (`triage/ ready/ doing/ done/`) holding `<id>.md`
|
|
595
631
|
* ticket files; optional — defaults to `.autobuild/tickets`, resolved
|
|
596
632
|
* relative to the repo. Kept schema-optional (not `.default()`) so the
|
|
@@ -644,7 +680,7 @@ const ORCHESTRATOR_APPROVAL_ENTRY = /^[a-z][a-z0-9_.]*(:[a-z0-9_-]+)?$/
|
|
|
644
680
|
/** The attention set a new session's wake settings inherit when the
|
|
645
681
|
* repository does not override `wake`: the build events an operator or agent
|
|
646
682
|
* must wake up for (the same set `ab watch` filters on by default) plus the
|
|
647
|
-
* repository-journal attention events (the same set `ab watch
|
|
683
|
+
* repository-journal attention events (the same set `ab watch`
|
|
648
684
|
* follows), spelled as literal globs. Both halves of the attention set can
|
|
649
685
|
* wake a session. */
|
|
650
686
|
export const defaultOrchestratorWakeGlobs: readonly string[] = [
|
|
@@ -725,11 +761,23 @@ export type OrchestratorConfig = z.infer<typeof orchestratorSchema>
|
|
|
725
761
|
|
|
726
762
|
// ── Whole file ───────────────────────────────────────────────────────────────
|
|
727
763
|
|
|
764
|
+
export const storeSchema = z.strictObject({
|
|
765
|
+
url: z.string().transform((value, ctx) => {
|
|
766
|
+
try {
|
|
767
|
+
return normalizeServiceURL(value)
|
|
768
|
+
} catch (error) {
|
|
769
|
+
ctx.addIssue({ code: 'custom', message: (error as Error).message })
|
|
770
|
+
return z.NEVER
|
|
771
|
+
}
|
|
772
|
+
}),
|
|
773
|
+
})
|
|
728
774
|
const configRootSchema = z.strictObject({
|
|
775
|
+
store: storeSchema.optional(),
|
|
729
776
|
/** Base branch builds cut from and target (§16.1). */
|
|
730
777
|
baseBranch: z.string().min(1).default('main'),
|
|
731
778
|
/** Concurrent builds for this repository (§16.1). */
|
|
732
779
|
capacity: z.number().int().positive().default(1),
|
|
780
|
+
ticketScanIntervalSeconds: z.number().int().positive().default(60),
|
|
733
781
|
/** Forge adapter selected from builtins and configured plugins. */
|
|
734
782
|
forge: z
|
|
735
783
|
.string()
|
|
@@ -769,7 +817,13 @@ const configRootSchema = z.strictObject({
|
|
|
769
817
|
})
|
|
770
818
|
|
|
771
819
|
/** Root metadata keeps strict error prose and documentation coverage honest. */
|
|
772
|
-
export const TOP_LEVEL_SCALARS = [
|
|
820
|
+
export const TOP_LEVEL_SCALARS = [
|
|
821
|
+
'baseBranch',
|
|
822
|
+
'capacity',
|
|
823
|
+
'ticketScanIntervalSeconds',
|
|
824
|
+
'forge',
|
|
825
|
+
'plugins',
|
|
826
|
+
] as const
|
|
773
827
|
export const TOP_LEVEL_TABLES = Object.keys(configRootSchema.shape).filter(
|
|
774
828
|
(key) => !(TOP_LEVEL_SCALARS as readonly string[]).includes(key),
|
|
775
829
|
)
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { normalizeGitRemoteUrl } from './kernel/origin'
|
|
2
|
+
/** Public device-approval protocol. Secrets travel only in capability POST bodies. */
|
|
3
|
+
export type ConnectionStatus = 'pending' | 'approved' | 'denied' | 'expired'
|
|
4
|
+
export interface ConnectionProposal {
|
|
5
|
+
repo: string
|
|
6
|
+
credentialId: string
|
|
7
|
+
digest: string
|
|
8
|
+
}
|
|
9
|
+
export interface ConnectionOutcome {
|
|
10
|
+
repo: string
|
|
11
|
+
credentialId: string
|
|
12
|
+
status: ConnectionStatus
|
|
13
|
+
}
|
|
14
|
+
export interface ConnectionStarted extends ConnectionOutcome {
|
|
15
|
+
id: string
|
|
16
|
+
capability: string
|
|
17
|
+
code: string
|
|
18
|
+
verificationURL: string
|
|
19
|
+
interval: number
|
|
20
|
+
expiresAt: string
|
|
21
|
+
}
|
|
22
|
+
export function normalizeServiceURL(raw: string): string {
|
|
23
|
+
let url: URL
|
|
24
|
+
try {
|
|
25
|
+
url = new URL(raw)
|
|
26
|
+
} catch {
|
|
27
|
+
throw new Error('store.url must be an absolute HTTPS service URL')
|
|
28
|
+
}
|
|
29
|
+
if (
|
|
30
|
+
!raw.trim() ||
|
|
31
|
+
raw !== raw.trim() ||
|
|
32
|
+
url.username ||
|
|
33
|
+
url.password ||
|
|
34
|
+
url.search ||
|
|
35
|
+
url.hash ||
|
|
36
|
+
!(
|
|
37
|
+
url.protocol === 'https:' ||
|
|
38
|
+
(url.protocol === 'http:' && ['localhost', '127.0.0.1', '[::1]'].includes(url.hostname))
|
|
39
|
+
)
|
|
40
|
+
) {
|
|
41
|
+
throw new Error(
|
|
42
|
+
'store.url must be HTTPS (HTTP is allowed on loopback), without credentials, query or fragment',
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
return url.href.replace(/\/+$/, '')
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function normalizeConnectionRepository(raw: string): string {
|
|
49
|
+
const value = raw.trim()
|
|
50
|
+
// SSH's conventional git user is transport syntax; URL passwords and HTTPS
|
|
51
|
+
// userinfo are never part of an identity (normalization would discard them).
|
|
52
|
+
if (/[\s\\?#]/.test(value) || !value || (/^[^/]*:[^/]*@/.test(value) && !value.includes('://')))
|
|
53
|
+
throw new Error('invalid repository identity')
|
|
54
|
+
if (value.includes('://')) {
|
|
55
|
+
let input: URL
|
|
56
|
+
try {
|
|
57
|
+
input = new URL(value)
|
|
58
|
+
} catch {
|
|
59
|
+
throw new Error('invalid repository identity')
|
|
60
|
+
}
|
|
61
|
+
if (input.password || (input.username && !['ssh:', 'git+ssh:'].includes(input.protocol))) {
|
|
62
|
+
throw new Error('repository identity must not contain credentials')
|
|
63
|
+
}
|
|
64
|
+
if (input.search || input.hash) throw new Error('invalid repository identity')
|
|
65
|
+
}
|
|
66
|
+
const normalized = normalizeGitRemoteUrl(value)
|
|
67
|
+
let url: URL
|
|
68
|
+
try {
|
|
69
|
+
url = new URL(normalized)
|
|
70
|
+
} catch {
|
|
71
|
+
throw new Error('invalid repository identity')
|
|
72
|
+
}
|
|
73
|
+
if (
|
|
74
|
+
url.protocol !== 'https:' ||
|
|
75
|
+
!url.hostname ||
|
|
76
|
+
url.username ||
|
|
77
|
+
url.password ||
|
|
78
|
+
url.search ||
|
|
79
|
+
url.hash ||
|
|
80
|
+
url.pathname === '/' ||
|
|
81
|
+
/%|\\/.test(url.pathname)
|
|
82
|
+
) {
|
|
83
|
+
throw new Error('repository identity must be a normalized HTTPS repository origin')
|
|
84
|
+
}
|
|
85
|
+
return normalized
|
|
86
|
+
}
|
|
@@ -51,6 +51,11 @@ export const allowedActorKinds: Record<EventType, readonly ActorKind[]> = {
|
|
|
51
51
|
'execution.ended': ['dispatcher'],
|
|
52
52
|
'infrastructure.failed': ['dispatcher'],
|
|
53
53
|
'infrastructure.cleanup-attempted': ['dispatcher'],
|
|
54
|
+
'pr-summary.requested': ['kernel'],
|
|
55
|
+
'pr-summary.attempted': ['kernel'],
|
|
56
|
+
'pr-summary.result': ['kernel'],
|
|
57
|
+
'pr-summary.settled': ['kernel'],
|
|
58
|
+
'finalize.step-started': ['kernel'],
|
|
54
59
|
'publication.requested': ['agent', 'kernel'],
|
|
55
60
|
// Only the dispatcher releases workspaces and settles publications (§15.7);
|
|
56
61
|
// the kernel never destroys one, so it never records a loss.
|
|
@@ -88,6 +93,7 @@ export const allowedActorKinds: Record<EventType, readonly ActorKind[]> = {
|
|
|
88
93
|
'code-review.started': ['kernel'],
|
|
89
94
|
'code-review.verdict': ['agent'],
|
|
90
95
|
|
|
96
|
+
'verify.reconcile-policy-captured': ['kernel'],
|
|
91
97
|
'verify.started': ['kernel'],
|
|
92
98
|
'verify.completed': ['kernel', 'agent'],
|
|
93
99
|
'finalize.started': ['kernel'],
|
|
@@ -177,10 +183,12 @@ export function validateEventWrite(input: {
|
|
|
177
183
|
const escalation = payloadResult.data as EventPayload<'escalation.raised'>
|
|
178
184
|
if (
|
|
179
185
|
actor.kind === 'dispatcher' &&
|
|
180
|
-
(escalation.source !== 'policy' ||
|
|
186
|
+
(escalation.source !== 'policy' ||
|
|
187
|
+
(escalation.policyCause !== 'infrastructure-failure-limit' &&
|
|
188
|
+
escalation.policyCause !== 'dispatch-failure-limit'))
|
|
181
189
|
) {
|
|
182
190
|
throw new EventValidationError(
|
|
183
|
-
'invalid dispatcher escalation: only policy/infrastructure-failure-limit may be raised outside the kernel',
|
|
191
|
+
'invalid dispatcher escalation: only policy/infrastructure-failure-limit or policy/dispatch-failure-limit may be raised outside the kernel',
|
|
184
192
|
)
|
|
185
193
|
}
|
|
186
194
|
if (escalation.source === 'policy' && escalation.policyCause === undefined) {
|
|
@@ -10,8 +10,9 @@ import { EVENT_TYPES } from './payloads'
|
|
|
10
10
|
import { REPOSITORY_EVENT_TYPES } from './repository'
|
|
11
11
|
|
|
12
12
|
export interface CompileEventGlobsOptions {
|
|
13
|
-
/** Also accept matches in the repository-journal catalog (the
|
|
14
|
-
* behavior of `ab watch
|
|
13
|
+
/** Also accept matches in the repository-journal catalog (the no-slug or
|
|
14
|
+
* explicit `--repository` behavior of `ab watch`; wait remains build-only). */
|
|
15
|
+
additionalCatalog?: readonly string[]
|
|
15
16
|
repository?: boolean
|
|
16
17
|
usage: string
|
|
17
18
|
}
|
|
@@ -35,7 +36,11 @@ export function compileEventGlobs(
|
|
|
35
36
|
const inBuildCatalog = EVENT_TYPES.some((type) => regex.test(type))
|
|
36
37
|
const inRepositoryCatalog =
|
|
37
38
|
opts.repository === true && REPOSITORY_EVENT_TYPES.some((type) => regex.test(type))
|
|
38
|
-
if (
|
|
39
|
+
if (
|
|
40
|
+
!inBuildCatalog &&
|
|
41
|
+
!inRepositoryCatalog &&
|
|
42
|
+
!opts.additionalCatalog?.some((type) => regex.test(type))
|
|
43
|
+
) {
|
|
39
44
|
throw new Error(
|
|
40
45
|
`--event "${glob}" matches no known event type${
|
|
41
46
|
opts.repository ? ' in the build or repository catalogs' : ' in the build event catalog'
|
|
@@ -172,6 +172,7 @@ const verifyCompletedPayloadSchema = z.union([
|
|
|
172
172
|
z.strictObject({
|
|
173
173
|
...verifyCompletionBase,
|
|
174
174
|
outcome: z.literal('skipped'),
|
|
175
|
+
skipCause: z.literal('post-reconcile-policy').optional(),
|
|
175
176
|
reason: z.string().trim().min(1, 'a skipped verification requires a non-blank reason'),
|
|
176
177
|
}),
|
|
177
178
|
]),
|
|
@@ -336,6 +337,26 @@ export const eventPayloadSchemas = {
|
|
|
336
337
|
})
|
|
337
338
|
.optional(),
|
|
338
339
|
}),
|
|
340
|
+
/** Optional summary publication identities and durable write budgets. */
|
|
341
|
+
'pr-summary.requested': z.strictObject({
|
|
342
|
+
occurrence: z.number().int().positive(),
|
|
343
|
+
boundary: z.number().int().positive(),
|
|
344
|
+
}),
|
|
345
|
+
'pr-summary.attempted': z.strictObject({
|
|
346
|
+
occurrence: z.number().int().positive(),
|
|
347
|
+
boundary: z.number().int().positive(),
|
|
348
|
+
pr: z.number().int().positive(),
|
|
349
|
+
}),
|
|
350
|
+
'pr-summary.result': z.strictObject({
|
|
351
|
+
attempt: z.number().int().positive(),
|
|
352
|
+
outcome: z.enum(['success', 'failure', 'rate-limited']),
|
|
353
|
+
eligibleAfter: z.string().datetime().optional(),
|
|
354
|
+
}),
|
|
355
|
+
'pr-summary.settled': z.strictObject({
|
|
356
|
+
request: z.number().int().positive(),
|
|
357
|
+
disposition: z.enum(['attempted', 'skipped', 'deferred']),
|
|
358
|
+
}),
|
|
359
|
+
'finalize.step-started': z.strictObject({ step: z.string().min(1) }),
|
|
339
360
|
/** Remote phases deposit their output and park before trusted publication.
|
|
340
361
|
* The dispatcher settles the request only after the VM session and execution
|
|
341
362
|
* lease are gone, then appends the ordinary phase completion fact. */
|
|
@@ -515,6 +536,27 @@ export const eventPayloadSchemas = {
|
|
|
515
536
|
* starts. */
|
|
516
537
|
feedback: feedbackSchema.optional(),
|
|
517
538
|
}),
|
|
539
|
+
'verify.reconcile-policy-captured': z.strictObject({
|
|
540
|
+
reconcileSeq: z.number().int().positive(),
|
|
541
|
+
steps: z
|
|
542
|
+
.array(
|
|
543
|
+
z.strictObject({
|
|
544
|
+
step: z.string().min(1),
|
|
545
|
+
enabled: z.boolean(),
|
|
546
|
+
source: z.string().min(1),
|
|
547
|
+
}),
|
|
548
|
+
)
|
|
549
|
+
.superRefine((steps, ctx) => {
|
|
550
|
+
if (new Set(steps.map((s) => s.step)).size !== steps.length)
|
|
551
|
+
ctx.addIssue({ code: 'custom', message: 'duplicate policy step' })
|
|
552
|
+
for (const s of steps)
|
|
553
|
+
if (
|
|
554
|
+
s.source !== 'verify.afterReconcile' &&
|
|
555
|
+
s.source !== `verify.${s.step}.afterReconcile`
|
|
556
|
+
)
|
|
557
|
+
ctx.addIssue({ code: 'custom', message: 'invalid policy source' })
|
|
558
|
+
}),
|
|
559
|
+
}),
|
|
518
560
|
'verify.completed': verifyCompletedPayloadSchema,
|
|
519
561
|
'finalize.started': empty,
|
|
520
562
|
'finalize.completed': z.strictObject({
|
|
@@ -630,6 +672,7 @@ export interface NormalizedVerifyCompletion {
|
|
|
630
672
|
outcome: VerifyOutcome
|
|
631
673
|
report?: ArtifactRef
|
|
632
674
|
reason?: string
|
|
675
|
+
skipCause?: 'post-reconcile-policy'
|
|
633
676
|
}
|
|
634
677
|
|
|
635
678
|
/**
|
|
@@ -653,6 +696,7 @@ export function normalizeVerifyCompletion(
|
|
|
653
696
|
attempt: payload.attempt,
|
|
654
697
|
outcome: 'skipped',
|
|
655
698
|
reason: payload.reason,
|
|
699
|
+
...(payload.skipCause !== undefined ? { skipCause: payload.skipCause } : {}),
|
|
656
700
|
}
|
|
657
701
|
}
|
|
658
702
|
return {
|
|
@@ -64,6 +64,7 @@ const tickCountersSchema = z.strictObject({
|
|
|
64
64
|
/** Added in plugin/API-era 1.4; optional so historical tick facts replay. */
|
|
65
65
|
creationWithheld: z.number().int().nonnegative().optional(),
|
|
66
66
|
dependencyBlocked: z.number().int().nonnegative(),
|
|
67
|
+
dependencyCheckFailed: z.number().int().nonnegative().optional(),
|
|
67
68
|
/** Foreign executions settled from durable facts plus provider liveness.
|
|
68
69
|
* Added with durable supervision; optional so historical tick facts replay. */
|
|
69
70
|
settled: z.number().int().nonnegative().optional(),
|
|
@@ -89,6 +90,7 @@ const tickCountersSchema = z.strictObject({
|
|
|
89
90
|
const restartRequiredConfigPathSchema = z.enum([
|
|
90
91
|
'forge',
|
|
91
92
|
'plugins',
|
|
93
|
+
'store',
|
|
92
94
|
'workspace.provider',
|
|
93
95
|
'workspace.config',
|
|
94
96
|
'tickets.source',
|
|
@@ -153,6 +155,16 @@ export const harvestEventPayloadSchemas = {
|
|
|
153
155
|
* command supervising the guest runner. Written as soon as the launch
|
|
154
156
|
* succeeds so a later process can re-observe the execution without
|
|
155
157
|
* process memory. */
|
|
158
|
+
'harvest.execution.authorized': z.strictObject({
|
|
159
|
+
execution: z.string().min(1),
|
|
160
|
+
run: z.string().min(1),
|
|
161
|
+
launch: z.enum(['started', 'resumed']),
|
|
162
|
+
leaseHolder: z.string().min(1),
|
|
163
|
+
}),
|
|
164
|
+
'harvest.execution.ended': z.strictObject({
|
|
165
|
+
execution: z.string().min(1),
|
|
166
|
+
run: z.string().min(1),
|
|
167
|
+
}),
|
|
156
168
|
'harvest.execution.started': z.strictObject({
|
|
157
169
|
execution: z.string().min(1),
|
|
158
170
|
provider: z.string().min(1),
|
|
@@ -353,14 +365,30 @@ export const orchestratorSandboxEventPayloadSchemas = {
|
|
|
353
365
|
} as const
|
|
354
366
|
|
|
355
367
|
export const dispatcherStatusEventPayloadSchemas = {
|
|
368
|
+
/** Completed ticket-source measurement, independent of dispatcher runs. */
|
|
369
|
+
'dispatcher.ticket-scan-completed': z.strictObject({
|
|
370
|
+
startedAt: z.iso.datetime(),
|
|
371
|
+
readinessFingerprint: z.string().min(1),
|
|
372
|
+
criteriaFingerprint: z.string().min(1),
|
|
373
|
+
mode: z.enum(['admission', 'observation']),
|
|
374
|
+
queued: z.number().int().nonnegative(),
|
|
375
|
+
invalidTickets: z.number().int().nonnegative(),
|
|
376
|
+
ticketDiagnostics: boundedDiagnostics,
|
|
377
|
+
dependencyBlocked: z.number().int().nonnegative(),
|
|
378
|
+
dependencyDiagnostics: boundedDiagnostics,
|
|
379
|
+
creationWithheld: z.number().int().nonnegative(),
|
|
380
|
+
creationDiagnostics: boundedDiagnostics,
|
|
381
|
+
}),
|
|
356
382
|
/** A terminal frontend follows exactly one run. The referenced artifact is
|
|
357
383
|
* the composed Config the kernel is actually using, never an on-disk guess. */
|
|
358
384
|
'dispatcher.run-started': z.strictObject({
|
|
359
385
|
run: dispatchRun,
|
|
360
386
|
pid: z.number().int().positive(),
|
|
387
|
+
host: z.string().trim().min(1).optional(),
|
|
361
388
|
effectiveConfig: artifactRefSchema,
|
|
362
389
|
roleWarnings: boundedDiagnostics,
|
|
363
390
|
}),
|
|
391
|
+
'dispatcher.stop-requested': z.strictObject({ run: dispatchRun, signal: z.string().min(1) }),
|
|
364
392
|
'dispatcher.run-stopped': z.strictObject({
|
|
365
393
|
run: dispatchRun,
|
|
366
394
|
outcome: z.enum(['normal', 'abnormal', 'forced']),
|
|
@@ -449,7 +477,32 @@ export const dispatcherSettingEventPayloadSchemas = {
|
|
|
449
477
|
'dispatcher.auto-merge-default-set': dispatcherSetting,
|
|
450
478
|
} as const
|
|
451
479
|
|
|
480
|
+
const ticketSourceCondition = {
|
|
481
|
+
source: z.string().min(1),
|
|
482
|
+
operation: z.enum(['ready-scan', 'dependency-check']),
|
|
483
|
+
message: z.string().min(1),
|
|
484
|
+
}
|
|
485
|
+
export const ticketSourceHealthEventPayloadSchemas = {
|
|
486
|
+
/** An existing listing-limit episode also encountered a limited dependency
|
|
487
|
+
* read. Strengthens recovery evidence without closing or reopening it. */
|
|
488
|
+
'dispatcher.ticket-source-dependency-limited': z.strictObject({ source: z.string().min(1) }),
|
|
489
|
+
'dispatcher.ticket-source-degraded': z.discriminatedUnion('cause', [
|
|
490
|
+
z.strictObject({
|
|
491
|
+
...ticketSourceCondition,
|
|
492
|
+
cause: z.literal('rate-limited'),
|
|
493
|
+
resetAt: z.iso.datetime(),
|
|
494
|
+
}),
|
|
495
|
+
z.strictObject({ ...ticketSourceCondition, cause: z.literal('ready-scan-failed') }),
|
|
496
|
+
]),
|
|
497
|
+
'dispatcher.ticket-source-recovered': z.strictObject({
|
|
498
|
+
source: z.string().min(1),
|
|
499
|
+
cause: z.enum(['rate-limited', 'ready-scan-failed']),
|
|
500
|
+
operation: z.enum(['ready-scan', 'dependency-check']),
|
|
501
|
+
}),
|
|
502
|
+
} as const
|
|
503
|
+
|
|
452
504
|
export const repositoryEventPayloadSchemas = {
|
|
505
|
+
...ticketSourceHealthEventPayloadSchemas,
|
|
453
506
|
...harvestEventPayloadSchemas,
|
|
454
507
|
...orchestratorSandboxEventPayloadSchemas,
|
|
455
508
|
...dispatcherStatusEventPayloadSchemas,
|
|
@@ -461,11 +514,13 @@ export const REPOSITORY_EVENT_TYPES = Object.keys(
|
|
|
461
514
|
repositoryEventPayloadSchemas,
|
|
462
515
|
) as RepositoryEventType[]
|
|
463
516
|
|
|
464
|
-
/**
|
|
465
|
-
*
|
|
517
|
+
/** Repository attention followed by `ab watch` with no slugs or explicit
|
|
518
|
+
* `--repository`, and scanned by the orchestrator wake pass. */
|
|
466
519
|
export const REPOSITORY_ATTENTION_EVENTS = [
|
|
467
520
|
'harvest.escalated',
|
|
468
521
|
'harvest.failed',
|
|
522
|
+
'dispatcher.ticket-source-degraded',
|
|
523
|
+
'dispatcher.ticket-source-recovered',
|
|
469
524
|
'dispatcher.tick-failed',
|
|
470
525
|
'dispatcher.config-rejected',
|
|
471
526
|
'dispatcher.harvest-runner-failed',
|
|
@@ -513,6 +568,8 @@ const allowedActorKinds: Record<RepositoryEventType, readonly ActorKind[]> = {
|
|
|
513
568
|
'harvest.recovery-requested': ['kernel'],
|
|
514
569
|
'harvest.recovery-exhausted': ['kernel'],
|
|
515
570
|
'harvest.started': ['dispatcher', 'kernel'],
|
|
571
|
+
'harvest.execution.authorized': ['dispatcher'],
|
|
572
|
+
'harvest.execution.ended': ['dispatcher'],
|
|
516
573
|
'harvest.execution.started': ['dispatcher'],
|
|
517
574
|
'harvest.execution.released': ['dispatcher'],
|
|
518
575
|
'harvest.step.started': ['kernel'],
|
|
@@ -539,12 +596,17 @@ const allowedActorKinds: Record<RepositoryEventType, readonly ActorKind[]> = {
|
|
|
539
596
|
'orchestrator.sandbox.published': ['human'],
|
|
540
597
|
'orchestrator.sandbox.publish-failed': ['human'],
|
|
541
598
|
'dispatcher.run-started': ['dispatcher'],
|
|
599
|
+
'dispatcher.stop-requested': ['dispatcher'],
|
|
542
600
|
'dispatcher.run-stopped': ['dispatcher'],
|
|
543
601
|
'dispatcher.tick-yielded': ['dispatcher'],
|
|
544
602
|
'dispatcher.config-rejected': ['dispatcher'],
|
|
545
603
|
'dispatcher.config-publication-failed': ['dispatcher'],
|
|
546
604
|
'dispatcher.tick-started': ['dispatcher'],
|
|
547
605
|
'dispatcher.tick-completed': ['dispatcher'],
|
|
606
|
+
'dispatcher.ticket-scan-completed': ['dispatcher'],
|
|
607
|
+
'dispatcher.ticket-source-dependency-limited': ['dispatcher'],
|
|
608
|
+
'dispatcher.ticket-source-degraded': ['dispatcher'],
|
|
609
|
+
'dispatcher.ticket-source-recovered': ['dispatcher'],
|
|
548
610
|
'dispatcher.tick-failed': ['dispatcher'],
|
|
549
611
|
'dispatcher.runner-settled': ['dispatcher'],
|
|
550
612
|
'dispatcher.harvest-runner-failed': ['dispatcher'],
|
|
@@ -124,6 +124,7 @@ export function readyTicket(id: string, over: Partial<Omit<Ticket, 'ref'>> = {})
|
|
|
124
124
|
title,
|
|
125
125
|
body: over.body ?? CONFORMING_BODY,
|
|
126
126
|
state: over.state ?? 'Ready',
|
|
127
|
+
resolved: false,
|
|
127
128
|
labels: over.labels ?? ['autobuild'],
|
|
128
129
|
}
|
|
129
130
|
}
|
|
@@ -369,7 +370,7 @@ export async function assertSnapshotEquivalence(store: BuildStore, repo: string)
|
|
|
369
370
|
dashboard: await attempt(() =>
|
|
370
371
|
getOperatorDashboard({ store: target, repo, clock: () => at }),
|
|
371
372
|
),
|
|
372
|
-
status: await attempt(() => getRepositoryStatus(target, repo)),
|
|
373
|
+
status: await attempt(() => getRepositoryStatus(target, repo, at)),
|
|
373
374
|
harvest: await attempt(() => getHarvestStatus(target, repo)),
|
|
374
375
|
lists: await Promise.all(
|
|
375
376
|
(['active', 'queued', 'all'] as const).map((scope) =>
|