merge-steward 0.19.1 → 0.19.2

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 (2) hide show
  1. package/README.md +48 -357
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -1,22 +1,26 @@
1
1
  # merge-steward
2
2
 
3
- `merge-steward` is a self-hosted merge queue for bot-managed and human-managed
4
- GitHub pull requests. It admits approved PRs whose required checks are green,
5
- builds cumulative speculative branches, waits for CI on those exact integrated
6
- SHAs, and then fast-forwards `main` to the tested result. On failure, it evicts
7
- with a durable incident record and GitHub check run so an agent or human can
8
- repair the branch and re-queue it.
3
+ Self-hosted serial merge queue for bot-managed and human-managed GitHub pull requests. Admits approved PRs whose required checks are green, builds speculative branches on top of the latest `main`, waits for CI on those integrated SHAs, and fast-forwards `main` to the tested result.
9
4
 
10
- Fully independent of PatchRelay. Communicates through GitHub — PRs, reviews,
11
- checks, labels, branches.
5
+ Independent of PatchRelay. Communicates through GitHub only — PRs, reviews, checks, labels, branches. Pairs with `review-quill`; neither requires the other.
12
6
 
13
- Shared protocol:
7
+ ## Why this matters
14
8
 
15
- - [../../docs/design-docs/pr-automation-loop.md](../../docs/design-docs/pr-automation-loop.md)
9
+ PRs delivered through the queue are tested against `main` as it was at admission time, and re-validated if `main` advances during validation. No more "CI was green yesterday, breaks on merge today" — the queue catches the integration bug before `main` ever sees it.
10
+
11
+ ## How it works
12
+
13
+ 1. A PR becomes eligible when GitHub says it is approved and its required checks are green.
14
+ 2. The steward notices through webhook wakeups or startup reconcile scans, and admits the PR to the queue.
15
+ 3. It builds a speculative branch — `main + PR` at the head, cumulative downstream (`main + A + B`, `main + A + B + C`).
16
+ 4. CI runs on that speculative SHA.
17
+ 5. If the head's speculative SHA is still a fast-forward from current `main`, the steward pushes that SHA to `main`; otherwise it retries.
18
+ 6. On CI failure: retry (gated on base SHA change), then evict with a durable incident record and GitHub check run.
19
+ 7. PatchRelay, the `ship-pr` skill, or any agent sees the check run failure and fixes the branch; when CI passes again the PR can be re-admitted.
16
20
 
17
21
  ## Use with your own agent
18
22
 
19
- If you want your own agent (Claude Code, Cursor, Codex CLI, …) to drive PRs through the queue and react to evictions / failing checks instead of running PatchRelay's full harness, install the [`ship-pr`](https://github.com/krasnoperov/patchrelay-agents) skill from the companion Claude Code marketplace:
23
+ For an agent that drives PRs through the queue and reacts to evictions / failing checks without running PatchRelay's full harness, install the [`ship-pr`](https://github.com/krasnoperov/patchrelay-agents) skill from the companion Claude Code marketplace:
20
24
 
21
25
  ```
22
26
  /plugin marketplace add krasnoperov/patchrelay-agents
@@ -25,384 +29,71 @@ If you want your own agent (Claude Code, Cursor, Codex CLI, …) to drive PRs th
25
29
 
26
30
  The skill wraps `merge-steward pr status --wait` and `review-quill pr status --wait` into a blocking-gate workflow with stable exit codes, so the agent only wakes on terminal outcomes.
27
31
 
28
- ## How it works
29
-
30
- 1. A PR becomes eligible when GitHub says it is approved and its required checks are green
31
- 2. The steward notices that through webhook wakeups or startup/reconcile scans
32
- 3. It admits the PR into the queue; the optional `queue` label is just an operator-friendly nudge, not the sole trigger
33
- 4. The steward builds a speculative branch:
34
- - head of queue: `main + PR`
35
- - downstream entries: cumulative specs like `main + A + B`
36
- 5. CI runs on that speculative SHA
37
- 6. If the queue head's speculative SHA is still a fast-forward from current `main`, the steward pushes that exact SHA to `main`
38
- 7. On failure: retry (gated on base SHA change), then evict with a durable incident record and GitHub check run
39
- 8. PatchRelay (or any agent) sees the check run failure and can fix the branch
40
- 9. When the branch is fixed and CI passes again, the PR can be re-admitted
41
-
42
- ## Setup
43
-
44
- ### Prerequisites
32
+ ## Quick start
45
33
 
46
- - Node.js 24+
47
- - `gh` CLI available in `PATH`
48
- - `git` binary
49
-
50
- ### Bootstrap
51
-
52
- Initialize the machine-level steward home once:
34
+ Prerequisites: Node.js 24+, `gh` CLI in `PATH`, `git`.
53
35
 
54
36
  ```bash
