@defrex/autobuild 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (147) hide show
  1. package/README.md +1 -1
  2. package/SPEC.md +313 -26
  3. package/bin/ab-build-runner.ts +8 -2
  4. package/docs/architecture.md +57 -14
  5. package/docs/assets/headline-wide.png +0 -0
  6. package/docs/configuration.md +151 -25
  7. package/docs/hosted-dispatcher.md +69 -13
  8. package/docs/operator-api.md +2 -2
  9. package/docs/remote-store-protocol.md +169 -16
  10. package/docs/remote-ticket-protocol.md +1 -1
  11. package/docs/setup.md +12 -9
  12. package/docs/website.md +10 -0
  13. package/package.json +43 -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/model.ts +57 -7
  19. package/packages/core/src/cli/dashboard/poll.ts +69 -10
  20. package/packages/core/src/cli/dashboard/render.ts +34 -5
  21. package/packages/core/src/cli/dispatch-frontend.ts +54 -5
  22. package/packages/core/src/cli/dispatch.ts +320 -46
  23. package/packages/core/src/cli/harvest.ts +122 -8
  24. package/packages/core/src/cli/help.ts +47 -8
  25. package/packages/core/src/cli/init-readiness-remote.ts +110 -0
  26. package/packages/core/src/cli/init-readiness-shared.ts +72 -0
  27. package/packages/core/src/cli/init-validation.ts +248 -222
  28. package/packages/core/src/cli/main.ts +70 -11
  29. package/packages/core/src/cli/mcp.ts +23 -1
  30. package/packages/core/src/cli/plugin.ts +7 -4
  31. package/packages/core/src/cli/remote-poll.ts +277 -0
  32. package/packages/core/src/cli/repo-state.ts +22 -0
  33. package/packages/core/src/cli/repository-status.ts +39 -1
  34. package/packages/core/src/cli/status.ts +149 -27
  35. package/packages/core/src/cli/terminals.ts +2 -1
  36. package/packages/core/src/cli/ticket-assets.ts +247 -0
  37. package/packages/core/src/cli/ticket.ts +307 -7
  38. package/packages/core/src/cli/wait.ts +147 -39
  39. package/packages/core/src/cli/watch.ts +128 -166
  40. package/packages/core/src/config/live.ts +22 -0
  41. package/packages/core/src/config/pipeline-source.ts +376 -0
  42. package/packages/core/src/config/schema.ts +230 -19
  43. package/packages/core/src/events/catalog.ts +1 -0
  44. package/packages/core/src/events/globs.ts +47 -0
  45. package/packages/core/src/events/payloads.ts +52 -2
  46. package/packages/core/src/events/repository.ts +52 -1
  47. package/packages/core/src/events/sessions.ts +69 -10
  48. package/packages/core/src/events/workspace-remote.ts +12 -0
  49. package/packages/core/src/integration/harness.ts +2 -1
  50. package/packages/core/src/kernel/auto-merge-default.ts +123 -0
  51. package/packages/core/src/kernel/phases.ts +16 -2
  52. package/packages/core/src/kernel/reducer.ts +40 -1
  53. package/packages/core/src/operator/annotations.ts +6 -0
  54. package/packages/core/src/operator/control.ts +19 -5
  55. package/packages/core/src/operator/index.ts +71 -6
  56. package/packages/core/src/operator/query.ts +208 -19
  57. package/packages/core/src/operator/registry.ts +55 -10
  58. package/packages/core/src/operator/requests.ts +9 -6
  59. package/packages/core/src/operator/sandbox-host.ts +140 -0
  60. package/packages/core/src/operator/sandbox.ts +247 -14
  61. package/packages/core/src/operator/ticket-source-backend.ts +47 -0
  62. package/packages/core/src/orchestrator/approvals.ts +115 -0
  63. package/packages/core/src/orchestrator/conversation.ts +167 -0
  64. package/packages/core/src/orchestrator/model.ts +86 -0
  65. package/packages/core/src/orchestrator/prompt.ts +61 -0
  66. package/packages/core/src/orchestrator/tools.ts +117 -0
  67. package/packages/core/src/orchestrator/turn-runner.ts +457 -0
  68. package/packages/core/src/plugin-sdk/index.ts +51 -1
  69. package/packages/core/src/plugins/load.ts +135 -7
  70. package/packages/core/src/plugins/manifest.ts +183 -10
  71. package/packages/core/src/plugins/registry.ts +73 -4
  72. package/packages/core/src/ports/forge/create.ts +1 -1
  73. package/packages/core/src/ports/forge/fake.ts +31 -0
  74. package/packages/core/src/ports/forge/github-transport.ts +13 -2
  75. package/packages/core/src/ports/forge/github.ts +13 -2
  76. package/packages/core/src/ports/forge/local-git.ts +31 -1
  77. package/packages/core/src/ports/runner/claude.ts +2 -2
  78. package/packages/core/src/ports/runner/codex.ts +6 -2
  79. package/packages/core/src/ports/runner/fake.ts +7 -1
  80. package/packages/core/src/ports/runner/pi.ts +8 -6
  81. package/packages/core/src/ports/runner/provider-error.ts +13 -3
  82. package/packages/core/src/ports/tickets/{remote-index.ts → hosted-index.ts} +3 -2
  83. package/packages/core/src/ports/types.ts +17 -2
  84. package/packages/core/src/ports/workspace/builtin-capabilities.ts +114 -0
  85. package/packages/core/src/ports/workspace/create.ts +74 -39
  86. package/packages/core/src/ports/workspace/distribution-archive.ts +171 -12
  87. package/packages/core/src/ports/workspace/fake.ts +169 -4
  88. package/packages/core/src/ports/workspace/git-worktree.ts +246 -26
  89. package/packages/core/src/ports/workspace/github-origin.ts +36 -0
  90. package/packages/core/src/ports/workspace/operator-sandbox.ts +7 -1
  91. package/packages/core/src/ports/workspace/provider-capabilities.ts +171 -0
  92. package/packages/core/src/ports/workspace/vercel-capabilities.ts +110 -0
  93. package/packages/core/src/ports/workspace/vercel-sandbox.ts +142 -114
  94. package/packages/core/src/processes/build-child.ts +17 -2
  95. package/packages/core/src/processes/build-execution-state.ts +37 -0
  96. package/packages/core/src/processes/build-runner.ts +54 -3
  97. package/packages/core/src/processes/dispatcher.ts +247 -41
  98. package/packages/core/src/processes/harvest-child.ts +5 -0
  99. package/packages/core/src/processes/harvest-runner.ts +66 -19
  100. package/packages/core/src/processes/harvest.ts +196 -24
  101. package/packages/core/src/processes/orchestrator-tick.ts +319 -0
  102. package/packages/core/src/processes/sandbox-state.ts +10 -0
  103. package/packages/core/src/store/adapter.ts +32 -0
  104. package/packages/core/src/store/build-scope.ts +64 -0
  105. package/packages/core/src/store/contract.ts +868 -0
  106. package/packages/core/src/store/digest.ts +49 -0
  107. package/packages/core/src/store/local/schema.ts +29 -2
  108. package/packages/core/src/store/local/store.ts +338 -4
  109. package/packages/core/src/store/memory.ts +179 -0
  110. package/packages/core/src/store/new-build.ts +17 -0
  111. package/packages/core/src/store/phase-session.ts +71 -0
  112. package/packages/core/src/store/remote/client.ts +260 -1
  113. package/packages/core/src/store/remote/index.ts +12 -2
  114. package/packages/core/src/store/remote/protocol.ts +100 -12
  115. package/packages/core/src/store/remote/version.ts +6 -2
  116. package/packages/core/src/store/repo-state-events.ts +140 -0
  117. package/packages/core/src/store/retention.ts +7 -0
  118. package/packages/core/src/store/session-handle.ts +64 -0
  119. package/packages/core/src/{sessions/reducer.ts → store/session-reducer.ts} +28 -9
  120. package/packages/core/src/store/streams/session-writer.ts +42 -2
  121. package/packages/core/src/store/streams/wait.ts +4 -5
  122. package/packages/core/src/store/ticket-assets.ts +425 -0
  123. package/packages/core/src/store/types.ts +122 -1
  124. package/packages/core/src/testing/index.ts +59 -0
  125. package/skills/code-review/SKILL.md +5 -1
  126. package/skills/guide/SKILL.md +126 -33
  127. package/skills/guide/references/operator-api.md +2 -2
  128. package/skills/guide/references/plugin-authoring.md +17 -0
  129. package/skills/guide/references/remote-store-protocol.md +169 -16
  130. package/skills/guide/references/remote-ticket-protocol.md +1 -1
  131. package/skills/guide/references/setup.md +12 -9
  132. package/skills/implement/SKILL.md +3 -0
  133. package/skills/operate/SKILL.md +126 -0
  134. package/skills/plan/SKILL.md +4 -0
  135. package/skills/plan-review/SKILL.md +5 -1
  136. package/skills/spec/SKILL.md +46 -3
  137. package/packages/core/src/cli/dashboard/frame-image.ts +0 -373
  138. package/packages/core/src/cli/store-opening.contract.ts +0 -289
  139. package/packages/core/src/cli/testkit.ts +0 -194
  140. package/packages/core/src/markdown.ts +0 -67
  141. package/packages/core/src/operator/client.ts +0 -311
  142. package/packages/core/src/operator/server.ts +0 -616
  143. package/packages/core/src/operator/sessions.ts +0 -223
  144. package/packages/core/src/ports/runner/live-contract-fixture.ts +0 -176
  145. package/packages/core/src/ports/tickets/remote-server.ts +0 -185
  146. package/packages/core/src/store/remote/server.ts +0 -923
  147. package/packages/core/src/testing/store-failures.ts +0 -32
