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.
@@ -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.activeRunType === "main_repair" || params.pendingRunType === "main_repair"
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":
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "service": "patchrelay",
3
- "version": "0.71.1",
4
- "commit": "a52a4e7f91af",
5
- "builtAt": "2026-05-23T22:11:42.868Z"
3
+ "version": "0.71.2",
4
+ "commit": "252cbbebc8d3",
5
+ "builtAt": "2026-05-24T00:18:53.503Z"
6
6
  }
@@ -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" && run.runType !== "main_repair") {
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" && run.runType !== "main_repair") {
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
- const RUN_TYPES = new Set(["implementation", "main_repair", "review_fix", "branch_upkeep", "ci_repair", "queue_repair"]);
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 = runType === "main_repair"
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;
@@ -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;
@@ -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 === "main_repair" ? "implementing"
129
- : params.runType === "ci_repair" ? "repairing_ci"
130
- : params.runType === "review_fix" || params.runType === "branch_upkeep" ? "changes_requested"
131
- : params.runType === "queue_repair" ? "repairing_queue"
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,
@@ -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();
@@ -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,6 +1,6 @@
1
1
  {
2
2
  "name": "patchrelay",
3
- "version": "0.71.1",
3
+ "version": "0.71.2",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "repository": {
@@ -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
- }
@@ -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
- }