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.
Files changed (60) 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/args.js +1 -1
  5. package/dist/cli/commands/issues.js +18 -18
  6. package/dist/cli/data.js +109 -298
  7. package/dist/cli/formatters/text.js +22 -28
  8. package/dist/cli/help.js +7 -7
  9. package/dist/cli/index.js +3 -3
  10. package/dist/config.js +13 -166
  11. package/dist/db/migrations.js +46 -154
  12. package/dist/db.js +369 -45
  13. package/dist/factory-state.js +55 -0
  14. package/dist/github-webhook-handler.js +199 -0
  15. package/dist/github-webhooks.js +166 -0
  16. package/dist/hook-runner.js +28 -0
  17. package/dist/http.js +48 -22
  18. package/dist/issue-query-service.js +33 -38
  19. package/dist/linear-workflow.js +5 -118
  20. package/dist/preflight.js +1 -6
  21. package/dist/project-resolution.js +12 -1
  22. package/dist/run-orchestrator.js +446 -0
  23. package/dist/{stage-reporting.js → run-reporting.js} +11 -13
  24. package/dist/service-runtime.js +12 -61
  25. package/dist/service-webhooks.js +7 -52
  26. package/dist/service.js +39 -61
  27. package/dist/webhook-handler.js +387 -0
  28. package/dist/webhook-installation-handler.js +3 -8
  29. package/package.json +2 -1
  30. package/dist/db/authoritative-ledger-store.js +0 -536
  31. package/dist/db/issue-projection-store.js +0 -54
  32. package/dist/db/issue-workflow-coordinator.js +0 -320
  33. package/dist/db/issue-workflow-store.js +0 -194
  34. package/dist/db/run-report-store.js +0 -33
  35. package/dist/db/stage-event-store.js +0 -33
  36. package/dist/db/webhook-event-store.js +0 -59
  37. package/dist/db-ports.js +0 -5
  38. package/dist/ledger-ports.js +0 -1
  39. package/dist/reconciliation-action-applier.js +0 -68
  40. package/dist/reconciliation-actions.js +0 -1
  41. package/dist/reconciliation-engine.js +0 -350
  42. package/dist/reconciliation-snapshot-builder.js +0 -135
  43. package/dist/reconciliation-types.js +0 -1
  44. package/dist/service-stage-finalizer.js +0 -753
  45. package/dist/service-stage-runner.js +0 -336
  46. package/dist/service-webhook-processor.js +0 -411
  47. package/dist/stage-agent-activity-publisher.js +0 -59
  48. package/dist/stage-event-ports.js +0 -1
  49. package/dist/stage-failure.js +0 -92
  50. package/dist/stage-handoff.js +0 -107
  51. package/dist/stage-launch.js +0 -84
  52. package/dist/stage-lifecycle-publisher.js +0 -284
  53. package/dist/stage-turn-input-dispatcher.js +0 -104
  54. package/dist/webhook-agent-session-handler.js +0 -228
  55. package/dist/webhook-comment-handler.js +0 -141
  56. package/dist/webhook-desired-stage-recorder.js +0 -122
  57. package/dist/webhook-event-ports.js +0 -1
  58. package/dist/workflow-policy.js +0 -149
  59. package/dist/workflow-ports.js +0 -1
  60. /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
- }