@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
@@ -2,22 +2,71 @@
2
2
 
3
3
  The hosted ticket protocol exposes the complete `TicketSource` port without
4
4
  exposing backend credentials. It shares the Autobuild and remote protocol
5
- version headers with the remote BuildStore. Both must exactly match the service;
6
- this release uses remote protocol version `3`.
5
+ version headers with the remote BuildStore. The version must be in the
6
+ published service window and the protocol must
7
+ match its known release-line identity (`3` for 0.9, 0.10, and 0.11).
8
+
9
+ A service accepts stable clients from its own `major.minor` release line and
10
+ the two preceding minor lines in the same major. A major starts a new family:
11
+ X.0 accepts only X.0, X.1 accepts X.0/X.1, and X.2 onward accepts three lines.
12
+ Patch versions are interchangeable in both directions and must never change
13
+ the protocol. Upgrade the service first, then projects independently within
14
+ its window. Supported release lines retain their wire handlers and known
15
+ protocol identities; an unknown protocol is refused independently of version.
16
+ Malformed, missing, and prerelease identities are refused with HTTP 409.
17
+ A newer client is told to upgrade the service; a client below the window is
18
+ told to upgrade the project's Autobuild. Diagnostics name the service version
19
+ and supported range.
20
+
21
+ Unauthenticated `GET /health` publishes `ok`, `autobuildVersion`,
22
+ `protocolVersion`, and `supportedClientRange: { minVersion, maxVersionExclusive }`.
23
+ The range is inclusive at the minimum and exclusive at the maximum; a 0.11.x
24
+ service publishes `>=0.9.0 <0.12.0`. Health needs no credentials, version
25
+ headers, or available backend.
7
26
 
8
27
  ## Authentication and context
9
28
 
10
- Every request is `POST /tickets/<operation>`, has `Content-Type:
11
- application/json`, and requires `Authorization: Bearer <token>`. Only a signed,
12
- unexpired **deployment operator** token (`{ "operator": true, … }`) is accepted.
13
- Attributed human-operator, legacy admin, build, repository, and session tokens
14
- remain valid for their own routes but are denied on `/tickets`. Mint a deployment
15
- operator token offline with:
29
+ Every request is `POST /tickets/<operation>`, has `Content-Type: application/json`,
30
+ and requires `Authorization: Bearer <project credential>`. Authentication and
31
+ registration checks precede body parsing and source construction. Owner admin
32
+ is the explicit cross-project exception; internal operator signatures have no
33
+ ticket authority.
34
+
35
+ Project credentials are opaque `abp_1_<id>_<secret>` bearer values bound to one
36
+ registered normalized repository. The service stores only a SHA-256 digest of
37
+ the random secret, plus project, creator, creation time, last use, expiry and
38
+ revocation metadata. It checks credential status and project registration on
39
+ every request. Revocation takes effect on the next request without affecting
40
+ other credentials. Unknown, expired or revoked credentials receive `401 auth`;
41
+ removed projects receive `403 auth` with `project not registered`.
42
+
43
+ The deployment owner runs these commands locally with database access:
16
44
 
17
45
  ```sh
18
- AB_STORE_SECRET='…' ab-hosted-store mint operator --ttl-seconds 3600
46
+ ab-hosted-store credentials issue https://github.com/acme/app --user owner@example.com
47
+ ab-hosted-store credentials list https://github.com/acme/app
48
+ ab-hosted-store credentials revoke https://github.com/acme/app --id ID --user owner@example.com
19
49
  ```
20
50
 
51
+ Issue prints the secret once; list contains metadata only. Issue optionally
52
+ accepts `--ttl-seconds N` or `--expires-at ISO-8601`. For administrative/non-interactive issuance, set the returned token as
53
+ `AB_TOKEN`. Interactive project onboarding uses `ab connect <service-url>`;
54
+ it saves the credential outside the repository and commits no secret. Previous `AB_TOKEN` values and `mint operator` / `mint build` are
55
+ retired: the service responds that the credential format changed and points
56
+ to `credentials issue`. Supported old protocol shapes still work with a new
57
+ credential; protocol compatibility does not restore retired authentication.
58
+
59
+ `mint admin --ttl-seconds N` creates an explicit versioned, attributed owner
60
+ break-glass credential. It can cross project boundaries. Keep it owner-only:
61
+ never give it or `AB_STORE_SECRET` to a dispatcher or build agent.
62
+
63
+ Hosted tickets, comments, blockers, creation-key deduplication and assets belong
64
+ to exactly one registered project, even when projects use the same team key.
65
+ Project credentials select that repository. An optional `context.repo` selector
66
+ must match the credential's project; mismatches are 403. Owner admin requests
67
+ require this selector and registration is checked before configuration is read.
68
+ Internal operator credentials remain forbidden on machine ticket routes.
69
+
21
70
  The strict request envelope is:
