patchrelay 0.50.6 → 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.
@@ -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(planForRunType("implementation", params), ["completed", "inProgress", "pending", "pending"]);
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 === "review_fix" || runType === "branch_upkeep" ? "changes_requested"
204
- : runType === "queue_repair" ? "repairing_queue"
205
- : "implementing",
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
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "service": "patchrelay",
3
- "version": "0.50.6",
4
- "commit": "93d2215a55bf",
5
- "builtAt": "2026-04-21T15:53:17.328Z"
3
+ "version": "0.51.0",
4
+ "commit": "c8ad40f06a1c",
5
+ "builtAt": "2026-04-21T21:25:15.871Z"
6
6
  }
@@ -16,6 +16,7 @@ function formatDuration(startedAt, endedAt) {
16
16
  }
17
17
  const RUN_LABELS = {
18
18
  implementation: "implementation",
19
+ main_repair: "main repair",
19
20
  ci_repair: "ci repair",
20
21
  review_fix: "review fix",
21
22
  branch_upkeep: "branch upkeep",
@@ -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: "main repair",
5
6
  ci_repair: "ci repair",
6
7
  review_fix: "review fix",
7
8
  branch_upkeep: "branch upkeep",
@@ -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",
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
  };
@@ -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 = issue.issueClass === "orchestration" ? "initial_delegate" : "delegated";
56
- }
57
- if (payload?.promptContext !== undefined) {
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":
@@ -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")
@@ -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());
@@ -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(),
@@ -3,6 +3,7 @@ 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",
6
7
  review_fix: "REVIEW_WORKFLOW.md",
7
8
  branch_upkeep: "REVIEW_WORKFLOW.md",
8
9
  ci_repair: "IMPLEMENTATION_WORKFLOW.md",
@@ -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 === "ci_repair" ? "repairing_ci"
97
- : params.runType === "review_fix" || params.runType === "branch_upkeep" ? "changes_requested"
98
- : params.runType === "queue_repair" ? "repairing_queue"
99
- : "implementing",
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,
@@ -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();
@@ -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"}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "patchrelay",
3
- "version": "0.50.6",
3
+ "version": "0.51.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "repository": {