package/README.md CHANGED
@@ -67,7 +67,7 @@ usable on your machine.
67
67
  <!-- release-install:start -->
68
68
 
69
69
  ```sh
70
- bun add -g @defrex/autobuild@0.8.0
70
+ bun add -g @defrex/autobuild@0.9.0
71
71
  ```
72
72
 
73
73
  <!-- release-install:end -->
package/SPEC.md CHANGED
@@ -218,10 +218,12 @@ Small, independently runnable, crash-safe:
218
218
  repo, so re-running `ab dispatch` resumes durable work rather than only
219
219
  looking for new tickets. Every ordinary tick also completes queued dispatch
220
220
  records whose pre-run sequence was interrupted: it reconstructs a missing
221
- `build.created` from the immutable record, executes only missing workspace,
221
+ `build.created` from the immutable record (only records an older dispatcher
222
+ left with an empty stream: new claims write it atomically with the record), executes only missing workspace,
222
223
  spec, and ticket-notification boundaries, and records each failed attempt as
223
224
  `dispatch.failed`. `build.created` retains any claim-time auto-merge
224
- attribution until its human-authored command fact is materialized, while
225
+ attribution and the sampled default fact's seq until the human-authored
226
+ command fact is materialized, while
225
227
  `dispatch.comment-posted` prevents retries from duplicating the ticket notice.
226
228
  It also owns observation back-pressure: settling
227
229
  outstanding recoverable harvest runs takes priority over starting new scans
@@ -386,6 +388,27 @@ raises an `escalation`; a human uses `ab answer --revise-spec` (or
386
388
  `--revise-spec-from-ticket`), the spec gets rev N+1, and the build restarts
387
389
  from `plan` (cheap — downstream was invalidated anyway).
388
390
 