22
71
 
23
72
  ```json
@@ -31,12 +80,30 @@ The strict request envelope is:
31
80
  }
32
81
  ```
33
82
 
34
- `teamKey` is required. The state overrides are optional. They come from the
35
- requesting repository's `[tickets]` configuration and are applied only to that
36
- request; the service does not cache a team-bound adapter. `list-ready` also
37
- receives that repository's exact ready state and label criteria in `input`.
38
- Backend credentials, including `LINEAR_API_KEY`, are service environment only
39
- and never occur in this envelope or any response.
83
+ `teamKey` remains required for older client compatibility. It and the optional
84
+ `claimedState` / `createState` fields are ignored as policy inputs. The service
85
+ loads the project's latest effective configuration from its repository journal
86
+ and artifact bytes for every operation. Missing or invalid configuration returns
87
+ an actionable 409; run the project's dispatcher to publish its configuration.
88
+ There is no deployment-wide lifecycle fallback.
89
+
90
+ Hosted lifecycle defaults are triage `Backlog`, claimed `Doing`, done `Done`,
91
+ and create/proposal equal to triage. `readyState` is required. The allowed states
92
+ are the distinct union of these configured roles, including custom create and
93
+ proposal states; roles may overlap. Historical states remain readable unchanged.
94
+ `tickets.doneState` controls completion and the dispatcher's merged transition.
95
+
96
+ The service never contacts Linear or relays external trackers. If the project's
97
+ source is Linear, file or another non-hosted source, machine HTTP, operator and
98
+ MCP ticket surfaces refuse with a message naming that project and source: its
99
+ tickets live in its own source, accessed directly by its dispatcher. This is a
100
+ 409 conflict, not an empty queue. The dispatcher's direct adapters are unchanged.
101
+
102
+ `AB_TICKET_BACKEND` and `LINEAR_API_KEY` are removed service settings. Their
103
+ presence (even blank or unknown values) is ignored and emits one startup removal
104
+ message. `AB_TICKET_TRIAGE_STATE`, `AB_TICKET_READY_STATE`,
105
+ `AB_TICKET_DOING_STATE` and `AB_TICKET_DONE_STATE` are removed and ignored,
106
+ including malformed values. These retired settings do not prevent startup.
40
107
 
41
108
  ## Operations
42
109
 
@@ -74,6 +141,32 @@ environment values, or credentials. Client 401/403 responses become
74
141
  `AuthError`; other backend messages remain actionable.
75
142
 
76
143
  The shared `TicketSource` contract is normative for the database adapter and
77
- the hosted client/server pair. External pass-through is tested with a fake
78
- source behind HTTP. The existing opt-in live Linear contract remains the proof
79
- of Linear's GraphQL behavior; the hosted service uses that adapter unchanged.
144
+ the hosted client/server pair. PostgreSQL migration and isolation tests prove
145
+ project partitions. The direct Linear client retains its separate opt-in
146
+ GraphQL contract; no Linear adapter ships through the hosted ticket entry point.
147
+
148
+ Every ticket returned by list, get, or create requires a boolean `resolved`,
149
+ with the same source-native completion semantics as `DependencyState.resolved`.
150
+ The client preserves it when remapping ticket references. Missing or non-boolean
151
+ values fail wire validation. Client and service follow the published compatibility window.
152
+
153
+ Legacy 0.9.x and 0.10.x ticket responses omit `resolved` and `blockersResolved`
154
+ because those clients use strict decoders without these fields. This projection
155
+ applies to list-ready, get, and create responses. Current clients retain the
156
+ current required resolution fields; domain and storage types are unchanged.
157
+
158
+ ## Delegated execution credentials
159
+
160
+ Build and harvest runners exchange project authority for signed, project-bound
161
+ execution and phase credentials through `/credentials/delegate` (see the remote
162
+ store protocol). Build-runner and build-phase credentials cannot call any ticket
163
+ operation. Harvest-phase credentials permit only list-ready, get and
164
+ dependency-states. Harvest-runner credentials permit those reads plus create
165
+ for filing proposals. Claim, comment, transition, update, add-blocker and
166
+ remove-blocker require project or admin authority; their access is unchanged.
167
+ Denied delegated calls return 403 with an error naming the grant kind and
168
+ operation before body parsing or source construction. Lifecycle retirement and
169
+ project removal are also checked before parsing or source construction.
170
+
171
+ Ticket admission and assets use the authenticated project, including for
172
+ delegated harvest grants.
package/docs/setup.md CHANGED
@@ -28,7 +28,7 @@ maintainer before selecting it:
28
28
  - a dispatcher-side push-capable `GITHUB_TOKEN` or `GH_TOKEN`;
