patchrelay 0.8.9 → 0.9.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/README.md +64 -62
- package/dist/agent-session-plan.js +17 -17
- package/dist/build-info.json +3 -3
- package/dist/cli/args.js +1 -1
- package/dist/cli/commands/issues.js +18 -18
- package/dist/cli/data.js +109 -298
- package/dist/cli/formatters/text.js +22 -28
- package/dist/cli/help.js +7 -7
- package/dist/cli/index.js +3 -3
- package/dist/config.js +13 -166
- package/dist/db/migrations.js +46 -154
- package/dist/db.js +369 -45
- package/dist/factory-state.js +55 -0
- package/dist/github-webhook-handler.js +199 -0
- package/dist/github-webhooks.js +166 -0
- package/dist/hook-runner.js +28 -0
- package/dist/http.js +48 -22
- package/dist/issue-query-service.js +33 -38
- package/dist/linear-workflow.js +5 -118
- package/dist/preflight.js +1 -6
- package/dist/project-resolution.js +12 -1
- package/dist/run-orchestrator.js +446 -0
- package/dist/{stage-reporting.js → run-reporting.js} +11 -13
- package/dist/service-runtime.js +12 -61
- package/dist/service-webhooks.js +7 -52
- package/dist/service.js +39 -61
- package/dist/webhook-handler.js +387 -0
- package/dist/webhook-installation-handler.js +3 -8
- package/package.json +2 -1
- package/dist/db/authoritative-ledger-store.js +0 -536
- package/dist/db/issue-projection-store.js +0 -54
- package/dist/db/issue-workflow-coordinator.js +0 -320
- package/dist/db/issue-workflow-store.js +0 -194
- package/dist/db/run-report-store.js +0 -33
- package/dist/db/stage-event-store.js +0 -33
- package/dist/db/webhook-event-store.js +0 -59
- package/dist/db-ports.js +0 -5
- package/dist/ledger-ports.js +0 -1
- package/dist/reconciliation-action-applier.js +0 -68
- package/dist/reconciliation-actions.js +0 -1
- package/dist/reconciliation-engine.js +0 -350
- package/dist/reconciliation-snapshot-builder.js +0 -135
- package/dist/reconciliation-types.js +0 -1
- package/dist/service-stage-finalizer.js +0 -753
- package/dist/service-stage-runner.js +0 -336
- package/dist/service-webhook-processor.js +0 -411
- package/dist/stage-agent-activity-publisher.js +0 -59
- package/dist/stage-event-ports.js +0 -1
- package/dist/stage-failure.js +0 -92
- package/dist/stage-handoff.js +0 -107
- package/dist/stage-launch.js +0 -84
- package/dist/stage-lifecycle-publisher.js +0 -284
- package/dist/stage-turn-input-dispatcher.js +0 -104
- package/dist/webhook-agent-session-handler.js +0 -228
- package/dist/webhook-comment-handler.js +0 -141
- package/dist/webhook-desired-stage-recorder.js +0 -122
- package/dist/webhook-event-ports.js +0 -1
- package/dist/workflow-policy.js +0 -149
- package/dist/workflow-ports.js +0 -1
- /package/dist/{installation-ports.js → github-types.js} +0 -0
package/dist/stage-launch.js
DELETED
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { buildCarryForwardPrompt } from "./stage-handoff.js";
|
|
4
|
-
import { listWorkflowStageIds, resolveWorkflowStageConfig } from "./workflow-policy.js";
|
|
5
|
-
function slugify(value) {
|
|
6
|
-
return value
|
|
7
|
-
.toLowerCase()
|
|
8
|
-
.replace(/[^a-z0-9]+/g, "-")
|
|
9
|
-
.replace(/^-+|-+$/g, "")
|
|
10
|
-
.slice(0, 60);
|
|
11
|
-
}
|
|
12
|
-
function sanitizePathSegment(value) {
|
|
13
|
-
return value.replace(/[^a-zA-Z0-9._-]+/g, "-");
|
|
14
|
-
}
|
|
15
|
-
export function isCodexThreadId(value) {
|
|
16
|
-
if (!value) {
|
|
17
|
-
return false;
|
|
18
|
-
}
|
|
19
|
-
return !value.startsWith("missing-thread-") && !value.startsWith("launch-failed-");
|
|
20
|
-
}
|
|
21
|
-
export function buildStageLaunchPlan(project, issue, stage, options) {
|
|
22
|
-
const workflow = resolveWorkflowStageConfig(project, stage, issue.selectedWorkflowId);
|
|
23
|
-
if (!workflow) {
|
|
24
|
-
throw new Error(`Workflow "${stage}" is not configured for project ${project.id}`);
|
|
25
|
-
}
|
|
26
|
-
const issueRef = sanitizePathSegment(issue.issueKey ?? issue.linearIssueId);
|
|
27
|
-
const slug = issue.title ? slugify(issue.title) : "";
|
|
28
|
-
const branchSuffix = slug ? `${issueRef}-${slug}` : issueRef;
|
|
29
|
-
return {
|
|
30
|
-
branchName: options?.branchName ?? `${project.branchPrefix}/${branchSuffix}`,
|
|
31
|
-
worktreePath: options?.worktreePath ?? path.join(project.worktreeRoot, issueRef),
|
|
32
|
-
workflowFile: workflow.workflowFile,
|
|
33
|
-
stage,
|
|
34
|
-
prompt: buildStagePrompt(project, issue, workflow.id, workflow.whenState, workflow.workflowFile, {
|
|
35
|
-
branchName: options?.branchName ?? `${project.branchPrefix}/${branchSuffix}`,
|
|
36
|
-
worktreePath: options?.worktreePath ?? path.join(project.worktreeRoot, issueRef),
|
|
37
|
-
...(issue.selectedWorkflowId ? { workflowDefinitionId: issue.selectedWorkflowId } : {}),
|
|
38
|
-
...(options?.previousStageRun ? { previousStageRun: options.previousStageRun } : {}),
|
|
39
|
-
...(options?.workspace ? { workspace: options.workspace } : {}),
|
|
40
|
-
stageHistory: options?.stageHistory ?? [],
|
|
41
|
-
}),
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
export function buildStagePrompt(project, issue, stage, triggerState, workflowFile, options) {
|
|
45
|
-
const workflowBody = existsSync(workflowFile) ? readFileSync(workflowFile, "utf8").trim() : "";
|
|
46
|
-
const carryForward = buildCarryForwardPrompt({
|
|
47
|
-
project,
|
|
48
|
-
currentStage: stage,
|
|
49
|
-
...(options?.workflowDefinitionId ? { workflowDefinitionId: options.workflowDefinitionId } : {}),
|
|
50
|
-
...(options?.previousStageRun ? { previousStageRun: options.previousStageRun } : {}),
|
|
51
|
-
...(options?.workspace ? { workspace: options.workspace } : {}),
|
|
52
|
-
stageHistory: options?.stageHistory ?? [],
|
|
53
|
-
});
|
|
54
|
-
const availableStages = listWorkflowStageIds(project, options?.workflowDefinitionId).join(", ");
|
|
55
|
-
return [
|
|
56
|
-
`Issue: ${issue.issueKey ?? issue.linearIssueId}`,
|
|
57
|
-
issue.title ? `Title: ${issue.title}` : undefined,
|
|
58
|
-
issue.issueUrl ? `Linear URL: ${issue.issueUrl}` : undefined,
|
|
59
|
-
issue.currentLinearState ? `Current Linear State: ${issue.currentLinearState}` : undefined,
|
|
60
|
-
`Workflow: ${stage}`,
|
|
61
|
-
`Triggered By State: ${triggerState}`,
|
|
62
|
-
options?.branchName ? `Branch: ${options.branchName}` : undefined,
|
|
63
|
-
options?.worktreePath ? `Worktree: ${options.worktreePath}` : undefined,
|
|
64
|
-
"",
|
|
65
|
-
"Complete only the current workflow stage. Do not invent a new workflow or skip directly to another stage.",
|
|
66
|
-
"If the correct next step is unclear, say so plainly and use `human_needed` as the next likely stage.",
|
|
67
|
-
"",
|
|
68
|
-
carryForward ? "Carry-forward Context:" : undefined,
|
|
69
|
-
carryForward,
|
|
70
|
-
"",
|
|
71
|
-
"Operate only inside the prepared worktree for this issue. Continue the issue lifecycle in this workspace.",
|
|
72
|
-
"Use the repo workflow instructions below for this stage.",
|
|
73
|
-
"End with a short `Stage result:` section in plain text with exactly four bullets:",
|
|
74
|
-
"- what happened",
|
|
75
|
-
"- key facts or artifacts",
|
|
76
|
-
`- Next likely stage: one of ${availableStages}, done, or human_needed`,
|
|
77
|
-
"- what the next stage or human should pay attention to",
|
|
78
|
-
"",
|
|
79
|
-
`Workflow File: ${path.basename(workflowFile)}`,
|
|
80
|
-
workflowBody,
|
|
81
|
-
]
|
|
82
|
-
.filter(Boolean)
|
|
83
|
-
.join("\n");
|
|
84
|
-
}
|
|
@@ -1,284 +0,0 @@
|
|
|
1
|
-
import { buildAwaitingHandoffSessionPlan, buildCompletedSessionPlan, buildPreparingSessionPlan, buildRunningSessionPlan, } from "./agent-session-plan.js";
|
|
2
|
-
import { buildAgentSessionExternalUrls } from "./agent-session-presentation.js";
|
|
3
|
-
import { buildAwaitingHandoffComment, buildHumanNeededComment, buildRunningStatusComment, resolveActiveLinearState, resolveWorkflowLabelCleanup, resolveWorkflowLabelNames, } from "./linear-workflow.js";
|
|
4
|
-
import { sanitizeDiagnosticText } from "./utils.js";
|
|
5
|
-
export class StageLifecyclePublisher {
|
|
6
|
-
config;
|
|
7
|
-
stores;
|
|
8
|
-
linearProvider;
|
|
9
|
-
logger;
|
|
10
|
-
feed;
|
|
11
|
-
constructor(config, stores, linearProvider, logger, feed) {
|
|
12
|
-
this.config = config;
|
|
13
|
-
this.stores = stores;
|
|
14
|
-
this.linearProvider = linearProvider;
|
|
15
|
-
this.logger = logger;
|
|
16
|
-
this.feed = feed;
|
|
17
|
-
}
|
|
18
|
-
async markStageActive(project, issue, stageRun) {
|
|
19
|
-
const activeState = resolveActiveLinearState(project, stageRun.stage, issue.selectedWorkflowId);
|
|
20
|
-
const linear = await this.linearProvider.forProject(stageRun.projectId);
|
|
21
|
-
if (!activeState || !linear) {
|
|
22
|
-
return;
|
|
23
|
-
}
|
|
24
|
-
await linear.setIssueState(stageRun.linearIssueId, activeState);
|
|
25
|
-
const labels = resolveWorkflowLabelNames(project, "working");
|
|
26
|
-
if (labels.add.length > 0 || labels.remove.length > 0) {
|
|
27
|
-
await linear.updateIssueLabels({
|
|
28
|
-
issueId: stageRun.linearIssueId,
|
|
29
|
-
...(labels.add.length > 0 ? { addNames: labels.add } : {}),
|
|
30
|
-
...(labels.remove.length > 0 ? { removeNames: labels.remove } : {}),
|
|
31
|
-
});
|
|
32
|
-
}
|
|
33
|
-
this.stores.workflowCoordinator.upsertTrackedIssue({
|
|
34
|
-
projectId: stageRun.projectId,
|
|
35
|
-
linearIssueId: stageRun.linearIssueId,
|
|
36
|
-
currentLinearState: activeState,
|
|
37
|
-
statusCommentId: issue.statusCommentId ?? null,
|
|
38
|
-
activeAgentSessionId: issue.activeAgentSessionId ?? null,
|
|
39
|
-
lifecycleStatus: "running",
|
|
40
|
-
});
|
|
41
|
-
}
|
|
42
|
-
async refreshRunningStatusComment(projectId, issueId, stageRunId, issueKey) {
|
|
43
|
-
const linear = await this.linearProvider.forProject(projectId);
|
|
44
|
-
if (!linear) {
|
|
45
|
-
return;
|
|
46
|
-
}
|
|
47
|
-
const issue = this.stores.issueWorkflows.getTrackedIssue(projectId, issueId);
|
|
48
|
-
const stageRun = this.stores.issueWorkflows.getStageRun(stageRunId);
|
|
49
|
-
const workspace = stageRun ? this.stores.issueWorkflows.getWorkspace(stageRun.workspaceId) : undefined;
|
|
50
|
-
if (!issue || !stageRun || !workspace) {
|
|
51
|
-
return;
|
|
52
|
-
}
|
|
53
|
-
try {
|
|
54
|
-
const result = await linear.upsertIssueComment({
|
|
55
|
-
issueId,
|
|
56
|
-
...(issue.statusCommentId ? { commentId: issue.statusCommentId } : {}),
|
|
57
|
-
body: buildRunningStatusComment({
|
|
58
|
-
issue,
|
|
59
|
-
stageRun,
|
|
60
|
-
branchName: workspace.branchName,
|
|
61
|
-
}),
|
|
62
|
-
});
|
|
63
|
-
this.stores.workflowCoordinator.setIssueStatusComment(projectId, issueId, result.id);
|
|
64
|
-
}
|
|
65
|
-
catch (error) {
|
|
66
|
-
this.logger.warn({
|
|
67
|
-
issueKey,
|
|
68
|
-
stageRunId,
|
|
69
|
-
issueId,
|
|
70
|
-
error: error instanceof Error ? error.message : String(error),
|
|
71
|
-
}, "Failed to refresh running status comment after stage startup");
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
async publishStageStarted(issue, stage) {
|
|
75
|
-
if (!issue.activeAgentSessionId) {
|
|
76
|
-
return false;
|
|
77
|
-
}
|
|
78
|
-
const linear = await this.linearProvider.forProject(issue.projectId);
|
|
79
|
-
if (!linear) {
|
|
80
|
-
return false;
|
|
81
|
-
}
|
|
82
|
-
const sessionUpdated = await this.updateAgentSession(linear, issue, buildRunningSessionPlan(stage), stage);
|
|
83
|
-
try {
|
|
84
|
-
await linear.createAgentActivity({
|
|
85
|
-
agentSessionId: issue.activeAgentSessionId,
|
|
86
|
-
content: {
|
|
87
|
-
type: "response",
|
|
88
|
-
body: `PatchRelay started the ${stage} workflow and is working in the background.`,
|
|
89
|
-
},
|
|
90
|
-
});
|
|
91
|
-
return true;
|
|
92
|
-
}
|
|
93
|
-
catch (error) {
|
|
94
|
-
this.logger.warn({
|
|
95
|
-
issueKey: issue.issueKey,
|
|
96
|
-
stage,
|
|
97
|
-
agentSessionId: issue.activeAgentSessionId,
|
|
98
|
-
error: error instanceof Error ? error.message : String(error),
|
|
99
|
-
}, "Failed to publish Linear agent activity after stage startup");
|
|
100
|
-
return sessionUpdated;
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
async publishStageCompletion(stageRun, enqueueIssue) {
|
|
104
|
-
const refreshedIssue = this.stores.issueWorkflows.getTrackedIssue(stageRun.projectId, stageRun.linearIssueId);
|
|
105
|
-
if (refreshedIssue?.desiredStage) {
|
|
106
|
-
const linear = await this.linearProvider.forProject(stageRun.projectId);
|
|
107
|
-
if (refreshedIssue.activeAgentSessionId && linear) {
|
|
108
|
-
await this.updateAgentSession(linear, refreshedIssue, buildPreparingSessionPlan(refreshedIssue.desiredStage), refreshedIssue.desiredStage);
|
|
109
|
-
}
|
|
110
|
-
this.feed?.publish({
|
|
111
|
-
level: "info",
|
|
112
|
-
kind: "stage",
|
|
113
|
-
issueKey: refreshedIssue.issueKey,
|
|
114
|
-
projectId: refreshedIssue.projectId,
|
|
115
|
-
stage: stageRun.stage,
|
|
116
|
-
...(refreshedIssue.selectedWorkflowId ? { workflowId: refreshedIssue.selectedWorkflowId } : {}),
|
|
117
|
-
nextStage: refreshedIssue.desiredStage,
|
|
118
|
-
status: "queued",
|
|
119
|
-
summary: `Completed ${stageRun.stage} workflow and queued ${refreshedIssue.desiredStage}`,
|
|
120
|
-
});
|
|
121
|
-
await this.publishAgentCompletion(refreshedIssue, {
|
|
122
|
-
type: "thought",
|
|
123
|
-
body: `The ${stageRun.stage} workflow finished. PatchRelay is preparing the ${refreshedIssue.desiredStage} workflow next.`,
|
|
124
|
-
});
|
|
125
|
-
enqueueIssue(stageRun.projectId, stageRun.linearIssueId);
|
|
126
|
-
return;
|
|
127
|
-
}
|
|
128
|
-
const project = this.config.projects.find((candidate) => candidate.id === stageRun.projectId);
|
|
129
|
-
const activeState = project ? resolveActiveLinearState(project, stageRun.stage, refreshedIssue?.selectedWorkflowId) : undefined;
|
|
130
|
-
const linear = project ? await this.linearProvider.forProject(stageRun.projectId) : undefined;
|
|
131
|
-
if (refreshedIssue && linear && project && activeState) {
|
|
132
|
-
try {
|
|
133
|
-
const linearIssue = await linear.getIssue(stageRun.linearIssueId);
|
|
134
|
-
if (linearIssue.stateName?.trim().toLowerCase() === activeState.trim().toLowerCase()) {
|
|
135
|
-
const labels = resolveWorkflowLabelNames(project, "awaitingHandoff");
|
|
136
|
-
if (labels.add.length > 0 || labels.remove.length > 0) {
|
|
137
|
-
await linear.updateIssueLabels({
|
|
138
|
-
issueId: stageRun.linearIssueId,
|
|
139
|
-
...(labels.add.length > 0 ? { addNames: labels.add } : {}),
|
|
140
|
-
...(labels.remove.length > 0 ? { removeNames: labels.remove } : {}),
|
|
141
|
-
});
|
|
142
|
-
}
|
|
143
|
-
this.stores.workflowCoordinator.setIssueLifecycleStatus(stageRun.projectId, stageRun.linearIssueId, "paused");
|
|
144
|
-
const finalStageRun = this.stores.issueWorkflows.getStageRun(stageRun.id) ?? stageRun;
|
|
145
|
-
let deliveredToSession = false;
|
|
146
|
-
if (refreshedIssue.activeAgentSessionId) {
|
|
147
|
-
deliveredToSession = await this.updateAgentSession(linear, refreshedIssue, buildAwaitingHandoffSessionPlan(stageRun.stage));
|
|
148
|
-
}
|
|
149
|
-
this.feed?.publish({
|
|
150
|
-
level: "info",
|
|
151
|
-
kind: "stage",
|
|
152
|
-
issueKey: refreshedIssue.issueKey,
|
|
153
|
-
projectId: refreshedIssue.projectId,
|
|
154
|
-
stage: stageRun.stage,
|
|
155
|
-
...(refreshedIssue.selectedWorkflowId ? { workflowId: refreshedIssue.selectedWorkflowId } : {}),
|
|
156
|
-
status: "handoff",
|
|
157
|
-
summary: `Completed ${stageRun.stage} workflow`,
|
|
158
|
-
detail: `Waiting for a Linear state change or follow-up input while the issue remains in ${activeState}.`,
|
|
159
|
-
});
|
|
160
|
-
deliveredToSession =
|
|
161
|
-
(await this.publishAgentCompletion(refreshedIssue, {
|
|
162
|
-
type: "elicitation",
|
|
163
|
-
body: `PatchRelay finished the ${stageRun.stage} workflow. Move the issue to its next workflow state or leave a follow-up prompt to continue.`,
|
|
164
|
-
})) || deliveredToSession;
|
|
165
|
-
if (!deliveredToSession && !refreshedIssue.activeAgentSessionId) {
|
|
166
|
-
const result = await linear.upsertIssueComment({
|
|
167
|
-
issueId: stageRun.linearIssueId,
|
|
168
|
-
...(refreshedIssue.statusCommentId ? { commentId: refreshedIssue.statusCommentId } : {}),
|
|
169
|
-
body: buildAwaitingHandoffComment({
|
|
170
|
-
issue: refreshedIssue,
|
|
171
|
-
stageRun: finalStageRun,
|
|
172
|
-
activeState,
|
|
173
|
-
}),
|
|
174
|
-
});
|
|
175
|
-
this.stores.workflowCoordinator.setIssueStatusComment(stageRun.projectId, stageRun.linearIssueId, result.id);
|
|
176
|
-
}
|
|
177
|
-
return;
|
|
178
|
-
}
|
|
179
|
-
const cleanup = resolveWorkflowLabelCleanup(project);
|
|
180
|
-
if (cleanup.remove.length > 0) {
|
|
181
|
-
await linear.updateIssueLabels({
|
|
182
|
-
issueId: stageRun.linearIssueId,
|
|
183
|
-
removeNames: cleanup.remove,
|
|
184
|
-
});
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
catch (error) {
|
|
188
|
-
this.logger.warn({
|
|
189
|
-
issueKey: refreshedIssue.issueKey,
|
|
190
|
-
issueId: stageRun.linearIssueId,
|
|
191
|
-
stageRunId: stageRun.id,
|
|
192
|
-
stage: stageRun.stage,
|
|
193
|
-
error: sanitizeDiagnosticText(error instanceof Error ? error.message : String(error)),
|
|
194
|
-
}, "Stage completed locally but PatchRelay could not finish the final Linear sync");
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
if (refreshedIssue) {
|
|
198
|
-
let deliveredToSession = false;
|
|
199
|
-
if (refreshedIssue.activeAgentSessionId && linear) {
|
|
200
|
-
deliveredToSession = await this.updateAgentSession(linear, refreshedIssue, refreshedIssue.lifecycleStatus === "paused"
|
|
201
|
-
? buildAwaitingHandoffSessionPlan(stageRun.stage)
|
|
202
|
-
: buildCompletedSessionPlan(stageRun.stage));
|
|
203
|
-
}
|
|
204
|
-
this.feed?.publish({
|
|
205
|
-
level: "info",
|
|
206
|
-
kind: "stage",
|
|
207
|
-
issueKey: refreshedIssue.issueKey,
|
|
208
|
-
projectId: refreshedIssue.projectId,
|
|
209
|
-
stage: stageRun.stage,
|
|
210
|
-
...(refreshedIssue.selectedWorkflowId ? { workflowId: refreshedIssue.selectedWorkflowId } : {}),
|
|
211
|
-
status: "completed",
|
|
212
|
-
summary: `Completed ${stageRun.stage} workflow`,
|
|
213
|
-
});
|
|
214
|
-
deliveredToSession =
|
|
215
|
-
(await this.publishAgentCompletion(refreshedIssue, {
|
|
216
|
-
type: refreshedIssue.lifecycleStatus === "paused" ? "elicitation" : "response",
|
|
217
|
-
body: refreshedIssue.lifecycleStatus === "paused"
|
|
218
|
-
? `PatchRelay finished the ${stageRun.stage} workflow and now needs human input before it can continue.`
|
|
219
|
-
: `PatchRelay finished the ${stageRun.stage} workflow.`,
|
|
220
|
-
})) || deliveredToSession;
|
|
221
|
-
if (refreshedIssue.lifecycleStatus === "paused" && linear && !deliveredToSession) {
|
|
222
|
-
const result = await linear.upsertIssueComment({
|
|
223
|
-
issueId: stageRun.linearIssueId,
|
|
224
|
-
...(refreshedIssue.statusCommentId ? { commentId: refreshedIssue.statusCommentId } : {}),
|
|
225
|
-
body: buildHumanNeededComment({
|
|
226
|
-
issue: refreshedIssue,
|
|
227
|
-
stageRun: this.stores.issueWorkflows.getStageRun(stageRun.id) ?? stageRun,
|
|
228
|
-
}),
|
|
229
|
-
});
|
|
230
|
-
this.stores.workflowCoordinator.setIssueStatusComment(stageRun.projectId, stageRun.linearIssueId, result.id);
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
async updateAgentSession(linear, issue, plan, stage) {
|
|
235
|
-
if (!issue.activeAgentSessionId) {
|
|
236
|
-
return false;
|
|
237
|
-
}
|
|
238
|
-
try {
|
|
239
|
-
const externalUrls = buildAgentSessionExternalUrls(this.config, issue.issueKey);
|
|
240
|
-
await linear.updateAgentSession?.({
|
|
241
|
-
agentSessionId: issue.activeAgentSessionId,
|
|
242
|
-
...(externalUrls ? { externalUrls } : {}),
|
|
243
|
-
plan,
|
|
244
|
-
});
|
|
245
|
-
return true;
|
|
246
|
-
}
|
|
247
|
-
catch (error) {
|
|
248
|
-
this.logger.warn({
|
|
249
|
-
issueKey: issue.issueKey,
|
|
250
|
-
issueId: issue.linearIssueId,
|
|
251
|
-
...(stage ? { stage } : {}),
|
|
252
|
-
agentSessionId: issue.activeAgentSessionId,
|
|
253
|
-
error: sanitizeDiagnosticText(error instanceof Error ? error.message : String(error)),
|
|
254
|
-
}, "Failed to update Linear agent session");
|
|
255
|
-
return false;
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
async publishAgentCompletion(issue, content) {
|
|
259
|
-
if (!issue.activeAgentSessionId) {
|
|
260
|
-
return false;
|
|
261
|
-
}
|
|
262
|
-
const linear = await this.linearProvider.forProject(issue.projectId);
|
|
263
|
-
if (!linear) {
|
|
264
|
-
return false;
|
|
265
|
-
}
|
|
266
|
-
try {
|
|
267
|
-
await linear.createAgentActivity({
|
|
268
|
-
agentSessionId: issue.activeAgentSessionId,
|
|
269
|
-
content,
|
|
270
|
-
});
|
|
271
|
-
return true;
|
|
272
|
-
}
|
|
273
|
-
catch (error) {
|
|
274
|
-
this.logger.warn({
|
|
275
|
-
issueKey: issue.issueKey,
|
|
276
|
-
issueId: issue.linearIssueId,
|
|
277
|
-
agentSessionId: issue.activeAgentSessionId,
|
|
278
|
-
activityType: content.type,
|
|
279
|
-
error: sanitizeDiagnosticText(error instanceof Error ? error.message : String(error)),
|
|
280
|
-
}, "Failed to publish Linear agent activity");
|
|
281
|
-
return false;
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
}
|
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
import { sanitizeDiagnosticText } from "./utils.js";
|
|
2
|
-
import { safeJsonParse } from "./utils.js";
|
|
3
|
-
export class StageTurnInputDispatcher {
|
|
4
|
-
inputs;
|
|
5
|
-
codex;
|
|
6
|
-
logger;
|
|
7
|
-
constructor(inputs, codex, logger) {
|
|
8
|
-
this.inputs = inputs;
|
|
9
|
-
this.codex = codex;
|
|
10
|
-
this.logger = logger;
|
|
11
|
-
}
|
|
12
|
-
routePendingInputs(stageRun, threadId, turnId) {
|
|
13
|
-
const issueControl = this.inputs.issueControl.getIssueControl(stageRun.projectId, stageRun.linearIssueId);
|
|
14
|
-
if (!issueControl?.activeRunLeaseId) {
|
|
15
|
-
return;
|
|
16
|
-
}
|
|
17
|
-
for (const obligation of this.listPendingInputObligations(stageRun.projectId, stageRun.linearIssueId, issueControl.activeRunLeaseId)) {
|
|
18
|
-
this.inputs.obligations.updateObligationRouting(obligation.id, {
|
|
19
|
-
runLeaseId: issueControl.activeRunLeaseId,
|
|
20
|
-
threadId,
|
|
21
|
-
turnId,
|
|
22
|
-
});
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
async flush(stageRun, options) {
|
|
26
|
-
if (!stageRun.threadId || !stageRun.turnId) {
|
|
27
|
-
return { deliveredInputIds: [], deliveredObligationIds: [], deliveredCount: 0, failedObligationIds: [] };
|
|
28
|
-
}
|
|
29
|
-
const issueControl = this.inputs.issueControl.getIssueControl(stageRun.projectId, stageRun.linearIssueId);
|
|
30
|
-
if (!issueControl?.activeRunLeaseId) {
|
|
31
|
-
return { deliveredInputIds: [], deliveredObligationIds: [], deliveredCount: 0, failedObligationIds: [] };
|
|
32
|
-
}
|
|
33
|
-
const deliveredInputIds = [];
|
|
34
|
-
const deliveredObligationIds = [];
|
|
35
|
-
const failedObligationIds = [];
|
|
36
|
-
let deliveredCount = 0;
|
|
37
|
-
const obligationQuery = options?.retryInProgress ? { includeInProgress: true } : undefined;
|
|
38
|
-
for (const obligation of this.listPendingInputObligations(stageRun.projectId, stageRun.linearIssueId, issueControl.activeRunLeaseId, obligationQuery)) {
|
|
39
|
-
const payload = safeJsonParse(obligation.payloadJson);
|
|
40
|
-
const body = payload?.body?.trim();
|
|
41
|
-
if (!body) {
|
|
42
|
-
this.inputs.obligations.markObligationStatus(obligation.id, "failed", "obligation payload had no deliverable body");
|
|
43
|
-
continue;
|
|
44
|
-
}
|
|
45
|
-
const claimed = obligation.status === "in_progress" && options?.retryInProgress
|
|
46
|
-
? true
|
|
47
|
-
: this.inputs.obligations.claimPendingObligation(obligation.id, {
|
|
48
|
-
runLeaseId: issueControl.activeRunLeaseId,
|
|
49
|
-
threadId: stageRun.threadId,
|
|
50
|
-
turnId: stageRun.turnId,
|
|
51
|
-
});
|
|
52
|
-
if (!claimed) {
|
|
53
|
-
continue;
|
|
54
|
-
}
|
|
55
|
-
try {
|
|
56
|
-
if (obligation.status === "in_progress") {
|
|
57
|
-
this.inputs.obligations.updateObligationRouting(obligation.id, {
|
|
58
|
-
runLeaseId: issueControl.activeRunLeaseId,
|
|
59
|
-
threadId: stageRun.threadId,
|
|
60
|
-
turnId: stageRun.turnId,
|
|
61
|
-
});
|
|
62
|
-
}
|
|
63
|
-
await this.codex.steerTurn({
|
|
64
|
-
threadId: stageRun.threadId,
|
|
65
|
-
turnId: stageRun.turnId,
|
|
66
|
-
input: body,
|
|
67
|
-
});
|
|
68
|
-
deliveredObligationIds.push(obligation.id);
|
|
69
|
-
this.inputs.obligations.markObligationStatus(obligation.id, "completed");
|
|
70
|
-
deliveredCount += 1;
|
|
71
|
-
this.logger.debug({
|
|
72
|
-
threadId: stageRun.threadId,
|
|
73
|
-
turnId: stageRun.turnId,
|
|
74
|
-
obligationId: obligation.id,
|
|
75
|
-
source: obligation.source,
|
|
76
|
-
}, "Delivered queued turn input to Codex");
|
|
77
|
-
}
|
|
78
|
-
catch (error) {
|
|
79
|
-
this.inputs.obligations.markObligationStatus(obligation.id, "pending", error instanceof Error ? error.message : String(error));
|
|
80
|
-
failedObligationIds.push(obligation.id);
|
|
81
|
-
this.logger.warn({
|
|
82
|
-
issueKey: options?.issueKey,
|
|
83
|
-
threadId: stageRun.threadId,
|
|
84
|
-
turnId: stageRun.turnId,
|
|
85
|
-
obligationId: obligation.id,
|
|
86
|
-
source: obligation.source,
|
|
87
|
-
error: sanitizeDiagnosticText(error instanceof Error ? error.message : String(error)),
|
|
88
|
-
}, options?.failureMessage ?? "Failed to deliver queued turn input");
|
|
89
|
-
break;
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
return { deliveredInputIds, deliveredObligationIds, deliveredCount, failedObligationIds };
|
|
93
|
-
}
|
|
94
|
-
listPendingInputObligations(projectId, linearIssueId, activeRunLeaseId, options) {
|
|
95
|
-
const query = options?.includeInProgress
|
|
96
|
-
? { kind: "deliver_turn_input", includeInProgress: true }
|
|
97
|
-
: { kind: "deliver_turn_input" };
|
|
98
|
-
return this.inputs.obligations
|
|
99
|
-
.listPendingObligations(query)
|
|
100
|
-
.filter((obligation) => obligation.projectId === projectId &&
|
|
101
|
-
obligation.linearIssueId === linearIssueId &&
|
|
102
|
-
(obligation.runLeaseId === undefined || obligation.runLeaseId === activeRunLeaseId));
|
|
103
|
-
}
|
|
104
|
-
}
|