@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,468 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { LinearPlatformAdapter } from './platform-adapter.js';
3
+ // ---------------------------------------------------------------------------
4
+ // Mock helpers
5
+ // ---------------------------------------------------------------------------
6
+ /**
7
+ * Create a mock Linear SDK Issue object (matching the pattern from frontend-adapter.test.ts).
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 LinearAgentClient for the platform adapter.
36
+ */
37
+ function createMockClient() {
38
+ const mocks = {
39
+ getIssue: vi.fn(),
40
+ updateIssue: vi.fn(),
41
+ getTeamStatuses: vi.fn(),
42
+ updateIssueStatus: vi.fn(),
43
+ createComment: vi.fn(),
44
+ getIssueComments: vi.fn(),
45
+ createIssue: vi.fn(),
46
+ isParentIssue: vi.fn(),
47
+ isChildIssue: vi.fn(),
48
+ getSubIssues: vi.fn(),
49
+ getIssueRelations: vi.fn(),
50
+ createIssueRelation: vi.fn(),
51
+ createAgentSessionOnIssue: vi.fn(),
52
+ updateAgentSession: vi.fn(),
53
+ createAgentActivity: vi.fn(),
54
+ listProjectIssues: vi.fn(),
55
+ };
56
+ const linearClient = {
57
+ issues: vi.fn(),
58
+ issueLabels: vi.fn(),
59
+ };
60
+ const client = {
61
+ ...mocks,
62
+ linearClient,
63
+ };
64
+ mocks.linearClientIssues = linearClient.issues;
65
+ return { client, mocks };
66
+ }
67
+ // ---------------------------------------------------------------------------
68
+ // Webhook payload factories
69
+ // ---------------------------------------------------------------------------
70
+ function makeIssueUpdatePayload(overrides = {}) {
71
+ const { stateName = 'Started', hasStateChange = true, action = 'update', id = 'issue-uuid-1', identifier = 'SUP-100', title = 'Test Issue', description = 'Issue description', labels = [{ name: 'Feature' }], parentId, projectName = 'MyProject', createdAt = '2025-01-15T10:00:00.000Z', } = overrides;
72
+ return {
73
+ action,
74
+ type: 'Issue',
75
+ data: {
76
+ id,
77
+ identifier,
78
+ title,
79
+ description,
80
+ url: `https://linear.app/team/issue/${identifier}`,
81
+ state: { id: 'state-1', name: stateName, type: 'started' },
82
+ labels: labels.map((l) => ({ id: `label-${l.name}`, ...l })),
83
+ parent: parentId ? { id: parentId, identifier: 'SUP-99', title: 'Parent' } : undefined,
84
+ project: projectName ? { id: 'proj-1', name: projectName } : undefined,
85
+ createdAt,
86
+ },
87
+ updatedFrom: hasStateChange ? { stateId: 'old-state-id' } : {},
88
+ createdAt: '2025-01-15T12:00:00.000Z',
89
+ };
90
+ }
91
+ function makeCommentPayload(overrides = {}) {
92
+ const { action = 'create', commentId = 'comment-uuid-1', body = 'This is a comment', issueId = 'issue-uuid-1', issueIdentifier = 'SUP-100', issueTitle = 'Test Issue', issueStateName = 'Started', userId = 'user-1', userName = 'Test User', } = overrides;
93
+ return {
94
+ action,
95
+ type: 'Comment',
96
+ data: {
97
+ id: commentId,
98
+ body,
99
+ issue: {
100
+ id: issueId,
101
+ identifier: issueIdentifier,
102
+ title: issueTitle,
103
+ url: `https://linear.app/team/issue/${issueIdentifier}`,
104
+ state: { id: 'state-1', name: issueStateName, type: 'started' },
105
+ labels: [{ id: 'label-1', name: 'Feature' }],
106
+ createdAt: '2025-01-15T10:00:00.000Z',
107
+ },
108
+ user: { id: userId, name: userName },
109
+ },
110
+ createdAt: '2025-01-15T12:00:00.000Z',
111
+ };
112
+ }
113
+ // ---------------------------------------------------------------------------
114
+ // Tests
115
+ // ---------------------------------------------------------------------------
116
+ describe('LinearPlatformAdapter', () => {
117
+ let adapter;
118
+ let mocks;
119
+ beforeEach(() => {
120
+ const { client, mocks: m } = createMockClient();
121
+ mocks = m;
122
+ adapter = new LinearPlatformAdapter(client);
123
+ });
124
+ // ========================================================================
125
+ // name
126
+ // ========================================================================
127
+ it('has name "linear"', () => {
128
+ expect(adapter.name).toBe('linear');
129
+ });
130
+ // ========================================================================
131
+ // normalizeWebhookEvent — Issue updates
132
+ // ========================================================================
133
+ describe('normalizeWebhookEvent — issue updates', () => {
134
+ it('returns IssueStatusChangedEvent for issue update with state change', () => {
135
+ const payload = makeIssueUpdatePayload({ stateName: 'Started' });
136
+ const events = adapter.normalizeWebhookEvent(payload);
137
+ expect(events).not.toBeNull();
138
+ expect(events).toHaveLength(1);
139
+ const event = events[0];
140
+ expect(event.type).toBe('issue-status-changed');
141
+ expect(event.issueId).toBe('issue-uuid-1');
142
+ expect(event.source).toBe('webhook');
143
+ if (event.type === 'issue-status-changed') {
144
+ expect(event.newStatus).toBe('Started');
145
+ expect(event.issue.id).toBe('issue-uuid-1');
146
+ expect(event.issue.identifier).toBe('SUP-100');
147
+ expect(event.issue.title).toBe('Test Issue');
148
+ expect(event.issue.status).toBe('Started');
149
+ expect(event.issue.labels).toEqual(['Feature']);
150
+ expect(event.issue.project).toBe('MyProject');
151
+ expect(event.timestamp).toBeTruthy();
152
+ }
153
+ });
154
+ it('returns null for issue update without state change', () => {
155
+ const payload = makeIssueUpdatePayload({ hasStateChange: false });
156
+ const events = adapter.normalizeWebhookEvent(payload);
157
+ expect(events).toBeNull();
158
+ });
159
+ it('returns null for issue create action', () => {
160
+ const payload = makeIssueUpdatePayload({ action: 'create' });
161
+ const events = adapter.normalizeWebhookEvent(payload);
162
+ expect(events).toBeNull();
163
+ });
164
+ it('returns null for issue remove action', () => {
165
+ const payload = makeIssueUpdatePayload({ action: 'remove' });
166
+ const events = adapter.normalizeWebhookEvent(payload);
167
+ expect(events).toBeNull();
168
+ });
169
+ it('includes parentId when issue has a parent', () => {
170
+ const payload = makeIssueUpdatePayload({ parentId: 'parent-uuid-1' });
171
+ const events = adapter.normalizeWebhookEvent(payload);
172
+ expect(events).not.toBeNull();
173
+ const event = events[0];
174
+ if (event.type === 'issue-status-changed') {
175
+ expect(event.issue.parentId).toBe('parent-uuid-1');
176
+ }
177
+ });
178
+ it('handles issue with no description', () => {
179
+ const payload = makeIssueUpdatePayload();
180
+ // Remove description from data
181
+ delete payload.data.description;
182
+ const events = adapter.normalizeWebhookEvent(payload);
183
+ expect(events).not.toBeNull();
184
+ const event = events[0];
185
+ if (event.type === 'issue-status-changed') {
186
+ expect(event.issue.description).toBeUndefined();
187
+ }
188
+ });
189
+ it('handles issue with no labels', () => {
190
+ const payload = makeIssueUpdatePayload({ labels: [] });
191
+ const events = adapter.normalizeWebhookEvent(payload);
192
+ expect(events).not.toBeNull();
193
+ const event = events[0];
194
+ if (event.type === 'issue-status-changed') {
195
+ expect(event.issue.labels).toEqual([]);
196
+ }
197
+ });
198
+ it('parses createdAt from webhook data into milliseconds', () => {
199
+ const payload = makeIssueUpdatePayload({ createdAt: '2025-06-01T00:00:00.000Z' });
200
+ const events = adapter.normalizeWebhookEvent(payload);
201
+ expect(events).not.toBeNull();
202
+ const event = events[0];
203
+ if (event.type === 'issue-status-changed') {
204
+ expect(event.issue.createdAt).toBe(new Date('2025-06-01T00:00:00.000Z').getTime());
205
+ }
206
+ });
207
+ });
208
+ // ========================================================================
209
+ // normalizeWebhookEvent — Comments
210
+ // ========================================================================
211
+ describe('normalizeWebhookEvent — comments', () => {
212
+ it('returns CommentAddedEvent for comment creation', () => {
213
+ const payload = makeCommentPayload();
214
+ const events = adapter.normalizeWebhookEvent(payload);
215
+ expect(events).not.toBeNull();
216
+ expect(events).toHaveLength(1);
217
+ const event = events[0];
218
+ expect(event.type).toBe('comment-added');
219
+ expect(event.issueId).toBe('issue-uuid-1');
220
+ expect(event.source).toBe('webhook');
221
+ if (event.type === 'comment-added') {
222
+ expect(event.commentId).toBe('comment-uuid-1');
223
+ expect(event.commentBody).toBe('This is a comment');
224
+ expect(event.userId).toBe('user-1');
225
+ expect(event.userName).toBe('Test User');
226
+ expect(event.issue.identifier).toBe('SUP-100');
227
+ expect(event.issue.status).toBe('Started');
228
+ }
229
+ });
230
+ it('returns null for comment update action', () => {
231
+ const payload = makeCommentPayload({ action: 'update' });
232
+ const events = adapter.normalizeWebhookEvent(payload);
233
+ expect(events).toBeNull();
234
+ });
235
+ it('returns null for comment remove action', () => {
236
+ const payload = makeCommentPayload({ action: 'remove' });
237
+ const events = adapter.normalizeWebhookEvent(payload);
238
+ expect(events).toBeNull();
239
+ });
240
+ it('handles comment without user', () => {
241
+ const payload = makeCommentPayload();
242
+ delete payload.data.user;
243
+ const events = adapter.normalizeWebhookEvent(payload);
244
+ expect(events).not.toBeNull();
245
+ const event = events[0];
246
+ if (event.type === 'comment-added') {
247
+ expect(event.userId).toBeUndefined();
248
+ expect(event.userName).toBeUndefined();
249
+ }
250
+ });
251
+ });
252
+ // ========================================================================
253
+ // normalizeWebhookEvent — Unrecognized payloads
254
+ // ========================================================================
255
+ describe('normalizeWebhookEvent — unrecognized payloads', () => {
256
+ it('returns null for null payload', () => {
257
+ expect(adapter.normalizeWebhookEvent(null)).toBeNull();
258
+ });
259
+ it('returns null for undefined payload', () => {
260
+ expect(adapter.normalizeWebhookEvent(undefined)).toBeNull();
261
+ });
262
+ it('returns null for non-object payload', () => {
263
+ expect(adapter.normalizeWebhookEvent('string')).toBeNull();
264
+ expect(adapter.normalizeWebhookEvent(42)).toBeNull();
265
+ });
266
+ it('returns null for AgentSessionEvent payloads', () => {
267
+ const payload = {
268
+ action: 'created',
269
+ type: 'AgentSessionEvent',
270
+ data: { id: 'session-1' },
271
+ };
272
+ expect(adapter.normalizeWebhookEvent(payload)).toBeNull();
273
+ });
274
+ it('returns null for unknown resource types', () => {
275
+ const payload = {
276
+ action: 'update',
277
+ type: 'Project',
278
+ data: { id: 'proj-1' },
279
+ };
280
+ expect(adapter.normalizeWebhookEvent(payload)).toBeNull();
281
+ });
282
+ it('returns null for payload without data', () => {
283
+ const payload = {
284
+ action: 'update',
285
+ type: 'Issue',
286
+ };
287
+ expect(adapter.normalizeWebhookEvent(payload)).toBeNull();
288
+ });
289
+ });
290
+ // ========================================================================
291
+ // scanProjectIssues
292
+ // ========================================================================
293
+ describe('scanProjectIssues', () => {
294
+ /** Helper: create a listProjectIssues result entry */
295
+ function mockListProjectIssue(overrides = {}) {
296
+ return {
297
+ id: overrides.id ?? 'issue-uuid-1',
298
+ identifier: overrides.identifier ?? 'SUP-100',
299
+ title: overrides.title ?? 'Test Issue',
300
+ description: overrides.description ?? 'A test issue description',
301
+ status: overrides.status ?? 'Backlog',
302
+ labels: overrides.labels ?? ['Feature'],
303
+ createdAt: overrides.createdAt ?? Date.now(),
304
+ parentId: overrides.parentId,
305
+ project: overrides.project ?? 'MyProject',
306
+ childCount: overrides.childCount ?? 0,
307
+ };
308
+ }
309
+ it('returns GovernorIssues for all non-terminal issues', async () => {
310
+ const issues = [
311
+ mockListProjectIssue({ id: 'i-1', identifier: 'SUP-1', status: 'Backlog' }),
312
+ mockListProjectIssue({ id: 'i-2', identifier: 'SUP-2', status: 'Started' }),
313
+ mockListProjectIssue({ id: 'i-3', identifier: 'SUP-3', status: 'Finished' }),
314
+ ];
315
+ mocks.listProjectIssues.mockResolvedValue(issues);
316
+ const result = await adapter.scanProjectIssues('MyProject');
317
+ expect(result).toHaveLength(3);
318
+ expect(result[0].id).toBe('i-1');
319
+ expect(result[0].identifier).toBe('SUP-1');
320
+ expect(result[0].status).toBe('Backlog');
321
+ expect(result[1].id).toBe('i-2');
322
+ expect(result[1].status).toBe('Started');
323
+ expect(result[2].id).toBe('i-3');
324
+ expect(result[2].status).toBe('Finished');
325
+ });
326
+ it('delegates to listProjectIssues with project name', async () => {
327
+ mocks.listProjectIssues.mockResolvedValue([]);
328
+ await adapter.scanProjectIssues('TestProject');
329
+ expect(mocks.listProjectIssues).toHaveBeenCalledWith('TestProject');
330
+ });
331
+ it('returns empty array when no issues found', async () => {
332
+ mocks.listProjectIssues.mockResolvedValue([]);
333
+ const result = await adapter.scanProjectIssues('EmptyProject');
334
+ expect(result).toEqual([]);
335
+ });
336
+ it('maps issue properties correctly', async () => {
337
+ const issue = mockListProjectIssue({
338
+ labels: ['Bug', 'Urgent'],
339
+ parentId: 'parent-1',
340
+ project: 'MyProject',
341
+ });
342
+ mocks.listProjectIssues.mockResolvedValue([issue]);
343
+ const result = await adapter.scanProjectIssues('MyProject');
344
+ expect(result[0].labels).toEqual(['Bug', 'Urgent']);
345
+ expect(result[0].parentId).toBe('parent-1');
346
+ expect(result[0].project).toBe('MyProject');
347
+ });
348
+ it('preserves createdAt as epoch milliseconds', async () => {
349
+ const createdAt = new Date('2025-03-01T08:00:00Z').getTime();
350
+ const issue = mockListProjectIssue({ createdAt });
351
+ mocks.listProjectIssues.mockResolvedValue([issue]);
352
+ const result = await adapter.scanProjectIssues('MyProject');
353
+ expect(result[0].createdAt).toBe(createdAt);
354
+ });
355
+ });
356
+ // ========================================================================
357
+ // scanProjectIssuesWithParents
358
+ // ========================================================================
359
+ describe('scanProjectIssuesWithParents', () => {
360
+ it('returns issues and parent IDs set', async () => {
361
+ mocks.listProjectIssues.mockResolvedValue([
362
+ {
363
+ id: 'p-1', identifier: 'SUP-10', title: 'Parent',
364
+ status: 'Backlog', labels: [], createdAt: Date.now(),
365
+ project: 'MyProject', childCount: 3,
366
+ },
367
+ {
368
+ id: 'c-1', identifier: 'SUP-11', title: 'Child',
369
+ status: 'Backlog', labels: [], createdAt: Date.now(),
370
+ parentId: 'p-1', project: 'MyProject', childCount: 0,
371
+ },
372
+ ]);
373
+ const { issues, parentIssueIds } = await adapter.scanProjectIssuesWithParents('MyProject');
374
+ expect(issues).toHaveLength(2);
375
+ expect(parentIssueIds.has('p-1')).toBe(true);
376
+ expect(parentIssueIds.has('c-1')).toBe(false);
377
+ });
378
+ });
379
+ // ========================================================================
380
+ // toGovernorIssue
381
+ // ========================================================================
382
+ describe('toGovernorIssue', () => {
383
+ it('converts a Linear SDK Issue to GovernorIssue', async () => {
384
+ const issue = mockLinearIssue({
385
+ id: 'conv-1',
386
+ identifier: 'SUP-50',
387
+ title: 'Convert me',
388
+ description: 'Some description',
389
+ stateName: 'Delivered',
390
+ labels: ['Feature', 'Frontend'],
391
+ parentId: 'parent-2',
392
+ projectName: 'ConvertProject',
393
+ createdAt: new Date('2025-02-20T00:00:00Z'),
394
+ });
395
+ const result = await adapter.toGovernorIssue(issue);
396
+ expect(result).toEqual({
397
+ id: 'conv-1',
398
+ identifier: 'SUP-50',
399
+ title: 'Convert me',
400
+ description: 'Some description',
401
+ status: 'Delivered',
402
+ labels: ['Feature', 'Frontend'],
403
+ parentId: 'parent-2',
404
+ project: 'ConvertProject',
405
+ createdAt: new Date('2025-02-20T00:00:00Z').getTime(),
406
+ });
407
+ });
408
+ it('handles issue with no parent', async () => {
409
+ const issue = mockLinearIssue({ parentId: null });
410
+ const result = await adapter.toGovernorIssue(issue);
411
+ expect(result.parentId).toBeUndefined();
412
+ });
413
+ it('handles issue with no project', async () => {
414
+ const issue = mockLinearIssue({ projectName: null });
415
+ const result = await adapter.toGovernorIssue(issue);
416
+ expect(result.project).toBeUndefined();
417
+ });
418
+ it('handles issue with no state (defaults to Backlog)', async () => {
419
+ const issue = mockLinearIssue({ stateName: '' });
420
+ // Simulate null state
421
+ Object.defineProperty(issue, 'state', {
422
+ get: () => Promise.resolve(null),
423
+ });
424
+ const result = await adapter.toGovernorIssue(issue);
425
+ expect(result.status).toBe('Backlog');
426
+ });
427
+ it('handles issue with null description', async () => {
428
+ const issue = mockLinearIssue({ description: null });
429
+ const result = await adapter.toGovernorIssue(issue);
430
+ expect(result.description).toBeUndefined();
431
+ });
432
+ it('throws for invalid native object', async () => {
433
+ await expect(adapter.toGovernorIssue(null)).rejects.toThrow('expected a Linear SDK Issue object');
434
+ });
435
+ it('throws for object without id', async () => {
436
+ await expect(adapter.toGovernorIssue({ title: 'no id' })).rejects.toThrow('expected a Linear SDK Issue object');
437
+ });
438
+ });
439
+ // ========================================================================
440
+ // isParentIssue (inherited from LinearFrontendAdapter)
441
+ // ========================================================================
442
+ describe('isParentIssue', () => {
443
+ it('delegates to client.isParentIssue', async () => {
444
+ mocks.isParentIssue.mockResolvedValue(true);
445
+ const result = await adapter.isParentIssue('issue-1');
446
+ expect(result).toBe(true);
447
+ expect(mocks.isParentIssue).toHaveBeenCalledWith('issue-1');
448
+ });
449
+ it('returns false for non-parent issues', async () => {
450
+ mocks.isParentIssue.mockResolvedValue(false);
451
+ const result = await adapter.isParentIssue('leaf-issue');
452
+ expect(result).toBe(false);
453
+ });
454
+ });
455
+ // ========================================================================
456
+ // Inherited methods still work
457
+ // ========================================================================
458
+ describe('inherits LinearFrontendAdapter', () => {
459
+ it('resolveStatus works', () => {
460
+ expect(adapter.resolveStatus('backlog')).toBe('Backlog');
461
+ expect(adapter.resolveStatus('started')).toBe('Started');
462
+ });
463
+ it('abstractStatus works', () => {
464
+ expect(adapter.abstractStatus('Backlog')).toBe('backlog');
465
+ expect(adapter.abstractStatus('Finished')).toBe('finished');
466
+ });
467
+ });
468
+ });
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Proxy Issue Tracker Client
3
+ *
4
+ * Drop-in replacement for LinearAgentClient that routes all calls
5
+ * through the centralized dashboard proxy endpoint instead of calling
6
+ * the issue tracker API directly.
7
+ *
8
+ * Used when `AGENTFACTORY_API_URL` env var is set.
9
+ *
10
+ * Benefits:
11
+ * - Zero direct API credentials needed on the agent side
12
+ * - Single shared rate limiter and circuit breaker on the proxy
13
+ * - OAuth token resolution stays server-side
14
+ * - Platform-agnostic: agents don't need to know Linear exists
15
+ */
16
+ import type { AgentActivityCreateInput, AgentActivityResult, AgentSessionUpdateInput, AgentSessionUpdateResult, AgentSessionCreateOnIssueInput, AgentSessionCreateResult, IssueRelationCreateInput, IssueRelationResult, IssueRelationsResult, LinearWorkflowStatus, StatusMapping, SubIssueGraph, SubIssueStatus } from './types.js';
17
+ import type { SerializedIssue, SerializedComment, SerializedViewer, SerializedTeam } from './issue-tracker-proxy.js';
18
+ export interface ProxyClientConfig {
19
+ /** Base URL of the dashboard (e.g., 'https://my-dashboard.vercel.app') */
20
+ apiUrl: string;
21
+ /** Worker API key for authentication */
22
+ apiKey: string;
23
+ /** Workspace/organization ID for multi-tenant routing */
24
+ organizationId?: string;
25
+ /** Request timeout in ms (default: 30_000) */
26
+ timeoutMs?: number;
27
+ }
28
+ /**
29
+ * Issue tracker client that proxies all calls through the dashboard server.
30
+ *
31
+ * Implements the same public interface as LinearAgentClient but serializes
32
+ * calls as JSON and sends them to POST /api/issue-tracker-proxy.
33
+ *
34
+ * All returned objects are plain JSON (no lazy-loaded SDK relations).
35
+ */
36
+ export declare class ProxyIssueTrackerClient {
37
+ private readonly apiUrl;
38
+ private readonly apiKey;
39
+ private readonly organizationId?;
40
+ private readonly timeoutMs;
41
+ constructor(config: ProxyClientConfig);
42
+ getIssue(issueIdOrIdentifier: string): Promise<SerializedIssue>;
43
+ updateIssue(issueId: string, data: {
44
+ title?: string;
45
+ description?: string;
46
+ stateId?: string;
47
+ assigneeId?: string | null;
48
+ priority?: number;
49
+ labelIds?: string[];
50
+ }): Promise<SerializedIssue>;
51
+ createIssue(input: {
52
+ title: string;
53
+ description?: string;
54
+ teamId: string;
55
+ projectId?: string;
56
+ stateId?: string;
57
+ labelIds?: string[];
58
+ parentId?: string;
59
+ priority?: number;
60
+ }): Promise<SerializedIssue>;
61
+ unassignIssue(issueId: string): Promise<SerializedIssue>;
62
+ getTeamStatuses(teamId: string): Promise<StatusMapping>;
63
+ updateIssueStatus(issueId: string, statusName: LinearWorkflowStatus): Promise<SerializedIssue>;
64
+ createComment(issueId: string, body: string): Promise<SerializedComment>;
65
+ getIssueComments(issueId: string): Promise<SerializedComment[]>;
66
+ createAgentActivity(input: AgentActivityCreateInput): Promise<AgentActivityResult>;
67
+ updateAgentSession(input: AgentSessionUpdateInput): Promise<AgentSessionUpdateResult>;
68
+ createAgentSessionOnIssue(input: AgentSessionCreateOnIssueInput): Promise<AgentSessionCreateResult>;
69
+ createIssueRelation(input: IssueRelationCreateInput): Promise<IssueRelationResult>;
70
+ getIssueRelations(issueId: string): Promise<IssueRelationsResult>;
71
+ deleteIssueRelation(relationId: string): Promise<{
72
+ success: boolean;
73
+ }>;
74
+ getSubIssues(issueIdOrIdentifier: string): Promise<SerializedIssue[]>;
75
+ getSubIssueStatuses(issueIdOrIdentifier: string): Promise<SubIssueStatus[]>;
76
+ getSubIssueGraph(issueIdOrIdentifier: string): Promise<SubIssueGraph>;
77
+ isParentIssue(issueIdOrIdentifier: string): Promise<boolean>;
78
+ isChildIssue(issueIdOrIdentifier: string): Promise<boolean>;
79
+ listProjectIssues(project: string): Promise<Array<{
80
+ id: string;
81
+ identifier: string;
82
+ title: string;
83
+ description?: string;
84
+ status: string;
85
+ labels: string[];
86
+ createdAt: number;
87
+ parentId?: string;
88
+ project?: string;
89
+ childCount: number;
90
+ }>>;
91
+ getProjectRepositoryUrl(projectId: string): Promise<string | null>;
92
+ getViewer(): Promise<SerializedViewer>;
93
+ getTeam(teamIdOrKey: string): Promise<SerializedTeam>;
94
+ private call;
95
+ }
96
+ /**
97
+ * Create a proxy client if AGENTFACTORY_API_URL is set, otherwise return null.
98
+ *
99
+ * @param fallbackApiKey - API key to use (default: WORKER_API_KEY env var)
100
+ * @param organizationId - Workspace ID for multi-tenant routing
101
+ */
102
+ export declare function createProxyClientIfConfigured(fallbackApiKey?: string, organizationId?: string): ProxyIssueTrackerClient | null;
103
+ //# sourceMappingURL=proxy-client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"proxy-client.d.ts","sourceRoot":"","sources":["../../src/proxy-client.ts"],"names":[],"mappings":"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"}