@defrex/autobuild 0.8.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (182) hide show
  1. package/README.md +1 -1
  2. package/SPEC.md +323 -26
  3. package/bin/ab-build-runner.ts +8 -2
  4. package/docs/architecture.md +166 -14
  5. package/docs/assets/headline-wide.png +0 -0
  6. package/docs/configuration.md +175 -28
  7. package/docs/hosted-dispatcher.md +69 -13
  8. package/docs/operator-api.md +2 -2
  9. package/docs/remote-store-protocol.md +204 -16
  10. package/docs/remote-ticket-protocol.md +1 -1
  11. package/docs/setup.md +12 -9
  12. package/docs/website.md +14 -0
  13. package/package.json +45 -23
  14. package/packages/core/src/cli/artifact.ts +130 -6
  15. package/packages/core/src/cli/binary.ts +9 -1
  16. package/packages/core/src/cli/build-control.ts +110 -13
  17. package/packages/core/src/cli/context.ts +46 -0
  18. package/packages/core/src/cli/dashboard/facts.ts +215 -0
  19. package/packages/core/src/cli/dashboard/frame-parse.ts +223 -0
  20. package/packages/core/src/cli/dashboard/model.ts +141 -153
  21. package/packages/core/src/cli/dashboard/poll.ts +76 -12
  22. package/packages/core/src/cli/dashboard/render.ts +34 -5
  23. package/packages/core/src/cli/dispatch-frontend.ts +54 -5
  24. package/packages/core/src/cli/dispatch-process.ts +1 -13
  25. package/packages/core/src/cli/dispatch.ts +392 -75
  26. package/packages/core/src/cli/harvest.ts +122 -8
  27. package/packages/core/src/cli/help.ts +62 -9
  28. package/packages/core/src/cli/init-readiness-remote.ts +110 -0
  29. package/packages/core/src/cli/init-readiness-shared.ts +72 -0
  30. package/packages/core/src/cli/init-validation.ts +266 -222
  31. package/packages/core/src/cli/installation.ts +50 -8
  32. package/packages/core/src/cli/main.ts +70 -11
  33. package/packages/core/src/cli/mcp.ts +23 -1
  34. package/packages/core/src/cli/plugin.ts +7 -4
  35. package/packages/core/src/cli/remote-poll.ts +277 -0
  36. package/packages/core/src/cli/repo-state.ts +22 -0
  37. package/packages/core/src/cli/repository-status.ts +39 -1
  38. package/packages/core/src/cli/self-update.ts +77 -2
  39. package/packages/core/src/cli/status.ts +150 -28
  40. package/packages/core/src/cli/terminals.ts +2 -1
  41. package/packages/core/src/cli/ticket-assets.ts +247 -0
  42. package/packages/core/src/cli/ticket.ts +453 -8
  43. package/packages/core/src/cli/wait.ts +157 -41
  44. package/packages/core/src/cli/watch.ts +138 -168
  45. package/packages/core/src/config/live.ts +23 -0
  46. package/packages/core/src/config/pipeline-source.ts +388 -0
  47. package/packages/core/src/config/schema.ts +251 -19
  48. package/packages/core/src/events/catalog.ts +1 -0
  49. package/packages/core/src/events/globs.ts +47 -0
  50. package/packages/core/src/events/payloads.ts +52 -2
  51. package/packages/core/src/events/repository.ts +52 -1
  52. package/packages/core/src/events/sessions.ts +69 -10
  53. package/packages/core/src/events/workspace-remote.ts +12 -0
  54. package/packages/core/src/integration/harness.ts +115 -2
  55. package/packages/core/src/kernel/__snapshots__/reducer-registry.test.ts.snap +1369 -0
  56. package/packages/core/src/kernel/auto-merge-default.ts +157 -0
  57. package/packages/core/src/kernel/auto-merge.ts +99 -22
  58. package/packages/core/src/kernel/dispatch-settings.ts +42 -17
  59. package/packages/core/src/kernel/dispatch-status.ts +23 -0
  60. package/packages/core/src/kernel/engine.ts +9 -438
  61. package/packages/core/src/kernel/generators/build-log.ts +259 -0
  62. package/packages/core/src/kernel/generators/repository-journals.ts +206 -0
  63. package/packages/core/src/kernel/generators/session-log.ts +72 -0
  64. package/packages/core/src/kernel/harvest.ts +62 -18
  65. package/packages/core/src/kernel/incremental-contract.ts +75 -0
  66. package/packages/core/src/kernel/incremental-fixtures.ts +211 -0
  67. package/packages/core/src/kernel/incremental.ts +53 -0
  68. package/packages/core/src/kernel/log-index.ts +620 -0
  69. package/packages/core/src/kernel/phases.ts +16 -2
  70. package/packages/core/src/kernel/pr-attachments.ts +164 -46
  71. package/packages/core/src/kernel/reducer-registry.ts +164 -0
  72. package/packages/core/src/kernel/reducer.ts +254 -73
  73. package/packages/core/src/operator/annotations.ts +6 -0
  74. package/packages/core/src/operator/control.ts +19 -5
  75. package/packages/core/src/operator/index.ts +71 -6
  76. package/packages/core/src/operator/query.ts +253 -24
  77. package/packages/core/src/operator/read-view.ts +98 -0
  78. package/packages/core/src/operator/registry.ts +55 -10
  79. package/packages/core/src/operator/requests.ts +9 -6
  80. package/packages/core/src/operator/sandbox-host.ts +140 -0
  81. package/packages/core/src/operator/sandbox.ts +247 -14
  82. package/packages/core/src/operator/ticket-source-backend.ts +47 -0
  83. package/packages/core/src/orchestrator/approvals.ts +115 -0
  84. package/packages/core/src/orchestrator/conversation.ts +167 -0
  85. package/packages/core/src/orchestrator/model.ts +86 -0
  86. package/packages/core/src/orchestrator/prompt.ts +61 -0
  87. package/packages/core/src/orchestrator/tools.ts +117 -0
  88. package/packages/core/src/orchestrator/turn-runner.ts +457 -0
  89. package/packages/core/src/plugin-sdk/index.ts +51 -1
  90. package/packages/core/src/plugins/load.ts +135 -7
  91. package/packages/core/src/plugins/manifest.ts +183 -10
  92. package/packages/core/src/plugins/registry.ts +73 -4
  93. package/packages/core/src/ports/forge/create.ts +1 -1
  94. package/packages/core/src/ports/forge/fake.ts +31 -0
  95. package/packages/core/src/ports/forge/github-transport.ts +13 -2
  96. package/packages/core/src/ports/forge/github.ts +13 -2
  97. package/packages/core/src/ports/forge/local-git.ts +31 -1
  98. package/packages/core/src/ports/runner/claude.ts +2 -2
  99. package/packages/core/src/ports/runner/codex.ts +6 -2
  100. package/packages/core/src/ports/runner/fake.ts +7 -1
  101. package/packages/core/src/ports/runner/pi.ts +8 -6
  102. package/packages/core/src/ports/runner/provider-error.ts +13 -3
  103. package/packages/core/src/ports/tickets/fake.ts +5 -1
  104. package/packages/core/src/ports/tickets/file.ts +169 -48
  105. package/packages/core/src/ports/tickets/{remote-index.ts → hosted-index.ts} +3 -2
  106. package/packages/core/src/ports/tickets/linear.ts +33 -1
  107. package/packages/core/src/ports/tickets/operations.ts +1 -1
  108. package/packages/core/src/ports/tickets/remote.ts +8 -1
  109. package/packages/core/src/ports/types.ts +49 -3
  110. package/packages/core/src/ports/workspace/builtin-capabilities.ts +114 -0
  111. package/packages/core/src/ports/workspace/create.ts +74 -39
  112. package/packages/core/src/ports/workspace/distribution-archive.ts +171 -12
  113. package/packages/core/src/ports/workspace/fake.ts +169 -4
  114. package/packages/core/src/ports/workspace/git-worktree.ts +246 -26
  115. package/packages/core/src/ports/workspace/github-origin.ts +36 -0
  116. package/packages/core/src/ports/workspace/operator-sandbox.ts +7 -1
  117. package/packages/core/src/ports/workspace/provider-capabilities.ts +171 -0
  118. package/packages/core/src/ports/workspace/vercel-capabilities.ts +110 -0
  119. package/packages/core/src/ports/workspace/vercel-sandbox.ts +142 -114
  120. package/packages/core/src/processes/build-child.ts +17 -2
  121. package/packages/core/src/processes/build-execution-state.ts +68 -15
  122. package/packages/core/src/processes/build-runner.ts +238 -93
  123. package/packages/core/src/processes/dispatcher-selectors.ts +123 -0
  124. package/packages/core/src/processes/dispatcher.ts +501 -201
  125. package/packages/core/src/processes/execution-settlement.ts +72 -39
  126. package/packages/core/src/processes/harvest-child.ts +5 -0
  127. package/packages/core/src/processes/harvest-execution-state.ts +41 -29
  128. package/packages/core/src/processes/harvest-runner.ts +66 -19
  129. package/packages/core/src/processes/harvest.ts +196 -24
  130. package/packages/core/src/processes/infrastructure-failure-budget.ts +31 -18
  131. package/packages/core/src/processes/orchestrator-tick.ts +360 -0
  132. package/packages/core/src/processes/publication-state.ts +164 -52
  133. package/packages/core/src/processes/repo-view.ts +1436 -0
  134. package/packages/core/src/processes/sandbox-state.ts +36 -2
  135. package/packages/core/src/processes/tick-state.ts +33 -0
  136. package/packages/core/src/store/adapter.ts +41 -0
  137. package/packages/core/src/store/build-scope.ts +90 -0
  138. package/packages/core/src/store/contract.ts +1205 -0
  139. package/packages/core/src/store/digest.ts +102 -0
  140. package/packages/core/src/store/local/schema.ts +46 -2
  141. package/packages/core/src/store/local/store.ts +445 -4
  142. package/packages/core/src/store/memory.ts +246 -0
  143. package/packages/core/src/store/new-build.ts +17 -0
  144. package/packages/core/src/store/phase-session.ts +94 -0
  145. package/packages/core/src/store/remote/client.ts +310 -2
  146. package/packages/core/src/store/remote/index.ts +14 -2
  147. package/packages/core/src/store/remote/protocol.ts +115 -12
  148. package/packages/core/src/store/remote/version.ts +6 -2
  149. package/packages/core/src/store/repo-state-events.ts +205 -0
  150. package/packages/core/src/store/retention.ts +7 -0
  151. package/packages/core/src/store/session-handle.ts +94 -0
  152. package/packages/core/src/{sessions/reducer.ts → store/session-reducer.ts} +79 -19
  153. package/packages/core/src/store/snapshots.ts +88 -0
  154. package/packages/core/src/store/streams/session-writer.ts +42 -2
  155. package/packages/core/src/store/streams/wait.ts +4 -5
  156. package/packages/core/src/store/ticket-assets.ts +458 -0
  157. package/packages/core/src/store/types.ts +160 -1
  158. package/packages/core/src/testing/index.ts +59 -0
  159. package/skills/code-review/SKILL.md +5 -1
  160. package/skills/guide/SKILL.md +152 -40
  161. package/skills/guide/references/operator-api.md +2 -2
  162. package/skills/guide/references/plugin-authoring.md +17 -0
  163. package/skills/guide/references/remote-store-protocol.md +204 -16
  164. package/skills/guide/references/remote-ticket-protocol.md +1 -1
  165. package/skills/guide/references/setup.md +12 -9
  166. package/skills/implement/SKILL.md +3 -0
  167. package/skills/operate/SKILL.md +126 -0
  168. package/skills/plan/SKILL.md +4 -0
  169. package/skills/plan-review/SKILL.md +5 -1
  170. package/skills/spec/SKILL.md +46 -3
  171. package/skills/tickets/SKILL.md +57 -5
  172. package/packages/core/src/cli/dashboard/frame-image.ts +0 -373
  173. package/packages/core/src/cli/store-opening.contract.ts +0 -289
  174. package/packages/core/src/cli/testkit.ts +0 -194
  175. package/packages/core/src/markdown.ts +0 -67
  176. package/packages/core/src/operator/client.ts +0 -311
  177. package/packages/core/src/operator/server.ts +0 -616
  178. package/packages/core/src/operator/sessions.ts +0 -223
  179. package/packages/core/src/ports/runner/live-contract-fixture.ts +0 -176
  180. package/packages/core/src/ports/tickets/remote-server.ts +0 -185
  181. package/packages/core/src/store/remote/server.ts +0 -923
  182. package/packages/core/src/testing/store-failures.ts +0 -32
