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.
- package/dist/build-info.json +3 -3
- package/dist/cli/cluster-health.js +65 -0
- package/dist/cli/watch/IssueRow.js +4 -0
- package/dist/cli/watch/detail-rows.js +4 -0
- package/dist/db.js +3 -0
- package/dist/issue-session-events.js +4 -3
- package/dist/linear-session-reporting.js +3 -2
- package/dist/presentation-text.js +12 -0
- package/dist/service.js +2 -0
- package/dist/status-note.js +2 -2
- package/dist/waiting-reason.js +8 -0
- package/package.json +1 -1
package/dist/build-info.json
CHANGED
|
@@ -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
|
|
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) {
|
package/dist/status-note.js
CHANGED
|
@@ -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
|
-
|
|
4
|
-
return trimmed ? trimmed : undefined;
|
|
4
|
+
return sanitizeOperatorFacingText(value);
|
|
5
5
|
}
|
|
6
6
|
function eventStatusNote(event) {
|
|
7
7
|
if (!event)
|
package/dist/waiting-reason.js
CHANGED
|
@@ -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) {
|