patchrelay 0.36.12 → 0.36.14

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.36.12",
4
- "commit": "c36b9a22a748",
5
- "builtAt": "2026-04-09T21:06:16.158Z"
3
+ "version": "0.36.14",
4
+ "commit": "cf7280bff401",
5
+ "builtAt": "2026-04-10T04:01:33.969Z"
6
6
  }
@@ -62,20 +62,51 @@ export function buildRunStartedActivity(runType) {
62
62
  }
63
63
  }
64
64
  export function buildRunCompletedActivity(params) {
65
- const label = formatRunTypeLabel(params.runType);
66
- const nextState = describeNextState(params.postRunState, params.prNumber);
65
+ const prLabel = params.prNumber ? `PR #${params.prNumber}` : "the pull request";
67
66
  const summary = trimSummary(params.completionSummary);
68
- const lines = [`${label} completed.`];
69
- if (nextState) {
70
- lines.push("", nextState);
71
- }
72
- if (summary) {
73
- lines.push("", summary);
67
+ const detail = summary ? ` ${summary}` : "";
68
+ switch (params.runType) {
69
+ case "implementation":
70
+ if (params.postRunState === "pr_open") {
71
+ return {
72
+ type: "response",
73
+ body: `${prLabel} opened:${detail || " Published and ready for review."}`,
74
+ };
75
+ }
76
+ return undefined;
77
+ case "review_fix":
78
+ return {
79
+ type: "response",
80
+ body: `Updated ${prLabel} to address review feedback.${detail}`,
81
+ };
82
+ case "ci_repair":
83
+ return {
84
+ type: "response",
85
+ body: `Updated ${prLabel} after CI repair.${detail}`,
86
+ };
87
+ case "queue_repair":
88
+ return {
89
+ type: "response",
90
+ body: `Updated ${prLabel} after merge-queue repair.${detail}`,
91
+ };
92
+ case "branch_upkeep":
93
+ return undefined;
94
+ default: {
95
+ const label = formatRunTypeLabel(params.runType);
96
+ const nextState = describeNextState(params.postRunState, params.prNumber);
97
+ const lines = [`${label} completed.`];
98
+ if (nextState) {
99
+ lines.push("", nextState);
100
+ }
101
+ if (summary) {
102
+ lines.push("", summary);
103
+ }
104
+ return {
105
+ type: "response",
106
+ body: lines.join("\n"),
107
+ };
108
+ }
74
109
  }
75
- return {
76
- type: "response",
77
- body: lines.join("\n"),
78
- };
79
110
  }
80
111
  export function buildRunFailureActivity(runType, reason) {
81
112
  const label = formatRunTypeLabel(runType);
@@ -92,33 +123,16 @@ export function buildStopConfirmationActivity() {
92
123
  }
93
124
  export function buildGitHubStateActivity(newState, event) {
94
125
  switch (newState) {
95
- case "pr_open": {
96
- const parts = [`PR #${event.prNumber ?? "?"} is open and ready for review.`];
97
- if (event.prUrl) {
98
- parts.push("", event.prUrl);
99
- }
100
- return { type: "response", body: parts.join("\n") };
101
- }
126
+ case "pr_open":
127
+ return undefined;
102
128
  case "awaiting_queue":
103
- return { type: "response", body: "Review approved. PatchRelay is moving the PR toward merge." };
129
+ return undefined;
104
130
  case "changes_requested":
105
- return {
106
- type: "action",
107
- action: "Addressing",
108
- parameter: event.reviewerName ? `review feedback from ${event.reviewerName}` : "review feedback",
109
- };
131
+ return undefined;
110
132
  case "repairing_ci":
111
- return {
112
- type: "action",
113
- action: "Repairing",
114
- parameter: event.checkName ? `CI failure: ${event.checkName}` : "failing CI checks",
115
- };
133
+ return undefined;
116
134
  case "repairing_queue":
117
- return {
118
- type: "action",
119
- action: "Repairing",
120
- parameter: "merge queue validation",
121
- };
135
+ return undefined;
122
136
  case "done":
123
137
  return { type: "response", body: `PR merged.${event.prNumber ? ` PR #${event.prNumber}` : ""}` };
124
138
  case "failed":
@@ -2,8 +2,10 @@ import { buildAgentSessionPlanForIssue } from "./agent-session-plan.js";
2
2
  import { buildAgentSessionExternalUrls } from "./agent-session-presentation.js";
3
3
  import { deriveIssueStatusNote } from "./status-note.js";
4
4
  import { derivePatchRelayWaitingReason } from "./waiting-reason.js";
5
- import { resolvePreferredReviewLinearState, resolvePreferredStartedLinearState } from "./linear-workflow.js";
5
+ import { resolvePreferredDeployingLinearState, resolvePreferredHumanNeededLinearState, resolvePreferredImplementingLinearState, resolvePreferredReviewLinearState, resolvePreferredReviewingLinearState, } from "./linear-workflow.js";
6
+ import { sanitizeOperatorFacingCommand, sanitizeOperatorFacingText } from "./presentation-text.js";
6
7
  const PROGRESS_THROTTLE_MS = 5_000;
8
+ const MAX_PROGRESS_TEXT_LENGTH = 220;
7
9
  export class LinearSessionSync {
8
10
  config;
9
11
  db;
@@ -11,6 +13,9 @@ export class LinearSessionSync {
11
13
  logger;
12
14
  feed;
13
15
  progressThrottle = new Map();
16
+ workingOnPublishedRuns = new Set();
17
+ agentMessageBuffers = new Map();
18
+ agentMessageProgressPublished = new Set();
14
19
  constructor(config, db, linearProvider, logger, feed) {
15
20
  this.config = config;
16
21
  this.db = db;
@@ -166,21 +171,103 @@ export class LinearSessionSync {
166
171
  }
167
172
  }
168
173
  maybeEmitProgress(notification, run) {
169
- const activity = resolveProgressActivity(notification);
170
- if (!activity)
174
+ const issue = this.db.issues.getIssue(run.projectId, run.linearIssueId);
175
+ if (!issue)
171
176
  return;
172
- const now = Date.now();
173
- const lastEmit = this.progressThrottle.get(run.id) ?? 0;
174
- if (now - lastEmit < PROGRESS_THROTTLE_MS)
177
+ const agentSentence = this.consumeAgentMessageSentence(notification, run);
178
+ const workingOn = this.resolveWorkingOnActivity(notification, agentSentence?.sentence);
179
+ if (workingOn && !this.workingOnPublishedRuns.has(run.id)) {
180
+ this.workingOnPublishedRuns.add(run.id);
181
+ void this.emitActivity(issue, workingOn);
182
+ }
183
+ const progress = this.resolveEphemeralProgressActivity(notification, agentSentence?.sentence);
184
+ if (!progress)
175
185
  return;
176
- this.progressThrottle.set(run.id, now);
177
- const issue = this.db.issues.getIssue(run.projectId, run.linearIssueId);
178
- if (issue) {
179
- void this.emitActivity(issue, activity, { ephemeral: true });
186
+ if (!progress.bypassThrottle) {
187
+ const now = Date.now();
188
+ const lastEmit = this.progressThrottle.get(run.id) ?? 0;
189
+ if (now - lastEmit < PROGRESS_THROTTLE_MS)
190
+ return;
191
+ this.progressThrottle.set(run.id, now);
180
192
  }
193
+ void this.emitActivity(issue, progress.activity, { ephemeral: true });
181
194
  }
182
195
  clearProgress(runId) {
183
196
  this.progressThrottle.delete(runId);
197
+ this.workingOnPublishedRuns.delete(runId);
198
+ for (const key of this.agentMessageBuffers.keys()) {
199
+ if (key.startsWith(`${runId}:`)) {
200
+ this.agentMessageBuffers.delete(key);
201
+ }
202
+ }
203
+ for (const key of this.agentMessageProgressPublished) {
204
+ if (key.startsWith(`${runId}:`)) {
205
+ this.agentMessageProgressPublished.delete(key);
206
+ }
207
+ }
208
+ }
209
+ resolveWorkingOnActivity(notification, agentSentence) {
210
+ const summary = resolveWorkingOnSummary(notification) ?? agentSentence;
211
+ if (!summary)
212
+ return undefined;
213
+ return { type: "response", body: `Working on: ${summary}` };
214
+ }
215
+ resolveEphemeralProgressActivity(notification, agentSentence) {
216
+ if (notification.method === "item/started") {
217
+ const item = notification.params.item;
218
+ if (!item)
219
+ return undefined;
220
+ const type = typeof item.type === "string" ? item.type : undefined;
221
+ if (type === "commandExecution") {
222
+ const cmd = item.command;
223
+ const cmdStr = Array.isArray(cmd)
224
+ ? sanitizeOperatorFacingCommand(cmd.map((part) => String(part)).join(" "))
225
+ : sanitizeOperatorFacingCommand(typeof cmd === "string" ? cmd : undefined);
226
+ return { activity: { type: "action", action: "Running", parameter: truncateProgressText(cmdStr ?? "command", 120) } };
227
+ }
228
+ if (type === "mcpToolCall") {
229
+ const server = typeof item.server === "string" ? item.server : "";
230
+ const tool = typeof item.tool === "string" ? item.tool : "";
231
+ return { activity: { type: "action", action: "Using", parameter: `${server}/${tool}` } };
232
+ }
233
+ if (type === "dynamicToolCall") {
234
+ const tool = typeof item.tool === "string" ? item.tool : "tool";
235
+ return { activity: { type: "action", action: "Using", parameter: tool } };
236
+ }
237
+ }
238
+ if (agentSentence) {
239
+ return {
240
+ activity: { type: "thought", body: agentSentence },
241
+ bypassThrottle: true,
242
+ };
243
+ }
244
+ return undefined;
245
+ }
246
+ consumeAgentMessageSentence(notification, run) {
247
+ const messageKey = resolveAgentMessageKey(notification, run);
248
+ if (!messageKey)
249
+ return undefined;
250
+ if (this.agentMessageProgressPublished.has(messageKey))
251
+ return undefined;
252
+ const delta = resolveAgentMessageDelta(notification);
253
+ if (delta) {
254
+ const previous = this.agentMessageBuffers.get(messageKey) ?? "";
255
+ const next = `${previous}${delta}`;
256
+ this.agentMessageBuffers.set(messageKey, next);
257
+ const sentence = extractFirstSentence(next);
258
+ if (!sentence)
259
+ return undefined;
260
+ this.agentMessageProgressPublished.add(messageKey);
261
+ return { sentence };
262
+ }
263
+ const completedText = resolveCompletedAgentMessageText(notification);
264
+ if (!completedText)
265
+ return undefined;
266
+ const sentence = extractFirstSentence(completedText);
267
+ if (!sentence)
268
+ return undefined;
269
+ this.agentMessageProgressPublished.add(messageKey);
270
+ return { sentence };
184
271
  }
185
272
  async syncStatusComment(issue, linear, options) {
186
273
  try {
@@ -205,29 +292,6 @@ export class LinearSessionSync {
205
292
  }
206
293
  }
207
294
  }
208
- function resolveProgressActivity(notification) {
209
- if (notification.method === "item/started") {
210
- const item = notification.params.item;
211
- if (!item)
212
- return undefined;
213
- const type = typeof item.type === "string" ? item.type : undefined;
214
- if (type === "commandExecution") {
215
- const cmd = item.command;
216
- const cmdStr = Array.isArray(cmd) ? cmd.join(" ") : typeof cmd === "string" ? cmd : undefined;
217
- return { type: "action", action: "Running", parameter: cmdStr?.slice(0, 120) ?? "command" };
218
- }
219
- if (type === "mcpToolCall") {
220
- const server = typeof item.server === "string" ? item.server : "";
221
- const tool = typeof item.tool === "string" ? item.tool : "";
222
- return { type: "action", action: "Using", parameter: `${server}/${tool}` };
223
- }
224
- if (type === "dynamicToolCall") {
225
- const tool = typeof item.tool === "string" ? item.tool : "tool";
226
- return { type: "action", action: "Using", parameter: tool };
227
- }
228
- }
229
- return undefined;
230
- }
231
295
  function renderStatusComment(db, issue, trackedIssue, options) {
232
296
  const activeRun = issue.activeRunId ? db.runs.getRunById(issue.activeRunId) : undefined;
233
297
  const latestRun = db.runs.getLatestRunForIssue(issue.projectId, issue.linearIssueId);
@@ -343,32 +407,127 @@ function humanize(value) {
343
407
  }
344
408
  function shouldAutoAdvanceLinearState(issue) {
345
409
  const normalizedType = issue.currentLinearStateType?.trim().toLowerCase();
346
- if (normalizedType === "backlog" || normalizedType === "unstarted") {
347
- return true;
410
+ if (normalizedType === "completed" || normalizedType === "canceled" || normalizedType === "cancelled") {
411
+ return false;
348
412
  }
349
413
  const normalizedName = issue.currentLinearState?.trim().toLowerCase();
350
- return normalizedName === "backlog" || normalizedName === "todo" || normalizedName === "to do" || normalizedName === "triage";
414
+ return normalizedName !== "done" && normalizedName !== "completed" && normalizedName !== "complete";
351
415
  }
352
416
  function resolveDesiredActiveWorkflowState(issue, trackedIssue, options, liveIssue) {
417
+ if (issue.factoryState === "awaiting_input" || issue.factoryState === "failed" || issue.factoryState === "escalated"
418
+ || trackedIssue?.sessionState === "waiting_input" || trackedIssue?.sessionState === "failed") {
419
+ return resolvePreferredHumanNeededLinearState(liveIssue);
420
+ }
421
+ const activelyWorking = issue.activeRunId !== undefined
422
+ || options?.activeRunType !== undefined
423
+ || trackedIssue?.sessionState === "running"
424
+ || issue.factoryState === "delegated"
425
+ || issue.factoryState === "implementing"
426
+ || issue.factoryState === "changes_requested"
427
+ || issue.factoryState === "repairing_ci"
428
+ || issue.factoryState === "repairing_queue";
429
+ if (activelyWorking) {
430
+ return resolvePreferredImplementingLinearState(liveIssue);
431
+ }
432
+ if (issue.factoryState === "awaiting_queue"
433
+ || issue.prReviewState === "approved"
434
+ || isApprovedAndGreen(issue.prReviewState, issue.prCheckStatus)) {
435
+ return resolvePreferredDeployingLinearState(liveIssue);
436
+ }
437
+ const reviewQuillActive = hasPendingReviewQuillVerdict(issue.lastGitHubCiSnapshotJson);
438
+ if (reviewQuillActive) {
439
+ return resolvePreferredReviewingLinearState(liveIssue);
440
+ }
353
441
  const reviewBound = issue.prNumber !== undefined
354
442
  || Boolean(issue.prUrl)
355
443
  || issue.factoryState === "pr_open"
356
- || issue.factoryState === "awaiting_queue"
357
- || issue.factoryState === "changes_requested"
358
- || issue.factoryState === "repairing_ci"
359
- || issue.factoryState === "repairing_queue"
360
444
  || issue.prReviewState !== undefined
361
445
  || issue.prCheckStatus !== undefined;
362
446
  if (reviewBound) {
363
447
  return resolvePreferredReviewLinearState(liveIssue);
364
448
  }
365
- const activelyWorking = issue.activeRunId !== undefined
366
- || options?.activeRunType !== undefined
367
- || trackedIssue?.sessionState === "running"
368
- || issue.factoryState === "delegated"
369
- || issue.factoryState === "implementing";
370
- if (activelyWorking) {
371
- return resolvePreferredStartedLinearState(liveIssue);
449
+ return undefined;
450
+ }
451
+ function isApprovedAndGreen(prReviewState, prCheckStatus) {
452
+ const normalizedReview = prReviewState?.trim().toLowerCase();
453
+ const normalizedChecks = prCheckStatus?.trim().toLowerCase();
454
+ return normalizedReview === "approved" && (normalizedChecks === "success" || normalizedChecks === "passed");
455
+ }
456
+ function hasPendingReviewQuillVerdict(snapshotJson) {
457
+ if (!snapshotJson)
458
+ return false;
459
+ try {
460
+ const parsed = JSON.parse(snapshotJson);
461
+ return Array.isArray(parsed.checks) && parsed.checks.some((check) => typeof check.name === "string"
462
+ && check.name === "review-quill/verdict"
463
+ && typeof check.status === "string"
464
+ && check.status.toLowerCase() === "pending");
465
+ }
466
+ catch {
467
+ return false;
468
+ }
469
+ }
470
+ function resolveWorkingOnSummary(notification) {
471
+ if (notification.method !== "turn/plan/updated") {
472
+ return undefined;
473
+ }
474
+ const plan = notification.params.plan;
475
+ if (!Array.isArray(plan))
476
+ return undefined;
477
+ const ranked = plan
478
+ .map((entry) => entry)
479
+ .filter((entry) => typeof entry.step === "string" && entry.step.trim().length > 0)
480
+ .sort((a, b) => rankPlanStatus(a.status) - rankPlanStatus(b.status));
481
+ const first = ranked[0];
482
+ return summarizeProgressSentence(typeof first?.step === "string" ? first.step : undefined);
483
+ }
484
+ function rankPlanStatus(status) {
485
+ return status === "inProgress" ? 0
486
+ : status === "pending" ? 1
487
+ : status === "completed" ? 2
488
+ : 3;
489
+ }
490
+ function resolveAgentMessageKey(notification, run) {
491
+ if (notification.method === "item/agentMessage/delta") {
492
+ const itemId = typeof notification.params.itemId === "string" ? notification.params.itemId : undefined;
493
+ return itemId ? `${run.id}:${itemId}` : undefined;
494
+ }
495
+ if (notification.method === "item/completed") {
496
+ const item = notification.params.item;
497
+ const itemId = typeof item?.id === "string" ? item.id : undefined;
498
+ const itemType = typeof item?.type === "string" ? item.type : undefined;
499
+ return itemId && itemType === "agentMessage" ? `${run.id}:${itemId}` : undefined;
372
500
  }
373
501
  return undefined;
374
502
  }
503
+ function resolveAgentMessageDelta(notification) {
504
+ if (notification.method !== "item/agentMessage/delta") {
505
+ return undefined;
506
+ }
507
+ return typeof notification.params.delta === "string" ? notification.params.delta : undefined;
508
+ }
509
+ function resolveCompletedAgentMessageText(notification) {
510
+ if (notification.method !== "item/completed") {
511
+ return undefined;
512
+ }
513
+ const item = notification.params.item;
514
+ if (!item || item.type !== "agentMessage")
515
+ return undefined;
516
+ return typeof item.text === "string" ? item.text : undefined;
517
+ }
518
+ function extractFirstSentence(text) {
519
+ const sanitized = sanitizeOperatorFacingText(text)?.replace(/\s+/g, " ").trim();
520
+ if (!sanitized)
521
+ return undefined;
522
+ const match = sanitized.match(/^(.+?[.!?])(?:\s|$)/);
523
+ return truncateProgressText((match?.[1] ?? sanitized).trim(), MAX_PROGRESS_TEXT_LENGTH);
524
+ }
525
+ function summarizeProgressSentence(text) {
526
+ const summary = extractFirstSentence(text);
527
+ if (!summary)
528
+ return undefined;
529
+ return summary.endsWith(".") || summary.endsWith("!") || summary.endsWith("?") ? summary : `${summary}.`;
530
+ }
531
+ function truncateProgressText(text, maxLength) {
532
+ return text.length <= maxLength ? text : `${text.slice(0, maxLength - 3).trimEnd()}...`;
533
+ }
@@ -2,6 +2,20 @@ function normalizeLinearState(value) {
2
2
  const trimmed = value?.trim();
3
3
  return trimmed ? trimmed.toLowerCase() : undefined;
4
4
  }
5
+ function includesAny(normalized, candidates) {
6
+ return Boolean(normalized && candidates.includes(normalized));
7
+ }
8
+ function resolvePreferredLinearState(issue, params) {
9
+ const match = issue.workflowStates.find((state) => {
10
+ const normalizedType = normalizeLinearState(state.type);
11
+ const normalizedName = normalizeLinearState(state.name);
12
+ if (params.types && !params.types.includes(normalizedType ?? "")) {
13
+ return false;
14
+ }
15
+ return includesAny(normalizedName, params.names);
16
+ });
17
+ return match?.name ?? params.fallback;
18
+ }
5
19
  export function resolvePreferredStartedLinearState(issue) {
6
20
  const startedStates = issue.workflowStates.filter((state) => normalizeLinearState(state.type) === "started");
7
21
  const preferred = startedStates.find((state) => {
@@ -10,14 +24,50 @@ export function resolvePreferredStartedLinearState(issue) {
10
24
  });
11
25
  return preferred?.name ?? startedStates[0]?.name;
12
26
  }
27
+ export function resolvePreferredImplementingLinearState(issue) {
28
+ return resolvePreferredLinearState(issue, {
29
+ names: ["implementing", "in progress", "in-progress", "started", "doing"],
30
+ types: ["started"],
31
+ fallback: resolvePreferredStartedLinearState(issue),
32
+ });
33
+ }
13
34
  export function resolvePreferredReviewLinearState(issue) {
14
- const reviewState = issue.workflowStates.find((state) => {
15
- if (normalizeLinearState(state.type) !== "started")
16
- return false;
17
- const normalized = normalizeLinearState(state.name);
18
- return normalized === "in review" || normalized === "review";
35
+ return resolvePreferredLinearState(issue, {
36
+ names: ["review", "awaiting review"],
37
+ types: ["unstarted"],
38
+ fallback: resolvePreferredLinearState(issue, {
39
+ names: ["reviewing", "in review", "review"],
40
+ types: ["started"],
41
+ fallback: resolvePreferredStartedLinearState(issue),
42
+ }),
43
+ });
44
+ }
45
+ export function resolvePreferredReviewingLinearState(issue) {
46
+ return resolvePreferredLinearState(issue, {
47
+ names: ["reviewing", "in review", "review"],
48
+ types: ["started"],
49
+ fallback: resolvePreferredReviewLinearState(issue),
50
+ });
51
+ }
52
+ export function resolvePreferredDeployLinearState(issue) {
53
+ return resolvePreferredLinearState(issue, {
54
+ names: ["deploy", "ready to deploy", "ready for deploy", "merge"],
55
+ types: ["unstarted"],
56
+ fallback: resolvePreferredReviewLinearState(issue),
57
+ });
58
+ }
59
+ export function resolvePreferredDeployingLinearState(issue) {
60
+ return resolvePreferredLinearState(issue, {
61
+ names: ["deploying", "merging", "shipping"],
62
+ types: ["started"],
63
+ fallback: resolvePreferredDeployLinearState(issue),
64
+ });
65
+ }
66
+ export function resolvePreferredHumanNeededLinearState(issue) {
67
+ return resolvePreferredLinearState(issue, {
68
+ names: ["human needed", "needs human", "help needed", "operator needed", "blocked"],
69
+ fallback: undefined,
19
70
  });
20
- return reviewState?.name ?? resolvePreferredStartedLinearState(issue);
21
71
  }
22
72
  export function resolvePreferredCompletedLinearState(issue) {
23
73
  const completed = issue.workflowStates.find((state) => normalizeLinearState(state.type) === "completed");
@@ -1,7 +1,16 @@
1
1
  function unwrapShellWrappedCommand(text) {
2
2
  return text
3
3
  .replace(/`(?:\/bin\/bash|bash|\/bin\/sh|sh)\s+-lc\s+'([^`\n]+)'`/g, "`$1`")
4
- .replace(/`(?:\/bin\/bash|bash|\/bin\/sh|sh)\s+-lc\s+"([^`\n]+)"`/g, "`$1`");
4
+ .replace(/`(?:\/bin\/bash|bash|\/bin\/sh|sh)\s+-lc\s+"([^`\n]+)"`/g, "`$1`")
5
+ .replace(/^(?:\/bin\/bash|bash|\/bin\/sh|sh)\s+-lc\s+'([^`\n]+)'$/g, "$1")
6
+ .replace(/^(?:\/bin\/bash|bash|\/bin\/sh|sh)\s+-lc\s+"([^`\n]+)"$/g, "$1");
7
+ }
8
+ export function sanitizeOperatorFacingCommand(command) {
9
+ const trimmed = command?.trim();
10
+ if (!trimmed) {
11
+ return undefined;
12
+ }
13
+ return unwrapShellWrappedCommand(trimmed);
5
14
  }
6
15
  export function sanitizeOperatorFacingText(text) {
7
16
  const trimmed = text?.trim();
@@ -159,12 +159,15 @@ export class RunFinalizer {
159
159
  });
160
160
  const updatedIssue = this.db.issues.getIssue(run.projectId, run.linearIssueId) ?? refreshedIssue;
161
161
  const completionSummary = report.assistantMessages.at(-1)?.slice(0, 300) ?? `${run.runType} completed.`;
162
- void this.linearSync.emitActivity(updatedIssue, buildRunCompletedActivity({
162
+ const linearActivity = buildRunCompletedActivity({
163
163
  runType: run.runType,
164
164
  completionSummary,
165
165
  postRunState: updatedIssue.factoryState,
166
166
  ...(updatedIssue.prNumber !== undefined ? { prNumber: updatedIssue.prNumber } : {}),
167
- }));
167
+ });
168
+ if (linearActivity) {
169
+ void this.linearSync.emitActivity(updatedIssue, linearActivity);
170
+ }
168
171
  void this.linearSync.syncSession(updatedIssue);
169
172
  this.linearSync.clearProgress(run.id);
170
173
  this.releaseLease(run.projectId, run.linearIssueId);
@@ -46,13 +46,12 @@ export class AgentSessionHandler {
46
46
  await this.publishAgentActivity(linear, normalized.agentSession.id, buildAlreadyRunningThought(activeRun.runType));
47
47
  return;
48
48
  }
49
- const blockerSummary = trackedIssue?.blockedByCount
50
- ? `PatchRelay is delegated and waiting on blockers to reach Done: ${trackedIssue.blockedByKeys.join(", ")}.`
51
- : "PatchRelay is delegated, but no work is queued. Delegate the issue or move it to Start to trigger implementation.";
52
- await this.publishAgentActivity(linear, normalized.agentSession.id, {
53
- type: "elicitation",
54
- body: blockerSummary,
55
- });
49
+ if (!trackedIssue?.blockedByCount) {
50
+ await this.publishAgentActivity(linear, normalized.agentSession.id, {
51
+ type: "elicitation",
52
+ body: "PatchRelay is delegated, but no work is queued. Delegate the issue or move it to Start to trigger implementation.",
53
+ });
54
+ }
56
55
  return;
57
56
  }
58
57
  if (normalized.triggerEvent === "agentSignal" && normalized.agentSession.signal === "stop") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "patchrelay",
3
- "version": "0.36.12",
3
+ "version": "0.36.14",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "repository": {