patchrelay 0.36.17 → 0.36.19

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.
@@ -0,0 +1,152 @@
1
+ import { extractCompletionCheck } from "./completion-check.js";
2
+ import { deriveIssueStatusNote } from "./status-note.js";
3
+ import { derivePatchRelayWaitingReason } from "./waiting-reason.js";
4
+ export async function syncVisibleStatusComment(params) {
5
+ const { db, issue, linear, logger, trackedIssue, options } = params;
6
+ try {
7
+ const body = renderStatusComment(db, issue, trackedIssue, options);
8
+ const result = await linear.upsertIssueComment({
9
+ issueId: issue.linearIssueId,
10
+ ...(issue.statusCommentId ? { commentId: issue.statusCommentId } : {}),
11
+ body,
12
+ });
13
+ if (result.id !== issue.statusCommentId) {
14
+ db.issues.upsertIssue({
15
+ projectId: issue.projectId,
16
+ linearIssueId: issue.linearIssueId,
17
+ statusCommentId: result.id,
18
+ });
19
+ }
20
+ }
21
+ catch (error) {
22
+ const msg = error instanceof Error ? error.message : String(error);
23
+ logger.warn({ issueKey: issue.issueKey, error: msg }, "Failed to sync Linear status comment");
24
+ }
25
+ }
26
+ export function shouldSyncVisibleIssueComment(issue, hasAgentSession) {
27
+ if (!hasAgentSession) {
28
+ return true;
29
+ }
30
+ if (issue.sessionState === "waiting_input" || issue.sessionState === "failed"
31
+ || issue.factoryState === "awaiting_input" || issue.factoryState === "failed" || issue.factoryState === "escalated") {
32
+ return true;
33
+ }
34
+ if ((issue.sessionState === "done" || issue.factoryState === "done") && issue.prNumber === undefined && !issue.prUrl) {
35
+ return true;
36
+ }
37
+ return false;
38
+ }
39
+ function renderStatusComment(db, issue, trackedIssue, options) {
40
+ const activeRun = issue.activeRunId ? db.runs.getRunById(issue.activeRunId) : undefined;
41
+ const latestRun = db.runs.getLatestRunForIssue(issue.projectId, issue.linearIssueId);
42
+ const latestEvent = db.issueSessions.listIssueSessionEvents(issue.projectId, issue.linearIssueId, { limit: 1 }).at(-1);
43
+ const activeRunType = issue.activeRunId !== undefined
44
+ ? (options?.activeRunType ?? activeRun?.runType)
45
+ : undefined;
46
+ const waitingReason = trackedIssue?.waitingReason ?? derivePatchRelayWaitingReason({
47
+ ...(activeRunType ? { activeRunType } : {}),
48
+ ...(issue.activeRunId !== undefined ? { activeRunId: issue.activeRunId } : {}),
49
+ factoryState: issue.factoryState,
50
+ pendingRunType: issue.pendingRunType,
51
+ ...(issue.prNumber !== undefined ? { prNumber: issue.prNumber } : {}),
52
+ prHeadSha: issue.prHeadSha,
53
+ prReviewState: issue.prReviewState,
54
+ prCheckStatus: issue.prCheckStatus,
55
+ lastBlockingReviewHeadSha: issue.lastBlockingReviewHeadSha,
56
+ latestFailureCheckName: issue.lastGitHubFailureCheckName,
57
+ });
58
+ const lines = [
59
+ "## PatchRelay status",
60
+ "",
61
+ statusHeadline(trackedIssue ?? issue, activeRunType),
62
+ ];
63
+ const statusNote = trackedIssue?.statusNote ?? deriveIssueStatusNote({ issue, latestRun, latestEvent, waitingReason });
64
+ if (waitingReason) {
65
+ lines.push("", `Waiting: ${waitingReason}`);
66
+ }
67
+ if (statusNote && statusNote !== waitingReason) {
68
+ const label = trackedIssue?.sessionState === "waiting_input" || issue.factoryState === "awaiting_input" ? "Input needed"
69
+ : trackedIssue?.sessionState === "failed" || issue.factoryState === "failed" || issue.factoryState === "escalated" ? "Action needed"
70
+ : "Note";
71
+ lines.push("", `${label}: ${statusNote}`);
72
+ }
73
+ const completionCheck = extractCompletionCheck(latestRun);
74
+ if (completionCheck?.outcome === "needs_input") {
75
+ if (completionCheck.why) {
76
+ lines.push("", `Why: ${completionCheck.why}`);
77
+ }
78
+ if (completionCheck.recommendedReply) {
79
+ lines.push("", `Suggested reply: ${completionCheck.recommendedReply}`);
80
+ }
81
+ const issueRef = issue.issueKey ?? issue.linearIssueId;
82
+ lines.push("", `Reply in a Linear comment to continue, or run \`patchrelay issue prompt ${issueRef} "..."\`.`);
83
+ }
84
+ if (issue.prNumber !== undefined || issue.prUrl) {
85
+ const prLabel = issue.prNumber !== undefined ? `#${issue.prNumber}` : "open";
86
+ lines.push("", `PR: ${issue.prUrl ? `[${prLabel}](${issue.prUrl})` : prLabel}`);
87
+ }
88
+ if (latestRun) {
89
+ lines.push("", `Latest run: ${formatLatestRun(latestRun)}`);
90
+ if (latestRun.failureReason) {
91
+ lines.push("", `Failure: ${latestRun.failureReason}`);
92
+ }
93
+ if (completionCheck && completionCheck.outcome !== "needs_input" && completionCheck.summary !== statusNote) {
94
+ lines.push("", `Completion check: ${completionCheck.summary}`);
95
+ }
96
+ }
97
+ if (issue.lastGitHubFailureCheckName && (issue.factoryState === "repairing_ci" || issue.prCheckStatus === "failed" || issue.prCheckStatus === "failure")) {
98
+ lines.push("", `Latest failing check: ${issue.lastGitHubFailureCheckName}`);
99
+ }
100
+ lines.push("", "_PatchRelay updates this comment as it works. Review and merge remain downstream._");
101
+ return lines.join("\n");
102
+ }
103
+ function statusHeadline(issue, activeRunType) {
104
+ if (activeRunType) {
105
+ return `Running ${humanize(activeRunType)}`;
106
+ }
107
+ switch (issue.sessionState) {
108
+ case "waiting_input":
109
+ return issue.waitingReason ?? "Waiting for more input";
110
+ case "running":
111
+ return issue.prNumber !== undefined ? `PR #${issue.prNumber} is actively running` : "Actively running";
112
+ case "done":
113
+ return issue.prNumber !== undefined ? `Completed with PR #${issue.prNumber}` : "Completed";
114
+ case "failed":
115
+ return "Needs operator intervention";
116
+ default:
117
+ break;
118
+ }
119
+ switch (issue.factoryState) {
120
+ case "delegated":
121
+ return "Queued to start work";
122
+ case "implementing":
123
+ return "Implementing requested change";
124
+ case "pr_open":
125
+ return issue.prNumber !== undefined ? `PR #${issue.prNumber} opened` : "PR opened";
126
+ case "changes_requested":
127
+ return "Addressing requested review changes";
128
+ case "repairing_ci":
129
+ return "Repairing failing CI";
130
+ case "awaiting_queue":
131
+ return "Handed off downstream for merge";
132
+ case "repairing_queue":
133
+ return "Repairing merge handoff";
134
+ case "awaiting_input":
135
+ return "Waiting for more input";
136
+ case "failed":
137
+ return "Needs operator intervention";
138
+ case "escalated":
139
+ return "Needs operator intervention";
140
+ case "done":
141
+ return issue.prNumber !== undefined ? `Completed with PR #${issue.prNumber}` : "Completed";
142
+ default:
143
+ return humanize(issue.factoryState);
144
+ }
145
+ }
146
+ function formatLatestRun(run) {
147
+ const at = run.endedAt ?? run.startedAt;
148
+ return `${humanize(run.runType)} ${run.status} at ${at}`;
149
+ }
150
+ function humanize(value) {
151
+ return value.replaceAll("_", " ");
152
+ }
@@ -0,0 +1,103 @@
1
+ import { resolvePreferredDeployingLinearState, resolvePreferredHumanNeededLinearState, resolvePreferredImplementingLinearState, resolvePreferredReviewLinearState, resolvePreferredReviewingLinearState, } from "./linear-workflow.js";
2
+ export async function syncActiveWorkflowState(params) {
3
+ const { db, issue, linear, trackedIssue, options } = params;
4
+ if (!shouldAutoAdvanceLinearState(issue)) {
5
+ return;
6
+ }
7
+ const liveIssue = await linear.getIssue(issue.linearIssueId).catch(() => undefined);
8
+ if (!liveIssue)
9
+ return;
10
+ if (!shouldAutoAdvanceLinearState({
11
+ currentLinearState: liveIssue.stateName,
12
+ currentLinearStateType: liveIssue.stateType,
13
+ })) {
14
+ db.issues.upsertIssue({
15
+ projectId: issue.projectId,
16
+ linearIssueId: issue.linearIssueId,
17
+ ...(liveIssue.stateName ? { currentLinearState: liveIssue.stateName } : {}),
18
+ ...(liveIssue.stateType ? { currentLinearStateType: liveIssue.stateType } : {}),
19
+ });
20
+ return;
21
+ }
22
+ const targetState = resolveDesiredActiveWorkflowState(issue, trackedIssue, options, liveIssue);
23
+ if (!targetState)
24
+ return;
25
+ const normalizedCurrent = liveIssue.stateName?.trim().toLowerCase();
26
+ if (normalizedCurrent === targetState.trim().toLowerCase()) {
27
+ db.issues.upsertIssue({
28
+ projectId: issue.projectId,
29
+ linearIssueId: issue.linearIssueId,
30
+ ...(liveIssue.stateName ? { currentLinearState: liveIssue.stateName } : {}),
31
+ ...(liveIssue.stateType ? { currentLinearStateType: liveIssue.stateType } : {}),
32
+ });
33
+ return;
34
+ }
35
+ const updated = await linear.setIssueState(issue.linearIssueId, targetState);
36
+ db.issues.upsertIssue({
37
+ projectId: issue.projectId,
38
+ linearIssueId: issue.linearIssueId,
39
+ ...(updated.stateName ? { currentLinearState: updated.stateName } : {}),
40
+ ...(updated.stateType ? { currentLinearStateType: updated.stateType } : {}),
41
+ });
42
+ }
43
+ function shouldAutoAdvanceLinearState(issue) {
44
+ const normalizedType = issue.currentLinearStateType?.trim().toLowerCase();
45
+ if (normalizedType === "completed" || normalizedType === "canceled" || normalizedType === "cancelled") {
46
+ return false;
47
+ }
48
+ const normalizedName = issue.currentLinearState?.trim().toLowerCase();
49
+ return normalizedName !== "done" && normalizedName !== "completed" && normalizedName !== "complete";
50
+ }
51
+ function resolveDesiredActiveWorkflowState(issue, trackedIssue, options, liveIssue) {
52
+ if (issue.factoryState === "awaiting_input" || issue.factoryState === "failed" || issue.factoryState === "escalated"
53
+ || trackedIssue?.sessionState === "waiting_input" || trackedIssue?.sessionState === "failed") {
54
+ return resolvePreferredHumanNeededLinearState(liveIssue);
55
+ }
56
+ const activelyWorking = issue.activeRunId !== undefined
57
+ || options?.activeRunType !== undefined
58
+ || trackedIssue?.sessionState === "running"
59
+ || issue.factoryState === "delegated"
60
+ || issue.factoryState === "implementing"
61
+ || issue.factoryState === "changes_requested"
62
+ || issue.factoryState === "repairing_ci"
63
+ || issue.factoryState === "repairing_queue";
64
+ if (activelyWorking) {
65
+ return resolvePreferredImplementingLinearState(liveIssue);
66
+ }
67
+ if (issue.factoryState === "awaiting_queue"
68
+ || issue.prReviewState === "approved"
69
+ || isApprovedAndGreen(issue.prReviewState, issue.prCheckStatus)) {
70
+ return resolvePreferredDeployingLinearState(liveIssue);
71
+ }
72
+ if (hasPendingReviewQuillVerdict(issue.lastGitHubCiSnapshotJson)) {
73
+ return resolvePreferredReviewingLinearState(liveIssue);
74
+ }
75
+ const reviewBound = issue.prNumber !== undefined
76
+ || Boolean(issue.prUrl)
77
+ || issue.factoryState === "pr_open"
78
+ || issue.prReviewState !== undefined
79
+ || issue.prCheckStatus !== undefined;
80
+ if (reviewBound) {
81
+ return resolvePreferredReviewLinearState(liveIssue);
82
+ }
83
+ return undefined;
84
+ }
85
+ function isApprovedAndGreen(prReviewState, prCheckStatus) {
86
+ const normalizedReview = prReviewState?.trim().toLowerCase();
87
+ const normalizedChecks = prCheckStatus?.trim().toLowerCase();
88
+ return normalizedReview === "approved" && (normalizedChecks === "success" || normalizedChecks === "passed");
89
+ }
90
+ function hasPendingReviewQuillVerdict(snapshotJson) {
91
+ if (!snapshotJson)
92
+ return false;
93
+ try {
94
+ const parsed = JSON.parse(snapshotJson);
95
+ return Array.isArray(parsed.checks) && parsed.checks.some((check) => typeof check.name === "string"
96
+ && check.name === "review-quill/verdict"
97
+ && typeof check.status === "string"
98
+ && check.status.toLowerCase() === "pending");
99
+ }
100
+ catch {
101
+ return false;
102
+ }
103
+ }
@@ -0,0 +1,48 @@
1
+ import { resolvePreferredCompletedLinearState } from "./linear-workflow.js";
2
+ export class MergedLinearCompletionReconciler {
3
+ db;
4
+ linearProvider;
5
+ logger;
6
+ constructor(db, linearProvider, logger) {
7
+ this.db = db;
8
+ this.linearProvider = linearProvider;
9
+ this.logger = logger;
10
+ }
11
+ async reconcile() {
12
+ for (const issue of this.db.issues.listIssues()) {
13
+ if (issue.prState !== "merged")
14
+ continue;
15
+ if (issue.currentLinearStateType?.trim().toLowerCase() === "completed")
16
+ continue;
17
+ const linear = await this.linearProvider.forProject(issue.projectId).catch(() => undefined);
18
+ if (!linear)
19
+ continue;
20
+ try {
21
+ const liveIssue = await linear.getIssue(issue.linearIssueId);
22
+ const targetState = resolvePreferredCompletedLinearState(liveIssue);
23
+ if (!targetState)
24
+ continue;
25
+ const normalizedCurrent = liveIssue.stateName?.trim().toLowerCase();
26
+ if (normalizedCurrent === targetState.trim().toLowerCase()) {
27
+ this.db.issues.upsertIssue({
28
+ projectId: issue.projectId,
29
+ linearIssueId: issue.linearIssueId,
30
+ ...(liveIssue.stateName ? { currentLinearState: liveIssue.stateName } : {}),
31
+ ...(liveIssue.stateType ? { currentLinearStateType: liveIssue.stateType } : {}),
32
+ });
33
+ continue;
34
+ }
35
+ const updated = await linear.setIssueState(issue.linearIssueId, targetState);
36
+ this.db.issues.upsertIssue({
37
+ projectId: issue.projectId,
38
+ linearIssueId: issue.linearIssueId,
39
+ ...(updated.stateName ? { currentLinearState: updated.stateName } : {}),
40
+ ...(updated.stateType ? { currentLinearStateType: updated.stateType } : {}),
41
+ });
42
+ }
43
+ catch (error) {
44
+ this.logger.warn({ issueKey: issue.issueKey, error: error instanceof Error ? error.message : String(error) }, "Failed to reconcile merged issue to a completed Linear state");
45
+ }
46
+ }
47
+ }
48
+ }
@@ -0,0 +1,199 @@
1
+ import { buildCompletionCheckActivity } from "./linear-session-reporting.js";
2
+ export async function handleNoPrCompletionCheck(params) {
3
+ const completedRunUpdate = buildCompletedRunUpdate({
4
+ threadId: params.threadId,
5
+ ...(params.completedTurnId ? { completedTurnId: params.completedTurnId } : {}),
6
+ report: params.report,
7
+ });
8
+ params.publishTurnEvent({
9
+ level: "info",
10
+ run: params.run,
11
+ issueKey: params.issue.issueKey,
12
+ status: "completion_check_started",
13
+ summary: "No PR found; checking next step",
14
+ detail: params.publishedOutcomeError,
15
+ });
16
+ void params.emitActivity(params.issue, buildCompletionCheckActivity("started"), { ephemeral: true });
17
+ let completionCheck;
18
+ try {
19
+ completionCheck = await params.completionCheck.run({
20
+ issue: params.issue,
21
+ run: params.run,
22
+ noPrSummary: params.publishedOutcomeError,
23
+ onStarted: ({ threadId: completionCheckThreadId, turnId: completionCheckTurnId }) => {
24
+ params.db.runs.markCompletionCheckStarted(params.run.id, {
25
+ threadId: completionCheckThreadId,
26
+ turnId: completionCheckTurnId,
27
+ });
28
+ },
29
+ });
30
+ }
31
+ catch (error) {
32
+ const message = error instanceof Error ? error.message : String(error);
33
+ const failureMessage = `No PR observed and the completion check failed: ${message}`;
34
+ params.failRunAndClear(params.run, failureMessage, "failed");
35
+ params.syncFailureOutcome({
36
+ run: params.run,
37
+ fallbackIssue: params.issue,
38
+ message: failureMessage,
39
+ level: "error",
40
+ status: "completion_check_failed",
41
+ summary: "No PR found; completion check failed",
42
+ detail: message,
43
+ });
44
+ return;
45
+ }
46
+ if (completionCheck.outcome === "continue") {
47
+ const continued = params.withHeldLease(params.run.projectId, params.run.linearIssueId, (lease) => {
48
+ params.db.runs.finishRun(params.run.id, completedRunUpdate);
49
+ params.db.runs.saveCompletionCheck(params.run.id, completionCheck);
50
+ params.db.issues.upsertIssue({
51
+ projectId: params.run.projectId,
52
+ linearIssueId: params.run.linearIssueId,
53
+ activeRunId: null,
54
+ factoryState: "delegated",
55
+ pendingRunType: null,
56
+ pendingRunContextJson: null,
57
+ });
58
+ return Boolean(params.db.issueSessions.appendIssueSessionEventWithLease(lease, {
59
+ projectId: params.run.projectId,
60
+ linearIssueId: params.run.linearIssueId,
61
+ eventType: "completion_check_continue",
62
+ eventJson: JSON.stringify({
63
+ runType: params.run.runType,
64
+ summary: completionCheck.summary,
65
+ }),
66
+ dedupeKey: `completion_check_continue:${params.run.id}`,
67
+ }));
68
+ });
69
+ if (!continued) {
70
+ params.logger.warn({ runId: params.run.id, issueId: params.run.linearIssueId }, "Skipping completion-check continue writes after losing issue-session lease");
71
+ params.clearProgressAndRelease(params.run);
72
+ return;
73
+ }
74
+ params.syncCompletionCheckOutcome({
75
+ run: params.run,
76
+ fallbackIssue: params.issue,
77
+ level: "info",
78
+ status: "completion_check_continue",
79
+ summary: "No PR found; continuing automatically",
80
+ detail: completionCheck.summary,
81
+ activity: buildCompletionCheckActivity("continue"),
82
+ enqueue: true,
83
+ });
84
+ return;
85
+ }
86
+ if (completionCheck.outcome === "needs_input") {
87
+ const completed = params.withHeldLease(params.run.projectId, params.run.linearIssueId, (lease) => {
88
+ params.db.runs.finishRun(params.run.id, completedRunUpdate);
89
+ params.db.runs.saveCompletionCheck(params.run.id, completionCheck);
90
+ params.db.issueSessions.clearPendingIssueSessionEventsWithLease(lease);
91
+ params.db.issues.upsertIssue({
92
+ projectId: params.run.projectId,
93
+ linearIssueId: params.run.linearIssueId,
94
+ activeRunId: null,
95
+ factoryState: "awaiting_input",
96
+ pendingRunType: null,
97
+ pendingRunContextJson: null,
98
+ });
99
+ return true;
100
+ });
101
+ if (!completed) {
102
+ params.logger.warn({ runId: params.run.id, issueId: params.run.linearIssueId }, "Skipping completion-check needs-input writes after losing issue-session lease");
103
+ params.clearProgressAndRelease(params.run);
104
+ return;
105
+ }
106
+ params.syncCompletionCheckOutcome({
107
+ run: params.run,
108
+ fallbackIssue: params.issue,
109
+ level: "warn",
110
+ status: "completion_check_needs_input",
111
+ summary: "No PR found; waiting for answer",
112
+ detail: completionCheck.question ?? completionCheck.summary,
113
+ activity: buildCompletionCheckActivity("needs_input", completionCheck),
114
+ });
115
+ return;
116
+ }
117
+ if (completionCheck.outcome === "done") {
118
+ const completed = params.withHeldLease(params.run.projectId, params.run.linearIssueId, (lease) => {
119
+ params.db.runs.finishRun(params.run.id, completedRunUpdate);
120
+ params.db.runs.saveCompletionCheck(params.run.id, completionCheck);
121
+ params.db.issueSessions.clearPendingIssueSessionEventsWithLease(lease);
122
+ params.db.issues.upsertIssue({
123
+ projectId: params.run.projectId,
124
+ linearIssueId: params.run.linearIssueId,
125
+ activeRunId: null,
126
+ factoryState: "done",
127
+ pendingRunType: null,
128
+ pendingRunContextJson: null,
129
+ lastGitHubFailureSource: null,
130
+ lastGitHubFailureHeadSha: null,
131
+ lastGitHubFailureSignature: null,
132
+ lastGitHubFailureCheckName: null,
133
+ lastGitHubFailureCheckUrl: null,
134
+ lastGitHubFailureContextJson: null,
135
+ lastGitHubFailureAt: null,
136
+ lastQueueIncidentJson: null,
137
+ lastAttemptedFailureHeadSha: null,
138
+ lastAttemptedFailureSignature: null,
139
+ });
140
+ return true;
141
+ });
142
+ if (!completed) {
143
+ params.logger.warn({ runId: params.run.id, issueId: params.run.linearIssueId }, "Skipping completion-check done writes after losing issue-session lease");
144
+ params.clearProgressAndRelease(params.run);
145
+ return;
146
+ }
147
+ params.syncCompletionCheckOutcome({
148
+ run: params.run,
149
+ fallbackIssue: params.issue,
150
+ level: "info",
151
+ status: "completion_check_done",
152
+ summary: "No PR found; confirmed done",
153
+ detail: completionCheck.summary,
154
+ activity: buildCompletionCheckActivity("done", completionCheck),
155
+ });
156
+ return;
157
+ }
158
+ const failureReason = `No PR observed and the completion check failed this run: ${completionCheck.summary}`;
159
+ const failed = params.withHeldLease(params.run.projectId, params.run.linearIssueId, () => {
160
+ params.db.runs.finishRun(params.run.id, {
161
+ ...completedRunUpdate,
162
+ status: "failed",
163
+ failureReason,
164
+ });
165
+ params.db.runs.saveCompletionCheck(params.run.id, completionCheck);
166
+ params.db.issues.upsertIssue({
167
+ projectId: params.run.projectId,
168
+ linearIssueId: params.run.linearIssueId,
169
+ activeRunId: null,
170
+ factoryState: "failed",
171
+ pendingRunType: null,
172
+ pendingRunContextJson: null,
173
+ });
174
+ return true;
175
+ });
176
+ if (!failed) {
177
+ params.logger.warn({ runId: params.run.id, issueId: params.run.linearIssueId }, "Skipping completion-check failed writes after losing issue-session lease");
178
+ params.clearProgressAndRelease(params.run);
179
+ return;
180
+ }
181
+ params.syncFailureOutcome({
182
+ run: params.run,
183
+ fallbackIssue: params.issue,
184
+ message: failureReason,
185
+ level: "warn",
186
+ status: "completion_check_failed",
187
+ summary: "No PR found; completion check failed",
188
+ detail: completionCheck.summary,
189
+ });
190
+ }
191
+ function buildCompletedRunUpdate(params) {
192
+ return {
193
+ status: "completed",
194
+ threadId: params.threadId,
195
+ ...(params.completedTurnId ? { turnId: params.completedTurnId } : {}),
196
+ summaryJson: JSON.stringify({ latestAssistantMessage: params.report.assistantMessages.at(-1) ?? null }),
197
+ reportJson: JSON.stringify(params.report),
198
+ };
199
+ }
@@ -0,0 +1,58 @@
1
+ function parseObjectJson(value) {
2
+ if (!value)
3
+ return undefined;
4
+ try {
5
+ const parsed = JSON.parse(value);
6
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : undefined;
7
+ }
8
+ catch {
9
+ return undefined;
10
+ }
11
+ }
12
+ export function buildOperatorRetryEvent(issue, runType) {
13
+ if (runType === "queue_repair") {
14
+ const queueIncident = parseObjectJson(issue.lastQueueIncidentJson);
15
+ const failureContext = parseObjectJson(issue.lastGitHubFailureContextJson);
16
+ return {
17
+ eventType: "merge_steward_incident",
18
+ eventJson: JSON.stringify({
19
+ ...(queueIncident ?? {}),
20
+ ...(failureContext ?? {}),
21
+ source: "operator_retry",
22
+ }),
23
+ dedupeKey: `operator_retry:queue_repair:${issue.linearIssueId}:${issue.prHeadSha ?? issue.lastGitHubFailureHeadSha ?? "unknown-sha"}`,
24
+ };
25
+ }
26
+ if (runType === "ci_repair") {
27
+ const failureContext = parseObjectJson(issue.lastGitHubFailureContextJson);
28
+ return {
29
+ eventType: "settled_red_ci",
30
+ eventJson: JSON.stringify({
31
+ ...(failureContext ?? {}),
32
+ source: "operator_retry",
33
+ }),
34
+ dedupeKey: `operator_retry:ci_repair:${issue.linearIssueId}:${issue.lastGitHubFailureSignature ?? issue.prHeadSha ?? "unknown-sha"}`,
35
+ };
36
+ }
37
+ if (runType === "review_fix" || runType === "branch_upkeep") {
38
+ return {
39
+ eventType: "review_changes_requested",
40
+ eventJson: JSON.stringify({
41
+ reviewBody: runType === "branch_upkeep"
42
+ ? "Operator requested retry of branch upkeep after requested changes."
43
+ : "Operator requested retry of review-fix work.",
44
+ ...(runType === "branch_upkeep" ? { branchUpkeepRequired: true, wakeReason: "branch_upkeep" } : {}),
45
+ source: "operator_retry",
46
+ }),
47
+ dedupeKey: `operator_retry:${runType}:${issue.linearIssueId}:${issue.prHeadSha ?? "unknown-sha"}`,
48
+ };
49
+ }
50
+ return {
51
+ eventType: "delegated",
52
+ eventJson: JSON.stringify({
53
+ promptContext: "Operator requested retry of PatchRelay work.",
54
+ source: "operator_retry",
55
+ }),
56
+ dedupeKey: `operator_retry:implementation:${issue.linearIssueId}`,
57
+ };
58
+ }
@@ -0,0 +1,52 @@
1
+ import { extractStageSummary, summarizeCurrentThread } from "./run-reporting.js";
2
+ import { parseStageReport } from "./issue-overview-query.js";
3
+ export class PublicAgentSessionStatusQuery {
4
+ db;
5
+ overviewQuery;
6
+ constructor(db, overviewQuery) {
7
+ this.db = db;
8
+ this.overviewQuery = overviewQuery;
9
+ }
10
+ async getStatus(issueKey) {
11
+ const overview = await this.overviewQuery.getIssueOverview(issueKey);
12
+ if (!overview)
13
+ return undefined;
14
+ const issueRecord = this.db.issues.getIssueByKey(issueKey);
15
+ const latestRunReport = parseStageReport(overview.latestRun?.reportJson, overview.latestRun?.status ?? "unknown");
16
+ const runs = (overview.runs ?? this.overviewQuery.buildRuns(overview.issue.projectId, overview.issue.linearIssueId)).map((run) => ({
17
+ run: {
18
+ id: run.id,
19
+ runType: run.runType,
20
+ status: run.status,
21
+ startedAt: run.startedAt,
22
+ ...(run.endedAt ? { endedAt: run.endedAt } : {}),
23
+ },
24
+ ...(run.report ? { report: run.report } : {}),
25
+ }));
26
+ return {
27
+ issue: {
28
+ issueKey: overview.issue.issueKey,
29
+ title: overview.issue.title,
30
+ issueUrl: overview.issue.issueUrl,
31
+ currentLinearState: overview.issue.currentLinearState,
32
+ ...(overview.session?.sessionState ? { sessionState: overview.session.sessionState } : {}),
33
+ factoryState: overview.issue.factoryState,
34
+ ...(overview.session?.prNumber !== undefined ? { prNumber: overview.session.prNumber } : {}),
35
+ ...(issueRecord?.prUrl ? { prUrl: issueRecord.prUrl } : {}),
36
+ ...(issueRecord?.prState ? { prState: issueRecord.prState } : {}),
37
+ ...(issueRecord?.prReviewState ? { prReviewState: issueRecord.prReviewState } : {}),
38
+ ...(issueRecord?.prCheckStatus ? { prCheckStatus: issueRecord.prCheckStatus } : {}),
39
+ ...(issueRecord ? { ciRepairAttempts: issueRecord.ciRepairAttempts, queueRepairAttempts: issueRecord.queueRepairAttempts } : {}),
40
+ ...(overview.issue.waitingReason ? { waitingReason: overview.issue.waitingReason } : {}),
41
+ ...(overview.issue.statusNote ? { statusNote: overview.issue.statusNote } : {}),
42
+ ...(overview.session?.lastWakeReason ? { lastWakeReason: overview.session.lastWakeReason } : {}),
43
+ },
44
+ ...(overview.activeRun ? { activeRun: overview.activeRun } : {}),
45
+ ...(overview.latestRun ? { latestRun: overview.latestRun } : {}),
46
+ ...(overview.liveThread ? { liveThread: summarizeCurrentThread(overview.liveThread) } : {}),
47
+ ...(latestRunReport ? { latestReportSummary: extractStageSummary(latestRunReport) } : {}),
48
+ runs,
49
+ generatedAt: new Date().toISOString(),
50
+ };
51
+ }
52
+ }