@defrex/autobuild 0.12.0 → 0.14.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 (151) hide show
  1. package/README.md +1 -1
  2. package/SPEC.md +138 -16
  3. package/bin/ab-dispatch-kernel.ts +4 -6
  4. package/docs/architecture.md +155 -73
  5. package/docs/configuration.md +41 -9
  6. package/docs/hosted-service.md +31 -5
  7. package/docs/operator-api.md +8 -1
  8. package/docs/remote-store-protocol.md +94 -2
  9. package/docs/remote-ticket-protocol.md +151 -9
  10. package/docs/setup.md +33 -7
  11. package/docs/ticket-migration.md +64 -0
  12. package/package.json +4 -3
  13. package/packages/core/src/cli/attention.ts +18 -164
  14. package/packages/core/src/cli/build-control.ts +23 -16
  15. package/packages/core/src/cli/bulk-control.ts +3 -2
  16. package/packages/core/src/cli/connect.ts +141 -38
  17. package/packages/core/src/cli/context.ts +23 -79
  18. package/packages/core/src/cli/dashboard/model.ts +36 -22
  19. package/packages/core/src/cli/dashboard/poll.ts +1 -1
  20. package/packages/core/src/cli/dispatch-frontend.ts +24 -67
  21. package/packages/core/src/cli/dispatch-process.ts +14 -7
  22. package/packages/core/src/cli/dispatch-repository-feed.ts +196 -0
  23. package/packages/core/src/cli/dispatch.ts +304 -82
  24. package/packages/core/src/cli/dispatcher-liveness.ts +26 -19
  25. package/packages/core/src/cli/harvest.ts +84 -42
  26. package/packages/core/src/cli/help.ts +14 -8
  27. package/packages/core/src/cli/historical-build-state.ts +27 -0
  28. package/packages/core/src/cli/installation.ts +67 -13
  29. package/packages/core/src/cli/main.ts +23 -5
  30. package/packages/core/src/cli/mcp.ts +2 -1
  31. package/packages/core/src/cli/pr-attachments.ts +10 -13
  32. package/packages/core/src/cli/pr-summary-publication.ts +27 -60
  33. package/packages/core/src/cli/pr-summary.ts +43 -13
  34. package/packages/core/src/cli/publication-settlement.ts +10 -20
  35. package/packages/core/src/cli/repository-status.ts +38 -2
  36. package/packages/core/src/cli/self-update.ts +5 -1
  37. package/packages/core/src/cli/status.ts +62 -32
  38. package/packages/core/src/cli/terminals.ts +25 -142
  39. package/packages/core/src/cli/ticket-assets.ts +7 -2
  40. package/packages/core/src/cli/ticket-editor.ts +82 -0
  41. package/packages/core/src/cli/ticket.ts +166 -36
  42. package/packages/core/src/cli/wait.ts +91 -84
  43. package/packages/core/src/cli/watch.ts +60 -97
  44. package/packages/core/src/config/pipeline-source.ts +4 -3
  45. package/packages/core/src/config/schema.ts +35 -3
  46. package/packages/core/src/connection.ts +3 -3
  47. package/packages/core/src/events/catalog.ts +3 -2
  48. package/packages/core/src/events/payloads.ts +6 -0
  49. package/packages/core/src/events/repository.ts +7 -3
  50. package/packages/core/src/integration/harness.ts +38 -4
  51. package/packages/core/src/kernel/__snapshots__/reducer-registry.test.ts.snap +2444 -9
  52. package/packages/core/src/kernel/auto-merge.ts +19 -3
  53. package/packages/core/src/kernel/dispatcher-liveness.ts +10 -4
  54. package/packages/core/src/kernel/generators/build-log.ts +1 -1
  55. package/packages/core/src/kernel/generators/repository-journals.ts +14 -3
  56. package/packages/core/src/kernel/harvest.ts +25 -23
  57. package/packages/core/src/kernel/incremental-fixtures.ts +13 -2
  58. package/packages/core/src/kernel/log-index-value.ts +38 -0
  59. package/packages/core/src/kernel/read-facts.ts +637 -0
  60. package/packages/core/src/kernel/reducer-registry.ts +139 -3
  61. package/packages/core/src/kernel/repository-attention.ts +179 -0
  62. package/packages/core/src/kernel/service-facts.ts +135 -0
  63. package/packages/core/src/kernel/terminal-guard.ts +133 -0
  64. package/packages/core/src/ontology.ts +1 -0
  65. package/packages/core/src/operator/annotations.ts +12 -0
  66. package/packages/core/src/operator/control.ts +5 -6
  67. package/packages/core/src/operator/index.ts +7 -0
  68. package/packages/core/src/operator/protocol.ts +19 -2
  69. package/packages/core/src/operator/query.ts +37 -20
  70. package/packages/core/src/operator/read-view.ts +4 -13
  71. package/packages/core/src/operator/registry.ts +61 -8
  72. package/packages/core/src/operator/requests.ts +2 -14
  73. package/packages/core/src/operator/sandbox.ts +10 -9
  74. package/packages/core/src/operator/ticket-source-backend.ts +17 -21
  75. package/packages/core/src/operator/tickets.ts +132 -22
  76. package/packages/core/src/plugin-sdk/index.ts +19 -0
  77. package/packages/core/src/plugins/runtimes.ts +5 -1
  78. package/packages/core/src/ports/runner/session-env.ts +2 -0
  79. package/packages/core/src/ports/tickets/contract.ts +66 -1
  80. package/packages/core/src/ports/tickets/create.ts +48 -12
  81. package/packages/core/src/ports/tickets/fake.ts +30 -1
  82. package/packages/core/src/ports/tickets/file-assets.ts +159 -0
  83. package/packages/core/src/ports/tickets/file-codec.ts +90 -0
  84. package/packages/core/src/ports/tickets/file.ts +114 -120
  85. package/packages/core/src/ports/tickets/grants.ts +47 -0
  86. package/packages/core/src/ports/tickets/hosted-index.ts +17 -0
  87. package/packages/core/src/ports/tickets/identity.ts +20 -0
  88. package/packages/core/src/ports/tickets/lifecycle.ts +18 -0
  89. package/packages/core/src/ports/tickets/linear.ts +74 -9
  90. package/packages/core/src/ports/tickets/migration.ts +336 -0
  91. package/packages/core/src/ports/tickets/operations.ts +38 -7
  92. package/packages/core/src/ports/tickets/portable-assets.ts +54 -0
  93. package/packages/core/src/ports/tickets/remote-protocol.ts +95 -0
  94. package/packages/core/src/ports/tickets/remote.ts +60 -3
  95. package/packages/core/src/ports/tickets/requested-id.ts +46 -0
  96. package/packages/core/src/ports/tickets/revision.ts +62 -0
  97. package/packages/core/src/ports/tickets/states.ts +61 -0
  98. package/packages/core/src/ports/types.ts +40 -3
  99. package/packages/core/src/ports/workspace/build-execution.ts +1 -0
  100. package/packages/core/src/ports/workspace/create.ts +5 -1
  101. package/packages/core/src/ports/workspace/operator-sandbox.ts +2 -0
  102. package/packages/core/src/ports/workspace/vercel-sandbox.ts +8 -4
  103. package/packages/core/src/processes/build-child.ts +6 -6
  104. package/packages/core/src/processes/build-runner.ts +62 -70
  105. package/packages/core/src/processes/dispatch-failure-budget.ts +5 -4
  106. package/packages/core/src/processes/dispatcher.ts +223 -205
  107. package/packages/core/src/processes/execution-no-progress.ts +176 -0
  108. package/packages/core/src/processes/harvest-child.ts +30 -4
  109. package/packages/core/src/processes/harvest-execution-state.ts +15 -9
  110. package/packages/core/src/processes/harvest-runner.ts +58 -74
  111. package/packages/core/src/processes/harvest.ts +72 -29
  112. package/packages/core/src/processes/infrastructure-failure-budget.ts +13 -15
  113. package/packages/core/src/processes/orchestrator-tick.ts +2 -2
  114. package/packages/core/src/processes/phase-failure-budget.ts +1 -0
  115. package/packages/core/src/processes/publication-loss.ts +35 -21
  116. package/packages/core/src/processes/repo-view.ts +229 -1099
  117. package/packages/core/src/processes/tick-publication.ts +110 -0
  118. package/packages/core/src/store/adapter.ts +23 -0
  119. package/packages/core/src/store/build-scope.ts +17 -0
  120. package/packages/core/src/store/contract.ts +283 -1
  121. package/packages/core/src/store/local/schema.ts +1 -0
  122. package/packages/core/src/store/local/store.ts +180 -55
  123. package/packages/core/src/store/memory.ts +119 -29
  124. package/packages/core/src/store/phase-session.ts +17 -0
  125. package/packages/core/src/store/reduced-reads.ts +239 -0
  126. package/packages/core/src/store/reducer-value-shapes.json +14056 -0
  127. package/packages/core/src/store/reducer-value-validation.ts +96 -0
  128. package/packages/core/src/store/reducer-values.ts +145 -0
  129. package/packages/core/src/store/remote/client.ts +41 -0
  130. package/packages/core/src/store/remote/compatibility.ts +2 -0
  131. package/packages/core/src/store/remote/index.ts +19 -0
  132. package/packages/core/src/store/remote/protocol.ts +16 -0
  133. package/packages/core/src/store/remote/version.ts +3 -1
  134. package/packages/core/src/store/repo-state-events.ts +1 -1
  135. package/packages/core/src/store/repo-tick.ts +72 -0
  136. package/packages/core/src/store/session-handle.ts +19 -0
  137. package/packages/core/src/store/snapshots.ts +10 -0
  138. package/packages/core/src/store/ticket-asset-snapshots.ts +192 -0
  139. package/packages/core/src/store/ticket-assets.ts +86 -8
  140. package/packages/core/src/store/types.ts +40 -3
  141. package/packages/core/src/testing/dispatch-read-budget.ts +215 -0
  142. package/packages/core/src/testing/index.ts +8 -0
  143. package/packages/core/src/testing/reduced-read-budget.ts +462 -0
  144. package/skills/guide/SKILL.md +97 -12
  145. package/skills/guide/references/operator-api.md +8 -1
  146. package/skills/guide/references/remote-store-protocol.md +94 -2
  147. package/skills/guide/references/remote-ticket-protocol.md +151 -9
  148. package/skills/guide/references/setup.md +33 -7
  149. package/skills/guide/references/ticket-migration.md +64 -0
  150. package/skills/tickets/SKILL.md +71 -0
  151. package/templates/hosted-service/package.json +2 -2
