patchrelay 0.54.3 → 0.54.4

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 CHANGED
@@ -8,9 +8,9 @@ This repository ships **three independent services**. Install one, two, or all t
8
8
 
9
9
  | Service | Package | Role |
10
10
  |-|-|-|
11
- | [`patchrelay`](./) | `npm install -g patchrelay` | Linear-driven harness that runs Codex sessions inside your real repos. Fully autonomous on webhooks: implementation, review fix, CI repair, queue repair. |
12
- | [`review-quill`](./packages/review-quill) | `npm install -g review-quill` | GitHub PR review bot. Reviews every merge-ready head from a real local checkout and posts a normal `APPROVE` / `REQUEST_CHANGES` review. |
13
- | [`merge-steward`](./packages/merge-steward) | `npm install -g merge-steward` | Serial merge queue. Speculatively integrates approved PRs on top of the latest `main`, runs CI on the integrated SHA, and fast-forwards `main` only when that tested result is green. |
11
+ | [`patchrelay`](./) | `pnpm add -g patchrelay` | Linear-driven harness that runs Codex sessions inside your real repos. Fully autonomous on webhooks: implementation, review fix, CI repair, queue repair. |
12
+ | [`review-quill`](./packages/review-quill) | `pnpm add -g review-quill` | GitHub PR review bot. Reviews every merge-ready head from a real local checkout and posts a normal `APPROVE` / `REQUEST_CHANGES` review. |
13
+ | [`merge-steward`](./packages/merge-steward) | `pnpm add -g merge-steward` | Serial merge queue. Speculatively integrates approved PRs on top of the latest `main`, runs CI on the integrated SHA, and fast-forwards `main` only when that tested result is green. |
14
14
 
15
15
  Common setups:
16
16
 
@@ -22,7 +22,7 @@ Common setups:
22
22
 
23
23
  - **PRs ship tested against the latest `main`.** The queue re-validates on the integrated SHA at admission time, and retries if `main` moves during validation. No more "green yesterday, broken today."
24
24
  - **Many PR failures have mechanical fixes an agent can handle.** Requested changes like a rename, a missing null check, a new test, refreshing against `main`, resolving a conflict surfaced by speculation, or rerunning a flaky job. Both services publish structured failure reasons (inline review comments, failing check names, queue incidents) an agent can act on directly.
25
- - **No prerequisites beyond GitHub.** A GitHub App, a webhook, and `npm install -g` per service.
25
+ - **No prerequisites beyond GitHub.** A GitHub App, a webhook, and `pnpm add -g` per service.
26
26
 
27
27
  ## Use with your own agent
28
28
 
@@ -45,7 +45,7 @@ Prerequisites:
45
45
  - a public HTTPS entrypoint (Caddy, nginx, tunnel) so Linear and GitHub can reach your webhooks
46
46
 
47
47
  ```bash
48
- npm install -g patchrelay
48
+ pnpm add -g patchrelay
49
49
  patchrelay init https://patchrelay.example.com
50
50
  ```
51
51
 
@@ -119,7 +119,7 @@ See the [merge-steward package README](./packages/merge-steward/README.md) for t
119
119
  - [Prompting](./docs/prompting.md) — how workflow files and the built-in scaffold compose
120
120
  - [Secrets](./docs/secrets.md) — systemd credentials, resolution order
121
121
  - [review-quill reference](./docs/review-quill.md) · [merge-steward reference](./docs/merge-steward.md)
122
- - [Product brief](./docs/product-specs/patchrelay.md) · [Dashboard guidance](./docs/dashboard-guidance.md) · [Design docs](./docs/design-docs/index.md)
122
+ - [Dashboard guidance](./docs/dashboard-guidance.md) · [Design docs](./docs/design-docs/index.md)
123
123
  - [Contributing](./CONTRIBUTING.md) · [Security policy](./SECURITY.md)
124
124
 
125
125
  ## Status
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "service": "patchrelay",
3
- "version": "0.54.3",
4
- "commit": "83c6b1f9e9e3",
5
- "builtAt": "2026-04-30T08:20:16.706Z"
3
+ "version": "0.54.4",
4
+ "commit": "fa7ff16eaeff",
5
+ "builtAt": "2026-04-30T14:26:42.396Z"
6
6
  }
@@ -4,25 +4,16 @@ import { Box, Text, useStdout } from "ink";
4
4
  import { IssueRow } from "./IssueRow.js";
5
5
  import { StatusBar } from "./StatusBar.js";
6
6
  import { HelpBar } from "./HelpBar.js";
7
- const CHROME_ROWS = 3;
7
+ import { computeIssueListLayout, computeVisibleIssueParts, computeVisibleWindowForTotal } from "./list-layout.js";
8
8
  export function computeVisibleWindow(issues, selectedIndex, maxRows) {
9
- if (issues.length === 0)
10
- return { start: 0, end: 0 };
11
- const clamped = Math.max(0, Math.min(selectedIndex, issues.length - 1));
12
- const half = Math.floor(maxRows / 2);
13
- let start = Math.max(0, clamped - half);
14
- let end = Math.min(issues.length, start + maxRows);
15
- if (end - start < maxRows) {
16
- start = Math.max(0, end - maxRows);
17
- }
18
- return { start, end };
9
+ return computeVisibleWindowForTotal(issues.length, selectedIndex, maxRows);
19
10
  }
