patchrelay 0.35.0 → 0.35.2
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
CHANGED
|
@@ -1,31 +1,35 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text } from "ink";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
3
|
+
const STATE_LABELS = {
|
|
4
|
+
delegated: "delegated",
|
|
5
|
+
implementing: "implementing",
|
|
6
|
+
pr_open: "PR open",
|
|
7
|
+
awaiting_queue: "merge queue",
|
|
8
|
+
done: "done",
|
|
9
|
+
changes_requested: "review fix",
|
|
10
|
+
repairing_ci: "CI repair",
|
|
11
|
+
repairing_queue: "queue repair",
|
|
12
|
+
awaiting_input: "needs input",
|
|
13
|
+
escalated: "escalated",
|
|
14
|
+
failed: "failed",
|
|
15
|
+
};
|
|
16
|
+
function displayLabel(state) {
|
|
17
|
+
return STATE_LABELS[state] ?? state;
|
|
12
18
|
}
|
|
13
|
-
function
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
return (_jsxs(Text, { color: statusColor(node.status), bold: node.status === "current", children: ["[", statusPrefix(node.status), " ", node.label, "]"] }));
|
|
25
|
-
}
|
|
26
|
-
function NodeRow({ label, nodes, connector = " -> ", }) {
|
|
27
|
-
return (_jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: label.padEnd(11, " ") }), nodes.map((node, index) => (_jsxs(Box, { children: [index > 0 && _jsx(Text, { dimColor: true, children: connector }), _jsx(NodePill, { node: node })] }, node.state)))] }));
|
|
19
|
+
function NodeRow({ nodes, connector }) {
|
|
20
|
+
const visible = nodes.filter((n) => connector === " \u2192 " || n.status !== "upcoming");
|
|
21
|
+
if (visible.length === 0)
|
|
22
|
+
return _jsx(_Fragment, {});
|
|
23
|
+
return (_jsx(Box, { gap: 0, children: visible.map((node, i) => {
|
|
24
|
+
const dot = node.status === "upcoming" ? "\u25cb" : "\u25cf";
|
|
25
|
+
const color = node.status === "current" ? "cyan"
|
|
26
|
+
: node.status === "visited" ? "green"
|
|
27
|
+
: "gray";
|
|
28
|
+
return (_jsxs(Box, { gap: 0, children: [i > 0 && _jsx(Text, { dimColor: true, children: connector }), _jsx(Text, { color: color, bold: node.status === "current", children: dot }), _jsx(Text, { color: color, bold: node.status === "current", children: ` ${displayLabel(node.state)}` })] }, node.state));
|
|
29
|
+
}) }));
|
|
28
30
|
}
|
|
29
31
|
export function FactoryStateGraph({ main, prLoops, queueLoop, exits, }) {
|
|
30
|
-
|
|
32
|
+
const hasLoops = prLoops.some((n) => n.status !== "upcoming") || queueLoop.some((n) => n.status !== "upcoming");
|
|
33
|
+
const hasExits = exits.some((n) => n.status !== "upcoming");
|
|
34
|
+
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(NodeRow, { nodes: main, connector: " \\u2192 " }), hasLoops && (_jsx(Box, { gap: 0, paddingLeft: 2, children: _jsx(NodeRow, { nodes: [...prLoops, ...queueLoop], connector: " " }) })), hasExits && (_jsx(Box, { gap: 0, paddingLeft: 2, children: _jsx(NodeRow, { nodes: exits, connector: " " }) }))] }));
|
|
31
35
|
}
|
|
@@ -9,12 +9,11 @@ export function Timeline({ entries, follow }) {
|
|
|
9
9
|
const rows = stdout?.rows ?? 24;
|
|
10
10
|
const maxActive = Math.max(ACTIVE_TAIL, rows - 12);
|
|
11
11
|
const displayRows = useMemo(() => buildTimelineRows(entries), [entries]);
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
const finalized = displayRows.slice(0, splitIndex);
|
|
12
|
+
// Always cap the rendered entries to prevent OOM/WASM crashes.
|
|
13
|
+
// In follow mode: older entries go to Static (terminal scrollback).
|
|
14
|
+
// Without follow: show last maxActive entries only.
|
|
15
|
+
const splitIndex = Math.max(0, displayRows.length - maxActive);
|
|
16
|
+
const finalized = follow ? displayRows.slice(0, splitIndex) : [];
|
|
18
17
|
const active = displayRows.slice(splitIndex);
|
|
19
18
|
if (displayRows.length === 0) {
|
|
20
19
|
return _jsx(Text, { dimColor: true, children: "No timeline events yet." });
|
package/dist/run-orchestrator.js
CHANGED
|
@@ -126,6 +126,10 @@ export class RunOrchestrator {
|
|
|
126
126
|
const issue = this.db.getIssue(item.projectId, item.issueId);
|
|
127
127
|
if (!issue?.pendingRunType || issue.activeRunId !== undefined)
|
|
128
128
|
return;
|
|
129
|
+
if (issue.prState === "merged") {
|
|
130
|
+
this.db.upsertIssue({ projectId: issue.projectId, linearIssueId: issue.linearIssueId, pendingRunType: null, factoryState: "done" });
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
129
133
|
const runType = issue.pendingRunType;
|
|
130
134
|
const contextJson = issue.pendingRunContextJson;
|
|
131
135
|
const context = contextJson ? JSON.parse(contextJson) : undefined;
|
|
@@ -632,15 +636,31 @@ export class RunOrchestrator {
|
|
|
632
636
|
const hasQueueLabel = pr.labels?.some((l) => l.name === protocol.admissionLabel) ?? false;
|
|
633
637
|
if (!hasQueueLabel)
|
|
634
638
|
return;
|
|
635
|
-
//
|
|
636
|
-
|
|
639
|
+
// Detect queue issues: either GitHub reports DIRTY, or the steward
|
|
640
|
+
// eviction check run failed (webhook may have been missed).
|
|
641
|
+
const isDirty = pr.mergeStateStatus === "DIRTY" || pr.mergeable === "CONFLICTING";
|
|
642
|
+
let hasEvictionCheckRun = false;
|
|
643
|
+
if (!isDirty) {
|
|
644
|
+
// Check for missed eviction webhook by looking for the steward's
|
|
645
|
+
// check run on the PR head.
|
|
646
|
+
try {
|
|
647
|
+
const { stdout: checksOut } = await execCommand("gh", [
|
|
648
|
+
"api", `repos/${project.github.repoFullName}/commits/${pr.headRefOid}/check-runs`,
|
|
649
|
+
"--jq", `.check_runs[] | select(.name == "${protocol.evictionCheckName}" and .conclusion == "failure") | .name`,
|
|
650
|
+
], { timeoutMs: 10_000 });
|
|
651
|
+
hasEvictionCheckRun = checksOut.trim().length > 0;
|
|
652
|
+
}
|
|
653
|
+
catch {
|
|
654
|
+
// Best-effort check.
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
if (isDirty || hasEvictionCheckRun) {
|
|
637
658
|
const headRefOid = pr.headRefOid ?? "unknown";
|
|
638
|
-
|
|
639
|
-
// main-only advance with the same PR head is recognized as a new conflict.
|
|
659
|
+
const reason = hasEvictionCheckRun ? "queue_eviction_missed" : "preemptive_conflict";
|
|
640
660
|
const signature = `preemptive_queue_conflict:${headRefOid}`;
|
|
641
661
|
const pendingRunContext = {
|
|
642
662
|
source: "queue_health_monitor",
|
|
643
|
-
failureReason:
|
|
663
|
+
failureReason: reason,
|
|
644
664
|
failureHeadSha: headRefOid,
|
|
645
665
|
failureSignature: signature,
|
|
646
666
|
};
|
|
@@ -657,15 +677,17 @@ export class RunOrchestrator {
|
|
|
657
677
|
pendingRunType: "queue_repair",
|
|
658
678
|
pendingRunContext,
|
|
659
679
|
});
|
|
660
|
-
this.logger.info({ issueKey: issue.issueKey, prNumber: issue.prNumber, headRefOid }, "Queue health:
|
|
680
|
+
this.logger.info({ issueKey: issue.issueKey, prNumber: issue.prNumber, headRefOid, reason }, "Queue health: queue issue detected, dispatching repair");
|
|
661
681
|
this.feed?.publish({
|
|
662
682
|
level: "warn",
|
|
663
683
|
kind: "github",
|
|
664
684
|
issueKey: issue.issueKey,
|
|
665
685
|
projectId: issue.projectId,
|
|
666
686
|
stage: "repairing_queue",
|
|
667
|
-
status: "queue_health_conflict_detected",
|
|
668
|
-
summary:
|
|
687
|
+
status: hasEvictionCheckRun ? "queue_health_eviction_detected" : "queue_health_conflict_detected",
|
|
688
|
+
summary: hasEvictionCheckRun
|
|
689
|
+
? `Queue health: missed eviction detected on PR #${issue.prNumber}, dispatching repair`
|
|
690
|
+
: `Queue health: merge conflict detected on PR #${issue.prNumber}, dispatching preemptive repair`,
|
|
669
691
|
});
|
|
670
692
|
}
|
|
671
693
|
}
|
package/dist/service.js
CHANGED
|
@@ -487,6 +487,10 @@ export class PatchRelayService {
|
|
|
487
487
|
return undefined;
|
|
488
488
|
if (issue.activeRunId)
|
|
489
489
|
return { error: "Issue already has an active run" };
|
|
490
|
+
if (issue.prState === "merged") {
|
|
491
|
+
this.db.upsertIssue({ projectId: issue.projectId, linearIssueId: issue.linearIssueId, factoryState: "done" });
|
|
492
|
+
return { issueKey, runType: "none" };
|
|
493
|
+
}
|
|
490
494
|
// Infer run type from current state instead of always resetting to implementation
|
|
491
495
|
let runType = "implementation";
|
|
492
496
|
let factoryState = "delegated";
|