@desplega.ai/agent-swarm 1.72.1 → 1.73.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/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.72.1",
5
+ "version": "1.73.1",
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@desplega.ai/agent-swarm",
3
- "version": "1.72.1",
3
+ "version": "1.73.1",
4
4
  "description": "Multi-agent orchestration for Claude Code, Codex, Gemini CLI, and other AI coding assistants",
5
5
  "license": "MIT",
6
6
  "author": "desplega.sh <contact@desplega.sh>",
@@ -136,6 +136,11 @@ function getWebhookUrl(req: IncomingMessage): string {
136
136
  }
137
137
 
138
138
  function getRedirectUri(req: IncomingMessage): string {
139
+ // Mirror src/jira/app.ts: prefer the explicit JIRA_REDIRECT_URI override,
140
+ // otherwise derive from the API base URL. Keeps the UI display consistent
141
+ // with the URI persisted into oauth_apps and used in the actual OAuth flow.
142
+ const override = process.env.JIRA_REDIRECT_URI?.trim();
143
+ if (override) return override;
139
144
  return `${deriveApiBaseUrl(req)}/api/trackers/jira/callback`;
140
145
  }
141
146
 
@@ -5,7 +5,7 @@
5
5
  * across responses.ts, handlers.ts, thread-buffer.ts).
6
6
  */
7
7
 
8
- const appUrl = process.env.APP_URL || "";
8
+ import { getAppUrl } from "../utils/constants";
9
9
 
10
10
  // Slack limits section text to 3000 chars; we use 2900 for safety
11
11
  const MAX_SECTION_LENGTH = 2900;
@@ -16,24 +16,21 @@ type SlackBlock = any;
16
16
  // --- Shared utilities ---
17
17
 
18
18
  /**
19
- * Get a Slack-formatted link to the task in the dashboard, or just the short ID.
19
+ * Get a Slack-formatted clickable link to the task in the dashboard.
20
+ * Always returns Slack mrkdwn link syntax (`<url|label>`) so partial task
21
+ * IDs are clickable in every message — falls back to the public dashboard
22
+ * when APP_URL is not configured.
20
23
  */
21
24
  export function getTaskLink(taskId: string): string {
22
25
  const shortId = taskId.slice(0, 8);
23
- if (appUrl) {
24
- return `<${appUrl}?tab=tasks&task=${taskId}&expand=true|\`${shortId}\`>`;
25
- }
26
- return `\`${shortId}\``;
26
+ return `<${getTaskUrl(taskId)}|\`${shortId}\`>`;
27
27
  }
28
28
 
29
29
  /**
30
30
  * Get a raw dashboard URL for a task (for link buttons).
31
31
  */
32
32
  export function getTaskUrl(taskId: string): string {
33
- if (appUrl) {
34
- return `${appUrl}?tab=tasks&task=${taskId}&expand=true`;
35
- }
36
- return "";
33
+ return `${getAppUrl()}/tasks/${taskId}`;
37
34
  }
38
35
 