@@ -14,9 +14,16 @@
14
14
  import { z } from 'zod'
15
15
  import { prImageHostSchema } from '../ontology'
16
16
  import { SANDBOX_FORBIDDEN_ENV } from '../ports/workspace/operator-sandbox'
17
+ import {
18
+ runtimeProvisioningMap,
19
+ runtimeProvisioningMissingMessage,
20
+ sandboxForbiddenEnvMessage,
21
+ } from '../ports/workspace/provider-capabilities'
17
22
  import { defineEntry, openMap, ownEntries, parseEntry } from '../open-map'
18
23
  import { forwardIssues } from '../zod-issues'
19
- import { displayName, effectiveRuntimeReferences, tomlKey } from './roles'
24
+ import { compileEventGlobs } from '../events/globs'
25
+ import { REPOSITORY_ATTENTION_EVENTS } from '../events/repository'
26
+ import { effectiveRuntimeReferences } from './roles'
20
27
 
21
28
  // ── Open maps ────────────────────────────────────────────────────────────────
22
29
 
@@ -242,6 +249,70 @@ export const workspaceSchema = z.strictObject({
242
249
  })
243
250
  export type WorkspaceConfig = z.infer<typeof workspaceSchema>
244
251
 
252
+ /**
253
+ * The parse-time subset of workspace-provider behavior a builtin declares
254
+ * (AUT-516). This table lives in `config/schema.ts` because the
255
+ * `[workspace.config]` validation runs inside `configSchema`'s
256
+ * `superRefine` — at parse time, long before plugins load, in processes that
257
+ * never touch the registry. The full capability objects (including the
258
+ * registry-seam declarations) are assembled in
259
+ * `ports/workspace/builtin-capabilities.ts`, which takes this subset from
260
+ * here — a parity now enforced by
261
+ * `ports/workspace/builtin-capabilities.test.ts` (AUT-573), so a builtin
262
+ * added to only one table fails tests rather than compiling silently. The
263
+ * construction-site refusal text differs by design from the
264
+ * parse-site one, so it is NOT copied from this table.
265
+ */
266
+ export interface WorkspaceProviderConfigDeclaration {
267
+ /** Strict schema applied to `[workspace.config]`. A builtin declaration
268
+ * carries EXACTLY ONE of `configSchema` / `configRefusalMessage`: when the
269
+ * schema is absent, the declaration must carry `configRefusalMessage` for
270
+ * nonempty provider config to be refused (the neither-key side is pinned by
271
+ * the AUT-565 test in config.test.ts), and carrying BOTH is forbidden —
272
+ * `configSchema`'s superRefine checks `configRefusalMessage` first, so the
273
+ * refusal branch would win and the schema would silently never apply (the
274
+ * both-keys side is pinned by its sibling test). Providers absent from
275
+ * `BUILTIN_WORKSPACE_PROVIDER_CONFIG` are plugin-owned pass-throughs and
276
+ * are never refused here. */
277
+ configSchema?: z.ZodType
278
+ /** Parse-site refusal text for a provider that rejects `[workspace.config]`
279
+ * outright. This is the sole parse-site refusal key: a declaration without
280
+ * it never refuses `[workspace.config]` here, with or without a schema.
281
+ * Distinct from the capability's construction-site `configRefusal`. */
282
+ configRefusalMessage?: string
283
+ /** Referenced runtimes must have `[workspace.config.runtimeProvisioning]`
284
+ * entries (checked only once roles, verify, and finalize are all present). */
285
+ requireRuntimeProvisioning?: boolean
286
+ /** Names added to the operator-sandbox forbidden-forwarding set beyond
287
+ * `SANDBOX_FORBIDDEN_ENV`. */
288
+ sandboxForbiddenEnv?: readonly string[]
289
+ }
290
+
291
+ /** Parse-time behavior of every builtin workspace provider, keyed by the
292
+ * provider name an operator writes in `[workspace].provider`. No provider
293
+ * name appears in any validation branch — every check reads this table. */
294
+ export const BUILTIN_WORKSPACE_PROVIDER_CONFIG: ReadonlyMap<
295
+ string,
296
+ WorkspaceProviderConfigDeclaration
297
+ > = new Map([
298
+ [
299
+ 'git-worktree',
300
+ {
301
+ configRefusalMessage:
302
+ '[workspace.config] is not supported by the builtin "git-worktree" provider — remove it or select a plugin workspace provider',
303
+ },
304
+ ],
305
+ [
306
+ 'vercel-sandbox',
307
+ {
308
+ configSchema: vercelSandboxConfigSchema,
309
+ requireRuntimeProvisioning: true,
310
+ // Its credentials are already in SANDBOX_FORBIDDEN_ENV.
311
+ sandboxForbiddenEnv: [],
312
+ },
313
+ ],
314
+ ])
315
+
245
316
  // ── [commands] ───────────────────────────────────────────────────────────────