391
+ **Ticket assets are frozen with the spec.** Claiming a ticket pins the
392
+ ticket's current assets (§7.1.3) into the build the same way the spec becomes
393
+ revision 0: `build.created.assets` lists exactly which `(kind, name, revision)`
394
+ the build sees, omitted when the ticket has none. The set is sampled
395
+ immediately after the claim succeeds and before spec authoring, and it is
396
+ written atomically with the build record (`createBuild` takes the initial
397
+ `build.created` event), so an edit made while a spec is being authored, or a
398
+ crash between the two writes, can never change or lose the pin; interrupted
399
+ dispatch recovery re-reads it from the log. Attaching, replacing, or removing
400
+ an asset afterwards leaves the running build alone, because a build reads its
401
+ assets by pinned revision, never "latest". `ab ticket attach` and `asset rm`
402
+ warn when the ticket has an active build. Revising the spec **from the ticket**
403
+ (`ab answer --revise-spec-from-ticket`) re-pins the ticket's then-current
404
+ assets together with its body, as `spec.revised.assets` (present, possibly
405
+ empty, means "re-pin to this set"); the set is sampled once, when the body is
406
+ read, and recorded on the `escalation.answered` authorization too, so a retry
407
+ after a crash publishes the originally authorized set. Revising from a supplied
408
+ body file leaves the pinned assets unchanged. The one case this cannot cover is
409
+ a record an older dispatcher created with an empty stream: recovery can only
410
+ sample the ticket's current assets for it.
411
+
389
412
  ## 7. The build store
390
413
 
391
414
  The v1 repo-bloat and no-observability problems share one root cause: the
@@ -407,6 +430,8 @@ artifacts build_id, kind, revision, blobRef, metadata
407
430
  repo_streams repo, created/updated, lease + heartbeat
408
431
  repo_events repo, seq, timestamp, actor, type, payload (JSON) — append-only
409
432
  repo_artifacts repo, kind, revision, blobRef, metadata
