@desplega.ai/agent-swarm 1.2.0 → 1.9.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 (123) hide show
  1. package/.claude/settings.local.json +20 -1
  2. package/.dockerignore +3 -0
  3. package/.env.docker.example +22 -1
  4. package/.env.example +17 -0
  5. package/.github/workflows/docker-publish.yml +92 -0
  6. package/CONTRIBUTING.md +270 -0
  7. package/DEPLOYMENT.md +391 -0
  8. package/Dockerfile.worker +29 -1
  9. package/FAQ.md +19 -0
  10. package/LICENSE +21 -0
  11. package/MCP.md +249 -0
  12. package/README.md +105 -185
  13. package/assets/agent-swarm-logo-orange.png +0 -0
  14. package/assets/agent-swarm-logo.png +0 -0
  15. package/assets/agent-swarm.png +0 -0
  16. package/deploy/docker-push.ts +30 -0
  17. package/docker-compose.example.yml +137 -0
  18. package/docker-entrypoint.sh +223 -7
  19. package/package.json +13 -4
  20. package/{cc-plugin → plugin}/.claude-plugin/plugin.json +1 -1
  21. package/plugin/README.md +1 -0
  22. package/plugin/agents/.gitkeep +0 -0
  23. package/plugin/agents/codebase-analyzer.md +143 -0
  24. package/plugin/agents/codebase-locator.md +122 -0
  25. package/plugin/agents/codebase-pattern-finder.md +227 -0
  26. package/plugin/agents/web-search-researcher.md +109 -0
  27. package/plugin/commands/create-plan.md +415 -0
  28. package/plugin/commands/implement-plan.md +89 -0
  29. package/plugin/commands/research.md +200 -0
  30. package/plugin/commands/start-leader.md +101 -0
  31. package/plugin/commands/start-worker.md +56 -0
  32. package/plugin/commands/swarm-chat.md +78 -0
  33. package/plugin/commands/todos.md +66 -0
  34. package/plugin/commands/work-on-task.md +44 -0
  35. package/plugin/skills/.gitkeep +0 -0
  36. package/scripts/generate-mcp-docs.ts +415 -0
  37. package/slack-manifest.json +69 -0
  38. package/src/be/db.ts +1431 -25
  39. package/src/cli.tsx +135 -11
  40. package/src/commands/lead.ts +13 -0
  41. package/src/commands/runner.ts +255 -0
  42. package/src/commands/setup.tsx +5 -5
  43. package/src/commands/worker.ts +8 -220
  44. package/src/hooks/hook.ts +108 -14
  45. package/src/http.ts +361 -5
  46. package/src/prompts/base-prompt.ts +131 -0
  47. package/src/server.ts +56 -0
  48. package/src/slack/app.ts +73 -0
  49. package/src/slack/commands.ts +88 -0
  50. package/src/slack/handlers.ts +281 -0
  51. package/src/slack/index.ts +3 -0
  52. package/src/slack/responses.ts +175 -0
  53. package/src/slack/router.ts +170 -0
  54. package/src/slack/types.ts +20 -0
  55. package/src/slack/watcher.ts +119 -0
  56. package/src/tools/create-channel.ts +80 -0
  57. package/src/tools/get-tasks.ts +54 -21
  58. package/src/tools/join-swarm.ts +28 -4
  59. package/src/tools/list-channels.ts +37 -0
  60. package/src/tools/list-services.ts +110 -0
  61. package/src/tools/poll-task.ts +47 -3
  62. package/src/tools/post-message.ts +87 -0
  63. package/src/tools/read-messages.ts +192 -0
  64. package/src/tools/register-service.ts +118 -0
  65. package/src/tools/send-task.ts +80 -7
  66. package/src/tools/store-progress.ts +9 -3
  67. package/src/tools/task-action.ts +211 -0
  68. package/src/tools/unregister-service.ts +110 -0
  69. package/src/tools/update-profile.ts +105 -0
  70. package/src/tools/update-service-status.ts +118 -0
  71. package/src/types.ts +110 -3
  72. package/src/utils/pretty-print.ts +224 -0
  73. package/thoughts/shared/plans/.gitkeep +0 -0
  74. package/thoughts/shared/plans/2025-12-18-inverse-teleport.md +1142 -0
  75. package/thoughts/shared/plans/2025-12-18-slack-integration.md +1195 -0
  76. package/thoughts/shared/plans/2025-12-19-agent-log-streaming.md +732 -0
  77. package/thoughts/shared/plans/2025-12-19-role-based-swarm-plugin.md +361 -0
  78. package/thoughts/shared/plans/2025-12-20-mobile-responsive-ui.md +501 -0
  79. package/thoughts/shared/plans/2025-12-20-startup-team-swarm.md +560 -0
  80. package/thoughts/shared/research/.gitkeep +0 -0
  81. package/thoughts/shared/research/2025-12-18-slack-integration.md +442 -0
  82. package/thoughts/shared/research/2025-12-19-agent-log-streaming.md +339 -0
  83. package/thoughts/shared/research/2025-12-19-agent-secrets-cli-research.md +390 -0
  84. package/thoughts/shared/research/2025-12-21-gemini-cli-integration.md +376 -0
  85. package/thoughts/shared/research/2025-12-22-setup-experience-improvements.md +264 -0
  86. package/tsconfig.json +3 -1
  87. package/ui/bun.lock +692 -0
  88. package/ui/index.html +22 -0
  89. package/ui/package.json +32 -0
  90. package/ui/pnpm-lock.yaml +3034 -0
  91. package/ui/postcss.config.js +6 -0
  92. package/ui/public/logo.png +0 -0
  93. package/ui/src/App.tsx +43 -0
  94. package/ui/src/components/ActivityFeed.tsx +415 -0
  95. package/ui/src/components/AgentDetailPanel.tsx +534 -0
  96. package/ui/src/components/AgentsPanel.tsx +549 -0
  97. package/ui/src/components/ChatPanel.tsx +1820 -0
  98. package/ui/src/components/ConfigModal.tsx +232 -0
  99. package/ui/src/components/Dashboard.tsx +534 -0
  100. package/ui/src/components/Header.tsx +168 -0
  101. package/ui/src/components/ServicesPanel.tsx +612 -0
  102. package/ui/src/components/StatsBar.tsx +288 -0
  103. package/ui/src/components/StatusBadge.tsx +124 -0
  104. package/ui/src/components/TaskDetailPanel.tsx +807 -0
  105. package/ui/src/components/TasksPanel.tsx +575 -0
  106. package/ui/src/hooks/queries.ts +170 -0
  107. package/ui/src/index.css +235 -0
  108. package/ui/src/lib/api.ts +161 -0
  109. package/ui/src/lib/config.ts +35 -0
  110. package/ui/src/lib/theme.ts +214 -0
  111. package/ui/src/lib/utils.ts +48 -0
  112. package/ui/src/main.tsx +32 -0
  113. package/ui/src/types/api.ts +164 -0
  114. package/ui/src/vite-env.d.ts +1 -0
  115. package/ui/tailwind.config.js +35 -0
  116. package/ui/tsconfig.json +31 -0
  117. package/ui/vite.config.ts +22 -0
  118. package/cc-plugin/README.md +0 -49
  119. package/cc-plugin/commands/setup-leader.md +0 -73
  120. package/cc-plugin/commands/start-worker.md +0 -64
  121. package/docker-compose.worker.yml +0 -35
  122. package/example-req-meta.json +0 -24
  123. /package/{cc-plugin → plugin}/hooks/hooks.json +0 -0
