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
|
@@ -10,6 +10,7 @@ export function buildTrackedIssueRecord(params) {
|
|
|
10
10
|
const failureContext = parseGitHubFailureContext(params.issue.lastGitHubFailureContextJson);
|
|
11
11
|
const blockedByKeys = unresolvedBlockedBy.map((entry) => entry.blockerIssueKey ?? entry.blockerLinearIssueId);
|
|
12
12
|
const waitingReason = derivePatchRelayWaitingReason({
|
|
13
|
+
delegatedToPatchRelay: params.issue.delegatedToPatchRelay,
|
|
13
14
|
...(params.issue.activeRunId !== undefined ? { activeRunId: params.issue.activeRunId } : {}),
|
|
14
15
|
blockedByKeys,
|
|
15
16
|
factoryState: params.issue.factoryState,
|
|
@@ -40,6 +41,7 @@ export function buildTrackedIssueRecord(params) {
|
|
|
40
41
|
id: params.issue.id,
|
|
41
42
|
projectId: params.issue.projectId,
|
|
42
43
|
linearIssueId: params.issue.linearIssueId,
|
|
44
|
+
delegatedToPatchRelay: params.issue.delegatedToPatchRelay,
|
|
43
45
|
...(params.issue.issueKey ? { issueKey: params.issue.issueKey } : {}),
|
|
44
46
|
...(params.issue.title ? { title: params.issue.title } : {}),
|
|
45
47
|
...(params.issue.url ? { issueUrl: params.issue.url } : {}),
|
|
@@ -56,6 +58,7 @@ export function buildTrackedIssueRecord(params) {
|
|
|
56
58
|
readyForExecution: isIssueSessionReadyForExecution({
|
|
57
59
|
sessionState: params.session?.sessionState,
|
|
58
60
|
factoryState: params.issue.factoryState,
|
|
61
|
+
delegatedToPatchRelay: params.issue.delegatedToPatchRelay,
|
|
59
62
|
activeRunId: params.issue.activeRunId,
|
|
60
63
|
blockedByCount: unresolvedBlockedBy.length,
|
|
61
64
|
hasPendingWake: params.hasPendingWake,
|
package/dist/waiting-reason.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { hasOpenPr } from "./pr-state.js";
|
|
2
2
|
export const PATCHRELAY_WAITING_REASONS = {
|
|
3
3
|
activeWork: "PatchRelay is actively working",
|
|
4
|
+
automationPaused: "PatchRelay automation is paused because the issue is undelegated",
|
|
5
|
+
automationPausedDownstream: "PatchRelay automation is paused; downstream merge may still continue until the PR is closed",
|
|
4
6
|
finalizingPublishedPr: "PatchRelay is finalizing a published PR",
|
|
5
7
|
finalizingMergedChange: "PatchRelay is finalizing a merged change",
|
|
6
8
|
waitingForOperatorInput: "Waiting on operator input",
|
|
@@ -14,6 +16,11 @@ export const PATCHRELAY_WAITING_REASONS = {
|
|
|
14
16
|
waitingForExternalReview: "Waiting on external review",
|
|
15
17
|
};
|
|
16
18
|
export function derivePatchRelayWaitingReason(params) {
|
|
19
|
+
if (params.delegatedToPatchRelay === false && params.factoryState !== "done" && params.factoryState !== "failed" && params.factoryState !== "escalated") {
|
|
20
|
+
return params.factoryState === "awaiting_queue" || (hasLiveOpenPr(params.prNumber, params.prState) && params.prReviewState === "approved")
|
|
21
|
+
? PATCHRELAY_WAITING_REASONS.automationPausedDownstream
|
|
22
|
+
: PATCHRELAY_WAITING_REASONS.automationPaused;
|
|
23
|
+
}
|
|
17
24
|
if (params.activeRunType) {
|
|
18
25
|
if (hasOpenPr(params.prNumber, params.prState) && (params.factoryState === "pr_open" || params.factoryState === "awaiting_queue")) {
|
|
19
26
|
return PATCHRELAY_WAITING_REASONS.finalizingPublishedPr;
|
|
@@ -78,3 +85,6 @@ export function derivePatchRelayWaitingReason(params) {
|
|
|
78
85
|
function humanize(value) {
|
|
79
86
|
return value.replaceAll("_", " ");
|
|
80
87
|
}
|
|
88
|
+
function hasLiveOpenPr(prNumber, prState) {
|
|
89
|
+
return prNumber !== undefined && (prState === undefined || prState === "open");
|
|
90
|
+
}
|
|
@@ -26,6 +26,7 @@ export class AgentSessionHandler {
|
|
|
26
26
|
return;
|
|
27
27
|
const existingIssue = this.db.issues.getIssue(project.id, normalized.issue.id);
|
|
28
28
|
const activeRun = existingIssue?.activeRunId ? this.db.runs.getRunById(existingIssue.activeRunId) : undefined;
|
|
29
|
+
const automationEnabled = delegated || existingIssue?.delegatedToPatchRelay === true;
|
|
29
30
|
if (normalized.triggerEvent === "agentSessionCreated") {
|
|
30
31
|
if (!delegated) {
|
|
31
32
|
const latestIssue = this.db.issues.getIssue(project.id, normalized.issue.id);
|
|
@@ -70,6 +71,13 @@ export class AgentSessionHandler {
|
|
|
70
71
|
if (!triggerEventAllowed(project, normalized.triggerEvent))
|
|
71
72
|
return;
|
|
72
73
|
const promptBody = normalized.agentSession.promptBody?.trim();
|
|
74
|
+
if (!automationEnabled && promptBody && existingIssue) {
|
|
75
|
+
await this.publishAgentActivity(linear, normalized.agentSession.id, {
|
|
76
|
+
type: "thought",
|
|
77
|
+
body: "PatchRelay is paused because the issue is undelegated.",
|
|
78
|
+
}, { ephemeral: true });
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
73
81
|
if (activeRun && promptBody && activeRun.threadId && activeRun.turnId) {
|
|
74
82
|
const input = `New Linear agent prompt received while you are working.\n\n${promptBody}`;
|
|
75
83
|
try {
|
|
@@ -99,7 +107,7 @@ export class AgentSessionHandler {
|
|
|
99
107
|
await this.publishAgentActivity(linear, normalized.agentSession.id, buildPromptDeliveredThought(activeRun.runType), { ephemeral: true });
|
|
100
108
|
return;
|
|
101
109
|
}
|
|
102
|
-
if (promptBody && existingIssue &&
|
|
110
|
+
if (promptBody && existingIssue && automationEnabled) {
|
|
103
111
|
const hadPendingWake = this.db.issueSessions.peekIssueSessionWake(project.id, normalized.issue.id) !== undefined;
|
|
104
112
|
const directReply = params.isDirectReplyToOutstandingQuestion(existingIssue);
|
|
105
113
|
this.db.issueSessions.appendIssueSessionEventRespectingActiveLease(project.id, normalized.issue.id, {
|
|
@@ -40,6 +40,18 @@ export class CommentWakeHandler {
|
|
|
40
40
|
});
|
|
41
41
|
return;
|
|
42
42
|
}
|
|
43
|
+
if (!issue.delegatedToPatchRelay) {
|
|
44
|
+
this.feed?.publish({
|
|
45
|
+
level: "info",
|
|
46
|
+
kind: "comment",
|
|
47
|
+
projectId: project.id,
|
|
48
|
+
issueKey: trackedIssue?.issueKey,
|
|
49
|
+
status: "ignored_undelegated",
|
|
50
|
+
summary: "Ignored comment because the issue is undelegated",
|
|
51
|
+
detail: trimmedBody.slice(0, 200),
|
|
52
|
+
});
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
43
55
|
if (!issue.activeRunId) {
|
|
44
56
|
if (ENQUEUEABLE_STATES.has(issue.factoryState)) {
|
|
45
57
|
const directReply = params.isDirectReplyToOutstandingQuestion(issue);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { TERMINAL_STATES } from "../factory-state.js";
|
|
2
|
+
import { deriveIssueSessionReactiveIntent } from "../issue-session.js";
|
|
2
3
|
export function decideRunIntent(p) {
|
|
3
4
|
const wakeEligibleState = p.currentState === undefined
|
|
4
5
|
|| p.currentState === "delegated"
|
|
@@ -26,10 +27,50 @@ export function decideUnDelegation(p) {
|
|
|
26
27
|
return { clearPending: false };
|
|
27
28
|
if (!p.currentState)
|
|
28
29
|
return { clearPending: false };
|
|
29
|
-
|
|
30
|
-
if (pastNoReturn)
|
|
30
|
+
if (TERMINAL_STATES.has(p.currentState))
|
|
31
31
|
return { clearPending: false };
|
|
32
|
-
return { factoryState:
|
|
32
|
+
return { factoryState: p.currentState, clearPending: true };
|
|
33
|
+
}
|
|
34
|
+
export function resolveReDelegationResume(p) {
|
|
35
|
+
if (!p.delegated || p.previouslyDelegated !== false) {
|
|
36
|
+
return {};
|
|
37
|
+
}
|
|
38
|
+
if (p.prState === "merged") {
|
|
39
|
+
return { factoryState: "done", pendingRunType: null };
|
|
40
|
+
}
|
|
41
|
+
const reactiveIntent = deriveIssueSessionReactiveIntent({
|
|
42
|
+
delegatedToPatchRelay: true,
|
|
43
|
+
prNumber: p.prNumber,
|
|
44
|
+
prState: p.prState,
|
|
45
|
+
prReviewState: p.prReviewState,
|
|
46
|
+
prCheckStatus: p.prCheckStatus,
|
|
47
|
+
latestFailureSource: p.latestFailureSource,
|
|
48
|
+
});
|
|
49
|
+
if (reactiveIntent) {
|
|
50
|
+
return {
|
|
51
|
+
factoryState: reactiveIntent.compatibilityFactoryState,
|
|
52
|
+
pendingRunType: reactiveIntent.runType,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
if (p.prNumber !== undefined && (p.prState === undefined || p.prState === "open")) {
|
|
56
|
+
if (p.prReviewState === "approved") {
|
|
57
|
+
return { factoryState: "awaiting_queue", pendingRunType: null };
|
|
58
|
+
}
|
|
59
|
+
return { factoryState: "pr_open", pendingRunType: null };
|
|
60
|
+
}
|
|
61
|
+
if (p.currentState === "awaiting_input" && p.awaitingInputReason === "completion_check_question") {
|
|
62
|
+
return {
|
|
63
|
+
factoryState: "awaiting_input",
|
|
64
|
+
pendingRunType: null,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
if (p.currentState === "awaiting_input" || p.currentState === "delegated" || p.currentState === "implementing") {
|
|
68
|
+
return {
|
|
69
|
+
factoryState: "delegated",
|
|
70
|
+
pendingRunType: (p.unresolvedBlockers ?? 0) === 0 ? "implementation" : null,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
return {};
|
|
33
74
|
}
|
|
34
75
|
export function decideAgentSession(p) {
|
|
35
76
|
if (p.sessionId)
|
|
@@ -27,6 +27,7 @@ export class DependencyReadinessHandler {
|
|
|
27
27
|
continue;
|
|
28
28
|
}
|
|
29
29
|
if (issue.factoryState !== "delegated"
|
|
30
|
+
|| !issue.delegatedToPatchRelay
|
|
30
31
|
|| issue.activeRunId !== undefined
|
|
31
32
|
|| this.db.issueSessions.hasPendingIssueSessionEvents(projectId, dependent.linearIssueId)) {
|
|
32
33
|
continue;
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { triggerEventAllowed } from "../project-resolution.js";
|
|
2
|
-
import {
|
|
2
|
+
import { resolveAwaitingInputReason } from "../awaiting-input-reason.js";
|
|
3
|
+
import { decideActiveRunRelease, decideAgentSession, decideRunIntent, decideUnDelegation, isTerminalDelegationState, mergeIssueMetadata, resolveReDelegationResume, } from "./decision-helpers.js";
|
|
4
|
+
import { buildOperatorRetryEvent } from "../operator-retry-event.js";
|
|
3
5
|
export class DesiredStageRecorder {
|
|
4
6
|
db;
|
|
5
7
|
linearProvider;
|
|
@@ -16,6 +18,7 @@ export class DesiredStageRecorder {
|
|
|
16
18
|
}
|
|
17
19
|
const existingIssue = this.db.issues.getIssue(params.project.id, normalizedIssue.id);
|
|
18
20
|
const activeRun = existingIssue?.activeRunId ? this.db.runs.getRunById(existingIssue.activeRunId) : undefined;
|
|
21
|
+
const latestRun = existingIssue ? this.db.runs.getLatestRunForIssue(params.project.id, normalizedIssue.id) : undefined;
|
|
19
22
|
const delegated = this.isDelegatedToPatchRelay(params.project, params.normalized);
|
|
20
23
|
const triggerAllowed = triggerEventAllowed(params.project, params.normalized.triggerEvent);
|
|
21
24
|
const incomingAgentSessionId = params.normalized.agentSession?.id;
|
|
@@ -46,11 +49,22 @@ export class DesiredStageRecorder {
|
|
|
46
49
|
triggerEvent: params.normalized.triggerEvent,
|
|
47
50
|
delegated,
|
|
48
51
|
currentState: existingIssue?.factoryState,
|
|
52
|
+
hasPr: existingIssue?.prNumber !== undefined && existingIssue?.prState !== "merged",
|
|
53
|
+
});
|
|
54
|
+
const reDelegationResume = resolveReDelegationResume({
|
|
55
|
+
delegated,
|
|
56
|
+
previouslyDelegated: existingIssue?.delegatedToPatchRelay,
|
|
57
|
+
currentState: existingIssue?.factoryState,
|
|
58
|
+
awaitingInputReason: existingIssue
|
|
59
|
+
? resolveAwaitingInputReason({ issue: existingIssue, latestRun })
|
|
60
|
+
: undefined,
|
|
61
|
+
unresolvedBlockers,
|
|
62
|
+
prNumber: existingIssue?.prNumber,
|
|
63
|
+
prState: existingIssue?.prState,
|
|
64
|
+
prReviewState: existingIssue?.prReviewState,
|
|
65
|
+
prCheckStatus: existingIssue?.prCheckStatus,
|
|
66
|
+
latestFailureSource: existingIssue?.lastGitHubFailureSource,
|
|
49
67
|
});
|
|
50
|
-
const delegatedStateRecovery = delegated
|
|
51
|
-
&& !terminal
|
|
52
|
-
&& existingIssue?.factoryState === "awaiting_input"
|
|
53
|
-
&& !undelegation.factoryState;
|
|
54
68
|
const existingWakeRunType = existingIssue
|
|
55
69
|
? params.peekPendingSessionWakeRunType(params.project.id, normalizedIssue.id)
|
|
56
70
|
: undefined;
|
|
@@ -73,9 +87,13 @@ export class DesiredStageRecorder {
|
|
|
73
87
|
...(hydratedIssue.estimate != null ? { estimate: hydratedIssue.estimate } : {}),
|
|
74
88
|
...(hydratedIssue.stateName ? { currentLinearState: hydratedIssue.stateName } : {}),
|
|
75
89
|
...(hydratedIssue.stateType ? { currentLinearStateType: hydratedIssue.stateType } : {}),
|
|
90
|
+
delegatedToPatchRelay: delegated,
|
|
76
91
|
...(!existingIssue && !delegated && incomingAgentSessionId ? { factoryState: "awaiting_input" } : {}),
|
|
77
|
-
...(
|
|
78
|
-
...(
|
|
92
|
+
...(reDelegationResume.factoryState ? { factoryState: reDelegationResume.factoryState } : {}),
|
|
93
|
+
...(reDelegationResume.pendingRunType !== undefined
|
|
94
|
+
? { pendingRunType: null, pendingRunContextJson: null }
|
|
95
|
+
: {}),
|
|
96
|
+
...(!reDelegationResume.factoryState && desiredStage ? { pendingRunType: null, pendingRunContextJson: null, factoryState: "delegated" } : {}),
|
|
79
97
|
...(clearPending ? { pendingRunType: null, pendingRunContextJson: null } : {}),
|
|
80
98
|
...(agentSessionId !== undefined ? { agentSessionId } : {}),
|
|
81
99
|
...(runRelease.release ? { activeRunId: null } : {}),
|
|
@@ -111,12 +129,24 @@ export class DesiredStageRecorder {
|
|
|
111
129
|
kind: "stage",
|
|
112
130
|
issueKey: issue.issueKey,
|
|
113
131
|
projectId: params.project.id,
|
|
114
|
-
stage:
|
|
132
|
+
stage: issue.factoryState,
|
|
115
133
|
status: "un_delegated",
|
|
116
|
-
summary:
|
|
134
|
+
summary: issue.factoryState === "awaiting_input"
|
|
135
|
+
? "Issue un-delegated from PatchRelay"
|
|
136
|
+
: `Issue un-delegated from PatchRelay; ${issue.factoryState} is now paused`,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
else if (reDelegationResume.pendingRunType) {
|
|
140
|
+
this.db.issueSessions.appendIssueSessionEventRespectingActiveLease(params.project.id, normalizedIssue.id, {
|
|
141
|
+
projectId: params.project.id,
|
|
142
|
+
linearIssueId: normalizedIssue.id,
|
|
143
|
+
...buildOperatorRetryEvent(issue, reDelegationResume.pendingRunType, "re_delegated"),
|
|
117
144
|
});
|
|
118
145
|
}
|
|
119
|
-
else if (
|
|
146
|
+
else if (!reDelegationResume.factoryState
|
|
147
|
+
&& !reDelegationResume.pendingRunType
|
|
148
|
+
&&
|
|
149
|
+
desiredStage === "implementation"
|
|
120
150
|
&& params.normalized.triggerEvent !== "commentCreated"
|
|
121
151
|
&& params.normalized.triggerEvent !== "commentUpdated"
|
|
122
152
|
&& params.normalized.triggerEvent !== "agentPrompted") {
|