@calltelemetry/openclaw-linear 0.9.9 → 0.9.10
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.
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Recorded API responses from sub-issue decomposition smoke test.
|
|
3
3
|
* Auto-generated — do not edit manually.
|
|
4
4
|
* Re-generate by running: npx vitest run src/__test__/smoke-linear-api.test.ts
|
|
5
|
-
* Last recorded: 2026-02-22T02:
|
|
5
|
+
* Last recorded: 2026-02-22T02:38:53.901Z
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
export const RECORDED = {
|
|
@@ -44,20 +44,20 @@ export const RECORDED = {
|
|
|
44
44
|
}
|
|
45
45
|
],
|
|
46
46
|
"createParent": {
|
|
47
|
-
"id": "
|
|
48
|
-
"identifier": "UAT-
|
|
47
|
+
"id": "953324f8-878f-40ba-b773-8d8815ee7987",
|
|
48
|
+
"identifier": "UAT-320"
|
|
49
49
|
},
|
|
50
50
|
"createSubIssue1": {
|
|
51
|
-
"id": "
|
|
52
|
-
"identifier": "UAT-
|
|
51
|
+
"id": "69ce9a01-3bdb-4f4a-a882-e28e23d1b628",
|
|
52
|
+
"identifier": "UAT-321"
|
|
53
53
|
},
|
|
54
54
|
"createSubIssue2": {
|
|
55
|
-
"id": "
|
|
56
|
-
"identifier": "UAT-
|
|
55
|
+
"id": "b39cc5eb-ec05-44aa-917b-562a290ecab6",
|
|
56
|
+
"identifier": "UAT-322"
|
|
57
57
|
},
|
|
58
58
|
"subIssue1Details": {
|
|
59
|
-
"id": "
|
|
60
|
-
"identifier": "UAT-
|
|
59
|
+
"id": "69ce9a01-3bdb-4f4a-a882-e28e23d1b628",
|
|
60
|
+
"identifier": "UAT-321",
|
|
61
61
|
"title": "[SMOKE TEST] Sub-Issue 1: Backend API",
|
|
62
62
|
"description": "Implement the backend search API endpoint.\n\nGiven a search query, when the API is called, then matching results are returned.",
|
|
63
63
|
"estimate": 2,
|
|
@@ -83,16 +83,16 @@ export const RECORDED = {
|
|
|
83
83
|
},
|
|
84
84
|
"project": null,
|
|
85
85
|
"parent": {
|
|
86
|
-
"id": "
|
|
87
|
-
"identifier": "UAT-
|
|
86
|
+
"id": "953324f8-878f-40ba-b773-8d8815ee7987",
|
|
87
|
+
"identifier": "UAT-320"
|
|
88
88
|
},
|
|
89
89
|
"relations": {
|
|
90
90
|
"nodes": []
|
|
91
91
|
}
|
|
92
92
|
},
|
|
93
93
|
"subIssue2Details": {
|
|
94
|
-
"id": "
|
|
95
|
-
"identifier": "UAT-
|
|
94
|
+
"id": "b39cc5eb-ec05-44aa-917b-562a290ecab6",
|
|
95
|
+
"identifier": "UAT-322",
|
|
96
96
|
"title": "[SMOKE TEST] Sub-Issue 2: Frontend UI",
|
|
97
97
|
"description": "Build the frontend search UI component.\n\nGiven the search page loads, when the user types a query, then results display in real-time.",
|
|
98
98
|
"estimate": 3,
|
|
@@ -114,28 +114,22 @@ export const RECORDED = {
|
|
|
114
114
|
"issueEstimationType": "tShirt"
|
|
115
115
|
},
|
|
116
116
|
"comments": {
|
|
117
|
-
"nodes": [
|
|
118
|
-
{
|
|
119
|
-
"body": "This thread is for an agent session with ctclaw.",
|
|
120
|
-
"user": null,
|
|
121
|
-
"createdAt": "2026-02-22T02:27:41.198Z"
|
|
122
|
-
}
|
|
123
|
-
]
|
|
117
|
+
"nodes": []
|
|
124
118
|
},
|
|
125
119
|
"project": null,
|
|
126
120
|
"parent": {
|
|
127
|
-
"id": "
|
|
128
|
-
"identifier": "UAT-
|
|
121
|
+
"id": "953324f8-878f-40ba-b773-8d8815ee7987",
|
|
122
|
+
"identifier": "UAT-320"
|
|
129
123
|
},
|
|
130
124
|
"relations": {
|
|
131
125
|
"nodes": []
|
|
132
126
|
}
|
|
133
127
|
},
|
|
134
128
|
"parentDetails": {
|
|
135
|
-
"id": "
|
|
136
|
-
"identifier": "UAT-
|
|
129
|
+
"id": "953324f8-878f-40ba-b773-8d8815ee7987",
|
|
130
|
+
"identifier": "UAT-320",
|
|
137
131
|
"title": "[SMOKE TEST] Sub-Issue Parent: Search Feature",
|
|
138
|
-
"description": "Auto-generated by smoke test to verify sub-issue decomposition.\n\nThis parent issue should have two sub-issues created under it.\n\nCreated: 2026-02-22T02:
|
|
132
|
+
"description": "Auto-generated by smoke test to verify sub-issue decomposition.\n\nThis parent issue should have two sub-issues created under it.\n\nCreated: 2026-02-22T02:38:52.132Z",
|
|
139
133
|
"estimate": null,
|
|
140
134
|
"state": {
|
|
141
135
|
"name": "Backlog",
|
|
@@ -155,13 +149,7 @@ export const RECORDED = {
|
|
|
155
149
|
"issueEstimationType": "tShirt"
|
|
156
150
|
},
|
|
157
151
|
"comments": {
|
|
158
|
-
"nodes": [
|
|
159
|
-
{
|
|
160
|
-
"body": "This thread is for an agent session with ctclaw.",
|
|
161
|
-
"user": null,
|
|
162
|
-
"createdAt": "2026-02-22T02:27:39.821Z"
|
|
163
|
-
}
|
|
164
|
-
]
|
|
152
|
+
"nodes": []
|
|
165
153
|
},
|
|
166
154
|
"project": null,
|
|
167
155
|
"parent": null,
|
|
@@ -170,11 +158,11 @@ export const RECORDED = {
|
|
|
170
158
|
}
|
|
171
159
|
},
|
|
172
160
|
"createRelation": {
|
|
173
|
-
"id": "
|
|
161
|
+
"id": "9207d26d-d65d-45a2-ac76-b9233acea181"
|
|
174
162
|
},
|
|
175
163
|
"subIssue1WithRelation": {
|
|
176
|
-
"id": "
|
|
177
|
-
"identifier": "UAT-
|
|
164
|
+
"id": "69ce9a01-3bdb-4f4a-a882-e28e23d1b628",
|
|
165
|
+
"identifier": "UAT-321",
|
|
178
166
|
"title": "[SMOKE TEST] Sub-Issue 1: Backend API",
|
|
179
167
|
"description": "Implement the backend search API endpoint.\n\nGiven a search query, when the API is called, then matching results are returned.",
|
|
180
168
|
"estimate": 2,
|
|
@@ -196,26 +184,20 @@ export const RECORDED = {
|
|
|
196
184
|
"issueEstimationType": "tShirt"
|
|
197
185
|
},
|
|
198
186
|
"comments": {
|
|
199
|
-
"nodes": [
|
|
200
|
-
{
|
|
201
|
-
"body": "This thread is for an agent session with ctclaw.",
|
|
202
|
-
"user": null,
|
|
203
|
-
"createdAt": "2026-02-22T02:27:40.662Z"
|
|
204
|
-
}
|
|
205
|
-
]
|
|
187
|
+
"nodes": []
|
|
206
188
|
},
|
|
207
189
|
"project": null,
|
|
208
190
|
"parent": {
|
|
209
|
-
"id": "
|
|
210
|
-
"identifier": "UAT-
|
|
191
|
+
"id": "953324f8-878f-40ba-b773-8d8815ee7987",
|
|
192
|
+
"identifier": "UAT-320"
|
|
211
193
|
},
|
|
212
194
|
"relations": {
|
|
213
195
|
"nodes": [
|
|
214
196
|
{
|
|
215
197
|
"type": "blocks",
|
|
216
198
|
"relatedIssue": {
|
|
217
|
-
"id": "
|
|
218
|
-
"identifier": "UAT-
|
|
199
|
+
"id": "b39cc5eb-ec05-44aa-917b-562a290ecab6",
|
|
200
|
+
"identifier": "UAT-322",
|
|
219
201
|
"title": "[SMOKE TEST] Sub-Issue 2: Frontend UI"
|
|
220
202
|
}
|
|
221
203
|
}
|
|
@@ -223,8 +205,8 @@ export const RECORDED = {
|
|
|
223
205
|
}
|
|
224
206
|
},
|
|
225
207
|
"subIssue2WithRelation": {
|
|
226
|
-
"id": "
|
|
227
|
-
"identifier": "UAT-
|
|
208
|
+
"id": "b39cc5eb-ec05-44aa-917b-562a290ecab6",
|
|
209
|
+
"identifier": "UAT-322",
|
|
228
210
|
"title": "[SMOKE TEST] Sub-Issue 2: Frontend UI",
|
|
229
211
|
"description": "Build the frontend search UI component.\n\nGiven the search page loads, when the user types a query, then results display in real-time.",
|
|
230
212
|
"estimate": 3,
|
|
@@ -250,14 +232,14 @@ export const RECORDED = {
|
|
|
250
232
|
{
|
|
251
233
|
"body": "This thread is for an agent session with ctclaw.",
|
|
252
234
|
"user": null,
|
|
253
|
-
"createdAt": "2026-02-22T02:
|
|
235
|
+
"createdAt": "2026-02-22T02:38:53.699Z"
|
|
254
236
|
}
|
|
255
237
|
]
|
|
256
238
|
},
|
|
257
239
|
"project": null,
|
|
258
240
|
"parent": {
|
|
259
|
-
"id": "
|
|
260
|
-
"identifier": "UAT-
|
|
241
|
+
"id": "953324f8-878f-40ba-b773-8d8815ee7987",
|
|
242
|
+
"identifier": "UAT-320"
|
|
261
243
|
},
|
|
262
244
|
"relations": {
|
|
263
245
|
"nodes": []
|
|
@@ -4,12 +4,18 @@ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
|
4
4
|
// Mocks
|
|
5
5
|
// ---------------------------------------------------------------------------
|
|
6
6
|
|
|
7
|
-
const { mockReadFileSync } = vi.hoisted(() => ({
|
|
7
|
+
const { mockReadFileSync, mockWriteFileSync, mockMkdirSync, mockExistsSync } = vi.hoisted(() => ({
|
|
8
8
|
mockReadFileSync: vi.fn(),
|
|
9
|
+
mockWriteFileSync: vi.fn(),
|
|
10
|
+
mockMkdirSync: vi.fn(),
|
|
11
|
+
mockExistsSync: vi.fn().mockReturnValue(true),
|
|
9
12
|
}));
|
|
10
13
|
|
|
11
14
|
vi.mock("node:fs", () => ({
|
|
12
15
|
readFileSync: mockReadFileSync,
|
|
16
|
+
writeFileSync: mockWriteFileSync,
|
|
17
|
+
mkdirSync: mockMkdirSync,
|
|
18
|
+
existsSync: mockExistsSync,
|
|
13
19
|
}));
|
|
14
20
|
|
|
15
21
|
// ---------------------------------------------------------------------------
|
|
@@ -21,6 +27,9 @@ import {
|
|
|
21
27
|
buildMentionPattern,
|
|
22
28
|
resolveAgentFromAlias,
|
|
23
29
|
resolveDefaultAgent,
|
|
30
|
+
createAgentProfilesFile,
|
|
31
|
+
validateProfiles,
|
|
32
|
+
PROFILES_PATH,
|
|
24
33
|
_resetProfilesCacheForTesting,
|
|
25
34
|
type AgentProfile,
|
|
26
35
|
} from "./shared-profiles.js";
|
|
@@ -61,6 +70,7 @@ beforeEach(() => {
|
|
|
61
70
|
vi.clearAllMocks();
|
|
62
71
|
_resetProfilesCacheForTesting();
|
|
63
72
|
mockReadFileSync.mockReturnValue(PROFILES_JSON);
|
|
73
|
+
mockExistsSync.mockReturnValue(true);
|
|
64
74
|
});
|
|
65
75
|
|
|
66
76
|
afterEach(() => {
|
|
@@ -260,3 +270,115 @@ describe("resolveDefaultAgent", () => {
|
|
|
260
270
|
expect(result).toBe("mal");
|
|
261
271
|
});
|
|
262
272
|
});
|
|
273
|
+
|
|
274
|
+
// ---------------------------------------------------------------------------
|
|
275
|
+
// createAgentProfilesFile
|
|
276
|
+
// ---------------------------------------------------------------------------
|
|
277
|
+
|
|
278
|
+
describe("createAgentProfilesFile", () => {
|
|
279
|
+
it("writes correct JSON structure to PROFILES_PATH", () => {
|
|
280
|
+
createAgentProfilesFile({
|
|
281
|
+
agentId: "bobbin",
|
|
282
|
+
label: "Bobbin",
|
|
283
|
+
mentionAliases: ["bobbin", "bob"],
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
expect(mockMkdirSync).toHaveBeenCalledTimes(1);
|
|
287
|
+
expect(mockWriteFileSync).toHaveBeenCalledTimes(1);
|
|
288
|
+
|
|
289
|
+
const [path, content] = mockWriteFileSync.mock.calls[0];
|
|
290
|
+
expect(path).toBe(PROFILES_PATH);
|
|
291
|
+
|
|
292
|
+
const parsed = JSON.parse(content);
|
|
293
|
+
expect(parsed.agents.bobbin).toEqual({
|
|
294
|
+
label: "Bobbin",
|
|
295
|
+
mission: "AI assistant for Linear issues",
|
|
296
|
+
isDefault: true,
|
|
297
|
+
mentionAliases: ["bobbin", "bob"],
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it("uses custom mission when provided", () => {
|
|
302
|
+
createAgentProfilesFile({
|
|
303
|
+
agentId: "claw",
|
|
304
|
+
label: "The Claw",
|
|
305
|
+
mentionAliases: ["claw"],
|
|
306
|
+
mission: "Code review specialist",
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
const [, content] = mockWriteFileSync.mock.calls[0];
|
|
310
|
+
const parsed = JSON.parse(content);
|
|
311
|
+
expect(parsed.agents.claw.mission).toBe("Code review specialist");
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it("creates parent directory recursively", () => {
|
|
315
|
+
createAgentProfilesFile({
|
|
316
|
+
agentId: "test",
|
|
317
|
+
label: "Test",
|
|
318
|
+
mentionAliases: ["test"],
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
expect(mockMkdirSync).toHaveBeenCalledWith(
|
|
322
|
+
expect.any(String),
|
|
323
|
+
{ recursive: true },
|
|
324
|
+
);
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it("busts the profile cache", () => {
|
|
328
|
+
// Load to populate cache
|
|
329
|
+
loadAgentProfiles();
|
|
330
|
+
expect(mockReadFileSync).toHaveBeenCalledTimes(1);
|
|
331
|
+
|
|
332
|
+
// Create a new profile — should bust cache
|
|
333
|
+
createAgentProfilesFile({
|
|
334
|
+
agentId: "fresh",
|
|
335
|
+
label: "Fresh",
|
|
336
|
+
mentionAliases: ["fresh"],
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
// Next load should re-read from disk (cache was busted)
|
|
340
|
+
loadAgentProfiles();
|
|
341
|
+
expect(mockReadFileSync).toHaveBeenCalledTimes(2);
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
// ---------------------------------------------------------------------------
|
|
346
|
+
// validateProfiles
|
|
347
|
+
// ---------------------------------------------------------------------------
|
|
348
|
+
|
|
349
|
+
describe("validateProfiles", () => {
|
|
350
|
+
it("returns error when file is missing", () => {
|
|
351
|
+
mockExistsSync.mockReturnValue(false);
|
|
352
|
+
|
|
353
|
+
const result = validateProfiles();
|
|
354
|
+
expect(result).not.toBeNull();
|
|
355
|
+
expect(result).toContain("not found");
|
|
356
|
+
expect(result).toContain("openclaw openclaw-linear setup");
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
it("returns null when file is valid with agents", () => {
|
|
360
|
+
mockExistsSync.mockReturnValue(true);
|
|
361
|
+
mockReadFileSync.mockReturnValue(PROFILES_JSON);
|
|
362
|
+
|
|
363
|
+
const result = validateProfiles();
|
|
364
|
+
expect(result).toBeNull();
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
it("returns error when JSON is invalid", () => {
|
|
368
|
+
mockExistsSync.mockReturnValue(true);
|
|
369
|
+
mockReadFileSync.mockImplementation(() => { throw new SyntaxError("Unexpected token"); });
|
|
370
|
+
|
|
371
|
+
const result = validateProfiles();
|
|
372
|
+
expect(result).not.toBeNull();
|
|
373
|
+
expect(result).toContain("could not be parsed");
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
it("returns error when agents object is empty", () => {
|
|
377
|
+
mockExistsSync.mockReturnValue(true);
|
|
378
|
+
mockReadFileSync.mockReturnValue(JSON.stringify({ agents: {} }));
|
|
379
|
+
|
|
380
|
+
const result = validateProfiles();
|
|
381
|
+
expect(result).not.toBeNull();
|
|
382
|
+
expect(result).toContain("no agents configured");
|
|
383
|
+
});
|
|
384
|
+
});
|
|
@@ -42,9 +42,16 @@ let _affinityTtlMs = 30 * 60_000; // 30 minutes default
|
|
|
42
42
|
/**
|
|
43
43
|
* Register the active session for an issue. Idempotent — calling again
|
|
44
44
|
* for the same issue just updates the session.
|
|
45
|
+
*
|
|
46
|
+
* Also eagerly records agent affinity so that follow-up webhooks arriving
|
|
47
|
+
* during or after the run resolve to the correct agent — even if the
|
|
48
|
+
* gateway restarts before clearActiveSession is called.
|
|
45
49
|
*/
|
|
46
50
|
export function setActiveSession(session: ActiveSession): void {
|
|
47
51
|
sessions.set(session.issueId, session);
|
|
52
|
+
if (session.agentId) {
|
|
53
|
+
recordIssueAffinity(session.issueId, session.agentId);
|
|
54
|
+
}
|
|
48
55
|
}
|
|
49
56
|
|
|
50
57
|
/**
|
package/src/pipeline/webhook.ts
CHANGED
|
@@ -484,10 +484,10 @@ export async function handleLinearWebhook(
|
|
|
484
484
|
activeRuns.add(issue.id);
|
|
485
485
|
void (async () => {
|
|
486
486
|
const profiles = loadAgentProfiles();
|
|
487
|
-
const
|
|
488
|
-
const label = defaultProfile?.[1]?.label ?? profiles[agentId]?.label ?? agentId;
|
|
487
|
+
const label = profiles[agentId]?.label ?? agentId;
|
|
489
488
|
|
|
490
489
|
// Register active session for tool resolution (code_run, etc.)
|
|
490
|
+
// Also eagerly records affinity so follow-ups route to the same agent.
|
|
491
491
|
setActiveSession({
|
|
492
492
|
agentSessionId: session.id,
|
|
493
493
|
issueIdentifier: enrichedIssue?.identifier ?? issue.identifier ?? issue.id,
|
|
@@ -531,7 +531,7 @@ export async function handleLinearWebhook(
|
|
|
531
531
|
}).then(() => true).catch(() => false);
|
|
532
532
|
|
|
533
533
|
if (!emitted) {
|
|
534
|
-
const avatarUrl =
|
|
534
|
+
const avatarUrl = profiles[agentId]?.avatarUrl;
|
|
535
535
|
const agentOpts = avatarUrl
|
|
536
536
|
? { createAsUser: label, displayIconUrl: avatarUrl }
|
|
537
537
|
: undefined;
|
|
@@ -648,8 +648,7 @@ export async function handleLinearWebhook(
|
|
|
648
648
|
activeRuns.add(issue.id);
|
|
649
649
|
void (async () => {
|
|
650
650
|
const profiles = loadAgentProfiles();
|
|
651
|
-
const
|
|
652
|
-
const label = defaultProfile?.[1]?.label ?? profiles[agentId]?.label ?? agentId;
|
|
651
|
+
const label = profiles[agentId]?.label ?? agentId;
|
|
653
652
|
|
|
654
653
|
// Fetch full issue details for context
|
|
655
654
|
let enrichedIssue: any = issue;
|
|
@@ -764,7 +763,7 @@ export async function handleLinearWebhook(
|
|
|
764
763
|
}).then(() => true).catch(() => false);
|
|
765
764
|
|
|
766
765
|
if (!emitted) {
|
|
767
|
-
const avatarUrl =
|
|
766
|
+
const avatarUrl = profiles[agentId]?.avatarUrl;
|
|
768
767
|
const agentOpts = avatarUrl
|
|
769
768
|
? { createAsUser: label, displayIconUrl: avatarUrl }
|
|
770
769
|
: undefined;
|
|
@@ -1150,9 +1149,8 @@ export async function handleLinearWebhook(
|
|
|
1150
1149
|
// Dispatch triage (non-blocking)
|
|
1151
1150
|
void (async () => {
|
|
1152
1151
|
const profiles = loadAgentProfiles();
|
|
1153
|
-
const
|
|
1154
|
-
const
|
|
1155
|
-
const avatarUrl = defaultProfile?.[1]?.avatarUrl ?? profiles[agentId]?.avatarUrl;
|
|
1152
|
+
const label = profiles[agentId]?.label ?? agentId;
|
|
1153
|
+
const avatarUrl = profiles[agentId]?.avatarUrl;
|
|
1156
1154
|
let agentSessionId: string | null = null;
|
|
1157
1155
|
|
|
1158
1156
|
try {
|