@defrex/autobuild 0.11.0 → 0.13.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 (136) hide show
  1. package/README.md +3 -1
  2. package/SPEC.md +335 -31
  3. package/docs/architecture.md +125 -7
  4. package/docs/configuration.md +142 -21
  5. package/docs/hosted-dispatcher.md +17 -2
  6. package/docs/hosted-service.md +240 -0
  7. package/docs/mcp.md +8 -2
  8. package/docs/operator-api.md +97 -7
  9. package/docs/remote-store-protocol.md +199 -144
  10. package/docs/remote-ticket-protocol.md +249 -19
  11. package/docs/setup.md +199 -16
  12. package/docs/ticket-dependencies.md +1 -1
  13. package/docs/ticket-migration.md +64 -0
  14. package/package.json +7 -6
  15. package/packages/core/src/cli/attention.ts +209 -0
  16. package/packages/core/src/cli/build-control.ts +12 -1
  17. package/packages/core/src/cli/configured-store.ts +17 -0
  18. package/packages/core/src/cli/connect.ts +364 -0
  19. package/packages/core/src/cli/context.ts +7 -1
  20. package/packages/core/src/cli/dashboard/model.ts +4 -5
  21. package/packages/core/src/cli/dashboard/poll.ts +1 -1
  22. package/packages/core/src/cli/dispatch.ts +336 -41
  23. package/packages/core/src/cli/harvest.ts +13 -3
  24. package/packages/core/src/cli/help.ts +51 -16
  25. package/packages/core/src/cli/init-validation.ts +27 -2
  26. package/packages/core/src/cli/installation.ts +67 -13
  27. package/packages/core/src/cli/main.ts +60 -16
  28. package/packages/core/src/cli/mcp.ts +9 -2
  29. package/packages/core/src/cli/pr-summary-publication.ts +220 -0
  30. package/packages/core/src/cli/pr-summary.ts +2 -0
  31. package/packages/core/src/cli/project-credentials.ts +132 -0
  32. package/packages/core/src/cli/repo-state.ts +15 -2
  33. package/packages/core/src/cli/self-update.ts +15 -1
  34. package/packages/core/src/cli/store-opening.ts +22 -14
  35. package/packages/core/src/cli/terminals.ts +0 -10
  36. package/packages/core/src/cli/ticket-assets.ts +7 -2
  37. package/packages/core/src/cli/ticket-editor.ts +82 -0
  38. package/packages/core/src/cli/ticket.ts +172 -35
  39. package/packages/core/src/cli/upgrade-compatibility.ts +80 -0
  40. package/packages/core/src/cli/wait.ts +84 -17
  41. package/packages/core/src/cli/watch.ts +197 -60
  42. package/packages/core/src/config/live.ts +3 -0
  43. package/packages/core/src/config/schema.ts +90 -11
  44. package/packages/core/src/connection.ts +86 -0
  45. package/packages/core/src/events/catalog.ts +11 -2
  46. package/packages/core/src/events/payloads.ts +50 -0
  47. package/packages/core/src/events/repository.ts +13 -0
  48. package/packages/core/src/integration/harness.ts +37 -2
  49. package/packages/core/src/kernel/__snapshots__/reducer-registry.test.ts.snap +145 -83
  50. package/packages/core/src/kernel/engine.ts +24 -0
  51. package/packages/core/src/kernel/generators/build-log.ts +11 -1
  52. package/packages/core/src/kernel/incremental-fixtures.ts +13 -2
  53. package/packages/core/src/kernel/log-index.ts +17 -0
  54. package/packages/core/src/kernel/pr-summary.ts +89 -0
  55. package/packages/core/src/kernel/reducer-registry.ts +10 -0
  56. package/packages/core/src/kernel/reducer.ts +8 -0
  57. package/packages/core/src/ontology.ts +2 -0
  58. package/packages/core/src/operator/annotations.ts +12 -0
  59. package/packages/core/src/operator/index.ts +12 -0
  60. package/packages/core/src/operator/protocol.ts +19 -2
  61. package/packages/core/src/operator/query.ts +38 -16
  62. package/packages/core/src/operator/registry.ts +61 -8
  63. package/packages/core/src/operator/ticket-source-backend.ts +17 -20
  64. package/packages/core/src/operator/tickets.ts +136 -28
  65. package/packages/core/src/plugin-sdk/index.ts +7 -0
  66. package/packages/core/src/plugins/manifest.ts +5 -3
  67. package/packages/core/src/plugins/runtimes.ts +7 -1
  68. package/packages/core/src/ports/forge/fake.ts +13 -1
  69. package/packages/core/src/ports/forge/github-transport.ts +36 -1
  70. package/packages/core/src/ports/forge/github.ts +29 -5
  71. package/packages/core/src/ports/forge/local-git.ts +12 -4
  72. package/packages/core/src/ports/forge/summary-marker.ts +8 -0
  73. package/packages/core/src/ports/runner/session-env.ts +3 -0
  74. package/packages/core/src/ports/tickets/contract.ts +66 -1
  75. package/packages/core/src/ports/tickets/create.ts +54 -10
  76. package/packages/core/src/ports/tickets/fake.ts +30 -1
  77. package/packages/core/src/ports/tickets/file-assets.ts +159 -0
  78. package/packages/core/src/ports/tickets/file-codec.ts +90 -0
  79. package/packages/core/src/ports/tickets/file.ts +114 -120
  80. package/packages/core/src/ports/tickets/grants.ts +47 -0
  81. package/packages/core/src/ports/tickets/hosted-index.ts +17 -3
  82. package/packages/core/src/ports/tickets/identity.ts +20 -0
  83. package/packages/core/src/ports/tickets/lifecycle.ts +18 -0
  84. package/packages/core/src/ports/tickets/linear.ts +74 -9
  85. package/packages/core/src/ports/tickets/migration.ts +336 -0
  86. package/packages/core/src/ports/tickets/operations.ts +38 -7
  87. package/packages/core/src/ports/tickets/portable-assets.ts +54 -0
  88. package/packages/core/src/ports/tickets/remote-protocol.ts +96 -0
  89. package/packages/core/src/ports/tickets/remote.ts +61 -3
  90. package/packages/core/src/ports/tickets/requested-id.ts +46 -0
  91. package/packages/core/src/ports/tickets/revision.ts +62 -0
  92. package/packages/core/src/ports/tickets/states.ts +61 -0
  93. package/packages/core/src/ports/types.ts +40 -3
  94. package/packages/core/src/ports/workspace/build-execution.ts +6 -1
  95. package/packages/core/src/ports/workspace/create.ts +15 -2
  96. package/packages/core/src/ports/workspace/distribution-archive.ts +3 -5
  97. package/packages/core/src/ports/workspace/harvest-execution.ts +3 -1
  98. package/packages/core/src/ports/workspace/local-build-execution.ts +8 -1
  99. package/packages/core/src/ports/workspace/operator-sandbox.ts +2 -0
  100. package/packages/core/src/ports/workspace/vercel-sandbox.ts +51 -19
  101. package/packages/core/src/processes/build-execution-state.ts +1 -1
  102. package/packages/core/src/processes/build-runner.ts +87 -26
  103. package/packages/core/src/processes/dispatch-failure-budget.ts +83 -0
  104. package/packages/core/src/processes/dispatcher.ts +168 -41
  105. package/packages/core/src/processes/execution-no-progress.ts +176 -0
  106. package/packages/core/src/processes/harvest-child.ts +31 -4
  107. package/packages/core/src/processes/harvest-execution-state.ts +19 -2
  108. package/packages/core/src/processes/harvest-runner.ts +166 -104
  109. package/packages/core/src/processes/harvest.ts +34 -14
  110. package/packages/core/src/processes/phase-failure-budget.ts +2 -0
  111. package/packages/core/src/processes/publication-loss.ts +18 -1
  112. package/packages/core/src/processes/repo-view.ts +35 -0
  113. package/packages/core/src/store/contract.ts +32 -1
  114. package/packages/core/src/store/local/store.ts +1 -1
  115. package/packages/core/src/store/remote/client.ts +1 -1
  116. package/packages/core/src/store/remote/compatibility.ts +90 -0
  117. package/packages/core/src/store/remote/credentials.ts +28 -0
  118. package/packages/core/src/store/remote/index.ts +14 -1
  119. package/packages/core/src/store/remote/protocol.ts +25 -0
  120. package/packages/core/src/store/remote/token.ts +74 -13
  121. package/packages/core/src/store/ticket-asset-snapshots.ts +192 -0
  122. package/packages/core/src/store/ticket-assets.ts +86 -8
  123. package/packages/core/src/store/types.ts +1 -1
  124. package/packages/core/src/testing/index.ts +7 -0
  125. package/skills/guide/SKILL.md +232 -34
  126. package/skills/guide/references/operator-api.md +142 -13
  127. package/skills/guide/references/plugin-authoring.md +10 -2
  128. package/skills/guide/references/remote-store-protocol.md +199 -144
  129. package/skills/guide/references/remote-ticket-protocol.md +253 -18
  130. package/skills/guide/references/setup.md +199 -16
  131. package/skills/guide/references/ticket-dependencies.md +1 -1
  132. package/skills/guide/references/ticket-migration.md +64 -0
  133. package/skills/operate/SKILL.md +126 -36
  134. package/skills/tickets/SKILL.md +71 -0
  135. package/templates/hosted-service/package.json +13 -0
  136. package/templates/hosted-service/vercel.json +7 -0