433
+ ticket_assets repo, ticket_id, kind, name, revision, layout, manifest (JSON), size,
434
+ removed, created — one row per revision
410
435
  ```
411
436
 
412
437
  #### 7.1.1 Operator sessions (the third resource kind)
@@ -464,6 +489,141 @@ over the remote protocol the read is
464
489
  stream read's ([docs/remote-store-protocol.md](docs/remote-store-protocol.md),
465
490
  §5).
466
491
 
492
+ #### 7.1.2 The embedded orchestrator (turns)
493
+
494
+ The orchestrator is the agent that files tickets, watches builds, unblocks
495
+ them, and discusses direction — played in the hosted service by a stateless
496
+ turn runner over the operator tool registry, not by an agent living in a
497
+ sandbox (parity with MCP; no credential plumbing) and not by an external
498
+ workflow engine (durability comes from the store; the tick pattern already
499
+ exists). Posting a message to a session of an enabled repository starts a
500
+ turn in the same invocation: an AI SDK tool-loop agent, running with the
501
+ registry bound in process, reads the conversation from the session's durable
502
+ state, streams its output as protocol parts onto the turn's session-scoped
503
+ stream (§7.6), calls tools with the operator's attributed identity — the
504
+ session operator as the human actor, `via: {kind: "session", id}` —
505
+ checkpoints as it goes, and completes or suspends.
506
+
507
+ **Turn lifecycle.** `turn.started` (naming a new session-scoped stream and
508
+ the trigger — a message or a wake) → running → either `turn.completed`
509
+ (with input/output token usage and the step count), `turn.suspended`
510
+ (cause `budget` or `approval`), or typed `turn.failed`. A suspended turn
511
+ resumes via `turn.resumed` — the sole resume fact, recorded by the runner
512
+ when it actually picks the turn up; `approval.answered` only clears the
513
+ pending approval, so an answered approval whose resuming invocation died
514
+ stays recoverable. The stream stays open across a suspension and is the
515
+ resume checkpoint; it closes `completed` or `aborted` at the outcome.
516
+ Conversation state is reconstructed from durable state alone — the finalized
517
+ artifacts of earlier turns, the live stream of the open turn, and the session
518
+ events — so a turn resumed in a fresh invocation continues from the last
519
+ checkpointed step with identical model input; no process memory survives
520
+ between invocations. A turn's assembled messages attach at its `turn.started`
521
+ event, so a message posted during a suspension reads as the next user message
522
+ after the checkpoint, never interleaved into it.
523
+
524
+ **Budget suspension.** `[orchestrator].invocationBudgetSeconds` (default 240,
525
+ clamped to the 300-second hosted function limit) bounds one invocation. When
526
+ elapsed time approaches the budget — or a hard abort at the deadline
527
+ interrupts a runaway step — the runner records `turn.suspended` cause
528
+ `budget` and ends the invocation; a new invocation resumes it within 60
529
+ seconds (immediately when the service can self-trigger, otherwise on the
530
+ next dispatcher tick). The dispatcher tick's orchestrator step runs LAST,
531
+ after every build stage, against min(configured, remaining tick budget) with
532
+ a 30-second floor: it can only consume slack, and skipping is always safe
533
+ because suspension state is durable.
534
+
535
+ **Approval suspension.** A call to a tool named in
536
+ `[orchestrator].approvals` records `approval.requested`, emits the protocol's
537
+ tool-approval-request part, and suspends the turn with cause `approval`. An
538
+ `approve` answer resumes the turn and executes the call; a `deny` answer
539
+ resumes it with a denied tool output. Tools not on the list run without
540
+ pause. Entries are `tool` or `tool:qualifier`; an entry naming an absent or
541
+ unknown tool is inert by design.
542
+
543
+ **Wake rule.** The dispatcher tick's orchestrator step wakes each idle
544
+ session with non-empty wake settings: it scans the repository's builds for
545
+ build events after the session's per-build wake cursor and the repository
546
+ journal for events after the session's journal wake cursor — both filtered
547
+ by the same compiled settings, which may name build event types,
548
+ repository-journal event types, or both — and starts one turn per session
549
+ per tick with a `wake` trigger naming the winning event's source: the build,
550
+ seq, and type for a build-log wake, or the journal flag, seq, and type for a
551
+ repository-journal wake. A build wake delivers the event record and the
552
+ build's reduced state — frozen into the `turn.started` fact — as the turn's
553
+ input; a journal wake delivers the event record only (the turn's operator
554
+ registry already exposes bounded repository reads). Each source's cursor
555
+ advances only through a recorded trigger naming that source, so a crash
556
+ between scan and start cannot skip or duplicate a wake, and other builds' —
557
+ and the journal's — matching events re-trigger on later ticks. Sessions with
558
+ empty wake settings are never woken. The default wake settings span both
559
+ halves of the attention set: the build events `ab watch` filters on by
560
+ default plus the repository-journal events `ab watch --repository` follows.
561
+
562
+ **Crash recovery.** A turn invocation that dies without an outcome leaves the
563
+ session `running` with a silent open stream; the tick's reaper fails such a
564
+ turn (`turn.failed`, kind `internal`) after 15 minutes of stream silence,
565
+ closing the stream `aborted` and leaving the session `idle`. The reaper never
566
+ touches suspended or awaiting-approval sessions — a turn suspended for
567
+ approval is precisely an open stream that goes quiet for however long the
568
+ operator takes.
569
+
570
+ **Typed failures.** Model failures are typed on `turn.failed`:
571
+ `provider-unavailable`, `exhausted`, `credentials`, `configuration` (plus
572
+ `internal` for non-model failures) — never retried unboundedly; a failed
573
+ turn leaves the session `idle` with the error class visible in the session's
574
+ reduced state.
575
+
576
+ **Opt-in guarantee.** The whole feature is off unless a repository's
577
+ effective config enables it, and it is doubly gated: the tick step is
578
+ constructed only in origin mode (a local `ab dispatch` never constructs it —
579
+ enabled = true in a local autobuild.toml lists no sessions, builds no
580
+ registry, and calls no model), and in origin mode it returns immediately
581
+ unless the effective config enables the orchestrator. The system prompt is
582
+ the canonical `ab-operate` skill shipped in `skills/operate/` — self-contained
583
+ per the vendored-skill rule, teaching the tool surface, the attention set,
584
+ and when to escalate to the operator rather than act — plus the current
585
+ `operator-notes` artifact, the session's wake settings, and the session's
586
+ repository identity. The model never supplies a repository argument: the
587
+ tools' model-facing schemas strip `repo`, and the runner injects the
588
+ session's repository before the registry re-validates. A message posted
589
+ while a turn is open is durable and enters the conversation at the next
590
+ resume or turn — no second turn, no concurrent turns, no subagents.
591
+
592
+ #### 7.1.3 Ticket assets
593
+
594
+ A **ticket asset** is a named, versioned bundle of files attached to a ticket —
595
+ a design export, a reference screenshot, a sample payload — that shapes a change
596
+ but belongs neither in the repository (where it goes stale once the change
597
+ ships) nor in the ticket tool (which build agents cannot reach). It is keyed by
598
+ `(repo, ticketId, kind, name)` and works identically whichever ticket source is
599
+ configured. It is a fourth resource family beside builds, repository journals,
600
+ and sessions, but a deliberately thin one: no event log, because nothing here
601
+ needs reduction, and no retention pruning, because assets live as long as the
602
+ ticket's other durable data and never expire on their own.
603
+
604
+ Each put creates a new 0-based **revision**; earlier revisions stay retrievable.
605
+ An asset is a `file` (exactly one file) or a `tree` (a folder: its root is
606
+ implicit, so a tree with zero entries is a valid empty folder). The manifest is
607
+ a list of entries — `file` entries with size and content address, and `dir`
608
+ entries so empty directories survive. File bytes live in the `BlobStore`; the
609
+ database holds only the manifest, the same rule as artifacts. **Removal is a
610
+ tombstone**: a row taking the next revision number with an empty manifest. The
611
+ latest row decides liveness, so the default read and list skip a removed asset,
612
+ while an explicit earlier revision stays readable — a build that pinned revision
613
+ N keeps retrieving it. Re-attaching after a removal takes the next revision, so
614
+ revision numbers may skip the tombstone.
615
+
616
+ Every adapter runs one shared validator before any write. `kind` and `name` are
617
+ `[A-Za-z0-9._-]+` and at most 64 characters, so they are safe to print in a
618
+ command line. Entry paths are relative, `/`-separated, with no empty, `.`, or
619
+ `..` segment, at most 256 bytes each and 128 per segment, with no duplicates and
620
+ no file as an ancestor of another entry. A revision holds at most 1000 entries
621
+ (files plus directories) and **25 MiB** of file bytes; a larger put is refused
622
+ with an error that names the limit, and nothing is stored. A remote deployment
623
+ may advertise a lower effective limit (§7.2): a platform that rejects oversized
624
+ request bodies before application code runs cannot rely on server-side
625
+ validation alone, so the client enforces the advertised limit before uploading.
626
+
467
627
  ### 7.2 Interface and adapters
468
628
 
469
629
  Deliberately narrow: build runners need `append(event)`, `putArtifact`,
@@ -476,11 +636,23 @@ snapshot or side ledger. Sequence 0 names an empty stream, candidates receive
476
636
  ordinary event validation, unknown builds reject, and a comparison miss leaves
477
637
  the log and build timestamps untouched.
478
638
  The same contract has repository-scoped `ensureRepo`, event/artifact deposit
479
- and read methods, plus a repository lease. Before execution, a runner obtains
639
+ and read methods, plus a repository lease. Ticket assets (§7.1) add
640
+ `putTicketAsset`, `getTicketAsset` (latest live revision, or an explicit
641
+ revision), `listTicketAssets` (summaries without manifests: the latest live
642
+ revision per kind and name, or every real revision), `removeTicketAsset`, and
643
+ `ticketAssetLimits(repo)`, the effective ceilings the store accepts. Scoped
644
+ build and session handles reject all of them. Before execution, a runner obtains
480
645
  an interface-enforced handle scoped to exactly its build. Own-build records,
481
646
  stream, artifacts, lease, and subscription remain available; another build,
482
647
  collection/admin operations, nested foreign scope, and every repository-journal
483
- operation reject. This guard applies identically to every adapter; remote tokens
648
+ operation reject. The one ticket-asset read a build handle keeps is
649
+ `getPinnedTicketAsset(slug, kind, name, rev?)`: the asset as that build froze it
650
+ (the latest pinned revision, or any revision the build ever pinned), found by
651
+ reducing the build's own events and then reading that exact revision. It needs
652
+ no lease, so it answers for a finished build, and remote build tokens reach it
653
+ through `GET /builds/:slug/ticket-assets`. `createBuild` optionally takes the
654
+ build's initial `build.created` event and appends it as seq 1 in the same atomic
655
+ step as the record on every adapter. This guard applies identically to every adapter; remote tokens
484
656
  carry the same authority over the wire as defense in depth rather than creating
485
657
  it. Two implementations of one contract (`packages/core/src/store/contract.ts` is the shared
486
658
  conformance suite):
@@ -515,6 +687,27 @@ conformance suite):
515
687
  `subscribe` is specced in the interface; the v2.0 implementation is polling
516
688
  `getEvents(since)`. True push comes later.
517
689
 
690
+ Operator UIs additionally have a repository-scoped batch read,
691
+ `getRepoBuildDigests(repo)`: the projection of every build's log onto its
692
+ terminal fact, its `observation.recorded` occurrences with their event
693
+ timestamps, and the ts of its latest `pr.merged`, derived from the event log on
694
+ each call. It lets a dashboard snapshot count unclaimed observations and gate
695
+ rows without reading the full history of finished builds (§16.1), and it backs
696
+ the harvest-launch pressure gate (dispatcher and harvest runner alike): launch
697
+ decisions evaluate from digests and the repository journal alone, so
698
+ gate-evaluation cost is flat in accumulated build history, with the full
699
+ per-build scan paid only when a gate actually trips and a scan packet is built.
700
+
701
+ Stateless repository-level readers also have a bounded journal read,
702
+ `getRepoStateEvents(repo)` (AUT-489): every durable, slow-growing event type
703
+ (all harvest facts, operator-sandbox facts, and the three dispatcher setting
704
+ types) plus, when the journal has one, the tail from the latest
705
+ `dispatcher.run-started`. Reducing this subset with the state readers yields
706
+ exactly what a full replay yields, so the cost of reading repository state no
707
+ longer grows with the number of past dispatcher invocations. The journal
708
+ itself stays append-only: full replay remains the normative truth, and
709
+ `ab watch` still sees every recorded fact.
710
+
518
711
  ### 7.3 Persistence granularity
519
712
 
520
713
  Required phase outputs persist at **phase/round boundaries**; a killed phase
@@ -782,11 +975,19 @@ A phase-scoped hydration into the gitignored scratch dir:
782
975
  findings.json # current feedback, when round > 1
783
976
  history/ # prior-round artifacts where the phase needs them
784
977
  verify/ # failure reports routed back to implement
978
+ assets/ # the ticket assets the build froze (§6.3), every phase:
979
+ # assets/<kind>/<name>/…, folders keep their trees
785
980
  ```
