patchrelay 0.52.0 → 0.52.2

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.52.0",
4
- "commit": "818b35cc9b05",
5
- "builtAt": "2026-04-22T14:41:18.426Z"
3
+ "version": "0.52.2",
4
+ "commit": "44bb9d45502d",
5
+ "builtAt": "2026-04-22T17:29:32.600Z"
6
6
  }
package/dist/cli/data.js CHANGED
@@ -6,6 +6,7 @@ import { extractCompletionCheck } from "../completion-check.js";
6
6
  import { getThreadTurns } from "../codex-thread-utils.js";
7
7
  import { PatchRelayDatabase } from "../db.js";
8
8
  import { buildManualRetryAttemptReset, resolveRetryTarget } from "../manual-issue-actions.js";
9
+ import { buildOperatorRetryEvent } from "../operator-retry-event.js";
9
10
  import { WorktreeManager } from "../worktree-manager.js";
10
11
  import { parseDelegationObservedPayload, parseRunReleasedAuthorityPayload } from "../delegation-audit.js";
11
12
  import { CliOperatorApiClient } from "./operator-client.js";
@@ -386,61 +387,10 @@ export class CliDataAccess extends CliOperatorApiClient {
386
387
  };
387
388
  }
388
389
  appendRetryWake(issue, runType) {
389
- if (runType === "queue_repair") {
390
- const queueIncident = parseObjectJson(issue.lastQueueIncidentJson);
391
- const failureContext = parseObjectJson(issue.lastGitHubFailureContextJson);
392
- this.db.issueSessions.appendIssueSessionEventRespectingActiveLease(issue.projectId, issue.linearIssueId, {
393
- projectId: issue.projectId,
394
- linearIssueId: issue.linearIssueId,
395
- eventType: "merge_steward_incident",
396
- eventJson: JSON.stringify({
397
- ...(queueIncident ?? {}),
398
- ...(failureContext ?? {}),
399
- source: "operator_retry",
400
- }),
401
- dedupeKey: `operator_retry:queue_repair:${issue.linearIssueId}:${issue.prHeadSha ?? issue.lastGitHubFailureHeadSha ?? "unknown-sha"}`,
402
- });
403
- return;
404
- }
405
- if (runType === "ci_repair") {
406
- const failureContext = parseObjectJson(issue.lastGitHubFailureContextJson);
407
- this.db.issueSessions.appendIssueSessionEventRespectingActiveLease(issue.projectId, issue.linearIssueId, {
408
- projectId: issue.projectId,
409
- linearIssueId: issue.linearIssueId,
410
- eventType: "settled_red_ci",
411
- eventJson: JSON.stringify({
412
- ...(failureContext ?? {}),
413
- source: "operator_retry",
414
- }),
415
- dedupeKey: `operator_retry:ci_repair:${issue.linearIssueId}:${issue.lastGitHubFailureSignature ?? issue.prHeadSha ?? "unknown-sha"}`,
416
- });
417
- return;
418
- }
419
- if (runType === "review_fix" || runType === "branch_upkeep") {
420
- this.db.issueSessions.appendIssueSessionEventRespectingActiveLease(issue.projectId, issue.linearIssueId, {
421
- projectId: issue.projectId,
422
- linearIssueId: issue.linearIssueId,
423
- eventType: "review_changes_requested",
424
- eventJson: JSON.stringify({
425
- reviewBody: runType === "branch_upkeep"
426
- ? "Operator requested retry of branch upkeep after requested changes."
427
- : "Operator requested retry of review-fix work.",
428
- ...(runType === "branch_upkeep" ? { branchUpkeepRequired: true, wakeReason: "branch_upkeep" } : {}),
429
- source: "operator_retry",
430
- }),
431
- dedupeKey: `operator_retry:${runType}:${issue.linearIssueId}:${issue.prHeadSha ?? "unknown-sha"}`,
432
- });
433
- return;
434
- }
435
390
  this.db.issueSessions.appendIssueSessionEventRespectingActiveLease(issue.projectId, issue.linearIssueId, {
436
391
  projectId: issue.projectId,
437
392
  linearIssueId: issue.linearIssueId,
438
- eventType: "delegated",
439
- eventJson: JSON.stringify({
440
- promptContext: "Operator requested retry of PatchRelay work.",
441
- source: "operator_retry",
442
- }),
443
- dedupeKey: `operator_retry:implementation:${issue.linearIssueId}`,
393
+ ...buildOperatorRetryEvent(issue, runType),
444
394
  });
445
395
  }
