patchrelay 0.36.0 → 0.36.1

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.36.0",
4
- "commit": "75e78336dd48",
5
- "builtAt": "2026-04-08T22:47:52.865Z"
3
+ "version": "0.36.1",
4
+ "commit": "ceada430ca0b",
5
+ "builtAt": "2026-04-08T23:27:56.372Z"
6
6
  }
@@ -96,6 +96,7 @@ export async function collectClusterHealth(config, db, runCommand) {
96
96
  });
97
97
  }
98
98
  }
99
+ checks.push(...await collectActiveOverlapFindings(snapshots, runCommand));
99
100
  for (const snapshot of snapshots) {
100
101
  if (!snapshot.issue.prNumber) {
101
102
  continue;
@@ -523,6 +524,70 @@ async function collectReviewQuillAttemptOwners(snapshots, config, runCommand) {
523
524
  }
524
525
  return owners;
525
526
  }
527
+ async function collectActiveOverlapFindings(snapshots, runCommand) {
528
+ const findings = [];
529
+ const diffsByProject = new Map();
530
+ for (const snapshot of snapshots) {
531
+ const { issue } = snapshot;
532
+ if (issue.activeRunId === undefined || !issue.worktreePath) {
533
+ continue;
534
+ }
535
+ const files = await listModifiedTrackedFiles(runCommand, issue.worktreePath);
536
+ if (files.size === 0) {
537
+ continue;
538
+ }
539
+ const projectDiffs = diffsByProject.get(issue.projectId) ?? [];
540
+ projectDiffs.push({ issue, files });
541
+ diffsByProject.set(issue.projectId, projectDiffs);
542
+ }
543
+ for (const [projectId, diffs] of diffsByProject) {
544
+ for (let leftIndex = 0; leftIndex < diffs.length; leftIndex += 1) {
545
+ const left = diffs[leftIndex];
546
+ for (let rightIndex = leftIndex + 1; rightIndex < diffs.length; rightIndex += 1) {
547
+ const right = diffs[rightIndex];
548
+ const overlap = [...left.files].filter((file) => right.files.has(file)).sort();
549
+ if (overlap.length === 0) {
550
+ continue;
551
+ }
552
+ findings.push({
553
+ status: "warn",
554
+ scope: "issue:overlap",
555
+ message: `Active work overlaps with ${right.issue.issueKey ?? right.issue.linearIssueId}: ${overlap.slice(0, 3).join(", ")}${overlap.length > 3 ? " ..." : ""}`,
556
+ ...(left.issue.issueKey ? { issueKey: left.issue.issueKey } : {}),
557
+ projectId,
558
+ });
559
+ }
560
+ }
561
+ }
562
+ return findings;
563
+ }
564
+ async function listModifiedTrackedFiles(runCommand, worktreePath) {
565
+ let result;
566
+ try {
567
+ result = await runCommand("git", ["-C", worktreePath, "status", "--porcelain", "--untracked-files=no"]);
568
+ }
569
+ catch {
570
+ return new Set();
571
+ }
572
+ if (result.exitCode !== 0) {
573
+ return new Set();
574
+ }
575
+ const files = new Set();
576
+ for (const line of result.stdout.split("\n")) {
577
+ if (line.trim().length === 0)
578
+ continue;
579
+ const rawPath = line.slice(3).trim();
580
+ if (!rawPath)
581
+ continue;
582
+ const normalized = rawPath.includes(" -> ")
583
+ ? rawPath.split(" -> ").at(-1)?.trim()
584
+ : rawPath;
585
+ if (normalized) {
586
+ files.add(normalized);
587
+ }
588
+ }
589
+ return files;
590
+ }
526
591
  function getGateCheckNames(project) {
527
592
  const configured = project?.gateChecks?.map((entry) => entry.trim()).filter(Boolean) ?? [];
528
593
  return configured.length > 0 ? configured : ["verify"];
@@ -110,6 +110,10 @@ function blockerText(issue) {
110
110
  return issue.waitingReason ?? "Waiting for input";
111
111
  if (needsOperatorIntervention(issue))
112
112
  return issue.statusNote ?? issue.waitingReason ?? "Needs operator intervention";
113
+ if (issue.waitingReason && issue.activeRunType && issue.factoryState === "pr_open")
114
+ return issue.waitingReason;
115
+ if (issue.waitingReason && issue.activeRunType && issue.factoryState === "awaiting_queue")
116
+ return issue.waitingReason;
113
117
  if (issue.waitingReason && !issue.activeRunType)
114
118
  return issue.waitingReason;
115
119
  if (issue.blockedByCount > 0)
@@ -523,6 +523,10 @@ function blockerText(issue, issueContext) {
523
523
  if (issue.sessionState === "failed" || issue.factoryState === "failed" || issue.factoryState === "escalated") {
524
524
  return issue.statusNote ?? issue.waitingReason ?? "Needs operator intervention";
525
525
  }
526
+ if (issue.waitingReason && issue.activeRunType && issue.factoryState === "pr_open")
527
+ return issue.waitingReason;
528
+ if (issue.waitingReason && issue.activeRunType && issue.factoryState === "awaiting_queue")
529
+ return issue.waitingReason;
526
530
  if (issue.waitingReason && !issue.activeRunType)
527
531
  return issue.waitingReason;
528
532
  if (issue.blockedByCount > 0)
package/dist/db.js CHANGED
@@ -1189,6 +1189,9 @@ export class PatchRelayDatabase {
1189
1189
  return explicitSummaryText;
1190
1190
  }
1191
1191
  const latestSummary = extractLatestAssistantSummary(latestRun);
1192
+ if (latestRun && (latestRun.status === "queued" || latestRun.status === "running")) {
1193
+ return latestSummary;
1194
+ }
1192
1195
  if (this.shouldKeepPreviousIssueSummary(issue, latestRun)) {
1193
1196
  return this.findLatestCompletedRunSummary(issue.projectId, issue.linearIssueId)
1194
1197
  ?? existingSummaryText
@@ -1,3 +1,4 @@
1
+ import { sanitizeOperatorFacingText } from "./presentation-text.js";
1
2
  const TERMINAL_SESSION_EVENTS = new Set([
2
3
  "stop_requested",
3
4
  "undelegated",
@@ -116,7 +117,7 @@ export function extractLatestAssistantSummary(run) {
116
117
  try {
117
118
  const parsed = JSON.parse(run.summaryJson);
118
119
  if (typeof parsed.latestAssistantMessage === "string" && parsed.latestAssistantMessage.trim()) {
119
- return parsed.latestAssistantMessage;
120
+ return sanitizeOperatorFacingText(parsed.latestAssistantMessage);
120
121
  }
121
122
  }
122
123
  catch {
@@ -129,14 +130,14 @@ export function extractLatestAssistantSummary(run) {
129
130
  if (Array.isArray(parsed.assistantMessages)) {
130
131
  const latest = parsed.assistantMessages.findLast((value) => typeof value === "string" && value.trim());
131
132
  if (typeof latest === "string")
132
- return latest;
133
+ return sanitizeOperatorFacingText(latest);
133
134
  }
134
135
  }
135
136
  catch {
136
137
  // ignore malformed report json
137
138
  }
138
139
  }
139
- return run.failureReason;
140
+ return sanitizeOperatorFacingText(run.failureReason);
140
141
  }
141
142
  function parseEventJson(raw) {
142
143
  if (!raw)
@@ -1,9 +1,10 @@
1
1
  import { formatRunTypeLabel } from "./agent-session-plan.js";
2
+ import { sanitizeOperatorFacingText } from "./presentation-text.js";
2
3
  function lowerRunTypeLabel(runType) {
3
4
  return formatRunTypeLabel(runType).toLowerCase();
4
5
  }
5
6
  function trimSummary(summary, maxLength = 300) {
6
- const value = summary?.trim();
7
+ const value = sanitizeOperatorFacingText(summary);
7
8
  if (!value) {
8
9
  return undefined;
9
10
  }
@@ -153,7 +154,7 @@ export function summarizeIssueStateForLinear(issue) {
153
154
  case "waiting_input":
154
155
  return issue.waitingReason ?? (issue.prNumber ? `PR #${issue.prNumber} is waiting for input.` : "Waiting for input.");
155
156
  case "running":
156
- return issue.prNumber ? `PR #${issue.prNumber} is actively running.` : "Actively running.";
157
+ return issue.waitingReason ?? (issue.prNumber ? `PR #${issue.prNumber} is actively running.` : "Actively running.");
157
158
  case "idle":
158
159
  return issue.waitingReason ?? (issue.prNumber ? `PR #${issue.prNumber} is idle.` : "Idle.");
159
160
  case "done":
@@ -0,0 +1,12 @@
1
+ function unwrapShellWrappedCommand(text) {
2
+ return text
3
+ .replace(/`(?:\/bin\/bash|bash|\/bin\/sh|sh)\s+-lc\s+'([^`\n]+)'`/g, "`$1`")
4
+ .replace(/`(?:\/bin\/bash|bash|\/bin\/sh|sh)\s+-lc\s+"([^`\n]+)"`/g, "`$1`");
5
+ }
6
+ export function sanitizeOperatorFacingText(text) {
7
+ const trimmed = text?.trim();
8
+ if (!trimmed) {
9
+ return undefined;
10
+ }
11
+ return unwrapShellWrappedCommand(trimmed);
12
+ }
package/dist/service.js CHANGED
@@ -31,6 +31,8 @@ function shouldSuppressStatusNote(params) {
31
31
  if (!note)
32
32
  return true;
33
33
  return note === "codex turn was interrupted"
34
+ || note.startsWith("zombie: never started")
35
+ || note === "stale thread after restart"
34
36
  || note === "patchrelay received your mention. delegate the issue to patchrelay to start work.";
35
37
  }
36
38
  export function parseCiSnapshotSummary(snapshotJson) {
@@ -1,7 +1,7 @@
1
1
  import { extractLatestAssistantSummary } from "./issue-session-events.js";
2
+ import { sanitizeOperatorFacingText } from "./presentation-text.js";
2
3
  function clean(value) {
3
- const trimmed = value?.trim();
4
- return trimmed ? trimmed : undefined;
4
+ return sanitizeOperatorFacingText(value);
5
5
  }
6
6
  function eventStatusNote(event) {
7
7
  if (!event)
@@ -1,5 +1,7 @@
1
1
  export const PATCHRELAY_WAITING_REASONS = {
2
2
  activeWork: "PatchRelay is actively working",
3
+ finalizingPublishedPr: "PatchRelay is finalizing a published PR",
4
+ finalizingMergedChange: "PatchRelay is finalizing a merged change",
3
5
  waitingForOperatorInput: "Waiting on operator input",
4
6
  waitingForReviewFeedback: "Waiting to address review feedback",
5
7
  waitingForReviewOnNewHead: "Waiting on review of a newer pushed head",
@@ -12,6 +14,12 @@ export const PATCHRELAY_WAITING_REASONS = {
12
14
  };
13
15
  export function derivePatchRelayWaitingReason(params) {
14
16
  if (params.activeRunType) {
17
+ if (params.prNumber !== undefined && (params.factoryState === "pr_open" || params.factoryState === "awaiting_queue")) {
18
+ return PATCHRELAY_WAITING_REASONS.finalizingPublishedPr;
19
+ }
20
+ if (params.factoryState === "done") {
21
+ return PATCHRELAY_WAITING_REASONS.finalizingMergedChange;
22
+ }
15
23
  return `PatchRelay is running ${humanize(params.activeRunType)}`;
16
24
  }
17
25
  if (params.activeRunId !== undefined) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "patchrelay",
3
- "version": "0.36.0",
3
+ "version": "0.36.1",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "repository": {