786
981
 
787
982
  The manifest tells the agent its contract — `required` deposits and
788
983
  `allowedTerminals` — so skills are self-checking against the same data the
789
- CLI validates with. Per-phase inputs and terminals:
984
+ CLI validates with. When the build pinned ticket assets, every phase also gets
985
+ them under `.ab/assets/<kind>/<name>/` and the manifest lists each as
986
+ `assets: [{kind, name, revision, layout, size, path}]`; a `file` asset is the
987
+ single file inside its directory. A build with no assets gets no `assets` key
988
+ and no directory. `ab build asset get <slug> <kind> <name> <dest>` downloads a
989
+ pinned asset by build, also after the build has finished. Per-phase inputs and
990
+ terminals:
790
991
 
791
992
  | Phase | Materialized inputs | Terminal |
792
993
  |---|---|---|
@@ -817,7 +1018,7 @@ Provider/runtime-declared turn failures are a separate `AgentRunner` result
817
1018
  alternates, availability failures (overload, rate limit, 5xx, timeout,
818
1019
  transport, and unclassified provider failure) and provider exhaustion (quota,
819
1020
  usage limit, and billing) start the next target inside the same phase attempt.
820
- Authentication, permission, and local runtime-configuration failures do not.
1021
+ Authentication, permission, and local runtime-configuration failures do not, with one exception: a runtime whose CLI executable is not installed is skipped like any other eligible failure, so the phase runs on the first declared target whose CLI is present. An unsupported installed version is still a stopping configuration failure. If no target's CLI is installed, the chain ends in one non-retried `configuration` failure carrying the last target's missing-CLI message.
821
1022
  Each failed target's transcript is deposited. Only a stopped or exhausted chain
822
1023
  emits one `phase.failed` with the final provider message verbatim; that final
823
1024
  failure controls retry policy. Provider exhaustion skips the retry budget and
@@ -867,7 +1068,12 @@ A no-op step has no push. Agents only ever commit locally. Consequences: forge
867
1068
  credentials **never enter the sandbox** (load-bearing once builds run on
868
1069
  remote sandboxes), history is extended rather than force-pushed, and the
869
1070
  push-at-boundary rule from [D3] is enforced by construction rather than
870
- convention.
1071
+ convention. The same rule bounds the orchestrator's small fixes: an operator
1072
+ sandbox publishes work only through the `sandbox.publish` tool, whose push and
1073
+ PR creation are kernel-side plumbing executed by the sandbox service through
1074
+ the workspace provider's publication capability — the only publication path
1075
+ from an operator sandbox, always to a dedicated operator branch and a pull
1076
+ request, never to the base branch.
871
1077
 
872
1078
  ### 8.7 Walkthroughs
873
1079
 
@@ -912,7 +1118,8 @@ implementer: ab context (findings.json now materialized) → …
912
1118
  ### 8.8 Outer-loop namespace
913
1119
 
914
1120
  Human/pre-build ticket grooming uses one configured-source namespace
