patchrelay 0.50.5 → 0.51.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/agent-session-plan.js +17 -4
- package/dist/build-info.json +3 -3
- package/dist/cli/watch/StateHistoryView.js +1 -0
- package/dist/cli/watch/event-log-rows.js +1 -0
- package/dist/cli/watch/history-builder.js +1 -0
- package/dist/cli/watch/issue-token.js +15 -4
- package/dist/cli/watch/state-visualization.js +23 -3
- package/dist/config.js +5 -0
- package/dist/delegation-linked-pr.js +2 -2
- package/dist/idle-reconciliation.js +33 -0
- package/dist/implementation-outcome-policy.js +48 -5
- package/dist/issue-session-events.js +6 -9
- package/dist/issue-session.js +2 -0
- package/dist/linear-client.js +29 -0
- package/dist/linear-session-reporting.js +14 -0
- package/dist/linear-status-comment-sync.js +21 -1
- package/dist/main-branch-health-monitor.js +163 -0
- package/dist/main-repair.js +47 -0
- package/dist/merge-queue-protocol.js +2 -0
- package/dist/patchrelay-customization.js +1 -0
- package/dist/pr-display-context.js +21 -0
- package/dist/prompting/patchrelay.js +34 -3
- package/dist/run-launcher.js +5 -4
- package/dist/run-orchestrator.js +4 -0
- package/dist/run-wake-planner.js +4 -0
- package/package.json +1 -1
|
@@ -51,6 +51,14 @@ function ciRepairPlan(attempt) {
|
|
|
51
51
|
{ content: "Merge", status: "pending" },
|
|
52
52
|
];
|
|
53
53
|
}
|
|
54
|
+
function mainRepairPlan(attempt) {
|
|
55
|
+
return [
|
|
56
|
+
{ content: "Inspect main failure", status: "pending" },
|
|
57
|
+
{ content: `Repairing main (${attemptLabel(attempt)})`, status: "pending" },
|
|
58
|
+
{ content: "Awaiting re-verification", status: "pending" },
|
|
59
|
+
{ content: "Priority merge", status: "pending" },
|
|
60
|
+
];
|
|
61
|
+
}
|
|
54
62
|
function queueRepairPlan(attempt) {
|
|
55
63
|
return [
|
|
56
64
|
{ content: "Prepare workspace", status: "completed" },
|
|
@@ -140,7 +148,9 @@ export function buildAgentSessionPlan(params) {
|
|
|
140
148
|
case "delegated":
|
|
141
149
|
return setStatuses(planForRunType(runType, params), ["inProgress", "pending", "pending", "pending"]);
|
|
142
150
|
case "implementing":
|
|
143
|
-
return setStatuses(
|
|
151
|
+
return setStatuses(params.activeRunType === "main_repair" || params.pendingRunType === "main_repair"
|
|
152
|
+
? mainRepairPlan(params.ciRepairAttempts ?? 1)
|
|
153
|
+
: planForRunType("implementation", params), ["completed", "inProgress", "pending", "pending"]);
|
|
144
154
|
case "pr_open":
|
|
145
155
|
return setStatuses(implementationPlan(), ["completed", "completed", "inProgress", "pending"]);
|
|
146
156
|
case "changes_requested":
|
|
@@ -173,6 +183,8 @@ export function buildAgentSessionPlan(params) {
|
|
|
173
183
|
}
|
|
174
184
|
function planForRunType(runType, params) {
|
|
175
185
|
switch (runType) {
|
|
186
|
+
case "main_repair":
|
|
187
|
+
return mainRepairPlan(params.ciRepairAttempts ?? 1);
|
|
176
188
|
case "review_fix":
|
|
177
189
|
return reviewFixPlan();
|
|
178
190
|
case "branch_upkeep":
|
|
@@ -200,9 +212,10 @@ export function buildAgentSessionPlanForIssue(issue, options) {
|
|
|
200
212
|
export function buildRunningSessionPlan(runType) {
|
|
201
213
|
return buildAgentSessionPlan({
|
|
202
214
|
factoryState: runType === "ci_repair" ? "repairing_ci"
|
|
203
|
-
: runType === "
|
|
204
|
-
: runType === "
|
|
205
|
-
: "
|
|
215
|
+
: runType === "main_repair" ? "implementing"
|
|
216
|
+
: runType === "review_fix" || runType === "branch_upkeep" ? "changes_requested"
|
|
217
|
+
: runType === "queue_repair" ? "repairing_queue"
|
|
218
|
+
: "implementing",
|
|
206
219
|
activeRunType: runType,
|
|
207
220
|
});
|
|
208
221
|
}
|
package/dist/build-info.json
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
const SIDE_TRIP_STATES = new Set(["changes_requested", "repairing_ci", "repairing_queue"]);
|
|
3
3
|
const RUN_TYPE_TO_STATE = {
|
|
4
4
|
implementation: "implementing",
|
|
5
|
+
main_repair: "implementing",
|
|
5
6
|
ci_repair: "repairing_ci",
|
|
6
7
|
review_fix: "changes_requested",
|
|
7
8
|
branch_upkeep: "changes_requested",
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { isUndelegatedPausedIssue } from "../../paused-issue-state.js";
|
|
2
|
+
import { derivePrDisplayContext } from "../../pr-display-context.js";
|
|
2
3
|
import { hasFailedPrChecks, hasPendingPrChecks, isApprovedReviewState, isAwaitingReviewState, isChangesRequestedReviewState, prChecksFact, } from "./pr-status.js";
|
|
3
4
|
const GLYPH = {
|
|
4
5
|
running: "\u25cf",
|
|
@@ -89,9 +90,14 @@ export function prTokenFor(issue) {
|
|
|
89
90
|
};
|
|
90
91
|
}
|
|
91
92
|
function prKind(issue) {
|
|
92
|
-
|
|
93
|
+
const prContext = derivePrDisplayContext(issue);
|
|
94
|
+
if (prContext.kind === "merged_pr")
|
|
93
95
|
return "approved";
|
|
94
|
-
if (
|
|
96
|
+
if (prContext.kind === "closed_replacement_pending")
|
|
97
|
+
return "queued";
|
|
98
|
+
if (prContext.kind === "closed_pr_paused")
|
|
99
|
+
return "attention";
|
|
100
|
+
if (prContext.kind === "closed_historical_pr")
|
|
95
101
|
return "declined";
|
|
96
102
|
if (issue.prReviewState === "approved")
|
|
97
103
|
return "approved";
|
|
@@ -104,9 +110,14 @@ function prKind(issue) {
|
|
|
104
110
|
return "running";
|
|
105
111
|
}
|
|
106
112
|
function prPhraseFor(issue) {
|
|
107
|
-
|
|
113
|
+
const prContext = derivePrDisplayContext(issue);
|
|
114
|
+
if (prContext.kind === "merged_pr")
|
|
108
115
|
return "merged";
|
|
109
|
-
if (
|
|
116
|
+
if (prContext.kind === "closed_replacement_pending")
|
|
117
|
+
return "replace pr";
|
|
118
|
+
if (prContext.kind === "closed_pr_paused")
|
|
119
|
+
return "redelegate";
|
|
120
|
+
if (prContext.kind === "closed_historical_pr")
|
|
110
121
|
return "closed";
|
|
111
122
|
if (isChangesRequestedReviewState(issue.prReviewState))
|
|
112
123
|
return "changes req";
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { hasOpenPr } from "../../pr-state.js";
|
|
2
|
+
import { derivePrDisplayContext } from "../../pr-display-context.js";
|
|
2
3
|
const STATE_LABELS = {
|
|
3
4
|
delegated: "delegated",
|
|
4
5
|
implementing: "implementing",
|
|
@@ -164,9 +165,28 @@ export function buildPatchRelayQueueObservations(issue, feedEvents) {
|
|
|
164
165
|
});
|
|
165
166
|
}
|
|
166
167
|
if (issue.prNumber !== undefined) {
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
|
|
168
|
+
const prContext = derivePrDisplayContext(issue);
|
|
169
|
+
let prLabel;
|
|
170
|
+
switch (prContext.kind) {
|
|
171
|
+
case "active_pr":
|
|
172
|
+
case "merged_pr":
|
|
173
|
+
prLabel = hasOpenPr(issue.prNumber, issue.prState)
|
|
174
|
+
? `Tracked PR: #${issue.prNumber}`
|
|
175
|
+
: `Tracked PR: #${issue.prNumber}${issue.prState ? ` (${issue.prState})` : ""}`;
|
|
176
|
+
break;
|
|
177
|
+
case "closed_historical_pr":
|
|
178
|
+
prLabel = `Previous PR: #${prContext.prNumber} (closed)`;
|
|
179
|
+
break;
|
|
180
|
+
case "closed_replacement_pending":
|
|
181
|
+
prLabel = `Previous PR: #${prContext.prNumber} (closed; replacement pending)`;
|
|
182
|
+
break;
|
|
183
|
+
case "closed_pr_paused":
|
|
184
|
+
prLabel = `Previous PR: #${prContext.prNumber} (closed; redelegate to replace)`;
|
|
185
|
+
break;
|
|
186
|
+
case "no_pr":
|
|
187
|
+
prLabel = "PR context unavailable";
|
|
188
|
+
break;
|
|
189
|
+
}
|
|
170
190
|
observations.push({
|
|
171
191
|
tone: "info",
|
|
172
192
|
text: `${prLabel}${issue.prReviewState ? ` (${issue.prReviewState})` : ""}`,
|
package/dist/config.js
CHANGED
|
@@ -66,6 +66,7 @@ const projectSchema = z.object({
|
|
|
66
66
|
webhook_secret: z.string().min(1).optional(),
|
|
67
67
|
repo_full_name: z.string().min(1).optional(),
|
|
68
68
|
base_branch: z.string().min(1).optional(),
|
|
69
|
+
priority_queue_label: z.string().min(1).optional(),
|
|
69
70
|
}).optional(),
|
|
70
71
|
});
|
|
71
72
|
const repositorySchema = z.object({
|
|
@@ -89,6 +90,7 @@ const repositorySchema = z.object({
|
|
|
89
90
|
base_branch: z.string().min(1).optional(),
|
|
90
91
|
merge_queue_label: z.string().min(1).optional(),
|
|
91
92
|
merge_queue_check_name: z.string().min(1).optional(),
|
|
93
|
+
priority_queue_label: z.string().min(1).optional(),
|
|
92
94
|
}).optional(),
|
|
93
95
|
});
|
|
94
96
|
const promptLayerSchema = z.object({
|
|
@@ -97,6 +99,7 @@ const promptLayerSchema = z.object({
|
|
|
97
99
|
});
|
|
98
100
|
const promptByRunTypeSchema = z.object({
|
|
99
101
|
implementation: promptLayerSchema.optional(),
|
|
102
|
+
main_repair: promptLayerSchema.optional(),
|
|
100
103
|
review_fix: promptLayerSchema.optional(),
|
|
101
104
|
branch_upkeep: promptLayerSchema.optional(),
|
|
102
105
|
ci_repair: promptLayerSchema.optional(),
|
|
@@ -474,6 +477,7 @@ export function loadConfig(configPath = process.env.PATCHRELAY_CONFIG ?? getDefa
|
|
|
474
477
|
...(repository.github?.webhook_secret ? { webhookSecret: repository.github.webhook_secret } : {}),
|
|
475
478
|
...(repository.github?.merge_queue_label ? { mergeQueueLabel: repository.github.merge_queue_label } : {}),
|
|
476
479
|
...(repository.github?.merge_queue_check_name ? { mergeQueueCheckName: repository.github.merge_queue_check_name } : {}),
|
|
480
|
+
...(repository.github?.priority_queue_label ? { priorityQueueLabel: repository.github.priority_queue_label } : {}),
|
|
477
481
|
},
|
|
478
482
|
};
|
|
479
483
|
});
|
|
@@ -514,6 +518,7 @@ export function loadConfig(configPath = process.env.PATCHRELAY_CONFIG ?? getDefa
|
|
|
514
518
|
...(project.github.webhook_secret ? { webhookSecret: project.github.webhook_secret } : {}),
|
|
515
519
|
...(project.github.repo_full_name ? { repoFullName: project.github.repo_full_name } : {}),
|
|
516
520
|
...(project.github.base_branch ? { baseBranch: project.github.base_branch } : {}),
|
|
521
|
+
...(project.github.priority_queue_label ? { priorityQueueLabel: project.github.priority_queue_label } : {}),
|
|
517
522
|
},
|
|
518
523
|
} : {}),
|
|
519
524
|
};
|
|
@@ -45,8 +45,8 @@ export function deriveLinkedPrAdoptionOutcome(project, prNumber, remote) {
|
|
|
45
45
|
}
|
|
46
46
|
if (prState === "closed") {
|
|
47
47
|
return {
|
|
48
|
-
factoryState: "
|
|
49
|
-
pendingRunType:
|
|
48
|
+
factoryState: "delegated",
|
|
49
|
+
pendingRunType: "implementation",
|
|
50
50
|
issueUpdates: {
|
|
51
51
|
...issueUpdates,
|
|
52
52
|
prIsDraft: false,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isMainRepairIssue } from "./main-repair.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";
|
|
@@ -421,6 +422,35 @@ export class IdleIssueReconciler {
|
|
|
421
422
|
const project = this.config.projects.find((candidate) => candidate.id === issue.projectId);
|
|
422
423
|
return resolveMergeQueueProtocol(project);
|
|
423
424
|
}
|
|
425
|
+
async ensurePriorityQueueLabel(issue, repoFullName) {
|
|
426
|
+
if (!isMainRepairIssue(issue) || !issue.prNumber)
|
|
427
|
+
return;
|
|
428
|
+
const priorityLabel = this.getIssueProtocol(issue).priorityLabel;
|
|
429
|
+
try {
|
|
430
|
+
const { stdout } = await execCommand("gh", [
|
|
431
|
+
"pr", "view", String(issue.prNumber),
|
|
432
|
+
"--repo", repoFullName,
|
|
433
|
+
"--json", "labels",
|
|
434
|
+
], { timeoutMs: 10_000 });
|
|
435
|
+
const payload = JSON.parse(stdout);
|
|
436
|
+
if ((payload.labels ?? []).some((entry) => entry.name === priorityLabel)) {
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
await execCommand("gh", [
|
|
440
|
+
"pr", "edit", String(issue.prNumber),
|
|
441
|
+
"--repo", repoFullName,
|
|
442
|
+
"--add-label", priorityLabel,
|
|
443
|
+
], { timeoutMs: 10_000 });
|
|
444
|
+
}
|
|
445
|
+
catch (error) {
|
|
446
|
+
this.logger.warn({
|
|
447
|
+
issueKey: issue.issueKey,
|
|
448
|
+
prNumber: issue.prNumber,
|
|
449
|
+
priorityLabel,
|
|
450
|
+
error: error instanceof Error ? error.message : String(error),
|
|
451
|
+
}, "Reconciliation: failed to enforce priority queue label");
|
|
452
|
+
}
|
|
453
|
+
}
|
|
424
454
|
async reconcileFromGitHub(issue) {
|
|
425
455
|
const project = this.config.projects.find((p) => p.id === issue.projectId);
|
|
426
456
|
if (!project?.github?.repoFullName || !issue.prNumber)
|
|
@@ -435,6 +465,9 @@ export class IdleIssueReconciler {
|
|
|
435
465
|
const previousHeadSha = issue.prHeadSha;
|
|
436
466
|
const gateCheckNames = getGateCheckNames(project);
|
|
437
467
|
const gateCheckStatus = deriveGateCheckStatusFromRollup(pr.statusCheckRollup, gateCheckNames);
|
|
468
|
+
if (pr.state === "OPEN") {
|
|
469
|
+
await this.ensurePriorityQueueLabel(issue, project.github.repoFullName);
|
|
470
|
+
}
|
|
438
471
|
this.db.issues.upsertIssue({
|
|
439
472
|
projectId: issue.projectId,
|
|
440
473
|
linearIssueId: issue.linearIssueId,
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { isMainRepairIssue } from "./main-repair.js";
|
|
2
|
+
import { resolveMergeQueueProtocol } from "./merge-queue-protocol.js";
|
|
1
3
|
import { execCommand } from "./utils.js";
|
|
2
4
|
export class ImplementationOutcomePolicy {
|
|
3
5
|
config;
|
|
@@ -11,12 +13,12 @@ export class ImplementationOutcomePolicy {
|
|
|
11
13
|
this.withHeldLease = withHeldLease;
|
|
12
14
|
}
|
|
13
15
|
async verifyPublishedRunOutcome(run, issue) {
|
|
14
|
-
if (run.runType !== "implementation") {
|
|
16
|
+
if (run.runType !== "implementation" && run.runType !== "main_repair") {
|
|
15
17
|
return undefined;
|
|
16
18
|
}
|
|
17
19
|
const project = this.config.projects.find((entry) => entry.id === run.projectId);
|
|
18
20
|
const baseBranch = project?.github?.baseBranch ?? "main";
|
|
19
|
-
const publishedPrState = await this.detectPublishedPrState(issue, project?.github?.repoFullName);
|
|
21
|
+
const publishedPrState = await this.detectPublishedPrState(run, issue, project?.github?.repoFullName);
|
|
20
22
|
if (publishedPrState === "open") {
|
|
21
23
|
return undefined;
|
|
22
24
|
}
|
|
@@ -24,18 +26,18 @@ export class ImplementationOutcomePolicy {
|
|
|
24
26
|
return details ?? `Implementation completed without opening a PR for branch ${issue.branchName ?? issue.linearIssueId}`;
|
|
25
27
|
}
|
|
26
28
|
async detectRecoverableFailedImplementationOutcome(run, issue) {
|
|
27
|
-
if (run.runType !== "implementation") {
|
|
29
|
+
if (run.runType !== "implementation" && run.runType !== "main_repair") {
|
|
28
30
|
return undefined;
|
|
29
31
|
}
|
|
30
32
|
const project = this.config.projects.find((entry) => entry.id === run.projectId);
|
|
31
|
-
const publishedPrState = await this.detectPublishedPrState(issue, project?.github?.repoFullName);
|
|
33
|
+
const publishedPrState = await this.detectPublishedPrState(run, issue, project?.github?.repoFullName);
|
|
32
34
|
if (publishedPrState === "open" || publishedPrState === "unknown") {
|
|
33
35
|
return undefined;
|
|
34
36
|
}
|
|
35
37
|
const baseBranch = project?.github?.baseBranch ?? "main";
|
|
36
38
|
return await this.describeLocalImplementationOutcome(issue, baseBranch);
|
|
37
39
|
}
|
|
38
|
-
async detectPublishedPrState(issue, repoFullName) {
|
|
40
|
+
async detectPublishedPrState(run, issue, repoFullName) {
|
|
39
41
|
if (issue.prNumber && issue.prState && issue.prState !== "closed") {
|
|
40
42
|
return "open";
|
|
41
43
|
}
|
|
@@ -73,6 +75,9 @@ export class ImplementationOutcomePolicy {
|
|
|
73
75
|
...(pr.headRefOid ? { prHeadSha: pr.headRefOid } : {}),
|
|
74
76
|
...(pr.author?.login ? { prAuthorLogin: pr.author.login } : {}),
|
|
75
77
|
}, "published PR verification refresh");
|
|
78
|
+
if (state !== "closed" && isMainRepairIssue(issue)) {
|
|
79
|
+
await this.ensurePriorityQueueLabel(run.projectId, pr.number, repoFullName);
|
|
80
|
+
}
|
|
76
81
|
return state === "closed" ? "closed" : "open";
|
|
77
82
|
}
|
|
78
83
|
catch (error) {
|
|
@@ -133,4 +138,42 @@ export class ImplementationOutcomePolicy {
|
|
|
133
138
|
}
|
|
134
139
|
return undefined;
|
|
135
140
|
}
|
|
141
|
+
async ensurePriorityQueueLabel(projectId, prNumber, repoFullName) {
|
|
142
|
+
const project = this.config.projects.find((entry) => entry.id === projectId);
|
|
143
|
+
if (!project || !repoFullName)
|
|
144
|
+
return;
|
|
145
|
+
const priorityLabel = resolveMergeQueueProtocol(project).priorityLabel;
|
|
146
|
+
try {
|
|
147
|
+
const { stdout } = await execCommand("gh", [
|
|
148
|
+
"pr",
|
|
149
|
+
"view",
|
|
150
|
+
String(prNumber),
|
|
151
|
+
"--repo",
|
|
152
|
+
repoFullName,
|
|
153
|
+
"--json",
|
|
154
|
+
"labels",
|
|
155
|
+
], { timeoutMs: 10_000 });
|
|
156
|
+
const labels = JSON.parse(stdout);
|
|
157
|
+
const hasLabel = (labels.labels ?? []).some((entry) => entry.name === priorityLabel);
|
|
158
|
+
if (hasLabel)
|
|
159
|
+
return;
|
|
160
|
+
await execCommand("gh", [
|
|
161
|
+
"pr",
|
|
162
|
+
"edit",
|
|
163
|
+
String(prNumber),
|
|
164
|
+
"--repo",
|
|
165
|
+
repoFullName,
|
|
166
|
+
"--add-label",
|
|
167
|
+
priorityLabel,
|
|
168
|
+
], { timeoutMs: 10_000 });
|
|
169
|
+
}
|
|
170
|
+
catch (error) {
|
|
171
|
+
this.logger.warn({
|
|
172
|
+
projectId,
|
|
173
|
+
prNumber,
|
|
174
|
+
priorityLabel,
|
|
175
|
+
error: error instanceof Error ? error.message : String(error),
|
|
176
|
+
}, "Failed to enforce priority queue label on main repair PR");
|
|
177
|
+
}
|
|
178
|
+
}
|
|
136
179
|
}
|
|
@@ -11,7 +11,7 @@ const NON_ACTIONABLE_SESSION_EVENTS = new Set([
|
|
|
11
11
|
"delegation_observed",
|
|
12
12
|
"run_released_authority",
|
|
13
13
|
]);
|
|
14
|
-
const RUN_TYPES = new Set(["implementation", "review_fix", "branch_upkeep", "ci_repair", "queue_repair"]);
|
|
14
|
+
const RUN_TYPES = new Set(["implementation", "main_repair", "review_fix", "branch_upkeep", "ci_repair", "queue_repair"]);
|
|
15
15
|
function parseRunType(value) {
|
|
16
16
|
return typeof value === "string" && RUN_TYPES.has(value) ? value : undefined;
|
|
17
17
|
}
|
|
@@ -51,15 +51,12 @@ export function deriveSessionWakePlan(issue, events) {
|
|
|
51
51
|
break;
|
|
52
52
|
case "delegated":
|
|
53
53
|
if (!runType) {
|
|
54
|
-
runType = "implementation";
|
|
55
|
-
wakeReason =
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
context.promptContext = payload.promptContext;
|
|
59
|
-
}
|
|
60
|
-
if (payload?.promptBody !== undefined) {
|
|
61
|
-
context.promptBody = payload.promptBody;
|
|
54
|
+
runType = parseRunType(payload?.runType) ?? "implementation";
|
|
55
|
+
wakeReason = runType === "main_repair"
|
|
56
|
+
? "main_repair"
|
|
57
|
+
: issue.issueClass === "orchestration" ? "initial_delegate" : "delegated";
|
|
62
58
|
}
|
|
59
|
+
Object.assign(context, payload ?? {});
|
|
63
60
|
break;
|
|
64
61
|
case "child_changed":
|
|
65
62
|
case "child_delivered":
|
package/dist/issue-session.js
CHANGED
|
@@ -18,6 +18,8 @@ export function deriveIssueSessionWakeReason(params) {
|
|
|
18
18
|
return undefined;
|
|
19
19
|
if (params.pendingRunType === "implementation")
|
|
20
20
|
return "delegated";
|
|
21
|
+
if (params.pendingRunType === "main_repair")
|
|
22
|
+
return "main_repair";
|
|
21
23
|
if (params.pendingRunType === "review_fix")
|
|
22
24
|
return "review_changes_requested";
|
|
23
25
|
if (params.pendingRunType === "branch_upkeep")
|
package/dist/linear-client.js
CHANGED
|
@@ -109,6 +109,35 @@ export class LinearGraphqlClient {
|
|
|
109
109
|
}
|
|
110
110
|
return this.mapIssue(response.issue);
|
|
111
111
|
}
|
|
112
|
+
async createIssue(params) {
|
|
113
|
+
const response = await this.request(`
|
|
114
|
+
mutation PatchRelayCreateIssue($input: IssueCreateInput!) {
|
|
115
|
+
issueCreate(input: $input) {
|
|
116
|
+
success
|
|
117
|
+
issue {
|
|
118
|
+
${LINEAR_ISSUE_SELECTION}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
`, {
|
|
123
|
+
input: {
|
|
124
|
+
teamId: params.teamId,
|
|
125
|
+
title: params.title,
|
|
126
|
+
...(params.description ? { description: params.description } : {}),
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
if (!response.issueCreate.success || !response.issueCreate.issue) {
|
|
130
|
+
throw new Error(`Linear rejected issue creation for team ${params.teamId}`);
|
|
131
|
+
}
|
|
132
|
+
let issue = this.mapIssue(response.issueCreate.issue);
|
|
133
|
+
if (params.labelNames && params.labelNames.length > 0) {
|
|
134
|
+
issue = await this.updateIssueLabels({
|
|
135
|
+
issueId: issue.id,
|
|
136
|
+
addNames: params.labelNames,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
return issue;
|
|
140
|
+
}
|
|
112
141
|
async setIssueState(issueId, stateName) {
|
|
113
142
|
const issue = await this.getIssue(issueId);
|
|
114
143
|
const state = issue.workflowStates.find((entry) => entry.name.trim().toLowerCase() === stateName.trim().toLowerCase());
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { formatRunTypeLabel } from "./agent-session-plan.js";
|
|
2
2
|
import { sanitizeOperatorFacingText } from "./presentation-text.js";
|
|
3
3
|
import { isClosedPrState } from "./pr-state.js";
|
|
4
|
+
import { derivePrDisplayContext } from "./pr-display-context.js";
|
|
4
5
|
function lowerRunTypeLabel(runType) {
|
|
5
6
|
return formatRunTypeLabel(runType).toLowerCase();
|
|
6
7
|
}
|
|
@@ -187,6 +188,7 @@ export function buildMergePrepEscalationActivity(attempts) {
|
|
|
187
188
|
};
|
|
188
189
|
}
|
|
189
190
|
export function summarizeIssueStateForLinear(issue) {
|
|
191
|
+
const prContext = derivePrDisplayContext(issue);
|
|
190
192
|
switch (issue.sessionState) {
|
|
191
193
|
case "waiting_input":
|
|
192
194
|
return issue.waitingReason ?? (issue.prNumber && !isClosedPrState(issue.prState) ? `PR #${issue.prNumber} is waiting for input.` : "Waiting for input.");
|
|
@@ -208,11 +210,23 @@ export function summarizeIssueStateForLinear(issue) {
|
|
|
208
210
|
}
|
|
209
211
|
switch (issue.factoryState) {
|
|
210
212
|
case "delegated":
|
|
213
|
+
if (prContext.kind === "closed_replacement_pending") {
|
|
214
|
+
return `Queued to replace closed PR #${prContext.prNumber}.`;
|
|
215
|
+
}
|
|
216
|
+
if (prContext.kind === "closed_pr_paused") {
|
|
217
|
+
return `Closed PR #${prContext.prNumber} needs redelegation before replacement.`;
|
|
218
|
+
}
|
|
211
219
|
if (!issue.delegatedToPatchRelay) {
|
|
212
220
|
return "PatchRelay is queued to start work, but automation is paused.";
|
|
213
221
|
}
|
|
214
222
|
return "Queued to start work.";
|
|
215
223
|
case "implementing":
|
|
224
|
+
if (prContext.kind === "closed_replacement_pending") {
|
|
225
|
+
return `Replacing closed PR #${prContext.prNumber} with a fresh PR.`;
|
|
226
|
+
}
|
|
227
|
+
if (prContext.kind === "closed_pr_paused") {
|
|
228
|
+
return `Closed PR #${prContext.prNumber} needs redelegation before replacement.`;
|
|
229
|
+
}
|
|
216
230
|
if (!issue.delegatedToPatchRelay) {
|
|
217
231
|
return "Implementation is paused because the issue is undelegated.";
|
|
218
232
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { extractCompletionCheck } from "./completion-check.js";
|
|
2
2
|
import { isClosedPrState } from "./pr-state.js";
|
|
3
|
+
import { derivePrDisplayContext } from "./pr-display-context.js";
|
|
3
4
|
import { deriveIssueStatusNote } from "./status-note.js";
|
|
4
5
|
import { derivePatchRelayWaitingReason } from "./waiting-reason.js";
|
|
5
6
|
export async function syncVisibleStatusComment(params) {
|
|
@@ -84,8 +85,17 @@ function renderStatusComment(db, issue, trackedIssue, options) {
|
|
|
84
85
|
lines.push("", `Reply in a Linear comment to continue, or run \`patchrelay issue prompt ${issueRef} "..."\`.`);
|
|
85
86
|
}
|
|
86
87
|
if (issue.prNumber !== undefined || issue.prUrl) {
|
|
88
|
+
const prContext = derivePrDisplayContext(issue);
|
|
87
89
|
const prLabel = issue.prNumber !== undefined ? `#${issue.prNumber}` : "open";
|
|
88
|
-
|
|
90
|
+
const linkedLabel = issue.prUrl ? `[${prLabel}](${issue.prUrl})` : prLabel;
|
|
91
|
+
const prLine = prContext.kind === "closed_historical_pr"
|
|
92
|
+
? `Previous PR: ${linkedLabel} (closed)`
|
|
93
|
+
: prContext.kind === "closed_replacement_pending"
|
|
94
|
+
? `Previous PR: ${linkedLabel} (closed; replacement PR needed)`
|
|
95
|
+
: prContext.kind === "closed_pr_paused"
|
|
96
|
+
? `Previous PR: ${linkedLabel} (closed; redelegate to replace it)`
|
|
97
|
+
: `PR: ${linkedLabel}`;
|
|
98
|
+
lines.push("", prLine);
|
|
89
99
|
}
|
|
90
100
|
if (latestRun) {
|
|
91
101
|
lines.push("", `Latest run: ${formatLatestRun(latestRun)}`);
|
|
@@ -103,6 +113,7 @@ function renderStatusComment(db, issue, trackedIssue, options) {
|
|
|
103
113
|
return lines.join("\n");
|
|
104
114
|
}
|
|
105
115
|
function statusHeadline(issue, activeRunType) {
|
|
116
|
+
const prContext = derivePrDisplayContext(issue);
|
|
106
117
|
if (activeRunType) {
|
|
107
118
|
return `Running ${humanize(activeRunType)}`;
|
|
108
119
|
}
|
|
@@ -123,6 +134,9 @@ function statusHeadline(issue, activeRunType) {
|
|
|
123
134
|
break;
|
|
124
135
|
}
|
|
125
136
|
if (!issue.delegatedToPatchRelay && issue.prNumber !== undefined) {
|
|
137
|
+
if (prContext.kind === "closed_pr_paused") {
|
|
138
|
+
return `Closed PR #${prContext.prNumber} is waiting for redelegation before replacement`;
|
|
139
|
+
}
|
|
126
140
|
if (issue.factoryState === "awaiting_queue" || issue.prReviewState === "approved") {
|
|
127
141
|
return `PR #${issue.prNumber} is awaiting downstream merge while PatchRelay is paused`;
|
|
128
142
|
}
|
|
@@ -144,8 +158,14 @@ function statusHeadline(issue, activeRunType) {
|
|
|
144
158
|
}
|
|
145
159
|
switch (issue.factoryState) {
|
|
146
160
|
case "delegated":
|
|
161
|
+
if (prContext.kind === "closed_replacement_pending") {
|
|
162
|
+
return `Queued to replace closed PR #${prContext.prNumber}`;
|
|
163
|
+
}
|
|
147
164
|
return "Queued to start work";
|
|
148
165
|
case "implementing":
|
|
166
|
+
if (prContext.kind === "closed_replacement_pending") {
|
|
167
|
+
return `Replacing closed PR #${prContext.prNumber} with a fresh PR`;
|
|
168
|
+
}
|
|
149
169
|
return "Implementing requested change";
|
|
150
170
|
case "pr_open":
|
|
151
171
|
return issue.prNumber !== undefined ? `PR #${issue.prNumber} opened` : "PR opened";
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { resolveMergeQueueProtocol } from "./merge-queue-protocol.js";
|
|
2
|
+
import { buildMainRepairBranchName, buildMainRepairDescription, buildMainRepairPromptContext, buildMainRepairTitle, isMainRepairIssue, } from "./main-repair.js";
|
|
3
|
+
import { execCommand } from "./utils.js";
|
|
4
|
+
const MAIN_BRANCH_HEALTH_GRACE_MS = 120_000;
|
|
5
|
+
export class MainBranchHealthMonitor {
|
|
6
|
+
db;
|
|
7
|
+
config;
|
|
8
|
+
linearProvider;
|
|
9
|
+
enqueueIssue;
|
|
10
|
+
logger;
|
|
11
|
+
feed;
|
|
12
|
+
constructor(db, config, linearProvider, enqueueIssue, logger, feed) {
|
|
13
|
+
this.db = db;
|
|
14
|
+
this.config = config;
|
|
15
|
+
this.linearProvider = linearProvider;
|
|
16
|
+
this.enqueueIssue = enqueueIssue;
|
|
17
|
+
this.logger = logger;
|
|
18
|
+
this.feed = feed;
|
|
19
|
+
}
|
|
20
|
+
async reconcile() {
|
|
21
|
+
for (const project of this.config.projects) {
|
|
22
|
+
await this.reconcileProject(project.id);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
async reconcileProject(projectId) {
|
|
26
|
+
const project = this.config.projects.find((entry) => entry.id === projectId);
|
|
27
|
+
if (!project?.github?.repoFullName)
|
|
28
|
+
return;
|
|
29
|
+
if (project.linearTeamIds.length === 0)
|
|
30
|
+
return;
|
|
31
|
+
const baseBranch = project.github.baseBranch ?? "main";
|
|
32
|
+
const branchName = buildMainRepairBranchName(baseBranch);
|
|
33
|
+
const existing = this.db.listIssues().find((issue) => (issue.projectId === projectId
|
|
34
|
+
&& issue.branchName === branchName
|
|
35
|
+
&& isMainRepairIssue(issue)
|
|
36
|
+
&& issue.factoryState !== "done"
|
|
37
|
+
&& issue.factoryState !== "failed"
|
|
38
|
+
&& issue.factoryState !== "escalated"));
|
|
39
|
+
if (existing) {
|
|
40
|
+
const age = Date.now() - Date.parse(existing.updatedAt);
|
|
41
|
+
if (age < MAIN_BRANCH_HEALTH_GRACE_MS) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
const summary = await this.readMainBranchFailure(project.github.repoFullName, baseBranch);
|
|
46
|
+
if (!summary) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const protocol = resolveMergeQueueProtocol(project);
|
|
50
|
+
if (existing) {
|
|
51
|
+
this.queueExistingMainRepair(existing, summary, protocol.priorityLabel);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const client = await this.linearProvider.forProject(projectId);
|
|
55
|
+
if (!client?.createIssue) {
|
|
56
|
+
this.logger.warn({ projectId, repoFullName: project.github.repoFullName }, "Cannot create main repair issue because Linear issue creation is unavailable");
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const created = await client.createIssue({
|
|
60
|
+
teamId: project.linearTeamIds[0],
|
|
61
|
+
title: buildMainRepairTitle(project),
|
|
62
|
+
description: buildMainRepairDescription(project, summary, protocol.priorityLabel),
|
|
63
|
+
});
|
|
64
|
+
const issue = this.db.upsertIssue({
|
|
65
|
+
projectId,
|
|
66
|
+
linearIssueId: created.id,
|
|
67
|
+
delegatedToPatchRelay: true,
|
|
68
|
+
...(created.identifier ? { issueKey: created.identifier } : {}),
|
|
69
|
+
...(created.title ? { title: created.title } : {}),
|
|
70
|
+
...(created.description ? { description: created.description } : {}),
|
|
71
|
+
...(created.url ? { url: created.url } : {}),
|
|
72
|
+
...(created.priority != null ? { priority: created.priority } : {}),
|
|
73
|
+
...(created.estimate != null ? { estimate: created.estimate } : {}),
|
|
74
|
+
...(created.stateName ? { currentLinearState: created.stateName } : {}),
|
|
75
|
+
...(created.stateType ? { currentLinearStateType: created.stateType } : {}),
|
|
76
|
+
branchName,
|
|
77
|
+
factoryState: "delegated",
|
|
78
|
+
});
|
|
79
|
+
this.db.issueSessions.appendIssueSessionEventRespectingActiveLease(projectId, issue.linearIssueId, {
|
|
80
|
+
projectId,
|
|
81
|
+
linearIssueId: issue.linearIssueId,
|
|
82
|
+
eventType: "delegated",
|
|
83
|
+
eventJson: JSON.stringify({
|
|
84
|
+
runType: "main_repair",
|
|
85
|
+
baseSha: summary.baseSha,
|
|
86
|
+
failingChecks: summary.failingChecks,
|
|
87
|
+
pendingChecks: summary.pendingChecks,
|
|
88
|
+
priorityLabel: protocol.priorityLabel,
|
|
89
|
+
promptContext: buildMainRepairPromptContext(project, summary, protocol.priorityLabel),
|
|
90
|
+
}),
|
|
91
|
+
dedupeKey: `main_repair:${projectId}:${summary.baseSha}:${summary.failingChecks.map((check) => check.name).join("|")}`,
|
|
92
|
+
});
|
|
93
|
+
if (this.db.issueSessions.peekIssueSessionWake(projectId, issue.linearIssueId)) {
|
|
94
|
+
this.enqueueIssue(projectId, issue.linearIssueId);
|
|
95
|
+
}
|
|
96
|
+
this.feed?.publish({
|
|
97
|
+
level: "warn",
|
|
98
|
+
kind: "github",
|
|
99
|
+
issueKey: issue.issueKey,
|
|
100
|
+
projectId,
|
|
101
|
+
stage: "delegated",
|
|
102
|
+
status: "main_repair_queued",
|
|
103
|
+
summary: `Queued main_repair for ${project.github.repoFullName}@${baseBranch}`,
|
|
104
|
+
detail: summary.failingChecks.map((check) => check.name).join(", "),
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
queueExistingMainRepair(issue, summary, priorityLabel) {
|
|
108
|
+
if (issue.activeRunId !== undefined)
|
|
109
|
+
return;
|
|
110
|
+
if (this.db.issueSessions.hasPendingIssueSessionEvents(issue.projectId, issue.linearIssueId))
|
|
111
|
+
return;
|
|
112
|
+
if (issue.prState === "open" || issue.factoryState === "awaiting_queue" || issue.factoryState === "pr_open")
|
|
113
|
+
return;
|
|
114
|
+
this.db.issueSessions.appendIssueSessionEventRespectingActiveLease(issue.projectId, issue.linearIssueId, {
|
|
115
|
+
projectId: issue.projectId,
|
|
116
|
+
linearIssueId: issue.linearIssueId,
|
|
117
|
+
eventType: "delegated",
|
|
118
|
+
eventJson: JSON.stringify({
|
|
119
|
+
runType: "main_repair",
|
|
120
|
+
baseSha: summary.baseSha,
|
|
121
|
+
failingChecks: summary.failingChecks,
|
|
122
|
+
pendingChecks: summary.pendingChecks,
|
|
123
|
+
priorityLabel,
|
|
124
|
+
}),
|
|
125
|
+
dedupeKey: `main_repair:${issue.projectId}:${summary.baseSha}:${summary.failingChecks.map((check) => check.name).join("|")}`,
|
|
126
|
+
});
|
|
127
|
+
if (this.db.issueSessions.peekIssueSessionWake(issue.projectId, issue.linearIssueId)) {
|
|
128
|
+
this.enqueueIssue(issue.projectId, issue.linearIssueId);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
async readMainBranchFailure(repoFullName, baseBranch) {
|
|
132
|
+
const { stdout: shaOut } = await execCommand("gh", [
|
|
133
|
+
"api",
|
|
134
|
+
`repos/${repoFullName}/branches/${baseBranch}`,
|
|
135
|
+
"--jq",
|
|
136
|
+
".commit.sha",
|
|
137
|
+
], { timeoutMs: 10_000 });
|
|
138
|
+
const baseSha = shaOut.trim();
|
|
139
|
+
if (!baseSha)
|
|
140
|
+
return undefined;
|
|
141
|
+
const { stdout: checksOut } = await execCommand("gh", [
|
|
142
|
+
"api",
|
|
143
|
+
`repos/${repoFullName}/commits/${baseSha}/check-runs`,
|
|
144
|
+
"--jq",
|
|
145
|
+
".check_runs",
|
|
146
|
+
], { timeoutMs: 10_000 });
|
|
147
|
+
const runs = JSON.parse(checksOut || "[]");
|
|
148
|
+
const failingChecks = runs
|
|
149
|
+
.filter((run) => run.status === "completed" && run.conclusion === "failure" && typeof run.name === "string" && run.name.trim())
|
|
150
|
+
.map((run) => ({ name: run.name.trim(), ...(run.details_url ? { url: run.details_url } : {}) }));
|
|
151
|
+
if (failingChecks.length === 0) {
|
|
152
|
+
return undefined;
|
|
153
|
+
}
|
|
154
|
+
const pendingChecks = runs
|
|
155
|
+
.filter((run) => run.status !== "completed" && typeof run.name === "string" && run.name.trim())
|
|
156
|
+
.map((run) => ({ name: run.name.trim(), ...(run.details_url ? { url: run.details_url } : {}) }));
|
|
157
|
+
return {
|
|
158
|
+
baseSha,
|
|
159
|
+
failingChecks,
|
|
160
|
+
pendingChecks,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export const MAIN_REPAIR_BRANCH_PREFIX = "main-repair";
|
|
2
|
+
export function buildMainRepairBranchName(baseBranch) {
|
|
3
|
+
return `${MAIN_REPAIR_BRANCH_PREFIX}/${baseBranch}`;
|
|
4
|
+
}
|
|
5
|
+
export function isMainRepairIssue(issue) {
|
|
6
|
+
return typeof issue.branchName === "string" && issue.branchName.startsWith(`${MAIN_REPAIR_BRANCH_PREFIX}/`);
|
|
7
|
+
}
|
|
8
|
+
export function buildMainRepairTitle(project) {
|
|
9
|
+
const repo = project.github?.repoFullName ?? project.id;
|
|
10
|
+
const baseBranch = project.github?.baseBranch ?? "main";
|
|
11
|
+
return `Repair ${baseBranch} for ${repo}`;
|
|
12
|
+
}
|
|
13
|
+
export function buildMainRepairDescription(project, summary, priorityLabel) {
|
|
14
|
+
const repo = project.github?.repoFullName ?? project.id;
|
|
15
|
+
const baseBranch = project.github?.baseBranch ?? "main";
|
|
16
|
+
const lines = [
|
|
17
|
+
`Automatically created because \`${repo}@${baseBranch}\` is red.`,
|
|
18
|
+
"",
|
|
19
|
+
`Base SHA: \`${summary.baseSha}\``,
|
|
20
|
+
"",
|
|
21
|
+
"Repair the base-branch failure on a PR branch, get the PR green, and keep it in the priority queue lane.",
|
|
22
|
+
`The repair PR must carry the GitHub label \`${priorityLabel}\`.`,
|
|
23
|
+
];
|
|
24
|
+
if (summary.failingChecks.length > 0) {
|
|
25
|
+
lines.push("", "Failing checks:");
|
|
26
|
+
for (const check of summary.failingChecks) {
|
|
27
|
+
lines.push(`- ${check.name}${check.url ? ` — ${check.url}` : ""}`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if (summary.pendingChecks.length > 0) {
|
|
31
|
+
lines.push("", "Pending checks:");
|
|
32
|
+
for (const check of summary.pendingChecks) {
|
|
33
|
+
lines.push(`- ${check.name}${check.url ? ` — ${check.url}` : ""}`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return lines.join("\n");
|
|
37
|
+
}
|
|
38
|
+
export function buildMainRepairPromptContext(project, summary, priorityLabel) {
|
|
39
|
+
const repo = project.github?.repoFullName ?? project.id;
|
|
40
|
+
const baseBranch = project.github?.baseBranch ?? "main";
|
|
41
|
+
const failingNames = summary.failingChecks.map((check) => check.name).join(", ") || "unknown failing checks";
|
|
42
|
+
return [
|
|
43
|
+
`Main repair for ${repo}.`,
|
|
44
|
+
`${baseBranch} is red at ${summary.baseSha}.`,
|
|
45
|
+
`Fix the failing base-branch checks (${failingNames}), publish a PR on this branch, and assign the GitHub label ${priorityLabel}.`,
|
|
46
|
+
].join(" ");
|
|
47
|
+
}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
export const DEFAULT_MERGE_QUEUE_LABEL = "queue";
|
|
2
2
|
export const DEFAULT_MERGE_QUEUE_CHECK_NAME = "merge-steward/queue";
|
|
3
|
+
export const DEFAULT_PRIORITY_QUEUE_LABEL = "queue:priority";
|
|
3
4
|
export function resolveMergeQueueProtocol(project) {
|
|
4
5
|
return {
|
|
5
6
|
repoFullName: project?.github?.repoFullName,
|
|
6
7
|
baseBranch: project?.github?.baseBranch,
|
|
7
8
|
admissionLabel: project?.github?.mergeQueueLabel ?? DEFAULT_MERGE_QUEUE_LABEL,
|
|
8
9
|
evictionCheckName: project?.github?.mergeQueueCheckName ?? DEFAULT_MERGE_QUEUE_CHECK_NAME,
|
|
10
|
+
priorityLabel: project?.github?.priorityQueueLabel ?? DEFAULT_PRIORITY_QUEUE_LABEL,
|
|
9
11
|
};
|
|
10
12
|
}
|
|
@@ -7,6 +7,7 @@ const promptLayerSchema = z.object({
|
|
|
7
7
|
});
|
|
8
8
|
const promptByRunTypeSchema = z.object({
|
|
9
9
|
implementation: promptLayerSchema.optional(),
|
|
10
|
+
main_repair: promptLayerSchema.optional(),
|
|
10
11
|
review_fix: promptLayerSchema.optional(),
|
|
11
12
|
branch_upkeep: promptLayerSchema.optional(),
|
|
12
13
|
ci_repair: promptLayerSchema.optional(),
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
function isTerminalFactoryState(factoryState) {
|
|
2
|
+
return factoryState === "done" || factoryState === "failed" || factoryState === "escalated";
|
|
3
|
+
}
|
|
4
|
+
export function derivePrDisplayContext(issue) {
|
|
5
|
+
if (issue.prNumber === undefined) {
|
|
6
|
+
return { kind: "no_pr" };
|
|
7
|
+
}
|
|
8
|
+
if (issue.prState === "merged") {
|
|
9
|
+
return { kind: "merged_pr", prNumber: issue.prNumber };
|
|
10
|
+
}
|
|
11
|
+
if (issue.prState === "closed") {
|
|
12
|
+
if (isTerminalFactoryState(issue.factoryState)) {
|
|
13
|
+
return { kind: "closed_historical_pr", prNumber: issue.prNumber };
|
|
14
|
+
}
|
|
15
|
+
if (issue.delegatedToPatchRelay === false) {
|
|
16
|
+
return { kind: "closed_pr_paused", prNumber: issue.prNumber };
|
|
17
|
+
}
|
|
18
|
+
return { kind: "closed_replacement_pending", prNumber: issue.prNumber };
|
|
19
|
+
}
|
|
20
|
+
return { kind: "active_pr", prNumber: issue.prNumber };
|
|
21
|
+
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { derivePrDisplayContext } from "../pr-display-context.js";
|
|
3
4
|
const WORKFLOW_FILES = {
|
|
4
5
|
implementation: "IMPLEMENTATION_WORKFLOW.md",
|
|
6
|
+
main_repair: "IMPLEMENTATION_WORKFLOW.md",
|
|
5
7
|
review_fix: "REVIEW_WORKFLOW.md",
|
|
6
8
|
branch_upkeep: "REVIEW_WORKFLOW.md",
|
|
7
9
|
ci_repair: "IMPLEMENTATION_WORKFLOW.md",
|
|
@@ -28,11 +30,23 @@ function hasWorkflowFile(repoPath, runType) {
|
|
|
28
30
|
return existsSync(filePath);
|
|
29
31
|
}
|
|
30
32
|
function buildPromptHeader(issue) {
|
|
33
|
+
const prContext = derivePrDisplayContext(issue);
|
|
34
|
+
const prLine = prContext.kind === "active_pr"
|
|
35
|
+
? `PR: #${prContext.prNumber}`
|
|
36
|
+
: prContext.kind === "merged_pr"
|
|
37
|
+
? `Merged PR: #${prContext.prNumber}`
|
|
38
|
+
: prContext.kind === "closed_historical_pr"
|
|
39
|
+
? `Previous PR: #${prContext.prNumber} (closed)`
|
|
40
|
+
: prContext.kind === "closed_replacement_pending"
|
|
41
|
+
? `Previous PR: #${prContext.prNumber} (closed; replacement PR needed)`
|
|
42
|
+
: prContext.kind === "closed_pr_paused"
|
|
43
|
+
? `Previous PR: #${prContext.prNumber} (closed; redelegate to replace it)`
|
|
44
|
+
: undefined;
|
|
31
45
|
return [
|
|
32
46
|
`Issue: ${issue.issueKey ?? issue.linearIssueId}`,
|
|
33
47
|
issue.title ? `Title: ${issue.title}` : undefined,
|
|
34
48
|
issue.branchName ? `Branch: ${issue.branchName}` : undefined,
|
|
35
|
-
|
|
49
|
+
prLine,
|
|
36
50
|
].filter(Boolean).join("\n");
|
|
37
51
|
}
|
|
38
52
|
function extractIssueSection(description, heading) {
|
|
@@ -354,6 +368,7 @@ function buildQueueRepairContext(context) {
|
|
|
354
368
|
return lines.filter(Boolean).join("\n");
|
|
355
369
|
}
|
|
356
370
|
function buildFollowUpContextLines(issue, runType, context) {
|
|
371
|
+
const prContext = derivePrDisplayContext(issue);
|
|
357
372
|
const wakeReason = typeof context?.wakeReason === "string" ? context.wakeReason : undefined;
|
|
358
373
|
const followUps = Array.isArray(context?.followUps) ? context.followUps : [];
|
|
359
374
|
const followUpLines = followUps
|
|
@@ -389,9 +404,25 @@ function buildFollowUpContextLines(issue, runType, context) {
|
|
|
389
404
|
followUpLines.forEach((line) => lines.push(`- ${line}`));
|
|
390
405
|
}
|
|
391
406
|
if (issue.prNumber || issue.prHeadSha || issue.prReviewState || context?.mergeStateStatus) {
|
|
392
|
-
|
|
407
|
+
const prHeading = prContext.kind === "closed_historical_pr"
|
|
408
|
+
|| prContext.kind === "closed_replacement_pending"
|
|
409
|
+
|| prContext.kind === "closed_pr_paused"
|
|
410
|
+
? "Previous PR facts:"
|
|
411
|
+
: "Current PR facts:";
|
|
412
|
+
const prLine = prContext.kind === "active_pr"
|
|
413
|
+
? `Current PR: #${prContext.prNumber}`
|
|
414
|
+
: prContext.kind === "merged_pr"
|
|
415
|
+
? `Merged PR: #${prContext.prNumber}`
|
|
416
|
+
: prContext.kind === "closed_historical_pr"
|
|
417
|
+
? `Previous PR: #${prContext.prNumber} (closed)`
|
|
418
|
+
: prContext.kind === "closed_replacement_pending"
|
|
419
|
+
? `Previous PR: #${prContext.prNumber} (closed; replacement PR needed)`
|
|
420
|
+
: prContext.kind === "closed_pr_paused"
|
|
421
|
+
? `Previous PR: #${prContext.prNumber} (closed; redelegate to replace it)`
|
|
422
|
+
: "";
|
|
423
|
+
lines.push("", prHeading, `Fact freshness: ${context?.githubFactsFresh === true
|
|
393
424
|
? "refreshed immediately before this turn was created."
|
|
394
|
-
: "may now be stale; refresh before making irreversible decisions."}`,
|
|
425
|
+
: "may now be stale; refresh before making irreversible decisions."}`, prLine, issue.prHeadSha ? `Current relevant head SHA: ${issue.prHeadSha}` : "", issue.prReviewState ? `Current review state: ${issue.prReviewState}` : "", typeof context?.mergeStateStatus === "string" ? `Merge state against ${String(context?.baseBranch ?? "main")}: ${String(context.mergeStateStatus)}` : "");
|
|
395
426
|
}
|
|
396
427
|
return lines.filter(Boolean);
|
|
397
428
|
}
|
package/dist/run-launcher.js
CHANGED
|
@@ -93,10 +93,11 @@ export class RunLauncher {
|
|
|
93
93
|
branchName: params.branchName,
|
|
94
94
|
worktreePath: params.worktreePath,
|
|
95
95
|
factoryState: params.runType === "implementation" ? "implementing"
|
|
96
|
-
: params.runType === "
|
|
97
|
-
: params.runType === "
|
|
98
|
-
: params.runType === "
|
|
99
|
-
: "
|
|
96
|
+
: params.runType === "main_repair" ? "implementing"
|
|
97
|
+
: params.runType === "ci_repair" ? "repairing_ci"
|
|
98
|
+
: params.runType === "review_fix" || params.runType === "branch_upkeep" ? "changes_requested"
|
|
99
|
+
: params.runType === "queue_repair" ? "repairing_queue"
|
|
100
|
+
: "implementing",
|
|
100
101
|
...((params.runType === "ci_repair" || params.runType === "queue_repair") && failureSignature
|
|
101
102
|
? {
|
|
102
103
|
lastAttemptedFailureSignature: failureSignature,
|
package/dist/run-orchestrator.js
CHANGED
|
@@ -3,6 +3,7 @@ import { buildRunStartedActivity, } from "./linear-session-reporting.js";
|
|
|
3
3
|
import { CompletionCheckService } from "./completion-check.js";
|
|
4
4
|
import { WorktreeManager } from "./worktree-manager.js";
|
|
5
5
|
import { MergedLinearCompletionReconciler } from "./merged-linear-completion-reconciler.js";
|
|
6
|
+
import { MainBranchHealthMonitor } from "./main-branch-health-monitor.js";
|
|
6
7
|
import { QueueHealthMonitor } from "./queue-health-monitor.js";
|
|
7
8
|
import { IdleIssueReconciler } from "./idle-reconciliation.js";
|
|
8
9
|
import { LinearSessionSync } from "./linear-session-sync.js";
|
|
@@ -44,6 +45,7 @@ export class RunOrchestrator {
|
|
|
44
45
|
feed;
|
|
45
46
|
configPath;
|
|
46
47
|
worktreeManager;
|
|
48
|
+
mainBranchHealthMonitor;
|
|
47
49
|
/** Tracks last probe-failure feed event per issue to avoid spamming the operator feed. */
|
|
48
50
|
queueHealthMonitor;
|
|
49
51
|
idleReconciler;
|
|
@@ -103,6 +105,7 @@ export class RunOrchestrator {
|
|
|
103
105
|
this.idleReconciler = new IdleIssueReconciler(db, config, {
|
|
104
106
|
enqueueIssue: (projectId, issueId) => this.enqueueIssue(projectId, issueId),
|
|
105
107
|
}, logger, feed);
|
|
108
|
+
this.mainBranchHealthMonitor = new MainBranchHealthMonitor(db, config, linearProvider, (projectId, issueId) => this.enqueueIssue(projectId, issueId), logger, feed);
|
|
106
109
|
this.mergedLinearCompletionReconciler = new MergedLinearCompletionReconciler(db, linearProvider, logger);
|
|
107
110
|
this.queueHealthMonitor = new QueueHealthMonitor(db, config, {
|
|
108
111
|
advanceIdleIssue: (issue, newState, options) => this.idleReconciler.advanceIdleIssue(issue, newState, options),
|
|
@@ -351,6 +354,7 @@ export class RunOrchestrator {
|
|
|
351
354
|
for (const run of this.db.runs.listRunningRuns()) {
|
|
352
355
|
await this.reconcileRun(run);
|
|
353
356
|
}
|
|
357
|
+
await this.mainBranchHealthMonitor.reconcile();
|
|
354
358
|
// Preemptively detect stuck merge-queue PRs (conflicts visible on
|
|
355
359
|
// GitHub) and dispatch queue_repair before the Steward evicts.
|
|
356
360
|
await this.queueHealthMonitor.reconcile();
|
package/dist/run-wake-planner.js
CHANGED
|
@@ -27,6 +27,10 @@ export class RunWakePlanner {
|
|
|
27
27
|
eventType = "settled_red_ci";
|
|
28
28
|
dedupeKey = `${dedupeScope ?? "wake"}:ci_repair:${issue.linearIssueId}:${issue.lastGitHubFailureSignature ?? issue.prHeadSha ?? "unknown-sha"}`;
|
|
29
29
|
}
|
|
30
|
+
else if (runType === "main_repair") {
|
|
31
|
+
eventType = "delegated";
|
|
32
|
+
dedupeKey = `${dedupeScope ?? "wake"}:main_repair:${issue.linearIssueId}`;
|
|
33
|
+
}
|
|
30
34
|
else if (runType === "review_fix" || runType === "branch_upkeep") {
|
|
31
35
|
eventType = "review_changes_requested";
|
|
32
36
|
dedupeKey = `${dedupeScope ?? "wake"}:${runType}:${issue.linearIssueId}:${issue.prHeadSha ?? "unknown-sha"}`;
|