37
+ npm install -g merge-steward
55
38
  merge-steward init https://queue.example.com
56
- ```
57
-
58
- That creates:
59
-
60
- - `~/.config/merge-steward/runtime.env`
61
- - `~/.config/merge-steward/service.env`
62
- - `~/.config/merge-steward/merge-steward.json`
63
- - `~/.config/merge-steward/repos/`
64
- - `/etc/systemd/system/merge-steward.service`
65
-
66
- Add one repo-scoped steward instance:
67
-
68
- ```bash
69
- merge-steward attach owner/repo
70
- ```
71
-
72
- That writes `~/.config/merge-steward/repos/<derived-id>.json`. By default, `attach` derives the repo id from the GitHub repo name and discovers the default branch from GitHub. Required checks are not stored locally; the running steward reads GitHub branch protection as the source of truth.
73
-
74
- If you also use `review-quill`, its `review-quill/verdict` check can be added to
75
- the repository's required checks if you want machine review to be part of the
76
- merge gate. Steward will naturally follow whatever required checks the repo
77
- configuration uses, but its primary admission gate is still GitHub's formal
78
- review state plus those required checks.
79
-
80
- Validate the setup:
81
-
82
- ```bash
39
+ merge-steward attach owner/repo --base-branch main
83
40
  merge-steward doctor --repo repo
84
41
  merge-steward service status
85
42
  merge-steward queue status --repo repo
86
43
  ```
87
44
 
88
- ### Secrets
89
-
90
- Keep only non-secret identifiers in:
45
+ - `init` writes config files, a systemd unit, and a generated webhook secret.
46
+ - `attach` discovers the default branch from GitHub and stores a per-repo config.
47
+ - Required checks are learned from GitHub branch protection at runtime — the steward does not keep a local copy.
91
48
 
92
- - `~/.config/merge-steward/service.env`
49
+ Full setup (GitHub App permissions, secrets, webhook events, systemd, HTTP API): [docs/merge-steward.md](../../docs/merge-steward.md).
93
50
 
94
- Example:
51
+ ## Everyday commands
95
52
 
96
53
  ```bash
