cyrus-edge-worker 0.2.1 → 0.2.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.
@@ -2,12 +2,12 @@ import { EventEmitter } from "node:events";
2
2
  import { mkdir, readdir, readFile, rename, writeFile } from "node:fs/promises";
3
3
  import { basename, dirname, extname, join, resolve } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
- import { LinearClient, } from "@linear/sdk";
5
+ import { LinearClient } from "@linear/sdk";
6
6
  import { watch as chokidarWatch } from "chokidar";
7
7
  import { AbortError, ClaudeRunner, createCyrusToolsServer, createImageToolsServer, createSoraToolsServer, getAllTools, getCoordinatorTools, getReadOnlyTools, getSafeTools, } from "cyrus-claude-runner";
8
8
  import { ConfigUpdater } from "cyrus-config-updater";
9
9
  import { DEFAULT_PROXY_URL, isAgentSessionCreatedWebhook, isAgentSessionPromptedWebhook, isIssueAssignedWebhook, isIssueCommentMentionWebhook, isIssueNewCommentWebhook, isIssueUnassignedWebhook, PersistenceManager, resolvePath, } from "cyrus-core";
10
- import { LinearEventTransport } from "cyrus-linear-event-transport";
10
+ import { LinearEventTransport, LinearIssueTrackerService, } from "cyrus-linear-event-transport";
11
11
  import { fileTypeFromBuffer } from "file-type";
12
12
  import { AgentSessionManager } from "./AgentSessionManager.js";
13
13
  import { ProcedureRouter, } from "./procedures/index.js";
