patchrelay 0.55.2 → 0.56.1
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/followup-intent.js +56 -0
- package/dist/linear-session-reporting.js +27 -0
- package/dist/webhook-handler.js +1 -0
- package/dist/webhooks/agent-session-handler.js +76 -3
- package/dist/webhooks/comment-policy.js +2 -0
- package/dist/webhooks/comment-wake-handler.js +93 -1
- package/package.json +1 -1
package/dist/build-info.json
CHANGED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
const IMPLEMENTATION_PATTERNS = [
|
|
2
|
+
/\b(add|address|adjust|change|create|delete|deploy|fix|implement|install|merge|open|polish|publish|push|remove|rename|set|ship|update)\b/i,
|
|
3
|
+
/\b(go on and|go ahead and|please|pls)\s+\b(add|address|adjust|change|create|delete|deploy|fix|implement|install|merge|open|polish|publish|push|remove|rename|set|ship|update|use)\b/i,
|
|
4
|
+
/\b(use|keep|switch|move)\b.+\b(instead|copy|api|contract|behavior|state|repo|branch|team|issue|pr)\b/i,
|
|
5
|
+
];
|
|
6
|
+
const RETRY_PATTERNS = [
|
|
7
|
+
/\b(continue|go on|keep going|proceed|resume)\b/i,
|
|
8
|
+
/\b(retry|try again|rerun|run again|restart)\b/i,
|
|
9
|
+
/\b(next task|next issue)\b/i,
|
|
10
|
+
];
|
|
11
|
+
const STOP_PATTERNS = [
|
|
12
|
+
/\b(stop|cancel|halt|abort)\b/i,
|
|
13
|
+
/\b(pause|hold)\s+(work|implementation|the run|patchrelay)\b/i,
|
|
14
|
+
/\bdo not\s+(continue|proceed|work|implement)\b/i,
|
|
15
|
+
];
|
|
16
|
+
const STATUS_PATTERNS = [
|
|
17
|
+
/\b(status|progress)\b/i,
|
|
18
|
+
/\b(status update|progress update|any update|quick update)\b/i,
|
|
19
|
+
/\b(where are we|what'?s happening|what is happening|what'?s deployed|what is deployed)\b/i,
|
|
20
|
+
/\b(done so far|deployed so far|current work|current run)\b/i,
|
|
21
|
+
];
|
|
22
|
+
const CLARIFICATION_PATTERNS = [
|
|
23
|
+
/\b(fyi|for context|heads up|to clarify|clarification|correction|actually|note that)\b/i,
|
|
24
|
+
/\b(i meant|what i meant|not asking you to|no action needed)\b/i,
|
|
25
|
+
];
|
|
26
|
+
const QUESTION_PATTERNS = [
|
|
27
|
+
/\?$/,
|
|
28
|
+
/\b(can|could|do|does|did|is|are|was|were|why|how|what|when|where|which|who|should|would)\b/i,
|
|
29
|
+
];
|
|
30
|
+
export function classifyFollowupIntent(input) {
|
|
31
|
+
const text = input.trim();
|
|
32
|
+
if (!text)
|
|
33
|
+
return "clarification";
|
|
34
|
+
if (matchesAny(text, STOP_PATTERNS))
|
|
35
|
+
return "stop";
|
|
36
|
+
if (matchesAny(text, RETRY_PATTERNS))
|
|
37
|
+
return "retry";
|
|
38
|
+
if (matchesAny(text, STATUS_PATTERNS))
|
|
39
|
+
return "status";
|
|
40
|
+
if (matchesAny(text, IMPLEMENTATION_PATTERNS))
|
|
41
|
+
return "implementation_request";
|
|
42
|
+
if (matchesAny(text, CLARIFICATION_PATTERNS))
|
|
43
|
+
return "clarification";
|
|
44
|
+
if (matchesAny(text, QUESTION_PATTERNS))
|
|
45
|
+
return "question";
|
|
46
|
+
return "clarification";
|
|
47
|
+
}
|
|
48
|
+
export function followupIntentQueuesWork(intent) {
|
|
49
|
+
return intent === "implementation_request" || intent === "retry";
|
|
50
|
+
}
|
|
51
|
+
export function followupIntentIsNonActionable(intent) {
|
|
52
|
+
return intent === "status" || intent === "question" || intent === "clarification";
|
|
53
|
+
}
|
|
54
|
+
function matchesAny(text, patterns) {
|
|
55
|
+
return patterns.some((pattern) => pattern.test(text));
|
|
56
|
+
}
|
|
@@ -42,6 +42,12 @@ export function buildAlreadyRunningThought(runType) {
|
|
|
42
42
|
body: `PatchRelay is already working on the ${lowerRunTypeLabel(runType)} workflow.`,
|
|
43
43
|
};
|
|
44
44
|
}
|
|
45
|
+
export function buildAgentSessionAcknowledgementThought() {
|
|
46
|
+
return {
|
|
47
|
+
type: "thought",
|
|
48
|
+
body: "PatchRelay received this agent session and is checking the issue state.",
|
|
49
|
+
};
|
|
50
|
+
}
|
|
45
51
|
export function buildBlockedDelegationActivity(blockedByKeys = []) {
|
|
46
52
|
const blockers = blockedByKeys.filter((key) => key.trim().length > 0);
|
|
47
53
|
const blockerText = blockers.length > 0
|
|
@@ -58,6 +64,24 @@ export function buildPromptDeliveredThought(runType) {
|
|
|
58
64
|
body: `PatchRelay routed your latest instructions into the active ${lowerRunTypeLabel(runType)} workflow.`,
|
|
59
65
|
};
|
|
60
66
|
}
|
|
67
|
+
export function buildFollowupStatusActivity(params) {
|
|
68
|
+
const subject = params.issue.issueKey ? `${params.issue.issueKey}` : "this issue";
|
|
69
|
+
const runNote = params.activeRunType
|
|
70
|
+
? ` Active workflow: ${lowerRunTypeLabel(params.activeRunType)}.`
|
|
71
|
+
: params.pendingRunType ? ` Queued workflow: ${lowerRunTypeLabel(params.pendingRunType)}.` : "";
|
|
72
|
+
const prNote = params.issue.prNumber ? ` PR #${params.issue.prNumber}.` : "";
|
|
73
|
+
const statusNote = params.statusNote ? ` ${params.statusNote}` : "";
|
|
74
|
+
return {
|
|
75
|
+
type: "response",
|
|
76
|
+
body: `PatchRelay status: ${subject} is ${formatFactoryState(params.issue.factoryState)}.${prNote}${runNote}${statusNote}`.trim(),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
export function buildNonActionableFollowupActivity(intent) {
|
|
80
|
+
const body = intent === "status"
|
|
81
|
+
? "PatchRelay status is available in the current agent session."
|
|
82
|
+
: "PatchRelay did not start implementation because this looks like a question or clarification. Ask PatchRelay to continue, retry, or implement when you want work to run.";
|
|
83
|
+
return { type: "response", body };
|
|
84
|
+
}
|
|
61
85
|
export function buildRunStartedActivity(runType) {
|
|
62
86
|
switch (runType) {
|
|
63
87
|
case "review_fix":
|
|
@@ -73,6 +97,9 @@ export function buildRunStartedActivity(runType) {
|
|
|
73
97
|
return { type: "action", action: "Implementing", parameter: "requested change" };
|
|
74
98
|
}
|
|
75
99
|
}
|
|
100
|
+
function formatFactoryState(state) {
|
|
101
|
+
return state.replaceAll("_", " ");
|
|
102
|
+
}
|
|
76
103
|
export function buildRunCompletedActivity(params) {
|
|
77
104
|
const prLabel = params.prNumber ? `PR #${params.prNumber}` : "the pull request";
|
|
78
105
|
const summary = trimSummary(params.completionSummary);
|
package/dist/webhook-handler.js
CHANGED
|
@@ -74,6 +74,7 @@ export class WebhookHandler {
|
|
|
74
74
|
this.db.webhookEvents.markWebhookProcessed(webhookEventId, "processed");
|
|
75
75
|
return;
|
|
76
76
|
}
|
|
77
|
+
await this.agentSessionHandler.acknowledgeCreated(normalized);
|
|
77
78
|
const routed = await this.contextLoader.load(normalized);
|
|
78
79
|
const project = routed?.project;
|
|
79
80
|
if (!project) {
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { buildAgentSessionPlanForIssue, } from "../agent-session-plan.js";
|
|
2
2
|
import { buildAgentSessionExternalUrls } from "../agent-session-presentation.js";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { classifyFollowupIntent, followupIntentIsNonActionable } from "../followup-intent.js";
|
|
4
|
+
import { extractLatestAssistantSummary } from "../issue-session-events.js";
|
|
5
|
+
import { buildAlreadyRunningThought, buildAgentSessionAcknowledgementThought, buildBlockedDelegationActivity, buildDelegationThought, buildFollowupStatusActivity, buildNonActionableFollowupActivity, buildPromptDeliveredThought, buildStopConfirmationActivity, } from "../linear-session-reporting.js";
|
|
6
|
+
import { resolveProject, triggerEventAllowed } from "../project-resolution.js";
|
|
7
|
+
import { deriveIssueStatusNote } from "../status-note.js";
|
|
5
8
|
const PATCHRELAY_AGENT_ACTIVITY_TYPES = new Set([
|
|
6
9
|
"action",
|
|
7
10
|
"elicitation",
|
|
@@ -24,6 +27,33 @@ export class AgentSessionHandler {
|
|
|
24
27
|
this.logger = logger;
|
|
25
28
|
this.feed = feed;
|
|
26
29
|
}
|
|
30
|
+
async acknowledgeCreated(normalized) {
|
|
31
|
+
if (normalized.triggerEvent !== "agentSessionCreated" || !normalized.agentSession?.id || !normalized.issue) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const project = resolveProject(this.config, normalized.issue);
|
|
35
|
+
if (!project || !triggerEventAllowed(project, normalized.triggerEvent)) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const linear = await this.linearProvider.forProject(project.id);
|
|
39
|
+
if (!linear?.createAgentActivity) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
await linear.createAgentActivity({
|
|
44
|
+
agentSessionId: normalized.agentSession.id,
|
|
45
|
+
content: buildAgentSessionAcknowledgementThought(),
|
|
46
|
+
ephemeral: true,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
this.logger.warn({
|
|
51
|
+
agentSessionId: normalized.agentSession.id,
|
|
52
|
+
issueKey: normalized.issue.identifier,
|
|
53
|
+
error: error instanceof Error ? error.message : String(error),
|
|
54
|
+
}, "Failed to acknowledge Linear agent session creation");
|
|
55
|
+
}
|
|
56
|
+
}
|
|
27
57
|
async handle(params) {
|
|
28
58
|
const { normalized, project, trackedIssue, wakeRunType, delegated } = params;
|
|
29
59
|
if (!normalized.agentSession?.id || !normalized.issue)
|
|
@@ -95,6 +125,28 @@ export class AgentSessionHandler {
|
|
|
95
125
|
return;
|
|
96
126
|
}
|
|
97
127
|
const promptBody = normalized.agentSession.promptBody?.trim();
|
|
128
|
+
const directReply = promptBody && existingIssue ? params.isDirectReplyToOutstandingQuestion(existingIssue) : false;
|
|
129
|
+
const promptIntent = promptBody ? classifyFollowupIntent(promptBody) : undefined;
|
|
130
|
+
if (promptBody && existingIssue && promptIntent === "stop") {
|
|
131
|
+
await this.handleStopSignal({
|
|
132
|
+
normalized,
|
|
133
|
+
project,
|
|
134
|
+
trackedIssue,
|
|
135
|
+
activeRun,
|
|
136
|
+
linear,
|
|
137
|
+
syncAgentSession: (agentSessionId, issue, options) => this.syncAgentSession(linear, agentSessionId, issue, params.peekPendingSessionWakeRunType, options),
|
|
138
|
+
});
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
if (promptBody && existingIssue && promptIntent === "status" && !directReply) {
|
|
142
|
+
await this.publishAgentActivity(linear, normalized.agentSession.id, this.buildStatusActivity(existingIssue, activeRun, params.peekPendingSessionWakeRunType));
|
|
143
|
+
await this.syncAgentSession(linear, normalized.agentSession.id, existingIssue ?? trackedIssue, params.peekPendingSessionWakeRunType, activeRun ? { activeRunType: activeRun.runType } : undefined);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
if (promptBody && promptIntent && followupIntentIsNonActionable(promptIntent) && !directReply) {
|
|
147
|
+
await this.publishAgentActivity(linear, normalized.agentSession.id, buildNonActionableFollowupActivity(promptIntent));
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
98
150
|
if (!automationEnabled && promptBody && existingIssue) {
|
|
99
151
|
await this.publishAgentActivity(linear, normalized.agentSession.id, {
|
|
100
152
|
type: "thought",
|
|
@@ -133,7 +185,10 @@ export class AgentSessionHandler {
|
|
|
133
185
|
}
|
|
134
186
|
if (promptBody && existingIssue && automationEnabled) {
|
|
135
187
|
const hadPendingWake = this.db.issueSessions.peekIssueSessionWake(project.id, normalized.issue.id) !== undefined;
|
|
136
|
-
|
|
188
|
+
if (!directReply && promptIntent && followupIntentIsNonActionable(promptIntent)) {
|
|
189
|
+
await this.publishAgentActivity(linear, normalized.agentSession.id, buildNonActionableFollowupActivity(promptIntent));
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
137
192
|
this.db.issueSessions.appendIssueSessionEventRespectingActiveLease(project.id, normalized.issue.id, {
|
|
138
193
|
projectId: project.id,
|
|
139
194
|
linearIssueId: normalized.issue.id,
|
|
@@ -157,6 +212,24 @@ export class AgentSessionHandler {
|
|
|
157
212
|
await this.publishAgentActivity(linear, normalized.agentSession.id, buildDelegationThought(wakeRunType, "prompt"), { ephemeral: true });
|
|
158
213
|
}
|
|
159
214
|
}
|
|
215
|
+
buildStatusActivity(issue, activeRun, peekPendingSessionWakeRunType) {
|
|
216
|
+
const latestRun = activeRun ?? this.db.runs.getLatestRunForIssue(issue.projectId, issue.linearIssueId);
|
|
217
|
+
const latestEvent = this.db.issueSessions.listIssueSessionEvents(issue.projectId, issue.linearIssueId).at(-1);
|
|
218
|
+
const statusNote = deriveIssueStatusNote({
|
|
219
|
+
issue,
|
|
220
|
+
latestRun,
|
|
221
|
+
latestEvent,
|
|
222
|
+
sessionSummary: extractLatestAssistantSummary(latestRun),
|
|
223
|
+
waitingReason: undefined,
|
|
224
|
+
});
|
|
225
|
+
const pendingRunType = peekPendingSessionWakeRunType(issue.projectId, issue.linearIssueId);
|
|
226
|
+
return buildFollowupStatusActivity({
|
|
227
|
+
issue,
|
|
228
|
+
...(statusNote ? { statusNote } : {}),
|
|
229
|
+
...(activeRun?.runType ? { activeRunType: activeRun.runType } : {}),
|
|
230
|
+
...(pendingRunType ? { pendingRunType } : {}),
|
|
231
|
+
});
|
|
232
|
+
}
|
|
160
233
|
async handleStopSignal(params) {
|
|
161
234
|
const issueId = params.normalized.issue.id;
|
|
162
235
|
const sessionId = params.normalized.agentSession.id;
|
|
@@ -31,6 +31,8 @@ export function isPatchRelayGeneratedActivityComment(body) {
|
|
|
31
31
|
|| body.startsWith("PatchRelay is already working on ")
|
|
32
32
|
|| body.startsWith("PatchRelay received the ")
|
|
33
33
|
|| body.startsWith("PatchRelay routed your latest instructions into ")
|
|
34
|
+
|| body.startsWith("PatchRelay status:")
|
|
35
|
+
|| body.startsWith("PatchRelay did not start implementation ")
|
|
34
36
|
|| body.startsWith("PatchRelay has stopped work as requested.")
|
|
35
37
|
|| body.startsWith("Merge preparation failed ")
|
|
36
38
|
|| body === "This thread is for an agent session with patchrelay.";
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { classifyFollowupIntent, followupIntentIsNonActionable, followupIntentQueuesWork } from "../followup-intent.js";
|
|
1
2
|
import { triggerEventAllowed } from "../project-resolution.js";
|
|
2
3
|
import { hasExplicitPatchRelayWakeIntent, isInertPatchRelayComment, isPatchRelayManagedCommentAuthor, } from "./comment-policy.js";
|
|
3
4
|
import { classifyIssue } from "../issue-class.js";
|
|
@@ -57,9 +58,10 @@ export class CommentWakeHandler {
|
|
|
57
58
|
});
|
|
58
59
|
return;
|
|
59
60
|
}
|
|
61
|
+
const directReply = params.isDirectReplyToOutstandingQuestion(issue);
|
|
62
|
+
const intent = classifyFollowupIntent(trimmedBody);
|
|
60
63
|
if (!issue.activeRunId) {
|
|
61
64
|
if (ENQUEUEABLE_STATES.has(issue.factoryState)) {
|
|
62
|
-
const directReply = params.isDirectReplyToOutstandingQuestion(issue);
|
|
63
65
|
const wakeIntent = issueClass === "orchestration" || directReply || hasExplicitPatchRelayWakeIntent(trimmedBody);
|
|
64
66
|
if (!wakeIntent) {
|
|
65
67
|
this.feed?.publish({
|
|
@@ -73,6 +75,42 @@ export class CommentWakeHandler {
|
|
|
73
75
|
});
|
|
74
76
|
return;
|
|
75
77
|
}
|
|
78
|
+
if (intent === "stop") {
|
|
79
|
+
this.db.issueSessions.appendIssueSessionEventRespectingActiveLease(project.id, normalized.issue.id, {
|
|
80
|
+
projectId: project.id,
|
|
81
|
+
linearIssueId: normalized.issue.id,
|
|
82
|
+
eventType: "stop_requested",
|
|
83
|
+
eventJson: JSON.stringify({
|
|
84
|
+
body: trimmedBody,
|
|
85
|
+
author: normalized.comment.userName,
|
|
86
|
+
}),
|
|
87
|
+
});
|
|
88
|
+
this.db.issueSessions.clearPendingIssueSessionEventsRespectingActiveLease(project.id, normalized.issue.id);
|
|
89
|
+
this.feed?.publish({
|
|
90
|
+
level: "info",
|
|
91
|
+
kind: "comment",
|
|
92
|
+
projectId: project.id,
|
|
93
|
+
issueKey: trackedIssue?.issueKey,
|
|
94
|
+
status: "stopped",
|
|
95
|
+
summary: "Stop request recorded from Linear comment",
|
|
96
|
+
detail: trimmedBody.slice(0, 200),
|
|
97
|
+
});
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (!directReply && !followupIntentQueuesWork(intent)) {
|
|
101
|
+
this.feed?.publish({
|
|
102
|
+
level: "info",
|
|
103
|
+
kind: "comment",
|
|
104
|
+
projectId: project.id,
|
|
105
|
+
issueKey: trackedIssue?.issueKey,
|
|
106
|
+
status: intent === "status" ? "status_requested" : "ignored",
|
|
107
|
+
summary: intent === "status"
|
|
108
|
+
? "Ignored status comment without queueing work"
|
|
109
|
+
: "Ignored non-actionable follow-up comment",
|
|
110
|
+
detail: trimmedBody.slice(0, 200),
|
|
111
|
+
});
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
76
114
|
const runType = issue.prReviewState === "changes_requested" ? "review_fix" : "implementation";
|
|
77
115
|
const hadPendingWake = this.db.issueSessions.peekIssueSessionWake(project.id, normalized.issue.id) !== undefined;
|
|
78
116
|
this.db.issueSessions.appendIssueSessionEventRespectingActiveLease(project.id, normalized.issue.id, {
|
|
@@ -102,6 +140,60 @@ export class CommentWakeHandler {
|
|
|
102
140
|
const run = this.db.runs.getRunById(issue.activeRunId);
|
|
103
141
|
if (!run?.threadId || !run.turnId)
|
|
104
142
|
return;
|
|
143
|
+
if (intent === "stop") {
|
|
144
|
+
try {
|
|
145
|
+
await this.codex.steerTurn({
|
|
146
|
+
threadId: run.threadId,
|
|
147
|
+
turnId: run.turnId,
|
|
148
|
+
input: "STOP: The user has requested you stop working immediately. Do not make further changes. Wrap up and exit.",
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
this.logger.warn({ issueKey: trackedIssue?.issueKey, error: error instanceof Error ? error.message : String(error) }, "Failed to steer Codex turn for comment stop request");
|
|
153
|
+
}
|
|
154
|
+
this.db.runs.finishRun(run.id, { status: "released", threadId: run.threadId, turnId: run.turnId });
|
|
155
|
+
this.db.issueSessions.upsertIssueRespectingActiveLease(project.id, normalized.issue.id, {
|
|
156
|
+
projectId: project.id,
|
|
157
|
+
linearIssueId: normalized.issue.id,
|
|
158
|
+
activeRunId: null,
|
|
159
|
+
factoryState: "awaiting_input",
|
|
160
|
+
});
|
|
161
|
+
this.db.issueSessions.appendIssueSessionEventRespectingActiveLease(project.id, normalized.issue.id, {
|
|
162
|
+
projectId: project.id,
|
|
163
|
+
linearIssueId: normalized.issue.id,
|
|
164
|
+
eventType: "stop_requested",
|
|
165
|
+
eventJson: JSON.stringify({
|
|
166
|
+
body: trimmedBody,
|
|
167
|
+
author: normalized.comment.userName,
|
|
168
|
+
}),
|
|
169
|
+
});
|
|
170
|
+
this.db.issueSessions.clearPendingIssueSessionEventsRespectingActiveLease(project.id, normalized.issue.id);
|
|
171
|
+
this.feed?.publish({
|
|
172
|
+
level: "info",
|
|
173
|
+
kind: "comment",
|
|
174
|
+
projectId: project.id,
|
|
175
|
+
issueKey: trackedIssue?.issueKey,
|
|
176
|
+
stage: run.runType,
|
|
177
|
+
status: "stopped",
|
|
178
|
+
summary: "Stop request delivered to active workflow",
|
|
179
|
+
});
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
if (!directReply && followupIntentIsNonActionable(intent)) {
|
|
183
|
+
this.feed?.publish({
|
|
184
|
+
level: "info",
|
|
185
|
+
kind: "comment",
|
|
186
|
+
projectId: project.id,
|
|
187
|
+
issueKey: trackedIssue?.issueKey,
|
|
188
|
+
stage: run.runType,
|
|
189
|
+
status: intent === "status" ? "status_requested" : "ignored",
|
|
190
|
+
summary: intent === "status"
|
|
191
|
+
? "Ignored status comment without steering active workflow"
|
|
192
|
+
: "Ignored non-actionable follow-up comment",
|
|
193
|
+
detail: trimmedBody.slice(0, 200),
|
|
194
|
+
});
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
105
197
|
const body = [
|
|
106
198
|
"New Linear comment received while you are working.",
|
|
107
199
|
normalized.comment.userName ? `Author: ${normalized.comment.userName}` : undefined,
|