patchrelay 0.50.5 → 0.50.6

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "service": "patchrelay",
3
- "version": "0.50.5",
4
- "commit": "7615071400bc",
5
- "builtAt": "2026-04-21T13:25:06.632Z"
3
+ "version": "0.50.6",
4
+ "commit": "93d2215a55bf",
5
+ "builtAt": "2026-04-21T15:53:17.328Z"
6
6
  }
@@ -1,4 +1,5 @@
1
1
  import { isUndelegatedPausedIssue } from "../../paused-issue-state.js";
2
+ import { derivePrDisplayContext } from "../../pr-display-context.js";
2
3
  import { hasFailedPrChecks, hasPendingPrChecks, isApprovedReviewState, isAwaitingReviewState, isChangesRequestedReviewState, prChecksFact, } from "./pr-status.js";
3
4
  const GLYPH = {
4
5
  running: "\u25cf",
@@ -89,9 +90,14 @@ export function prTokenFor(issue) {
89
90
  };
90
91
  }
91
92
  function prKind(issue) {
92
- if (issue.prState === "merged")
93
+ const prContext = derivePrDisplayContext(issue);
94
+ if (prContext.kind === "merged_pr")
93
95
  return "approved";
94
- if (issue.prState === "closed")
96
+ if (prContext.kind === "closed_replacement_pending")
97
+ return "queued";
98
+ if (prContext.kind === "closed_pr_paused")
99
+ return "attention";
100
+ if (prContext.kind === "closed_historical_pr")
95
101
  return "declined";
96
102
  if (issue.prReviewState === "approved")
97
103
  return "approved";
@@ -104,9 +110,14 @@ function prKind(issue) {
104
110
  return "running";
105
111
  }
106
112
  function prPhraseFor(issue) {
107
- if (issue.prState === "merged")
113
+ const prContext = derivePrDisplayContext(issue);
114
+ if (prContext.kind === "merged_pr")
108
115
  return "merged";
109
- if (issue.prState === "closed")
116
+ if (prContext.kind === "closed_replacement_pending")
117
+ return "replace pr";
118
+ if (prContext.kind === "closed_pr_paused")
119
+ return "redelegate";
120
+ if (prContext.kind === "closed_historical_pr")
110
121
  return "closed";
111
122
  if (isChangesRequestedReviewState(issue.prReviewState))
112
123
  return "changes req";
@@ -1,4 +1,5 @@
1
1
  import { hasOpenPr } from "../../pr-state.js";
2
+ import { derivePrDisplayContext } from "../../pr-display-context.js";
2
3
  const STATE_LABELS = {
3
4
  delegated: "delegated",
4
5
  implementing: "implementing",
@@ -164,9 +165,28 @@ export function buildPatchRelayQueueObservations(issue, feedEvents) {
164
165
  });
165
166
  }
166
167
  if (issue.prNumber !== undefined) {
167
- const prLabel = hasOpenPr(issue.prNumber, issue.prState)
168
- ? `Tracked PR: #${issue.prNumber}`
169
- : `Tracked PR: #${issue.prNumber}${issue.prState ? ` (${issue.prState})` : ""}`;
168
+ const prContext = derivePrDisplayContext(issue);
169
+ let prLabel;
170
+ switch (prContext.kind) {
171
+ case "active_pr":
172
+ case "merged_pr":
173
+ prLabel = hasOpenPr(issue.prNumber, issue.prState)
174
+ ? `Tracked PR: #${issue.prNumber}`
175
+ : `Tracked PR: #${issue.prNumber}${issue.prState ? ` (${issue.prState})` : ""}`;
176
+ break;
177
+ case "closed_historical_pr":
178
+ prLabel = `Previous PR: #${prContext.prNumber} (closed)`;
179
+ break;
180
+ case "closed_replacement_pending":
181
+ prLabel = `Previous PR: #${prContext.prNumber} (closed; replacement pending)`;
182
+ break;
183
+ case "closed_pr_paused":
184
+ prLabel = `Previous PR: #${prContext.prNumber} (closed; redelegate to replace)`;
185
+ break;
186
+ case "no_pr":
187
+ prLabel = "PR context unavailable";
188
+ break;
189
+ }
170
190
  observations.push({
171
191
  tone: "info",
172
192
  text: `${prLabel}${issue.prReviewState ? ` (${issue.prReviewState})` : ""}`,
@@ -45,8 +45,8 @@ export function deriveLinkedPrAdoptionOutcome(project, prNumber, remote) {
45
45
  }
46
46
  if (prState === "closed") {
47
47
  return {
48
- factoryState: "awaiting_input",
49
- pendingRunType: null,
48
+ factoryState: "delegated",
49
+ pendingRunType: "implementation",
50
50
  issueUpdates: {
51
51
  ...issueUpdates,
52
52
  prIsDraft: false,
@@ -1,6 +1,7 @@
1
1
  import { formatRunTypeLabel } from "./agent-session-plan.js";
2
2
  import { sanitizeOperatorFacingText } from "./presentation-text.js";
3
3
  import { isClosedPrState } from "./pr-state.js";
4
+ import { derivePrDisplayContext } from "./pr-display-context.js";
4
5
  function lowerRunTypeLabel(runType) {
5
6
  return formatRunTypeLabel(runType).toLowerCase();
6
7
  }
@@ -187,6 +188,7 @@ export function buildMergePrepEscalationActivity(attempts) {
187
188
  };
188
189
  }
189
190
  export function summarizeIssueStateForLinear(issue) {
191
+ const prContext = derivePrDisplayContext(issue);
190
192
  switch (issue.sessionState) {
191
193
  case "waiting_input":
192
194
  return issue.waitingReason ?? (issue.prNumber && !isClosedPrState(issue.prState) ? `PR #${issue.prNumber} is waiting for input.` : "Waiting for input.");
@@ -208,11 +210,23 @@ export function summarizeIssueStateForLinear(issue) {
208
210
  }
209
211
  switch (issue.factoryState) {
210
212
  case "delegated":
213
+ if (prContext.kind === "closed_replacement_pending") {
214
+ return `Queued to replace closed PR #${prContext.prNumber}.`;
215
+ }
216
+ if (prContext.kind === "closed_pr_paused") {
217
+ return `Closed PR #${prContext.prNumber} needs redelegation before replacement.`;
218
+ }
211
219
  if (!issue.delegatedToPatchRelay) {
212
220
  return "PatchRelay is queued to start work, but automation is paused.";
213
221
  }
214
222
  return "Queued to start work.";
215
223
  case "implementing":
224
+ if (prContext.kind === "closed_replacement_pending") {
225
+ return `Replacing closed PR #${prContext.prNumber} with a fresh PR.`;
226
+ }
227
+ if (prContext.kind === "closed_pr_paused") {
228
+ return `Closed PR #${prContext.prNumber} needs redelegation before replacement.`;
229
+ }
216
230
  if (!issue.delegatedToPatchRelay) {
217
231
  return "Implementation is paused because the issue is undelegated.";
218
232
  }
@@ -1,5 +1,6 @@
1
1
  import { extractCompletionCheck } from "./completion-check.js";
2
2
  import { isClosedPrState } from "./pr-state.js";
3
+ import { derivePrDisplayContext } from "./pr-display-context.js";
3
4
  import { deriveIssueStatusNote } from "./status-note.js";
4
5
  import { derivePatchRelayWaitingReason } from "./waiting-reason.js";
5
6
  export async function syncVisibleStatusComment(params) {
@@ -84,8 +85,17 @@ function renderStatusComment(db, issue, trackedIssue, options) {
84
85
  lines.push("", `Reply in a Linear comment to continue, or run \`patchrelay issue prompt ${issueRef} "..."\`.`);
85
86
  }
86
87
  if (issue.prNumber !== undefined || issue.prUrl) {
88
+ const prContext = derivePrDisplayContext(issue);
87
89
  const prLabel = issue.prNumber !== undefined ? `#${issue.prNumber}` : "open";
88
- lines.push("", `PR: ${issue.prUrl ? `[${prLabel}](${issue.prUrl})` : prLabel}`);
90
+ const linkedLabel = issue.prUrl ? `[${prLabel}](${issue.prUrl})` : prLabel;
91
+ const prLine = prContext.kind === "closed_historical_pr"
92
+ ? `Previous PR: ${linkedLabel} (closed)`
93
+ : prContext.kind === "closed_replacement_pending"
94
+ ? `Previous PR: ${linkedLabel} (closed; replacement PR needed)`
95
+ : prContext.kind === "closed_pr_paused"
96
+ ? `Previous PR: ${linkedLabel} (closed; redelegate to replace it)`
97
+ : `PR: ${linkedLabel}`;
98
+ lines.push("", prLine);
89
99
  }
90
100
  if (latestRun) {
91
101
  lines.push("", `Latest run: ${formatLatestRun(latestRun)}`);
@@ -103,6 +113,7 @@ function renderStatusComment(db, issue, trackedIssue, options) {
103
113
  return lines.join("\n");
104
114
  }
105
115
  function statusHeadline(issue, activeRunType) {
116
+ const prContext = derivePrDisplayContext(issue);
106
117
  if (activeRunType) {
107
118
  return `Running ${humanize(activeRunType)}`;
108
119
  }
@@ -123,6 +134,9 @@ function statusHeadline(issue, activeRunType) {
123
134
  break;
124
135
  }
125
136
  if (!issue.delegatedToPatchRelay && issue.prNumber !== undefined) {
137
+ if (prContext.kind === "closed_pr_paused") {
138
+ return `Closed PR #${prContext.prNumber} is waiting for redelegation before replacement`;
139
+ }
126
140
  if (issue.factoryState === "awaiting_queue" || issue.prReviewState === "approved") {
127
141
  return `PR #${issue.prNumber} is awaiting downstream merge while PatchRelay is paused`;
128
142
  }
@@ -144,8 +158,14 @@ function statusHeadline(issue, activeRunType) {
144
158
  }
145
159
  switch (issue.factoryState) {
146
160
  case "delegated":
161
+ if (prContext.kind === "closed_replacement_pending") {
162
+ return `Queued to replace closed PR #${prContext.prNumber}`;
163
+ }
147
164
  return "Queued to start work";
148
165
  case "implementing":
166
+ if (prContext.kind === "closed_replacement_pending") {
167
+ return `Replacing closed PR #${prContext.prNumber} with a fresh PR`;
168
+ }
149
169
  return "Implementing requested change";
150
170
  case "pr_open":
151
171
  return issue.prNumber !== undefined ? `PR #${issue.prNumber} opened` : "PR opened";
@@ -0,0 +1,21 @@
1
+ function isTerminalFactoryState(factoryState) {
2
+ return factoryState === "done" || factoryState === "failed" || factoryState === "escalated";
3
+ }
4
+ export function derivePrDisplayContext(issue) {
5
+ if (issue.prNumber === undefined) {
6
+ return { kind: "no_pr" };
7
+ }
8
+ if (issue.prState === "merged") {
9
+ return { kind: "merged_pr", prNumber: issue.prNumber };
10
+ }
11
+ if (issue.prState === "closed") {
12
+ if (isTerminalFactoryState(issue.factoryState)) {
13
+ return { kind: "closed_historical_pr", prNumber: issue.prNumber };
14
+ }
15
+ if (issue.delegatedToPatchRelay === false) {
16
+ return { kind: "closed_pr_paused", prNumber: issue.prNumber };
17
+ }
18
+ return { kind: "closed_replacement_pending", prNumber: issue.prNumber };
19
+ }
20
+ return { kind: "active_pr", prNumber: issue.prNumber };
21
+ }
@@ -1,5 +1,6 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import path from "node:path";
3
+ import { derivePrDisplayContext } from "../pr-display-context.js";
3
4
  const WORKFLOW_FILES = {
4
5
  implementation: "IMPLEMENTATION_WORKFLOW.md",
5
6
  review_fix: "REVIEW_WORKFLOW.md",
@@ -28,11 +29,23 @@ function hasWorkflowFile(repoPath, runType) {
28
29
  return existsSync(filePath);
29
30
  }
30
31
  function buildPromptHeader(issue) {
32
+ const prContext = derivePrDisplayContext(issue);
33
+ const prLine = prContext.kind === "active_pr"
34
+ ? `PR: #${prContext.prNumber}`
35
+ : prContext.kind === "merged_pr"
36
+ ? `Merged PR: #${prContext.prNumber}`
37
+ : prContext.kind === "closed_historical_pr"
38
+ ? `Previous PR: #${prContext.prNumber} (closed)`
39
+ : prContext.kind === "closed_replacement_pending"
40
+ ? `Previous PR: #${prContext.prNumber} (closed; replacement PR needed)`
41
+ : prContext.kind === "closed_pr_paused"
42
+ ? `Previous PR: #${prContext.prNumber} (closed; redelegate to replace it)`
43
+ : undefined;
31
44
  return [
32
45
  `Issue: ${issue.issueKey ?? issue.linearIssueId}`,
33
46
  issue.title ? `Title: ${issue.title}` : undefined,
34
47
  issue.branchName ? `Branch: ${issue.branchName}` : undefined,
35
- issue.prNumber ? `PR: #${issue.prNumber}` : undefined,
48
+ prLine,
36
49
  ].filter(Boolean).join("\n");
37
50
  }
38
51
  function extractIssueSection(description, heading) {
@@ -354,6 +367,7 @@ function buildQueueRepairContext(context) {
354
367
  return lines.filter(Boolean).join("\n");
355
368
  }
356
369
  function buildFollowUpContextLines(issue, runType, context) {
370
+ const prContext = derivePrDisplayContext(issue);
357
371
  const wakeReason = typeof context?.wakeReason === "string" ? context.wakeReason : undefined;
358
372
  const followUps = Array.isArray(context?.followUps) ? context.followUps : [];
359
373
  const followUpLines = followUps
@@ -389,9 +403,25 @@ function buildFollowUpContextLines(issue, runType, context) {
389
403
  followUpLines.forEach((line) => lines.push(`- ${line}`));
390
404
  }
391
405
  if (issue.prNumber || issue.prHeadSha || issue.prReviewState || context?.mergeStateStatus) {
392
- lines.push("", "Current PR facts:", `Fact freshness: ${context?.githubFactsFresh === true
406
+ const prHeading = prContext.kind === "closed_historical_pr"
407
+ || prContext.kind === "closed_replacement_pending"
408
+ || prContext.kind === "closed_pr_paused"
409
+ ? "Previous PR facts:"
410
+ : "Current PR facts:";
411
+ const prLine = prContext.kind === "active_pr"
412
+ ? `Current PR: #${prContext.prNumber}`
413
+ : prContext.kind === "merged_pr"
414
+ ? `Merged PR: #${prContext.prNumber}`
415
+ : prContext.kind === "closed_historical_pr"
416
+ ? `Previous PR: #${prContext.prNumber} (closed)`
417
+ : prContext.kind === "closed_replacement_pending"
418
+ ? `Previous PR: #${prContext.prNumber} (closed; replacement PR needed)`
419
+ : prContext.kind === "closed_pr_paused"
420
+ ? `Previous PR: #${prContext.prNumber} (closed; redelegate to replace it)`
421
+ : "";
422
+ lines.push("", prHeading, `Fact freshness: ${context?.githubFactsFresh === true
393
423
  ? "refreshed immediately before this turn was created."
394
- : "may now be stale; refresh before making irreversible decisions."}`, issue.prNumber ? `Current PR: #${issue.prNumber}` : "", issue.prHeadSha ? `Current relevant head SHA: ${issue.prHeadSha}` : "", issue.prReviewState ? `Current review state: ${issue.prReviewState}` : "", typeof context?.mergeStateStatus === "string" ? `Merge state against ${String(context?.baseBranch ?? "main")}: ${String(context.mergeStateStatus)}` : "");
424
+ : "may now be stale; refresh before making irreversible decisions."}`, prLine, issue.prHeadSha ? `Current relevant head SHA: ${issue.prHeadSha}` : "", issue.prReviewState ? `Current review state: ${issue.prReviewState}` : "", typeof context?.mergeStateStatus === "string" ? `Merge state against ${String(context?.baseBranch ?? "main")}: ${String(context.mergeStateStatus)}` : "");
395
425
  }
396
426
  return lines.filter(Boolean);
397
427
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "patchrelay",
3
- "version": "0.50.5",
3
+ "version": "0.50.6",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "repository": {