@defrex/autobuild 0.10.1 → 0.12.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 (120) hide show
  1. package/README.md +3 -1
  2. package/SPEC.md +237 -23
  3. package/bin/ab-dispatch-kernel.ts +1 -1
  4. package/docs/architecture.md +66 -4
  5. package/docs/configuration.md +137 -19
  6. package/docs/hosted-dispatcher.md +17 -2
  7. package/docs/hosted-service.md +214 -0
  8. package/docs/mcp.md +8 -2
  9. package/docs/operator-api.md +107 -7
  10. package/docs/remote-store-protocol.md +189 -144
  11. package/docs/remote-ticket-protocol.md +111 -18
  12. package/docs/setup.md +173 -16
  13. package/docs/ticket-dependencies.md +1 -1
  14. package/package.json +7 -6
  15. package/packages/core/src/cli/attention.ts +209 -0
  16. package/packages/core/src/cli/binary.ts +3 -3
  17. package/packages/core/src/cli/configured-store.ts +17 -0
  18. package/packages/core/src/cli/connect.ts +261 -0
  19. package/packages/core/src/cli/dashboard/model.ts +8 -5
  20. package/packages/core/src/cli/dashboard/render.ts +37 -7
  21. package/packages/core/src/cli/dispatch-frontend.ts +16 -1
  22. package/packages/core/src/cli/dispatch-process.ts +25 -7
  23. package/packages/core/src/cli/dispatch.ts +247 -47
  24. package/packages/core/src/cli/dispatcher-liveness.ts +91 -0
  25. package/packages/core/src/cli/help.ts +45 -13
  26. package/packages/core/src/cli/init-validation.ts +27 -2
  27. package/packages/core/src/cli/main.ts +41 -15
  28. package/packages/core/src/cli/mcp.ts +7 -1
  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/repository-status.ts +45 -5
  34. package/packages/core/src/cli/self-update.ts +10 -0
  35. package/packages/core/src/cli/store-opening.ts +22 -14
  36. package/packages/core/src/cli/terminals.ts +0 -10
  37. package/packages/core/src/cli/ticket.ts +8 -1
  38. package/packages/core/src/cli/upgrade-compatibility.ts +80 -0
  39. package/packages/core/src/cli/wait.ts +84 -17
  40. package/packages/core/src/cli/watch.ts +350 -68
  41. package/packages/core/src/config/live.ts +4 -0
  42. package/packages/core/src/config/schema.ts +65 -11
  43. package/packages/core/src/connection.ts +86 -0
  44. package/packages/core/src/events/catalog.ts +10 -2
  45. package/packages/core/src/events/globs.ts +8 -3
  46. package/packages/core/src/events/payloads.ts +44 -0
  47. package/packages/core/src/events/repository.ts +64 -2
  48. package/packages/core/src/integration/harness.ts +2 -1
  49. package/packages/core/src/kernel/__snapshots__/reducer-registry.test.ts.snap +148 -83
  50. package/packages/core/src/kernel/dispatch-status.ts +9 -1
  51. package/packages/core/src/kernel/dispatcher-liveness.ts +70 -0
  52. package/packages/core/src/kernel/engine.ts +24 -0
  53. package/packages/core/src/kernel/generators/build-log.ts +10 -0
  54. package/packages/core/src/kernel/log-index.ts +17 -0
  55. package/packages/core/src/kernel/pr-summary.ts +89 -0
  56. package/packages/core/src/kernel/reducer-registry.ts +12 -0
  57. package/packages/core/src/kernel/reducer.ts +8 -0
  58. package/packages/core/src/kernel/ticket-source-health.ts +30 -0
  59. package/packages/core/src/ontology.ts +1 -0
  60. package/packages/core/src/operator/index.ts +7 -0
  61. package/packages/core/src/operator/query.ts +44 -19
  62. package/packages/core/src/operator/ticket-source-backend.ts +1 -0
  63. package/packages/core/src/operator/tickets.ts +6 -8
  64. package/packages/core/src/plugin-sdk/index.ts +1 -0
  65. package/packages/core/src/plugins/manifest.ts +5 -3
  66. package/packages/core/src/plugins/runtimes.ts +3 -1
  67. package/packages/core/src/ports/forge/fake.ts +13 -1
  68. package/packages/core/src/ports/forge/github-transport.ts +36 -1
  69. package/packages/core/src/ports/forge/github.ts +29 -5
  70. package/packages/core/src/ports/forge/local-git.ts +12 -4
  71. package/packages/core/src/ports/forge/summary-marker.ts +8 -0
  72. package/packages/core/src/ports/runner/session-env.ts +1 -0
  73. package/packages/core/src/ports/tickets/contract.ts +25 -0
  74. package/packages/core/src/ports/tickets/create.ts +11 -3
  75. package/packages/core/src/ports/tickets/fake.ts +10 -8
  76. package/packages/core/src/ports/tickets/file.ts +1 -0
  77. package/packages/core/src/ports/tickets/hosted-index.ts +0 -3
  78. package/packages/core/src/ports/tickets/linear.ts +241 -49
  79. package/packages/core/src/ports/tickets/remote-protocol.ts +3 -0
  80. package/packages/core/src/ports/tickets/remote.ts +1 -0
  81. package/packages/core/src/ports/types.ts +17 -0
  82. package/packages/core/src/ports/workspace/build-execution.ts +5 -1
  83. package/packages/core/src/ports/workspace/create.ts +11 -2
  84. package/packages/core/src/ports/workspace/distribution-archive.ts +3 -5
  85. package/packages/core/src/ports/workspace/harvest-execution.ts +3 -1
  86. package/packages/core/src/ports/workspace/local-build-execution.ts +8 -1
  87. package/packages/core/src/ports/workspace/vercel-sandbox.ts +47 -19
  88. package/packages/core/src/processes/build-execution-state.ts +1 -1
  89. package/packages/core/src/processes/build-runner.ts +87 -26
  90. package/packages/core/src/processes/dispatch-failure-budget.ts +83 -0
  91. package/packages/core/src/processes/dispatcher.ts +336 -83
  92. package/packages/core/src/processes/harvest-child.ts +1 -0
  93. package/packages/core/src/processes/harvest-execution-state.ts +19 -2
  94. package/packages/core/src/processes/harvest-runner.ts +142 -100
  95. package/packages/core/src/processes/phase-failure-budget.ts +1 -0
  96. package/packages/core/src/processes/publication-loss.ts +18 -1
  97. package/packages/core/src/processes/repo-view.ts +2 -1
  98. package/packages/core/src/store/adapter.ts +1 -0
  99. package/packages/core/src/store/contract.ts +109 -0
  100. package/packages/core/src/store/digest.ts +14 -2
  101. package/packages/core/src/store/local/store.ts +18 -5
  102. package/packages/core/src/store/remote/client.ts +1 -1
  103. package/packages/core/src/store/remote/compatibility.ts +90 -0
  104. package/packages/core/src/store/remote/credentials.ts +28 -0
  105. package/packages/core/src/store/remote/index.ts +14 -1
  106. package/packages/core/src/store/remote/protocol.ts +26 -0
  107. package/packages/core/src/store/remote/token.ts +74 -13
  108. package/packages/core/src/store/repo-state-events.ts +49 -59
  109. package/packages/core/src/store/types.ts +5 -1
  110. package/packages/core/src/testing/index.ts +7 -0
  111. package/skills/guide/SKILL.md +175 -26
  112. package/skills/guide/references/operator-api.md +135 -13
  113. package/skills/guide/references/plugin-authoring.md +10 -2
  114. package/skills/guide/references/remote-store-protocol.md +189 -144
  115. package/skills/guide/references/remote-ticket-protocol.md +111 -18
  116. package/skills/guide/references/setup.md +173 -16
  117. package/skills/guide/references/ticket-dependencies.md +1 -1
  118. package/skills/operate/SKILL.md +159 -34
  119. package/templates/hosted-service/package.json +13 -0
  120. package/templates/hosted-service/vercel.json +7 -0
