patchrelay 0.35.16 → 0.36.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/README.md +11 -2
- package/dist/agent-session-plan.js +14 -2
- package/dist/build-info.json +3 -3
- package/dist/cli/args.js +1 -0
- package/dist/cli/cluster-health.js +739 -0
- package/dist/cli/commands/cluster.js +14 -0
- package/dist/cli/data.js +9 -5
- package/dist/cli/help.js +21 -0
- package/dist/cli/index.js +27 -2
- package/dist/cli/output.js +38 -0
- package/dist/cli/watch/StateHistoryView.js +1 -0
- package/dist/cli/watch/TimelineRow.js +1 -0
- package/dist/cli/watch/detail-rows.js +1 -0
- package/dist/cli/watch/history-builder.js +1 -0
- package/dist/db/migrations.js +9 -0
- package/dist/db.js +32 -8
- package/dist/github-webhook-handler.js +5 -78
- package/dist/idle-reconciliation.js +88 -6
- package/dist/issue-query-service.js +2 -0
- package/dist/issue-session-events.js +2 -2
- package/dist/issue-session.js +2 -0
- package/dist/linear-session-reporting.js +2 -0
- package/dist/linear-session-sync.js +2 -0
- package/dist/run-orchestrator.js +196 -31
- package/dist/service.js +13 -5
- package/dist/waiting-reason.js +8 -2
- package/dist/webhook-handler.js +71 -13
- package/package.json +1 -1
|
@@ -7,6 +7,20 @@ import { execCommand } from "./utils.js";
|
|
|
7
7
|
function isFailingCheckStatus(status) {
|
|
8
8
|
return status === "failed" || status === "failure";
|
|
9
9
|
}
|
|
10
|
+
function isReviewDecisionApproved(value) {
|
|
11
|
+
return value?.trim().toUpperCase() === "APPROVED";
|
|
12
|
+
}
|
|
13
|
+
function isReviewDecisionChangesRequested(value) {
|
|
14
|
+
return value?.trim().toUpperCase() === "CHANGES_REQUESTED";
|
|
15
|
+
}
|
|
16
|
+
function isReviewDecisionReviewRequired(value) {
|
|
17
|
+
return value?.trim().toUpperCase() === "REVIEW_REQUIRED";
|
|
18
|
+
}
|
|
19
|
+
function hasCompletedReviewQuillVerdict(entries) {
|
|
20
|
+
return (entries ?? []).some((entry) => entry.__typename === "CheckRun"
|
|
21
|
+
&& entry.name === "review-quill/verdict"
|
|
22
|
+
&& entry.status === "COMPLETED");
|
|
23
|
+
}
|
|
10
24
|
function getGateCheckNames(project) {
|
|
11
25
|
const configured = project?.gateChecks?.map((entry) => entry.trim()).filter(Boolean) ?? [];
|
|
12
26
|
return configured.length > 0 ? configured : ["verify"];
|
|
@@ -115,6 +129,11 @@ export class IdleIssueReconciler {
|
|
|
115
129
|
await this.reconcileFromGitHub(issue);
|
|
116
130
|
}
|
|
117
131
|
}
|
|
132
|
+
for (const issue of this.db.listIssues()) {
|
|
133
|
+
if (!this.shouldProbeTerminalIssueFromGitHub(issue))
|
|
134
|
+
continue;
|
|
135
|
+
await this.reconcileFromGitHub(issue);
|
|
136
|
+
}
|
|
118
137
|
for (const issue of this.db.listBlockedDelegatedIssues()) {
|
|
119
138
|
const unresolved = this.db.countUnresolvedBlockers(issue.projectId, issue.linearIssueId);
|
|
120
139
|
if (unresolved === 0) {
|
|
@@ -130,6 +149,15 @@ export class IdleIssueReconciler {
|
|
|
130
149
|
}
|
|
131
150
|
}
|
|
132
151
|
}
|
|
152
|
+
shouldProbeTerminalIssueFromGitHub(issue) {
|
|
153
|
+
if (issue.prNumber === undefined)
|
|
154
|
+
return false;
|
|
155
|
+
if (issue.activeRunId !== undefined)
|
|
156
|
+
return false;
|
|
157
|
+
if (issue.pendingRunType !== undefined)
|
|
158
|
+
return false;
|
|
159
|
+
return issue.factoryState === "escalated" || issue.factoryState === "failed";
|
|
160
|
+
}
|
|
133
161
|
advanceIdleIssue(issue, newState, options) {
|
|
134
162
|
if (issue.factoryState === newState && !options?.pendingRunType && !options?.clearFailureProvenance) {
|
|
135
163
|
return;
|
|
@@ -191,9 +219,9 @@ export class IdleIssueReconciler {
|
|
|
191
219
|
eventType = "settled_red_ci";
|
|
192
220
|
dedupeKey = `${dedupeScope}:ci_repair:${issue.linearIssueId}:${issue.lastGitHubFailureSignature ?? issue.prHeadSha ?? issue.lastGitHubFailureHeadSha ?? "unknown"}`;
|
|
193
221
|
}
|
|
194
|
-
else if (runType === "review_fix") {
|
|
222
|
+
else if (runType === "review_fix" || runType === "branch_upkeep") {
|
|
195
223
|
eventType = "review_changes_requested";
|
|
196
|
-
dedupeKey = `${dedupeScope}
|
|
224
|
+
dedupeKey = `${dedupeScope}:${runType}:${issue.linearIssueId}:${issue.prHeadSha ?? "unknown"}`;
|
|
197
225
|
}
|
|
198
226
|
else {
|
|
199
227
|
eventType = "delegated";
|
|
@@ -363,6 +391,7 @@ export class IdleIssueReconciler {
|
|
|
363
391
|
"--json", "headRefOid,state,reviewDecision,mergeable,mergeStateStatus,statusCheckRollup",
|
|
364
392
|
], { timeoutMs: 10_000 });
|
|
365
393
|
const pr = JSON.parse(stdout);
|
|
394
|
+
const previousHeadSha = issue.prHeadSha;
|
|
366
395
|
const gateCheckNames = getGateCheckNames(project);
|
|
367
396
|
const gateCheckStatus = deriveGateCheckStatusFromRollup(pr.statusCheckRollup, gateCheckNames);
|
|
368
397
|
this.db.upsertIssue({
|
|
@@ -370,11 +399,13 @@ export class IdleIssueReconciler {
|
|
|
370
399
|
linearIssueId: issue.linearIssueId,
|
|
371
400
|
...(pr.headRefOid ? { prHeadSha: pr.headRefOid } : {}),
|
|
372
401
|
...(pr.state === "OPEN" ? { prState: "open" } : {}),
|
|
373
|
-
...(pr.reviewDecision
|
|
402
|
+
...(isReviewDecisionApproved(pr.reviewDecision)
|
|
374
403
|
? { prReviewState: "approved" }
|
|
375
|
-
: pr.reviewDecision
|
|
404
|
+
: isReviewDecisionChangesRequested(pr.reviewDecision)
|
|
376
405
|
? { prReviewState: "changes_requested" }
|
|
377
|
-
:
|
|
406
|
+
: isReviewDecisionReviewRequired(pr.reviewDecision)
|
|
407
|
+
? { prReviewState: "commented" }
|
|
408
|
+
: {}),
|
|
378
409
|
...(gateCheckStatus ? { prCheckStatus: gateCheckStatus } : {}),
|
|
379
410
|
...(pr.headRefOid && gateCheckStatus
|
|
380
411
|
? {
|
|
@@ -399,6 +430,39 @@ export class IdleIssueReconciler {
|
|
|
399
430
|
});
|
|
400
431
|
return;
|
|
401
432
|
}
|
|
433
|
+
const headAdvanced = Boolean(pr.headRefOid && pr.headRefOid !== previousHeadSha);
|
|
434
|
+
if (issue.factoryState !== "awaiting_input") {
|
|
435
|
+
const terminalRecoveryState = this.deriveTerminalRecoveryState(issue, pr.reviewDecision, gateCheckStatus, headAdvanced);
|
|
436
|
+
if (terminalRecoveryState) {
|
|
437
|
+
this.logger.info({
|
|
438
|
+
issueKey: issue.issueKey,
|
|
439
|
+
prNumber: issue.prNumber,
|
|
440
|
+
from: issue.factoryState,
|
|
441
|
+
to: terminalRecoveryState,
|
|
442
|
+
gateCheckStatus,
|
|
443
|
+
reviewDecision: pr.reviewDecision,
|
|
444
|
+
headAdvanced,
|
|
445
|
+
}, "Reconciliation: recovered terminal issue from newer GitHub truth");
|
|
446
|
+
this.advanceIdleIssue(issue, terminalRecoveryState, { clearFailureProvenance: true });
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
if (isReviewDecisionReviewRequired(pr.reviewDecision)
|
|
451
|
+
&& gateCheckStatus === "success"
|
|
452
|
+
&& hasCompletedReviewQuillVerdict(pr.statusCheckRollup)) {
|
|
453
|
+
this.logger.warn({ issueKey: issue.issueKey, prNumber: issue.prNumber, reviewDecision: pr.reviewDecision }, "Reconciliation: review-quill completed without a decisive GitHub review; escalating for operator input");
|
|
454
|
+
this.advanceIdleIssue(issue, "awaiting_input");
|
|
455
|
+
this.feed?.publish({
|
|
456
|
+
level: "warn",
|
|
457
|
+
kind: "github",
|
|
458
|
+
issueKey: issue.issueKey,
|
|
459
|
+
projectId: issue.projectId,
|
|
460
|
+
stage: "awaiting_input",
|
|
461
|
+
status: "non_decisive_review",
|
|
462
|
+
summary: `PR #${issue.prNumber} needs operator input: review-quill finished but GitHub still requires review`,
|
|
463
|
+
});
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
402
466
|
const downstreamOwned = issue.factoryState === "awaiting_queue" || issue.prReviewState === "approved" || pr.reviewDecision === "APPROVED";
|
|
403
467
|
const mergeConflictDetected = pr.mergeable === "CONFLICTING" || pr.mergeStateStatus === "DIRTY";
|
|
404
468
|
const refreshedIssue = this.db.getIssue(issue.projectId, issue.linearIssueId) ?? issue;
|
|
@@ -432,7 +496,7 @@ export class IdleIssueReconciler {
|
|
|
432
496
|
});
|
|
433
497
|
return;
|
|
434
498
|
}
|
|
435
|
-
if (pr.reviewDecision
|
|
499
|
+
if (isReviewDecisionApproved(pr.reviewDecision)) {
|
|
436
500
|
this.db.upsertIssue({
|
|
437
501
|
projectId: issue.projectId,
|
|
438
502
|
linearIssueId: issue.linearIssueId,
|
|
@@ -460,4 +524,22 @@ export class IdleIssueReconciler {
|
|
|
460
524
|
}
|
|
461
525
|
}
|
|
462
526
|
}
|
|
527
|
+
deriveTerminalRecoveryState(issue, reviewDecision, gateCheckStatus, headAdvanced) {
|
|
528
|
+
if (issue.factoryState !== "escalated" && issue.factoryState !== "failed") {
|
|
529
|
+
return undefined;
|
|
530
|
+
}
|
|
531
|
+
if (isReviewDecisionApproved(reviewDecision) && !isFailingCheckStatus(gateCheckStatus)) {
|
|
532
|
+
return "awaiting_queue";
|
|
533
|
+
}
|
|
534
|
+
if (gateCheckStatus === "pending") {
|
|
535
|
+
return "pr_open";
|
|
536
|
+
}
|
|
537
|
+
if (headAdvanced && !isFailingCheckStatus(gateCheckStatus)) {
|
|
538
|
+
return "pr_open";
|
|
539
|
+
}
|
|
540
|
+
if (isReviewDecisionReviewRequired(reviewDecision) && !isFailingCheckStatus(gateCheckStatus)) {
|
|
541
|
+
return "pr_open";
|
|
542
|
+
}
|
|
543
|
+
return undefined;
|
|
544
|
+
}
|
|
463
545
|
}
|
|
@@ -143,8 +143,10 @@ export class IssueQueryService {
|
|
|
143
143
|
factoryState: issueRecord?.factoryState ?? "delegated",
|
|
144
144
|
pendingRunType: issueRecord?.pendingRunType,
|
|
145
145
|
prNumber: session.prNumber,
|
|
146
|
+
prHeadSha: issueRecord?.prHeadSha ?? session.prHeadSha,
|
|
146
147
|
prReviewState: issueRecord?.prReviewState,
|
|
147
148
|
prCheckStatus: issueRecord?.prCheckStatus,
|
|
149
|
+
lastBlockingReviewHeadSha: issueRecord?.lastBlockingReviewHeadSha,
|
|
148
150
|
latestFailureCheckName: issueRecord?.lastGitHubFailureCheckName,
|
|
149
151
|
});
|
|
150
152
|
const issue = {
|
|
@@ -33,8 +33,8 @@ export function deriveSessionWakePlan(issue, events) {
|
|
|
33
33
|
break;
|
|
34
34
|
case "review_changes_requested":
|
|
35
35
|
if (runType !== "queue_repair" && runType !== "ci_repair") {
|
|
36
|
-
runType = "review_fix";
|
|
37
|
-
wakeReason = "review_changes_requested";
|
|
36
|
+
runType = payload?.branchUpkeepRequired === true ? "branch_upkeep" : "review_fix";
|
|
37
|
+
wakeReason = payload?.branchUpkeepRequired === true ? "branch_upkeep" : "review_changes_requested";
|
|
38
38
|
Object.assign(context, payload ?? {});
|
|
39
39
|
}
|
|
40
40
|
break;
|
package/dist/issue-session.js
CHANGED
|
@@ -18,6 +18,8 @@ export function deriveIssueSessionWakeReason(params) {
|
|
|
18
18
|
return "delegated";
|
|
19
19
|
if (params.pendingRunType === "review_fix")
|
|
20
20
|
return "review_changes_requested";
|
|
21
|
+
if (params.pendingRunType === "branch_upkeep")
|
|
22
|
+
return "branch_upkeep";
|
|
21
23
|
if (params.pendingRunType === "ci_repair")
|
|
22
24
|
return "settled_red_ci";
|
|
23
25
|
if (params.pendingRunType === "queue_repair")
|
|
@@ -49,6 +49,8 @@ export function buildRunStartedActivity(runType) {
|
|
|
49
49
|
switch (runType) {
|
|
50
50
|
case "review_fix":
|
|
51
51
|
return { type: "action", action: "Addressing", parameter: "review feedback" };
|
|
52
|
+
case "branch_upkeep":
|
|
53
|
+
return { type: "action", action: "Repairing", parameter: "PR branch upkeep after requested changes" };
|
|
52
54
|
case "ci_repair":
|
|
53
55
|
return { type: "action", action: "Repairing", parameter: "failing CI checks" };
|
|
54
56
|
case "queue_repair":
|
|
@@ -241,8 +241,10 @@ function renderStatusComment(db, issue, trackedIssue, options) {
|
|
|
241
241
|
factoryState: issue.factoryState,
|
|
242
242
|
pendingRunType: issue.pendingRunType,
|
|
243
243
|
...(issue.prNumber !== undefined ? { prNumber: issue.prNumber } : {}),
|
|
244
|
+
prHeadSha: issue.prHeadSha,
|
|
244
245
|
prReviewState: issue.prReviewState,
|
|
245
246
|
prCheckStatus: issue.prCheckStatus,
|
|
247
|
+
lastBlockingReviewHeadSha: issue.lastBlockingReviewHeadSha,
|
|
246
248
|
latestFailureCheckName: issue.lastGitHubFailureCheckName,
|
|
247
249
|
});
|
|
248
250
|
const lines = [
|