patchrelay 0.70.0 → 0.71.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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "service": "patchrelay",
3
- "version": "0.70.0",
4
- "commit": "b70636ecd6d4",
5
- "builtAt": "2026-05-23T19:04:31.282Z"
3
+ "version": "0.71.0",
4
+ "commit": "f59e5792709d",
5
+ "builtAt": "2026-05-23T21:09:23.450Z"
6
6
  }
@@ -1,6 +1,5 @@
1
- import { resolveMergeQueueProtocol } from "./merge-queue-protocol.js";
2
1
  import { resolvePreferredCompletedLinearState } from "./linear-workflow.js";
3
- import { buildMainRepairBranchName, buildMainRepairDescription, buildMainRepairPromptContext, buildMainRepairTitle, isMainRepairIssue, } from "./main-repair.js";
2
+ import { buildMainRepairBranchName, isMainRepairIssue, } from "./main-repair.js";
4
3
  import { execCommand } from "./utils.js";
5
4
  const MAIN_BRANCH_HEALTH_GRACE_MS = 120_000;
6
5
  function isUnhealthyMainConclusion(conclusion) {
@@ -14,14 +13,14 @@ export class MainBranchHealthMonitor {
14
13
  db;
15
14
  config;
16
15
  linearProvider;
17
- wakeDispatcher;
18
16
  logger;
19
17
  feed;
20
- constructor(db, config, linearProvider, wakeDispatcher, logger, 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
21
  this.db = db;
22
22
  this.config = config;
23
23
  this.linearProvider = linearProvider;
24
- this.wakeDispatcher = wakeDispatcher;
25
24
  this.logger = logger;
26
25
  this.feed = feed;
27
26
  }
@@ -46,64 +45,27 @@ export class MainBranchHealthMonitor {
46
45
  }
47
46
  return;
48
47
  }
49
- const protocol = resolveMergeQueueProtocol(project);
50
- if (existing) {
51
- const age = Date.now() - Date.parse(existing.updatedAt);
52
- if (age < MAIN_BRANCH_HEALTH_GRACE_MS) {
53
- return;
54
- }
55
- }
56
- if (existing) {
57
- this.queueExistingMainRepair(existing, summary, protocol.priorityLabel);
58
- return;
59
- }
60
- const client = await this.linearProvider.forProject(projectId);
61
- if (!client?.createIssue) {
62
- this.logger.warn({ projectId, repoFullName: project.github.repoFullName }, "Cannot create main repair issue because Linear issue creation is unavailable");
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) {
63
59
  return;
64
60
  }
65
- const created = await client.createIssue({
66
- teamId: project.linearTeamIds[0],
67
- title: buildMainRepairTitle(project),
68
- description: buildMainRepairDescription(project, summary, protocol.priorityLabel),
69
- });
70
- const issue = this.db.upsertIssue({
61
+ this.lastUnhealthyReportAt.set(projectId, now);
62
+ this.logger.warn({
71
63
  projectId,
72
- linearIssueId: created.id,
73
- delegatedToPatchRelay: true,
74
- ...(created.identifier ? { issueKey: created.identifier } : {}),
75
- ...(created.title ? { title: created.title } : {}),
76
- ...(created.description ? { description: created.description } : {}),
77
- ...(created.url ? { url: created.url } : {}),
78
- ...(created.priority != null ? { priority: created.priority } : {}),
79
- ...(created.estimate != null ? { estimate: created.estimate } : {}),
80
- ...(created.stateName ? { currentLinearState: created.stateName } : {}),
81
- ...(created.stateType ? { currentLinearStateType: created.stateType } : {}),
82
- branchName,
83
- factoryState: "delegated",
84
- });
85
- this.wakeDispatcher.recordEventAndDispatch(projectId, issue.linearIssueId, {
86
- eventType: "delegated",
87
- eventJson: JSON.stringify({
88
- runType: "main_repair",
89
- baseSha: summary.baseSha,
90
- failingChecks: summary.failingChecks,
91
- pendingChecks: summary.pendingChecks,
92
- priorityLabel: protocol.priorityLabel,
93
- promptContext: buildMainRepairPromptContext(project, summary, protocol.priorityLabel),
94
- }),
95
- dedupeKey: `main_repair:${projectId}:${summary.baseSha}:${summary.failingChecks.map((check) => check.name).join("|")}`,
96
- });
97
- this.feed?.publish({
98
- level: "warn",
99
- kind: "github",
100
- issueKey: issue.issueKey,
101
- projectId,
102
- stage: "delegated",
103
- status: "main_repair_queued",
104
- summary: `Queued main_repair for ${project.github.repoFullName}@${baseBranch}`,
105
- detail: summary.failingChecks.map((check) => check.name).join(", "),
106
- });
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)");
107
69
  }
108
70
  findExistingMainRepair(projectId, branchName) {
109
71
  const candidates = this.db.listIssues()
@@ -132,35 +94,6 @@ export class MainBranchHealthMonitor {
132
94
  return 3;
133
95
  return 4;
134
96
  }
135
- queueExistingMainRepair(issue, summary, priorityLabel) {
136
- if (issue.activeRunId !== undefined)
137
- return;
138
- if (this.db.issueSessions.hasPendingIssueSessionEvents(issue.projectId, issue.linearIssueId))
139
- return;
140
- if (issue.prState === "open" || issue.factoryState === "awaiting_queue" || issue.factoryState === "pr_open")
141
- return;
142
- this.db.upsertIssue({
143
- projectId: issue.projectId,
144
- linearIssueId: issue.linearIssueId,
145
- delegatedToPatchRelay: true,
146
- factoryState: "delegated",
147
- pendingRunType: null,
148
- pendingRunContextJson: null,
149
- activeRunId: null,
150
- });
151
- this.wakeDispatcher.recordEventAndDispatch(issue.projectId, issue.linearIssueId, {
152
- eventType: "delegated",
153
- eventJson: JSON.stringify({
154
- runType: "main_repair",
155
- baseSha: summary.baseSha,
156
- failingChecks: summary.failingChecks,
157
- pendingChecks: summary.pendingChecks,
158
- priorityLabel,
159
- promptContext: buildMainRepairPromptContext(this.config.projects.find((project) => project.id === issue.projectId) ?? { id: issue.projectId }, summary, priorityLabel),
160
- }),
161
- dedupeKey: `main_repair:${issue.projectId}:${summary.baseSha}:${summary.failingChecks.map((check) => check.name).join("|")}`,
162
- });
163
- }
164
97
  async resolveRecoveredMainRepair(issue) {
165
98
  if (issue.activeRunId !== undefined)
166
99
  return;
@@ -137,7 +137,7 @@ export class RunOrchestrator {
137
137
  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
138
  this.runWakePlanner = new RunWakePlanner(db);
139
139
  this.idleReconciler = new IdleIssueReconciler(db, config, this.wakeDispatcher, logger, feed);
140
- this.mainBranchHealthMonitor = new MainBranchHealthMonitor(db, config, linearProvider, this.wakeDispatcher, logger, feed);
140
+ this.mainBranchHealthMonitor = new MainBranchHealthMonitor(db, config, linearProvider, logger, feed);
141
141
  this.mergedLinearCompletionReconciler = new MergedLinearCompletionReconciler(db, linearProvider, logger);
142
142
  this.queueHealthMonitor = new QueueHealthMonitor(db, config, {
143
143
  advanceIdleIssue: (issue, newState, options) => this.idleReconciler.advanceIdleIssue(issue, newState, options),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "patchrelay",
3
- "version": "0.70.0",
3
+ "version": "0.71.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "repository": {