@@ -234,6 +234,19 @@ Every other known table is closed to unknown keys. The removed `[dashboardFrames
234
234
  `[dispatcher]`, `[harvest]`, and `[outer]` tables have no aliases or migration
235
235
  shims; they fail as ordinary unknown top-level keys.
236
236
 
237
+ Ticket-source ready scans run immediately when no completed measurement exists,
238
+ then every
239
+ `ticketScanIntervalSeconds` (default 60 seconds, a positive integer). Actual
240
+ tick spacing is the floor when the configured interval is shorter. A
241
+ same-repository `pr.merged`, `build.completed`, or `build.aborted` fact forces
242
+ another scan on the next tick; changed ready criteria also force a scan.
243
+ Housekeeping keeps its ordinary tick cadence. Between scans, queue depth and
244
+ standing diagnostics repeat the last completed measurement, including across
245
+ process restarts. Tickets groomed directly in the tracker are picked up within
246
+ one scan interval plus one tick when capacity permits. Observation while intake
247
+ is off shares the cadence; an observation measurement cannot suppress a fresh
248
+ admission scan when intake is enabled.
249
+
237
250
  ### Root scalars
238
251
 
239
252
  Root scalars must appear before the first TOML table; TOML otherwise nests them
@@ -242,6 +255,7 @@ under the preceding table.
242
255
  | Field | Default | Allowed / constraints | Effect |
243
256
  |---|---|---|---|
244
257
  | `baseBranch` | `"main"` | nonempty string | The branch builds branch from and target with their PR; what `reconcile` merges into the build branch. |
258
+ | `ticketScanIntervalSeconds` | `60` | positive integer seconds | Hot-reloaded ready scan cadence; tick spacing is the floor, and same-repository merge/completion/abort forces an early scan. |
245
259
  | `capacity` | `1` | positive integer | Maximum concurrent builds for this repository. |
246
260
  | `forge` | `"github"` | nonblank string | Selects shipped `github` or `local-git`, or a plugin-registered Forge adapter. `github` authenticates with `GITHUB_TOKEN`, then `GH_TOKEN`, then the gh CLI's stored login. |
247
261
  | `plugins` | `[]` | array of unique nonblank module specifiers | Trusted Bun plugin modules loaded before dispatch, `ab ticket`, and scoped phase wiring. |
@@ -269,6 +283,19 @@ through the strict versioned manifest and environment-only secrets, to the
269
283
  unchanged shared contract suite and optional npm publication—read
270
284
  [`references/plugin-authoring.md`](references/plugin-authoring.md).
271
285
 
286
+ ### `[store]`
287
+
288
+ | Key | Required | Values / purpose |
289
+ |---|---|---|
290
+ | `url` | When `[store]` exists | Absolute HTTPS service base URL; loopback HTTP is allowed for development. No credentials, query or fragment. Trailing slashes are normalized. |
291
+
292
+ Selection is explicit `--store` > nonblank `AB_STORE` > `[store] url` > the main
293
+ checkout's `.autobuild/`. A configured service without a user credential fails
294
+ with `ab connect` guidance. `AB_TOKEN` overrides the saved service/project
295
+ credential. See [repository setup](references/setup.md#connect-a-project).
296
+ Changing the store requires restarting dispatch.
297
+
298
+
272
299
  ### `[pr]`
273
300
 
274
301
  Optional PR-projection settings. Omission leaves artifact attachments text-only.
@@ -394,6 +421,28 @@ manager.
394
421
 
395
422
  ### `[verify]`
396
423
 
424
+ Post-reconcile verification defaults to enabled: `[verify].afterReconcile = true`.
425
+ Set it to `false` to retain pre-reconcile verification and skip all steps after
426
+ reconciliation. An optional boolean `[verify.<step>].afterReconcile` overrides
427
+ the repository setting in either direction; overrides require a configured step.
428
+ For example, set `afterReconcile = false` in `[verify]` and
429
+ `afterReconcile = true` in `[verify.types]` to keep only that step re-running.
430
+
431
+ The kernel freezes the resolved ordered policy in
432
+ `verify.reconcile-policy-captured` at the next engine action after reconcile
433
+ completion. Live configuration changes apply at that boundary; a cycle already
434
+ started keeps its policy through recovery. Historical cycles already started
435
+ without a captured policy retain the rerun-all behavior. Initial verification
436
+ and verification after subsequent code review always use ordinary applicability.
437
+ Policy exclusion precedes plan selection and paths, including `always = true`.
438
+ Enabled steps still follow plan selection and path rules. Excluded steps record
439
+ `verify.completed` with outcome `skipped`, `skipCause = "post-reconcile-policy"`,
440
+ and a reason naming the setting. Dashboards and PR summaries call this
441
+ "skipped by post-reconcile policy"; it is never a pass. Forge-side required
442
+ checks and branch protections still gate merge. For excluded steps, merged code
443
+ is not re-verified, so breakage that only exists in the combination can land.
444
+
445
+
397
446
  `steps` orders the verify phases; each entry `"<step>"` becomes a
398
447
  `verify:<step>` phase and needs its own `[verify.<step>]` subtable. Those
399
448
  subtables are part of this section — their fields are listed here.
@@ -405,6 +454,7 @@ subtables are part of this section — their fields are listed here.
405
454
  | `command` | — | **required when `kind = "check"`**, nonempty string | Ref into `[commands]` — the key, not a shell string. Pass/fail is the command's exit status. |
406
455
  | `skill` | — | **required when `kind = "agent"`**, nonempty string | Installed skill name to run (e.g. a repository-authored `"verify-app-e2e"`). |
407
456
  | `paths` | — | optional nonempty array of positive repository-relative globs | Makes either step kind apply when any changed path matches any selector. Omitted means unconditional. |
457
+ | `afterReconcile` | repository: `true`; step: inherited | boolean | Step override wins over `[verify].afterReconcile`; false skips this step after reconciliation only. |
408
458
  | `always` | — | optional boolean | `true` makes the step unconditional and mandatory (a plan cannot deselect it); `false` is equivalent to omission. |
409
459
 
410
460
  A plan may begin with strict TOML front matter selecting the complete set of
@@ -474,7 +524,7 @@ A selected step's `paths` are evaluated immediately before the step against
474
524
  current `HEAD`, relative to the initial branch-cut SHA or the refreshed base
475
525
  from the latest completed reconcile. It uses a NUL-delimited, no-rename Git diff, so
476
526
  adds/modifications/deletions and both rename sides participate. A reconcile
477
- starts a fresh cycle and evaluation repeats; upstream-only merged paths are
527
+ starts a fresh cycle and evaluation repeats for policy-enabled steps; upstream-only merged paths are
478
528
  excluded while build-owned resolutions remain visible. A miss starts no
479
529
  command, agent, or session and records `skipped` with
480
530
  `excluded by [verify.<step>].paths: no changed path matched <JSON paths>`.
@@ -674,6 +724,7 @@ nonnegative integer so zero can disable that trigger.
674
724
  | `sessionBudgetSeconds` | `3600` | positive integer | Wall-clock bound for each build agent session unless its logical role overrides it. |
675
725
  | `stallRounds` | `3` | positive integer | The same finding surviving this many review rounds auto-escalates to a human — the anti-loop guard. |
676
726
  | `maxVerifyAttempts` | `3` | positive integer | Caps the `verify → implement → verify` cycle before escalation. |
727
+ | `maxDispatchAttempts` | `3` | positive integer | Caps consecutive dispatch failures before a setup-targeted `dispatch-failure-limit` escalation. |
677
728
  | `maxSetupAttempts` | `3` | positive integer | Caps consecutive `[commands].setup` failures before a setup-targeted human escalation. |
678
729
  | `maxInfrastructureAttempts` | `3` | positive integer | Caps consecutive remote provider lifecycle failures before a setup-targeted human escalation; a `retry` answer re-arms the budget. |
679
730
  | `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. |
