patchrelay 0.40.0 → 0.41.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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "service": "patchrelay",
3
- "version": "0.40.0",
4
- "commit": "c1cef920f5e3",
5
- "builtAt": "2026-04-11T22:19:02.944Z"
3
+ "version": "0.41.0",
4
+ "commit": "4ad3af5dce7c",
5
+ "builtAt": "2026-04-12T21:16:04.426Z"
6
6
  }
@@ -2,7 +2,7 @@ import { setTimeout as delay } from "node:timers/promises";
2
2
  import { getRunTypeFlag } from "../args.js";
3
3
  import { CliUsageError } from "../errors.js";
4
4
  import { formatJson } from "../formatters/json.js";
5
- import { formatClose, formatInspect, formatList, formatLive, formatOpen, formatRetry, formatSessionHistory, formatTranscriptSource, formatWorktree } from "../formatters/text.js";
5
+ import { formatAudit, formatClose, formatInspect, formatList, formatLive, formatOpen, formatRetry, formatSessionHistory, formatTranscriptSource, formatWorktree } from "../formatters/text.js";
6
6
  import { buildOpenCommand } from "../interactive.js";
7
7
  import { writeOutput } from "../output.js";
8
8
  export async function handleIssueCommand(params) {
@@ -36,6 +36,8 @@ export async function handleIssueCommand(params) {
36
36
  return await handleOpenCommand(nested);
37
37
  case "sessions":
38
38
  return await handleSessionsCommand(nested);
39
+ case "audit":
40
+ return await handleAuditCommand(nested);
39
41
  case "transcript-source":
40
42
  return await handleTranscriptSourceCommand(nested);
41
43
  case "retry":
@@ -156,6 +158,18 @@ export async function handleSessionsCommand(params) {
156
158
  : formatSessionHistory(result, (threadId) => buildOpenCommand(params.config, result.worktreePath ?? "", threadId)));
157
159
  return 0;
158
160
  }
161
+ export async function handleAuditCommand(params) {
162
+ const issueKey = params.commandArgs[0];
163
+ if (!issueKey) {
164
+ throw new Error("audit requires <issueKey>.");
165
+ }
166
+ const result = params.data.audit(issueKey);
167
+ if (!result) {
168
+ throw new Error(`Issue not found: ${issueKey}`);
169
+ }
170
+ writeOutput(params.stdout, params.json ? formatJson(result) : formatAudit(result));
171
+ return 0;
172
+ }
159
173
  export async function handleRetryCommand(params) {
160
174
  const issueKey = params.commandArgs[0];
161
175
  if (!issueKey) {
package/dist/cli/data.js CHANGED
@@ -7,6 +7,7 @@ import { getThreadTurns } from "../codex-thread-utils.js";
7
7
  import { PatchRelayDatabase } from "../db.js";
8
8
  import { buildManualRetryAttemptReset, resolveRetryTarget } from "../manual-issue-actions.js";
9
9
  import { WorktreeManager } from "../worktree-manager.js";
10
+ import { parseDelegationObservedPayload, parseRunReleasedAuthorityPayload } from "../delegation-audit.js";
10
11
  import { CliOperatorApiClient } from "./operator-client.js";
11
12
  function safeJsonParse(value) {
12
13
  if (!value)
@@ -273,6 +274,50 @@ export class CliDataAccess extends CliOperatorApiClient {
273
274
  ...(run ? { releasedRunId: run.id } : {}),
274
275
  };
275
276
  }
277
+ audit(issueKey) {
278
+ const issue = this.db.getTrackedIssueByKey(issueKey);
279
+ if (!issue)
280
+ return undefined;
281
+ const events = this.db.issueSessions
282
+ .listIssueSessionEvents(issue.projectId, issue.linearIssueId)
283
+ .flatMap((event) => {
284
+ const delegationObserved = parseDelegationObservedPayload(event);
285
+ if (delegationObserved) {
286
+ return [{
287
+ createdAt: event.createdAt,
288
+ eventType: event.eventType,
289
+ summary: [
290
+ delegationObserved.source,
291
+ `observed=${delegationObserved.observedDelegatedToPatchRelay ? "delegated" : "undelegated"}`,
292
+ `applied=${delegationObserved.appliedDelegatedToPatchRelay ? "delegated" : "undelegated"}`,
293
+ `hydration=${delegationObserved.hydration}`,
294
+ delegationObserved.reason ? `reason=${delegationObserved.reason}` : undefined,
295
+ ].filter(Boolean).join(" "),
296
+ details: delegationObserved,
297
+ }];
298
+ }
299
+ const authorityRelease = parseRunReleasedAuthorityPayload(event);
300
+ if (authorityRelease) {
301
+ return [{
302
+ createdAt: event.createdAt,
303
+ eventType: event.eventType,
304
+ summary: `released run #${authorityRelease.runId} (${authorityRelease.runType}) via ${authorityRelease.source}: ${authorityRelease.reason}`,
305
+ details: authorityRelease,
306
+ }];
307
+ }
308
+ if (event.eventType === "delegated" || event.eventType === "undelegated") {
309
+ return [{
310
+ createdAt: event.createdAt,
311
+ eventType: event.eventType,
312
+ summary: event.eventType === "delegated"
313
+ ? "PatchRelay accepted delegation"
314
+ : "PatchRelay recorded undelegation",
315
+ }];
316
+ }
317
+ return [];
318
+ });
319
+ return { issue, events };
320
+ }
276
321
  transcriptSource(issueKey, runId) {
277
322
  const issue = this.db.getTrackedIssueByKey(issueKey);
278
323
  if (!issue)
@@ -84,6 +84,26 @@ export function formatRetry(result) {
84
84
  .filter(Boolean)
85
85
  .join("\n")}\n`;
86
86
  }
87
+ export function formatAudit(result) {
88
+ const lines = [
89
+ `${result.issue.issueKey ?? result.issue.linearIssueId}${result.issue.currentLinearState ? ` ${result.issue.currentLinearState}` : ""}`,
90
+ ];
91
+ if (result.events.length === 0) {
92
+ lines.push("No delegation audit events recorded.");
93
+ return `${lines.join("\n")}\n`;
94
+ }
95
+ for (const event of result.events) {
96
+ lines.push("");
97
+ lines.push([event.createdAt, event.eventType].join(" "));
98
+ lines.push(event.summary);
99
+ if (event.details && Object.keys(event.details).length > 0) {
100
+ lines.push(Object.entries(event.details)
101
+ .map(([key, value]) => `${key}=${value === undefined ? "-" : typeof value === "string" ? value : JSON.stringify(value)}`)
102
+ .join(" "));
103
+ }
104
+ }
105
+ return `${lines.join("\n")}\n`;
106
+ }
87
107
  export function formatClose(result) {
88
108
  return `${[
89
109
  value("Issue", result.issue.issueKey ?? result.issue.linearIssueId),
package/dist/cli/help.js CHANGED
@@ -34,6 +34,7 @@ export function rootHelpText() {
34
34
  " issue list [--active] [--failed] [--repo <id>] [--json]",
35
35
  " List tracked issues",
36
36
  " issue show <issueKey> [--json] Show the latest known issue state",
37
+ " issue audit <issueKey> [--json] Show delegation/release audit events for one issue",
37
38
  " issue watch <issueKey> [--json] Follow the active run until it settles",
38
39
  " issue open <issueKey> [--print] [--json] Open Codex in the issue worktree",
39
40
  " issue sessions <issueKey> [--json] Show recorded Codex app-server sessions for one issue",
@@ -146,6 +147,7 @@ export function issueHelpText() {
146
147
  "",
147
148
  "Commands:",
148
149
  " show <issueKey> Show the latest known issue state",
150
+ " audit <issueKey> Show delegation/release audit events",
149
151
  " list List tracked issues",
150
152
  " watch <issueKey> Follow issue activity until it settles",
151
153
  " path <issueKey> Print the issue worktree path",
@@ -127,7 +127,7 @@ export function buildPatchRelayQueueObservations(issue, feedEvents) {
127
127
  case "awaiting_queue":
128
128
  observations.push({
129
129
  tone: "info",
130
- text: "PatchRelay has finished active work and is waiting for downstream merge flow.",
130
+ text: "PatchRelay has finished active work. Delivery now depends on downstream review and merge automation.",
131
131
  });
132
132
  break;
133
133
  case "repairing_queue":
@@ -1,4 +1,4 @@
1
- import { deriveSessionWakePlan } from "../issue-session-events.js";
1
+ import { deriveSessionWakePlan, isActionableIssueSessionEventType } from "../issue-session-events.js";
2
2
  import { isoNow } from "./shared.js";
3
3
  export class IssueSessionStore {
4
4
  connection;
@@ -90,13 +90,8 @@ export class IssueSessionStore {
90
90
  `).run(isoNow(), projectId, linearIssueId);
91
91
  }
92
92
  hasPendingIssueSessionEvents(projectId, linearIssueId) {
93
- const row = this.connection.prepare(`
94
- SELECT 1
95
- FROM issue_session_events
96
- WHERE project_id = ? AND linear_issue_id = ? AND processed_at IS NULL
97
- LIMIT 1
98
- `).get(projectId, linearIssueId);
99
- return row !== undefined;
93
+ return this.listIssueSessionEvents(projectId, linearIssueId, { pendingOnly: true })
94
+ .some((event) => isActionableIssueSessionEventType(event.eventType));
100
95
  }
101
96
  peekIssueSessionWake(projectId, linearIssueId) {
102
97
  const issue = this.issues.getIssue(projectId, linearIssueId);
@@ -104,6 +104,10 @@ export class IssueStore {
104
104
  sets.push("pr_state = @prState");
105
105
  values.prState = params.prState;
106
106
  }
107
+ if (params.prIsDraft !== undefined) {
108
+ sets.push("pr_is_draft = @prIsDraft");
109
+ values.prIsDraft = params.prIsDraft == null ? null : params.prIsDraft ? 1 : 0;
110
+ }
107
111
  if (params.prHeadSha !== undefined) {
108
112
  sets.push("pr_head_sha = @prHeadSha");
109
113
  values.prHeadSha = params.prHeadSha;
@@ -218,7 +222,7 @@ export class IssueStore {
218
222
  current_linear_state, current_linear_state_type, factory_state, pending_run_type, pending_run_context_json,
219
223
  branch_name, worktree_path, thread_id, active_run_id, status_comment_id,
220
224
  agent_session_id, last_linear_activity_key,
221
- pr_number, pr_url, pr_state, pr_head_sha, pr_author_login, pr_review_state, pr_check_status, last_blocking_review_head_sha,
225
+ pr_number, pr_url, pr_state, pr_is_draft, pr_head_sha, pr_author_login, pr_review_state, pr_check_status, last_blocking_review_head_sha,
222
226
  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,
223
227
  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,
224
228
  last_queue_signal_at, last_queue_incident_json,
@@ -231,7 +235,7 @@ export class IssueStore {
231
235
  @currentLinearState, @currentLinearStateType, @factoryState, @pendingRunType, @pendingRunContextJson,
232
236
  @branchName, @worktreePath, @threadId, @activeRunId, @statusCommentId,
233
237
  @agentSessionId, @lastLinearActivityKey,
234
- @prNumber, @prUrl, @prState, @prHeadSha, @prAuthorLogin, @prReviewState, @prCheckStatus, @lastBlockingReviewHeadSha,
238
+ @prNumber, @prUrl, @prState, @prIsDraft, @prHeadSha, @prAuthorLogin, @prReviewState, @prCheckStatus, @lastBlockingReviewHeadSha,
235
239
  @lastGitHubFailureSource, @lastGitHubFailureHeadSha, @lastGitHubFailureSignature, @lastGitHubFailureCheckName, @lastGitHubFailureCheckUrl, @lastGitHubFailureContextJson, @lastGitHubFailureAt,
236
240
  @lastGitHubCiSnapshotHeadSha, @lastGitHubCiSnapshotGateCheckName, @lastGitHubCiSnapshotGateCheckStatus, @lastGitHubCiSnapshotJson, @lastGitHubCiSnapshotSettledAt,
237
241
  @lastQueueSignalAt, @lastQueueIncidentJson,
@@ -264,6 +268,7 @@ export class IssueStore {
264
268
  prNumber: params.prNumber ?? null,
265
269
  prUrl: params.prUrl ?? null,
266
270
  prState: params.prState ?? null,
271
+ prIsDraft: params.prIsDraft == null ? null : params.prIsDraft ? 1 : 0,
267
272
  prHeadSha: params.prHeadSha ?? null,
268
273
  prAuthorLogin: params.prAuthorLogin ?? null,
269
274
  prReviewState: params.prReviewState ?? null,
@@ -495,6 +500,7 @@ export function mapIssueRow(row) {
495
500
  ...(row.pr_number !== null && row.pr_number !== undefined ? { prNumber: Number(row.pr_number) } : {}),
496
501
  ...(row.pr_url !== null && row.pr_url !== undefined ? { prUrl: String(row.pr_url) } : {}),
497
502
  ...(row.pr_state !== null && row.pr_state !== undefined ? { prState: String(row.pr_state) } : {}),
503
+ ...(row.pr_is_draft !== null && row.pr_is_draft !== undefined ? { prIsDraft: Boolean(row.pr_is_draft) } : {}),
498
504
  ...(row.pr_head_sha !== null && row.pr_head_sha !== undefined ? { prHeadSha: String(row.pr_head_sha) } : {}),
499
505
  ...(row.pr_author_login !== null && row.pr_author_login !== undefined ? { prAuthorLogin: String(row.pr_author_login) } : {}),
500
506
  ...(row.pr_review_state !== null && row.pr_review_state !== undefined ? { prReviewState: String(row.pr_review_state) } : {}),
@@ -22,6 +22,7 @@ CREATE TABLE IF NOT EXISTS issues (
22
22
  pr_number INTEGER,
23
23
  pr_url TEXT,
24
24
  pr_state TEXT,
25
+ pr_is_draft INTEGER,
25
26
  pr_head_sha TEXT,
26
27
  pr_author_login TEXT,
27
28
  pr_review_state TEXT,
@@ -275,6 +276,7 @@ export function runPatchRelayMigrations(connection) {
275
276
  // branch CI failures from merge-queue evictions after webhook delivery.
276
277
  addColumnIfMissing(connection, "issues", "pr_head_sha", "TEXT");
277
278
  addColumnIfMissing(connection, "issues", "pr_author_login", "TEXT");
279
+ addColumnIfMissing(connection, "issues", "pr_is_draft", "INTEGER");
278
280
  addColumnIfMissing(connection, "issues", "last_github_failure_source", "TEXT");
279
281
  addColumnIfMissing(connection, "issues", "last_github_failure_head_sha", "TEXT");
280
282
  addColumnIfMissing(connection, "issues", "last_github_failure_signature", "TEXT");
@@ -335,6 +337,7 @@ function removeRetiredIssueColumnsIfPresent(connection) {
335
337
  pr_number INTEGER,
336
338
  pr_url TEXT,
337
339
  pr_state TEXT,
340
+ pr_is_draft INTEGER,
338
341
  pr_head_sha TEXT,
339
342
  pr_author_login TEXT,
340
343
  pr_review_state TEXT,
@@ -391,6 +394,7 @@ function removeRetiredIssueColumnsIfPresent(connection) {
391
394
  pr_number,
392
395
  pr_url,
393
396
  pr_state,
397
+ pr_is_draft,
394
398
  pr_head_sha,
395
399
  pr_author_login,
396
400
  pr_review_state,
@@ -445,6 +449,7 @@ function removeRetiredIssueColumnsIfPresent(connection) {
445
449
  pr_number,
446
450
  pr_url,
447
451
  pr_state,
452
+ pr_is_draft,
448
453
  pr_head_sha,
449
454
  pr_author_login,
450
455
  pr_review_state,
@@ -0,0 +1,39 @@
1
+ export function appendDelegationObservedEvent(db, params) {
2
+ db.issueSessions.appendIssueSessionEventRespectingActiveLease(params.projectId, params.linearIssueId, {
3
+ projectId: params.projectId,
4
+ linearIssueId: params.linearIssueId,
5
+ eventType: "delegation_observed",
6
+ eventJson: JSON.stringify(params.payload),
7
+ });
8
+ }
9
+ export function appendRunReleasedAuthorityEvent(db, params) {
10
+ db.issueSessions.appendIssueSessionEventRespectingActiveLease(params.projectId, params.linearIssueId, {
11
+ projectId: params.projectId,
12
+ linearIssueId: params.linearIssueId,
13
+ eventType: "run_released_authority",
14
+ eventJson: JSON.stringify(params.payload),
15
+ });
16
+ }
17
+ export function parseDelegationObservedPayload(event) {
18
+ if (event.eventType !== "delegation_observed" || !event.eventJson) {
19
+ return undefined;
20
+ }
21
+ return parseObject(event.eventJson);
22
+ }
23
+ export function parseRunReleasedAuthorityPayload(event) {
24
+ if (event.eventType !== "run_released_authority" || !event.eventJson) {
25
+ return undefined;
26
+ }
27
+ return parseObject(event.eventJson);
28
+ }
29
+ function parseObject(raw) {
30
+ try {
31
+ const parsed = JSON.parse(raw);
32
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed)
33
+ ? parsed
34
+ : undefined;
35
+ }
36
+ catch {
37
+ return undefined;
38
+ }
39
+ }
@@ -0,0 +1,104 @@
1
+ import { deriveGateCheckStatusFromRollup } from "./github-rollup.js";
2
+ import { deriveIssueSessionReactiveIntent } from "./issue-session.js";
3
+ import { buildClosedPrCleanupFields } from "./pr-state.js";
4
+ import { buildReviewFixBranchUpkeepContext, normalizeRemotePrState, normalizeRemoteReviewDecision, } from "./reactive-pr-state.js";
5
+ export function deriveLinkedPrAdoptionOutcome(project, prNumber, remote) {
6
+ const prState = normalizeRemotePrState(remote.state);
7
+ const reviewState = normalizeRemoteReviewDecision(remote.reviewDecision);
8
+ const configuredGateChecks = (project.gateChecks ?? []).map((entry) => entry.trim()).filter(Boolean);
9
+ const gateCheckNames = configuredGateChecks.length > 0 ? configuredGateChecks : ["verify"];
10
+ const primaryGateCheck = gateCheckNames[0];
11
+ const gateCheckStatus = deriveGateCheckStatusFromRollup(remote.statusCheckRollup, gateCheckNames);
12
+ const mergeConflictDetected = remote.mergeable === "CONFLICTING" || remote.mergeStateStatus === "DIRTY";
13
+ const downstreamOwned = reviewState === "approved";
14
+ const issueUpdates = {
15
+ prNumber,
16
+ ...(remote.url ? { prUrl: remote.url } : {}),
17
+ ...(prState ? { prState } : {}),
18
+ ...(typeof remote.isDraft === "boolean" ? { prIsDraft: remote.isDraft } : {}),
19
+ ...(remote.headRefName ? { branchName: remote.headRefName } : {}),
20
+ ...(remote.headRefOid ? { prHeadSha: remote.headRefOid } : {}),
21
+ ...(remote.author?.login ? { prAuthorLogin: remote.author.login } : {}),
22
+ ...(reviewState ? { prReviewState: reviewState } : {}),
23
+ ...(gateCheckStatus ? { prCheckStatus: gateCheckStatus } : {}),
24
+ ...(reviewState === "changes_requested"
25
+ ? { lastBlockingReviewHeadSha: remote.headRefOid ?? null }
26
+ : { lastBlockingReviewHeadSha: null }),
27
+ ...(remote.headRefOid && gateCheckStatus
28
+ ? {
29
+ lastGitHubCiSnapshotHeadSha: remote.headRefOid,
30
+ lastGitHubCiSnapshotGateCheckName: primaryGateCheck,
31
+ lastGitHubCiSnapshotGateCheckStatus: gateCheckStatus,
32
+ lastGitHubCiSnapshotSettledAt: gateCheckStatus === "pending" ? null : new Date().toISOString(),
33
+ }
34
+ : {}),
35
+ };
36
+ if (prState === "merged") {
37
+ return {
38
+ factoryState: "done",
39
+ pendingRunType: null,
40
+ issueUpdates: {
41
+ ...issueUpdates,
42
+ prIsDraft: false,
43
+ },
44
+ };
45
+ }
46
+ if (prState === "closed") {
47
+ return {
48
+ factoryState: "awaiting_input",
49
+ pendingRunType: null,
50
+ issueUpdates: {
51
+ ...issueUpdates,
52
+ prIsDraft: false,
53
+ ...buildClosedPrCleanupFields(),
54
+ },
55
+ };
56
+ }
57
+ if (remote.isCrossRepository) {
58
+ return {
59
+ factoryState: "awaiting_input",
60
+ pendingRunType: null,
61
+ issueUpdates,
62
+ };
63
+ }
64
+ if (remote.isDraft) {
65
+ return {
66
+ factoryState: "delegated",
67
+ pendingRunType: "implementation",
68
+ issueUpdates,
69
+ };
70
+ }
71
+ const reactiveIntent = deriveIssueSessionReactiveIntent({
72
+ delegatedToPatchRelay: true,
73
+ prNumber,
74
+ prState,
75
+ prReviewState: reviewState,
76
+ prCheckStatus: gateCheckStatus,
77
+ mergeConflictDetected,
78
+ downstreamOwned,
79
+ });
80
+ if (reactiveIntent) {
81
+ return {
82
+ factoryState: reactiveIntent.compatibilityFactoryState,
83
+ pendingRunType: reactiveIntent.runType,
84
+ ...(reactiveIntent.runType === "branch_upkeep"
85
+ ? {
86
+ pendingRunContext: buildReviewFixBranchUpkeepContext(prNumber, project.github?.baseBranch ?? "main", remote),
87
+ }
88
+ : {}),
89
+ issueUpdates,
90
+ };
91
+ }
92
+ if (reviewState === "approved") {
93
+ return {
94
+ factoryState: "awaiting_queue",
95
+ pendingRunType: null,
96
+ issueUpdates,
97
+ };
98
+ }
99
+ return {
100
+ factoryState: "pr_open",
101
+ pendingRunType: null,
102
+ issueUpdates,
103
+ };
104
+ }
@@ -88,7 +88,7 @@ export class IssueOverviewQuery {
88
88
  const runCount = runs.length;
89
89
  const liveThread = await this.readLiveThread(activeRun);
90
90
  const failureContext = parseGitHubFailureContext(issueRecord?.lastGitHubFailureContextJson);
91
- const waitingReason = session.waitingReason ?? derivePatchRelayWaitingReason({
91
+ const derivedWaitingReason = derivePatchRelayWaitingReason({
92
92
  delegatedToPatchRelay: issueRecord?.delegatedToPatchRelay,
93
93
  ...(activeRun ? { activeRunType: activeRun.runType } : {}),
94
94
  blockedByKeys,
@@ -102,6 +102,7 @@ export class IssueOverviewQuery {
102
102
  lastBlockingReviewHeadSha: issueRecord?.lastBlockingReviewHeadSha,
103
103
  latestFailureCheckName: issueRecord?.lastGitHubFailureCheckName,
104
104
  });
105
+ const waitingReason = derivedWaitingReason ?? session.waitingReason;
105
106
  const issue = {
106
107
  id: issueRecord?.id ?? session.id,
107
108
  projectId: session.projectId,
@@ -7,14 +7,19 @@ const TERMINAL_SESSION_EVENTS = new Set([
7
7
  "pr_closed",
8
8
  "pr_merged",
9
9
  ]);
10
+ const NON_ACTIONABLE_SESSION_EVENTS = new Set([
11
+ "delegation_observed",
12
+ "run_released_authority",
13
+ ]);
10
14
  const RUN_TYPES = new Set(["implementation", "review_fix", "branch_upkeep", "ci_repair", "queue_repair"]);
11
15
  function parseRunType(value) {
12
16
  return typeof value === "string" && RUN_TYPES.has(value) ? value : undefined;
13
17
  }
14
18
  export function deriveSessionWakePlan(issue, events) {
15
- if (events.length === 0)
19
+ const actionableEvents = events.filter((event) => !NON_ACTIONABLE_SESSION_EVENTS.has(event.eventType));
20
+ if (actionableEvents.length === 0)
16
21
  return undefined;
17
- if (events.some((event) => TERMINAL_SESSION_EVENTS.has(event.eventType))) {
22
+ if (actionableEvents.some((event) => TERMINAL_SESSION_EVENTS.has(event.eventType))) {
18
23
  return undefined;
19
24
  }
20
25
  const context = {};
@@ -22,7 +27,7 @@ export function deriveSessionWakePlan(issue, events) {
22
27
  let wakeReason;
23
28
  let runType;
24
29
  let resumeThread = false;
25
- for (const event of events) {
30
+ for (const event of actionableEvents) {
26
31
  const payload = parseEventJson(event.eventJson);
27
32
  switch (event.eventType) {
28
33
  case "merge_steward_incident":
@@ -128,6 +133,9 @@ export function deriveSessionWakePlan(issue, events) {
128
133
  }
129
134
  return { runType, wakeReason, resumeThread, context };
130
135
  }
136
+ export function isActionableIssueSessionEventType(eventType) {
137
+ return !NON_ACTIONABLE_SESSION_EVENTS.has(eventType);
138
+ }
131
139
  export function extractLatestAssistantSummary(run) {
132
140
  if (!run)
133
141
  return undefined;
@@ -49,6 +49,8 @@ export function deriveIssueSessionReactiveIntent(params) {
49
49
  return undefined;
50
50
  if (params.prState && params.prState !== "open")
51
51
  return undefined;
52
+ if (params.prIsDraft)
53
+ return undefined;
52
54
  if (params.latestFailureSource === "queue_eviction" || (params.mergeConflictDetected && params.downstreamOwned)) {
53
55
  return {
54
56
  runType: "queue_repair",
@@ -6,6 +6,14 @@ const LINEAR_ISSUE_SELECTION = `
6
6
  title
7
7
  description
8
8
  url
9
+ attachments {
10
+ nodes {
11
+ id
12
+ title
13
+ subtitle
14
+ url
15
+ }
16
+ }
9
17
  priority
10
18
  estimate
11
19
  delegate {
@@ -316,12 +324,21 @@ export class LinearGraphqlClient {
316
324
  const teamLabels = (issue.team?.labels?.nodes ?? []).map((label) => ({ id: label.id, name: label.name }));
317
325
  const blocksRelations = (issue.relations?.nodes ?? []).filter((relation) => relation.type?.trim().toLowerCase() === "blocks");
318
326
  const blockedByRelations = (issue.inverseRelations?.nodes ?? []).filter((relation) => relation.type?.trim().toLowerCase() === "blocks");
327
+ const attachments = (issue.attachments?.nodes ?? [])
328
+ .filter((attachment) => Boolean(attachment?.url))
329
+ .map((attachment) => ({
330
+ id: attachment.id,
331
+ ...(attachment.title ? { title: attachment.title } : {}),
332
+ ...(attachment.subtitle ? { subtitle: attachment.subtitle } : {}),
333
+ url: attachment.url,
334
+ }));
319
335
  return {
320
336
  id: issue.id,
321
337
  ...(issue.identifier ? { identifier: issue.identifier } : {}),
322
338
  ...(issue.title ? { title: issue.title } : {}),
323
339
  ...(issue.description ? { description: issue.description } : {}),
324
340
  ...(issue.url ? { url: issue.url } : {}),
341
+ ...(attachments.length > 0 ? { attachments } : {}),
325
342
  ...(issue.priority != null ? { priority: issue.priority } : {}),
326
343
  ...(issue.estimate != null ? { estimate: issue.estimate } : {}),
327
344
  ...(issue.state?.id ? { stateId: issue.state.id } : {}),
@@ -0,0 +1,44 @@
1
+ const GITHUB_PR_URL_PATTERN = /^https:\/\/github\.com\/([^/]+)\/([^/]+)\/pull\/(\d+)(?:[/?#].*)?$/i;
2
+ export function resolveLinkedPullRequest(attachments, repoFullName) {
3
+ if (!repoFullName || !attachments || attachments.length === 0) {
4
+ return { kind: "none" };
5
+ }
6
+ const matches = attachments
7
+ .map((attachment) => parseGitHubPullRequestUrl(attachment.url))
8
+ .filter((reference) => Boolean(reference))
9
+ .filter((reference) => reference.repoFullName.toLowerCase() === repoFullName.toLowerCase());
10
+ const unique = dedupeReferences(matches);
11
+ if (unique.length === 0) {
12
+ return { kind: "none" };
13
+ }
14
+ if (unique.length === 1) {
15
+ return { kind: "matched", reference: unique[0] };
16
+ }
17
+ return { kind: "ambiguous", references: unique };
18
+ }
19
+ function parseGitHubPullRequestUrl(url) {
20
+ const match = url.trim().match(GITHUB_PR_URL_PATTERN);
21
+ if (!match)
22
+ return undefined;
23
+ const [, owner, repo, prNumberRaw] = match;
24
+ const prNumber = Number(prNumberRaw);
25
+ if (!Number.isInteger(prNumber) || prNumber <= 0)
26
+ return undefined;
27
+ return {
28
+ repoFullName: `${owner}/${repo}`,
29
+ prNumber,
30
+ url,
31
+ };
32
+ }
33
+ function dedupeReferences(references) {
34
+ const seen = new Set();
35
+ const unique = [];
36
+ for (const reference of references) {
37
+ const key = `${reference.repoFullName.toLowerCase()}#${reference.prNumber}`;
38
+ if (seen.has(key))
39
+ continue;
40
+ seen.add(key);
41
+ unique.push(reference);
42
+ }
43
+ return unique;
44
+ }