@@ -535,7 +535,8 @@ export type PolicyConfig = z.infer<typeof policySchema>
535
535
  // API key comes from the LINEAR_API_KEY environment variable, never from this
536
536
  // file.
537
537
 
538
- export const ticketsSchema = z.strictObject({
538
+ export const ticketsInputSchema = z.strictObject({
539
+ url: z.string().optional(),
539
540
  /** Builtin or plugin-registered TicketSource name. Registry membership is
540
541
  * validated after configured plugins load. */
541
542
  source: z
@@ -569,7 +570,8 @@ export const ticketsSchema = z.strictObject({
569
570
  .refine(
570
571
  (s) => s.trim().length > 0,
571
572
  '[tickets].readyState must not be blank — name the one workflow state a ticket must sit in to be dispatched (e.g. "ready").',
572
- ),
573
+ )
574
+ .optional(),
573
575
  /**
574
576
  * Narrows readiness to tickets assigned to one Linear user — a third
575
577
  * conjunctive gate beside readyState and readyLabels. `"me"` is the owner of
@@ -592,7 +594,7 @@ export const ticketsSchema = z.strictObject({
592
594
  .min(1)
593
595
  .refine((value) => value.trim().length > 0, 'state must be nonblank')
594
596
  .optional(),
595
- /** Hosted project completion state; defaults to Done. */
597
+ /** Hosted project completion state; defaults to done. */
596
598
  doneState: z
597
599
  .string()
598
600
  .min(1)
@@ -634,6 +636,36 @@ export const ticketsSchema = z.strictObject({
634
636
  * still tell a defaulted dir from an explicit one. */
635
637
  dir: z.string().min(1).optional(),
636
638
  })
639
+ export const ticketsSchema = ticketsInputSchema
640
+ .superRefine((tickets, ctx) => {
641
+ if (tickets.source === 'hosted' && tickets.url !== undefined) {
642
+ try {
643
+ normalizeServiceURL(tickets.url, 'tickets.url')
644
+ } catch (error) {
645
+ ctx.addIssue({ code: 'custom', path: ['url'], message: (error as Error).message })
646
+ }
647
+ }
648
+ if (tickets.source !== 'hosted' && tickets.readyState === undefined)
649
+ ctx.addIssue({
650
+ code: 'custom',
651
+ path: ['readyState'],
652
+ message:
653
+ '[tickets].readyState is required — name the one workflow state a ticket must sit in to be dispatched (e.g. "ready").',
654
+ })
655
+ if (['file', 'linear'].includes(tickets.source) && tickets.url !== undefined)
656
+ ctx.addIssue({
657
+ code: 'custom',
658
+ path: ['url'],
659
+ message: '[tickets].url applies only to source = "hosted"',
660
+ })
661
+ })
662
+ .transform((tickets) => ({
663
+ ...tickets,
664
+ readyState: tickets.readyState ?? 'ready',
665
+ ...(tickets.source === 'hosted' && tickets.url !== undefined
666
+ ? { url: normalizeServiceURL(tickets.url, 'tickets.url') }
667
+ : {}),
668
+ }))
637
669
  export type TicketsConfig = z.infer<typeof ticketsSchema>
638
670
 
639
671
  // ── [orchestrator] ──────────────────────────────────────────────────────────
@@ -19,12 +19,12 @@ export interface ConnectionStarted extends ConnectionOutcome {
19
19
  interval: number
20
20
  expiresAt: string
21
21
  }
22
- export function normalizeServiceURL(raw: string): string {
22
+ export function normalizeServiceURL(raw: string, setting = 'store.url'): string {
23
23
  let url: URL
24
24
  try {
25
25
  url = new URL(raw)
26
26
  } catch {
27
- throw new Error('store.url must be an absolute HTTPS service URL')
27
+ throw new Error(`${setting} must be an absolute HTTPS service URL`)
28
28
  }
29
29
  if (
30
30
  !raw.trim() ||
@@ -39,7 +39,7 @@ export function normalizeServiceURL(raw: string): string {
39
39
  )
40
40
  ) {
41
41
  throw new Error(
42
- 'store.url must be HTTPS (HTTP is allowed on loopback), without credentials, query or fragment',
42
+ `${setting} must be HTTPS (HTTP is allowed on loopback), without credentials, query or fragment`,
43
43
  )
44
44
  }
45
45
  return url.href.replace(/\/+$/, '')
@@ -185,10 +185,11 @@ export function validateEventWrite(input: {
185
185
  actor.kind === 'dispatcher' &&
186
186
  (escalation.source !== 'policy' ||
187
187
  (escalation.policyCause !== 'infrastructure-failure-limit' &&
188
- escalation.policyCause !== 'dispatch-failure-limit'))
188
+ escalation.policyCause !== 'dispatch-failure-limit' &&
189
+ escalation.policyCause !== 'execution-no-progress'))
189
190
  ) {
190
191
  throw new EventValidationError(
191
- 'invalid dispatcher escalation: only policy/infrastructure-failure-limit or policy/dispatch-failure-limit may be raised outside the kernel',
192
+ 'invalid dispatcher escalation: only policy/infrastructure-failure-limit or policy/dispatch-failure-limit or policy/execution-no-progress may be raised outside the kernel',
192
193
  )
193
194
  }
194
195
  if (escalation.source === 'policy' && escalation.policyCause === undefined) {
@@ -196,6 +196,12 @@ const finalizeStepCompletedPayloadSchema = z.discriminatedUnion('ok', [
196
196
  /** One ticket asset revision a build froze (SPEC §6.3): the identity and
197
197
  * shape of what `listTicketAssets` returned at the pinning moment. */
198
198
  export const pinnedAssetSchema = z.strictObject({
199
+ snapshot: z
200
+ .strictObject({
201
+ kind: z.string().regex(/^ticket-snapshot-[a-f0-9-]+$/),
202
+ revision: z.number().int().nonnegative(),
203
+ })
204
+ .optional(),
199
205
  kind: z.string().min(1),
200
206
  name: z.string().min(1),
201
207
  revision: z.number().int().nonnegative(),
@@ -411,9 +411,12 @@ export const dispatcherStatusEventPayloadSchemas = {
411
411
  run: dispatchRun,
412
412
  error: z.string().min(1),
413
413
  }),
414
- /** Brackets the one unsafe-to-force dispatcher turn. A supervisor may kill
415
- * an unresponsive child only when no started turn remains open. */
416
- 'dispatcher.tick-started': z.strictObject({ run: dispatchRun }),
414
+ /** Recorded boundaries of meaningful turns. The repository tick observation
415
+ * protects active work from forced stop while these facts are buffered. */
416
+ 'dispatcher.tick-started': z.strictObject({
417
+ run: dispatchRun,
418
+ startedAt: z.iso.datetime().optional(),
419
+ }),
417
420
  'dispatcher.tick-completed': z.strictObject({
418
421
  run: dispatchRun,
419
422
  queued: z.number().int().nonnegative(),
@@ -421,6 +424,7 @@ export const dispatcherStatusEventPayloadSchemas = {
421
424
  * dispatcher child transported a display sample in its tick fact. */
422
425
  observations: z.number().int().nonnegative().optional(),
423
426
  counters: tickCountersSchema,
427
+ blockedDiagnostics: boundedDiagnostics.optional(),
424
428
  janitorDiagnostics: boundedDiagnostics,
425
429
  ticketDiagnostics: boundedDiagnostics,
426
430
  creationDiagnostics: boundedDiagnostics.optional(),
@@ -57,7 +57,13 @@ import type {
57
57
  import { GitWorktreeProvider, spawnExec } from '../ports/workspace/git-worktree'
58
58
  import { InProcessBuildExecution } from '../ports/workspace/in-process-build-execution'
59
59
  import { BuildRunner, LeaseHeldError, SetupFailureError } from '../processes/build-runner'
60
- import { diagnosticArtifact } from '../processes/build-execution-state'
60
+ import {
61
+ diagnosticArtifact,
62
+ BUILD_EFFECTIVE_CONFIG_ARTIFACT,
63
+ effectiveBuildConfigContent,
64
+ parseEffectiveBuildConfig,
65
+ } from '../processes/build-execution-state'
66
+ import { composeBuildConfig } from '../config/live'
61
67
  import { Dispatcher, type LaunchRunnerResult } from '../processes/dispatcher'
62
68
  import {
63
69
  getHarvestStatus,
@@ -120,6 +126,7 @@ export const CONFORMING_BODY = [
120
126
  export function readyTicket(id: string, over: Partial<Omit<Ticket, 'ref'>> = {}): Ticket {
121
127
  const title = over.title ?? 'Add rate limiting'
122
128
  return {
129
+ revision: 'fixture',
123
130
  ref: { source: 'fake', id, title },
124
131
  title,
125
132
  body: over.body ?? CONFORMING_BODY,
@@ -330,7 +337,7 @@ async function repositoryReading(store: BuildStore, repo: string) {
330
337
  builds[slug] = { facts: await plainFacts(view, slug), state: await view.buildState(slug) }
331
338
  }
332
339
  return {
333
- journal: view.recordedJournal(),
340
+ journal: (await store.getRepo(repo)) === null ? [] : await store.getRepoStateEvents(repo),
334
341
  digests: [...(await view.getRepoBuildDigests(repo)).entries()].sort(([a], [b]) =>
335
342
  a.localeCompare(b),
336
343
  ),
@@ -353,7 +360,6 @@ export async function assertSnapshotEquivalence(store: BuildStore, repo: string)
353
360
  if (canonical(got) !== canonical(want)) {
354
361
  throw new Error(`snapshot-restored reading differs from full replay (${round})`)
355
362
  }
356
- await restoredView.persistSnapshots({ force: true })
357
363
  }
358
364
  // The operator routes derive from the same view: each response with
359
365
  // snapshots present equals the response with every snapshot deleted.
@@ -398,6 +404,10 @@ export async function makeHarness(opts: {
398
404
  * CONFIG_TOML, so every existing scenario is untouched; a scenario proving
399
405
  * two-axis routing (§9) supplies its own `[roles.default]`/phase roles. */
400
406
  configToml?: string
407
+ /** Force a routing disagreement while retaining the runner's branch config. */
408
+ dispatcherConfig?: (config: Config) => Config
409
+ pinnedConfig?: boolean
410
+ getConfig?: () => Config
401
411
  /** Forge gate existence is independent from current PR mergeability. */
402
412
  gatePresence?: 'present' | 'absent'
403
413
  /** Optional adapter-level runtime composition for focused integration tests.
@@ -602,6 +612,20 @@ export async function makeHarness(opts: {
602
612
  runner: new BuildRunner({
603
613
  store: activeStore.scopeBuild(slug),
604
614
  config,
615
+ ...(opts.pinnedConfig
616
+ ? {
617
+ getConfig: async () => {
618
+ const artifact = await activeStore.getArtifact(
619
+ slug,
620
+ BUILD_EFFECTIVE_CONFIG_ARTIFACT,
621
+ )
622
+ return composeBuildConfig(
623
+ artifact === null ? config : parseEffectiveBuildConfig(artifact),
624
+ opts.getConfig?.() ?? config,
625
+ )
626
+ },
627
+ }
628
+ : {}),
605
629
  // Two-axis registry (§9): every runtime is backed by the SAME scripted
606
630
  // runner instance, so the `s_1…s_N` session numbering scenarios rely on
607
631
  // is preserved regardless of which runtime a role selects. `scripted`
@@ -629,6 +653,15 @@ export async function makeHarness(opts: {
629
653
  }
630
654
  }
631
655
  const launchRunner = async (slug: string): Promise<LaunchRunnerResult> => {
656
+ if (
657
+ opts.pinnedConfig &&
658
+ (await activeStore.getArtifact(slug, BUILD_EFFECTIVE_CONFIG_ARTIFACT)) === null
659
+ ) {
660
+ await activeStore.putArtifact(slug, {
661
+ kind: BUILD_EFFECTIVE_CONFIG_ARTIFACT,
662
+ content: effectiveBuildConfigContent(config),
663
+ })
664
+ }
632
665
  launched.push({ slug, ...(await makeRunner(slug)) })
633
666
  return 'scheduled'
634
667
  }
@@ -638,10 +671,11 @@ export async function makeHarness(opts: {
638
671
  tickets: ticketSource,
639
672
  workspaces,
640
673
  forge: selectedForge,
641
- config,
674
+ config: opts.dispatcherConfig?.(structuredClone(config)) ?? config,
642
675
  repo: origin,
643
676
  exec: spawnExec,
644
677
  launchRunner,
678
+ ...(opts.getConfig ? { getConfig: opts.getConfig } : {}),
645
679
  ids,
646
680
  clock,
647
681
  })