sylas-edge-worker 0.2.21

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 (163) hide show
  1. package/README.md +293 -0
  2. package/dist/ActivityPoster.d.ts +15 -0
  3. package/dist/ActivityPoster.d.ts.map +1 -0
  4. package/dist/ActivityPoster.js +194 -0
  5. package/dist/ActivityPoster.js.map +1 -0
  6. package/dist/AgentSessionManager.d.ts +280 -0
  7. package/dist/AgentSessionManager.d.ts.map +1 -0
  8. package/dist/AgentSessionManager.js +1412 -0
  9. package/dist/AgentSessionManager.js.map +1 -0
  10. package/dist/AskUserQuestionHandler.d.ts +97 -0
  11. package/dist/AskUserQuestionHandler.d.ts.map +1 -0
  12. package/dist/AskUserQuestionHandler.js +206 -0
  13. package/dist/AskUserQuestionHandler.js.map +1 -0
  14. package/dist/AttachmentService.d.ts +69 -0
  15. package/dist/AttachmentService.d.ts.map +1 -0
  16. package/dist/AttachmentService.js +369 -0
  17. package/dist/AttachmentService.js.map +1 -0
  18. package/dist/ChatSessionHandler.d.ts +87 -0
  19. package/dist/ChatSessionHandler.d.ts.map +1 -0
  20. package/dist/ChatSessionHandler.js +231 -0
  21. package/dist/ChatSessionHandler.js.map +1 -0
  22. package/dist/ConfigManager.d.ts +91 -0
  23. package/dist/ConfigManager.d.ts.map +1 -0
  24. package/dist/ConfigManager.js +227 -0
  25. package/dist/ConfigManager.js.map +1 -0
  26. package/dist/EdgeWorker.d.ts +670 -0
  27. package/dist/EdgeWorker.d.ts.map +1 -0
  28. package/dist/EdgeWorker.js +3801 -0
  29. package/dist/EdgeWorker.js.map +1 -0
  30. package/dist/GitService.d.ts +39 -0
  31. package/dist/GitService.d.ts.map +1 -0
  32. package/dist/GitService.js +432 -0
  33. package/dist/GitService.js.map +1 -0
  34. package/dist/GlobalSessionRegistry.d.ts +142 -0
  35. package/dist/GlobalSessionRegistry.d.ts.map +1 -0
  36. package/dist/GlobalSessionRegistry.js +254 -0
  37. package/dist/GlobalSessionRegistry.js.map +1 -0
  38. package/dist/PromptBuilder.d.ts +175 -0
  39. package/dist/PromptBuilder.d.ts.map +1 -0
  40. package/dist/PromptBuilder.js +884 -0
  41. package/dist/PromptBuilder.js.map +1 -0
  42. package/dist/RepositoryRouter.d.ts +152 -0
  43. package/dist/RepositoryRouter.d.ts.map +1 -0
  44. package/dist/RepositoryRouter.js +480 -0
  45. package/dist/RepositoryRouter.js.map +1 -0
  46. package/dist/RunnerSelectionService.d.ts +62 -0
  47. package/dist/RunnerSelectionService.d.ts.map +1 -0
  48. package/dist/RunnerSelectionService.js +379 -0
  49. package/dist/RunnerSelectionService.js.map +1 -0
  50. package/dist/SharedApplicationServer.d.ts +107 -0
  51. package/dist/SharedApplicationServer.d.ts.map +1 -0
  52. package/dist/SharedApplicationServer.js +247 -0
  53. package/dist/SharedApplicationServer.js.map +1 -0
  54. package/dist/SharedWebhookServer.d.ts +39 -0
  55. package/dist/SharedWebhookServer.d.ts.map +1 -0
  56. package/dist/SharedWebhookServer.js +150 -0
  57. package/dist/SharedWebhookServer.js.map +1 -0
  58. package/dist/SlackChatAdapter.d.ts +25 -0
  59. package/dist/SlackChatAdapter.d.ts.map +1 -0
  60. package/dist/SlackChatAdapter.js +143 -0
  61. package/dist/SlackChatAdapter.js.map +1 -0
  62. package/dist/UserAccessControl.d.ts +69 -0
  63. package/dist/UserAccessControl.d.ts.map +1 -0
  64. package/dist/UserAccessControl.js +171 -0
  65. package/dist/UserAccessControl.js.map +1 -0
  66. package/dist/WorktreeIncludeService.d.ts +32 -0
  67. package/dist/WorktreeIncludeService.d.ts.map +1 -0
  68. package/dist/WorktreeIncludeService.js +123 -0
  69. package/dist/WorktreeIncludeService.js.map +1 -0
  70. package/dist/index.d.ts +22 -0
  71. package/dist/index.d.ts.map +1 -0
  72. package/dist/index.js +17 -0
  73. package/dist/index.js.map +1 -0
  74. package/dist/label-prompt-template.md +27 -0
  75. package/dist/procedures/ProcedureAnalyzer.d.ts +69 -0
  76. package/dist/procedures/ProcedureAnalyzer.d.ts.map +1 -0
  77. package/dist/procedures/ProcedureAnalyzer.js +271 -0
  78. package/dist/procedures/ProcedureAnalyzer.js.map +1 -0
  79. package/dist/procedures/index.d.ts +7 -0
  80. package/dist/procedures/index.d.ts.map +1 -0
  81. package/dist/procedures/index.js +7 -0
  82. package/dist/procedures/index.js.map +1 -0
  83. package/dist/procedures/registry.d.ts +156 -0
  84. package/dist/procedures/registry.d.ts.map +1 -0
  85. package/dist/procedures/registry.js +240 -0
  86. package/dist/procedures/registry.js.map +1 -0
  87. package/dist/procedures/types.d.ts +103 -0
  88. package/dist/procedures/types.d.ts.map +1 -0
  89. package/dist/procedures/types.js +5 -0
  90. package/dist/procedures/types.js.map +1 -0
  91. package/dist/prompt-assembly/types.d.ts +80 -0
  92. package/dist/prompt-assembly/types.d.ts.map +1 -0
  93. package/dist/prompt-assembly/types.js +8 -0
  94. package/dist/prompt-assembly/types.js.map +1 -0
  95. package/dist/prompts/builder.md +191 -0
  96. package/dist/prompts/debugger.md +128 -0
  97. package/dist/prompts/graphite-orchestrator.md +362 -0
  98. package/dist/prompts/orchestrator.md +290 -0
  99. package/dist/prompts/scoper.md +95 -0
  100. package/dist/prompts/standard-issue-assigned-user-prompt.md +33 -0
  101. package/dist/prompts/subroutines/changelog-update.md +79 -0
  102. package/dist/prompts/subroutines/coding-activity.md +12 -0
  103. package/dist/prompts/subroutines/concise-summary.md +67 -0
  104. package/dist/prompts/subroutines/debugger-fix.md +92 -0
  105. package/dist/prompts/subroutines/debugger-reproduction.md +74 -0
  106. package/dist/prompts/subroutines/full-delegation.md +68 -0
  107. package/dist/prompts/subroutines/get-approval.md +175 -0
  108. package/dist/prompts/subroutines/gh-pr.md +80 -0
  109. package/dist/prompts/subroutines/git-commit.md +37 -0
  110. package/dist/prompts/subroutines/plan-summary.md +21 -0
  111. package/dist/prompts/subroutines/preparation.md +16 -0
  112. package/dist/prompts/subroutines/question-answer.md +8 -0
  113. package/dist/prompts/subroutines/question-investigation.md +8 -0
  114. package/dist/prompts/subroutines/release-execution.md +81 -0
  115. package/dist/prompts/subroutines/release-summary.md +60 -0
  116. package/dist/prompts/subroutines/user-testing-summary.md +87 -0
  117. package/dist/prompts/subroutines/user-testing.md +48 -0
  118. package/dist/prompts/subroutines/validation-fixer.md +56 -0
  119. package/dist/prompts/subroutines/verbose-summary.md +46 -0
  120. package/dist/prompts/subroutines/verifications.md +77 -0
  121. package/dist/prompts/todolist-system-prompt-extension.md +15 -0
  122. package/dist/sinks/IActivitySink.d.ts +60 -0
  123. package/dist/sinks/IActivitySink.d.ts.map +1 -0
  124. package/dist/sinks/IActivitySink.js +2 -0
  125. package/dist/sinks/IActivitySink.js.map +1 -0
  126. package/dist/sinks/LinearActivitySink.d.ts +69 -0
  127. package/dist/sinks/LinearActivitySink.d.ts.map +1 -0
  128. package/dist/sinks/LinearActivitySink.js +111 -0
  129. package/dist/sinks/LinearActivitySink.js.map +1 -0
  130. package/dist/sinks/NoopActivitySink.d.ts +13 -0
  131. package/dist/sinks/NoopActivitySink.d.ts.map +1 -0
  132. package/dist/sinks/NoopActivitySink.js +17 -0
  133. package/dist/sinks/NoopActivitySink.js.map +1 -0
  134. package/dist/sinks/index.d.ts +9 -0
  135. package/dist/sinks/index.d.ts.map +1 -0
  136. package/dist/sinks/index.js +8 -0
  137. package/dist/sinks/index.js.map +1 -0
  138. package/dist/types.d.ts +32 -0
  139. package/dist/types.d.ts.map +1 -0
  140. package/dist/types.js +2 -0
  141. package/dist/types.js.map +1 -0
  142. package/dist/validation/ValidationLoopController.d.ts +54 -0
  143. package/dist/validation/ValidationLoopController.d.ts.map +1 -0
  144. package/dist/validation/ValidationLoopController.js +242 -0
  145. package/dist/validation/ValidationLoopController.js.map +1 -0
  146. package/dist/validation/index.d.ts +7 -0
  147. package/dist/validation/index.d.ts.map +1 -0
  148. package/dist/validation/index.js +7 -0
  149. package/dist/validation/index.js.map +1 -0
  150. package/dist/validation/types.d.ts +82 -0
  151. package/dist/validation/types.d.ts.map +1 -0
  152. package/dist/validation/types.js +29 -0
  153. package/dist/validation/types.js.map +1 -0
  154. package/label-prompt-template.md +27 -0
  155. package/package.json +56 -0
  156. package/prompt-template.md +116 -0
  157. package/prompts/builder.md +191 -0
  158. package/prompts/debugger.md +128 -0
  159. package/prompts/graphite-orchestrator.md +362 -0
  160. package/prompts/orchestrator.md +290 -0
  161. package/prompts/scoper.md +95 -0
  162. package/prompts/standard-issue-assigned-user-prompt.md +33 -0
  163. package/prompts/todolist-system-prompt-extension.md +15 -0