@@ -681,6 +732,11 @@ nonnegative integer so zero can disable that trigger.
681
732
  | `harvestThreshold` | `5` | positive integer | Newly unclaimed `observation.recorded` occurrences required to start one repository harvest run. |
682
733
  | `harvestMaxDrift` | `3` | nonnegative integer | Other builds merged after the oldest unclaimed observation required to start a run; `0` disables drift. |
683
734
 
735
+ Dispatch failures share one budget across stages until runner attachment. At
736
+ exhaustion dispatch parks durably; bare `ab answer <slug>` and text guidance
737
+ both grant a fresh budget, while abort terminates without retrying. Workspace
738
+ failures owned by provider infrastructure recovery use only its existing budget.
739
+
684
740
  For each repeat conflict, Autobuild compares the base merged by the latest
685
741
  completed reconcile with a fresh authoritative base snapshot. An advanced base
686
742
  proves the attempt lost a race and permits another reconcile regardless of the
@@ -698,7 +754,7 @@ roundless no-progress answer. An already exhausted no-progress budget raises its
698
754
  matching cause separately before another reconcile. Cause-less historical
699
755
  raises retain their former round-shaped reset behavior, and roundless
700
756
  policy/reconcile raises retain their former no-progress meaning, on replay
701
- without migration. Post-reconcile verification still runs in full and remains
757
+ without migration. Post-reconcile verification follows the captured policy and remains
702
758
  independently bounded by `maxVerifyAttempts`.
703
759
 
704
760
  `stallRounds` counts *persistence chains*, which reviewers mark and the kernel
@@ -734,10 +790,11 @@ state eligible.
734
790
  | `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. |
735
791
  | `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. |
