patchrelay 0.17.0 → 0.18.0
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/agent-session-plan.js +1 -2
- package/dist/build-info.json +3 -3
- package/dist/cli/watch/App.js +18 -1
- package/dist/cli/watch/FeedView.js +32 -0
- package/dist/cli/watch/HelpBar.js +9 -3
- package/dist/cli/watch/IssueDetailView.js +69 -3
- package/dist/cli/watch/IssueListView.js +2 -2
- package/dist/cli/watch/IssueRow.js +0 -1
- package/dist/cli/watch/StatusBar.js +4 -2
- package/dist/cli/watch/use-detail-stream.js +19 -0
- package/dist/cli/watch/use-feed-stream.js +92 -0
- package/dist/cli/watch/watch-state.js +40 -0
- package/dist/db/migrations.js +8 -0
- package/dist/db.js +27 -2
- package/dist/factory-state.js +3 -1
- package/dist/github-webhook-handler.js +5 -4
- package/dist/issue-query-service.js +18 -1
- package/dist/linear-client.js +12 -0
- package/dist/linear-session-reporting.js +0 -2
- package/dist/merge-queue.js +0 -1
- package/dist/run-orchestrator.js +33 -17
- package/dist/webhook-handler.js +3 -0
- package/dist/webhooks.js +8 -0
- package/package.json +1 -1
|
@@ -93,7 +93,6 @@ export function buildAgentSessionPlan(params) {
|
|
|
93
93
|
case "implementing":
|
|
94
94
|
return setStatuses(planForRunType("implementation", params), ["completed", "inProgress", "pending", "pending"]);
|
|
95
95
|
case "pr_open":
|
|
96
|
-
case "awaiting_review":
|
|
97
96
|
return setStatuses(implementationPlan(), ["completed", "completed", "inProgress", "pending"]);
|
|
98
97
|
case "changes_requested":
|
|
99
98
|
return setStatuses(reviewFixPlan(), ["completed", "inProgress", "pending", "pending"]);
|
|
@@ -164,7 +163,7 @@ export function buildCompletedSessionPlan(runType) {
|
|
|
164
163
|
if (runType === "ci_repair" || runType === "queue_repair") {
|
|
165
164
|
return buildAgentSessionPlan({ factoryState: "awaiting_queue" });
|
|
166
165
|
}
|
|
167
|
-
return buildAgentSessionPlan({ factoryState: "
|
|
166
|
+
return buildAgentSessionPlan({ factoryState: "pr_open" });
|
|
168
167
|
}
|
|
169
168
|
export function buildAwaitingHandoffSessionPlan(runType) {
|
|
170
169
|
return buildCompletedSessionPlan(runType);
|
package/dist/build-info.json
CHANGED
package/dist/cli/watch/App.js
CHANGED
|
@@ -4,8 +4,10 @@ import { Box, useApp, useInput } from "ink";
|
|
|
4
4
|
import { watchReducer, initialWatchState, filterIssues } from "./watch-state.js";
|
|
5
5
|
import { useWatchStream } from "./use-watch-stream.js";
|
|
6
6
|
import { useDetailStream } from "./use-detail-stream.js";
|
|
7
|
+
import { useFeedStream } from "./use-feed-stream.js";
|
|
7
8
|
import { IssueListView } from "./IssueListView.js";
|
|
8
9
|
import { IssueDetailView } from "./IssueDetailView.js";
|
|
10
|
+
import { FeedView } from "./FeedView.js";
|
|
9
11
|
async function postRetry(baseUrl, issueKey, bearerToken) {
|
|
10
12
|
const headers = { "content-type": "application/json" };
|
|
11
13
|
if (bearerToken)
|
|
@@ -25,6 +27,7 @@ export function App({ baseUrl, bearerToken, initialIssueKey }) {
|
|
|
25
27
|
const filtered = useMemo(() => filterIssues(state.issues, state.filter), [state.issues, state.filter]);
|
|
26
28
|
useWatchStream({ baseUrl, bearerToken, dispatch });
|
|
27
29
|
useDetailStream({ baseUrl, bearerToken, issueKey: state.activeDetailKey, dispatch });
|
|
30
|
+
useFeedStream({ baseUrl, bearerToken, active: state.view === "feed", dispatch });
|
|
28
31
|
const handleRetry = useCallback(() => {
|
|
29
32
|
if (state.activeDetailKey) {
|
|
30
33
|
void postRetry(baseUrl, state.activeDetailKey, bearerToken);
|
|
@@ -51,6 +54,9 @@ export function App({ baseUrl, bearerToken, initialIssueKey }) {
|
|
|
51
54
|
else if (key.tab) {
|
|
52
55
|
dispatch({ type: "cycle-filter" });
|
|
53
56
|
}
|
|
57
|
+
else if (input === "F" || input === "f") {
|
|
58
|
+
dispatch({ type: "enter-feed" });
|
|
59
|
+
}
|
|
54
60
|
}
|
|
55
61
|
else if (state.view === "detail") {
|
|
56
62
|
if (key.escape || key.backspace || key.delete) {
|
|
@@ -62,7 +68,18 @@ export function App({ baseUrl, bearerToken, initialIssueKey }) {
|
|
|
62
68
|
else if (input === "r") {
|
|
63
69
|
handleRetry();
|
|
64
70
|
}
|
|
71
|
+
else if (input === "j" || key.downArrow) {
|
|
72
|
+
dispatch({ type: "detail-navigate", direction: "next", filtered });
|
|
73
|
+
}
|
|
74
|
+
else if (input === "k" || key.upArrow) {
|
|
75
|
+
dispatch({ type: "detail-navigate", direction: "prev", filtered });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
else if (state.view === "feed") {
|
|
79
|
+
if (key.escape || key.backspace || key.delete) {
|
|
80
|
+
dispatch({ type: "exit-feed" });
|
|
81
|
+
}
|
|
65
82
|
}
|
|
66
83
|
});
|
|
67
|
-
return (_jsx(Box, { flexDirection: "column", children: state.view === "list" ? (_jsx(IssueListView, { issues: filtered, selectedIndex: state.selectedIndex, connected: state.connected, filter: state.filter, totalCount: state.issues.length })) : (_jsx(IssueDetailView, { issue: state.issues.find((i) => i.issueKey === state.activeDetailKey), timeline: state.timeline, follow: state.follow, activeRunStartedAt: state.activeRunStartedAt, tokenUsage: state.tokenUsage, diffSummary: state.diffSummary, plan: state.plan })) }));
|
|
84
|
+
return (_jsx(Box, { flexDirection: "column", children: state.view === "list" ? (_jsx(IssueListView, { issues: filtered, allIssues: state.issues, selectedIndex: state.selectedIndex, connected: state.connected, filter: state.filter, totalCount: state.issues.length })) : state.view === "detail" ? (_jsx(IssueDetailView, { issue: state.issues.find((i) => i.issueKey === state.activeDetailKey), timeline: state.timeline, follow: state.follow, activeRunStartedAt: state.activeRunStartedAt, tokenUsage: state.tokenUsage, diffSummary: state.diffSummary, plan: state.plan, issueContext: state.issueContext, allIssues: filtered, activeDetailKey: state.activeDetailKey })) : (_jsx(FeedView, { events: state.feedEvents, connected: state.connected })) }));
|
|
68
85
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import { HelpBar } from "./HelpBar.js";
|
|
4
|
+
const TAIL_SIZE = 30;
|
|
5
|
+
const LEVEL_COLORS = {
|
|
6
|
+
info: "white",
|
|
7
|
+
warn: "yellow",
|
|
8
|
+
error: "red",
|
|
9
|
+
};
|
|
10
|
+
const KIND_COLORS = {
|
|
11
|
+
stage: "cyan",
|
|
12
|
+
turn: "yellow",
|
|
13
|
+
github: "green",
|
|
14
|
+
webhook: "blue",
|
|
15
|
+
agent: "magenta",
|
|
16
|
+
service: "white",
|
|
17
|
+
workflow: "cyan",
|
|
18
|
+
linear: "blue",
|
|
19
|
+
};
|
|
20
|
+
function formatTime(iso) {
|
|
21
|
+
return new Date(iso).toLocaleTimeString("en-GB", { hour12: false });
|
|
22
|
+
}
|
|
23
|
+
function FeedEventRow({ event }) {
|
|
24
|
+
const kindColor = KIND_COLORS[event.kind] ?? "white";
|
|
25
|
+
const levelColor = LEVEL_COLORS[event.level] ?? "white";
|
|
26
|
+
return (_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: formatTime(event.at) }), _jsx(Text, { color: kindColor, children: event.kind.padEnd(10) }), event.issueKey && _jsx(Text, { bold: true, children: event.issueKey.padEnd(10) }), event.stage && _jsx(Text, { color: "cyan", children: event.stage.padEnd(16) }), _jsx(Text, { color: levelColor, children: event.summary })] }));
|
|
27
|
+
}
|
|
28
|
+
export function FeedView({ events, connected }) {
|
|
29
|
+
const visible = events.length > TAIL_SIZE ? events.slice(-TAIL_SIZE) : events;
|
|
30
|
+
const skipped = events.length - visible.length;
|
|
31
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { justifyContent: "space-between", children: [_jsx(Text, { bold: true, children: "Operator Feed" }), _jsx(Text, { color: connected ? "green" : "red", children: connected ? "\u25cf connected" : "\u25cb disconnected" })] }), _jsx(Text, { dimColor: true, children: "\u2500".repeat(72) }), events.length === 0 ? (_jsx(Text, { dimColor: true, children: "No feed events yet." })) : (_jsxs(Box, { flexDirection: "column", children: [skipped > 0 && _jsxs(Text, { dimColor: true, children: [" ... ", skipped, " earlier events"] }), visible.map((event) => (_jsx(FeedEventRow, { event: event }, event.id)))] })), _jsx(Text, { dimColor: true, children: "\u2500".repeat(72) }), _jsx(HelpBar, { view: "feed" })] }));
|
|
32
|
+
}
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text } from "ink";
|
|
3
|
+
const HELP_TEXT = {
|
|
4
|
+
list: "j/k: navigate Enter: detail F: feed Tab: filter q: quit",
|
|
5
|
+
detail: "",
|
|
6
|
+
feed: "Esc: list q: quit",
|
|
7
|
+
};
|
|
3
8
|
export function HelpBar({ view, follow }) {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
9
|
+
const text = view === "detail"
|
|
10
|
+
? `j/k: prev/next Esc: list f: follow ${follow ? "on" : "off"} r: retry q: quit`
|
|
11
|
+
: HELP_TEXT[view];
|
|
12
|
+
return (_jsx(Box, { children: _jsx(Text, { dimColor: true, children: text }) }));
|
|
7
13
|
}
|
|
@@ -35,10 +35,76 @@ function planStepColor(status) {
|
|
|
35
35
|
return "yellow";
|
|
36
36
|
return "white";
|
|
37
37
|
}
|
|
38
|
-
|
|
38
|
+
// ─── Compact Issue Sidebar (#4 split-pane) ───────────────────────
|
|
39
|
+
const SIDEBAR_STATE_COLORS = {
|
|
40
|
+
delegated: "blue", preparing: "blue",
|
|
41
|
+
implementing: "yellow", awaiting_input: "yellow",
|
|
42
|
+
pr_open: "cyan", awaiting_review: "cyan",
|
|
43
|
+
changes_requested: "magenta", repairing_ci: "magenta", repairing_queue: "magenta",
|
|
44
|
+
awaiting_queue: "green", done: "green",
|
|
45
|
+
failed: "red", escalated: "red",
|
|
46
|
+
};
|
|
47
|
+
function CompactSidebar({ issues, activeKey }) {
|
|
48
|
+
return (_jsxs(Box, { flexDirection: "column", width: 28, borderStyle: "single", borderColor: "gray", paddingLeft: 1, paddingRight: 1, children: [_jsx(Text, { bold: true, dimColor: true, children: "Issues" }), issues.map((issue) => {
|
|
49
|
+
const key = issue.issueKey ?? issue.projectId;
|
|
50
|
+
const isCurrent = key === activeKey;
|
|
51
|
+
const stateColor = SIDEBAR_STATE_COLORS[issue.factoryState] ?? "white";
|
|
52
|
+
return (_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: isCurrent ? "blueBright" : "white", bold: isCurrent, children: isCurrent ? "\u25b8" : " " }), _jsx(Text, { bold: isCurrent, children: key.padEnd(10) }), _jsx(Text, { color: stateColor, children: issue.factoryState.slice(0, 12) })] }, key));
|
|
53
|
+
})] }));
|
|
54
|
+
}
|
|
55
|
+
// ─── Issue Context Panel (#5) ────────────────────────────────────
|
|
56
|
+
const PRIORITY_LABELS = {
|
|
57
|
+
0: { label: "none", color: "" },
|
|
58
|
+
1: { label: "urgent", color: "red" },
|
|
59
|
+
2: { label: "high", color: "yellow" },
|
|
60
|
+
3: { label: "medium", color: "cyan" },
|
|
61
|
+
4: { label: "low", color: "" },
|
|
62
|
+
};
|
|
63
|
+
function ContextPanel({ issue, ctx }) {
|
|
64
|
+
const parts = [];
|
|
65
|
+
if (ctx.priority != null && ctx.priority > 0) {
|
|
66
|
+
const p = PRIORITY_LABELS[ctx.priority] ?? { label: String(ctx.priority), color: "" };
|
|
67
|
+
parts.push({ label: "priority", value: p.label, color: p.color });
|
|
68
|
+
}
|
|
69
|
+
if (ctx.estimate != null) {
|
|
70
|
+
parts.push({ label: "estimate", value: String(ctx.estimate), color: "" });
|
|
71
|
+
}
|
|
72
|
+
if (ctx.currentLinearState) {
|
|
73
|
+
parts.push({ label: "linear", value: ctx.currentLinearState, color: "" });
|
|
74
|
+
}
|
|
75
|
+
if (issue.prNumber) {
|
|
76
|
+
const prInfo = `#${issue.prNumber}${issue.prReviewState === "approved" ? " \u2713" : issue.prReviewState === "changes_requested" ? " \u2717" : ""}${issue.prCheckStatus ? ` ci:${issue.prCheckStatus}` : ""}`;
|
|
77
|
+
const prColor = issue.prReviewState === "approved" ? "green" : issue.prReviewState === "changes_requested" ? "red" : "";
|
|
78
|
+
parts.push({ label: "pr", value: prInfo, color: prColor });
|
|
79
|
+
}
|
|
80
|
+
if (ctx.runCount > 0) {
|
|
81
|
+
parts.push({ label: "runs", value: String(ctx.runCount), color: "" });
|
|
82
|
+
}
|
|
83
|
+
const retries = [
|
|
84
|
+
ctx.ciRepairAttempts > 0 ? `ci:${ctx.ciRepairAttempts}` : "",
|
|
85
|
+
ctx.queueRepairAttempts > 0 ? `queue:${ctx.queueRepairAttempts}` : "",
|
|
86
|
+
ctx.reviewFixAttempts > 0 ? `review:${ctx.reviewFixAttempts}` : "",
|
|
87
|
+
].filter(Boolean).join(" ");
|
|
88
|
+
if (retries) {
|
|
89
|
+
parts.push({ label: "retries", value: retries, color: "yellow" });
|
|
90
|
+
}
|
|
91
|
+
if (ctx.branchName) {
|
|
92
|
+
parts.push({ label: "branch", value: ctx.branchName, color: "" });
|
|
93
|
+
}
|
|
94
|
+
const hasDescription = Boolean(ctx.description);
|
|
95
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { gap: 2, flexWrap: "wrap", children: parts.map((p) => (_jsxs(Text, { dimColor: true, children: [p.label, ": ", p.color ? _jsx(Text, { color: p.color, children: p.value }) : _jsx(Text, { dimColor: true, children: p.value })] }, p.label))) }), hasDescription && (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [ctx.description.slice(0, 200), ctx.description.length > 200 ? "\u2026" : ""] }))] }));
|
|
96
|
+
}
|
|
97
|
+
// ─── Detail Panel (right side of split) ──────────────────────────
|
|
98
|
+
function DetailPanel({ issue, timeline, follow, activeRunStartedAt, tokenUsage, diffSummary, plan, issueContext, }) {
|
|
39
99
|
if (!issue) {
|
|
40
|
-
return
|
|
100
|
+
return _jsx(Text, { color: "red", children: "Issue not found." });
|
|
41
101
|
}
|
|
42
102
|
const key = issue.issueKey ?? issue.projectId;
|
|
43
|
-
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.activeRunType && _jsx(Text, { color: "yellow", children: issue.activeRunType }), issue.prNumber !== undefined && _jsxs(Text, { dimColor: true, children: ["PR #", issue.prNumber] }), activeRunStartedAt && _jsx(ElapsedTime, { startedAt: activeRunStartedAt })] }), issue.title && _jsx(Text, { dimColor: true, children: issue.title }), _jsxs(Box, { gap: 2, children: [tokenUsage && (_jsxs(Text, { dimColor: true, children: ["tokens: ", formatTokens(tokenUsage.inputTokens), " in / ", formatTokens(tokenUsage.outputTokens), " out"] })), diffSummary && diffSummary.filesChanged > 0 && (_jsxs(Text, { dimColor: true, children: ["diff: ", diffSummary.filesChanged, " file", diffSummary.filesChanged !== 1 ? "s" : "", " ", "+", diffSummary.linesAdded, " -", diffSummary.linesRemoved] })), follow && _jsx(Text, { color: "yellow", children: "follow" })] }), plan && plan.length > 0 && (_jsx(Box, { flexDirection: "column", children: 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(Text, { dimColor: true, children: "
|
|
103
|
+
return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsxs(Box, { gap: 2, children: [_jsx(Text, { bold: true, children: key }), _jsx(Text, { color: "cyan", children: issue.factoryState }), issue.activeRunType && _jsx(Text, { color: "yellow", children: issue.activeRunType }), issue.prNumber !== undefined && _jsxs(Text, { dimColor: true, children: ["PR #", issue.prNumber] }), activeRunStartedAt && _jsx(ElapsedTime, { startedAt: activeRunStartedAt })] }), issue.title && _jsx(Text, { dimColor: true, children: issue.title }), _jsxs(Box, { gap: 2, children: [tokenUsage && (_jsxs(Text, { dimColor: true, children: ["tokens: ", formatTokens(tokenUsage.inputTokens), " in / ", formatTokens(tokenUsage.outputTokens), " out"] })), diffSummary && diffSummary.filesChanged > 0 && (_jsxs(Text, { dimColor: true, children: ["diff: ", diffSummary.filesChanged, " file", diffSummary.filesChanged !== 1 ? "s" : "", " ", "+", diffSummary.linesAdded, " -", diffSummary.linesRemoved] })), follow && _jsx(Text, { color: "yellow", children: "follow" })] }), issueContext && _jsx(ContextPanel, { issue: issue, ctx: issueContext }), plan && plan.length > 0 && (_jsx(Box, { flexDirection: "column", children: 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(Text, { dimColor: true, children: "\u2500".repeat(60) }), _jsx(Timeline, { entries: timeline, follow: follow })] }));
|
|
104
|
+
}
|
|
105
|
+
// ─── Main Detail View (split layout) ─────────────────────────────
|
|
106
|
+
export function IssueDetailView(props) {
|
|
107
|
+
const { allIssues, activeDetailKey, follow, ...detailProps } = props;
|
|
108
|
+
const showSidebar = allIssues.length > 1;
|
|
109
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { flexDirection: "row", flexGrow: 1, children: [showSidebar && _jsx(CompactSidebar, { issues: allIssues, activeKey: activeDetailKey }), _jsx(DetailPanel, { ...detailProps, follow: follow })] }), _jsx(Text, { dimColor: true, children: "\u2500".repeat(72) }), _jsx(HelpBar, { view: "detail", follow: follow })] }));
|
|
44
110
|
}
|
|
@@ -3,6 +3,6 @@ import { Box, Text } from "ink";
|
|
|
3
3
|
import { IssueRow } from "./IssueRow.js";
|
|
4
4
|
import { StatusBar } from "./StatusBar.js";
|
|
5
5
|
import { HelpBar } from "./HelpBar.js";
|
|
6
|
-
export function IssueListView({ issues, selectedIndex, connected, filter, totalCount }) {
|
|
7
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(StatusBar, { issues: issues, totalCount: totalCount, filter: filter, connected: connected }), _jsx(Text, { dimColor: true, children: "
|
|
6
|
+
export function IssueListView({ issues, allIssues, selectedIndex, connected, filter, totalCount }) {
|
|
7
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(StatusBar, { issues: issues, totalCount: totalCount, filter: filter, connected: connected, allIssues: allIssues }), _jsx(Text, { dimColor: true, children: "\u2500".repeat(72) }), issues.length === 0 ? (_jsx(Text, { dimColor: true, children: "No issues match the current filter." })) : (_jsx(Box, { flexDirection: "column", children: issues.map((issue, index) => (_jsx(IssueRow, { issue: issue, selected: index === selectedIndex }, issue.issueKey ?? `${issue.projectId}-${index}`))) })), _jsx(Text, { dimColor: true, children: "\u2500".repeat(72) }), _jsx(HelpBar, { view: "list" })] }));
|
|
8
8
|
}
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text } from "ink";
|
|
3
|
+
import { computeAggregates } from "./watch-state.js";
|
|
3
4
|
const FILTER_LABELS = {
|
|
4
5
|
"all": "all",
|
|
5
6
|
"active": "active",
|
|
6
7
|
"non-done": "in progress",
|
|
7
8
|
};
|
|
8
|
-
export function StatusBar({ issues, totalCount, filter, connected }) {
|
|
9
|
+
export function StatusBar({ issues, totalCount, filter, connected, allIssues }) {
|
|
9
10
|
const showing = filter === "all" ? `${totalCount} issues` : `${issues.length}/${totalCount} issues`;
|
|
10
|
-
|
|
11
|
+
const agg = computeAggregates(allIssues);
|
|
12
|
+
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: "yellow", children: [agg.active, " active"] }), agg.done > 0 && _jsxs(Text, { color: "green", children: [agg.done, " done"] }), agg.failed > 0 && _jsxs(Text, { color: "red", children: [agg.failed, " failed"] })] }), _jsx(Text, { color: connected ? "green" : "red", children: connected ? "\u25cf connected" : "\u25cb disconnected" })] }));
|
|
11
13
|
}
|
|
@@ -38,12 +38,31 @@ async function rehydrate(baseUrl, issueKey, headers, signal, dispatch) {
|
|
|
38
38
|
threadId: r.threadId,
|
|
39
39
|
...(r.report ? { report: r.report } : {}),
|
|
40
40
|
}));
|
|
41
|
+
let issueContext = null;
|
|
42
|
+
if (data.issue) {
|
|
43
|
+
const i = data.issue;
|
|
44
|
+
issueContext = {
|
|
45
|
+
description: typeof i.description === "string" ? i.description : undefined,
|
|
46
|
+
currentLinearState: typeof i.currentLinearState === "string" ? i.currentLinearState : undefined,
|
|
47
|
+
issueUrl: typeof i.issueUrl === "string" ? i.issueUrl : undefined,
|
|
48
|
+
worktreePath: typeof i.worktreePath === "string" ? i.worktreePath : undefined,
|
|
49
|
+
branchName: typeof i.branchName === "string" ? i.branchName : undefined,
|
|
50
|
+
prUrl: typeof i.prUrl === "string" ? i.prUrl : undefined,
|
|
51
|
+
priority: typeof i.priority === "number" ? i.priority : undefined,
|
|
52
|
+
estimate: typeof i.estimate === "number" ? i.estimate : undefined,
|
|
53
|
+
ciRepairAttempts: typeof i.ciRepairAttempts === "number" ? i.ciRepairAttempts : 0,
|
|
54
|
+
queueRepairAttempts: typeof i.queueRepairAttempts === "number" ? i.queueRepairAttempts : 0,
|
|
55
|
+
reviewFixAttempts: typeof i.reviewFixAttempts === "number" ? i.reviewFixAttempts : 0,
|
|
56
|
+
runCount: runs.length,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
41
59
|
dispatch({
|
|
42
60
|
type: "timeline-rehydrate",
|
|
43
61
|
runs,
|
|
44
62
|
feedEvents: data.feedEvents ?? [],
|
|
45
63
|
liveThread: data.liveThread ?? null,
|
|
46
64
|
activeRunId: data.activeRunId ?? null,
|
|
65
|
+
issueContext,
|
|
47
66
|
});
|
|
48
67
|
}
|
|
49
68
|
catch {
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { useEffect, useRef } from "react";
|
|
2
|
+
export function useFeedStream(options) {
|
|
3
|
+
const optionsRef = useRef(options);
|
|
4
|
+
optionsRef.current = options;
|
|
5
|
+
useEffect(() => {
|
|
6
|
+
if (!options.active)
|
|
7
|
+
return;
|
|
8
|
+
const abortController = new AbortController();
|
|
9
|
+
const { baseUrl, bearerToken, dispatch } = optionsRef.current;
|
|
10
|
+
void (async () => {
|
|
11
|
+
try {
|
|
12
|
+
const url = new URL("/api/feed", baseUrl);
|
|
13
|
+
url.searchParams.set("follow", "1");
|
|
14
|
+
url.searchParams.set("limit", "100");
|
|
15
|
+
const headers = { accept: "text/event-stream" };
|
|
16
|
+
if (bearerToken)
|
|
17
|
+
headers.authorization = `Bearer ${bearerToken}`;
|
|
18
|
+
const response = await fetch(url, { headers, signal: abortController.signal });
|
|
19
|
+
if (!response.ok || !response.body)
|
|
20
|
+
return;
|
|
21
|
+
const reader = response.body.getReader();
|
|
22
|
+
const decoder = new TextDecoder();
|
|
23
|
+
let buffer = "";
|
|
24
|
+
let eventType = "";
|
|
25
|
+
let dataLines = [];
|
|
26
|
+
let initialBatch = [];
|
|
27
|
+
let snapshotSent = false;
|
|
28
|
+
while (true) {
|
|
29
|
+
const { done, value } = await reader.read();
|
|
30
|
+
if (done)
|
|
31
|
+
break;
|
|
32
|
+
buffer += decoder.decode(value, { stream: true });
|
|
33
|
+
let newlineIndex = buffer.indexOf("\n");
|
|
34
|
+
while (newlineIndex !== -1) {
|
|
35
|
+
const rawLine = buffer.slice(0, newlineIndex);
|
|
36
|
+
buffer = buffer.slice(newlineIndex + 1);
|
|
37
|
+
const line = rawLine.endsWith("\r") ? rawLine.slice(0, -1) : rawLine;
|
|
38
|
+
if (!line) {
|
|
39
|
+
if (dataLines.length > 0 && eventType === "feed") {
|
|
40
|
+
try {
|
|
41
|
+
const event = JSON.parse(dataLines.join("\n"));
|
|
42
|
+
if (!snapshotSent) {
|
|
43
|
+
initialBatch.push(event);
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
dispatch({ type: "feed-new-event", event });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
catch { /* ignore parse errors */ }
|
|
50
|
+
dataLines = [];
|
|
51
|
+
eventType = "";
|
|
52
|
+
}
|
|
53
|
+
// After processing a batch of initial events, flush snapshot
|
|
54
|
+
if (!snapshotSent && initialBatch.length > 0) {
|
|
55
|
+
// Use a microtask to batch initial events
|
|
56
|
+
const batch = initialBatch;
|
|
57
|
+
initialBatch = [];
|
|
58
|
+
snapshotSent = true;
|
|
59
|
+
dispatch({ type: "feed-snapshot", events: batch });
|
|
60
|
+
}
|
|
61
|
+
newlineIndex = buffer.indexOf("\n");
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (line.startsWith(":")) {
|
|
65
|
+
// Keepalive or comment - flush initial batch if pending
|
|
66
|
+
if (!snapshotSent && initialBatch.length > 0) {
|
|
67
|
+
snapshotSent = true;
|
|
68
|
+
dispatch({ type: "feed-snapshot", events: initialBatch });
|
|
69
|
+
initialBatch = [];
|
|
70
|
+
}
|
|
71
|
+
newlineIndex = buffer.indexOf("\n");
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (line.startsWith("event:")) {
|
|
75
|
+
eventType = line.slice(6).trim();
|
|
76
|
+
}
|
|
77
|
+
else if (line.startsWith("data:")) {
|
|
78
|
+
dataLines.push(line.slice(5).trimStart());
|
|
79
|
+
}
|
|
80
|
+
newlineIndex = buffer.indexOf("\n");
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
// Stream ended or aborted
|
|
86
|
+
}
|
|
87
|
+
})();
|
|
88
|
+
return () => {
|
|
89
|
+
abortController.abort();
|
|
90
|
+
};
|
|
91
|
+
}, [options.active]);
|
|
92
|
+
}
|
|
@@ -6,6 +6,7 @@ const DETAIL_INITIAL = {
|
|
|
6
6
|
tokenUsage: null,
|
|
7
7
|
diffSummary: null,
|
|
8
8
|
plan: null,
|
|
9
|
+
issueContext: null,
|
|
9
10
|
};
|
|
10
11
|
export const initialWatchState = {
|
|
11
12
|
connected: false,
|
|
@@ -16,6 +17,7 @@ export const initialWatchState = {
|
|
|
16
17
|
filter: "non-done",
|
|
17
18
|
follow: true,
|
|
18
19
|
...DETAIL_INITIAL,
|
|
20
|
+
feedEvents: [],
|
|
19
21
|
};
|
|
20
22
|
const TERMINAL_FACTORY_STATES = new Set(["done", "failed"]);
|
|
21
23
|
export function filterIssues(issues, filter) {
|
|
@@ -28,6 +30,22 @@ export function filterIssues(issues, filter) {
|
|
|
28
30
|
return issues.filter((i) => !TERMINAL_FACTORY_STATES.has(i.factoryState));
|
|
29
31
|
}
|
|
30
32
|
}
|
|
33
|
+
const DONE_STATES = new Set(["done"]);
|
|
34
|
+
const FAILED_STATES = new Set(["failed", "escalated"]);
|
|
35
|
+
export function computeAggregates(issues) {
|
|
36
|
+
let active = 0;
|
|
37
|
+
let done = 0;
|
|
38
|
+
let failed = 0;
|
|
39
|
+
for (const issue of issues) {
|
|
40
|
+
if (issue.activeRunType)
|
|
41
|
+
active++;
|
|
42
|
+
if (DONE_STATES.has(issue.factoryState))
|
|
43
|
+
done++;
|
|
44
|
+
if (FAILED_STATES.has(issue.factoryState))
|
|
45
|
+
failed++;
|
|
46
|
+
}
|
|
47
|
+
return { active, done, failed, total: issues.length };
|
|
48
|
+
}
|
|
31
49
|
function nextFilter(filter) {
|
|
32
50
|
switch (filter) {
|
|
33
51
|
case "non-done": return "active";
|
|
@@ -59,6 +77,19 @@ export function watchReducer(state, action) {
|
|
|
59
77
|
return { ...state, view: "detail", activeDetailKey: action.issueKey, ...DETAIL_INITIAL };
|
|
60
78
|
case "exit-detail":
|
|
61
79
|
return { ...state, view: "list", activeDetailKey: null, ...DETAIL_INITIAL };
|
|
80
|
+
case "detail-navigate": {
|
|
81
|
+
const list = action.filtered;
|
|
82
|
+
if (list.length === 0)
|
|
83
|
+
return state;
|
|
84
|
+
const curIdx = list.findIndex((i) => i.issueKey === state.activeDetailKey);
|
|
85
|
+
const nextIdx = action.direction === "next"
|
|
86
|
+
? (curIdx + 1) % list.length
|
|
87
|
+
: (curIdx - 1 + list.length) % list.length;
|
|
88
|
+
const nextIssue = list[nextIdx];
|
|
89
|
+
if (!nextIssue?.issueKey || nextIssue.issueKey === state.activeDetailKey)
|
|
90
|
+
return state;
|
|
91
|
+
return { ...state, activeDetailKey: nextIssue.issueKey, selectedIndex: nextIdx, ...DETAIL_INITIAL };
|
|
92
|
+
}
|
|
62
93
|
case "timeline-rehydrate": {
|
|
63
94
|
const timeline = buildTimelineFromRehydration(action.runs, action.feedEvents, action.liveThread, action.activeRunId);
|
|
64
95
|
const activeRun = action.runs.find((r) => r.id === action.activeRunId);
|
|
@@ -67,6 +98,7 @@ export function watchReducer(state, action) {
|
|
|
67
98
|
timeline,
|
|
68
99
|
activeRunId: action.activeRunId,
|
|
69
100
|
activeRunStartedAt: activeRun?.startedAt ?? null,
|
|
101
|
+
issueContext: action.issueContext,
|
|
70
102
|
};
|
|
71
103
|
}
|
|
72
104
|
case "codex-notification":
|
|
@@ -75,6 +107,14 @@ export function watchReducer(state, action) {
|
|
|
75
107
|
return { ...state, filter: nextFilter(state.filter), selectedIndex: 0 };
|
|
76
108
|
case "toggle-follow":
|
|
77
109
|
return { ...state, follow: !state.follow };
|
|
110
|
+
case "enter-feed":
|
|
111
|
+
return { ...state, view: "feed", activeDetailKey: null, ...DETAIL_INITIAL };
|
|
112
|
+
case "exit-feed":
|
|
113
|
+
return { ...state, view: "list" };
|
|
114
|
+
case "feed-snapshot":
|
|
115
|
+
return { ...state, feedEvents: action.events };
|
|
116
|
+
case "feed-new-event":
|
|
117
|
+
return { ...state, feedEvents: [...state.feedEvents, action.event] };
|
|
78
118
|
}
|
|
79
119
|
}
|
|
80
120
|
// ─── Feed Event → Issue List + Timeline ───────────────────────────
|
package/dist/db/migrations.js
CHANGED
|
@@ -133,6 +133,14 @@ export function runPatchRelayMigrations(connection) {
|
|
|
133
133
|
addColumnIfMissing(connection, "issues", "pending_merge_prep", "INTEGER NOT NULL DEFAULT 0");
|
|
134
134
|
// Add merge_prep_attempts for retry budget / escalation
|
|
135
135
|
addColumnIfMissing(connection, "issues", "merge_prep_attempts", "INTEGER NOT NULL DEFAULT 0");
|
|
136
|
+
// Add review_fix_attempts counter
|
|
137
|
+
addColumnIfMissing(connection, "issues", "review_fix_attempts", "INTEGER NOT NULL DEFAULT 0");
|
|
138
|
+
// Collapse awaiting_review into pr_open (state normalization)
|
|
139
|
+
connection.prepare("UPDATE issues SET factory_state = 'pr_open' WHERE factory_state = 'awaiting_review'").run();
|
|
140
|
+
// Add Linear issue description, priority, estimate
|
|
141
|
+
addColumnIfMissing(connection, "issues", "description", "TEXT");
|
|
142
|
+
addColumnIfMissing(connection, "issues", "priority", "INTEGER");
|
|
143
|
+
addColumnIfMissing(connection, "issues", "estimate", "REAL");
|
|
136
144
|
}
|
|
137
145
|
function addColumnIfMissing(connection, table, column, definition) {
|
|
138
146
|
const cols = connection.prepare(`PRAGMA table_info(${table})`).all();
|
package/dist/db.js
CHANGED
|
@@ -81,10 +81,22 @@ export class PatchRelayDatabase {
|
|
|
81
81
|
sets.push("title = COALESCE(@title, title)");
|
|
82
82
|
values.title = params.title;
|
|
83
83
|
}
|
|
84
|
+
if (params.description !== undefined) {
|
|
85
|
+
sets.push("description = COALESCE(@description, description)");
|
|
86
|
+
values.description = params.description;
|
|
87
|
+
}
|
|
84
88
|
if (params.url !== undefined) {
|
|
85
89
|
sets.push("url = COALESCE(@url, url)");
|
|
86
90
|
values.url = params.url;
|
|
87
91
|
}
|
|
92
|
+
if (params.priority !== undefined) {
|
|
93
|
+
sets.push("priority = @priority");
|
|
94
|
+
values.priority = params.priority;
|
|
95
|
+
}
|
|
96
|
+
if (params.estimate !== undefined) {
|
|
97
|
+
sets.push("estimate = @estimate");
|
|
98
|
+
values.estimate = params.estimate;
|
|
99
|
+
}
|
|
88
100
|
if (params.currentLinearState !== undefined) {
|
|
89
101
|
sets.push("current_linear_state = COALESCE(@currentLinearState, current_linear_state)");
|
|
90
102
|
values.currentLinearState = params.currentLinearState;
|
|
@@ -149,6 +161,10 @@ export class PatchRelayDatabase {
|
|
|
149
161
|
sets.push("queue_repair_attempts = @queueRepairAttempts");
|
|
150
162
|
values.queueRepairAttempts = params.queueRepairAttempts;
|
|
151
163
|
}
|
|
164
|
+
if (params.reviewFixAttempts !== undefined) {
|
|
165
|
+
sets.push("review_fix_attempts = @reviewFixAttempts");
|
|
166
|
+
values.reviewFixAttempts = params.reviewFixAttempts;
|
|
167
|
+
}
|
|
152
168
|
if (params.mergePrepAttempts !== undefined) {
|
|
153
169
|
sets.push("merge_prep_attempts = @mergePrepAttempts");
|
|
154
170
|
values.mergePrepAttempts = params.mergePrepAttempts;
|
|
@@ -162,13 +178,15 @@ export class PatchRelayDatabase {
|
|
|
162
178
|
else {
|
|
163
179
|
this.connection.prepare(`
|
|
164
180
|
INSERT INTO issues (
|
|
165
|
-
project_id, linear_issue_id, issue_key, title, url,
|
|
181
|
+
project_id, linear_issue_id, issue_key, title, description, url,
|
|
182
|
+
priority, estimate,
|
|
166
183
|
current_linear_state, factory_state, pending_run_type, pending_run_context_json,
|
|
167
184
|
branch_name, worktree_path, thread_id, active_run_id,
|
|
168
185
|
agent_session_id,
|
|
169
186
|
updated_at
|
|
170
187
|
) VALUES (
|
|
171
|
-
@projectId, @linearIssueId, @issueKey, @title, @url,
|
|
188
|
+
@projectId, @linearIssueId, @issueKey, @title, @description, @url,
|
|
189
|
+
@priority, @estimate,
|
|
172
190
|
@currentLinearState, @factoryState, @pendingRunType, @pendingRunContextJson,
|
|
173
191
|
@branchName, @worktreePath, @threadId, @activeRunId,
|
|
174
192
|
@agentSessionId,
|
|
@@ -179,7 +197,10 @@ export class PatchRelayDatabase {
|
|
|
179
197
|
linearIssueId: params.linearIssueId,
|
|
180
198
|
issueKey: params.issueKey ?? null,
|
|
181
199
|
title: params.title ?? null,
|
|
200
|
+
description: params.description ?? null,
|
|
182
201
|
url: params.url ?? null,
|
|
202
|
+
priority: params.priority ?? null,
|
|
203
|
+
estimate: params.estimate ?? null,
|
|
183
204
|
currentLinearState: params.currentLinearState ?? null,
|
|
184
205
|
factoryState: params.factoryState ?? "delegated",
|
|
185
206
|
pendingRunType: params.pendingRunType ?? null,
|
|
@@ -371,7 +392,10 @@ function mapIssueRow(row) {
|
|
|
371
392
|
linearIssueId: String(row.linear_issue_id),
|
|
372
393
|
...(row.issue_key !== null ? { issueKey: String(row.issue_key) } : {}),
|
|
373
394
|
...(row.title !== null ? { title: String(row.title) } : {}),
|
|
395
|
+
...(row.description !== null && row.description !== undefined ? { description: String(row.description) } : {}),
|
|
374
396
|
...(row.url !== null ? { url: String(row.url) } : {}),
|
|
397
|
+
...(row.priority !== null && row.priority !== undefined ? { priority: Number(row.priority) } : {}),
|
|
398
|
+
...(row.estimate !== null && row.estimate !== undefined ? { estimate: Number(row.estimate) } : {}),
|
|
375
399
|
...(row.current_linear_state !== null ? { currentLinearState: String(row.current_linear_state) } : {}),
|
|
376
400
|
factoryState: String(row.factory_state ?? "delegated"),
|
|
377
401
|
...(row.pending_run_type !== null && row.pending_run_type !== undefined ? { pendingRunType: String(row.pending_run_type) } : {}),
|
|
@@ -389,6 +413,7 @@ function mapIssueRow(row) {
|
|
|
389
413
|
...(row.pr_check_status !== null && row.pr_check_status !== undefined ? { prCheckStatus: String(row.pr_check_status) } : {}),
|
|
390
414
|
ciRepairAttempts: Number(row.ci_repair_attempts ?? 0),
|
|
391
415
|
queueRepairAttempts: Number(row.queue_repair_attempts ?? 0),
|
|
416
|
+
reviewFixAttempts: Number(row.review_fix_attempts ?? 0),
|
|
392
417
|
mergePrepAttempts: Number(row.merge_prep_attempts ?? 0),
|
|
393
418
|
pendingMergePrep: Boolean(row.pending_merge_prep),
|
|
394
419
|
};
|
package/dist/factory-state.js
CHANGED
|
@@ -5,10 +5,12 @@ export const ACTIVE_RUN_STATES = new Set([
|
|
|
5
5
|
"changes_requested",
|
|
6
6
|
"repairing_queue",
|
|
7
7
|
]);
|
|
8
|
-
/** Which factory states are terminal (no further transitions possible). */
|
|
8
|
+
/** Which factory states are terminal (no further transitions possible except pr_merged → done). */
|
|
9
9
|
export const TERMINAL_STATES = new Set([
|
|
10
10
|
"done",
|
|
11
11
|
"escalated",
|
|
12
|
+
"failed",
|
|
13
|
+
"awaiting_input",
|
|
12
14
|
]);
|
|
13
15
|
// ─── Semantic guards ─────────────────────────────────────────────
|
|
14
16
|
//
|
|
@@ -158,8 +158,11 @@ export class GitHubWebhookHandler {
|
|
|
158
158
|
}
|
|
159
159
|
}
|
|
160
160
|
}
|
|
161
|
-
//
|
|
162
|
-
|
|
161
|
+
// Re-read issue after all upserts so reactive run logic sees current state
|
|
162
|
+
const freshIssue = this.db.getIssue(issue.projectId, issue.linearIssueId) ?? issue;
|
|
163
|
+
// Reset repair counters on new push — but only when no repair run is active,
|
|
164
|
+
// since Codex pushes during repair and resetting mid-run would bypass budgets.
|
|
165
|
+
if (event.triggerEvent === "pr_synchronize" && !freshIssue.activeRunId) {
|
|
163
166
|
this.db.upsertIssue({
|
|
164
167
|
projectId: issue.projectId,
|
|
165
168
|
linearIssueId: issue.linearIssueId,
|
|
@@ -167,8 +170,6 @@ export class GitHubWebhookHandler {
|
|
|
167
170
|
queueRepairAttempts: 0,
|
|
168
171
|
});
|
|
169
172
|
}
|
|
170
|
-
// Re-read issue after all upserts so reactive run logic sees current state
|
|
171
|
-
const freshIssue = this.db.getIssue(issue.projectId, issue.linearIssueId) ?? issue;
|
|
172
173
|
this.logger.info({ issueKey: issue.issueKey, branchName: event.branchName, triggerEvent: event.triggerEvent, prNumber: event.prNumber }, "GitHub webhook: updated issue PR state");
|
|
173
174
|
this.feed?.publish({
|
|
174
175
|
level: event.triggerEvent.includes("failed") ? "warn" : "info",
|
|
@@ -79,7 +79,24 @@ export class IssueQueryService {
|
|
|
79
79
|
if (activeRun?.threadId) {
|
|
80
80
|
liveThread = await this.codex.readThread(activeRun.threadId, true).catch(() => undefined);
|
|
81
81
|
}
|
|
82
|
-
return {
|
|
82
|
+
return {
|
|
83
|
+
issue: {
|
|
84
|
+
...issue,
|
|
85
|
+
...(fullIssue?.description ? { description: fullIssue.description } : {}),
|
|
86
|
+
...(fullIssue?.branchName ? { branchName: fullIssue.branchName } : {}),
|
|
87
|
+
...(fullIssue?.worktreePath ? { worktreePath: fullIssue.worktreePath } : {}),
|
|
88
|
+
...(fullIssue?.prUrl ? { prUrl: fullIssue.prUrl } : {}),
|
|
89
|
+
...(fullIssue?.priority != null ? { priority: fullIssue.priority } : {}),
|
|
90
|
+
...(fullIssue?.estimate != null ? { estimate: fullIssue.estimate } : {}),
|
|
91
|
+
ciRepairAttempts: fullIssue?.ciRepairAttempts ?? 0,
|
|
92
|
+
queueRepairAttempts: fullIssue?.queueRepairAttempts ?? 0,
|
|
93
|
+
reviewFixAttempts: fullIssue?.reviewFixAttempts ?? 0,
|
|
94
|
+
},
|
|
95
|
+
runs,
|
|
96
|
+
feedEvents,
|
|
97
|
+
liveThread,
|
|
98
|
+
activeRunId,
|
|
99
|
+
};
|
|
83
100
|
}
|
|
84
101
|
async getActiveRunStatus(issueKey) {
|
|
85
102
|
return await this.runStatusProvider.getActiveRunStatus(issueKey);
|
package/dist/linear-client.js
CHANGED
|
@@ -14,7 +14,10 @@ export class LinearGraphqlClient {
|
|
|
14
14
|
id
|
|
15
15
|
identifier
|
|
16
16
|
title
|
|
17
|
+
description
|
|
17
18
|
url
|
|
19
|
+
priority
|
|
20
|
+
estimate
|
|
18
21
|
delegate {
|
|
19
22
|
id
|
|
20
23
|
name
|
|
@@ -68,7 +71,10 @@ export class LinearGraphqlClient {
|
|
|
68
71
|
id
|
|
69
72
|
identifier
|
|
70
73
|
title
|
|
74
|
+
description
|
|
71
75
|
url
|
|
76
|
+
priority
|
|
77
|
+
estimate
|
|
72
78
|
delegate {
|
|
73
79
|
id
|
|
74
80
|
name
|
|
@@ -205,7 +211,10 @@ export class LinearGraphqlClient {
|
|
|
205
211
|
id
|
|
206
212
|
identifier
|
|
207
213
|
title
|
|
214
|
+
description
|
|
208
215
|
url
|
|
216
|
+
priority
|
|
217
|
+
estimate
|
|
209
218
|
state {
|
|
210
219
|
id
|
|
211
220
|
name
|
|
@@ -293,7 +302,10 @@ export class LinearGraphqlClient {
|
|
|
293
302
|
id: issue.id,
|
|
294
303
|
...(issue.identifier ? { identifier: issue.identifier } : {}),
|
|
295
304
|
...(issue.title ? { title: issue.title } : {}),
|
|
305
|
+
...(issue.description ? { description: issue.description } : {}),
|
|
296
306
|
...(issue.url ? { url: issue.url } : {}),
|
|
307
|
+
...(issue.priority != null ? { priority: issue.priority } : {}),
|
|
308
|
+
...(issue.estimate != null ? { estimate: issue.estimate } : {}),
|
|
297
309
|
...(issue.state?.id ? { stateId: issue.state.id } : {}),
|
|
298
310
|
...(issue.state?.name ? { stateName: issue.state.name } : {}),
|
|
299
311
|
...(issue.team?.id ? { teamId: issue.team.id } : {}),
|
|
@@ -13,7 +13,6 @@ function describeNextState(state, prNumber) {
|
|
|
13
13
|
const prLabel = prNumber ? `PR #${prNumber}` : "the pull request";
|
|
14
14
|
switch (state) {
|
|
15
15
|
case "pr_open":
|
|
16
|
-
case "awaiting_review":
|
|
17
16
|
return `${prLabel} is ready for review.`;
|
|
18
17
|
case "awaiting_queue":
|
|
19
18
|
return `${prLabel} is approved and back in the merge flow.`;
|
|
@@ -149,7 +148,6 @@ export function buildMergePrepEscalationActivity(attempts) {
|
|
|
149
148
|
}
|
|
150
149
|
export function summarizeIssueStateForLinear(issue) {
|
|
151
150
|
switch (issue.factoryState) {
|
|
152
|
-
case "awaiting_review":
|
|
153
151
|
case "pr_open":
|
|
154
152
|
return issue.prNumber ? `PR #${issue.prNumber} is awaiting review.` : "Awaiting review.";
|
|
155
153
|
case "awaiting_queue":
|
package/dist/merge-queue.js
CHANGED
|
@@ -103,7 +103,6 @@ export class MergeQueue {
|
|
|
103
103
|
pendingRunType: "queue_repair",
|
|
104
104
|
pendingRunContextJson: JSON.stringify({ failureReason: "merge_conflict" }),
|
|
105
105
|
pendingMergePrep: false,
|
|
106
|
-
mergePrepAttempts: 0,
|
|
107
106
|
});
|
|
108
107
|
this.enqueueIssue(issue.projectId, issue.linearIssueId);
|
|
109
108
|
this.feed?.publish({
|
package/dist/run-orchestrator.js
CHANGED
|
@@ -9,8 +9,9 @@ import { buildAgentSessionExternalUrls } from "./agent-session-presentation.js";
|
|
|
9
9
|
import { WorktreeManager } from "./worktree-manager.js";
|
|
10
10
|
import { resolveAuthoritativeLinearStopState } from "./linear-workflow.js";
|
|
11
11
|
import { execCommand } from "./utils.js";
|
|
12
|
-
const DEFAULT_CI_REPAIR_BUDGET =
|
|
13
|
-
const DEFAULT_QUEUE_REPAIR_BUDGET =
|
|
12
|
+
const DEFAULT_CI_REPAIR_BUDGET = 3;
|
|
13
|
+
const DEFAULT_QUEUE_REPAIR_BUDGET = 3;
|
|
14
|
+
const DEFAULT_REVIEW_FIX_BUDGET = 3;
|
|
14
15
|
function slugify(value) {
|
|
15
16
|
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60);
|
|
16
17
|
}
|
|
@@ -114,6 +115,10 @@ export class RunOrchestrator {
|
|
|
114
115
|
this.escalate(issue, runType, `Queue repair budget exhausted (${DEFAULT_QUEUE_REPAIR_BUDGET} attempts)`);
|
|
115
116
|
return;
|
|
116
117
|
}
|
|
118
|
+
if (runType === "review_fix" && issue.reviewFixAttempts >= DEFAULT_REVIEW_FIX_BUDGET) {
|
|
119
|
+
this.escalate(issue, runType, `Review fix budget exhausted (${DEFAULT_REVIEW_FIX_BUDGET} attempts)`);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
117
122
|
// Increment repair counters
|
|
118
123
|
if (runType === "ci_repair") {
|
|
119
124
|
this.db.upsertIssue({ projectId: issue.projectId, linearIssueId: issue.linearIssueId, ciRepairAttempts: issue.ciRepairAttempts + 1 });
|
|
@@ -121,6 +126,9 @@ export class RunOrchestrator {
|
|
|
121
126
|
if (runType === "queue_repair") {
|
|
122
127
|
this.db.upsertIssue({ projectId: issue.projectId, linearIssueId: issue.linearIssueId, queueRepairAttempts: issue.queueRepairAttempts + 1 });
|
|
123
128
|
}
|
|
129
|
+
if (runType === "review_fix") {
|
|
130
|
+
this.db.upsertIssue({ projectId: issue.projectId, linearIssueId: issue.linearIssueId, reviewFixAttempts: issue.reviewFixAttempts + 1 });
|
|
131
|
+
}
|
|
124
132
|
// Build prompt
|
|
125
133
|
const prompt = buildRunPrompt(issue, runType, project.repoPath, context);
|
|
126
134
|
// Resolve workspace
|
|
@@ -296,22 +304,9 @@ export class RunOrchestrator {
|
|
|
296
304
|
// Complete the run
|
|
297
305
|
const trackedIssue = this.db.issueToTrackedIssue(issue);
|
|
298
306
|
const report = buildStageReport(run, trackedIssue, thread, countEventMethods(this.db.listThreadEvents(run.id)));
|
|
299
|
-
// Determine post-run state
|
|
300
|
-
// and makes no changes, no pr_opened webhook arrives — the state would
|
|
301
|
-
// stay in the active-run state forever. Advance based on PR metadata.
|
|
307
|
+
// Determine post-run state based on current PR metadata.
|
|
302
308
|
const freshIssue = this.db.getIssue(run.projectId, run.linearIssueId) ?? issue;
|
|
303
|
-
|
|
304
|
-
if (ACTIVE_RUN_STATES.has(freshIssue.factoryState) && freshIssue.prNumber) {
|
|
305
|
-
if (freshIssue.prReviewState === "approved") {
|
|
306
|
-
postRunState = "awaiting_queue";
|
|
307
|
-
}
|
|
308
|
-
else if (freshIssue.prState === "merged") {
|
|
309
|
-
postRunState = "done";
|
|
310
|
-
}
|
|
311
|
-
else {
|
|
312
|
-
postRunState = "awaiting_review";
|
|
313
|
-
}
|
|
314
|
-
}
|
|
309
|
+
const postRunState = resolvePostRunState(freshIssue);
|
|
315
310
|
this.db.transaction(() => {
|
|
316
311
|
this.db.finishRun(run.id, {
|
|
317
312
|
status: "completed",
|
|
@@ -544,6 +539,8 @@ export class RunOrchestrator {
|
|
|
544
539
|
if (latestTurn?.status === "completed") {
|
|
545
540
|
const trackedIssue = this.db.issueToTrackedIssue(issue);
|
|
546
541
|
const report = buildStageReport(run, trackedIssue, thread, countEventMethods(this.db.listThreadEvents(run.id)));
|
|
542
|
+
const freshIssue = this.db.getIssue(run.projectId, run.linearIssueId) ?? issue;
|
|
543
|
+
const postRunState = resolvePostRunState(freshIssue);
|
|
547
544
|
this.db.transaction(() => {
|
|
548
545
|
this.db.finishRun(run.id, {
|
|
549
546
|
status: "completed",
|
|
@@ -556,8 +553,13 @@ export class RunOrchestrator {
|
|
|
556
553
|
projectId: run.projectId,
|
|
557
554
|
linearIssueId: run.linearIssueId,
|
|
558
555
|
activeRunId: null,
|
|
556
|
+
...(postRunState ? { factoryState: postRunState } : {}),
|
|
557
|
+
...(postRunState === "awaiting_queue" ? { pendingMergePrep: true } : {}),
|
|
559
558
|
});
|
|
560
559
|
});
|
|
560
|
+
if (postRunState === "awaiting_queue") {
|
|
561
|
+
this.enqueueIssue(run.projectId, run.linearIssueId);
|
|
562
|
+
}
|
|
561
563
|
}
|
|
562
564
|
}
|
|
563
565
|
// ─── Internal helpers ─────────────────────────────────────────────
|
|
@@ -660,3 +662,17 @@ export class RunOrchestrator {
|
|
|
660
662
|
throw new Error(`Failed to read thread ${threadId}`);
|
|
661
663
|
}
|
|
662
664
|
}
|
|
665
|
+
/**
|
|
666
|
+
* Determine post-run factory state from current PR metadata.
|
|
667
|
+
* Used by both the normal completion path and reconciliation.
|
|
668
|
+
*/
|
|
669
|
+
function resolvePostRunState(issue) {
|
|
670
|
+
if (ACTIVE_RUN_STATES.has(issue.factoryState) && issue.prNumber) {
|
|
671
|
+
if (issue.prReviewState === "approved")
|
|
672
|
+
return "awaiting_queue";
|
|
673
|
+
if (issue.prState === "merged")
|
|
674
|
+
return "done";
|
|
675
|
+
return "pr_open";
|
|
676
|
+
}
|
|
677
|
+
return undefined;
|
|
678
|
+
}
|
package/dist/webhook-handler.js
CHANGED
|
@@ -155,7 +155,10 @@ export class WebhookHandler {
|
|
|
155
155
|
linearIssueId: normalizedIssue.id,
|
|
156
156
|
...(normalizedIssue.identifier ? { issueKey: normalizedIssue.identifier } : {}),
|
|
157
157
|
...(normalizedIssue.title ? { title: normalizedIssue.title } : {}),
|
|
158
|
+
...(normalizedIssue.description ? { description: normalizedIssue.description } : {}),
|
|
158
159
|
...(normalizedIssue.url ? { url: normalizedIssue.url } : {}),
|
|
160
|
+
...(normalizedIssue.priority != null ? { priority: normalizedIssue.priority } : {}),
|
|
161
|
+
...(normalizedIssue.estimate != null ? { estimate: normalizedIssue.estimate } : {}),
|
|
159
162
|
...(normalizedIssue.stateName ? { currentLinearState: normalizedIssue.stateName } : {}),
|
|
160
163
|
...(pendingRunType ? { pendingRunType, factoryState: "delegated" } : {}),
|
|
161
164
|
...((pendingRunType || existingIssue?.pendingRunType === "implementation") && pendingRunContextJson
|
package/dist/webhooks.js
CHANGED
|
@@ -207,10 +207,16 @@ function extractIssueMetadata(payload) {
|
|
|
207
207
|
const stateType = getString(stateRecord ?? {}, "type");
|
|
208
208
|
const delegateId = getString(issueRecord, "delegateId") ?? getString(delegateRecord ?? {}, "id");
|
|
209
209
|
const delegateName = getString(delegateRecord ?? {}, "name");
|
|
210
|
+
const description = getString(issueRecord, "description");
|
|
211
|
+
const rawPriority = issueRecord.priority;
|
|
212
|
+
const priority = typeof rawPriority === "number" ? rawPriority : undefined;
|
|
213
|
+
const rawEstimate = issueRecord.estimate;
|
|
214
|
+
const estimate = typeof rawEstimate === "number" ? rawEstimate : undefined;
|
|
210
215
|
return {
|
|
211
216
|
id,
|
|
212
217
|
...(identifier ? { identifier } : {}),
|
|
213
218
|
...(title ? { title } : {}),
|
|
219
|
+
...(description ? { description } : {}),
|
|
214
220
|
...(url ? { url } : {}),
|
|
215
221
|
...(teamId ? { teamId } : {}),
|
|
216
222
|
...(teamKey ? { teamKey } : {}),
|
|
@@ -219,6 +225,8 @@ function extractIssueMetadata(payload) {
|
|
|
219
225
|
...(stateType ? { stateType } : {}),
|
|
220
226
|
...(delegateId ? { delegateId } : {}),
|
|
221
227
|
...(delegateName ? { delegateName } : {}),
|
|
228
|
+
...(priority != null ? { priority } : {}),
|
|
229
|
+
...(estimate != null ? { estimate } : {}),
|
|
222
230
|
labelNames: extractLabelNames(issueRecord),
|
|
223
231
|
};
|
|
224
232
|
}
|