patchrelay 0.78.0 → 0.79.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.
@@ -121,14 +121,8 @@ function summarizeRelationEntries(entries, options) {
121
121
  return lines;
122
122
  }
123
123
  function buildIssueTopology(context) {
124
- const unresolvedBlockers = Array.isArray(context?.unresolvedBlockers)
125
- ? context.unresolvedBlockers.filter((entry) => Boolean(entry) && typeof entry === "object")
126
- : [];
127
- const childIssues = Array.isArray(context?.childIssues)
128
- ? context.childIssues.filter((entry) => Boolean(entry) && typeof entry === "object")
129
- : Array.isArray(context?.trackedDependents)
130
- ? context.trackedDependents.filter((entry) => Boolean(entry) && typeof entry === "object")
131
- : [];
124
+ const unresolvedBlockers = context?.unresolvedBlockers ?? [];
125
+ const childIssues = context?.childIssues ?? context?.trackedDependents ?? [];
132
126
  if (unresolvedBlockers.length === 0 && childIssues.length === 0) {
133
127
  return [];
134
128
  }
@@ -165,14 +159,8 @@ function buildConstraints(issue, context) {
165
159
  ].join("\n");
166
160
  }
167
161
  function buildOrchestrationConstraints(context) {
168
- const unresolvedBlockers = Array.isArray(context?.unresolvedBlockers)
169
- ? context.unresolvedBlockers.filter((entry) => Boolean(entry) && typeof entry === "object")
170
- : [];
171
- const childIssues = Array.isArray(context?.childIssues)
172
- ? context.childIssues.filter((entry) => Boolean(entry) && typeof entry === "object")
173
- : Array.isArray(context?.trackedDependents)
174
- ? context.trackedDependents.filter((entry) => Boolean(entry) && typeof entry === "object")
175
- : [];
162
+ const unresolvedBlockers = context?.unresolvedBlockers ?? [];
163
+ const childIssues = context?.childIssues ?? context?.trackedDependents ?? [];
176
164
  return [
177
165
  "## Constraints",
178
166
  "",
@@ -201,13 +189,11 @@ function buildOrchestrationConstraints(context) {
201
189
  ].join("\n");
202
190
  }
203
191
  function buildHumanContextLines(context) {
204
- const promptContext = typeof context?.promptContext === "string" ? context.promptContext.trim() : "";
205
- const latestPrompt = typeof context?.promptBody === "string" ? context.promptBody.trim() : "";
206
- const operatorPrompt = typeof context?.operatorPrompt === "string" ? context.operatorPrompt.trim() : "";
207
- const userComment = typeof context?.userComment === "string" ? context.userComment.trim() : "";
208
- const linearAgentActivityContext = typeof context?.linearAgentActivityContext === "string"
209
- ? context.linearAgentActivityContext.trim()
210
- : "";
192
+ const promptContext = context?.promptContext?.trim() ?? "";
193
+ const latestPrompt = context?.promptBody?.trim() ?? "";
194
+ const operatorPrompt = context?.operatorPrompt?.trim() ?? "";
195
+ const userComment = context?.userComment?.trim() ?? "";
196
+ const linearAgentActivityContext = context?.linearAgentActivityContext?.trim() ?? "";
211
197
  const lines = [];
212
198
  if (promptContext) {
213
199
  lines.push("Linear session context:", promptContext, "");
@@ -235,35 +221,28 @@ function resolveRequestedChangesMode(runType, context) {
235
221
  : "address_review_feedback";
236
222
  }
237
223
  function readReviewFixComments(context) {
238
- const raw = context?.reviewComments;
239
- if (!Array.isArray(raw)) {
240
- return [];
241
- }
242
224
  const comments = [];
243
- for (const entry of raw) {
244
- if (!entry || typeof entry !== "object")
245
- continue;
246
- const record = entry;
247
- const body = typeof record.body === "string" ? record.body.trim() : "";
225
+ for (const record of context?.reviewComments ?? []) {
226
+ const body = record.body?.trim() ?? "";
248
227
  if (!body)
249
228
  continue;
250
229
  comments.push({
251
230
  body,
252
- ...(typeof record.path === "string" ? { path: record.path } : {}),
253
- ...(typeof record.line === "number" ? { line: record.line } : {}),
254
- ...(typeof record.side === "string" ? { side: record.side } : {}),
255
- ...(typeof record.startLine === "number" ? { startLine: record.startLine } : {}),
256
- ...(typeof record.startSide === "string" ? { startSide: record.startSide } : {}),
257
- ...(typeof record.url === "string" ? { url: record.url } : {}),
258
- ...(typeof record.authorLogin === "string" ? { authorLogin: record.authorLogin } : {}),
231
+ ...(record.path !== undefined ? { path: record.path } : {}),
232
+ ...(record.line !== undefined ? { line: record.line } : {}),
233
+ ...(record.side !== undefined ? { side: record.side } : {}),
234
+ ...(record.startLine !== undefined ? { startLine: record.startLine } : {}),
235
+ ...(record.startSide !== undefined ? { startSide: record.startSide } : {}),
236
+ ...(record.url !== undefined ? { url: record.url } : {}),
237
+ ...(record.authorLogin !== undefined ? { authorLogin: record.authorLogin } : {}),
259
238
  });
260
239
  }
261
240
  return comments;
262
241
  }
263
242
  function buildStructuredReviewContext(context) {
264
- const reviewId = typeof context?.reviewId === "number" ? context.reviewId : undefined;
265
- const reviewCommitId = typeof context?.reviewCommitId === "string" ? context.reviewCommitId : undefined;
266
- const reviewUrl = typeof context?.reviewUrl === "string" ? context.reviewUrl : undefined;
243
+ const reviewId = context?.reviewId;
244
+ const reviewCommitId = context?.reviewCommitId;
245
+ const reviewUrl = context?.reviewUrl;
267
246
  const reviewComments = readReviewFixComments(context);
268
247
  const degraded = context?.reviewContextDegraded === true;
269
248
  if (!degraded && !reviewId && !reviewCommitId && !reviewUrl && reviewComments.length === 0) {
@@ -271,9 +250,8 @@ function buildStructuredReviewContext(context) {
271
250
  }
272
251
  const lines = ["## Structured Review Context", ""];
273
252
  if (degraded) {
274
- const reason = typeof context?.reviewContextDegradedReason === "string" && context.reviewContextDegradedReason.trim()
275
- ? context.reviewContextDegradedReason.trim()
276
- : "GitHub requested-changes context could not be refreshed before launch.";
253
+ const reason = context?.reviewContextDegradedReason?.trim()
254
+ || "GitHub requested-changes context could not be refreshed before launch.";
277
255
  lines.push("GitHub review context refresh: degraded", reason, "Do not assume cached review details are current. Re-read the PR review in GitHub before making review-fix changes.");
278
256
  }
279
257
  if (reviewId !== undefined)
@@ -311,17 +289,15 @@ function buildRequestedChangesContext(runType, context) {
311
289
  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.");
312
290
  }
313
291
  else {
314
- const reviewer = typeof context?.reviewerName === "string" ? context.reviewerName : undefined;
315
- const reviewBody = typeof context?.reviewBody === "string" ? context.reviewBody.trim() : "";
292
+ const reviewer = context?.reviewerName;
293
+ const reviewBody = context?.reviewBody?.trim() ?? "";
316
294
  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.", "For each review comment, identify the resource, epoch, or token it touches (e.g. session, capture, route, persistence handle, in-flight turn id), enumerate the other transitions that share that same resource, and verify each one before pushing — not just the exact path called out. If you find an adjacent transition that violates the same invariant, fix it in this iteration rather than waiting for the reviewer to surface it next round.", reviewer ? `Reviewer: ${reviewer}` : "", reviewBody ? `Review summary:\n${reviewBody}` : "");
317
295
  appendStructuredReviewContext(lines, context);
318
296
  }
319
297
  return lines.join("\n").trim();
320
298
  }
321
299
  function buildCiRepairContext(context) {
322
- const snapshot = context?.ciSnapshot && typeof context.ciSnapshot === "object"
323
- ? context.ciSnapshot
324
- : undefined;
300
+ const snapshot = context?.ciSnapshot;
325
301
  return [
326
302
  "Settled CI failure on the existing PR branch.",
327
303
  "Goal: restore CI readiness and push a branch that is likely to pass the next full CI run.",
@@ -346,31 +322,26 @@ function buildCiRepairContext(context) {
346
322
  ].filter(Boolean).join("\n");
347
323
  }
348
324
  function appendQueueRepairContext(lines, context) {
349
- const queueContext = context?.mergeQueueContext;
350
- if (!queueContext || typeof queueContext !== "object") {
325
+ const record = context?.mergeQueueContext;
326
+ if (!record) {
351
327
  return;
352
328
  }
353
- const record = queueContext;
354
- const conflictingFiles = Array.isArray(record.conflictingFiles)
355
- ? record.conflictingFiles.filter((entry) => typeof entry === "string" && entry.trim().length > 0)
356
- : [];
357
- const operatorHints = Array.isArray(record.operatorHints)
358
- ? record.operatorHints.filter((entry) => typeof entry === "string" && entry.trim().length > 0)
359
- : [];
329
+ const conflictingFiles = (record.conflictingFiles ?? []).filter((entry) => entry.trim().length > 0);
330
+ const operatorHints = (record.operatorHints ?? []).filter((entry) => entry.trim().length > 0);
360
331
  lines.push("## Merge Queue Context", "");
361
- if (typeof record.baseBranch === "string") {
332
+ if (record.baseBranch !== undefined) {
362
333
  lines.push(`Base branch: ${record.baseBranch}`);
363
334
  }
364
- if (typeof record.baseSha === "string") {
335
+ if (record.baseSha !== undefined) {
365
336
  lines.push(`Base SHA at eviction: ${record.baseSha}`);
366
337
  }
367
- if (typeof record.mergeCommitSha === "string") {
338
+ if (record.mergeCommitSha !== undefined) {
368
339
  lines.push(`Synthetic merge commit SHA: ${record.mergeCommitSha}`);
369
340
  }
370
- if (typeof record.checkRunUrl === "string") {
341
+ if (record.checkRunUrl !== undefined) {
371
342
  lines.push(`Steward check run: ${record.checkRunUrl}`);
372
343
  }
373
- if (typeof record.incidentSummary === "string") {
344
+ if (record.incidentSummary !== undefined) {
374
345
  lines.push(`Steward summary: ${record.incidentSummary}`);
375
346
  }
376
347
  if (conflictingFiles.length > 0) {
@@ -391,11 +362,9 @@ function buildQueueRepairContext(context) {
391
362
  }
392
363
  function buildFollowUpContextLines(issue, runType, context) {
393
364
  const prContext = derivePrDisplayContext(issue);
394
- const wakeReason = typeof context?.wakeReason === "string" ? context.wakeReason : undefined;
395
- const followUps = Array.isArray(context?.followUps) ? context.followUps : [];
396
- const followUpLines = followUps
397
- .filter((entry) => Boolean(entry) && typeof entry === "object")
398
- .map((entry) => `${String(entry.type ?? "follow_up")} from ${String(entry.author ?? "unknown")}: ${String(entry.text ?? "").trim()}`.trim())
365
+ const wakeReason = context?.wakeReason;
366
+ const followUpLines = (context?.followUps ?? [])
367
+ .map((entry) => `${entry.type ?? "follow_up"} from ${entry.author ?? "unknown"}: ${(entry.text ?? "").trim()}`.trim())
399
368
  .filter((line) => !line.endsWith(":"));
400
369
  const lines = [];
401
370
  const turnReason = wakeReason === "direct_reply"
@@ -418,17 +387,15 @@ function buildFollowUpContextLines(issue, runType, context) {
418
387
  ? "A human follow-up comment arrived after the previous turn."
419
388
  : `Continue the existing ${runType} run from the latest issue state.`;
420
389
  lines.push(`Turn reason: ${turnReason}`);
421
- if (wakeReason === "completion_check_continue" && typeof context?.completionCheckSummary === "string" && context.completionCheckSummary.trim()) {
390
+ if (wakeReason === "completion_check_continue" && context?.completionCheckSummary?.trim()) {
422
391
  lines.push(`Completion check summary: ${context.completionCheckSummary.trim()}`);
423
392
  }
424
393
  if (context?.preserveDirtyWorktree === true) {
425
394
  lines.push("", "Unpublished local work:", "PatchRelay detected that the previous repair turn ended with uncommitted changes in this worktree.", "Do not reset, clean, stash-drop, or otherwise discard the current worktree. Inspect the existing local diff, keep the intended in-scope repair, then commit and push a fresh PR head.");
426
- if (typeof context.dirtyWorktreeSummary === "string" && context.dirtyWorktreeSummary.trim()) {
395
+ if (context.dirtyWorktreeSummary?.trim()) {
427
396
  lines.push(`Dirty worktree summary: ${context.dirtyWorktreeSummary.trim()}`);
428
397
  }
429
- const changedPaths = Array.isArray(context.dirtyWorktreeChangedPaths)
430
- ? context.dirtyWorktreeChangedPaths.filter((entry) => typeof entry === "string" && entry.trim().length > 0)
431
- : [];
398
+ const changedPaths = (context.dirtyWorktreeChangedPaths ?? []).filter((entry) => entry.trim().length > 0);
432
399
  if (changedPaths.length > 0) {
433
400
  lines.push("Changed paths:");
434
401
  changedPaths.slice(0, 12).forEach((entry) => lines.push(`- ${entry}`));
@@ -443,16 +410,16 @@ function buildFollowUpContextLines(issue, runType, context) {
443
410
  }
444
411
  if (context?.replacementPrRequired === true) {
445
412
  lines.push("", "Previous PR facts:");
446
- if (typeof context.previousPrNumber === "number") {
413
+ if (context.previousPrNumber !== undefined) {
447
414
  lines.push(`Previous PR: #${context.previousPrNumber} (replacement PR needed)`);
448
415
  }
449
- if (typeof context.previousPrUrl === "string") {
416
+ if (context.previousPrUrl !== undefined) {
450
417
  lines.push(`Previous PR URL: ${context.previousPrUrl}`);
451
418
  }
452
- if (typeof context.previousPrState === "string") {
419
+ if (context.previousPrState !== undefined) {
453
420
  lines.push(`Previous PR state: ${context.previousPrState}`);
454
421
  }
455
- if (typeof context.previousPrHeadSha === "string") {
422
+ if (context.previousPrHeadSha !== undefined) {
456
423
  lines.push(`Previous PR head SHA: ${context.previousPrHeadSha}`);
457
424
  }
458
425
  lines.push("Create a fresh replacement PR for the new requested changes; do not mutate or republish the completed PR.");
@@ -476,7 +443,7 @@ function buildFollowUpContextLines(issue, runType, context) {
476
443
  : "";
477
444
  lines.push("", prHeading, `Fact freshness: ${context?.githubFactsFresh === true
478
445
  ? "refreshed immediately before this turn was created."
479
- : "may now be stale; refresh before making irreversible decisions."}`, prLine, issue.prHeadSha ? `Current relevant head SHA: ${issue.prHeadSha}` : "", issue.prReviewState ? `Current review state: ${issue.prReviewState}` : "", typeof context?.mergeStateStatus === "string" ? `Merge state against ${String(context?.baseBranch ?? "main")}: ${String(context.mergeStateStatus)}` : "");
446
+ : "may now be stale; refresh before making irreversible decisions."}`, prLine, issue.prHeadSha ? `Current relevant head SHA: ${issue.prHeadSha}` : "", issue.prReviewState ? `Current review state: ${issue.prReviewState}` : "", context?.mergeStateStatus !== undefined ? `Merge state against ${context?.baseBranch ?? "main"}: ${context.mergeStateStatus}` : "");
480
447
  }
481
448
  return lines.filter(Boolean);
482
449
  }
@@ -670,7 +637,7 @@ function shouldBuildFollowUpPrompt(runType, context) {
670
637
  return true;
671
638
  if (runType !== "implementation")
672
639
  return true;
673
- const wakeReason = typeof context?.wakeReason === "string" ? context.wakeReason : undefined;
640
+ const wakeReason = context?.wakeReason;
674
641
  return Boolean(wakeReason && wakeReason !== "delegated");
675
642
  }
676
643
  export function resolvePromptLayers(config, runType) {
@@ -1,5 +1,6 @@
1
1
  import { resolveMergeQueueProtocol } from "./merge-queue-protocol.js";
2
2
  import { buildRepairWakeDedupeKey } from "./reactive-wake-keys.js";
3
+ import { serializeRunContext } from "./run-context.js";
3
4
  import { execCommand } from "./utils.js";
4
5
  const WRITER = "queue-health-monitor";
5
6
  const QUEUE_HEALTH_GRACE_MS = 120_000;
@@ -10,8 +11,8 @@ const QUEUE_HEALTH_PROBE_FAILURE_COOLDOWN_MS = 300_000;
10
11
  const IN_REVIEW_STUCK_THRESHOLD_MS = 30 * 60 * 1000;
11
12
  const IN_REVIEW_STUCK_FEED_COOLDOWN_MS = 30 * 60 * 1000;
12
13
  function isDuplicateProbe(issue, context) {
13
- const signature = typeof context?.failureSignature === "string" ? context.failureSignature : undefined;
14
- const headSha = typeof context?.failureHeadSha === "string" ? context.failureHeadSha : undefined;
14
+ const signature = context?.failureSignature;
15
+ const headSha = context?.failureHeadSha;
15
16
  if (!signature)
16
17
  return false;
17
18
  if (context?.requiresFreshHead === true)
@@ -176,7 +177,7 @@ export class QueueHealthMonitor {
176
177
  const probed = probedCommit.outcome === "applied" ? probedCommit.issue : issue;
177
178
  this.advancer.wakeDispatcher.recordEventAndDispatch(issue.projectId, issue.linearIssueId, {
178
179
  eventType: "merge_steward_incident",
179
- eventJson: JSON.stringify(pendingRunContext),
180
+ eventJson: serializeRunContext(pendingRunContext, "queue health repair context"),
180
181
  dedupeKey: buildRepairWakeDedupeKey({
181
182
  scope: "queue_health",
182
183
  runType: "queue_repair",