39
36
  /**
@@ -214,9 +211,9 @@ export function buildProgressBlocks(opts: {
214
211
  taskId: string;
215
212
  progress: string;
216
213
  }): SlackBlock[] {
217
- const shortId = opts.taskId.slice(0, 8);
214
+ const taskLink = getTaskLink(opts.taskId);
218
215
  return [
219
- sectionBlock(`*${opts.agentName}* (\`${shortId}\`): ${opts.progress}`),
216
+ sectionBlock(`*${opts.agentName}* (${taskLink}): ${opts.progress}`),
220
217
  cancelActionBlock(opts.taskId),
221
218
  ];
222
219
  }
@@ -352,12 +349,10 @@ function renderTree(root: TreeNode): string {
352
349
  const visibleChildren = root.children.slice(0, MAX_VISIBLE_CHILDREN);
353
350
  const hiddenCount = root.children.length - visibleChildren.length;
354
351
 
355
- for (let i = 0; i < visibleChildren.length; i++) {
356
- const child = visibleChildren[i] as TreeNode;
357
- const isLast = i === visibleChildren.length - 1 && hiddenCount === 0;
358
- const prefix = isLast ? "└ " : "├ ";
359
- const continuationPrefix = isLast ? " " : "│ ";
352
+ const prefix = "↳ ";
353
+ const continuationPrefix = " ";
360
354
 
355
+ for (const child of visibleChildren) {
361
356
  lines.push(`${prefix}${renderNodeLine(child)}`);
362
357
 
363
358
  for (const detail of renderChildDetail(child, continuationPrefix)) {
@@ -366,7 +361,7 @@ function renderTree(root: TreeNode): string {
366
361
  }
367
362
 
368
363
  if (hiddenCount > 0) {
369
- lines.push(`└ _and ${hiddenCount} more..._`);
364
+ lines.push(`↳ _and ${hiddenCount} more..._`);
370
365
  }
371
366
 
372
367
  return lines.join("\n");
@@ -44,22 +44,65 @@ describe("markdownToSlack", () => {
44
44
  });
45
45
 
46
46
  describe("getTaskLink", () => {
47
- test("returns short ID when no APP_URL", () => {
48
- // APP_URL is not set in test env
49
- const link = getTaskLink("abcdef12-3456-7890-abcd-ef1234567890");
50
- expect(link).toContain("abcdef12");
47
+ test("always returns a Slack hyperlink with clickable short ID", () => {
48
+ const taskId = "abcdef12-3456-7890-abcd-ef1234567890";
49
+ const link = getTaskLink(taskId);
50
+ // Slack mrkdwn link syntax: <url|label>
51
+ expect(link).toMatch(
52
+ /^<https?:\/\/.+\/tasks\/abcdef12-3456-7890-abcd-ef1234567890\|`abcdef12`>$/,
53
+ );
54
+ expect(link).toContain("|`abcdef12`>");
55
+ expect(link).toContain(taskId);
56
+ });
57
+
58
+ test("uses APP_URL when set", () => {
59
+ const original = process.env.APP_URL;
60
+ process.env.APP_URL = "https://my-custom-dashboard.example.com";
61
+ try {
62
+ const link = getTaskLink("abcdef12-3456-7890-abcd-ef1234567890");
63
+ expect(link).toContain(
64
+ "https://my-custom-dashboard.example.com/tasks/abcdef12-3456-7890-abcd-ef1234567890",
65
+ );
66
+ } finally {
67
+ if (original === undefined) delete process.env.APP_URL;
68
+ else process.env.APP_URL = original;
69
+ }
70
+ });
71
+
72
+ test("strips trailing slash from APP_URL", () => {
73
+ const original = process.env.APP_URL;
74
+ process.env.APP_URL = "https://dashboard.example.com/";
75
+ try {
76
+ const link = getTaskLink("abcdef12-3456-7890-abcd-ef1234567890");
77
+ expect(link).toContain("https://dashboard.example.com/tasks/");
78
+ expect(link).not.toContain("//tasks/");
79
+ } finally {
80
+ if (original === undefined) delete process.env.APP_URL;
81
+ else process.env.APP_URL = original;
82
+ }
83
+ });
84
+
85
+ test("falls back to public dashboard when APP_URL is unset", () => {
86
+ const original = process.env.APP_URL;
87
+ delete process.env.APP_URL;
88
+ try {
89
+ const link = getTaskLink("abcdef12-3456-7890-abcd-ef1234567890");
90
+ expect(link).toContain(
91
+ "https://app.agent-swarm.dev/tasks/abcdef12-3456-7890-abcd-ef1234567890",
92
+ );
93
+ expect(link.startsWith("<")).toBe(true);
94
+ expect(link.endsWith(">")).toBe(true);
95
+ } finally {
96
+ if (original !== undefined) process.env.APP_URL = original;
97
+ }
51
98
  });
52
99
  });
53
100
 
54
101
  describe("getTaskUrl", () => {
55
- test("returns URL with task ID or empty string", () => {
102
+ test("always returns a non-empty URL containing the task ID", () => {
56
103
  const url = getTaskUrl("some-id");
57
- // When APP_URL is set, URL contains the task ID; when not set, returns ""
58
- if (url) {
59
- expect(url).toContain("some-id");
60
- } else {
61
- expect(url).toBe("");
62
- }
104
+ expect(url).toContain("/tasks/some-id");
105
+ expect(url).toMatch(/^https?:\/\//);
63
106
  });
64
107
  });
65
108
 
@@ -93,6 +136,17 @@ describe("buildCompletedBlocks", () => {
93
136
  expect(blocks[0].text.text).toContain("45s");
94
137
  });
95
138
 
139
+ test("partial task ID is rendered as a clickable Slack hyperlink", () => {
140
+ const blocks = buildCompletedBlocks({
141
+ agentName: "Alpha",
142
+ taskId: "abcdef12-3456-7890-abcd-ef1234567890",
143
+ body: "Done",
144
+ });
145
+ expect(blocks[0].text.text).toMatch(
146
+ /<https?:\/\/[^|>]+\/tasks\/abcdef12-3456-7890-abcd-ef1234567890\|`abcdef12`>/,
147
+ );
148
+ });
149
+
96
150
  test("splits long body into multiple sections", () => {
97
151
  const longBody = "x".repeat(6000);
98
152
  const blocks = buildCompletedBlocks({
@@ -148,7 +202,7 @@ describe("buildProgressBlocks", () => {
148
202
  });
149
203
 
150
204
  expect(blocks.length).toBe(2);
151
- // Single line: *Gamma* (`aabbccdd`): Analyzing codebase...
205
+ // Single line: *Gamma* (<URL|`aabbccdd`>): Analyzing codebase...
152
206
  // (no ⏳ prefix — progress strings now carry their own emoji)
153
207
  expect(blocks[0].type).toBe("section");
154
208
  expect(blocks[0].text.text).not.toContain("⏳");
@@ -161,6 +215,19 @@ describe("buildProgressBlocks", () => {
161
215
  expect(blocks[1].elements[0].style).toBe("danger");
162
216
  expect(blocks[1].elements[0].confirm).toBeDefined();
163
217
  });
218
+
219
+ test("partial task ID is rendered as a clickable Slack hyperlink", () => {
220
+ const taskId = "aabbccdd-1234-5678-9012-abcdefabcdef";
221
+ const blocks = buildProgressBlocks({
222
+ agentName: "Gamma",
223
+ taskId,
224
+ progress: "Working...",
225
+ });
226
+ // Slack mrkdwn link syntax: <url|`shortId`>
227
+ expect(blocks[0].text.text).toMatch(
228
+ /<https?:\/\/[^|>]+\/tasks\/aabbccdd-1234-5678-9012-abcdefabcdef\|`aabbccdd`>/,
229
+ );
230
+ });
164
231
  });
165
232
 
166
233
  describe("buildAssignmentSummaryBlocks", () => {
@@ -549,14 +616,14 @@ describe("buildTreeBlocks", () => {
549
616
 
550
617
  // Root line
551
618
  expect(lines[0]).toContain("⏳ *Lead*");
552
- // Worker1 line with prefix
553
- expect(lines[1]).toMatch(/^├ ⏳ \*Worker1\*/);
554
- // Worker1 progress indented under continuation
555
- expect(lines[2]).toMatch(/^│ {3}Fetching data\.\.\.$/);
556
- // Worker2 line with prefix (last child)
557
- expect(lines[3]).toMatch(/^└ ⏳ \*Worker2\*/);
619
+ // Worker1 line with prefix
620
+ expect(lines[1]).toMatch(/^↳ ⏳ \*Worker1\*/);
621
+ // Worker1 progress indented under continuation (3 spaces, aligned under ↳ )
622
+ expect(lines[2]).toMatch(/^ {3}Fetching data\.\.\.$/);
623
+ // Worker2 line with prefix
624
+ expect(lines[3]).toMatch(/^↳ ⏳ \*Worker2\*/);
558
625
  // Worker2 progress indented
