patchrelay 0.56.0 → 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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "service": "patchrelay",
3
- "version": "0.56.0",
4
- "commit": "43aa8a020a26",
5
- "builtAt": "2026-05-01T14:26:54.243Z"
3
+ "version": "0.56.1",
4
+ "commit": "39a7bc21b981",
5
+ "builtAt": "2026-05-01T15:22:20.307Z"
6
6
  }
@@ -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
+ }
@@ -64,6 +64,24 @@ export function buildPromptDeliveredThought(runType) {
64
64
  body: `PatchRelay routed your latest instructions into the active ${lowerRunTypeLabel(runType)} workflow.`,
65
65
  };
66
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
+ }
67
85
  export function buildRunStartedActivity(runType) {
68
86
  switch (runType) {
69
87
  case "review_fix":
@@ -79,6 +97,9 @@ export function buildRunStartedActivity(runType) {
79
97
  return { type: "action", action: "Implementing", parameter: "requested change" };
80
98
  }
81
99
  }
100
+ function formatFactoryState(state) {
101
+ return state.replaceAll("_", " ");
102
+ }
82
103
  export function buildRunCompletedActivity(params) {
83
104
  const prLabel = params.prNumber ? `PR #${params.prNumber}` : "the pull request";
84
105
  const summary = trimSummary(params.completionSummary);
@@ -1,7 +1,10 @@
1
1
  import { buildAgentSessionPlanForIssue, } from "../agent-session-plan.js";
2
2
  import { buildAgentSessionExternalUrls } from "../agent-session-presentation.js";
3
- import { buildAlreadyRunningThought, buildAgentSessionAcknowledgementThought, buildBlockedDelegationActivity, buildDelegationThought, buildPromptDeliveredThought, buildStopConfirmationActivity, } from "../linear-session-reporting.js";
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";
4
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",
@@ -122,6 +125,28 @@ export class AgentSessionHandler {
122
125
  return;
123
126
  }
124
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
+ }
125
150
  if (!automationEnabled && promptBody && existingIssue) {
126
151
  await this.publishAgentActivity(linear, normalized.agentSession.id, {
127
152
  type: "thought",
@@ -160,7 +185,10 @@ export class AgentSessionHandler {
160
185
  }
161
186
  if (promptBody && existingIssue && automationEnabled) {
162
187
  const hadPendingWake = this.db.issueSessions.peekIssueSessionWake(project.id, normalized.issue.id) !== undefined;
163
- const directReply = params.isDirectReplyToOutstandingQuestion(existingIssue);
188
+ if (!directReply && promptIntent && followupIntentIsNonActionable(promptIntent)) {
189
+ await this.publishAgentActivity(linear, normalized.agentSession.id, buildNonActionableFollowupActivity(promptIntent));
190
+ return;
191
+ }
164
192
  this.db.issueSessions.appendIssueSessionEventRespectingActiveLease(project.id, normalized.issue.id, {
165
193
  projectId: project.id,
166
194
  linearIssueId: normalized.issue.id,
@@ -184,6 +212,24 @@ export class AgentSessionHandler {
184
212
  await this.publishAgentActivity(linear, normalized.agentSession.id, buildDelegationThought(wakeRunType, "prompt"), { ephemeral: true });
185
213
  }
186
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
+ }
187
233
  async handleStopSignal(params) {
188
234
  const issueId = params.normalized.issue.id;
189
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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "patchrelay",
3
- "version": "0.56.0",
3
+ "version": "0.56.1",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "repository": {