@defrex/autobuild 0.11.0 → 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.
- package/README.md +3 -1
- package/SPEC.md +222 -22
- package/docs/architecture.md +66 -4
- package/docs/configuration.md +105 -16
- package/docs/hosted-dispatcher.md +17 -2
- package/docs/hosted-service.md +214 -0
- package/docs/mcp.md +8 -2
- package/docs/operator-api.md +90 -7
- package/docs/remote-store-protocol.md +189 -144
- package/docs/remote-ticket-protocol.md +107 -19
- package/docs/setup.md +173 -16
- package/docs/ticket-dependencies.md +1 -1
- package/package.json +7 -6
- package/packages/core/src/cli/attention.ts +209 -0
- package/packages/core/src/cli/configured-store.ts +17 -0
- package/packages/core/src/cli/connect.ts +261 -0
- package/packages/core/src/cli/dashboard/model.ts +4 -5
- package/packages/core/src/cli/dispatch.ts +183 -41
- package/packages/core/src/cli/help.ts +39 -10
- package/packages/core/src/cli/init-validation.ts +27 -2
- package/packages/core/src/cli/main.ts +41 -15
- package/packages/core/src/cli/mcp.ts +7 -1
- package/packages/core/src/cli/pr-summary-publication.ts +220 -0
- package/packages/core/src/cli/pr-summary.ts +2 -0
- package/packages/core/src/cli/project-credentials.ts +132 -0
- package/packages/core/src/cli/repo-state.ts +15 -2
- package/packages/core/src/cli/self-update.ts +10 -0
- package/packages/core/src/cli/store-opening.ts +22 -14
- package/packages/core/src/cli/terminals.ts +0 -10
- package/packages/core/src/cli/ticket.ts +8 -1
- package/packages/core/src/cli/upgrade-compatibility.ts +80 -0
- package/packages/core/src/cli/wait.ts +84 -17
- package/packages/core/src/cli/watch.ts +197 -60
- package/packages/core/src/config/live.ts +3 -0
- package/packages/core/src/config/schema.ts +56 -9
- package/packages/core/src/connection.ts +86 -0
- package/packages/core/src/events/catalog.ts +10 -2
- package/packages/core/src/events/payloads.ts +44 -0
- package/packages/core/src/events/repository.ts +13 -0
- package/packages/core/src/kernel/__snapshots__/reducer-registry.test.ts.snap +130 -83
- package/packages/core/src/kernel/engine.ts +24 -0
- package/packages/core/src/kernel/generators/build-log.ts +10 -0
- package/packages/core/src/kernel/log-index.ts +17 -0
- package/packages/core/src/kernel/pr-summary.ts +89 -0
- package/packages/core/src/kernel/reducer-registry.ts +8 -0
- package/packages/core/src/kernel/reducer.ts +8 -0
- package/packages/core/src/ontology.ts +1 -0
- package/packages/core/src/operator/index.ts +7 -0
- package/packages/core/src/operator/query.ts +38 -16
- package/packages/core/src/operator/ticket-source-backend.ts +1 -0
- package/packages/core/src/operator/tickets.ts +6 -8
- package/packages/core/src/plugin-sdk/index.ts +1 -0
- package/packages/core/src/plugins/manifest.ts +5 -3
- package/packages/core/src/plugins/runtimes.ts +3 -1
- package/packages/core/src/ports/forge/fake.ts +13 -1
- package/packages/core/src/ports/forge/github-transport.ts +36 -1
- package/packages/core/src/ports/forge/github.ts +29 -5
- package/packages/core/src/ports/forge/local-git.ts +12 -4
- package/packages/core/src/ports/forge/summary-marker.ts +8 -0
- package/packages/core/src/ports/runner/session-env.ts +1 -0
- package/packages/core/src/ports/tickets/create.ts +11 -3
- package/packages/core/src/ports/tickets/hosted-index.ts +0 -3
- package/packages/core/src/ports/tickets/remote-protocol.ts +1 -0
- package/packages/core/src/ports/tickets/remote.ts +1 -0
- package/packages/core/src/ports/workspace/build-execution.ts +5 -1
- package/packages/core/src/ports/workspace/create.ts +11 -2
- package/packages/core/src/ports/workspace/distribution-archive.ts +3 -5
- package/packages/core/src/ports/workspace/harvest-execution.ts +3 -1
- package/packages/core/src/ports/workspace/local-build-execution.ts +8 -1
- package/packages/core/src/ports/workspace/vercel-sandbox.ts +47 -19
- package/packages/core/src/processes/build-execution-state.ts +1 -1
- package/packages/core/src/processes/build-runner.ts +87 -26
- package/packages/core/src/processes/dispatch-failure-budget.ts +83 -0
- package/packages/core/src/processes/dispatcher.ts +59 -10
- package/packages/core/src/processes/harvest-child.ts +1 -0
- package/packages/core/src/processes/harvest-execution-state.ts +19 -2
- package/packages/core/src/processes/harvest-runner.ts +142 -100
- package/packages/core/src/processes/phase-failure-budget.ts +1 -0
- package/packages/core/src/processes/publication-loss.ts +18 -1
- package/packages/core/src/store/remote/client.ts +1 -1
- package/packages/core/src/store/remote/compatibility.ts +90 -0
- package/packages/core/src/store/remote/credentials.ts +28 -0
- package/packages/core/src/store/remote/index.ts +14 -1
- package/packages/core/src/store/remote/protocol.ts +25 -0
- package/packages/core/src/store/remote/token.ts +74 -13
- package/packages/core/src/testing/index.ts +7 -0
- package/skills/guide/SKILL.md +144 -26
- package/skills/guide/references/operator-api.md +135 -13
- package/skills/guide/references/plugin-authoring.md +10 -2
- package/skills/guide/references/remote-store-protocol.md +189 -144
- package/skills/guide/references/remote-ticket-protocol.md +111 -18
- package/skills/guide/references/setup.md +173 -16
- package/skills/guide/references/ticket-dependencies.md +1 -1
- package/skills/operate/SKILL.md +126 -36
- package/templates/hosted-service/package.json +13 -0
- package/templates/hosted-service/vercel.json +7 -0
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
|
|
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 `
|
|
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
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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.
|
|
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.
|
|
143
|
-
"@defrex/autobuild-vercel-sandbox": "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
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
import { parse } from 'smol-toml'
|
|
4
|
+
import { storeSchema } from '../config/schema'
|
|
5
|
+
|
|
6
|
+
/** Store selection happens before full dispatch configuration is loaded. */
|
|
7
|
+
export function configuredStore(checkout: string): string | undefined {
|
|
8
|
+
let text: string
|
|
9
|
+
try {
|
|
10
|
+
text = readFileSync(join(checkout, 'autobuild.toml'), 'utf8')
|
|
11
|
+
} catch (error) {
|
|
12
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return undefined
|
|
13
|
+
throw error
|
|
14
|
+
}
|
|
15
|
+
const table = parse(text)
|
|
16
|
+
return table.store === undefined ? undefined : storeSchema.parse(table.store).url
|
|
17
|
+
}
|