97
- MERGE_STEWARD_GITHUB_APP_ID=123456
98
- MERGE_STEWARD_GITHUB_APP_INSTALLATION_ID=12345678
99
- ```
100
-
101
- Store secrets in encrypted systemd credentials:
102
-
103
- - `/etc/credstore.encrypted/merge-steward-webhook-secret.cred`
104
- - `/etc/credstore.encrypted/merge-steward-github-app-pem.cred`
105
-
106
- The running service resolves the webhook secret in this order:
107
-
108
- 1. `$CREDENTIALS_DIRECTORY/<name>`
109
- 2. `${ENV_KEY}_FILE`
110
- 3. `${ENV_KEY}`
111
-
112
- The running service resolves GitHub auth in this order:
113
-
114
- 1. `MERGE_STEWARD_GITHUB_APP_ID` + `merge-steward-github-app-pem` / `MERGE_STEWARD_GITHUB_APP_PRIVATE_KEY`
115
-
116
- In practice, use:
117
-
118
- - `merge-steward-webhook-secret` for validating incoming GitHub webhooks
119
- - `MERGE_STEWARD_GITHUB_APP_ID` plus `merge-steward-github-app-pem` for production GitHub auth
120
- - `MERGE_STEWARD_GITHUB_APP_INSTALLATION_ID` if you want to pin a single installation instead of resolving one per repo
121
-
122
- When GitHub App auth is configured, Merge Steward mints short-lived installation tokens and uses them for both `gh` API calls and `git clone/fetch/push` over HTTPS. In multi-repo setups it resolves the installation per repository, so repos in different GitHub App installations can still coexist.
123
-
124
- Recommended GitHub App repository permissions:
125
-
126
- - `Contents: Read and write`
127
- - `Pull requests: Read and write`
128
- - `Checks: Read and write`
129
- - `Metadata: Read-only`
130
- - `Administration: Read-only`
131
-
132
- `Contents: Read and write` is the important merge-path permission because the
133
- steward lands tested speculative SHAs by fast-forward pushing `main`.
134
- `Administration: Read-only` is not required for merging itself, but it lets the
135
- doctor and attach/refresh flows discover branch rules and required checks
136
- without falling back to a local `gh` user token.
137
-
138
- The machine-level env files created by `merge-steward init` are:
139
-
140
- - `~/.config/merge-steward/runtime.env`
141
- - `~/.config/merge-steward/service.env`
142
-
143
- `runtime.env` is for non-secret runtime settings.
144
- `service.env` is for non-secret machine-level service config like `MERGE_STEWARD_GITHUB_APP_ID`.
145
- The CLI is a thin local client and does not need direct access to secret credentials.
146
-
147
- ### Repo Config
148
-
149
- `merge-steward attach` writes a repo-scoped config like:
150
-
151
- ```json
152
- {
153
- "repoId": "app",
154
- "repoFullName": "owner/repo",
155
- "baseBranch": "main",
156
- "clonePath": "~/.local/state/merge-steward/repos/app",
157
- "maxRetries": 2,
158
- "flakyRetries": 1,
159
- "pollIntervalMs": 30000,
160
- "admissionLabel": "queue",
161
- "mergeQueueCheckName": "merge-steward/queue",
162
- "server": {
163
- "bind": "127.0.0.1",
164
- "port": 8790
165
- },
166
- "database": {
167
- "path": "~/.local/state/merge-steward/app.sqlite"
168
- }
169
- }
170
- ```
171
-
172
- | Field | Description |
173
- |-|-|
174
- | `repoId` | Internal ID for this repo (used in DB keys) |
175
- | `repoFullName` | GitHub `owner/repo` |
176
- | `baseBranch` | Target branch for merges (usually `main`) |
177
- | `clonePath` | Local clone directory (created on first run) |
178
- | `maxRetries` | Rebase/CI retry attempts before eviction |
179
- | `flakyRetries` | CI-only retries before counting toward maxRetries |
180
- | `pollIntervalMs` | Reconciliation loop interval |
181
- | `admissionLabel` | Optional GitHub label used as a manual/operator admission nudge |
182
- | `mergeQueueCheckName` | GitHub check run name emitted on eviction |
183
-
184
- `attach` discovers these values from GitHub when possible:
185
-
186
- - `repoId` defaults to the repo name portion of `owner/repo`
187
- - `baseBranch` defaults to the GitHub default branch
188
-
189
- Pass `--refresh` to re-discover the base branch for an existing repo config. `merge-steward doctor --repo <id>` reports the GitHub-required checks currently enforced for that branch.
190
-
191
- ### GitHub Webhook
192
-
193
- Configure one webhook on the repository pointing to the steward:
194
-
195
- - **Payload URL:** `https://queue.example.com/webhooks/github`
196
- - **Content type:** `application/json`
197
- - **Secret:** same as `MERGE_STEWARD_WEBHOOK_SECRET` or the `merge-steward-webhook-secret` systemd credential
198
- - **Events:** Pull requests, Pull request reviews, Check suites, Pushes, Branch protection rules, Repository rulesets
199
-
200
- The steward uses a single multi-repo webhook endpoint and routes events by `repository.full_name`.
201
-
202
- It can wake up on:
203
-
204
- - PR label changes
205
- - review approvals
206
- - successful check-suite completion
207
- - pushes to the base branch
208
- - branch protection edits
209
- - repository ruleset edits
210
-
211
- On startup, the steward reconciles GitHub protection for every attached repo. Policy changes are normally learned from GitHub policy webhooks. If a merge is rejected unexpectedly, the steward performs a guarded one-shot policy refresh to recover from a missed webhook without polling GitHub continuously.
212
-
213
- The dashboard project view and `merge-steward queue status --repo <id>` also show the live GitHub-required checks and the last policy refresh, so an operator can tell whether a queue pause came from a policy change or from the branch state itself.
214
-
215
- Current policy decision: Merge Steward uses the same GitHub-required check names for both PR/spec admission and current-`main` drift detection. For example, if branch protection requires `Tests`, the steward treats `Tests` as both the merge gate for candidate commits and the signal that current `main` is still in-policy. This is an explicit simplification for now and may be split into separate merge-gate and main-health signals later.
216
-
217
- ### Running
218
-
219
- ```bash
220
- # Happy path
221
- merge-steward init https://queue.example.com
222
- merge-steward attach owner/repo
223
- merge-steward doctor --repo repo
224
- merge-steward service status
225
- merge-steward queue status --repo repo
226
- merge-steward queue show --repo repo --pr 123
227
- merge-steward dashboard
228
-
229
- # Manual foreground start
230
- merge-steward serve
231
-
232
- # Open one project directly in the dashboard
233
- merge-steward dashboard --repo app
234
- ```
235
-
236
- ### Dashboard
237
-
238
- `merge-steward dashboard` is the operator surface for day-to-day queue work.
239
-
240
- The first screen shows all configured projects with:
241
-
242
- - project-level queue health
243
- - readable queue stats
244
- - a compact queue chain like `#123 ● #124 ○`
245
- - clear bad states such as blocked, stuck, or needs attention
246
- - explicit startup states such as `Initializing` and `Init failed` for repo-local boot problems
247
-
248
- Press `Enter` on a project to open the second screen. That project detail view shows:
249
-
250
- - the same top-level queue stats for that project
251
- - a readable list of PRs in the queue
252
- - recent queue activity in plain language
253
- - incidents for evicted PRs
254
- - direct actions like reconcile and dequeue
255
- - live GitHub-required checks plus the last policy refresh
256
-
257
- Use `merge-steward dashboard --repo <id>` to open the project detail screen directly. Use `--pr <number>` to preselect a PR when you already know what you need to inspect.
258
-
259
- Controls:
260
-
261
- - `j` / `k` or arrows — move selection
262
- - `Enter` — open the selected project from overview
263
- - `Esc` — return to the overview
264
- - `a` — toggle `active` vs `all` in project view
265
- - `r` — run a reconcile tick for the selected project
266
- - `d` — dequeue the selected PR in project view
267
- - `q` — quit
268
-
269
- ### Validation, Visibility, And Troubleshooting
270
-
271
- The gateway binds its HTTP port before repo initialization finishes. Each repo then initializes independently in the background, so a bad clone or GitHub discovery problem stays local to that repo instead of taking down the whole dashboard.
272
-
273
- These are the first commands to reach for after setup or when a queue looks wrong:
274
-
275
- ```bash
276
- merge-steward doctor --repo app
277
- merge-steward service status
278
- merge-steward service restart
279
- merge-steward dashboard
280
- merge-steward pr status # from a git checkout; resolves repo + PR automatically
281
- merge-steward queue status --repo app
282
- merge-steward queue show --repo app --pr 123
54
+ merge-steward dashboard # operator UI across all projects
55
+ merge-steward pr status # one-PR verdict (inside a git checkout)
56
+ merge-steward queue status --repo <id> # quick text snapshot
57
+ merge-steward queue show --pr <num> # one PR's queue events and incidents
58
+ merge-steward queue reconcile --repo <id> # force one reconcile tick
283
59
  merge-steward service logs --lines 100
284
60
  ```