@@ -1,13 +1,15 @@
1
1
  /**
2
2
  * Ticket assets (SPEC §7.1): named, versioned bundles of files attached to a
3
- * ticket and kept in the BuildStore, keyed by `(repo, ticketId, kind, name)`.
4
- * The ticket source never stores the contents; it only gets a note through
3
+ * ticket and kept in its service (hosted) or BuildStore (other sources), keyed by `(repo, ticketId, kind, name)`.
4
+ * Sources also get a human-readable note through
5
5
  * `TicketSource.comment()` (SPEC §13).
6
6
  *
7
7
  * This module is the shared, adapter-independent half: the types, the limits,
8
8
  * and the one validator every adapter calls before any write.
9
9
  */
10
10
 
11
+ import { readAssetSnapshot, prepareAssetSnapshots } from './ticket-asset-snapshots'
12
+ import type { TicketSource, TicketAssetStore } from '../ports/types'
11
13
  import type { AbEvent } from '../events/catalog'
12
14
  import type { PinnedAsset } from '../events/payloads'
13
15
  import { defineReducer } from '../kernel/incremental'
@@ -56,7 +58,8 @@ export interface TicketAssetMeta {
56
58
  ticketId: string
57
59
  kind: string
58
60
  name: string
59
- /** 0-based per `(repo, ticketId, kind, name)`. A removal consumes a number
61
+ /** Increasing per `(repo, ticketId, kind, name)` (normally from zero; native
62
+ * file revisions reserve a separate range for legacy compatibility). A removal consumes a number
60
63
  * too (a tombstone), so revisions may skip. */
61
64
  revision: number
62
65
  layout: TicketAssetLayout
@@ -380,7 +383,27 @@ export async function samplePinnedAssets(
380
383
  return (await store.listTicketAssets(repo, ticketId)).map(pinnedAssetOf)
381
384
  }
382
385
 
383
- export const PINNED_ASSETS_REDUCER_VERSION = 1
386
+ export async function preparePinnedAssets(
387
+ source: TicketSource,
388
+ store: BuildStore,
389
+ repo: string,
390
+ ticketId: string,
391
+ ): Promise<PinnedAsset[]> {
392
+ if (!source.assets) {
393
+ if (source.name === 'hosted') throw new Error('hosted ticket source has no asset capability')
394
+ return samplePinnedAssets(store, repo, ticketId)
395
+ }
396
+ const ticket = await source.get(ticketId)
397
+ return prepareAssetSnapshots(
398
+ composeTicketAssets(source, store),
399
+ store,
400
+ repo,
401
+ ticketId,
402
+ ticket?.ref.id ?? ticketId,
403
+ )
404
+ }
405
+
406
+ export const PINNED_ASSETS_REDUCER_VERSION = 2
384
407
 
385
408
  /** The asset sets recorded by every `build.created`/`spec.revised` pin event
386
409
  * that carries one, in array order. Each pin event replaces the set, so the
@@ -443,7 +466,7 @@ export function findPinnedRevision(
443
466
  * needed, so a finished build still answers. Null when the build never pinned
444
467
  * `kind/name` (or the revision). Rejects an unknown build. */
445
468
  export async function resolvePinnedAsset(
446
- store: Pick<BuildStore, 'getBuild' | 'getEvents' | 'getTicketAsset'>,
469
+ store: Pick<BuildStore, 'getBuild' | 'getEvents' | 'getTicketAsset' | 'getRepoArtifact'>,
447
470
  slug: string,
448
471
  kind: string,
449
472
  name: string,
@@ -452,7 +475,62 @@ export async function resolvePinnedAsset(
452
475
  const record = await store.getBuild(slug)
453
476
  if (record === null) throw new Error(`unknown build "${slug}"`)
454
477
  if (record.ticket === undefined) return null
455
- const revision = findPinnedRevision(await store.getEvents(slug), kind, name, rev)
456
- if (revision === undefined) return null
457
- return store.getTicketAsset(record.repo, record.ticket.id, kind, name, revision)
478
+ const events = await store.getEvents(slug)
479
+ const sets = pinnedAssetsReducer.reduce(events).pins
480
+ const pin =
481
+ rev === undefined
482
+ ? sets.at(-1)?.find((a) => a.kind === kind && a.name === name)
483
+ : sets.flat().find((a) => a.kind === kind && a.name === name && a.revision === rev)
484
+ if (!pin) return null
485
+ if (pin.snapshot) {
486
+ const created = events.find((event) => event.type === 'build.created')
487
+ const asset = await readAssetSnapshot(
488
+ store,
489
+ record.repo,
490
+ record.ticket.id,
491
+ pin,
492
+ created?.type === 'build.created' ? created.payload.ticket?.id : undefined,
493
+ )
494
+ return { ...asset, meta: { ...asset.meta, ticketId: record.ticket.id } }
495
+ }
496
+ return store.getTicketAsset(record.repo, record.ticket.id, kind, name, pin.revision)
497
+ }
498
+
499
+ /** Native writes; merged reads preserve legacy file attachments and recorded revisions. */
500
+ export function composeTicketAssets(
501
+ source: TicketSource,
502
+ legacy: TicketAssetStore,
503
+ ): TicketAssetStore {
504
+ const native = source.assets
505
+ if (!native) return legacy
506
+ if (!native.hasTicketAssetHistory) return native
507
+ const owns = (repo: string, id: string, kind: string, name: string) =>
508
+ native.hasTicketAssetHistory!(repo, id, kind, name)
509
+ return {
510
+ ticketAssetLimits: native.ticketAssetLimits.bind(native),
511
+ putTicketAsset: native.putTicketAsset.bind(native),
512
+ async getTicketAsset(repo, id, kind, name, rev) {
513
+ if (rev !== undefined)
514
+ return (
515
+ (await native.getTicketAsset(repo, id, kind, name, rev)) ??
516
+ legacy.getTicketAsset(repo, id, kind, name, rev)
517
+ )
518
+ return (await owns(repo, id, kind, name))
519
+ ? native.getTicketAsset(repo, id, kind, name)
520
+ : legacy.getTicketAsset(repo, id, kind, name)
521
+ },
522
+ async listTicketAssets(repo, id) {
523
+ const current = await native.listTicketAssets(repo, id)
524
+ for (const item of await legacy.listTicketAssets(repo, id)) {
525
+ if (!(await owns(repo, id, item.kind, item.name))) current.push(item)
526
+ }
527
+ return current
528
+ },
529
+ async removeTicketAsset(repo, id, kind, name) {
530
+ const previous = await this.getTicketAsset(repo, id, kind, name)
531
+ if (!previous) return null
532
+ await native.removeTicketAsset(repo, id, kind, name)
533
+ return previous?.meta ?? null
534
+ },
535
+ }
458
536
  }
@@ -412,7 +412,7 @@ export interface BuildStore {
412
412
  listRepoArtifacts(repo: string, kind?: string): Promise<RepositoryArtifactMeta[]>
413
413
  // ── Ticket assets (SPEC §7.1 — files attached to a ticket) ─────────────
414
414
  // Keyed by (repo, ticketId, kind, name); revisions are 0-based. The store
415
- // owns the bytes, the ticket source only ever gets a note (SPEC §13).
415
+ // owns these bytes; hosted current assets use the ticket service store (SPEC §13).
416
416
  /** The effective ceilings this store accepts for `repo` (a remote
417
417
  * deployment may advertise lower ones than the defaults). */
418
418
  ticketAssetLimits(repo: string): Promise<TicketAssetLimits>
@@ -57,3 +57,10 @@ export {
57
57
  type E2eHarness,
58
58
  type SkillHandlers,
59
59
  } from '../integration/harness'
60
+
61
+ // Harvest lifecycle and CLI seams for delegated-credential integration tests.
62
+ export { HarvestRunner } from '../processes/harvest-runner'
63
+ export { resolveHarvestCliEnv } from '../cli/env'
64
+ export { buildHarvestContext, submitHarvestProposals, submitHarvestVerdict } from '../cli/harvest'
65
+ export { abConnect, abDisconnect } from '../cli/connect'
66
+ export { ProjectCredentials } from '../cli/project-credentials'
@@ -197,7 +197,9 @@ The distinctions that change an administrator's answer:
197
197
  agent turn or command already running. Missing, unreadable, malformed, and
198
198
  routing-invalid candidates retain that snapshot with deduplicated operator
199
199
  notice, and restoring a valid file resumes reload. Scoped phase CLI processes
200
- still read the build worktree's branch-owned file.
200
+ still read the build worktree's branch-owned file. Dispatcher merge readiness,
201
+ resume, and execution recovery use the runner's pinned commands, verify, and
202
+ finalize configuration; builds without a pin retain the live fallback.
201
203
  - **Workspaces** are provisioned per build and remain the locally reachable
202
204
  execution context for that build.
203
205
  - **Ticket providers** (`linear`, `file`) sit behind one port; the dispatcher
@@ -215,6 +217,22 @@ The distinctions that change an administrator's answer:
215
217
  guidance and later dispatcher ticks retry. Without consent it remains open for
216
218
  ordinary `git diff` inspection; it has no review UI or image host.
217
219
 
220
+ For moving a project between sources, follow the [ticket migration procedure](references/ticket-migration.md).
221
+
222
+ ## Independent hosted tickets
223
+
224
+ Hosted tickets support local builds. Configure `[tickets] source = "hosted"`,
225
+ `teamKey`, and `url`, or run `ab connect --tickets URL` to register the project,
226
+ save its credential and publish ticket policy without changing the build store.
227
+ Without `tickets.url`, tickets use the selected remote build service; local builds
228
+ require an explicit ticket URL. No dispatcher is needed for ticket commands.
229
+ Hosted defaults are `triage`, `ready`, `doing`, `done`; create/proposal use triage.
230
+ Other sources still require readyState. Explicit state names retain their spelling.
231
+ Legacy clients without independent policy retain uploaded configuration defaults.
232
+ Harvest ticket grants are service-bound read/create authority; child phases are
233
+ read-only and cannot publish policy. See [setup](references/setup.md) and the
234
+ [ticket protocol](references/remote-ticket-protocol.md).
235
+
218
236
  ## `autobuild.toml` reference
219
237
 
220
238
  One declarative file at the repo root. **Declarative, not executable** —
@@ -283,6 +301,19 @@ through the strict versioned manifest and environment-only secrets, to the
283
301
  unchanged shared contract suite and optional npm publication—read
284
302
  [`references/plugin-authoring.md`](references/plugin-authoring.md).
285
303
 
304
+ ### `[store]`
305
+
306
+ | Key | Required | Values / purpose |
307
+ |---|---|---|
308
+ | `url` | When `[store]` exists | Absolute HTTPS service base URL; loopback HTTP is allowed for development. No credentials, query or fragment. Trailing slashes are normalized. |
309
+
310
+ Selection is explicit `--store` > nonblank `AB_STORE` > `[store] url` > the main
311
+ checkout's `.autobuild/`. A configured service without a user credential fails
312
+ with `ab connect` guidance. `AB_TOKEN` overrides the saved service/project
313
+ credential. See [repository setup](references/setup.md#connect-a-project).
314
+ Changing the store requires restarting dispatch.
315
+
316
+
286
317
  ### `[pr]`
287
318
 
288
319
  Optional PR-projection settings. Omission leaves artifact attachments text-only.
@@ -408,6 +439,28 @@ manager.
408
439
 
409
440
  ### `[verify]`
410
441
 
442
+ Post-reconcile verification defaults to enabled: `[verify].afterReconcile = true`.
443
+ Set it to `false` to retain pre-reconcile verification and skip all steps after
444
+ reconciliation. An optional boolean `[verify.<step>].afterReconcile` overrides
445
+ the repository setting in either direction; overrides require a configured step.
446
+ For example, set `afterReconcile = false` in `[verify]` and
447
+ `afterReconcile = true` in `[verify.types]` to keep only that step re-running.
448
+
449
+ The kernel freezes the resolved ordered policy in
450
+ `verify.reconcile-policy-captured` at the next engine action after reconcile
451
+ completion. Live configuration changes apply at that boundary; a cycle already
452
+ started keeps its policy through recovery. Historical cycles already started
453
+ without a captured policy retain the rerun-all behavior. Initial verification
454
+ and verification after subsequent code review always use ordinary applicability.
455
+ Policy exclusion precedes plan selection and paths, including `always = true`.
456
+ Enabled steps still follow plan selection and path rules. Excluded steps record
457
+ `verify.completed` with outcome `skipped`, `skipCause = "post-reconcile-policy"`,
458
+ and a reason naming the setting. Dashboards and PR summaries call this
459
+ "skipped by post-reconcile policy"; it is never a pass. Forge-side required
460
+ checks and branch protections still gate merge. For excluded steps, merged code
461
+ is not re-verified, so breakage that only exists in the combination can land.
462
+
463
+
411
464
  `steps` orders the verify phases; each entry `"<step>"` becomes a
412
465
  `verify:<step>` phase and needs its own `[verify.<step>]` subtable. Those
413
466
  subtables are part of this section — their fields are listed here.
@@ -419,6 +472,7 @@ subtables are part of this section — their fields are listed here.
419
472
  | `command` | — | **required when `kind = "check"`**, nonempty string | Ref into `[commands]` — the key, not a shell string. Pass/fail is the command's exit status. |
420
473
  | `skill` | — | **required when `kind = "agent"`**, nonempty string | Installed skill name to run (e.g. a repository-authored `"verify-app-e2e"`). |
421
474
  | `paths` | — | optional nonempty array of positive repository-relative globs | Makes either step kind apply when any changed path matches any selector. Omitted means unconditional. |
475
+ | `afterReconcile` | repository: `true`; step: inherited | boolean | Step override wins over `[verify].afterReconcile`; false skips this step after reconciliation only. |
422
476
  | `always` | — | optional boolean | `true` makes the step unconditional and mandatory (a plan cannot deselect it); `false` is equivalent to omission. |
423
477
 
424
478
  A plan may begin with strict TOML front matter selecting the complete set of
@@ -488,7 +542,7 @@ A selected step's `paths` are evaluated immediately before the step against
488
542
  current `HEAD`, relative to the initial branch-cut SHA or the refreshed base
489
543
  from the latest completed reconcile. It uses a NUL-delimited, no-rename Git diff, so
490
544
  adds/modifications/deletions and both rename sides participate. A reconcile
491
- starts a fresh cycle and evaluation repeats; upstream-only merged paths are
545
+ starts a fresh cycle and evaluation repeats for policy-enabled steps; upstream-only merged paths are
492
546
  excluded while build-owned resolutions remain visible. A miss starts no
493
547
  command, agent, or session and records `skipped` with
494
548
  `excluded by [verify.<step>].paths: no changed path matched <JSON paths>`.
@@ -688,6 +742,7 @@ nonnegative integer so zero can disable that trigger.
688
742
  | `sessionBudgetSeconds` | `3600` | positive integer | Wall-clock bound for each build agent session unless its logical role overrides it. |
689
743
  | `stallRounds` | `3` | positive integer | The same finding surviving this many review rounds auto-escalates to a human — the anti-loop guard. |
690
744
  | `maxVerifyAttempts` | `3` | positive integer | Caps the `verify → implement → verify` cycle before escalation. |
745
+ | `maxDispatchAttempts` | `3` | positive integer | Caps consecutive dispatch failures before a setup-targeted `dispatch-failure-limit` escalation. |
691
746
  | `maxSetupAttempts` | `3` | positive integer | Caps consecutive `[commands].setup` failures before a setup-targeted human escalation. |
692
747
  | `maxInfrastructureAttempts` | `3` | positive integer | Caps consecutive remote provider lifecycle failures before a setup-targeted human escalation; a `retry` answer re-arms the budget. |
693
748
  | `maxReconcileAttempts` | `3` | positive integer | Caps completed reconciles that leave the PR conflicted against an unchanged authoritative base. Moving-base races do not consume the budget. |
@@ -695,6 +750,26 @@ nonnegative integer so zero can disable that trigger.
695
750
  | `harvestThreshold` | `5` | positive integer | Newly unclaimed `observation.recorded` occurrences required to start one repository harvest run. |
696
751
  | `harvestMaxDrift` | `3` | nonnegative integer | Other builds merged after the oldest unclaimed observation required to start a run; `0` disables drift. |
697
752
 
753
+ Dispatch failures share one budget across stages until runner attachment. At
754
+ exhaustion dispatch parks durably; bare `ab answer <slug>` and text guidance
755
+ both grant a fresh budget, while abort terminates without retrying. Workspace
756
+ failures owned by provider infrastructure recovery use only its existing budget.
757
+
758
+ The dispatcher also applies a fixed, non-configurable limit of three consecutive
759
+ executions that complete normally with exit code zero and no durable build
760
+ progress. Execution start/end, runner attachment and workspace lifecycle
761
+ bookkeeping do not count as progress. Pipeline, session, spec, publication/PR,
762
+ observation and acknowledged operator facts reset the streak; infrastructure,
763
+ budget and provider failures do not consume the allowance and retain their own
764
+ retry limits. Before a fourth actionable launch, the dispatcher raises one
765
+ setup-targeted `execution-no-progress` policy escalation naming the count and
766
+ the next action it expected the runner to take. Waiting, paused and terminal
767
+ builds are left parked, and pending operator commands remain executable.
768
+ Answering that escalation with guidance or a bare `ab answer <slug>` retry grants
769
+ three fresh executions. The guard is derived from durable facts, including on
770
+ supervisor restart, and uses the ordinary watch, dashboard and operator API
771
+ attention paths.
772
+
698
773
  For each repeat conflict, Autobuild compares the base merged by the latest
699
774
  completed reconcile with a fresh authoritative base snapshot. An advanced base
700
775
  proves the attempt lost a race and permits another reconcile regardless of the
@@ -712,7 +787,7 @@ roundless no-progress answer. An already exhausted no-progress budget raises its
712
787
  matching cause separately before another reconcile. Cause-less historical
713
788
  raises retain their former round-shaped reset behavior, and roundless
714
789
  policy/reconcile raises retain their former no-progress meaning, on replay
715
- without migration. Post-reconcile verification still runs in full and remains
790
+ without migration. Post-reconcile verification follows the captured policy and remains
716
791
  independently bounded by `maxVerifyAttempts`.
717
792
 
718
793
  `stallRounds` counts *persistence chains*, which reviewers mark and the kernel
@@ -746,12 +821,14 @@ state eligible.
746
821
  |---|---|---|---|
747
822
  | `source` | — | **required**, nonblank builtin or plugin registration name | Which provider backs all ticket operations. Unknown names report every available builtin and plugin source. |
748
823
  | `readyLabels` | — (source-aware) | optional; array of nonempty strings | A ticket must carry **every** listed label to be dispatchable. `[]` = **no label gate**. Absent uses the source default below. |
749
- | `readyState` | — | **required**, non-blank string | The one workflow state a ticket must sit in to be dispatchable. Linear matches exactly and case-sensitively; file matches it case-insensitively to a state directory (`ready` → `ready/`). There is no any-state mode. |
824
+ | `url` | selected build service | HTTPS URL (loopback HTTP allowed), no userinfo/query/fragment; hosted only | Ticket service independent of builds; required for local builds. |
825
+ | `readyState` | hosted: `ready` | non-blank string; required for other sources | The one workflow state a ticket must sit in to be dispatchable. Linear matches exactly and case-sensitively; file matches it case-insensitively to a state directory (`ready` → `ready/`). There is no any-state mode. |
750
826
  | `readyAssignee` | — | optional; `"me"` or an email; `linear` only | Also require the ticket to be assigned to that Linear user (`me` = owner of `LINEAR_API_KEY`); unassigned tickets are never ready. |
751
- | `teamKey` | — | required by `linear`, forbidden by `file`, allowed for plugins | The Linear team key (e.g. `"ENG"`) or an existing plugin configuration field. |
827
+ | `teamKey` | — | required by `linear` and `hosted`, forbidden by `file`, allowed for plugins | The Linear team key (e.g. `"ENG"`), the captured prefix for new hosted ticket ids (`ENG-1`), or a plugin configuration field. Hosted numbers are project-local and continue across prefix changes; legacy ids remain aliases and readable inputs are case-insensitive. |
752
828
  | `claimedState` | — | optional nonempty string; forbidden by `file`, allowed for plugins | Workflow state `claim()` moves a ticket to when a build starts. |
829
+ | `doneState` | hosted: `"done"` | optional nonblank string | Project completion state and merged-ticket transition; hosted tickets only. |
753
830
  | `createState` | — | optional, nonempty string | Default state for new tickets when a create does not name one. Absent = the provider's default (Linear: the team's default, e.g. Backlog; file: Triage). |
754
- | `triageState` | — | optional, nonempty string | State the dispatcher hands tickets back to for human triage — spec-gate bounces, aborted builds, closed-unmerged PRs. Absent = Linear: Backlog; file/plugin: Triage. Must name a state the tracker actually has — a Linear team only has "Triage" when its triage feature is enabled. |
831
+ | `triageState` | — | optional, nonempty string | State the dispatcher hands tickets back to for human triage — spec-gate bounces, aborted builds, closed-unmerged PRs. Absent = Linear: Backlog; hosted: triage; file/plugin: Triage. Must name a state the tracker actually has — a Linear team only has "Triage" when its triage feature is enabled. |
755
832
  | `proposalState` | — | optional, nonempty string | State observation harvest files approved proposals into. Absent = the resolved `triageState`, which keeps the human grooming gate. Setting it to `readyState` waives that gate: harvested proposals dispatch without being read. Keep it distinct from `triageState` — see below. |
756
833
  | `dir` | file: `.autobuild/tickets`; plugin: — | optional nonempty string; forbidden by `linear`, allowed for plugins | Root holding file state directories, or an existing plugin configuration field. |
757
834
 
@@ -829,7 +906,7 @@ changing it requires a dispatcher restart.
829
906
  | `model` | — (required when `enabled = true`) | nonblank provider-qualified string, same vocabulary as `[roles]` | The turn runner's model, resolved through the deployment's gateway credential; a leading `vercel-ai-gateway/` prefix is accepted and stripped. |
830
907
  | `invocationBudgetSeconds` | `240` | positive integer, clamped to 300 | Wall-clock budget for one turn invocation. A larger value is clamped — not rejected — to the 300-second hosted function limit the runner executes under. |
831
908
  | `approvals` | `["builds.control:abort", "builds.control:discard", "builds.answer:revise-spec", "sandbox.publish", "tickets.move:ready"]` | array of `tool` or `tool:qualifier` strings | Registry tool calls that suspend a turn until the operator answers. Empty (`[]`) means none — autonomous operation. An entry naming an absent or unknown tool is **inert by design**, never an error. A qualified entry applies to one discriminator value only (`tickets.move:ready` approves moving a ticket to `ready`); for a tool without a discriminator, only the bare form matches. |
832
- | `wake` | inherits the default | array of event-type globs | The attention events a **new** session wakes for. Absent inherits the default attention set (the events `ab watch` filters on by default); an explicit `[]` means never wake — message-only. Each glob must match at least one build event type. Existing sessions never retroactively inherit; an explicit wake update always wins. |
909
+ | `wake` | inherits the default | array of event-type globs | The attention events a **new** session wakes for. Absent inherits the legacy hosted wake event set, independent of CLI watch defaults; an explicit `[]` means never wake — message-only. Each glob must match at least one build event type. Existing sessions never retroactively inherit; an explicit wake update always wins. |
833
910
  | `sandbox` | — | strict subtable | The sandbox behavior knobs; absence reads as the defaults below. |
834
911
 
835
912
  `[orchestrator.sandbox]` fields:
@@ -1049,7 +1126,7 @@ There is no home-directory fallback: a repository with no state tree starts
1049
1126
  empty.
1050
1127
 
1051
1128
  Every command uses the same precedence: explicit `--store <ref>` > nonempty
1052
- `AB_STORE` > `<main-repo>/.autobuild`. Relative local overrides are normalized
1129
+ `AB_STORE` > `[store] url` > `<main-repo>/.autobuild`. Relative local overrides are normalized
1053
1130
  against the main checkout. A local override relocates the whole state tree,
1054
1131
  including `worktrees/` and default file tickets; an explicit `[tickets].dir`
1055
1132
  remains repo-relative. An HTTP(S) URL still selects the remote store unchanged,
@@ -1057,6 +1134,46 @@ while worktrees and default file tickets remain under the repository-default
1057
1134
  `.autobuild/` directory. The dispatcher passes its normalized selection to
1058
1135
  every agent session as `AB_STORE`.
1059
1136
 
1137
+ ## Connect a project
1138
+
1139
+ From a checkout with an origin remote, run `ab connect https://YOUR_ORIGIN`.
1140
+ The CLI prints a verification URL and short code and attempts to open a browser.
1141
+ Sign in as an allowed owner, check the full repository identity and code, and
1142
+ approve or deny. Another device can open the printed URL, or visit `/connect`
1143
+ and enter the code. Approval requires the web-enabled service origin; a
1144
+ standalone machine-only listener cannot present browser consent. Requests
1145
+ expire after ten minutes. Denial and timeout leave local configuration unchanged.
1146
+
1147
+ On approval the service registers the project and issues a project credential.
1148
+ The CLI keeps it in `$XDG_CONFIG_HOME/autobuild/credentials` (or
1149
+ `~/.config/autobuild/credentials`), keyed by normalized service and origin,
1150
+ with directory mode 0700 and file mode 0600. Credentials never belong in the
1151
+ repository. It adds the non-secret `[store] url` to `autobuild.toml` when
1152
+ absent; review and commit that change yourself. An existing different URL
1153
+ must be changed explicitly before connecting. Reconnecting replaces the saved
1154
+ credential only after approval and revokes the previous credential.
1155
+
1156
+ All worktrees share the main checkout's configuration and the user's saved
1157
+ credential. A service manager running as that user uses the same HOME and
1158
+ XDG_CONFIG_HOME, with no AB_STORE or AB_TOKEN exports. A fresh clone with
1159
+ committed store configuration but no saved credential fails with `ab connect`
1160
+ guidance; it never silently creates local state. Run connect on each host/user.
1161
+
1162
+ `ab connect` without a URL reports service, project, and credential status:
1163
+ accepted, rejected, missing, or unreachable. `ab disconnect` revokes the saved
1164
+ credential remotely before removing it locally, keeping the project registered,
1165
+ its data, and store configuration. A network/server failure retains the local
1166
+ entry for retry; an already revoked credential can be removed. If saving a new
1167
+ connection fails, the CLI cancels/revokes it and retains the previous entry.
1168
+ If remote cleanup cannot be confirmed, check the printed credential id with
1169
+ the owner's administrative tools before retrying.
1170
+
1171
+ Store selection is `--store` > nonblank `AB_STORE` > `[store] url` > the
1172
+ main checkout's `.autobuild/`. `AB_TOKEN` overrides the exact service/project's
1173
+ saved credential for CI and other non-interactive hosts. A service override
1174
+ never borrows another service's credential. Build and harvest sessions keep
1175
+ their explicit scoped authority.
1176
+
1060
1177
  ## Source-agnostic ticket operations
1061
1178
 
1062
1179
  The sessionless `ab ticket` namespace constructs whichever TicketSource the
@@ -1111,7 +1228,7 @@ to a ticket that has already been claimed into a build does not stop that build.
1111
1228
  For dependent tickets, follow the
1112
1229
  [safe blocker-filing workflow](references/ticket-dependencies.md): determine the
1113
1230
  intended final destination, stage outside ready, verify all blocker relationships,
1114
- and move to ready last. Direct and hosted Linear create the issue and its blockers
1231
+ and move to ready last. Direct Linear creates the issue and its blockers
1115
1232
  in separate requests; one `create --blocked-by` call is not atomic publication.
1116
1233
  Harvest's creation reservations cover Harvest only, not interactive CLI creates.
1117
1234
  Dependency order and JSON id extraction are still required, but do not alone
@@ -1131,16 +1248,19 @@ transitions, so an invalid state fails with the
1131
1248
  source's known states. A missing id fails nonzero with an error naming both the
1132
1249
  id and configured source.
1133
1250
 
1134
- Human-readable output is the default and must not be parsed for ids. Every
1135
- subcommand accepts `--json`, which emits one bare JSON value and no prose: a
1251
+ Human-readable output is the default and must not be parsed for ids. The CRUD
1252
+ subcommands accept `--json`, which emits one bare JSON value and no prose: a
1136
1253
  `Ticket[]` for `list`, and the complete resulting `Ticket` for `create`,
1137
1254
  `update`, `block`, `unblock`, `show`, or `move`.
1138
1255
 
1139
1256
  ### Ticket assets
1140
1257
 
1141
- A ticket asset is a file or folder attached to a ticket and stored in the
1142
- BuildStore, never in the ticket source: a design export, reference screenshots,
1143
- a sample payload. Each has a kind and a name.
1258
+ A ticket asset is a file or folder attached to a ticket: a design export,
1259
+ reference screenshots, or a sample payload. Hosted tickets store assets in their
1260
+ ticket service, independently of the build store; file tickets use native portable
1261
+ assets composed with legacy BuildStore attachments. Sources without native assets
1262
+ use the BuildStore. Existing service assets need no migration. Old local hosted-ticket
1263
+ assets are not moved and are not a fallback. Each has a kind and a name.
1144
1264
 
1145
1265
  - `ab ticket attach <id> <kind> <path> [--name <name>] [--json]` stores `<path>`
1146
1266
  (default name: its basename). Attaching the same kind and name again makes a
@@ -1152,7 +1272,10 @@ a sample payload. Each has a kind and a name.
1152
1272
  - `ab ticket asset get <id> <kind> <name> <dest> [--rev <n>]` downloads the
1153
1273
  exact bytes of the latest (or given) revision; `ab ticket asset rm <id> <kind>
1154
1274
  <name>` drops it from the current assets.
1155
- - **Frozen at claim.** A claim pins the current revisions into the build. A
1275
+ - **Frozen at claim.** A claim pins the current revisions into the build.
1276
+ Hosted revisions are copied into immutable snapshots in the build store,
1277
+ preserving revision numbers and allowing reads without the ticket service.
1278
+ Old pins without snapshots keep reading their original ticket rows. A
1156
1279
  later attach or remove leaves a running build alone; a human answer with
1157
1280
  `--revise-spec-from-ticket` pins again. `ab build asset get <slug> <kind>
1158
1281
  <name> <dest> [--rev <n>]` downloads what the build froze, also after it ends.
@@ -1173,7 +1296,7 @@ judge against it.
1173
1296
  is the read-only binary retrieval form. Operators need no session identity: it
1174
1297
  works after a build is terminal, writes exact bytes (creating parent
1175
1298
  directories), verifies that the build belongs to this repository, and uses the
1176
- standard explicit `--store` > nonblank `AB_STORE` > repository-local selection.
1299
+ standard explicit `--store` > nonblank `AB_STORE` > `[store] url` > repository-local selection.
1177
1300
  Inside a phase, a complete ambient tuple permits only the ambient build; a
1178
1301
  foreign build or malformed/partial identity is rejected. Use the exact pinned
1179
1302
  `@rev` from a PR attachment command.
@@ -1507,7 +1630,7 @@ Every command requires the target to exist in this repository and be active
1507
1630
  `ab answer` additionally requires an open escalation. A stale, missing, done,
1508
1631
  aborted, or otherwise inapplicable target is an error and gets no new event. Attribution uses `USER`, then `USERNAME`, then the
1509
1632
  same stable fallback on both surfaces. All five commands run sessionless and
1510
- accept `--store <ref>` with the usual explicit flag > `AB_STORE` > repository
1633
+ accept `--store <ref>` with the usual explicit flag > `AB_STORE` > `[store] url` > repository
1511
1634
  local precedence. If nonblank `AB_SESSION` and matching `AB_BUILD` identify the
1512
1635
  caller's own phase build, the command refuses it; a phase cannot pause, resume,
1513
1636
  auto-merge, answer, or abort itself. `ab pause --all` and `ab resume --all`
@@ -1553,7 +1676,7 @@ repository-wide pause (`paused`, default `false`), and the claim-time auto-merge
1553
1676
  default (`defaultAutoMerge`, default `false`). Those defaults also describe a
1554
1677
  repository with no journal row; the read does not create one. Human output is
1555
1678
  the default. `--json` emits one bare object with those exact fields plus the
1556
- canonical `repo`, and `--store` follows the usual explicit flag > `AB_STORE` >
1679
+ canonical `repo`, and `--store` follows the usual explicit flag > `AB_STORE` > `[store] url` >
1557
1680
  repository-local precedence. The command uses the same reducer as the
1558
1681
  dispatcher and dashboard, writes no state, claims no ticket, attaches no runner,
1559
1682
  and starts no dispatcher work.
@@ -1636,19 +1759,51 @@ streams matching events as they land instead of answering once. With no slug it
1636
1759
  follows every nonterminal build in the repository — a build created while the
1637
1760
  watch runs joins it without a restart — and naming one or more slugs watches
1638
1761
  exactly those builds, ending the watch when all of them reach a terminal status.
1639
- `--repository` additionally follows the repository journal; without it,
1640
- repository events are never emitted.
1641
-
1642
- The default filter is the attention set — the events something must wake up
1643
- for. For builds that is exactly: `escalation.raised`, `phase.failed`,
1644
- `infrastructure.failed`, `runner.setup-failed`, `dispatch.failed`,
1645
- `publication.lost`, `finalize.completed`, `pr.conflicted`, `pr.merged`,
1646
- `pr.closed`, `build.completed`, `build.aborted`. For the repository journal:
1647
- `harvest.escalated`, `harvest.failed`, `dispatcher.tick-failed`,
1648
- `dispatcher.config-rejected`, `dispatcher.harvest-runner-failed`. A repeatable
1649
- `--event <glob>` (`escalation.*`, `pr.*`, `*` — `*` matches any run of
1650
- characters, `?` one) replaces that filter, and every glob must match at least
1651
- one known event type or the command fails before reading the store.
1762
+ With no slugs the repository journal is included; named watches add it with
1763
+ `--repository`.
1764
+
1765
+ The default watch reports actionable conditions only:
1766
+
1767
+ - Build `escalation.raised`, or `build.merge-decision`: a finalized open PR
1768
+ with auto-merge off or a current actionable merge-gate deferral. Armed consent
1769
+ awaiting application is silent. One merge decision is reported per build/PR.
1770
+ - Repository `harvest.escalated`, `harvest.recovery-exhausted`,
1771
+ `dispatcher.config-rejected`, `dispatcher.config-reloaded` with nonempty
1772
+ `restartRequired`, abnormal `dispatcher.run-stopped`, and `dispatcher.lost`.
1773
+ - `dispatcher.ticket-source-persistent`: degradation past its advertised
1774
+ reset time, once a tick started at or after reset has completed or failed
1775
+ with the episode still open; crossing reset alone is silent. Without a reset,
1776
+ the deadline is 15 minutes after onset. Repeated evidence never postpones
1777
+ the deadline; recovery ends the episode.
1778
+ - `dispatcher.tick-failure-persistent`: continuously failing completed ticks
1779
+ for five minutes in one run. A successful tick, new run, or matching stop
1780
+ ends the episode. Source grace and cooldown-driven failures do not start
1781
+ this clock; after grace, the first qualifying failure starts a new five-minute
1782
+ interval. Each persistence episode is reported once.
1783
+
1784
+ Repository conditions are included with no slugs or with `--repository`.
1785
+ Routine outcomes and self-healing failures are silent. Repeatable
1786
+ `--event <glob>` replaces the default and selects exactly matching catalog
1787
+ events (plus the existing `dispatcher.lost` observation); use explicit globs
1788
+ to opt into failures, finalize, merge, or completion events.
1789
+
1790
+ Derived records use `observation` instead of `event`, without inventing a
1791
+ durable sequence or actor. Merge observations carry `type`, `pr`, `reason`,
1792
+ and `observedAt`, plus the build slug and normal reduced state. Persistence
1793
+ observations carry source/cause/message/resetAt when advertised (or
1794
+ run/firstFailure/error), `episodeSeq`, and `observedAt`; repository records
1795
+ have `repo` and null build/state. Source observations also report
1796
+ `dependencyLimited`.
1797
+
1798
+ Every record carries a cursor, including timer-only records. Resuming suppresses
1799
+ delivered merge decisions and persistence episodes even when stream positions
1800
+ did not advance. Old v1 cursors remain valid: historical eligible merge
1801
+ decisions are treated as consumed; active overdue repository episodes may
1802
+ report because old watches had no persistence notification. Fresh attachment
1803
+ silently baselines historical direct events and merge decisions, but retains
1804
+ active repository timers. Reconcile existing conditions through status after
1805
+ arming the watch. Named watches still end when every named build is terminal;
1806
+ learn their outcomes from status reconciliation at exit.
1652
1807
 
1653
1808
  With `--json`, stdout is newline-delimited JSON — exactly one object per
1654
1809
  matching event, flushed as soon as it is written — with fields `build` (slug,
@@ -1745,7 +1900,7 @@ vocabulary is closed: `blocked`, `paused`, `running`, `done`, `aborted`,
1745
1900
  `terminal` (done or aborted), `pr=open`, `pr=merged`, `pr=closed`,
1746
1901
  `pr=conflicted`, `phase=<phase>` (a core phase — `plan`, `plan-review`,
1747
1902
  `implement`, `code-review`, `finalize`, `reconcile` — or `verify:<step>`), and
1748
- `attention` (any event in the `ab watch` attention set). Repeatable `--event`
1903
+ `attention` (an open escalation or a build waiting for a merge decision). Repeatable `--event`
1749
1904
  globs follow the `ab watch` rules. Multiple values are alternatives: the first
1750
1905
  one satisfied ends the wait — all `--for` values in supply order, then all
1751
1906
  globs in supply order — and the record names what matched.
@@ -1756,8 +1911,11 @@ means an open escalation exactly as everywhere else. In the manner of `kubectl
1756
1911
  wait`, a condition that already holds when the command starts returns
1757
1912
  immediately with that build's latest event (`--since` does not suppress this);
1758
1913
  otherwise the first later event whose reduction produces it ends the wait.
1759
- `attention` and `--event` conditions fire only on events appended after the
1760
- command started, or on a cursor-resumed backlog after `--since`.
1914
+ `attention` is also a current-state condition: an already open escalation or
1915
+ merge decision returns immediately, including with `--since`. Merge matches
1916
+ use the same observation as watch. During replay, only new escalation events
1917
+ or undelivered eligible merge decisions satisfy attention. `--event` conditions
1918
+ fire only on new events or a cursor-resumed backlog.
1761
1919
 
1762
1920
  The record is the `ab watch` record plus a `condition` field naming the
1763
1921
  satisfied `--for` value or `--event` glob. Unlike `ab watch`, there is no
@@ -1920,3 +2078,43 @@ build phase, which is the criterion for membership (§16.3): `ab-spec` and
1920
2078
  `ab-tickets` are human/agent-facing pre-build surfaces, `ab-guide` is
1921
2079
  read-only reference including the setup handoff guidance, and `ab-operate` is
1922
2080
  the manual for the orchestrator role, which any agent may take on.
2081
+
2082
+ ## Revision-checked ticket writes
2083
+
2084
+ Every ticket read includes an opaque `revision`. Pass it as `expectedRevision`
2085
+ (on MCP/operator requests) or `--expected-revision <opaque>` on `ab ticket update`,
2086
+ `move`, `block` and `unblock`. A stale write fails without changing anything and
2087
+ reports the current revision. Omission preserves unguarded behavior. Blocker
2088
+ checks cover the entire batch. Dispatcher moves remain unguarded and change the
2089
+ revision. Sources without guarded writes, including Linear, refuse supplied
2090
+ revisions rather than ignoring them.
2091
+
2092
+ `ab ticket edit <id>` opens `$VISUAL`, falling back to `$EDITOR`, using TOML
2093
+ frontmatter and a Markdown body. Only title, body and labels are editable;
2094
+ id, blockers, creation key and reference comments are read-only. It saves only
2095
+ changed fields with the opening revision. Unchanged or formatting-only edits
2096
+ write nothing. On conflict, invalid input or editor failure, the error names an
2097
+ absolute file path containing the edited text. Read the current ticket and
2098
+ reapply the intended changes before retrying; conflicts are never merged.
2099
+
2100
+ File and Linear revisions fingerprint title, body, canonical label/blocker sets
2101
+ and state; reverting identical content restores its fingerprint. File checks
2102
+ re-read immediately before writing, with the existing no-lock race window.
2103
+ Direct file edits and state-directory moves change revisions. File revisions also
2104
+ cover structured comments and portable attachments. Legacy file comments change
2105
+ the body; structured comments preserve it. Separate hosted comments do not change
2106
+ ticket revisions. Hosted revisions
2107
+ are monotonic decimal bigint strings, and checks run under the same row lock
2108
+ and transaction as all requested changes. Hosted reads return a coherent
2109
+ snapshot of fields, blockers and revision.
2110
+
2111
+ Read before writing, and copy the opaque revision verbatim:
2112
+
2113
+ ```sh
2114
+ ab ticket show AUT-8 --json
2115
+ ab ticket update AUT-8 --title 'Revised title' --expected-revision '<revision from show>'
2116
+ ab ticket edit AUT-8
2117
+ ```
2118
+
2119
+ Use `expectedRevision` on MCP `tickets.update`, `tickets.move`, `tickets.block`
2120
+ and `tickets.unblock` with the revision from `tickets.get` or `tickets.list`.