246
317
  //
247
318
  // Open map of deterministic verbs the kernel may run. `setup`, `lint`,
@@ -485,6 +556,20 @@ export const ticketsSchema = z.strictObject({
485
556
  (s) => s.trim().length > 0,
486
557
  '[tickets].readyState must not be blank — name the one workflow state a ticket must sit in to be dispatched (e.g. "ready").',
487
558
  ),
559
+ /**
560
+ * Narrows readiness to tickets assigned to one Linear user — a third
561
+ * conjunctive gate beside readyState and readyLabels. `"me"` is the owner of
562
+ * the dispatcher's LINEAR_API_KEY; an email names a specific user (for a
563
+ * shared or bot key). Unassigned tickets are never ready while it is set.
564
+ * Linear only; absent leaves readiness unchanged.
565
+ */
566
+ readyAssignee: z
567
+ .string()
568
+ .refine(
569
+ (s) => s === 'me' || (s === s.trim() && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(s)),
570
+ '[tickets].readyAssignee must be "me" (the owner of the API key) or a user email address (e.g. "person@example.com")',
571
+ )
572
+ .optional(),
488
573
  /** Backend team key (e.g. "ENG") — required for linear and hosted. */
489
574
  teamKey: z.string().min(1).optional(),
490
575
  /** Workflow state claim() moves an issue to (§12); Linear and hosted. */
@@ -534,9 +619,107 @@ export const orchestratorSandboxSchema = z.strictObject({
534
619
  })
535
620
  export type OrchestratorSandboxConfig = z.infer<typeof orchestratorSandboxSchema>
536
621
 
622
+ /** The turn runner lives inside the hosted operator route and the dispatcher
623
+ * tick, both of which run under a Vercel function duration limit the config's
624
+ * invocation budget can never exceed: a value above this is clamped (not
625
+ * rejected) at parse time so a mis-set budget degrades to the limit instead of
626
+ * failing every deploy. The operator route pins `maxDuration` to this same
627
+ * constant, so one number is cited by the config, the route, and the docs. */
628
+ export const ORCHESTRATOR_ROUTE_LIMIT_SECONDS = 300
629
+
630
+ /** Turn budget floor for the dispatcher tick's orchestrator step: when less
631
+ * than this many seconds of tick budget remain, the step skips the pass
632
+ * entirely (suspension state is durable, so skipping is always safe). */
633
+ export const ORCHESTRATOR_MIN_TURN_SECONDS = 30
634
+
635
+ /** The turn approval entries' shape: a registry tool name, optionally
636
+ * qualified by the discriminator field's accepted value (`tool:qualifier`).
637
+ * Validation is syntactic only — an entry naming an absent or unknown tool is
638
+ * inert by design (documented in docs/configuration.md), because the list
639
+ * is meant to stay stable while the registry grows — e.g. `sandbox.publish`
640
+ * was named here before its ticket landed, and stays inert in bindings
641
+ * where no sandbox backend is bound (the hosted default). */
642
+ const ORCHESTRATOR_APPROVAL_ENTRY = /^[a-z][a-z0-9_.]*(:[a-z0-9_-]+)?$/
643
+
644
+ /** The attention set a new session's wake settings inherit when the
645
+ * repository does not override `wake`: the build events an operator or agent
646
+ * 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`
648
+ * follows), spelled as literal globs. Both halves of the attention set can
649
+ * wake a session. */
650
+ export const defaultOrchestratorWakeGlobs: readonly string[] = [
651
+ 'escalation.raised',
652
+ 'phase.failed',
653
+ 'infrastructure.failed',
654
+ 'runner.setup-failed',
655
+ 'dispatch.failed',
656
+ 'publication.lost',
657
+ 'finalize.completed',
658
+ 'pr.conflicted',
659
+ 'pr.merged',
660
+ 'pr.closed',
661
+ 'build.completed',
662
+ 'build.aborted',
663
+ ...REPOSITORY_ATTENTION_EVENTS,
664
+ ]
665
+
666
+ /** The registry tools a turn asks its operator about before executing.
667
+ * The maintainer sets this to `[]` for autonomous operation. */
668
+ export const defaultOrchestratorApprovals: readonly string[] = [
669
+ 'builds.control:abort',
670
+ 'builds.control:discard',
671
+ 'builds.answer:revise-spec',
672
+ 'sandbox.publish',
673
+ 'tickets.move:ready',
674
+ ]
675
+
676
+ /** A session's effective wake globs: the configured `wake` when declared
677
+ * (an explicit `[]` means never wake), else the default attention set. Kept
678
+ * as a resolver rather than a schema default so `wake`'s absence (inherit)
679
+ * and emptiness (never wake) stay distinct. */
680
+ export function orchestratorWakeGlobs(config: OrchestratorConfig): string[] {
681
+ return [...(config.wake ?? defaultOrchestratorWakeGlobs)]
682
+ }
683
+
537
684
  export const orchestratorSchema = z.strictObject({
538
685
  enabled: z.boolean().default(false),
539
686
  sandbox: orchestratorSandboxSchema.prefault({}),
687
+ /** Provider-qualified model string for the turn runner, in the same
688
+ * vocabulary as `[roles]` model strings (nonblank; a leading
689
+ * `vercel-ai-gateway/` prefix is stripped before the gateway provider).
690
+ * Resolved through the deployment's gateway credential. Required when
691
+ * `enabled = true` (cross-validated below); the runner never starts a turn
692
+ * on a repository whose config omits it. */
693
+ model: z.string().min(1).optional(),
694
+ /** Wall-clock budget for one turn invocation, in seconds. Clamped — not
695
+ * rejected — to ORCHESTRATOR_ROUTE_LIMIT_SECONDS, the hosted function
696
+ * duration the runner runs under. */
697
+ invocationBudgetSeconds: z
698
+ .number()
699
+ .int()
700
+ .positive()
701
+ .default(240)
702
+ .transform((value) => Math.min(value, ORCHESTRATOR_ROUTE_LIMIT_SECONDS)),
703
+ /** Registry tool names (optionally `tool:qualifier`) whose calls suspend a
704
+ * turn until the operator answers. Empty means none. Entries naming an
705
+ * absent or unknown tool are inert by design, which is what keeps the
706
+ * default list stable while the registry grows. */
707
+ approvals: z
708
+ .array(
709
+ z
710
+ .string()
711
+ .regex(
712
+ ORCHESTRATOR_APPROVAL_ENTRY,
713
+ 'approval entries look like "tool" or "tool:qualifier"',
714
+ ),
715
+ )
716
+ .default([...defaultOrchestratorApprovals]),
717
+ /** Wake-filter globs a NEW session inherits. Kept schema-optional (not
718
+ * `.default()`) so absent ("inherit the default attention set") and `[]`
719
+ * ("never wake") stay distinct. Each glob must match at least one build
720
+ * or repository-journal event type — validated below with the shared
721
+ * `ab watch` compiler. */
722
+ wake: z.array(z.string().min(1)).optional(),
540
723
  })
541
724
  export type OrchestratorConfig = z.infer<typeof orchestratorSchema>
542
725
 
@@ -614,33 +797,34 @@ export const configSchema = configRootSchema.superRefine((config, ctx) => {
614
797
  })
615
798
  })
