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