patchrelay 0.1.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 (78) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +271 -0
  3. package/config/patchrelay.example.json +5 -0
  4. package/dist/build-info.js +29 -0
  5. package/dist/build-info.json +6 -0
  6. package/dist/cli/data.js +461 -0
  7. package/dist/cli/formatters/json.js +3 -0
  8. package/dist/cli/formatters/text.js +119 -0
  9. package/dist/cli/index.js +761 -0
  10. package/dist/codex-app-server.js +353 -0
  11. package/dist/codex-types.js +1 -0
  12. package/dist/config-types.js +1 -0
  13. package/dist/config.js +494 -0
  14. package/dist/db/authoritative-ledger-store.js +437 -0
  15. package/dist/db/issue-workflow-store.js +690 -0
  16. package/dist/db/linear-installation-store.js +184 -0
  17. package/dist/db/migrations.js +183 -0
  18. package/dist/db/shared.js +101 -0
  19. package/dist/db/stage-event-store.js +33 -0
  20. package/dist/db/webhook-event-store.js +46 -0
  21. package/dist/db-ports.js +5 -0
  22. package/dist/db-types.js +1 -0
  23. package/dist/db.js +40 -0
  24. package/dist/file-permissions.js +40 -0
  25. package/dist/http.js +321 -0
  26. package/dist/index.js +69 -0
  27. package/dist/install.js +302 -0
  28. package/dist/installation-ports.js +1 -0
  29. package/dist/issue-query-service.js +68 -0
  30. package/dist/ledger-ports.js +1 -0
  31. package/dist/linear-client.js +338 -0
  32. package/dist/linear-oauth-service.js +131 -0
  33. package/dist/linear-oauth.js +154 -0
  34. package/dist/linear-types.js +1 -0
  35. package/dist/linear-workflow.js +78 -0
  36. package/dist/logging.js +62 -0
  37. package/dist/preflight.js +227 -0
  38. package/dist/project-resolution.js +51 -0
  39. package/dist/reconciliation-action-applier.js +55 -0
  40. package/dist/reconciliation-actions.js +1 -0
  41. package/dist/reconciliation-engine.js +312 -0
  42. package/dist/reconciliation-snapshot-builder.js +96 -0
  43. package/dist/reconciliation-types.js +1 -0
  44. package/dist/runtime-paths.js +89 -0
  45. package/dist/service-queue.js +49 -0
  46. package/dist/service-runtime.js +96 -0
  47. package/dist/service-stage-finalizer.js +348 -0
  48. package/dist/service-stage-runner.js +233 -0
  49. package/dist/service-webhook-processor.js +181 -0
  50. package/dist/service-webhooks.js +148 -0
  51. package/dist/service.js +139 -0
  52. package/dist/stage-agent-activity-publisher.js +33 -0
  53. package/dist/stage-event-ports.js +1 -0
  54. package/dist/stage-failure.js +92 -0
  55. package/dist/stage-launch.js +54 -0
  56. package/dist/stage-lifecycle-publisher.js +213 -0
  57. package/dist/stage-reporting.js +153 -0
  58. package/dist/stage-turn-input-dispatcher.js +102 -0
  59. package/dist/token-crypto.js +21 -0
  60. package/dist/types.js +5 -0
  61. package/dist/utils.js +163 -0
  62. package/dist/webhook-agent-session-handler.js +157 -0
  63. package/dist/webhook-archive.js +24 -0
  64. package/dist/webhook-comment-handler.js +89 -0
  65. package/dist/webhook-desired-stage-recorder.js +150 -0
  66. package/dist/webhook-event-ports.js +1 -0
  67. package/dist/webhook-installation-handler.js +57 -0
  68. package/dist/webhooks.js +301 -0
  69. package/dist/workflow-policy.js +42 -0
  70. package/dist/workflow-ports.js +1 -0
  71. package/dist/workflow-types.js +1 -0
  72. package/dist/worktree-manager.js +66 -0
  73. package/infra/patchrelay-reload.service +6 -0
  74. package/infra/patchrelay.path +11 -0
  75. package/infra/patchrelay.service +28 -0
  76. package/package.json +55 -0
  77. package/runtime.env.example +8 -0
  78. package/service.env.example +7 -0