616
799
 
617
- if (
618
- config.workspace.provider === 'git-worktree' &&
619
- Object.keys(config.workspace.config).length > 0
620
- ) {
621
- ctx.addIssue({
622
- code: 'custom',
623
- path: ['workspace', 'config'],
624
- message:
625
- '[workspace.config] is not supported by the builtin "git-worktree" provider — remove it or select a plugin workspace provider',
626
- })
627
- } else if (config.workspace.provider === 'vercel-sandbox') {
628
- const parsed = vercelSandboxConfigSchema.safeParse(config.workspace.config)
800
+ const workspaceDeclaration = BUILTIN_WORKSPACE_PROVIDER_CONFIG.get(config.workspace.provider)
801
+ // The parse-site refusal fires only for a declaration carrying
802
+ // `configRefusalMessage`; providers absent from the table are plugin-owned
803
+ // pass-throughs and are never refused here.
804
+ if (workspaceDeclaration?.configRefusalMessage !== undefined) {
805
+ if (Object.keys(config.workspace.config).length > 0) {
806
+ ctx.addIssue({
807
+ code: 'custom',
808
+ path: ['workspace', 'config'],
809
+ message: workspaceDeclaration.configRefusalMessage,
810
+ })
811
+ }
812
+ } else if (workspaceDeclaration?.configSchema !== undefined) {
813
+ const parsed = workspaceDeclaration.configSchema.safeParse(config.workspace.config)
629
814
  if (!parsed.success) {
630
815
  forwardIssues(parsed.error.issues, ctx, ['workspace', 'config'])
631
816
  } else if (
817
+ workspaceDeclaration.requireRuntimeProvisioning === true &&
632
818
  config.roles !== undefined &&
633
819
  config.verify !== undefined &&
634
820
  config.finalize !== undefined
635
821
  ) {
636
822
  for (const group of effectiveRuntimeReferences(config)) {
637
- if (Object.hasOwn(parsed.data.runtimeProvisioning, group.runtime)) continue
823
+ if (Object.hasOwn(runtimeProvisioningMap(parsed.data), group.runtime)) continue
638
824
  ctx.addIssue({
639
825
  code: 'custom',
640
826
  path: ['workspace', 'config', 'runtimeProvisioning', group.runtime],
641
- message:
642
- `runtime ${displayName(group.runtime)} is selected by ${group.references.join(', ')} but has no sandbox provisioning; add ` +
643
- `[workspace.config.runtimeProvisioning.${tomlKey(group.runtime)}] with nonblank install and preflight commands`,
827
+ message: runtimeProvisioningMissingMessage(group),
644
828
  })
645
829
  }
646
830
  }
@@ -753,18 +937,66 @@ export const configSchema = configRootSchema.superRefine((config, ctx) => {
753
937
  }
754
938
  }
755
939
  }
940
+ if (tickets.source !== 'linear' && tickets.readyAssignee !== undefined) {
941
+ ctx.addIssue({
942
+ code: 'custom',
943
+ path: ['tickets', 'readyAssignee'],
944
+ message: `[tickets].readyAssignee applies only to source = "linear" — remove it or select that source (selected source: "${tickets.source}")`,
945
+ })
946
+ }
756
947
  // Plugin sources receive the existing ticket lifecycle/configuration fields
757
948
  // unchanged; adapter-specific validation belongs to their factory.
758
949
 
759
950
  // The credential-free sandbox rule (AUT-340): a forwarded variable name may
760
951
  // never name a store, forge, ticket-provider, model, or Vercel credential.
952
+ // A workspace provider can declare additional forbidden names beyond the
953
+ // shared set. This check is deliberately builtin-only: plugins are not
954
+ // loaded when config is parsed, so a plugin provider's declared names cannot
955
+ // be seen here (AUT-536). The deferral is accepted, not an oversight — the
956
+ // declared extras are enforced at the registry-aware seams instead,
957
+ // `createWorkspaceProvider` (construction) and `validateInitReadiness`
958
+ // (init validation), both of which run after plugin load; no site loads
959
+ // plugins earlier to widen parse-time coverage.
960
+ // Wake globs are validated with the same compiler `ab watch` uses, over the
961
+ // build AND repository-journal catalogs — the wake pass scans both — so a
962
+ // typo'd glob fails at parse time instead of silently matching nothing —
963
+ // regardless of `enabled`, since a later enablement would inherit the
964
+ // already-broken value.
965
+ if (config.orchestrator.wake !== undefined) {
966
+ try {
967
+ compileEventGlobs(config.orchestrator.wake, {
968
+ repository: true,
969
+ usage: '[orchestrator].wake',
970
+ })
971
+ } catch (error) {
972
+ ctx.addIssue({
973
+ code: 'custom',
974
+ path: ['orchestrator', 'wake'],
975
+ message: error instanceof Error ? error.message : String(error),
976
+ })
977
+ }
978
+ }
979
+
761
980
  if (config.orchestrator.enabled) {
981
+ // The turn runner's model is not optional when the orchestrator is on:
982
+ // an enabled table without one would fail at first turn start, far from
983
+ // the edit that caused it.
984
+ if (config.orchestrator.model === undefined) {
985
+ ctx.addIssue({
986
+ code: 'custom',
987
+ path: ['orchestrator', 'model'],
988
+ message:
989
+ '[orchestrator].enabled = true requires [orchestrator].model — name the provider-qualified model string the turn runner uses (same vocabulary as [roles], resolved through the deployment gateway credential)',
990
+ })
991
+ }
992
+ const providerForbidden =
993
+ BUILTIN_WORKSPACE_PROVIDER_CONFIG.get(config.workspace.provider)?.sandboxForbiddenEnv ?? []
762
994
  config.orchestrator.sandbox.environmentVariables.forEach((name, index) => {
763
- if (SANDBOX_FORBIDDEN_ENV.includes(name)) {
995
+ if (SANDBOX_FORBIDDEN_ENV.includes(name) || providerForbidden.includes(name)) {
764
996
  ctx.addIssue({
765
997
  code: 'custom',
766
998
  path: ['orchestrator', 'sandbox', 'environmentVariables', index],
767
- message: `environment variable ${JSON.stringify(name)} is a store, forge, ticket-provider, model, or Vercel credential and may never be forwarded into an operator sandbox`,
999
+ message: sandboxForbiddenEnvMessage(name),
768
1000
  })
769
1001
  }
770
1002
  })
@@ -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': empty,
367
- 'build.auto-merge-cancelled': empty,
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'],