@@ -0,0 +1,170 @@
1
+ import { getAgentById, getAgentWorkingOnThread, getAllAgents } from "../be/db";
2
+ import type { AgentMatch } from "./types";
3
+
4
+ interface ThreadContext {
5
+ channelId: string;
6
+ threadTs: string;
7
+ }
8
+
9
+ // Common 3-letter words to exclude from matching
10
+ const COMMON_WORDS = new Set([
11
+ "the",
12
+ "and",
13
+ "for",
14
+ "are",
15
+ "but",
16
+ "not",
17
+ "you",
18
+ "all",
19
+ "can",
20
+ "had",
21
+ "her",
22
+ "was",
23
+ "one",
24
+ "our",
25
+ "out",
26
+ "has",
27
+ "his",
28
+ "how",
29
+ "its",
30
+ "let",
31
+ "may",
32
+ "new",
33
+ "now",
34
+ "old",
35
+ "see",
36
+ "way",
37
+ "who",
38
+ "boy",
39
+ "did",
40
+ "get",
41
+ "say",
42
+ "she",
43
+ "too",
44
+ "use",
45
+ "hey",
46
+ "hi",
47
+ "hello",
48
+ "please",
49
+ "help",
50
+ ]);
51
+
52
+ /**
53
+ * Check if a word is suitable for agent name matching.
54
+ * Allows 3+ char words if they're not common words.
55
+ * Always allows uppercase words (CEO, CTO, etc).
56
+ */
57
+ function isMatchableWord(word: string): boolean {
58
+ if (word.length < 3) return false;
59
+ // Always allow fully uppercase words (acronyms like CEO, CTO)
60
+ if (word === word.toUpperCase() && word.length >= 3) return true;
61
+ // Allow 3+ char words that aren't common
62
+ if (word.length >= 3 && !COMMON_WORDS.has(word.toLowerCase())) return true;
63
+ return false;
64
+ }
65
+
66
+ /**
67
+ * Routes a Slack message to the appropriate agent(s) based on mentions.
68
+ *
69
+ * Routing rules:
70
+ * - `swarm#<uuid>` → exact agent by ID
71
+ * - `swarm#all` → all non-lead agents
72
+ * - Partial name match (3+ chars, not common words) → agent by name
73
+ * - Multiple partial matches → route to lead (let lead decide)
74
+ * - Thread follow-up (no match but agent working on thread) → route to that agent
75
+ * - Bot @mention only → lead agent
76
+ */
77
+ export function routeMessage(
78
+ text: string,
79
+ _botUserId: string,
80
+ botMentioned: boolean,
81
+ threadContext?: ThreadContext,
82
+ ): AgentMatch[] {
83
+ const matches: AgentMatch[] = [];
84
+ const agents = getAllAgents().filter((a) => a.status !== "offline");
85
+
86
+ // Check for explicit swarm#<id> syntax
87
+ const idMatches = text.matchAll(/swarm#([a-f0-9-]{36})/gi);
88
+ for (const match of idMatches) {
89
+ const agentId = match[1];
90
+ if (!agentId) continue;
91
+ const agent = getAgentById(agentId);
92
+ if (agent && agent.status !== "offline") {
93
+ matches.push({ agent, matchedText: match[0] });
94
+ }
95
+ }
96
+
97
+ // Check for swarm#all broadcast
98
+ if (/swarm#all/i.test(text)) {
99
+ const nonLeadAgents = agents.filter((a) => !a.isLead);
100
+ for (const agent of nonLeadAgents) {
101
+ if (!matches.some((m) => m.agent.id === agent.id)) {
102
+ matches.push({ agent, matchedText: "swarm#all" });
103
+ }
104
+ }
105
+ }
106
+
107
+ // Check for partial name matches (3+ chars, not common words)
108
+ if (matches.length === 0) {
109
+ for (const agent of agents) {
110
+ const nameWords = agent.name.split(/\s+/).filter(isMatchableWord);
111
+ for (const word of nameWords) {
112
+ const regex = new RegExp(`\\b${escapeRegex(word)}\\b`, "i");
113
+ if (regex.test(text)) {
114
+ if (!matches.some((m) => m.agent.id === agent.id)) {
115
+ matches.push({ agent, matchedText: word });
116
+ }
117
+ break;
118
+ }
119
+ }
120
+ }
121
+
122
+ // If multiple agents matched a partial name, route to lead instead (let lead decide)
123
+ if (matches.length > 1) {
124
+ const lead = agents.find((a) => a.isLead);
125
+ if (lead) {
126
+ const matchedWords = matches.map((m) => m.matchedText).join(", ");
127
+ const matchedAgents = matches.map((m) => m.agent.name).join(", ");
128
+ return [
129
+ {
130
+ agent: lead,
131
+ matchedText: `ambiguous match "${matchedWords}" (could be: ${matchedAgents})`,
132
+ },
133
+ ];
134
+ }
135
+ }
136
+ }
137
+
138
+ // Thread follow-up: If no matches and we're in a thread, check if an agent is working on it
139
+ if (matches.length === 0 && threadContext) {
140
+ const workingAgent = getAgentWorkingOnThread(threadContext.channelId, threadContext.threadTs);
141
+ if (workingAgent && workingAgent.status !== "offline") {
142
+ matches.push({ agent: workingAgent, matchedText: "thread follow-up" });
143
+ }
144
+ }
145
+
146
+ // If only bot was mentioned and no agents matched, route to lead
147
+ if (matches.length === 0 && botMentioned) {
148
+ const lead = agents.find((a) => a.isLead);
149
+ if (lead) {
150
+ matches.push({ agent: lead, matchedText: "@bot" });
151
+ }
152
+ }
153
+
154
+ return matches;
155
+ }
156
+
157
+ function escapeRegex(str: string): string {
158
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
159
+ }
160
+
161
+ /**
162
+ * Extracts the task description from a message, removing bot mentions and agent references.
163
+ */
164
+ export function extractTaskFromMessage(text: string, botUserId: string): string {
165
+ return text
166
+ .replace(new RegExp(`<@${botUserId}>`, "g"), "") // Remove bot mentions
167
+ .replace(/swarm#[a-f0-9-]{36}/gi, "") // Remove swarm#<id>
168
+ .replace(/swarm#all/gi, "") // Remove swarm#all
169
+ .trim();
170
+ }
@@ -0,0 +1,20 @@
1
+ import type { Agent } from "../types";
2
+
3
+ export interface SlackMessageContext {
4
+ channelId: string;
5
+ threadTs?: string;
6
+ userId: string;
7
+ text: string;
8
+ botUserId: string;
9
+ }
10
+
11
+ export interface AgentMatch {
12
+ agent: Agent;
13
+ matchedText: string;
14
+ }
15
+
16
+ export interface SlackConfig {
17
+ botToken: string;
18
+ appToken: string;
19
+ signingSecret?: string;
20
+ }
@@ -0,0 +1,119 @@
1
+ import { getCompletedSlackTasks, getInProgressSlackTasks } from "../be/db";
2
+ import { getSlackApp } from "./app";
3
+ import { sendProgressUpdate, sendTaskResponse } from "./responses";
4
+
5
+ let watcherInterval: ReturnType<typeof setInterval> | null = null;
6
+ let isProcessing = false;
7
+
8
+ // Track notified completion tasks (taskId -> timestamp)
9
+ const notifiedCompletions = new Map<string, number>();
10
+
11
+ // Track sent progress messages (taskId -> last progress text)
12
+ const sentProgress = new Map<string, string>();
13
+
14
+ // Track in-flight sends to prevent race conditions
15
+ const pendingSends = new Set<string>();
16
+
17
+ // Track last send time per task to throttle (taskId -> timestamp)
18
+ const lastSendTime = new Map<string, number>();
19
+ const MIN_SEND_INTERVAL = 1000; // Don't send for same task within 1 second
20
+
21
+ /**
22
+ * Start watching for Slack task updates and sending responses.
23
+ */
24
+ export function startTaskWatcher(intervalMs = 3000): void {
25
+ if (watcherInterval) {
26
+ console.log("[Slack] Task watcher already running");
27
+ return;
28
+ }
29
+
30
+ // Initialize with existing completed tasks to avoid re-notifying on restart
31
+ const existingCompleted = getCompletedSlackTasks();
32
+ const now = Date.now();
33
+ for (const task of existingCompleted) {
34
+ notifiedCompletions.set(task.id, now);
35
+ }
36
+ console.log(`[Slack] Initialized with ${existingCompleted.length} existing completed tasks`);
37
+
38
+ watcherInterval = setInterval(async () => {
39
+ // Prevent overlapping processing cycles
40
+ if (isProcessing || !getSlackApp()) return;
41
+ isProcessing = true;
42
+
43
+ try {
44
+ // Check for progress updates on in-progress tasks
45
+ const inProgressTasks = getInProgressSlackTasks();
46
+ const now = Date.now();
47
+ for (const task of inProgressTasks) {
48
+ const progressKey = `progress:${task.id}`;
49
+
50
+ // Skip if already sending or sent recently (throttle)
51
+ if (pendingSends.has(progressKey)) continue;
52
+ const lastSent = lastSendTime.get(progressKey);
53
+ if (lastSent && now - lastSent < MIN_SEND_INTERVAL) continue;
54
+
55
+ const lastSentProgress = sentProgress.get(task.id);
56
+ // Only send if progress exists and is different from last sent
57
+ if (task.progress && task.progress !== lastSentProgress) {
58
+ // Mark as pending and sent BEFORE sending
59
+ pendingSends.add(progressKey);
60
+ sentProgress.set(task.id, task.progress);
61
+ lastSendTime.set(progressKey, now);
62
+ try {
63
+ await sendProgressUpdate(task, task.progress);
64
+ console.log(`[Slack] Sent progress update for task ${task.id.slice(0, 8)}`);
65
+ } catch (error) {
66
+ // If send fails, clear markers so we can retry
67
+ sentProgress.delete(task.id);
68
+ lastSendTime.delete(progressKey);
69
+ console.error(`[Slack] Failed to send progress:`, error);
70
+ } finally {
71
+ pendingSends.delete(progressKey);
72
+ }
73
+ }
74
+ }
75
+
76
+ // Check for completed tasks
77
+ const completedTasks = getCompletedSlackTasks();
78
+ for (const task of completedTasks) {
79
+ const completionKey = `completion:${task.id}`;
80
+
81
+ // Skip if already notified or currently sending or sent recently
82
+ if (notifiedCompletions.has(task.id) || pendingSends.has(completionKey)) continue;
83
+ const lastSent = lastSendTime.get(completionKey);
84
+ if (lastSent && now - lastSent < MIN_SEND_INTERVAL) continue;
85
+
86
+ // Mark as pending and notified BEFORE sending
87
+ pendingSends.add(completionKey);
88
+ notifiedCompletions.set(task.id, now);
89
+ lastSendTime.set(completionKey, now);
90
+ try {
91
+ await sendTaskResponse(task);
92
+ // Clean up progress tracking
93
+ sentProgress.delete(task.id);
94
+ console.log(`[Slack] Sent ${task.status} response for task ${task.id.slice(0, 8)}`);
95
+ } catch (error) {
96
+ // If send fails, remove from notified so we can retry
97
+ notifiedCompletions.delete(task.id);
98
+ lastSendTime.delete(completionKey);
99
+ console.error(`[Slack] Failed to send completion:`, error);
100
+ } finally {
101
+ pendingSends.delete(completionKey);
102
+ }
103
+ }
104
+ } finally {
105
+ isProcessing = false;
106
+ }
107
+ }, intervalMs);
108
+
109
+ console.log(`[Slack] Task watcher started (interval: ${intervalMs}ms)`);
110
+ }
111
+
112
+ export function stopTaskWatcher(): void {
113
+ if (watcherInterval) {
114
+ clearInterval(watcherInterval);
115
+ watcherInterval = null;
116
+ isProcessing = false;
117
+ console.log("[Slack] Task watcher stopped");
118
+ }
119
+ }
@@ -0,0 +1,80 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import * as z from "zod";
3
+ import { createChannel, getChannelByName } from "@/be/db";
4
+ import { createToolRegistrar } from "@/tools/utils";
5
+ import { ChannelSchema, ChannelTypeSchema } from "@/types";
6
+
7
+ export const registerCreateChannelTool = (server: McpServer) => {
8
+ createToolRegistrar(server)(
9
+ "create-channel",
10
+ {
11
+ title: "Create Channel",
12
+ description: "Creates a new channel for cross-agent communication.",
13
+ inputSchema: z.object({
14
+ name: z.string().min(1).max(100).describe("Channel name (must be unique)."),
15
+ description: z.string().max(500).optional().describe("Channel description."),
16
+ type: ChannelTypeSchema.optional().describe("Channel type: 'public' (default) or 'dm'."),
17
+ participants: z.array(z.uuid()).optional().describe("Agent IDs for DM channels."),
18
+ }),
19
+ outputSchema: z.object({
20
+ success: z.boolean(),
21
+ message: z.string(),
22
+ channel: ChannelSchema.optional(),
23
+ }),
24
+ },
25
+ async ({ name, description, type, participants }, requestInfo, _meta) => {
26
+ if (!requestInfo.agentId) {
27
+ return {
28
+ content: [{ type: "text", text: 'Agent ID not found. Set the "X-Agent-ID" header.' }],
29
+ structuredContent: {
30
+ success: false,
31
+ message: 'Agent ID not found. Set the "X-Agent-ID" header.',
32
+ },
33
+ };
34
+ }
35
+
36
+ // Check if channel already exists
37
+ const existing = getChannelByName(name);
38
+ if (existing) {
39
+ return {
40
+ content: [{ type: "text", text: `Channel "${name}" already exists.` }],
41
+ structuredContent: {
42
+ yourAgentId: requestInfo.agentId,
43
+ success: false,
44
+ message: `Channel "${name}" already exists.`,
45
+ channel: existing,
46
+ },
47
+ };
48
+ }
49
+
50
+ try {
51
+ const channel = createChannel(name, {
52
+ description,
53
+ type: type ?? "public",
54
+ createdBy: requestInfo.agentId,
55
+ participants,
56
+ });
57
+
58
+ return {
59
+ content: [{ type: "text", text: `Created channel "${name}".` }],
60
+ structuredContent: {
61
+ yourAgentId: requestInfo.agentId,
62
+ success: true,
63
+ message: `Created channel "${name}".`,
64
+ channel,
65
+ },
66
+ };
67
+ } catch (error) {
68
+ const message = error instanceof Error ? error.message : "Unknown error";
69
+ return {
70
+ content: [{ type: "text", text: `Failed to create channel: ${message}` }],
71
+ structuredContent: {
72
+ yourAgentId: requestInfo.agentId,
73
+ success: false,
74
+ message: `Failed to create channel: ${message}`,
75
+ },
76
+ };
77
+ }
78
+ },
79
+ );
80
+ };
@@ -6,8 +6,14 @@ import { AgentTaskStatusSchema } from "@/types";
6
6
 
7
7
  const TaskSummarySchema = z.object({
8
8
  id: z.string(),
9
+ agentId: z.string().nullable(),
9
10
  task: z.string(),
10
11
  status: AgentTaskStatusSchema,
12
+ taskType: z.string().optional(),
13
+ tags: z.array(z.string()),
14
+ priority: z.number(),
15
+ dependsOn: z.array(z.string()),
16
+ offeredTo: z.string().optional(),
11
17
  createdAt: z.string(),
12
18
  lastUpdatedAt: z.string(),
13
19
  finishedAt: z.string().optional(),
@@ -20,56 +26,83 @@ export const registerGetTasksTool = (server: McpServer) => {
20
26
  {
21
27
  title: "Get tasks",
22
28
  description:
23
- "Returns a list of tasks in the swarm, filtered by status and sorted by lastUpdatedAt desc. Defaults to in_progress tasks only. Does not return output or failure reason.",
29
+ "Returns a list of tasks in the swarm with various filters. Sorted by priority (desc) then lastUpdatedAt (desc).",
24
30
  inputSchema: z.object({
25
31
  status: AgentTaskStatusSchema.optional().describe(
26
- "Filter by task status. Defaults to 'in_progress'.",
32
+ "Filter by task status (unassigned, offered, pending, in_progress, completed, failed).",
27
33
  ),
28
- mineOnly: z
34
+ mineOnly: z.boolean().optional().describe("Only return tasks assigned to you."),
35
+ unassigned: z.boolean().optional().describe("Only return unassigned tasks in the pool."),
36
+ offeredToMe: z
29
37
  .boolean()
30
38
  .optional()
31
- .describe(
32
- "If true, only return tasks assigned to your agent. Requires X-Agent-ID header.",
33
- ),
39
+ .describe("Only return tasks offered to you (awaiting accept/reject)."),
40
+ readyOnly: z.boolean().optional().describe("Only return tasks whose dependencies are met."),
41
+ taskType: z.string().optional().describe("Filter by task type (e.g., 'bug', 'feature')."),
42
+ tags: z.array(z.string()).optional().describe("Filter by any matching tag."),
43
+ search: z.string().optional().describe("Search in task description."),
34
44
  }),
35
45
  outputSchema: z.object({
36
46
  tasks: z.array(TaskSummarySchema),
37
47
  }),
38
48
  },
39
- async ({ status, mineOnly }, requestInfo, _meta) => {
40
- const filterStatus = status ?? "in_progress";
41
- let tasks = getAllTasks(filterStatus);
49
+ async (
50
+ { status, mineOnly, unassigned, offeredToMe, readyOnly, taskType, tags, search },
51
+ requestInfo,
52
+ _meta,
53
+ ) => {
54
+ const agentId = requestInfo.agentId;
42
55
 
43
- // Filter to only tasks assigned to this agent if mineOnly is true
44
- if (mineOnly) {
45
- if (!requestInfo.agentId) {
46
- // No agent ID set, return empty list
47
- tasks = [];
48
- } else {
49
- tasks = tasks.filter((t) => t.agentId === requestInfo.agentId);
50
- }
51
- }
56
+ // Build filters
57
+ const tasks = getAllTasks({
58
+ status,
59
+ agentId: mineOnly ? (agentId ?? undefined) : undefined,
60
+ unassigned,
61
+ offeredTo: offeredToMe ? (agentId ?? undefined) : undefined,
62
+ readyOnly,
63
+ taskType,
64
+ tags,
65
+ search,
66
+ });
52
67
 
53
68
  const taskSummaries = tasks.map((t) => ({
54
69
  id: t.id,
70
+ agentId: t.agentId,
55
71
  task: t.task,
56
72
  status: t.status,
73
+ taskType: t.taskType,
74
+ tags: t.tags,
75
+ priority: t.priority,
76
+ dependsOn: t.dependsOn,
77
+ offeredTo: t.offeredTo,
57
78
  createdAt: t.createdAt,
58
79
  lastUpdatedAt: t.lastUpdatedAt,
59
80
  finishedAt: t.finishedAt,
60
81
  progress: t.progress,
61
82
  }));
62
83
 
63
- const mineOnlyMsg = mineOnly ? " (mine only)" : "";
84
+ // Build filter description for message
85
+ const filters: string[] = [];
86
+ if (status) filters.push(`status='${status}'`);
87
+ if (mineOnly) filters.push("mine only");
88
+ if (unassigned) filters.push("unassigned");
89
+ if (offeredToMe) filters.push("offered to me");
90
+ if (readyOnly) filters.push("ready only");
91
+ if (taskType) filters.push(`type='${taskType}'`);
92
+ if (tags?.length) filters.push(`tags=[${tags.join(", ")}]`);
93
+ if (search) filters.push(`search='${search}'`);
94
+
95
+ const filterMsg = filters.length > 0 ? ` (${filters.join(", ")})` : "";
96
+
64
97
  return {
65
98
  content: [
66
99
  {
67
100
  type: "text",
68
- text: `Found ${taskSummaries.length} task(s) with status '${filterStatus}'${mineOnlyMsg}.`,
101
+ text: `Found ${taskSummaries.length} task(s)${filterMsg}.`,
69
102
  },
70
103
  ],
71
104
  structuredContent: {
72
- yourAgentId: requestInfo.agentId,
105
+ yourAgentId: agentId,
73
106
  tasks: taskSummaries,
74
107
  },
75
108
  };
@@ -1,6 +1,6 @@
1
1
  import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import * as z from "zod";
3
- import { createAgent, getAllAgents, getDb } from "@/be/db";
3
+ import { createAgent, getAllAgents, getDb, updateAgentProfile } from "@/be/db";
4
4
  import { createToolRegistrar } from "@/tools/utils";
5
5
  import { AgentSchema } from "@/types";
6
6
 
@@ -9,7 +9,8 @@ export const registerJoinSwarmTool = (server: McpServer) => {
9
9
  "join-swarm",
10
10
  {
11
11
  title: "Join the agent swarm",
12
- description: "Tool for an agent to join the swarm of agents.",
12
+ description:
13
+ "Tool for an agent to join the swarm of agents with optional profile information.",
13
14
  inputSchema: z.object({
14
15
  requestedId: z
15
16
  .string()
@@ -17,6 +18,16 @@ export const registerJoinSwarmTool = (server: McpServer) => {
17
18
  .describe("Requested ID for the agent (overridden by X-Agent-ID header)."),
18
19
  lead: z.boolean().default(false).describe("Whether this agent should be the lead."),
19
20
  name: z.string().min(1).describe("The name of the agent joining the swarm."),
21
+ description: z.string().optional().describe("Agent description."),
22
+ role: z
23
+ .string()
24
+ .max(100)
25
+ .optional()
26
+ .describe("Agent role (free-form, e.g., 'frontend dev', 'code reviewer')."),
27
+ capabilities: z
28
+ .array(z.string())
29
+ .optional()
30
+ .describe("List of capabilities (e.g., ['typescript', 'react', 'testing'])."),
20
31
  }),
21
32
  outputSchema: z.object({
22
33
  success: z.boolean(),
@@ -24,7 +35,7 @@ export const registerJoinSwarmTool = (server: McpServer) => {
24
35
  agent: AgentSchema.optional(),
25
36
  }),
26
37
  },
27
- async ({ lead, name, requestedId }, requestInfo, _meta) => {
38
+ async ({ lead, name, requestedId, description, role, capabilities }, requestInfo, _meta) => {
28
39
  // Check if agent ID is set
29
40
  if (!requestInfo.agentId && !requestedId) {
30
41
  return {
@@ -70,12 +81,25 @@ export const registerJoinSwarmTool = (server: McpServer) => {
70
81
  );
71
82
  }
72
83
 
73
- return createAgent({
84
+ const agent = createAgent({
74
85
  id: agentId,
75
86
  name,
76
87
  isLead: lead,
77
88
  status: "idle",
89
+ capabilities: [],
78
90
  });
91
+
92
+ // Update profile if any profile fields were provided
93
+ if (description !== undefined || role !== undefined || capabilities !== undefined) {
94
+ const updatedAgent = updateAgentProfile(agent.id, {
95
+ description,
96
+ role,
97
+ capabilities,
98
+ });
99
+ return updatedAgent ?? agent;
100
+ }
101
+
102
+ return agent;
79
103
  });
80
104
 
81
105
  const agent = agentTx();
@@ -0,0 +1,37 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import * as z from "zod";
3
+ import { getAllChannels } from "@/be/db";
4
+ import { createToolRegistrar } from "@/tools/utils";
5
+ import { ChannelSchema } from "@/types";
6
+
7
+ export const registerListChannelsTool = (server: McpServer) => {
8
+ createToolRegistrar(server)(
9
+ "list-channels",
10
+ {
11
+ title: "List Channels",
12
+ description: "Lists all available channels for cross-agent communication.",
13
+ inputSchema: z.object({}),
14
+ outputSchema: z.object({
15
+ success: z.boolean(),
16
+ channels: z.array(ChannelSchema),
17
+ }),
18
+ },
19
+ async (_input, requestInfo, _meta) => {
20
+ const channels = getAllChannels();
21
+
22
+ return {
23
+ content: [
24
+ {
25
+ type: "text",
26
+ text: `Found ${channels.length} channel(s): ${channels.map((c) => c.name).join(", ") || "(none)"}`,
27
+ },
28
+ ],
29
+ structuredContent: {
30
+ yourAgentId: requestInfo.agentId,
31
+ success: true,
32
+ channels,
33
+ },
34
+ };
35
+ },
36
+ );
37
+ };