patchrelay 0.69.1 → 0.69.3
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/agent-input-service.js +300 -0
- package/dist/build-info.json +3 -3
- package/dist/cli/commands/issues.js +2 -2
- package/dist/cli/connect-flow.js +2 -2
- package/dist/cli/operator-client.js +4 -6
- package/dist/cli/watch/IssueDetailView.js +1 -1
- package/dist/cli/watch/StateHistoryView.js +0 -1
- package/dist/cli/watch/render-rich-text.js +6 -6
- package/dist/codex-conversation-adapter.js +1 -270
- package/dist/hook-runner.js +1 -1
- package/dist/idle-reconciliation.js +2 -3
- package/dist/install.js +2 -2
- package/dist/operator-retry-event.js +3 -3
- package/dist/orchestration-parent-wake.js +1 -3
- package/dist/reactive-pr-state.js +1 -1
- package/dist/service-issue-actions.js +19 -31
- package/dist/service.js +5 -2
- package/dist/webhook-handler.js +5 -5
- package/dist/webhooks/agent-session-handler.js +6 -6
- package/dist/webhooks/comment-wake-handler.js +5 -5
- package/dist/webhooks/desired-stage-recorder.js +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
import { buildFollowupStatusActivity, buildNonActionableFollowupActivity, buildPromptDeliveryFailedActivity, buildPromptDeliveredThought, } from "./linear-session-reporting.js";
|
|
2
|
+
import { deriveIssueStatusNote } from "./status-note.js";
|
|
3
|
+
import { extractLatestAssistantSummary } from "./issue-session-events.js";
|
|
4
|
+
export class AgentInputService {
|
|
5
|
+
db;
|
|
6
|
+
codex;
|
|
7
|
+
wakeDispatcher;
|
|
8
|
+
logger;
|
|
9
|
+
feed;
|
|
10
|
+
followupClassifier;
|
|
11
|
+
constructor(db, codex, wakeDispatcher, logger, feed, followupClassifier) {
|
|
12
|
+
this.db = db;
|
|
13
|
+
this.codex = codex;
|
|
14
|
+
this.wakeDispatcher = wakeDispatcher;
|
|
15
|
+
this.logger = logger;
|
|
16
|
+
this.feed = feed;
|
|
17
|
+
this.followupClassifier = followupClassifier;
|
|
18
|
+
}
|
|
19
|
+
async deliverAgentInput(params) {
|
|
20
|
+
const body = params.body.trim();
|
|
21
|
+
if (!body)
|
|
22
|
+
return { status: "ignored" };
|
|
23
|
+
const issue = this.db.issues.getIssue(params.issue.projectId, params.issue.linearIssueId) ?? params.issue;
|
|
24
|
+
const activeRun = issue.activeRunId ? this.db.runs.getRunById(issue.activeRunId) : undefined;
|
|
25
|
+
const intent = await this.classify(body, params.source, issue, activeRun, params.directReply === true);
|
|
26
|
+
if (intent?.intent === "status" && !params.directReply) {
|
|
27
|
+
await params.emitActivity?.(this.buildStatusActivity(issue, activeRun, params.peekPendingSessionWakeRunType, activeRun ? "thought" : "response"), activeRun ? { ephemeral: true } : undefined);
|
|
28
|
+
return { status: "answered", ...(activeRun?.runType ? { activeRunType: activeRun.runType } : {}) };
|
|
29
|
+
}
|
|
30
|
+
if (intent?.intent === "stop") {
|
|
31
|
+
if (activeRun) {
|
|
32
|
+
await this.stopActiveRun(issue, activeRun, body, params.source);
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
this.wakeDispatcher.recordEventAndDispatch(issue.projectId, issue.linearIssueId, {
|
|
36
|
+
eventType: "stop_requested",
|
|
37
|
+
eventJson: JSON.stringify({ body, source: params.source, ...(params.author ? { author: params.author } : {}) }),
|
|
38
|
+
});
|
|
39
|
+
this.db.issueSessions.clearPendingIssueSessionEventsRespectingActiveLease(issue.projectId, issue.linearIssueId);
|
|
40
|
+
}
|
|
41
|
+
return { status: "stopped", ...(activeRun?.runType ? { activeRunType: activeRun.runType } : {}) };
|
|
42
|
+
}
|
|
43
|
+
if (!issue.delegatedToPatchRelay && !(activeRun && params.source === "patchrelay_operator_prompt")) {
|
|
44
|
+
await params.emitActivity?.({ type: "thought", body: "PatchRelay is paused because the issue is undelegated." }, { ephemeral: true });
|
|
45
|
+
return { status: "ignored" };
|
|
46
|
+
}
|
|
47
|
+
if (activeRun) {
|
|
48
|
+
return await this.steerActiveRun({
|
|
49
|
+
issue,
|
|
50
|
+
activeRun,
|
|
51
|
+
body,
|
|
52
|
+
source: params.source,
|
|
53
|
+
author: params.author,
|
|
54
|
+
operatorSource: params.operatorSource,
|
|
55
|
+
emitActivity: params.emitActivity,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
return await this.queueIdleInput({
|
|
59
|
+
project: params.project,
|
|
60
|
+
issue,
|
|
61
|
+
body,
|
|
62
|
+
source: params.source,
|
|
63
|
+
author: params.author,
|
|
64
|
+
operatorSource: params.operatorSource,
|
|
65
|
+
directReply: params.directReply === true,
|
|
66
|
+
emitActivity: params.emitActivity,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
async classify(body, source, issue, activeRun, directReply) {
|
|
70
|
+
if (source === "patchrelay_operator_prompt")
|
|
71
|
+
return undefined;
|
|
72
|
+
if (!this.followupClassifier)
|
|
73
|
+
return undefined;
|
|
74
|
+
return await this.followupClassifier.classify(body, {
|
|
75
|
+
source: source === "linear_agent_session" ? "agentPrompted" : "comment",
|
|
76
|
+
...(activeRun?.runType ? { activeRunType: activeRun.runType } : {}),
|
|
77
|
+
factoryState: issue.factoryState,
|
|
78
|
+
directReply,
|
|
79
|
+
delegatedToPatchRelay: issue.delegatedToPatchRelay,
|
|
80
|
+
prReviewState: issue.prReviewState,
|
|
81
|
+
explicitWakeIntent: true,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
async steerActiveRun(params) {
|
|
85
|
+
const { issue, activeRun, body, source } = params;
|
|
86
|
+
if (!activeRun.threadId || !activeRun.turnId) {
|
|
87
|
+
const queuedRunType = this.queueFollowUpEvent(issue, body, source, params.author, params.operatorSource, false);
|
|
88
|
+
return { status: "queued", ...(queuedRunType ? { queuedRunType } : {}) };
|
|
89
|
+
}
|
|
90
|
+
const input = [
|
|
91
|
+
source === "linear_agent_session"
|
|
92
|
+
? "New Linear agent-session prompt received while you are working."
|
|
93
|
+
: source === "linear_addressed_comment"
|
|
94
|
+
? "New explicitly addressed Linear issue comment received while you are working."
|
|
95
|
+
: "New PatchRelay operator prompt received while you are working.",
|
|
96
|
+
params.author ? `Author: ${params.author}` : undefined,
|
|
97
|
+
"",
|
|
98
|
+
"Checkpoint contract: incorporate this instruction before your next meaningful side effect when possible. If you are already inside a non-interruptible command, finish that command, then re-plan with this input before continuing.",
|
|
99
|
+
"",
|
|
100
|
+
body,
|
|
101
|
+
].filter(Boolean).join("\n");
|
|
102
|
+
try {
|
|
103
|
+
await this.codex.steerTurn({ threadId: activeRun.threadId, turnId: activeRun.turnId, input });
|
|
104
|
+
this.recordPromptDelivery({
|
|
105
|
+
issue,
|
|
106
|
+
source,
|
|
107
|
+
runId: activeRun.id,
|
|
108
|
+
runType: activeRun.runType,
|
|
109
|
+
status: "delivered",
|
|
110
|
+
body,
|
|
111
|
+
primitive: "turn/steer",
|
|
112
|
+
threadId: activeRun.threadId,
|
|
113
|
+
turnId: activeRun.turnId,
|
|
114
|
+
});
|
|
115
|
+
this.feed?.publish({
|
|
116
|
+
level: "info",
|
|
117
|
+
kind: inputFeedKind(source),
|
|
118
|
+
projectId: issue.projectId,
|
|
119
|
+
issueKey: issue.issueKey,
|
|
120
|
+
stage: activeRun.runType,
|
|
121
|
+
status: "delivered",
|
|
122
|
+
summary: `Delivered agent input to active ${activeRun.runType} workflow`,
|
|
123
|
+
});
|
|
124
|
+
await params.emitActivity?.(buildPromptDeliveredThought(activeRun.runType), { ephemeral: true });
|
|
125
|
+
return { status: "steered", activeRunType: activeRun.runType };
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
129
|
+
this.logger.warn({ issueKey: issue.issueKey, error: message }, "Failed to deliver agent input to active Codex turn");
|
|
130
|
+
this.recordPromptDelivery({
|
|
131
|
+
issue,
|
|
132
|
+
source,
|
|
133
|
+
runId: activeRun.id,
|
|
134
|
+
runType: activeRun.runType,
|
|
135
|
+
status: "delivery_failed",
|
|
136
|
+
body,
|
|
137
|
+
primitive: "turn/steer",
|
|
138
|
+
threadId: activeRun.threadId,
|
|
139
|
+
turnId: activeRun.turnId,
|
|
140
|
+
error: message,
|
|
141
|
+
});
|
|
142
|
+
const queuedRunType = this.queueFollowUpEvent(issue, body, source, params.author, params.operatorSource, false);
|
|
143
|
+
this.feed?.publish({
|
|
144
|
+
level: "warn",
|
|
145
|
+
kind: inputFeedKind(source),
|
|
146
|
+
projectId: issue.projectId,
|
|
147
|
+
issueKey: issue.issueKey,
|
|
148
|
+
stage: activeRun.runType,
|
|
149
|
+
status: "delivery_failed",
|
|
150
|
+
summary: `Could not deliver agent input to active ${activeRun.runType} workflow`,
|
|
151
|
+
});
|
|
152
|
+
await params.emitActivity?.(buildPromptDeliveryFailedActivity(activeRun.runType, message));
|
|
153
|
+
return { status: "delivery_failed", activeRunType: activeRun.runType, ...(queuedRunType ? { queuedRunType } : {}) };
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
async queueIdleInput(params) {
|
|
157
|
+
const originalIssue = params.issue;
|
|
158
|
+
let issue = originalIssue;
|
|
159
|
+
const replacementPrRequired = originalIssue.factoryState === "done" && originalIssue.prNumber !== undefined;
|
|
160
|
+
if (replacementPrRequired) {
|
|
161
|
+
issue = this.prepareReplacementWork(params.project, originalIssue);
|
|
162
|
+
}
|
|
163
|
+
const queuedRunType = this.queueFollowUpEvent(issue, params.body, params.source, params.author, params.operatorSource, params.directReply, replacementPrRequired ? originalIssue : undefined);
|
|
164
|
+
if (queuedRunType) {
|
|
165
|
+
await params.emitActivity?.(replacementPrRequired
|
|
166
|
+
? { type: "action", action: "Reopening", parameter: `completed PR #${originalIssue.prNumber} for replacement work` }
|
|
167
|
+
: buildPromptDeliveredThought(queuedRunType), { ephemeral: true });
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
await params.emitActivity?.(buildNonActionableFollowupActivity("unknown_needs_ack"));
|
|
171
|
+
}
|
|
172
|
+
return { status: queuedRunType ? "queued" : "ignored", ...(queuedRunType ? { queuedRunType } : {}) };
|
|
173
|
+
}
|
|
174
|
+
queueFollowUpEvent(issue, body, source, author, operatorSource, directReply, previousIssue) {
|
|
175
|
+
return this.wakeDispatcher.recordEventAndDispatch(issue.projectId, issue.linearIssueId, {
|
|
176
|
+
eventType: directReply ? "direct_reply" : inputSourceEventType(source),
|
|
177
|
+
eventJson: JSON.stringify({
|
|
178
|
+
...(source === "linear_addressed_comment" ? { body } : { text: body }),
|
|
179
|
+
source: inputSourcePayloadSource(source),
|
|
180
|
+
...(author ? { author } : {}),
|
|
181
|
+
...(operatorSource ? { operatorSource } : {}),
|
|
182
|
+
...(previousIssue?.prNumber !== undefined
|
|
183
|
+
? {
|
|
184
|
+
replacementPrRequired: true,
|
|
185
|
+
previousPrNumber: previousIssue.prNumber,
|
|
186
|
+
...(previousIssue.prUrl ? { previousPrUrl: previousIssue.prUrl } : {}),
|
|
187
|
+
...(previousIssue.prState ? { previousPrState: previousIssue.prState } : {}),
|
|
188
|
+
...(previousIssue.prHeadSha ? { previousPrHeadSha: previousIssue.prHeadSha } : {}),
|
|
189
|
+
}
|
|
190
|
+
: {}),
|
|
191
|
+
}),
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
prepareReplacementWork(project, issue) {
|
|
195
|
+
const issueRef = (issue.issueKey ?? issue.linearIssueId).replace(/[^a-zA-Z0-9._-]+/g, "-");
|
|
196
|
+
const suffix = Date.now().toString(36);
|
|
197
|
+
return this.db.issueSessions.upsertIssueRespectingActiveLease(issue.projectId, issue.linearIssueId, {
|
|
198
|
+
projectId: issue.projectId,
|
|
199
|
+
linearIssueId: issue.linearIssueId,
|
|
200
|
+
factoryState: "delegated",
|
|
201
|
+
branchName: `${project.branchPrefix}/${issueRef}-replacement-${suffix}`,
|
|
202
|
+
prNumber: null,
|
|
203
|
+
prUrl: null,
|
|
204
|
+
prState: null,
|
|
205
|
+
prIsDraft: null,
|
|
206
|
+
prHeadSha: null,
|
|
207
|
+
prAuthorLogin: null,
|
|
208
|
+
prReviewState: null,
|
|
209
|
+
prCheckStatus: null,
|
|
210
|
+
lastBlockingReviewHeadSha: null,
|
|
211
|
+
}) ?? issue;
|
|
212
|
+
}
|
|
213
|
+
async stopActiveRun(issue, run, body, source) {
|
|
214
|
+
if (run.threadId && run.turnId) {
|
|
215
|
+
try {
|
|
216
|
+
await this.codex.steerTurn({
|
|
217
|
+
threadId: run.threadId,
|
|
218
|
+
turnId: run.turnId,
|
|
219
|
+
input: "STOP: The user has requested you stop working immediately. Do not make further changes. Wrap up and exit.",
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
catch (error) {
|
|
223
|
+
this.logger.warn({ issueKey: issue.issueKey, error: error instanceof Error ? error.message : String(error) }, "Failed to steer Codex turn for stop request");
|
|
224
|
+
}
|
|
225
|
+
this.db.runs.finishRun(run.id, { status: "released", threadId: run.threadId, turnId: run.turnId });
|
|
226
|
+
}
|
|
227
|
+
this.db.issueSessions.upsertIssueRespectingActiveLease(issue.projectId, issue.linearIssueId, {
|
|
228
|
+
projectId: issue.projectId,
|
|
229
|
+
linearIssueId: issue.linearIssueId,
|
|
230
|
+
activeRunId: null,
|
|
231
|
+
factoryState: "awaiting_input",
|
|
232
|
+
});
|
|
233
|
+
this.wakeDispatcher.recordEventAndDispatch(issue.projectId, issue.linearIssueId, {
|
|
234
|
+
eventType: "stop_requested",
|
|
235
|
+
eventJson: JSON.stringify({ body, source }),
|
|
236
|
+
});
|
|
237
|
+
this.db.issueSessions.clearPendingIssueSessionEventsRespectingActiveLease(issue.projectId, issue.linearIssueId);
|
|
238
|
+
this.db.issueSessions.releaseIssueSessionLeaseRespectingActiveLease(issue.projectId, issue.linearIssueId);
|
|
239
|
+
}
|
|
240
|
+
buildStatusActivity(issue, activeRun, peekPendingSessionWakeRunType, activityType) {
|
|
241
|
+
const latestRun = activeRun ?? this.db.runs.getLatestRunForIssue(issue.projectId, issue.linearIssueId);
|
|
242
|
+
const latestEvent = this.db.issueSessions.listIssueSessionEvents(issue.projectId, issue.linearIssueId).at(-1);
|
|
243
|
+
const statusNote = deriveIssueStatusNote({
|
|
244
|
+
issue,
|
|
245
|
+
latestRun,
|
|
246
|
+
latestEvent,
|
|
247
|
+
sessionSummary: extractLatestAssistantSummary(latestRun),
|
|
248
|
+
waitingReason: undefined,
|
|
249
|
+
});
|
|
250
|
+
const pendingRunType = peekPendingSessionWakeRunType?.(issue.projectId, issue.linearIssueId);
|
|
251
|
+
return buildFollowupStatusActivity({
|
|
252
|
+
issue,
|
|
253
|
+
...(statusNote ? { statusNote } : {}),
|
|
254
|
+
...(activeRun?.runType ? { activeRunType: activeRun.runType } : {}),
|
|
255
|
+
...(pendingRunType ? { pendingRunType } : {}),
|
|
256
|
+
activityType,
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
recordPromptDelivery(params) {
|
|
260
|
+
this.db.issueSessions.appendIssueSessionEventRespectingActiveLease(params.issue.projectId, params.issue.linearIssueId, {
|
|
261
|
+
projectId: params.issue.projectId,
|
|
262
|
+
linearIssueId: params.issue.linearIssueId,
|
|
263
|
+
eventType: "prompt_delivered",
|
|
264
|
+
eventJson: JSON.stringify({
|
|
265
|
+
source: inputSourcePayloadSource(params.source),
|
|
266
|
+
runId: params.runId,
|
|
267
|
+
runType: params.runType,
|
|
268
|
+
status: params.status,
|
|
269
|
+
body: params.body,
|
|
270
|
+
primitive: params.primitive,
|
|
271
|
+
...(params.threadId ? { threadId: params.threadId } : {}),
|
|
272
|
+
...(params.turnId ? { turnId: params.turnId } : {}),
|
|
273
|
+
...(params.error ? { error: params.error } : {}),
|
|
274
|
+
}),
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
function inputSourceEventType(source) {
|
|
279
|
+
switch (source) {
|
|
280
|
+
case "linear_agent_session":
|
|
281
|
+
return "followup_prompt";
|
|
282
|
+
case "linear_addressed_comment":
|
|
283
|
+
return "followup_comment";
|
|
284
|
+
case "patchrelay_operator_prompt":
|
|
285
|
+
return "operator_prompt";
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
function inputSourcePayloadSource(source) {
|
|
289
|
+
switch (source) {
|
|
290
|
+
case "linear_agent_session":
|
|
291
|
+
return "linear_agent_prompt";
|
|
292
|
+
case "linear_addressed_comment":
|
|
293
|
+
return "linear_comment";
|
|
294
|
+
case "patchrelay_operator_prompt":
|
|
295
|
+
return "patchrelay_operator_prompt";
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
function inputFeedKind(source) {
|
|
299
|
+
return source === "linear_agent_session" ? "agent" : "comment";
|
|
300
|
+
}
|
package/dist/build-info.json
CHANGED
|
@@ -68,7 +68,7 @@ export async function handleLiveCommand(params) {
|
|
|
68
68
|
throw new Error(`${params.parsed.flags.get("watch") === true ? "watch" : "status"} requires <issueKey>.`);
|
|
69
69
|
}
|
|
70
70
|
const watch = params.parsed.flags.get("watch") === true;
|
|
71
|
-
|
|
71
|
+
for (;;) {
|
|
72
72
|
const result = await params.data.live(issueKey);
|
|
73
73
|
if (!result) {
|
|
74
74
|
throw new Error(`No active stage found for ${issueKey}`);
|
|
@@ -78,7 +78,7 @@ export async function handleLiveCommand(params) {
|
|
|
78
78
|
break;
|
|
79
79
|
}
|
|
80
80
|
await delay(2000);
|
|
81
|
-
}
|
|
81
|
+
}
|
|
82
82
|
return 0;
|
|
83
83
|
}
|
|
84
84
|
export async function handleWorktreeCommand(params) {
|
package/dist/cli/connect-flow.js
CHANGED
|
@@ -29,7 +29,7 @@ export async function runConnectFlow(params) {
|
|
|
29
29
|
writeOutput(params.stdout, `${result.projectId ? `Repo: ${result.projectId}\n` : ""}${opened ? "Opened browser for Linear OAuth.\n" : "Open this URL in a browser:\n"}${opened ? result.authorizeUrl : `${result.authorizeUrl}\n`}Waiting for OAuth approval...\n`);
|
|
30
30
|
const deadline = Date.now() + (params.timeoutSeconds ?? 180) * 1000;
|
|
31
31
|
const pollIntervalMs = params.connectPollIntervalMs ?? 1000;
|
|
32
|
-
|
|
32
|
+
for (;;) {
|
|
33
33
|
const status = await params.data.connectStatus(result.state);
|
|
34
34
|
if (status.status === "completed") {
|
|
35
35
|
const label = status.installation?.workspaceName ?? status.installation?.actorName ?? `installation #${status.installation?.id ?? "unknown"}`;
|
|
@@ -50,5 +50,5 @@ export async function runConnectFlow(params) {
|
|
|
50
50
|
throw new Error(`Timed out waiting for Linear OAuth after ${params.timeoutSeconds ?? 180} seconds.`);
|
|
51
51
|
}
|
|
52
52
|
await delay(pollIntervalMs);
|
|
53
|
-
}
|
|
53
|
+
}
|
|
54
54
|
}
|
|
@@ -5,9 +5,8 @@ export class CliOperatorApiClient {
|
|
|
5
5
|
}
|
|
6
6
|
close() { }
|
|
7
7
|
async connect(projectId) {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
});
|
|
8
|
+
const query = projectId ? { projectId } : undefined;
|
|
9
|
+
return await this.requestJson("/api/oauth/linear/start", query);
|
|
11
10
|
}
|
|
12
11
|
async connectStatus(state) {
|
|
13
12
|
if (!state) {
|
|
@@ -31,9 +30,8 @@ export class CliOperatorApiClient {
|
|
|
31
30
|
return await this.requestJson("/api/linear/workspaces");
|
|
32
31
|
}
|
|
33
32
|
async syncLinearWorkspace(workspace) {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}, { method: "POST" });
|
|
33
|
+
const query = workspace ? { workspace } : undefined;
|
|
34
|
+
return await this.requestJson("/api/linear/workspaces/sync", query, { method: "POST" });
|
|
37
35
|
}
|
|
38
36
|
async disconnectLinearWorkspace(workspace) {
|
|
39
37
|
return await this.requestJson(`/api/linear/workspaces/${encodeURIComponent(workspace)}`, undefined, { method: "DELETE" });
|
|
@@ -5,7 +5,7 @@ import { HelpBar, buildHelpBarText } from "./HelpBar.js";
|
|
|
5
5
|
import { buildDetailLines } from "./detail-rows.js";
|
|
6
6
|
import { buildDetailStatusSegments, buildDetailStatusText } from "./detail-status.js";
|
|
7
7
|
import { measureRenderedTextRows } from "./layout-measure.js";
|
|
8
|
-
export function IssueDetailView({ issue, timeline, follow, scrollOffset, unreadBelow, activeRunStartedAt, activeRunId, tokenUsage, diffSummary, plan, issueContext, detailTab, rawRuns, rawFeedEvents, connected, lastServerMessageAt, reservedRows = 0,
|
|
8
|
+
export function IssueDetailView({ issue, timeline, follow, scrollOffset, unreadBelow, activeRunStartedAt, activeRunId, tokenUsage, diffSummary, plan, issueContext, detailTab, rawRuns, rawFeedEvents, connected, lastServerMessageAt, reservedRows = 0, onLayoutChange, }) {
|
|
9
9
|
const { stdout } = useStdout();
|
|
10
10
|
const width = Math.max(20, stdout?.columns ?? 80);
|
|
11
11
|
const totalRows = stdout?.rows ?? 24;
|
|
@@ -95,7 +95,6 @@ function MainPathNode({ node, isLast, runOffset, plan, activeRunId, }) {
|
|
|
95
95
|
const stateLabel = STATE_LABELS[node.state] ?? node.state;
|
|
96
96
|
const marker = node.isCurrent ? "\u25c9" : "\u25cb";
|
|
97
97
|
const stateColor = node.isCurrent ? "green" : "white";
|
|
98
|
-
const hasActiveRun = node.runs.some((r) => r.id === activeRunId);
|
|
99
98
|
const gutter = isLast && node.sideTrips.length === 0 ? " " : " \u2502 ";
|
|
100
99
|
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsxs(Text, { color: stateColor, bold: node.isCurrent, children: [" ", marker, " "] }), _jsx(Text, { color: stateColor, bold: node.isCurrent, children: stateLabel }), _jsxs(Text, { dimColor: true, children: [" ", formatTime(node.enteredAt)] })] }), node.reason && (_jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: gutter }), _jsx(Text, { dimColor: true, children: node.reason })] })), node.runs.length > 5 && (_jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: gutter }), _jsx(RunSummary, { runs: node.runs })] })), node.runs.map((run, ri) => (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: gutter }), _jsx(RunLine, { run: run, index: runOffset + ri, gutter: gutter })] }), run.id === activeRunId && plan && plan.length > 0 && (_jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: gutter }), _jsx(PlanSteps, { plan: plan })] }))] }, `run-${run.id}`))), node.sideTrips.length > 0 && (_jsx(Box, { flexDirection: "column", children: node.sideTrips.map((trip, ti) => {
|
|
101
100
|
// Count runs before this side-trip for numbering
|
|
@@ -8,7 +8,7 @@ export function renderTextLines(text, options) {
|
|
|
8
8
|
const lines = [];
|
|
9
9
|
for (let index = 0; index < sourceLines.length; index += 1) {
|
|
10
10
|
const sourceLine = sourceLines[index] ?? "";
|
|
11
|
-
const wrapped = wrapSegments(tokenizeSegments([{ text: sourceLine, ...
|
|
11
|
+
const wrapped = wrapSegments(tokenizeSegments([{ text: sourceLine, ...options.style }]), width, index === 0 ? options.firstPrefix : options.continuationPrefix ?? options.firstPrefix, options.continuationPrefix ?? options.firstPrefix, `${options.key}-${index}`);
|
|
12
12
|
lines.push(...wrapped);
|
|
13
13
|
}
|
|
14
14
|
return lines.length > 0 ? lines : [{ key: `${options.key}-0`, segments: [] }];
|
|
@@ -91,7 +91,7 @@ export function renderRichTextLines(text, options) {
|
|
|
91
91
|
const bulletMatch = line.match(/^\s*[-*]\s+(.*)$/);
|
|
92
92
|
if (bulletMatch?.[1]) {
|
|
93
93
|
flushParagraph();
|
|
94
|
-
lines.push(...wrapSegments(tokenizeSegments(parseInlineMarkdown(bulletMatch[1], options.style)), width, appendSegments(options.firstPrefix, [{ text: "• ", ...
|
|
94
|
+
lines.push(...wrapSegments(tokenizeSegments(parseInlineMarkdown(bulletMatch[1], options.style)), width, appendSegments(options.firstPrefix, [{ text: "• ", ...options.style }]), appendSegments(options.continuationPrefix ?? options.firstPrefix, [{ text: " ", ...options.style }]), `${options.key}-b-${blockIndex}`));
|
|
95
95
|
blockIndex += 1;
|
|
96
96
|
continue;
|
|
97
97
|
}
|
|
@@ -110,7 +110,7 @@ function parseInlineMarkdown(text, style) {
|
|
|
110
110
|
for (const match of text.matchAll(pattern)) {
|
|
111
111
|
const index = match.index ?? 0;
|
|
112
112
|
if (index > lastIndex) {
|
|
113
|
-
segments.push({ text: text.slice(lastIndex, index), ...
|
|
113
|
+
segments.push({ text: text.slice(lastIndex, index), ...style });
|
|
114
114
|
}
|
|
115
115
|
if (match[1] && match[2]) {
|
|
116
116
|
segments.push({ text: match[1], color: "cyan" });
|
|
@@ -119,14 +119,14 @@ function parseInlineMarkdown(text, style) {
|
|
|
119
119
|
segments.push({ text: match[3], color: "yellow" });
|
|
120
120
|
}
|
|
121
121
|
else if (match[4]) {
|
|
122
|
-
segments.push({ text: match[4], ...
|
|
122
|
+
segments.push({ text: match[4], ...style, bold: true });
|
|
123
123
|
}
|
|
124
124
|
lastIndex = index + match[0].length;
|
|
125
125
|
}
|
|
126
126
|
if (lastIndex < text.length) {
|
|
127
|
-
segments.push({ text: text.slice(lastIndex), ...
|
|
127
|
+
segments.push({ text: text.slice(lastIndex), ...style });
|
|
128
128
|
}
|
|
129
|
-
return segments.length > 0 ? segments : [{ text, ...
|
|
129
|
+
return segments.length > 0 ? segments : [{ text, ...style }];
|
|
130
130
|
}
|
|
131
131
|
function wrapSegments(tokens, width, firstPrefix, continuationPrefix, keyPrefix) {
|
|
132
132
|
const initialPrefix = cloneSegments(firstPrefix);
|
|
@@ -1,270 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { deriveIssueStatusNote } from "./status-note.js";
|
|
3
|
-
import { extractLatestAssistantSummary } from "./issue-session-events.js";
|
|
4
|
-
export class CodexConversationAdapter {
|
|
5
|
-
db;
|
|
6
|
-
codex;
|
|
7
|
-
wakeDispatcher;
|
|
8
|
-
logger;
|
|
9
|
-
feed;
|
|
10
|
-
followupClassifier;
|
|
11
|
-
constructor(db, codex, wakeDispatcher, logger, feed, followupClassifier) {
|
|
12
|
-
this.db = db;
|
|
13
|
-
this.codex = codex;
|
|
14
|
-
this.wakeDispatcher = wakeDispatcher;
|
|
15
|
-
this.logger = logger;
|
|
16
|
-
this.feed = feed;
|
|
17
|
-
this.followupClassifier = followupClassifier;
|
|
18
|
-
}
|
|
19
|
-
async deliverAgentInput(params) {
|
|
20
|
-
const body = params.body.trim();
|
|
21
|
-
if (!body)
|
|
22
|
-
return { status: "ignored" };
|
|
23
|
-
const issue = this.db.issues.getIssue(params.issue.projectId, params.issue.linearIssueId) ?? params.issue;
|
|
24
|
-
const activeRun = issue.activeRunId ? this.db.runs.getRunById(issue.activeRunId) : undefined;
|
|
25
|
-
const intent = await this.classify(body, params.source, issue, activeRun, params.directReply === true);
|
|
26
|
-
if (intent?.intent === "status" && !params.directReply) {
|
|
27
|
-
await params.emitActivity?.(this.buildStatusActivity(issue, activeRun, params.peekPendingSessionWakeRunType, activeRun ? "thought" : "response"), activeRun ? { ephemeral: true } : undefined);
|
|
28
|
-
return { status: "answered", ...(activeRun?.runType ? { activeRunType: activeRun.runType } : {}) };
|
|
29
|
-
}
|
|
30
|
-
if (intent?.intent === "stop") {
|
|
31
|
-
if (activeRun) {
|
|
32
|
-
await this.stopActiveRun(issue, activeRun, body, params.source);
|
|
33
|
-
}
|
|
34
|
-
else {
|
|
35
|
-
this.wakeDispatcher.recordEventAndDispatch(issue.projectId, issue.linearIssueId, {
|
|
36
|
-
eventType: "stop_requested",
|
|
37
|
-
eventJson: JSON.stringify({ body, source: params.source, ...(params.author ? { author: params.author } : {}) }),
|
|
38
|
-
});
|
|
39
|
-
this.db.issueSessions.clearPendingIssueSessionEventsRespectingActiveLease(issue.projectId, issue.linearIssueId);
|
|
40
|
-
}
|
|
41
|
-
return { status: "stopped", ...(activeRun?.runType ? { activeRunType: activeRun.runType } : {}) };
|
|
42
|
-
}
|
|
43
|
-
if (!issue.delegatedToPatchRelay) {
|
|
44
|
-
await params.emitActivity?.({ type: "thought", body: "PatchRelay is paused because the issue is undelegated." }, { ephemeral: true });
|
|
45
|
-
return { status: "ignored" };
|
|
46
|
-
}
|
|
47
|
-
if (activeRun) {
|
|
48
|
-
return await this.steerActiveRun({
|
|
49
|
-
issue,
|
|
50
|
-
activeRun,
|
|
51
|
-
body,
|
|
52
|
-
source: params.source,
|
|
53
|
-
author: params.author,
|
|
54
|
-
emitActivity: params.emitActivity,
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
return await this.queueIdleInput({
|
|
58
|
-
project: params.project,
|
|
59
|
-
issue,
|
|
60
|
-
body,
|
|
61
|
-
source: params.source,
|
|
62
|
-
author: params.author,
|
|
63
|
-
directReply: params.directReply === true,
|
|
64
|
-
emitActivity: params.emitActivity,
|
|
65
|
-
});
|
|
66
|
-
}
|
|
67
|
-
async classify(body, source, issue, activeRun, directReply) {
|
|
68
|
-
if (!this.followupClassifier)
|
|
69
|
-
return undefined;
|
|
70
|
-
return await this.followupClassifier.classify(body, {
|
|
71
|
-
source: source === "agent_session_prompt" ? "agentPrompted" : "comment",
|
|
72
|
-
...(activeRun?.runType ? { activeRunType: activeRun.runType } : {}),
|
|
73
|
-
factoryState: issue.factoryState,
|
|
74
|
-
directReply,
|
|
75
|
-
delegatedToPatchRelay: issue.delegatedToPatchRelay,
|
|
76
|
-
prReviewState: issue.prReviewState,
|
|
77
|
-
explicitWakeIntent: true,
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
-
async steerActiveRun(params) {
|
|
81
|
-
const { issue, activeRun, body, source } = params;
|
|
82
|
-
if (!activeRun.threadId || !activeRun.turnId) {
|
|
83
|
-
const queuedRunType = this.queueFollowUpEvent(issue, body, source, params.author, false);
|
|
84
|
-
return { status: "queued", ...(queuedRunType ? { queuedRunType } : {}) };
|
|
85
|
-
}
|
|
86
|
-
const input = [
|
|
87
|
-
source === "agent_session_prompt"
|
|
88
|
-
? "New Linear agent-session prompt received while you are working."
|
|
89
|
-
: "New explicitly addressed Linear issue comment received while you are working.",
|
|
90
|
-
params.author ? `Author: ${params.author}` : undefined,
|
|
91
|
-
"",
|
|
92
|
-
"Checkpoint contract: incorporate this instruction before your next meaningful side effect when possible. If you are already inside a non-interruptible command, finish that command, then re-plan with this input before continuing.",
|
|
93
|
-
"",
|
|
94
|
-
body,
|
|
95
|
-
].filter(Boolean).join("\n");
|
|
96
|
-
try {
|
|
97
|
-
await this.codex.steerTurn({ threadId: activeRun.threadId, turnId: activeRun.turnId, input });
|
|
98
|
-
this.recordPromptDelivery({
|
|
99
|
-
issue,
|
|
100
|
-
source,
|
|
101
|
-
runId: activeRun.id,
|
|
102
|
-
runType: activeRun.runType,
|
|
103
|
-
status: "delivered",
|
|
104
|
-
body,
|
|
105
|
-
primitive: "turn/steer",
|
|
106
|
-
threadId: activeRun.threadId,
|
|
107
|
-
turnId: activeRun.turnId,
|
|
108
|
-
});
|
|
109
|
-
this.feed?.publish({
|
|
110
|
-
level: "info",
|
|
111
|
-
kind: source === "agent_session_prompt" ? "agent" : "comment",
|
|
112
|
-
projectId: issue.projectId,
|
|
113
|
-
issueKey: issue.issueKey,
|
|
114
|
-
stage: activeRun.runType,
|
|
115
|
-
status: "delivered",
|
|
116
|
-
summary: `Delivered agent input to active ${activeRun.runType} workflow`,
|
|
117
|
-
});
|
|
118
|
-
await params.emitActivity?.(buildPromptDeliveredThought(activeRun.runType), { ephemeral: true });
|
|
119
|
-
return { status: "steered", activeRunType: activeRun.runType };
|
|
120
|
-
}
|
|
121
|
-
catch (error) {
|
|
122
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
123
|
-
this.logger.warn({ issueKey: issue.issueKey, error: message }, "Failed to deliver agent input to active Codex turn");
|
|
124
|
-
this.recordPromptDelivery({
|
|
125
|
-
issue,
|
|
126
|
-
source,
|
|
127
|
-
runId: activeRun.id,
|
|
128
|
-
runType: activeRun.runType,
|
|
129
|
-
status: "delivery_failed",
|
|
130
|
-
body,
|
|
131
|
-
primitive: "turn/steer",
|
|
132
|
-
threadId: activeRun.threadId,
|
|
133
|
-
turnId: activeRun.turnId,
|
|
134
|
-
error: message,
|
|
135
|
-
});
|
|
136
|
-
const queuedRunType = this.queueFollowUpEvent(issue, body, source, params.author, false);
|
|
137
|
-
this.feed?.publish({
|
|
138
|
-
level: "warn",
|
|
139
|
-
kind: source === "agent_session_prompt" ? "agent" : "comment",
|
|
140
|
-
projectId: issue.projectId,
|
|
141
|
-
issueKey: issue.issueKey,
|
|
142
|
-
stage: activeRun.runType,
|
|
143
|
-
status: "delivery_failed",
|
|
144
|
-
summary: `Could not deliver agent input to active ${activeRun.runType} workflow`,
|
|
145
|
-
});
|
|
146
|
-
await params.emitActivity?.(buildPromptDeliveryFailedActivity(activeRun.runType, message));
|
|
147
|
-
return { status: "delivery_failed", activeRunType: activeRun.runType, ...(queuedRunType ? { queuedRunType } : {}) };
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
async queueIdleInput(params) {
|
|
151
|
-
const originalIssue = params.issue;
|
|
152
|
-
let issue = originalIssue;
|
|
153
|
-
const replacementPrRequired = originalIssue.factoryState === "done" && originalIssue.prNumber !== undefined;
|
|
154
|
-
if (replacementPrRequired) {
|
|
155
|
-
issue = this.prepareReplacementWork(params.project, originalIssue);
|
|
156
|
-
}
|
|
157
|
-
const queuedRunType = this.queueFollowUpEvent(issue, params.body, params.source, params.author, params.directReply, replacementPrRequired ? originalIssue : undefined);
|
|
158
|
-
if (queuedRunType) {
|
|
159
|
-
await params.emitActivity?.(replacementPrRequired
|
|
160
|
-
? { type: "action", action: "Reopening", parameter: `completed PR #${originalIssue.prNumber} for replacement work` }
|
|
161
|
-
: buildPromptDeliveredThought(queuedRunType), { ephemeral: true });
|
|
162
|
-
}
|
|
163
|
-
else {
|
|
164
|
-
await params.emitActivity?.(buildNonActionableFollowupActivity("unknown_needs_ack"));
|
|
165
|
-
}
|
|
166
|
-
return { status: queuedRunType ? "queued" : "ignored", ...(queuedRunType ? { queuedRunType } : {}) };
|
|
167
|
-
}
|
|
168
|
-
queueFollowUpEvent(issue, body, source, author, directReply, previousIssue) {
|
|
169
|
-
return this.wakeDispatcher.recordEventAndDispatch(issue.projectId, issue.linearIssueId, {
|
|
170
|
-
eventType: directReply ? "direct_reply" : source === "agent_session_prompt" ? "followup_prompt" : "followup_comment",
|
|
171
|
-
eventJson: JSON.stringify({
|
|
172
|
-
...(source === "agent_session_prompt" ? { text: body } : { body }),
|
|
173
|
-
source: source === "agent_session_prompt" ? "linear_agent_prompt" : "linear_comment",
|
|
174
|
-
...(author ? { author } : {}),
|
|
175
|
-
...(previousIssue?.prNumber !== undefined
|
|
176
|
-
? {
|
|
177
|
-
replacementPrRequired: true,
|
|
178
|
-
previousPrNumber: previousIssue.prNumber,
|
|
179
|
-
...(previousIssue.prUrl ? { previousPrUrl: previousIssue.prUrl } : {}),
|
|
180
|
-
...(previousIssue.prState ? { previousPrState: previousIssue.prState } : {}),
|
|
181
|
-
...(previousIssue.prHeadSha ? { previousPrHeadSha: previousIssue.prHeadSha } : {}),
|
|
182
|
-
}
|
|
183
|
-
: {}),
|
|
184
|
-
}),
|
|
185
|
-
});
|
|
186
|
-
}
|
|
187
|
-
prepareReplacementWork(project, issue) {
|
|
188
|
-
const issueRef = (issue.issueKey ?? issue.linearIssueId).replace(/[^a-zA-Z0-9._-]+/g, "-");
|
|
189
|
-
const suffix = Date.now().toString(36);
|
|
190
|
-
return this.db.issueSessions.upsertIssueRespectingActiveLease(issue.projectId, issue.linearIssueId, {
|
|
191
|
-
projectId: issue.projectId,
|
|
192
|
-
linearIssueId: issue.linearIssueId,
|
|
193
|
-
factoryState: "delegated",
|
|
194
|
-
branchName: `${project.branchPrefix}/${issueRef}-replacement-${suffix}`,
|
|
195
|
-
prNumber: null,
|
|
196
|
-
prUrl: null,
|
|
197
|
-
prState: null,
|
|
198
|
-
prIsDraft: null,
|
|
199
|
-
prHeadSha: null,
|
|
200
|
-
prAuthorLogin: null,
|
|
201
|
-
prReviewState: null,
|
|
202
|
-
prCheckStatus: null,
|
|
203
|
-
lastBlockingReviewHeadSha: null,
|
|
204
|
-
}) ?? issue;
|
|
205
|
-
}
|
|
206
|
-
async stopActiveRun(issue, run, body, source) {
|
|
207
|
-
if (run.threadId && run.turnId) {
|
|
208
|
-
try {
|
|
209
|
-
await this.codex.steerTurn({
|
|
210
|
-
threadId: run.threadId,
|
|
211
|
-
turnId: run.turnId,
|
|
212
|
-
input: "STOP: The user has requested you stop working immediately. Do not make further changes. Wrap up and exit.",
|
|
213
|
-
});
|
|
214
|
-
}
|
|
215
|
-
catch (error) {
|
|
216
|
-
this.logger.warn({ issueKey: issue.issueKey, error: error instanceof Error ? error.message : String(error) }, "Failed to steer Codex turn for stop request");
|
|
217
|
-
}
|
|
218
|
-
this.db.runs.finishRun(run.id, { status: "released", threadId: run.threadId, turnId: run.turnId });
|
|
219
|
-
}
|
|
220
|
-
this.db.issueSessions.upsertIssueRespectingActiveLease(issue.projectId, issue.linearIssueId, {
|
|
221
|
-
projectId: issue.projectId,
|
|
222
|
-
linearIssueId: issue.linearIssueId,
|
|
223
|
-
activeRunId: null,
|
|
224
|
-
factoryState: "awaiting_input",
|
|
225
|
-
});
|
|
226
|
-
this.wakeDispatcher.recordEventAndDispatch(issue.projectId, issue.linearIssueId, {
|
|
227
|
-
eventType: "stop_requested",
|
|
228
|
-
eventJson: JSON.stringify({ body, source }),
|
|
229
|
-
});
|
|
230
|
-
this.db.issueSessions.clearPendingIssueSessionEventsRespectingActiveLease(issue.projectId, issue.linearIssueId);
|
|
231
|
-
this.db.issueSessions.releaseIssueSessionLeaseRespectingActiveLease(issue.projectId, issue.linearIssueId);
|
|
232
|
-
}
|
|
233
|
-
buildStatusActivity(issue, activeRun, peekPendingSessionWakeRunType, activityType) {
|
|
234
|
-
const latestRun = activeRun ?? this.db.runs.getLatestRunForIssue(issue.projectId, issue.linearIssueId);
|
|
235
|
-
const latestEvent = this.db.issueSessions.listIssueSessionEvents(issue.projectId, issue.linearIssueId).at(-1);
|
|
236
|
-
const statusNote = deriveIssueStatusNote({
|
|
237
|
-
issue,
|
|
238
|
-
latestRun,
|
|
239
|
-
latestEvent,
|
|
240
|
-
sessionSummary: extractLatestAssistantSummary(latestRun),
|
|
241
|
-
waitingReason: undefined,
|
|
242
|
-
});
|
|
243
|
-
const pendingRunType = peekPendingSessionWakeRunType?.(issue.projectId, issue.linearIssueId);
|
|
244
|
-
return buildFollowupStatusActivity({
|
|
245
|
-
issue,
|
|
246
|
-
...(statusNote ? { statusNote } : {}),
|
|
247
|
-
...(activeRun?.runType ? { activeRunType: activeRun.runType } : {}),
|
|
248
|
-
...(pendingRunType ? { pendingRunType } : {}),
|
|
249
|
-
activityType,
|
|
250
|
-
});
|
|
251
|
-
}
|
|
252
|
-
recordPromptDelivery(params) {
|
|
253
|
-
this.db.issueSessions.appendIssueSessionEventRespectingActiveLease(params.issue.projectId, params.issue.linearIssueId, {
|
|
254
|
-
projectId: params.issue.projectId,
|
|
255
|
-
linearIssueId: params.issue.linearIssueId,
|
|
256
|
-
eventType: "prompt_delivered",
|
|
257
|
-
eventJson: JSON.stringify({
|
|
258
|
-
source: params.source === "agent_session_prompt" ? "linear_agent_prompt" : "linear_comment",
|
|
259
|
-
runId: params.runId,
|
|
260
|
-
runType: params.runType,
|
|
261
|
-
status: params.status,
|
|
262
|
-
body: params.body,
|
|
263
|
-
primitive: params.primitive,
|
|
264
|
-
...(params.threadId ? { threadId: params.threadId } : {}),
|
|
265
|
-
...(params.turnId ? { turnId: params.turnId } : {}),
|
|
266
|
-
...(params.error ? { error: params.error } : {}),
|
|
267
|
-
}),
|
|
268
|
-
});
|
|
269
|
-
}
|
|
270
|
-
}
|
|
1
|
+
export { AgentInputService as CodexConversationAdapter, } from "./agent-input-service.js";
|
package/dist/hook-runner.js
CHANGED
|
@@ -8,7 +8,7 @@ export async function runProjectHook(repoPath, hookName, options) {
|
|
|
8
8
|
}
|
|
9
9
|
const result = await execCommand(hookPath, [], {
|
|
10
10
|
cwd: options.cwd,
|
|
11
|
-
env: { ...sanitizedParentEnv(), ...
|
|
11
|
+
env: { ...sanitizedParentEnv(), ...options.env },
|
|
12
12
|
timeoutMs: options.timeoutMs ?? 120_000,
|
|
13
13
|
});
|
|
14
14
|
return {
|
|
@@ -580,9 +580,8 @@ export class IdleIssueReconciler {
|
|
|
580
580
|
prReviewState: "approved",
|
|
581
581
|
});
|
|
582
582
|
if (issue.factoryState !== "awaiting_queue" || hasFailureProvenance(issue)) {
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
});
|
|
583
|
+
const options = hasFailureProvenance(issue) ? { clearFailureProvenance: true } : undefined;
|
|
584
|
+
this.advanceIdleIssue(issue, "awaiting_queue", options);
|
|
586
585
|
}
|
|
587
586
|
return;
|
|
588
587
|
}
|
package/dist/install.js
CHANGED
|
@@ -206,7 +206,7 @@ export async function upsertProjectInConfig(options) {
|
|
|
206
206
|
const existingProject = existingIndex >= 0 ? existingProjects[existingIndex] : undefined;
|
|
207
207
|
const resolvedProjectId = existingProject ? String(existingProject.id ?? projectId) : projectId;
|
|
208
208
|
const nextProject = {
|
|
209
|
-
...
|
|
209
|
+
...existingProject,
|
|
210
210
|
id: resolvedProjectId,
|
|
211
211
|
repo_path: repoPath,
|
|
212
212
|
};
|
|
@@ -328,7 +328,7 @@ export async function upsertRepositoryInConfig(options) {
|
|
|
328
328
|
const existingIndex = existingRepositories.findIndex((repository) => String(repository.github_repo ?? "") === githubRepo);
|
|
329
329
|
const existing = existingIndex >= 0 ? existingRepositories[existingIndex] : undefined;
|
|
330
330
|
const nextRepository = {
|
|
331
|
-
...
|
|
331
|
+
...existing,
|
|
332
332
|
github_repo: githubRepo,
|
|
333
333
|
local_path: localPath,
|
|
334
334
|
...(workspace ? { workspace } : {}),
|
|
@@ -16,8 +16,8 @@ export function buildOperatorRetryEvent(issue, runType, source = "operator_retry
|
|
|
16
16
|
return {
|
|
17
17
|
eventType: "merge_steward_incident",
|
|
18
18
|
eventJson: JSON.stringify({
|
|
19
|
-
...
|
|
20
|
-
...
|
|
19
|
+
...queueIncident,
|
|
20
|
+
...failureContext,
|
|
21
21
|
source,
|
|
22
22
|
requiresFreshHead: true,
|
|
23
23
|
promptContext: [
|
|
@@ -35,7 +35,7 @@ export function buildOperatorRetryEvent(issue, runType, source = "operator_retry
|
|
|
35
35
|
return {
|
|
36
36
|
eventType: "settled_red_ci",
|
|
37
37
|
eventJson: JSON.stringify({
|
|
38
|
-
...
|
|
38
|
+
...failureContext,
|
|
39
39
|
source,
|
|
40
40
|
}),
|
|
41
41
|
dedupeKey: `${source}:ci_repair:${issue.linearIssueId}:${issue.lastGitHubFailureSignature ?? issue.prHeadSha ?? "unknown-sha"}`,
|
|
@@ -40,9 +40,7 @@ export function queueSettledOrchestrationIssue(params) {
|
|
|
40
40
|
const dispatched = params.wakeDispatcher.recordEventAndDispatch(params.issue.projectId, params.issue.linearIssueId, {
|
|
41
41
|
eventType: "delegated",
|
|
42
42
|
eventJson: JSON.stringify({
|
|
43
|
-
|
|
44
|
-
? { promptContext: params.promptContext }
|
|
45
|
-
: { promptContext: "The orchestration child set has settled enough to begin planning." }),
|
|
43
|
+
promptContext: params.promptContext ?? "The orchestration child set has settled enough to begin planning.",
|
|
46
44
|
}),
|
|
47
45
|
dedupeKey: `delegated:orchestration_settle:${params.issue.linearIssueId}`,
|
|
48
46
|
});
|
|
@@ -32,7 +32,7 @@ export function buildReviewFixBranchUpkeepContext(prNumber, baseBranch, pr, cont
|
|
|
32
32
|
"Do not stop just because the requested code change is already present. Review can only move forward after a new pushed head.",
|
|
33
33
|
].join(" ");
|
|
34
34
|
return {
|
|
35
|
-
...
|
|
35
|
+
...context,
|
|
36
36
|
branchUpkeepRequired: true,
|
|
37
37
|
reviewFixMode: "branch_upkeep",
|
|
38
38
|
wakeReason: "branch_upkeep",
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import { buildOperatorRetryEvent } from "./operator-retry-event.js";
|
|
2
2
|
import { buildManualRetryAttemptReset, resolveRetryTarget } from "./manual-issue-actions.js";
|
|
3
3
|
export class ServiceIssueActions {
|
|
4
|
+
config;
|
|
4
5
|
db;
|
|
6
|
+
agentInput;
|
|
5
7
|
codex;
|
|
6
8
|
runtime;
|
|
7
9
|
feed;
|
|
8
10
|
logger;
|
|
9
|
-
constructor(db, codex, runtime, feed, logger) {
|
|
11
|
+
constructor(config, db, agentInput, codex, runtime, feed, logger) {
|
|
12
|
+
this.config = config;
|
|
10
13
|
this.db = db;
|
|
14
|
+
this.agentInput = agentInput;
|
|
11
15
|
this.codex = codex;
|
|
12
16
|
this.runtime = runtime;
|
|
13
17
|
this.feed = feed;
|
|
@@ -20,6 +24,9 @@ export class ServiceIssueActions {
|
|
|
20
24
|
if (!issue.delegatedToPatchRelay && !issue.activeRunId) {
|
|
21
25
|
return { error: "Issue is undelegated from PatchRelay; delegate it again before prompting work" };
|
|
22
26
|
}
|
|
27
|
+
const project = this.config.projects.find((entry) => entry.id === issue.projectId);
|
|
28
|
+
if (!project)
|
|
29
|
+
return { error: `Project ${issue.projectId} is not configured` };
|
|
23
30
|
this.feed.publish({
|
|
24
31
|
level: "info",
|
|
25
32
|
kind: "comment",
|
|
@@ -30,28 +37,18 @@ export class ServiceIssueActions {
|
|
|
30
37
|
summary: `Operator prompt (${source})`,
|
|
31
38
|
detail: text.slice(0, 200),
|
|
32
39
|
});
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
threadId: run.threadId,
|
|
44
|
-
turnId: run.turnId,
|
|
45
|
-
input: `Operator prompt (${source}):\n\n${text}`,
|
|
46
|
-
});
|
|
47
|
-
return { delivered: true };
|
|
48
|
-
}
|
|
49
|
-
catch (error) {
|
|
50
|
-
const msg = error instanceof Error ? error.message : String(error);
|
|
51
|
-
this.logger.warn({ issueKey, error: msg }, "steerTurn failed, queuing prompt for next run");
|
|
52
|
-
this.queueOperatorPrompt(issue, text, source);
|
|
40
|
+
const result = await this.agentInput.deliverAgentInput({
|
|
41
|
+
project,
|
|
42
|
+
issue,
|
|
43
|
+
source: "patchrelay_operator_prompt",
|
|
44
|
+
body: text,
|
|
45
|
+
operatorSource: source,
|
|
46
|
+
});
|
|
47
|
+
if (result.status === "ignored")
|
|
48
|
+
return { delivered: false };
|
|
49
|
+
if (result.status === "delivery_failed" || result.status === "queued")
|
|
53
50
|
return { delivered: false, queued: true };
|
|
54
|
-
}
|
|
51
|
+
return { delivered: result.status === "steered" || result.status === "answered" || result.status === "stopped" };
|
|
55
52
|
}
|
|
56
53
|
async stopIssue(issueKey) {
|
|
57
54
|
const issue = this.db.issues.getIssueByKey(issueKey);
|
|
@@ -206,15 +203,6 @@ export class ServiceIssueActions {
|
|
|
206
203
|
...(run ? { releasedRunId: run.id } : {}),
|
|
207
204
|
};
|
|
208
205
|
}
|
|
209
|
-
queueOperatorPrompt(issue, text, source) {
|
|
210
|
-
this.db.issueSessions.appendIssueSessionEventRespectingActiveLease(issue.projectId, issue.linearIssueId, {
|
|
211
|
-
projectId: issue.projectId,
|
|
212
|
-
linearIssueId: issue.linearIssueId,
|
|
213
|
-
eventType: "operator_prompt",
|
|
214
|
-
eventJson: JSON.stringify({ text, source }),
|
|
215
|
-
});
|
|
216
|
-
this.runtime.enqueueIssue(issue.projectId, issue.linearIssueId);
|
|
217
|
-
}
|
|
218
206
|
appendOperatorRetryEvent(issue, runType) {
|
|
219
207
|
this.db.issueSessions.appendIssueSessionEventRespectingActiveLease(issue.projectId, issue.linearIssueId, {
|
|
220
208
|
projectId: issue.projectId,
|
package/dist/service.js
CHANGED
|
@@ -13,6 +13,8 @@ import { WakeDispatcher } from "./wake-dispatcher.js";
|
|
|
13
13
|
import { WebhookHandler } from "./webhook-handler.js";
|
|
14
14
|
import { acceptIncomingWebhook } from "./service-webhooks.js";
|
|
15
15
|
import { parseStringArray, TrackedIssueListQuery } from "./tracked-issue-list-query.js";
|
|
16
|
+
import { AgentInputService } from "./agent-input-service.js";
|
|
17
|
+
import { CodexFollowupIntentClassifier } from "./followup-intent.js";
|
|
16
18
|
export class PatchRelayService {
|
|
17
19
|
config;
|
|
18
20
|
db;
|
|
@@ -52,9 +54,10 @@ export class PatchRelayService {
|
|
|
52
54
|
// orchestrator (its construction depends on the Codex client). All
|
|
53
55
|
// downstream consumers receive this single dispatcher instance.
|
|
54
56
|
const dispatcher = new WakeDispatcher(db, (projectId, issueId) => enqueueIssue(projectId, issueId), (projectId, issueId) => leaseRelease(projectId, issueId), logger, this.feed);
|
|
57
|
+
const agentInput = new AgentInputService(db, codex, dispatcher, logger, this.feed, new CodexFollowupIntentClassifier(codex, logger));
|
|
55
58
|
this.orchestrator = new RunOrchestrator(config, db, codex, this.linearProvider, (projectId, issueId) => enqueueIssue(projectId, issueId), dispatcher, logger, this.feed, this.configPath);
|
|
56
59
|
leaseRelease = (projectId, issueId) => this.orchestrator.leaseService.release(projectId, issueId);
|
|
57
|
-
this.webhookHandler = new WebhookHandler(config, db, this.linearProvider, codex, dispatcher, logger, this.feed);
|
|
60
|
+
this.webhookHandler = new WebhookHandler(config, db, this.linearProvider, codex, dispatcher, logger, this.feed, undefined, agentInput);
|
|
58
61
|
this.githubWebhookHandler = new GitHubWebhookHandler(config, db, this.linearProvider, dispatcher, logger, codex, this.feed);
|
|
59
62
|
const runtime = new ServiceRuntime(codex, logger, this.orchestrator, { listIssuesReadyForExecution: () => db.listIssuesReadyForExecution() }, this.webhookHandler, {
|
|
60
63
|
processIssue: async (item) => {
|
|
@@ -65,7 +68,7 @@ export class PatchRelayService {
|
|
|
65
68
|
this.oauthService = new LinearOAuthService(config, { linearInstallations: db.linearInstallations }, logger);
|
|
66
69
|
this.queryService = new IssueQueryService(db, codex, this.orchestrator);
|
|
67
70
|
this.runtime = runtime;
|
|
68
|
-
this.issueActions = new ServiceIssueActions(db, codex, runtime, this.feed, logger);
|
|
71
|
+
this.issueActions = new ServiceIssueActions(config, db, agentInput, codex, runtime, this.feed, logger);
|
|
69
72
|
this.startupRecovery = new ServiceStartupRecovery(db, this.linearProvider, this.orchestrator.linearSync, (projectId, issueId) => runtime.enqueueIssue(projectId, issueId), logger);
|
|
70
73
|
this.trackedIssueListQuery = new TrackedIssueListQuery(db);
|
|
71
74
|
// Optional GitHub App token management for bot identity
|
package/dist/webhook-handler.js
CHANGED
|
@@ -13,7 +13,7 @@ import { safeJsonParse, sanitizeDiagnosticText } from "./utils.js";
|
|
|
13
13
|
import { extractLatestAssistantSummary } from "./issue-session-events.js";
|
|
14
14
|
import { WakeDispatcher } from "./wake-dispatcher.js";
|
|
15
15
|
import { CodexFollowupIntentClassifier } from "./followup-intent.js";
|
|
16
|
-
import {
|
|
16
|
+
import { AgentInputService } from "./agent-input-service.js";
|
|
17
17
|
export class WebhookHandler {
|
|
18
18
|
config;
|
|
19
19
|
db;
|
|
@@ -30,7 +30,7 @@ export class WebhookHandler {
|
|
|
30
30
|
dependencyReadinessHandler;
|
|
31
31
|
linearSync;
|
|
32
32
|
wakeDispatcher;
|
|
33
|
-
constructor(config, db, linearProvider, codex, wakeDispatcherOrEnqueueIssue, logger, feed, followupClassifier) {
|
|
33
|
+
constructor(config, db, linearProvider, codex, wakeDispatcherOrEnqueueIssue, logger, feed, followupClassifier, agentInput) {
|
|
34
34
|
this.config = config;
|
|
35
35
|
this.db = db;
|
|
36
36
|
this.linearProvider = linearProvider;
|
|
@@ -48,9 +48,9 @@ export class WebhookHandler {
|
|
|
48
48
|
this.issueRemovalHandler = new IssueRemovalHandler(db, feed);
|
|
49
49
|
this.linearSync = new LinearSessionSync(config, db, linearProvider, logger, feed);
|
|
50
50
|
const intentClassifier = followupClassifier ?? new CodexFollowupIntentClassifier(codex, logger);
|
|
51
|
-
const
|
|
52
|
-
this.commentWakeHandler = new CommentWakeHandler(db, this.wakeDispatcher, feed,
|
|
53
|
-
this.agentSessionHandler = new AgentSessionHandler(config, db, linearProvider, codex, this.wakeDispatcher, logger, feed,
|
|
51
|
+
const agentInputService = agentInput ?? new AgentInputService(db, codex, this.wakeDispatcher, logger, feed, intentClassifier);
|
|
52
|
+
this.commentWakeHandler = new CommentWakeHandler(db, this.wakeDispatcher, feed, agentInputService, (issue, content, options) => this.linearSync.emitActivity(issue, content, options));
|
|
53
|
+
this.agentSessionHandler = new AgentSessionHandler(config, db, linearProvider, codex, this.wakeDispatcher, logger, feed, agentInputService);
|
|
54
54
|
this.desiredStageRecorder = new DesiredStageRecorder(db, linearProvider, this.wakeDispatcher, feed);
|
|
55
55
|
this.contextLoader = new WebhookContextLoader(config, linearProvider);
|
|
56
56
|
this.dependencyReadinessHandler = new DependencyReadinessHandler(db, this.wakeDispatcher, (projectId, issueId) => this.peekPendingSessionWakeRunType(projectId, issueId));
|
|
@@ -17,8 +17,8 @@ export class AgentSessionHandler {
|
|
|
17
17
|
wakeDispatcher;
|
|
18
18
|
logger;
|
|
19
19
|
feed;
|
|
20
|
-
|
|
21
|
-
constructor(config, db, linearProvider, codex, wakeDispatcher, logger, feed,
|
|
20
|
+
agentInput;
|
|
21
|
+
constructor(config, db, linearProvider, codex, wakeDispatcher, logger, feed, agentInput) {
|
|
22
22
|
this.config = config;
|
|
23
23
|
this.db = db;
|
|
24
24
|
this.linearProvider = linearProvider;
|
|
@@ -26,7 +26,7 @@ export class AgentSessionHandler {
|
|
|
26
26
|
this.wakeDispatcher = wakeDispatcher;
|
|
27
27
|
this.logger = logger;
|
|
28
28
|
this.feed = feed;
|
|
29
|
-
this.
|
|
29
|
+
this.agentInput = agentInput;
|
|
30
30
|
}
|
|
31
31
|
async acknowledgeCreated(normalized) {
|
|
32
32
|
if (normalized.triggerEvent !== "agentSessionCreated" || !normalized.agentSession?.id || !normalized.issue) {
|
|
@@ -126,11 +126,11 @@ export class AgentSessionHandler {
|
|
|
126
126
|
}
|
|
127
127
|
const promptBody = normalized.agentSession.promptBody?.trim();
|
|
128
128
|
const directReply = promptBody && existingIssue ? params.isDirectReplyToOutstandingQuestion(existingIssue) : false;
|
|
129
|
-
if (promptBody && existingIssue && this.
|
|
130
|
-
const result = await this.
|
|
129
|
+
if (promptBody && existingIssue && this.agentInput) {
|
|
130
|
+
const result = await this.agentInput.deliverAgentInput({
|
|
131
131
|
project,
|
|
132
132
|
issue: existingIssue,
|
|
133
|
-
source: "
|
|
133
|
+
source: "linear_agent_session",
|
|
134
134
|
body: promptBody,
|
|
135
135
|
directReply,
|
|
136
136
|
emitActivity: (content, options) => this.publishAgentActivity(linear, normalized.agentSession.id, content, options),
|
|
@@ -4,13 +4,13 @@ export class CommentWakeHandler {
|
|
|
4
4
|
db;
|
|
5
5
|
wakeDispatcher;
|
|
6
6
|
feed;
|
|
7
|
-
|
|
7
|
+
agentInput;
|
|
8
8
|
emitLinearActivity;
|
|
9
|
-
constructor(db, wakeDispatcher, feed,
|
|
9
|
+
constructor(db, wakeDispatcher, feed, agentInput, emitLinearActivity) {
|
|
10
10
|
this.db = db;
|
|
11
11
|
this.wakeDispatcher = wakeDispatcher;
|
|
12
12
|
this.feed = feed;
|
|
13
|
-
this.
|
|
13
|
+
this.agentInput = agentInput;
|
|
14
14
|
this.emitLinearActivity = emitLinearActivity;
|
|
15
15
|
}
|
|
16
16
|
async handle(params) {
|
|
@@ -64,10 +64,10 @@ export class CommentWakeHandler {
|
|
|
64
64
|
});
|
|
65
65
|
return;
|
|
66
66
|
}
|
|
67
|
-
const result = await this.
|
|
67
|
+
const result = await this.agentInput?.deliverAgentInput({
|
|
68
68
|
project,
|
|
69
69
|
issue,
|
|
70
|
-
source: "
|
|
70
|
+
source: "linear_addressed_comment",
|
|
71
71
|
body: addressedText,
|
|
72
72
|
author: normalized.comment.userName,
|
|
73
73
|
directReply: params.isDirectReplyToOutstandingQuestion(issue),
|
|
@@ -175,7 +175,7 @@ export class DesiredStageRecorder {
|
|
|
175
175
|
...(hydratedIssue.estimate != null ? { estimate: hydratedIssue.estimate } : {}),
|
|
176
176
|
...(hydratedIssue.stateName ? { currentLinearState: hydratedIssue.stateName } : {}),
|
|
177
177
|
...(hydratedIssue.stateType ? { currentLinearStateType: hydratedIssue.stateType } : {}),
|
|
178
|
-
...
|
|
178
|
+
...linkedPrAdoption?.issueUpdates,
|
|
179
179
|
delegatedToPatchRelay: delegated,
|
|
180
180
|
...resolvedPlan,
|
|
181
181
|
});
|