patchrelay 0.40.1 → 0.41.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/build-info.json +3 -3
- package/dist/db/issue-store.js +8 -2
- package/dist/db/migrations.js +5 -0
- package/dist/delegation-linked-pr.js +104 -0
- package/dist/issue-session.js +2 -0
- package/dist/linear-client.js +17 -0
- package/dist/linear-linked-pr-reconciliation.js +44 -0
- package/dist/remote-pr-state.js +1 -1
- package/dist/webhooks/decision-helpers.js +8 -0
- package/dist/webhooks/desired-stage-recorder.js +93 -32
- package/dist/worktree-manager.js +9 -5
- package/package.json +1 -1
package/dist/build-info.json
CHANGED
package/dist/db/issue-store.js
CHANGED
|
@@ -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) } : {}),
|
package/dist/db/migrations.js
CHANGED
|
@@ -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
|
+
}
|
package/dist/issue-session.js
CHANGED
|
@@ -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",
|
package/dist/linear-client.js
CHANGED
|
@@ -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
|
+
}
|
package/dist/remote-pr-state.js
CHANGED
|
@@ -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 =
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
...(
|
|
115
|
-
...(
|
|
116
|
-
? {
|
|
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
|
-
...(!
|
|
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 (
|
|
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,
|
|
211
|
+
...buildOperatorRetryEvent(issue, startupResume.pendingRunType, startupResume.source),
|
|
183
212
|
});
|
|
184
213
|
}
|
|
185
|
-
else if (!
|
|
186
|
-
&& !
|
|
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/dist/worktree-manager.js
CHANGED
|
@@ -48,7 +48,7 @@ export class WorktreeManager {
|
|
|
48
48
|
await execCommand(gitBin, ["-C", worktreePath, "reset", "--hard", "HEAD"], { timeoutMs: 30_000 });
|
|
49
49
|
await execCommand(gitBin, ["-C", worktreePath, "clean", "-fd"], { timeoutMs: 30_000 });
|
|
50
50
|
const checkoutTarget = hasRemoteBranch ? `origin/${branchName}` : branchName;
|
|
51
|
-
const checkoutResult = await execCommand(gitBin, ["-C", worktreePath, "checkout", "-B", branchName, checkoutTarget], { timeoutMs: 30_000 });
|
|
51
|
+
const checkoutResult = await execCommand(gitBin, ["-C", worktreePath, "checkout", "--ignore-other-worktrees", "-B", branchName, checkoutTarget], { timeoutMs: 30_000 });
|
|
52
52
|
if (checkoutResult.exitCode !== 0) {
|
|
53
53
|
throw new Error(`Failed to restore ${branchName} worktree state: ${checkoutResult.stderr?.slice(0, 300) ?? "git checkout failed"}`);
|
|
54
54
|
}
|
|
@@ -84,12 +84,16 @@ export class WorktreeManager {
|
|
|
84
84
|
// Fetch latest main so the branch forks from a clean, up-to-date base.
|
|
85
85
|
// This prevents branch contamination when local HEAD has drifted.
|
|
86
86
|
// freshenWorktree in run-orchestrator acts as a secondary safety net.
|
|
87
|
-
await execCommand(this.config.runner.gitBin, ["-C", repoPath, "fetch", "origin", "main"], {
|
|
87
|
+
const fetchResult = await execCommand(this.config.runner.gitBin, ["-C", repoPath, "fetch", "origin", "main"], {
|
|
88
88
|
timeoutMs: 60_000,
|
|
89
89
|
});
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
}
|
|
90
|
+
if (fetchResult.exitCode !== 0) {
|
|
91
|
+
throw new Error(`Failed to fetch origin/main before creating issue worktree: ${fetchResult.stderr?.slice(0, 300) ?? "git fetch failed"}`);
|
|
92
|
+
}
|
|
93
|
+
const addResult = await execCommand(this.config.runner.gitBin, ["-C", repoPath, "worktree", "add", "--detach", worktreePath, "origin/main"], { timeoutMs: 120_000 });
|
|
94
|
+
if (addResult.exitCode !== 0) {
|
|
95
|
+
throw new Error(`Failed to create issue worktree at ${worktreePath}: ${addResult.stderr?.slice(0, 300) ?? "git worktree add failed"}`);
|
|
96
|
+
}
|
|
93
97
|
}
|
|
94
98
|
async assertTrustedExistingWorktree(repoPath, worktreeRoot, worktreePath, options) {
|
|
95
99
|
const worktreeStats = lstatSync(worktreePath);
|