@@ -23,7 +23,7 @@ export class EdgeWorker extends EventEmitter {
23
23
  config;
24
24
  repositories = new Map(); // repository 'id' (internal, stored in config.json) mapped to the full repo config
25
25
  agentSessionManagers = new Map(); // Maps repository ID to AgentSessionManager, which manages ClaudeRunners for a repo
26
- linearClients = new Map(); // one linear client per 'repository'
26
+ issueTrackers = new Map(); // one issue tracker per 'repository'
27
27
  linearEventTransport = null; // Single event transport for webhook delivery
28
28
  configUpdater = null; // Single config updater for configuration updates
29
29
  persistenceManager;
@@ -49,20 +49,16 @@ export class EdgeWorker extends EventEmitter {
49
49
  // Initialize repository router with dependencies
50
50
  const repositoryRouterDeps = {
51
51
  fetchIssueLabels: async (issueId, workspaceId) => {
52
- // Get Linear client for this workspace
53
- const linearClient = this.getLinearClientForWorkspace(workspaceId);
54
- if (!linearClient) {
55
- console.warn(`[EdgeWorker] No Linear client found for workspace ${workspaceId}`);
52
+ // Find repository for this workspace
53
+ const repo = Array.from(this.repositories.values()).find((r) => r.linearWorkspaceId === workspaceId);
54
+ if (!repo)
56
55
  return [];
57
- }
58
- try {
59
- const issue = await linearClient.issue(issueId);
60
- return await this.fetchIssueLabels(issue);
61
- }
62
- catch (error) {
63
- console.error(`[EdgeWorker] Failed to fetch issue labels for ${issueId}:`, error);
56
+ // Get issue tracker for this repository
57
+ const issueTracker = this.issueTrackers.get(repo.id);
58
+ if (!issueTracker)
64
59
  return [];
65
- }
60
+ // Use platform-agnostic getIssueLabels method
61
+ return await issueTracker.getIssueLabels(issueId);
66
62
  },
67
63
  hasActiveSession: (issueId, repositoryId) => {
68
64
  const sessionManager = this.agentSessionManagers.get(repositoryId);
@@ -71,8 +67,8 @@ export class EdgeWorker extends EventEmitter {
71
67
  const activeSessions = sessionManager.getActiveSessionsByIssueId(issueId);
72
68
  return activeSessions.length > 0;
73
69
  },
74
- getLinearClient: (workspaceId) => {
75
- return this.getLinearClientForWorkspace(workspaceId);
70
+ getIssueTracker: (workspaceId) => {
71
+ return this.getIssueTrackerForWorkspace(workspaceId);
76
72
  },
77
73
  };
78
74
  this.repositoryRouter = new RepositoryRouter(repositoryRouterDeps);
@@ -103,11 +99,12 @@ export class EdgeWorker extends EventEmitter {
103
99
  : undefined,
104
100
  };
105
101
  this.repositories.set(repo.id, resolvedRepo);
106
- // Create Linear client for this repository's workspace
102
+ // Create issue tracker for this repository's workspace
107
103
  const linearClient = new LinearClient({
108
104
  accessToken: repo.linearToken,
109
105
  });
110
- this.linearClients.set(repo.id, linearClient);
106
+ const issueTracker = new LinearIssueTrackerService(linearClient);
107
+ this.issueTrackers.set(repo.id, issueTracker);
111
108
  // Create AgentSessionManager for this repository with parent session lookup and resume callback
112
109
  //
113
110
  // Note: This pattern works (despite appearing recursive) because:
@@ -118,7 +115,7 @@ export class EdgeWorker extends EventEmitter {
118
115
  //
119
116
  // This allows the AgentSessionManager to call back into itself to access its own sessions,
120
117
  // enabling child sessions to trigger parent session resumption using the same manager instance.
121
- const agentSessionManager = new AgentSessionManager(linearClient, (childSessionId) => {
118
+ const agentSessionManager = new AgentSessionManager(issueTracker, (childSessionId) => {
122
119
  console.log(`[Parent-Child Lookup] Looking up parent session for child ${childSessionId}`);
123
120
  const parentId = this.childToParentAgentSession.get(childSessionId);
124
121
  console.log(`[Parent-Child Lookup] Child ${childSessionId} -> Parent ${parentId || "not found"}`);
@@ -208,10 +205,10 @@ export class EdgeWorker extends EventEmitter {
208
205
  secret,
209
206
  });
210
207
  // Listen for webhook events
211
- this.linearEventTransport.on("webhook", (payload) => {
208
+ this.linearEventTransport.on("event", (event) => {
212
209
  // Get all active repositories for webhook handling
213
210
  const repos = Array.from(this.repositories.values());
214
- this.handleWebhook(payload, repos);
211
+ this.handleWebhook(event, repos);
215
212
  });
216
213
  // Listen for errors
217
214
  this.linearEventTransport.on("error", (error) => {
@@ -301,12 +298,12 @@ export class EdgeWorker extends EventEmitter {
301
298
  }
302
299
  await this.postParentResumeAcknowledgment(parentSessionId, repo.id);
303
300
  // Post thought to Linear showing child result receipt
304
- const linearClient = this.linearClients.get(repo.id);
305
- if (linearClient && childSession) {
301
+ const issueTracker = this.issueTrackers.get(repo.id);
302
+ if (issueTracker && childSession) {
306
303
  const childIssueIdentifier = childSession.issue?.identifier || childSession.issueId;
307
304
  const resultThought = `Received result from sub-issue ${childIssueIdentifier}:\n\n---\n\n${prompt}\n\n---`;
308
305
  try {
309
- const result = await linearClient.createAgentActivity({
306
+ const result = await issueTracker.createAgentActivity({
310
307
  agentSessionId: parentSessionId,
311
308
  content: {
312
309
  type: "thought",
@@ -502,13 +499,14 @@ export class EdgeWorker extends EventEmitter {
502
499
  };
503
500
  // Add to internal map
504
501
  this.repositories.set(repo.id, resolvedRepo);
505
- // Create Linear client
502
+ // Create issue tracker
506
503
  const linearClient = new LinearClient({
507
504
  accessToken: repo.linearToken,
508
505
  });
509
- this.linearClients.set(repo.id, linearClient);
506
+ const issueTracker = new LinearIssueTrackerService(linearClient);
507
+ this.issueTrackers.set(repo.id, issueTracker);
510
508
  // Create AgentSessionManager with same pattern as constructor
511
- const agentSessionManager = new AgentSessionManager(linearClient, (childSessionId) => {
509
+ const agentSessionManager = new AgentSessionManager(issueTracker, (childSessionId) => {
512
510
  return this.childToParentAgentSession.get(childSessionId);
513
511
  }, async (parentSessionId, prompt, childSessionId) => {
514
512
  await this.handleResumeParentSession(parentSessionId, prompt, childSessionId, repo, agentSessionManager);
@@ -553,13 +551,14 @@ export class EdgeWorker extends EventEmitter {
553
551
  };
554
552
  // Update stored config
555
553
  this.repositories.set(repo.id, resolvedRepo);
556
- // If token changed, recreate Linear client
554
+ // If token changed, recreate issue tracker
557
555
  if (oldRepo.linearToken !== repo.linearToken) {
558
- console.log(` 🔑 Token changed, recreating Linear client`);
556
+ console.log(` 🔑 Token changed, recreating issue tracker`);
559
557
  const linearClient = new LinearClient({
560
558
  accessToken: repo.linearToken,
561
559
  });
562
- this.linearClients.set(repo.id, linearClient);
560
+ const issueTracker = new LinearIssueTrackerService(linearClient);
561
+ this.issueTrackers.set(repo.id, issueTracker);
563
562
  }
564
563
  // If active status changed
565
564
  if (oldRepo.isActive !== repo.isActive) {
@@ -601,9 +600,9 @@ export class EdgeWorker extends EventEmitter {
601
600
  console.log(` ✅ Stopped Claude runner for session ${session.linearAgentActivitySessionId}`);
602
601
  }
603
602
  // Post cancellation message to Linear
604
- const linearClient = this.linearClients.get(repo.id);
605
- if (linearClient) {
606
- await linearClient.createAgentActivity({
603
+ const issueTracker = this.issueTrackers.get(repo.id);
604
+ if (issueTracker) {
605
+ await issueTracker.createAgentActivity({
607
606
  agentSessionId: session.linearAgentActivitySessionId,
608
607
  content: {
609
608
  type: "response",
@@ -620,7 +619,7 @@ export class EdgeWorker extends EventEmitter {
620
619
  }
621
620
  // Remove repository from all maps
622
621
  this.repositories.delete(repo.id);
623
- this.linearClients.delete(repo.id);
622
+ this.issueTrackers.delete(repo.id);
624
623
  this.agentSessionManagers.delete(repo.id);
625
624
  console.log(`✅ Repository removed successfully: ${repo.name}`);
626
625
  }
@@ -688,6 +687,10 @@ export class EdgeWorker extends EventEmitter {
688
687
  * Handle issue unassignment webhook
689
688
  */
690
689
  async handleIssueUnassignedWebhook(webhook) {
690
+ if (!webhook.notification.issue) {
691
+ console.warn("[EdgeWorker] Received issue unassignment webhook without issue");
692
+ return;
693
+ }
691
694
  const issueId = webhook.notification.issue.id;
692
695
  // Get cached repository (unassignment should only happen on issues with active sessions)
693
696
  const repository = this.getCachedRepository(issueId);
@@ -703,12 +706,12 @@ export class EdgeWorker extends EventEmitter {
703
706
  await this.handleIssueUnassigned(webhook.notification.issue, repository);
704
707
  }
705
708
  /**
706
- * Get Linear client for a workspace by finding first repository with that workspace ID
709
+ * Get issue tracker for a workspace by finding first repository with that workspace ID
707
710
  */
708
- getLinearClientForWorkspace(workspaceId) {
711
+ getIssueTrackerForWorkspace(workspaceId) {
709
712
  for (const [repoId, repo] of this.repositories) {
710
713
  if (repo.linearWorkspaceId === workspaceId) {
711
- return this.linearClients.get(repoId);
714
+ return this.issueTrackers.get(repoId);
712
715
  }
713
716
  }
714
717
  return undefined;
@@ -811,6 +814,10 @@ export class EdgeWorker extends EventEmitter {
811
814
  // Post agent activity showing auto-matched routing
812
815
  await this.postRepositorySelectionActivity(webhook.agentSession.id, repository.id, repository.name, routingMethod);
813
816
  }
817
+ if (!webhook.agentSession.issue) {
818
+ console.warn("[EdgeWorker] Agent session created webhook missing issue");
819
+ return;
820
+ }
814
821
  console.log(`[EdgeWorker] Handling agent session created: ${webhook.agentSession.issue.identifier}`);
815
822
  const { agentSession, guidance } = webhook;
816
823
  const commentBody = agentSession.comment?.body;
@@ -832,6 +839,10 @@ export class EdgeWorker extends EventEmitter {
832
839
  async initializeClaudeRunner(agentSession, repository, guidance, commentBody) {
833
840
  const linearAgentActivitySessionId = agentSession.id;
834
841
  const { issue } = agentSession;
842
+ if (!issue) {
843
+ console.warn("[EdgeWorker] Cannot initialize Claude runner without issue");
844
+ return;
845
+ }
835
846
  // Log guidance if present
836
847
  if (guidance && guidance.length > 0) {
837
848
  console.log(`[EdgeWorker] Agent guidance received: ${guidance.length} rule(s)`);
@@ -931,7 +942,7 @@ export class EdgeWorker extends EventEmitter {
931
942
  repository,
932
943
  userComment: commentBody || "", // Empty for delegation, present for mentions
933
944
  attachmentManifest: attachmentResult.manifest,
934
- guidance,
945
+ guidance: guidance || undefined,
935
946
  agentSession,
936
947
  labels,
937
948
  isNewSession: true,
@@ -1027,7 +1038,7 @@ export class EdgeWorker extends EventEmitter {
1027
1038
  console.log(`[EdgeWorker] Stopped Claude session for agent activity session ${agentSessionId}`);
1028
1039
  }
1029
1040
  // Post confirmation
1030
- const issueTitle = issue.title || "this issue";
1041
+ const issueTitle = issue?.title || "this issue";
1031
1042
  const stopConfirmation = `I've stopped working on ${issueTitle} as requested.\n\n**Stop Signal:** Received from ${webhook.agentSession.creator?.name || "user"}\n**Action Taken:** All ongoing work has been halted`;
1032
1043
  await foundManager.createResponseActivity(agentSessionId, stopConfirmation);
1033
1044
  }
@@ -1043,6 +1054,14 @@ export class EdgeWorker extends EventEmitter {
1043
1054
  const { agentSession, agentActivity, guidance } = webhook;
1044
1055
  const commentBody = agentSession.comment?.body;
1045
1056
  const agentSessionId = agentSession.id;
1057
+ if (!agentActivity) {
1058
+ console.warn("[EdgeWorker] Cannot handle repository selection without agentActivity");
1059
+ return;
1060
+ }
1061
+ if (!agentSession.issue) {
1062
+ console.warn("[EdgeWorker] Cannot handle repository selection without issue");
1063
+ return;
1064
+ }
1046
1065
  const userMessage = agentActivity.content.body;
1047
1066
  console.log(`[EdgeWorker] Processing repository selection response: "${userMessage}"`);
1048
1067
  // Get the selected repository (or fallback)
@@ -1068,6 +1087,14 @@ export class EdgeWorker extends EventEmitter {
1068
1087
  const { agentSession } = webhook;
1069
1088
  const linearAgentActivitySessionId = agentSession.id;
1070
1089
  const { issue } = agentSession;
1090
+ if (!issue) {
1091
+ console.warn("[EdgeWorker] Cannot handle prompted activity without issue");
1092
+ return;
1093
+ }
1094
+ if (!webhook.agentActivity) {
1095
+ console.warn("[EdgeWorker] Cannot handle prompted activity without agentActivity");
1096
+ return;
1097
+ }
1071
1098
  const commentId = webhook.agentActivity.sourceCommentId;
1072
1099
  // Initialize the agent session in AgentSessionManager
1073
1100
  const agentSessionManager = this.agentSessionManagers.get(repository.id);
@@ -1101,10 +1128,10 @@ export class EdgeWorker extends EventEmitter {
1101
1128
  const isCurrentlyStreaming = session?.claudeRunner?.isStreaming() || false;
1102
1129
  await this.postInstantPromptedAcknowledgment(linearAgentActivitySessionId, repository.id, isCurrentlyStreaming);
1103
1130
  // Need to fetch full issue for routing context
1104
- const linearClient = this.linearClients.get(repository.id);
1105
- if (linearClient) {
1131
+ const issueTracker = this.issueTrackers.get(repository.id);
1132
+ if (issueTracker) {
1106
1133
  try {
1107
- fullIssue = await linearClient.issue(issue.id);
1134
+ fullIssue = await issueTracker.fetchIssue(issue.id);
1108
1135
  }
1109
1136
  catch (error) {
1110
1137
  console.warn(`[EdgeWorker] Failed to fetch full issue for routing: ${issue.id}`, error);
@@ -1120,10 +1147,10 @@ export class EdgeWorker extends EventEmitter {
1120
1147
  }
1121
1148
  // Acknowledgment already posted above for both new and existing sessions
1122
1149
  // (before any async routing work to ensure instant user feedback)
1123
- // Get Linear client for this repository
1124
- const linearClient = this.linearClients.get(repository.id);
1125
- if (!linearClient) {
1126
- console.error("Unexpected: There was no LinearClient for the repository with id", repository.id);
1150
+ // Get issue tracker for this repository
1151
+ const issueTracker = this.issueTrackers.get(repository.id);
1152
+ if (!issueTracker) {
1153
+ console.error("Unexpected: There was no IssueTrackerService for the repository with id", repository.id);
1127
1154
  return;
1128
1155
  }
1129
1156
  // Always set up attachments directory, even if no attachments in current comment
@@ -1134,37 +1161,34 @@ export class EdgeWorker extends EventEmitter {
1134
1161
  let attachmentManifest = "";
1135
1162
  let commentAuthor;
1136
1163
  let commentTimestamp;
1164
+ if (!commentId) {
1165
+ console.warn("[EdgeWorker] No comment ID provided for attachment handling");
1166
+ }
1137
1167
  try {
1138
- const result = await linearClient.client.rawRequest(`
1139
- query GetComment($id: String!) {
1140
- comment(id: $id) {
1141
- id
1142
- body
1143
- createdAt
1144
- updatedAt
1145
- user {
1146
- name
1147
- displayName
1148
- email
1149
- id
1150
- }
1151
- }
1152
- }
1153
- `, { id: commentId });
1154
- // Extract comment data
1155
- const comment = result.data.comment;
1168
+ const comment = commentId
1169
+ ? await issueTracker.fetchComment(commentId)
1170
+ : null;
1156
1171
  // Extract comment metadata for multi-player context
1157
1172
  if (comment) {
1158
- const user = comment.user;
1173
+ const user = await comment.user;
1159
1174
  commentAuthor =
1160
1175
  user?.displayName || user?.name || user?.email || "Unknown";
1161
- commentTimestamp = comment.createdAt || new Date().toISOString();
1176
+ commentTimestamp = comment.createdAt
1177
+ ? comment.createdAt.toISOString()
1178
+ : new Date().toISOString();
1162
1179
  }
1163
1180
  // Count existing attachments
1164
1181
  const existingFiles = await readdir(attachmentsDir).catch(() => []);
1165
1182
  const existingAttachmentCount = existingFiles.filter((file) => file.startsWith("attachment_") || file.startsWith("image_")).length;
1166
1183
  // Download new attachments from the comment
1167
- const downloadResult = await this.downloadCommentAttachments(comment.body, attachmentsDir, repository.linearToken, existingAttachmentCount);
1184
+ const downloadResult = comment
1185
+ ? await this.downloadCommentAttachments(comment.body, attachmentsDir, repository.linearToken, existingAttachmentCount)
1186
+ : {
1187
+ totalNewAttachments: 0,
1188
+ newAttachmentMap: {},
1189
+ newImageMap: {},
1190
+ failedCount: 0,
1191
+ };
1168
1192
  if (downloadResult.totalNewAttachments > 0) {
1169
1193
  attachmentManifest = this.generateNewAttachmentManifest(downloadResult);
1170
1194
  }
@@ -1196,7 +1220,7 @@ export class EdgeWorker extends EventEmitter {
1196
1220
  // Branch 1: Handle stop signal (checked FIRST, before any routing work)
1197
1221
  // Per CLAUDE.md: "an agentSession MUST already exist" for stop signals
1198
1222
  // IMPORTANT: Stop signals do NOT require repository lookup
1199
- if (webhook.agentActivity.signal === "stop") {
1223
+ if (webhook.agentActivity?.signal === "stop") {
1200
1224
  await this.handleStopSignal(webhook);
1201
1225
  return;
1202
1226
  }
@@ -1373,10 +1397,10 @@ export class EdgeWorker extends EventEmitter {
1373
1397
  console.warn(`[EdgeWorker] Failed to fetch assignee details:`, error);
1374
1398
  }
1375
1399
  // Get LinearClient for this repository
1376
- const linearClient = this.linearClients.get(repository.id);
1377
- if (!linearClient) {
1378
- console.error(`No LinearClient found for repository ${repository.id}`);
1379
- throw new Error(`No LinearClient found for repository ${repository.id}`);
1400
+ const issueTracker = this.issueTrackers.get(repository.id);
1401
+ if (!issueTracker) {
1402
+ console.error(`No IssueTrackerService found for repository ${repository.id}`);
1403
+ throw new Error(`No IssueTrackerService found for repository ${repository.id}`);
1380
1404
  }
1381
1405
  // Fetch workspace teams and labels
1382
1406
  let workspaceTeams = "";
@@ -1384,7 +1408,7 @@ export class EdgeWorker extends EventEmitter {
1384
1408
  try {
1385
1409
  console.log(`[EdgeWorker] Fetching workspace teams and labels for repository ${repository.id}`);
1386
1410
  // Fetch teams
1387
- const teamsConnection = await linearClient.teams();
1411
+ const teamsConnection = await issueTracker.fetchTeams();
1388
1412
  const teamsArray = [];
1389
1413
  for (const team of teamsConnection.nodes) {
1390
1414
  teamsArray.push({
@@ -1399,7 +1423,7 @@ export class EdgeWorker extends EventEmitter {
1399
1423
  .map((team) => `- ${team.name} (${team.key}): ${team.id}${team.description ? ` - ${team.description}` : ""}`)
1400
1424
  .join("\n");
1401
1425
  // Fetch labels
1402
- const labelsConnection = await linearClient.issueLabels();
1426
+ const labelsConnection = await issueTracker.fetchLabels();
1403
1427
  const labelsArray = [];
1404
1428
  for (const label of labelsConnection.nodes) {
1405
1429
  labelsArray.push({
@@ -1721,14 +1745,12 @@ ${reply.body}
1721
1745
  // Determine the base branch considering parent issues
1722
1746
  const baseBranch = await this.determineBaseBranch(issue, repository);
1723
1747
  // Get formatted comment threads
1724
- const linearClient = this.linearClients.get(repository.id);
1748
+ const issueTracker = this.issueTrackers.get(repository.id);
1725
1749
  let commentThreads = "No comments yet.";
1726
- if (linearClient && issue.id) {
1750
+ if (issueTracker && issue.id) {
1727
1751
  try {
1728
1752
  console.log(`[EdgeWorker] Fetching comments for issue ${issue.identifier}`);
1729
- const comments = await linearClient.comments({
1730
- filter: { issue: { id: { eq: issue.id } } },
1731
- });
1753
+ const comments = await issueTracker.fetchComments(issue.id);
1732
1754
  const commentNodes = comments.nodes;
1733
1755
  if (commentNodes.length > 0) {
1734
1756
  commentThreads = await this.formatCommentThreads(commentNodes);
@@ -1771,11 +1793,9 @@ IMPORTANT: Focus specifically on addressing the new comment above. This is a new
1771
1793
  // Now replace the new comment variables
1772
1794
  // We'll need to fetch the comment author
1773
1795
  let authorName = "Unknown";
1774
- if (linearClient) {
1796
+ if (issueTracker) {
1775
1797
  try {
1776
- const fullComment = await linearClient.comment({
1777
- id: newComment.id,
1778
- });
1798
+ const fullComment = await issueTracker.fetchComment(newComment.id);
1779
1799
  const user = await fullComment.user;
1780
1800
  authorName =
1781
1801
  user?.displayName || user?.name || user?.email || "Unknown";
@@ -1876,13 +1896,13 @@ ${newComment ? `New comment to address:\n${newComment.body}\n\n` : ""}Please ana
1876
1896
  /**
1877
1897
  * Move issue to started state when assigned
1878
1898
  * @param issue Full Linear issue object from Linear SDK
1879
- * @param repositoryId Repository ID for Linear client lookup
1899
+ * @param repositoryId Repository ID for issue tracker lookup
1880
1900
  */
1881
1901
  async moveIssueToStartedState(issue, repositoryId) {
1882
1902
  try {
1883
- const linearClient = this.linearClients.get(repositoryId);
1884
- if (!linearClient) {
1885
- console.warn(`No Linear client found for repository ${repositoryId}, skipping state update`);
1903
+ const issueTracker = this.issueTrackers.get(repositoryId);
1904
+ if (!issueTracker) {
1905
+ console.warn(`No issue tracker found for repository ${repositoryId}, skipping state update`);
1886
1906
  return;
1887
1907
  }
1888
1908
  // Check if issue is already in a started state
@@ -1898,9 +1918,7 @@ ${newComment ? `New comment to address:\n${newComment.body}\n\n` : ""}Please ana
1898
1918
  return;
1899
1919
  }
1900
1920
  // Get available workflow states for the issue's team
1901
- const teamStates = await linearClient.workflowStates({
1902
- filter: { team: { id: { eq: team.id } } },
1903
- });
1921
+ const teamStates = await issueTracker.fetchWorkflowStates(team.id);
1904
1922
  const states = teamStates;
1905
1923
  // Find all states with type "started" and pick the one with lowest position
1906
1924
  // This ensures we pick "In Progress" over "In Review" when both have type "started"
@@ -1916,7 +1934,7 @@ ${newComment ? `New comment to address:\n${newComment.body}\n\n` : ""}Please ana
1916
1934
  console.warn(`Issue ${issue.identifier} has no ID, skipping state update`);
1917
1935
  return;
1918
1936
  }
1919
- await linearClient.updateIssue(issue.id, {
1937
+ await issueTracker.updateIssue(issue.id, {
1920
1938
  stateId: startedState.id,
1921
1939
  });
1922
1940
  console.log(`✅ Successfully moved issue ${issue.identifier} to ${startedState.name} state`);
@@ -1931,35 +1949,33 @@ ${newComment ? `New comment to address:\n${newComment.body}\n\n` : ""}Please ana
1931
1949
  */
1932
1950
  // private async postInitialComment(issueId: string, repositoryId: string): Promise<void> {
1933
1951
  // const body = "I'm getting started right away."
1934
- // // Get the Linear client for this repository
1935
- // const linearClient = this.linearClients.get(repositoryId)
1936
- // if (!linearClient) {
1937
- // throw new Error(`No Linear client found for repository ${repositoryId}`)
1952
+ // // Get the issue tracker for this repository
1953
+ // const issueTracker = this.issueTrackers.get(repositoryId)
1954
+ // if (!issueTracker) {
1955
+ // throw new Error(`No issue tracker found for repository ${repositoryId}`)
1938
1956
  // }
1939
1957
  // const commentData = {
1940
- // issueId,
1941
1958
  // body
1942
1959
  // }
1943
- // await linearClient.createComment(commentData)
1960
+ // await issueTracker.createComment(commentData)
1944
1961
  // }
1945
1962
  /**
1946
1963
  * Post a comment to Linear
1947
1964
  */
1948
1965
  async postComment(issueId, body, repositoryId, parentId) {
1949
- // Get the Linear client for this repository
1950
- const linearClient = this.linearClients.get(repositoryId);
1951
- if (!linearClient) {
1952
- throw new Error(`No Linear client found for repository ${repositoryId}`);
1966
+ // Get the issue tracker for this repository
1967
+ const issueTracker = this.issueTrackers.get(repositoryId);
1968
+ if (!issueTracker) {
1969
+ throw new Error(`No issue tracker found for repository ${repositoryId}`);
1953
1970
  }
1954
- const commentData = {
1955
- issueId,
1971
+ const commentInput = {
1956
1972
  body,
1957
1973
  };
1958
1974
  // Add parent ID if provided (for reply)
1959
1975
  if (parentId) {
1960
- commentData.parentId = parentId;
1976
+ commentInput.parentId = parentId;
1961
1977
  }
1962
- await linearClient.createComment(commentData);
1978
+ await issueTracker.createComment(issueId, commentInput);
1963
1979
  }
1964
1980
  /**
1965
1981
  * Format todos as Linear checklist markdown
@@ -2008,10 +2024,10 @@ ${newComment ? `New comment to address:\n${newComment.body}\n\n` : ""}Please ana
2008
2024
  const descriptionUrls = this.extractAttachmentUrls(issue.description || "");
2009
2025
  // Extract URLs from comments if available
2010
2026
  const commentUrls = [];
2011
- const linearClient = this.linearClients.get(repository.id);
2027
+ const issueTracker = this.issueTrackers.get(repository.id);
2012
2028
  // Fetch native Linear attachments (e.g., Sentry links)
2013
2029
  const nativeAttachments = [];
2014
- if (linearClient && issue.id) {
2030
+ if (issueTracker && issue.id) {
2015
2031
  try {
2016
2032
  // Fetch native attachments using Linear SDK
2017
2033
  console.log(`[EdgeWorker] Fetching native attachments for issue ${issue.identifier}`);
@@ -2030,9 +2046,7 @@ ${newComment ? `New comment to address:\n${newComment.body}\n\n` : ""}Please ana
2030
2046
  console.error("Failed to fetch native attachments:", error);
2031
2047
  }
2032
2048
  try {
2033
- const comments = await linearClient.comments({
2034
- filter: { issue: { id: { eq: issue.id } } },
2035
- });
2049
+ const comments = await issueTracker.fetchComments(issue.id);
2036
2050
  const commentNodes = comments.nodes;
2037
2051
  for (const comment of commentNodes) {
2038
2052
  const urls = this.extractAttachmentUrls(comment.body);
@@ -2395,13 +2409,13 @@ ${newComment ? `New comment to address:\n${newComment.body}\n\n` : ""}Please ana
2395
2409
  }
2396
2410
  }
2397
2411
  // Post thought to Linear showing feedback receipt
2398
- const linearClient = this.linearClients.get(childRepo.id);
2399
- if (linearClient) {
2412
+ const issueTracker = this.issueTrackers.get(childRepo.id);
2413
+ if (issueTracker) {
2400
2414
  const feedbackThought = parentIssueId
2401
2415
  ? `Received feedback from orchestrator (${parentIssueId}):\n\n---\n\n${message}\n\n---`
2402
2416
  : `Received feedback from orchestrator:\n\n---\n\n${message}\n\n---`;
2403
2417
  try {
2404
- const result = await linearClient.createAgentActivity({
2418
+ const result = await issueTracker.createAgentActivity({
2405
2419
  agentSessionId: childSessionId,
2406
2420
  content: {
2407
2421
  type: "thought",
@@ -3024,9 +3038,9 @@ ${input.userComment}
3024
3038
  */
3025
3039
  async postInstantAcknowledgment(linearAgentActivitySessionId, repositoryId) {
3026
3040
  try {
3027
- const linearClient = this.linearClients.get(repositoryId);
3028
- if (!linearClient) {
3029
- console.warn(`[EdgeWorker] No Linear client found for repository ${repositoryId}`);
3041
+ const issueTracker = this.issueTrackers.get(repositoryId);
3042
+ if (!issueTracker) {
3043
+ console.warn(`[EdgeWorker] No issue tracker found for repository ${repositoryId}`);
3030
3044
  return;
3031
3045
  }
3032
3046
  const activityInput = {
@@ -3036,7 +3050,7 @@ ${input.userComment}
3036
3050
  body: "I've received your request and I'm starting to work on it. Let me analyze the issue and prepare my approach.",
3037
3051
  },
3038
3052
  };
3039
- const result = await linearClient.createAgentActivity(activityInput);
3053
+ const result = await issueTracker.createAgentActivity(activityInput);
3040
3054
  if (result.success) {
3041
3055
  console.log(`[EdgeWorker] Posted instant acknowledgment thought for session ${linearAgentActivitySessionId}`);
3042
3056
  }
@@ -3053,9 +3067,9 @@ ${input.userComment}
3053
3067
  */
3054
3068
  async postParentResumeAcknowledgment(linearAgentActivitySessionId, repositoryId) {
3055
3069
  try {
3056
- const linearClient = this.linearClients.get(repositoryId);
3057
- if (!linearClient) {
3058
- console.warn(`[EdgeWorker] No Linear client found for repository ${repositoryId}`);
3070
+ const issueTracker = this.issueTrackers.get(repositoryId);
3071
+ if (!issueTracker) {
3072
+ console.warn(`[EdgeWorker] No issue tracker found for repository ${repositoryId}`);
3059
3073
  return;
3060
3074
  }
3061
3075
  const activityInput = {
@@ -3065,7 +3079,7 @@ ${input.userComment}
3065
3079
  body: "Resuming from child session",
3066
3080
  },
3067
3081
  };
3068
- const result = await linearClient.createAgentActivity(activityInput);
3082
+ const result = await issueTracker.createAgentActivity(activityInput);
3069
3083
  if (result.success) {
3070
3084
  console.log(`[EdgeWorker] Posted parent resumption acknowledgment thought for session ${linearAgentActivitySessionId}`);
3071
3085
  }
@@ -3083,9 +3097,9 @@ ${input.userComment}
3083
3097
  */
3084
3098
  async postRepositorySelectionActivity(linearAgentActivitySessionId, repositoryId, repositoryName, selectionMethod) {
3085
3099
  try {
3086
- const linearClient = this.linearClients.get(repositoryId);
3087
- if (!linearClient) {
3088
- console.warn(`[EdgeWorker] No Linear client found for repository ${repositoryId}`);
3100
+ const issueTracker = this.issueTrackers.get(repositoryId);
3101
+ if (!issueTracker) {
3102
+ console.warn(`[EdgeWorker] No issue tracker found for repository ${repositoryId}`);
3089
3103
  return;
3090
3104
  }
3091
3105
  let methodDisplay;
@@ -3117,7 +3131,7 @@ ${input.userComment}
3117
3131
  body: `Repository "${repositoryName}" has been ${methodDisplay}.`,
3118
3132
  },
3119
3133
  };
3120
- const result = await linearClient.createAgentActivity(activityInput);
3134
+ const result = await issueTracker.createAgentActivity(activityInput);
3121
3135
  if (result.success) {
3122
3136
  console.log(`[EdgeWorker] Posted repository selection activity for session ${linearAgentActivitySessionId} (${selectionMethod})`);
3123
3137
  }
@@ -3141,11 +3155,11 @@ ${input.userComment}
3141
3155
  // Post ephemeral "Routing..." thought
3142
3156
  await agentSessionManager.postRoutingThought(linearAgentActivitySessionId);
3143
3157
  // Fetch full issue and labels to check for Orchestrator label override
3144
- const linearClient = this.linearClients.get(repository.id);
3158
+ const issueTracker = this.issueTrackers.get(repository.id);
3145
3159
  let hasOrchestratorLabel = false;
3146
- if (linearClient) {
3160
+ if (issueTracker) {
3147
3161
  try {
3148
- const fullIssue = await linearClient.issue(session.issueId);
3162
+ const fullIssue = await issueTracker.fetchIssue(session.issueId);
3149
3163
  const labels = await this.fetchIssueLabels(fullIssue);
3150
3164
  // Check for Orchestrator label (same logic as initial routing)
3151
3165
  const orchestratorConfig = repository.labelPrompts?.orchestrator;
@@ -3241,9 +3255,9 @@ ${input.userComment}
3241
3255
  */
3242
3256
  async postSystemPromptSelectionThought(linearAgentActivitySessionId, labels, repositoryId) {
3243
3257
  try {
3244
- const linearClient = this.linearClients.get(repositoryId);
3245
- if (!linearClient) {
3246
- console.warn(`[EdgeWorker] No Linear client found for repository ${repositoryId}`);
3258
+ const issueTracker = this.issueTrackers.get(repositoryId);
3259
+ if (!issueTracker) {
3260
+ console.warn(`[EdgeWorker] No issue tracker found for repository ${repositoryId}`);
3247
3261
  return;
3248
3262
  }
3249
3263
  // Determine which prompt type was selected and which label triggered it
@@ -3309,7 +3323,7 @@ ${input.userComment}
3309
3323
  body: `Entering '${selectedPromptType}' mode because of the '${triggerLabel}' label. I'll follow the ${selectedPromptType} process...`,
3310
3324
  },
3311
3325
  };
3312
- const result = await linearClient.createAgentActivity(activityInput);
3326
+ const result = await issueTracker.createAgentActivity(activityInput);
3313
3327
  if (result.success) {
3314
3328
  console.log(`[EdgeWorker] Posted system prompt selection thought for session ${linearAgentActivitySessionId} (${selectedPromptType} mode)`);
3315
3329
  }
@@ -3401,9 +3415,9 @@ ${input.userComment}
3401
3415
  */
3402
3416
  async postInstantPromptedAcknowledgment(linearAgentActivitySessionId, repositoryId, isStreaming) {
3403
3417
  try {
3404
- const linearClient = this.linearClients.get(repositoryId);
3405
- if (!linearClient) {
3406
- console.warn(`[EdgeWorker] No Linear client found for repository ${repositoryId}`);
3418
+ const issueTracker = this.issueTrackers.get(repositoryId);
3419
+ if (!issueTracker) {
3420
+ console.warn(`[EdgeWorker] No issue tracker found for repository ${repositoryId}`);
3407
3421
  return;
3408
3422
  }
3409
3423
  const message = isStreaming
@@ -3416,7 +3430,7 @@ ${input.userComment}
3416
3430
  body: message,
3417
3431
  },
3418
3432
  };
3419
- const result = await linearClient.createAgentActivity(activityInput);
3433
+ const result = await issueTracker.createAgentActivity(activityInput);
3420
3434
  if (result.success) {
3421
3435
  console.log(`[EdgeWorker] Posted instant prompted acknowledgment thought for session ${linearAgentActivitySessionId} (streaming: ${isStreaming})`);
3422
3436
  }
@@ -3432,14 +3446,14 @@ ${input.userComment}
3432
3446
  * Fetch complete issue details from Linear API
3433
3447
  */
3434
3448
  async fetchFullIssueDetails(issueId, repositoryId) {
3435
- const linearClient = this.linearClients.get(repositoryId);
3436
- if (!linearClient) {
3437
- console.warn(`[EdgeWorker] No Linear client found for repository ${repositoryId}`);
3449
+ const issueTracker = this.issueTrackers.get(repositoryId);
3450
+ if (!issueTracker) {
3451
+ console.warn(`[EdgeWorker] No issue tracker found for repository ${repositoryId}`);
3438
3452
  return null;
3439
3453
  }
3440
3454
  try {
3441
3455
  console.log(`[EdgeWorker] Fetching full issue details for ${issueId}`);
3442
- const fullIssue = await linearClient.issue(issueId);
3456
+ const fullIssue = await issueTracker.fetchIssue(issueId);
3443
3457
  console.log(`[EdgeWorker] Successfully fetched issue details for ${issueId}`);
3444
3458
  // Check if issue has a parent
3445
3459
  try {