patchrelay 0.40.0 → 0.41.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/build-info.json +3 -3
- package/dist/cli/commands/issues.js +15 -1
- package/dist/cli/data.js +45 -0
- package/dist/cli/formatters/text.js +20 -0
- package/dist/cli/help.js +2 -0
- package/dist/cli/watch/state-visualization.js +1 -1
- package/dist/db/issue-session-store.js +3 -8
- package/dist/db/issue-store.js +8 -2
- package/dist/db/migrations.js +5 -0
- package/dist/delegation-audit.js +39 -0
- package/dist/delegation-linked-pr.js +104 -0
- package/dist/issue-overview-query.js +2 -1
- package/dist/issue-session-events.js +11 -3
- 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/merged-linear-completion-reconciler.js +84 -2
- package/dist/remote-pr-state.js +1 -1
- package/dist/run-reconciler.js +131 -22
- package/dist/service-runtime.js +1 -2
- package/dist/service-startup-recovery.js +19 -0
- package/dist/service.js +4 -1
- package/dist/waiting-reason.js +1 -1
- package/dist/webhooks/decision-helpers.js +8 -0
- package/dist/webhooks/desired-stage-recorder.js +149 -35
- package/package.json +1 -1
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { triggerEventAllowed } from "../project-resolution.js";
|
|
2
2
|
import { resolveAwaitingInputReason } from "../awaiting-input-reason.js";
|
|
3
|
+
import { appendDelegationObservedEvent } from "../delegation-audit.js";
|
|
3
4
|
import { decideActiveRunRelease, decideAgentSession, decideRunIntent, decideUnDelegation, isTerminalDelegationState, mergeIssueMetadata, resolveReDelegationResume, } from "./decision-helpers.js";
|
|
4
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";
|
|
5
9
|
export class DesiredStageRecorder {
|
|
6
10
|
db;
|
|
7
11
|
linearProvider;
|
|
@@ -25,8 +29,27 @@ export class DesiredStageRecorder {
|
|
|
25
29
|
if (!existingIssue && !this.isDelegatedToPatchRelay(params.project, normalizedIssue) && !incomingAgentSessionId) {
|
|
26
30
|
return { issue: undefined, wakeRunType: undefined, delegated: false };
|
|
27
31
|
}
|
|
28
|
-
const
|
|
29
|
-
const
|
|
32
|
+
const syncResult = await this.syncIssueDependencies(params.project.id, normalizedIssue);
|
|
33
|
+
const hydratedIssue = syncResult.issue;
|
|
34
|
+
const delegation = this.resolveDelegationTruth({
|
|
35
|
+
project: params.project,
|
|
36
|
+
normalizedIssue,
|
|
37
|
+
hydratedIssue,
|
|
38
|
+
existingIssue,
|
|
39
|
+
triggerEvent: params.normalized.triggerEvent,
|
|
40
|
+
webhookId: params.normalized.webhookId,
|
|
41
|
+
actorId: params.normalized.actor?.id,
|
|
42
|
+
hydration: syncResult.hydration,
|
|
43
|
+
activeRunId: activeRun?.id,
|
|
44
|
+
});
|
|
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
|
+
});
|
|
30
53
|
const unresolvedBlockers = this.db.issues.countUnresolvedBlockers(params.project.id, normalizedIssue.id);
|
|
31
54
|
const terminal = isTerminalDelegationState(existingIssue, hydratedIssue);
|
|
32
55
|
const openPrExists = existingIssue?.prNumber !== undefined
|
|
@@ -35,16 +58,18 @@ export class DesiredStageRecorder {
|
|
|
35
58
|
const blockerPausedImplementation = unresolvedBlockers > 0
|
|
36
59
|
&& activeRun?.runType === "implementation"
|
|
37
60
|
&& !openPrExists;
|
|
38
|
-
const desiredStage =
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
+
});
|
|
48
73
|
const runRelease = decideActiveRunRelease({
|
|
49
74
|
hasActiveRun: Boolean(activeRun),
|
|
50
75
|
terminal,
|
|
@@ -60,20 +85,31 @@ export class DesiredStageRecorder {
|
|
|
60
85
|
currentState: existingIssue?.factoryState,
|
|
61
86
|
hasPr: existingIssue?.prNumber !== undefined && existingIssue?.prState !== "merged",
|
|
62
87
|
});
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
+
};
|
|
77
113
|
const existingWakeRunType = existingIssue
|
|
78
114
|
? params.peekPendingSessionWakeRunType(params.project.id, normalizedIssue.id)
|
|
79
115
|
: undefined;
|
|
@@ -96,13 +132,19 @@ export class DesiredStageRecorder {
|
|
|
96
132
|
...(hydratedIssue.estimate != null ? { estimate: hydratedIssue.estimate } : {}),
|
|
97
133
|
...(hydratedIssue.stateName ? { currentLinearState: hydratedIssue.stateName } : {}),
|
|
98
134
|
...(hydratedIssue.stateType ? { currentLinearStateType: hydratedIssue.stateType } : {}),
|
|
135
|
+
...(linkedPrAdoption?.issueUpdates ?? {}),
|
|
99
136
|
delegatedToPatchRelay: delegated,
|
|
100
137
|
...(!existingIssue && !delegated && incomingAgentSessionId ? { factoryState: "awaiting_input" } : {}),
|
|
101
|
-
...(
|
|
102
|
-
...(
|
|
103
|
-
? {
|
|
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
|
+
}
|
|
104
146
|
: {}),
|
|
105
|
-
...(!
|
|
147
|
+
...(!startupResume.factoryState && desiredStage ? { pendingRunType: null, pendingRunContextJson: null, factoryState: "delegated" } : {}),
|
|
106
148
|
...(clearPending ? { pendingRunType: null, pendingRunContextJson: null } : {}),
|
|
107
149
|
...(agentSessionId !== undefined ? { agentSessionId } : {}),
|
|
108
150
|
...(effectiveRunRelease.release ? { activeRunId: null } : {}),
|
|
@@ -162,15 +204,15 @@ export class DesiredStageRecorder {
|
|
|
162
204
|
summary: `Implementation paused because ${issue.issueKey ?? normalizedIssue.id} is now blocked`,
|
|
163
205
|
});
|
|
164
206
|
}
|
|
165
|
-
else if (
|
|
207
|
+
else if (startupResume.pendingRunType) {
|
|
166
208
|
this.db.issueSessions.appendIssueSessionEventRespectingActiveLease(params.project.id, normalizedIssue.id, {
|
|
167
209
|
projectId: params.project.id,
|
|
168
210
|
linearIssueId: normalizedIssue.id,
|
|
169
|
-
...buildOperatorRetryEvent(issue,
|
|
211
|
+
...buildOperatorRetryEvent(issue, startupResume.pendingRunType, startupResume.source),
|
|
170
212
|
});
|
|
171
213
|
}
|
|
172
|
-
else if (!
|
|
173
|
-
&& !
|
|
214
|
+
else if (!startupResume.factoryState
|
|
215
|
+
&& !startupResume.pendingRunType
|
|
174
216
|
&&
|
|
175
217
|
desiredStage === "implementation"
|
|
176
218
|
&& params.normalized.triggerEvent !== "commentCreated"
|
|
@@ -200,16 +242,56 @@ export class DesiredStageRecorder {
|
|
|
200
242
|
return false;
|
|
201
243
|
return issue.delegateId === installation.actorId;
|
|
202
244
|
}
|
|
245
|
+
resolveDelegationTruth(params) {
|
|
246
|
+
const previousDelegated = params.existingIssue?.delegatedToPatchRelay;
|
|
247
|
+
const observedDelegated = this.isDelegatedToPatchRelay(params.project, params.hydratedIssue);
|
|
248
|
+
const explicitDelegateSignal = params.triggerEvent === "delegateChanged";
|
|
249
|
+
const hasObservedDelegate = params.hydratedIssue.delegateId !== undefined;
|
|
250
|
+
let delegated = observedDelegated;
|
|
251
|
+
let reason = hasObservedDelegate
|
|
252
|
+
? "delegate_id_present"
|
|
253
|
+
: `missing_delegate_identity_after_${params.hydration}`;
|
|
254
|
+
if (!hasObservedDelegate && !explicitDelegateSignal && previousDelegated !== undefined) {
|
|
255
|
+
delegated = previousDelegated;
|
|
256
|
+
reason = `preserved_previous_delegation_after_${params.hydration}`;
|
|
257
|
+
}
|
|
258
|
+
if (previousDelegated !== delegated
|
|
259
|
+
|| params.hydration === "live_linear_failed"
|
|
260
|
+
|| (!hasObservedDelegate && previousDelegated !== undefined)) {
|
|
261
|
+
appendDelegationObservedEvent(this.db, {
|
|
262
|
+
projectId: params.project.id,
|
|
263
|
+
linearIssueId: params.normalizedIssue.id,
|
|
264
|
+
payload: {
|
|
265
|
+
source: "linear_webhook",
|
|
266
|
+
webhookId: params.webhookId,
|
|
267
|
+
triggerEvent: params.triggerEvent,
|
|
268
|
+
...(params.actorId ? { actorId: params.actorId } : {}),
|
|
269
|
+
...(params.hydratedIssue.delegateId ? { observedDelegateId: params.hydratedIssue.delegateId } : {}),
|
|
270
|
+
...(previousDelegated !== undefined ? { previousDelegatedToPatchRelay: previousDelegated } : {}),
|
|
271
|
+
observedDelegatedToPatchRelay: observedDelegated,
|
|
272
|
+
appliedDelegatedToPatchRelay: delegated,
|
|
273
|
+
hydration: params.hydration,
|
|
274
|
+
...(params.activeRunId !== undefined ? { activeRunId: params.activeRunId } : {}),
|
|
275
|
+
decision: "none",
|
|
276
|
+
reason,
|
|
277
|
+
},
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
return { delegated };
|
|
281
|
+
}
|
|
203
282
|
async syncIssueDependencies(projectId, issue) {
|
|
204
283
|
let source = issue;
|
|
284
|
+
let hydration = "webhook_only";
|
|
205
285
|
if (!source.relationsKnown) {
|
|
206
286
|
const linear = await this.linearProvider.forProject(projectId);
|
|
207
287
|
if (linear) {
|
|
208
288
|
try {
|
|
209
289
|
source = mergeIssueMetadata(source, await linear.getIssue(issue.id));
|
|
290
|
+
hydration = "live_linear";
|
|
210
291
|
}
|
|
211
292
|
catch {
|
|
212
293
|
// Preserve existing dependency rows when webhook relation data is incomplete.
|
|
294
|
+
hydration = "live_linear_failed";
|
|
213
295
|
}
|
|
214
296
|
}
|
|
215
297
|
}
|
|
@@ -226,6 +308,38 @@ export class DesiredStageRecorder {
|
|
|
226
308
|
})),
|
|
227
309
|
});
|
|
228
310
|
}
|
|
229
|
-
return source;
|
|
311
|
+
return { issue: source, hydration };
|
|
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);
|
|
230
344
|
}
|
|
231
345
|
}
|