915
- (`ab ticket create|update|block|unblock|list|show|move`). These commands are
1121
+ (`ab ticket create|update|block|unblock|list|show|move`, plus the ticket-asset
1122
+ forms below). These commands are
916
1123
  sessionless, source-agnostic, and never available as a mid-build spec
917
1124
  mutation path. Every subcommand accepts `--json` and then writes exactly one
918
1125
  bare value: `list` emits `Ticket[]`; every other form emits the complete
@@ -929,6 +1136,27 @@ creates tickets directly in its ready state must therefore create a dependency
929
1136
  chain in dependency order, carrying each predecessor id into the next create,
930
1137
  before a later ticket can be claimed.
931
1138
 
1139
+ **Ticket assets** (§7.1) attach files to a ticket without touching the ticket
1140
+ source's storage:
1141
+
1142
+ - `ab ticket attach <id> <kind> <path> [--name <name>] [--store <ref>] [--json]`
1143
+ stores a file or a whole folder (its relative tree preserved) as an asset.
1144
+ `--name` defaults to the path's basename. Attaching again under the same kind
1145
+ and name creates a new revision. The CLI asks the source first, so an unknown
1146
+ id stores nothing; it refuses symlinks and non-regular files, and refuses a
1147
+ bundle over the store's limit from `stat` sizes before reading any bytes. The
1148
+ store write precedes the note: if the note then fails, the error says the
1149
+ asset was stored as revision N, and the write is not rolled back.
1150
+ - `ab ticket asset get <id> <kind> <name> <dest> [--rev <n>]` writes the exact
1151
+ bytes of the latest (or a named) revision: a file asset to `<dest>` (or into
1152
+ `<dest>` when it is an existing directory), a folder asset into the directory
1153
+ `<dest>`, empty directories included. Stored paths are re-checked against
1154
+ traversal on write.
1155
+ - `ab ticket asset rm <id> <kind> <name>` removes it from the ticket's current
1156
+ assets; revisions a build has pinned stay retrievable.
1157
+ - `ab ticket show <id>` lists the current assets (kind/name, revision, size);
1158
+ with `--json` the `Ticket` is extended with an `assets` array.
1159
+
932
1160
  Observation harvest uses a separate typed, repository-scoped namespace
933
1161
  (`ab harvest context|submit|verdict|status`), mirroring the build session
934
1162
  commands: context hydration, a producer terminal, a reviewer terminal, and a
@@ -936,7 +1164,7 @@ sessionless read-only status projection.
936
1164
 
937
1165
  `ab repository status [--json] [--store <ref>]` is the sessionless read-only
938
1166
  projection of the repository journal's dispatcher controls: ticket intake, the
939
- repository-wide pause, and the claim-time auto-merge default. It uses the same
1167
+ repository-wide pause, and the auto-merge default. It uses the same
940
1168
  dispatch-settings reducer as dispatcher decisions and the dashboard. An absent
941
1169
  repository stream reduces as an empty journal and reports intake on, repository
942
1170
  pause off, and auto-merge default off; the query does not create that stream,
@@ -990,6 +1218,9 @@ distinction from `no-terminal`. `failure.message` preserves provider text and
990
1218
  and local runtime configuration. `permanent` remains the retry-policy and
991
1219
  legacy-plugin compatibility bit: exhaustion and credential/configuration
992
1220
  failures are permanent, while availability uses existing bounded retry. A
1221
+ `configuration` failure flagged `missingExecutable` (the runtime's CLI is not
1222
+ installed) is the one configuration failure that is alternate-eligible; it is
1223
+ still permanent and never retried. A
993
1224
  legacy plugin may omit `cause`; `permanent: false` remains alternate-eligible
994
1225
  and `permanent: true` remains a stopping failure. A failed `start` still returns
995
1226
  an endable session handle, guaranteeing transcript deposition.
@@ -1078,7 +1309,8 @@ deterministic fail-safe.
1078
1309
  runtime touches only the adapter registry, never the kernel. Mixing models
1079
1310
  across roles is intentional — a different reviewer catches more. The
1080
1311
  Within one agent-session attempt, eligible failures try the primary then each
1081
- alternate in order without spending another phase attempt. Each target has a
1312
+ alternate in order without spending another phase attempt. A runtime whose CLI executable is not installed is skipped the same way;
1313
+ credential, permission, and other configuration failures stop the chain. Each target has a
1082
1314
  fresh session bracket. A failed producer continuation may therefore continue
1083
1315
  on a different runtime only by rehydrating durable context, not by inheriting
1084
1316
  the failed conversation. Selection is non-sticky: every later phase, loop
@@ -1204,6 +1436,10 @@ escalation after an exhausted failed verify report feeds `implement`, where its
1204
1436
  guidance outranks
1205
1437
  the pending report.
1206
1438
 
1439
+ Answering with `--revise-spec-from-ticket` also re-pins the build's ticket assets
1440
+ (§6.3): the sampled set rides on the answer and on `spec.revised`. A supplied
1441
+ body file changes the spec only.
1442
+
1207
1443
  Only an operator answering an open `review-round-limit` policy escalation may
1208
1444
  set `escalation.answered.reviewRoundCeiling`. The same event both clears the
1209
1445
  human gate and replaces the cap; no standalone or pre-emptive form exists. A
@@ -1366,6 +1602,16 @@ posted as a comment, final summary, status transitions) flow outward only.
1366
1602
  This keeps the abstraction honest: a file-based TicketSource with nowhere to
1367
1603
  put blobs must be fully workable.
1368
1604
 
1605
+ **Ticket assets follow the same rule.** The contents of a ticket asset (§7.1)
1606
+ live in the BuildStore and the ticket source never stores them. Attaching,
1607
+ replacing, or removing an asset instead leaves a note on the ticket through the
1608
+ existing `comment()` port — the outward projection every source, including the
1609
+ plugin surface, already supports. The note names the asset, its revision, file
1610
+ count, and size, and gives the `ab ticket asset get` command that fetches it, so
1611
+ a person reading the ticket in Linear, the hosted service, or a local file knows
1612
+ the asset exists. The note is a projection outward only; nothing reads it back,
1613
+ and no TicketSource port or plugin-protocol change is needed.
1614
+
1369
1615
  **Claim lifecycle.** Claim acquires nonterminal work: it returns `true` and
