@defrex/autobuild 0.10.0 → 0.11.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 (47) hide show
  1. package/README.md +97 -113
  2. package/SPEC.md +22 -5
  3. package/bin/ab-dispatch-kernel.ts +1 -1
  4. package/docs/configuration.md +34 -7
  5. package/docs/operator-api.md +17 -0
  6. package/docs/remote-ticket-protocol.md +5 -0
  7. package/package.json +3 -3
  8. package/packages/core/src/cli/binary.ts +3 -3
  9. package/packages/core/src/cli/dashboard/model.ts +4 -0
  10. package/packages/core/src/cli/dashboard/render.ts +37 -7
  11. package/packages/core/src/cli/dispatch-frontend.ts +16 -1
  12. package/packages/core/src/cli/dispatch-process.ts +25 -7
  13. package/packages/core/src/cli/dispatch.ts +64 -6
  14. package/packages/core/src/cli/dispatcher-liveness.ts +91 -0
  15. package/packages/core/src/cli/help.ts +8 -5
  16. package/packages/core/src/cli/init.ts +3 -2
  17. package/packages/core/src/cli/repository-status.ts +45 -5
  18. package/packages/core/src/cli/watch.ts +159 -14
  19. package/packages/core/src/config/live.ts +1 -0
  20. package/packages/core/src/config/schema.ts +9 -2
  21. package/packages/core/src/events/globs.ts +8 -3
  22. package/packages/core/src/events/repository.ts +51 -2
  23. package/packages/core/src/integration/harness.ts +2 -1
  24. package/packages/core/src/kernel/__snapshots__/reducer-registry.test.ts.snap +18 -0
  25. package/packages/core/src/kernel/dispatch-status.ts +9 -1
  26. package/packages/core/src/kernel/dispatcher-liveness.ts +70 -0
  27. package/packages/core/src/kernel/reducer-registry.ts +4 -0
  28. package/packages/core/src/kernel/ticket-source-health.ts +30 -0
  29. package/packages/core/src/operator/query.ts +6 -3
  30. package/packages/core/src/orchestrator/prompt.ts +3 -1
  31. package/packages/core/src/ports/tickets/contract.ts +25 -0
  32. package/packages/core/src/ports/tickets/fake.ts +10 -8
  33. package/packages/core/src/ports/tickets/file.ts +1 -0
  34. package/packages/core/src/ports/tickets/linear.ts +241 -49
  35. package/packages/core/src/ports/tickets/remote-protocol.ts +2 -0
  36. package/packages/core/src/ports/types.ts +17 -0
  37. package/packages/core/src/processes/dispatcher.ts +277 -73
  38. package/packages/core/src/processes/repo-view.ts +2 -1
  39. package/packages/core/src/store/adapter.ts +1 -0
  40. package/packages/core/src/store/contract.ts +109 -0
  41. package/packages/core/src/store/digest.ts +14 -2
  42. package/packages/core/src/store/local/store.ts +18 -5
  43. package/packages/core/src/store/remote/protocol.ts +1 -0
  44. package/packages/core/src/store/repo-state-events.ts +49 -59
  45. package/packages/core/src/store/types.ts +5 -1
  46. package/skills/guide/SKILL.md +43 -14
  47. package/skills/operate/SKILL.md +180 -18
package/README.md CHANGED
@@ -2,72 +2,29 @@
2
2
 
3
3
  # Autobuild
4
4
 
5
- **Tickets in, PRs out. No babysitting required.**
5
+ **Tickets in, Product out.**
6
6
 
7
7
  </div>
8
8
 
9
- Every build runs headless, so ten tickets in flight cost the same
10
- attention as one. You groom tickets in, you review pull requests out.
11
- Agents do the work, and deterministic code keeps them honest.
9
+ Autobuild runs coding agents through a fixed pipeline that plans, implements,
10
+ reviews, and verifies every ticket before it merges. You groom tickets in and
11
+ review pull requests out; agents do the work, and deterministic code keeps
12
+ them honest.
12
13
 
13
14
  ![The autobuild dispatch dashboard with four builds in flight, each partway
14
15
  through the pipeline](docs/assets/headline-wide.png)
15
16
 