559
- expect(lines[4]).toMatch(/^ {4}Compiling\.\.\.$/);
626
+ expect(lines[4]).toMatch(/^ {3}Compiling\.\.\.$/);
560
627
  });
561
628
 
562
629
  test("max children collapse (9+ children -> 8 shown + 'and 1 more...')", () => {
@@ -587,8 +654,8 @@ describe("buildTreeBlocks", () => {
587
654
  expect(text).toContain("*Worker8*");
588
655
  expect(text).not.toContain("*Worker9*");
589
656
  expect(text).toContain("and 1 more...");
590
- // The "and N more..." line uses prefix
591
- expect(lines[lines.length - 1]).toContain(" _and 1 more..._");
657
+ // The "and N more..." line uses prefix
658
+ expect(lines[lines.length - 1]).toContain(" _and 1 more..._");
592
659
  });
593
660
 
594
661
  test("max children collapse with many hidden", () => {
@@ -770,7 +837,7 @@ describe("buildTreeBlocks", () => {
770
837
  expect(blocks.length).toBe(1);
771
838
  });
772
839
 
773
- test("tree connectors: for non-last, for last child", () => {
840
+ test("tree indent: all children use prefix", () => {
774
841
  const root: TreeNode = {
775
842
  taskId: makeTaskId("nnnn0001"),
776
843
  agentName: "Lead",
@@ -800,10 +867,10 @@ describe("buildTreeBlocks", () => {
800
867
  const blocks = buildTreeBlocks([root]);
801
868
  const lines = blocks[0].text.text.split("\n");
802
869
 
803
- // First two children use ├, last uses
804
- expect(lines[1]).toMatch(/^├ /);
805
- expect(lines[2]).toMatch(/^├ /);
806
- expect(lines[3]).toMatch(/^└ /);
870
+ // All children use (no branching distinction in proportional fonts)
871
+ expect(lines[1]).toMatch(/^↳ /);
872
+ expect(lines[2]).toMatch(/^↳ /);
873
+ expect(lines[3]).toMatch(/^↳ /);
807
874
  });
808
875
 
809
876
  test("completed root with output (no slackReplySent, no children)", () => {
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Shared constants used across worker- and server-side code.
3
+ */
4
+
5
+ /**
6
+ * Default dashboard URL used when `APP_URL` is unset. Points at the public
7
+ * production dashboard so links (Slack messages, approval URLs, etc.) are
8
+ * always renderable. Self-hosted operators should set `APP_URL` to override.
9
+ */
10
+ export const DEFAULT_APP_URL = "https://app.agent-swarm.dev";
11
+
12
+ /**
13
+ * Resolve the effective app/dashboard URL from `APP_URL` (with trailing
14
+ * slashes stripped), falling back to {@link DEFAULT_APP_URL}.
15
+ */
16
+ export function getAppUrl(): string {
17
+ const raw = process.env.APP_URL?.trim();
18
+ return (raw || DEFAULT_APP_URL).replace(/\/+$/, "");
19
+ }
@@ -1,5 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import type { ExecutorMeta } from "../../types";
3
+ import { getAppUrl } from "../../utils/constants";
3
4
  import type { ExecutorResult } from "./base";
4
5
  import { BaseExecutor } from "./base";
5
6
 
@@ -173,7 +174,7 @@ export class HumanInTheLoopExecutor extends BaseExecutor<
173
174
  ): Promise<void> {
174
175
  if (!config.notifications?.length) return;
175
176
 
176
- const approvalUrl = `https://app.agent-swarm.dev/approval-requests/${requestId}`;
177
+ const approvalUrl = `${getAppUrl()}/approval-requests/${requestId}`;
177
178
  const updatedChannels = [...config.notifications] as Array<
178
179
  z.infer<typeof NotificationConfigSchema> & { messageTs?: string }
179
180
  >;