patchrelay 0.81.0 → 0.83.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/cluster-health/index.js +10 -3
- package/dist/db/migrations.js +43 -0
- package/dist/db/run-store.js +62 -4
- package/dist/db/schema-guard.js +2 -0
- package/dist/db/workflow-observation-store.js +61 -0
- package/dist/db/workflow-task-store.js +111 -0
- package/dist/db.js +60 -3
- package/dist/github-review-context.js +90 -0
- package/dist/github-webhook-handler.js +64 -11
- package/dist/idle-reconciliation.js +33 -6
- package/dist/issue-overview-query.js +2 -1
- package/dist/linear-issue-projection.js +37 -0
- package/dist/run-context.js +6 -6
- package/dist/run-finalizer.js +102 -22
- package/dist/run-launcher.js +1 -0
- package/dist/run-orchestrator.js +7 -0
- package/dist/run-wake-planner.js +58 -8
- package/dist/service-startup-recovery.js +51 -61
- package/dist/tracked-issue-list-query.js +5 -1
- package/dist/wake-dispatcher.js +59 -21
- package/dist/webhook-handler.js +75 -30
- package/dist/webhooks/dependency-readiness-handler.js +19 -13
- package/dist/webhooks/desired-stage-recorder.js +3 -18
- package/dist/workflow-runtime.js +381 -0
- package/dist/workflow-task-reconciler.js +64 -0
- package/package.json +1 -1
- package/dist/github-webhook-reactive-run.js +0 -309
|
@@ -6,11 +6,12 @@ import { createGitHubCiSnapshotResolver, createGitHubFailureContextResolver, } f
|
|
|
6
6
|
import { resolveGitHubWebhookIssue } from "./github-webhook-issue-resolution.js";
|
|
7
7
|
import { maybeCloseLatePublishedImplementationPr } from "./github-webhook-late-publication-guard.js";
|
|
8
8
|
import { projectGitHubWebhookState } from "./github-webhook-state-projector.js";
|
|
9
|
-
import {
|
|
9
|
+
import { resolveGitHubRequestedChangesContext } from "./github-review-context.js";
|
|
10
10
|
import { maybeRunSequenceBackstop } from "./github-webhook-sequence-backstop.js";
|
|
11
11
|
import { maybeFanChildRebaseWakes } from "./github-webhook-stack-coordination.js";
|
|
12
12
|
import { handleGitHubTerminalPrEvent } from "./github-webhook-terminal-handler.js";
|
|
13
13
|
import { WakeDispatcher } from "./wake-dispatcher.js";
|
|
14
|
+
import { reconcileWorkflowTasksForIssue } from "./workflow-task-reconciler.js";
|
|
14
15
|
export class GitHubWebhookHandler {
|
|
15
16
|
config;
|
|
16
17
|
db;
|
|
@@ -126,16 +127,68 @@ export class GitHubWebhookHandler {
|
|
|
126
127
|
failureContextResolver: this.failureContextResolver,
|
|
127
128
|
ciSnapshotResolver: this.ciSnapshotResolver,
|
|
128
129
|
}, issue, event, project, resolved.linkedBy);
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
130
|
+
const requestedChangesContext = event.triggerEvent === "review_changes_requested"
|
|
131
|
+
? await resolveGitHubRequestedChangesContext({
|
|
132
|
+
linearIssueId: freshIssue.linearIssueId,
|
|
133
|
+
event,
|
|
134
|
+
fetchImpl: this.fetchImpl,
|
|
135
|
+
}).catch((error) => {
|
|
136
|
+
this.logger.warn({
|
|
137
|
+
issueKey: freshIssue.issueKey,
|
|
138
|
+
prNumber: event.prNumber,
|
|
139
|
+
reviewId: event.reviewId,
|
|
140
|
+
error: error instanceof Error ? error.message : String(error),
|
|
141
|
+
}, "Failed to fetch inline review comments for requested-changes observation");
|
|
142
|
+
return resolveGitHubRequestedChangesContext({
|
|
143
|
+
linearIssueId: freshIssue.linearIssueId,
|
|
144
|
+
event,
|
|
145
|
+
fetchImpl: this.fetchImpl,
|
|
146
|
+
includeInlineComments: false,
|
|
147
|
+
});
|
|
148
|
+
})
|
|
149
|
+
: undefined;
|
|
150
|
+
this.db.workflowObservations.appendObservation({
|
|
151
|
+
projectId: freshIssue.projectId,
|
|
152
|
+
subjectId: freshIssue.linearIssueId,
|
|
153
|
+
source: "github",
|
|
154
|
+
type: `github.${event.triggerEvent}`,
|
|
155
|
+
payloadJson: JSON.stringify({
|
|
156
|
+
triggerEvent: event.triggerEvent,
|
|
157
|
+
repoFullName: event.repoFullName,
|
|
158
|
+
branchName: event.branchName,
|
|
159
|
+
headSha: event.headSha,
|
|
160
|
+
prNumber: event.prNumber,
|
|
161
|
+
prState: event.prState,
|
|
162
|
+
reviewState: event.reviewState,
|
|
163
|
+
reviewId: event.reviewId,
|
|
164
|
+
reviewCommitId: event.reviewCommitId,
|
|
165
|
+
reviewerName: event.reviewerName,
|
|
166
|
+
requestedChangesContext: requestedChangesContext?.context,
|
|
167
|
+
checkStatus: event.checkStatus,
|
|
168
|
+
checkName: event.checkName,
|
|
169
|
+
checkUrl: event.checkUrl,
|
|
170
|
+
}),
|
|
171
|
+
dedupeKey: requestedChangesContext?.dedupeKey ?? [
|
|
172
|
+
event.triggerEvent,
|
|
173
|
+
event.repoFullName,
|
|
174
|
+
event.prNumber ?? event.branchName,
|
|
175
|
+
event.headSha,
|
|
176
|
+
event.reviewId ?? event.checkName ?? "",
|
|
177
|
+
event.reviewState ?? event.checkStatus ?? event.prState ?? "",
|
|
178
|
+
].join(":"),
|
|
179
|
+
});
|
|
180
|
+
const workflowReconciliation = reconcileWorkflowTasksForIssue(this.db, freshIssue);
|
|
181
|
+
const changedRunnableWorkflowTask = [
|
|
182
|
+
...workflowReconciliation.result.opened,
|
|
183
|
+
...workflowReconciliation.result.updated,
|
|
184
|
+
].some((task) => task.gateAction === "start" && task.runType);
|
|
185
|
+
const shouldDispatchWorkflowTask = event.triggerEvent === "review_changes_requested"
|
|
186
|
+
|| event.triggerEvent === "check_failed"
|
|
187
|
+
|| event.triggerEvent === "pr_closed";
|
|
188
|
+
await this.wakeDispatcher.withTick(async () => {
|
|
189
|
+
if (shouldDispatchWorkflowTask && changedRunnableWorkflowTask) {
|
|
190
|
+
this.wakeDispatcher.dispatchIfWakePending(freshIssue.projectId, freshIssue.linearIssueId);
|
|
191
|
+
}
|
|
139
192
|
});
|
|
140
193
|
if (event.triggerEvent === "pr_opened") {
|
|
141
194
|
await maybeRunSequenceBackstop({
|
|
@@ -13,6 +13,7 @@ import { queueSettledOrchestrationIssue } from "./orchestration-parent-wake.js";
|
|
|
13
13
|
import { fetchPullRequestSnapshot } from "./reconcile-pr-fetch.js";
|
|
14
14
|
import { buildPrStateUpdates } from "./reconcile-pr-state-updates.js";
|
|
15
15
|
import { buildRepairWakeDedupeKey, buildRequestedChangesWakeIdentity, reactiveWakeEventType } from "./reactive-wake-keys.js";
|
|
16
|
+
import { reconcileWorkflowTasksForIssue } from "./workflow-task-reconciler.js";
|
|
16
17
|
import { execCommand } from "./utils.js";
|
|
17
18
|
import { LinearIssueProjectionService } from "./linear-issue-projection.js";
|
|
18
19
|
import { TerminalWakeReconciler } from "./terminal-wake-reconciler.js";
|
|
@@ -561,6 +562,29 @@ export class IdleIssueReconciler {
|
|
|
561
562
|
headAdvanced,
|
|
562
563
|
...(prState === "closed" ? { closedPrDisposition: resolveClosedPrDisposition(issue) } : {}),
|
|
563
564
|
};
|
|
565
|
+
this.db.workflowObservations.appendObservation({
|
|
566
|
+
projectId: issue.projectId,
|
|
567
|
+
subjectId: issue.linearIssueId,
|
|
568
|
+
source: "github",
|
|
569
|
+
type: "github.pr_reconciled",
|
|
570
|
+
payloadJson: JSON.stringify({
|
|
571
|
+
...observed,
|
|
572
|
+
repoFullName: project.github.repoFullName,
|
|
573
|
+
mergeable: pr.mergeable,
|
|
574
|
+
mergeStateStatus: pr.mergeStateStatus,
|
|
575
|
+
}),
|
|
576
|
+
dedupeKey: [
|
|
577
|
+
"pr_reconciled",
|
|
578
|
+
project.github.repoFullName,
|
|
579
|
+
prNumber,
|
|
580
|
+
prState,
|
|
581
|
+
pr.headRefOid ?? "",
|
|
582
|
+
pr.reviewDecision ?? "",
|
|
583
|
+
gateCheckStatus ?? "",
|
|
584
|
+
pr.mergeable ?? "",
|
|
585
|
+
pr.mergeStateStatus ?? "",
|
|
586
|
+
].join(":"),
|
|
587
|
+
});
|
|
564
588
|
const currentFacts = (record) => ({
|
|
565
589
|
factoryState: record.factoryState,
|
|
566
590
|
prReviewState: record.prReviewState,
|
|
@@ -720,18 +744,21 @@ export class IdleIssueReconciler {
|
|
|
720
744
|
return;
|
|
721
745
|
}
|
|
722
746
|
if (issue.delegatedToPatchRelay
|
|
723
|
-
&& reactiveIntent?.runType === "review_fix"
|
|
724
|
-
&& this.db.workflowWakes.peekIssueWake(issue.projectId, issue.linearIssueId) === undefined) {
|
|
747
|
+
&& reactiveIntent?.runType === "review_fix") {
|
|
725
748
|
this.logger.info({
|
|
726
749
|
issueKey: issue.issueKey,
|
|
727
750
|
prNumber: issue.prNumber,
|
|
728
751
|
from: issue.factoryState,
|
|
729
752
|
runType: reactiveIntent.runType,
|
|
730
753
|
}, "Reconciliation: re-queued requested-changes follow-up from GitHub truth");
|
|
731
|
-
this.advanceIdleIssue(issue, reactiveIntent.compatibilityFactoryState,
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
754
|
+
this.advanceIdleIssue(issue, reactiveIntent.compatibilityFactoryState, mayClearFailureProvenance(issue, provenanceEvidence)
|
|
755
|
+
? { clearFailureProvenance: true }
|
|
756
|
+
: undefined);
|
|
757
|
+
const currentIssue = this.db.issues.getIssue(issue.projectId, issue.linearIssueId);
|
|
758
|
+
if (currentIssue) {
|
|
759
|
+
reconcileWorkflowTasksForIssue(this.db, currentIssue);
|
|
760
|
+
this.wakeDispatcher.dispatchIfWakePending(currentIssue.projectId, currentIssue.linearIssueId);
|
|
761
|
+
}
|
|
735
762
|
this.feed?.publish({
|
|
736
763
|
level: "warn",
|
|
737
764
|
kind: "github",
|
|
@@ -127,7 +127,8 @@ export class IssueOverviewQuery {
|
|
|
127
127
|
delegatedToPatchRelay: issueRecord?.delegatedToPatchRelay,
|
|
128
128
|
...(activeRun ? { activeRunId: activeRun.id } : {}),
|
|
129
129
|
blockedByCount: unresolvedBlockedBy.length,
|
|
130
|
-
hasPendingWake: this.db.workflowWakes.peekIssueWake(session.projectId, session.linearIssueId) !== undefined
|
|
130
|
+
hasPendingWake: this.db.workflowWakes.peekIssueWake(session.projectId, session.linearIssueId) !== undefined
|
|
131
|
+
|| this.db.workflowTasks.listOpenRunnableTasks(session.projectId).some((task) => task.subjectId === session.linearIssueId),
|
|
131
132
|
hasLegacyPendingRun: issueRecord?.pendingRunType !== undefined,
|
|
132
133
|
orchestrationSettleUntil: issueRecord?.orchestrationSettleUntil,
|
|
133
134
|
...(session.prNumber !== undefined ? { prNumber: session.prNumber } : {}),
|
|
@@ -46,6 +46,43 @@ export async function refreshIssueFromLinear(params) {
|
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
48
|
export function upsertLinearIssueProjection(db, projectId, liveIssue) {
|
|
49
|
+
db.workflowObservations.appendObservation({
|
|
50
|
+
projectId,
|
|
51
|
+
subjectId: liveIssue.id,
|
|
52
|
+
source: "linear",
|
|
53
|
+
type: "linear.issue_reconciled",
|
|
54
|
+
payloadJson: JSON.stringify({
|
|
55
|
+
issueId: liveIssue.id,
|
|
56
|
+
issueKey: liveIssue.identifier,
|
|
57
|
+
title: liveIssue.title,
|
|
58
|
+
stateName: liveIssue.stateName,
|
|
59
|
+
stateType: liveIssue.stateType,
|
|
60
|
+
delegateId: liveIssue.delegateId,
|
|
61
|
+
parentId: liveIssue.parentId,
|
|
62
|
+
blockedBy: liveIssue.blockedBy.map((blocker) => ({
|
|
63
|
+
id: blocker.id,
|
|
64
|
+
identifier: blocker.identifier,
|
|
65
|
+
title: blocker.title,
|
|
66
|
+
stateName: blocker.stateName,
|
|
67
|
+
stateType: blocker.stateType,
|
|
68
|
+
})),
|
|
69
|
+
}),
|
|
70
|
+
dedupeKey: [
|
|
71
|
+
"issue_reconciled",
|
|
72
|
+
liveIssue.id,
|
|
73
|
+
liveIssue.identifier ?? "",
|
|
74
|
+
liveIssue.stateName ?? "",
|
|
75
|
+
liveIssue.stateType ?? "",
|
|
76
|
+
liveIssue.delegateId ?? "",
|
|
77
|
+
liveIssue.parentId ?? "",
|
|
78
|
+
...liveIssue.blockedBy.map((blocker) => [
|
|
79
|
+
blocker.id,
|
|
80
|
+
blocker.identifier ?? "",
|
|
81
|
+
blocker.stateName ?? "",
|
|
82
|
+
blocker.stateType ?? "",
|
|
83
|
+
].join("/")),
|
|
84
|
+
].join(":"),
|
|
85
|
+
});
|
|
49
86
|
replaceIssueDependenciesFromLinearIssue(db, projectId, liveIssue);
|
|
50
87
|
db.issues.replaceIssueParentLink({
|
|
51
88
|
projectId,
|
package/dist/run-context.js
CHANGED
|
@@ -33,8 +33,8 @@ const followUpEntryShape = {
|
|
|
33
33
|
author: z.string().optional(),
|
|
34
34
|
};
|
|
35
35
|
/** Inline review comment captured from GitHub. Produced by
|
|
36
|
-
* github-
|
|
37
|
-
*
|
|
36
|
+
* github-review-context.ts and reactive-run-policy.ts
|
|
37
|
+
* hydrateRequestedChangesContext (remote-pr-review.ts);
|
|
38
38
|
* consumed by prompting/patchrelay.ts readReviewFixComments and
|
|
39
39
|
* run-orchestrator.ts (review round activity comment count). */
|
|
40
40
|
const reviewCommentShape = {
|
|
@@ -74,8 +74,8 @@ const ciSnapshotCheckShape = {
|
|
|
74
74
|
summary: z.string().optional(),
|
|
75
75
|
};
|
|
76
76
|
/** Settled CI snapshot. Produced by github-failure-context.ts
|
|
77
|
-
* buildCiSnapshotFromChecks (attached to
|
|
78
|
-
*
|
|
77
|
+
* buildCiSnapshotFromChecks (attached to workflow task payloads by
|
|
78
|
+
* workflow-runtime.ts and to implicit ci_repair wakes by
|
|
79
79
|
* workflow-wake-resolver.ts); consumed by prompting/patchrelay.ts
|
|
80
80
|
* buildCiRepairContext. */
|
|
81
81
|
const ciSnapshotShape = {
|
|
@@ -196,7 +196,7 @@ const runContextShape = {
|
|
|
196
196
|
// ── Requested-changes / review fix ────────────────────────────────
|
|
197
197
|
/** Coalescing identity for review_changes_requested wakes. Produced by
|
|
198
198
|
* buildRequestedChangesWakeIdentity callers (run-wake-planner.ts,
|
|
199
|
-
* github-
|
|
199
|
+
* github-review-context.ts, operator-retry-event.ts,
|
|
200
200
|
* idle-reconciliation.ts); consumed by reactive-wake-keys.ts
|
|
201
201
|
* readRequestedChangesCoalesceKey for event coalescing. */
|
|
202
202
|
requestedChangesCoalesceKey: z.string().optional(),
|
|
@@ -211,7 +211,7 @@ const runContextShape = {
|
|
|
211
211
|
* review_changes_requested payloads from operator-retry-event.ts and
|
|
212
212
|
* deriveSessionWakePlan branch selection). */
|
|
213
213
|
branchUpkeepRequired: z.boolean().optional(),
|
|
214
|
-
/** GitHub review id. Produced by github-
|
|
214
|
+
/** GitHub review id. Produced by github-review-context.ts and
|
|
215
215
|
* reactive-run-policy.ts hydrateRequestedChangesContext; consumed by
|
|
216
216
|
* prompting/patchrelay.ts buildStructuredReviewContext. */
|
|
217
217
|
reviewId: z.number().optional(),
|
package/dist/run-finalizer.js
CHANGED
|
@@ -9,7 +9,21 @@ import { computeChangeIdentityFromWorktree } from "./change-identity.js";
|
|
|
9
9
|
import { inspectGitWorktreeStatus, isRepairRunType } from "./git-worktree-status.js";
|
|
10
10
|
import { buildRunOutcomeSummary } from "./run-outcome-summary.js";
|
|
11
11
|
import { settleRun } from "./run-settlement.js";
|
|
12
|
+
import { evaluateTaskCompletion, projectWorkflowSnapshot } from "./workflow-runtime.js";
|
|
12
13
|
const WRITER = "run-finalizer";
|
|
14
|
+
function parseObjectJson(raw) {
|
|
15
|
+
if (!raw)
|
|
16
|
+
return undefined;
|
|
17
|
+
try {
|
|
18
|
+
const parsed = JSON.parse(raw);
|
|
19
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed)
|
|
20
|
+
? parsed
|
|
21
|
+
: undefined;
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
13
27
|
function buildRunSummaryJson(report, outcomeSummary) {
|
|
14
28
|
return JSON.stringify({
|
|
15
29
|
latestAssistantMessage: report.assistantMessages.at(-1) ?? null,
|
|
@@ -201,20 +215,16 @@ export class RunFinalizer {
|
|
|
201
215
|
...(options?.publishDeferredFollowUp ? { publishDeferredFollowUp: true } : {}),
|
|
202
216
|
});
|
|
203
217
|
}
|
|
204
|
-
//
|
|
205
|
-
//
|
|
206
|
-
//
|
|
207
|
-
|
|
208
|
-
// clean recap event. No publication, no follow-up enqueue —
|
|
209
|
-
// the approval that triggered supersedure already advanced the
|
|
210
|
-
// factoryState.
|
|
211
|
-
releaseSupersededRun(run, threadId, completedTurnId) {
|
|
218
|
+
// Finalize a run whose authority/premise was revoked mid-flight.
|
|
219
|
+
// No publication, no follow-up enqueue: the current external truth
|
|
220
|
+
// already superseded the run's right to act.
|
|
221
|
+
releaseSuppressedRun(run, threadId, completedTurnId, reason) {
|
|
212
222
|
this.withHeldLease(run.projectId, run.linearIssueId, () => {
|
|
213
223
|
this.db.runs.finishRun(run.id, {
|
|
214
224
|
status: "superseded",
|
|
215
225
|
threadId,
|
|
216
226
|
...(completedTurnId ? { turnId: completedTurnId } : {}),
|
|
217
|
-
failureReason: run.failureReason ??
|
|
227
|
+
failureReason: run.failureReason ?? reason,
|
|
218
228
|
});
|
|
219
229
|
this.db.issueSessions.commitIssueState({
|
|
220
230
|
writer: WRITER,
|
|
@@ -231,10 +241,28 @@ export class RunFinalizer {
|
|
|
231
241
|
this.feed?.publish({
|
|
232
242
|
level: "info",
|
|
233
243
|
kind: "agent",
|
|
234
|
-
summary: `Run #${run.id} superseded — publication suppressed (
|
|
244
|
+
summary: `Run #${run.id} superseded — publication suppressed (${reason})`,
|
|
235
245
|
...(run.projectId ? { projectId: run.projectId } : {}),
|
|
236
246
|
});
|
|
237
247
|
}
|
|
248
|
+
resolveSuppressedRunReason(run, issue) {
|
|
249
|
+
if (run.shouldNotPublish || run.status === "superseded") {
|
|
250
|
+
return run.leaseRevokeReason ?? run.failureReason ?? "publication suppressed";
|
|
251
|
+
}
|
|
252
|
+
const workflowSnapshot = projectWorkflowSnapshot({
|
|
253
|
+
issue,
|
|
254
|
+
observations: this.db.workflowObservations.listObservations(issue.projectId, issue.linearIssueId),
|
|
255
|
+
blockerCount: this.db.issues.countUnresolvedBlockers(issue.projectId, issue.linearIssueId),
|
|
256
|
+
childCount: this.db.issues.listCanonicalChildIssues(issue.projectId, issue.linearIssueId).length,
|
|
257
|
+
});
|
|
258
|
+
if (!workflowSnapshot.authority.delegated) {
|
|
259
|
+
return "authority revoked before run completion";
|
|
260
|
+
}
|
|
261
|
+
if (run.authorityEpoch > 0 && workflowSnapshot.authority.epoch > run.authorityEpoch) {
|
|
262
|
+
return `authority epoch changed from ${run.authorityEpoch} to ${workflowSnapshot.authority.epoch}`;
|
|
263
|
+
}
|
|
264
|
+
return undefined;
|
|
265
|
+
}
|
|
238
266
|
publishTurnEvent(params) {
|
|
239
267
|
this.feed?.publish({
|
|
240
268
|
level: params.level,
|
|
@@ -287,6 +315,49 @@ export class RunFinalizer {
|
|
|
287
315
|
return undefined;
|
|
288
316
|
return status;
|
|
289
317
|
}
|
|
318
|
+
resolveWorkflowCompletionTask(run) {
|
|
319
|
+
if (!isRepairRunType(run.runType))
|
|
320
|
+
return undefined;
|
|
321
|
+
const record = this.db.workflowTasks.getTask(run.projectId, run.linearIssueId, `run:${run.runType}`);
|
|
322
|
+
if (!record?.runType)
|
|
323
|
+
return undefined;
|
|
324
|
+
const requirements = parseObjectJson(record.requirementsJson);
|
|
325
|
+
return {
|
|
326
|
+
id: record.taskId,
|
|
327
|
+
type: record.taskType,
|
|
328
|
+
runType: record.runType,
|
|
329
|
+
reason: record.reason,
|
|
330
|
+
...(requirements ? { requirements } : {}),
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
evaluateWorkflowCompletionGate(run, issue) {
|
|
334
|
+
const task = this.resolveWorkflowCompletionTask(run);
|
|
335
|
+
if (!task)
|
|
336
|
+
return undefined;
|
|
337
|
+
const snapshot = projectWorkflowSnapshot({
|
|
338
|
+
issue,
|
|
339
|
+
observations: this.db.workflowObservations.listObservations(issue.projectId, issue.linearIssueId),
|
|
340
|
+
blockerCount: this.db.issues.countUnresolvedBlockers(issue.projectId, issue.linearIssueId),
|
|
341
|
+
childCount: this.db.issues.listCanonicalChildIssues(issue.projectId, issue.linearIssueId).length,
|
|
342
|
+
});
|
|
343
|
+
const decision = evaluateTaskCompletion(snapshot, task);
|
|
344
|
+
if (decision.action === "start")
|
|
345
|
+
return undefined;
|
|
346
|
+
if (decision.action === "ask") {
|
|
347
|
+
return {
|
|
348
|
+
message: decision.question,
|
|
349
|
+
nextState: "awaiting_input",
|
|
350
|
+
status: decision.reason,
|
|
351
|
+
level: "warn",
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
return {
|
|
355
|
+
message: decision.reason,
|
|
356
|
+
nextState: "escalated",
|
|
357
|
+
status: decision.reason,
|
|
358
|
+
level: decision.action === "wait" ? "warn" : "error",
|
|
359
|
+
};
|
|
360
|
+
}
|
|
290
361
|
continueDirtyRepairWorktree(params) {
|
|
291
362
|
const message = params.status.summary
|
|
292
363
|
? `Repair run finished with a dirty worktree; ${params.status.summary}`
|
|
@@ -363,22 +434,18 @@ export class RunFinalizer {
|
|
|
363
434
|
}
|
|
364
435
|
async finalizeCompletedRun(params) {
|
|
365
436
|
const { run, issue, thread, threadId } = params;
|
|
366
|
-
|
|
367
|
-
//
|
|
368
|
-
//
|
|
369
|
-
//
|
|
370
|
-
//
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
// release the lease.
|
|
375
|
-
if (run.shouldNotPublish || run.status === "superseded") {
|
|
376
|
-
this.releaseSupersededRun(run, threadId, params.completedTurnId);
|
|
437
|
+
const freshIssue = this.db.issues.getIssue(run.projectId, run.linearIssueId) ?? issue;
|
|
438
|
+
// A run flagged shouldNotPublish, or whose authority epoch no
|
|
439
|
+
// longer matches current truth, must not enter publication or
|
|
440
|
+
// completion-verification paths. Those policies assume the run is
|
|
441
|
+
// still allowed to publish and may open follow-up work.
|
|
442
|
+
const suppressedReason = this.resolveSuppressedRunReason(run, freshIssue);
|
|
443
|
+
if (suppressedReason) {
|
|
444
|
+
this.releaseSuppressedRun(run, threadId, params.completedTurnId, suppressedReason);
|
|
377
445
|
return;
|
|
378
446
|
}
|
|
379
447
|
const trackedIssue = this.db.issueToTrackedIssue(issue);
|
|
380
448
|
const report = buildStageReport({ ...run, status: "completed" }, trackedIssue, thread, countEventMethods(this.db.runs.listThreadEvents(run.id)));
|
|
381
|
-
const freshIssue = this.db.issues.getIssue(run.projectId, run.linearIssueId) ?? issue;
|
|
382
449
|
const dirtyRepairWorktree = this.inspectDirtyRepairWorktree(run, freshIssue);
|
|
383
450
|
if (dirtyRepairWorktree) {
|
|
384
451
|
this.continueDirtyRepairWorktree({
|
|
@@ -391,6 +458,19 @@ export class RunFinalizer {
|
|
|
391
458
|
});
|
|
392
459
|
return;
|
|
393
460
|
}
|
|
461
|
+
const workflowCompletionGate = this.evaluateWorkflowCompletionGate(run, freshIssue);
|
|
462
|
+
if (workflowCompletionGate) {
|
|
463
|
+
this.failRunAndClear(run, workflowCompletionGate.message, workflowCompletionGate.nextState);
|
|
464
|
+
this.syncFailureOutcome({
|
|
465
|
+
run,
|
|
466
|
+
fallbackIssue: freshIssue,
|
|
467
|
+
message: workflowCompletionGate.message,
|
|
468
|
+
level: workflowCompletionGate.level,
|
|
469
|
+
status: workflowCompletionGate.status,
|
|
470
|
+
summary: workflowCompletionGate.message,
|
|
471
|
+
});
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
394
474
|
const verifiedRepairError = await this.completionPolicy.verifyReactiveRunAdvancedBranch(run, freshIssue);
|
|
395
475
|
if (verifiedRepairError) {
|
|
396
476
|
// The run failed verification — it did not do its work, so resolve
|
package/dist/run-launcher.js
CHANGED
|
@@ -123,6 +123,7 @@ export class RunLauncher {
|
|
|
123
123
|
linearIssueId: params.item.issueId,
|
|
124
124
|
runType: params.runType,
|
|
125
125
|
...(params.sourceHeadSha ? { sourceHeadSha: params.sourceHeadSha } : {}),
|
|
126
|
+
...(params.authorityEpoch !== undefined ? { authorityEpoch: params.authorityEpoch } : {}),
|
|
126
127
|
promptText: params.prompt,
|
|
127
128
|
});
|
|
128
129
|
const failureHeadSha = params.effectiveContext?.failureHeadSha ?? params.effectiveContext?.headSha;
|
package/dist/run-orchestrator.js
CHANGED
|
@@ -26,6 +26,7 @@ import { CodexThreadMaterializingError, isThreadMaterializingError } from "./cod
|
|
|
26
26
|
import { emitTelemetry, noopTelemetry } from "./telemetry.js";
|
|
27
27
|
import { LinearIssueProjectionService } from "./linear-issue-projection.js";
|
|
28
28
|
import { RunAdmissionController } from "./run-admission-controller.js";
|
|
29
|
+
import { reconcileWorkflowTasksForIssue } from "./workflow-task-reconciler.js";
|
|
29
30
|
const WRITER = "run-orchestrator";
|
|
30
31
|
function lowerCaseFirst(value) {
|
|
31
32
|
return value ? `${value.slice(0, 1).toLowerCase()}${value.slice(1)}` : value;
|
|
@@ -434,6 +435,7 @@ export class RunOrchestrator {
|
|
|
434
435
|
const sourceHeadSha = effectiveContext?.failureHeadSha
|
|
435
436
|
?? effectiveContext?.headSha
|
|
436
437
|
?? issue.prHeadSha;
|
|
438
|
+
const workflowSnapshot = reconcileWorkflowTasksForIssue(this.db, issue).snapshot;
|
|
437
439
|
const budgetExceeded = this.runWakePlanner.budgetExceeded(issue, project, runType, isRequestedChangesRunType);
|
|
438
440
|
if (budgetExceeded) {
|
|
439
441
|
this.emitRunSkipped(item, "budget_exceeded", issue, { runType });
|
|
@@ -458,6 +460,7 @@ export class RunOrchestrator {
|
|
|
458
460
|
runType,
|
|
459
461
|
prompt,
|
|
460
462
|
...(sourceHeadSha ? { sourceHeadSha } : {}),
|
|
463
|
+
authorityEpoch: workflowSnapshot.authority.epoch,
|
|
461
464
|
...(effectiveContext ? { effectiveContext } : {}),
|
|
462
465
|
materializeLegacyPendingWake: (targetIssue, lease) => this.materializeLegacyPendingWake(targetIssue, lease),
|
|
463
466
|
resolveRunWake: (targetIssue) => this.resolveRunWake(targetIssue),
|
|
@@ -469,6 +472,10 @@ export class RunOrchestrator {
|
|
|
469
472
|
this.releaseIssueSessionLease(item.projectId, item.issueId);
|
|
470
473
|
return;
|
|
471
474
|
}
|
|
475
|
+
const claimedIssue = this.db.issues.getIssue(item.projectId, item.issueId);
|
|
476
|
+
if (claimedIssue) {
|
|
477
|
+
reconcileWorkflowTasksForIssue(this.db, claimedIssue);
|
|
478
|
+
}
|
|
472
479
|
this.feed?.publish({
|
|
473
480
|
level: "info",
|
|
474
481
|
kind: "stage",
|
package/dist/run-wake-planner.js
CHANGED
|
@@ -1,8 +1,21 @@
|
|
|
1
1
|
import { getCiRepairBudget, getQueueRepairBudget, getReviewFixBudget, } from "./run-budgets.js";
|
|
2
2
|
import { buildRequestedChangesWakeIdentity } from "./reactive-wake-keys.js";
|
|
3
|
-
import { parseRunContextOrWarn, serializeRunContext } from "./run-context.js";
|
|
3
|
+
import { parseRunContextOrWarn, serializeRunContext, tryParseRunContextValue } from "./run-context.js";
|
|
4
4
|
import { assertNever } from "./utils.js";
|
|
5
5
|
const WRITER = "run-wake-planner";
|
|
6
|
+
function parseObjectJson(raw) {
|
|
7
|
+
if (!raw)
|
|
8
|
+
return undefined;
|
|
9
|
+
try {
|
|
10
|
+
const parsed = JSON.parse(raw);
|
|
11
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed)
|
|
12
|
+
? parsed
|
|
13
|
+
: undefined;
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
6
19
|
export class RunWakePlanner {
|
|
7
20
|
db;
|
|
8
21
|
logger;
|
|
@@ -11,15 +24,52 @@ export class RunWakePlanner {
|
|
|
11
24
|
this.logger = logger;
|
|
12
25
|
}
|
|
13
26
|
resolveRunWake(issue) {
|
|
14
|
-
|
|
15
|
-
|
|
27
|
+
if (this.db.issues.countUnresolvedBlockers(issue.projectId, issue.linearIssueId) > 0) {
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
const sessionWake = this.db.issueSessions.peekIssueSessionWake(issue.projectId, issue.linearIssueId);
|
|
31
|
+
if (sessionWake) {
|
|
32
|
+
return {
|
|
33
|
+
runType: sessionWake.runType,
|
|
34
|
+
context: sessionWake.context,
|
|
35
|
+
wakeReason: sessionWake.wakeReason,
|
|
36
|
+
resumeThread: sessionWake.resumeThread,
|
|
37
|
+
eventIds: sessionWake.eventIds,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
const workflowTaskWake = this.resolveWorkflowTaskWake(issue);
|
|
41
|
+
if (workflowTaskWake)
|
|
42
|
+
return workflowTaskWake;
|
|
43
|
+
const implicitWake = this.db.workflowWakes.peekIssueWake(issue.projectId, issue.linearIssueId);
|
|
44
|
+
if (!implicitWake)
|
|
45
|
+
return undefined;
|
|
46
|
+
return {
|
|
47
|
+
runType: implicitWake.runType,
|
|
48
|
+
context: implicitWake.context,
|
|
49
|
+
wakeReason: implicitWake.wakeReason,
|
|
50
|
+
resumeThread: implicitWake.resumeThread,
|
|
51
|
+
eventIds: implicitWake.eventIds,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
resolveWorkflowTaskWake(issue) {
|
|
55
|
+
const task = this.db.workflowTasks
|
|
56
|
+
.listOpenRunnableTasks(issue.projectId)
|
|
57
|
+
.find((entry) => entry.subjectId === issue.linearIssueId);
|
|
58
|
+
if (!task?.runType)
|
|
16
59
|
return undefined;
|
|
60
|
+
const runType = task.runType;
|
|
61
|
+
const rawRequirements = parseObjectJson(task.requirementsJson);
|
|
62
|
+
const context = tryParseRunContextValue({
|
|
63
|
+
...rawRequirements,
|
|
64
|
+
...(rawRequirements?.blockingHeadSha ? { requestedChangesHeadSha: rawRequirements.blockingHeadSha } : {}),
|
|
65
|
+
source: "workflow_task",
|
|
66
|
+
}) ?? { source: "workflow_task" };
|
|
17
67
|
return {
|
|
18
|
-
runType
|
|
19
|
-
context:
|
|
20
|
-
wakeReason:
|
|
21
|
-
resumeThread:
|
|
22
|
-
eventIds:
|
|
68
|
+
runType,
|
|
69
|
+
...(Object.keys(context).length > 0 ? { context } : {}),
|
|
70
|
+
wakeReason: task.taskId,
|
|
71
|
+
resumeThread: runType !== "implementation",
|
|
72
|
+
eventIds: [],
|
|
23
73
|
};
|
|
24
74
|
}
|
|
25
75
|
appendWakeEventWithLease(lease, issue, runType, context, dedupeScope) {
|