patchrelay 0.50.4 → 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.4",
4
- "commit": "0063b47a1df7",
5
- "builtAt": "2026-04-21T12:01:24.460Z"
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})` : ""}`,
package/dist/config.js CHANGED
@@ -21,6 +21,8 @@ const DEFAULT_PATCHRELAY_DEVELOPER_INSTRUCTIONS = [
21
21
  "- If you change files for an implementation run, commit, push the issue branch, and open or update the PR.",
22
22
  "- For repair runs, work on the existing PR branch and do not open a new PR.",
23
23
  "- A requested-changes repair is only complete after a newer PR head is pushed, unless a genuine external blocker prevents correct publication.",
24
+ "- If you change schema, enums, shared vocabulary, normalization helpers, or compatibility mappings, inspect the main read/write paths that can bypass the new abstraction and fix or cover any mismatch before publishing.",
25
+ "- For CI repair, do not change code or config until you either reproduce the failure on the exact failing head or can point to a concrete log signature that justifies the fix. If you cannot reproduce it, prefer a rerun-only repair over speculative changes.",
24
26
  "- If a broader inconsistency is not required to make this task correct, mention it briefly instead of expanding scope.",
25
27
  "- Before publishing, do one brief reviewer-minded pass on the current head and fix likely in-scope blockers.",
26
28
  ].join("\n");
@@ -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) {
@@ -290,6 +303,8 @@ function buildCiRepairContext(context) {
290
303
  return [
291
304
  "Settled CI failure on the existing PR branch.",
292
305
  "Goal: restore CI readiness and push a branch that is likely to pass the next full CI run.",
306
+ "Before changing code or config, reproduce the failure on the exact failing head or identify the concrete log signature that justifies the fix.",
307
+ "If the exact failing head does not reproduce locally and the logs do not support a scoped fix, prefer a rerun-only repair over speculative branch changes.",
293
308
  snapshot?.gateCheckName ? `Gate check: ${String(snapshot.gateCheckName)}` : "",
294
309
  snapshot?.gateCheckStatus ? `Gate status: ${String(snapshot.gateCheckStatus)}` : "",
295
310
  snapshot?.settledAt ? `Settled at: ${String(snapshot.settledAt)}` : "",
@@ -352,6 +367,7 @@ function buildQueueRepairContext(context) {
352
367
  return lines.filter(Boolean).join("\n");
353
368
  }
354
369
  function buildFollowUpContextLines(issue, runType, context) {
370
+ const prContext = derivePrDisplayContext(issue);
355
371
  const wakeReason = typeof context?.wakeReason === "string" ? context.wakeReason : undefined;
356
372
  const followUps = Array.isArray(context?.followUps) ? context.followUps : [];
357
373
  const followUpLines = followUps
@@ -387,9 +403,25 @@ function buildFollowUpContextLines(issue, runType, context) {
387
403
  followUpLines.forEach((line) => lines.push(`- ${line}`));
388
404
  }
389
405
  if (issue.prNumber || issue.prHeadSha || issue.prReviewState || context?.mergeStateStatus) {
390
- 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
391
423
  ? "refreshed immediately before this turn was created."
392
- : "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)}` : "");
393
425
  }
394
426
  return lines.filter(Boolean);
395
427
  }
@@ -443,17 +475,21 @@ function buildOrchestrationWorkflowGuidance() {
443
475
  "Keep outputs concise and observable in Linear.",
444
476
  ].join("\n");
445
477
  }
446
- function buildPrePushSelfReviewSection(target) {
478
+ function buildPrePushSelfReviewSection(target, runType) {
447
479
  const publishTarget = target === "new_pr"
448
480
  ? "open or update the PR"
449
481
  : "push the existing PR branch";
450
- return [
482
+ const lines = [
451
483
  "## Final Self-Review Before Push",
452
484
  "",
453
485
  `Before you ${publishTarget}, do one brief reviewer-minded pass on the current head.`,
454
486
  "Fix any likely in-scope blocker you can see now: missing edge-case handling, broken adjacent invariant in the touched flow, mismatch between the PR explanation and the code, or an obviously unreviewable half-finished branch.",
455
- "Do not widen scope for optional cleanup. If the issue explicitly allows a non-PR outcome, complete that outcome clearly; otherwise publish before stopping.",
456
487
  ];
488
+ if (runType === "implementation") {
489
+ lines.push("Name 2-4 concrete invariants most likely to regress in the touched flow, confirm which file or path enforces each one, and verify at least one adjacent path you did not edit directly.", "If you changed schema, enums, shared vocabulary, normalization helpers, or compatibility mappings, inspect the main read/write paths that can bypass the new abstraction and verify one legacy-flow and one new-flow case before publishing.");
490
+ }
491
+ lines.push("Do not widen scope for optional cleanup. If the issue explicitly allows a non-PR outcome, complete that outcome clearly; otherwise publish before stopping.");
492
+ return lines;
457
493
  }
458
494
  function buildPublicationContract(runType, issueClass) {
459
495
  if (issueClass === "orchestration") {
@@ -471,7 +507,7 @@ function buildPublicationContract(runType, issueClass) {
471
507
  "If this is code-delivery work, publish before stopping: commit, push the issue branch, and open or update the PR.",
472
508
  "If the issue explicitly allows a non-PR outcome, complete that outcome clearly instead of inventing a PR.",
473
509
  "",
474
- ...buildPrePushSelfReviewSection("new_pr"),
510
+ ...buildPrePushSelfReviewSection("new_pr", runType),
475
511
  ].join("\n");
476
512
  }
477
513
  return [
@@ -481,7 +517,7 @@ function buildPublicationContract(runType, issueClass) {
481
517
  "Do not open a new PR.",
482
518
  "A PR-less stop is not a successful outcome for a repair run unless a genuine external blocker prevents any correct push.",
483
519
  "",
484
- ...buildPrePushSelfReviewSection("existing_pr"),
520
+ ...buildPrePushSelfReviewSection("existing_pr", runType),
485
521
  ].join("\n");
486
522
  }
487
523
  function buildSections(issue, runType, repoPath, context, followUp = false) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "patchrelay",
3
- "version": "0.50.4",
3
+ "version": "0.50.6",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "repository": {