@renseiai/agentfactory-linear 0.8.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 (84) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +91 -0
  3. package/dist/src/agent-client-project-repo.test.d.ts +2 -0
  4. package/dist/src/agent-client-project-repo.test.d.ts.map +1 -0
  5. package/dist/src/agent-client-project-repo.test.js +153 -0
  6. package/dist/src/agent-client.d.ts +261 -0
  7. package/dist/src/agent-client.d.ts.map +1 -0
  8. package/dist/src/agent-client.js +902 -0
  9. package/dist/src/agent-session.d.ts +303 -0
  10. package/dist/src/agent-session.d.ts.map +1 -0
  11. package/dist/src/agent-session.js +969 -0
  12. package/dist/src/checkbox-utils.d.ts +88 -0
  13. package/dist/src/checkbox-utils.d.ts.map +1 -0
  14. package/dist/src/checkbox-utils.js +120 -0
  15. package/dist/src/circuit-breaker.d.ts +76 -0
  16. package/dist/src/circuit-breaker.d.ts.map +1 -0
  17. package/dist/src/circuit-breaker.js +229 -0
  18. package/dist/src/circuit-breaker.test.d.ts +2 -0
  19. package/dist/src/circuit-breaker.test.d.ts.map +1 -0
  20. package/dist/src/circuit-breaker.test.js +292 -0
  21. package/dist/src/constants.d.ts +87 -0
  22. package/dist/src/constants.d.ts.map +1 -0
  23. package/dist/src/constants.js +101 -0
  24. package/dist/src/defaults/auto-trigger.d.ts +35 -0
  25. package/dist/src/defaults/auto-trigger.d.ts.map +1 -0
  26. package/dist/src/defaults/auto-trigger.js +36 -0
  27. package/dist/src/defaults/index.d.ts +12 -0
  28. package/dist/src/defaults/index.d.ts.map +1 -0
  29. package/dist/src/defaults/index.js +11 -0
  30. package/dist/src/defaults/priority.d.ts +20 -0
  31. package/dist/src/defaults/priority.d.ts.map +1 -0
  32. package/dist/src/defaults/priority.js +37 -0
  33. package/dist/src/defaults/prompts.d.ts +42 -0
  34. package/dist/src/defaults/prompts.d.ts.map +1 -0
  35. package/dist/src/defaults/prompts.js +310 -0
  36. package/dist/src/defaults/prompts.test.d.ts +2 -0
  37. package/dist/src/defaults/prompts.test.d.ts.map +1 -0
  38. package/dist/src/defaults/prompts.test.js +263 -0
  39. package/dist/src/defaults/work-type-detection.d.ts +19 -0
  40. package/dist/src/defaults/work-type-detection.d.ts.map +1 -0
  41. package/dist/src/defaults/work-type-detection.js +93 -0
  42. package/dist/src/errors.d.ts +91 -0
  43. package/dist/src/errors.d.ts.map +1 -0
  44. package/dist/src/errors.js +173 -0
  45. package/dist/src/frontend-adapter.d.ts +168 -0
  46. package/dist/src/frontend-adapter.d.ts.map +1 -0
  47. package/dist/src/frontend-adapter.js +314 -0
  48. package/dist/src/frontend-adapter.test.d.ts +2 -0
  49. package/dist/src/frontend-adapter.test.d.ts.map +1 -0
  50. package/dist/src/frontend-adapter.test.js +545 -0
  51. package/dist/src/index.d.ts +28 -0
  52. package/dist/src/index.d.ts.map +1 -0
  53. package/dist/src/index.js +30 -0
  54. package/dist/src/issue-tracker-proxy.d.ts +140 -0
  55. package/dist/src/issue-tracker-proxy.d.ts.map +1 -0
  56. package/dist/src/issue-tracker-proxy.js +10 -0
  57. package/dist/src/platform-adapter.d.ts +132 -0
  58. package/dist/src/platform-adapter.d.ts.map +1 -0
  59. package/dist/src/platform-adapter.js +260 -0
  60. package/dist/src/platform-adapter.test.d.ts +2 -0
  61. package/dist/src/platform-adapter.test.d.ts.map +1 -0
  62. package/dist/src/platform-adapter.test.js +468 -0
  63. package/dist/src/proxy-client.d.ts +103 -0
  64. package/dist/src/proxy-client.d.ts.map +1 -0
  65. package/dist/src/proxy-client.js +191 -0
  66. package/dist/src/rate-limiter.d.ts +64 -0
  67. package/dist/src/rate-limiter.d.ts.map +1 -0
  68. package/dist/src/rate-limiter.js +163 -0
  69. package/dist/src/rate-limiter.test.d.ts +2 -0
  70. package/dist/src/rate-limiter.test.d.ts.map +1 -0
  71. package/dist/src/rate-limiter.test.js +217 -0
  72. package/dist/src/retry.d.ts +59 -0
  73. package/dist/src/retry.d.ts.map +1 -0
  74. package/dist/src/retry.js +82 -0
  75. package/dist/src/types.d.ts +492 -0
  76. package/dist/src/types.d.ts.map +1 -0
  77. package/dist/src/types.js +143 -0
  78. package/dist/src/utils.d.ts +52 -0
  79. package/dist/src/utils.d.ts.map +1 -0
  80. package/dist/src/utils.js +277 -0
  81. package/dist/src/webhook-types.d.ts +308 -0
  82. package/dist/src/webhook-types.d.ts.map +1 -0
  83. package/dist/src/webhook-types.js +46 -0
  84. package/package.json +70 -0
