patchrelay 0.40.1 → 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.1",
4
- "commit": "a0d40b1e8b30",
5
- "builtAt": "2026-04-12T19:50:43.295Z"
3
+ "version": "0.41.0",
4
+ "commit": "4ad3af5dce7c",
5
+ "builtAt": "2026-04-12T21:16:04.426Z"
6
6
  }
@@ -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,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
+ }
@@ -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
+ }
@@ -3,7 +3,7 @@ export async function readRemotePrState(repoFullName, prNumber) {
3
3
  const { stdout, exitCode } = await execCommand("gh", [
4
4
  "pr", "view", String(prNumber),
5
5
  "--repo", repoFullName,
6
- "--json", "headRefOid,state,reviewDecision,mergeStateStatus",
6
+ "--json", "url,headRefName,headRefOid,isDraft,isCrossRepository,state,author,reviewDecision,mergeable,mergeStateStatus,statusCheckRollup",
7
7
  ], { timeoutMs: 10_000 });
8
8
  if (exitCode !== 0)
9
9
  return undefined;
@@ -38,10 +38,17 @@ export function resolveReDelegationResume(p) {
38
38
  if (p.prState === "merged") {
39
39
  return { factoryState: "done", pendingRunType: null };
40
40
  }
41
+ if (p.prNumber !== undefined && (p.prState === undefined || p.prState === "open") && p.prIsDraft) {
42
+ return {
43
+ factoryState: "delegated",
44
+ pendingRunType: (p.unresolvedBlockers ?? 0) === 0 ? "implementation" : null,
45
+ };
46
+ }
41
47
  const reactiveIntent = deriveIssueSessionReactiveIntent({
42
48
  delegatedToPatchRelay: true,
43
49
  prNumber: p.prNumber,
44
50
  prState: p.prState,
51
+ prIsDraft: p.prIsDraft,
45
52
  prReviewState: p.prReviewState,
46
53
  prCheckStatus: p.prCheckStatus,
47
54
  latestFailureSource: p.latestFailureSource,
@@ -100,6 +107,7 @@ export function mergeIssueMetadata(issue, liveIssue) {
100
107
  ...(issue.identifier ? {} : liveIssue.identifier ? { identifier: liveIssue.identifier } : {}),
101
108
  ...(issue.title ? {} : liveIssue.title ? { title: liveIssue.title } : {}),
102
109
  ...(issue.url ? {} : liveIssue.url ? { url: liveIssue.url } : {}),
110
+ ...(issue.attachments && issue.attachments.length > 0 ? {} : liveIssue.attachments ? { attachments: liveIssue.attachments } : {}),
103
111
  ...(issue.teamId ? {} : liveIssue.teamId ? { teamId: liveIssue.teamId } : {}),
104
112
  ...(issue.teamKey ? {} : liveIssue.teamKey ? { teamKey: liveIssue.teamKey } : {}),
105
113
  ...(issue.stateId ? {} : liveIssue.stateId ? { stateId: liveIssue.stateId } : {}),
@@ -3,6 +3,9 @@ import { resolveAwaitingInputReason } from "../awaiting-input-reason.js";
3
3
  import { appendDelegationObservedEvent } from "../delegation-audit.js";
4
4
  import { decideActiveRunRelease, decideAgentSession, decideRunIntent, decideUnDelegation, isTerminalDelegationState, mergeIssueMetadata, resolveReDelegationResume, } from "./decision-helpers.js";
5
5
  import { buildOperatorRetryEvent } from "../operator-retry-event.js";
6
+ import { resolveLinkedPullRequest } from "../linear-linked-pr-reconciliation.js";
7
+ import { readRemotePrState } from "../remote-pr-state.js";
8
+ import { deriveLinkedPrAdoptionOutcome } from "../delegation-linked-pr.js";
6
9
  export class DesiredStageRecorder {
7
10
  db;
8
11
  linearProvider;
@@ -40,6 +43,13 @@ export class DesiredStageRecorder {
40
43
  activeRunId: activeRun?.id,
41
44
  });
42
45
  const delegated = delegation.delegated;
46
+ const linkedPrAdoption = await this.resolveLinkedPrAdoption({
47
+ project: params.project,
48
+ issue: hydratedIssue,
49
+ existingIssue,
50
+ delegated,
51
+ triggerEvent: params.normalized.triggerEvent,
52
+ });
43
53
  const unresolvedBlockers = this.db.issues.countUnresolvedBlockers(params.project.id, normalizedIssue.id);
44
54
  const terminal = isTerminalDelegationState(existingIssue, hydratedIssue);
45
55
  const openPrExists = existingIssue?.prNumber !== undefined
@@ -48,16 +58,18 @@ export class DesiredStageRecorder {
48
58
  const blockerPausedImplementation = unresolvedBlockers > 0
49
59
  && activeRun?.runType === "implementation"
50
60
  && !openPrExists;
51
- const desiredStage = decideRunIntent({
52
- delegated,
53
- triggerAllowed,
54
- triggerEvent: params.normalized.triggerEvent,
55
- unresolvedBlockers,
56
- hasActiveRun: Boolean(activeRun),
57
- hasPendingWake,
58
- terminal,
59
- currentState: existingIssue?.factoryState,
60
- });
61
+ const desiredStage = linkedPrAdoption
62
+ ? undefined
63
+ : decideRunIntent({
64
+ delegated,
65
+ triggerAllowed,
66
+ triggerEvent: params.normalized.triggerEvent,
67
+ unresolvedBlockers,
68
+ hasActiveRun: Boolean(activeRun),
69
+ hasPendingWake,
70
+ terminal,
71
+ currentState: existingIssue?.factoryState,
72
+ });
61
73
  const runRelease = decideActiveRunRelease({
62
74
  hasActiveRun: Boolean(activeRun),
63
75
  terminal,
@@ -73,20 +85,31 @@ export class DesiredStageRecorder {
73
85
  currentState: existingIssue?.factoryState,
74
86
  hasPr: existingIssue?.prNumber !== undefined && existingIssue?.prState !== "merged",
75
87
  });
76
- const reDelegationResume = resolveReDelegationResume({
77
- delegated,
78
- previouslyDelegated: existingIssue?.delegatedToPatchRelay,
79
- currentState: existingIssue?.factoryState,
80
- awaitingInputReason: existingIssue
81
- ? resolveAwaitingInputReason({ issue: existingIssue, latestRun })
82
- : undefined,
83
- unresolvedBlockers,
84
- prNumber: existingIssue?.prNumber,
85
- prState: existingIssue?.prState,
86
- prReviewState: existingIssue?.prReviewState,
87
- prCheckStatus: existingIssue?.prCheckStatus,
88
- latestFailureSource: existingIssue?.lastGitHubFailureSource,
89
- });
88
+ const startupResume = linkedPrAdoption
89
+ ? {
90
+ factoryState: linkedPrAdoption.factoryState,
91
+ pendingRunType: linkedPrAdoption.pendingRunType,
92
+ pendingRunContext: linkedPrAdoption.pendingRunContext,
93
+ source: "linked_pr_adoption",
94
+ }
95
+ : {
96
+ ...resolveReDelegationResume({
97
+ delegated,
98
+ previouslyDelegated: existingIssue?.delegatedToPatchRelay,
99
+ currentState: existingIssue?.factoryState,
100
+ awaitingInputReason: existingIssue
101
+ ? resolveAwaitingInputReason({ issue: existingIssue, latestRun })
102
+ : undefined,
103
+ unresolvedBlockers,
104
+ prNumber: existingIssue?.prNumber,
105
+ prState: existingIssue?.prState,
106
+ prIsDraft: existingIssue?.prIsDraft,
107
+ prReviewState: existingIssue?.prReviewState,
108
+ prCheckStatus: existingIssue?.prCheckStatus,
109
+ latestFailureSource: existingIssue?.lastGitHubFailureSource,
110
+ }),
111
+ source: "re_delegated",
112
+ };
90
113
  const existingWakeRunType = existingIssue
91
114
  ? params.peekPendingSessionWakeRunType(params.project.id, normalizedIssue.id)
92
115
  : undefined;
@@ -109,13 +132,19 @@ export class DesiredStageRecorder {
109
132
  ...(hydratedIssue.estimate != null ? { estimate: hydratedIssue.estimate } : {}),
110
133
  ...(hydratedIssue.stateName ? { currentLinearState: hydratedIssue.stateName } : {}),
111
134
  ...(hydratedIssue.stateType ? { currentLinearStateType: hydratedIssue.stateType } : {}),
135
+ ...(linkedPrAdoption?.issueUpdates ?? {}),
112
136
  delegatedToPatchRelay: delegated,
113
137
  ...(!existingIssue && !delegated && incomingAgentSessionId ? { factoryState: "awaiting_input" } : {}),
114
- ...(reDelegationResume.factoryState ? { factoryState: reDelegationResume.factoryState } : {}),
115
- ...(reDelegationResume.pendingRunType !== undefined
116
- ? { pendingRunType: null, pendingRunContextJson: null }
138
+ ...(startupResume.factoryState ? { factoryState: startupResume.factoryState } : {}),
139
+ ...(startupResume.pendingRunType !== undefined
140
+ ? {
141
+ pendingRunType: null,
142
+ pendingRunContextJson: startupResume.pendingRunContext
143
+ ? JSON.stringify(startupResume.pendingRunContext)
144
+ : null,
145
+ }
117
146
  : {}),
118
- ...(!reDelegationResume.factoryState && desiredStage ? { pendingRunType: null, pendingRunContextJson: null, factoryState: "delegated" } : {}),
147
+ ...(!startupResume.factoryState && desiredStage ? { pendingRunType: null, pendingRunContextJson: null, factoryState: "delegated" } : {}),
119
148
  ...(clearPending ? { pendingRunType: null, pendingRunContextJson: null } : {}),
120
149
  ...(agentSessionId !== undefined ? { agentSessionId } : {}),
121
150
  ...(effectiveRunRelease.release ? { activeRunId: null } : {}),
@@ -175,15 +204,15 @@ export class DesiredStageRecorder {
175
204
  summary: `Implementation paused because ${issue.issueKey ?? normalizedIssue.id} is now blocked`,
176
205
  });
177
206
  }
178
- else if (reDelegationResume.pendingRunType) {
207
+ else if (startupResume.pendingRunType) {
179
208
  this.db.issueSessions.appendIssueSessionEventRespectingActiveLease(params.project.id, normalizedIssue.id, {
180
209
  projectId: params.project.id,
181
210
  linearIssueId: normalizedIssue.id,
182
- ...buildOperatorRetryEvent(issue, reDelegationResume.pendingRunType, "re_delegated"),
211
+ ...buildOperatorRetryEvent(issue, startupResume.pendingRunType, startupResume.source),
183
212
  });
184
213
  }
185
- else if (!reDelegationResume.factoryState
186
- && !reDelegationResume.pendingRunType
214
+ else if (!startupResume.factoryState
215
+ && !startupResume.pendingRunType
187
216
  &&
188
217
  desiredStage === "implementation"
189
218
  && params.normalized.triggerEvent !== "commentCreated"
@@ -281,4 +310,36 @@ export class DesiredStageRecorder {
281
310
  }
282
311
  return { issue: source, hydration };
283
312
  }
313
+ async resolveLinkedPrAdoption(params) {
314
+ if (!params.delegated)
315
+ return undefined;
316
+ if (params.triggerEvent !== "delegateChanged")
317
+ return undefined;
318
+ if (params.existingIssue?.prNumber !== undefined)
319
+ return undefined;
320
+ const resolution = resolveLinkedPullRequest(params.issue.attachments, params.project.github?.repoFullName);
321
+ if (resolution.kind === "none")
322
+ return undefined;
323
+ if (resolution.kind === "ambiguous") {
324
+ return {
325
+ factoryState: "awaiting_input",
326
+ pendingRunType: null,
327
+ pendingRunContext: undefined,
328
+ issueUpdates: {},
329
+ };
330
+ }
331
+ const remote = await readRemotePrState(resolution.reference.repoFullName, resolution.reference.prNumber);
332
+ if (!remote) {
333
+ return {
334
+ factoryState: "awaiting_input",
335
+ pendingRunType: null,
336
+ pendingRunContext: undefined,
337
+ issueUpdates: {
338
+ prNumber: resolution.reference.prNumber,
339
+ prUrl: resolution.reference.url,
340
+ },
341
+ };
342
+ }
343
+ return deriveLinkedPrAdoptionOutcome(params.project, resolution.reference.prNumber, remote);
344
+ }
284
345
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "patchrelay",
3
- "version": "0.40.1",
3
+ "version": "0.41.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "repository": {