@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
package/README.md CHANGED
@@ -24,7 +24,7 @@ needs an authenticated [`gh` CLI](https://cli.github.com) or an exported
24
24
  <!-- release-install:start -->
25
25
 
26
26
  ```sh
27
- bun add -g @defrex/autobuild@0.12.0
27
+ bun add -g @defrex/autobuild@0.14.0
28
28
  ```
29
29
 
30
30
  <!-- release-install:end -->
package/SPEC.md CHANGED
@@ -432,6 +432,13 @@ body file leaves the pinned assets unchanged. The one case this cannot cover is
432
432
  a record an older dispatcher created with an empty stream: recovery can only
433
433
  sample the ticket's current assets for it.
434
434
 
435
+ For hosted tickets, exact sampled revisions are copied into immutable repository
436
+ artifacts in the destination build store before pins are published. Pins carry
437
+ explicit manifest references; bounded binary chunks preserve files and empty
438
+ directories without contacting the ticket service during build reads. Original
439
+ ticket revision numbers are preserved. Old pins without snapshot references
440
+ continue reading their existing ticket rows.
441
+
435
442
  ## 7. The build store
436
443
 
437
444
  The v1 repo-bloat and no-observability problems share one root cause: the
@@ -450,7 +457,7 @@ builds id/slug, ticket ref, repo, branch, status (derived), created/update
450
457
  lease + heartbeat (mutable liveness columns — §15.2.6, never events)
451
458
  events build_id, seq, timestamp, actor, type, payload (JSON) — append-only
452
459
  artifacts build_id, kind, revision, blobRef, metadata
453
- repo_streams repo, created/updated, lease + heartbeat
460
+ repo_streams repo, created/updated, lease + heartbeat, bounded tick observation
454
461
  repo_events repo, seq, timestamp, actor, type, payload (JSON) — append-only
455
462
  repo_artifacts repo, kind, revision, blobRef, metadata
456
463
  ticket_assets repo, ticket_id, kind, name, revision, layout, manifest (JSON), size,
@@ -618,15 +625,23 @@ resume or turn — no second turn, no concurrent turns, no subagents.
618
625
 
619
626
  A **ticket asset** is a named, versioned bundle of files attached to a ticket —
620
627
  a design export, a reference screenshot, a sample payload — that shapes a change
621
- but belongs neither in the repository (where it goes stale once the change
622
- ships) nor in the ticket tool (which build agents cannot reach). It is keyed by
628
+ but belongs outside the repository, where it would go stale once the change ships. It is keyed by
623
629
  `(repo, ticketId, kind, name)` and works identically whichever ticket source is
624
630
  configured. It is a fourth resource family beside builds, repository journals,
625
631
  and sessions, but a deliberately thin one: no event log, because nothing here
626
632
  needs reduction, and no retention pruning, because assets live as long as the
627
633
  ticket's other durable data and never expire on their own.
628
634
 
629
- Each put creates a new 0-based **revision**; earlier revisions stay retrievable.
635
+ Hosted ticket assets live in the ticket service's repository-keyed storage,
636
+ regardless of where builds run or which BuildStore is selected. File tickets use
637
+ native portable storage composed with legacy BuildStore attachments; Linear
638
+ tickets retain BuildStore storage. Existing service assets need no migration;
639
+ old local hosted-ticket attachments are not moved or used as a fallback.
640
+
641
+ Each put creates a new increasing **revision**; earlier revisions stay retrievable.
642
+ Adapters normally start at 0. Native file assets reserve revisions from
643
+ 1,000,000,000 upward so explicit reads of old BuildStore revisions remain
644
+ unambiguous when native and legacy assets are composed.
630
645
  An asset is a `file` (exactly one file) or a `tree` (a folder: its root is
631
646
  implicit, so a tree with zero entries is a valid empty folder). The manifest is
632
647
  a list of entries — `file` entries with size and content address, and `dir`
@@ -1071,9 +1086,9 @@ kind and operation.
1071
1086
 
1072
1087
  Ticket admission uses the credential project. Hosted tickets, comments, blockers, creation-key
1073
1088
  deduplication and assets are partitioned by registered repository, never team.
1074
- Lifecycle comes from that project's latest durable effective configuration
1075
- (triage Backlog, claim Doing, completion Done by default; configurable
1076
- `doneState`). Owner admin ticket requests explicitly select a registered
1089
+ Lifecycle comes from independent project ticket policy (triage, ready, doing, done
1090
+ by default; configurable doneState). Legacy projects without independent policy
1091
+ use their uploaded configuration with Backlog / Doing / Done defaults. Owner admin ticket requests explicitly select a registered
1077
1092
  repository. The service has no Linear ticket backend: external tracker and
1078
1093
  file sources remain dispatcher-side, and service ticket surfaces refuse those
1079
1094
  projects with actionable source feedback. Legacy team queues migrate atomically
@@ -1273,9 +1288,10 @@ implementer: ab context (findings.json now materialized) → …
1273
1288
 
1274
1289
  Human/pre-build ticket grooming uses one configured-source namespace
1275
1290
  (`ab ticket create|update|block|unblock|list|show|move`, plus the ticket-asset
1276
- forms below). These commands are
1291
+ forms below). Migration adds `ab ticket export <dir>` and `ab ticket import <dir>`
1292
+ (see [Ticket source migration](#ticket-source-migration)). These commands are
1277
1293
  sessionless, source-agnostic, and never available as a mid-build spec
1278
- mutation path. Every subcommand accepts `--json` and then writes exactly one
1294
+ mutation path. The CRUD subcommands accept `--json` and then writes exactly one
1279
1295
  bare value: `list` emits `Ticket[]`; every other form emits the complete
1280
1296
  created, read, or post-mutation `Ticket`. Human-readable confirmations are not
1281
1297
  a data format and callers must not parse ids from them.
@@ -1290,8 +1306,10 @@ creates tickets directly in its ready state must therefore create a dependency
1290
1306
  chain in dependency order, carrying each predecessor id into the next create,
1291
1307
  before a later ticket can be claimed.
1292
1308
 
1293
- **Ticket assets** (§7.1) attach files to a ticket without touching the ticket
1294
- source's storage:
1309
+ **Ticket assets** (§7.1) attach files to a ticket. Hosted tickets use their
1310
+ ticket service storage independently of the selected build store; file tickets use
1311
+ native storage with legacy fallback, and sources without native assets use the
1312
+ selected BuildStore:
1295
1313
 
1296
1314
  - `ab ticket attach <id> <kind> <path> [--name <name>] [--store <ref>] [--json]`
1297
1315
  stores a file or a whole folder (its relative tree preserved) as an asset.
@@ -1762,9 +1780,10 @@ event-level mechanics live in the repository catalog and reducer tests):
1762
1780
  repository; an owner selects a registered repository explicitly. Team and state
1763
1781
  fields from clients convey no authority or lifecycle policy. Tickets, comments,
1764
1782
  blockers, creation-key deduplication and assets are isolated by repository.
1765
- The project's latest effective configuration supplies hosted lifecycle names:
1766
- triage Backlog, claimed Doing, completion Done (`doneState`) and create/proposal
1767
- equal to triage by default; `readyState` is required. Configured roles may overlap.
1783
+ Independent project ticket policy supplies hosted lifecycle names: triage, ready,
1784
+ doing, done (`doneState`), and create/proposal equal to triage by default. Hosted
1785
+ readyState defaults to ready; other sources require it. Legacy clients without
1786
+ independent policy use uploaded dispatcher config and its old defaults. Configured roles may overlap.
1768
1787
  The service has no Linear backend and never relays external ticket sources.
1769
1788
  Projects using Linear, files or plugins keep their direct dispatcher connection;
1770
1789
  service ticket surfaces refuse with project/source feedback instead of empty data.
@@ -1780,14 +1799,16 @@ This keeps the abstraction honest: a file-based TicketSource with nowhere to
1780
1799
  put blobs must be fully workable.
1781
1800
 
1782
1801
  **Ticket assets follow the same rule.** The contents of a ticket asset (§7.1)
1783
- live in the BuildStore and the ticket source never stores them. Attaching,
1802
+ live in the ticket service for hosted tickets, independently of build storage;
1803
+ file sources keep portable bundles and source-local history, while sources without
1804
+ native assets keep them in the BuildStore. Attaching,
1784
1805
  replacing, or removing an asset instead leaves a note on the ticket through the
1785
1806
  existing `comment()` port — the outward projection every source, including the
1786
1807
  plugin surface, already supports. The note names the asset, its revision, file
1787
1808
  count, and size, and gives the `ab ticket asset get` command that fetches it, so
1788
1809
  a person reading the ticket in Linear, the hosted service, or a local file knows
1789
1810
  the asset exists. The note is a projection outward only; nothing reads it back,
1790
- and no TicketSource port or plugin-protocol change is needed.
1811
+ and existing sources and plugins need no asset capability.
1791
1812
 
1792
1813
  **Claim lifecycle.** Claim acquires nonterminal work: it returns `true` and
1793
1814
  moves a claimable ticket to the source's claimed state. It returns `false` for
@@ -1905,6 +1926,24 @@ head (or an explicit commit) to a deterministic operator branch and a pull
1905
1926
  request against the base branch and records published/publish-failed journal
1906
1927
  facts.
1907
1928
 
1929
+ Successful dispatcher ticks with no effects, counters, diagnostics, queue change,
1930
+ or attention transition append no tick boundary events. The first queue baseline,
1931
+ failed ticks, meaningful ticks, diagnostic clearing, failure recovery, and the
1932
+ first retry after a source cooldown remain recorded. Recorded starts may arrive
1933
+ with completion; optional `startedAt` preserves the actual start time, falling
1934
+ back to envelope time for historical journals.
1935
+
1936
+ Repository tick telemetry is bounded mutable operational metadata, like leases.
1937
+ `beginRepoTick` persists the open boundary before unsafe work; `finishRepoTick`
1938
+ atomically replaces it with actual completion time and outcome and appends any
1939
+ validated tick boundaries. Both are fenced by the unexpired repository lease,
1940
+ and neither renews it. Quiet completions advance telemetry without advancing the
1941
+ journal. Readers refresh telemetry independently of event cursors, correlate it
1942
+ by run, and merge with historical completions by actual time; heartbeat never
1943
+ stands in for tick completion. Telemetry survives lease release. Safe shutdown
1944
+ uses the matching run's open observation, historical journal boundaries when
1945
+ absent, and refuses forced termination when the observation cannot be read.
1946
+
1908
1947
  The operator's job across many concurrent builds: see status at a glance,
1909
1948
  act on a selected build, find blocked builds, answer escalations, and inspect
1910
1949
  any build's trail. Each dashboard build row spends its header on identity and
@@ -2230,6 +2269,21 @@ adapter never re-cuts it from a newer base (§7.4). `ab context` rehydrates
2230
2269
  scratch from the store into a fresh session. Uncommitted round-2 work is lost by
2231
2270
  design (§7.3 — phase boundaries are the resume points).
2232
2271
 
2272
+ The dispatcher also applies a fixed, non-configurable limit of three consecutive
2273
+ executions that complete normally with exit code zero and no durable build
2274
+ progress. Execution start/end, runner attachment and workspace lifecycle
2275
+ bookkeeping do not count as progress. Pipeline, session, spec, publication/PR,
2276
+ observation and acknowledged operator facts reset the streak; infrastructure,
2277
+ budget and provider failures do not consume the allowance and retain their own
2278
+ retry limits. Before a fourth actionable launch, the dispatcher raises one
2279
+ setup-targeted `execution-no-progress` policy escalation naming the count and
2280
+ the next action it expected the runner to take. Waiting, paused and terminal
2281
+ builds are left parked, and pending operator commands remain executable.
2282
+ Answering that escalation with guidance or a bare `ab answer <slug>` retry grants
2283
+ three fresh executions. The guard is derived from durable facts, including on
2284
+ supervisor restart, and uses the ordinary watch, dashboard and operator API
2285
+ attention paths.
2286
+
2233
2287
  Two liveness rules complete the picture. Within one dispatcher process,
2234
2288
  build-runner launches are single-flighted by slug through supervised process
2235
2289
  liveness — execution completion means the complete environment has been reaped,
@@ -2781,3 +2835,71 @@ or installs releases in the foreground or background.
2781
2835
  enforcement and will colocate later session-authenticated human routes.
2782
2836
  Artifacts remain content-by-value for now; direct-to-blob references are a
2783
2837
  possible later protocol revision, not a second current deployment path.
2838
+
2839
+ ## Revision-checked ticket writes
2840
+
2841
+ Every ticket read includes an opaque `revision`. Pass it as `expectedRevision`
2842
+ (on MCP/operator requests) or `--expected-revision <opaque>` on `ab ticket update`,
2843
+ `move`, `block` and `unblock`. A stale write fails without changing anything and
2844
+ reports the current revision. Omission preserves unguarded behavior. Blocker
2845
+ checks cover the entire batch. Dispatcher moves remain unguarded and change the
2846
+ revision. Sources without guarded writes, including Linear, refuse supplied
2847
+ revisions rather than ignoring them.
2848
+
2849
+ `ab ticket edit <id>` opens `$VISUAL`, falling back to `$EDITOR`, using TOML
2850
+ frontmatter and a Markdown body. Only title, body and labels are editable;
2851
+ id, blockers, creation key and reference comments are read-only. It saves only
2852
+ changed fields with the opening revision. Unchanged or formatting-only edits
2853
+ write nothing. On conflict, invalid input or editor failure, the error names an
2854
+ absolute file path containing the edited text. Read the current ticket and
2855
+ reapply the intended changes before retrying; conflicts are never merged.
2856
+
2857
+ File and Linear revisions fingerprint title, body, canonical label/blocker sets
2858
+ and state; reverting identical content restores its fingerprint. File checks
2859
+ re-read immediately before writing, with the existing no-lock race window.
2860
+ Direct file edits and state-directory moves change revisions. File revisions also
2861
+ cover structured comments and portable attachments. Legacy file comments change
2862
+ the body; structured comments preserve it. Separate hosted comments do not change
2863
+ ticket revisions. Hosted revisions
2864
+ are monotonic decimal bigint strings, and checks run under the same row lock
2865
+ and transaction as all requested changes. Hosted reads return a coherent
2866
+ snapshot of fields, blockers and revision.
2867
+
2868
+ ### Independent hosted ticket service
2869
+
2870
+ `[tickets] url` selects hosted ticket transport independently of BuildStore selection;
2871
+ without it, tickets use the selected remote build store. Local builds require an
2872
+ explicit ticket URL. Project registration and ticket policy publication through
2873
+ `ab connect --tickets URL` work before dispatch. Policy is a durable per-registration
2874
+ record, outside BuildStore events/artifacts; legacy uploaded configuration remains
2875
+ a compatibility fallback. Hosted lifecycle defaults are triage/ready/doing/done.
2876
+ Independent harvest uses revocable, expiring, service-bound ticket grants with
2877
+ read/create runner authority and read-only child authority. Build state and harvest
2878
+ journals remain exclusively in the selected BuildStore.
2879
+
2880
+ ### Ticket source migration
2881
+
2882
+ `ab ticket export <dir>` enumerates all states through TicketSource and writes a
2883
+ portable file tracker atomically to an absent or empty destination.
2884
+ `ab ticket import <dir>` preflights the entire tracker and target inventory, then
2885
+ creates or adopts tickets, maps blockers, deduplicates comments by exact body and
2886
+ restores current attachments. Both run sessionless without forge access.
2887
+
2888
+ TicketCreateOptions includes optional `requestedId`. Unsupported sources reject
2889
+ it. File keeps safe ids exactly; hosted preserves canonical own-team positive
2890
+ numbers and advances its counter above every occupied number. Creation-key
2891
+ adoption precedes collision checks. Known kept-number clashes stop import before
2892
+ any target mutation. Stable migration identities and atomic mapping files make
2893
+ interrupted imports resumable without content-based adoption.
2894
+
2895
+ File frontmatter additionally supports optional structured `comments` (body,
2896
+ createdAt, optional author) and `attachments` (kind, name, layout, entries with
2897
+ file base64 bytes or directory paths). Legacy absent fields retain old behavior;
2898
+ new creates use structured comments. Native file assets retain historical
2899
+ revisions in hidden source-local metadata and compose with legacy BuildStore
2900
+ assets. Build claims and spec re-pins freeze native assets into durable snapshots.
2901
+
2902
+ State normalization and the complete operator procedure are documented in
2903
+ [Ticket migration](docs/ticket-migration.md). The same reference is delivered in
2904
+ the installed guide. Export keeps lifecycle membership without inferring roles;
2905
+ import reuses unique case-insensitive states and creates normalized missing ones.
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env bun
2
+ import { readDispatcherRunFacts } from '../packages/core/src/store/reduced-reads'
2
3
  /** Private, shipped child for the interactive `ab dispatch` kernel. It owns no
3
4
  * terminal handles; the public command launches it with immutable options. */
4
5
  import { abDispatch } from '../packages/core/src/cli/dispatch'
@@ -61,12 +62,9 @@ try {
61
62
  const opened = openStoreForRepoState(state, { env: process.env })
62
63
  try {
63
64
  await opened.store.ensureRepo(state.repo)
64
- const events = await opened.store.getRepoEvents(state.repo)
65
- if (
66
- !events.some(
67
- (event) => event.type === 'dispatcher.run-stopped' && event.payload.run === options.run,
68
- )
69
- ) {
65
+ const facts = (await readDispatcherRunFacts(opened.store, state.repo, options.run, false))
66
+ .value
67
+ if (!facts.stop) {
70
68
  await opened.store.appendRepo(state.repo, {
71
69
  actor: DISPATCHER,
72
70
  type: 'dispatcher.run-stopped',
@@ -145,9 +145,9 @@ by a later advance. Each reducer publishes a `version`; bump it whenever the
145
145
  accumulator shape or the fold semantics change, because a cached accumulator is
146
146
  only valid for the version that wrote it. `kernel/reducer-registry.ts` lists
147
147
  every reducer with its version, and its snapshot test fails when an accumulator
148
- shape changes without a bump. Most callers still read whole logs (the terminal
149
- dashboard poll advances a carried build accumulator by per-build deltas; the
150
- dispatcher view below restores from stored accumulators).
148
+ shape changes without a bump. Current-state consumers use the typed persisted
149
+ read helpers in `store/reduced-reads.ts`; whole-array reducers remain pure
150
+ reference projections for tests and explicit historical output.
151
151
 
152
152
  **Reducer snapshots.** The `BuildStore` port can keep, per scope (one build log
153
153
  or the repository journal) and registered reducer, that reducer's accumulator as
@@ -173,72 +173,86 @@ reads every snapshot as missing and replays (SQLSTATE `42P01` is never an
173
173
  error), and an older client never asks. Run `bun run postgres:migrate` once to
174
174
  gain the table; until then the dispatcher and every other reader replay in full.
175
175
 
176
- **Dispatcher repository view.** A dispatcher process builds its view of the
177
- repository once and refreshes it by deltas
178
- (`packages/core/src/processes/repo-view.ts`). `RepoViewStore` decorates the
179
- `BuildStore` port: the first `refresh()` reads the journal's high-water mark
180
- (`getRepoHighWater`), the bounded journal subset, the build listing and digests,
181
- and the full log of every build that still has work (the same predicate the tick
182
- loops use); every later `refresh()` reads only events newer than its cursors —
183
- one journal delta, one delta per held build, plus the listing and digest reads
184
- that discover new builds. Every tick stage reads and writes through the view, so
185
- an event a stage appends is folded in at once and later stages in the same tick
186
- see it with no re-read; a foreign append interleaved with ours leaves a seq gap
187
- that marks the log dirty, a lost compare-and-set does the same, and any delta
188
- that is not contiguous from its cursor discards the log and re-reads it in full,
189
- so no event is skipped. Every state transition of one source (a build, the
190
- journal) runs on that source's single promise chain — snapshot and delta reads,
191
- window opening and widening, the fold after an own append, a lost
192
- compare-and-set's invalidation — and readers queue on it too, so overlapping
193
- readers join the repair in flight instead of observing half-applied state.
194
- Decisions that deliberately re-read at the last moment
195
- (`refreshBuild`) take an explicit delta. Cursor-bearing readers such as the
196
- orchestrator wake pass get per-source windows that hold only events above the
197
- lowest session cursor. A `--once` invocation builds the view cold and reads each
198
- log once; a long-running dispatcher pays that replay at its first tick and not
199
- again.
200
-
201
- The view restores from reducer snapshots on cold start. The journal comes from
202
- the `journalView` reducer (the journal pruned on arrival: durable types plus the
203
- tail from the latest `dispatcher.run-started`); each work build comes from one
204
- `work` snapshot row — the `build`, `logIndex`, `openExecution`,
205
- `openBuildWorkspace`, `publicationState`, `lastExecutionOutcome`, `buildDigest`,
206
- `dashboardFacts` and `currentDeferralObservation` accumulators at a single
207
- cursor. One row is one atomic write on every adapter, so a reader in another
208
- process sees the old bundle or the new, never a mix; the bundle version is
209
- derived from the component reducer versions, so a stale bundle misses and the
210
- build is replayed once. The view
211
- reads only the events newer than that cursor and holds, for a snapshot-backed
212
- build, just that delta plus the reduced state of everything before it, so a tick
213
- with no new events reads zero event rows (apart from the discovery listing and
214
- digests) and one with M new events reads exactly M. Tick stages that run for every
215
- build each tick take `buildFacts(slug)` (state, log index, open execution, open
216
- workspace, publication view) instead of the event array, and read raw history
217
- only on the branches that act on it. `persistSnapshots()` writes the advanced
218
- state back at the end of each tick and on shutdown — best effort, throttled in a
219
- resident dispatcher, always run by a cold single-tick process — and never writes
220
- a cursor that was not read from the log.
221
-
222
- **Operator reads.** The web dashboard, the repository and Harvest status
223
- routes, the operator build listings, and the `ab watch`/`ab wait` discovery
224
- passes derive from that same view instead of replaying the log
225
- (`packages/core/src/operator/read-view.ts`). A dashboard row renders from the
226
- `BuildState` plus the `dashboardFacts` reducer
227
- (`packages/core/src/cli/dashboard/facts.ts`: queued-row dispatch text, session
228
- brackets, phase intervals, pause time, and the per-round loop-output seqs) and
229
- the auto-merge deferral ledger; `projectBuild(…, events)` is the same projection
230
- over the whole-array reduction, so the snapshot and replay paths cannot differ.
231
- Within one serving process, passes are single-flight per repository: requests
232
- arriving while a pass runs join the one trailing pass, which restores from the
233
- snapshot the previous pass persisted. A poll at idle therefore reads no event
234
- rows beyond snapshot lookups and build discovery, and a poll after M new events
235
- reads those M rows once, however many tabs or watchers the process serves.
236
- Serving processes do not coordinate: with P of them, each may read the same M
237
- rows once, so duplicate reads are bounded by P and never by history, tabs, or
238
- watchers. A settled build listed under `--all` is restored from the snapshot the
239
- dispatcher left at settlement, or read once and then persisted. The first
240
- operator poll after upgrading replays each work build once, because the stored
241
- bundle layout changed.
176
+ **Named reducer values.** `BuildStore.getReducerValue(scope, {reducer, args?})`
177
+ returns `{version, cursor, value}` for each eligible entry in the executable
178
+ reducer registry. Eligible reducers consume build or repository events and finish
179
+ as plain JSON; raw journal-event lists, operator-session reducers, collections,
180
+ selector functions, and top-level optional results are explicitly excluded with
181
+ reasons. Factory arguments are validated and participate in a bounded digest
182
+ cache key. The persisted envelope also records the canonical scope, arguments,
183
+ name and version, and nested accumulator shapes are validated before restore.
184
+ The `value.*` namespace is reserved: public snapshot reads and writes reject it,
185
+ and the engine persists through private adapter hooks. Versioned cache keys
186
+ exclude checkpoints produced while the old namespace was publicly writable.
187
+ `tools/reducer-value-shapes.ts` regenerates those structural validators from the
188
+ declared accumulator types.
189
+
190
+ Memory, SQLite and Postgres use the same algorithm: capture the authoritative
191
+ high-water, load persisted state on every call, fold the indexed contiguous delta
192
+ through that boundary, persist, and return an independent JSON value. There is
193
+ no process-local accumulator cache. A foreign checkpoint newer than the initial
194
+ boundary causes a fresh high-water check. Normal writes are monotone; corrupt,
195
+ wrong-version or ahead-of-log rows are repaired only by comparing the exact row
196
+ observed, atomically with the log boundary. A losing writer still returns its
197
+ correct prefix. Missing or unreadable cache state replays; log failures propagate.
198
+
199
+ The remote adapter makes one value request; only the finished answer crosses the
200
+ wire. The service uses named effective-configuration references for hosted ticket
201
+ operations (including policy publication) and semantic build/harvest lifecycle
202
+ facts for delegated authentication and issuance. Lifecycle snapshots keep session,
203
+ execution and terminal evidence, including historical endings, without dispatcher
204
+ tick tails. Anchor membership follows the contiguous log prefix. The legacy
205
+ state-events route remains a deprecated compatibility endpoint. Public generic
206
+ snapshot routes remain compatible, but current consumer reads do not restore the
207
+ former dispatcher work bundles.
208
+
209
+ **Dispatcher repository view.** `RepoViewStore`
210
+ (`packages/core/src/processes/repo-view.ts`) coordinates repository metadata and
211
+ source promise chains. Its build and journal facts come from persisted reduced
212
+ values. Reads after an append or lost conditional append consult that boundary
213
+ again, so no process-local memo suppresses a required delta. The adapter owns
214
+ cache persistence and recovery; the view neither writes work bundles nor hydrates
215
+ whole logs for decisions. Discovery uses persisted build digests, including for
216
+ settled builds. Raw methods forward actual cursor event output.
217
+
218
+ JSON-safe companion projections preserve collection and optional-value contracts.
219
+ The build facts projection returns one prefix for engine routing; its log-index
220
+ rounds and finalize steps reconstruct Maps and Sets explicitly, while publication
221
+ selectors reconstruct from a JSON ledger. Repository facts retain configuration,
222
+ control, Harvest, and dispatcher run correlations. Lease metadata and clocks stay
223
+ outside event reducers: liveness combines durable run facts with the current
224
+ repository record and observation time, including an older lease holder hidden
225
+ by a newer run. The shared repository value keeps a bounded recent-run window, including the
226
+ latest started and stopped runs. Fresh invocations use those shared facts for
227
+ first-tick publication, liveness and supervision without creating a cold
228
+ parameterized cache. Older holder correlations can use `dispatcherRunFacts` for
229
+ the requested historical run. A never-started fresh invocation does not request
230
+ historical correlation. Stopped dispatcher history is not copied into every
231
+ current read.
232
+
233
+ **Operator and CLI reads.** Build listings and details, repository and Harvest
234
+ status, dispatcher decisions, operator API queries, MCP delegation and web
235
+ dashboard refreshes use the same typed persisted read helpers. Shared projection
236
+ functions render those facts; whole-array overloads remain reference oracles.
237
+ Operator passes are single-flight per repository, with requests arriving during a
238
+ pass queued for one trailing pass. Fresh readers use persisted cache rows too;
239
+ steady-state event-row cost depends on newly appended events at fixed build/run
240
+ cardinality, not on log length. Reduced collections may legitimately grow in JSON
241
+ size. Cache loss or corruption can replay history once to recover the same answer.
242
+
243
+ Watch and wait register from a compound persisted `buildStreamSeed` at cursor C.
244
+ Its build accumulator, deferral ledger and insertion-ordered historical PR
245
+ eligibility describe that same prefix. Ordinary polling, including the first
246
+ batch on an old build, reads only events after the cursor and advances that seed
247
+ event by event, preserving state at event N in each emitted record. Watch uses
248
+ historical eligibility for baseline suppression; wait retains its separate
249
+ current-attention registration policy. Only an explicit `--since` older than C
250
+ reconstructs a historical prefix once in `cli/historical-build-state.ts`.
251
+ Actual event listings, transcripts and cursor streams remain raw reads. The
252
+ interactive terminal frontend retains its independently established incremental
253
+ feed. `tools/persisted-reduced-reads.test.ts` scans production TypeScript calls
254
+ and maintains precise, reason-bearing exceptions for these boundaries and pure
255
+ reference projections; stale exceptions fail the check.
242
256
 
243
257
  **Session Store authority.** `packages/core/src/cli/binary.ts` validates a complete build or
244
258
  Harvest ambient tuple before opening its phase Store through
@@ -364,6 +378,10 @@ once. Guidance answering the policy escalation after failed-report exhaustion
364
378
  instead routes to `implement` and outranks the pending report. A bare retry carries no
365
379
  feedback.
366
380
 
381
+ The dispatcher launch boundary uses `processes/execution-no-progress.ts` to
382
+ project clean empty execution streaks through `RepoViewStore` snapshots and
383
+ conditionally raise an ordinary setup policy escalation before a fourth launch.
384
+
367
385
  **Launch ownership.** `packages/core/src/cli/dispatch.ts` is the kernel owner and
368
386
  single-flights supervised build executions per slug within its process. It
369
387
  publishes effective config into the build artifact namespace, claims the exact
@@ -383,7 +401,19 @@ SIGTERM restore terminal modes immediately and begin one supervised drain;
383
401
  persistent frontend and child handlers keep repeated signals graceful until the
384
402
  kernel child is reaped, after which a non-SIGINT terminating signal is replayed
385
403
  in the frontend. A parent-liveness watch remains the detached-kernel fallback.
386
- Timeout escalation is held while a durable `tick-started` fact remains open,
404
+ The repository record carries constant-size, lease-holder-fenced tick telemetry:
405
+ `beginRepoTick` opens the unsafe boundary, and `finishRepoTick` atomically closes
406
+ it and appends selected tick facts. Successful unchanged ticks append nothing;
407
+ queue transitions, counters, diagnostics, attention resets/retries, and failures
408
+ retain their journal evidence. Buffered starts preserve actual `startedAt` for
409
+ attention cooldowns; old starts use their envelope time. Liveness reads refresh
410
+ the repository record even with an unchanged event cursor, merging matching-run
411
+ completion samples with journal ticks. Heartbeat renewal cannot invent progress.
412
+ The memory, SQLite, Postgres and remote adapters share this contract, including
413
+ no mutation after lease expiry and no partial publication on validation failure.
414
+
415
+ Timeout escalation is held while the matching repository tick observation is open
416
+ (with historical `tick-started` journal fallback when no observation exists),
387
417
  because force-killing the ticket claim before build creation would not be
388
418
  recoverable. After a build child claims a takeover lease, it reduces one
389
419
  captured event prefix and appends transcriptless reclaimed `session.ended`
@@ -771,17 +801,24 @@ selected through the plugin ticket-source registry, the real filesystem and
771
801
  local-git adapters, the injected Claude and Codex CLI subprocess contracts,
772
802
  and the injected Pi CLI/RPC contract.
773
803
  Ticket assets (SPEC §7.1.3) are files attached to a ticket and kept in the
774
- BuildStore, never in the ticket source. `packages/core/src/store/ticket-assets.ts`
804
+ ticket service for hosted tickets, independently of build storage, and in the
805
+ BuildStore for other sources. `packages/core/src/store/ticket-assets.ts`
775
806
  holds the types, the limits, and the one validator every adapter calls;
776
807
  `packages/core/src/cli/ticket-assets.ts` holds the filesystem walk, the download
777
808
  writer, and the note builder; `ab ticket attach`, `asset get`, and `asset rm`
778
- live in `packages/core/src/cli/ticket.ts`. The only thing a ticket source sees is
779
- a note through `comment()`, so no source or plugin protocol changed.
809
+ live in `packages/core/src/cli/ticket.ts`. The optional hosted source asset
810
+ capability reuses RemoteBuildStore transport with the ticket URL and credential.
811
+ Other sources retain the existing store path.
780
812
  A claim freezes the ticket's current assets into the build (SPEC §6.3):
781
813
  `build.created.assets` (and, when a spec is revised from the ticket,
782
814
  `spec.revised.assets`) record the pinned revisions, `state.pinnedAssets` in the
783
815
  reducer projects them, `getPinnedTicketAsset` reads them back by pinned revision,
784
816
  and `ab context` writes them under `.ab/assets/`.
817
+ For hosted tickets, `store/ticket-asset-snapshots.ts` copies exact revisions
818
+ into chunked immutable repository artifacts in the build store before pinning.
819
+ Reads use explicit manifest/chunk revisions without a ticket connection. Old
820
+ pins retain ticket-row reads; existing service attachments need no migration,
821
+ and attachments in old local stores are not moved.
785
822
 
786
823
  Both `ab dispatch` and sessionless `ab ticket` load the repository's plugins
787
824
  before selecting their TicketSource; dispatch passes that one adapter instance
@@ -938,3 +975,48 @@ the shared `connection.ts` wire types, and Postgres `connections.ts`. Approval
938
975
  commits registry membership, a credential digest, and the outcome atomically.
939
976
  The web connection routes authenticate the existing Better Auth session and
940
977
  reuse the sign-in/consent layout; CLI polling uses a separate private capability.
978
+
979
+ ## Revision-checked ticket writes
980
+
981
+ Every ticket read includes an opaque `revision`. Pass it as `expectedRevision`
982
+ (on MCP/operator requests) or `--expected-revision <opaque>` on `ab ticket update`,
983
+ `move`, `block` and `unblock`. A stale write fails without changing anything and
984
+ reports the current revision. Omission preserves unguarded behavior. Blocker
985
+ checks cover the entire batch. Dispatcher moves remain unguarded and change the
986
+ revision. Sources without guarded writes, including Linear, refuse supplied
987
+ revisions rather than ignoring them.
988
+
989
+ `ab ticket edit <id>` opens `$VISUAL`, falling back to `$EDITOR`, using TOML
990
+ frontmatter and a Markdown body. Only title, body and labels are editable;
991
+ id, blockers, creation key and reference comments are read-only. It saves only
992
+ changed fields with the opening revision. Unchanged or formatting-only edits
993
+ write nothing. On conflict, invalid input or editor failure, the error names an
994
+ absolute file path containing the edited text. Read the current ticket and
995
+ reapply the intended changes before retrying; conflicts are never merged.
996
+
997
+ File and Linear revisions fingerprint title, body, canonical label/blocker sets
998
+ and state; reverting identical content restores its fingerprint. File checks
999
+ re-read immediately before writing, with the existing no-lock race window.
1000
+ Direct file edits and state-directory moves change revisions. File comments
1001
+ change the body and revision; separate hosted comments do not. Hosted revisions
1002
+ are monotonic decimal bigint strings, and checks run under the same row lock
1003
+ and transaction as all requested changes. Hosted reads return a coherent
1004
+ snapshot of fields, blockers and revision.
1005
+
1006
+ ### Independent hosted ticket service
1007
+
1008
+ `[tickets] url` selects hosted ticket transport independently of BuildStore selection;
1009
+ without it, tickets use the selected remote build store. Local builds require an
1010
+ explicit ticket URL. Project registration and ticket policy publication through
1011
+ `ab connect --tickets URL` work before dispatch. Policy is a durable per-registration
1012
+ record, outside BuildStore events/artifacts; legacy uploaded configuration remains
1013
+ a compatibility fallback. Hosted lifecycle defaults are triage/ready/doing/done.
1014
+ Independent harvest uses revocable, expiring, service-bound ticket grants with
1015
+ read/create runner authority and read-only child authority. Build state and harvest
1016
+ journals remain exclusively in the selected BuildStore.
1017
+
1018
+ Hosted ticket detail reads expose complete, oldest-first structured comments in
1019
+ `ticket.comments`, preserving exact bodies. Scans do not fetch comments. Hosted
1020
+ HTTP clients opt in with `x-autobuild-ticket-comments: 1` so historical strict
1021
+ decoders retain compatible responses. File comments remain embedded in the body;
1022
+ Linear comment reads are unchanged.