patchrelay 0.8.9 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/README.md +64 -62
  2. package/dist/agent-session-plan.js +17 -17
  3. package/dist/build-info.json +3 -3
  4. package/dist/cli/commands/issues.js +12 -12
  5. package/dist/cli/data.js +109 -298
  6. package/dist/cli/formatters/text.js +22 -28
  7. package/dist/config.js +13 -166
  8. package/dist/db/migrations.js +46 -154
  9. package/dist/db.js +369 -45
  10. package/dist/factory-state.js +55 -0
  11. package/dist/github-webhook-handler.js +199 -0
  12. package/dist/github-webhooks.js +166 -0
  13. package/dist/hook-runner.js +28 -0
  14. package/dist/http.js +48 -22
  15. package/dist/issue-query-service.js +33 -38
  16. package/dist/linear-workflow.js +5 -118
  17. package/dist/preflight.js +1 -6
  18. package/dist/project-resolution.js +12 -1
  19. package/dist/run-orchestrator.js +446 -0
  20. package/dist/{stage-reporting.js → run-reporting.js} +11 -13
  21. package/dist/service-runtime.js +12 -61
  22. package/dist/service-webhooks.js +7 -52
  23. package/dist/service.js +39 -61
  24. package/dist/webhook-handler.js +387 -0
  25. package/dist/webhook-installation-handler.js +3 -8
  26. package/package.json +2 -1
  27. package/dist/db/authoritative-ledger-store.js +0 -536
  28. package/dist/db/issue-projection-store.js +0 -54
  29. package/dist/db/issue-workflow-coordinator.js +0 -320
  30. package/dist/db/issue-workflow-store.js +0 -194
  31. package/dist/db/run-report-store.js +0 -33
  32. package/dist/db/stage-event-store.js +0 -33
  33. package/dist/db/webhook-event-store.js +0 -59
  34. package/dist/db-ports.js +0 -5
  35. package/dist/ledger-ports.js +0 -1
  36. package/dist/reconciliation-action-applier.js +0 -68
  37. package/dist/reconciliation-actions.js +0 -1
  38. package/dist/reconciliation-engine.js +0 -350
  39. package/dist/reconciliation-snapshot-builder.js +0 -135
  40. package/dist/reconciliation-types.js +0 -1
  41. package/dist/service-stage-finalizer.js +0 -753
  42. package/dist/service-stage-runner.js +0 -336
  43. package/dist/service-webhook-processor.js +0 -411
  44. package/dist/stage-agent-activity-publisher.js +0 -59
  45. package/dist/stage-event-ports.js +0 -1
  46. package/dist/stage-failure.js +0 -92
  47. package/dist/stage-handoff.js +0 -107
  48. package/dist/stage-launch.js +0 -84
  49. package/dist/stage-lifecycle-publisher.js +0 -284
  50. package/dist/stage-turn-input-dispatcher.js +0 -104
  51. package/dist/webhook-agent-session-handler.js +0 -228
  52. package/dist/webhook-comment-handler.js +0 -141
  53. package/dist/webhook-desired-stage-recorder.js +0 -122
  54. package/dist/webhook-event-ports.js +0 -1
  55. package/dist/workflow-policy.js +0 -149
  56. package/dist/workflow-ports.js +0 -1
  57. /package/dist/{installation-ports.js → github-types.js} +0 -0
