patchrelay 0.37.1 → 0.38.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/README.md +47 -9
- package/dist/awaiting-input-reason.js +9 -0
- package/dist/build-info.json +3 -3
- package/dist/cli/cluster-health.js +59 -3
- package/dist/cli/help.js +1 -1
- package/dist/cli/output.js +2 -0
- package/dist/db/issue-session-store.js +0 -14
- package/dist/db/issue-store.js +8 -16
- package/dist/db/migrations.js +6 -13
- package/dist/db.js +1 -3
- package/dist/github-linear-session-sync.js +57 -0
- package/dist/github-pr-comment-handler.js +74 -0
- package/dist/github-webhook-failure-context.js +70 -0
- package/dist/github-webhook-handler.js +49 -965
- package/dist/github-webhook-issue-resolution.js +46 -0
- package/dist/github-webhook-policy.js +105 -0
- package/dist/github-webhook-reactive-run.js +302 -0
- package/dist/github-webhook-state-projector.js +231 -0
- package/dist/github-webhook-terminal-handler.js +111 -0
- package/dist/github-webhooks.js +4 -0
- package/dist/idle-reconciliation.js +22 -23
- package/dist/issue-overview-query.js +11 -57
- package/dist/issue-session-projector.js +1 -0
- package/dist/issue-session.js +8 -0
- package/dist/legacy-issue-overview.js +58 -0
- package/dist/linear-session-reporting.js +30 -1
- package/dist/linear-session-sync.js +9 -1
- package/dist/linear-status-comment-sync.js +34 -1
- package/dist/linear-workflow-state-sync.js +2 -2
- package/dist/operator-retry-event.js +15 -12
- package/dist/paused-issue-state.js +24 -0
- package/dist/reactive-pr-state.js +65 -0
- package/dist/reactive-run-policy.js +35 -118
- package/dist/remote-pr-state.js +11 -0
- package/dist/run-launcher.js +0 -1
- package/dist/run-orchestrator.js +22 -11
- package/dist/run-reconciler.js +10 -0
- package/dist/run-recovery-service.js +1 -10
- package/dist/service-issue-actions.js +5 -0
- package/dist/service-startup-recovery.js +9 -6
- package/dist/service.js +0 -1
- package/dist/tracked-issue-list-query.js +3 -1
- package/dist/tracked-issue-projector.js +3 -0
- package/dist/waiting-reason.js +10 -0
- package/dist/webhooks/agent-session-handler.js +9 -1
- package/dist/webhooks/comment-wake-handler.js +12 -0
- package/dist/webhooks/decision-helpers.js +44 -3
- package/dist/webhooks/dependency-readiness-handler.js +1 -0
- package/dist/webhooks/desired-stage-recorder.js +40 -10
- package/package.json +1 -1
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { resolveClosedPrDisposition, resolveClosedPrFactoryState } from "./pr-state.js";
|
|
2
|
+
import { resolvePreferredCompletedLinearState } from "./linear-workflow.js";
|
|
3
|
+
import { syncGitHubLinearSession } from "./github-linear-session-sync.js";
|
|
4
|
+
export async function handleGitHubTerminalPrEvent(params) {
|
|
5
|
+
const { db, linearProvider, enqueueIssue, logger, codex, issue, event, config } = params;
|
|
6
|
+
const eventType = event.triggerEvent === "pr_merged" ? "pr_merged" : "pr_closed";
|
|
7
|
+
db.issueSessions.appendIssueSessionEvent({
|
|
8
|
+
projectId: issue.projectId,
|
|
9
|
+
linearIssueId: issue.linearIssueId,
|
|
10
|
+
eventType,
|
|
11
|
+
dedupeKey: [eventType, issue.prNumber ?? event.prNumber ?? "unknown-pr", issue.prHeadSha ?? event.headSha ?? "unknown-sha"].join("::"),
|
|
12
|
+
});
|
|
13
|
+
db.issueSessions.clearPendingIssueSessionEventsRespectingActiveLease(issue.projectId, issue.linearIssueId);
|
|
14
|
+
const run = issue.activeRunId ? db.runs.getRunById(issue.activeRunId) : undefined;
|
|
15
|
+
if (run?.threadId && run.turnId) {
|
|
16
|
+
try {
|
|
17
|
+
await codex.steerTurn({
|
|
18
|
+
threadId: run.threadId,
|
|
19
|
+
turnId: run.turnId,
|
|
20
|
+
input: event.triggerEvent === "pr_merged"
|
|
21
|
+
? "STOP: The pull request has already merged. Stop working immediately and exit without making further changes."
|
|
22
|
+
: "STOP: The pull request was closed. Stop working immediately and exit without making further changes.",
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
logger.warn({ issueKey: issue.issueKey, runId: run.id, error: error instanceof Error ? error.message : String(error) }, "Failed to steer active run after terminal PR event");
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
const commitTerminalUpdate = () => {
|
|
30
|
+
if (run) {
|
|
31
|
+
db.runs.finishRun(run.id, {
|
|
32
|
+
status: "released",
|
|
33
|
+
failureReason: event.triggerEvent === "pr_merged"
|
|
34
|
+
? "Pull request merged during active run"
|
|
35
|
+
: "Pull request closed during active run",
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
const terminalFactoryState = event.triggerEvent === "pr_merged"
|
|
39
|
+
? "done"
|
|
40
|
+
: resolveClosedPrFactoryState(issue);
|
|
41
|
+
db.issues.upsertIssue({
|
|
42
|
+
projectId: issue.projectId,
|
|
43
|
+
linearIssueId: issue.linearIssueId,
|
|
44
|
+
activeRunId: null,
|
|
45
|
+
factoryState: terminalFactoryState,
|
|
46
|
+
});
|
|
47
|
+
};
|
|
48
|
+
const activeLease = db.issueSessions.getActiveIssueSessionLease(issue.projectId, issue.linearIssueId);
|
|
49
|
+
if (activeLease) {
|
|
50
|
+
db.issueSessions.withIssueSessionLease(issue.projectId, issue.linearIssueId, activeLease.leaseId, commitTerminalUpdate);
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
db.transaction(commitTerminalUpdate);
|
|
54
|
+
}
|
|
55
|
+
db.issueSessions.releaseIssueSessionLeaseRespectingActiveLease(issue.projectId, issue.linearIssueId);
|
|
56
|
+
const updatedIssue = db.issues.getIssue(issue.projectId, issue.linearIssueId) ?? issue;
|
|
57
|
+
if (event.triggerEvent === "pr_closed" && resolveClosedPrDisposition(issue) === "redelegate") {
|
|
58
|
+
db.issueSessions.appendIssueSessionEventRespectingActiveLease(issue.projectId, issue.linearIssueId, {
|
|
59
|
+
projectId: issue.projectId,
|
|
60
|
+
linearIssueId: issue.linearIssueId,
|
|
61
|
+
eventType: "delegated",
|
|
62
|
+
dedupeKey: `github_pr_closed:implementation:${issue.linearIssueId}`,
|
|
63
|
+
});
|
|
64
|
+
if (db.issueSessions.peekIssueSessionWake(issue.projectId, issue.linearIssueId)) {
|
|
65
|
+
enqueueIssue(issue.projectId, issue.linearIssueId);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (event.triggerEvent === "pr_merged") {
|
|
69
|
+
await completeLinearIssueAfterMerge(params, updatedIssue);
|
|
70
|
+
}
|
|
71
|
+
void syncGitHubLinearSession({
|
|
72
|
+
config,
|
|
73
|
+
linearProvider,
|
|
74
|
+
logger,
|
|
75
|
+
issue: updatedIssue,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
async function completeLinearIssueAfterMerge(params, issue) {
|
|
79
|
+
const linear = await params.linearProvider.forProject(issue.projectId).catch(() => undefined);
|
|
80
|
+
if (!linear)
|
|
81
|
+
return;
|
|
82
|
+
try {
|
|
83
|
+
const liveIssue = await linear.getIssue(issue.linearIssueId);
|
|
84
|
+
const targetState = resolvePreferredCompletedLinearState(liveIssue);
|
|
85
|
+
if (!targetState) {
|
|
86
|
+
params.logger.warn({ issueKey: issue.issueKey }, "Could not find a completed Linear workflow state after merge");
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const normalizedCurrent = liveIssue.stateName?.trim().toLowerCase();
|
|
90
|
+
if (normalizedCurrent === targetState.trim().toLowerCase()) {
|
|
91
|
+
params.db.issues.upsertIssue({
|
|
92
|
+
projectId: issue.projectId,
|
|
93
|
+
linearIssueId: issue.linearIssueId,
|
|
94
|
+
...(liveIssue.stateName ? { currentLinearState: liveIssue.stateName } : {}),
|
|
95
|
+
...(liveIssue.stateType ? { currentLinearStateType: liveIssue.stateType } : {}),
|
|
96
|
+
});
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const updated = await linear.setIssueState(issue.linearIssueId, targetState);
|
|
100
|
+
params.db.issues.upsertIssue({
|
|
101
|
+
projectId: issue.projectId,
|
|
102
|
+
linearIssueId: issue.linearIssueId,
|
|
103
|
+
...(updated.stateName ? { currentLinearState: updated.stateName } : {}),
|
|
104
|
+
...(updated.stateType ? { currentLinearStateType: updated.stateType } : {}),
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
109
|
+
params.logger.warn({ issueKey: issue.issueKey, error: msg }, "Failed to move merged issue to a completed Linear state");
|
|
110
|
+
}
|
|
111
|
+
}
|
package/dist/github-webhooks.js
CHANGED
|
@@ -61,6 +61,8 @@ function normalizePullRequestEvent(payload, repoFullName) {
|
|
|
61
61
|
repoFullName,
|
|
62
62
|
branchName: pr.head.ref,
|
|
63
63
|
headSha: pr.head.sha,
|
|
64
|
+
prTitle: pr.title ?? undefined,
|
|
65
|
+
prBody: pr.body ?? undefined,
|
|
64
66
|
prNumber: pr.number,
|
|
65
67
|
prUrl: pr.html_url,
|
|
66
68
|
prState,
|
|
@@ -98,6 +100,8 @@ function normalizePullRequestReviewEvent(payload, repoFullName) {
|
|
|
98
100
|
repoFullName,
|
|
99
101
|
branchName: pr.head.ref,
|
|
100
102
|
headSha: pr.head.sha,
|
|
103
|
+
prTitle: pr.title ?? undefined,
|
|
104
|
+
prBody: pr.body ?? undefined,
|
|
101
105
|
prNumber: pr.number,
|
|
102
106
|
prUrl: pr.html_url,
|
|
103
107
|
prState: "open",
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import {} from "./factory-state.js";
|
|
2
1
|
import { resolveMergeQueueProtocol } from "./merge-queue-protocol.js";
|
|
3
2
|
import { parseGitHubFailureContext } from "./github-failure-context.js";
|
|
4
3
|
import { deriveGateCheckStatusFromRollup } from "./github-rollup.js";
|
|
@@ -90,15 +89,6 @@ function hasFailureProvenance(issue) {
|
|
|
90
89
|
|| issue.lastAttemptedFailureHeadSha
|
|
91
90
|
|| issue.lastAttemptedFailureSignature);
|
|
92
91
|
}
|
|
93
|
-
export function resolveBranchOwnerForStateTransition(newState, pendingRunType) {
|
|
94
|
-
if (pendingRunType)
|
|
95
|
-
return "patchrelay";
|
|
96
|
-
if (newState === "awaiting_queue")
|
|
97
|
-
return "patchrelay";
|
|
98
|
-
if (newState === "repairing_ci" || newState === "repairing_queue")
|
|
99
|
-
return "patchrelay";
|
|
100
|
-
return undefined;
|
|
101
|
-
}
|
|
102
92
|
export class IdleIssueReconciler {
|
|
103
93
|
db;
|
|
104
94
|
config;
|
|
@@ -154,6 +144,8 @@ export class IdleIssueReconciler {
|
|
|
154
144
|
await this.reconcileFromGitHub(issue);
|
|
155
145
|
}
|
|
156
146
|
for (const issue of this.db.issues.listBlockedDelegatedIssues()) {
|
|
147
|
+
if (!issue.delegatedToPatchRelay)
|
|
148
|
+
continue;
|
|
157
149
|
const unresolved = this.db.issues.countUnresolvedBlockers(issue.projectId, issue.linearIssueId);
|
|
158
150
|
if (unresolved === 0) {
|
|
159
151
|
this.db.issueSessions.appendIssueSessionEventRespectingActiveLease(issue.projectId, issue.linearIssueId, {
|
|
@@ -207,10 +199,6 @@ export class IdleIssueReconciler {
|
|
|
207
199
|
}
|
|
208
200
|
: {}),
|
|
209
201
|
});
|
|
210
|
-
const branchOwner = resolveBranchOwnerForStateTransition(newState, options?.pendingRunType);
|
|
211
|
-
if (branchOwner) {
|
|
212
|
-
this.db.issues.setBranchOwner(issue.projectId, issue.linearIssueId, branchOwner);
|
|
213
|
-
}
|
|
214
202
|
if (options?.pendingRunType) {
|
|
215
203
|
this.appendWakeEvent(issue, options.pendingRunType, options.pendingRunContext, "idle_reconciliation");
|
|
216
204
|
}
|
|
@@ -255,6 +243,9 @@ export class IdleIssueReconciler {
|
|
|
255
243
|
});
|
|
256
244
|
}
|
|
257
245
|
async routeFailedIssue(issue) {
|
|
246
|
+
if (!issue.delegatedToPatchRelay) {
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
258
249
|
issue = await this.refreshMissingFailureProvenance(issue);
|
|
259
250
|
issue = await this.reclassifyStaleBranchFailure(issue);
|
|
260
251
|
const latestRun = this.db.runs.getLatestRunForIssue(issue.projectId, issue.linearIssueId);
|
|
@@ -457,11 +448,17 @@ export class IdleIssueReconciler {
|
|
|
457
448
|
this.logger.info({ issueKey: issue.issueKey, prNumber: issue.prNumber, factoryState: issue.factoryState }, "Reconciliation: PR was closed on a terminal issue; preserving terminal state");
|
|
458
449
|
return;
|
|
459
450
|
}
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
451
|
+
if (issue.delegatedToPatchRelay) {
|
|
452
|
+
this.logger.info({ issueKey: issue.issueKey, prNumber: issue.prNumber }, "Reconciliation: PR was closed on unfinished delegated work, re-delegating for implementation");
|
|
453
|
+
this.advanceIdleIssue(issue, "delegated", {
|
|
454
|
+
pendingRunType: "implementation",
|
|
455
|
+
clearFailureProvenance: true,
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
else {
|
|
459
|
+
this.logger.info({ issueKey: issue.issueKey, prNumber: issue.prNumber }, "Reconciliation: PR was closed while undelegated; preserving paused local-work state");
|
|
460
|
+
this.advanceIdleIssue(issue, "delegated", { clearFailureProvenance: true });
|
|
461
|
+
}
|
|
465
462
|
return;
|
|
466
463
|
}
|
|
467
464
|
const headAdvanced = Boolean(pr.headRefOid && pr.headRefOid !== previousHeadSha);
|
|
@@ -481,7 +478,8 @@ export class IdleIssueReconciler {
|
|
|
481
478
|
return;
|
|
482
479
|
}
|
|
483
480
|
}
|
|
484
|
-
if (
|
|
481
|
+
if (issue.delegatedToPatchRelay
|
|
482
|
+
&& isReviewDecisionReviewRequired(pr.reviewDecision)
|
|
485
483
|
&& gateCheckStatus === "success"
|
|
486
484
|
&& hasCompletedReviewQuillVerdict(pr.statusCheckRollup)) {
|
|
487
485
|
this.logger.warn({ issueKey: issue.issueKey, prNumber: issue.prNumber, reviewDecision: pr.reviewDecision }, "Reconciliation: review-quill completed without a decisive GitHub review; escalating for operator input");
|
|
@@ -509,7 +507,8 @@ export class IdleIssueReconciler {
|
|
|
509
507
|
mergeConflictDetected,
|
|
510
508
|
downstreamOwned,
|
|
511
509
|
});
|
|
512
|
-
if (
|
|
510
|
+
if (issue.delegatedToPatchRelay
|
|
511
|
+
&& (issue.factoryState === "escalated" || issue.factoryState === "failed")
|
|
513
512
|
&& (reactiveIntent?.runType === "review_fix" || reactiveIntent?.runType === "branch_upkeep")) {
|
|
514
513
|
if (issue.reviewFixAttempts >= DEFAULT_REVIEW_FIX_BUDGET) {
|
|
515
514
|
this.logger.debug({
|
|
@@ -538,7 +537,7 @@ export class IdleIssueReconciler {
|
|
|
538
537
|
});
|
|
539
538
|
return;
|
|
540
539
|
}
|
|
541
|
-
if (reactiveIntent?.runType === "branch_upkeep" && mergeConflictDetected) {
|
|
540
|
+
if (issue.delegatedToPatchRelay && reactiveIntent?.runType === "branch_upkeep" && mergeConflictDetected) {
|
|
542
541
|
this.logger.info({ issueKey: issue.issueKey, prNumber: issue.prNumber, mergeable: pr.mergeable, mergeStateStatus: pr.mergeStateStatus }, "Reconciliation: PR still needs branch upkeep after requested changes");
|
|
543
542
|
this.advanceIdleIssue(issue, reactiveIntent.compatibilityFactoryState, {
|
|
544
543
|
pendingRunType: reactiveIntent.runType,
|
|
@@ -555,7 +554,7 @@ export class IdleIssueReconciler {
|
|
|
555
554
|
});
|
|
556
555
|
return;
|
|
557
556
|
}
|
|
558
|
-
if (reactiveIntent?.runType === "queue_repair" && mergeConflictDetected) {
|
|
557
|
+
if (issue.delegatedToPatchRelay && reactiveIntent?.runType === "queue_repair" && mergeConflictDetected) {
|
|
559
558
|
this.logger.info({ issueKey: issue.issueKey, prNumber: issue.prNumber, mergeable: pr.mergeable }, "Reconciliation: PR needs queue repair from fresh GitHub truth");
|
|
560
559
|
this.advanceIdleIssue(issue, reactiveIntent.compatibilityFactoryState, {
|
|
561
560
|
pendingRunType: reactiveIntent.runType,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { parseGitHubFailureContext } from "./github-failure-context.js";
|
|
2
2
|
import { isIssueSessionReadyForExecution } from "./issue-session.js";
|
|
3
|
+
import { getLegacyIssueOverview } from "./legacy-issue-overview.js";
|
|
3
4
|
import { deriveIssueStatusNote } from "./status-note.js";
|
|
4
5
|
import { derivePatchRelayWaitingReason } from "./waiting-reason.js";
|
|
5
6
|
export function parseStageReport(reportJson, runStatus) {
|
|
@@ -25,7 +26,13 @@ export class IssueOverviewQuery {
|
|
|
25
26
|
async getIssueOverview(issueKey) {
|
|
26
27
|
const session = this.db.issueSessions.getIssueSessionByKey(issueKey);
|
|
27
28
|
if (!session) {
|
|
28
|
-
return await
|
|
29
|
+
return await getLegacyIssueOverview({
|
|
30
|
+
db: this.db,
|
|
31
|
+
issueKey,
|
|
32
|
+
runStatusProvider: this.runStatusProvider,
|
|
33
|
+
buildRuns: (projectId, linearIssueId) => this.buildRuns(projectId, linearIssueId),
|
|
34
|
+
readLiveThread: (run) => this.readLiveThread(run),
|
|
35
|
+
});
|
|
29
36
|
}
|
|
30
37
|
return await this.getSessionIssueOverview(issueKey, session);
|
|
31
38
|
}
|
|
@@ -66,62 +73,6 @@ export class IssueOverviewQuery {
|
|
|
66
73
|
})(),
|
|
67
74
|
}));
|
|
68
75
|
}
|
|
69
|
-
async getLegacyIssueOverview(issueKey) {
|
|
70
|
-
const legacy = this.db.getIssueOverview(issueKey);
|
|
71
|
-
if (!legacy)
|
|
72
|
-
return undefined;
|
|
73
|
-
const issueRecord = this.db.issues.getIssueByKey(issueKey);
|
|
74
|
-
const activeStatus = await this.runStatusProvider.getActiveRunStatus(issueKey);
|
|
75
|
-
const activeRun = activeStatus?.run ?? legacy.activeRun;
|
|
76
|
-
const latestRun = this.db.runs.getLatestRunForIssue(legacy.issue.projectId, legacy.issue.linearIssueId);
|
|
77
|
-
const latestEvent = this.db.issueSessions.listIssueSessionEvents(legacy.issue.projectId, legacy.issue.linearIssueId, { limit: 1 }).at(-1);
|
|
78
|
-
const runs = this.buildRuns(legacy.issue.projectId, legacy.issue.linearIssueId);
|
|
79
|
-
const runCount = runs.length;
|
|
80
|
-
const liveThread = await this.readLiveThread(activeRun);
|
|
81
|
-
const statusNote = issueRecord
|
|
82
|
-
? deriveIssueStatusNote({
|
|
83
|
-
issue: issueRecord,
|
|
84
|
-
latestRun,
|
|
85
|
-
latestEvent,
|
|
86
|
-
failureSummary: legacy.issue.latestFailureSummary,
|
|
87
|
-
blockedByKeys: legacy.issue.blockedByKeys,
|
|
88
|
-
waitingReason: legacy.issue.waitingReason,
|
|
89
|
-
})
|
|
90
|
-
: legacy.issue.statusNote;
|
|
91
|
-
return {
|
|
92
|
-
issue: {
|
|
93
|
-
...legacy.issue,
|
|
94
|
-
...(statusNote ? { statusNote } : {}),
|
|
95
|
-
},
|
|
96
|
-
...(activeRun ? { activeRun } : {}),
|
|
97
|
-
...(latestRun ? { latestRun } : {}),
|
|
98
|
-
...(liveThread ? { liveThread } : {}),
|
|
99
|
-
...(runs.length > 0 ? { runs } : {}),
|
|
100
|
-
...(issueRecord
|
|
101
|
-
? {
|
|
102
|
-
issueContext: {
|
|
103
|
-
...(issueRecord.description ? { description: issueRecord.description } : {}),
|
|
104
|
-
...(issueRecord.currentLinearState ? { currentLinearState: issueRecord.currentLinearState } : {}),
|
|
105
|
-
...(issueRecord.url ? { issueUrl: issueRecord.url } : {}),
|
|
106
|
-
...(issueRecord.worktreePath ? { worktreePath: issueRecord.worktreePath } : {}),
|
|
107
|
-
...(issueRecord.branchName ? { branchName: issueRecord.branchName } : {}),
|
|
108
|
-
...(issueRecord.prUrl ? { prUrl: issueRecord.prUrl } : {}),
|
|
109
|
-
...(issueRecord.priority != null ? { priority: issueRecord.priority } : {}),
|
|
110
|
-
...(issueRecord.estimate != null ? { estimate: issueRecord.estimate } : {}),
|
|
111
|
-
ciRepairAttempts: issueRecord.ciRepairAttempts,
|
|
112
|
-
queueRepairAttempts: issueRecord.queueRepairAttempts,
|
|
113
|
-
reviewFixAttempts: issueRecord.reviewFixAttempts,
|
|
114
|
-
...(legacy.issue.latestFailureSource ? { latestFailureSource: legacy.issue.latestFailureSource } : {}),
|
|
115
|
-
...(legacy.issue.latestFailureHeadSha ? { latestFailureHeadSha: legacy.issue.latestFailureHeadSha } : {}),
|
|
116
|
-
...(legacy.issue.latestFailureCheckName ? { latestFailureCheckName: legacy.issue.latestFailureCheckName } : {}),
|
|
117
|
-
...(legacy.issue.latestFailureStepName ? { latestFailureStepName: legacy.issue.latestFailureStepName } : {}),
|
|
118
|
-
...(legacy.issue.latestFailureSummary ? { latestFailureSummary: legacy.issue.latestFailureSummary } : {}),
|
|
119
|
-
runCount,
|
|
120
|
-
},
|
|
121
|
-
}
|
|
122
|
-
: {}),
|
|
123
|
-
};
|
|
124
|
-
}
|
|
125
76
|
async getSessionIssueOverview(issueKey, session) {
|
|
126
77
|
const issueRecord = this.db.issues.getIssueByKey(issueKey);
|
|
127
78
|
const blockedBy = this.db.issues.listIssueDependencies(session.projectId, session.linearIssueId);
|
|
@@ -138,6 +89,7 @@ export class IssueOverviewQuery {
|
|
|
138
89
|
const liveThread = await this.readLiveThread(activeRun);
|
|
139
90
|
const failureContext = parseGitHubFailureContext(issueRecord?.lastGitHubFailureContextJson);
|
|
140
91
|
const waitingReason = session.waitingReason ?? derivePatchRelayWaitingReason({
|
|
92
|
+
delegatedToPatchRelay: issueRecord?.delegatedToPatchRelay,
|
|
141
93
|
...(activeRun ? { activeRunType: activeRun.runType } : {}),
|
|
142
94
|
blockedByKeys,
|
|
143
95
|
factoryState: issueRecord?.factoryState ?? "delegated",
|
|
@@ -154,6 +106,7 @@ export class IssueOverviewQuery {
|
|
|
154
106
|
id: issueRecord?.id ?? session.id,
|
|
155
107
|
projectId: session.projectId,
|
|
156
108
|
linearIssueId: session.linearIssueId,
|
|
109
|
+
delegatedToPatchRelay: issueRecord?.delegatedToPatchRelay ?? true,
|
|
157
110
|
...(session.issueKey ? { issueKey: session.issueKey } : {}),
|
|
158
111
|
...(issueRecord?.title ? { title: issueRecord.title } : {}),
|
|
159
112
|
...(issueRecord?.url ? { issueUrl: issueRecord.url } : {}),
|
|
@@ -169,6 +122,7 @@ export class IssueOverviewQuery {
|
|
|
169
122
|
readyForExecution: isIssueSessionReadyForExecution({
|
|
170
123
|
sessionState: session.sessionState,
|
|
171
124
|
factoryState: issueRecord?.factoryState ?? "delegated",
|
|
125
|
+
delegatedToPatchRelay: issueRecord?.delegatedToPatchRelay,
|
|
172
126
|
...(activeRun ? { activeRunId: activeRun.id } : {}),
|
|
173
127
|
blockedByCount: unresolvedBlockedBy.length,
|
|
174
128
|
hasPendingWake: this.db.issueSessions.peekIssueSessionWake(session.projectId, session.linearIssueId) !== undefined,
|
|
@@ -26,6 +26,7 @@ export function syncIssueSessionFromIssue(params) {
|
|
|
26
26
|
});
|
|
27
27
|
const lastWakeReason = options?.lastWakeReason
|
|
28
28
|
?? deriveIssueSessionWakeReason({
|
|
29
|
+
delegatedToPatchRelay: issue.delegatedToPatchRelay,
|
|
29
30
|
pendingRunType: issue.pendingRunType,
|
|
30
31
|
factoryState: issue.factoryState,
|
|
31
32
|
prNumber: issue.prNumber,
|
package/dist/issue-session.js
CHANGED
|
@@ -14,6 +14,8 @@ export function deriveIssueSessionWaitingReason(params) {
|
|
|
14
14
|
return derivePatchRelayWaitingReason(params);
|
|
15
15
|
}
|
|
16
16
|
export function deriveIssueSessionWakeReason(params) {
|
|
17
|
+
if (params.delegatedToPatchRelay === false)
|
|
18
|
+
return undefined;
|
|
17
19
|
if (params.pendingRunType === "implementation")
|
|
18
20
|
return "delegated";
|
|
19
21
|
if (params.pendingRunType === "review_fix")
|
|
@@ -27,6 +29,7 @@ export function deriveIssueSessionWakeReason(params) {
|
|
|
27
29
|
if (params.factoryState === "awaiting_input")
|
|
28
30
|
return "waiting_for_human_reply";
|
|
29
31
|
const reactiveIntent = deriveIssueSessionReactiveIntent({
|
|
32
|
+
delegatedToPatchRelay: params.delegatedToPatchRelay,
|
|
30
33
|
prNumber: params.prNumber,
|
|
31
34
|
prState: params.prState,
|
|
32
35
|
prReviewState: params.prReviewState,
|
|
@@ -38,6 +41,8 @@ export function deriveIssueSessionWakeReason(params) {
|
|
|
38
41
|
return undefined;
|
|
39
42
|
}
|
|
40
43
|
export function deriveIssueSessionReactiveIntent(params) {
|
|
44
|
+
if (params.delegatedToPatchRelay === false)
|
|
45
|
+
return undefined;
|
|
41
46
|
if (params.activeRunId !== undefined)
|
|
42
47
|
return undefined;
|
|
43
48
|
if (params.prNumber === undefined)
|
|
@@ -75,6 +80,8 @@ export function deriveIssueSessionReactiveIntent(params) {
|
|
|
75
80
|
return undefined;
|
|
76
81
|
}
|
|
77
82
|
export function isIssueSessionReadyForExecution(params) {
|
|
83
|
+
if (params.delegatedToPatchRelay === false)
|
|
84
|
+
return false;
|
|
78
85
|
if (params.activeRunId !== undefined)
|
|
79
86
|
return false;
|
|
80
87
|
if (params.blockedByCount > 0)
|
|
@@ -92,6 +99,7 @@ export function isIssueSessionReadyForExecution(params) {
|
|
|
92
99
|
return false;
|
|
93
100
|
}
|
|
94
101
|
if (deriveIssueSessionReactiveIntent({
|
|
102
|
+
delegatedToPatchRelay: params.delegatedToPatchRelay,
|
|
95
103
|
prNumber: params.prNumber,
|
|
96
104
|
prState: params.prState,
|
|
97
105
|
prReviewState: params.prReviewState,
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { deriveIssueStatusNote } from "./status-note.js";
|
|
2
|
+
export async function getLegacyIssueOverview(params) {
|
|
3
|
+
const { db, issueKey, runStatusProvider, buildRuns, readLiveThread } = params;
|
|
4
|
+
const legacy = db.getIssueOverview(issueKey);
|
|
5
|
+
if (!legacy)
|
|
6
|
+
return undefined;
|
|
7
|
+
const issueRecord = db.issues.getIssueByKey(issueKey);
|
|
8
|
+
const activeStatus = await runStatusProvider.getActiveRunStatus(issueKey);
|
|
9
|
+
const activeRun = activeStatus?.run ?? legacy.activeRun;
|
|
10
|
+
const latestRun = db.runs.getLatestRunForIssue(legacy.issue.projectId, legacy.issue.linearIssueId);
|
|
11
|
+
const latestEvent = db.issueSessions.listIssueSessionEvents(legacy.issue.projectId, legacy.issue.linearIssueId, { limit: 1 }).at(-1);
|
|
12
|
+
const runs = buildRuns(legacy.issue.projectId, legacy.issue.linearIssueId);
|
|
13
|
+
const runCount = runs.length;
|
|
14
|
+
const liveThread = await readLiveThread(activeRun);
|
|
15
|
+
const statusNote = issueRecord
|
|
16
|
+
? deriveIssueStatusNote({
|
|
17
|
+
issue: issueRecord,
|
|
18
|
+
latestRun,
|
|
19
|
+
latestEvent,
|
|
20
|
+
failureSummary: legacy.issue.latestFailureSummary,
|
|
21
|
+
blockedByKeys: legacy.issue.blockedByKeys,
|
|
22
|
+
waitingReason: legacy.issue.waitingReason,
|
|
23
|
+
})
|
|
24
|
+
: legacy.issue.statusNote;
|
|
25
|
+
return {
|
|
26
|
+
issue: {
|
|
27
|
+
...legacy.issue,
|
|
28
|
+
...(statusNote ? { statusNote } : {}),
|
|
29
|
+
},
|
|
30
|
+
...(activeRun ? { activeRun } : {}),
|
|
31
|
+
...(latestRun ? { latestRun } : {}),
|
|
32
|
+
...(liveThread ? { liveThread } : {}),
|
|
33
|
+
...(runs.length > 0 ? { runs } : {}),
|
|
34
|
+
...(issueRecord
|
|
35
|
+
? {
|
|
36
|
+
issueContext: {
|
|
37
|
+
...(issueRecord.description ? { description: issueRecord.description } : {}),
|
|
38
|
+
...(issueRecord.currentLinearState ? { currentLinearState: issueRecord.currentLinearState } : {}),
|
|
39
|
+
...(issueRecord.url ? { issueUrl: issueRecord.url } : {}),
|
|
40
|
+
...(issueRecord.worktreePath ? { worktreePath: issueRecord.worktreePath } : {}),
|
|
41
|
+
...(issueRecord.branchName ? { branchName: issueRecord.branchName } : {}),
|
|
42
|
+
...(issueRecord.prUrl ? { prUrl: issueRecord.prUrl } : {}),
|
|
43
|
+
...(issueRecord.priority != null ? { priority: issueRecord.priority } : {}),
|
|
44
|
+
...(issueRecord.estimate != null ? { estimate: issueRecord.estimate } : {}),
|
|
45
|
+
ciRepairAttempts: issueRecord.ciRepairAttempts,
|
|
46
|
+
queueRepairAttempts: issueRecord.queueRepairAttempts,
|
|
47
|
+
reviewFixAttempts: issueRecord.reviewFixAttempts,
|
|
48
|
+
...(legacy.issue.latestFailureSource ? { latestFailureSource: legacy.issue.latestFailureSource } : {}),
|
|
49
|
+
...(legacy.issue.latestFailureHeadSha ? { latestFailureHeadSha: legacy.issue.latestFailureHeadSha } : {}),
|
|
50
|
+
...(legacy.issue.latestFailureCheckName ? { latestFailureCheckName: legacy.issue.latestFailureCheckName } : {}),
|
|
51
|
+
...(legacy.issue.latestFailureStepName ? { latestFailureStepName: legacy.issue.latestFailureStepName } : {}),
|
|
52
|
+
...(legacy.issue.latestFailureSummary ? { latestFailureSummary: legacy.issue.latestFailureSummary } : {}),
|
|
53
|
+
runCount,
|
|
54
|
+
},
|
|
55
|
+
}
|
|
56
|
+
: {}),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
@@ -193,7 +193,10 @@ export function summarizeIssueStateForLinear(issue) {
|
|
|
193
193
|
case "running":
|
|
194
194
|
return issue.waitingReason ?? (issue.prNumber && !isClosedPrState(issue.prState) ? `PR #${issue.prNumber} is actively running.` : "Actively running.");
|
|
195
195
|
case "idle":
|
|
196
|
-
|
|
196
|
+
if (!issue.delegatedToPatchRelay) {
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
199
|
+
return issue.waitingReason ?? (issue.prNumber ? `PR #${issue.prNumber} is idle.` : "Idle.");
|
|
197
200
|
case "done":
|
|
198
201
|
if (issue.prNumber && issue.prState === "merged")
|
|
199
202
|
return `PR #${issue.prNumber} has merged.`;
|
|
@@ -204,9 +207,35 @@ export function summarizeIssueStateForLinear(issue) {
|
|
|
204
207
|
return issue.waitingReason ?? (issue.prNumber && !isClosedPrState(issue.prState) ? `PR #${issue.prNumber} needs help to recover.` : "Needs help to recover.");
|
|
205
208
|
}
|
|
206
209
|
switch (issue.factoryState) {
|
|
210
|
+
case "delegated":
|
|
211
|
+
if (!issue.delegatedToPatchRelay) {
|
|
212
|
+
return "PatchRelay is queued to start work, but automation is paused.";
|
|
213
|
+
}
|
|
214
|
+
return "Queued to start work.";
|
|
215
|
+
case "implementing":
|
|
216
|
+
if (!issue.delegatedToPatchRelay) {
|
|
217
|
+
return "Implementation is paused because the issue is undelegated.";
|
|
218
|
+
}
|
|
219
|
+
return "Implementation in progress.";
|
|
207
220
|
case "pr_open":
|
|
221
|
+
if (!issue.delegatedToPatchRelay && issue.prNumber) {
|
|
222
|
+
return `PR #${issue.prNumber} is awaiting review while PatchRelay is paused.`;
|
|
223
|
+
}
|
|
208
224
|
return issue.prNumber ? `PR #${issue.prNumber} is awaiting review.` : "Awaiting review.";
|
|
225
|
+
case "changes_requested":
|
|
226
|
+
if (!issue.delegatedToPatchRelay && issue.prNumber) {
|
|
227
|
+
return `PR #${issue.prNumber} has requested changes while PatchRelay is paused.`;
|
|
228
|
+
}
|
|
229
|
+
return issue.prNumber ? `PR #${issue.prNumber} has requested changes.` : "Requested changes received.";
|
|
230
|
+
case "repairing_ci":
|
|
231
|
+
if (!issue.delegatedToPatchRelay && issue.prNumber) {
|
|
232
|
+
return `PR #${issue.prNumber} has failing CI while PatchRelay is paused.`;
|
|
233
|
+
}
|
|
234
|
+
return issue.prNumber ? `PR #${issue.prNumber} has failing CI.` : "Failing CI.";
|
|
209
235
|
case "awaiting_queue":
|
|
236
|
+
if (!issue.delegatedToPatchRelay && issue.prNumber) {
|
|
237
|
+
return `PR #${issue.prNumber} is approved and awaiting merge while PatchRelay is paused.`;
|
|
238
|
+
}
|
|
210
239
|
return issue.prNumber ? `PR #${issue.prNumber} is approved and awaiting merge.` : "Approved and awaiting merge.";
|
|
211
240
|
case "done":
|
|
212
241
|
if (issue.prNumber && issue.prState === "merged")
|
|
@@ -29,9 +29,17 @@ export class LinearSessionSync {
|
|
|
29
29
|
if (!linear)
|
|
30
30
|
return;
|
|
31
31
|
const trackedIssue = this.db.getTrackedIssue(syncedIssue.projectId, syncedIssue.linearIssueId);
|
|
32
|
+
const visibleIssue = trackedIssue
|
|
33
|
+
? {
|
|
34
|
+
...trackedIssue,
|
|
35
|
+
delegatedToPatchRelay: syncedIssue.delegatedToPatchRelay,
|
|
36
|
+
prNumber: syncedIssue.prNumber,
|
|
37
|
+
prUrl: syncedIssue.prUrl,
|
|
38
|
+
}
|
|
39
|
+
: syncedIssue;
|
|
32
40
|
await syncActiveWorkflowState({ db: this.db, issue: syncedIssue, linear, ...(trackedIssue ? { trackedIssue } : {}), ...(options ? { options } : {}) });
|
|
33
41
|
await this.agentSessions.syncSessionPlan(syncedIssue, linear, options);
|
|
34
|
-
if (shouldSyncVisibleIssueComment(
|
|
42
|
+
if (shouldSyncVisibleIssueComment(visibleIssue, Boolean(syncedIssue.agentSessionId))) {
|
|
35
43
|
await syncVisibleStatusComment({
|
|
36
44
|
db: this.db,
|
|
37
45
|
issue: syncedIssue,
|
|
@@ -2,6 +2,7 @@ import { extractCompletionCheck } from "./completion-check.js";
|
|
|
2
2
|
import { deriveIssueStatusNote } from "./status-note.js";
|
|
3
3
|
import { derivePatchRelayWaitingReason } from "./waiting-reason.js";
|
|
4
4
|
import { isClosedPrState } from "./pr-state.js";
|
|
5
|
+
import { isUndelegatedPausedIssue } from "./paused-issue-state.js";
|
|
5
6
|
export async function syncVisibleStatusComment(params) {
|
|
6
7
|
const { db, issue, linear, logger, trackedIssue, options } = params;
|
|
7
8
|
try {
|
|
@@ -32,6 +33,9 @@ export function shouldSyncVisibleIssueComment(issue, hasAgentSession) {
|
|
|
32
33
|
|| issue.factoryState === "awaiting_input" || issue.factoryState === "failed" || issue.factoryState === "escalated") {
|
|
33
34
|
return true;
|
|
34
35
|
}
|
|
36
|
+
if (isUndelegatedPausedIssue(issue)) {
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
35
39
|
if ((issue.sessionState === "done" || issue.factoryState === "done")
|
|
36
40
|
&& ((issue.prNumber === undefined && !issue.prUrl)
|
|
37
41
|
|| isClosedPrState(issue.prState))) {
|
|
@@ -47,6 +51,7 @@ function renderStatusComment(db, issue, trackedIssue, options) {
|
|
|
47
51
|
? (options?.activeRunType ?? activeRun?.runType)
|
|
48
52
|
: undefined;
|
|
49
53
|
const waitingReason = trackedIssue?.waitingReason ?? derivePatchRelayWaitingReason({
|
|
54
|
+
delegatedToPatchRelay: issue.delegatedToPatchRelay,
|
|
50
55
|
...(activeRunType ? { activeRunType } : {}),
|
|
51
56
|
...(issue.activeRunId !== undefined ? { activeRunId: issue.activeRunId } : {}),
|
|
52
57
|
factoryState: issue.factoryState,
|
|
@@ -62,7 +67,15 @@ function renderStatusComment(db, issue, trackedIssue, options) {
|
|
|
62
67
|
const lines = [
|
|
63
68
|
"## PatchRelay status",
|
|
64
69
|
"",
|
|
65
|
-
statusHeadline(trackedIssue
|
|
70
|
+
statusHeadline(trackedIssue
|
|
71
|
+
? {
|
|
72
|
+
...trackedIssue,
|
|
73
|
+
delegatedToPatchRelay: issue.delegatedToPatchRelay,
|
|
74
|
+
prNumber: issue.prNumber,
|
|
75
|
+
prReviewState: issue.prReviewState,
|
|
76
|
+
prCheckStatus: issue.prCheckStatus,
|
|
77
|
+
}
|
|
78
|
+
: issue, activeRunType),
|
|
66
79
|
];
|
|
67
80
|
const statusNote = trackedIssue?.statusNote ?? deriveIssueStatusNote({ issue, latestRun, latestEvent, waitingReason });
|
|
68
81
|
if (waitingReason) {
|
|
@@ -124,6 +137,26 @@ function statusHeadline(issue, activeRunType) {
|
|
|
124
137
|
default:
|
|
125
138
|
break;
|
|
126
139
|
}
|
|
140
|
+
if (!issue.delegatedToPatchRelay && issue.prNumber !== undefined) {
|
|
141
|
+
if (issue.factoryState === "awaiting_queue" || issue.prReviewState === "approved") {
|
|
142
|
+
return `PR #${issue.prNumber} is awaiting downstream merge while PatchRelay is paused`;
|
|
143
|
+
}
|
|
144
|
+
if (issue.factoryState === "changes_requested" || issue.prReviewState === "changes_requested") {
|
|
145
|
+
return `PR #${issue.prNumber} has requested changes while PatchRelay is paused`;
|
|
146
|
+
}
|
|
147
|
+
if (issue.factoryState === "repairing_ci" || issue.prCheckStatus === "failed" || issue.prCheckStatus === "failure") {
|
|
148
|
+
return `PR #${issue.prNumber} has failing CI while PatchRelay is paused`;
|
|
149
|
+
}
|
|
150
|
+
return `PR #${issue.prNumber} is awaiting review while PatchRelay is paused`;
|
|
151
|
+
}
|
|
152
|
+
if (!issue.delegatedToPatchRelay) {
|
|
153
|
+
if (issue.factoryState === "implementing") {
|
|
154
|
+
return "Implementation is paused because the issue is undelegated";
|
|
155
|
+
}
|
|
156
|
+
if (issue.factoryState === "delegated") {
|
|
157
|
+
return "Queued to start work while PatchRelay is paused";
|
|
158
|
+
}
|
|
159
|
+
}
|
|
127
160
|
switch (issue.factoryState) {
|
|
128
161
|
case "delegated":
|
|
129
162
|
return "Queued to start work";
|
|
@@ -53,14 +53,14 @@ function resolveDesiredActiveWorkflowState(issue, trackedIssue, options, liveIss
|
|
|
53
53
|
|| trackedIssue?.sessionState === "waiting_input" || trackedIssue?.sessionState === "failed") {
|
|
54
54
|
return resolvePreferredHumanNeededLinearState(liveIssue);
|
|
55
55
|
}
|
|
56
|
-
const activelyWorking = issue.activeRunId !== undefined
|
|
56
|
+
const activelyWorking = issue.delegatedToPatchRelay !== false && (issue.activeRunId !== undefined
|
|
57
57
|
|| options?.activeRunType !== undefined
|
|
58
58
|
|| trackedIssue?.sessionState === "running"
|
|
59
59
|
|| issue.factoryState === "delegated"
|
|
60
60
|
|| issue.factoryState === "implementing"
|
|
61
61
|
|| issue.factoryState === "changes_requested"
|
|
62
62
|
|| issue.factoryState === "repairing_ci"
|
|
63
|
-
|| issue.factoryState === "repairing_queue";
|
|
63
|
+
|| issue.factoryState === "repairing_queue");
|
|
64
64
|
if (activelyWorking) {
|
|
65
65
|
return resolvePreferredImplementingLinearState(liveIssue);
|
|
66
66
|
}
|