@desplega.ai/agent-swarm 1.53.0 → 1.53.1
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/README.md +2 -0
- package/openapi.json +1 -1
- package/package.json +1 -1
- package/plugin/commands/work-on-task.md +11 -5
- package/plugin/pi-skills/work-on-task/SKILL.md +11 -5
- package/src/be/db.ts +10 -6
- package/src/linear/sync.ts +38 -11
- package/src/linear/templates.ts +17 -0
- package/src/tests/context-snapshot.test.ts +127 -0
- package/src/tests/linear-webhook.test.ts +105 -4
package/README.md
CHANGED
|
@@ -58,6 +58,8 @@ Agent Swarm lets you run a team of AI coding agents that coordinate autonomously
|
|
|
58
58
|
- **Onboarding wizard** — Interactive CLI wizard (`agent-swarm onboard`) to set up a new swarm from scratch with presets, credential collection, and docker-compose generation
|
|
59
59
|
- **Skill system** — Reusable procedural knowledge: create, install, publish, and sync skills from GitHub with scope resolution (agent → swarm → global)
|
|
60
60
|
- **Human-in-the-Loop** — Workflow nodes that pause for human approval or input, with a dashboard UI for reviewing and responding to requests
|
|
61
|
+
- **MCP server management** — Register, install, and manage MCP servers for agents with scope cascade (agent → swarm → global) and auto-injection into worker containers
|
|
62
|
+
- **Context usage tracking** — Monitor context window utilization and compaction events per task with visual indicators in the dashboard
|
|
61
63
|
|
|
62
64
|
## Quick Start
|
|
63
65
|
|
package/openapi.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"openapi": "3.1.0",
|
|
3
3
|
"info": {
|
|
4
4
|
"title": "Agent Swarm API",
|
|
5
|
-
"version": "1.
|
|
5
|
+
"version": "1.53.0",
|
|
6
6
|
"description": "Multi-agent orchestration API for Claude Code, Codex, and Gemini CLI. Enables task distribution, agent communication, and service discovery.\n\nMCP tools are documented separately in [MCP.md](./MCP.md)."
|
|
7
7
|
},
|
|
8
8
|
"servers": [
|
package/package.json
CHANGED
|
@@ -21,11 +21,17 @@ Once you have the task details, you should:
|
|
|
21
21
|
- Use `memory-get` on any highly relevant results to get full details
|
|
22
22
|
- This step is NOT optional. Past learnings compound your effectiveness.
|
|
23
23
|
<!-- /claude-only -->
|
|
24
|
-
2.
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
24
|
+
2. **Check Installed Skills (REQUIRED):** Before researching or implementing, review your "Installed Skills" section in the system prompt:
|
|
25
|
+
- If any skill's description or trigger matches this task, invoke it via the `Skill` tool BEFORE doing manual research
|
|
26
|
+
- Skills contain pre-built, tested procedures that save context window and cost
|
|
27
|
+
- Example: task involves Linear → use `linear-interaction` skill, task involves email → use `agentmail-sending` skill
|
|
28
|
+
- Only proceed to manual research/web search if NO installed skill covers the task
|
|
29
|
+
- This step is NOT optional. Skipping it wastes context and money.
|
|
30
|
+
3. Figure out if you need to use any of the available commands to help you with your work (see below for available commands)
|
|
31
|
+
4. Use the `/todos` command to add a new todo item indicating you are starting to work on the task (e.g. "Work on task XXX: <short description>"). This will help on restarts, as it will be easier to remember what you were doing.
|
|
32
|
+
5. Call `store-progress` tool to mark the task as "in-progress" with a progress set to something like "Starting work on the task XXX, blah blah". Additionally use `/swarm-chat` command to notify the swarm, human and lead when applicable. Do not be too verbose, nor spammy.
|
|
33
|
+
6. Start working on the task, providing updates as needed by calling `store-progress` tool, use the `progress` field to indicate what you are doing.
|
|
34
|
+
7. Once you either done or in a dead-end, see the "Completion" section below.
|
|
29
35
|
|
|
30
36
|
### Available commands
|
|
31
37
|
|
|
@@ -13,11 +13,17 @@ Once you get a task assigned, you need to immediately start working on it. To do
|
|
|
13
13
|
|
|
14
14
|
Once you have the task details, you should:
|
|
15
15
|
|
|
16
|
-
1.
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
16
|
+
1. **Check Installed Skills (REQUIRED):** Before researching or implementing, review your "Installed Skills" section in the system prompt:
|
|
17
|
+
- If any skill's description or trigger matches this task, invoke it via the `Skill` tool BEFORE doing manual research
|
|
18
|
+
- Skills contain pre-built, tested procedures that save context window and cost
|
|
19
|
+
- Example: task involves Linear → use `linear-interaction` skill, task involves email → use `agentmail-sending` skill
|
|
20
|
+
- Only proceed to manual research/web search if NO installed skill covers the task
|
|
21
|
+
- This step is NOT optional. Skipping it wastes context and money.
|
|
22
|
+
2. Figure out if you need to perform any research or planning before starting (see below)
|
|
23
|
+
3. Use the `/skill:todos` to add a new todo item indicating you are starting to work on the task (e.g. "Work on task XXX: <short description>"). This will help on restarts, as it will be easier to remember what you were doing.
|
|
24
|
+
4. Call `store-progress` tool to mark the task as "in-progress" with a progress set to something like "Starting work on the task XXX, blah blah". Additionally use `/skill:swarm-chat` to notify the swarm, human and lead when applicable. Do not be too verbose, nor spammy.
|
|
25
|
+
5. Start working on the task, providing updates as needed by calling `store-progress` tool, use the `progress` field to indicate what you are doing.
|
|
26
|
+
6. Once you either done or in a dead-end, see the "Completion" section below.
|
|
21
27
|
|
|
22
28
|
### Research and Planning
|
|
23
29
|
|
package/src/be/db.ts
CHANGED
|
@@ -7855,6 +7855,13 @@ export function createContextSnapshot(input: CreateContextSnapshotInput): Contex
|
|
|
7855
7855
|
.run(input.contextPercent, input.taskId);
|
|
7856
7856
|
}
|
|
7857
7857
|
|
|
7858
|
+
// Keep totalContextTokensUsed up to date with the latest known value
|
|
7859
|
+
if (input.contextUsedTokens != null) {
|
|
7860
|
+
getDb()
|
|
7861
|
+
.prepare("UPDATE agent_tasks SET totalContextTokensUsed = ? WHERE id = ?")
|
|
7862
|
+
.run(input.contextUsedTokens, input.taskId);
|
|
7863
|
+
}
|
|
7864
|
+
|
|
7858
7865
|
if (input.eventType === "compaction") {
|
|
7859
7866
|
getDb()
|
|
7860
7867
|
.prepare(
|
|
@@ -7863,13 +7870,10 @@ export function createContextSnapshot(input: CreateContextSnapshotInput): Contex
|
|
|
7863
7870
|
.run(input.taskId);
|
|
7864
7871
|
}
|
|
7865
7872
|
|
|
7866
|
-
if (input.eventType === "completion") {
|
|
7873
|
+
if (input.eventType === "completion" && input.contextTotalTokens != null) {
|
|
7867
7874
|
getDb()
|
|
7868
|
-
.prepare(
|
|
7869
|
-
|
|
7870
|
-
WHERE id = ?`,
|
|
7871
|
-
)
|
|
7872
|
-
.run(input.contextUsedTokens ?? null, input.contextTotalTokens ?? null, input.taskId);
|
|
7875
|
+
.prepare("UPDATE agent_tasks SET contextWindowSize = ? WHERE id = ?")
|
|
7876
|
+
.run(input.contextTotalTokens, input.taskId);
|
|
7873
7877
|
}
|
|
7874
7878
|
|
|
7875
7879
|
return {
|
package/src/linear/sync.ts
CHANGED
|
@@ -271,18 +271,40 @@ export async function handleAgentSessionEvent(event: Record<string, unknown>): P
|
|
|
271
271
|
|
|
272
272
|
// Check if we already track this issue
|
|
273
273
|
const existing = getTrackerSyncByExternalId("linear", "task", issueId);
|
|
274
|
+
const sessionId = agentSession ? String(agentSession.id ?? "") : "";
|
|
275
|
+
|
|
274
276
|
if (existing) {
|
|
277
|
+
const existingTask = getTaskById(existing.swarmId);
|
|
278
|
+
|
|
279
|
+
// If the task is still active, acknowledge the new session but don't create a duplicate
|
|
280
|
+
if (existingTask && !["completed", "failed", "cancelled"].includes(existingTask.status)) {
|
|
281
|
+
console.log(
|
|
282
|
+
`[Linear Sync] Issue ${issueIdentifier} already tracked as active task ${existing.swarmId}, skipping`,
|
|
283
|
+
);
|
|
284
|
+
if (sessionId) {
|
|
285
|
+
taskSessionMap.set(existingTask.id, sessionId);
|
|
286
|
+
acknowledgeAgentSession(
|
|
287
|
+
sessionId,
|
|
288
|
+
`This issue is already being worked on (task ${existing.swarmId}).`,
|
|
289
|
+
).catch((err) => {
|
|
290
|
+
console.error("[Linear Sync] Failed to acknowledge duplicate AgentSession:", err);
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Task is done/failed/cancelled — create a follow-up task below
|
|
275
297
|
console.log(
|
|
276
|
-
`[Linear Sync] Issue ${issueIdentifier}
|
|
298
|
+
`[Linear Sync] Issue ${issueIdentifier} was tracked as ${existingTask?.status ?? "unknown"} task ${existing.swarmId}, creating follow-up`,
|
|
277
299
|
);
|
|
278
|
-
return;
|
|
279
300
|
}
|
|
280
301
|
|
|
281
302
|
const lead = findLeadAgent();
|
|
282
303
|
|
|
283
304
|
const sessionSection = sessionUrl ? `\nSession: ${sessionUrl}` : "";
|
|
284
305
|
const descriptionSection = issueDescription ? `\nDescription:\n${issueDescription}\n` : "";
|
|
285
|
-
const
|
|
306
|
+
const templateName = existing ? "linear.issue.reassigned" : "linear.issue.assigned";
|
|
307
|
+
const templateResult = resolveTemplate(templateName, {
|
|
286
308
|
issue_identifier: issueIdentifier,
|
|
287
309
|
issue_title: issueTitle,
|
|
288
310
|
issue_url: issueUrl,
|
|
@@ -290,16 +312,21 @@ export async function handleAgentSessionEvent(event: Record<string, unknown>): P
|
|
|
290
312
|
description_section: descriptionSection,
|
|
291
313
|
});
|
|
292
314
|
|
|
293
|
-
if (
|
|
315
|
+
if (templateResult.skipped) {
|
|
294
316
|
return;
|
|
295
317
|
}
|
|
296
318
|
|
|
297
|
-
const task = createTaskExtended(
|
|
319
|
+
const task = createTaskExtended(templateResult.text, {
|
|
298
320
|
agentId: lead?.id ?? "",
|
|
299
321
|
source: "linear",
|
|
300
322
|
taskType: "linear-issue",
|
|
301
323
|
});
|
|
302
324
|
|
|
325
|
+
// Delete old tracker_sync before creating new one (UNIQUE constraint)
|
|
326
|
+
if (existing) {
|
|
327
|
+
deleteTrackerSync(existing.id);
|
|
328
|
+
}
|
|
329
|
+
|
|
303
330
|
createTrackerSync({
|
|
304
331
|
provider: "linear",
|
|
305
332
|
entityType: "task",
|
|
@@ -313,15 +340,14 @@ export async function handleAgentSessionEvent(event: Record<string, unknown>): P
|
|
|
313
340
|
});
|
|
314
341
|
|
|
315
342
|
// Track the AgentSession so outbound sync can post activities to it
|
|
316
|
-
const sessionId = agentSession ? String(agentSession.id ?? "") : "";
|
|
317
343
|
if (sessionId) {
|
|
318
344
|
taskSessionMap.set(task.id, sessionId);
|
|
319
345
|
|
|
320
346
|
// Acknowledge the AgentSession (pending → active)
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
`Task received by Agent Swarm (${task.id}). Processing
|
|
324
|
-
).catch((err) => {
|
|
347
|
+
const ackMsg = existing
|
|
348
|
+
? `Follow-up task created (${task.id}). Previous task was ${existing.swarmId}. Processing...`
|
|
349
|
+
: `Task received by Agent Swarm (${task.id}). Processing...`;
|
|
350
|
+
acknowledgeAgentSession(sessionId, ackMsg).catch((err) => {
|
|
325
351
|
console.error("[Linear Sync] Failed to acknowledge AgentSession:", err);
|
|
326
352
|
});
|
|
327
353
|
|
|
@@ -336,8 +362,9 @@ export async function handleAgentSessionEvent(event: Record<string, unknown>): P
|
|
|
336
362
|
}
|
|
337
363
|
}
|
|
338
364
|
|
|
365
|
+
const action = existing ? "follow-up" : "new";
|
|
339
366
|
console.log(
|
|
340
|
-
`[Linear Sync] Created task ${task.id} for ${issueIdentifier} -> ${lead?.name ?? "unassigned"}`,
|
|
367
|
+
`[Linear Sync] Created ${action} task ${task.id} for ${issueIdentifier} -> ${lead?.name ?? "unassigned"}`,
|
|
341
368
|
);
|
|
342
369
|
}
|
|
343
370
|
|
package/src/linear/templates.ts
CHANGED
|
@@ -27,6 +27,23 @@ URL: {{issue_url}}{{session_section}}
|
|
|
27
27
|
category: "event",
|
|
28
28
|
});
|
|
29
29
|
|
|
30
|
+
registerTemplate({
|
|
31
|
+
eventType: "linear.issue.reassigned",
|
|
32
|
+
header: "[Linear {{issue_identifier}}] Re-assigned: {{issue_title}}",
|
|
33
|
+
defaultBody: `Source: Linear (Agent Session re-assignment)
|
|
34
|
+
URL: {{issue_url}}{{session_section}}
|
|
35
|
+
{{description_section}}
|
|
36
|
+
This issue was previously tracked but the original task has completed. A new task has been created to handle the re-assignment.`,
|
|
37
|
+
variables: [
|
|
38
|
+
{ name: "issue_identifier", description: "Linear issue identifier (e.g. ENG-123)" },
|
|
39
|
+
{ name: "issue_title", description: "Issue title" },
|
|
40
|
+
{ name: "issue_url", description: "Issue URL on Linear" },
|
|
41
|
+
{ name: "session_section", description: "Session URL line or empty string" },
|
|
42
|
+
{ name: "description_section", description: "Description section or empty string" },
|
|
43
|
+
],
|
|
44
|
+
category: "event",
|
|
45
|
+
});
|
|
46
|
+
|
|
30
47
|
registerTemplate({
|
|
31
48
|
eventType: "linear.issue.followup",
|
|
32
49
|
header: "[Linear {{issue_identifier}}] Follow-up: {{issue_title}}",
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
2
|
+
import { unlink } from "node:fs/promises";
|
|
3
|
+
import {
|
|
4
|
+
closeDb,
|
|
5
|
+
createAgent,
|
|
6
|
+
createContextSnapshot,
|
|
7
|
+
createTaskExtended,
|
|
8
|
+
getContextSnapshotsByTaskId,
|
|
9
|
+
getContextSummaryByTaskId,
|
|
10
|
+
initDb,
|
|
11
|
+
} from "../be/db";
|
|
12
|
+
|
|
13
|
+
const TEST_DB_PATH = "./test-context-snapshot.sqlite";
|
|
14
|
+
|
|
15
|
+
describe("Context Snapshots", () => {
|
|
16
|
+
const agentId = "aaaa0000-0000-4000-8000-000000000001";
|
|
17
|
+
const sessionId = "sess-001";
|
|
18
|
+
let taskId: string;
|
|
19
|
+
|
|
20
|
+
beforeAll(async () => {
|
|
21
|
+
for (const suffix of ["", "-wal", "-shm"]) {
|
|
22
|
+
try {
|
|
23
|
+
await unlink(TEST_DB_PATH + suffix);
|
|
24
|
+
} catch {
|
|
25
|
+
// File doesn't exist
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
initDb(TEST_DB_PATH);
|
|
30
|
+
createAgent({ id: agentId, name: "Test Worker", isLead: false, status: "idle" });
|
|
31
|
+
const task = createTaskExtended("Test task for context snapshots", {
|
|
32
|
+
agentId,
|
|
33
|
+
source: "mcp",
|
|
34
|
+
});
|
|
35
|
+
taskId = task.id;
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
afterAll(async () => {
|
|
39
|
+
closeDb();
|
|
40
|
+
for (const suffix of ["", "-wal", "-shm"]) {
|
|
41
|
+
try {
|
|
42
|
+
await unlink(TEST_DB_PATH + suffix);
|
|
43
|
+
} catch {
|
|
44
|
+
// ignore
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("completion snapshot without contextUsedTokens preserves last known usage", () => {
|
|
50
|
+
// Simulate progress snapshots during task execution
|
|
51
|
+
createContextSnapshot({
|
|
52
|
+
taskId,
|
|
53
|
+
agentId,
|
|
54
|
+
sessionId,
|
|
55
|
+
eventType: "progress",
|
|
56
|
+
contextUsedTokens: 50000,
|
|
57
|
+
contextTotalTokens: 200000,
|
|
58
|
+
contextPercent: 25,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
createContextSnapshot({
|
|
62
|
+
taskId,
|
|
63
|
+
agentId,
|
|
64
|
+
sessionId,
|
|
65
|
+
eventType: "progress",
|
|
66
|
+
contextUsedTokens: 80000,
|
|
67
|
+
contextTotalTokens: 200000,
|
|
68
|
+
contextPercent: 40,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// Simulate completion snapshot — runner doesn't have contextUsedTokens at session end
|
|
72
|
+
createContextSnapshot({
|
|
73
|
+
taskId,
|
|
74
|
+
agentId,
|
|
75
|
+
sessionId,
|
|
76
|
+
eventType: "completion",
|
|
77
|
+
// No contextUsedTokens or contextPercent — this is the bug scenario
|
|
78
|
+
contextTotalTokens: 200000,
|
|
79
|
+
cumulativeInputTokens: 100000,
|
|
80
|
+
cumulativeOutputTokens: 20000,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// The summary should preserve the last known context usage, not null/0
|
|
84
|
+
const summary = getContextSummaryByTaskId(taskId);
|
|
85
|
+
expect(summary.totalContextTokensUsed).toBe(80000);
|
|
86
|
+
expect(summary.contextWindowSize).toBe(200000);
|
|
87
|
+
expect(summary.peakContextPercent).toBe(40);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("completion snapshot with contextUsedTokens uses provided value", () => {
|
|
91
|
+
// Create a second task for an isolated test
|
|
92
|
+
const task2 = createTaskExtended("Test task 2", { agentId, source: "mcp" });
|
|
93
|
+
|
|
94
|
+
createContextSnapshot({
|
|
95
|
+
taskId: task2.id,
|
|
96
|
+
agentId,
|
|
97
|
+
sessionId,
|
|
98
|
+
eventType: "progress",
|
|
99
|
+
contextUsedTokens: 50000,
|
|
100
|
+
contextTotalTokens: 200000,
|
|
101
|
+
contextPercent: 25,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// Completion with explicit contextUsedTokens should use that value
|
|
105
|
+
createContextSnapshot({
|
|
106
|
+
taskId: task2.id,
|
|
107
|
+
agentId,
|
|
108
|
+
sessionId,
|
|
109
|
+
eventType: "completion",
|
|
110
|
+
contextUsedTokens: 60000,
|
|
111
|
+
contextTotalTokens: 200000,
|
|
112
|
+
contextPercent: 30,
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const summary = getContextSummaryByTaskId(task2.id);
|
|
116
|
+
expect(summary.totalContextTokensUsed).toBe(60000);
|
|
117
|
+
expect(summary.contextWindowSize).toBe(200000);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test("snapshots are returned in chronological order", () => {
|
|
121
|
+
const snapshots = getContextSnapshotsByTaskId(taskId);
|
|
122
|
+
expect(snapshots.length).toBe(3);
|
|
123
|
+
expect(snapshots[0].eventType).toBe("progress");
|
|
124
|
+
expect(snapshots[1].eventType).toBe("progress");
|
|
125
|
+
expect(snapshots[2].eventType).toBe("completion");
|
|
126
|
+
});
|
|
127
|
+
});
|
|
@@ -207,7 +207,7 @@ describe("handleAgentSessionEvent", () => {
|
|
|
207
207
|
expect(task!.task).toContain("Fix login bug");
|
|
208
208
|
});
|
|
209
209
|
|
|
210
|
-
test("skips
|
|
210
|
+
test("skips when already-tracked issue has an active task", async () => {
|
|
211
211
|
const event = {
|
|
212
212
|
type: "AgentSession",
|
|
213
213
|
action: "create",
|
|
@@ -221,11 +221,112 @@ describe("handleAgentSessionEvent", () => {
|
|
|
221
221
|
},
|
|
222
222
|
};
|
|
223
223
|
|
|
224
|
-
//
|
|
224
|
+
// The task from the previous test is still pending (active)
|
|
225
|
+
const syncBefore = getTrackerSyncByExternalId("linear", "task", "issue-agent-session-001");
|
|
226
|
+
expect(syncBefore).not.toBeNull();
|
|
227
|
+
const originalSwarmId = syncBefore!.swarmId;
|
|
228
|
+
|
|
225
229
|
await handleAgentSessionEvent(event);
|
|
226
|
-
|
|
227
|
-
|
|
230
|
+
|
|
231
|
+
// Sync should still point to the same task (no follow-up created)
|
|
232
|
+
const syncAfter = getTrackerSyncByExternalId("linear", "task", "issue-agent-session-001");
|
|
233
|
+
expect(syncAfter).not.toBeNull();
|
|
234
|
+
expect(syncAfter!.swarmId).toBe(originalSwarmId);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
test("creates follow-up task when already-tracked issue has a completed task", async () => {
|
|
238
|
+
// Create a task and tracker_sync, then mark the task as completed
|
|
239
|
+
const originalTask = createTaskExtended("Original linear task", {
|
|
240
|
+
source: "linear",
|
|
241
|
+
taskType: "linear-issue",
|
|
242
|
+
});
|
|
243
|
+
const { getDb } = await import("../be/db");
|
|
244
|
+
getDb().query("UPDATE agent_tasks SET status = 'completed' WHERE id = ?").run(originalTask.id);
|
|
245
|
+
|
|
246
|
+
createTrackerSync({
|
|
247
|
+
provider: "linear",
|
|
248
|
+
entityType: "task",
|
|
249
|
+
providerEntityType: "Issue",
|
|
250
|
+
swarmId: originalTask.id,
|
|
251
|
+
externalId: "issue-followup-completed-001",
|
|
252
|
+
externalIdentifier: "ENG-150",
|
|
253
|
+
externalUrl: "https://linear.app/team/issue/ENG-150",
|
|
254
|
+
lastSyncOrigin: "external",
|
|
255
|
+
syncDirection: "inbound",
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
const event = {
|
|
259
|
+
type: "AgentSession",
|
|
260
|
+
action: "create",
|
|
261
|
+
data: {
|
|
262
|
+
issue: {
|
|
263
|
+
id: "issue-followup-completed-001",
|
|
264
|
+
identifier: "ENG-150",
|
|
265
|
+
title: "Fix login bug again",
|
|
266
|
+
url: "https://linear.app/team/issue/ENG-150",
|
|
267
|
+
description: "Still broken",
|
|
268
|
+
},
|
|
269
|
+
},
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
await handleAgentSessionEvent(event);
|
|
273
|
+
|
|
274
|
+
// tracker_sync should now point to a NEW task
|
|
275
|
+
const sync = getTrackerSyncByExternalId("linear", "task", "issue-followup-completed-001");
|
|
228
276
|
expect(sync).not.toBeNull();
|
|
277
|
+
expect(sync!.swarmId).not.toBe(originalTask.id);
|
|
278
|
+
|
|
279
|
+
// New task should exist and use the reassigned template
|
|
280
|
+
const followupTask = getTaskById(sync!.swarmId);
|
|
281
|
+
expect(followupTask).not.toBeNull();
|
|
282
|
+
expect(followupTask!.source).toBe("linear");
|
|
283
|
+
expect(followupTask!.taskType).toBe("linear-issue");
|
|
284
|
+
expect(followupTask!.task).toContain("[Linear ENG-150]");
|
|
285
|
+
expect(followupTask!.task).toContain("Re-assigned");
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
test("creates follow-up task when already-tracked issue has a failed task", async () => {
|
|
289
|
+
const originalTask = createTaskExtended("Failed linear task", {
|
|
290
|
+
source: "linear",
|
|
291
|
+
taskType: "linear-issue",
|
|
292
|
+
});
|
|
293
|
+
const { getDb } = await import("../be/db");
|
|
294
|
+
getDb().query("UPDATE agent_tasks SET status = 'failed' WHERE id = ?").run(originalTask.id);
|
|
295
|
+
|
|
296
|
+
createTrackerSync({
|
|
297
|
+
provider: "linear",
|
|
298
|
+
entityType: "task",
|
|
299
|
+
providerEntityType: "Issue",
|
|
300
|
+
swarmId: originalTask.id,
|
|
301
|
+
externalId: "issue-followup-failed-001",
|
|
302
|
+
externalIdentifier: "ENG-151",
|
|
303
|
+
externalUrl: "https://linear.app/team/issue/ENG-151",
|
|
304
|
+
lastSyncOrigin: "external",
|
|
305
|
+
syncDirection: "inbound",
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
const event = {
|
|
309
|
+
type: "AgentSession",
|
|
310
|
+
action: "create",
|
|
311
|
+
data: {
|
|
312
|
+
issue: {
|
|
313
|
+
id: "issue-followup-failed-001",
|
|
314
|
+
identifier: "ENG-151",
|
|
315
|
+
title: "Deploy pipeline fix",
|
|
316
|
+
url: "https://linear.app/team/issue/ENG-151",
|
|
317
|
+
},
|
|
318
|
+
},
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
await handleAgentSessionEvent(event);
|
|
322
|
+
|
|
323
|
+
const sync = getTrackerSyncByExternalId("linear", "task", "issue-followup-failed-001");
|
|
324
|
+
expect(sync).not.toBeNull();
|
|
325
|
+
expect(sync!.swarmId).not.toBe(originalTask.id);
|
|
326
|
+
|
|
327
|
+
const followupTask = getTaskById(sync!.swarmId);
|
|
328
|
+
expect(followupTask).not.toBeNull();
|
|
329
|
+
expect(followupTask!.source).toBe("linear");
|
|
229
330
|
});
|
|
230
331
|
|
|
231
332
|
test("skips event with no issue data", async () => {
|