285
61
 
286
- Use them this way:
62
+ `pr status`, `queue status`, `queue show`, and `queue reconcile` auto-resolve `--repo` and `--pr` from the current git checkout. `pr status` supports `--wait --timeout <s> --poll <s>` for blocking until a terminal state. Exit codes:
287
63
 
288
- - `doctor` checks config, GitHub auth, branch rules, and required checks.
289
- - `dashboard` is the best live operator view across all configured projects.
290
- - `pr status` gives a single agent-friendly verdict on one PR (queue entry when it exists, GitHub state otherwise) with a stable exit code so scripts can chain with `&&`. Supports `--wait` to poll until a terminal state is reached.
291
- - `queue status` is the fastest text snapshot when you need one repo in a shell script or over SSH.
292
- - `queue show --pr <number>` is the most direct way to inspect one PR's queue events and incidents.
293
- - `service logs` helps when the queue is not reacting to webhooks, GitHub auth is failing, or reconcile ticks are erroring.
294
-
295
- ### Resolving --repo and --pr from the current checkout
296
-
297
- `pr status`, `queue status`, `queue show`, and `queue reconcile` accept `--repo` and `--pr` but you can omit them when running from inside a git checkout. `merge-steward` reads `origin`'s remote URL, matches it to an attached repoId, and uses `gh pr view` to find the PR for the current branch. Pass `--cwd <path>` to resolve from a different directory.
298
-
299
- ### Exit codes (pr status)
300
-
301
- | code | meaning |
64
+ | Code | Meaning |
302
65
  |-|-|