29
29
  - for a private repository, a separate read-only clone identity named by
30
30
  `gitUsernameEnv` and `gitPasswordEnv`;
31
- - an HTTPS hosted `AB_STORE` reachable from Vercel and a scoped `AB_TOKEN`;
31
+ - an HTTPS hosted service reachable from Vercel, configured through `ab connect <url>`;
32
32
  - each selected role and alternate's runtime/model, its pinned
33
33
  `[workspace.config.runtimeProvisioning.<runtime>]` commands, and the API
34
34
  credential names it needs in `[workspace.config].environmentVariables`.
@@ -177,7 +177,7 @@ ab dispatch --repository https://github.com/owner/repository \\
177
177
  ```
178
178
 
179
179
  `--repository` (or `AB_REPOSITORY`) is the normalized origin. Origin mode
180
- requires an HTTPS `AB_STORE` plus `AB_TOKEN`, an exported `GITHUB_TOKEN` or
180
+ requires an HTTPS service selected through `[store] url` or `AB_STORE`, a saved project credential or `AB_TOKEN`, an exported `GITHUB_TOKEN` or
181
181
  `GH_TOKEN`, and `forge = "github"`; the interactive dashboard is unavailable
182
182
  (`--plain` or no TTY). Origin mode never consults the gh CLI: a checkout-less
183
183
  host has no gh and no keyring, and the only workspace provider it can run,
@@ -208,17 +208,34 @@ sources agree with the running system by construction:
208
208
  **running dispatcher's version** (read from the deployed distribution's
209
209
  `package.json`), so the guest runs the dispatcher's own release.
210
210
 
211
- The hosted remote store enforces exact **version lockstep**: every client
212
- request must carry an `x-autobuild-version` equal to the server's, and any
213
- mismatch — a guest older *or* newer than the store — is rejected with a 409
214
- `remote store version mismatch`. Guest, store, and dispatcher versions
215
- therefore cannot be mixed: a distribution fix reaches guests only after
216
- `tools/release.ts` cuts a release (credential-gated; requires GitHub
217
- credentials — a documented delivery dependency, not an automatic propagation)
218
- **and** the hosted store and dispatcher are upgraded to that release in the
219
- same lockstep window. If the release is cut but the store upgrade lags, guests
220
- carrying the new distribution fail loudly against the old store (409) rather
221
- than running silently stale.
211
+ A service accepts stable clients from its own `major.minor` release line and
212
+ the two preceding minor lines in the same major. A major starts a new family:
213
+ X.0 accepts only X.0, X.1 accepts X.0/X.1, and X.2 onward accepts three lines.
214
+ Patch versions are interchangeable in both directions and must never change
215
+ the protocol. Upgrade the service first, then projects independently within
216
+ its window. Supported release lines retain their wire handlers and known
217
+ protocol identities; an unknown protocol is refused independently of version.
218
+ Malformed, missing, and prerelease identities are refused with HTTP 409.
219
+ A newer client is told to upgrade the service; a client below the window is
220
+ told to upgrade the project's Autobuild. Diagnostics name the service version
221
+ and supported range.
222
+
223
+ Unauthenticated `GET /health` publishes `ok`, `autobuildVersion`,
224
+ `protocolVersion`, and `supportedClientRange: { minVersion, maxVersionExclusive }`.
225
+ The range is inclusive at the minimum and exclusive at the maximum; a 0.11.x
226
+ service publishes `>=0.9.0 <0.12.0`. Health needs no credentials, version
227
+ headers, or available backend.
228
+
229
+ `ab upgrade` probes this public range before installation or project changes.
230
+ It warns and continues when the target is outside the range, naming the
231
+ required service upgrade for a newer target or a compatible project range for
232
+ an older one. An unavailable or malformed probe warns that connectivity may
233
+ fail after upgrade. Legacy health without a range means exact-version matching;
234
+ upgrade that service to the compatibility-window release first.
235
+
236
+ A distribution fix reaches origin-mode guests after a release is cut and the
237
+ dispatcher is upgraded. Guests continue to run the dispatcher's own version;
238
+ the hosted service must support that release line.
222
239
 
223
240
  Provisioning records the installed distribution's identity — package version
224
241
  and remote-store protocol version — in the guest's
@@ -229,10 +246,10 @@ against the identity the current system would deliver and, when they disagree
229
246
  (which includes every guest provisioned before the marker existed), refreshes
230
247
  it in place (archive re-fetched, extracted, and production-installed) first,
231
248
  so an upgraded dispatcher retrofits its existing guests instead of resuming a
232
- stale distribution the hosted store would reject. A refresh failure deletes
249
+ stale distribution. A refresh failure deletes
233
250
  the sandbox so the next provisioning pass rematerializes it from scratch.
234
251
 
235
- After a lockstep upgrade, confirm a fix-bearing guest from the guest
252
+ After a dispatcher upgrade, confirm a fix-bearing guest from the guest
236
253
  perspective:
237
254
 
238
255
  1. `ab build status <slug> --json` with the ambient build identity for a
@@ -250,7 +267,7 @@ apart from the identity change, `ab dispatch` behaves as it did.
250
267
  A hosted-store deployment configures its web/auth variables once — see the
251
268
  hosted service README and the complete environment reference in the pinned
252
269
  distribution checkout: `BETTER_AUTH_URL`, `AB_WEB_ALLOWED_EMAILS`,
253
- `AB_WEB_REPOSITORIES`, and the optional `AB_WEB_MCP_RESOURCE` — the protected
270
+ and the optional `AB_WEB_MCP_RESOURCE` — the protected
254
271
  resource the hosted MCP server (docs/mcp.md) binds its OAuth tokens to,
255
272
  defaulting to `<BETTER_AUTH_URL>/mcp`. Set it only when a proxy or custom
256
273
  domain fronts the deployment under a different public URL than
@@ -258,6 +275,93 @@ domain fronts the deployment under a different public URL than
258
275
  production) or every MCP client's discovery binds tokens to the wrong
259
276
  resource.
260
277
 
278
+
279
+ ## Connect a project
280
+
281
+ From a checkout with an origin remote, run `ab connect https://YOUR_ORIGIN`.
282
+ The CLI prints a verification URL and short code and attempts to open a browser.
283
+ Sign in as an allowed owner, check the full repository identity and code, and
284
+ approve or deny. Another device can open the printed URL, or visit `/connect`
285
+ and enter the code. Approval requires the web-enabled service origin; a
286
+ standalone machine-only listener cannot present browser consent. Requests
287
+ expire after ten minutes. Denial and timeout leave local configuration unchanged.
288
+
289
+ On approval the service registers the project and issues a project credential.
290
+ The CLI keeps it in `$XDG_CONFIG_HOME/autobuild/credentials` (or
291
+ `~/.config/autobuild/credentials`), keyed by normalized service and origin,
292
+ with directory mode 0700 and file mode 0600. Credentials never belong in the
293
+ repository. It adds the non-secret `[store] url` to `autobuild.toml` when
294
+ absent; review and commit that change yourself. An existing different URL
295
+ must be changed explicitly before connecting. Reconnecting replaces the saved
296
+ credential only after approval and revokes the previous credential.
297
+
298
+ All worktrees share the main checkout's configuration and the user's saved
299
+ credential. A service manager running as that user uses the same HOME and
300
+ XDG_CONFIG_HOME, with no AB_STORE or AB_TOKEN exports. A fresh clone with
301
+ committed store configuration but no saved credential fails with `ab connect`
302
+ guidance; it never silently creates local state. Run connect on each host/user.
303
+
304
+ `ab connect` without a URL reports service, project, and credential status:
305
+ accepted, rejected, missing, or unreachable. `ab disconnect` revokes the saved
306
+ credential remotely before removing it locally, keeping the project registered,
307
+ its data, and store configuration. A network/server failure retains the local
308
+ entry for retry; an already revoked credential can be removed. If saving a new
309
+ connection fails, the CLI cancels/revokes it and retains the previous entry.
310
+ If remote cleanup cannot be confirmed, check the printed credential id with
311
+ the owner's administrative tools before retrying.
312
+
313
+ Store selection is `--store` > nonblank `AB_STORE` > `[store] url` > the
314
+ main checkout's `.autobuild/`. `AB_TOKEN` overrides the exact service/project's
315
+ saved credential for CI and other non-interactive hosts. A service override
316
+ never borrows another service's credential. Build and harvest sessions keep
317
+ their explicit scoped authority.
318
+
319
+ ## Connected projects
320
+
321
+ Migrate PostgreSQL from the pinned release before deploying (`bun run
322
+ postgres:migrate`). The service keeps an independent, durable project registry;
323
+ existing build or ticket data does not register a project. An administrator
324
+ with deployment database credentials manages it without redeploying:
325
+
326
+ ```sh
327
+ ab-hosted-store projects list
328
+ ab-hosted-store projects add https://github.com/owner/project --user owner@example.com
329
+ ab-hosted-store projects remove https://github.com/owner/project --user owner@example.com
330
+ ```
331
+
332
+ From a source checkout, substitute `bun packages/hosted-store-service/src/bin.ts`
333
+ for `ab-hosted-store`. Configure `AB_POSTGRES_URL` or `DATABASE_URL` in that
334
+ process. Database possession grants administration; `--user` is declared audit
335
+ attribution, not a verified login. Ordinary project/store tokens cannot manage
336
+ registrations. Each entry records the normalized origin URL, addition time,
337
+ and adding identity. SSH git spellings normalize to HTTPS; local paths and
338
+ credential-bearing HTTPS URLs are rejected. Duplicate adds preserve metadata;
339
+ remove is idempotent, and re-adding records a fresh addition.
340
+
341
+ Every allowlisted signed-in user owns every registered project. The dashboard,
342
+ MCP discovery and calls, and every operator API route consult current
343
+ registration. Removal hides a project from all these surfaces and subsequent
344
+ hosted dispatcher invocations; builds, events, artifacts and tickets remain,
345
+ and re-registration restores access. Already-started work is not cancelled.
346
+ Project credentials issued by connect authorize only their registered project. Administrative credential issuance remains available for non-interactive hosts.
347
+
348
+ Link directly to `/?repo=<URL-encoded normalized origin>`. The page selects the
349
+ first registered origin when the requested one is missing or removed, and an
350
+ empty registry shows no project data or controls. Registry errors fail closed;
351
+ there is no environment fallback on database failure.
352
+
353
+ `AB_WEB_REPOSITORIES` is deprecated. A nonempty CSV is imported exactly once per
354
+ database, on the first registry initialization with that variable configured,
355
+ with actor `system:AB_WEB_REPOSITORIES`. The import and durable marker commit
356
+ atomically across concurrent starts. An absent variable does not consume the
357
+ marker. Later restarts or edits to the variable never re-add removed projects;
358
+ use the owner commands for subsequent administration.
359
+
360
+ The hosted dispatcher queries current registrations on every authorized
361
+ invocation. With `AB_DISPATCHER_REPOSITORIES` unset, it serves all registered
362
+ projects; an explicit list preserves its order and is intersected with current
363
+ registration. An empty registry is a successful zero-project invocation.
364
+
261
365
  ## Authoring an agent verifier
