patchrelay 0.71.1 → 0.72.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-session-plan.js +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/linear-workflow-state-sync.js +108 -45
- package/dist/linear-workflow.js +24 -3
- 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")
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { resolvePreferredQueuedLinearState, resolvePreferredCompletedLinearState, resolvePreferredDeployingLinearState,
|
|
1
|
+
import { resolvePreferredQueuedLinearState, resolvePreferredCompletedLinearState, resolvePreferredDeployingLinearState, resolvePreferredHumanNeededLinearState, resolvePreferredImplementingLinearState, resolvePreferredMergeQueueLinearState, resolvePreferredReviewingLinearState, } from "./linear-workflow.js";
|
|
2
2
|
import { resolveMergeQueueProtocol } from "./merge-queue-protocol.js";
|
|
3
3
|
import { isCompletedLinearState } from "./pr-state.js";
|
|
4
4
|
import { hasTrustedNoPrCompletion } from "./trusted-no-pr-completion.js";
|
|
@@ -67,24 +67,21 @@ async function syncQueuedForDeployLabel(params) {
|
|
|
67
67
|
await linear.updateIssueLabels({ issueId: issue.linearIssueId, removeNames: [labelName] });
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
|
-
// True only when (a) the issue is
|
|
71
|
-
// Linear workflow has no In
|
|
72
|
-
// preferred-
|
|
73
|
-
//
|
|
74
|
-
// state, `setIssueState` flows the issue there and
|
|
75
|
-
// unnecessary.
|
|
70
|
+
// True only when (a) the issue is in the merge queue (`awaiting_queue`)
|
|
71
|
+
// AND (b) the project's Linear workflow has no dedicated In Merge Queue
|
|
72
|
+
// state — detected by the preferred merge-queue state collapsing to the
|
|
73
|
+
// same name as the reviewing state. When the project has a real In Merge
|
|
74
|
+
// Queue (or Deploying) state, `setIssueState` flows the issue there and
|
|
75
|
+
// the label is unnecessary.
|
|
76
76
|
function isQueuedForDeployFallback(issue, liveIssue) {
|
|
77
77
|
if (issue.factoryState !== "awaiting_queue")
|
|
78
78
|
return false;
|
|
79
|
-
const
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
if (!deploying || !review)
|
|
79
|
+
const mergeQueue = resolvePreferredMergeQueueLinearState(liveIssue);
|
|
80
|
+
const reviewing = resolvePreferredReviewingLinearState(liveIssue);
|
|
81
|
+
if (!mergeQueue || !reviewing)
|
|
83
82
|
return false;
|
|
84
|
-
// No
|
|
85
|
-
|
|
86
|
-
return deploying.trim().toLowerCase() === review.trim().toLowerCase()
|
|
87
|
-
&& (deployUnstarted ?? "").trim().toLowerCase() === review.trim().toLowerCase();
|
|
83
|
+
// No dedicated merge-queue state → it collapses to the reviewing state.
|
|
84
|
+
return mergeQueue.trim().toLowerCase() === reviewing.trim().toLowerCase();
|
|
88
85
|
}
|
|
89
86
|
async function syncCompletedLinearState(params) {
|
|
90
87
|
const { db, issue, linear, liveIssue } = params;
|
|
@@ -121,49 +118,115 @@ function shouldAutoAdvanceLinearState(issue) {
|
|
|
121
118
|
const normalizedName = issue.currentLinearState?.trim().toLowerCase();
|
|
122
119
|
return normalizedName !== "done" && normalizedName !== "completed" && normalizedName !== "complete";
|
|
123
120
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
121
|
+
// ─── Unified PR-lifecycle → Linear-state mapping ─────────────────────
|
|
122
|
+
//
|
|
123
|
+
// Five phases, in lifecycle order:
|
|
124
|
+
// Implementing → Reviewing → In Merge Queue → Deploying → Done
|
|
125
|
+
//
|
|
126
|
+
// Every phase is decided from DURABLE signals (factoryState, prState,
|
|
127
|
+
// prReviewState) — never the ephemeral activeRunId / sessionState / run
|
|
128
|
+
// type. That is what kills the Implementing↔Reviewing flap: the state
|
|
129
|
+
// only moves on a real lifecycle handoff (a review verdict, an approval,
|
|
130
|
+
// a merge), not on whichever transient webhook happens to recompute it
|
|
131
|
+
// while a run briefly holds a lease.
|
|
132
|
+
//
|
|
133
|
+
// Branches are ordered "furthest along the lifecycle wins" so a stale
|
|
134
|
+
// earlier signal can never pull a more-advanced issue backwards.
|
|
135
|
+
function resolveDesiredActiveWorkflowState(issue, trackedIssue, _options, liveIssue) {
|
|
136
|
+
// 1. Operator must act — overrides everything.
|
|
137
|
+
if (needsHumanAttention(issue, trackedIssue)) {
|
|
127
138
|
return resolvePreferredHumanNeededLinearState(liveIssue);
|
|
128
139
|
}
|
|
140
|
+
// 2. Completed → Done. Covers today's merge→done path (the factory has
|
|
141
|
+
// no post-merge state yet), so a done issue never reads as Deploying.
|
|
142
|
+
if (issue.factoryState === "done") {
|
|
143
|
+
return resolvePreferredCompletedLinearState(liveIssue);
|
|
144
|
+
}
|
|
145
|
+
// 3. Paused with no PR and nothing for us to do → backlog.
|
|
129
146
|
const blocked = (trackedIssue?.blockedByCount ?? 0) > 0;
|
|
130
|
-
const
|
|
131
|
-
if (
|
|
147
|
+
const noPr = issue.prNumber === undefined && !issue.prUrl;
|
|
148
|
+
if (noPr && (issue.delegatedToPatchRelay === false || blocked)) {
|
|
132
149
|
return resolvePreferredQueuedLinearState(liveIssue);
|
|
133
150
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
151
|
+
// 4. Post-merge: the change is on main, deploy running → Deploying.
|
|
152
|
+
// Durable signal: the PR is merged. (Until PR3 makes merge a
|
|
153
|
+
// non-terminal phase this only fires in the merged-not-yet-done
|
|
154
|
+
// window; branch 2 already caught factoryState==="done".)
|
|
155
|
+
if (normalize(issue.prState) === "merged") {
|
|
156
|
+
return resolvePreferredDeployingLinearState(liveIssue);
|
|
157
|
+
}
|
|
158
|
+
// 5. Patchrelay is actively addressing review/CI/queue feedback →
|
|
159
|
+
// Implementing. These factory states persist for the run's whole
|
|
160
|
+
// duration, so this is stable, not flappy — and it is exactly the
|
|
161
|
+
// "show when patchrelay handles feedback" behavior we want.
|
|
162
|
+
if (isAddressingFeedback(issue)) {
|
|
143
163
|
return resolvePreferredImplementingLinearState(liveIssue);
|
|
144
164
|
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
return resolvePreferredDeployingLinearState(liveIssue);
|
|
165
|
+
// 6. Approved / admitted to the merge queue → In Merge Queue.
|
|
166
|
+
if (isInMergeQueue(issue)) {
|
|
167
|
+
return resolvePreferredMergeQueueLinearState(liveIssue);
|
|
149
168
|
}
|
|
150
|
-
|
|
169
|
+
// 7. Pre-review-feedback implementation work (incl. a draft PR) →
|
|
170
|
+
// Implementing.
|
|
171
|
+
if (isImplementing(issue, trackedIssue)) {
|
|
172
|
+
return resolvePreferredImplementingLinearState(liveIssue);
|
|
173
|
+
}
|
|
174
|
+
// 8. PR exists and is under review → Reviewing.
|
|
175
|
+
if (isReviewBound(issue)) {
|
|
151
176
|
return resolvePreferredReviewingLinearState(liveIssue);
|
|
152
177
|
}
|
|
153
|
-
|
|
178
|
+
return undefined;
|
|
179
|
+
}
|
|
180
|
+
function normalize(value) {
|
|
181
|
+
const trimmed = value?.trim().toLowerCase();
|
|
182
|
+
return trimmed ? trimmed : undefined;
|
|
183
|
+
}
|
|
184
|
+
function needsHumanAttention(issue, trackedIssue) {
|
|
185
|
+
return issue.factoryState === "awaiting_input"
|
|
186
|
+
|| issue.factoryState === "failed"
|
|
187
|
+
|| issue.factoryState === "escalated"
|
|
188
|
+
|| trackedIssue?.sessionState === "waiting_input"
|
|
189
|
+
|| trackedIssue?.sessionState === "failed";
|
|
190
|
+
}
|
|
191
|
+
// Active code work to address feedback. Durable factory states +
|
|
192
|
+
// changes-requested review verdict — no run-id involvement. Gated on
|
|
193
|
+
// delegation: an undelegated PR (operator paused us) is not being worked
|
|
194
|
+
// by patchrelay, so it must not read as Implementing.
|
|
195
|
+
function isAddressingFeedback(issue) {
|
|
196
|
+
if (issue.delegatedToPatchRelay === false)
|
|
197
|
+
return false;
|
|
198
|
+
return issue.factoryState === "changes_requested"
|
|
199
|
+
|| issue.factoryState === "repairing_ci"
|
|
200
|
+
|| issue.factoryState === "repairing_queue"
|
|
201
|
+
|| normalize(issue.prReviewState) === "changes_requested";
|
|
202
|
+
}
|
|
203
|
+
// Approved and heading to / sitting in the merge queue. Not yet merged
|
|
204
|
+
// (branch 4 catches merged first).
|
|
205
|
+
function isInMergeQueue(issue) {
|
|
206
|
+
return issue.factoryState === "awaiting_queue"
|
|
207
|
+
|| normalize(issue.prReviewState) === "approved";
|
|
208
|
+
}
|
|
209
|
+
// Initial implementation, before review starts. A draft PR still counts
|
|
210
|
+
// as implementing. Gated on delegation so we never claim Implementing
|
|
211
|
+
// for work that isn't ours.
|
|
212
|
+
function isImplementing(issue, trackedIssue) {
|
|
213
|
+
if (issue.delegatedToPatchRelay === false)
|
|
214
|
+
return false;
|
|
215
|
+
if (issue.factoryState === "implementing")
|
|
216
|
+
return true;
|
|
217
|
+
if (issue.factoryState === "delegated") {
|
|
218
|
+
const blocked = (trackedIssue?.blockedByCount ?? 0) > 0;
|
|
219
|
+
return !blocked && trackedIssue?.readyForExecution !== false;
|
|
220
|
+
}
|
|
221
|
+
return issue.prIsDraft === true;
|
|
222
|
+
}
|
|
223
|
+
function isReviewBound(issue) {
|
|
224
|
+
return issue.prNumber !== undefined
|
|
154
225
|
|| Boolean(issue.prUrl)
|
|
155
226
|
|| issue.factoryState === "pr_open"
|
|
156
227
|
|| issue.prReviewState !== undefined
|
|
157
|
-
|| issue.prCheckStatus !== undefined
|
|
158
|
-
|
|
159
|
-
return resolvePreferredReviewLinearState(liveIssue);
|
|
160
|
-
}
|
|
161
|
-
return undefined;
|
|
162
|
-
}
|
|
163
|
-
function isApprovedAndGreen(prReviewState, prCheckStatus) {
|
|
164
|
-
const normalizedReview = prReviewState?.trim().toLowerCase();
|
|
165
|
-
const normalizedChecks = prCheckStatus?.trim().toLowerCase();
|
|
166
|
-
return normalizedReview === "approved" && (normalizedChecks === "success" || normalizedChecks === "passed");
|
|
228
|
+
|| issue.prCheckStatus !== undefined
|
|
229
|
+
|| hasPendingReviewQuillVerdict(issue.lastGitHubCiSnapshotJson);
|
|
167
230
|
}
|
|
168
231
|
function hasPendingReviewQuillVerdict(snapshotJson) {
|
|
169
232
|
if (!snapshotJson)
|
package/dist/linear-workflow.js
CHANGED
|
@@ -59,16 +59,37 @@ export function resolvePreferredReviewingLinearState(issue) {
|
|
|
59
59
|
fallback: resolvePreferredReviewLinearState(issue),
|
|
60
60
|
});
|
|
61
61
|
}
|
|
62
|
+
// The pre-merge "approved, awaiting/undergoing landing" phase. Covers a
|
|
63
|
+
// PR that is queued, being tested in the speculative branch, or actively
|
|
64
|
+
// merging — i.e. everything the merge queue owns up to (but not past)
|
|
65
|
+
// the merge. NOT post-merge: that is `resolvePreferredDeployingLinearState`.
|
|
66
|
+
// Without a dedicated queue state, collapses to the reviewing state (and
|
|
67
|
+
// the `queued-for-deploy` label disambiguates — see state-sync).
|
|
68
|
+
export function resolvePreferredMergeQueueLinearState(issue) {
|
|
69
|
+
return resolvePreferredLinearState(issue, {
|
|
70
|
+
names: ["in merge queue", "merge queue", "in queue", "queued", "queue", "merging", "landing", "ready to merge"],
|
|
71
|
+
types: ["started"],
|
|
72
|
+
fallback: resolvePreferredLinearState(issue, {
|
|
73
|
+
names: ["in merge queue", "merge queue", "queued", "ready to merge", "ready to deploy", "ready for deploy", "to deploy", "merge"],
|
|
74
|
+
types: ["unstarted"],
|
|
75
|
+
fallback: resolvePreferredReviewingLinearState(issue),
|
|
76
|
+
}),
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
// Unstarted deploy column, used as a fallback by the started variant.
|
|
62
80
|
export function resolvePreferredDeployLinearState(issue) {
|
|
63
81
|
return resolvePreferredLinearState(issue, {
|
|
64
|
-
names: ["deploy", "
|
|
82
|
+
names: ["deploy", "to deploy", "ready to ship"],
|
|
65
83
|
types: ["unstarted"],
|
|
66
|
-
fallback:
|
|
84
|
+
fallback: resolvePreferredMergeQueueLinearState(issue),
|
|
67
85
|
});
|
|
68
86
|
}
|
|
87
|
+
// Strictly POST-merge: the change is on main and the deploy workflow is
|
|
88
|
+
// running. "merging" lives in the merge-queue phase, not here. Without a
|
|
89
|
+
// dedicated deploy state, collapses back to the merge-queue state.
|
|
69
90
|
export function resolvePreferredDeployingLinearState(issue) {
|
|
70
91
|
return resolvePreferredLinearState(issue, {
|
|
71
|
-
names: ["deploying", "
|
|
92
|
+
names: ["deploying", "deployment", "in deploy", "shipping", "releasing", "rollout"],
|
|
72
93
|
types: ["started"],
|
|
73
94
|
fallback: resolvePreferredDeployLinearState(issue),
|
|
74
95
|
});
|
|
@@ -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
|
-
}
|