736
792
  | `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. |
737
- | `teamKey` | — | required by `linear`, forbidden by `file`, allowed for plugins | The Linear team key (e.g. `"ENG"`) or an existing plugin configuration field. |
793
+ | `teamKey` | — | required by `linear` and `hosted`, forbidden by `file`, allowed for plugins | The Linear team key (e.g. `"ENG"`) or an existing plugin configuration field. |
738
794
  | `claimedState` | — | optional nonempty string; forbidden by `file`, allowed for plugins | Workflow state `claim()` moves a ticket to when a build starts. |
795
+ | `doneState` | hosted: `"Done"` | optional nonblank string | Project completion state and merged-ticket transition; hosted tickets only. |
739
796
  | `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). |
740
- | `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. |
797
+ | `triageState` | — | optional, nonempty string | State the dispatcher hands tickets back to for human triage — spec-gate bounces, aborted builds, closed-unmerged PRs. Absent = Linear/hosted: Backlog; file/plugin: Triage. Must name a state the tracker actually has — a Linear team only has "Triage" when its triage feature is enabled. |
741
798
  | `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. |
742
799
  | `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. |
743
800
 
@@ -815,7 +872,7 @@ changing it requires a dispatcher restart.
815
872
  | `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. |
816
873
  | `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. |
817
874
  | `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. |
818
- | `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. |
875
+ | `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. |
819
876
  | `sandbox` | — | strict subtable | The sandbox behavior knobs; absence reads as the defaults below. |
820
877
 
821
878
  `[orchestrator.sandbox]` fields:
@@ -1035,7 +1092,7 @@ There is no home-directory fallback: a repository with no state tree starts
1035
1092
  empty.
1036
1093
 
1037
1094
  Every command uses the same precedence: explicit `--store <ref>` > nonempty
1038
- `AB_STORE` > `<main-repo>/.autobuild`. Relative local overrides are normalized
1095
+ `AB_STORE` > `[store] url` > `<main-repo>/.autobuild`. Relative local overrides are normalized
1039
1096
  against the main checkout. A local override relocates the whole state tree,
1040
1097
  including `worktrees/` and default file tickets; an explicit `[tickets].dir`
1041
1098
  remains repo-relative. An HTTP(S) URL still selects the remote store unchanged,
@@ -1043,6 +1100,46 @@ while worktrees and default file tickets remain under the repository-default
1043
1100
  `.autobuild/` directory. The dispatcher passes its normalized selection to
1044
1101
  every agent session as `AB_STORE`.
1045
1102
 
1103
+ ## Connect a project
1104
+
1105
+ From a checkout with an origin remote, run `ab connect https://YOUR_ORIGIN`.
1106
+ The CLI prints a verification URL and short code and attempts to open a browser.
1107
+ Sign in as an allowed owner, check the full repository identity and code, and
1108
+ approve or deny. Another device can open the printed URL, or visit `/connect`
1109
+ and enter the code. Approval requires the web-enabled service origin; a
1110
+ standalone machine-only listener cannot present browser consent. Requests
1111
+ expire after ten minutes. Denial and timeout leave local configuration unchanged.
1112
+
1113
+ On approval the service registers the project and issues a project credential.
1114
+ The CLI keeps it in `$XDG_CONFIG_HOME/autobuild/credentials` (or
1115
+ `~/.config/autobuild/credentials`), keyed by normalized service and origin,
1116
+ with directory mode 0700 and file mode 0600. Credentials never belong in the
1117
+ repository. It adds the non-secret `[store] url` to `autobuild.toml` when
1118
+ absent; review and commit that change yourself. An existing different URL
1119
+ must be changed explicitly before connecting. Reconnecting replaces the saved
1120
+ credential only after approval and revokes the previous credential.
1121
+
1122
+ All worktrees share the main checkout's configuration and the user's saved
1123
+ credential. A service manager running as that user uses the same HOME and
1124
+ XDG_CONFIG_HOME, with no AB_STORE or AB_TOKEN exports. A fresh clone with
1125
+ committed store configuration but no saved credential fails with `ab connect`
1126
+ guidance; it never silently creates local state. Run connect on each host/user.
1127
+
1128
+ `ab connect` without a URL reports service, project, and credential status:
1129
+ accepted, rejected, missing, or unreachable. `ab disconnect` revokes the saved
1130
+ credential remotely before removing it locally, keeping the project registered,
1131
+ its data, and store configuration. A network/server failure retains the local
1132
+ entry for retry; an already revoked credential can be removed. If saving a new
1133
+ connection fails, the CLI cancels/revokes it and retains the previous entry.
1134
+ If remote cleanup cannot be confirmed, check the printed credential id with
1135
+ the owner's administrative tools before retrying.
1136
+
1137
+ Store selection is `--store` > nonblank `AB_STORE` > `[store] url` > the
1138
+ main checkout's `.autobuild/`. `AB_TOKEN` overrides the exact service/project's
1139
+ saved credential for CI and other non-interactive hosts. A service override
1140
+ never borrows another service's credential. Build and harvest sessions keep
1141
+ their explicit scoped authority.
1142
+
1046
1143
  ## Source-agnostic ticket operations
1047
1144
 
1048
1145
  The sessionless `ab ticket` namespace constructs whichever TicketSource the
@@ -1097,7 +1194,7 @@ to a ticket that has already been claimed into a build does not stop that build.
1097
1194
  For dependent tickets, follow the
1098
1195
  [safe blocker-filing workflow](references/ticket-dependencies.md): determine the
1099
1196
  intended final destination, stage outside ready, verify all blocker relationships,
1100
- and move to ready last. Direct and hosted Linear create the issue and its blockers
1197
+ and move to ready last. Direct Linear creates the issue and its blockers
1101
1198
  in separate requests; one `create --blocked-by` call is not atomic publication.
1102
1199
  Harvest's creation reservations cover Harvest only, not interactive CLI creates.
1103
1200
  Dependency order and JSON id extraction are still required, but do not alone
@@ -1159,7 +1256,7 @@ judge against it.
1159
1256
  is the read-only binary retrieval form. Operators need no session identity: it