@@ -1,336 +0,0 @@
1
- import { resolveAuthoritativeLinearStopState } from "./linear-workflow.js";
2
- import { buildStageLaunchPlan, isCodexThreadId } from "./stage-launch.js";
3
- import { syncFailedStageToLinear } from "./stage-failure.js";
4
- import { buildFailedStageReport } from "./stage-reporting.js";
5
- import { StageLifecyclePublisher } from "./stage-lifecycle-publisher.js";
6
- import { StageTurnInputDispatcher } from "./stage-turn-input-dispatcher.js";
7
- import { safeJsonParse } from "./utils.js";
8
- import { WorktreeManager } from "./worktree-manager.js";
9
- export class ServiceStageRunner {
10
- config;
11
- stores;
12
- codex;
13
- linearProvider;
14
- logger;
15
- feed;
16
- worktreeManager;
17
- inputDispatcher;
18
- lifecyclePublisher;
19
- runAtomically;
20
- constructor(config, stores, codex, linearProvider, logger, runAtomically = (fn) => fn(), feed) {
21
- this.config = config;
22
- this.stores = stores;
23
- this.codex = codex;
24
- this.linearProvider = linearProvider;
25
- this.logger = logger;
26
- this.feed = feed;
27
- this.runAtomically = runAtomically;
28
- this.worktreeManager = new WorktreeManager(config);
29
- this.inputDispatcher = new StageTurnInputDispatcher(stores, codex, logger);
30
- this.lifecyclePublisher = new StageLifecyclePublisher(config, stores, linearProvider, logger, feed);
31
- }
32
- async run(item) {
33
- const project = this.config.projects.find((candidate) => candidate.id === item.projectId);
34
- if (!project) {
35
- return;
36
- }
37
- const issueControl = this.stores.issueControl.getIssueControl(item.projectId, item.issueId);
38
- if (!issueControl?.desiredStage || issueControl.activeRunLeaseId !== undefined) {
39
- return;
40
- }
41
- const receipt = issueControl.desiredReceiptId !== undefined ? this.stores.eventReceipts.getEventReceipt(issueControl.desiredReceiptId) : undefined;
42
- if (!receipt?.externalId) {
43
- return;
44
- }
45
- const desiredStage = issueControl.desiredStage;
46
- const desiredWebhookId = receipt.externalId;
47
- const issue = await this.ensureLaunchIssueMirror(project, item.issueId, desiredStage, desiredWebhookId);
48
- if (!issue) {
49
- return;
50
- }
51
- if (issue.lifecycleStatus === "completed" || issue.lifecycleStatus === "paused") {
52
- this.feed?.publish({
53
- level: "info",
54
- kind: "workflow",
55
- issueKey: issue.issueKey,
56
- projectId: item.projectId,
57
- stage: desiredStage,
58
- ...(issue.selectedWorkflowId ? { workflowId: issue.selectedWorkflowId } : {}),
59
- status: issue.lifecycleStatus === "completed" ? "completed" : "transition_suppressed",
60
- summary: issue.lifecycleStatus === "completed"
61
- ? `Skipped ${desiredStage} because the issue is already complete`
62
- : `Skipped ${desiredStage} because the issue is already paused`,
63
- detail: issue.currentLinearState ? `Live Linear state is ${issue.currentLinearState}.` : undefined,
64
- });
65
- return;
66
- }
67
- const existingWorkspace = this.stores.workspaceOwnership.getWorkspaceOwnershipForIssue(item.projectId, item.issueId);
68
- const stageHistory = this.stores.issueWorkflows.listStageRunsForIssue(item.projectId, item.issueId);
69
- const previousStageRun = stageHistory.at(-1);
70
- const defaultPlan = buildStageLaunchPlan(project, issue, desiredStage, {
71
- ...(previousStageRun ? { previousStageRun } : {}),
72
- ...(existingWorkspace
73
- ? {
74
- workspace: {
75
- branchName: existingWorkspace.branchName,
76
- worktreePath: existingWorkspace.worktreePath,
77
- },
78
- }
79
- : {}),
80
- stageHistory,
81
- });
82
- const plan = existingWorkspace
83
- ? {
84
- ...defaultPlan,
85
- branchName: existingWorkspace.branchName,
86
- worktreePath: existingWorkspace.worktreePath,
87
- }
88
- : defaultPlan;
89
- this.feed?.publish({
90
- level: "info",
91
- kind: "stage",
92
- issueKey: issue.issueKey,
93
- projectId: item.projectId,
94
- stage: desiredStage,
95
- ...(issue.selectedWorkflowId ? { workflowId: issue.selectedWorkflowId } : {}),
96
- status: "starting",
97
- summary: `Starting ${desiredStage} workflow`,
98
- detail: `Preparing ${plan.branchName}`,
99
- });
100
- const claim = this.stores.workflowCoordinator.claimStageRun({
101
- projectId: item.projectId,
102
- linearIssueId: item.issueId,
103
- stage: desiredStage,
104
- triggerWebhookId: desiredWebhookId,
105
- branchName: plan.branchName,
106
- worktreePath: plan.worktreePath,
107
- workflowFile: plan.workflowFile,
108
- promptText: plan.prompt,
109
- });
110
- if (!claim) {
111
- return;
112
- }
113
- let threadLaunch;
114
- let turn;
115
- try {
116
- await this.worktreeManager.ensureIssueWorktree(project.repoPath, project.worktreeRoot, plan.worktreePath, plan.branchName, {
117
- allowExistingOutsideRoot: existingWorkspace !== undefined,
118
- });
119
- await this.lifecyclePublisher.markStageActive(project, claim.issue, claim.stageRun);
120
- threadLaunch = await this.launchStageThread(item.projectId, item.issueId, claim.stageRun.id, plan.worktreePath);
121
- const pendingLaunchInput = this.collectPendingLaunchInput(item.projectId, item.issueId);
122
- const initialTurnInput = pendingLaunchInput.combinedInput
123
- ? [plan.prompt, "", pendingLaunchInput.combinedInput].join("\n")
124
- : plan.prompt;
125
- turn = await this.codex.startTurn({
126
- threadId: threadLaunch.threadId,
127
- cwd: plan.worktreePath,
128
- input: initialTurnInput,
129
- });
130
- this.completeDeliveredLaunchInput(pendingLaunchInput.obligationIds, claim.stageRun.id, threadLaunch.threadId, turn.turnId);
131
- }
132
- catch (error) {
133
- const err = error instanceof Error ? error : new Error(String(error));
134
- await this.markLaunchFailed(project, claim.issue, claim.stageRun, err.message, threadLaunch?.threadId);
135
- this.logger.error({
136
- issueKey: issue.issueKey,
137
- stage: claim.stageRun.stage,
138
- worktreePath: plan.worktreePath,
139
- branchName: plan.branchName,
140
- error: err.message,
141
- stack: err.stack,
142
- }, "Failed to launch Codex stage run");
143
- this.feed?.publish({
144
- level: "error",
145
- kind: "stage",
146
- issueKey: issue.issueKey,
147
- projectId: item.projectId,
148
- stage: claim.stageRun.stage,
149
- status: "failed",
150
- summary: `Failed to launch ${claim.stageRun.stage} workflow`,
151
- detail: err.message,
152
- });
153
- throw err;
154
- }
155
- this.stores.workflowCoordinator.updateStageRunThread({
156
- stageRunId: claim.stageRun.id,
157
- threadId: threadLaunch.threadId,
158
- ...(threadLaunch.parentThreadId ? { parentThreadId: threadLaunch.parentThreadId } : {}),
159
- turnId: turn.turnId,
160
- });
161
- this.inputDispatcher.routePendingInputs(claim.stageRun, threadLaunch.threadId, turn.turnId);
162
- const deliveredToSession = await this.lifecyclePublisher.publishStageStarted(claim.issue, claim.stageRun.stage);
163
- if (!deliveredToSession && !claim.issue.activeAgentSessionId) {
164
- await this.lifecyclePublisher.refreshRunningStatusComment(item.projectId, item.issueId, claim.stageRun.id, issue.issueKey);
165
- }
166
- this.logger.info({
167
- issueKey: issue.issueKey,
168
- stage: claim.stageRun.stage,
169
- worktreePath: plan.worktreePath,
170
- branchName: plan.branchName,
171
- threadId: threadLaunch.threadId,
172
- turnId: turn.turnId,
173
- }, "Started Codex stage run");
174
- this.feed?.publish({
175
- level: "info",
176
- kind: "stage",
177
- issueKey: issue.issueKey,
178
- projectId: item.projectId,
179
- stage: claim.stageRun.stage,
180
- ...(claim.issue.selectedWorkflowId ? { workflowId: claim.issue.selectedWorkflowId } : {}),
181
- status: "running",
182
- summary: `Started ${claim.stageRun.stage} workflow`,
183
- detail: `Turn ${turn.turnId} is running in ${plan.branchName}.`,
184
- });
185
- }
186
- collectPendingLaunchInput(projectId, issueId) {
187
- const obligationIds = [];
188
- const bodies = [];
189
- for (const obligation of this.stores.obligations.listPendingObligations({ kind: "deliver_turn_input" })) {
190
- if (obligation.projectId !== projectId ||
191
- obligation.linearIssueId !== issueId ||
192
- !obligation.source.startsWith("linear-agent-launch:")) {
193
- continue;
194
- }
195
- const payload = safeJsonParse(obligation.payloadJson);
196
- const body = payload?.body?.trim();
197
- if (!body) {
198
- this.stores.obligations.markObligationStatus(obligation.id, "failed", "obligation payload had no deliverable body");
199
- continue;
200
- }
201
- obligationIds.push(obligation.id);
202
- bodies.push(body);
203
- }
204
- return {
205
- ...(bodies.length > 0 ? { combinedInput: bodies.join("\n\n") } : {}),
206
- obligationIds,
207
- };
208
- }
209
- completeDeliveredLaunchInput(obligationIds, runLeaseId, threadId, turnId) {
210
- for (const obligationId of obligationIds) {
211
- this.stores.obligations.updateObligationRouting(obligationId, {
212
- runLeaseId,
213
- threadId,
214
- turnId,
215
- });
216
- this.stores.obligations.markObligationStatus(obligationId, "completed");
217
- }
218
- }
219
- async ensureLaunchIssueMirror(project, linearIssueId, _desiredStage, _desiredWebhookId) {
220
- const existing = this.stores.issueWorkflows.getTrackedIssue(project.id, linearIssueId);
221
- const liveIssue = await this.linearProvider
222
- .forProject(project.id)
223
- .then((linear) => linear?.getIssue(linearIssueId))
224
- .catch(() => undefined);
225
- const authoritativeStopState = liveIssue ? resolveAuthoritativeLinearStopState(liveIssue) : undefined;
226
- if (authoritativeStopState) {
227
- this.stores.workflowCoordinator.setIssueDesiredStage(project.id, linearIssueId, undefined, {
228
- lifecycleStatus: authoritativeStopState.lifecycleStatus,
229
- });
230
- return this.stores.workflowCoordinator.upsertTrackedIssue({
231
- projectId: project.id,
232
- linearIssueId,
233
- ...(liveIssue?.identifier ? { issueKey: liveIssue.identifier } : existing?.issueKey ? { issueKey: existing.issueKey } : {}),
234
- ...(liveIssue?.title ? { title: liveIssue.title } : existing?.title ? { title: existing.title } : {}),
235
- ...(liveIssue?.url ? { issueUrl: liveIssue.url } : existing?.issueUrl ? { issueUrl: existing.issueUrl } : {}),
236
- currentLinearState: authoritativeStopState.stateName,
237
- lifecycleStatus: authoritativeStopState.lifecycleStatus,
238
- });
239
- }
240
- if (!liveIssue && existing?.issueKey && existing.title && existing.issueUrl && existing.currentLinearState) {
241
- return existing;
242
- }
243
- return this.stores.workflowCoordinator.recordDesiredStage({
244
- projectId: project.id,
245
- linearIssueId,
246
- ...(liveIssue?.identifier ? { issueKey: liveIssue.identifier } : existing?.issueKey ? { issueKey: existing.issueKey } : {}),
247
- ...(liveIssue?.title ? { title: liveIssue.title } : existing?.title ? { title: existing.title } : {}),
248
- ...(liveIssue?.url ? { issueUrl: liveIssue.url } : existing?.issueUrl ? { issueUrl: existing.issueUrl } : {}),
249
- ...(liveIssue?.stateName
250
- ? { currentLinearState: liveIssue.stateName }
251
- : existing?.currentLinearState
252
- ? { currentLinearState: existing.currentLinearState }
253
- : {}),
254
- lastWebhookAt: new Date().toISOString(),
255
- });
256
- }
257
- async launchStageThread(projectId, issueId, stageRunId, worktreePath) {
258
- const previousStageRun = this.stores.issueWorkflows
259
- .listStageRunsForIssue(projectId, issueId)
260
- .filter((stageRun) => stageRun.id !== stageRunId)
261
- .at(-1);
262
- const parentThreadId = previousStageRun?.status === "completed" && isCodexThreadId(previousStageRun.threadId)
263
- ? previousStageRun.threadId
264
- : undefined;
265
- const thread = await this.codex.startThread({ cwd: worktreePath });
266
- return {
267
- threadId: thread.id,
268
- ...(parentThreadId ? { parentThreadId } : {}),
269
- };
270
- }
271
- async markLaunchFailed(project, issue, stageRun, message, threadId) {
272
- const failureThreadId = threadId ?? `launch-failed-${stageRun.id}`;
273
- this.runAtomically(() => {
274
- this.stores.workflowCoordinator.finishStageRun({
275
- stageRunId: stageRun.id,
276
- status: "failed",
277
- threadId: failureThreadId,
278
- summaryJson: JSON.stringify({ message }),
279
- reportJson: JSON.stringify(buildFailedStageReport(stageRun, "failed", { threadId: failureThreadId })),
280
- });
281
- this.finishRunLease(stageRun.projectId, stageRun.linearIssueId, "failed", {
282
- threadId: failureThreadId,
283
- failureReason: message,
284
- });
285
- });
286
- await syncFailedStageToLinear({
287
- stores: this.stores,
288
- linearProvider: this.linearProvider,
289
- project,
290
- issue,
291
- stageRun: {
292
- ...stageRun,
293
- threadId: failureThreadId,
294
- },
295
- message,
296
- mode: "launch",
297
- });
298
- }
299
- finishRunLease(projectId, linearIssueId, status, params) {
300
- const issueControl = this.stores.issueControl.getIssueControl(projectId, linearIssueId);
301
- if (!issueControl?.activeRunLeaseId) {
302
- return;
303
- }
304
- this.stores.runLeases.finishRunLease({
305
- runLeaseId: issueControl.activeRunLeaseId,
306
- status,
307
- ...(params.threadId ? { threadId: params.threadId } : {}),
308
- ...(params.turnId ? { turnId: params.turnId } : {}),
309
- ...(params.failureReason ? { failureReason: params.failureReason } : {}),
310
- });
311
- if (issueControl.activeWorkspaceOwnershipId !== undefined) {
312
- const workspace = this.stores.workspaceOwnership.getWorkspaceOwnership(issueControl.activeWorkspaceOwnershipId);
313
- if (workspace) {
314
- this.stores.workspaceOwnership.upsertWorkspaceOwnership({
315
- projectId,
316
- linearIssueId,
317
- branchName: workspace.branchName,
318
- worktreePath: workspace.worktreePath,
319
- status: "paused",
320
- currentRunLeaseId: null,
321
- });
322
- }
323
- }
324
- this.stores.issueControl.upsertIssueControl({
325
- projectId,
326
- linearIssueId,
327
- activeRunLeaseId: null,
328
- lifecycleStatus: "failed",
329
- ...(issueControl.activeWorkspaceOwnershipId !== undefined
330
- ? { activeWorkspaceOwnershipId: issueControl.activeWorkspaceOwnershipId }
331
- : {}),
332
- ...(issueControl.serviceOwnedCommentId ? { serviceOwnedCommentId: issueControl.serviceOwnedCommentId } : {}),
333
- ...(issueControl.activeAgentSessionId ? { activeAgentSessionId: issueControl.activeAgentSessionId } : {}),
334
- });
335
- }
336
- }