patchrelay 0.36.18 → 0.37.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.
- package/dist/build-info.json +3 -3
- package/dist/cli/watch/App.js +226 -27
- package/dist/cli/watch/HelpBar.js +18 -9
- package/dist/cli/watch/IssueDetailView.js +32 -14
- package/dist/cli/watch/detail-rows.js +1 -22
- package/dist/cli/watch/detail-status.js +38 -0
- package/dist/cli/watch/layout-measure.js +7 -0
- package/dist/cli/watch/prompt-layout.js +14 -0
- package/dist/cli/watch/timeline-builder.js +169 -18
- package/dist/cli/watch/timeline-presentation.js +21 -1
- package/dist/cli/watch/transient-status.js +28 -0
- package/dist/cli/watch/watch-actions.js +76 -0
- package/dist/cli/watch/watch-state.js +2 -12
- package/dist/linear-agent-session-client.js +109 -0
- package/dist/linear-progress-reporter.js +185 -0
- package/dist/linear-session-sync.js +23 -519
- package/dist/linear-status-comment-sync.js +152 -0
- package/dist/linear-workflow-state-sync.js +103 -0
- package/dist/no-pr-completion-check.js +199 -0
- package/dist/operator-retry-event.js +58 -0
- package/dist/run-finalizer.js +72 -237
- package/dist/service-issue-actions.js +164 -0
- package/dist/service-startup-recovery.js +104 -0
- package/dist/service.js +15 -556
- package/dist/tracked-issue-list-query.js +259 -0
- package/package.json +1 -1
- package/dist/cli/watch/ItemLine.js +0 -80
- package/dist/cli/watch/Timeline.js +0 -22
- package/dist/cli/watch/TimelineRow.js +0 -77
|
@@ -1,92 +1,45 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { derivePatchRelayWaitingReason } from "./waiting-reason.js";
|
|
6
|
-
import { resolvePreferredDeployingLinearState, resolvePreferredHumanNeededLinearState, resolvePreferredImplementingLinearState, resolvePreferredReviewLinearState, resolvePreferredReviewingLinearState, } from "./linear-workflow.js";
|
|
7
|
-
import { sanitizeOperatorFacingCommand, sanitizeOperatorFacingText } from "./presentation-text.js";
|
|
8
|
-
const PROGRESS_THROTTLE_MS = 5_000;
|
|
9
|
-
const MAX_PROGRESS_TEXT_LENGTH = 220;
|
|
1
|
+
import { shouldSyncVisibleIssueComment, syncVisibleStatusComment, } from "./linear-status-comment-sync.js";
|
|
2
|
+
import { LinearAgentSessionClient } from "./linear-agent-session-client.js";
|
|
3
|
+
import { LinearProgressReporter } from "./linear-progress-reporter.js";
|
|
4
|
+
import { syncActiveWorkflowState } from "./linear-workflow-state-sync.js";
|
|
10
5
|
export class LinearSessionSync {
|
|
11
6
|
config;
|
|
12
7
|
db;
|
|
13
8
|
linearProvider;
|
|
14
9
|
logger;
|
|
15
10
|
feed;
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
agentMessageBuffers = new Map();
|
|
19
|
-
agentMessageProgressPublished = new Set();
|
|
11
|
+
agentSessions;
|
|
12
|
+
progressReporter;
|
|
20
13
|
constructor(config, db, linearProvider, logger, feed) {
|
|
21
14
|
this.config = config;
|
|
22
15
|
this.db = db;
|
|
23
16
|
this.linearProvider = linearProvider;
|
|
24
17
|
this.logger = logger;
|
|
25
18
|
this.feed = feed;
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
if (issue.agentSessionId) {
|
|
29
|
-
return issue;
|
|
30
|
-
}
|
|
31
|
-
const recoveredAgentSessionId = this.db.webhookEvents.findLatestAgentSessionIdForIssue(issue.linearIssueId);
|
|
32
|
-
if (!recoveredAgentSessionId)
|
|
33
|
-
return issue;
|
|
34
|
-
this.logger.info({ issueKey: issue.issueKey, agentSessionId: recoveredAgentSessionId }, "Recovered missing Linear agent session id from webhook history");
|
|
35
|
-
return this.db.issues.upsertIssue({
|
|
36
|
-
projectId: issue.projectId,
|
|
37
|
-
linearIssueId: issue.linearIssueId,
|
|
38
|
-
agentSessionId: recoveredAgentSessionId,
|
|
39
|
-
});
|
|
19
|
+
this.agentSessions = new LinearAgentSessionClient(config, db, linearProvider, logger, feed);
|
|
20
|
+
this.progressReporter = new LinearProgressReporter(db, (issue, content, options) => this.agentSessions.emitActivity(issue, content, options));
|
|
40
21
|
}
|
|
41
22
|
async emitActivity(issue, content, options) {
|
|
42
|
-
|
|
43
|
-
if (!syncedIssue.agentSessionId)
|
|
44
|
-
return;
|
|
45
|
-
try {
|
|
46
|
-
const linear = await this.linearProvider.forProject(syncedIssue.projectId);
|
|
47
|
-
if (!linear)
|
|
48
|
-
return;
|
|
49
|
-
const allowEphemeral = content.type === "thought" || content.type === "action";
|
|
50
|
-
await linear.createAgentActivity({
|
|
51
|
-
agentSessionId: syncedIssue.agentSessionId,
|
|
52
|
-
content,
|
|
53
|
-
...(options?.ephemeral && allowEphemeral ? { ephemeral: true } : {}),
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
catch (error) {
|
|
57
|
-
const msg = error instanceof Error ? error.message : String(error);
|
|
58
|
-
this.logger.warn({ issueKey: syncedIssue.issueKey, type: content.type, error: msg }, "Failed to emit Linear activity");
|
|
59
|
-
this.feed?.publish({
|
|
60
|
-
level: "warn",
|
|
61
|
-
kind: "linear",
|
|
62
|
-
issueKey: syncedIssue.issueKey,
|
|
63
|
-
projectId: syncedIssue.projectId,
|
|
64
|
-
status: "linear_error",
|
|
65
|
-
summary: `Linear activity failed: ${msg}`,
|
|
66
|
-
});
|
|
67
|
-
}
|
|
23
|
+
await this.agentSessions.emitActivity(issue, content, options);
|
|
68
24
|
}
|
|
69
25
|
async syncSession(issue, options) {
|
|
70
|
-
const syncedIssue = this.ensureAgentSessionIssue(issue);
|
|
26
|
+
const syncedIssue = this.agentSessions.ensureAgentSessionIssue(issue);
|
|
71
27
|
try {
|
|
72
28
|
const linear = await this.linearProvider.forProject(syncedIssue.projectId);
|
|
73
29
|
if (!linear)
|
|
74
30
|
return;
|
|
75
31
|
const trackedIssue = this.db.getTrackedIssue(syncedIssue.projectId, syncedIssue.linearIssueId);
|
|
76
|
-
await this.
|
|
77
|
-
|
|
78
|
-
const externalUrls = buildAgentSessionExternalUrls(this.config, {
|
|
79
|
-
...(syncedIssue.issueKey ? { issueKey: syncedIssue.issueKey } : {}),
|
|
80
|
-
...(syncedIssue.prUrl ? { prUrl: syncedIssue.prUrl } : {}),
|
|
81
|
-
});
|
|
82
|
-
await linear.updateAgentSession({
|
|
83
|
-
agentSessionId: syncedIssue.agentSessionId,
|
|
84
|
-
plan: buildAgentSessionPlanForIssue(syncedIssue, options),
|
|
85
|
-
...(externalUrls ? { externalUrls } : {}),
|
|
86
|
-
});
|
|
87
|
-
}
|
|
32
|
+
await syncActiveWorkflowState({ db: this.db, issue: syncedIssue, linear, ...(trackedIssue ? { trackedIssue } : {}), ...(options ? { options } : {}) });
|
|
33
|
+
await this.agentSessions.syncSessionPlan(syncedIssue, linear, options);
|
|
88
34
|
if (shouldSyncVisibleIssueComment(trackedIssue ?? syncedIssue, Boolean(syncedIssue.agentSessionId))) {
|
|
89
|
-
await
|
|
35
|
+
await syncVisibleStatusComment({
|
|
36
|
+
db: this.db,
|
|
37
|
+
issue: syncedIssue,
|
|
38
|
+
linear,
|
|
39
|
+
logger: this.logger,
|
|
40
|
+
...(trackedIssue ? { trackedIssue } : {}),
|
|
41
|
+
...(options ? { options } : {}),
|
|
42
|
+
});
|
|
90
43
|
}
|
|
91
44
|
}
|
|
92
45
|
catch (error) {
|
|
@@ -94,462 +47,13 @@ export class LinearSessionSync {
|
|
|
94
47
|
this.logger.warn({ issueKey: syncedIssue.issueKey, error: msg }, "Failed to update Linear plan");
|
|
95
48
|
}
|
|
96
49
|
}
|
|
97
|
-
async syncActiveWorkflowState(issue, linear, trackedIssue, options) {
|
|
98
|
-
if (!shouldAutoAdvanceLinearState(issue)) {
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
const liveIssue = await linear.getIssue(issue.linearIssueId).catch(() => undefined);
|
|
102
|
-
if (!liveIssue)
|
|
103
|
-
return;
|
|
104
|
-
if (!shouldAutoAdvanceLinearState({
|
|
105
|
-
currentLinearState: liveIssue.stateName,
|
|
106
|
-
currentLinearStateType: liveIssue.stateType,
|
|
107
|
-
})) {
|
|
108
|
-
this.db.issues.upsertIssue({
|
|
109
|
-
projectId: issue.projectId,
|
|
110
|
-
linearIssueId: issue.linearIssueId,
|
|
111
|
-
...(liveIssue.stateName ? { currentLinearState: liveIssue.stateName } : {}),
|
|
112
|
-
...(liveIssue.stateType ? { currentLinearStateType: liveIssue.stateType } : {}),
|
|
113
|
-
});
|
|
114
|
-
return;
|
|
115
|
-
}
|
|
116
|
-
const targetState = resolveDesiredActiveWorkflowState(issue, trackedIssue, options, liveIssue);
|
|
117
|
-
if (!targetState)
|
|
118
|
-
return;
|
|
119
|
-
const normalizedCurrent = liveIssue.stateName?.trim().toLowerCase();
|
|
120
|
-
if (normalizedCurrent === targetState.trim().toLowerCase()) {
|
|
121
|
-
this.db.issues.upsertIssue({
|
|
122
|
-
projectId: issue.projectId,
|
|
123
|
-
linearIssueId: issue.linearIssueId,
|
|
124
|
-
...(liveIssue.stateName ? { currentLinearState: liveIssue.stateName } : {}),
|
|
125
|
-
...(liveIssue.stateType ? { currentLinearStateType: liveIssue.stateType } : {}),
|
|
126
|
-
});
|
|
127
|
-
return;
|
|
128
|
-
}
|
|
129
|
-
const updated = await linear.setIssueState(issue.linearIssueId, targetState);
|
|
130
|
-
this.db.issues.upsertIssue({
|
|
131
|
-
projectId: issue.projectId,
|
|
132
|
-
linearIssueId: issue.linearIssueId,
|
|
133
|
-
...(updated.stateName ? { currentLinearState: updated.stateName } : {}),
|
|
134
|
-
...(updated.stateType ? { currentLinearStateType: updated.stateType } : {}),
|
|
135
|
-
});
|
|
136
|
-
}
|
|
137
50
|
async syncCodexPlan(issue, params) {
|
|
138
|
-
|
|
139
|
-
if (!syncedIssue.agentSessionId)
|
|
140
|
-
return;
|
|
141
|
-
const plan = params.plan;
|
|
142
|
-
if (!Array.isArray(plan))
|
|
143
|
-
return;
|
|
144
|
-
const STATUS_MAP = {
|
|
145
|
-
pending: "pending",
|
|
146
|
-
inProgress: "inProgress",
|
|
147
|
-
completed: "completed",
|
|
148
|
-
};
|
|
149
|
-
const steps = plan.map((entry) => {
|
|
150
|
-
const e = entry;
|
|
151
|
-
const step = typeof e.step === "string" ? e.step : String(e.step ?? "");
|
|
152
|
-
const status = typeof e.status === "string" ? (STATUS_MAP[e.status] ?? "pending") : "pending";
|
|
153
|
-
return { content: step, status };
|
|
154
|
-
});
|
|
155
|
-
const fullPlan = [
|
|
156
|
-
{ content: "Prepare workspace", status: "completed" },
|
|
157
|
-
...steps,
|
|
158
|
-
{ content: "Merge", status: "pending" },
|
|
159
|
-
];
|
|
160
|
-
try {
|
|
161
|
-
const linear = await this.linearProvider.forProject(syncedIssue.projectId);
|
|
162
|
-
if (!linear?.updateAgentSession)
|
|
163
|
-
return;
|
|
164
|
-
await linear.updateAgentSession({
|
|
165
|
-
agentSessionId: syncedIssue.agentSessionId,
|
|
166
|
-
plan: fullPlan,
|
|
167
|
-
});
|
|
168
|
-
}
|
|
169
|
-
catch (error) {
|
|
170
|
-
const msg = error instanceof Error ? error.message : String(error);
|
|
171
|
-
this.logger.warn({ issueKey: syncedIssue.issueKey, error: msg }, "Failed to sync codex plan to Linear");
|
|
172
|
-
}
|
|
51
|
+
await this.agentSessions.syncCodexPlan(issue, params);
|
|
173
52
|
}
|
|
174
53
|
maybeEmitProgress(notification, run) {
|
|
175
|
-
|
|
176
|
-
if (!issue)
|
|
177
|
-
return;
|
|
178
|
-
const agentSentence = this.consumeAgentMessageSentence(notification, run);
|
|
179
|
-
const workingOn = this.resolveWorkingOnActivity(notification, agentSentence?.sentence);
|
|
180
|
-
if (workingOn && !this.workingOnPublishedRuns.has(run.id)) {
|
|
181
|
-
this.workingOnPublishedRuns.add(run.id);
|
|
182
|
-
void this.emitActivity(issue, workingOn);
|
|
183
|
-
}
|
|
184
|
-
const progress = this.resolveEphemeralProgressActivity(notification, agentSentence?.sentence);
|
|
185
|
-
if (!progress)
|
|
186
|
-
return;
|
|
187
|
-
if (!progress.bypassThrottle) {
|
|
188
|
-
const now = Date.now();
|
|
189
|
-
const lastEmit = this.progressThrottle.get(run.id) ?? 0;
|
|
190
|
-
if (now - lastEmit < PROGRESS_THROTTLE_MS)
|
|
191
|
-
return;
|
|
192
|
-
this.progressThrottle.set(run.id, now);
|
|
193
|
-
}
|
|
194
|
-
void this.emitActivity(issue, progress.activity, { ephemeral: true });
|
|
54
|
+
this.progressReporter.maybeEmitProgress(notification, run);
|
|
195
55
|
}
|
|
196
56
|
clearProgress(runId) {
|
|
197
|
-
this.
|
|
198
|
-
this.workingOnPublishedRuns.delete(runId);
|
|
199
|
-
for (const key of this.agentMessageBuffers.keys()) {
|
|
200
|
-
if (key.startsWith(`${runId}:`)) {
|
|
201
|
-
this.agentMessageBuffers.delete(key);
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
for (const key of this.agentMessageProgressPublished) {
|
|
205
|
-
if (key.startsWith(`${runId}:`)) {
|
|
206
|
-
this.agentMessageProgressPublished.delete(key);
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
resolveWorkingOnActivity(notification, agentSentence) {
|
|
211
|
-
const summary = resolveWorkingOnSummary(notification) ?? agentSentence;
|
|
212
|
-
if (!summary)
|
|
213
|
-
return undefined;
|
|
214
|
-
return { type: "response", body: `Working on: ${summary}` };
|
|
215
|
-
}
|
|
216
|
-
resolveEphemeralProgressActivity(notification, agentSentence) {
|
|
217
|
-
if (notification.method === "item/started") {
|
|
218
|
-
const item = notification.params.item;
|
|
219
|
-
if (!item)
|
|
220
|
-
return undefined;
|
|
221
|
-
const type = typeof item.type === "string" ? item.type : undefined;
|
|
222
|
-
if (type === "commandExecution") {
|
|
223
|
-
const cmd = item.command;
|
|
224
|
-
const cmdStr = Array.isArray(cmd)
|
|
225
|
-
? sanitizeOperatorFacingCommand(cmd.map((part) => String(part)).join(" "))
|
|
226
|
-
: sanitizeOperatorFacingCommand(typeof cmd === "string" ? cmd : undefined);
|
|
227
|
-
return { activity: { type: "action", action: "Running", parameter: truncateProgressText(cmdStr ?? "command", 120) } };
|
|
228
|
-
}
|
|
229
|
-
if (type === "mcpToolCall") {
|
|
230
|
-
const server = typeof item.server === "string" ? item.server : "";
|
|
231
|
-
const tool = typeof item.tool === "string" ? item.tool : "";
|
|
232
|
-
return { activity: { type: "action", action: "Using", parameter: `${server}/${tool}` } };
|
|
233
|
-
}
|
|
234
|
-
if (type === "dynamicToolCall") {
|
|
235
|
-
const tool = typeof item.tool === "string" ? item.tool : "tool";
|
|
236
|
-
return { activity: { type: "action", action: "Using", parameter: tool } };
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
if (agentSentence) {
|
|
240
|
-
return {
|
|
241
|
-
activity: { type: "thought", body: agentSentence },
|
|
242
|
-
bypassThrottle: true,
|
|
243
|
-
};
|
|
244
|
-
}
|
|
245
|
-
return undefined;
|
|
246
|
-
}
|
|
247
|
-
consumeAgentMessageSentence(notification, run) {
|
|
248
|
-
const messageKey = resolveAgentMessageKey(notification, run);
|
|
249
|
-
if (!messageKey)
|
|
250
|
-
return undefined;
|
|
251
|
-
if (this.agentMessageProgressPublished.has(messageKey))
|
|
252
|
-
return undefined;
|
|
253
|
-
const delta = resolveAgentMessageDelta(notification);
|
|
254
|
-
if (delta) {
|
|
255
|
-
const previous = this.agentMessageBuffers.get(messageKey) ?? "";
|
|
256
|
-
const next = `${previous}${delta}`;
|
|
257
|
-
this.agentMessageBuffers.set(messageKey, next);
|
|
258
|
-
const sentence = extractFirstCompletedSentence(next);
|
|
259
|
-
if (!sentence)
|
|
260
|
-
return undefined;
|
|
261
|
-
this.agentMessageProgressPublished.add(messageKey);
|
|
262
|
-
return { sentence };
|
|
263
|
-
}
|
|
264
|
-
const completedText = resolveCompletedAgentMessageText(notification);
|
|
265
|
-
if (!completedText)
|
|
266
|
-
return undefined;
|
|
267
|
-
const sentence = extractFirstSentence(completedText);
|
|
268
|
-
if (!sentence)
|
|
269
|
-
return undefined;
|
|
270
|
-
this.agentMessageProgressPublished.add(messageKey);
|
|
271
|
-
return { sentence };
|
|
272
|
-
}
|
|
273
|
-
async syncStatusComment(issue, linear, options) {
|
|
274
|
-
try {
|
|
275
|
-
const trackedIssue = this.db.getTrackedIssue(issue.projectId, issue.linearIssueId);
|
|
276
|
-
const body = renderStatusComment(this.db, issue, trackedIssue, options);
|
|
277
|
-
const result = await linear.upsertIssueComment({
|
|
278
|
-
issueId: issue.linearIssueId,
|
|
279
|
-
...(issue.statusCommentId ? { commentId: issue.statusCommentId } : {}),
|
|
280
|
-
body,
|
|
281
|
-
});
|
|
282
|
-
if (result.id !== issue.statusCommentId) {
|
|
283
|
-
this.db.issues.upsertIssue({
|
|
284
|
-
projectId: issue.projectId,
|
|
285
|
-
linearIssueId: issue.linearIssueId,
|
|
286
|
-
statusCommentId: result.id,
|
|
287
|
-
});
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
catch (error) {
|
|
291
|
-
const msg = error instanceof Error ? error.message : String(error);
|
|
292
|
-
this.logger.warn({ issueKey: issue.issueKey, error: msg }, "Failed to sync Linear status comment");
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
function renderStatusComment(db, issue, trackedIssue, options) {
|
|
297
|
-
const activeRun = issue.activeRunId ? db.runs.getRunById(issue.activeRunId) : undefined;
|
|
298
|
-
const latestRun = db.runs.getLatestRunForIssue(issue.projectId, issue.linearIssueId);
|
|
299
|
-
const latestEvent = db.issueSessions.listIssueSessionEvents(issue.projectId, issue.linearIssueId, { limit: 1 }).at(-1);
|
|
300
|
-
const activeRunType = issue.activeRunId !== undefined
|
|
301
|
-
? (options?.activeRunType ?? activeRun?.runType)
|
|
302
|
-
: undefined;
|
|
303
|
-
const waitingReason = trackedIssue?.waitingReason ?? derivePatchRelayWaitingReason({
|
|
304
|
-
...(activeRunType ? { activeRunType } : {}),
|
|
305
|
-
...(issue.activeRunId !== undefined ? { activeRunId: issue.activeRunId } : {}),
|
|
306
|
-
factoryState: issue.factoryState,
|
|
307
|
-
pendingRunType: issue.pendingRunType,
|
|
308
|
-
...(issue.prNumber !== undefined ? { prNumber: issue.prNumber } : {}),
|
|
309
|
-
prHeadSha: issue.prHeadSha,
|
|
310
|
-
prReviewState: issue.prReviewState,
|
|
311
|
-
prCheckStatus: issue.prCheckStatus,
|
|
312
|
-
lastBlockingReviewHeadSha: issue.lastBlockingReviewHeadSha,
|
|
313
|
-
latestFailureCheckName: issue.lastGitHubFailureCheckName,
|
|
314
|
-
});
|
|
315
|
-
const lines = [
|
|
316
|
-
"## PatchRelay status",
|
|
317
|
-
"",
|
|
318
|
-
statusHeadline(trackedIssue ?? issue, activeRunType),
|
|
319
|
-
];
|
|
320
|
-
const statusNote = trackedIssue?.statusNote ?? deriveIssueStatusNote({ issue, latestRun, latestEvent, waitingReason });
|
|
321
|
-
if (waitingReason) {
|
|
322
|
-
lines.push("", `Waiting: ${waitingReason}`);
|
|
323
|
-
}
|
|
324
|
-
if (statusNote && statusNote !== waitingReason) {
|
|
325
|
-
const label = trackedIssue?.sessionState === "waiting_input" || issue.factoryState === "awaiting_input" ? "Input needed"
|
|
326
|
-
: trackedIssue?.sessionState === "failed" || issue.factoryState === "failed" || issue.factoryState === "escalated" ? "Action needed"
|
|
327
|
-
: "Note";
|
|
328
|
-
lines.push("", `${label}: ${statusNote}`);
|
|
329
|
-
}
|
|
330
|
-
const completionCheck = extractCompletionCheck(latestRun);
|
|
331
|
-
if (completionCheck?.outcome === "needs_input") {
|
|
332
|
-
if (completionCheck.why) {
|
|
333
|
-
lines.push("", `Why: ${completionCheck.why}`);
|
|
334
|
-
}
|
|
335
|
-
if (completionCheck.recommendedReply) {
|
|
336
|
-
lines.push("", `Suggested reply: ${completionCheck.recommendedReply}`);
|
|
337
|
-
}
|
|
338
|
-
const issueRef = issue.issueKey ?? issue.linearIssueId;
|
|
339
|
-
lines.push("", `Reply in a Linear comment to continue, or run \`patchrelay issue prompt ${issueRef} "..."\`.`);
|
|
340
|
-
}
|
|
341
|
-
if (issue.prNumber !== undefined || issue.prUrl) {
|
|
342
|
-
const prLabel = issue.prNumber !== undefined ? `#${issue.prNumber}` : "open";
|
|
343
|
-
lines.push("", `PR: ${issue.prUrl ? `[${prLabel}](${issue.prUrl})` : prLabel}`);
|
|
344
|
-
}
|
|
345
|
-
if (latestRun) {
|
|
346
|
-
lines.push("", `Latest run: ${formatLatestRun(latestRun)}`);
|
|
347
|
-
if (latestRun.failureReason) {
|
|
348
|
-
lines.push("", `Failure: ${latestRun.failureReason}`);
|
|
349
|
-
}
|
|
350
|
-
if (completionCheck && completionCheck.outcome !== "needs_input" && completionCheck.summary !== statusNote) {
|
|
351
|
-
lines.push("", `Completion check: ${completionCheck.summary}`);
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
if (issue.lastGitHubFailureCheckName && (issue.factoryState === "repairing_ci" || issue.prCheckStatus === "failed" || issue.prCheckStatus === "failure")) {
|
|
355
|
-
lines.push("", `Latest failing check: ${issue.lastGitHubFailureCheckName}`);
|
|
356
|
-
}
|
|
357
|
-
lines.push("", "_PatchRelay updates this comment as it works. Review and merge remain downstream._");
|
|
358
|
-
return lines.join("\n");
|
|
359
|
-
}
|
|
360
|
-
function shouldSyncVisibleIssueComment(issue, hasAgentSession) {
|
|
361
|
-
if (!hasAgentSession) {
|
|
362
|
-
return true;
|
|
363
|
-
}
|
|
364
|
-
if (issue.sessionState === "waiting_input" || issue.sessionState === "failed"
|
|
365
|
-
|| issue.factoryState === "awaiting_input" || issue.factoryState === "failed" || issue.factoryState === "escalated") {
|
|
366
|
-
return true;
|
|
367
|
-
}
|
|
368
|
-
if ((issue.sessionState === "done" || issue.factoryState === "done") && issue.prNumber === undefined && !issue.prUrl) {
|
|
369
|
-
return true;
|
|
370
|
-
}
|
|
371
|
-
return false;
|
|
372
|
-
}
|
|
373
|
-
function statusHeadline(issue, activeRunType) {
|
|
374
|
-
if (activeRunType) {
|
|
375
|
-
return `Running ${humanize(activeRunType)}`;
|
|
376
|
-
}
|
|
377
|
-
switch (issue.sessionState) {
|
|
378
|
-
case "waiting_input":
|
|
379
|
-
return issue.waitingReason ?? "Waiting for more input";
|
|
380
|
-
case "running":
|
|
381
|
-
return issue.prNumber !== undefined ? `PR #${issue.prNumber} is actively running` : "Actively running";
|
|
382
|
-
case "done":
|
|
383
|
-
return issue.prNumber !== undefined ? `Completed with PR #${issue.prNumber}` : "Completed";
|
|
384
|
-
case "failed":
|
|
385
|
-
return "Needs operator intervention";
|
|
386
|
-
default:
|
|
387
|
-
break;
|
|
388
|
-
}
|
|
389
|
-
switch (issue.factoryState) {
|
|
390
|
-
case "delegated":
|
|
391
|
-
return "Queued to start work";
|
|
392
|
-
case "implementing":
|
|
393
|
-
return "Implementing requested change";
|
|
394
|
-
case "pr_open":
|
|
395
|
-
return issue.prNumber !== undefined ? `PR #${issue.prNumber} opened` : "PR opened";
|
|
396
|
-
case "changes_requested":
|
|
397
|
-
return "Addressing requested review changes";
|
|
398
|
-
case "repairing_ci":
|
|
399
|
-
return "Repairing failing CI";
|
|
400
|
-
case "awaiting_queue":
|
|
401
|
-
return "Handed off downstream for merge";
|
|
402
|
-
case "repairing_queue":
|
|
403
|
-
return "Repairing merge handoff";
|
|
404
|
-
case "awaiting_input":
|
|
405
|
-
return "Waiting for more input";
|
|
406
|
-
case "failed":
|
|
407
|
-
return "Needs operator intervention";
|
|
408
|
-
case "escalated":
|
|
409
|
-
return "Needs operator intervention";
|
|
410
|
-
case "done":
|
|
411
|
-
return issue.prNumber !== undefined ? `Completed with PR #${issue.prNumber}` : "Completed";
|
|
412
|
-
default:
|
|
413
|
-
return humanize(issue.factoryState);
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
function formatLatestRun(run) {
|
|
417
|
-
const at = run.endedAt ?? run.startedAt;
|
|
418
|
-
return `${humanize(run.runType)} ${run.status} at ${at}`;
|
|
419
|
-
}
|
|
420
|
-
function humanize(value) {
|
|
421
|
-
return value.replaceAll("_", " ");
|
|
422
|
-
}
|
|
423
|
-
function shouldAutoAdvanceLinearState(issue) {
|
|
424
|
-
const normalizedType = issue.currentLinearStateType?.trim().toLowerCase();
|
|
425
|
-
if (normalizedType === "completed" || normalizedType === "canceled" || normalizedType === "cancelled") {
|
|
426
|
-
return false;
|
|
57
|
+
this.progressReporter.clearProgress(runId);
|
|
427
58
|
}
|
|
428
|
-
const normalizedName = issue.currentLinearState?.trim().toLowerCase();
|
|
429
|
-
return normalizedName !== "done" && normalizedName !== "completed" && normalizedName !== "complete";
|
|
430
|
-
}
|
|
431
|
-
function resolveDesiredActiveWorkflowState(issue, trackedIssue, options, liveIssue) {
|
|
432
|
-
if (issue.factoryState === "awaiting_input" || issue.factoryState === "failed" || issue.factoryState === "escalated"
|
|
433
|
-
|| trackedIssue?.sessionState === "waiting_input" || trackedIssue?.sessionState === "failed") {
|
|
434
|
-
return resolvePreferredHumanNeededLinearState(liveIssue);
|
|
435
|
-
}
|
|
436
|
-
const activelyWorking = issue.activeRunId !== undefined
|
|
437
|
-
|| options?.activeRunType !== undefined
|
|
438
|
-
|| trackedIssue?.sessionState === "running"
|
|
439
|
-
|| issue.factoryState === "delegated"
|
|
440
|
-
|| issue.factoryState === "implementing"
|
|
441
|
-
|| issue.factoryState === "changes_requested"
|
|
442
|
-
|| issue.factoryState === "repairing_ci"
|
|
443
|
-
|| issue.factoryState === "repairing_queue";
|
|
444
|
-
if (activelyWorking) {
|
|
445
|
-
return resolvePreferredImplementingLinearState(liveIssue);
|
|
446
|
-
}
|
|
447
|
-
if (issue.factoryState === "awaiting_queue"
|
|
448
|
-
|| issue.prReviewState === "approved"
|
|
449
|
-
|| isApprovedAndGreen(issue.prReviewState, issue.prCheckStatus)) {
|
|
450
|
-
return resolvePreferredDeployingLinearState(liveIssue);
|
|
451
|
-
}
|
|
452
|
-
const reviewQuillActive = hasPendingReviewQuillVerdict(issue.lastGitHubCiSnapshotJson);
|
|
453
|
-
if (reviewQuillActive) {
|
|
454
|
-
return resolvePreferredReviewingLinearState(liveIssue);
|
|
455
|
-
}
|
|
456
|
-
const reviewBound = issue.prNumber !== undefined
|
|
457
|
-
|| Boolean(issue.prUrl)
|
|
458
|
-
|| issue.factoryState === "pr_open"
|
|
459
|
-
|| issue.prReviewState !== undefined
|
|
460
|
-
|| issue.prCheckStatus !== undefined;
|
|
461
|
-
if (reviewBound) {
|
|
462
|
-
return resolvePreferredReviewLinearState(liveIssue);
|
|
463
|
-
}
|
|
464
|
-
return undefined;
|
|
465
|
-
}
|
|
466
|
-
function isApprovedAndGreen(prReviewState, prCheckStatus) {
|
|
467
|
-
const normalizedReview = prReviewState?.trim().toLowerCase();
|
|
468
|
-
const normalizedChecks = prCheckStatus?.trim().toLowerCase();
|
|
469
|
-
return normalizedReview === "approved" && (normalizedChecks === "success" || normalizedChecks === "passed");
|
|
470
|
-
}
|
|
471
|
-
function hasPendingReviewQuillVerdict(snapshotJson) {
|
|
472
|
-
if (!snapshotJson)
|
|
473
|
-
return false;
|
|
474
|
-
try {
|
|
475
|
-
const parsed = JSON.parse(snapshotJson);
|
|
476
|
-
return Array.isArray(parsed.checks) && parsed.checks.some((check) => typeof check.name === "string"
|
|
477
|
-
&& check.name === "review-quill/verdict"
|
|
478
|
-
&& typeof check.status === "string"
|
|
479
|
-
&& check.status.toLowerCase() === "pending");
|
|
480
|
-
}
|
|
481
|
-
catch {
|
|
482
|
-
return false;
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
function resolveWorkingOnSummary(notification) {
|
|
486
|
-
if (notification.method !== "turn/plan/updated") {
|
|
487
|
-
return undefined;
|
|
488
|
-
}
|
|
489
|
-
const plan = notification.params.plan;
|
|
490
|
-
if (!Array.isArray(plan))
|
|
491
|
-
return undefined;
|
|
492
|
-
const ranked = plan
|
|
493
|
-
.map((entry) => entry)
|
|
494
|
-
.filter((entry) => typeof entry.step === "string" && entry.step.trim().length > 0)
|
|
495
|
-
.sort((a, b) => rankPlanStatus(a.status) - rankPlanStatus(b.status));
|
|
496
|
-
const first = ranked[0];
|
|
497
|
-
return summarizeProgressSentence(typeof first?.step === "string" ? first.step : undefined);
|
|
498
|
-
}
|
|
499
|
-
function rankPlanStatus(status) {
|
|
500
|
-
return status === "inProgress" ? 0
|
|
501
|
-
: status === "pending" ? 1
|
|
502
|
-
: status === "completed" ? 2
|
|
503
|
-
: 3;
|
|
504
|
-
}
|
|
505
|
-
function resolveAgentMessageKey(notification, run) {
|
|
506
|
-
if (notification.method === "item/agentMessage/delta") {
|
|
507
|
-
const itemId = typeof notification.params.itemId === "string" ? notification.params.itemId : undefined;
|
|
508
|
-
return itemId ? `${run.id}:${itemId}` : undefined;
|
|
509
|
-
}
|
|
510
|
-
if (notification.method === "item/completed") {
|
|
511
|
-
const item = notification.params.item;
|
|
512
|
-
const itemId = typeof item?.id === "string" ? item.id : undefined;
|
|
513
|
-
const itemType = typeof item?.type === "string" ? item.type : undefined;
|
|
514
|
-
return itemId && itemType === "agentMessage" ? `${run.id}:${itemId}` : undefined;
|
|
515
|
-
}
|
|
516
|
-
return undefined;
|
|
517
|
-
}
|
|
518
|
-
function resolveAgentMessageDelta(notification) {
|
|
519
|
-
if (notification.method !== "item/agentMessage/delta") {
|
|
520
|
-
return undefined;
|
|
521
|
-
}
|
|
522
|
-
return typeof notification.params.delta === "string" ? notification.params.delta : undefined;
|
|
523
|
-
}
|
|
524
|
-
function resolveCompletedAgentMessageText(notification) {
|
|
525
|
-
if (notification.method !== "item/completed") {
|
|
526
|
-
return undefined;
|
|
527
|
-
}
|
|
528
|
-
const item = notification.params.item;
|
|
529
|
-
if (!item || item.type !== "agentMessage")
|
|
530
|
-
return undefined;
|
|
531
|
-
return typeof item.text === "string" ? item.text : undefined;
|
|
532
|
-
}
|
|
533
|
-
function extractFirstSentence(text) {
|
|
534
|
-
const sanitized = sanitizeOperatorFacingText(text)?.replace(/\s+/g, " ").trim();
|
|
535
|
-
if (!sanitized)
|
|
536
|
-
return undefined;
|
|
537
|
-
const match = sanitized.match(/^(.+?[.!?])(?:\s|$)/);
|
|
538
|
-
return truncateProgressText((match?.[1] ?? sanitized).trim(), MAX_PROGRESS_TEXT_LENGTH);
|
|
539
|
-
}
|
|
540
|
-
function extractFirstCompletedSentence(text) {
|
|
541
|
-
const sanitized = sanitizeOperatorFacingText(text)?.replace(/\s+/g, " ").trim();
|
|
542
|
-
if (!sanitized)
|
|
543
|
-
return undefined;
|
|
544
|
-
const match = sanitized.match(/^(.+?[.!?])(?:\s|$)/);
|
|
545
|
-
return match?.[1] ? truncateProgressText(match[1].trim(), MAX_PROGRESS_TEXT_LENGTH) : undefined;
|
|
546
|
-
}
|
|
547
|
-
function summarizeProgressSentence(text) {
|
|
548
|
-
const summary = extractFirstSentence(text);
|
|
549
|
-
if (!summary)
|
|
550
|
-
return undefined;
|
|
551
|
-
return summary.endsWith(".") || summary.endsWith("!") || summary.endsWith("?") ? summary : `${summary}.`;
|
|
552
|
-
}
|
|
553
|
-
function truncateProgressText(text, maxLength) {
|
|
554
|
-
return text.length <= maxLength ? text : `${text.slice(0, maxLength - 3).trimEnd()}...`;
|
|
555
59
|
}
|