1160
1257
  works after a build is terminal, writes exact bytes (creating parent
1161
1258
  directories), verifies that the build belongs to this repository, and uses the
1162
- standard explicit `--store` > nonblank `AB_STORE` > repository-local selection.
1259
+ standard explicit `--store` > nonblank `AB_STORE` > `[store] url` > repository-local selection.
1163
1260
  Inside a phase, a complete ambient tuple permits only the ambient build; a
1164
1261
  foreign build or malformed/partial identity is rejected. Use the exact pinned
1165
1262
  `@rev` from a PR attachment command.
@@ -1493,7 +1590,7 @@ Every command requires the target to exist in this repository and be active
1493
1590
  `ab answer` additionally requires an open escalation. A stale, missing, done,
1494
1591
  aborted, or otherwise inapplicable target is an error and gets no new event. Attribution uses `USER`, then `USERNAME`, then the
1495
1592
  same stable fallback on both surfaces. All five commands run sessionless and
1496
- accept `--store <ref>` with the usual explicit flag > `AB_STORE` > repository
1593
+ accept `--store <ref>` with the usual explicit flag > `AB_STORE` > `[store] url` > repository
1497
1594
  local precedence. If nonblank `AB_SESSION` and matching `AB_BUILD` identify the
1498
1595
  caller's own phase build, the command refuses it; a phase cannot pause, resume,
1499
1596
  auto-merge, answer, or abort itself. `ab pause --all` and `ab resume --all`
@@ -1539,7 +1636,7 @@ repository-wide pause (`paused`, default `false`), and the claim-time auto-merge
1539
1636
  default (`defaultAutoMerge`, default `false`). Those defaults also describe a
1540
1637
  repository with no journal row; the read does not create one. Human output is
1541
1638
  the default. `--json` emits one bare object with those exact fields plus the
1542
- canonical `repo`, and `--store` follows the usual explicit flag > `AB_STORE` >
1639
+ canonical `repo`, and `--store` follows the usual explicit flag > `AB_STORE` > `[store] url` >
1543
1640
  repository-local precedence. The command uses the same reducer as the
1544
1641
  dispatcher and dashboard, writes no state, claims no ticket, attaches no runner,
1545
1642
  and starts no dispatcher work.
@@ -1622,19 +1719,51 @@ streams matching events as they land instead of answering once. With no slug it
1622
1719
  follows every nonterminal build in the repository — a build created while the
1623
1720
  watch runs joins it without a restart — and naming one or more slugs watches
1624
1721
  exactly those builds, ending the watch when all of them reach a terminal status.
1625
- `--repository` additionally follows the repository journal; without it,
1626
- repository events are never emitted.
1627
-
1628
- The default filter is the attention set — the events something must wake up
1629
- for. For builds that is exactly: `escalation.raised`, `phase.failed`,
1630
- `infrastructure.failed`, `runner.setup-failed`, `dispatch.failed`,
1631
- `publication.lost`, `finalize.completed`, `pr.conflicted`, `pr.merged`,
1632
- `pr.closed`, `build.completed`, `build.aborted`. For the repository journal:
1633
- `harvest.escalated`, `harvest.failed`, `dispatcher.tick-failed`,
1634
- `dispatcher.config-rejected`, `dispatcher.harvest-runner-failed`. A repeatable
1635
- `--event <glob>` (`escalation.*`, `pr.*`, `*` — `*` matches any run of
1636
- characters, `?` one) replaces that filter, and every glob must match at least
1637
- one known event type or the command fails before reading the store.
1722
+ With no slugs the repository journal is included; named watches add it with
1723
+ `--repository`.
1724
+
1725
+ The default watch reports actionable conditions only:
1726
+
1727
+ - Build `escalation.raised`, or `build.merge-decision`: a finalized open PR
1728
+ with auto-merge off or a current actionable merge-gate deferral. Armed consent
1729
+ awaiting application is silent. One merge decision is reported per build/PR.
1730
+ - Repository `harvest.escalated`, `harvest.recovery-exhausted`,
1731
+ `dispatcher.config-rejected`, `dispatcher.config-reloaded` with nonempty
1732
+ `restartRequired`, abnormal `dispatcher.run-stopped`, and `dispatcher.lost`.
1733
+ - `dispatcher.ticket-source-persistent`: degradation past its advertised
1734
+ reset time, once a tick started at or after reset has completed or failed
1735
+ with the episode still open; crossing reset alone is silent. Without a reset,
1736
+ the deadline is 15 minutes after onset. Repeated evidence never postpones
1737
+ the deadline; recovery ends the episode.
1738
+ - `dispatcher.tick-failure-persistent`: continuously failing completed ticks
1739
+ for five minutes in one run. A successful tick, new run, or matching stop
1740
+ ends the episode. Source grace and cooldown-driven failures do not start
1741
+ this clock; after grace, the first qualifying failure starts a new five-minute
1742
+ interval. Each persistence episode is reported once.
1743
+
1744
+ Repository conditions are included with no slugs or with `--repository`.
1745
+ Routine outcomes and self-healing failures are silent. Repeatable
1746
+ `--event <glob>` replaces the default and selects exactly matching catalog
1747
+ events (plus the existing `dispatcher.lost` observation); use explicit globs
1748
+ to opt into failures, finalize, merge, or completion events.
1749
+
1750
+ Derived records use `observation` instead of `event`, without inventing a
1751
+ durable sequence or actor. Merge observations carry `type`, `pr`, `reason`,
1752
+ and `observedAt`, plus the build slug and normal reduced state. Persistence
1753
+ observations carry source/cause/message/resetAt when advertised (or
1754
+ run/firstFailure/error), `episodeSeq`, and `observedAt`; repository records
1755
+ have `repo` and null build/state. Source observations also report
1756
+ `dependencyLimited`.
1757
+
1758
+ Every record carries a cursor, including timer-only records. Resuming suppresses
1759
+ delivered merge decisions and persistence episodes even when stream positions
1760
+ did not advance. Old v1 cursors remain valid: historical eligible merge
1761
+ decisions are treated as consumed; active overdue repository episodes may
1762
+ report because old watches had no persistence notification. Fresh attachment
1763
+ silently baselines historical direct events and merge decisions, but retains
1764
+ active repository timers. Reconcile existing conditions through status after
1765
+ arming the watch. Named watches still end when every named build is terminal;
1766
+ learn their outcomes from status reconciliation at exit.
1638
1767
 