1370
1616
  moves a claimable ticket to the source's claimed state. It returns `false` for
1371
1617
  an unknown ticket, an already-claimed or contested ticket, and a ticket that
@@ -1455,12 +1701,15 @@ signal, and error evidence. `--plain`, non-TTY,
1455
1701
  and `--once` kernel semantics remain the line-oriented/direct compatibility
1456
1702
  path; `--once` still performs one tick and drains its in-flight work.
1457
1703
 
1458
- Durable operator settings (intake, the repository-wide pause, the claim-time
1459
- auto-merge default, the harvest gate) are repository-journal facts: they
1460
- survive restarts, propagate between dispatchers by ordinary polling, and are
1461
- never optimistically rendered — the UI shows acknowledged state. The
1462
- repository-wide pause holds every queued build: while it is set, no dispatcher
1463
- tick may attach a runner to a build that does not have one yet.
1704
+ Durable operator settings (intake, the repository-wide pause, the auto-merge
1705
+ default, the harvest gate) are repository-journal facts: they survive restarts,
1706
+ propagate between dispatchers by ordinary polling, and are never optimistically
1707
+ rendered — the UI shows acknowledged state. The repository-wide pause holds
1708
+ every queued build: while it is set, no dispatcher tick may attach a runner to
1709
+ a build that does not have one yet. The auto-merge default both seeds new
1710
+ claims and, on each change, fans out onto every current non-terminal build of
1711
+ the repository as a bulk request or withdrawal attributed to the toggling
1712
+ operator (§15.7).
1464
1713
 
1465
1714
  The **operator sandbox** is the third execution kind beside build and harvest
1466
1715
  executions: a persistent, credential-free, per-operator-per-repository
@@ -1473,7 +1722,11 @@ is repository-journal fact: provisioned/resumed/activity/stopped/released facts
1473
1722
  bracket one deterministic environment per operator × repository, the
1474
1723
  dispatcher tick's idle settlement stops an environment whose evidence is
1475
1724
  stale (settling an orphan exactly as harvest executions are settled), and
1476
- `ab repository status` reports each operator's sandbox.
1725
+ `ab repository status` reports each operator's sandbox. An operator's work
1726
+ leaves the sandbox only through `sandbox.publish`, which takes the checkout's
1727
+ head (or an explicit commit) to a deterministic operator branch and a pull
1728
+ request against the base branch and records published/publish-failed journal
1729
+ facts.
1477
1730
 
1478
1731
  The operator's job across many concurrent builds: see status at a glance,
1479
1732
  act on a selected build, find blocked builds, answer escalations, and inspect
@@ -1587,9 +1840,9 @@ with illustrative members:
1587
1840
 
1588
1841
  | Family | Examples |
1589
1842
  |---|---|
