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.
- package/dist/build-info.json +3 -3
- package/dist/linear-session-reporting.js +49 -35
- package/dist/linear-session-sync.js +206 -47
- package/dist/linear-workflow.js +56 -6
- package/dist/presentation-text.js +10 -1
- package/dist/run-finalizer.js +5 -2
- package/dist/webhooks/agent-session-handler.js +6 -7
- package/package.json +1 -1
package/dist/build-info.json
CHANGED
|
@@ -62,20 +62,51 @@ export function buildRunStartedActivity(runType) {
|
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
64
|
export function buildRunCompletedActivity(params) {
|
|
65
|
-
const
|
|
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
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
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
|
|
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,
|
|
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
|
|
170
|
-
if (!
|
|
174
|
+
const issue = this.db.issues.getIssue(run.projectId, run.linearIssueId);
|
|
175
|
+
if (!issue)
|
|
171
176
|
return;
|
|
172
|
-
const
|
|
173
|
-
const
|
|
174
|
-
if (
|
|
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
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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 === "
|
|
347
|
-
return
|
|
410
|
+
if (normalizedType === "completed" || normalizedType === "canceled" || normalizedType === "cancelled") {
|
|
411
|
+
return false;
|
|
348
412
|
}
|
|
349
413
|
const normalizedName = issue.currentLinearState?.trim().toLowerCase();
|
|
350
|
-
return normalizedName
|
|
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
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
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
|
+
}
|
package/dist/linear-workflow.js
CHANGED
|
@@ -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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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();
|
package/dist/run-finalizer.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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") {
|