patchrelay 0.37.0 → 0.37.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/build-info.json +3 -3
- package/dist/cli/cluster-health.js +25 -22
- package/dist/cli/data.js +1 -0
- package/dist/cli/formatters/text.js +5 -1
- package/dist/cli/watch/IssueRow.js +4 -3
- package/dist/cli/watch/StatusBar.js +2 -1
- package/dist/cli/watch/detail-rows.js +4 -3
- package/dist/cli/watch/pr-status.js +2 -1
- package/dist/cli/watch/state-visualization.js +5 -1
- package/dist/factory-state.js +1 -1
- package/dist/github-webhook-handler.js +26 -4
- package/dist/idle-reconciliation.js +19 -2
- package/dist/implementation-outcome-policy.js +3 -1
- package/dist/issue-overview-query.js +5 -0
- package/dist/linear-session-reporting.js +15 -6
- package/dist/linear-status-comment-sync.js +13 -1
- package/dist/pr-state.js +49 -0
- package/dist/service-issue-actions.js +5 -4
- package/dist/tracked-issue-list-query.js +3 -1
- package/dist/tracked-issue-projector.js +5 -0
- package/dist/waiting-reason.js +3 -2
- package/package.json +1 -1
package/dist/build-info.json
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { deriveGateCheckStatusFromRollup } from "../github-rollup.js";
|
|
2
2
|
import { ACTIVE_RUN_STATES } from "../factory-state.js";
|
|
3
|
+
import { hasOpenPr, resolveClosedPrDisposition } from "../pr-state.js";
|
|
3
4
|
const RECONCILIATION_GRACE_MS = 120_000;
|
|
4
5
|
const DOWNSTREAM_STALE_MS = 900_000;
|
|
5
6
|
export async function collectClusterHealth(config, db, runCommand) {
|
|
@@ -98,7 +99,7 @@ export async function collectClusterHealth(config, db, runCommand) {
|
|
|
98
99
|
}
|
|
99
100
|
checks.push(...await collectActiveOverlapFindings(snapshots, runCommand));
|
|
100
101
|
for (const snapshot of snapshots) {
|
|
101
|
-
if (!snapshot.issue.prNumber) {
|
|
102
|
+
if (!hasOpenPr(snapshot.issue.prNumber, snapshot.issue.prState)) {
|
|
102
103
|
continue;
|
|
103
104
|
}
|
|
104
105
|
const githubHealth = await evaluateGitHubIssueHealth(snapshot, config, runCommand, reviewQuillProbe, reviewQuillAttemptOwners, mergeStewardProbe);
|
|
@@ -277,19 +278,8 @@ async function evaluateGitHubIssueHealth(snapshot, config, runCommand, reviewQui
|
|
|
277
278
|
const latestBlockingReviewHeadSha = extractLatestBlockingReviewHeadSha(pr.latestReviews);
|
|
278
279
|
const mergeConflictDetected = pr.mergeable === "CONFLICTING" || pr.mergeStateStatus === "DIRTY";
|
|
279
280
|
const reviewQuillAttempt = issue.issueKey ? reviewQuillAttemptOwners?.get(issue.issueKey) : undefined;
|
|
280
|
-
const ciEntry = buildCiEntry({
|
|
281
|
-
issue,
|
|
282
|
-
gateCheckStatus,
|
|
283
|
-
reviewDecision,
|
|
284
|
-
reviewRequested,
|
|
285
|
-
currentHeadSha: pr.headRefOid,
|
|
286
|
-
latestBlockingReviewHeadSha,
|
|
287
|
-
mergeConflictDetected,
|
|
288
|
-
reviewQuillAttempt,
|
|
289
|
-
});
|
|
290
281
|
if (pr.state === "MERGED" && issue.factoryState !== "done" && ageMs >= RECONCILIATION_GRACE_MS) {
|
|
291
282
|
return {
|
|
292
|
-
ciEntry,
|
|
293
283
|
finding: {
|
|
294
284
|
status: "fail",
|
|
295
285
|
scope: "github:reconcile",
|
|
@@ -297,16 +287,29 @@ async function evaluateGitHubIssueHealth(snapshot, config, runCommand, reviewQui
|
|
|
297
287
|
},
|
|
298
288
|
};
|
|
299
289
|
}
|
|
300
|
-
if (pr.state === "CLOSED"
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
290
|
+
if (pr.state === "CLOSED") {
|
|
291
|
+
const closedPrDisposition = resolveClosedPrDisposition(issue);
|
|
292
|
+
if (closedPrDisposition === "redelegate" && issue.factoryState !== "delegated" && ageMs >= RECONCILIATION_GRACE_MS) {
|
|
293
|
+
return {
|
|
294
|
+
finding: {
|
|
295
|
+
status: "fail",
|
|
296
|
+
scope: "github:reconcile",
|
|
297
|
+
message: "PR is closed but unfinished work has not been re-delegated",
|
|
298
|
+
},
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
return {};
|
|
309
302
|
}
|
|
303
|
+
const ciEntry = buildCiEntry({
|
|
304
|
+
issue,
|
|
305
|
+
gateCheckStatus,
|
|
306
|
+
reviewDecision,
|
|
307
|
+
reviewRequested,
|
|
308
|
+
currentHeadSha: pr.headRefOid,
|
|
309
|
+
latestBlockingReviewHeadSha,
|
|
310
|
+
mergeConflictDetected,
|
|
311
|
+
reviewQuillAttempt,
|
|
312
|
+
});
|
|
310
313
|
if (gateCheckStatus === "failure" && issue.factoryState !== "repairing_ci" && issue.activeRunId === undefined && ageMs >= RECONCILIATION_GRACE_MS) {
|
|
311
314
|
return {
|
|
312
315
|
ciEntry,
|
|
@@ -521,7 +524,7 @@ function needsReviewAutomation(issue) {
|
|
|
521
524
|
if (issue.factoryState === "awaiting_queue" || issue.factoryState === "done") {
|
|
522
525
|
return false;
|
|
523
526
|
}
|
|
524
|
-
return issue.prNumber
|
|
527
|
+
return hasOpenPr(issue.prNumber, issue.prState);
|
|
525
528
|
}
|
|
526
529
|
async function collectReviewQuillAttemptOwners(snapshots, config, runCommand) {
|
|
527
530
|
const owners = new Map();
|
package/dist/cli/data.js
CHANGED
|
@@ -119,6 +119,7 @@ export class CliDataAccess extends CliOperatorApiClient {
|
|
|
119
119
|
...(latestReport ? { latestReport } : {}),
|
|
120
120
|
...(latestSummary ? { latestSummary } : {}),
|
|
121
121
|
...(dbIssue.prNumber ? { prNumber: dbIssue.prNumber } : {}),
|
|
122
|
+
...(dbIssue.prState ? { prState: dbIssue.prState } : {}),
|
|
122
123
|
...(dbIssue.prReviewState ? { prReviewState: dbIssue.prReviewState } : {}),
|
|
123
124
|
...((dbIssue.sessionState) ? { sessionState: dbIssue.sessionState } : {}),
|
|
124
125
|
...((dbIssue.waitingReason) ? { waitingReason: dbIssue.waitingReason } : {}),
|
|
@@ -20,7 +20,11 @@ export function formatInspect(result) {
|
|
|
20
20
|
value("Debug stage", result.issue?.factoryState),
|
|
21
21
|
result.activeRun ? value("Active run", `${result.activeRun.runType} (${result.activeRun.status})`) : undefined,
|
|
22
22
|
result.latestRun && !result.activeRun ? value("Latest run", `${result.latestRun.runType} (${result.latestRun.status})`) : undefined,
|
|
23
|
-
result.prNumber
|
|
23
|
+
result.prNumber
|
|
24
|
+
? value("PR", `#${result.prNumber}${result.prState || result.prReviewState
|
|
25
|
+
? ` [${[result.prState, result.prReviewState].filter(Boolean).join(", ")}]`
|
|
26
|
+
: ""}`)
|
|
27
|
+
: undefined,
|
|
24
28
|
result.completionCheckOutcome ? value("Completion check", result.completionCheckOutcome) : undefined,
|
|
25
29
|
result.completionCheckSummary ? value("Completion summary", truncateLine(result.completionCheckSummary)) : undefined,
|
|
26
30
|
result.completionCheckQuestion ? value("Question", truncateLine(result.completionCheckQuestion)) : undefined,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text } from "ink";
|
|
3
|
+
import { hasOpenPr } from "../../pr-state.js";
|
|
3
4
|
import { summarizeIssueStatusNote } from "./issue-status-note.js";
|
|
4
5
|
import { relativeTime, truncate } from "./format-utils.js";
|
|
5
6
|
import { hasDisplayPrBlocker, isApprovedReviewState, isAwaitingReviewState, isChangesRequestedReviewState, isRereviewNeeded, prChecksFact, } from "./pr-status.js";
|
|
@@ -19,7 +20,7 @@ function effectiveState(issue) {
|
|
|
19
20
|
return "blocked";
|
|
20
21
|
if (issue.sessionState === "waiting_input")
|
|
21
22
|
return "awaiting_input";
|
|
22
|
-
if (issue.prNumber
|
|
23
|
+
if (hasOpenPr(issue.prNumber, issue.prState))
|
|
23
24
|
return issue.factoryState;
|
|
24
25
|
if (issue.readyForExecution && !issue.activeRunType && !hasDisplayPrBlocker(issue))
|
|
25
26
|
return "ready";
|
|
@@ -94,7 +95,7 @@ function buildFacts(issue, selected) {
|
|
|
94
95
|
else if (isChangesRequestedReviewState(issue.prReviewState)) {
|
|
95
96
|
facts.push({ text: "changes requested", color: "yellow" });
|
|
96
97
|
}
|
|
97
|
-
else if (issue.prNumber
|
|
98
|
+
else if (hasOpenPr(issue.prNumber, issue.prState)
|
|
98
99
|
&& (isAwaitingReviewState(issue.prReviewState) || (!issue.prReviewState && !TERMINAL_STATES.has(effectiveState(issue))))) {
|
|
99
100
|
facts.push({ text: "awaiting review", color: "yellow" });
|
|
100
101
|
}
|
|
@@ -146,7 +147,7 @@ function blockerText(issue) {
|
|
|
146
147
|
return "Awaiting re-review after requested changes";
|
|
147
148
|
if (isChangesRequestedReviewState(issue.prReviewState))
|
|
148
149
|
return "Review changes requested";
|
|
149
|
-
if (issue.prNumber
|
|
150
|
+
if (hasOpenPr(issue.prNumber, issue.prState) && isAwaitingReviewState(issue.prReviewState))
|
|
150
151
|
return "Awaiting review";
|
|
151
152
|
return null;
|
|
152
153
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text } from "ink";
|
|
3
|
+
import { hasOpenPr } from "../../pr-state.js";
|
|
3
4
|
import { computeAggregates } from "./watch-state.js";
|
|
4
5
|
import { FreshnessBadge } from "./FreshnessBadge.js";
|
|
5
6
|
const FILTER_LABELS = {
|
|
@@ -11,7 +12,7 @@ export function StatusBar({ issues, totalCount, filter, connected, lastServerMes
|
|
|
11
12
|
const showing = filter === "all" ? `${totalCount} issues` : `${issues.length}/${totalCount} issues`;
|
|
12
13
|
const aggregateSource = filter === "all" ? allIssues : issues;
|
|
13
14
|
const agg = computeAggregates(aggregateSource);
|
|
14
|
-
const withPr = aggregateSource.filter((i) => i.prNumber
|
|
15
|
+
const withPr = aggregateSource.filter((i) => hasOpenPr(i.prNumber, i.prState)).length;
|
|
15
16
|
const waitingInput = aggregateSource.filter((i) => i.sessionState === "waiting_input" || i.factoryState === "awaiting_input").length;
|
|
16
17
|
const intervention = aggregateSource.filter((i) => i.sessionState === "failed" || i.factoryState === "failed" || i.factoryState === "escalated").length;
|
|
17
18
|
const running = aggregateSource.filter((i) => i.sessionState === "running").length;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { hasOpenPr } from "../../pr-state.js";
|
|
1
2
|
import { buildStateHistory } from "./history-builder.js";
|
|
2
3
|
import { buildTimelineRows } from "./timeline-presentation.js";
|
|
3
4
|
import { planStepColor, planStepSymbol } from "./plan-helpers.js";
|
|
@@ -396,7 +397,7 @@ function buildFactSegments(issue, issueContext) {
|
|
|
396
397
|
facts.push([{ text: "re-review needed", color: "yellow" }]);
|
|
397
398
|
else if (isChangesRequestedReviewState(issue.prReviewState))
|
|
398
399
|
facts.push([{ text: "changes requested", color: "yellow" }]);
|
|
399
|
-
else if (issue.prNumber
|
|
400
|
+
else if (hasOpenPr(issue.prNumber, issue.prState)
|
|
400
401
|
&& (isAwaitingReviewState(issue.prReviewState) || (!issue.prReviewState && issue.factoryState === "pr_open")))
|
|
401
402
|
facts.push([{ text: "awaiting review", color: "yellow" }]);
|
|
402
403
|
if (issue.factoryState === "awaiting_queue")
|
|
@@ -503,7 +504,7 @@ function effectiveState(issue) {
|
|
|
503
504
|
return "blocked";
|
|
504
505
|
if (issue.sessionState === "waiting_input")
|
|
505
506
|
return "awaiting_input";
|
|
506
|
-
if (issue.prNumber
|
|
507
|
+
if (hasOpenPr(issue.prNumber, issue.prState))
|
|
507
508
|
return issue.factoryState;
|
|
508
509
|
if (issue.readyForExecution && !issue.activeRunType && !hasDisplayPrBlocker(issue))
|
|
509
510
|
return "ready";
|
|
@@ -546,7 +547,7 @@ function blockerText(issue, issueContext) {
|
|
|
546
547
|
return "Awaiting re-review after requested changes";
|
|
547
548
|
if (isChangesRequestedReviewState(issue.prReviewState))
|
|
548
549
|
return "Review changes requested";
|
|
549
|
-
if (issue.prNumber
|
|
550
|
+
if (hasOpenPr(issue.prNumber, issue.prState) && (isAwaitingReviewState(issue.prReviewState) || (!issue.prReviewState && effectiveState(issue) !== "done"))) {
|
|
550
551
|
return "Awaiting review";
|
|
551
552
|
}
|
|
552
553
|
return null;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { hasOpenPr } from "../../pr-state.js";
|
|
1
2
|
function isPassingCheckStatus(status) {
|
|
2
3
|
return status === "passed" || status === "success";
|
|
3
4
|
}
|
|
@@ -72,7 +73,7 @@ export function prChecksFact(issue) {
|
|
|
72
73
|
return undefined;
|
|
73
74
|
}
|
|
74
75
|
export function hasDisplayPrBlocker(issue) {
|
|
75
|
-
if (issue.prNumber
|
|
76
|
+
if (!hasOpenPr(issue.prNumber, issue.prState) || issue.activeRunType) {
|
|
76
77
|
return false;
|
|
77
78
|
}
|
|
78
79
|
if (issue.factoryState === "pr_open" || issue.factoryState === "awaiting_queue" || issue.factoryState === "repairing_queue") {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { hasOpenPr } from "../../pr-state.js";
|
|
1
2
|
const STATE_LABELS = {
|
|
2
3
|
delegated: "delegated",
|
|
3
4
|
implementing: "implementing",
|
|
@@ -163,9 +164,12 @@ export function buildPatchRelayQueueObservations(issue, feedEvents) {
|
|
|
163
164
|
});
|
|
164
165
|
}
|
|
165
166
|
if (issue.prNumber !== undefined) {
|
|
167
|
+
const prLabel = hasOpenPr(issue.prNumber, issue.prState)
|
|
168
|
+
? `Tracked PR: #${issue.prNumber}`
|
|
169
|
+
: `Tracked PR: #${issue.prNumber}${issue.prState ? ` (${issue.prState})` : ""}`;
|
|
166
170
|
observations.push({
|
|
167
171
|
tone: "info",
|
|
168
|
-
text:
|
|
172
|
+
text: `${prLabel}${issue.prReviewState ? ` (${issue.prReviewState})` : ""}`,
|
|
169
173
|
});
|
|
170
174
|
}
|
|
171
175
|
return observations.slice(0, 3);
|
package/dist/factory-state.js
CHANGED
|
@@ -30,7 +30,7 @@ const TRANSITION_RULES = [
|
|
|
30
30
|
// pr_closed during an active run is suppressed — Codex may reopen.
|
|
31
31
|
// Without a guard match, the event produces no transition (undefined).
|
|
32
32
|
{ event: "pr_closed",
|
|
33
|
-
guard: (
|
|
33
|
+
guard: (s, ctx) => ctx.activeRunId === undefined && !TERMINAL_STATES.has(s),
|
|
34
34
|
to: "failed" },
|
|
35
35
|
// ── PR lifecycle ───────────────────────────────────────────────
|
|
36
36
|
{ event: "pr_opened",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { resolveFactoryStateFromGitHub
|
|
1
|
+
import { resolveFactoryStateFromGitHub } from "./factory-state.js";
|
|
2
2
|
import { createGitHubCiSnapshotResolver, createGitHubFailureContextResolver, summarizeGitHubFailureContext, } from "./github-failure-context.js";
|
|
3
3
|
import { normalizeGitHubWebhook, verifyGitHubWebhookSignature } from "./github-webhooks.js";
|
|
4
4
|
import { buildAgentSessionPlanForIssue } from "./agent-session-plan.js";
|
|
@@ -7,6 +7,7 @@ import { buildGitHubStateActivity } from "./linear-session-reporting.js";
|
|
|
7
7
|
import { resolveMergeQueueProtocol, } from "./merge-queue-protocol.js";
|
|
8
8
|
import { buildQueueRepairContextFromEvent } from "./merge-queue-incident.js";
|
|
9
9
|
import { resolvePreferredCompletedLinearState } from "./linear-workflow.js";
|
|
10
|
+
import { buildClosedPrCleanupFields, isIssueTerminal, resolveClosedPrFactoryState, resolveClosedPrDisposition, } from "./pr-state.js";
|
|
10
11
|
import { resolveSecret } from "./resolve-secret.js";
|
|
11
12
|
import { safeJsonParse } from "./utils.js";
|
|
12
13
|
/**
|
|
@@ -153,6 +154,9 @@ export class GitHubWebhookHandler {
|
|
|
153
154
|
: event.reviewState === "approved"
|
|
154
155
|
? { lastBlockingReviewHeadSha: null }
|
|
155
156
|
: {}),
|
|
157
|
+
...(event.triggerEvent === "pr_closed"
|
|
158
|
+
? buildClosedPrCleanupFields()
|
|
159
|
+
: {}),
|
|
156
160
|
});
|
|
157
161
|
await this.updateCiSnapshot(issue, event, project);
|
|
158
162
|
await this.updateFailureProvenance(issue, event, project);
|
|
@@ -227,11 +231,14 @@ export class GitHubWebhookHandler {
|
|
|
227
231
|
}
|
|
228
232
|
}
|
|
229
233
|
resolveFactoryStateForEvent(issue, event, project) {
|
|
234
|
+
if (event.triggerEvent === "pr_closed") {
|
|
235
|
+
return undefined;
|
|
236
|
+
}
|
|
230
237
|
if (event.triggerEvent === "check_failed"
|
|
231
238
|
&& this.isQueueEvictionFailure(issue, event, project)
|
|
232
239
|
&& issue.prState === "open"
|
|
233
240
|
&& issue.activeRunId === undefined
|
|
234
|
-
&& !
|
|
241
|
+
&& !isIssueTerminal(issue)) {
|
|
235
242
|
return "repairing_queue";
|
|
236
243
|
}
|
|
237
244
|
return resolveFactoryStateFromGitHub(event.triggerEvent, issue.factoryState, {
|
|
@@ -318,7 +325,7 @@ export class GitHubWebhookHandler {
|
|
|
318
325
|
return;
|
|
319
326
|
// Don't trigger on terminal issues — late-arriving webhooks (e.g.
|
|
320
327
|
// merge_group_failed after pr_merged) must not resurrect done issues.
|
|
321
|
-
if (
|
|
328
|
+
if (isIssueTerminal(issue))
|
|
322
329
|
return;
|
|
323
330
|
if (!this.isPatchRelayOwnedPr(issue)) {
|
|
324
331
|
this.feed?.publish({
|
|
@@ -521,11 +528,14 @@ export class GitHubWebhookHandler {
|
|
|
521
528
|
: "Pull request closed during active run",
|
|
522
529
|
});
|
|
523
530
|
}
|
|
531
|
+
const terminalFactoryState = event.triggerEvent === "pr_merged"
|
|
532
|
+
? "done"
|
|
533
|
+
: resolveClosedPrFactoryState(issue);
|
|
524
534
|
this.db.issues.upsertIssue({
|
|
525
535
|
projectId: issue.projectId,
|
|
526
536
|
linearIssueId: issue.linearIssueId,
|
|
527
537
|
activeRunId: null,
|
|
528
|
-
factoryState:
|
|
538
|
+
factoryState: terminalFactoryState,
|
|
529
539
|
});
|
|
530
540
|
};
|
|
531
541
|
const activeLease = this.db.issueSessions.getActiveIssueSessionLease(issue.projectId, issue.linearIssueId);
|
|
@@ -537,6 +547,18 @@ export class GitHubWebhookHandler {
|
|
|
537
547
|
}
|
|
538
548
|
this.db.issueSessions.releaseIssueSessionLeaseRespectingActiveLease(issue.projectId, issue.linearIssueId);
|
|
539
549
|
const updatedIssue = this.db.issues.getIssue(issue.projectId, issue.linearIssueId) ?? issue;
|
|
550
|
+
if (event.triggerEvent === "pr_closed" && resolveClosedPrDisposition(issue) === "redelegate") {
|
|
551
|
+
this.db.issueSessions.appendIssueSessionEventRespectingActiveLease(issue.projectId, issue.linearIssueId, {
|
|
552
|
+
projectId: issue.projectId,
|
|
553
|
+
linearIssueId: issue.linearIssueId,
|
|
554
|
+
eventType: "delegated",
|
|
555
|
+
dedupeKey: `github_pr_closed:implementation:${issue.linearIssueId}`,
|
|
556
|
+
});
|
|
557
|
+
this.db.issueSessions.setBranchOwnerRespectingActiveLease(issue.projectId, issue.linearIssueId, "patchrelay");
|
|
558
|
+
if (this.db.issueSessions.peekIssueSessionWake(issue.projectId, issue.linearIssueId)) {
|
|
559
|
+
this.enqueueIssue(issue.projectId, issue.linearIssueId);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
540
562
|
if (event.triggerEvent === "pr_merged") {
|
|
541
563
|
await this.completeLinearIssueAfterMerge(updatedIssue);
|
|
542
564
|
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import {} from "./factory-state.js";
|
|
1
2
|
import { resolveMergeQueueProtocol } from "./merge-queue-protocol.js";
|
|
2
3
|
import { parseGitHubFailureContext } from "./github-failure-context.js";
|
|
3
4
|
import { deriveGateCheckStatusFromRollup } from "./github-rollup.js";
|
|
4
5
|
import { deriveIssueSessionReactiveIntent } from "./issue-session.js";
|
|
5
6
|
import { parseStoredQueueRepairContext } from "./merge-queue-incident.js";
|
|
7
|
+
import { buildClosedPrCleanupFields, resolveClosedPrDisposition } from "./pr-state.js";
|
|
6
8
|
import { execCommand } from "./utils.js";
|
|
7
9
|
const DEFAULT_REVIEW_FIX_BUDGET = 12;
|
|
8
10
|
function isFailingCheckStatus(status) {
|
|
@@ -439,8 +441,23 @@ export class IdleIssueReconciler {
|
|
|
439
441
|
return;
|
|
440
442
|
}
|
|
441
443
|
if (pr.state === "CLOSED") {
|
|
442
|
-
|
|
443
|
-
this.db.issues.upsertIssue({
|
|
444
|
+
const closedPrDisposition = resolveClosedPrDisposition(issue);
|
|
445
|
+
this.db.issues.upsertIssue({
|
|
446
|
+
projectId: issue.projectId,
|
|
447
|
+
linearIssueId: issue.linearIssueId,
|
|
448
|
+
prState: "closed",
|
|
449
|
+
...buildClosedPrCleanupFields(),
|
|
450
|
+
});
|
|
451
|
+
if (closedPrDisposition === "done") {
|
|
452
|
+
this.logger.info({ issueKey: issue.issueKey, prNumber: issue.prNumber }, "Reconciliation: PR was closed for an already completed issue; preserving done state");
|
|
453
|
+
this.advanceIdleIssue(issue, "done", { clearFailureProvenance: true });
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
if (closedPrDisposition === "terminal") {
|
|
457
|
+
this.logger.info({ issueKey: issue.issueKey, prNumber: issue.prNumber, factoryState: issue.factoryState }, "Reconciliation: PR was closed on a terminal issue; preserving terminal state");
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
this.logger.info({ issueKey: issue.issueKey, prNumber: issue.prNumber }, "Reconciliation: PR was closed on unfinished work, re-delegating for implementation");
|
|
444
461
|
this.advanceIdleIssue(issue, "delegated", {
|
|
445
462
|
pendingRunType: "implementation",
|
|
446
463
|
clearFailureProvenance: true,
|
|
@@ -46,7 +46,9 @@ export class ImplementationOutcomePolicy {
|
|
|
46
46
|
...(pr.headRefOid ? { prHeadSha: pr.headRefOid } : {}),
|
|
47
47
|
...(pr.author?.login ? { prAuthorLogin: pr.author.login } : {}),
|
|
48
48
|
}, "published PR verification refresh");
|
|
49
|
-
|
|
49
|
+
if (pr.state?.toLowerCase() !== "closed") {
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
50
52
|
}
|
|
51
53
|
}
|
|
52
54
|
}
|
|
@@ -143,6 +143,7 @@ export class IssueOverviewQuery {
|
|
|
143
143
|
factoryState: issueRecord?.factoryState ?? "delegated",
|
|
144
144
|
pendingRunType: issueRecord?.pendingRunType,
|
|
145
145
|
prNumber: session.prNumber,
|
|
146
|
+
prState: issueRecord?.prState,
|
|
146
147
|
prHeadSha: issueRecord?.prHeadSha ?? session.prHeadSha,
|
|
147
148
|
prReviewState: issueRecord?.prReviewState,
|
|
148
149
|
prCheckStatus: issueRecord?.prCheckStatus,
|
|
@@ -159,6 +160,10 @@ export class IssueOverviewQuery {
|
|
|
159
160
|
...(issueRecord?.currentLinearState ? { currentLinearState: issueRecord.currentLinearState } : {}),
|
|
160
161
|
sessionState: session.sessionState,
|
|
161
162
|
factoryState: issueRecord?.factoryState ?? "delegated",
|
|
163
|
+
...(session.prNumber !== undefined ? { prNumber: session.prNumber } : {}),
|
|
164
|
+
...(issueRecord?.prState ? { prState: issueRecord.prState } : {}),
|
|
165
|
+
...(issueRecord?.prReviewState ? { prReviewState: issueRecord.prReviewState } : {}),
|
|
166
|
+
...(issueRecord?.prCheckStatus ? { prCheckStatus: issueRecord.prCheckStatus } : {}),
|
|
162
167
|
blockedByCount: unresolvedBlockedBy.length,
|
|
163
168
|
blockedByKeys,
|
|
164
169
|
readyForExecution: isIssueSessionReadyForExecution({
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { formatRunTypeLabel } from "./agent-session-plan.js";
|
|
2
2
|
import { sanitizeOperatorFacingText } from "./presentation-text.js";
|
|
3
|
+
import { isClosedPrState } from "./pr-state.js";
|
|
3
4
|
function lowerRunTypeLabel(runType) {
|
|
4
5
|
return formatRunTypeLabel(runType).toLowerCase();
|
|
5
6
|
}
|
|
@@ -188,15 +189,19 @@ export function buildMergePrepEscalationActivity(attempts) {
|
|
|
188
189
|
export function summarizeIssueStateForLinear(issue) {
|
|
189
190
|
switch (issue.sessionState) {
|
|
190
191
|
case "waiting_input":
|
|
191
|
-
return issue.waitingReason ?? (issue.prNumber ? `PR #${issue.prNumber} is waiting for input.` : "Waiting for input.");
|
|
192
|
+
return issue.waitingReason ?? (issue.prNumber && !isClosedPrState(issue.prState) ? `PR #${issue.prNumber} is waiting for input.` : "Waiting for input.");
|
|
192
193
|
case "running":
|
|
193
|
-
return issue.waitingReason ?? (issue.prNumber ? `PR #${issue.prNumber} is actively running.` : "Actively running.");
|
|
194
|
+
return issue.waitingReason ?? (issue.prNumber && !isClosedPrState(issue.prState) ? `PR #${issue.prNumber} is actively running.` : "Actively running.");
|
|
194
195
|
case "idle":
|
|
195
|
-
return issue.waitingReason ?? (issue.prNumber ? `PR #${issue.prNumber} is idle.` : "Idle.");
|
|
196
|
+
return issue.waitingReason ?? (issue.prNumber && !isClosedPrState(issue.prState) ? `PR #${issue.prNumber} is idle.` : "Idle.");
|
|
196
197
|
case "done":
|
|
197
|
-
|
|
198
|
+
if (issue.prNumber && issue.prState === "merged")
|
|
199
|
+
return `PR #${issue.prNumber} has merged.`;
|
|
200
|
+
if (issue.prNumber && isClosedPrState(issue.prState))
|
|
201
|
+
return `Completed without merging PR #${issue.prNumber}.`;
|
|
202
|
+
return issue.prNumber ? `Completed with PR #${issue.prNumber}.` : "Completed.";
|
|
198
203
|
case "failed":
|
|
199
|
-
return issue.waitingReason ?? (issue.prNumber ? `PR #${issue.prNumber} needs help to recover.` : "Needs help to recover.");
|
|
204
|
+
return issue.waitingReason ?? (issue.prNumber && !isClosedPrState(issue.prState) ? `PR #${issue.prNumber} needs help to recover.` : "Needs help to recover.");
|
|
200
205
|
}
|
|
201
206
|
switch (issue.factoryState) {
|
|
202
207
|
case "pr_open":
|
|
@@ -204,7 +209,11 @@ export function summarizeIssueStateForLinear(issue) {
|
|
|
204
209
|
case "awaiting_queue":
|
|
205
210
|
return issue.prNumber ? `PR #${issue.prNumber} is approved and awaiting merge.` : "Approved and awaiting merge.";
|
|
206
211
|
case "done":
|
|
207
|
-
|
|
212
|
+
if (issue.prNumber && issue.prState === "merged")
|
|
213
|
+
return `PR #${issue.prNumber} has merged.`;
|
|
214
|
+
if (issue.prNumber && isClosedPrState(issue.prState))
|
|
215
|
+
return `Completed without merging PR #${issue.prNumber}.`;
|
|
216
|
+
return issue.prNumber ? `Completed with PR #${issue.prNumber}.` : "Completed.";
|
|
208
217
|
default:
|
|
209
218
|
return undefined;
|
|
210
219
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { extractCompletionCheck } from "./completion-check.js";
|
|
2
2
|
import { deriveIssueStatusNote } from "./status-note.js";
|
|
3
3
|
import { derivePatchRelayWaitingReason } from "./waiting-reason.js";
|
|
4
|
+
import { isClosedPrState } from "./pr-state.js";
|
|
4
5
|
export async function syncVisibleStatusComment(params) {
|
|
5
6
|
const { db, issue, linear, logger, trackedIssue, options } = params;
|
|
6
7
|
try {
|
|
@@ -31,7 +32,9 @@ export function shouldSyncVisibleIssueComment(issue, hasAgentSession) {
|
|
|
31
32
|
|| issue.factoryState === "awaiting_input" || issue.factoryState === "failed" || issue.factoryState === "escalated") {
|
|
32
33
|
return true;
|
|
33
34
|
}
|
|
34
|
-
if ((issue.sessionState === "done" || issue.factoryState === "done")
|
|
35
|
+
if ((issue.sessionState === "done" || issue.factoryState === "done")
|
|
36
|
+
&& ((issue.prNumber === undefined && !issue.prUrl)
|
|
37
|
+
|| isClosedPrState(issue.prState))) {
|
|
35
38
|
return true;
|
|
36
39
|
}
|
|
37
40
|
return false;
|
|
@@ -49,6 +52,7 @@ function renderStatusComment(db, issue, trackedIssue, options) {
|
|
|
49
52
|
factoryState: issue.factoryState,
|
|
50
53
|
pendingRunType: issue.pendingRunType,
|
|
51
54
|
...(issue.prNumber !== undefined ? { prNumber: issue.prNumber } : {}),
|
|
55
|
+
...(issue.prState ? { prState: issue.prState } : {}),
|
|
52
56
|
prHeadSha: issue.prHeadSha,
|
|
53
57
|
prReviewState: issue.prReviewState,
|
|
54
58
|
prCheckStatus: issue.prCheckStatus,
|
|
@@ -110,6 +114,10 @@ function statusHeadline(issue, activeRunType) {
|
|
|
110
114
|
case "running":
|
|
111
115
|
return issue.prNumber !== undefined ? `PR #${issue.prNumber} is actively running` : "Actively running";
|
|
112
116
|
case "done":
|
|
117
|
+
if (issue.prNumber !== undefined && issue.prState === "merged")
|
|
118
|
+
return `Completed with merged PR #${issue.prNumber}`;
|
|
119
|
+
if (issue.prNumber !== undefined && isClosedPrState(issue.prState))
|
|
120
|
+
return `Completed without merging PR #${issue.prNumber}`;
|
|
113
121
|
return issue.prNumber !== undefined ? `Completed with PR #${issue.prNumber}` : "Completed";
|
|
114
122
|
case "failed":
|
|
115
123
|
return "Needs operator intervention";
|
|
@@ -138,6 +146,10 @@ function statusHeadline(issue, activeRunType) {
|
|
|
138
146
|
case "escalated":
|
|
139
147
|
return "Needs operator intervention";
|
|
140
148
|
case "done":
|
|
149
|
+
if (issue.prNumber !== undefined && issue.prState === "merged")
|
|
150
|
+
return `Completed with merged PR #${issue.prNumber}`;
|
|
151
|
+
if (issue.prNumber !== undefined && isClosedPrState(issue.prState))
|
|
152
|
+
return `Completed without merging PR #${issue.prNumber}`;
|
|
141
153
|
return issue.prNumber !== undefined ? `Completed with PR #${issue.prNumber}` : "Completed";
|
|
142
154
|
default:
|
|
143
155
|
return humanize(issue.factoryState);
|
package/dist/pr-state.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { TERMINAL_STATES } from "./factory-state.js";
|
|
2
|
+
export function isOpenPrState(prState) {
|
|
3
|
+
return prState === undefined || prState === "open";
|
|
4
|
+
}
|
|
5
|
+
export function hasOpenPr(prNumber, prState) {
|
|
6
|
+
// Transitional compatibility: older rows may still have a tracked PR number
|
|
7
|
+
// before webhook/reconciliation has populated pr_state.
|
|
8
|
+
return prNumber !== undefined && isOpenPrState(prState);
|
|
9
|
+
}
|
|
10
|
+
export function isClosedPrState(prState) {
|
|
11
|
+
return prState === "closed";
|
|
12
|
+
}
|
|
13
|
+
export function isCompletedLinearState(currentLinearStateType, currentLinearState) {
|
|
14
|
+
return currentLinearStateType === "completed"
|
|
15
|
+
|| currentLinearState?.trim().toLowerCase() === "done";
|
|
16
|
+
}
|
|
17
|
+
export function isIssueCompleted(issue) {
|
|
18
|
+
return issue.factoryState === "done" || isCompletedLinearState(issue.currentLinearStateType, issue.currentLinearState);
|
|
19
|
+
}
|
|
20
|
+
export function isIssueTerminal(issue) {
|
|
21
|
+
return issue.factoryState !== undefined && TERMINAL_STATES.has(issue.factoryState);
|
|
22
|
+
}
|
|
23
|
+
export function resolveClosedPrDisposition(issue) {
|
|
24
|
+
if (isIssueCompleted(issue))
|
|
25
|
+
return "done";
|
|
26
|
+
if (isIssueTerminal(issue))
|
|
27
|
+
return "terminal";
|
|
28
|
+
return "redelegate";
|
|
29
|
+
}
|
|
30
|
+
export function resolveClosedPrFactoryState(issue) {
|
|
31
|
+
const disposition = resolveClosedPrDisposition(issue);
|
|
32
|
+
if (disposition === "done")
|
|
33
|
+
return "done";
|
|
34
|
+
if (disposition === "terminal")
|
|
35
|
+
return issue.factoryState;
|
|
36
|
+
return "delegated";
|
|
37
|
+
}
|
|
38
|
+
export function buildClosedPrCleanupFields() {
|
|
39
|
+
return {
|
|
40
|
+
prReviewState: null,
|
|
41
|
+
prCheckStatus: null,
|
|
42
|
+
lastBlockingReviewHeadSha: null,
|
|
43
|
+
lastGitHubCiSnapshotHeadSha: null,
|
|
44
|
+
lastGitHubCiSnapshotGateCheckName: null,
|
|
45
|
+
lastGitHubCiSnapshotGateCheckStatus: null,
|
|
46
|
+
lastGitHubCiSnapshotJson: null,
|
|
47
|
+
lastGitHubCiSnapshotSettledAt: null,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { buildOperatorRetryEvent } from "./operator-retry-event.js";
|
|
2
|
+
import { hasOpenPr } from "./pr-state.js";
|
|
2
3
|
export class ServiceIssueActions {
|
|
3
4
|
db;
|
|
4
5
|
codex;
|
|
@@ -107,21 +108,21 @@ export class ServiceIssueActions {
|
|
|
107
108
|
}
|
|
108
109
|
let runType = "implementation";
|
|
109
110
|
let factoryState = "delegated";
|
|
110
|
-
if (issue.prNumber && issue.lastGitHubFailureSource === "queue_eviction") {
|
|
111
|
+
if (hasOpenPr(issue.prNumber, issue.prState) && issue.lastGitHubFailureSource === "queue_eviction") {
|
|
111
112
|
runType = "queue_repair";
|
|
112
113
|
factoryState = "repairing_queue";
|
|
113
114
|
}
|
|
114
|
-
else if (issue.prNumber && (issue.prCheckStatus === "failed" || issue.prCheckStatus === "failure" || issue.lastGitHubFailureSource === "branch_ci")) {
|
|
115
|
+
else if (hasOpenPr(issue.prNumber, issue.prState) && (issue.prCheckStatus === "failed" || issue.prCheckStatus === "failure" || issue.lastGitHubFailureSource === "branch_ci")) {
|
|
115
116
|
runType = "ci_repair";
|
|
116
117
|
factoryState = "repairing_ci";
|
|
117
118
|
}
|
|
118
|
-
else if (issue.prNumber && issue.prReviewState === "changes_requested") {
|
|
119
|
+
else if (hasOpenPr(issue.prNumber, issue.prState) && issue.prReviewState === "changes_requested") {
|
|
119
120
|
runType = issue.pendingRunType === "branch_upkeep" || issueSession?.lastRunType === "branch_upkeep"
|
|
120
121
|
? "branch_upkeep"
|
|
121
122
|
: "review_fix";
|
|
122
123
|
factoryState = "changes_requested";
|
|
123
124
|
}
|
|
124
|
-
else if (issue.prNumber) {
|
|
125
|
+
else if (hasOpenPr(issue.prNumber, issue.prState)) {
|
|
125
126
|
runType = "implementation";
|
|
126
127
|
factoryState = "implementing";
|
|
127
128
|
}
|
|
@@ -82,7 +82,7 @@ export class TrackedIssueListQuery {
|
|
|
82
82
|
s.project_id, s.linear_issue_id, s.issue_key, i.title,
|
|
83
83
|
i.current_linear_state, i.factory_state, s.session_state, s.waiting_reason, s.summary_text, s.updated_at,
|
|
84
84
|
i.pending_run_type,
|
|
85
|
-
i.pr_number, i.pr_head_sha, i.pr_review_state, i.pr_check_status, i.last_blocking_review_head_sha,
|
|
85
|
+
i.pr_number, i.pr_state, i.pr_head_sha, i.pr_review_state, i.pr_check_status, i.last_blocking_review_head_sha,
|
|
86
86
|
i.last_github_ci_snapshot_json,
|
|
87
87
|
i.last_github_failure_source,
|
|
88
88
|
i.last_github_failure_head_sha,
|
|
@@ -180,6 +180,7 @@ export class TrackedIssueListQuery {
|
|
|
180
180
|
factoryState: String(row.factory_state ?? "delegated"),
|
|
181
181
|
...(row.pending_run_type !== null ? { pendingRunType: String(row.pending_run_type) } : {}),
|
|
182
182
|
...(row.pr_number !== null ? { prNumber: Number(row.pr_number) } : {}),
|
|
183
|
+
...(row.pr_state !== null ? { prState: String(row.pr_state) } : {}),
|
|
183
184
|
...(row.pr_head_sha !== null ? { prHeadSha: String(row.pr_head_sha) } : {}),
|
|
184
185
|
...(row.pr_review_state !== null ? { prReviewState: String(row.pr_review_state) } : {}),
|
|
185
186
|
...(row.pr_check_status !== null ? { prCheckStatus: String(row.pr_check_status) } : {}),
|
|
@@ -242,6 +243,7 @@ export class TrackedIssueListQuery {
|
|
|
242
243
|
...(row.latest_run_type !== null ? { latestRunType: String(row.latest_run_type) } : {}),
|
|
243
244
|
...(row.latest_run_status !== null ? { latestRunStatus: String(row.latest_run_status) } : {}),
|
|
244
245
|
...(row.pr_number !== null ? { prNumber: Number(row.pr_number) } : {}),
|
|
246
|
+
...(row.pr_state !== null ? { prState: String(row.pr_state) } : {}),
|
|
245
247
|
...(row.pr_review_state !== null ? { prReviewState: String(row.pr_review_state) } : {}),
|
|
246
248
|
...(row.pr_check_status !== null ? { prCheckStatus: String(row.pr_check_status) } : {}),
|
|
247
249
|
...(prChecksSummary ? { prChecksSummary } : {}),
|
|
@@ -15,6 +15,7 @@ export function buildTrackedIssueRecord(params) {
|
|
|
15
15
|
factoryState: params.issue.factoryState,
|
|
16
16
|
pendingRunType: params.issue.pendingRunType,
|
|
17
17
|
prNumber: params.issue.prNumber,
|
|
18
|
+
prState: params.issue.prState,
|
|
18
19
|
prHeadSha: params.issue.prHeadSha,
|
|
19
20
|
prReviewState: params.issue.prReviewState,
|
|
20
21
|
prCheckStatus: params.issue.prCheckStatus,
|
|
@@ -46,6 +47,10 @@ export function buildTrackedIssueRecord(params) {
|
|
|
46
47
|
...(params.issue.currentLinearState ? { currentLinearState: params.issue.currentLinearState } : {}),
|
|
47
48
|
...(params.session?.sessionState ? { sessionState: params.session.sessionState } : {}),
|
|
48
49
|
factoryState: params.issue.factoryState,
|
|
50
|
+
...(params.issue.prNumber !== undefined ? { prNumber: params.issue.prNumber } : {}),
|
|
51
|
+
...(params.issue.prState ? { prState: params.issue.prState } : {}),
|
|
52
|
+
...(params.issue.prReviewState ? { prReviewState: params.issue.prReviewState } : {}),
|
|
53
|
+
...(params.issue.prCheckStatus ? { prCheckStatus: params.issue.prCheckStatus } : {}),
|
|
49
54
|
blockedByCount: unresolvedBlockedBy.length,
|
|
50
55
|
blockedByKeys,
|
|
51
56
|
readyForExecution: isIssueSessionReadyForExecution({
|
package/dist/waiting-reason.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { hasOpenPr } from "./pr-state.js";
|
|
1
2
|
export const PATCHRELAY_WAITING_REASONS = {
|
|
2
3
|
activeWork: "PatchRelay is actively working",
|
|
3
4
|
finalizingPublishedPr: "PatchRelay is finalizing a published PR",
|
|
@@ -14,7 +15,7 @@ export const PATCHRELAY_WAITING_REASONS = {
|
|
|
14
15
|
};
|
|
15
16
|
export function derivePatchRelayWaitingReason(params) {
|
|
16
17
|
if (params.activeRunType) {
|
|
17
|
-
if (params.prNumber
|
|
18
|
+
if (hasOpenPr(params.prNumber, params.prState) && (params.factoryState === "pr_open" || params.factoryState === "awaiting_queue")) {
|
|
18
19
|
return PATCHRELAY_WAITING_REASONS.finalizingPublishedPr;
|
|
19
20
|
}
|
|
20
21
|
if (params.factoryState === "done") {
|
|
@@ -66,7 +67,7 @@ export function derivePatchRelayWaitingReason(params) {
|
|
|
66
67
|
if (params.prReviewState === "approved") {
|
|
67
68
|
return PATCHRELAY_WAITING_REASONS.waitingForDownstreamAutomation;
|
|
68
69
|
}
|
|
69
|
-
if (params.prNumber
|
|
70
|
+
if (hasOpenPr(params.prNumber, params.prState)) {
|
|
70
71
|
return PATCHRELAY_WAITING_REASONS.waitingForExternalReview;
|
|
71
72
|
}
|
|
72
73
|
if (params.pendingRunType) {
|