262
366
 
263
367
  A repository-owned agent-verify skill is a verifier, not a reviewer: it drives
@@ -290,3 +394,56 @@ explanation, never a skip or silent pass. Use `skip` only when the complete step
290
394
  does not apply. If guidance still cannot make a verdict possible, escalate and
291
395
  explain what remains unresolved. Record out-of-scope work with `ab observe`;
292
396
  the verifier does not fix it.
397
+
398
+ ### Project-scoped hosted tickets
399
+
400
+ Hosted ticket queues belong to the credential's registered repository, even
401
+ when projects share a team key. The latest uploaded effective `[tickets]`
402
+ configuration owns lifecycle: hosted defaults are Backlog / Doing / Done,
403
+ `readyState` is required, `doneState` customizes completion, and create/proposal
404
+ default to triage. The union of configured roles is allowed; roles may overlap.
405
+ Owner admin requests require an explicit registered `context.repo`; project
406
+ credentials cannot select another project. Missing effective configuration is
407
+ an actionable refusal: run the project's dispatcher to publish it.
408
+
409
+ The service never contacts Linear. Projects with external sources (including
410
+ file and Linear) keep using those sources directly through their dispatcher;
411
+ service ticket HTTP, operator and MCP surfaces refuse with project/source
412
+ feedback rather than returning an empty queue. Removed `AB_TICKET_BACKEND` and
413
+ `LINEAR_API_KEY` values are ignored with one startup removal message, including
414
+ blank/unknown values. Removed `AB_TICKET_TRIAGE_STATE`, `AB_TICKET_READY_STATE`,
415
+ `AB_TICKET_DOING_STATE` and `AB_TICKET_DONE_STATE` values are ignored even when
416
+ malformed. None of these retired settings prevents startup.
417
+
418
+ The environment-aware migration imports `AB_WEB_REPOSITORIES` once before
419
+ ownership preflight, including released main-v9 deployments without registry
420
+ tables. A failed migration rolls back the import, main-schema promotion and
421
+ ticket changes together. Owner `projects add` and `tickets assign-team` commands
422
+ bootstrap only the registry/recovery journals and work while the main marker
423
+ is still v9. Without the retired environment, first register the intended
424
+ project with `ab-hosted-store projects add REPOSITORY --user IDENTITY`, then
425
+ assign the team and rerun migration. A successful one-time import is never
426
+ reapplied from later environment edits.
427
+
428
+ Ticket schema v1 migration assigns each whole legacy team to its sole claimant
429
+ among registered projects' latest effective configs, reading their actual blob
430
+ bytes. Projects with no dispatcher run or no effective-config reference make
431
+ no team claim and do not block other projects. Unclaimed teams still fail closed.
432
+ When inferring ownership, a selected configuration whose revision/blob is missing,
433
+ unreadable or invalid stops unchanged with recovery guidance. Restore the selected
434
+ configuration or assign every remaining legacy team explicitly; complete owner
435
+ assignments to registered projects bypass unrelated configuration reads. Conflicts
436
+ and asset ownership mismatches also stop unchanged. Resolve ownership, then rerun:
437
+
438
+ ```sh
439
+ ab-hosted-store tickets assign-team TEAM https://github.com/acme/app --user owner@example.com
440
+ ab-postgres-store migrate
441
+ ```
442
+
443
+ The assignment journal works before v2 assertion, retains the actor, and grants
444
+ no future cross-project access. Migration preserves ids, states, comments,
445
+ blockers, timestamps and assets. Populated-v1 migration and deployment builds
446
+ require the running service's blob backend, access mode, credentials and prefix
447
+ (`AB_BLOB_PREFIX`), not just PostgreSQL access. `deploy:build` inherits this
448
+ environment and migration failure stops Next build. Fresh databases, empty v1
449
+ queues and v2 reruns stay SQL-only and do not initialize blob storage.
@@ -2,7 +2,7 @@
2
2
 
