patchrelay 0.71.1 → 0.71.2
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 +1 -13
- 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/config.js +1 -0
- package/dist/idle-reconciliation.js +0 -33
- package/dist/implementation-outcome-policy.js +2 -45
- package/dist/issue-session-events.js +5 -4
- package/dist/issue-session.js +0 -2
- package/dist/no-pr-completion-check.js +0 -44
- package/dist/patchrelay-customization.js +1 -0
- package/dist/prompting/patchrelay.js +0 -21
- package/dist/run-launcher.js +4 -5
- package/dist/run-orchestrator.js +0 -4
- package/dist/run-wake-planner.js +0 -4
- package/package.json +1 -1
- package/dist/main-branch-health-monitor.js +0 -179
- package/dist/main-repair.js +0 -47
|
@@ -51,14 +51,6 @@ 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: "Fresh head pushed", status: "pending" },
|
|
59
|
-
{ content: "Priority merge", status: "pending" },
|
|
60
|
-
];
|
|
61
|
-
}
|
|
62
54
|
function queueRepairPlan(attempt) {
|
|
63
55
|
return [
|
|
64
56
|
{ content: "Prepare workspace", status: "completed" },
|
|
@@ -148,9 +140,7 @@ export function buildAgentSessionPlan(params) {
|
|
|
148
140
|
case "delegated":
|
|
149
141
|
return setStatuses(planForRunType(runType, params), ["inProgress", "pending", "pending", "pending"]);
|
|
150
142
|
case "implementing":
|
|
151
|
-
return setStatuses(params
|
|
152
|
-
? mainRepairPlan(params.ciRepairAttempts ?? 1)
|
|
153
|
-
: planForRunType("implementation", params), ["completed", "inProgress", "pending", "pending"]);
|
|
143
|
+
return setStatuses(planForRunType("implementation", params), ["completed", "inProgress", "pending", "pending"]);
|
|
154
144
|
case "pr_open":
|
|
155
145
|
return setStatuses([
|
|
156
146
|
{ content: "Prepare workspace", status: "completed" },
|
|
@@ -217,8 +207,6 @@ function normalizeState(value) {
|
|
|
217
207
|
}
|
|
218
208
|
function planForRunType(runType, params) {
|
|
219
209
|
switch (runType) {
|
|
220
|
-
case "main_repair":
|
|
221
|
-
return mainRepairPlan(params.ciRepairAttempts ?? 1);
|
|
222
210
|
case "review_fix":
|
|
223
211
|
return reviewFixPlan();
|
|
224
212
|
case "branch_upkeep":
|
package/dist/build-info.json
CHANGED
|
@@ -16,6 +16,7 @@ function formatDuration(startedAt, endedAt) {
|
|
|
16
16
|
}
|
|
17
17
|
const RUN_LABELS = {
|
|
18
18
|
implementation: "implementation",
|
|
19
|
+
// main_repair is a removed run type; label retained to render historical runs.
|
|
19
20
|
main_repair: "main repair",
|
|
20
21
|
ci_repair: "ci repair",
|
|
21
22
|
review_fix: "review fix",
|
|
@@ -2,6 +2,7 @@ import { relativeTime, truncate } from "./format-utils.js";
|
|
|
2
2
|
export { relativeTime };
|
|
3
3
|
const RUN_LABEL = {
|
|
4
4
|
implementation: "implementation",
|
|
5
|
+
// main_repair is a removed run type; label retained to render historical runs.
|
|
5
6
|
main_repair: "main repair",
|
|
6
7
|
ci_repair: "ci repair",
|
|
7
8
|
review_fix: "review fix",
|
|
@@ -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 is a removed run type; label retained to render historical runs.
|
|
5
6
|
main_repair: "implementing",
|
|
6
7
|
ci_repair: "repairing_ci",
|
|
7
8
|
review_fix: "changes_requested",
|
package/dist/config.js
CHANGED
|
@@ -102,6 +102,7 @@ const promptLayerSchema = z.object({
|
|
|
102
102
|
});
|
|
103
103
|
const promptByRunTypeSchema = z.object({
|
|
104
104
|
implementation: promptLayerSchema.optional(),
|
|
105
|
+
// main_repair is a removed run type; key retained (optional) so pre-existing configs still validate.
|
|
105
106
|
main_repair: promptLayerSchema.optional(),
|
|
106
107
|
review_fix: promptLayerSchema.optional(),
|
|
107
108
|
branch_upkeep: promptLayerSchema.optional(),
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { buildBranchUpkeepContext, buildFailureContext, getGateCheckNames, hasCompletedReviewQuillVerdict, hasFailureProvenance, isDuplicateRepairAttempt, isFailingCheckStatus, isReviewDecisionApproved, isReviewDecisionReviewRequired, } from "./idle-reconciliation-helpers.js";
|
|
2
|
-
import { isMainRepairIssue } from "./main-repair.js";
|
|
3
2
|
import { resolveMergeQueueProtocol } from "./merge-queue-protocol.js";
|
|
4
3
|
import { deriveGateCheckStatusFromRollup } from "./github-rollup.js";
|
|
5
4
|
import { deriveIssueSessionReactiveIntent } from "./issue-session.js";
|
|
@@ -342,35 +341,6 @@ export class IdleIssueReconciler {
|
|
|
342
341
|
const project = this.config.projects.find((candidate) => candidate.id === issue.projectId);
|
|
343
342
|
return resolveMergeQueueProtocol(project);
|
|
344
343
|
}
|
|
345
|
-
async ensurePriorityQueueLabel(issue, repoFullName) {
|
|
346
|
-
if (!isMainRepairIssue(issue) || !issue.prNumber)
|
|
347
|
-
return;
|
|
348
|
-
const priorityLabel = this.getIssueProtocol(issue).priorityLabel;
|
|
349
|
-
try {
|
|
350
|
-
const { stdout } = await execCommand("gh", [
|
|
351
|
-
"pr", "view", String(issue.prNumber),
|
|
352
|
-
"--repo", repoFullName,
|
|
353
|
-
"--json", "labels",
|
|
354
|
-
], { timeoutMs: 10_000 });
|
|
355
|
-
const payload = JSON.parse(stdout);
|
|
356
|
-
if ((payload.labels ?? []).some((entry) => entry.name === priorityLabel)) {
|
|
357
|
-
return;
|
|
358
|
-
}
|
|
359
|
-
await execCommand("gh", [
|
|
360
|
-
"pr", "edit", String(issue.prNumber),
|
|
361
|
-
"--repo", repoFullName,
|
|
362
|
-
"--add-label", priorityLabel,
|
|
363
|
-
], { timeoutMs: 10_000 });
|
|
364
|
-
}
|
|
365
|
-
catch (error) {
|
|
366
|
-
this.logger.warn({
|
|
367
|
-
issueKey: issue.issueKey,
|
|
368
|
-
prNumber: issue.prNumber,
|
|
369
|
-
priorityLabel,
|
|
370
|
-
error: error instanceof Error ? error.message : String(error),
|
|
371
|
-
}, "Reconciliation: failed to enforce priority queue label");
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
344
|
async reconcileFromGitHub(issue) {
|
|
375
345
|
const project = this.config.projects.find((p) => p.id === issue.projectId);
|
|
376
346
|
if (!project?.github?.repoFullName || !issue.prNumber)
|
|
@@ -390,9 +360,6 @@ export class IdleIssueReconciler {
|
|
|
390
360
|
const previousHeadSha = issue.prHeadSha;
|
|
391
361
|
const gateCheckNames = getGateCheckNames(project);
|
|
392
362
|
const gateCheckStatus = deriveGateCheckStatusFromRollup(pr.statusCheckRollup, gateCheckNames);
|
|
393
|
-
if (pr.state === "OPEN") {
|
|
394
|
-
await this.ensurePriorityQueueLabel(issue, project.github.repoFullName);
|
|
395
|
-
}
|
|
396
363
|
this.db.issues.upsertIssue({
|
|
397
364
|
projectId: issue.projectId,
|
|
398
365
|
linearIssueId: issue.linearIssueId,
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { isMainRepairIssue } from "./main-repair.js";
|
|
2
|
-
import { resolveMergeQueueProtocol } from "./merge-queue-protocol.js";
|
|
3
1
|
import { execCommand } from "./utils.js";
|
|
4
2
|
export class ImplementationOutcomePolicy {
|
|
5
3
|
config;
|
|
@@ -13,7 +11,7 @@ export class ImplementationOutcomePolicy {
|
|
|
13
11
|
this.withHeldLease = withHeldLease;
|
|
14
12
|
}
|
|
15
13
|
async verifyPublishedRunOutcome(run, issue) {
|
|
16
|
-
if (run.runType !== "implementation"
|
|
14
|
+
if (run.runType !== "implementation") {
|
|
17
15
|
return undefined;
|
|
18
16
|
}
|
|
19
17
|
const project = this.config.projects.find((entry) => entry.id === run.projectId);
|
|
@@ -26,7 +24,7 @@ export class ImplementationOutcomePolicy {
|
|
|
26
24
|
return details ?? `Implementation completed without opening a PR for branch ${issue.branchName ?? issue.linearIssueId}`;
|
|
27
25
|
}
|
|
28
26
|
async detectRecoverableFailedImplementationOutcome(run, issue) {
|
|
29
|
-
if (run.runType !== "implementation"
|
|
27
|
+
if (run.runType !== "implementation") {
|
|
30
28
|
return undefined;
|
|
31
29
|
}
|
|
32
30
|
const project = this.config.projects.find((entry) => entry.id === run.projectId);
|
|
@@ -81,9 +79,6 @@ export class ImplementationOutcomePolicy {
|
|
|
81
79
|
else {
|
|
82
80
|
this.clearObservedPrIfLeaseHeld(issue, "published PR verification found only historical PRs for branch");
|
|
83
81
|
}
|
|
84
|
-
if (isOpenPrState(state) && isMainRepairIssue(issue)) {
|
|
85
|
-
await this.ensurePriorityQueueLabel(run.projectId, pr.number, repoFullName);
|
|
86
|
-
}
|
|
87
82
|
return isOpenPrState(state) ? "open" : "closed";
|
|
88
83
|
}
|
|
89
84
|
catch (error) {
|
|
@@ -158,44 +153,6 @@ export class ImplementationOutcomePolicy {
|
|
|
158
153
|
}
|
|
159
154
|
return undefined;
|
|
160
155
|
}
|
|
161
|
-
async ensurePriorityQueueLabel(projectId, prNumber, repoFullName) {
|
|
162
|
-
const project = this.config.projects.find((entry) => entry.id === projectId);
|
|
163
|
-
if (!project || !repoFullName)
|
|
164
|
-
return;
|
|
165
|
-
const priorityLabel = resolveMergeQueueProtocol(project).priorityLabel;
|
|
166
|
-
try {
|
|
167
|
-
const { stdout } = await execCommand("gh", [
|
|
168
|
-
"pr",
|
|
169
|
-
"view",
|
|
170
|
-
String(prNumber),
|
|
171
|
-
"--repo",
|
|
172
|
-
repoFullName,
|
|
173
|
-
"--json",
|
|
174
|
-
"labels",
|
|
175
|
-
], { timeoutMs: 10_000 });
|
|
176
|
-
const labels = JSON.parse(stdout);
|
|
177
|
-
const hasLabel = (labels.labels ?? []).some((entry) => entry.name === priorityLabel);
|
|
178
|
-
if (hasLabel)
|
|
179
|
-
return;
|
|
180
|
-
await execCommand("gh", [
|
|
181
|
-
"pr",
|
|
182
|
-
"edit",
|
|
183
|
-
String(prNumber),
|
|
184
|
-
"--repo",
|
|
185
|
-
repoFullName,
|
|
186
|
-
"--add-label",
|
|
187
|
-
priorityLabel,
|
|
188
|
-
], { timeoutMs: 10_000 });
|
|
189
|
-
}
|
|
190
|
-
catch (error) {
|
|
191
|
-
this.logger.warn({
|
|
192
|
-
projectId,
|
|
193
|
-
prNumber,
|
|
194
|
-
priorityLabel,
|
|
195
|
-
error: error instanceof Error ? error.message : String(error),
|
|
196
|
-
}, "Failed to enforce priority queue label on main repair PR");
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
156
|
}
|
|
200
157
|
function isOpenPrState(state) {
|
|
201
158
|
if (!state)
|
|
@@ -12,7 +12,10 @@ const NON_ACTIONABLE_SESSION_EVENTS = new Set([
|
|
|
12
12
|
"prompt_delivered",
|
|
13
13
|
"run_released_authority",
|
|
14
14
|
]);
|
|
15
|
-
|
|
15
|
+
// "main_repair" was removed as a run type; legacy session-event payloads carrying it
|
|
16
|
+
// are not in this set, so parseRunType returns undefined and callers fall back to
|
|
17
|
+
// "implementation" (see deriveSessionWakePlan below).
|
|
18
|
+
const RUN_TYPES = new Set(["implementation", "review_fix", "branch_upkeep", "ci_repair", "queue_repair"]);
|
|
16
19
|
function parseRunType(value) {
|
|
17
20
|
return typeof value === "string" && RUN_TYPES.has(value) ? value : undefined;
|
|
18
21
|
}
|
|
@@ -53,9 +56,7 @@ export function deriveSessionWakePlan(issue, events) {
|
|
|
53
56
|
case "delegated":
|
|
54
57
|
if (!runType) {
|
|
55
58
|
runType = parseRunType(payload?.runType) ?? "implementation";
|
|
56
|
-
wakeReason =
|
|
57
|
-
? "main_repair"
|
|
58
|
-
: issue.issueClass === "orchestration" ? "initial_delegate" : "delegated";
|
|
59
|
+
wakeReason = issue.issueClass === "orchestration" ? "initial_delegate" : "delegated";
|
|
59
60
|
}
|
|
60
61
|
Object.assign(context, payload ?? {});
|
|
61
62
|
break;
|
package/dist/issue-session.js
CHANGED
|
@@ -18,8 +18,6 @@ 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";
|
|
23
21
|
if (params.pendingRunType === "review_fix")
|
|
24
22
|
return "review_changes_requested";
|
|
25
23
|
if (params.pendingRunType === "branch_upkeep")
|
|
@@ -168,50 +168,6 @@ export async function handleNoPrCompletionCheck(params) {
|
|
|
168
168
|
});
|
|
169
169
|
return;
|
|
170
170
|
}
|
|
171
|
-
if (params.run.runType === "main_repair") {
|
|
172
|
-
const continued = params.withHeldLease(params.run.projectId, params.run.linearIssueId, (lease) => {
|
|
173
|
-
params.db.runs.finishRun(params.run.id, runUpdate);
|
|
174
|
-
params.db.runs.saveCompletionCheck(params.run.id, {
|
|
175
|
-
...completionCheck,
|
|
176
|
-
outcome: "continue",
|
|
177
|
-
summary: "Main repair cannot finish without a published repair PR; continuing automatically until the fix is published or main recovers externally.",
|
|
178
|
-
why: completionCheck.summary,
|
|
179
|
-
});
|
|
180
|
-
params.db.issues.upsertIssue({
|
|
181
|
-
projectId: params.run.projectId,
|
|
182
|
-
linearIssueId: params.run.linearIssueId,
|
|
183
|
-
activeRunId: null,
|
|
184
|
-
factoryState: "delegated",
|
|
185
|
-
pendingRunType: null,
|
|
186
|
-
pendingRunContextJson: null,
|
|
187
|
-
});
|
|
188
|
-
return Boolean(params.db.issueSessions.appendIssueSessionEventWithLease(lease, {
|
|
189
|
-
projectId: params.run.projectId,
|
|
190
|
-
linearIssueId: params.run.linearIssueId,
|
|
191
|
-
eventType: "completion_check_continue",
|
|
192
|
-
eventJson: JSON.stringify({
|
|
193
|
-
runType: params.run.runType,
|
|
194
|
-
summary: params.publishedOutcomeError,
|
|
195
|
-
}),
|
|
196
|
-
dedupeKey: `completion_check_continue:${params.run.id}`,
|
|
197
|
-
}));
|
|
198
|
-
});
|
|
199
|
-
if (!continued) {
|
|
200
|
-
params.logger.warn({ runId: params.run.id, issueId: params.run.linearIssueId }, "Skipping main-repair completion-check continue writes after losing issue-session lease");
|
|
201
|
-
params.clearProgressAndRelease(params.run);
|
|
202
|
-
return;
|
|
203
|
-
}
|
|
204
|
-
params.syncCompletionCheckOutcome({
|
|
205
|
-
run: params.run,
|
|
206
|
-
fallbackIssue: params.issue,
|
|
207
|
-
level: "info",
|
|
208
|
-
status: "completion_check_continue",
|
|
209
|
-
summary: "No repair PR found; continuing automatically",
|
|
210
|
-
detail: "Main repair cannot close until PatchRelay publishes a repair PR or main recovers externally.",
|
|
211
|
-
activity: buildCompletionCheckActivity("continue"),
|
|
212
|
-
});
|
|
213
|
-
return;
|
|
214
|
-
}
|
|
215
171
|
const orchestrationOpenChildren = params.issue.issueClass === "orchestration"
|
|
216
172
|
? params.db.issues.countOpenChildIssues(params.run.projectId, params.run.linearIssueId)
|
|
217
173
|
: 0;
|
|
@@ -7,6 +7,7 @@ const promptLayerSchema = z.object({
|
|
|
7
7
|
});
|
|
8
8
|
const promptByRunTypeSchema = z.object({
|
|
9
9
|
implementation: promptLayerSchema.optional(),
|
|
10
|
+
// main_repair is a removed run type; key retained (optional) so pre-existing configs still validate.
|
|
10
11
|
main_repair: promptLayerSchema.optional(),
|
|
11
12
|
review_fix: promptLayerSchema.optional(),
|
|
12
13
|
branch_upkeep: promptLayerSchema.optional(),
|
|
@@ -3,7 +3,6 @@ import path from "node:path";
|
|
|
3
3
|
import { derivePrDisplayContext } from "../pr-display-context.js";
|
|
4
4
|
const WORKFLOW_FILES = {
|
|
5
5
|
implementation: "IMPLEMENTATION_WORKFLOW.md",
|
|
6
|
-
main_repair: "IMPLEMENTATION_WORKFLOW.md",
|
|
7
6
|
review_fix: "REVIEW_WORKFLOW.md",
|
|
8
7
|
branch_upkeep: "REVIEW_WORKFLOW.md",
|
|
9
8
|
ci_repair: "IMPLEMENTATION_WORKFLOW.md",
|
|
@@ -346,23 +345,6 @@ function buildCiRepairContext(context) {
|
|
|
346
345
|
: "",
|
|
347
346
|
].filter(Boolean).join("\n");
|
|
348
347
|
}
|
|
349
|
-
function buildMainRepairContext(context) {
|
|
350
|
-
const failingCheckNames = Array.isArray(context?.failingChecks)
|
|
351
|
-
? context.failingChecks
|
|
352
|
-
.filter((entry) => Boolean(entry) && typeof entry === "object")
|
|
353
|
-
.map((entry) => String(entry.name ?? "").trim())
|
|
354
|
-
.filter((name) => name.length > 0)
|
|
355
|
-
: [];
|
|
356
|
-
return [
|
|
357
|
-
"Base-branch repair on the red mainline.",
|
|
358
|
-
"Goal: restore main by fixing the real persistent failure, not by papering over a transient runner incident.",
|
|
359
|
-
"Before changing code or workflow config, verify that the original incident still persists on the exact failing main SHA or identify a concrete log signature that justifies the fix.",
|
|
360
|
-
"For transient infrastructure symptoms such as disk pressure, runner exhaustion, or network flakiness, prefer a rerun-only repair if the rerun clears the branch.",
|
|
361
|
-
"Do not propose or implement moving CI, deploy, or tests onto different nodes or runner pools unless a human explicitly asked for that infrastructure migration.",
|
|
362
|
-
context?.baseSha ? `Failing main SHA: ${String(context.baseSha)}` : "",
|
|
363
|
-
failingCheckNames.length > 0 ? `Failing checks: ${failingCheckNames.join(", ")}` : "",
|
|
364
|
-
].filter(Boolean).join("\n");
|
|
365
|
-
}
|
|
366
348
|
function appendQueueRepairContext(lines, context) {
|
|
367
349
|
const queueContext = context?.mergeQueueContext;
|
|
368
350
|
if (!queueContext || typeof queueContext !== "object") {
|
|
@@ -489,9 +471,6 @@ function buildCurrentContext(runType, issue, context, followUp = false) {
|
|
|
489
471
|
}
|
|
490
472
|
lines.push(...buildHumanContextLines(context));
|
|
491
473
|
switch (runType) {
|
|
492
|
-
case "main_repair":
|
|
493
|
-
lines.push(buildMainRepairContext(context));
|
|
494
|
-
break;
|
|
495
474
|
case "ci_repair":
|
|
496
475
|
lines.push(buildCiRepairContext(context));
|
|
497
476
|
break;
|
package/dist/run-launcher.js
CHANGED
|
@@ -125,11 +125,10 @@ export class RunLauncher {
|
|
|
125
125
|
branchName: params.branchName,
|
|
126
126
|
worktreePath: params.worktreePath,
|
|
127
127
|
factoryState: params.runType === "implementation" ? "implementing"
|
|
128
|
-
: params.runType === "
|
|
129
|
-
: params.runType === "
|
|
130
|
-
: params.runType === "
|
|
131
|
-
:
|
|
132
|
-
: "implementing",
|
|
128
|
+
: params.runType === "ci_repair" ? "repairing_ci"
|
|
129
|
+
: params.runType === "review_fix" || params.runType === "branch_upkeep" ? "changes_requested"
|
|
130
|
+
: params.runType === "queue_repair" ? "repairing_queue"
|
|
131
|
+
: "implementing",
|
|
133
132
|
...((params.runType === "ci_repair" || params.runType === "queue_repair") && failureSignature
|
|
134
133
|
? {
|
|
135
134
|
lastAttemptedFailureSignature: failureSignature,
|
package/dist/run-orchestrator.js
CHANGED
|
@@ -4,7 +4,6 @@ import { CompletionCheckService } from "./completion-check.js";
|
|
|
4
4
|
import { PublicationRecapService } from "./publication-recap.js";
|
|
5
5
|
import { WorktreeManager } from "./worktree-manager.js";
|
|
6
6
|
import { MergedLinearCompletionReconciler } from "./merged-linear-completion-reconciler.js";
|
|
7
|
-
import { MainBranchHealthMonitor } from "./main-branch-health-monitor.js";
|
|
8
7
|
import { QueueHealthMonitor } from "./queue-health-monitor.js";
|
|
9
8
|
import { IdleIssueReconciler } from "./idle-reconciliation.js";
|
|
10
9
|
import { LinearSessionSync } from "./linear-session-sync.js";
|
|
@@ -47,7 +46,6 @@ export class RunOrchestrator {
|
|
|
47
46
|
linearProvider;
|
|
48
47
|
enqueueIssue;
|
|
49
48
|
worktreeManager;
|
|
50
|
-
mainBranchHealthMonitor;
|
|
51
49
|
/** Tracks last probe-failure feed event per issue to avoid spamming the operator feed. */
|
|
52
50
|
queueHealthMonitor;
|
|
53
51
|
idleReconciler;
|
|
@@ -137,7 +135,6 @@ export class RunOrchestrator {
|
|
|
137
135
|
this.runReconciler = new RunReconciler(db, logger, linearProvider, this.linearSync, this.interruptedRunRecovery, this.runFinalizer, this.leasePorts.withHeldLease, this.leasePorts.releaseLease, this.threadPorts.readThreadWithRetry, this.recoveryPorts.recoverOrEscalate, (projectId) => this.config.projects.find((project) => project.id === projectId)?.github?.repoFullName, feed);
|
|
138
136
|
this.runWakePlanner = new RunWakePlanner(db);
|
|
139
137
|
this.idleReconciler = new IdleIssueReconciler(db, config, this.wakeDispatcher, logger, feed);
|
|
140
|
-
this.mainBranchHealthMonitor = new MainBranchHealthMonitor(db, config, linearProvider, logger, feed);
|
|
141
138
|
this.mergedLinearCompletionReconciler = new MergedLinearCompletionReconciler(db, linearProvider, logger);
|
|
142
139
|
this.queueHealthMonitor = new QueueHealthMonitor(db, config, {
|
|
143
140
|
advanceIdleIssue: (issue, newState, options) => this.idleReconciler.advanceIdleIssue(issue, newState, options),
|
|
@@ -454,7 +451,6 @@ export class RunOrchestrator {
|
|
|
454
451
|
for (const run of this.db.runs.listRunningRuns()) {
|
|
455
452
|
await this.reconcileRun(run);
|
|
456
453
|
}
|
|
457
|
-
await this.mainBranchHealthMonitor.reconcile();
|
|
458
454
|
// Preemptively detect stuck merge-queue PRs (conflicts visible on
|
|
459
455
|
// GitHub) and dispatch queue_repair before the Steward evicts.
|
|
460
456
|
await this.queueHealthMonitor.reconcile();
|
package/dist/run-wake-planner.js
CHANGED
|
@@ -27,10 +27,6 @@ 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
|
-
}
|
|
34
30
|
else if (runType === "review_fix" || runType === "branch_upkeep") {
|
|
35
31
|
eventType = "review_changes_requested";
|
|
36
32
|
dedupeKey = `${dedupeScope ?? "wake"}:${runType}:${issue.linearIssueId}:${issue.prHeadSha ?? "unknown-sha"}`;
|
package/package.json
CHANGED
|
@@ -1,179 +0,0 @@
|
|
|
1
|
-
import { resolvePreferredCompletedLinearState } from "./linear-workflow.js";
|
|
2
|
-
import { buildMainRepairBranchName, isMainRepairIssue, } from "./main-repair.js";
|
|
3
|
-
import { execCommand } from "./utils.js";
|
|
4
|
-
const MAIN_BRANCH_HEALTH_GRACE_MS = 120_000;
|
|
5
|
-
function isUnhealthyMainConclusion(conclusion) {
|
|
6
|
-
return conclusion === "failure"
|
|
7
|
-
|| conclusion === "timed_out"
|
|
8
|
-
|| conclusion === "cancelled"
|
|
9
|
-
|| conclusion === "action_required"
|
|
10
|
-
|| conclusion === "stale";
|
|
11
|
-
}
|
|
12
|
-
export class MainBranchHealthMonitor {
|
|
13
|
-
db;
|
|
14
|
-
config;
|
|
15
|
-
linearProvider;
|
|
16
|
-
logger;
|
|
17
|
-
feed;
|
|
18
|
-
/** Per-project throttle for the information-only "main is red" log. */
|
|
19
|
-
lastUnhealthyReportAt = new Map();
|
|
20
|
-
constructor(db, config, linearProvider, logger, feed) {
|
|
21
|
-
this.db = db;
|
|
22
|
-
this.config = config;
|
|
23
|
-
this.linearProvider = linearProvider;
|
|
24
|
-
this.logger = logger;
|
|
25
|
-
this.feed = feed;
|
|
26
|
-
}
|
|
27
|
-
async reconcile() {
|
|
28
|
-
for (const project of this.config.projects) {
|
|
29
|
-
await this.reconcileProject(project.id);
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
async reconcileProject(projectId) {
|
|
33
|
-
const project = this.config.projects.find((entry) => entry.id === projectId);
|
|
34
|
-
if (!project?.github?.repoFullName)
|
|
35
|
-
return;
|
|
36
|
-
if (project.linearTeamIds.length === 0)
|
|
37
|
-
return;
|
|
38
|
-
const baseBranch = project.github.baseBranch ?? "main";
|
|
39
|
-
const branchName = buildMainRepairBranchName(baseBranch);
|
|
40
|
-
const existing = this.findExistingMainRepair(projectId, branchName);
|
|
41
|
-
const summary = await this.readMainBranchFailure(project.github.repoFullName, baseBranch);
|
|
42
|
-
if (!summary) {
|
|
43
|
-
if (existing) {
|
|
44
|
-
await this.resolveRecoveredMainRepair(existing);
|
|
45
|
-
}
|
|
46
|
-
return;
|
|
47
|
-
}
|
|
48
|
-
// main CI is red. The merge queue (merge-steward) gates only on its own
|
|
49
|
-
// speculative-SHA checks and ignores main entirely, so a red main no longer
|
|
50
|
-
// warrants an automated repair job — main CI is information-only. Report it
|
|
51
|
-
// (throttled) and post nothing. Any pre-existing repair issue is left to close
|
|
52
|
-
// via resolveRecoveredMainRepair once main recovers.
|
|
53
|
-
this.reportUnhealthyMain(projectId, project.github.repoFullName, baseBranch, summary);
|
|
54
|
-
}
|
|
55
|
-
reportUnhealthyMain(projectId, repoFullName, baseBranch, summary) {
|
|
56
|
-
const now = Date.now();
|
|
57
|
-
const lastReportedAt = this.lastUnhealthyReportAt.get(projectId);
|
|
58
|
-
if (lastReportedAt !== undefined && now - lastReportedAt < MAIN_BRANCH_HEALTH_GRACE_MS) {
|
|
59
|
-
return;
|
|
60
|
-
}
|
|
61
|
-
this.lastUnhealthyReportAt.set(projectId, now);
|
|
62
|
-
this.logger.warn({
|
|
63
|
-
projectId,
|
|
64
|
-
repoFullName,
|
|
65
|
-
baseBranch,
|
|
66
|
-
baseSha: summary.baseSha,
|
|
67
|
-
failingChecks: summary.failingChecks.map((check) => check.name),
|
|
68
|
-
}, "main branch CI is red — information only; no repair job posted (merge queue gates on its own spec CI)");
|
|
69
|
-
}
|
|
70
|
-
findExistingMainRepair(projectId, branchName) {
|
|
71
|
-
const candidates = this.db.listIssues()
|
|
72
|
-
.filter((issue) => (issue.projectId === projectId
|
|
73
|
-
&& issue.branchName === branchName
|
|
74
|
-
&& isMainRepairIssue(issue)
|
|
75
|
-
&& issue.factoryState !== "done"))
|
|
76
|
-
.sort((left, right) => this.compareMainRepairCandidates(left, right));
|
|
77
|
-
return candidates[0];
|
|
78
|
-
}
|
|
79
|
-
compareMainRepairCandidates(left, right) {
|
|
80
|
-
const leftPriority = this.rankMainRepairCandidate(left);
|
|
81
|
-
const rightPriority = this.rankMainRepairCandidate(right);
|
|
82
|
-
if (leftPriority !== rightPriority)
|
|
83
|
-
return leftPriority - rightPriority;
|
|
84
|
-
return Date.parse(right.updatedAt) - Date.parse(left.updatedAt);
|
|
85
|
-
}
|
|
86
|
-
rankMainRepairCandidate(issue) {
|
|
87
|
-
if (issue.activeRunId !== undefined)
|
|
88
|
-
return 0;
|
|
89
|
-
if (issue.prState === "open" || issue.factoryState === "awaiting_queue" || issue.factoryState === "pr_open")
|
|
90
|
-
return 1;
|
|
91
|
-
if (issue.factoryState === "delegated" || issue.factoryState === "implementing")
|
|
92
|
-
return 2;
|
|
93
|
-
if (issue.factoryState === "failed" || issue.factoryState === "escalated")
|
|
94
|
-
return 3;
|
|
95
|
-
return 4;
|
|
96
|
-
}
|
|
97
|
-
async resolveRecoveredMainRepair(issue) {
|
|
98
|
-
if (issue.activeRunId !== undefined)
|
|
99
|
-
return;
|
|
100
|
-
if (issue.prState === "open" || issue.factoryState === "awaiting_queue" || issue.factoryState === "pr_open") {
|
|
101
|
-
return;
|
|
102
|
-
}
|
|
103
|
-
const linear = await this.linearProvider.forProject(issue.projectId).catch(() => undefined);
|
|
104
|
-
if (linear) {
|
|
105
|
-
const liveIssue = await linear.getIssue(issue.linearIssueId).catch(() => undefined);
|
|
106
|
-
if (liveIssue) {
|
|
107
|
-
const targetState = resolvePreferredCompletedLinearState(liveIssue);
|
|
108
|
-
const normalizedCurrent = liveIssue.stateName?.trim().toLowerCase();
|
|
109
|
-
if (targetState && normalizedCurrent !== targetState.trim().toLowerCase()) {
|
|
110
|
-
const updated = await linear.setIssueState(issue.linearIssueId, targetState).catch(() => undefined);
|
|
111
|
-
if (updated) {
|
|
112
|
-
this.db.upsertIssue({
|
|
113
|
-
projectId: issue.projectId,
|
|
114
|
-
linearIssueId: issue.linearIssueId,
|
|
115
|
-
...(updated.stateName ? { currentLinearState: updated.stateName } : {}),
|
|
116
|
-
...(updated.stateType ? { currentLinearStateType: updated.stateType } : {}),
|
|
117
|
-
});
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
else {
|
|
121
|
-
this.db.upsertIssue({
|
|
122
|
-
projectId: issue.projectId,
|
|
123
|
-
linearIssueId: issue.linearIssueId,
|
|
124
|
-
...(liveIssue.stateName ? { currentLinearState: liveIssue.stateName } : {}),
|
|
125
|
-
...(liveIssue.stateType ? { currentLinearStateType: liveIssue.stateType } : {}),
|
|
126
|
-
});
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
this.db.issueSessions.clearPendingIssueSessionEventsRespectingActiveLease(issue.projectId, issue.linearIssueId);
|
|
131
|
-
this.db.upsertIssue({
|
|
132
|
-
projectId: issue.projectId,
|
|
133
|
-
linearIssueId: issue.linearIssueId,
|
|
134
|
-
factoryState: "done",
|
|
135
|
-
pendingRunType: null,
|
|
136
|
-
});
|
|
137
|
-
this.feed?.publish({
|
|
138
|
-
level: "info",
|
|
139
|
-
kind: "github",
|
|
140
|
-
issueKey: issue.issueKey,
|
|
141
|
-
projectId: issue.projectId,
|
|
142
|
-
stage: "done",
|
|
143
|
-
status: "main_repair_resolved",
|
|
144
|
-
summary: "Closed stale main_repair after main recovered externally",
|
|
145
|
-
});
|
|
146
|
-
}
|
|
147
|
-
async readMainBranchFailure(repoFullName, baseBranch) {
|
|
148
|
-
const { stdout: shaOut } = await execCommand("gh", [
|
|
149
|
-
"api",
|
|
150
|
-
`repos/${repoFullName}/branches/${baseBranch}`,
|
|
151
|
-
"--jq",
|
|
152
|
-
".commit.sha",
|
|
153
|
-
], { timeoutMs: 10_000 });
|
|
154
|
-
const baseSha = shaOut.trim();
|
|
155
|
-
if (!baseSha)
|
|
156
|
-
return undefined;
|
|
157
|
-
const { stdout: checksOut } = await execCommand("gh", [
|
|
158
|
-
"api",
|
|
159
|
-
`repos/${repoFullName}/commits/${baseSha}/check-runs`,
|
|
160
|
-
"--jq",
|
|
161
|
-
".check_runs",
|
|
162
|
-
], { timeoutMs: 10_000 });
|
|
163
|
-
const runs = JSON.parse(checksOut || "[]");
|
|
164
|
-
const failingChecks = runs
|
|
165
|
-
.filter((run) => run.status === "completed" && isUnhealthyMainConclusion(run.conclusion) && typeof run.name === "string" && run.name.trim())
|
|
166
|
-
.map((run) => ({ name: run.name.trim(), ...(run.details_url ? { url: run.details_url } : {}) }));
|
|
167
|
-
if (failingChecks.length === 0) {
|
|
168
|
-
return undefined;
|
|
169
|
-
}
|
|
170
|
-
const pendingChecks = runs
|
|
171
|
-
.filter((run) => run.status !== "completed" && typeof run.name === "string" && run.name.trim())
|
|
172
|
-
.map((run) => ({ name: run.name.trim(), ...(run.details_url ? { url: run.details_url } : {}) }));
|
|
173
|
-
return {
|
|
174
|
-
baseSha,
|
|
175
|
-
failingChecks,
|
|
176
|
-
pendingChecks,
|
|
177
|
-
};
|
|
178
|
-
}
|
|
179
|
-
}
|
package/dist/main-repair.js
DELETED
|
@@ -1,47 +0,0 @@
|
|
|
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
|
-
}
|