1590
- | Build lifecycle | `build.created`, `workspace.provisioned`, `workspace.released`, `execution.started`, `execution.ended`, `infrastructure.failed`, `infrastructure.cleanup-attempted`, `dispatch.comment-posted`, `dispatch.failed`, `runner.attached`, `runner.setup-failed`, `abort.remote-branch-deleted`, `abort.local-branch-deleted`, `abort.ticket-returned`, `build.completed` |
1591
- | Operator commands [D2] | `build.pause-requested` → `build.paused`; `build.discard-requested`; `build.auto-merge-requested`; `escalation.answered` (optionally carrying a validated `reviewRoundCeiling`) |
1592
- | Spec | `spec.imported`, `spec.authored`, `spec.revised` |
1843
+ | Build lifecycle | `build.created` (carrying `assets`, the claim-time pin, §6.3), `workspace.provisioned`, `workspace.released`, `execution.started`, `execution.ended`, `infrastructure.failed`, `infrastructure.cleanup-attempted`, `dispatch.comment-posted`, `dispatch.failed`, `runner.attached`, `runner.setup-failed`, `abort.remote-branch-deleted`, `abort.local-branch-deleted`, `abort.ticket-returned`, `build.completed` |
1844
+ | Operator commands [D2] | `build.pause-requested` → `build.paused`; `build.discard-requested`; `build.auto-merge-requested`; `escalation.answered` (optionally carrying a validated `reviewRoundCeiling`, or the `assets` sampled with a revise-from-ticket authorization) |
1845
+ | Spec | `spec.imported`, `spec.authored`, `spec.revised` (optionally carrying `assets`, a re-pin of the ticket's assets, §6.3) |
1593
1846
  | Sessions | `session.started`; `session.ended` with transcript ref and usage (ordinary completion — the analysis corpus), or `session.ended {outcome: reclaimed, reclaimedBy: {instance, resumedFromSeq}}` (explicit transcriptless takeover) |
1594
1847
  | Plan/code loops | `plan.started` … `plan-review.verdict`; `implement.started` … `code-review.verdict` |
1595
1848
  | Verify/finalize | `verify.started {step, attempt, feedback?}`, `verify.completed {step, outcome}`, `finalize.completed {pr}`, `finalize.step-completed {step, ok, headSha?}` |
@@ -1954,13 +2207,18 @@ Decisions here continue the series: **[D9]** declarative repo config and
1954
2207
  One declarative file at the repo root. A running dispatcher owns an accepted
1955
2208
  snapshot read from the **main checkout** and refreshes it before each dispatch
1956
2209
  tick. Before launch and after each accepted revision, the dispatcher deposits
1957
- the composed snapshot into each affected build's artifact namespace. The child
1958
- samples that durable artifact at setup and pipeline-step boundaries, so a valid
1959
- change may affect the next action of an in-flight build but never interrupts an agent turn
1960
- or deterministic command already running. Scoped phase CLI processes separately
2210
+ one composed snapshot into each affected build's artifact namespace: its
2211
+ build-owned sections come from the build's own branch, and its deployment- and
2212
+ dispatcher-owned sections from the live snapshot (the three ownership classes
2213
+ are defined below). The child samples that durable artifact at setup and
2214
+ pipeline-step boundaries, so a valid change to a deployment-owned section may
2215
+ affect the next action of an in-flight build but never interrupts an agent turn
2216
+ or deterministic command already running, and a change to a build-owned section
2217
+ never retargets an in-flight build at all. Scoped phase CLI processes separately
1961
2218
  read the build worktree's branch-owned file. Because the file is repo-versioned,
1962
- changes still flow through the pipeline itself: once merged, the system can
1963
- adopt a retuned configuration without restarting the dispatcher.
2219
+ changes still flow through the pipeline itself: once a build's own branch
2220
+ carries them, the system can adopt a retuned configuration without restarting
2221
+ the dispatcher.
1964
2222
 
1965
2223
  A missing or unreadable file and a malformed or routing-invalid candidate are
1966
2224
  rejected atomically: the last valid snapshot, resolver, and revision remain in
@@ -2072,6 +2330,35 @@ existing ticket table as factory config, and fails with the complete available
2072
2330
  name set when unregistered. Plugin-declared `requiredEnv` values are never TOML
2073
2331
  fields: secrets stay in the process environment or local `.env`.
2074
2332
 
2333
+ `autobuild.toml` sections belong to one of three ownership classes, which
2334
+ decide where a build's effective configuration is composed from:
2335
+
2336
+ | Class | Sections | Source at deposit |
2337
+ |---|---|---|
2338
+ | Build-owned (pinned) | `[verify]` and `[verify.<step>]`, `[finalize]` and `[finalize.<step>]`, `[commands]`, `[workspace]` (provider and config, including `provisioning` and `runtimeProvisioning`) | The build's own branch: its branch head once it has published commits, otherwise the recorded base commit its workspace was cut from. The exact commit is recorded once per deposit in the effective-config artifact metadata — except in checkout mode when the worktree read cannot be attributed to the branch-head commit: because the worktree file has uncommitted edits, because the clean/dirty comparison could not be performed (non-git workspace or a transient git error), or because the branch head never carried the file. The deposit then records the source `worktree-dirty` and no commit, because no commit claims the worktree bytes (the build still runs them). |
2339
+ | Deployment-owned (live) | `[roles]` (runtime, model, args, alternates, per-role `sessionBudgetSeconds`) and `[policy]` (budgets, retry bounds, stall and harvest knobs) | The dispatcher's live snapshot; an accepted reload delivers these to active builds. |
2340
+ | Dispatcher-owned | `baseBranch`, `capacity`, `forge`, `plugins`, `pr`, `tickets`, `orchestrator` | The dispatcher's live snapshot; the build runner never interprets them. |
2341
+
2342
+ A build's pipeline definition is therefore composed from the build's own
2343
+ branch: in origin mode the dispatcher reads `autobuild.toml` at the build's
2344
+ recorded base commit (or its branch head once the build has published commits)
2345
+ through the forging adapter; in checkout mode it reads the workspace's file. The
2346
+ effective-config artifact records the commit the build-owned sections were
2347
+ taken from — except in checkout mode when the worktree read cannot be
2348
+ attributed to the branch-head commit: because the worktree file has uncommitted
2349
+ edits, because the clean/dirty comparison could not be performed (non-git
2350
+ workspace or a transient git error), or because the branch head never carried
2351
+ the file. The artifact then records the source `worktree-dirty` and no commit,
2352
+ because attributing worktree bytes to a commit that has not been shown to carry
2353
+ them would be false provenance. A base-branch change to `[verify]`, `[finalize]`, `[commands]`, or
2354
+ the workspace provisioning after a build's base was cut does not change that
2355
+ build's verify universe, finalize steps, commands, or provisioning; a build
2356
+ that publishes such a change to its own branch picks it up at its next launch.
2357
+ A live reload delivers only deployment-owned sections to active builds, so the
2358
+ artifact deposited on reload changes only those sections. In-flight
2359
+ environments are never re-provisioned; a fresh build from the new base is the
2360
+ path for a changed provisioning definition.
2361
+
2075
2362
  Two configurable narrowing mechanisms govern which verify steps run, both
2076
2363
  resolving to the ordinary `skipped` outcome so exclusions stay queryable:
2077
2364
 
@@ -19,6 +19,12 @@ try {
19
19
  if (input === undefined) process.exit(2)
20
20
 
21
21
  let exitCode = 0
22
+ // Guest tolerance (AUT-517): an environment-supervised build child is
23
+ // structurally a guest — it never constructs workspace providers — so a
24
+ // configured provider plugin it cannot resolve is skipped with a notice
25
+ // instead of failing the build. A local-parent child runs on the host with
26
+ // the repository's full dependency tree, so it stays strict.
27
+ const guest = input.supervision.kind === 'environment'
22
28
  if (input.supervision.kind === 'local-parent') {
23
29
  // Every local exit first transfers group teardown to an owner outside this
24
30
  // session. Environment-owned execution deliberately installs neither this
@@ -30,7 +36,7 @@ if (input.supervision.kind === 'local-parent') {
30
36
  watchBuildParent(input.supervision.parentPid, () => terminal.terminate(143)),
31
37
  )
32
38
  try {
33
- await runBuildChild(input, process.env)
39
+ await runBuildChild(input, process.env, undefined, { guest })
34
40
  } catch {
35
41
  exitCode = 1
36
42
  }
@@ -39,7 +45,7 @@ if (input.supervision.kind === 'local-parent') {
39
45
  // Vercel owns teardown of the complete VM session. Addressing a POSIX group
40
46
  // here could leave the SDK believing a still-running environment completed.
41
47
  try {
42
- await runBuildChild(input, process.env)
48
+ await runBuildChild(input, process.env, undefined, { guest })
43
49
  } catch {
44
50
  exitCode = 1
45
51
  }