446
396
  list(options) {
@@ -18,32 +18,33 @@ function deriveProgressFactFromCompletedItem(rawItem, issue) {
18
18
  if (item.type !== "agentMessage" || typeof item.text !== "string") {
19
19
  return undefined;
20
20
  }
21
- const body = compactOperatorSentence(item.text);
22
- if (!body) {
21
+ const fullBody = sanitizeOperatorFacingText(item.text)?.replace(/\s+/g, " ").trim();
22
+ if (!fullBody) {
23
23
  return undefined;
24
24
  }
25
- if (looksLikeVerification(body)) {
25
+ const ephemeralBody = compactOperatorSentence(fullBody) ?? fullBody;
26
+ if (looksLikeVerification(fullBody)) {
26
27
  return {
27
28
  kind: "verification_started",
28
- meaningKey: `verification:${normalizeMeaningKey(body)}`,
29
- ephemeralContent: { type: "thought", body },
30
- historyContent: { type: "thought", body },
29
+ meaningKey: `verification:${normalizeMeaningKey(fullBody)}`,
30
+ ephemeralContent: { type: "thought", body: ephemeralBody },
31
+ historyContent: { type: "thought", body: fullBody },
31
32
  };
32
33
  }
33
- if (looksLikePublishing(body)) {
34
+ if (looksLikePublishing(fullBody)) {
34
35
  return {
35
36
  kind: "publishing_started",
36
- meaningKey: `publishing:${normalizeMeaningKey(body)}`,
37
- ephemeralContent: { type: "thought", body },
38
- historyContent: { type: "thought", body },
37
+ meaningKey: `publishing:${normalizeMeaningKey(fullBody)}`,
38
+ ephemeralContent: { type: "thought", body: ephemeralBody },
39
+ historyContent: { type: "thought", body: fullBody },
39
40
  };
40
41
  }
41
- if (looksLikeRootCause(body)) {
42
+ if (looksLikeRootCause(fullBody)) {
42
43
  return {
43
44
  kind: "root_cause_found",
44
- meaningKey: `finding:${normalizeMeaningKey(body)}`,
45
- ephemeralContent: { type: "thought", body },
46
- historyContent: { type: "thought", body },
45
+ meaningKey: `finding:${normalizeMeaningKey(fullBody)}`,
46
+ ephemeralContent: { type: "thought", body: ephemeralBody },
47
+ historyContent: { type: "thought", body: fullBody },
47
48
  };
48
49
  }
49
50
  return undefined;
@@ -77,20 +77,35 @@ export function buildRunCompletedActivity(params) {
77
77
  }
78
78
  return undefined;
79
79
  case "review_fix":
80
- return {
81
- type: "response",
82
- body: `Updated ${prLabel} to address review feedback.${detail}`,
83
- };
80
+ return summary
81
+ ? {
82
+ type: "response",
83
+ body: summary,
84
+ }
85
+ : {
86
+ type: "response",
87
+ body: `Updated ${prLabel} to address review feedback.`,
88
+ };
84
89
  case "ci_repair":
85
- return {
86
- type: "response",
87
- body: `Updated ${prLabel} after CI repair.${detail}`,
88
- };
90
+ return summary
91
+ ? {
92
+ type: "response",
93
+ body: summary,
94
+ }
95
+ : {
96
+ type: "response",
97
+ body: `Updated ${prLabel} after CI repair.`,
98
+ };
89
99
  case "queue_repair":
90
- return {
91
- type: "response",
92
- body: `Updated ${prLabel} after merge-queue repair.${detail}`,
93
- };
100
+ return summary
101
+ ? {
102
+ type: "response",
103
+ body: summary,
104
+ }
105
+ : {
106
+ type: "response",
107
+ body: `Updated ${prLabel} after merge-queue repair.`,
108
+ };
94
109
  case "branch_upkeep":
95
110
  return undefined;
96
111
  default: {
@@ -38,9 +38,9 @@ export function buildOperatorRetryEvent(issue, runType, source = "operator_retry
38
38
  return {
39
39
  eventType: "review_changes_requested",
40
40
  eventJson: JSON.stringify({
41
- reviewBody: runType === "branch_upkeep"
42
- ? `${humanizeSource(source)} requested retry of branch upkeep after requested changes.`
43
- : `${humanizeSource(source)} requested retry of review-fix work.`,
41
+ ...(runType === "branch_upkeep"
42
+ ? { reviewBody: `${humanizeSource(source)} requested retry of branch upkeep after requested changes.` }
43
+ : { promptContext: `${humanizeSource(source)} requested retry of review-fix work.` }),
44
44
  ...(runType === "branch_upkeep" ? { branchUpkeepRequired: true, wakeReason: "branch_upkeep" } : {}),
45
45
  source,
46
46
  }),
@@ -1,4 +1,5 @@
1
1
  import { buildReviewFixBranchUpkeepContext, isDirtyMergeStateStatus, isRequestedChangesRunType, readReactivePrSnapshot, } from "./reactive-pr-state.js";
2
+ import { readLatestRequestedChangesReviewContext } from "./remote-pr-review.js";
2
3
  export class ReactiveRunPolicy {
3
4
  config;
4
5
  db;
@@ -136,6 +137,7 @@ export class ReactiveRunPolicy {
136
137
  const snapshot = await readReactivePrSnapshot(this.config, issue.projectId, issue.prNumber);
137
138
  if (!snapshot)
138
139
  return context;
140
+ const refreshedContext = await this.hydrateRequestedChangesContext(issue.projectId, issue.prNumber, snapshot.repoFullName, snapshot.headSha, context);
139
141
  this.upsertIssueIfLeaseHeld(issue.projectId, issue.linearIssueId, {
140
142
  projectId: issue.projectId,
141
143
  linearIssueId: issue.linearIssueId,
@@ -144,12 +146,12 @@ export class ReactiveRunPolicy {
144
146
  ...(snapshot.reviewState ? { prReviewState: snapshot.reviewState } : {}),
145
147
  }, "review-fix wake refresh");
146
148
  if (snapshot.prState !== "open")
147
- return context;
149
+ return refreshedContext;
148
150
  if (snapshot.reviewState && snapshot.reviewState !== "changes_requested")
149
- return context;
151
+ return refreshedContext;
150
152
  if (!isDirtyMergeStateStatus(snapshot.pr.mergeStateStatus))
151
- return context;
152
- return buildReviewFixBranchUpkeepContext(issue.prNumber, snapshot.baseBranch, snapshot.pr, context);
153
+ return refreshedContext;
154
+ return buildReviewFixBranchUpkeepContext(issue.prNumber, snapshot.baseBranch, snapshot.pr, refreshedContext);
153
155
  }
154
156
  catch (error) {
155
157
  this.logger.debug({
@@ -210,4 +212,44 @@ export class ReactiveRunPolicy {
210
212
  }
211
213
  return updated;
212
214
  }
215
+ async hydrateRequestedChangesContext(projectId, prNumber, repoFullName, headSha, context) {
216
+ const merged = {
217
+ ...(context ?? {}),
218
+ ...(headSha ? { headSha } : {}),
219
+ };
220
+ if (hasStructuredReviewContext(merged)) {
221
+ return merged;
222
+ }
223
+ const liveReview = await readLatestRequestedChangesReviewContext(repoFullName, prNumber);
224
+ if (!liveReview) {
225
+ return Object.keys(merged).length > 0 ? merged : context;
226
+ }
227
+ return {
228
+ ...merged,
229
+ ...(liveReview.reviewId !== undefined ? { reviewId: liveReview.reviewId } : {}),
230
+ ...(liveReview.reviewCommitId ? { reviewCommitId: liveReview.reviewCommitId } : {}),
231
+ ...(liveReview.reviewUrl ? { reviewUrl: liveReview.reviewUrl } : {}),
232
+ ...(liveReview.reviewerName ? { reviewerName: liveReview.reviewerName } : {}),
233
+ ...(liveReview.reviewBody ? { reviewBody: liveReview.reviewBody } : {}),
234
+ ...(liveReview.reviewComments ? { reviewComments: liveReview.reviewComments } : {}),
235
+ };
236
+ }
237
+ }
238
+ function hasStructuredReviewContext(context) {
239
+ if (!context)
240
+ return false;
241
+ const reviewBody = typeof context.reviewBody === "string" ? context.reviewBody.trim() : "";
242
+ const isOperatorRetryPlaceholder = typeof context.source === "string"
243
+ && context.source === "operator_retry"
244
+ && /^operator requested retry of review-fix work\.?$/i.test(reviewBody);
245
+ if (isOperatorRetryPlaceholder) {
246
+ return false;
247
+ }
248
+ if (reviewBody)
249
+ return true;
250
+ if (typeof context.reviewUrl === "string" && context.reviewUrl.trim())
251
+ return true;
252
+ if (typeof context.reviewerName === "string" && context.reviewerName.trim())
253
+ return true;
254
+ return Array.isArray(context.reviewComments) && context.reviewComments.length > 0;
213
255
  }
@@ -0,0 +1,60 @@
1
+ import { execCommand, safeJsonParse } from "./utils.js";
2
+ export async function readLatestRequestedChangesReviewContext(repoFullName, prNumber) {
3
+ const [owner, repo] = repoFullName.split("/", 2);
4
+ if (!owner || !repo) {
5
+ return undefined;
6
+ }
7
+ const reviewsResult = await execCommand("gh", [
8
+ "api",
9
+ `repos/${owner}/${repo}/pulls/${prNumber}/reviews?per_page=100`,
10
+ ], { timeoutMs: 10_000 });
11
+ if (reviewsResult.exitCode !== 0) {
12
+ return undefined;
13
+ }
14
+ const reviews = safeJsonParse(reviewsResult.stdout);
15
+ if (!Array.isArray(reviews)) {
16
+ return undefined;
17
+ }
18
+ const review = [...reviews].reverse().find((entry) => entry?.state?.trim().toUpperCase() === "CHANGES_REQUESTED");
19
+ if (!review?.id) {
20
+ return undefined;
21
+ }
22
+ const comments = await readReviewComments(owner, repo, prNumber, review.id);
23
+ return {
24
+ reviewId: review.id,
25
+ ...(typeof review.commit_id === "string" && review.commit_id.trim() ? { reviewCommitId: review.commit_id.trim() } : {}),
26
+ ...(typeof review.html_url === "string" && review.html_url.trim() ? { reviewUrl: review.html_url.trim() } : {}),
27
+ ...(typeof review.body === "string" && review.body.trim() ? { reviewBody: review.body.trim() } : {}),
28
+ ...(typeof review.user?.login === "string" && review.user.login.trim() ? { reviewerName: review.user.login.trim() } : {}),
29
+ ...(comments.length > 0 ? { reviewComments: comments } : {}),
30
+ };
31
+ }
32
+ async function readReviewComments(owner, repo, prNumber, reviewId) {
33
+ const commentsResult = await execCommand("gh", [
34
+ "api",
35
+ `repos/${owner}/${repo}/pulls/${prNumber}/reviews/${reviewId}/comments?per_page=100`,
36
+ ], { timeoutMs: 10_000 });
37
+ if (commentsResult.exitCode !== 0) {
38
+ return [];
39
+ }
40
+ const comments = safeJsonParse(commentsResult.stdout);
41
+ if (!Array.isArray(comments)) {
42
+ return [];
43
+ }
44
+ return comments.flatMap((entry) => {
45
+ const body = typeof entry.body === "string" ? entry.body.trim() : "";
46
+ if (!body) {
47
+ return [];
48
+ }
49
+ return [{
50
+ body,
51
+ ...(typeof entry.path === "string" ? { path: entry.path } : {}),
52
+ ...(typeof entry.line === "number" ? { line: entry.line } : {}),
53
+ ...(typeof entry.side === "string" ? { side: entry.side } : {}),
54
+ ...(typeof entry.start_line === "number" ? { startLine: entry.start_line } : {}),
55
+ ...(typeof entry.start_side === "string" ? { startSide: entry.start_side } : {}),
56
+ ...(typeof entry.html_url === "string" ? { url: entry.html_url } : {}),
57
+ ...(typeof entry.user?.login === "string" ? { authorLogin: entry.user.login } : {}),
58
+ }];
59
+ });
60
+ }
@@ -18,6 +18,16 @@ function shouldCompactThread(issue, threadGeneration, context) {
18
18
  export function shouldReuseIssueThread(params) {
19
19
  return Boolean(params.existingThreadId) && !params.compactThread && params.resumeThread;
20
20
  }
21
+ export function shouldFreshenWorktreeBeforeLaunch(params) {
22
+ if (params.runType === "queue_repair") {
23
+ return false;
24
+ }
25
+ if (params.runType === "review_fix") {
26
+ return params.effectiveContext?.branchUpkeepRequired === true
27
+ || params.effectiveContext?.reviewFixMode === "branch_upkeep";
28
+ }
29
+ return true;
30
+ }
21
31
  export class RunLauncher {
22
32
  config;
23
33
  db;
@@ -125,7 +135,10 @@ export class RunLauncher {
125
135
  });
126
136
  }
127
137
  await this.worktreeManager.resetWorktreeToTrackedBranch(params.worktreePath, params.branchName, params.issue, this.logger);
128
- if (params.runType !== "queue_repair") {
138
+ if (shouldFreshenWorktreeBeforeLaunch({
139
+ runType: params.runType,
140
+ ...(params.effectiveContext ? { effectiveContext: params.effectiveContext } : {}),
141
+ })) {
129
142
  await this.worktreeManager.freshenWorktree(params.worktreePath, params.project, params.issue, this.logger);
130
143
  }
131
144
  const hookEnv = buildHookEnv(params.issue.issueKey ?? params.issue.linearIssueId, params.branchName, params.runType, params.worktreePath);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "patchrelay",
3
- "version": "0.52.0",
3
+ "version": "0.52.2",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "repository": {