3
3
  `ab ticket create --blocked-by` validates prerequisite ids and records blocking
4
4
  relationships, but a single CLI command is **not an atomic publication guarantee**.
5
- In particular, direct Linear and hosted Linear create the issue first and add
5
+ In particular, direct Linear creates the issue first and adds
6
6
  its blocker relationships in separate requests. If the issue is already in the
7
7
  ready state, a dispatcher can see it before those relationships exist. A
8
8
  successful create response containing every blocker does not prove that no build
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@defrex/autobuild",
3
- "version": "0.10.1",
3
+ "version": "0.12.0",
4
4
  "description": "Agent-driven software delivery from groomed ticket to merged PR",
5
5
  "repository": {
6
6
  "type": "git",
@@ -86,6 +86,10 @@
86
86
  "types": "./packages/core/src/operator/sandbox-host.ts",
87
87
  "import": "./packages/core/src/operator/sandbox-host.ts",
88
88
  "default": "./packages/core/src/operator/sandbox-host.ts"
89
+ },
90
+ "./connection": {
91
+ "types": "./packages/core/src/connection.ts",
92
+ "import": "./packages/core/src/connection.ts"
89
93
  }
90
94
  },
91
95
  "files": [
@@ -139,8 +143,8 @@
139
143
  },
140
144
  "devDependencies": {
141
145
  "@biomejs/biome": "2.5.5",
142
- "@defrex/autobuild-postgres-store": "0.9.0",
143
- "@defrex/autobuild-vercel-sandbox": "0.9.0",
146
+ "@defrex/autobuild-postgres-store": "0.11.0",
147
+ "@defrex/autobuild-vercel-sandbox": "0.11.0",
144
148
  "@resvg/resvg-js": "2.6.2",
145
149
  "@types/bun": "^1.3.14",
146
150
  "@types/node": "^24.0.0",
@@ -150,8 +154,5 @@
150
154
  "git-format-staged": "4.0.1",
151
155
  "husky": "9.1.7",
152
156
  "typescript": "5.9.3"
153
- },
154
- "patchedDependencies": {
155
- "better-auth@1.4.18": "patches/better-auth@1.4.18.patch"
156
157
  }