303
66
  | 0 | merged / approved with green required checks |
304
67
  | 2 | changes_requested / failing required checks / evicted / closed |
305
68
  | 3 | still in flight (queued, preparing, validating, merging, pending) |
306
- | 4 | `--wait` timed out before a terminal state was reached |
69
+ | 4 | `--wait` timed out |
307
70
  | 1 | usage or configuration error |
308
71
 
309
- ### systemd
310
-
311
- ```ini
312
- [Unit]
313
- Description=merge-steward
314
- After=network-online.target
315
- Wants=network-online.target
316
-
317
- [Service]
318
- Type=simple
319
- EnvironmentFile=-/home/your-user/.config/merge-steward/runtime.env
320
- EnvironmentFile=-/home/your-user/.config/merge-steward/service.env
321
- LoadCredentialEncrypted=merge-steward-webhook-secret:/etc/credstore.encrypted/merge-steward-webhook-secret.cred
322
- LoadCredentialEncrypted=merge-steward-github-app-pem:/etc/credstore.encrypted/merge-steward-github-app-pem.cred
323
- ExecStart=/usr/bin/env merge-steward serve
324
- Restart=on-failure
325
- RestartSec=5s
72
+ ## Merge gate
326
73
 
327
- [Install]
328
- WantedBy=multi-user.target
329
- ```
330
-
331
- ## API
332
-
333
- | Endpoint | Method | Description |
334
- |-|-|-|
335
- | `/health` | GET | Liveness check |
336
- | `/repos/:repoId/queue/status` | GET | All queue entries for one configured repo |
337
- | `/repos/:repoId/queue/watch` | GET | Queue snapshot used by the dashboard |
338
- | `/repos/:repoId/queue/enqueue` | POST | Manually enqueue a PR |
339
- | `/repos/:repoId/queue/reconcile` | POST | Trigger one reconcile tick immediately |
340
- | `/repos/:repoId/queue/entries/:id/detail` | GET | Entry detail with recent events and incidents |
341
- | `/repos/:repoId/queue/entries/:id/dequeue` | POST | Remove from queue (non-destructive) |
342
- | `/repos/:repoId/queue/entries/:id/update-head` | POST | Update head SHA (force-push) |
343
- | `/repos/:repoId/queue/incidents/:id` | GET | Get incident details |
344
- | `/repos/:repoId/queue/entries/:id/incidents` | GET | List incidents for an entry |
345
- | `/webhooks/github` | POST | GitHub webhook receiver for all configured repos |
346
-
347
- ## Queue state machine
348
-
349
- ```
350
- queued → preparing_head → validating → merging → merged
351
- → evicted (on failure after retries)
352
- ```
353
-
354
- - **queued**: waiting in line
355
- - **preparing_head**: fetching + rebasing onto base branch
356
- - **validating**: CI running
357
- - **merging**: revalidation + merge
358
- - **merged**: done
359
- - **evicted**: failed after retry budget, incident created
360
- - **dequeued**: manually removed
361
-
362
- ## Merge Gate
363
-
364
- For the steward path, the real gate is:
74
+ The real gate is:
365
75
 