@@ -0,0 +1,213 @@
1
+ import { buildAwaitingHandoffComment, buildRunningStatusComment, resolveActiveLinearState, resolveWorkflowLabelCleanup, resolveWorkflowLabelNames, } from "./linear-workflow.js";
2
+ import { sanitizeDiagnosticText } from "./utils.js";
3
+ export class StageLifecyclePublisher {
4
+ config;
5
+ stores;
6
+ linearProvider;
7
+ logger;
8
+ constructor(config, stores, linearProvider, logger) {
9
+ this.config = config;
10
+ this.stores = stores;
11
+ this.linearProvider = linearProvider;
12
+ this.logger = logger;
13
+ }
14
+ async markStageActive(project, issue, stageRun) {
15
+ const activeState = resolveActiveLinearState(project, stageRun.stage);
16
+ const linear = await this.linearProvider.forProject(stageRun.projectId);
17
+ if (!activeState || !linear) {
18
+ return;
19
+ }
20
+ await linear.setIssueState(stageRun.linearIssueId, activeState);
21
+ const labels = resolveWorkflowLabelNames(project, "working");
22
+ if (labels.add.length > 0 || labels.remove.length > 0) {
23
+ await linear.updateIssueLabels({
24
+ issueId: stageRun.linearIssueId,
25
+ ...(labels.add.length > 0 ? { addNames: labels.add } : {}),
26
+ ...(labels.remove.length > 0 ? { removeNames: labels.remove } : {}),
27
+ });
28
+ }
29
+ this.stores.issueWorkflows.upsertTrackedIssue({
30
+ projectId: stageRun.projectId,
31
+ linearIssueId: stageRun.linearIssueId,
32
+ currentLinearState: activeState,
33
+ statusCommentId: issue.statusCommentId ?? null,
34
+ lifecycleStatus: "running",
35
+ });
36
+ this.stores.issueControl.upsertIssueControl({
37
+ projectId: stageRun.projectId,
38
+ linearIssueId: stageRun.linearIssueId,
39
+ ...(issue.statusCommentId ? { serviceOwnedCommentId: issue.statusCommentId } : {}),
40
+ ...(issue.activeAgentSessionId ? { activeAgentSessionId: issue.activeAgentSessionId } : {}),
41
+ lifecycleStatus: "running",
42
+ });
43
+ }
44
+ async refreshRunningStatusComment(projectId, issueId, stageRunId, issueKey) {
45
+ const linear = await this.linearProvider.forProject(projectId);
46
+ if (!linear) {
47
+ return;
48
+ }
49
+ const issue = this.stores.issueWorkflows.getTrackedIssue(projectId, issueId);
50
+ const stageRun = this.stores.issueWorkflows.getStageRun(stageRunId);
51
+ const workspace = stageRun ? this.stores.issueWorkflows.getWorkspace(stageRun.workspaceId) : undefined;
52
+ if (!issue || !stageRun || !workspace) {
53
+ return;
54
+ }
55
+ try {
56
+ const result = await linear.upsertIssueComment({
57
+ issueId,
58
+ ...(issue.statusCommentId ? { commentId: issue.statusCommentId } : {}),
59
+ body: buildRunningStatusComment({
60
+ issue,
61
+ stageRun,
62
+ branchName: workspace.branchName,
63
+ }),
64
+ });
65
+ this.stores.issueWorkflows.setIssueStatusComment(projectId, issueId, result.id);
66
+ this.stores.issueControl.upsertIssueControl({
67
+ projectId,
68
+ linearIssueId: issueId,
69
+ serviceOwnedCommentId: result.id,
70
+ lifecycleStatus: issue.lifecycleStatus,
71
+ });
72
+ }
73
+ catch (error) {
74
+ this.logger.warn({
75
+ issueKey,
76
+ stageRunId,
77
+ issueId,
78
+ error: error instanceof Error ? error.message : String(error),
79
+ }, "Failed to refresh running status comment after stage startup");
80
+ }
81
+ }
82
+ async publishStageStarted(issue, stage) {
83
+ if (!issue.activeAgentSessionId) {
84
+ return;
85
+ }
86
+ const linear = await this.linearProvider.forProject(issue.projectId);
87
+ if (!linear) {
88
+ return;
89
+ }
90
+ try {
91
+ await linear.createAgentActivity({
92
+ agentSessionId: issue.activeAgentSessionId,
93
+ content: {
94
+ type: "action",
95
+ action: "running_workflow",
96
+ parameter: stage,
97
+ result: `PatchRelay started the ${stage} workflow.`,
98
+ },
99
+ ephemeral: true,
100
+ });
101
+ }
102
+ catch (error) {
103
+ this.logger.warn({
104
+ issueKey: issue.issueKey,
105
+ stage,
106
+ agentSessionId: issue.activeAgentSessionId,
107
+ error: error instanceof Error ? error.message : String(error),
108
+ }, "Failed to publish Linear agent activity after stage startup");
109
+ }
110
+ }
111
+ async publishStageCompletion(stageRun, enqueueIssue) {
112
+ const refreshedIssue = this.stores.issueWorkflows.getTrackedIssue(stageRun.projectId, stageRun.linearIssueId);
113
+ if (refreshedIssue?.desiredStage) {
114
+ await this.publishAgentCompletion(refreshedIssue, {
115
+ type: "thought",
116
+ body: `The ${stageRun.stage} workflow finished. PatchRelay is preparing the next requested workflow.`,
117
+ });
118
+ enqueueIssue(stageRun.projectId, stageRun.linearIssueId);
119
+ return;
120
+ }
121
+ const project = this.config.projects.find((candidate) => candidate.id === stageRun.projectId);
122
+ const activeState = project ? resolveActiveLinearState(project, stageRun.stage) : undefined;
123
+ const linear = project ? await this.linearProvider.forProject(stageRun.projectId) : undefined;
124
+ if (refreshedIssue && linear && project && activeState) {
125
+ try {
126
+ const linearIssue = await linear.getIssue(stageRun.linearIssueId);
127
+ if (linearIssue.stateName?.trim().toLowerCase() === activeState.trim().toLowerCase()) {
128
+ const labels = resolveWorkflowLabelNames(project, "awaitingHandoff");
129
+ if (labels.add.length > 0 || labels.remove.length > 0) {
130
+ await linear.updateIssueLabels({
131
+ issueId: stageRun.linearIssueId,
132
+ ...(labels.add.length > 0 ? { addNames: labels.add } : {}),
133
+ ...(labels.remove.length > 0 ? { removeNames: labels.remove } : {}),
134
+ });
135
+ }
136
+ this.stores.issueWorkflows.setIssueLifecycleStatus(stageRun.projectId, stageRun.linearIssueId, "paused");
137
+ this.stores.issueControl.upsertIssueControl({
138
+ projectId: stageRun.projectId,
139
+ linearIssueId: stageRun.linearIssueId,
140
+ lifecycleStatus: "paused",
141
+ });
142
+ const finalStageRun = this.stores.issueWorkflows.getStageRun(stageRun.id) ?? stageRun;
143
+ const result = await linear.upsertIssueComment({
144
+ issueId: stageRun.linearIssueId,
145
+ ...(refreshedIssue.statusCommentId ? { commentId: refreshedIssue.statusCommentId } : {}),
146
+ body: buildAwaitingHandoffComment({
147
+ issue: refreshedIssue,
148
+ stageRun: finalStageRun,
149
+ activeState,
150
+ }),
151
+ });
152
+ this.stores.issueWorkflows.setIssueStatusComment(stageRun.projectId, stageRun.linearIssueId, result.id);
153
+ this.stores.issueControl.upsertIssueControl({
154
+ projectId: stageRun.projectId,
155
+ linearIssueId: stageRun.linearIssueId,
156
+ serviceOwnedCommentId: result.id,
157
+ lifecycleStatus: "paused",
158
+ });
159
+ await this.publishAgentCompletion(refreshedIssue, {
160
+ type: "elicitation",
161
+ body: `PatchRelay finished the ${stageRun.stage} workflow. Move the issue to its next workflow state or leave a follow-up prompt to continue.`,
162
+ });
163
+ return;
164
+ }
165
+ const cleanup = resolveWorkflowLabelCleanup(project);
166
+ if (cleanup.remove.length > 0) {
167
+ await linear.updateIssueLabels({
168
+ issueId: stageRun.linearIssueId,
169
+ removeNames: cleanup.remove,
170
+ });
171
+ }
172
+ }
173
+ catch (error) {
174
+ this.logger.warn({
175
+ issueKey: refreshedIssue.issueKey,
176
+ issueId: stageRun.linearIssueId,
177
+ stageRunId: stageRun.id,
178
+ stage: stageRun.stage,
179
+ error: sanitizeDiagnosticText(error instanceof Error ? error.message : String(error)),
180
+ }, "Stage completed locally but PatchRelay could not finish the final Linear sync");
181
+ }
182
+ }
183
+ if (refreshedIssue) {
184
+ await this.publishAgentCompletion(refreshedIssue, {
185
+ type: "response",
186
+ body: `PatchRelay finished the ${stageRun.stage} workflow.`,
187
+ });
188
+ }
189
+ }
190
+ async publishAgentCompletion(issue, content) {
191
+ if (!issue.activeAgentSessionId) {
192
+ return;
193
+ }
194
+ const linear = await this.linearProvider.forProject(issue.projectId);
195
+ if (!linear) {
196
+ return;
197
+ }
198
+ await linear
199
+ .createAgentActivity({
200
+ agentSessionId: issue.activeAgentSessionId,
201
+ content,
202
+ })
203
+ .catch((error) => {
204
+ this.logger.warn({
205
+ issueKey: issue.issueKey,
206
+ issueId: issue.linearIssueId,
207
+ agentSessionId: issue.activeAgentSessionId,
208
+ activityType: content.type,
209
+ error: sanitizeDiagnosticText(error instanceof Error ? error.message : String(error)),
210
+ }, "Failed to publish Linear agent activity");
211
+ });
212
+ }
213
+ }
@@ -0,0 +1,153 @@
1
+ export function extractStageSummary(report) {
2
+ return {
3
+ assistantMessageCount: report.assistantMessages.length,
4
+ commandCount: report.commands.length,
5
+ fileChangeCount: report.fileChanges.length,
6
+ toolCallCount: report.toolCalls.length,
7
+ latestAssistantMessage: report.assistantMessages.at(-1) ?? null,
8
+ };
9
+ }
10
+ export function summarizeCurrentThread(thread) {
11
+ const latestTurn = thread.turns.at(-1);
12
+ const latestAgentMessage = latestTurn?.items
13
+ .filter((item) => item.type === "agentMessage")
14
+ .at(-1)?.text;
15
+ return {
16
+ threadId: thread.id,
17
+ threadStatus: thread.status,
18
+ ...(latestTurn ? { latestTurnId: latestTurn.id, latestTurnStatus: latestTurn.status } : {}),
19
+ ...(latestAgentMessage ? { latestAgentMessage } : {}),
20
+ };
21
+ }
22
+ export function buildStageReport(stageRun, issue, thread, eventCounts) {
23
+ const assistantMessages = [];
24
+ const plans = [];
25
+ const reasoning = [];
26
+ const commands = [];
27
+ const fileChanges = [];
28
+ const toolCalls = [];
29
+ for (const turn of thread.turns) {
30
+ for (const rawItem of turn.items) {
31
+ const item = rawItem;
32
+ if (item.type === "agentMessage" && typeof item.text === "string") {
33
+ assistantMessages.push(item.text);
34
+ }
35
+ else if (item.type === "plan" && typeof item.text === "string") {
36
+ plans.push(item.text);
37
+ }
38
+ else if (item.type === "reasoning" && Array.isArray(item.summary) && Array.isArray(item.content)) {
39
+ reasoning.push(...item.summary, ...item.content);
40
+ }
41
+ else if (item.type === "commandExecution" && typeof item.command === "string" && typeof item.cwd === "string") {
42
+ commands.push({
43
+ command: item.command,
44
+ cwd: item.cwd,
45
+ status: typeof item.status === "string" ? item.status : "unknown",
46
+ ...(typeof item.exitCode === "number" || item.exitCode === null
47
+ ? { exitCode: item.exitCode }
48
+ : {}),
49
+ ...(typeof item.durationMs === "number" || item.durationMs === null
50
+ ? { durationMs: item.durationMs }
51
+ : {}),
52
+ });
53
+ }
54
+ else if (item.type === "fileChange" && Array.isArray(item.changes)) {
55
+ fileChanges.push(...item.changes);
56
+ }
57
+ else if (item.type === "mcpToolCall" && typeof item.server === "string" && typeof item.tool === "string") {
58
+ toolCalls.push({
59
+ type: "mcp",
60
+ name: `${item.server}/${item.tool}`,
61
+ status: typeof item.status === "string" ? item.status : "unknown",
62
+ ...(typeof item.durationMs === "number" || item.durationMs === null
63
+ ? { durationMs: item.durationMs }
64
+ : {}),
65
+ });
66
+ }
67
+ else if (item.type === "dynamicToolCall" && typeof item.tool === "string") {
68
+ toolCalls.push({
69
+ type: "dynamic",
70
+ name: item.tool,
71
+ status: typeof item.status === "string" ? item.status : "unknown",
72
+ ...(typeof item.durationMs === "number" || item.durationMs === null
73
+ ? { durationMs: item.durationMs }
74
+ : {}),
75
+ });
76
+ }
77
+ }
78
+ }
79
+ return {
80
+ ...(issue.issueKey ? { issueKey: issue.issueKey } : {}),
81
+ stage: stageRun.stage,
82
+ status: stageRun.status,
83
+ ...(stageRun.threadId ? { threadId: stageRun.threadId } : {}),
84
+ ...(stageRun.parentThreadId ? { parentThreadId: stageRun.parentThreadId } : {}),
85
+ ...(stageRun.turnId ? { turnId: stageRun.turnId } : {}),
86
+ prompt: stageRun.promptText,
87
+ workflowFile: stageRun.workflowFile,
88
+ assistantMessages,
89
+ plans,
90
+ reasoning,
91
+ commands,
92
+ fileChanges,
93
+ toolCalls,
94
+ eventCounts,
95
+ };
96
+ }
97
+ export function buildFailedStageReport(stageRun, status, options) {
98
+ return {
99
+ stage: stageRun.stage,
100
+ status,
101
+ ...(options?.threadId ? { threadId: options.threadId } : {}),
102
+ ...(options?.turnId ? { turnId: options.turnId } : {}),
103
+ prompt: stageRun.promptText,
104
+ workflowFile: stageRun.workflowFile,
105
+ assistantMessages: [],
106
+ plans: [],
107
+ reasoning: [],
108
+ commands: [],
109
+ fileChanges: [],
110
+ toolCalls: [],
111
+ eventCounts: {},
112
+ };
113
+ }
114
+ export function countEventMethods(events) {
115
+ return events.reduce((counts, event) => {
116
+ counts[event.method] = (counts[event.method] ?? 0) + 1;
117
+ return counts;
118
+ }, {});
119
+ }
120
+ export function resolveStageRunStatus(params) {
121
+ const turn = params.turn;
122
+ if (!turn || typeof turn !== "object") {
123
+ return "failed";
124
+ }
125
+ const status = String(turn.status ?? "failed");
126
+ return status === "completed" ? "completed" : "failed";
127
+ }
128
+ export function extractTurnId(params) {
129
+ const turn = params.turn;
130
+ if (!turn || typeof turn !== "object") {
131
+ return undefined;
132
+ }
133
+ const id = turn.id;
134
+ return typeof id === "string" ? id : undefined;
135
+ }
136
+ export function buildPendingMaterializationThread(stageRun, error) {
137
+ return {
138
+ id: stageRun.threadId ?? "pending-thread",
139
+ preview: "",
140
+ cwd: "",
141
+ status: "pending-materialization",
142
+ turns: [
143
+ {
144
+ id: stageRun.turnId ?? "pending-turn",
145
+ status: "inProgress",
146
+ error: {
147
+ message: error.message,
148
+ },
149
+ items: [],
150
+ },
151
+ ],
152
+ };
153
+ }
@@ -0,0 +1,102 @@
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 };
28
+ }
29
+ const issueControl = this.inputs.issueControl.getIssueControl(stageRun.projectId, stageRun.linearIssueId);
30
+ if (!issueControl?.activeRunLeaseId) {
31
+ return { deliveredInputIds: [], deliveredObligationIds: [], deliveredCount: 0 };
32
+ }
33
+ const deliveredInputIds = [];
34
+ const deliveredObligationIds = [];
35
+ let deliveredCount = 0;
36
+ const obligationQuery = options?.retryInProgress ? { includeInProgress: true } : undefined;
37
+ for (const obligation of this.listPendingInputObligations(stageRun.projectId, stageRun.linearIssueId, issueControl.activeRunLeaseId, obligationQuery)) {
38
+ const payload = safeJsonParse(obligation.payloadJson);
39
+ const body = payload?.body?.trim();
40
+ if (!body) {
41
+ this.inputs.obligations.markObligationStatus(obligation.id, "failed", "obligation payload had no deliverable body");
42
+ continue;
43
+ }
44
+ const claimed = obligation.status === "in_progress" && options?.retryInProgress
45
+ ? true
46
+ : this.inputs.obligations.claimPendingObligation(obligation.id, {
47
+ runLeaseId: issueControl.activeRunLeaseId,
48
+ threadId: stageRun.threadId,
49
+ turnId: stageRun.turnId,
50
+ });
51
+ if (!claimed) {
52
+ continue;
53
+ }
54
+ try {
55
+ if (obligation.status === "in_progress") {
56
+ this.inputs.obligations.updateObligationRouting(obligation.id, {
57
+ runLeaseId: issueControl.activeRunLeaseId,
58
+ threadId: stageRun.threadId,
59
+ turnId: stageRun.turnId,
60
+ });
61
+ }
62
+ await this.codex.steerTurn({
63
+ threadId: stageRun.threadId,
64
+ turnId: stageRun.turnId,
65
+ input: body,
66
+ });
67
+ deliveredObligationIds.push(obligation.id);
68
+ this.inputs.obligations.markObligationStatus(obligation.id, "completed");
69
+ deliveredCount += 1;
70
+ this.logger.debug({
71
+ threadId: stageRun.threadId,
72
+ turnId: stageRun.turnId,
73
+ obligationId: obligation.id,
74
+ source: obligation.source,
75
+ }, "Delivered queued turn input to Codex");
76
+ }
77
+ catch (error) {
78
+ this.inputs.obligations.markObligationStatus(obligation.id, "pending", error instanceof Error ? error.message : String(error));
79
+ this.logger.warn({
80
+ issueKey: options?.issueKey,
81
+ threadId: stageRun.threadId,
82
+ turnId: stageRun.turnId,
83
+ obligationId: obligation.id,
84
+ source: obligation.source,
85
+ error: sanitizeDiagnosticText(error instanceof Error ? error.message : String(error)),
86
+ }, options?.failureMessage ?? "Failed to deliver queued turn input");
87
+ break;
88
+ }
89
+ }
90
+ return { deliveredInputIds, deliveredObligationIds, deliveredCount };
91
+ }
92
+ listPendingInputObligations(projectId, linearIssueId, activeRunLeaseId, options) {
93
+ const query = options?.includeInProgress
94
+ ? { kind: "deliver_turn_input", includeInProgress: true }
95
+ : { kind: "deliver_turn_input" };
96
+ return this.inputs.obligations
97
+ .listPendingObligations(query)
98
+ .filter((obligation) => obligation.projectId === projectId &&
99
+ obligation.linearIssueId === linearIssueId &&
100
+ (obligation.runLeaseId === undefined || obligation.runLeaseId === activeRunLeaseId));
101
+ }
102
+ }
@@ -0,0 +1,21 @@
1
+ import crypto from "node:crypto";
2
+ function deriveKey(secret) {
3
+ return crypto.createHash("sha256").update(secret, "utf8").digest();
4
+ }
5
+ export function encryptSecret(value, secret) {
6
+ const iv = crypto.randomBytes(12);
7
+ const key = deriveKey(secret);
8
+ const cipher = crypto.createCipheriv("aes-256-gcm", key, iv);
9
+ const ciphertext = Buffer.concat([cipher.update(value, "utf8"), cipher.final()]);
10
+ const tag = cipher.getAuthTag();
11
+ return Buffer.concat([iv, tag, ciphertext]).toString("base64");
12
+ }
13
+ export function decryptSecret(ciphertext, secret) {
14
+ const payload = Buffer.from(ciphertext, "base64");
15
+ const iv = payload.subarray(0, 12);
16
+ const tag = payload.subarray(12, 28);
17
+ const encrypted = payload.subarray(28);
18
+ const decipher = crypto.createDecipheriv("aes-256-gcm", deriveKey(secret), iv);
19
+ decipher.setAuthTag(tag);
20
+ return Buffer.concat([decipher.update(encrypted), decipher.final()]).toString("utf8");
21
+ }
package/dist/types.js ADDED
@@ -0,0 +1,5 @@
1
+ export * from "./config-types.js";
2
+ export * from "./workflow-types.js";
3
+ export * from "./linear-types.js";
4
+ export * from "./db-types.js";
5
+ export * from "./codex-types.js";
package/dist/utils.js ADDED
@@ -0,0 +1,163 @@
1
+ import crypto from "node:crypto";
2
+ import { mkdir } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { execFile } from "node:child_process";
5
+ const REDACTED_HEADER_NAMES = new Set(["authorization", "cookie", "set-cookie", "linear-signature"]);
6
+ const DIAGNOSTIC_REPLACEMENTS = [
7
+ [/\bBearer\s+[A-Za-z0-9._~+/=-]+\b/gi, "Bearer [redacted]"],
8
+ [/\b(access[_-]?token|refresh[_-]?token|client[_-]?secret|webhook[_-]?secret|api[_-]?key|password|tokenEncryptionKey|bearerToken|secret)=([^\s&]+)/gi, "$1=[redacted]"],
9
+ [/"(access_token|refresh_token|client_secret|accessToken|refreshToken|clientSecret|webhookSecret|apiKey|password|tokenEncryptionKey|bearerToken|secret)"\s*:\s*"[^"]*"/g, "\"$1\":\"[redacted]\""],
10
+ ];
11
+ export function ensureAbsolutePath(inputPath) {
12
+ return path.isAbsolute(inputPath) ? inputPath : path.resolve(process.cwd(), inputPath);
13
+ }
14
+ export async function ensureDir(dirPath) {
15
+ await mkdir(dirPath, { recursive: true });
16
+ }
17
+ export function timestampMsWithinSkew(timestampMs, maxSkewSeconds) {
18
+ return Math.abs(Date.now() - timestampMs) <= maxSkewSeconds * 1000;
19
+ }
20
+ export function verifyHmacSha256Hex(rawBody, secret, providedHex) {
21
+ if (!providedHex) {
22
+ return false;
23
+ }
24
+ const normalized = providedHex.trim().toLowerCase();
25
+ if (!/^[0-9a-f]+$/.test(normalized) || normalized.length !== 64) {
26
+ return false;
27
+ }
28
+ const expected = crypto.createHmac("sha256", secret).update(rawBody).digest();
29
+ const provided = Buffer.from(normalized, "hex");
30
+ return crypto.timingSafeEqual(expected, provided);
31
+ }
32
+ export function interpolateTemplate(input, context) {
33
+ return input.replace(/\{([a-zA-Z0-9_]+)\}/g, (_match, key) => context[key] ?? "");
34
+ }
35
+ export function interpolateTemplateArray(input, context) {
36
+ return input.map((value) => interpolateTemplate(value, context));
37
+ }
38
+ export async function execCommand(command, args, options = {}) {
39
+ return new Promise((resolve, reject) => {
40
+ execFile(command, args, {
41
+ cwd: options.cwd,
42
+ env: options.env,
43
+ timeout: options.timeoutMs,
44
+ killSignal: "SIGTERM",
45
+ encoding: "utf8",
46
+ maxBuffer: 10 * 1024 * 1024,
47
+ ...(options.stdio ? { stdio: options.stdio } : {}),
48
+ }, (error, stdout, stderr) => {
49
+ if (error) {
50
+ const timeoutError = typeof error === "object" &&
51
+ error !== null &&
52
+ "killed" in error &&
53
+ "signal" in error &&
54
+ error.killed === true &&
55
+ error.signal === "SIGTERM" &&
56
+ options.timeoutMs;
57
+ if (timeoutError) {
58
+ reject(new Error(`Command timed out after ${options.timeoutMs}ms: ${command}`));
59
+ return;
60
+ }
61
+ const rawCode = typeof error === "object" && error !== null && "code" in error ? error.code : undefined;
62
+ if (typeof rawCode === "string" && !/^-?\d+$/.test(rawCode)) {
63
+ reject(error);
64
+ return;
65
+ }
66
+ const exitCode = typeof rawCode === "number" ? rawCode : typeof rawCode === "string" ? Number(rawCode) : 1;
67
+ resolve({
68
+ stdout: typeof stdout === "string" ? stdout : "",
69
+ stderr: typeof stderr === "string" ? stderr : "",
70
+ exitCode,
71
+ });
72
+ return;
73
+ }
74
+ resolve({
75
+ stdout: typeof stdout === "string" ? stdout : "",
76
+ stderr: typeof stderr === "string" ? stderr : "",
77
+ exitCode: 0,
78
+ });
79
+ });
80
+ });
81
+ }
82
+ export function safeJsonParse(value) {
83
+ try {
84
+ return JSON.parse(value);
85
+ }
86
+ catch {
87
+ return undefined;
88
+ }
89
+ }
90
+ export function redactSensitiveHeaders(headers) {
91
+ return Object.fromEntries(Object.entries(headers).map(([name, value]) => [name, REDACTED_HEADER_NAMES.has(name.toLowerCase()) ? "[redacted]" : value]));
92
+ }
93
+ export function sanitizeDiagnosticText(text, maxLength = 500) {
94
+ let sanitized = text;
95
+ for (const [pattern, replacement] of DIAGNOSTIC_REPLACEMENTS) {
96
+ sanitized = sanitized.replace(pattern, replacement);
97
+ }
98
+ if (sanitized.length <= maxLength) {
99
+ return sanitized;
100
+ }
101
+ return `${sanitized.slice(0, Math.max(0, maxLength - 12))}[truncated]`;
102
+ }
103
+ export function encryptSecret(plaintext, keyMaterial) {
104
+ const key = crypto.createHash("sha256").update(keyMaterial).digest();
105
+ const iv = crypto.randomBytes(12);
106
+ const cipher = crypto.createCipheriv("aes-256-gcm", key, iv);
107
+ const ciphertext = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
108
+ const tag = cipher.getAuthTag();
109
+ return JSON.stringify({
110
+ iv: iv.toString("base64"),
111
+ tag: tag.toString("base64"),
112
+ ciphertext: ciphertext.toString("base64"),
113
+ });
114
+ }
115
+ export function decryptSecret(payload, keyMaterial) {
116
+ const parsed = JSON.parse(payload);
117
+ const key = crypto.createHash("sha256").update(keyMaterial).digest();
118
+ const decipher = crypto.createDecipheriv("aes-256-gcm", key, Buffer.from(parsed.iv, "base64"));
119
+ decipher.setAuthTag(Buffer.from(parsed.tag, "base64"));
120
+ const plaintext = Buffer.concat([
121
+ decipher.update(Buffer.from(parsed.ciphertext, "base64")),
122
+ decipher.final(),
123
+ ]);
124
+ return plaintext.toString("utf8");
125
+ }
126
+ export function extractFirstJsonObject(text) {
127
+ const start = text.indexOf("{");
128
+ if (start === -1) {
129
+ return undefined;
130
+ }
131
+ let depth = 0;
132
+ let inString = false;
133
+ let escaped = false;
134
+ for (let index = start; index < text.length; index += 1) {
135
+ const char = text[index];
136
+ if (inString) {
137
+ if (escaped) {
138
+ escaped = false;
139
+ }
140
+ else if (char === "\\") {
141
+ escaped = true;
142
+ }
143
+ else if (char === "\"") {
144
+ inString = false;
145
+ }
146
+ continue;
147
+ }
148
+ if (char === "\"") {
149
+ inString = true;
150
+ continue;
151
+ }
152
+ if (char === "{") {
153
+ depth += 1;
154
+ }
155
+ else if (char === "}") {
156
+ depth -= 1;
157
+ if (depth === 0) {
158
+ return text.slice(start, index + 1);
159
+ }
160
+ }
161
+ }
162
+ return undefined;
163
+ }