16
- ## Groomed ticket in, reviewed PR out
17
-
18
- Running several coding agents by hand doesn't scale well. Every session wants
19
- prompts, permissions, and attention, and your focus becomes the ceiling.
20
- With Autobuild, each build runs the whole loop headlessly and escalates only
21
- when it is truly blocked. With attention off the critical path, throughput
22
- is only limited by how fast you can write good tickets.
23
-
24
- Headless is safe because the pipeline is deterministic code, not model
25
- judgment. State lives in a typed, append-only event log, phase transitions
26
- are owned by tested code, and every build leaves a queryable paper trail. Your
27
- merge gates are never bypassed: a PR lands when your required checks and your
28
- consent say it lands.
29
-
30
- The loop also feeds itself. While building, agents record observations —
31
- latent bugs, worthwhile refactors, follow-ups they noticed but rightly left
32
- alone. A harvester distills them into proposed tickets and files them for
33
- triage; approve one and it runs the same loop.
34
-
35
- Every seam is an adapter: ticket sources (the hosted service with database or
36
- Linear backend, direct Linear, local files, or a configured plugin), agent runtimes (Claude, Codex, Pi, or a configured plugin), the forge
37
- (GitHub via `gh`, credential-free local Git, or a configured plugin), workspaces,
38
- and the build store all sit behind
39
- narrow interfaces. Trusted Bun plugins declared in `autobuild.toml` can register
40
- third-party ticket, runtime, workspace, and forge adapters against the versioned
41
- `@defrex/autobuild/plugin-sdk` surface. Repositories may also opt into the shipped
42
- `vercel-sandbox` workspace provider: the complete build runs in an isolated
43
- Vercel VM while the local supervisor retains credentialed branch/PR publication.
44
- The default remains a local Git worktree. See the [workspace configuration and
45
- security contract](docs/configuration.md#vercel-sandbox).
46
- Teams that need one durable store across hosts
47
- can deploy the optional [hosted service](packages/hosted-store-service/README.md),
48
- which also serves a GitHub-authenticated browser dashboard, can own ticket
49
- persistence and external tracker credentials, and can run the dispatcher itself on a schedule (see [the hosted dispatcher](docs/hosted-dispatcher.md)). Repository identity across the
50
- Store, dispatcher, query commands, and that dashboard is the repository's
51
- normalized origin URL (e.g. `https://github.com/defrex/autobuild`), never a
52
- checkout path; pre-origin records are not migrated. The web UI uses a server-managed
53
- session; automation clients can use the versioned [operator API](docs/operator-api.md)
54
- for the same controls and projections without exposing service credentials, and
55
- external MCP agents connect through the [hosted MCP server](docs/mcp.md).
56
-
57
17
  ## Quickstart
58
18
 
59
19
  You need [Bun](https://bun.sh), `git`, and the local prerequisites for the
60
- agent runtime you select: Claude Code, Codex CLI, or Pi. Choosing the GitHub
61
- forge also requires an authenticated [`gh` CLI](https://cli.github.com)
62
- (`gh auth login`) or an exported `GITHUB_TOKEN`/`GH_TOKEN`; the local-git forge needs no
63
- account or remote.
64
- Init suggests only runtimes whose executable and/or provider authentication is
65
- usable on your machine.
20
+ agent runtime you select: Claude Code, Codex CLI, or Pi. The GitHub forge also
21
+ needs an authenticated [`gh` CLI](https://cli.github.com) or an exported
22
+ `GITHUB_TOKEN`/`GH_TOKEN`; the local-git forge needs no account or remote.
66
23
 
67
24
  <!-- release-install:start -->
68
25
 
69
26
  ```sh
70
- bun add -g @defrex/autobuild@0.10.0
27
+ bun add -g @defrex/autobuild@0.11.0
71
28
  ```
72
29
 
73
30
  <!-- release-install:end -->
@@ -78,44 +35,36 @@ Then, from the repository you want built:
78
35
  ab init
79
36
  ```
80
37
 
81
- Vendors the editable `ab-*` skills, writes a stack-neutral config, and hands
82
- repository-aware setup to a coding agent. Follow the canonical
83
- [installation and setup procedure](docs/setup.md) to keep local worktrees or
84
- explicitly configure Vercel, then commit the setup and check its real execution
85
- environment without creating build history:
86
-
87
- ```sh
88
- ab init --validate
89
- ```
90
-
91
- See the [configuration reference](docs/configuration.md) for every field. Only
92
- a clean readiness result should be followed by:
38
+ This vendors the editable `ab-*` skills, writes a stack-neutral config, and
39
+ hands repository-aware setup to a coding agent. The
40
+ [configuration reference](docs/configuration.md) covers every field. Then
41
+ start the dispatcher and its live dashboard:
93
42
 
94
43
  ```sh
95
44
  ab dispatch
96
45
  ```
97
46
 
98
- Starts the dispatcher, with the live dashboard on a TTY.
99
-
100
- In your selected coding agent, invoke the ticket-grooming skill:
47
+ Then groom your first ticket in your coding agent:
101
48
 
102
49
  ```text
103
- /ab-spec I want to build a feature!
50
+ /ab-spec add a retry budget
104
51
  ```
105
52
 
106
- The agent asks the questions needed to produce a quality ticket and adds it
107
- directly to your ticketing system. Using the skill to create tickets then
108
- largely becomes your input workflow.
53
+ The agent asks the questions needed to produce a buildable ticket and files it
54
+ in your tracker. From then on, that skill is how work goes in.
109
55
 
110
56
  ## How it works
111
57
 
112
- Every build moves through the same fixed pipeline:
58
+ ### Every ticket runs the pipeline
113
59
 
114
60
  ```text
115
61
  spec → plan ⇄ plan-review → implement ⇄ code-review → verify:* → finalize
116
62
  epilogue: (pr.conflicted → reconcile → verify:*)* → merged or closed
117
63
  ```
118
64
 
65
+ Each ticket moves through deterministic phases inside its own build-runner.
66
+ Every phase is a fresh agent session, with artifacts knitting them together.
67
+
119
68
  1. **spec** — the dispatcher claims a ready ticket, establishes the final
120
69
  spec, and cuts a branch. The spec is the contract for everything after.
121
70
  2. **plan ⇄ plan-review** — a planner writes an implementation plan; an
@@ -125,54 +74,88 @@ spec → plan ⇄ plan-review → implement ⇄ code-review → verify:* → fin
125
74
  you instead of looping forever.
126
75
  4. **verify:\*** — your verification steps, in the order you declare them:
127
76
  shell commands judged by exit code, or agent verifiers that return a
128
- verdict.
129
- 5. **finalize** — the PR opens with an agent-written description and a summary
130
- of explicitly attached evidence, then any post-PR steps you've configured
131
- (changelogs, release notes) run failure-tolerant. A content-producing step
132
- commits selected files locally; the runner extends the open PR branch with
133
- a regular push. A no-op adds no commit, and a publication failure becomes a
134
- follow-up observation rather than failing the green build. Agent verifiers
135
- attach an exact screenshot, trace, or other artifact with
136
- `ab artifact put <kind> <file> --attach`; the PR always gets a pinned
137
- retrieval command, and configured public image hosting can also render
138
- images inline.
77
+ verdict and can attach evidence such as screenshots.
78
+ 5. **finalize** — the PR opens with an agent-written description and the
79
+ attached evidence, then any post-PR steps you've configured (changelogs,
80
+ release notes) run.
139
81
  6. **epilogue** — the dispatcher watches the open PR. Conflicts route back
140
82
  through reconcile and re-verify; the build ends `merged` or `closed`.
141
83
 
142
- Each phase is an agent session, but the pipeline itself is not agentic:
143
- agents never decide what phase comes next, and outcomes are never inferred
144
- from what a model printed. Every phase reports through a typed CLI into an
145
- append-only event log, and tested code decides the transition. That log is
146
- the build — kill the process at any point and the dispatcher resumes from
147
- durable state, and every decision along the way stays queryable after the
148
- fact.
84
+ ### A dispatcher spawns builds
85
+
86
+ The dispatcher claims ready tickets and starts a build-runner for each.
87
+ Runners are isolated, and all state is logged. Agents never decide what phase
88
+ comes next, and outcomes are never inferred from what a model printed: every
89
+ phase reports through a typed CLI into the event log, and tested code decides
90
+ the transition. That log is the build. Kill the process at any point and the
91
+ dispatcher resumes from durable state.
149
92
 
150
93
  The pipeline grammar is fixed on purpose; `verify:*` and `finalize:*` are the
151
- extension points, declared per-repo in `autobuild.toml`. Post-step agents may
152
- commit locally but never push or call the forge; publication stays
153
- kernel-owned. For the seams and the reasoning behind them, see
154
- [`docs/architecture.md`](docs/architecture.md) and [`SPEC.md`](SPEC.md).
155
-
156
- ### Observation harvesting
157
-
158
- Builds notice things they shouldn't fix. An implementer that spots a latent
159
- bug, a worthwhile refactor, or a missing follow-up outside its spec records a
160
- structured observation (`ab observe`) and moves on — the insight is kept, and
161
- scope creep stays out of the PR.
162
-
163
- Observations accumulate per repository, and once enough pile up the
164
- dispatcher runs a separate outer workflow — scan → synthesize ⇄ review →
165
- file — that distills them into proposed tickets, deduplicated against work
166
- already filed. By default, proposals land in triage with the reserved
167
- `autobuild:proposal` label and never dispatch themselves: the label means
168
- observation harvest created the ticket for human triage, not that Autobuild has
169
- groomed or claimed it. Autobuild does not use this label as a readiness gate or
170
- remove it; it is distinct from any configured or historical `autobuild` ready
171
- label. You groom and ready proposals like any ticket you wrote yourself. Agents
172
- propose; humans dispatch.
94
+ extension points, declared per repository in `autobuild.toml`. For the seams
95
+ and the reasoning behind them, see [`docs/architecture.md`](docs/architecture.md)
96
+ and [`SPEC.md`](SPEC.md).
97
+
98
+ ## All local, all remote, or anywhere between
99
+
100
+ Every seam is an adapter you can customize for your project. The build
101
+ pipeline stays the same.
102
+
103
+ | Seam | Decides | Adapters |
104
+ | ---------- | --------------------- | ------------------------------------------ |
105
+ | tickets | where work comes from | local files, Linear, hosted, plugin |
106
+ | dispatcher | what starts builds | local process, scheduled (hosted) |
107
+ | workspace | where a build runs | git worktree, Vercel Sandbox, plugin |
108
+ | runtime | who does the thinking | Claude Code, Codex, Pi, plugin |
109
+ | forge | where code lands | local git, GitHub, plugin |
110
+ | store | where state lives | SQLite, Postgres, any HTTP store |
111
+ | operators | how you watch | terminal, web UI, MCP |
112
+
113
+ Plugins are trusted Bun modules declared in `autobuild.toml` against the
114
+ versioned `@defrex/autobuild/plugin-sdk`. Teams that want one durable store
115
+ across hosts can deploy the optional
116
+ [hosted service](packages/hosted-store-service/README.md), which adds a
117
+ browser dashboard, a [scheduled dispatcher](docs/hosted-dispatcher.md), an
118
+ [operator API](docs/operator-api.md), and an [MCP server](docs/mcp.md).
119
+
120
+
121
+ ## Built for throughput, not latency
122
+
123
+ One build may take longer than a chat with an agent. The trade is far more
124
+ changes landing every day, with much greater reliability.
125
+
126
+ - **File it and walk away.** Nothing in the pipeline waits on you. You don't
127
+ approve tool calls, paste errors back, or watch a terminal. A build
128
+ escalates to you only when it is truly blocked.
129
+ - **As many builds as you can groom.** Builds run side by side, each in its
130
+ own workspace, up to the limit you set. Your attention stops being the
131
+ ceiling; how fast you can write good tickets becomes the limit.
132
+
133
+ Headless is safe because the pipeline is deterministic code, not model
134
+ judgment. State lives in a typed, append-only event log, phase transitions
135
+ are owned by tested code, and every build leaves a queryable paper trail. Your
136
+ merge gates are never bypassed: a PR lands when your required checks and your
137
+ consent say it lands.
138
+
139
+ ## Let the queue fill itself
140
+
141
+ Once tickets are the interface, anything that can write a ticket can put work
142
+ in front of you.
143
+
144
+ Builds feed the queue themselves. An implementer that spots a latent bug, a
145
+ worthwhile refactor, or a missing follow-up outside its spec records an
146
+ observation (`ab observe`) and moves on: the insight is kept, and scope creep
147
+ stays out of the PR. Once enough pile up, the dispatcher distills them into
148
+ proposed tickets, deduplicated against work already filed, and by default
149
+ files them into triage. You groom them like any ticket you wrote yourself.
150
+ Agents propose; humans dispatch.
151
+
152
+ Support threads, meeting notes, and error telemetry can feed it the same way.
153
+ An agent with the `ab` CLI or the MCP server can file tickets and answer
154
+ routine escalations, leaving you only the real product calls.
173
155
 
174
156
  ## Learn more
175
157
 
158
+ - [autobuild.run](https://www.autobuild.run) — the project site.
176
159
  - [`docs/spec-standard.md`](docs/spec-standard.md) — what makes a ticket
177
160
  buildable: the standard every dispatched spec must meet.
178
161
  - [`docs/configuration.md`](docs/configuration.md) — the complete strict
@@ -181,7 +164,8 @@ propose; humans dispatch.
181
164
  configuration, as a worked example of the config surface.
182
165
  - [`docs/architecture.md`](docs/architecture.md) — how the design maps to the
183
166
  codebase: kernel, ports, processes, and stores.
184
- - [`packages/postgres-store/README.md`](packages/postgres-store/README.md) — serverless PostgreSQL persistence with S3-compatible or Vercel Blob storage.
167
+ - [`packages/postgres-store/README.md`](packages/postgres-store/README.md) —
168
+ serverless PostgreSQL persistence with S3-compatible or Vercel Blob storage.
185
169
  - [`docs/remote-store-protocol.md`](docs/remote-store-protocol.md) — the
186
170
  complete HTTP server contract and BuildStore conformance instructions.
187
171
  - [`SPEC.md`](SPEC.md) — the source of truth for the design and its
package/SPEC.md CHANGED
@@ -233,6 +233,19 @@ Small, independently runnable, crash-safe:
233
233
  - **ingesters** — other outer-loop processes turning signals into proposals (§12).
234
234
  - **operator** — UI process(es); see §14.
235
235
 
236
+ Ticket-source ready scans run immediately when no completed measurement exists,
237
+ then every
238
+ `ticketScanIntervalSeconds` (default 60 seconds, a positive integer). Actual
239
+ tick spacing is the floor when the configured interval is shorter. A
240
+ same-repository `pr.merged`, `build.completed`, or `build.aborted` fact forces
241
+ another scan on the next tick; changed ready criteria also force a scan.
242
+ Housekeeping keeps its ordinary tick cadence. Between scans, queue depth and
243
+ standing diagnostics repeat the last completed measurement, including across
244
+ process restarts. Tickets groomed directly in the tracker are picked up within
245
+ one scan interval plus one tick when capacity permits. Observation while intake
246
+ is off shares the cadence; an observation measurement cannot suppress a fresh
247
+ admission scan when intake is enabled.
248
+
236
249
  ### 3.4 The event log spine
237
250
 
238
251
  Build processes append typed events to per-build logs; repository-scoped outer
@@ -501,8 +514,11 @@ stream read's ([docs/remote-store-protocol.md](docs/remote-store-protocol.md),
501
514
 
502
515
  #### 7.1.2 The embedded orchestrator (turns)
503
516
 
504
- The orchestrator is the agent that files tickets, watches builds, unblocks
505
- them, and discusses direction — played in the hosted service by a stateless
517
+ The orchestrator is a role, not a deployment: the agent that files tickets,
518
+ watches builds, unblocks them, and discusses direction. Any agent can take it
519
+ on — a coding-agent session at the repository root using the `ab` CLI or
520
+ `ab mcp` needs no initialization — and the canonical `ab-operate` skill is its
521
+ manual in either setting. In the hosted service it is played by a stateless
506
522
  turn runner over the operator tool registry, not by an agent living in a
507
523
  sandbox (parity with MCP; no credential plumbing) and not by an external
508
524
  workflow engine (durability comes from the store; the tick pattern already
@@ -2240,6 +2256,7 @@ ordinary boundary reload resumes.
2240
2256
  ```toml
2241
2257
  baseBranch = "main"
2242
2258
  capacity = 3 # concurrent builds for this repo
2259
+ ticketScanIntervalSeconds = 60 # positive integer; ready scans, with tick spacing as floor
2243
2260
  forge = "github" # builtin: github | local-git; or a plugin name
2244
2261
  plugins = ["./plugins/local.ts", "@acme/autobuild-plugin"]
2245
2262
 
@@ -2347,7 +2364,7 @@ decide where a build's effective configuration is composed from:
2347
2364
  |---|---|---|
2348
2365
  | Build-owned (pinned) | `[verify]` and `[verify.<step>]`, `[finalize]` and `[finalize.<step>]`, `[commands]`, `[workspace]` (provider and config, including `provisioning` and `runtimeProvisioning`) | The build's own branch: its branch head once it has published commits, otherwise the recorded base commit its workspace was cut from. The exact commit is recorded once per deposit in the effective-config artifact metadata — except in checkout mode when the worktree read cannot be attributed to the branch-head commit: because the worktree file has uncommitted edits, because the clean/dirty comparison could not be performed (non-git workspace or a transient git error), or because the branch head never carried the file. The deposit then records the source `worktree-dirty` and no commit, because no commit claims the worktree bytes (the build still runs them). |
2349
2366
  | Deployment-owned (live) | `[roles]` (runtime, model, args, alternates, per-role `sessionBudgetSeconds`) and `[policy]` (budgets, retry bounds, stall and harvest knobs) | The dispatcher's live snapshot; an accepted reload delivers these to active builds. |
2350
- | Dispatcher-owned | `baseBranch`, `capacity`, `forge`, `plugins`, `pr`, `tickets`, `orchestrator` | The dispatcher's live snapshot; the build runner never interprets them. |
2367
+ | Dispatcher-owned | `baseBranch`, `capacity`, `ticketScanIntervalSeconds`, `forge`, `plugins`, `pr`, `tickets`, `orchestrator` | The dispatcher's live snapshot; the build runner never interprets them. |
2351
2368
 
2352
2369
  A build's pipeline definition is therefore composed from the build's own
2353
2370
  branch: in origin mode the dispatcher reads `autobuild.toml` at the build's
@@ -2423,8 +2440,8 @@ installs into a repo:
2423
2440
  non-agent-invocable: they are invoked explicitly by the runner or a human,
2424
2441
  never auto-triggered by a model pattern-matching a description — a model
2425
2442
  must not start a pipeline phase by accident. The model-invocable exceptions
2426
- (`ab-spec`, `ab-tickets`, `ab-guide`) are exactly the three skills that
2427
- **drive no phase**; membership is decided by that criterion, not taste.
2443
+ (`ab-spec`, `ab-tickets`, `ab-guide`, `ab-operate`) are exactly the four
2444
+ skills that **drive no phase**; membership is decided by that criterion, not taste.
2428
2445
 
2429
2446
  When `.agents/skills` and `.claude/skills` resolve to the same filesystem
2430
2447
  entry, that repository-level alias already satisfies harness discovery. Init
@@ -23,7 +23,7 @@ try {
23
23
  }
24
24
 
25
25
  const stop = new AbortController()
26
- const requestStop = (): void => stop.abort()
26
+ const requestStop = (signal?: string): void => stop.abort(signal)
27
27
  // Keep both handlers installed through the complete graceful drain. The first
28
28
  // targeted SIGINT comes from the supervisor; later foreground-group signals
29
29
  // must not regain their default disposition inside an open claim tick.
@@ -168,7 +168,7 @@ the branch head never carried the file — the deposit records the source
168
168
  provenance declines to attribute them to any commit. **Deployment-owned** sections — `[roles]`
169
169
  (runtime, model, args, alternates, per-role `sessionBudgetSeconds`) and
170
170
  `[policy]` — keep flowing live. **Dispatcher-owned** sections (`baseBranch`,
171
- `capacity`, `forge`, `plugins`, `pr`, `tickets`, `orchestrator`) configure the
171
+ `capacity`, `ticketScanIntervalSeconds`, `forge`, `plugins`, `pr`, `tickets`, `orchestrator`) configure the
172
172
  dispatcher itself and are never interpreted by the build runner.
173
173
 
174
174
  Consequently, an accepted base-branch reload re-deposits only the
@@ -195,6 +195,7 @@ These fields hot-reload:
195
195
  | Field or table | Boundary where a new value applies |
196
196
  |---|---|
197
197
  | `baseBranch` | Next dispatch or base fallback decision; existing `build.created` facts remain immutable. |
198
+ | `ticketScanIntervalSeconds` | Next tick; evaluates elapsed time against the new interval. |
198
199
  | `capacity` | Next dispatcher tick. |
199
200
  | `[pr]` | Next build creation. |
200
201
  | `[commands]` | Next setup, verify check, or finalize check selected for a build whose OWN branch carries it. A base-branch reload does not retarget an in-flight build. |
@@ -229,10 +230,24 @@ any table header.
229
230
  | Field | Default | Constraints | Purpose |
230
231
  |---|---:|---|---|
231
232
  | `baseBranch` | `"main"` | nonempty string | Branch used to cut builds, target PRs, and merge during reconciliation. |
233
+ | `ticketScanIntervalSeconds` | `60` | positive integer seconds | Ticket-source ready scan cadence; tick spacing is the effective minimum. |
232
234
  | `capacity` | `1` | positive integer | Maximum concurrent nonterminal builds for this repository. Paused and blocked builds still occupy capacity. |
233
235
  | `forge` | `"github"` | nonblank string | Forge adapter name. Shipped values are `github` and `local-git`; configured plugins may register other names. |
234
236
  | `plugins` | `[]` | array of unique nonblank module specifiers | Trusted Bun plugin modules loaded by dispatch, `ab ticket`, and scoped phase CLI processes. |
235
237
 
238
+ Ticket-source ready scans run immediately when no completed measurement exists,
239
+ then every
240
+ `ticketScanIntervalSeconds` (default 60 seconds, a positive integer). Actual
241
+ tick spacing is the floor when the configured interval is shorter. A
242
+ same-repository `pr.merged`, `build.completed`, or `build.aborted` fact forces
243
+ another scan on the next tick; changed ready criteria also force a scan.
244
+ Housekeeping keeps its ordinary tick cadence. Between scans, queue depth and
245
+ standing diagnostics repeat the last completed measurement, including across
246
+ process restarts. Tickets groomed directly in the tracker are picked up within
247
+ one scan interval plus one tick when capacity permits. Observation while intake
248
+ is off shares the cadence; an observation measurement cannot suppress a fresh
249
+ admission scan when intake is enabled.
250
+
236
251
  ### Plugin modules
237
252
 
238
253
  <!-- config-fragment:plugins -->
@@ -313,7 +328,12 @@ without adding a runtime Autobuild dependency to the plugin. That entry point
313
328
  exports the manifest/factory types, port types, fake adapters, and reusable
314
329
  TicketSource, AgentRunner, WorkspaceProvider, Forge, BuildStore, and BlobStore
315
330
  contract suites. Adapter values may use the backward-compatible bare factory or
316
- carry a contract fixture descriptor. Ticket sources may also declare
331
+ carry a contract fixture descriptor.
332
+ TicketSource adapters must populate the required `Ticket.resolved` boolean on
333
+ listReady, get, and create projections, using the same native lifecycle rules as
334
+ `dependencyStates`. Plugin authors upgrading older adapters must add this field;
335
+ ready-state membership does not imply that a ticket is unresolved. Dispatch
336
+ reuses these facts only within the current tick. Ticket sources may also declare
317
337
  `requiredEnv`; the host checks every declared variable for a nonempty value
318
338
  before invoking the adapter factory. Plugins using Forge abort-cleanup and
319
339
  AgentRunner turn-cancellation capabilities introduced in API 1.2 should require
@@ -1259,6 +1279,15 @@ triageState = "Backlog"
1259
1279
  Linear credentials do not belong in this table. Set `LINEAR_API_KEY` in the
1260
1280
  environment or local `.env` file described below.
1261
1281
 
1282
+ [Linear's request budget](https://linear.app/developers/rate-limiting) is shared
1283
+ by every dispatcher and `ab ticket` command using the API key, and by other keys
1284
+ belonging to the same authenticated user. A rate-limited ready scan or dependency
1285
+ read fails the evaluation tick without new intake claims or synthetic
1286
+ dependency-blocked counts. That source instance suppresses requests until the
1287
+ reported reset time; the next call then runs normally. Missing, invalid or expired
1288
+ reset headers use a conservative 60-second cooldown. Cooldowns are local to the
1289
+ instance: processes and restarts do not coordinate their budgets.
1290
+
1262
1291
  For a hosted backend, repository policy remains local while credentials remain
1263
1292
  at the service:
1264
1293
 
@@ -1323,7 +1352,7 @@ restart-classified as a whole.
1323
1352
  | `model` | — | nonblank string, required when `enabled = true` | Provider-qualified model string for the turn runner, in the same vocabulary as `[roles]` model strings (a leading `vercel-ai-gateway/` prefix is accepted and stripped). Resolved through the deployment's gateway credential. |
1324
1353
  | `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 (`ORCHESTRATOR_ROUTE_LIMIT_SECONDS`, the same number the operator route pins as `maxDuration`). |
1325
1354
  | `approvals` | see below | array of `tool` or `tool:qualifier` strings | Registry tool names whose calls suspend a turn until the operator answers. Default: `["builds.control:abort", "builds.control:discard", "builds.answer:revise-spec", "sandbox.publish", "tickets.move:ready"]`. Empty (`[]`) means none — autonomous operation. An entry naming an absent or unknown tool is **inert by design**, never a config error: the list stays stable while the registry grows, and a tool's removal cannot break parsing. A `tool:qualifier` entry applies to the named discriminator value only (for example `tickets.move:ready` approves moving a ticket to `ready`); for a tool without a discriminator, only the bare form matches. |
1326
- | `wake` | inherits the default | array of event-type globs | The attention events a **new** session wakes for. Kept schema-optional so absent (inherit the default attention set — the build events `ab watch` filters on by default plus the repository-journal events `ab watch --repository` follows) and `[]` (never wake; message-only) stay distinct. Each glob must match at least one build or repository-journal event type, validated with the same compiler `ab watch` uses. Existing sessions never retroactively inherit; an explicit `PUT .../wake` always wins. |
1355
+ | `wake` | inherits the default | array of event-type globs | The attention events a **new** session wakes for. Kept schema-optional so absent (inherit the default attention set — the build events `ab watch` filters on by default plus the repository-journal events `ab watch` follows) and `[]` (never wake; message-only) stay distinct. Each glob must match at least one build or repository-journal event type, validated with the same compiler `ab watch` uses. Existing sessions never retroactively inherit; an explicit `PUT .../wake` always wins. |
1327
1356
  | `sandbox` | — | strict subtable; absence reads as the defaults | Sandbox behavior knobs. |
1328
1357
 
1329
1358
  `[orchestrator.sandbox]` fields:
@@ -1572,10 +1601,8 @@ pre-attempt index and reports the original Git failure; merged worktree files
1572
1601
  remain in place and the merge's exit status is unchanged. Upgrade never pushes
1573
1602
  or rewrites history.
1574
1603
 
1575
- Autobuild now installs 12 skills; only `ab-spec`, `ab-tickets`, and `ab-guide`
1576
- are model-invocable. (`ab-operate` — the embedded orchestrator's operating
1577
- manual — also carries `disable-model-invocation: true`; it is read by the
1578
- hosted turn runner, never invoked by a model in a build session.) The setup reference is an ordinary support file in the
1604
+ Autobuild now installs 12 skills; only `ab-spec`, `ab-tickets`, `ab-guide`,
1605
+ and `ab-operate` are model-invocable. The setup reference is an ordinary support file in the
1579
1606
  editable/pristine `ab-guide` tree and participates in the same three-way
1580
1607
  upgrade merge as every other vendored file.
1581
1608
 
@@ -184,3 +184,20 @@ The API does not expose phase-session commands, runner startup, a push or
184
184
  long-poll streaming transport, or a generic event-append operation; event
185
185
  streaming is composed client-side by `ab watch`, which polls the store's
186
186
  event reads.
187
+
188
+
189
+ Dispatcher liveness is also visible through `ab repository status --json`:
190
+ `dispatcher.holder` identifies the unexpired repository lease owner, independently
191
+ of `latestRun`. Heartbeat age measures attributable renewals; tick age measures
192
+ durable progress. Missing historical evidence is null. The liveness query uses
193
+ bounded repository facts first, with a full-history fallback when a later
194
+ invocation hides the holder start or the latest stop. Other state reads retain
195
+ their bounded semantics.
196
+
197
+ Repository-following `ab watch` additionally emits a read-only `dispatcher.lost`
198
+ observation when an observed lease expires without a matching stop. JSON carries
199
+ `observation` instead of `event`, with run, observedAt, leaseExpiresAt and available
200
+ identity, plus the usual repo/build/state/cursor fields. It invents no durable
201
+ sequence or actor. Retain the cursor to avoid repeat reports across invocations.
202
+ No lease alone is not evidence of death. SIGINT, SIGHUP and SIGTERM drain dispatch,
203
+ record the first signal in the stop fact, and release its lease before exit.
@@ -77,3 +77,8 @@ The shared `TicketSource` contract is normative for the database adapter and
77
77
  the hosted client/server pair. External pass-through is tested with a fake
78
78
  source behind HTTP. The existing opt-in live Linear contract remains the proof
79
79
  of Linear's GraphQL behavior; the hosted service uses that adapter unchanged.
80
+
81
+ Every ticket returned by list, get, or create requires a boolean `resolved`,
82
+ with the same source-native completion semantics as `DependencyState.resolved`.
83
+ The client preserves it when remapping ticket references. Missing or non-boolean
84
+ values fail wire validation. Client and service still require exact version matches.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@defrex/autobuild",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "Agent-driven software delivery from groomed ticket to merged PR",
5
5
  "repository": {
6
6
  "type": "git",
@@ -139,8 +139,8 @@
139
139
  },
140
140
  "devDependencies": {
141
141
  "@biomejs/biome": "2.5.5",
142
- "@defrex/autobuild-postgres-store": "0.9.0",
143
- "@defrex/autobuild-vercel-sandbox": "0.9.0",
142
+ "@defrex/autobuild-postgres-store": "0.10.1",
143
+ "@defrex/autobuild-vercel-sandbox": "0.10.1",
144
144
  "@resvg/resvg-js": "2.6.2",
145
145
  "@types/bun": "^1.3.14",
146
146
  "@types/node": "^24.0.0",
@@ -53,7 +53,7 @@ export async function withDispatchTerminalRestore<T>(
53
53
  terminal: TerminalOut,
54
54
  run: () => Promise<T>,
55
55
  boundary: TerminalRestoreProcess = process as unknown as TerminalRestoreProcess,
56
- requestStop: () => void = () => {},
56
+ requestStop: (signal: NodeJS.Signals) => void = () => {},
57
57
  ): Promise<T> {
58
58
  const hook =
59
59
  command === 'dispatch'
@@ -96,7 +96,7 @@ export async function runBinary(
96
96
  // cleanly at the next tick boundary (§15.6-C: in-flight leases expire and
97
97
  // a future dispatch re-attaches).
98
98
  const controller = new AbortController()
99
- const onSigint = (): void => controller.abort()
99
+ const onSigint = (): void => controller.abort('SIGINT')
100
100
  const ownsSigint = usesGenericSessionlessSigintHandler(command)
101
101
  const terminal = processTerminal(process.stdout)
102
102
  const removeSigint = installSessionlessSigintHandler(command, onSigint)
@@ -119,7 +119,7 @@ export async function runBinary(
119
119
  ...(resolveDashboardRenderer !== undefined ? { resolveDashboardRenderer } : {}),
120
120
  }),
121
121
  process as unknown as TerminalRestoreProcess,
122
- onSigint,
122
+ (signal) => controller.abort(signal),
123
123
  )
124
124
  } finally {
125
125
  removeSigint()
@@ -268,6 +268,8 @@ export interface DashboardModel {
268
268
  harvestPaused: boolean
269
269
  /** Stable row identity, never a row index. */
270
270
  selection?: DashboardSelection
271
+ /** Invocation-local standby identity, independent of notices and build rows. */
272
+ standby?: string
271
273
  /** Release courtesy, separate from transient and startup warnings so those
272
274
  * notices can never overwrite one another. Store-only frontends project it
273
275
  * from their correlated dispatcher run. */
@@ -1090,6 +1092,7 @@ interface DashboardFrameHeader {
1090
1092
  queued: number
1091
1093
  observationCount: number
1092
1094
  selection?: DashboardSelection
1095
+ standby?: string
1093
1096
  availableUpgrade?: string
1094
1097
  warningLines?: readonly string[]
1095
1098
  resumeInput?: ResumeInputView
@@ -1125,6 +1128,7 @@ export function buildDashboardFromProjected(
1125
1128
  defaultAutoMerge: settings.defaultAutoMerge,
1126
1129
  harvestPaused: harvestProjection.harvestPaused,
1127
1130
  ...(header.selection !== undefined ? { selection: header.selection } : {}),
1131
+ ...(header.standby !== undefined ? { standby: header.standby } : {}),
1128
1132
  ...(header.availableUpgrade !== undefined ? { availableUpgrade: header.availableUpgrade } : {}),
1129
1133
  ...(header.warningLines !== undefined && header.warningLines.length > 0
1130
1134
  ? { warningLines: header.warningLines }