@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.
Files changed (120) hide show
  1. package/README.md +3 -1
  2. package/SPEC.md +237 -23
  3. package/bin/ab-dispatch-kernel.ts +1 -1
  4. package/docs/architecture.md +66 -4
  5. package/docs/configuration.md +137 -19
  6. package/docs/hosted-dispatcher.md +17 -2
  7. package/docs/hosted-service.md +214 -0
  8. package/docs/mcp.md +8 -2
  9. package/docs/operator-api.md +107 -7
  10. package/docs/remote-store-protocol.md +189 -144
  11. package/docs/remote-ticket-protocol.md +111 -18
  12. package/docs/setup.md +173 -16
  13. package/docs/ticket-dependencies.md +1 -1
  14. package/package.json +7 -6
  15. package/packages/core/src/cli/attention.ts +209 -0
  16. package/packages/core/src/cli/binary.ts +3 -3
  17. package/packages/core/src/cli/configured-store.ts +17 -0
  18. package/packages/core/src/cli/connect.ts +261 -0
  19. package/packages/core/src/cli/dashboard/model.ts +8 -5
  20. package/packages/core/src/cli/dashboard/render.ts +37 -7
  21. package/packages/core/src/cli/dispatch-frontend.ts +16 -1
  22. package/packages/core/src/cli/dispatch-process.ts +25 -7
  23. package/packages/core/src/cli/dispatch.ts +247 -47
  24. package/packages/core/src/cli/dispatcher-liveness.ts +91 -0
  25. package/packages/core/src/cli/help.ts +45 -13
  26. package/packages/core/src/cli/init-validation.ts +27 -2
  27. package/packages/core/src/cli/main.ts +41 -15
  28. package/packages/core/src/cli/mcp.ts +7 -1
  29. package/packages/core/src/cli/pr-summary-publication.ts +220 -0
  30. package/packages/core/src/cli/pr-summary.ts +2 -0
  31. package/packages/core/src/cli/project-credentials.ts +132 -0
  32. package/packages/core/src/cli/repo-state.ts +15 -2
  33. package/packages/core/src/cli/repository-status.ts +45 -5
  34. package/packages/core/src/cli/self-update.ts +10 -0
  35. package/packages/core/src/cli/store-opening.ts +22 -14
  36. package/packages/core/src/cli/terminals.ts +0 -10
  37. package/packages/core/src/cli/ticket.ts +8 -1
  38. package/packages/core/src/cli/upgrade-compatibility.ts +80 -0
  39. package/packages/core/src/cli/wait.ts +84 -17
  40. package/packages/core/src/cli/watch.ts +350 -68
  41. package/packages/core/src/config/live.ts +4 -0
  42. package/packages/core/src/config/schema.ts +65 -11
  43. package/packages/core/src/connection.ts +86 -0
  44. package/packages/core/src/events/catalog.ts +10 -2
  45. package/packages/core/src/events/globs.ts +8 -3
  46. package/packages/core/src/events/payloads.ts +44 -0
  47. package/packages/core/src/events/repository.ts +64 -2
  48. package/packages/core/src/integration/harness.ts +2 -1
  49. package/packages/core/src/kernel/__snapshots__/reducer-registry.test.ts.snap +148 -83
  50. package/packages/core/src/kernel/dispatch-status.ts +9 -1
  51. package/packages/core/src/kernel/dispatcher-liveness.ts +70 -0
  52. package/packages/core/src/kernel/engine.ts +24 -0
  53. package/packages/core/src/kernel/generators/build-log.ts +10 -0
  54. package/packages/core/src/kernel/log-index.ts +17 -0
  55. package/packages/core/src/kernel/pr-summary.ts +89 -0
  56. package/packages/core/src/kernel/reducer-registry.ts +12 -0
  57. package/packages/core/src/kernel/reducer.ts +8 -0
  58. package/packages/core/src/kernel/ticket-source-health.ts +30 -0
  59. package/packages/core/src/ontology.ts +1 -0
  60. package/packages/core/src/operator/index.ts +7 -0
  61. package/packages/core/src/operator/query.ts +44 -19
  62. package/packages/core/src/operator/ticket-source-backend.ts +1 -0
  63. package/packages/core/src/operator/tickets.ts +6 -8
  64. package/packages/core/src/plugin-sdk/index.ts +1 -0
  65. package/packages/core/src/plugins/manifest.ts +5 -3
  66. package/packages/core/src/plugins/runtimes.ts +3 -1
  67. package/packages/core/src/ports/forge/fake.ts +13 -1
  68. package/packages/core/src/ports/forge/github-transport.ts +36 -1
  69. package/packages/core/src/ports/forge/github.ts +29 -5
  70. package/packages/core/src/ports/forge/local-git.ts +12 -4
  71. package/packages/core/src/ports/forge/summary-marker.ts +8 -0
  72. package/packages/core/src/ports/runner/session-env.ts +1 -0
  73. package/packages/core/src/ports/tickets/contract.ts +25 -0
  74. package/packages/core/src/ports/tickets/create.ts +11 -3
  75. package/packages/core/src/ports/tickets/fake.ts +10 -8
  76. package/packages/core/src/ports/tickets/file.ts +1 -0
  77. package/packages/core/src/ports/tickets/hosted-index.ts +0 -3
  78. package/packages/core/src/ports/tickets/linear.ts +241 -49
  79. package/packages/core/src/ports/tickets/remote-protocol.ts +3 -0
  80. package/packages/core/src/ports/tickets/remote.ts +1 -0
  81. package/packages/core/src/ports/types.ts +17 -0
  82. package/packages/core/src/ports/workspace/build-execution.ts +5 -1
  83. package/packages/core/src/ports/workspace/create.ts +11 -2
  84. package/packages/core/src/ports/workspace/distribution-archive.ts +3 -5
  85. package/packages/core/src/ports/workspace/harvest-execution.ts +3 -1
  86. package/packages/core/src/ports/workspace/local-build-execution.ts +8 -1
  87. package/packages/core/src/ports/workspace/vercel-sandbox.ts +47 -19
  88. package/packages/core/src/processes/build-execution-state.ts +1 -1
  89. package/packages/core/src/processes/build-runner.ts +87 -26
  90. package/packages/core/src/processes/dispatch-failure-budget.ts +83 -0
  91. package/packages/core/src/processes/dispatcher.ts +336 -83
  92. package/packages/core/src/processes/harvest-child.ts +1 -0
  93. package/packages/core/src/processes/harvest-execution-state.ts +19 -2
  94. package/packages/core/src/processes/harvest-runner.ts +142 -100
  95. package/packages/core/src/processes/phase-failure-budget.ts +1 -0
  96. package/packages/core/src/processes/publication-loss.ts +18 -1
  97. package/packages/core/src/processes/repo-view.ts +2 -1
  98. package/packages/core/src/store/adapter.ts +1 -0
  99. package/packages/core/src/store/contract.ts +109 -0
  100. package/packages/core/src/store/digest.ts +14 -2
  101. package/packages/core/src/store/local/store.ts +18 -5
  102. package/packages/core/src/store/remote/client.ts +1 -1
  103. package/packages/core/src/store/remote/compatibility.ts +90 -0
  104. package/packages/core/src/store/remote/credentials.ts +28 -0
  105. package/packages/core/src/store/remote/index.ts +14 -1
  106. package/packages/core/src/store/remote/protocol.ts +26 -0
  107. package/packages/core/src/store/remote/token.ts +74 -13
  108. package/packages/core/src/store/repo-state-events.ts +49 -59
  109. package/packages/core/src/store/types.ts +5 -1
  110. package/packages/core/src/testing/index.ts +7 -0
  111. package/skills/guide/SKILL.md +175 -26
  112. package/skills/guide/references/operator-api.md +135 -13
  113. package/skills/guide/references/plugin-authoring.md +10 -2
  114. package/skills/guide/references/remote-store-protocol.md +189 -144
  115. package/skills/guide/references/remote-ticket-protocol.md +111 -18
  116. package/skills/guide/references/setup.md +173 -16
  117. package/skills/guide/references/ticket-dependencies.md +1 -1
  118. package/skills/operate/SKILL.md +159 -34
  119. package/templates/hosted-service/package.json +13 -0
  120. 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>) => C,
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 one known key (`steps`) with an open set of per-step
410
- * subtables, so it cannot be a strictObject; instead every non-`steps` key is
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.string().min(1).optional(),
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.string().min(1).optional(),
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.string().min(1).optional(),
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.string().min(1).optional(),
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 --repository`
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 = ['baseBranch', 'capacity', 'forge', 'plugins'] as const
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' || escalation.policyCause !== 'infrastructure-failure-limit')
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 `--repository`
14
- * behavior of `ab watch`/`ab wait`). */
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 (!inBuildCatalog && !inRepositoryCatalog) {
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
- /** The repository-journal attention set, followed only under `--repository`
465
- * by `ab watch` and scanned by the orchestrator wake pass. */
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) =>