@@ -0,0 +1,884 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { dirname, join, resolve } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ /**
5
+ * Responsible for building various prompt types used in the EdgeWorker.
6
+ *
7
+ * Extracted from EdgeWorker to improve separation of concerns.
8
+ * Handles label-based prompts, mention prompts, issue context prompts,
9
+ * issue update prompts, subroutine prompt loading, and related utilities.
10
+ */
11
+ export class PromptBuilder {
12
+ logger;
13
+ repositories;
14
+ issueTrackers;
15
+ gitService;
16
+ config;
17
+ constructor(deps) {
18
+ this.logger = deps.logger;
19
+ this.repositories = deps.repositories;
20
+ this.issueTrackers = deps.issueTrackers;
21
+ this.gitService = deps.gitService;
22
+ this.config = deps.config;
23
+ }
24
+ // ========================================================================
25
+ // PROMPT BUILDING METHODS
26
+ // ========================================================================
27
+ /**
28
+ * Determine system prompt based on issue labels and repository configuration
29
+ */
30
+ async determineSystemPromptFromLabels(labels, repository) {
31
+ if (labels.length === 0) {
32
+ return undefined;
33
+ }
34
+ // Lowercase labels for case-insensitive comparison
35
+ const lowercaseLabels = labels.map((label) => label.toLowerCase());
36
+ // HARDCODED RULE: Always check for 'orchestrator' label (case-insensitive)
37
+ // regardless of whether repository.labelPrompts is configured.
38
+ // This matches the hardcoded routing behavior from CYPACK-715.
39
+ const hasHardcodedOrchestratorLabel = lowercaseLabels.includes("orchestrator");
40
+ // If no labelPrompts configured but has hardcoded orchestrator label,
41
+ // load orchestrator system prompt directly
42
+ if (!repository.labelPrompts && hasHardcodedOrchestratorLabel) {
43
+ try {
44
+ const __filename = fileURLToPath(import.meta.url);
45
+ const __dirname = dirname(__filename);
46
+ const promptPath = join(__dirname, "..", "prompts", "orchestrator.md");
47
+ const promptContent = await readFile(promptPath, "utf-8");
48
+ this.logger.debug(`Using orchestrator system prompt (hardcoded rule) for labels: ${labels.join(", ")}`);
49
+ const promptVersion = this.extractVersionTag(promptContent);
50
+ if (promptVersion) {
51
+ this.logger.debug(`orchestrator system prompt version: ${promptVersion}`);
52
+ }
53
+ return {
54
+ prompt: promptContent,
55
+ version: promptVersion,
56
+ type: "orchestrator",
57
+ };
58
+ }
59
+ catch (error) {
60
+ this.logger.error(`Failed to load orchestrator prompt template:`, error);
61
+ return undefined;
62
+ }
63
+ }
64
+ // If no labelPrompts configured and no hardcoded orchestrator, return undefined
65
+ if (!repository.labelPrompts) {
66
+ return undefined;
67
+ }
68
+ // Check for graphite-orchestrator first (requires BOTH graphite AND orchestrator labels)
69
+ const graphiteConfig = repository.labelPrompts.graphite;
70
+ const graphiteLabels = Array.isArray(graphiteConfig)
71
+ ? graphiteConfig
72
+ : (graphiteConfig?.labels ?? ["graphite"]);
73
+ const hasGraphiteLabel = graphiteLabels?.some((label) => lowercaseLabels.includes(label.toLowerCase()));
74
+ const orchestratorConfig = repository.labelPrompts.orchestrator;
75
+ const orchestratorLabels = Array.isArray(orchestratorConfig)
76
+ ? orchestratorConfig
77
+ : (orchestratorConfig?.labels ?? ["orchestrator"]);
78
+ // Use hardcoded check OR config-based check for orchestrator
79
+ const hasOrchestratorLabel = hasHardcodedOrchestratorLabel ||
80
+ orchestratorLabels?.some((label) => lowercaseLabels.includes(label.toLowerCase()));
81
+ // If both graphite AND orchestrator labels are present, use graphite-orchestrator prompt
82
+ if (hasGraphiteLabel && hasOrchestratorLabel) {
83
+ try {
84
+ const __filename = fileURLToPath(import.meta.url);
85
+ const __dirname = dirname(__filename);
86
+ const promptPath = join(__dirname, "..", "prompts", "graphite-orchestrator.md");
87
+ const promptContent = await readFile(promptPath, "utf-8");
88
+ this.logger.debug(`Using graphite-orchestrator system prompt for labels: ${labels.join(", ")}`);
89
+ const promptVersion = this.extractVersionTag(promptContent);
90
+ if (promptVersion) {
91
+ this.logger.debug(`graphite-orchestrator system prompt version: ${promptVersion}`);
92
+ }
93
+ return {
94
+ prompt: promptContent,
95
+ version: promptVersion,
96
+ type: "graphite-orchestrator",
97
+ };
98
+ }
99
+ catch (error) {
100
+ this.logger.error(`Failed to load graphite-orchestrator prompt template:`, error);
101
+ // Fall through to regular orchestrator if graphite-orchestrator prompt fails
102
+ }
103
+ }
104
+ // Check each prompt type for matching labels
105
+ const promptTypes = [
106
+ "debugger",
107
+ "builder",
108
+ "scoper",
109
+ "orchestrator",
110
+ ];
111
+ for (const promptType of promptTypes) {
112
+ const promptConfig = repository.labelPrompts[promptType];
113
+ // Handle both old array format and new object format for backward compatibility
114
+ const configuredLabels = Array.isArray(promptConfig)
115
+ ? promptConfig
116
+ : promptConfig?.labels;
117
+ // For orchestrator type, also check the hardcoded 'orchestrator' label
118
+ // This ensures orchestrator prompt loads even without explicit labelPrompts config
119
+ const matchesLabel = promptType === "orchestrator"
120
+ ? hasHardcodedOrchestratorLabel ||
121
+ configuredLabels?.some((label) => lowercaseLabels.includes(label.toLowerCase()))
122
+ : configuredLabels?.some((label) => lowercaseLabels.includes(label.toLowerCase()));
123
+ if (matchesLabel) {
124
+ try {
125
+ // Load the prompt template from file
126
+ const __filename = fileURLToPath(import.meta.url);
127
+ const __dirname = dirname(__filename);
128
+ const promptPath = join(__dirname, "..", "prompts", `${promptType}.md`);
129
+ const promptContent = await readFile(promptPath, "utf-8");
130
+ this.logger.debug(`Using ${promptType} system prompt for labels: ${labels.join(", ")}`);
131
+ // Extract and log version tag if present
132
+ const promptVersion = this.extractVersionTag(promptContent);
133
+ if (promptVersion) {
134
+ this.logger.debug(`${promptType} system prompt version: ${promptVersion}`);
135
+ }
136
+ return {
137
+ prompt: promptContent,
138
+ version: promptVersion,
139
+ type: promptType,
140
+ };
141
+ }
142
+ catch (error) {
143
+ this.logger.error(`Failed to load ${promptType} prompt template:`, error);
144
+ return undefined;
145
+ }
146
+ }
147
+ }
148
+ return undefined;
149
+ }
150
+ /**
151
+ * Build simplified prompt for label-based workflows
152
+ * @param issue Full Linear issue
153
+ * @param repository Repository configuration
154
+ * @param attachmentManifest Optional attachment manifest
155
+ * @param guidance Optional agent guidance rules from Linear
156
+ * @returns Formatted prompt string
157
+ */
158
+ async buildLabelBasedPrompt(issue, repository, attachmentManifest = "", guidance) {
159
+ this.logger.debug(`buildLabelBasedPrompt called for issue ${issue.identifier}`);
160
+ try {
161
+ // Load the label-based prompt template
162
+ const __filename = fileURLToPath(import.meta.url);
163
+ const __dirname = dirname(__filename);
164
+ const templatePath = resolve(__dirname, "../label-prompt-template.md");
165
+ this.logger.debug(`Loading label prompt template from: ${templatePath}`);
166
+ const template = await readFile(templatePath, "utf-8");
167
+ this.logger.debug(`Template loaded, length: ${template.length} characters`);
168
+ // Extract and log version tag if present
169
+ const templateVersion = this.extractVersionTag(template);
170
+ if (templateVersion) {
171
+ this.logger.debug(`Label prompt template version: ${templateVersion}`);
172
+ }
173
+ // Determine the base branch considering parent issues
174
+ const baseBranch = await this.determineBaseBranch(issue, repository);
175
+ // Fetch assignee information
176
+ let assigneeId = "";
177
+ let assigneeName = "";
178
+ try {
179
+ if (issue.assigneeId) {
180
+ assigneeId = issue.assigneeId;
181
+ // Fetch the full assignee object to get the name
182
+ const assignee = await issue.assignee;
183
+ if (assignee) {
184
+ assigneeName = assignee.displayName || assignee.name || "";
185
+ }
186
+ }
187
+ }
188
+ catch (error) {
189
+ this.logger.warn(`Failed to fetch assignee details:`, error);
190
+ }
191
+ // Get IssueTrackerService for this repository
192
+ const issueTracker = this.issueTrackers.get(repository.id);
193
+ if (!issueTracker) {
194
+ this.logger.error(`No IssueTrackerService found for repository ${repository.id}`);
195
+ throw new Error(`No IssueTrackerService found for repository ${repository.id}`);
196
+ }
197
+ // Fetch workspace teams and labels
198
+ let workspaceTeams = "";
199
+ let workspaceLabels = "";
200
+ try {
201
+ this.logger.debug(`Fetching workspace teams and labels for repository ${repository.id}`);
202
+ // Fetch teams
203
+ const teamsConnection = await issueTracker.fetchTeams();
204
+ const teamsArray = [];
205
+ for (const team of teamsConnection.nodes) {
206
+ teamsArray.push({
207
+ id: team.id,
208
+ name: team.name,
209
+ key: team.key,
210
+ description: team.description || "",
211
+ color: team.color,
212
+ });
213
+ }
214
+ workspaceTeams = teamsArray
215
+ .map((team) => `- ${team.name} (${team.key}): ${team.id}${team.description ? ` - ${team.description}` : ""}`)
216
+ .join("\n");
217
+ // Fetch labels
218
+ const labelsConnection = await issueTracker.fetchLabels();
219
+ const labelsArray = [];
220
+ for (const label of labelsConnection.nodes) {
221
+ labelsArray.push({
222
+ id: label.id,
223
+ name: label.name,
224
+ description: label.description || "",
225
+ color: label.color,
226
+ });
227
+ }
228
+ workspaceLabels = labelsArray
229
+ .map((label) => `- ${label.name}: ${label.id}${label.description ? ` - ${label.description}` : ""}`)
230
+ .join("\n");
231
+ this.logger.debug(`Fetched ${teamsArray.length} teams and ${labelsArray.length} labels`);
232
+ }
233
+ catch (error) {
234
+ this.logger.warn(`Failed to fetch workspace teams and labels:`, error);
235
+ }
236
+ // Generate routing context for orchestrator mode
237
+ const routingContext = this.generateRoutingContext(repository);
238
+ // Build the simplified prompt with only essential variables
239
+ let prompt = template
240
+ .replace(/{{repository_name}}/g, repository.name)
241
+ .replace(/{{base_branch}}/g, baseBranch)
242
+ .replace(/{{issue_id}}/g, issue.id || "")
243
+ .replace(/{{issue_identifier}}/g, issue.identifier || "")
244
+ .replace(/{{issue_title}}/g, issue.title || "")
245
+ .replace(/{{issue_description}}/g, issue.description || "No description provided")
246
+ .replace(/{{issue_url}}/g, issue.url || "")
247
+ .replace(/{{assignee_id}}/g, assigneeId)
248
+ .replace(/{{assignee_name}}/g, assigneeName)
249
+ .replace(/{{workspace_teams}}/g, workspaceTeams)
250
+ .replace(/{{workspace_labels}}/g, workspaceLabels)
251
+ // Replace routing context - if empty, also remove the preceding newlines
252
+ .replace(routingContext ? /{{routing_context}}/g : /\n*{{routing_context}}/g, routingContext);
253
+ // Append agent guidance if present
254
+ prompt += this.formatAgentGuidance(guidance);
255
+ if (attachmentManifest) {
256
+ this.logger.debug(`Adding attachment manifest to label-based prompt, length: ${attachmentManifest.length} characters`);
257
+ prompt = `${prompt}\n\n${attachmentManifest}`;
258
+ }
259
+ this.logger.debug(`Label-based prompt built successfully, length: ${prompt.length} characters`);
260
+ return { prompt, version: templateVersion };
261
+ }
262
+ catch (error) {
263
+ this.logger.error(`Error building label-based prompt:`, error);
264
+ throw error;
265
+ }
266
+ }
267
+ /**
268
+ * Generate routing context for orchestrator mode
269
+ *
270
+ * This provides the orchestrator with information about available repositories
271
+ * and how to route sub-issues to them. The context includes:
272
+ * - List of configured repositories in the workspace
273
+ * - Routing rules for each repository (labels, teams, projects)
274
+ * - Instructions on using description tags for explicit routing
275
+ *
276
+ * @param currentRepository The repository handling the current orchestrator issue
277
+ * @returns XML-formatted routing context string, or empty string if no routing info available
278
+ */
279
+ generateRoutingContext(currentRepository) {
280
+ // Get all repositories in the same workspace
281
+ const workspaceRepos = Array.from(this.repositories.values()).filter((repo) => repo.linearWorkspaceId === currentRepository.linearWorkspaceId &&
282
+ repo.isActive !== false);
283
+ // If there's only one repository, no routing context needed
284
+ if (workspaceRepos.length <= 1) {
285
+ return "";
286
+ }
287
+ const repoDescriptions = workspaceRepos.map((repo) => {
288
+ const routingMethods = [];
289
+ // Description tag routing (always available)
290
+ const repoIdentifier = repo.githubUrl
291
+ ? repo.githubUrl.replace("https://github.com/", "")
292
+ : repo.name;
293
+ routingMethods.push(` - Description tag: Add \`[repo=${repoIdentifier}]\` to sub-issue description`);
294
+ // Label-based routing
295
+ if (repo.routingLabels && repo.routingLabels.length > 0) {
296
+ routingMethods.push(` - Routing labels: ${repo.routingLabels.map((l) => `"${l}"`).join(", ")}`);
297
+ }
298
+ // Team-based routing
299
+ if (repo.teamKeys && repo.teamKeys.length > 0) {
300
+ routingMethods.push(` - Team keys: ${repo.teamKeys.map((t) => `"${t}"`).join(", ")} (create issue in this team)`);
301
+ }
302
+ // Project-based routing
303
+ if (repo.projectKeys && repo.projectKeys.length > 0) {
304
+ routingMethods.push(` - Project keys: ${repo.projectKeys.map((p) => `"${p}"`).join(", ")} (add issue to this project)`);
305
+ }
306
+ const currentMarker = repo.id === currentRepository.id ? " (current)" : "";
307
+ return ` <repository name="${repo.name}"${currentMarker}>
308
+ <github_url>${repo.githubUrl || "N/A"}</github_url>
309
+ <routing_methods>
310
+ ${routingMethods.join("\n")}
311
+ </routing_methods>
312
+ </repository>`;
313
+ });
314
+ return `<repository_routing_context>
315
+ <description>
316
+ When creating sub-issues that should be handled in a DIFFERENT repository, use one of these routing methods.
317
+
318
+ **IMPORTANT - Routing Priority Order:**
319
+ The system evaluates routing methods in this strict priority order. The FIRST match wins:
320
+
321
+ 1. **Description Tag (Priority 1 - Highest, Recommended)**: Add \`[repo=org/repo-name]\` or \`[repo=repo-name]\` to the sub-issue description. This is the most explicit and reliable method.
322
+ 2. **Routing Labels (Priority 2)**: Apply a label configured to route to the target repository.
323
+ 3. **Project Assignment (Priority 3)**: Add the issue to a project that routes to the target repository.
324
+ 4. **Team Selection (Priority 4 - Lowest)**: Create the issue in a Linear team that routes to the target repository.
325
+
326
+ For reliable cross-repository routing, prefer Description Tags as they are explicit and unambiguous.
327
+ </description>
328
+
329
+ <available_repositories>
330
+ ${repoDescriptions.join("\n")}
331
+ </available_repositories>
332
+ </repository_routing_context>`;
333
+ }
334
+ /**
335
+ * Build prompt for mention-triggered sessions
336
+ * @param issue Full Linear issue object
337
+ * @param agentSession The agent session containing the mention
338
+ * @param attachmentManifest Optional attachment manifest to append
339
+ * @param guidance Optional agent guidance rules from Linear
340
+ * @returns The constructed prompt and optional version tag
341
+ */
342
+ async buildMentionPrompt(issue, agentSession, attachmentManifest = "", guidance) {
343
+ try {
344
+ this.logger.debug(`Building mention prompt for issue ${issue.identifier}`);
345
+ // Get the mention comment metadata
346
+ const mentionContent = agentSession.comment?.body || "";
347
+ const authorName = agentSession.creator?.name || agentSession.creator?.id || "Unknown";
348
+ const timestamp = agentSession.createdAt || new Date().toISOString();
349
+ // Build a focused prompt with comment metadata
350
+ let prompt = `You were mentioned in a Linear comment on this issue:
351
+
352
+ <linear_issue>
353
+ <id>${issue.id}</id>
354
+ <identifier>${issue.identifier}</identifier>
355
+ <title>${issue.title}</title>
356
+ <url>${issue.url}</url>
357
+ </linear_issue>
358
+
359
+ <mention_comment>
360
+ <author>${authorName}</author>
361
+ <timestamp>${timestamp}</timestamp>
362
+ <content>
363
+ ${mentionContent}
364
+ </content>
365
+ </mention_comment>
366
+
367
+ Focus on addressing the specific request in the mention. You can use the Linear MCP tools to fetch additional context if needed.`;
368
+ // Append agent guidance if present
369
+ prompt += this.formatAgentGuidance(guidance);
370
+ // Append attachment manifest if any
371
+ if (attachmentManifest) {
372
+ prompt = `${prompt}\n\n${attachmentManifest}`;
373
+ }
374
+ return { prompt };
375
+ }
376
+ catch (error) {
377
+ this.logger.error(`Error building mention prompt:`, error);
378
+ throw error;
379
+ }
380
+ }
381
+ /**
382
+ * Build a prompt for Claude using the improved XML-style template
383
+ * @param issue Full Linear issue
384
+ * @param repository Repository configuration
385
+ * @param newComment Optional new comment to focus on (for handleNewRootComment)
386
+ * @param attachmentManifest Optional attachment manifest
387
+ * @param guidance Optional agent guidance rules from Linear
388
+ * @returns Formatted prompt string
389
+ */
390
+ async buildIssueContextPrompt(issue, repository, newComment, attachmentManifest = "", guidance) {
391
+ this.logger.debug(`buildIssueContextPrompt called for issue ${issue.identifier}${newComment ? " with new comment" : ""}`);
392
+ try {
393
+ // Use custom template if provided (repository-specific)
394
+ let templatePath = repository.promptTemplatePath;
395
+ // If no custom template, use the standard issue assigned user prompt template
396
+ if (!templatePath) {
397
+ const __filename = fileURLToPath(import.meta.url);
398
+ const __dirname = dirname(__filename);
399
+ templatePath = resolve(__dirname, "../prompts/standard-issue-assigned-user-prompt.md");
400
+ }
401
+ // Load the template
402
+ this.logger.debug(`Loading prompt template from: ${templatePath}`);
403
+ const template = await readFile(templatePath, "utf-8");
404
+ this.logger.debug(`Template loaded, length: ${template.length} characters`);
405
+ // Extract and log version tag if present
406
+ const templateVersion = this.extractVersionTag(template);
407
+ if (templateVersion) {
408
+ this.logger.debug(`Prompt template version: ${templateVersion}`);
409
+ }
410
+ // Get state name from Linear API
411
+ const state = await issue.state;
412
+ const stateName = state?.name || "Unknown";
413
+ // Determine the base branch considering parent issues
414
+ const baseBranch = await this.determineBaseBranch(issue, repository);
415
+ // Get formatted comment threads
416
+ const issueTracker = this.issueTrackers.get(repository.id);
417
+ let commentThreads = "No comments yet.";
418
+ if (issueTracker && issue.id) {
419
+ try {
420
+ this.logger.debug(`Fetching comments for issue ${issue.identifier}`);
421
+ const comments = await issueTracker.fetchComments(issue.id);
422
+ const commentNodes = comments.nodes;
423
+ if (commentNodes.length > 0) {
424
+ commentThreads = await this.formatCommentThreads(commentNodes);
425
+ this.logger.debug(`Formatted ${commentNodes.length} comments into threads`);
426
+ }
427
+ }
428
+ catch (error) {
429
+ this.logger.error("Failed to fetch comments:", error);
430
+ }
431
+ }
432
+ // Build the prompt with all variables
433
+ let prompt = template
434
+ .replace(/{{repository_name}}/g, repository.name)
435
+ .replace(/{{issue_id}}/g, issue.id || "")
436
+ .replace(/{{issue_identifier}}/g, issue.identifier || "")
437
+ .replace(/{{issue_title}}/g, issue.title || "")
438
+ .replace(/{{issue_description}}/g, issue.description || "No description provided")
439
+ .replace(/{{issue_state}}/g, stateName)
440
+ .replace(/{{issue_priority}}/g, issue.priority?.toString() || "None")
441
+ .replace(/{{issue_url}}/g, issue.url || "")
442
+ .replace(/{{comment_threads}}/g, commentThreads)
443
+ .replace(/{{working_directory}}/g, this.config.handlers?.createWorkspace
444
+ ? "Will be created based on issue"
445
+ : repository.repositoryPath)
446
+ .replace(/{{base_branch}}/g, baseBranch)
447
+ .replace(/{{branch_name}}/g, this.gitService.sanitizeBranchName(issue.branchName));
448
+ // Handle the optional new comment section
449
+ if (newComment) {
450
+ // Replace the conditional block
451
+ const newCommentSection = `<new_comment_to_address>
452
+ <author>{{new_comment_author}}</author>
453
+ <timestamp>{{new_comment_timestamp}}</timestamp>
454
+ <content>
455
+ {{new_comment_content}}
456
+ </content>
457
+ </new_comment_to_address>
458
+
459
+ IMPORTANT: Focus specifically on addressing the new comment above. This is a new request that requires your attention.`;
460
+ prompt = prompt.replace(/{{#if new_comment}}[\s\S]*?{{\/if}}/g, newCommentSection);
461
+ // Now replace the new comment variables
462
+ // We'll need to fetch the comment author
463
+ let authorName = "Unknown";
464
+ if (issueTracker) {
465
+ try {
466
+ const fullComment = await issueTracker.fetchComment(newComment.id);
467
+ const user = await fullComment.user;
468
+ authorName =
469
+ user?.displayName || user?.name || user?.email || "Unknown";
470
+ }
471
+ catch (error) {
472
+ this.logger.error("Failed to fetch comment author:", error);
473
+ }
474
+ }
475
+ prompt = prompt
476
+ .replace(/{{new_comment_author}}/g, authorName)
477
+ .replace(/{{new_comment_timestamp}}/g, new Date().toLocaleString())
478
+ .replace(/{{new_comment_content}}/g, newComment.body || "");
479
+ }
480
+ else {
481
+ // Remove the new comment section entirely (including preceding newlines)
482
+ prompt = prompt.replace(/\n*{{#if new_comment}}[\s\S]*?{{\/if}}/g, "");
483
+ }
484
+ // Append agent guidance if present
485
+ prompt += this.formatAgentGuidance(guidance);
486
+ // Append attachment manifest if provided
487
+ if (attachmentManifest) {
488
+ this.logger.debug(`Adding attachment manifest, length: ${attachmentManifest.length} characters`);
489
+ prompt = `${prompt}\n\n${attachmentManifest}`;
490
+ }
491
+ // Append repository-specific instruction if provided
492
+ if (repository.appendInstruction) {
493
+ this.logger.debug(`Adding repository-specific instruction`);
494
+ prompt = `${prompt}\n\n<repository-specific-instruction>\n${repository.appendInstruction}\n</repository-specific-instruction>`;
495
+ }
496
+ this.logger.debug(`Final prompt length: ${prompt.length} characters`);
497
+ return { prompt, version: templateVersion };
498
+ }
499
+ catch (error) {
500
+ this.logger.error("Failed to load prompt template:", error);
501
+ // Fallback to simple prompt
502
+ const state = await issue.state;
503
+ const stateName = state?.name || "Unknown";
504
+ // Determine the base branch considering parent issues
505
+ const baseBranch = await this.determineBaseBranch(issue, repository);
506
+ const fallbackPrompt = `Please help me with the following Linear issue:
507
+
508
+ Repository: ${repository.name}
509
+ Issue: ${issue.identifier}
510
+ Title: ${issue.title}
511
+ Description: ${issue.description || "No description provided"}
512
+ State: ${stateName}
513
+ Priority: ${issue.priority?.toString() || "None"}
514
+ Branch: ${issue.branchName}
515
+
516
+ Working directory: ${repository.repositoryPath}
517
+ Base branch: ${baseBranch}
518
+
519
+ ${newComment ? `New comment to address:\n${newComment.body}\n\n` : ""}Please analyze this issue and help implement a solution.`;
520
+ return { prompt: fallbackPrompt, version: undefined };
521
+ }
522
+ }
523
+ /**
524
+ * Build XML-formatted prompt for issue content updates (title/description/attachments)
525
+ *
526
+ * The prompt clearly shows what fields changed by comparing old vs new values,
527
+ * and includes guidance for the agent to evaluate whether these changes affect
528
+ * its current implementation or action plan.
529
+ */
530
+ buildIssueUpdatePrompt(issueIdentifier, issueData, updatedFrom) {
531
+ const timestamp = new Date().toISOString();
532
+ const parts = [];
533
+ parts.push(`<issue_update>`);
534
+ parts.push(` <identifier>${issueIdentifier}</identifier>`);
535
+ parts.push(` <timestamp>${timestamp}</timestamp>`);
536
+ // Add title change if title was updated
537
+ if ("title" in updatedFrom) {
538
+ parts.push(` <title_change>`);
539
+ parts.push(` <old_title>${updatedFrom.title ?? ""}</old_title>`);
540
+ parts.push(` <new_title>${issueData.title}</new_title>`);
541
+ parts.push(` </title_change>`);
542
+ }
543
+ // Add description change if description was updated
544
+ if ("description" in updatedFrom) {
545
+ parts.push(` <description_change>`);
546
+ parts.push(` <old_description>${updatedFrom.description ?? ""}</old_description>`);
547
+ parts.push(` <new_description>${issueData.description ?? ""}</new_description>`);
548
+ parts.push(` </description_change>`);
549
+ }
550
+ // Add attachments change if attachments were updated
551
+ if ("attachments" in updatedFrom) {
552
+ parts.push(` <attachments_change>`);
553
+ parts.push(` <old_attachments>${JSON.stringify(updatedFrom.attachments ?? null)}</old_attachments>`);
554
+ parts.push(` <new_attachments>${JSON.stringify(issueData.attachments ?? null)}</new_attachments>`);
555
+ parts.push(` </attachments_change>`);
556
+ }
557
+ parts.push(`</issue_update>`);
558
+ // Add guidance for the agent on how to respond to this update
559
+ parts.push(``);
560
+ parts.push(`<guidance>`);
561
+ parts.push(` The issue has been updated while you are working on it. Please evaluate whether these changes`);
562
+ parts.push(` affect your current implementation or action plan. Consider the following:`);
563
+ parts.push(` - Does the updated content change the requirements or scope of your work?`);
564
+ parts.push(` - Are there new details, clarifications, or attachments that should inform your approach?`);
565
+ parts.push(` - Should you adjust your implementation strategy based on this update?`);
566
+ parts.push(` If the changes are relevant, incorporate them into your work. If not, you may continue as planned.`);
567
+ parts.push(`</guidance>`);
568
+ return parts.join("\n");
569
+ }
570
+ // ========================================================================
571
+ // COMMENT / GUIDANCE FORMATTING
572
+ // ========================================================================
573
+ /**
574
+ * Format Linear comments into a threaded structure that mirrors the Linear UI
575
+ * @param comments Array of Linear comments
576
+ * @returns Formatted string showing comment threads
577
+ */
578
+ async formatCommentThreads(comments) {
579
+ if (comments.length === 0) {
580
+ return "No comments yet.";
581
+ }
582
+ // Group comments by thread (root comments and their replies)
583
+ const threads = new Map();
584
+ const rootComments = [];
585
+ // First pass: identify root comments and create thread structure
586
+ for (const comment of comments) {
587
+ const parent = await comment.parent;
588
+ if (!parent) {
589
+ // This is a root comment
590
+ rootComments.push(comment);
591
+ threads.set(comment.id, { root: comment, replies: [] });
592
+ }
593
+ }
594
+ // Second pass: assign replies to their threads
595
+ for (const comment of comments) {
596
+ const parent = await comment.parent;
597
+ if (parent?.id) {
598
+ const thread = threads.get(parent.id);
599
+ if (thread) {
600
+ thread.replies.push(comment);
601
+ }
602
+ }
603
+ }
604
+ // Format threads in chronological order
605
+ const formattedThreads = [];
606
+ for (const rootComment of rootComments) {
607
+ const thread = threads.get(rootComment.id);
608
+ if (!thread)
609
+ continue;
610
+ // Format root comment
611
+ const rootUser = await rootComment.user;
612
+ const rootAuthor = rootUser?.displayName || rootUser?.name || rootUser?.email || "Unknown";
613
+ const rootTime = new Date(rootComment.createdAt).toLocaleString();
614
+ let threadText = `<comment_thread>
615
+ <root_comment>
616
+ <author>@${rootAuthor}</author>
617
+ <timestamp>${rootTime}</timestamp>
618
+ <content>
619
+ ${rootComment.body}
620
+ </content>
621
+ </root_comment>`;
622
+ // Format replies if any
623
+ if (thread.replies.length > 0) {
624
+ threadText += "\n <replies>";
625
+ for (const reply of thread.replies) {
626
+ const replyUser = await reply.user;
627
+ const replyAuthor = replyUser?.displayName ||
628
+ replyUser?.name ||
629
+ replyUser?.email ||
630
+ "Unknown";
631
+ const replyTime = new Date(reply.createdAt).toLocaleString();
632
+ threadText += `
633
+ <reply>
634
+ <author>@${replyAuthor}</author>
635
+ <timestamp>${replyTime}</timestamp>
636
+ <content>
637
+ ${reply.body}
638
+ </content>
639
+ </reply>`;
640
+ }
641
+ threadText += "\n </replies>";
642
+ }
643
+ threadText += "\n</comment_thread>";
644
+ formattedThreads.push(threadText);
645
+ }
646
+ return formattedThreads.join("\n\n");
647
+ }
648
+ /**
649
+ * Format agent guidance rules as markdown for injection into prompts
650
+ * @param guidance Array of guidance rules from Linear
651
+ * @returns Formatted markdown string with guidance, or empty string if no guidance
652
+ */
653
+ formatAgentGuidance(guidance) {
654
+ if (!guidance || guidance.length === 0) {
655
+ return "";
656
+ }
657
+ let formatted = "\n\n<agent_guidance>\nThe following guidance has been configured for this workspace/team in Linear. Team-specific guidance takes precedence over workspace-level guidance.\n";
658
+ for (const rule of guidance) {
659
+ let origin = "Global";
660
+ if (rule.origin) {
661
+ if (rule.origin.__typename === "TeamOriginWebhookPayload") {
662
+ origin = `Team (${rule.origin.team.displayName})`;
663
+ }
664
+ else {
665
+ origin = "Organization";
666
+ }
667
+ }
668
+ formatted += `\n## Guidance from ${origin}\n${rule.body}\n`;
669
+ }
670
+ formatted += "\n</agent_guidance>";
671
+ return formatted;
672
+ }
673
+ /**
674
+ * Extract version tag from template content
675
+ * @param templateContent The template content to parse
676
+ * @returns The version value if found, undefined otherwise
677
+ */
678
+ extractVersionTag(templateContent) {
679
+ // Match the version tag pattern: <version-tag value="..." />
680
+ const versionTagMatch = templateContent.match(/<version-tag\s+value="([^"]*)"\s*\/>/i);
681
+ const version = versionTagMatch ? versionTagMatch[1] : undefined;
682
+ // Return undefined for empty strings
683
+ return version?.trim() ? version : undefined;
684
+ }
685
+ // ========================================================================
686
+ // SUBROUTINE / SHARED INSTRUCTION LOADING
687
+ // ========================================================================
688
+ /**
689
+ * Load a subroutine prompt file
690
+ * Extracted helper to make prompt assembly more readable
691
+ */
692
+ async loadSubroutinePrompt(subroutine, workspaceSlug) {
693
+ // Skip loading for "primary" - it's a placeholder that doesn't have a file
694
+ if (subroutine.promptPath === "primary") {
695
+ return null;
696
+ }
697
+ const __filename = fileURLToPath(import.meta.url);
698
+ const __dirname = dirname(__filename);
699
+ const subroutinePromptPath = join(__dirname, "prompts", subroutine.promptPath);
700
+ try {
701
+ let prompt = await readFile(subroutinePromptPath, "utf-8");
702
+ this.logger.debug(`Loaded ${subroutine.name} subroutine prompt (${prompt.length} characters)`);
703
+ // Perform template substitution if workspace slug is provided
704
+ if (workspaceSlug) {
705
+ prompt = prompt.replace(/https:\/\/linear\.app\/linear\/profiles\//g, `https://linear.app/${workspaceSlug}/profiles/`);
706
+ }
707
+ return prompt;
708
+ }
709
+ catch (error) {
710
+ this.logger.warn(`Failed to load subroutine prompt from ${subroutinePromptPath}:`, error);
711
+ return null;
712
+ }
713
+ }
714
+ /**
715
+ * Load shared instructions that get appended to all system prompts
716
+ */
717
+ async loadSharedInstructions() {
718
+ const __filename = fileURLToPath(import.meta.url);
719
+ const __dirname = dirname(__filename);
720
+ const instructionsPath = join(__dirname, "..", "prompts", "todolist-system-prompt-extension.md");
721
+ try {
722
+ const instructions = await readFile(instructionsPath, "utf-8");
723
+ return instructions;
724
+ }
725
+ catch (error) {
726
+ this.logger.error(`Failed to load shared instructions from ${instructionsPath}:`, error);
727
+ return ""; // Return empty string if file can't be loaded
728
+ }
729
+ }
730
+ // ========================================================================
731
+ // BRANCH / ISSUE UTILITIES
732
+ // ========================================================================
733
+ /**
734
+ * Determine the base branch for an issue, considering parent issues and blocked-by relationships
735
+ *
736
+ * Priority order:
737
+ * 1. If issue has graphite label AND has a "blocked by" relationship, use the blocking issue's branch
738
+ * (This enables Graphite stacking where each sub-issue branches off the previous)
739
+ * 2. If issue has a parent, use the parent's branch
740
+ * 3. Fall back to repository's default base branch
741
+ */
742
+ async determineBaseBranch(issue, repository) {
743
+ // Start with the repository's default base branch
744
+ let baseBranch = repository.baseBranch;
745
+ // Check if this issue has the graphite label - if so, blocked-by relationship takes priority
746
+ const isGraphiteIssue = await this.hasGraphiteLabel(issue, repository);
747
+ if (isGraphiteIssue) {
748
+ // For Graphite stacking: use the blocking issue's branch as base
749
+ const blockingIssues = await this.fetchBlockingIssues(issue);
750
+ if (blockingIssues.length > 0) {
751
+ // Use the first blocking issue's branch (typically there's only one in a stack)
752
+ const blockingIssue = blockingIssues[0];
753
+ this.logger.debug(`Issue ${issue.identifier} has graphite label and is blocked by ${blockingIssue.identifier}`);
754
+ // Get blocking issue's branch name
755
+ const blockingRawBranchName = blockingIssue.branchName ||
756
+ `${blockingIssue.identifier}-${(blockingIssue.title ?? "")
757
+ .toLowerCase()
758
+ .replace(/\s+/g, "-")
759
+ .substring(0, 30)}`;
760
+ const blockingBranchName = this.gitService.sanitizeBranchName(blockingRawBranchName);
761
+ // Check if blocking issue's branch exists
762
+ const blockingBranchExists = await this.gitService.branchExists(blockingBranchName, repository.repositoryPath);
763
+ if (blockingBranchExists) {
764
+ baseBranch = blockingBranchName;
765
+ this.logger.debug(`Using blocking issue branch '${blockingBranchName}' as base for Graphite-stacked issue ${issue.identifier}`);
766
+ return baseBranch;
767
+ }
768
+ this.logger.debug(`Blocking issue branch '${blockingBranchName}' not found, falling back to parent/default`);
769
+ }
770
+ }
771
+ // Check if issue has a parent (standard sub-issue behavior)
772
+ try {
773
+ const parent = await issue.parent;
774
+ if (parent) {
775
+ this.logger.debug(`Issue ${issue.identifier} has parent: ${parent.identifier}`);
776
+ // Get parent's branch name
777
+ const parentRawBranchName = parent.branchName ||
778
+ `${parent.identifier}-${parent.title
779
+ ?.toLowerCase()
780
+ .replace(/\s+/g, "-")
781
+ .substring(0, 30)}`;
782
+ const parentBranchName = this.gitService.sanitizeBranchName(parentRawBranchName);
783
+ // Check if parent branch exists
784
+ const parentBranchExists = await this.gitService.branchExists(parentBranchName, repository.repositoryPath);
785
+ if (parentBranchExists) {
786
+ baseBranch = parentBranchName;
787
+ this.logger.debug(`Using parent issue branch '${parentBranchName}' as base for sub-issue ${issue.identifier}`);
788
+ }
789
+ else {
790
+ this.logger.debug(`Parent branch '${parentBranchName}' not found, using default base branch '${repository.baseBranch}'`);
791
+ }
792
+ }
793
+ }
794
+ catch (_error) {
795
+ // Parent field might not exist or couldn't be fetched, use default base branch
796
+ this.logger.debug(`No parent issue found for ${issue.identifier}, using default base branch '${repository.baseBranch}'`);
797
+ }
798
+ return baseBranch;
799
+ }
800
+ /**
801
+ * Check if an issue has the graphite label
802
+ *
803
+ * @param issue The issue to check
804
+ * @param repository The repository configuration
805
+ * @returns True if the issue has the graphite label
806
+ */
807
+ async hasGraphiteLabel(issue, repository) {
808
+ const graphiteConfig = repository.labelPrompts?.graphite;
809
+ const graphiteLabels = Array.isArray(graphiteConfig)
810
+ ? graphiteConfig
811
+ : (graphiteConfig?.labels ?? ["graphite"]);
812
+ const issueLabels = await this.fetchIssueLabels(issue);
813
+ return graphiteLabels.some((label) => issueLabels.includes(label));
814
+ }
815
+ /**
816
+ * Fetch issues that block this issue (i.e., issues this one is "blocked by")
817
+ * Uses the inverseRelations field with type "blocks"
818
+ *
819
+ * Linear relations work like this:
820
+ * - When Issue A "blocks" Issue B, a relation is created with:
821
+ * - issue = A (the blocker)
822
+ * - relatedIssue = B (the blocked one)
823
+ * - type = "blocks"
824
+ *
825
+ * So to find "who blocks Issue B", we need inverseRelations (where B is the relatedIssue)
826
+ * and look for type === "blocks", then get the `issue` field (the blocker).
827
+ *
828
+ * @param issue The issue to fetch blocking issues for
829
+ * @returns Array of issues that block this one, or empty array if none
830
+ */
831
+ async fetchBlockingIssues(issue) {
832
+ try {
833
+ // inverseRelations contains relations where THIS issue is the relatedIssue
834
+ // When type is "blocks", it means the `issue` field blocks THIS issue
835
+ const inverseRelations = await issue.inverseRelations();
836
+ if (!inverseRelations?.nodes) {
837
+ return [];
838
+ }
839
+ const blockingIssues = [];
840
+ for (const relation of inverseRelations.nodes) {
841
+ // "blocks" type in inverseRelations means the `issue` blocks this one
842
+ if (relation.type === "blocks") {
843
+ // The `issue` field is the one that blocks THIS issue
844
+ const blockingIssue = await relation.issue;
845
+ if (blockingIssue) {
846
+ blockingIssues.push(blockingIssue);
847
+ }
848
+ }
849
+ }
850
+ this.logger.debug(`Issue ${issue.identifier} is blocked by ${blockingIssues.length} issue(s): ${blockingIssues.map((i) => i.identifier).join(", ") || "none"}`);
851
+ return blockingIssues;
852
+ }
853
+ catch (error) {
854
+ this.logger.error(`Failed to fetch blocking issues for ${issue.identifier}:`, error);
855
+ return [];
856
+ }
857
+ }
858
+ /**
859
+ * Convert full Linear SDK issue to CoreIssue interface for Session creation
860
+ */
861
+ convertLinearIssueToCore(issue) {
862
+ return {
863
+ id: issue.id,
864
+ identifier: issue.identifier,
865
+ title: issue.title || "",
866
+ description: issue.description || undefined,
867
+ branchName: issue.branchName, // Use the real branchName property!
868
+ };
869
+ }
870
+ /**
871
+ * Fetch issue labels for a given issue
872
+ */
873
+ async fetchIssueLabels(issue) {
874
+ try {
875
+ const labels = await issue.labels();
876
+ return labels.nodes.map((label) => label.name);
877
+ }
878
+ catch (error) {
879
+ this.logger.error(`Failed to fetch labels for issue ${issue.id}:`, error);
880
+ return [];
881
+ }
882
+ }
883
+ }
884
+ //# sourceMappingURL=PromptBuilder.js.map