@@ -0,0 +1,545 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { LinearFrontendAdapter } from './frontend-adapter.js';
3
+ // ---------------------------------------------------------------------------
4
+ // Mock helpers
5
+ // ---------------------------------------------------------------------------
6
+ /**
7
+ * Create a mock Linear SDK Issue object.
8
+ */
9
+ function mockLinearIssue(overrides = {}) {
10
+ const { id = 'issue-uuid-1', identifier = 'SUP-100', title = 'Test Issue', description = 'A test issue description', url = 'https://linear.app/team/issue/SUP-100', priority = 2, createdAt = new Date('2025-01-15T10:00:00Z'), stateName = 'Backlog', labels = ['Feature'], parentId = null, projectName = 'MyProject', } = overrides;
11
+ return {
12
+ id,
13
+ identifier,
14
+ title,
15
+ description,
16
+ url,
17
+ priority,
18
+ createdAt,
19
+ get state() {
20
+ return Promise.resolve(stateName ? { name: stateName } : null);
21
+ },
22
+ labels: () => Promise.resolve({ nodes: labels.map((name) => ({ name })) }),
23
+ get parent() {
24
+ return Promise.resolve(parentId ? { id: parentId } : null);
25
+ },
26
+ get project() {
27
+ return Promise.resolve(projectName ? { name: projectName } : null);
28
+ },
29
+ get team() {
30
+ return Promise.resolve({ id: 'team-1', name: 'Engineering' });
31
+ },
32
+ };
33
+ }
34
+ /**
35
+ * Create a mock Linear SDK Comment object.
36
+ */
37
+ function mockLinearComment(overrides = {}) {
38
+ const { id = 'comment-uuid-1', body = 'Test comment', userId = 'user-1', userName = 'Test User', createdAt = new Date('2025-01-15T12:00:00Z'), } = overrides;
39
+ return {
40
+ id,
41
+ body,
42
+ createdAt,
43
+ get user() {
44
+ return Promise.resolve({ id: userId, name: userName });
45
+ },
46
+ };
47
+ }
48
+ /**
49
+ * Create a mock LinearAgentClient with all methods stubbed.
50
+ */
51
+ function createMockClient() {
52
+ const mocks = {
53
+ getIssue: vi.fn(),
54
+ updateIssue: vi.fn(),
55
+ getTeamStatuses: vi.fn(),
56
+ updateIssueStatus: vi.fn(),
57
+ createComment: vi.fn(),
58
+ getIssueComments: vi.fn(),
59
+ createIssue: vi.fn(),
60
+ isParentIssue: vi.fn(),
61
+ isChildIssue: vi.fn(),
62
+ getSubIssues: vi.fn(),
63
+ getIssueRelations: vi.fn(),
64
+ createIssueRelation: vi.fn(),
65
+ createAgentSessionOnIssue: vi.fn(),
66
+ updateAgentSession: vi.fn(),
67
+ createAgentActivity: vi.fn(),
68
+ };
69
+ const linearClient = {
70
+ issues: vi.fn(),
71
+ issueLabels: vi.fn(),
72
+ };
73
+ const client = {
74
+ ...mocks,
75
+ linearClient,
76
+ };
77
+ // Store the linearClient mock for direct access
78
+ mocks.linearClientIssues = linearClient.issues;
79
+ mocks.linearClientIssueLabels = linearClient.issueLabels;
80
+ return { client, mocks };
81
+ }
82
+ // ---------------------------------------------------------------------------
83
+ // Tests
84
+ // ---------------------------------------------------------------------------
85
+ describe('LinearFrontendAdapter', () => {
86
+ let adapter;
87
+ let mocks;
88
+ beforeEach(() => {
89
+ const { client, mocks: m } = createMockClient();
90
+ mocks = m;
91
+ adapter = new LinearFrontendAdapter(client);
92
+ });
93
+ // ========================================================================
94
+ // name
95
+ // ========================================================================
96
+ it('has name "linear"', () => {
97
+ expect(adapter.name).toBe('linear');
98
+ });
99
+ // ========================================================================
100
+ // Status mapping
101
+ // ========================================================================
102
+ describe('resolveStatus', () => {
103
+ const cases = [
104
+ ['icebox', 'Icebox'],
105
+ ['backlog', 'Backlog'],
106
+ ['started', 'Started'],
107
+ ['finished', 'Finished'],
108
+ ['delivered', 'Delivered'],
109
+ ['accepted', 'Accepted'],
110
+ ['rejected', 'Rejected'],
111
+ ['canceled', 'Canceled'],
112
+ ];
113
+ it.each(cases)('maps "%s" to "%s"', (abstract, expected) => {
114
+ expect(adapter.resolveStatus(abstract)).toBe(expected);
115
+ });
116
+ });
117
+ describe('abstractStatus', () => {
118
+ const cases = [
119
+ ['Icebox', 'icebox'],
120
+ ['Backlog', 'backlog'],
121
+ ['Started', 'started'],
122
+ ['Finished', 'finished'],
123
+ ['Delivered', 'delivered'],
124
+ ['Accepted', 'accepted'],
125
+ ['Rejected', 'rejected'],
126
+ ['Canceled', 'canceled'],
127
+ ];
128
+ it.each(cases)('maps "%s" to "%s"', (native, expected) => {
129
+ expect(adapter.abstractStatus(native)).toBe(expected);
130
+ });
131
+ it('defaults unknown statuses to "backlog"', () => {
132
+ expect(adapter.abstractStatus('UnknownStatus')).toBe('backlog');
133
+ });
134
+ });
135
+ describe('status mapping round-trip', () => {
136
+ const allStatuses = [
137
+ 'icebox', 'backlog', 'started', 'finished',
138
+ 'delivered', 'accepted', 'rejected', 'canceled',
139
+ ];
140
+ it.each(allStatuses)('round-trips "%s" through resolve -> abstract', (status) => {
141
+ const native = adapter.resolveStatus(status);
142
+ const roundTripped = adapter.abstractStatus(native);
143
+ expect(roundTripped).toBe(status);
144
+ });
145
+ });
146
+ // ========================================================================
147
+ // getIssue
148
+ // ========================================================================
149
+ describe('getIssue', () => {
150
+ it('maps a Linear Issue to AbstractIssue', async () => {
151
+ const linearIssue = mockLinearIssue({
152
+ id: 'uuid-42',
153
+ identifier: 'SUP-42',
154
+ title: 'Implement feature X',
155
+ description: 'Full description here',
156
+ url: 'https://linear.app/team/issue/SUP-42',
157
+ priority: 1,
158
+ stateName: 'Started',
159
+ labels: ['Bug', 'Urgent'],
160
+ parentId: 'parent-uuid',
161
+ projectName: 'Alpha',
162
+ });
163
+ mocks.getIssue.mockResolvedValue(linearIssue);
164
+ const result = await adapter.getIssue('SUP-42');
165
+ expect(mocks.getIssue).toHaveBeenCalledWith('SUP-42');
166
+ expect(result).toEqual({
167
+ id: 'uuid-42',
168
+ identifier: 'SUP-42',
169
+ title: 'Implement feature X',
170
+ description: 'Full description here',
171
+ url: 'https://linear.app/team/issue/SUP-42',
172
+ status: 'started',
173
+ priority: 1,
174
+ labels: ['Bug', 'Urgent'],
175
+ parentId: 'parent-uuid',
176
+ project: 'Alpha',
177
+ createdAt: linearIssue.createdAt,
178
+ });
179
+ });
180
+ it('handles issue with no description, parent, or project', async () => {
181
+ const linearIssue = mockLinearIssue({
182
+ description: null,
183
+ parentId: null,
184
+ projectName: null,
185
+ });
186
+ mocks.getIssue.mockResolvedValue(linearIssue);
187
+ const result = await adapter.getIssue('SUP-100');
188
+ expect(result.description).toBeUndefined();
189
+ expect(result.parentId).toBeUndefined();
190
+ expect(result.project).toBeUndefined();
191
+ });
192
+ });
193
+ // ========================================================================
194
+ // transitionIssue
195
+ // ========================================================================
196
+ describe('transitionIssue', () => {
197
+ it('delegates to updateIssueStatus for standard statuses', async () => {
198
+ mocks.updateIssueStatus.mockResolvedValue(mockLinearIssue());
199
+ await adapter.transitionIssue('issue-1', 'started');
200
+ expect(mocks.updateIssueStatus).toHaveBeenCalledWith('issue-1', 'Started');
201
+ });
202
+ it('handles icebox status via updateIssue (not in LinearWorkflowStatus)', async () => {
203
+ const issue = mockLinearIssue();
204
+ mocks.getIssue.mockResolvedValue(issue);
205
+ mocks.getTeamStatuses.mockResolvedValue({ Icebox: 'state-icebox-id' });
206
+ mocks.updateIssue.mockResolvedValue(issue);
207
+ await adapter.transitionIssue('issue-1', 'icebox');
208
+ expect(mocks.updateIssue).toHaveBeenCalledWith('issue-1', { stateId: 'state-icebox-id' });
209
+ });
210
+ it.each([
211
+ ['backlog', 'Backlog'],
212
+ ['finished', 'Finished'],
213
+ ['delivered', 'Delivered'],
214
+ ['accepted', 'Accepted'],
215
+ ['rejected', 'Rejected'],
216
+ ['canceled', 'Canceled'],
217
+ ])('transitions "%s" using updateIssueStatus with "%s"', async (abstract, native) => {
218
+ mocks.updateIssueStatus.mockResolvedValue(mockLinearIssue());
219
+ await adapter.transitionIssue('issue-1', abstract);
220
+ expect(mocks.updateIssueStatus).toHaveBeenCalledWith('issue-1', native);
221
+ });
222
+ });
223
+ // ========================================================================
224
+ // createComment
225
+ // ========================================================================
226
+ describe('createComment', () => {
227
+ it('delegates to client.createComment', async () => {
228
+ mocks.createComment.mockResolvedValue(mockLinearComment());
229
+ await adapter.createComment('issue-1', 'Hello from adapter');
230
+ expect(mocks.createComment).toHaveBeenCalledWith('issue-1', 'Hello from adapter');
231
+ });
232
+ });
233
+ // ========================================================================
234
+ // getIssueComments
235
+ // ========================================================================
236
+ describe('getIssueComments', () => {
237
+ it('maps Linear comments to AbstractComment[]', async () => {
238
+ const comments = [
239
+ mockLinearComment({ id: 'c1', body: 'First', userId: 'u1', userName: 'Alice' }),
240
+ mockLinearComment({ id: 'c2', body: 'Second', userId: 'u2', userName: 'Bob' }),
241
+ ];
242
+ mocks.getIssueComments.mockResolvedValue(comments);
243
+ const result = await adapter.getIssueComments('issue-1');
244
+ expect(result).toHaveLength(2);
245
+ expect(result[0]).toMatchObject({ id: 'c1', body: 'First', userId: 'u1', userName: 'Alice' });
246
+ expect(result[1]).toMatchObject({ id: 'c2', body: 'Second', userId: 'u2', userName: 'Bob' });
247
+ });
248
+ });
249
+ // ========================================================================
250
+ // isParentIssue
251
+ // ========================================================================
252
+ describe('isParentIssue', () => {
253
+ it('delegates to client.isParentIssue', async () => {
254
+ mocks.isParentIssue.mockResolvedValue(true);
255
+ const result = await adapter.isParentIssue('issue-1');
256
+ expect(mocks.isParentIssue).toHaveBeenCalledWith('issue-1');
257
+ expect(result).toBe(true);
258
+ });
259
+ it('returns false when issue has no children', async () => {
260
+ mocks.isParentIssue.mockResolvedValue(false);
261
+ const result = await adapter.isParentIssue('issue-1');
262
+ expect(result).toBe(false);
263
+ });
264
+ });
265
+ // ========================================================================
266
+ // getSubIssues
267
+ // ========================================================================
268
+ describe('getSubIssues', () => {
269
+ it('maps sub-issues to AbstractIssue[]', async () => {
270
+ const subIssues = [
271
+ mockLinearIssue({ id: 'sub-1', identifier: 'SUP-101', title: 'Sub 1', stateName: 'Backlog' }),
272
+ mockLinearIssue({ id: 'sub-2', identifier: 'SUP-102', title: 'Sub 2', stateName: 'Started' }),
273
+ ];
274
+ mocks.getSubIssues.mockResolvedValue(subIssues);
275
+ const result = await adapter.getSubIssues('parent-1');
276
+ expect(mocks.getSubIssues).toHaveBeenCalledWith('parent-1');
277
+ expect(result).toHaveLength(2);
278
+ expect(result[0].identifier).toBe('SUP-101');
279
+ expect(result[0].status).toBe('backlog');
280
+ expect(result[1].identifier).toBe('SUP-102');
281
+ expect(result[1].status).toBe('started');
282
+ });
283
+ });
284
+ // ========================================================================
285
+ // createAgentSession
286
+ // ========================================================================
287
+ describe('createAgentSession', () => {
288
+ it('delegates to createAgentSessionOnIssue and returns sessionId', async () => {
289
+ mocks.createAgentSessionOnIssue.mockResolvedValue({
290
+ success: true,
291
+ sessionId: 'session-123',
292
+ });
293
+ const sessionId = await adapter.createAgentSession('issue-1', [
294
+ { label: 'Dashboard', url: 'https://example.com/dash' },
295
+ ]);
296
+ expect(mocks.createAgentSessionOnIssue).toHaveBeenCalledWith({
297
+ issueId: 'issue-1',
298
+ externalUrls: [{ label: 'Dashboard', url: 'https://example.com/dash' }],
299
+ });
300
+ expect(sessionId).toBe('session-123');
301
+ });
302
+ it('throws when sessionId is not returned', async () => {
303
+ mocks.createAgentSessionOnIssue.mockResolvedValue({
304
+ success: true,
305
+ sessionId: undefined,
306
+ });
307
+ await expect(adapter.createAgentSession('issue-1')).rejects.toThrow('Failed to create agent session on issue: issue-1');
308
+ });
309
+ });
310
+ // ========================================================================
311
+ // updateAgentSession
312
+ // ========================================================================
313
+ describe('updateAgentSession', () => {
314
+ it('delegates to client.updateAgentSession', async () => {
315
+ mocks.updateAgentSession.mockResolvedValue({ success: true });
316
+ await adapter.updateAgentSession('session-1', {
317
+ externalUrls: [{ label: 'Logs', url: 'https://example.com/logs' }],
318
+ plan: [{ content: 'Step 1', status: 'pending' }],
319
+ });
320
+ expect(mocks.updateAgentSession).toHaveBeenCalledWith({
321
+ sessionId: 'session-1',
322
+ externalUrls: [{ label: 'Logs', url: 'https://example.com/logs' }],
323
+ plan: [{ content: 'Step 1', status: 'pending' }],
324
+ });
325
+ });
326
+ });
327
+ // ========================================================================
328
+ // createActivity
329
+ // ========================================================================
330
+ describe('createActivity', () => {
331
+ it('wraps content as ThoughtActivityContent and delegates', async () => {
332
+ mocks.createAgentActivity.mockResolvedValue({ success: true });
333
+ await adapter.createActivity('session-1', 'thought', 'Analyzing the codebase');
334
+ expect(mocks.createAgentActivity).toHaveBeenCalledWith({
335
+ agentSessionId: 'session-1',
336
+ content: {
337
+ type: 'thought',
338
+ body: 'Analyzing the codebase',
339
+ },
340
+ });
341
+ });
342
+ });
343
+ // ========================================================================
344
+ // createIssue
345
+ // ========================================================================
346
+ describe('createIssue', () => {
347
+ it('creates an issue and maps result to AbstractIssue', async () => {
348
+ const createdIssue = mockLinearIssue({
349
+ id: 'new-uuid',
350
+ identifier: 'SUP-200',
351
+ title: 'New Issue',
352
+ stateName: 'Backlog',
353
+ });
354
+ mocks.getTeamStatuses.mockResolvedValue({ Backlog: 'state-backlog-id' });
355
+ mocks.createIssue.mockResolvedValue(createdIssue);
356
+ const result = await adapter.createIssue({
357
+ title: 'New Issue',
358
+ teamId: 'team-1',
359
+ description: 'Issue description',
360
+ status: 'backlog',
361
+ parentId: 'parent-1',
362
+ priority: 2,
363
+ });
364
+ expect(mocks.createIssue).toHaveBeenCalledWith({
365
+ title: 'New Issue',
366
+ teamId: 'team-1',
367
+ description: 'Issue description',
368
+ stateId: 'state-backlog-id',
369
+ parentId: 'parent-1',
370
+ priority: 2,
371
+ projectId: undefined,
372
+ });
373
+ expect(result.identifier).toBe('SUP-200');
374
+ expect(result.status).toBe('backlog');
375
+ });
376
+ it('creates an issue without status resolution when status not provided', async () => {
377
+ const createdIssue = mockLinearIssue({ id: 'new-uuid', identifier: 'SUP-201' });
378
+ mocks.createIssue.mockResolvedValue(createdIssue);
379
+ await adapter.createIssue({
380
+ title: 'Simple Issue',
381
+ teamId: 'team-1',
382
+ });
383
+ expect(mocks.getTeamStatuses).not.toHaveBeenCalled();
384
+ expect(mocks.createIssue).toHaveBeenCalledWith({
385
+ title: 'Simple Issue',
386
+ teamId: 'team-1',
387
+ description: undefined,
388
+ stateId: undefined,
389
+ parentId: undefined,
390
+ priority: undefined,
391
+ projectId: undefined,
392
+ });
393
+ });
394
+ });
395
+ // ========================================================================
396
+ // listIssuesByStatus
397
+ // ========================================================================
398
+ describe('listIssuesByStatus', () => {
399
+ it('queries LinearClient with project and status filters', async () => {
400
+ const issues = [
401
+ mockLinearIssue({ id: 'i1', identifier: 'SUP-10', stateName: 'Backlog' }),
402
+ mockLinearIssue({ id: 'i2', identifier: 'SUP-11', stateName: 'Backlog' }),
403
+ ];
404
+ mocks.linearClientIssues.mockResolvedValue({ nodes: issues });
405
+ const result = await adapter.listIssuesByStatus('MyProject', 'backlog');
406
+ expect(mocks.linearClientIssues).toHaveBeenCalledWith({
407
+ filter: {
408
+ project: { name: { eq: 'MyProject' } },
409
+ state: { name: { eq: 'Backlog' } },
410
+ },
411
+ });
412
+ expect(result).toHaveLength(2);
413
+ expect(result[0].identifier).toBe('SUP-10');
414
+ expect(result[1].identifier).toBe('SUP-11');
415
+ });
416
+ });
417
+ // ========================================================================
418
+ // getUnblockedIssues
419
+ // ========================================================================
420
+ describe('getUnblockedIssues', () => {
421
+ it('returns only issues without incoming "blocks" relations', async () => {
422
+ const issues = [
423
+ mockLinearIssue({ id: 'i1', identifier: 'SUP-10', stateName: 'Backlog' }),
424
+ mockLinearIssue({ id: 'i2', identifier: 'SUP-11', stateName: 'Backlog' }),
425
+ mockLinearIssue({ id: 'i3', identifier: 'SUP-12', stateName: 'Backlog' }),
426
+ ];
427
+ mocks.linearClientIssues.mockResolvedValue({ nodes: issues });
428
+ // i1 is unblocked, i2 is blocked, i3 is unblocked
429
+ mocks.getIssueRelations
430
+ .mockResolvedValueOnce({ relations: [], inverseRelations: [] })
431
+ .mockResolvedValueOnce({
432
+ relations: [],
433
+ inverseRelations: [{ id: 'rel-1', type: 'blocks', issueId: 'blocker-id', relatedIssueId: 'i2' }],
434
+ })
435
+ .mockResolvedValueOnce({ relations: [], inverseRelations: [] });
436
+ const result = await adapter.getUnblockedIssues('MyProject', 'backlog');
437
+ expect(result).toHaveLength(2);
438
+ expect(result[0].identifier).toBe('SUP-10');
439
+ expect(result[1].identifier).toBe('SUP-12');
440
+ });
441
+ it('returns all issues when none are blocked', async () => {
442
+ const issues = [
443
+ mockLinearIssue({ id: 'i1', identifier: 'SUP-10', stateName: 'Backlog' }),
444
+ ];
445
+ mocks.linearClientIssues.mockResolvedValue({ nodes: issues });
446
+ mocks.getIssueRelations.mockResolvedValue({ relations: [], inverseRelations: [] });
447
+ const result = await adapter.getUnblockedIssues('MyProject', 'backlog');
448
+ expect(result).toHaveLength(1);
449
+ expect(result[0].identifier).toBe('SUP-10');
450
+ });
451
+ it('returns empty array when all issues are blocked', async () => {
452
+ const issues = [
453
+ mockLinearIssue({ id: 'i1', identifier: 'SUP-10', stateName: 'Backlog' }),
454
+ ];
455
+ mocks.linearClientIssues.mockResolvedValue({ nodes: issues });
456
+ mocks.getIssueRelations.mockResolvedValue({
457
+ relations: [],
458
+ inverseRelations: [{ id: 'rel-1', type: 'blocks', issueId: 'blocker-id', relatedIssueId: 'i1' }],
459
+ });
460
+ const result = await adapter.getUnblockedIssues('MyProject', 'backlog');
461
+ expect(result).toHaveLength(0);
462
+ });
463
+ });
464
+ // ========================================================================
465
+ // isChildIssue
466
+ // ========================================================================
467
+ describe('isChildIssue', () => {
468
+ it('delegates to client.isChildIssue', async () => {
469
+ mocks.isChildIssue.mockResolvedValue(true);
470
+ const result = await adapter.isChildIssue('issue-1');
471
+ expect(mocks.isChildIssue).toHaveBeenCalledWith('issue-1');
472
+ expect(result).toBe(true);
473
+ });
474
+ it('returns false when issue has no parent', async () => {
475
+ mocks.isChildIssue.mockResolvedValue(false);
476
+ const result = await adapter.isChildIssue('issue-1');
477
+ expect(result).toBe(false);
478
+ });
479
+ });
480
+ // ========================================================================
481
+ // createBlockerIssue
482
+ // ========================================================================
483
+ describe('createBlockerIssue', () => {
484
+ it('creates a blocker issue in Icebox with blocking relation', async () => {
485
+ const sourceIssue = mockLinearIssue({ id: 'source-1' });
486
+ mocks.getIssue.mockResolvedValue(sourceIssue);
487
+ mocks.getTeamStatuses.mockResolvedValue({ Icebox: 'state-icebox-id', Backlog: 'state-backlog-id' });
488
+ mocks.linearClientIssueLabels.mockResolvedValue({
489
+ nodes: [{ id: 'label-1', name: 'Needs Human' }, { id: 'label-2', name: 'Bug' }],
490
+ });
491
+ const createdIssue = mockLinearIssue({
492
+ id: 'blocker-uuid',
493
+ identifier: 'SUP-300',
494
+ title: 'Human review needed',
495
+ stateName: 'Icebox',
496
+ });
497
+ mocks.createIssue.mockResolvedValue(createdIssue);
498
+ mocks.createIssueRelation.mockResolvedValue({ success: true, relationId: 'rel-1' });
499
+ const result = await adapter.createBlockerIssue('source-1', {
500
+ title: 'Human review needed',
501
+ description: 'This needs human review',
502
+ });
503
+ expect(mocks.createIssue).toHaveBeenCalledWith({
504
+ title: 'Human review needed',
505
+ description: 'This needs human review',
506
+ teamId: 'team-1',
507
+ projectId: undefined,
508
+ stateId: 'state-icebox-id',
509
+ labelIds: ['label-1'],
510
+ });
511
+ expect(mocks.createIssueRelation).toHaveBeenCalledWith({
512
+ issueId: 'blocker-uuid',
513
+ relatedIssueId: 'source-1',
514
+ type: 'blocks',
515
+ });
516
+ expect(result.identifier).toBe('SUP-300');
517
+ });
518
+ it('uses provided teamId instead of looking up from source issue', async () => {
519
+ mocks.getTeamStatuses.mockResolvedValue({ Icebox: 'state-icebox-id' });
520
+ mocks.linearClientIssueLabels.mockResolvedValue({ nodes: [] });
521
+ const createdIssue = mockLinearIssue({ id: 'blocker-uuid', identifier: 'SUP-301' });
522
+ mocks.createIssue.mockResolvedValue(createdIssue);
523
+ mocks.createIssueRelation.mockResolvedValue({ success: true, relationId: 'rel-1' });
524
+ await adapter.createBlockerIssue('source-1', {
525
+ title: 'Blocker',
526
+ teamId: 'explicit-team',
527
+ });
528
+ // Should NOT call getIssue since teamId was provided
529
+ expect(mocks.getIssue).not.toHaveBeenCalled();
530
+ expect(mocks.getTeamStatuses).toHaveBeenCalledWith('explicit-team');
531
+ });
532
+ it('creates issue without label when "Needs Human" label not found', async () => {
533
+ const sourceIssue = mockLinearIssue({ id: 'source-1' });
534
+ mocks.getIssue.mockResolvedValue(sourceIssue);
535
+ mocks.getTeamStatuses.mockResolvedValue({ Icebox: 'state-icebox-id' });
536
+ mocks.linearClientIssueLabels.mockResolvedValue({ nodes: [{ id: 'l1', name: 'Bug' }] });
537
+ const createdIssue = mockLinearIssue({ id: 'blocker-uuid', identifier: 'SUP-302' });
538
+ mocks.createIssue.mockResolvedValue(createdIssue);
539
+ mocks.createIssueRelation.mockResolvedValue({ success: true });
540
+ await adapter.createBlockerIssue('source-1', { title: 'Blocker' });
541
+ // Should not include labelIds since "Needs Human" was not found
542
+ expect(mocks.createIssue).toHaveBeenCalledWith(expect.not.objectContaining({ labelIds: expect.anything() }));
543
+ });
544
+ });
545
+ });
@@ -0,0 +1,28 @@
1
+ export type { AgentSessionState, AgentActivityType, AgentActivitySignal, AgentActivityContent, AgentActivityContentPayload, ThoughtActivityContent, ActionActivityContent, ResponseActivityContent, ElicitationActivityContent, ErrorActivityContent, PromptActivityContent, AgentActivityCreateInput, AgentActivityResult, CreateActivityOptions, AgentPlanItemState, AgentPlanItem, AgentPlan, AgentSignals, LinearAgentClientConfig, RetryConfig, LinearWorkflowStatus, StatusMapping, AgentSessionConfig, SessionOperationResult, AgentSessionExternalUrl, AgentSessionUpdateInput, AgentSessionUpdateResult, AgentSessionCreateOnIssueInput, AgentSessionCreateResult, AgentWorkType, IssueRelationType, IssueRelationCreateInput, IssueRelationResult, IssueRelationBatchResult, IssueRelationInfo, IssueRelationsResult, SubIssueGraphNode, SubIssueGraph, SubIssueStatus, RateLimiterStrategy, CircuitBreakerStrategy, CircuitBreakerConfig, LinearApiQuota, } from './types.js';
2
+ export { STATUS_WORK_TYPE_MAP, WORK_TYPE_START_STATUS, WORK_TYPE_COMPLETE_STATUS, WORK_TYPE_FAIL_STATUS, WORK_TYPE_ALLOWED_STATUSES, STATUS_VALID_WORK_TYPES, TERMINAL_STATUSES, WORK_TYPES_REQUIRING_WORKTREE, validateWorkTypeForStatus, getValidWorkTypesForStatus, } from './types.js';
3
+ export type { WorkTypeValidationResult } from './types.js';
4
+ export { LinearAgentError, LinearApiError, LinearRetryExhaustedError, LinearSessionError, LinearActivityError, LinearPlanError, LinearStatusTransitionError, AgentSpawnError, CircuitOpenError, isLinearAgentError, isRetryableError, isAgentSpawnError, isCircuitOpenError, } from './errors.js';
5
+ export { DEFAULT_RETRY_CONFIG, sleep, calculateDelay, withRetry, createRetryWrapper, } from './retry.js';
6
+ export type { RetryContext, RetryCallback, WithRetryOptions } from './retry.js';
7
+ export { LINEAR_COMMENT_MAX_LENGTH, TRUNCATION_MARKER, MAX_COMPLETION_COMMENTS, COMMENT_OVERHEAD, CONTINUATION_MARKER, getDefaultTeamId, getDefaultTeamName, LINEAR_PROJECTS, LINEAR_LABELS, ENVIRONMENT_ISSUE_TYPES, } from './constants.js';
8
+ export type { EnvironmentIssueType } from './constants.js';
9
+ export { truncateText, buildCompletionComment, splitContentIntoComments, buildCompletionComments, } from './utils.js';
10
+ export type { CommentChunk } from './utils.js';
11
+ export { parseCheckboxes, updateCheckbox, updateCheckboxByText, updateCheckboxes, hasCheckboxes, getCheckboxSummary, } from './checkbox-utils.js';
12
+ export type { CheckboxItem, CheckboxUpdate } from './checkbox-utils.js';
13
+ export { TokenBucket, DEFAULT_RATE_LIMIT_CONFIG, extractRetryAfterMs } from './rate-limiter.js';
14
+ export type { TokenBucketConfig } from './rate-limiter.js';
15
+ export { CircuitBreaker, DEFAULT_CIRCUIT_BREAKER_CONFIG } from './circuit-breaker.js';
16
+ export type { CircuitState } from './circuit-breaker.js';
17
+ export type { ProxyRequest, ProxyResponse, IssueTrackerMethod, SerializedIssue, SerializedComment, SerializedViewer, SerializedTeam, ProxyHealthStatus, } from './issue-tracker-proxy.js';
18
+ export { ProxyIssueTrackerClient, createProxyClientIfConfigured } from './proxy-client.js';
19
+ export type { ProxyClientConfig } from './proxy-client.js';
20
+ export { LinearAgentClient, createLinearAgentClient } from './agent-client.js';
21
+ export { AgentSession, createAgentSession } from './agent-session.js';
22
+ export * from './webhook-types.js';
23
+ export { LinearFrontendAdapter } from './frontend-adapter.js';
24
+ export type { AbstractStatus as LinearAbstractStatus, AbstractIssue as LinearAbstractIssue, AbstractComment as LinearAbstractComment, ExternalUrl as LinearExternalUrl, CreateIssueInput as LinearCreateIssueInput, UpdateSessionInput as LinearUpdateSessionInput, } from './frontend-adapter.js';
25
+ export { defaultGeneratePrompt, defaultBuildParentQAContext, defaultBuildParentAcceptanceContext, buildFailureContextBlock, WORK_RESULT_MARKER_INSTRUCTION, PR_SELECTION_GUIDANCE, defaultDetectWorkTypeFromPrompt, defaultGetPriority, defaultParseAutoTriggerConfig, type DefaultAutoTriggerConfig, type WorkflowContext, } from './defaults/index.js';
26
+ export { LinearPlatformAdapter } from './platform-adapter.js';
27
+ export type { GovernorIssue as LinearGovernorIssue } from './platform-adapter.js';
28
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,30 @@
1
+ // Work type mappings for status-based routing
2
+ export { STATUS_WORK_TYPE_MAP, WORK_TYPE_START_STATUS, WORK_TYPE_COMPLETE_STATUS, WORK_TYPE_FAIL_STATUS, WORK_TYPE_ALLOWED_STATUSES, STATUS_VALID_WORK_TYPES, TERMINAL_STATUSES, WORK_TYPES_REQUIRING_WORKTREE, validateWorkTypeForStatus, getValidWorkTypesForStatus, } from './types.js';
3
+ // Errors
4
+ export { LinearAgentError, LinearApiError, LinearRetryExhaustedError, LinearSessionError, LinearActivityError, LinearPlanError, LinearStatusTransitionError, AgentSpawnError, CircuitOpenError, isLinearAgentError, isRetryableError, isAgentSpawnError, isCircuitOpenError, } from './errors.js';
5
+ // Retry utilities
6
+ export { DEFAULT_RETRY_CONFIG, sleep, calculateDelay, withRetry, createRetryWrapper, } from './retry.js';
7
+ // Constants
8
+ export { LINEAR_COMMENT_MAX_LENGTH, TRUNCATION_MARKER, MAX_COMPLETION_COMMENTS, COMMENT_OVERHEAD, CONTINUATION_MARKER, getDefaultTeamId, getDefaultTeamName, LINEAR_PROJECTS, LINEAR_LABELS, ENVIRONMENT_ISSUE_TYPES, } from './constants.js';
9
+ // Utilities
10
+ export { truncateText, buildCompletionComment, splitContentIntoComments, buildCompletionComments, } from './utils.js';
11
+ // Checkbox utilities
12
+ export { parseCheckboxes, updateCheckbox, updateCheckboxByText, updateCheckboxes, hasCheckboxes, getCheckboxSummary, } from './checkbox-utils.js';
13
+ // Rate limiter
14
+ export { TokenBucket, DEFAULT_RATE_LIMIT_CONFIG, extractRetryAfterMs } from './rate-limiter.js';
15
+ // Circuit breaker
16
+ export { CircuitBreaker, DEFAULT_CIRCUIT_BREAKER_CONFIG } from './circuit-breaker.js';
17
+ // Proxy client (routes calls through dashboard instead of Linear directly)
18
+ export { ProxyIssueTrackerClient, createProxyClientIfConfigured } from './proxy-client.js';
19
+ // Client
20
+ export { LinearAgentClient, createLinearAgentClient } from './agent-client.js';
21
+ // Session
22
+ export { AgentSession, createAgentSession } from './agent-session.js';
23
+ // Webhook types
24
+ export * from './webhook-types.js';
25
+ // Frontend adapter
26
+ export { LinearFrontendAdapter } from './frontend-adapter.js';
27
+ // Default implementations (prompt templates, work type detection, priority, auto-trigger)
28
+ export { defaultGeneratePrompt, defaultBuildParentQAContext, defaultBuildParentAcceptanceContext, buildFailureContextBlock, WORK_RESULT_MARKER_INSTRUCTION, PR_SELECTION_GUIDANCE, defaultDetectWorkTypeFromPrompt, defaultGetPriority, defaultParseAutoTriggerConfig, } from './defaults/index.js';
29
+ // Platform adapter (Governor event integration)
30
+ export { LinearPlatformAdapter } from './platform-adapter.js';