366
76
  - GitHub says the PR review state is approved
367
- - the configured required checks are green
77
+ - configured required checks are green
368
78
  - the steward's speculative integrated branch also passes CI
369
79
 
370
- `review-quill/verdict` only matters if you choose to include it in the repo's
371
- required checks.
372
-
373
- GitHub branch protection is still useful as defense in depth, but steward does
374
- not merge by pressing GitHub's merge button. It fast-forwards `main` to the
375
- already-tested speculative SHA, so successful queue merges also depend on the
376
- steward App being allowed to push that result to the protected branch.
80
+ `review-quill/verdict` only matters if you include it in the repo's required checks. Branch protection is useful as defense in depth, but the steward merges by fast-forwarding `main` to the already-tested speculative SHA — not by pressing GitHub's merge button. Successful merges therefore depend on the steward App being allowed to push to the protected branch. See [docs/merge-steward.md](../../docs/merge-steward.md) for the full App permission set.
377
81
 
378
82
  ## Interaction with PatchRelay
379
83
 
380
- The steward and PatchRelay are independent services that communicate through GitHub:
381
-
382
- - PatchRelay adds the `queue` label when an issue reaches `awaiting_queue`
383
- - The steward merges the PR or evicts it (creating the configured queue eviction check run, default `merge-steward/queue`)
384
- - PatchRelay watches for that check run failure and triggers `queue_repair`
385
- - After repair, PatchRelay re-adds the `queue` label
386
- - The steward re-admits the PR
84
+ Independent services, GitHub as the shared bus:
387
85
 
388
- Neither service calls the other's API. GitHub is the shared bus.
86
+ 1. PatchRelay adds the `queue` label when an issue reaches `awaiting_queue`.
87
+ 2. The steward merges the PR, or evicts and creates the eviction check run (default `merge-steward/queue`).
88
+ 3. PatchRelay watches for that check run failure and triggers `queue_repair`.
89
+ 4. After repair, PatchRelay re-adds the `queue` label; the steward re-admits.
389
90
 
390
- ## Current scope
91
+ Neither service calls the other's API.
391
92
 
392
- What's implemented:
393
- - **Speculative execution**: cumulative branches (`main+A`, `main+A+B`, `main+A+B+C`) tested in parallel. Configurable depth (default 10, set `speculativeDepth: 1` for serial mode).
394
- - **Speculative consistency**: when head merges, downstream entries that already passed don't re-test.
395
- - **Cascade invalidation**: when mid-chain entry fails, downstream speculative branches are rebuilt without it.
396
- - Non-spinning conflict retry: gated on base SHA change
397
- - Flaky CI retry budget (separate from retry budget)
398
- - Revalidation before merge (approval, SHA, external merge)
399
- - Durable incident records on eviction
400
- - GitHub check run as eviction signal
401
- - Label-based admission and re-admission
402
- - Structured reconciler event stream for observability
93
+ ## Reference
403
94
 
404
- What's not built yet (see [design doc](https://github.com/krasnoperov/patchrelay/blob/main/docs/design-docs/merge-steward.md)):
405
- - Binary bisection on batch failure
406
- - File-path conflict detection for parallel lanes
407
- - Flaky test learning (only retry budget, no historical analysis)
408
- - Priority reordering after enqueue
95
+ - [docs/merge-steward.md](../../docs/merge-steward.md) — operator reference: GitHub App permissions, secrets, webhook, repo config, full CLI, HTTP API, queue state machine, systemd, troubleshooting
96
+ - [docs/merge-queue.md](../../docs/merge-queue.md) — the two-service delivery story
97
+ - [docs/github-queue-contract.md](../../docs/github-queue-contract.md) — shared GitHub artifacts
98
+ - [docs/design-docs/merge-steward.md](../../docs/design-docs/merge-steward.md) — design rationale
99
+ - [../../README.md](../../README.md) — the three-service stack overview
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "merge-steward",
3
- "version": "0.19.1",
3
+ "version": "0.19.2",
4
4
  "description": "Serial merge queue for GitHub — rebase, CI-gate, and merge PRs one at a time",
5
5
  "type": "module",
6
6
  "repository": {