patchrelay 0.59.1 → 0.61.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.59.1",
4
- "commit": "1e520e0829e3",
5
- "builtAt": "2026-05-02T23:50:20.540Z"
3
+ "version": "0.61.0",
4
+ "commit": "d6c22120c20c",
5
+ "builtAt": "2026-05-04T22:20:57.540Z"
6
6
  }
@@ -336,6 +336,10 @@ async function evaluateGitHubIssueHealth(snapshot, config, runCommand, reviewQui
336
336
  if (issue.delegatedToPatchRelay
337
337
  && gateCheckStatus === "failure"
338
338
  && issue.factoryState !== "repairing_ci"
339
+ // Plan §6.1 / §4.3: branch CI failures while In Deploy are
340
+ // metadata only — the lander's spec CI is the gate. Don't flag
341
+ // these as a missing-ci-repair condition.
342
+ && issue.factoryState !== "awaiting_queue"
339
343
  && issue.activeRunId === undefined
340
344
  && ageMs >= RECONCILIATION_GRACE_MS) {
341
345
  return {
@@ -58,9 +58,29 @@ const TRANSITION_RULES = [
58
58
  { event: "check_passed",
59
59
  guard: (s) => s === "repairing_ci",
60
60
  to: (_, ctx) => ctx.prReviewState === "approved" ? "awaiting_queue" : "pr_open" },
61
- // CI failure when no run is active triggers repair.
61
+ // CI failure when no run is active. Plan §4.3: classification by
62
+ // failureSource determines whether we route to queue or branch repair.
63
+ // While In Deploy (`awaiting_queue`), branch CI failures are metadata
64
+ // only — the lander handles the spec; we don't initiate a ci_repair.
62
65
  { event: "check_failed",
63
- guard: (s, ctx) => isOpen(s) && ctx.activeRunId === undefined,
66
+ guard: (s, ctx) => isOpen(s)
67
+ && ctx.activeRunId === undefined
68
+ && ctx.failureSource === "queue_eviction",
69
+ to: "repairing_queue" },
70
+ { event: "check_failed",
71
+ guard: (s, ctx) => isOpen(s)
72
+ && ctx.activeRunId === undefined
73
+ && ctx.failureSource === "branch_ci"
74
+ && s !== "awaiting_queue",
75
+ to: "repairing_ci" },
76
+ // Backward-compat fallback for callers that haven't classified the
77
+ // failure (older code paths or test fixtures). Behaves like today's
78
+ // single rule but still skips while In Deploy.
79
+ { event: "check_failed",
80
+ guard: (s, ctx) => isOpen(s)
81
+ && ctx.activeRunId === undefined
82
+ && ctx.failureSource === undefined
83
+ && s !== "awaiting_queue",
64
84
  to: "repairing_ci" },
65
85
  // pr_synchronize: no rule → no transition (resets counters only)
66
86
  // merge_group events: not used — merge queue is handled by external steward
@@ -1,6 +1,5 @@
1
1
  import { resolveFactoryStateFromGitHub } from "./factory-state.js";
2
2
  import { resolveMergeQueueProtocol, } from "./merge-queue-protocol.js";
3
- import { isIssueTerminal } from "./pr-state.js";
4
3
  const DEFAULT_GATE_CHECK_NAMES = ["verify", "tests"];
5
4
  /**
6
5
  * GitHub sends both check_run and check_suite completion events.
@@ -84,16 +83,16 @@ export function resolveGitHubFactoryStateForEvent(issue, event, project) {
84
83
  && (event.prState === "open" || event.prNumber !== undefined)
85
84
  ? "pr_open"
86
85
  : issue.factoryState;
87
- if (event.triggerEvent === "check_failed"
88
- && isQueueEvictionFailure(issue, event, project)
89
- && issue.prState === "open"
90
- && issue.activeRunId === undefined
91
- && !isIssueTerminal(issue)) {
92
- return "repairing_queue";
93
- }
86
+ // Classify check_failed events so the rule table can route them.
87
+ // The duplicate short-circuit that lived here before is gone — the
88
+ // table now handles queue_eviction via failureSource (plan §4.3).
89
+ const failureSource = event.triggerEvent === "check_failed"
90
+ ? (isQueueEvictionFailure(issue, event, project) ? "queue_eviction" : "branch_ci")
91
+ : undefined;
94
92
  const resolved = resolveFactoryStateFromGitHub(event.triggerEvent, effectiveCurrentState, {
95
93
  prReviewState: issue.prReviewState,
96
94
  activeRunId: issue.activeRunId,
95
+ failureSource,
97
96
  });
98
97
  if (resolved !== undefined) {
99
98
  return resolved;
@@ -49,6 +49,22 @@ export async function maybeEnqueueGitHubReactiveRun(params) {
49
49
  }
50
50
  async function handleCheckFailedEvent(params) {
51
51
  const { db, logger, feed, enqueueIssue, issue, event, project, failureContextResolver } = params;
52
+ // Plan §4.3: while In Deploy (`awaiting_queue`), branch CI is metadata
53
+ // only — the lander owns admission, and its spec CI on the integration
54
+ // tree is the gate. Queue eviction failures still flow through (they're
55
+ // how the lander signals a real integration regression).
56
+ if (issue.factoryState === "awaiting_queue" && !isQueueEvictionFailure(issue, event, project)) {
57
+ feed?.publish({
58
+ level: "info",
59
+ kind: "github",
60
+ issueKey: issue.issueKey,
61
+ projectId: issue.projectId,
62
+ stage: issue.factoryState,
63
+ status: "branch_ci_metadata_in_deploy",
64
+ summary: `Ignored ${event.checkName ?? "branch CI"} failure while In Deploy; lander owns admission`,
65
+ });
66
+ return;
67
+ }
52
68
  if (isQueueEvictionFailure(issue, event, project)) {
53
69
  const queueRepairContext = buildQueueRepairContextFromEvent(event);
54
70
  const failureContext = buildGitHubQueueFailureContext(event, project, queueRepairContext);
@@ -271,7 +271,7 @@ function buildStructuredReviewContext(context) {
271
271
  lines.push("No inline review comments were captured for this review.");
272
272
  return lines.join("\n");
273
273
  }
274
- lines.push(`Inline review comments captured: ${reviewComments.length}`, "Resolve each comment below or verify it is already fixed on the current head before you stop.", "A requested-changes turn is only complete if you push a newer PR head or deliberately escalate because you are blocked.", "");
274
+ lines.push(`Inline review comments captured: ${reviewComments.length}`, "Resolve each comment below or verify it is already fixed on the current head before you stop.", "Complete the turn either by pushing a newer PR head with the fix, or — if your reviewer-pass produces only comments, test wording, or PR-body changes — by editing the PR body via `gh pr edit` instead of pushing. Do not push a commit that produces a patch-id-equivalent diff just to make the fix unmistakable.", "If you are blocked, deliberately escalate instead of pushing.", "");
275
275
  for (const comment of reviewComments) {
276
276
  const location = comment.path
277
277
  ? `${comment.path}${comment.line !== undefined ? `:${comment.line}` : ""}${comment.side ? ` (${comment.side})` : ""}`
@@ -293,12 +293,12 @@ function buildRequestedChangesContext(runType, context) {
293
293
  const mode = resolveRequestedChangesMode(runType, context);
294
294
  const lines = [];
295
295
  if (mode === "branch_upkeep") {
296
- lines.push("Branch upkeep is required on the existing PR branch.", "Goal: restore merge readiness on the current branch and push a newer head without regressing review or CI readiness.");
296
+ lines.push("Branch upkeep is required on the existing PR branch.", "Goal: restore merge readiness on the current branch. Push a newer head only when the work actually changes the diff against the base; do not republish a patch-id-equivalent head.");
297
297
  }
298
298
  else {
299
299
  const reviewer = typeof context?.reviewerName === "string" ? context.reviewerName : undefined;
300
300
  const reviewBody = typeof context?.reviewBody === "string" ? context.reviewBody.trim() : "";
301
- lines.push("Requested changes on the existing PR branch.", "Goal: restore review readiness and push a newer head on the current PR branch.", "Address the real concern behind the feedback and verify nearby invariants in the touched flow before you publish.", reviewer ? `Reviewer: ${reviewer}` : "", reviewBody ? `Review summary:\n${reviewBody}` : "");
301
+ lines.push("Requested changes on the existing PR branch.", "Goal: restore review readiness on the current PR branch. Push a newer head only when the fix actually changes the diff; if the reviewer-pass produces only comments, test wording, or PR-body changes, edit the PR body via `gh pr edit` instead.", "Address the real concern behind the feedback and verify nearby invariants in the touched flow before you publish.", reviewer ? `Reviewer: ${reviewer}` : "", reviewBody ? `Review summary:\n${reviewBody}` : "");
302
302
  appendStructuredReviewContext(lines, context);
303
303
  }
304
304
  return lines.join("\n").trim();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "patchrelay",
3
- "version": "0.59.1",
3
+ "version": "0.61.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "repository": {