157
158
  }
@@ -0,0 +1,209 @@
1
+ import type { AbEvent } from '../events/catalog'
2
+ import type { RepositoryEvent } from '../events/repository'
3
+ import { currentAutoMergeDeferral } from '../kernel/auto-merge'
4
+ import { reduceBuild, type BuildState } from '../kernel/reducer'
5
+
6
+ export interface MergeDecision {
7
+ type: 'build.merge-decision'
8
+ pr: { number: number; url: string; headSha: string }
9
+ reason: string
10
+ observedAt: string
11
+ }
12
+
13
+ /** Consent pending application is armed; only a current actionable deferral
14
+ * or withdrawn consent needs a person. Currency is owned by the kernel. */
15
+ export function mergeDecision(
16
+ events: AbEvent[],
17
+ now: Date,
18
+ state = reduceBuild(events),
19
+ ): MergeDecision | null {
20
+ if (
21
+ !state.finalizeCompletedSeq ||
22
+ !state.pr ||
23
+ state.prState !== 'open' ||
24
+ state.status === 'done' ||
25
+ state.status === 'aborted'
26
+ )
27
+ return null
28
+ const deferral = currentAutoMergeDeferral(events, state)
29
+ if (state.autoMerge.requested && deferral === undefined) return null
30
+ return {
31
+ type: 'build.merge-decision',
32
+ pr: state.pr,
33
+ reason: deferral ?? 'Auto-merge is not armed',
34
+ observedAt: now.toISOString(),
35
+ }
36
+ }
37
+
38
+ export function buildNeedsAttention(events: AbEvent[], state: BuildState): boolean {
39
+ return (
40
+ (state.openEscalations?.length ?? 0) > 0 || mergeDecision(events, new Date(0), state) !== null
41
+ )
42
+ }
43
+
44
+ export function mergeIdentity(slug: string, pr: number): string {
45
+ return JSON.stringify([slug, pr])
46
+ }
47
+
48
+ export function repositoryNeedsAttention(event: RepositoryEvent): boolean {
49
+ switch (event.type) {
50
+ case 'harvest.escalated':
51
+ case 'harvest.recovery-exhausted':
52
+ case 'dispatcher.config-rejected':
53
+ return true
54
+ case 'dispatcher.config-reloaded':
55
+ return event.payload.restartRequired.length > 0
56
+ case 'dispatcher.run-stopped':
57
+ return event.payload.outcome === 'abnormal'
58
+ default:
59
+ return false
60
+ }
61
+ }
62
+
63
+ type Degraded = Extract<RepositoryEvent, { type: 'dispatcher.ticket-source-degraded' }>
64
+ type Failed = Extract<RepositoryEvent, { type: 'dispatcher.tick-failed' }>
65
+ export type PersistenceObservation =
66
+ | {
67
+ type: 'dispatcher.ticket-source-persistent'
68
+ source: string
69
+ cause: string
70
+ message: string
71
+ resetAt?: string
72
+ episodeSeq: number
73
+ observedAt: string
74
+ dependencyLimited: boolean
75
+ }
76
+ | {
77
+ type: 'dispatcher.tick-failure-persistent'
78
+ run: string
79
+ firstFailure: string
80
+ error: string
81
+ episodeSeq: number
82
+ observedAt: string
83
+ }
84
+
85
+ /** Incremental journal projection. Episode starts survive run anchors and
86
+ * repeated evidence can strengthen, but never postpone, an existing deadline. */
87
+ export class RepositoryAttention {
88
+ private sources = new Map<
89
+ string,
90
+ {
91
+ event: Degraded
92
+ deadline: number
93
+ dependencyLimited: boolean
94
+ requiresRetry: boolean
95
+ retryObserved: boolean
96
+ }
97
+ >()
98
+ private failure: Failed | undefined
99
+ private tickStarts = new Map<string, number>()
100
+
101
+ private sourceDefersTick(now: number): boolean {
102
+ return [...this.sources.values()].some(
103
+ (source) => now < source.deadline || (source.requiresRetry && !source.retryObserved),
104
+ )
105
+ }
106
+
107
+ /** Crossing resetAt alone does not prove persistence: the dispatcher must
108
+ * finish a tick that could actually retry after the provider cooldown. */
109
+ private observeRetry(run: string): void {
110
+ const startedAt = this.tickStarts.get(run)
111
+ this.tickStarts.delete(run)
112
+ if (startedAt === undefined) return
113
+ for (const source of this.sources.values()) {
114
+ if (startedAt >= source.deadline) source.retryObserved = true
115
+ }
116
+ }
117
+ fold(event: RepositoryEvent): void {
118
+ switch (event.type) {
119
+ case 'dispatcher.ticket-source-degraded': {
120
+ const existing = this.sources.get(event.payload.source)
121
+ const deadline =
122
+ 'resetAt' in event.payload
123
+ ? Date.parse(event.payload.resetAt)
124
+ : Date.parse(event.ts) + 15 * 60_000
125
+ if (existing) {
126
+ existing.deadline = Math.min(existing.deadline, deadline)
127
+ existing.requiresRetry ||= 'resetAt' in event.payload
128
+ } else
129
+ this.sources.set(event.payload.source, {
130
+ event,
131
+ deadline,
132
+ dependencyLimited: false,
133
+ requiresRetry: 'resetAt' in event.payload,
134
+ retryObserved: false,
135
+ })
136
+ // Cooldown-driven failures are not an independent dispatcher outage.
137
+ if (this.sourceDefersTick(Date.parse(event.ts))) this.failure = undefined
138
+ break
139
+ }
140
+ case 'dispatcher.ticket-source-dependency-limited': {
141
+ const source = this.sources.get(event.payload.source)
142
+ if (source) source.dependencyLimited = true
143
+ break
144
+ }
145
+ case 'dispatcher.ticket-source-recovered':
146
+ this.sources.delete(event.payload.source)
147
+ break
148
+ case 'dispatcher.run-started':
149
+ this.failure = undefined
150
+ this.tickStarts.clear()
151
+ break
152
+ case 'dispatcher.run-stopped':
153
+ this.tickStarts.delete(event.payload.run)
154
+ if (this.failure?.payload.run === event.payload.run) this.failure = undefined
155
+ break
156
+ case 'dispatcher.tick-started':
157
+ this.tickStarts.set(event.payload.run, Date.parse(event.ts))
158
+ break
159
+ case 'dispatcher.tick-completed':
160
+ this.observeRetry(event.payload.run)
161
+ if (this.failure?.payload.run === event.payload.run) this.failure = undefined
162
+ break
163
+ case 'dispatcher.tick-failed':
164
+ this.observeRetry(event.payload.run)
165
+ if (this.sourceDefersTick(Date.parse(event.ts))) this.failure = undefined
166
+ else if (this.failure?.payload.run !== event.payload.run) this.failure = event
167
+ break
168
+ }
169
+ }
170
+ pending(now: Date): { identity: string; observation: PersistenceObservation }[] {
171
+ const results: { identity: string; observation: PersistenceObservation }[] = []
172
+ for (const {
173
+ event,
174
+ deadline,
175
+ dependencyLimited,
176
+ requiresRetry,
177
+ retryObserved,
178
+ } of this.sources.values()) {
179
+ if (now.getTime() < deadline || (requiresRetry && !retryObserved)) continue
180
+ results.push({
181
+ identity: `source:${event.seq}`,
182
+ observation: {
183
+ type: 'dispatcher.ticket-source-persistent',
184
+ ...event.payload,
185
+ episodeSeq: event.seq,
186
+ observedAt: now.toISOString(),
187
+ dependencyLimited,
188
+ },
189
+ })
190
+ }
191
+ const event = this.failure
192
+ if (
193
+ event &&
194
+ !this.sourceDefersTick(now.getTime()) &&
195
+ now.getTime() >= Date.parse(event.ts) + 5 * 60_000
196
+ )
197
+ results.push({
198
+ identity: `tick:${event.seq}`,
199
+ observation: {
200
+ type: 'dispatcher.tick-failure-persistent',
201
+ ...event.payload,
202
+ firstFailure: event.ts,
203
+ episodeSeq: event.seq,
204
+ observedAt: now.toISOString(),
205
+ },
206
+ })
207
+ return results
208
+ }
209
+ }