1639
1768
  With `--json`, stdout is newline-delimited JSON — exactly one object per
1640
1769
  matching event, flushed as soon as it is written — with fields `build` (slug,
@@ -1650,6 +1779,23 @@ matching event after that cursor is delivered exactly once, in per-stream
1650
1779
  sequence order — including events that landed while no watch was running. A
1651
1780
  cursor from a different repository or store is rejected.
1652
1781
 
1782
+
1783
+ Dispatcher liveness is also visible through `ab repository status --json`:
1784
+ `dispatcher.holder` identifies the unexpired repository lease owner, independently
1785
+ of `latestRun`. Heartbeat age measures attributable renewals; tick age measures
1786
+ durable progress. Missing historical evidence is null. The liveness query uses
1787
+ bounded repository facts first, with a full-history fallback when a later
1788
+ invocation hides the holder start or the latest stop. Other state reads retain
1789
+ their bounded semantics.
1790
+
1791
+ Repository-following `ab watch` additionally emits a read-only `dispatcher.lost`
1792
+ observation when an observed lease expires without a matching stop. JSON carries
1793
+ `observation` instead of `event`, with run, observedAt, leaseExpiresAt and available
1794
+ identity, plus the usual repo/build/state/cursor fields. It invents no durable
1795
+ sequence or actor. Retain the cursor to avoid repeat reports across invocations.
1796
+ No lease alone is not evidence of death. SIGINT, SIGHUP and SIGTERM drain dispatch,
1797
+ record the first signal in the stop fact, and release its lease before exit.
1798
+
1653
1799
  The watch ends with exit 0 when `--timeout` elapses (30 minutes by default;
1654
1800
  `0` unbounded), after `--count <n>` records, or when named builds have all
1655
1801
  gone terminal; with no slugs named, an empty set of active builds does not end
@@ -1714,7 +1860,7 @@ vocabulary is closed: `blocked`, `paused`, `running`, `done`, `aborted`,
1714
1860
  `terminal` (done or aborted), `pr=open`, `pr=merged`, `pr=closed`,
1715
1861
  `pr=conflicted`, `phase=<phase>` (a core phase — `plan`, `plan-review`,
1716
1862
  `implement`, `code-review`, `finalize`, `reconcile` — or `verify:<step>`), and
1717
- `attention` (any event in the `ab watch` attention set). Repeatable `--event`
1863
+ `attention` (an open escalation or a build waiting for a merge decision). Repeatable `--event`
1718
1864
  globs follow the `ab watch` rules. Multiple values are alternatives: the first
1719
1865
  one satisfied ends the wait — all `--for` values in supply order, then all
1720
1866
  globs in supply order — and the record names what matched.
@@ -1725,8 +1871,11 @@ means an open escalation exactly as everywhere else. In the manner of `kubectl
1725
1871
  wait`, a condition that already holds when the command starts returns
1726
1872
  immediately with that build's latest event (`--since` does not suppress this);
1727
1873
  otherwise the first later event whose reduction produces it ends the wait.
1728
- `attention` and `--event` conditions fire only on events appended after the
1729
- command started, or on a cursor-resumed backlog after `--since`.
1874
+ `attention` is also a current-state condition: an already open escalation or
1875
+ merge decision returns immediately, including with `--since`. Merge matches
1876
+ use the same observation as watch. During replay, only new escalation events
1877
+ or undelivered eligible merge decisions satisfy attention. `--event` conditions
1878
+ fire only on new events or a cursor-resumed backlog.
1730
1879
 
1731
1880
  The record is the `ab watch` record plus a `condition` field naming the
1732
1881
  satisfied `--for` value or `--event` glob. Unlike `ab watch`, there is no
@@ -6,17 +6,48 @@ Every request must send `X-Autobuild-Version`,
6
6
  `X-Autobuild-Protocol-Version: 3`, and `Authorization: Bearer …`. Identity is
7
7
  checked before authentication or resource lookup.
8
8
 
9
- Mint a token offline; the signed user is used as the human actor on every write:
9
+ Project credentials are opaque `abp_1_<id>_<secret>` bearer values bound to one
10
+ registered normalized repository. The service stores only a SHA-256 digest of
11
+ the random secret, plus project, creator, creation time, last use, expiry and
12
+ revocation metadata. It checks credential status and project registration on
13
+ every request. Revocation takes effect on the next request without affecting
14
+ other credentials. Unknown, expired or revoked credentials receive `401 auth`;
15
+ removed projects receive `403 auth` with `project not registered`.
16
+
17
+ The deployment owner runs these commands locally with database access:
10
18
 
11
19
  ```sh
12
- AB_STORE_SECRET='…' bun packages/hosted-store-service/src/bin.ts mint operator \
13
- --user 'Ada Lovelace' --ttl-seconds 3600
20
+ ab-hosted-store credentials issue https://github.com/acme/app --user owner@example.com
21
+ ab-hosted-store credentials list https://github.com/acme/app
22
+ ab-hosted-store credentials revoke https://github.com/acme/app --id ID --user owner@example.com
14
23
  ```
15
24
 
16
- Admin, build, repository, and deployment (`{ "operator": true, … }`) tokens
17
- have no attributed operator authority. Human-operator tokens have no raw
18
- `/builds`, `/repos`, `/sessions`, or `/tickets` authority. Encode repository,
19
- build, session, and artifact kind as separate URL path segments.
25
+ Issue prints the secret once; list contains metadata only. Issue optionally
26
+ accepts `--ttl-seconds N` or `--expires-at ISO-8601`. For administrative/non-interactive issuance, set the returned token as
27
+ `AB_TOKEN`. Interactive project onboarding uses `ab connect <service-url>`;
28
+ it saves the credential outside the repository and commits no secret. Previous `AB_TOKEN` values and `mint operator` / `mint build` are
29
+ retired: the service responds that the credential format changed and points
30
+ to `credentials issue`. Supported old protocol shapes still work with a new
31
+ credential; protocol compatibility does not restore retired authentication.
32
+
33
+ `mint admin --ttl-seconds N` creates an explicit versioned, attributed owner
34
+ break-glass credential. It can cross project boundaries. Keep it owner-only:
35
+ never give it or `AB_STORE_SECRET` to a dispatcher or build agent.
36
+
37
+ Owner-purpose project credentials attribute every write to their recorded creator.
38
+ Dispatcher-purpose credentials are refused. Signed-in owners in the dashboard
39
+ and MCP receive short-lived internal signatures with an explicit version,
40
+ selected repository, signed user, expiry and MCP `via`. Only the service holds
41
+ the signing key. These signatures authorize operator routes only and require a
42
+ registered project; replay against another project is refused.
43
+
44
+ The path project is checked before registry lookup, body parsing and generic
45
+ tool execution. Body repositories and secondary build/session identifiers are
46
+ also checked inside that project. Explicit project mismatches return `403 auth:
47
+ wrong project`; removal returns `403 auth: project not registered`. Encode each
48
+ repository, build, session and artifact kind as a separate URL path segment.
49
+ Registration changes use the database-local `projects list|add|remove` owner
50
+ commands; see [owner workflow](setup.md#connected-projects).
20
51
 
21
52
  ## Operator sessions
22
53
 
@@ -66,7 +97,11 @@ supplied forms return `400 validation`.
66
97
  Dashboard reads use the latest durable, run-correlated `effectiveConfig`
67
98
  repository artifact. Missing, corrupt, or invalid configuration returns a typed
68
99
  `409 effective-config-unavailable`; the service never guesses from a checkout.
69
- Clients poll these reads; streaming is not provided. `triageState`, `readyState`,
100
+ Clients poll these reads — the operator HTTP API exposes no push or long-poll
101
+ event transport. Streaming is provided as a CLI read surface: `ab watch`
102
+ streams build and repository events by polling the same event logs client-side
103
+ (the store protocol's `GET /builds/{slug}/events` and `GET /repos/{repo}/events`
104
+ reads), not as a transport on this API. `triageState`, `readyState`,
70
105
  and every entry in `states` are backend state names and must be sent back verbatim
71
106
  when used with the move control.
72
107
 
@@ -105,12 +140,25 @@ The retry object is deliberately bare: adding `text` is a `400 validation`
105
140
  rather than silently changing the resolution to guidance. A revision object may
106
141
  also carry `ceiling` only to receive the shared `incompatible-answer-options`
107
142
  refusal; a ceiling and spec revision cannot be performed together.
108
-
109
143
  The hosted service has no ticket-provider credentials. For ticket-origin
110
144
  revision, a trusted caller fetches the current ticket body and supplies it; the
111
145
  build must still have a recorded ticket. Revision conformance, lazy retry,
112
146
  artifact metadata, event ordering, and refusal rules are the same as the CLI.
113
147
 
148
+ ## The registry's protocol face: `POST …/tools/{tool}`
149
+
150
+ One generic route executes any entry of the agent tool registry (the same
151
+ closed table `ab mcp` serves over stdio and the hosted `/mcp` endpoint serves
152
+ over Streamable HTTP): the body is the tool's validated input — `repo`
153
+ included and required to equal the URL's repository — and the response is the
154
+ registry result verbatim. The verified operator token supplies the acting
155
+ identity and, when minted with one, the `via` attribution marker; the body
156
+ never does. Failures keep the shared `{kind,error,code?,progress?}` shape:
157
+ validation is `400`, auth `403`, not-found `404`, conflict and refusal `409`,
158
+ internal `500`. The route adds no authority of its own — it is the registry
159
+ behind the token check — so a tool available here is exactly the tool table,
160
+ and the table's contract suite is the single gate.
161
+
114
162
  ### Success results
115
163
 
116
164
  Build control returns one of:
@@ -157,8 +205,82 @@ headers with a server-minted token that expires after 30 seconds and carries
157
205
  the normalized signed-in email. Consequently every durable control event has
158
206
  the browser user's human actor while no token or signing secret reaches client
159
207
  code. Responses are private/no-store; a 401 sends the application back to sign
160
- in. Browser clients poll the visible dashboard every two seconds; live transcript
161
- streaming is not provided. Delegated bearer tokens remain server-side.
208
+ in. Browser clients poll the visible dashboard every two seconds; the browser
209
+ surface offers no streaming of its own — live transcript streaming remains
210
+ absent here, and the streaming read surface is the CLI's `ab watch`, which
211
+ polls the store's event reads client-side. Delegated bearer tokens remain
212
+ server-side.
213
+
214
+ The API does not expose phase-session commands, runner startup, a push or
215
+ long-poll streaming transport, or a generic event-append operation; event
216
+ streaming is composed client-side by `ab watch`, which polls the store's
217
+ event reads.
218
+
219
+
220
+ Dispatcher liveness is also visible through `ab repository status --json`:
221
+ `dispatcher.holder` identifies the unexpired repository lease owner, independently
222
+ of `latestRun`. Heartbeat age measures attributable renewals; tick age measures
223
+ durable progress. Missing historical evidence is null. The liveness query uses
224
+ bounded repository facts first, with a full-history fallback when a later
225
+ invocation hides the holder start or the latest stop. Other state reads retain
226
+ their bounded semantics.
227
+
228
+ Repository-following `ab watch` additionally emits a read-only `dispatcher.lost`
229
+ observation when an observed lease expires without a matching stop. JSON carries
230
+ `observation` instead of `event`, with run, observedAt, leaseExpiresAt and available
231
+ identity, plus the usual repo/build/state/cursor fields. It invents no durable
232
+ sequence or actor. Retain the cursor to avoid repeat reports across invocations.
233
+ No lease alone is not evidence of death. SIGINT, SIGHUP and SIGTERM drain dispatch,
234
+ record the first signal in the stop fact, and release its lease before exit.
235
+
236
+ ### CLI attention monitoring
237
+
238
+ The default watch reports actionable conditions only:
239
+
240
+ - Build `escalation.raised`, or `build.merge-decision`: a finalized open PR
241
+ with auto-merge off or a current actionable merge-gate deferral. Armed consent
242
+ awaiting application is silent. One merge decision is reported per build/PR.
243
+ - Repository `harvest.escalated`, `harvest.recovery-exhausted`,
244
+ `dispatcher.config-rejected`, `dispatcher.config-reloaded` with nonempty
245
+ `restartRequired`, abnormal `dispatcher.run-stopped`, and `dispatcher.lost`.
246
+ - `dispatcher.ticket-source-persistent`: degradation past its advertised
247
+ reset time, once a tick started at or after reset has completed or failed
248
+ with the episode still open; crossing reset alone is silent. Without a reset,
249
+ the deadline is 15 minutes after onset. Repeated evidence never postpones
250
+ the deadline; recovery ends the episode.
251
+ - `dispatcher.tick-failure-persistent`: continuously failing completed ticks
252
+ for five minutes in one run. A successful tick, new run, or matching stop
253
+ ends the episode. Source grace and cooldown-driven failures do not start
254
+ this clock; after grace, the first qualifying failure starts a new five-minute
255
+ interval. Each persistence episode is reported once.
256
+
257
+ Repository conditions are included with no slugs or with `--repository`.
258
+ Routine outcomes and self-healing failures are silent. Repeatable
259
+ `--event <glob>` replaces the default and selects exactly matching catalog
260
+ events (plus the existing `dispatcher.lost` observation); use explicit globs
261
+ to opt into failures, finalize, merge, or completion events.
262
+
263
+ Derived records use `observation` instead of `event`, without inventing a
264
+ durable sequence or actor. Merge observations carry `type`, `pr`, `reason`,
265
+ and `observedAt`, plus the build slug and normal reduced state. Persistence
266
+ observations carry source/cause/message/resetAt when advertised (or
267
+ run/firstFailure/error), `episodeSeq`, and `observedAt`; repository records
268
+ have `repo` and null build/state. Source observations also report
269
+ `dependencyLimited`.
270
+
271
+ Every record carries a cursor, including timer-only records. Resuming suppresses
272
+ delivered merge decisions and persistence episodes even when stream positions
273
+ did not advance. Old v1 cursors remain valid: historical eligible merge
274
+ decisions are treated as consumed; active overdue repository episodes may
275
+ report because old watches had no persistence notification. Fresh attachment
276
+ silently baselines historical direct events and merge decisions, but retains
277
+ active repository timers. Reconcile existing conditions through status after
278
+ arming the watch. Named watches still end when every named build is terminal;
279
+ learn their outcomes from status reconciliation at exit.
162
280
 
163
- The API does not expose phase-session commands, runner startup, live streaming,
164
- or a generic event-append operation.
281
+ `ab wait --for attention` is build-only and uses the same build conditions:
282
+ an open escalation or a merge decision. Existing conditions return immediately,
283
+ even with `--since`; live and backlog matches ignore routine outcomes and
284
+ self-healing failures. A merge match uses the watch observation with
285
+ `condition: "attention"`. Explicit `--event` globs remain event-only.
286
+ Watch and wait preserve each other's cursor notification metadata.
@@ -40,7 +40,7 @@ should declare an API range containing `^1.5.0`.
40
40
 
41
41
  Plugin API 1.7 extends the workspace-provider factory context with the
42
42
  host-derived seams a store-requiring remote provider needs. Workspace-provider
43
- factories additionally receive `storeRef` and `storeToken` (guaranteed present
43
+ factories additionally receive `storeRef` and `delegateCredential` (guaranteed present
44
44
  when the registration declares `capabilities.storeRequirements`),
45
45
  `runtimeReferences` (the host's effective runtime routes; absent at call sites
46
46
  that reference no runtimes), and the checkout-less `origin` and
@@ -125,7 +125,7 @@ Every module must default-export one strict `AutobuildPluginManifest`:
125
125
  `[tickets]` fields, workspace providers receive `[workspace.config]`, and
126
126
  runtime and forge factories currently receive an empty `config` object.
127
127
  Workspace providers additionally receive the host-derived seams (API 1.7):
128
- `storeRef` and `storeToken` — guaranteed strings when the registration
128
+ `storeRef` and `delegateCredential` — guaranteed URL and exchange callback when the registration
129
129
  declares `capabilities.storeRequirements` — plus `runtimeReferences`,
130
130
  `origin`, and `remoteBranchHead`. Each seam is optional and absent when the
131
131
  host call site supplies no such value; absent `origin`/`remoteBranchHead`
@@ -369,3 +369,11 @@ policy says otherwise.
369
369
  Official npm references: [`package.json` fields](https://docs.npmjs.com/cli/v11/configuring-npm/package-json),
370
370
  [`npm pack`](https://docs.npmjs.com/cli/v11/commands/npm-pack), and
371
371
  [`npm publish`](https://docs.npmjs.com/cli/v11/commands/npm-publish).
372
+
373
+ Remote workspace factories must use `delegateCredential(storeRef, undefined,
374
+ target)` to obtain per-resource grants before provisioning or launching guests.
375
+ The callback keeps parent authority in host composition. `storeToken` is retired
376
+ and is never supplied; factory environments also exclude `AB_TOKEN`. Launch
377
+ credentials travel through the separate transient execution-auth argument, never
378
+ through serialized execution identity. Existing plugins that forwarded
379
+ `storeToken` must migrate to this callback and fail closed if it is unavailable.