patchrelay 0.31.0 → 0.32.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/commands/watch.js +9 -3
- package/dist/cli/watch/App.js +9 -4
- package/dist/cli/watch/HelpBar.js +2 -2
- package/dist/cli/watch/IssueDetailView.js +89 -1
- package/dist/cli/watch/IssueListView.js +8 -3
- package/dist/cli/watch/IssueRow.js +58 -5
- package/dist/cli/watch/StatusBar.js +2 -2
- package/dist/cli/watch/use-detail-stream.js +4 -2
- package/dist/cli/watch/use-watch-stream.js +3 -1
- package/dist/db/linear-installation-store.js +67 -3
- package/dist/db/migrations.js +10 -0
- package/dist/db.js +68 -2
- package/dist/github-failure-context.js +93 -0
- package/dist/github-webhook-handler.js +187 -19
- package/dist/linear-client.js +3 -3
- package/dist/run-orchestrator.js +39 -15
- package/dist/service.js +5 -1
- package/dist/webhook-handler.js +15 -1
- package/package.json +3 -3
package/dist/build-info.json
CHANGED
|
@@ -24,7 +24,13 @@ export async function handleWatchCommand(params) {
|
|
|
24
24
|
const issueKey = typeof params.parsed.flags.get("issue") === "string"
|
|
25
25
|
? String(params.parsed.flags.get("issue"))
|
|
26
26
|
: undefined;
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
process.stderr.write("\u001b[?1049h\u001b[2J\u001b[H");
|
|
28
|
+
try {
|
|
29
|
+
const instance = render(createElement(App, { baseUrl, bearerToken, initialIssueKey: issueKey }), { stdout: process.stderr, stdin: process.stdin, patchConsole: false });
|
|
30
|
+
await instance.waitUntilExit();
|
|
31
|
+
return 0;
|
|
32
|
+
}
|
|
33
|
+
finally {
|
|
34
|
+
process.stderr.write("\u001b[?1049l");
|
|
35
|
+
}
|
|
30
36
|
}
|
package/dist/cli/watch/App.js
CHANGED
|
@@ -68,9 +68,10 @@ export function App({ baseUrl, bearerToken, initialIssueKey }) {
|
|
|
68
68
|
...(initialIssueKey ? { view: "detail", activeDetailKey: initialIssueKey } : {}),
|
|
69
69
|
});
|
|
70
70
|
const filtered = useMemo(() => filterIssues(state.issues, state.filter), [state.issues, state.filter]);
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
71
|
+
const [frozen, setFrozen] = useState(false);
|
|
72
|
+
useWatchStream({ baseUrl, bearerToken, dispatch, active: !frozen });
|
|
73
|
+
useDetailStream({ baseUrl, bearerToken, issueKey: state.activeDetailKey, dispatch, active: !frozen });
|
|
74
|
+
useFeedStream({ baseUrl, bearerToken, active: state.view === "feed" && !frozen, dispatch });
|
|
74
75
|
const [promptMode, setPromptMode] = useState(false);
|
|
75
76
|
const [promptBuffer, setPromptBuffer] = useState("");
|
|
76
77
|
const handleRetry = useCallback(() => {
|
|
@@ -133,6 +134,10 @@ export function App({ baseUrl, bearerToken, initialIssueKey }) {
|
|
|
133
134
|
exit();
|
|
134
135
|
return;
|
|
135
136
|
}
|
|
137
|
+
if (input === "x") {
|
|
138
|
+
setFrozen((value) => !value);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
136
141
|
if (state.view === "list") {
|
|
137
142
|
if (input === "j" || key.downArrow) {
|
|
138
143
|
dispatch({ type: "select", index: state.selectedIndex + 1 });
|
|
@@ -197,5 +202,5 @@ export function App({ baseUrl, bearerToken, initialIssueKey }) {
|
|
|
197
202
|
}
|
|
198
203
|
}
|
|
199
204
|
});
|
|
200
|
-
return (_jsx(Box, { flexDirection: "column", children: state.view === "list" ? (_jsx(IssueListView, { issues: filtered, allIssues: state.issues, selectedIndex: state.selectedIndex, connected: state.connected, lastServerMessageAt: state.lastServerMessageAt, filter: state.filter, totalCount: state.issues.length })) : state.view === "detail" ? (_jsxs(Box, { flexDirection: "column", children: [state.activeDetailKey && (_jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: "Issues" }), _jsx(Text, { dimColor: true, children: " \u203A " }), _jsx(Text, { bold: true, children: state.activeDetailKey }), _jsx(Text, { dimColor: true, children: " \u203A " }), _jsx(Text, { dimColor: true, children: state.detailTab === "timeline" ? "Timeline" : "History" })] })), _jsx(IssueDetailView, { issue: state.issues.find((i) => i.issueKey === state.activeDetailKey), timeline: state.timeline, follow: state.follow, activeRunStartedAt: state.activeRunStartedAt, activeRunId: state.activeRunId, tokenUsage: state.tokenUsage, diffSummary: state.diffSummary, plan: state.plan, issueContext: state.issueContext, detailTab: state.detailTab, timelineMode: state.timelineMode, rawRuns: state.rawRuns, rawFeedEvents: state.rawFeedEvents, connected: state.connected, lastServerMessageAt: state.lastServerMessageAt }), promptMode && (_jsxs(Box, { children: [_jsx(Text, { color: "yellow", children: "prompt> " }), _jsx(Text, { children: promptBuffer }), _jsx(Text, { dimColor: true, children: "_" })] })), promptStatus && !promptMode && (_jsx(Text, { dimColor: true, children: promptStatus }))] })) : (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: "Issues" }), _jsx(Text, { dimColor: true, children: " \u203A " }), _jsx(Text, { bold: true, children: "Operator Feed" })] }), _jsx(FeedView, { events: state.feedEvents, connected: state.connected, lastServerMessageAt: state.lastServerMessageAt })] })) }));
|
|
205
|
+
return (_jsx(Box, { flexDirection: "column", children: state.view === "list" ? (_jsx(IssueListView, { issues: filtered, allIssues: state.issues, selectedIndex: state.selectedIndex, connected: state.connected, lastServerMessageAt: state.lastServerMessageAt, filter: state.filter, totalCount: state.issues.length, frozen: frozen })) : state.view === "detail" ? (_jsxs(Box, { flexDirection: "column", children: [state.activeDetailKey && (_jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: "Issues" }), _jsx(Text, { dimColor: true, children: " \u203A " }), _jsx(Text, { bold: true, children: state.activeDetailKey }), _jsx(Text, { dimColor: true, children: " \u203A " }), _jsx(Text, { dimColor: true, children: state.detailTab === "timeline" ? "Timeline" : "History" })] })), _jsx(IssueDetailView, { issue: state.issues.find((i) => i.issueKey === state.activeDetailKey), timeline: state.timeline, follow: state.follow, activeRunStartedAt: state.activeRunStartedAt, activeRunId: state.activeRunId, tokenUsage: state.tokenUsage, diffSummary: state.diffSummary, plan: state.plan, issueContext: state.issueContext, detailTab: state.detailTab, timelineMode: state.timelineMode, rawRuns: state.rawRuns, rawFeedEvents: state.rawFeedEvents, connected: state.connected, lastServerMessageAt: state.lastServerMessageAt }), promptMode && (_jsxs(Box, { children: [_jsx(Text, { color: "yellow", children: "prompt> " }), _jsx(Text, { children: promptBuffer }), _jsx(Text, { dimColor: true, children: "_" })] })), promptStatus && !promptMode && (_jsx(Text, { dimColor: true, children: promptStatus }))] })) : (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: "Issues" }), _jsx(Text, { dimColor: true, children: " \u203A " }), _jsx(Text, { bold: true, children: "Operator Feed" })] }), _jsx(FeedView, { events: state.feedEvents, connected: state.connected, lastServerMessageAt: state.lastServerMessageAt })] })) }));
|
|
201
206
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text } from "ink";
|
|
3
3
|
const HELP_TEXT = {
|
|
4
|
-
list: "j/k: navigate Enter: detail F: feed Tab: filter q: quit",
|
|
4
|
+
list: "j/k: navigate Enter: detail F: feed Tab: filter x: freeze q: quit",
|
|
5
5
|
detail: "",
|
|
6
6
|
feed: "Esc: list q: quit",
|
|
7
7
|
};
|
|
@@ -10,7 +10,7 @@ export function HelpBar({ view, follow, detailTab, timelineMode }) {
|
|
|
10
10
|
if (view === "detail") {
|
|
11
11
|
const tabHint = detailTab === "history" ? "t: timeline" : "h: history";
|
|
12
12
|
const timelineHint = detailTab === "timeline" ? `v: ${timelineMode === "verbose" ? "compact" : "verbose"}` : undefined;
|
|
13
|
-
text = [tabHint, timelineHint, "j/k: prev/next", "Esc: list", `f: follow ${follow ? "on" : "off"}`, "p: prompt", "s: stop", "r: retry", "q: quit"]
|
|
13
|
+
text = [tabHint, timelineHint, "j/k: prev/next", "Esc: list", `f: follow ${follow ? "on" : "off"}`, "x: freeze", "p: prompt", "s: stop", "r: retry", "q: quit"]
|
|
14
14
|
.filter(Boolean)
|
|
15
15
|
.join(" ");
|
|
16
16
|
}
|
|
@@ -18,6 +18,92 @@ function formatTokens(n) {
|
|
|
18
18
|
return `${(n / 1_000).toFixed(1)}k`;
|
|
19
19
|
return String(n);
|
|
20
20
|
}
|
|
21
|
+
function formatReviewState(reviewState) {
|
|
22
|
+
switch (reviewState) {
|
|
23
|
+
case "approved":
|
|
24
|
+
return "approved";
|
|
25
|
+
case "changes_requested":
|
|
26
|
+
return "changes requested";
|
|
27
|
+
case "commented":
|
|
28
|
+
return "commented";
|
|
29
|
+
default:
|
|
30
|
+
return reviewState ? reviewState.replaceAll("_", " ") : null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function formatCheckState(checkState) {
|
|
34
|
+
switch (checkState) {
|
|
35
|
+
case "passed":
|
|
36
|
+
case "success":
|
|
37
|
+
return "checks passed";
|
|
38
|
+
case "failed":
|
|
39
|
+
case "failure":
|
|
40
|
+
return "checks failed";
|
|
41
|
+
case "pending":
|
|
42
|
+
case "in_progress":
|
|
43
|
+
case "queued":
|
|
44
|
+
return "checks pending";
|
|
45
|
+
default:
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function buildPrStatusSummary(issue, issueContext) {
|
|
50
|
+
if (issue.prNumber === undefined)
|
|
51
|
+
return [];
|
|
52
|
+
const summary = [`PR #${issue.prNumber}`];
|
|
53
|
+
const checkState = formatCheckState(issue.prCheckStatus);
|
|
54
|
+
const reviewState = formatReviewState(issue.prReviewState);
|
|
55
|
+
const failedCheck = issueContext?.latestFailureCheckName ?? issue.latestFailureCheckName;
|
|
56
|
+
if (checkState === "checks failed" && failedCheck) {
|
|
57
|
+
summary.push(`${failedCheck} failed`);
|
|
58
|
+
}
|
|
59
|
+
else if (checkState) {
|
|
60
|
+
summary.push(checkState);
|
|
61
|
+
}
|
|
62
|
+
if (reviewState) {
|
|
63
|
+
summary.push(`review ${reviewState}`);
|
|
64
|
+
}
|
|
65
|
+
else if (issue.factoryState === "pr_open" || issue.factoryState === "repairing_ci" || issue.factoryState === "awaiting_queue") {
|
|
66
|
+
summary.push("review pending");
|
|
67
|
+
}
|
|
68
|
+
if (issue.factoryState === "awaiting_queue") {
|
|
69
|
+
summary.push("queued for merge");
|
|
70
|
+
}
|
|
71
|
+
else if (issue.factoryState === "repairing_queue") {
|
|
72
|
+
summary.push("merge queue repair needed");
|
|
73
|
+
}
|
|
74
|
+
else if (issue.factoryState === "done") {
|
|
75
|
+
summary.push("merged");
|
|
76
|
+
}
|
|
77
|
+
else if (issue.prCheckStatus === "failed" || issue.prReviewState === undefined || issue.prReviewState === "changes_requested") {
|
|
78
|
+
summary.push("not mergeable");
|
|
79
|
+
}
|
|
80
|
+
return summary;
|
|
81
|
+
}
|
|
82
|
+
function resolvePrimaryBlocker(issue, issueContext) {
|
|
83
|
+
if (issue.blockedByCount > 0) {
|
|
84
|
+
return {
|
|
85
|
+
text: `Waiting on blockers: ${issue.blockedByKeys.join(", ")}`,
|
|
86
|
+
color: "yellow",
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
if (issue.prCheckStatus === "failed") {
|
|
90
|
+
const failedCheck = issueContext?.latestFailureCheckName ?? issue.latestFailureCheckName;
|
|
91
|
+
return {
|
|
92
|
+
text: failedCheck ? `Blocked by failed check: ${failedCheck}` : "Blocked by failed PR checks",
|
|
93
|
+
color: "red",
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
if (issue.prReviewState === "changes_requested") {
|
|
97
|
+
return { text: "Blocked by requested review changes", color: "yellow" };
|
|
98
|
+
}
|
|
99
|
+
if (issue.prNumber !== undefined && !issue.prReviewState && issue.factoryState !== "done") {
|
|
100
|
+
return { text: "Blocked pending review approval", color: "yellow" };
|
|
101
|
+
}
|
|
102
|
+
if (issue.factoryState === "awaiting_queue") {
|
|
103
|
+
return { text: "Waiting in merge queue", color: "yellow" };
|
|
104
|
+
}
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
21
107
|
function ElapsedTime({ startedAt }) {
|
|
22
108
|
const [, tick] = useReducer((c) => c + 1, 0);
|
|
23
109
|
useEffect(() => {
|
|
@@ -44,5 +130,7 @@ export function IssueDetailView({ issue, timeline, follow, activeRunStartedAt, a
|
|
|
44
130
|
const history = useMemo(() => buildStateHistory(rawRuns, rawFeedEvents, issue.factoryState, activeRunId), [rawRuns, rawFeedEvents, issue.factoryState, activeRunId]);
|
|
45
131
|
const graph = useMemo(() => buildPatchRelayStateGraph(history, issue.factoryState), [history, issue.factoryState]);
|
|
46
132
|
const queueObservations = useMemo(() => buildPatchRelayQueueObservations(issue, rawFeedEvents), [issue, rawFeedEvents]);
|
|
47
|
-
|
|
133
|
+
const prStatusSummary = useMemo(() => buildPrStatusSummary(issue, issueContext), [issue, issueContext]);
|
|
134
|
+
const primaryBlocker = useMemo(() => resolvePrimaryBlocker(issue, issueContext), [issue, issueContext]);
|
|
135
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { gap: 2, children: [_jsx(Text, { bold: true, children: key }), _jsx(Text, { color: "cyan", children: issue.factoryState }), issue.blockedByCount > 0 && _jsxs(Text, { color: "yellow", children: ["blocked by ", issue.blockedByKeys.join(", ")] }), issue.readyForExecution && !issue.activeRunType && issue.blockedByCount === 0 && _jsx(Text, { color: "blueBright", children: "ready" }), issue.activeRunType && _jsx(Text, { color: "yellow", children: issue.activeRunType }), issue.prNumber !== undefined && _jsxs(Text, { dimColor: true, children: ["#", issue.prNumber] }), activeRunStartedAt && _jsx(ElapsedTime, { startedAt: activeRunStartedAt }), meta.length > 0 && _jsx(Text, { dimColor: true, children: meta.join(" ") }), detailTab === "timeline" && _jsx(Text, { dimColor: true, children: timelineMode }), follow && _jsx(Text, { color: "yellow", children: "follow" }), _jsx(FreshnessBadge, { connected: connected, lastServerMessageAt: lastServerMessageAt })] }), issue.title && _jsx(Text, { children: issue.title }), prStatusSummary.length > 0 && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: prStatusSummary.join(" | ") }) })), primaryBlocker && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: primaryBlocker.color, children: ["Blocked by: ", primaryBlocker.text] }) })), issueContext?.latestFailureSummary && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: issueContext.latestFailureSource === "queue_eviction" ? "yellow" : "red", children: ["Latest failure: ", issueContext.latestFailureSummary, issueContext.latestFailureHeadSha ? ` @ ${issueContext.latestFailureHeadSha.slice(0, 8)}` : ""] }) })), detailTab === "timeline" ? (_jsxs(_Fragment, { children: [plan && plan.length > 0 && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "Plan" }), _jsx(Text, { children: progressBar(plan.filter((s) => s.status === "completed").length, plan.length, 16) }), _jsxs(Text, { dimColor: true, children: [plan.filter((s) => s.status === "completed").length, "/", plan.length] })] }), plan.map((entry, i) => (_jsxs(Box, { gap: 1, children: [_jsxs(Text, { color: planStepColor(entry.status), children: ["[", planStepSymbol(entry.status), "]"] }), _jsx(Text, { children: entry.step })] }, `plan-${i}`)))] })), _jsx(Box, { marginTop: 1, flexDirection: "column", children: _jsx(Timeline, { entries: timeline, follow: follow, mode: timelineMode }) })] })) : (_jsxs(_Fragment, { children: [_jsx(FactoryStateGraph, { main: graph.main, prLoops: graph.prLoops, queueLoop: graph.queueLoop, exits: graph.exits }), _jsx(QueueObservationView, { observations: queueObservations }), _jsx(Box, { marginTop: 1, children: _jsx(StateHistoryView, { history: history, plan: plan, activeRunId: activeRunId }) })] })), _jsx(Box, { marginTop: 1, children: _jsx(HelpBar, { view: "detail", follow: follow, detailTab: detailTab, timelineMode: timelineMode }) })] }));
|
|
48
136
|
}
|
|
@@ -7,7 +7,7 @@ import { HelpBar } from "./HelpBar.js";
|
|
|
7
7
|
// selector(2) + key(10) + status(13) + pr(7) + ago(4) + gaps = ~36
|
|
8
8
|
const FIXED_COLS = 40;
|
|
9
9
|
const CHROME_ROWS = 4;
|
|
10
|
-
export function IssueListView({ issues, allIssues, selectedIndex, connected, lastServerMessageAt, filter, totalCount, }) {
|
|
10
|
+
export function IssueListView({ issues, allIssues, selectedIndex, connected, lastServerMessageAt, filter, totalCount, frozen, }) {
|
|
11
11
|
const { stdout } = useStdout();
|
|
12
12
|
const cols = stdout?.columns ?? 80;
|
|
13
13
|
const rows = stdout?.rows ?? 24;
|
|
@@ -15,7 +15,12 @@ export function IssueListView({ issues, allIssues, selectedIndex, connected, las
|
|
|
15
15
|
const maxVisible = Math.max(1, rows - CHROME_ROWS);
|
|
16
16
|
// Periodic refresh for elapsed times
|
|
17
17
|
const [, tick] = useReducer((c) => c + 1, 0);
|
|
18
|
-
useEffect(() => {
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
if (frozen)
|
|
20
|
+
return;
|
|
21
|
+
const id = setInterval(tick, 5000);
|
|
22
|
+
return () => clearInterval(id);
|
|
23
|
+
}, [frozen]);
|
|
19
24
|
let startIndex = 0;
|
|
20
25
|
if (issues.length > maxVisible) {
|
|
21
26
|
startIndex = Math.max(0, Math.min(selectedIndex - Math.floor(maxVisible / 2), issues.length - maxVisible));
|
|
@@ -23,5 +28,5 @@ export function IssueListView({ issues, allIssues, selectedIndex, connected, las
|
|
|
23
28
|
const visible = issues.slice(startIndex, startIndex + maxVisible);
|
|
24
29
|
const hiddenAbove = startIndex;
|
|
25
30
|
const hiddenBelow = Math.max(0, issues.length - startIndex - maxVisible);
|
|
26
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(StatusBar, { issues: issues, totalCount: totalCount, filter: filter, connected: connected, lastServerMessageAt: lastServerMessageAt, allIssues: allIssues }), _jsx(Box, { marginTop: 1, flexDirection: "column", children: issues.length === 0 ? (_jsx(Text, { dimColor: true, children: "No issues match the current filter." })) : (_jsxs(_Fragment, { children: [hiddenAbove > 0 && _jsxs(Text, { dimColor: true, children: [" ", hiddenAbove, " more above"] }), visible.map((issue, i) => (_jsx(IssueRow, { issue: issue, selected: startIndex + i === selectedIndex, titleWidth: titleWidth }, issue.issueKey ?? `${issue.projectId}-${startIndex + i}`))), hiddenBelow > 0 && _jsxs(Text, { dimColor: true, children: [" ", hiddenBelow, " more below"] })] })) }), _jsx(Box, { marginTop: 1, children: _jsx(HelpBar, { view: "list" }) })] }));
|
|
31
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(StatusBar, { issues: issues, totalCount: totalCount, filter: filter, connected: connected, lastServerMessageAt: lastServerMessageAt, allIssues: allIssues, frozen: frozen ?? false }), _jsx(Box, { marginTop: 1, flexDirection: "column", children: issues.length === 0 ? (_jsx(Text, { dimColor: true, children: "No issues match the current filter." })) : (_jsxs(_Fragment, { children: [hiddenAbove > 0 && _jsxs(Text, { dimColor: true, children: [" ", hiddenAbove, " more above"] }), visible.map((issue, i) => (_jsx(IssueRow, { issue: issue, selected: startIndex + i === selectedIndex, titleWidth: titleWidth }, issue.issueKey ?? `${issue.projectId}-${startIndex + i}`))), hiddenBelow > 0 && _jsxs(Text, { dimColor: true, children: [" ", hiddenBelow, " more below"] })] })) }), _jsx(Box, { marginTop: 1, children: _jsx(HelpBar, { view: "list" }) })] }));
|
|
27
32
|
}
|
|
@@ -44,12 +44,65 @@ function formatPr(issue) {
|
|
|
44
44
|
if (!issue.prNumber)
|
|
45
45
|
return "";
|
|
46
46
|
const parts = [`#${issue.prNumber}`];
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
47
|
+
const review = formatReviewState(issue.prReviewState);
|
|
48
|
+
const checks = formatCheckState(issue.prCheckStatus);
|
|
49
|
+
const merge = formatMergeState(issue);
|
|
50
|
+
if (review)
|
|
51
|
+
parts.push(review);
|
|
52
|
+
if (checks)
|
|
53
|
+
parts.push(checks);
|
|
54
|
+
if (merge)
|
|
55
|
+
parts.push(merge);
|
|
51
56
|
return parts.join("");
|
|
52
57
|
}
|
|
58
|
+
function formatReviewState(reviewState) {
|
|
59
|
+
switch (reviewState) {
|
|
60
|
+
case "approved":
|
|
61
|
+
return "rev:+";
|
|
62
|
+
case "changes_requested":
|
|
63
|
+
return "rev:x";
|
|
64
|
+
case "commented":
|
|
65
|
+
return "rev:c";
|
|
66
|
+
case "dismissed":
|
|
67
|
+
return "rev:-";
|
|
68
|
+
default:
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function formatCheckState(checkState) {
|
|
73
|
+
switch (checkState) {
|
|
74
|
+
case "passed":
|
|
75
|
+
case "success":
|
|
76
|
+
return "ci:+";
|
|
77
|
+
case "failed":
|
|
78
|
+
case "failure":
|
|
79
|
+
return "ci:x";
|
|
80
|
+
case "pending":
|
|
81
|
+
case "in_progress":
|
|
82
|
+
case "queued":
|
|
83
|
+
return "ci:…";
|
|
84
|
+
default:
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function formatMergeState(issue) {
|
|
89
|
+
if (!issue.prNumber)
|
|
90
|
+
return null;
|
|
91
|
+
switch (issue.factoryState) {
|
|
92
|
+
case "awaiting_queue":
|
|
93
|
+
return "queue";
|
|
94
|
+
case "repairing_queue":
|
|
95
|
+
return "mq-fix";
|
|
96
|
+
case "done":
|
|
97
|
+
return "merged";
|
|
98
|
+
case "pr_open":
|
|
99
|
+
if (issue.prReviewState === "approved" && issue.prCheckStatus === "passed")
|
|
100
|
+
return "ready";
|
|
101
|
+
return "open";
|
|
102
|
+
default:
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
53
106
|
function relativeTime(iso) {
|
|
54
107
|
const ms = Date.now() - new Date(iso).getTime();
|
|
55
108
|
if (ms < 0)
|
|
@@ -97,5 +150,5 @@ export function IssueRow({ issue, selected, titleWidth }) {
|
|
|
97
150
|
const tw = titleWidth ?? 30;
|
|
98
151
|
const title = issue.title ? truncate(issue.title, tw) : "";
|
|
99
152
|
const detail = selected ? summarizeIssueStatusNote(issue.statusNote) : undefined;
|
|
100
|
-
return (_jsxs(Box, { flexDirection: "column", marginBottom: detail ? 1 : 0, children: [_jsxs(Box, { children: [_jsx(Text, { color: selected ? "blueBright" : "white", bold: selected, children: selected ? "\u25b8" : " " }), _jsx(Text, { bold: true, children: ` ${key.padEnd(9)}` }), _jsx(Text, { color: stateColor(issue.blockedByCount > 0 && !issue.activeRunType ? "blocked" : issue.readyForExecution && !issue.activeRunType ? "ready" : issue.factoryState), children: ` ${status.padEnd(12)}` }), _jsx(Text, { dimColor: true, children: ` ${pr.padEnd(
|
|
153
|
+
return (_jsxs(Box, { flexDirection: "column", marginBottom: detail ? 1 : 0, children: [_jsxs(Box, { children: [_jsx(Text, { color: selected ? "blueBright" : "white", bold: selected, children: selected ? "\u25b8" : " " }), _jsx(Text, { bold: true, children: ` ${key.padEnd(9)}` }), _jsx(Text, { color: stateColor(issue.blockedByCount > 0 && !issue.activeRunType ? "blocked" : issue.readyForExecution && !issue.activeRunType ? "ready" : issue.factoryState), children: ` ${status.padEnd(12)}` }), _jsx(Text, { dimColor: true, children: ` ${pr.padEnd(26)}` }), _jsx(Text, { dimColor: true, children: ` ${ago.padStart(3)}` }), title ? _jsx(Text, { dimColor: true, children: ` ${title}` }) : null] }), detail ? (_jsx(Box, { paddingLeft: 4, children: _jsx(Text, { dimColor: true, wrap: "wrap", children: detail }) })) : null] }));
|
|
101
154
|
}
|
|
@@ -7,10 +7,10 @@ const FILTER_LABELS = {
|
|
|
7
7
|
"active": "active",
|
|
8
8
|
"non-done": "in progress",
|
|
9
9
|
};
|
|
10
|
-
export function StatusBar({ issues, totalCount, filter, connected, lastServerMessageAt, allIssues, }) {
|
|
10
|
+
export function StatusBar({ issues, totalCount, filter, connected, lastServerMessageAt, allIssues, frozen, }) {
|
|
11
11
|
const showing = filter === "all" ? `${totalCount} issues` : `${issues.length}/${totalCount} issues`;
|
|
12
12
|
const agg = computeAggregates(allIssues);
|
|
13
13
|
const withPr = allIssues.filter((i) => i.prNumber !== undefined).length;
|
|
14
14
|
const awaitingInput = allIssues.filter((i) => i.factoryState === "awaiting_input").length;
|
|
15
|
-
return (_jsxs(Box, { justifyContent: "space-between", children: [_jsxs(Box, { gap: 1, children: [_jsx(Text, { bold: true, children: showing }), _jsxs(Text, { dimColor: true, children: ["[", FILTER_LABELS[filter], "]"] }), _jsx(Text, { dimColor: true, children: "|" }), agg.active > 0 && _jsxs(Text, { color: "cyan", children: [agg.active, " active"] }), agg.ready > 0 && _jsxs(Text, { color: "blueBright", children: [agg.ready, " ready"] }), agg.blocked > 0 && _jsxs(Text, { color: "yellow", children: [agg.blocked, " blocked"] }), withPr > 0 && _jsxs(Text, { dimColor: true, children: [withPr, " PRs"] }), awaitingInput > 0 && _jsxs(Text, { color: "yellow", children: [awaitingInput, " awaiting input"] }), agg.done > 0 && _jsxs(Text, { color: "green", children: [agg.done, " done"] }), agg.failed > 0 && _jsxs(Text, { color: "red", children: [agg.failed, " failed"] })] }), _jsx(FreshnessBadge, { connected: connected, lastServerMessageAt: lastServerMessageAt })] }));
|
|
15
|
+
return (_jsxs(Box, { justifyContent: "space-between", children: [_jsxs(Box, { gap: 1, children: [_jsx(Text, { bold: true, children: showing }), _jsxs(Text, { dimColor: true, children: ["[", FILTER_LABELS[filter], "]"] }), _jsx(Text, { dimColor: true, children: "|" }), agg.active > 0 && _jsxs(Text, { color: "cyan", children: [agg.active, " active"] }), agg.ready > 0 && _jsxs(Text, { color: "blueBright", children: [agg.ready, " ready"] }), agg.blocked > 0 && _jsxs(Text, { color: "yellow", children: [agg.blocked, " blocked"] }), withPr > 0 && _jsxs(Text, { dimColor: true, children: [withPr, " PRs"] }), awaitingInput > 0 && _jsxs(Text, { color: "yellow", children: [awaitingInput, " awaiting input"] }), agg.done > 0 && _jsxs(Text, { color: "green", children: [agg.done, " done"] }), agg.failed > 0 && _jsxs(Text, { color: "red", children: [agg.failed, " failed"] }), frozen && _jsx(Text, { color: "magenta", children: "frozen" })] }), _jsx(FreshnessBadge, { connected: connected, lastServerMessageAt: lastServerMessageAt })] }));
|
|
16
16
|
}
|
|
@@ -3,7 +3,9 @@ export function useDetailStream(options) {
|
|
|
3
3
|
const optionsRef = useRef(options);
|
|
4
4
|
optionsRef.current = options;
|
|
5
5
|
useEffect(() => {
|
|
6
|
-
const { issueKey } = optionsRef.current;
|
|
6
|
+
const { issueKey, active } = optionsRef.current;
|
|
7
|
+
if (active === false)
|
|
8
|
+
return;
|
|
7
9
|
if (!issueKey)
|
|
8
10
|
return;
|
|
9
11
|
const abortController = new AbortController();
|
|
@@ -19,7 +21,7 @@ export function useDetailStream(options) {
|
|
|
19
21
|
return () => {
|
|
20
22
|
abortController.abort();
|
|
21
23
|
};
|
|
22
|
-
}, [options.issueKey]);
|
|
24
|
+
}, [options.issueKey, options.active]);
|
|
23
25
|
}
|
|
24
26
|
// ─── Rehydration ──────────────────────────────────────────────────
|
|
25
27
|
async function rehydrate(baseUrl, issueKey, headers, signal, dispatch) {
|
|
@@ -3,6 +3,8 @@ export function useWatchStream(options) {
|
|
|
3
3
|
const optionsRef = useRef(options);
|
|
4
4
|
optionsRef.current = options;
|
|
5
5
|
useEffect(() => {
|
|
6
|
+
if (options.active === false)
|
|
7
|
+
return;
|
|
6
8
|
let abortController = new AbortController();
|
|
7
9
|
let reconnectTimeout;
|
|
8
10
|
let attempt = 0;
|
|
@@ -110,7 +112,7 @@ export function useWatchStream(options) {
|
|
|
110
112
|
}
|
|
111
113
|
clearInterval(snapshotInterval);
|
|
112
114
|
};
|
|
113
|
-
}, []);
|
|
115
|
+
}, [options.active]);
|
|
114
116
|
}
|
|
115
117
|
function processEvent(dispatch, eventType, data) {
|
|
116
118
|
try {
|
|
@@ -122,6 +122,27 @@ export class LinearInstallationStore {
|
|
|
122
122
|
.all();
|
|
123
123
|
return rows.map((row) => mapProjectInstallation(row));
|
|
124
124
|
}
|
|
125
|
+
repairProjectInstallations(projectIds) {
|
|
126
|
+
const repairs = [];
|
|
127
|
+
for (const projectId of projectIds) {
|
|
128
|
+
const existing = this.getProjectInstallation(projectId);
|
|
129
|
+
const existingInstallation = existing ? this.getLinearInstallation(existing.installationId) : undefined;
|
|
130
|
+
if (existing && existingInstallation) {
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
const installationId = this.resolveRepairInstallationId(projectId);
|
|
134
|
+
if (installationId === undefined) {
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
this.linkProjectInstallation(projectId, installationId);
|
|
138
|
+
repairs.push({
|
|
139
|
+
projectId,
|
|
140
|
+
installationId,
|
|
141
|
+
reason: existing ? "dangling" : "missing",
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
return repairs;
|
|
145
|
+
}
|
|
125
146
|
unlinkProjectInstallation(projectId) {
|
|
126
147
|
this.connection.prepare("DELETE FROM project_installations WHERE project_id = ?").run(projectId);
|
|
127
148
|
}
|
|
@@ -136,12 +157,55 @@ export class LinearInstallationStore {
|
|
|
136
157
|
.prepare(`
|
|
137
158
|
SELECT li.*
|
|
138
159
|
FROM linear_installations li
|
|
139
|
-
|
|
140
|
-
|
|
160
|
+
WHERE li.id = COALESCE(
|
|
161
|
+
(
|
|
162
|
+
SELECT pi.installation_id
|
|
163
|
+
FROM project_installations pi
|
|
164
|
+
WHERE pi.project_id = ?
|
|
165
|
+
LIMIT 1
|
|
166
|
+
),
|
|
167
|
+
(
|
|
168
|
+
SELECT rl.installation_id
|
|
169
|
+
FROM repository_links rl
|
|
170
|
+
WHERE rl.github_repo = ?
|
|
171
|
+
LIMIT 1
|
|
172
|
+
),
|
|
173
|
+
(
|
|
174
|
+
SELECT li_single.id
|
|
175
|
+
FROM linear_installations li_single
|
|
176
|
+
WHERE (SELECT COUNT(*) FROM linear_installations) = 1
|
|
177
|
+
ORDER BY li_single.updated_at DESC, li_single.id DESC
|
|
178
|
+
LIMIT 1
|
|
179
|
+
)
|
|
180
|
+
)
|
|
141
181
|
`)
|
|
142
|
-
.get(projectId);
|
|
182
|
+
.get(projectId, projectId);
|
|
143
183
|
return row ? mapLinearInstallation(row) : undefined;
|
|
144
184
|
}
|
|
185
|
+
resolveRepairInstallationId(projectId) {
|
|
186
|
+
const repoLink = this.connection
|
|
187
|
+
.prepare(`
|
|
188
|
+
SELECT rl.installation_id AS installation_id
|
|
189
|
+
FROM repository_links rl
|
|
190
|
+
INNER JOIN linear_installations li ON li.id = rl.installation_id
|
|
191
|
+
WHERE rl.github_repo = ?
|
|
192
|
+
LIMIT 1
|
|
193
|
+
`)
|
|
194
|
+
.get(projectId);
|
|
195
|
+
if (repoLink) {
|
|
196
|
+
return Number(repoLink.installation_id);
|
|
197
|
+
}
|
|
198
|
+
const singleInstallation = this.connection
|
|
199
|
+
.prepare(`
|
|
200
|
+
SELECT li.id AS installation_id
|
|
201
|
+
FROM linear_installations li
|
|
202
|
+
WHERE (SELECT COUNT(*) FROM linear_installations) = 1
|
|
203
|
+
ORDER BY li.updated_at DESC, li.id DESC
|
|
204
|
+
LIMIT 1
|
|
205
|
+
`)
|
|
206
|
+
.get();
|
|
207
|
+
return singleInstallation ? Number(singleInstallation.installation_id) : undefined;
|
|
208
|
+
}
|
|
145
209
|
createOAuthState(params) {
|
|
146
210
|
const now = isoNow();
|
|
147
211
|
const result = this.connection
|
package/dist/db/migrations.js
CHANGED
|
@@ -12,6 +12,8 @@ CREATE TABLE IF NOT EXISTS issues (
|
|
|
12
12
|
pending_run_type TEXT,
|
|
13
13
|
pending_run_context_json TEXT,
|
|
14
14
|
branch_name TEXT,
|
|
15
|
+
branch_owner TEXT NOT NULL DEFAULT 'patchrelay',
|
|
16
|
+
branch_ownership_changed_at TEXT,
|
|
15
17
|
worktree_path TEXT,
|
|
16
18
|
thread_id TEXT,
|
|
17
19
|
active_run_id INTEGER,
|
|
@@ -180,6 +182,9 @@ export function runPatchRelayMigrations(connection) {
|
|
|
180
182
|
connection.prepare("UPDATE webhook_events SET processing_status = 'processed' WHERE processing_status = 'pending' AND payload_json IS NULL").run();
|
|
181
183
|
// Add pending_merge_prep column for merge queue stewardship
|
|
182
184
|
addColumnIfMissing(connection, "issues", "pending_merge_prep", "INTEGER NOT NULL DEFAULT 0");
|
|
185
|
+
// Explicit PR branch ownership hand-off between PatchRelay and MergeSteward
|
|
186
|
+
addColumnIfMissing(connection, "issues", "branch_owner", "TEXT NOT NULL DEFAULT 'patchrelay'");
|
|
187
|
+
addColumnIfMissing(connection, "issues", "branch_ownership_changed_at", "TEXT");
|
|
183
188
|
// Add merge_prep_attempts for retry budget / escalation
|
|
184
189
|
addColumnIfMissing(connection, "issues", "merge_prep_attempts", "INTEGER NOT NULL DEFAULT 0");
|
|
185
190
|
// Add review_fix_attempts counter
|
|
@@ -204,6 +209,11 @@ export function runPatchRelayMigrations(connection) {
|
|
|
204
209
|
addColumnIfMissing(connection, "issues", "last_github_failure_check_url", "TEXT");
|
|
205
210
|
addColumnIfMissing(connection, "issues", "last_github_failure_context_json", "TEXT");
|
|
206
211
|
addColumnIfMissing(connection, "issues", "last_github_failure_at", "TEXT");
|
|
212
|
+
addColumnIfMissing(connection, "issues", "last_github_ci_snapshot_head_sha", "TEXT");
|
|
213
|
+
addColumnIfMissing(connection, "issues", "last_github_ci_snapshot_gate_check_name", "TEXT");
|
|
214
|
+
addColumnIfMissing(connection, "issues", "last_github_ci_snapshot_gate_check_status", "TEXT");
|
|
215
|
+
addColumnIfMissing(connection, "issues", "last_github_ci_snapshot_json", "TEXT");
|
|
216
|
+
addColumnIfMissing(connection, "issues", "last_github_ci_snapshot_settled_at", "TEXT");
|
|
207
217
|
addColumnIfMissing(connection, "issues", "last_queue_signal_at", "TEXT");
|
|
208
218
|
addColumnIfMissing(connection, "issues", "last_queue_incident_json", "TEXT");
|
|
209
219
|
addColumnIfMissing(connection, "issues", "last_attempted_failure_head_sha", "TEXT");
|
package/dist/db.js
CHANGED
|
@@ -189,6 +189,26 @@ export class PatchRelayDatabase {
|
|
|
189
189
|
sets.push("last_github_failure_at = @lastGitHubFailureAt");
|
|
190
190
|
values.lastGitHubFailureAt = params.lastGitHubFailureAt;
|
|
191
191
|
}
|
|
192
|
+
if (params.lastGitHubCiSnapshotHeadSha !== undefined) {
|
|
193
|
+
sets.push("last_github_ci_snapshot_head_sha = @lastGitHubCiSnapshotHeadSha");
|
|
194
|
+
values.lastGitHubCiSnapshotHeadSha = params.lastGitHubCiSnapshotHeadSha;
|
|
195
|
+
}
|
|
196
|
+
if (params.lastGitHubCiSnapshotGateCheckName !== undefined) {
|
|
197
|
+
sets.push("last_github_ci_snapshot_gate_check_name = @lastGitHubCiSnapshotGateCheckName");
|
|
198
|
+
values.lastGitHubCiSnapshotGateCheckName = params.lastGitHubCiSnapshotGateCheckName;
|
|
199
|
+
}
|
|
200
|
+
if (params.lastGitHubCiSnapshotGateCheckStatus !== undefined) {
|
|
201
|
+
sets.push("last_github_ci_snapshot_gate_check_status = @lastGitHubCiSnapshotGateCheckStatus");
|
|
202
|
+
values.lastGitHubCiSnapshotGateCheckStatus = params.lastGitHubCiSnapshotGateCheckStatus;
|
|
203
|
+
}
|
|
204
|
+
if (params.lastGitHubCiSnapshotJson !== undefined) {
|
|
205
|
+
sets.push("last_github_ci_snapshot_json = @lastGitHubCiSnapshotJson");
|
|
206
|
+
values.lastGitHubCiSnapshotJson = params.lastGitHubCiSnapshotJson;
|
|
207
|
+
}
|
|
208
|
+
if (params.lastGitHubCiSnapshotSettledAt !== undefined) {
|
|
209
|
+
sets.push("last_github_ci_snapshot_settled_at = @lastGitHubCiSnapshotSettledAt");
|
|
210
|
+
values.lastGitHubCiSnapshotSettledAt = params.lastGitHubCiSnapshotSettledAt;
|
|
211
|
+
}
|
|
192
212
|
if (params.lastQueueSignalAt !== undefined) {
|
|
193
213
|
sets.push("last_queue_signal_at = @lastQueueSignalAt");
|
|
194
214
|
values.lastQueueSignalAt = params.lastQueueSignalAt;
|
|
@@ -236,7 +256,9 @@ export class PatchRelayDatabase {
|
|
|
236
256
|
branch_name, worktree_path, thread_id, active_run_id,
|
|
237
257
|
agent_session_id,
|
|
238
258
|
pr_number, pr_url, pr_state, pr_review_state, pr_check_status,
|
|
239
|
-
last_github_failure_source, last_github_failure_head_sha, last_github_failure_signature, last_github_failure_check_name, last_github_failure_check_url, last_github_failure_context_json, last_github_failure_at,
|
|
259
|
+
last_github_failure_source, last_github_failure_head_sha, last_github_failure_signature, last_github_failure_check_name, last_github_failure_check_url, last_github_failure_context_json, last_github_failure_at,
|
|
260
|
+
last_github_ci_snapshot_head_sha, last_github_ci_snapshot_gate_check_name, last_github_ci_snapshot_gate_check_status, last_github_ci_snapshot_json, last_github_ci_snapshot_settled_at,
|
|
261
|
+
last_queue_signal_at, last_queue_incident_json,
|
|
240
262
|
last_attempted_failure_head_sha, last_attempted_failure_signature,
|
|
241
263
|
updated_at
|
|
242
264
|
) VALUES (
|
|
@@ -246,7 +268,9 @@ export class PatchRelayDatabase {
|
|
|
246
268
|
@branchName, @worktreePath, @threadId, @activeRunId,
|
|
247
269
|
@agentSessionId,
|
|
248
270
|
@prNumber, @prUrl, @prState, @prReviewState, @prCheckStatus,
|
|
249
|
-
@lastGitHubFailureSource, @lastGitHubFailureHeadSha, @lastGitHubFailureSignature, @lastGitHubFailureCheckName, @lastGitHubFailureCheckUrl, @lastGitHubFailureContextJson, @lastGitHubFailureAt,
|
|
271
|
+
@lastGitHubFailureSource, @lastGitHubFailureHeadSha, @lastGitHubFailureSignature, @lastGitHubFailureCheckName, @lastGitHubFailureCheckUrl, @lastGitHubFailureContextJson, @lastGitHubFailureAt,
|
|
272
|
+
@lastGitHubCiSnapshotHeadSha, @lastGitHubCiSnapshotGateCheckName, @lastGitHubCiSnapshotGateCheckStatus, @lastGitHubCiSnapshotJson, @lastGitHubCiSnapshotSettledAt,
|
|
273
|
+
@lastQueueSignalAt, @lastQueueIncidentJson,
|
|
250
274
|
@lastAttemptedFailureHeadSha, @lastAttemptedFailureSignature,
|
|
251
275
|
@now
|
|
252
276
|
)
|
|
@@ -281,6 +305,11 @@ export class PatchRelayDatabase {
|
|
|
281
305
|
lastGitHubFailureCheckUrl: params.lastGitHubFailureCheckUrl ?? null,
|
|
282
306
|
lastGitHubFailureContextJson: params.lastGitHubFailureContextJson ?? null,
|
|
283
307
|
lastGitHubFailureAt: params.lastGitHubFailureAt ?? null,
|
|
308
|
+
lastGitHubCiSnapshotHeadSha: params.lastGitHubCiSnapshotHeadSha ?? null,
|
|
309
|
+
lastGitHubCiSnapshotGateCheckName: params.lastGitHubCiSnapshotGateCheckName ?? null,
|
|
310
|
+
lastGitHubCiSnapshotGateCheckStatus: params.lastGitHubCiSnapshotGateCheckStatus ?? null,
|
|
311
|
+
lastGitHubCiSnapshotJson: params.lastGitHubCiSnapshotJson ?? null,
|
|
312
|
+
lastGitHubCiSnapshotSettledAt: params.lastGitHubCiSnapshotSettledAt ?? null,
|
|
284
313
|
lastQueueSignalAt: params.lastQueueSignalAt ?? null,
|
|
285
314
|
lastQueueIncidentJson: params.lastQueueIncidentJson ?? null,
|
|
286
315
|
lastAttemptedFailureHeadSha: params.lastAttemptedFailureHeadSha ?? null,
|
|
@@ -312,6 +341,13 @@ export class PatchRelayDatabase {
|
|
|
312
341
|
const row = this.connection.prepare("SELECT * FROM issues WHERE pr_number = ?").get(prNumber);
|
|
313
342
|
return row ? mapIssueRow(row) : undefined;
|
|
314
343
|
}
|
|
344
|
+
setBranchOwner(projectId, linearIssueId, owner) {
|
|
345
|
+
this.connection.prepare(`
|
|
346
|
+
UPDATE issues
|
|
347
|
+
SET branch_owner = ?, branch_ownership_changed_at = ?, updated_at = ?
|
|
348
|
+
WHERE project_id = ? AND linear_issue_id = ?
|
|
349
|
+
`).run(owner, isoNow(), isoNow(), projectId, linearIssueId);
|
|
350
|
+
}
|
|
315
351
|
replaceIssueDependencies(params) {
|
|
316
352
|
const now = isoNow();
|
|
317
353
|
this.connection
|
|
@@ -381,6 +417,17 @@ export class PatchRelayDatabase {
|
|
|
381
417
|
linearIssueId: String(row.linear_issue_id),
|
|
382
418
|
}));
|
|
383
419
|
}
|
|
420
|
+
getLatestGitHubCiSnapshot(projectId, linearIssueId) {
|
|
421
|
+
const issue = this.getIssue(projectId, linearIssueId);
|
|
422
|
+
if (!issue?.lastGitHubCiSnapshotJson)
|
|
423
|
+
return undefined;
|
|
424
|
+
try {
|
|
425
|
+
return JSON.parse(issue.lastGitHubCiSnapshotJson);
|
|
426
|
+
}
|
|
427
|
+
catch {
|
|
428
|
+
return undefined;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
384
431
|
countUnresolvedBlockers(projectId, linearIssueId) {
|
|
385
432
|
const row = this.connection.prepare(`
|
|
386
433
|
SELECT COUNT(*) AS count
|
|
@@ -601,6 +648,10 @@ function mapIssueRow(row) {
|
|
|
601
648
|
...(row.pending_run_type !== null && row.pending_run_type !== undefined ? { pendingRunType: String(row.pending_run_type) } : {}),
|
|
602
649
|
...(row.pending_run_context_json !== null && row.pending_run_context_json !== undefined ? { pendingRunContextJson: String(row.pending_run_context_json) } : {}),
|
|
603
650
|
...(row.branch_name !== null ? { branchName: String(row.branch_name) } : {}),
|
|
651
|
+
...(row.branch_owner !== null && row.branch_owner !== undefined ? { branchOwner: String(row.branch_owner) } : { branchOwner: "patchrelay" }),
|
|
652
|
+
...(row.branch_ownership_changed_at !== null && row.branch_ownership_changed_at !== undefined
|
|
653
|
+
? { branchOwnershipChangedAt: String(row.branch_ownership_changed_at) }
|
|
654
|
+
: {}),
|
|
604
655
|
...(row.worktree_path !== null ? { worktreePath: String(row.worktree_path) } : {}),
|
|
605
656
|
...(row.thread_id !== null ? { threadId: String(row.thread_id) } : {}),
|
|
606
657
|
...(row.active_run_id !== null ? { activeRunId: Number(row.active_run_id) } : {}),
|
|
@@ -632,6 +683,21 @@ function mapIssueRow(row) {
|
|
|
632
683
|
...(row.last_github_failure_at !== null && row.last_github_failure_at !== undefined
|
|
633
684
|
? { lastGitHubFailureAt: String(row.last_github_failure_at) }
|
|
634
685
|
: {}),
|
|
686
|
+
...(row.last_github_ci_snapshot_head_sha !== null && row.last_github_ci_snapshot_head_sha !== undefined
|
|
687
|
+
? { lastGitHubCiSnapshotHeadSha: String(row.last_github_ci_snapshot_head_sha) }
|
|
688
|
+
: {}),
|
|
689
|
+
...(row.last_github_ci_snapshot_gate_check_name !== null && row.last_github_ci_snapshot_gate_check_name !== undefined
|
|
690
|
+
? { lastGitHubCiSnapshotGateCheckName: String(row.last_github_ci_snapshot_gate_check_name) }
|
|
691
|
+
: {}),
|
|
692
|
+
...(row.last_github_ci_snapshot_gate_check_status !== null && row.last_github_ci_snapshot_gate_check_status !== undefined
|
|
693
|
+
? { lastGitHubCiSnapshotGateCheckStatus: String(row.last_github_ci_snapshot_gate_check_status) }
|
|
694
|
+
: {}),
|
|
695
|
+
...(row.last_github_ci_snapshot_json !== null && row.last_github_ci_snapshot_json !== undefined
|
|
696
|
+
? { lastGitHubCiSnapshotJson: String(row.last_github_ci_snapshot_json) }
|
|
697
|
+
: {}),
|
|
698
|
+
...(row.last_github_ci_snapshot_settled_at !== null && row.last_github_ci_snapshot_settled_at !== undefined
|
|
699
|
+
? { lastGitHubCiSnapshotSettledAt: String(row.last_github_ci_snapshot_settled_at) }
|
|
700
|
+
: {}),
|
|
635
701
|
...(row.last_queue_signal_at !== null && row.last_queue_signal_at !== undefined
|
|
636
702
|
? { lastQueueSignalAt: String(row.last_queue_signal_at) }
|
|
637
703
|
: {}),
|