20
11
  export function IssueListView({ issues, selectedIndex, connected, lastServerMessageAt, filter, frozen, compact = false, }) {
21
12
  const { stdout } = useStdout();
22
13
  const cols = stdout?.columns ?? 80;
23
- const rows = stdout?.rows ?? 24;
14
+ const rows = Math.max(1, stdout?.rows ?? 24);
24
15
  const titleWidth = Math.max(0, cols - 42);
25
- const maxVisibleRows = Math.max(1, rows - CHROME_ROWS);
16
+ const layout = computeIssueListLayout(rows);
26
17
  const [, tick] = useReducer((c) => c + 1, 0);
27
18
  useEffect(() => {
28
19
  if (frozen)
@@ -30,9 +21,9 @@ export function IssueListView({ issues, selectedIndex, connected, lastServerMess
30
21
  const id = setInterval(tick, 5000);
31
22
  return () => clearInterval(id);
32
23
  }, [frozen]);
33
- const { start: startIndex, end: endIndex } = computeVisibleWindow(issues, selectedIndex, maxVisibleRows);
24
+ const { start: startIndex, end: endIndex, showAbove, showBelow, } = computeVisibleIssueParts(issues.length, selectedIndex, layout.bodyRows);
34
25
  const visible = issues.slice(startIndex, endIndex);
35
26
  const hiddenAbove = startIndex;
36
27
  const hiddenBelow = Math.max(0, issues.length - endIndex);
37
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(StatusBar, { filter: filter, connected: connected, lastServerMessageAt: lastServerMessageAt, frozen: frozen ?? false }), _jsx(Box, { marginTop: 1, flexDirection: "column", children: issues.length === 0 ? (_jsx(Text, { dimColor: true, children: " " })) : (_jsxs(_Fragment, { children: [hiddenAbove > 0 ? _jsx(Text, { dimColor: true, children: ` ↑${hiddenAbove}` }) : null, visible.map((issue, i) => (_jsx(IssueRow, { issue: issue, selected: startIndex + i === selectedIndex, titleWidth: titleWidth, compact: compact }, issue.issueKey ?? `${issue.projectId}-${startIndex + i}`))), hiddenBelow > 0 ? _jsx(Text, { dimColor: true, children: ` ↓${hiddenBelow}` }) : null] })) }), _jsx(Box, { marginTop: 1, children: _jsx(HelpBar, { view: "list" }) })] }));
28
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(StatusBar, { filter: filter, connected: connected, lastServerMessageAt: lastServerMessageAt, frozen: frozen ?? false }), _jsx(Box, { marginTop: layout.showBodyGap ? 1 : 0, flexDirection: "column", children: issues.length === 0 ? (_jsx(Text, { dimColor: true, children: " " })) : (_jsxs(_Fragment, { children: [showAbove ? _jsx(Text, { dimColor: true, children: ` ↑${hiddenAbove}` }) : null, visible.map((issue, i) => (_jsx(IssueRow, { issue: issue, selected: startIndex + i === selectedIndex, titleWidth: titleWidth, compact: compact }, issue.issueKey ?? `${issue.projectId}-${startIndex + i}`))), showBelow ? _jsx(Text, { dimColor: true, children: ` ↓${hiddenBelow}` }) : null] })) }), layout.showHelp ? (_jsx(Box, { marginTop: 1, children: _jsx(HelpBar, { view: "list" }) })) : null] }));
38
29
  }
@@ -0,0 +1,44 @@
1
+ export function computeIssueListLayout(totalRows) {
2
+ const rows = Math.max(1, totalRows);
3
+ const showBodyGap = rows >= 5;
4
+ const showHelp = rows >= 8;
5
+ const chromeRows = 1 + (showBodyGap ? 1 : 0) + (showHelp ? 2 : 0);
6
+ return {
7
+ bodyRows: Math.max(1, rows - chromeRows),
8
+ showBodyGap,
9
+ showHelp,
10
+ };
11
+ }
12
+ export function computeVisibleWindowForTotal(total, selectedIndex, maxRows) {
13
+ if (total === 0)
14
+ return { start: 0, end: 0 };
15
+ const clamped = Math.max(0, Math.min(selectedIndex, total - 1));
16
+ const half = Math.floor(maxRows / 2);
17
+ let start = Math.max(0, clamped - half);
18
+ let end = Math.min(total, start + maxRows);
19
+ if (end - start < maxRows) {
20
+ start = Math.max(0, end - maxRows);
21
+ }
22
+ return { start, end };
23
+ }
24
+ export function computeVisibleIssueParts(total, selectedIndex, rowBudget) {
25
+ if (total === 0 || rowBudget <= 0) {
26
+ return { start: 0, end: 0, showAbove: false, showBelow: false };
27
+ }
28
+ let { start, end } = computeVisibleWindowForTotal(total, selectedIndex, Math.max(1, rowBudget));
29
+ let hiddenAbove = start > 0;
30
+ let hiddenBelow = end < total;
31
+ if (rowBudget >= 3 && (hiddenAbove || hiddenBelow)) {
32
+ const indicatorRows = (hiddenAbove ? 1 : 0) + (hiddenBelow ? 1 : 0);
33
+ ({ start, end } = computeVisibleWindowForTotal(total, selectedIndex, Math.max(1, rowBudget - indicatorRows)));
34
+ hiddenAbove = start > 0;
35
+ hiddenBelow = end < total;
36
+ }
37
+ const usedRows = end - start;
38
+ let remaining = Math.max(0, rowBudget - usedRows);
39
+ const showAbove = hiddenAbove && remaining > 0;
40
+ if (showAbove)
41
+ remaining -= 1;
42
+ const showBelow = hiddenBelow && remaining > 0;
43
+ return { start, end, showAbove, showBelow };
44
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "patchrelay",
3
- "version": "0.54.3",
3
+ "version": "0.54.4",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "repository": {