@desplega.ai/agent-swarm 1.52.0 → 1.52.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/src/http/poll.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { IncomingMessage, ServerResponse } from "node:http";
2
+ import { ensure } from "@desplega.ai/business-use";
2
3
  import { z } from "zod";
3
4
  import {
4
5
  claimMentions,
@@ -128,6 +129,20 @@ export async function handlePoll(
128
129
  if (pendingTask) {
129
130
  // Mark task as in_progress immediately to prevent duplicate polling
130
131
  startTask(pendingTask.id);
132
+
133
+ ensure({
134
+ id: "started",
135
+ flow: "task",
136
+ runId: pendingTask.id,
137
+ depIds: ["created"],
138
+ data: {
139
+ taskId: pendingTask.id,
140
+ agentId: myAgentId,
141
+ previousStatus: pendingTask.status,
142
+ },
143
+ validator: (data) => data.previousStatus === "pending",
144
+ });
145
+
131
146
  return {
132
147
  trigger: {
133
148
  type: "task_assigned",
package/src/http/tasks.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { IncomingMessage, ServerResponse } from "node:http";
2
+ import { ensure } from "@desplega.ai/business-use";
2
3
  import { z } from "zod";
3
4
  import {
4
5
  cancelTask,
@@ -232,6 +233,24 @@ export async function handleTasks(
232
233
  source: (parsed.body.source as import("../types").AgentTaskSource) || "api",
233
234
  outputSchema: parsed.body.outputSchema || undefined,
234
235
  });
236
+
237
+ ensure({
238
+ id: "created",
239
+ flow: "task",
240
+ runId: task.id,
241
+ data: {
242
+ taskId: task.id,
243
+ agentId: task.agentId,
244
+ source: parsed.body.source || "api",
245
+ status: task.status,
246
+ task: task.task.slice(0, 200),
247
+ priority: task.priority,
248
+ tags: task.tags,
249
+ parentTaskId: task.parentTaskId,
250
+ epicId: task.epicId,
251
+ },
252
+ });
253
+
235
254
  json(res, task, 201);
236
255
  } catch (error) {
237
256
  console.error("[HTTP] Failed to create task:", error);
@@ -291,6 +310,36 @@ export async function handleTasks(
291
310
  return true;
292
311
  }
293
312
 
313
+ if (task.status === "pending") {
314
+ ensure({
315
+ id: "cancelled_pending",
316
+ flow: "task",
317
+ runId: parsed.params.id,
318
+ depIds: ["created"],
319
+ data: {
320
+ taskId: parsed.params.id,
321
+ agentId: task.agentId,
322
+ previousStatus: task.status,
323
+ reason,
324
+ },
325
+ validator: (data) => data.previousStatus === "pending",
326
+ });
327
+ } else {
328
+ ensure({
329
+ id: "cancelled_in_progress",
330
+ flow: "task",
331
+ runId: parsed.params.id,
332
+ depIds: ["started"],
333
+ data: {
334
+ taskId: parsed.params.id,
335
+ agentId: task.agentId,
336
+ previousStatus: task.status,
337
+ reason,
338
+ },
339
+ validator: (data) => data.previousStatus === "in_progress",
340
+ });
341
+ }
342
+
294
343
  if (task.agentId) {
295
344
  updateAgentStatusFromCapacity(task.agentId);
296
345
  }
@@ -386,6 +435,25 @@ export async function handleTasks(
386
435
  return true;
387
436
  }
388
437
 
438
+ if (result.task && !("alreadyFinished" in result && result.alreadyFinished)) {
439
+ const finishEventId = parsed.body.status === "completed" ? "completed" : "failed";
440
+ ensure({
441
+ id: finishEventId,
442
+ flow: "task",
443
+ runId: parsed.params.id,
444
+ depIds: ["started"],
445
+ data: {
446
+ taskId: parsed.params.id,
447
+ agentId: myAgentId,
448
+ previousStatus: "in_progress",
449
+ ...(finishEventId === "completed"
450
+ ? { hasOutput: !!parsed.body.output }
451
+ : { failureReason: parsed.body.failureReason }),
452
+ },
453
+ validator: (data) => data.previousStatus === "in_progress",
454
+ });
455
+ }
456
+
389
457
  json(res, {
390
458
  success: true,
391
459
  alreadyFinished: "alreadyFinished" in result ? result.alreadyFinished : false,
@@ -430,6 +498,19 @@ export async function handleTasks(
430
498
  return true;
431
499
  }
432
500
 
501
+ ensure({
502
+ id: "paused",
503
+ flow: "task",
504
+ runId: parsed.params.id,
505
+ depIds: ["started"],
506
+ data: {
507
+ taskId: parsed.params.id,
508
+ agentId: task.agentId,
509
+ previousStatus: task.status,
510
+ },
511
+ validator: (data) => data.previousStatus === "in_progress",
512
+ });
513
+
433
514
  json(res, { success: true, task: pausedTask });
434
515
  return true;
435
516
  }
@@ -460,6 +541,19 @@ export async function handleTasks(
460
541
  return true;
461
542
  }
462
543
 
544
+ ensure({
545
+ id: "resumed",
546
+ flow: "task",
547
+ runId: parsed.params.id,
548
+ depIds: ["paused"],
549
+ data: {
550
+ taskId: parsed.params.id,
551
+ agentId: task.agentId,
552
+ previousStatus: task.status,
553
+ },
554
+ validator: (data) => data.previousStatus === "paused",
555
+ });
556
+
463
557
  json(res, { success: true, task: resumedTask });
464
558
  return true;
465
559
  }
@@ -1,12 +1,7 @@
1
1
  import { getTrackerSync, updateTrackerSync } from "../be/db-queries/tracker";
2
2
  import { workflowEventBus } from "../workflows/event-bus";
3
3
  import { getLinearClient } from "./client";
4
- import {
5
- endAgentSession,
6
- postAgentSessionAction,
7
- postAgentSessionThought,
8
- taskSessionMap,
9
- } from "./sync";
4
+ import { endAgentSession, postAgentSessionAction, taskSessionMap } from "./sync";
10
5
 
11
6
  let subscribed = false;
12
7
 
@@ -60,8 +55,9 @@ async function handleTaskProgress(data: unknown): Promise<void> {
60
55
  const sessionId = taskSessionMap.get(taskId);
61
56
  if (!sessionId) return;
62
57
 
63
- postAgentSessionThought(sessionId, progress).catch((err) => {
64
- console.error(`[Linear Outbound] Failed to post progress thought for task ${taskId}:`, err);
58
+ // Use 'action' activity type — Linear renders it as a structured tool invocation card
59
+ postAgentSessionAction(sessionId, progress).catch((err) => {
60
+ console.error(`[Linear Outbound] Failed to post progress action for task ${taskId}:`, err);
65
61
  });
66
62
  }
67
63
 
@@ -75,7 +71,9 @@ async function handleTaskCompleted(data: unknown): Promise<void> {
75
71
  if (shouldSkipForLoopPrevention(sync)) return;
76
72
 
77
73
  const sessionId = taskSessionMap.get(taskId);
78
- const body = output ? `Task completed.\n\n${output.slice(0, 2000)}` : "Task completed.";
74
+ const body = output
75
+ ? `Task completed.\n\n+++ Output\n${output.slice(0, 2000)}\n+++`
76
+ : "Task completed.";
79
77
 
80
78
  // Prefer AgentSession activity (shows in the agent panel) over issue comment (avoids duplication)
81
79
  if (sessionId) {
@@ -93,7 +91,7 @@ async function handleTaskCompleted(data: unknown): Promise<void> {
93
91
  return;
94
92
  }
95
93
  const comment = output
96
- ? `Task completed by swarm agent.\n\nOutput:\n${output.slice(0, 2000)}`
94
+ ? `Task completed by swarm agent.\n\n+++ Output\n${output.slice(0, 2000)}\n+++`
97
95
  : "Task completed by swarm agent.";
98
96
  await client.createComment({ issueId: sync.externalId, body: comment });
99
97
  console.log(`[Linear Outbound] Posted completion comment for task ${taskId}`);
@@ -121,7 +119,9 @@ async function handleTaskFailed(data: unknown): Promise<void> {
121
119
  if (shouldSkipForLoopPrevention(sync)) return;
122
120
 
123
121
  const sessionId = taskSessionMap.get(taskId);
124
- const body = failureReason ? `Task failed.\n\n${failureReason.slice(0, 2000)}` : "Task failed.";
122
+ const body = failureReason
123
+ ? `Task failed.\n\n+++ Error Details\n${failureReason.slice(0, 2000)}\n+++`
124
+ : "Task failed.";
125
125
 
126
126
  // Prefer AgentSession error activity over issue comment (avoids duplication)
127
127
  if (sessionId) {
@@ -139,7 +139,7 @@ async function handleTaskFailed(data: unknown): Promise<void> {
139
139
  return;
140
140
  }
141
141
  const comment = failureReason
142
- ? `Task failed.\n\nReason:\n${failureReason.slice(0, 2000)}`
142
+ ? `Task failed.\n\n+++ Error Details\n${failureReason.slice(0, 2000)}\n+++`
143
143
  : "Task failed.";
144
144
  await client.createComment({ issueId: sync.externalId, body: comment });
145
145
  console.log(`[Linear Outbound] Posted failure comment for task ${taskId}`);
@@ -1,5 +1,5 @@
1
1
  import { unlink, writeFile } from "node:fs/promises";
2
- import { join } from "node:path";
2
+ import { dirname, join } from "node:path";
3
3
  import { validateClaudeCredentials } from "../utils/credentials";
4
4
  import {
5
5
  parseStderrForErrors,
@@ -48,10 +48,26 @@ async function cleanupTaskFile(pid: number): Promise<void> {
48
48
  * Returns the path to the per-session config, or null if no config exists.
49
49
  */
50
50
  async function createSessionMcpConfig(cwd: string, taskId: string): Promise<string | null> {
51
- const mcpJsonPath = join(cwd, ".mcp.json");
51
+ // Walk up from cwd to find .mcp.json (mirrors Claude CLI's project-level config discovery).
52
+ // In Docker, .mcp.json lives at /workspace/.mcp.json but tasks often run with cwd set to
53
+ // a subdirectory like /workspace/repos/<repo>, so a single-directory check misses it.
54
+ let searchDir = cwd;
55
+ let mcpJsonPath: string | null = null;
56
+ while (true) {
57
+ const candidate = join(searchDir, ".mcp.json");
58
+ if (await Bun.file(candidate).exists()) {
59
+ mcpJsonPath = candidate;
60
+ break;
61
+ }
62
+ const parent = dirname(searchDir);
63
+ if (parent === searchDir) break; // reached filesystem root
64
+ searchDir = parent;
65
+ }
66
+
67
+ if (!mcpJsonPath) return null;
68
+
52
69
  try {
53
70
  const file = Bun.file(mcpJsonPath);
54
- if (!(await file.exists())) return null;
55
71
 
56
72
  const config = await file.json();
57
73
  const servers = config?.mcpServers;
@@ -1,3 +1,4 @@
1
+ import { ensure } from "@desplega.ai/business-use";
1
2
  import { CronExpressionParser } from "cron-parser";
2
3
  import {
3
4
  createTaskExtended,
@@ -239,7 +240,11 @@ async function executeSchedule(schedule: ScheduledTask): Promise<void> {
239
240
  * @param registry ExecutorRegistry for triggering workflows linked to schedules
240
241
  * @param intervalMs Polling interval in milliseconds (default: 10000)
241
242
  */
242
- export function startScheduler(registry: ExecutorRegistry, intervalMs = 10000): void {
243
+ export function startScheduler(
244
+ registry: ExecutorRegistry,
245
+ intervalMs = 10000,
246
+ opts?: { runId?: string },
247
+ ): void {
243
248
  if (schedulerInterval) {
244
249
  console.log("[Scheduler] Already running");
245
250
  return;
@@ -254,6 +259,24 @@ export function startScheduler(registry: ExecutorRegistry, intervalMs = 10000):
254
259
  schedulerInterval = setInterval(async () => {
255
260
  await processSchedules();
256
261
  }, intervalMs);
262
+
263
+ ensure({
264
+ id: "scheduler_started",
265
+ flow: "api",
266
+ runId: opts?.runId ?? "",
267
+ depIds: ["listen"],
268
+ data: {},
269
+ // biome-ignore lint/correctness/noEmptyPattern: data unused, ctx needed
270
+ filter: ({}, ctx) => {
271
+ const start = ctx.deps.find((d) => d.id === "listen");
272
+ return !!start && start.data?.capabilities?.includes("scheduling");
273
+ },
274
+ // biome-ignore lint/correctness/noEmptyPattern: data unused, ctx needed
275
+ validator: ({}, ctx) => {
276
+ const start = ctx.deps.find((d) => d.id === "listen");
277
+ return !!start && start.data?.capabilities?.includes("scheduling");
278
+ },
279
+ });
257
280
  }
258
281
 
259
282
  /**
@@ -179,7 +179,7 @@ export function buildProgressBlocks(opts: {
179
179
  }): SlackBlock[] {
180
180
  const shortId = opts.taskId.slice(0, 8);
181
181
  return [
182
- sectionBlock(`⏳ *${opts.agentName}* (\`${shortId}\`): ${opts.progress}`),
182
+ sectionBlock(`*${opts.agentName}* (\`${shortId}\`): ${opts.progress}`),
183
183
  cancelActionBlock(opts.taskId),
184
184
  ];
185
185
  }
@@ -3,13 +3,18 @@ import { unlink } from "node:fs/promises";
3
3
  import { createServer as createHttpServer, type Server } from "node:http";
4
4
  import {
5
5
  closeDb,
6
+ createAgent,
6
7
  createApprovalRequest,
8
+ createTaskExtended,
9
+ getAgentCurrentTask,
7
10
  getApprovalRequestById,
8
11
  getApprovalRequestByStepId,
9
12
  getExpiredPendingApprovals,
10
13
  initDb,
11
14
  listApprovalRequests,
12
15
  resolveApprovalRequest,
16
+ startTask,
17
+ updateApprovalRequestNotifications,
13
18
  } from "../be/db";
14
19
  import type { ExecutorMeta } from "../types";
15
20
  import type { ExecutorDependencies, ExecutorInput } from "../workflows/executors/base";
@@ -543,7 +548,7 @@ describe("Approval Requests", () => {
543
548
  dryRun: false,
544
549
  };
545
550
 
546
- function executorInput(
551
+ function _executorInput(
547
552
  config: Record<string, unknown>,
548
553
  context: Record<string, unknown> = {},
549
554
  ): ExecutorInput {
@@ -572,8 +577,11 @@ describe("Approval Requests", () => {
572
577
  });
573
578
 
574
579
  expect(result.status).toBe("success");
580
+ // biome-ignore lint/suspicious/noExplicitAny: test assertion on untyped executor result
575
581
  expect((result as any).async).toBe(true);
582
+ // biome-ignore lint/suspicious/noExplicitAny: test assertion on untyped executor result
576
583
  expect((result as any).waitFor).toBe("approval.resolved");
584
+ // biome-ignore lint/suspicious/noExplicitAny: test assertion on untyped executor result
577
585
  expect((result as any).correlationId).toBeTruthy();
578
586
 
579
587
  // Verify the request was created in DB
@@ -608,7 +616,9 @@ describe("Approval Requests", () => {
608
616
  });
609
617
 
610
618
  expect(result.status).toBe("success");
619
+ // biome-ignore lint/suspicious/noExplicitAny: test assertion on untyped executor result
611
620
  expect((result as any).async).toBe(true);
621
+ // biome-ignore lint/suspicious/noExplicitAny: test assertion on untyped executor result
612
622
  expect((result as any).correlationId).toBe(existingId);
613
623
  });
614
624
 
@@ -640,6 +650,7 @@ describe("Approval Requests", () => {
640
650
  });
641
651
 
642
652
  expect(result.status).toBe("success");
653
+ // biome-ignore lint/suspicious/noExplicitAny: test assertion on untyped executor result
643
654
  expect((result as any).async).toBeUndefined();
644
655
  expect(result.output).toBeDefined();
645
656
  expect(result.output!.requestId).toBe(existingId);
@@ -732,4 +743,206 @@ describe("Approval Requests", () => {
732
743
  expect(result.status).toBe("failed");
733
744
  });
734
745
  });
746
+
747
+ // ─── Follow-up task flow ─────────────────────────────────────
748
+ describe("Follow-up task: Slack metadata inheritance", () => {
749
+ test("sourceTaskId is stored and returned on resolved approval request", () => {
750
+ // Create a source task with Slack metadata
751
+ const agent = createAgent({
752
+ name: "test-follow-up-agent",
753
+ isLead: false,
754
+ status: "idle",
755
+ });
756
+ const sourceTask = createTaskExtended("original task with slack context", {
757
+ agentId: agent.id,
758
+ source: "mcp",
759
+ slackChannelId: "C_TEST_CHANNEL",
760
+ slackThreadTs: "1234567890.123456",
761
+ slackUserId: "U_TEST_USER",
762
+ });
763
+
764
+ // Create approval request linked to source task
765
+ const approvalData = makeApprovalData({ sourceTaskId: sourceTask.id });
766
+ const approval = createApprovalRequest(approvalData);
767
+ expect(approval.sourceTaskId).toBe(sourceTask.id);
768
+
769
+ // Resolve it
770
+ const resolved = resolveApprovalRequest(approval.id, {
771
+ status: "approved",
772
+ responses: { q1: { approved: true } },
773
+ });
774
+ expect(resolved).not.toBeNull();
775
+ expect(resolved!.sourceTaskId).toBe(sourceTask.id);
776
+ });
777
+
778
+ test("follow-up task inherits Slack metadata from source task via parentTaskId", () => {
779
+ const agent = createAgent({
780
+ name: "test-slack-inherit-agent",
781
+ isLead: false,
782
+ status: "idle",
783
+ });
784
+ const sourceTask = createTaskExtended("source task", {
785
+ agentId: agent.id,
786
+ source: "mcp",
787
+ slackChannelId: "C_FOLLOW_UP",
788
+ slackThreadTs: "9999999999.000000",
789
+ slackUserId: "U_FOLLOW_UP",
790
+ });
791
+
792
+ // Simulate what the respond handler does: create follow-up with parentTaskId
793
+ const followUp = createTaskExtended("follow-up task text", {
794
+ agentId: sourceTask.agentId ?? undefined,
795
+ parentTaskId: sourceTask.id,
796
+ source: "system",
797
+ taskType: "hitl-follow-up",
798
+ tags: ["hitl", "follow-up"],
799
+ // Explicit Slack metadata (as the handler now does)
800
+ slackChannelId: sourceTask.slackChannelId ?? undefined,
801
+ slackThreadTs: sourceTask.slackThreadTs ?? undefined,
802
+ slackUserId: sourceTask.slackUserId ?? undefined,
803
+ });
804
+
805
+ expect(followUp.slackChannelId).toBe("C_FOLLOW_UP");
806
+ expect(followUp.slackThreadTs).toBe("9999999999.000000");
807
+ expect(followUp.slackUserId).toBe("U_FOLLOW_UP");
808
+ expect(followUp.parentTaskId).toBe(sourceTask.id);
809
+ expect(followUp.taskType).toBe("hitl-follow-up");
810
+ });
811
+
812
+ test("follow-up task inherits Slack metadata even without explicit pass (auto-inheritance)", () => {
813
+ const agent = createAgent({
814
+ name: "test-auto-inherit-agent",
815
+ isLead: false,
816
+ status: "idle",
817
+ });
818
+ const sourceTask = createTaskExtended("source task auto", {
819
+ agentId: agent.id,
820
+ source: "mcp",
821
+ slackChannelId: "C_AUTO",
822
+ slackThreadTs: "1111111111.000000",
823
+ slackUserId: "U_AUTO",
824
+ });
825
+
826
+ // Without explicit Slack metadata — relies on auto-inheritance from parentTaskId
827
+ const followUp = createTaskExtended("auto-inherit follow-up", {
828
+ agentId: sourceTask.agentId ?? undefined,
829
+ parentTaskId: sourceTask.id,
830
+ source: "system",
831
+ taskType: "hitl-follow-up",
832
+ });
833
+
834
+ expect(followUp.slackChannelId).toBe("C_AUTO");
835
+ expect(followUp.slackThreadTs).toBe("1111111111.000000");
836
+ expect(followUp.slackUserId).toBe("U_AUTO");
837
+ });
838
+
839
+ test("no follow-up for workflow-linked requests (workflowRunId set)", () => {
840
+ const approvalData = makeApprovalData({
841
+ sourceTaskId: crypto.randomUUID(),
842
+ workflowRunId: crypto.randomUUID(),
843
+ workflowRunStepId: crypto.randomUUID(),
844
+ });
845
+ const approval = createApprovalRequest(approvalData);
846
+
847
+ // The condition in the handler is: !updated.workflowRunId && updated.sourceTaskId
848
+ // With workflowRunId set, this should be false
849
+ expect(approval.workflowRunId).toBeTruthy();
850
+ expect(approval.sourceTaskId).toBeTruthy();
851
+ // The handler would NOT create a follow-up task here
852
+ expect(!approval.workflowRunId && approval.sourceTaskId).toBe(false);
853
+ });
854
+
855
+ test("no follow-up when sourceTaskId is missing", () => {
856
+ const approvalData = makeApprovalData(); // no sourceTaskId
857
+ const approval = createApprovalRequest(approvalData);
858
+
859
+ expect(approval.sourceTaskId).toBeNull();
860
+ // The handler condition would be false
861
+ expect(!approval.workflowRunId && approval.sourceTaskId).toBeFalsy();
862
+ });
863
+ });
864
+
865
+ // ─── Server-side sourceTaskId fallback ───────────────────────
866
+ describe("getAgentCurrentTask fallback for sourceTaskId", () => {
867
+ test("returns the most recent in-progress task for an agent", () => {
868
+ const agent = createAgent({
869
+ name: "test-current-task-agent",
870
+ isLead: true,
871
+ status: "idle",
872
+ });
873
+
874
+ // Create a task and set it to in_progress
875
+ const task = createTaskExtended("lead agent task", {
876
+ agentId: agent.id,
877
+ source: "mcp",
878
+ });
879
+ startTask(task.id);
880
+
881
+ const currentTask = getAgentCurrentTask(agent.id);
882
+ expect(currentTask).not.toBeNull();
883
+ expect(currentTask!.id).toBe(task.id);
884
+ });
885
+
886
+ test("returns null when agent has no in-progress tasks", () => {
887
+ const agent = createAgent({
888
+ name: "test-no-task-agent",
889
+ isLead: true,
890
+ status: "idle",
891
+ });
892
+
893
+ const currentTask = getAgentCurrentTask(agent.id);
894
+ expect(currentTask).toBeNull();
895
+ });
896
+
897
+ test("fallback sourceTaskId resolves correctly for approval request", () => {
898
+ const agent = createAgent({
899
+ name: "test-fallback-agent",
900
+ isLead: true,
901
+ status: "idle",
902
+ });
903
+ const task = createTaskExtended("lead task calling request-human-input", {
904
+ agentId: agent.id,
905
+ source: "mcp",
906
+ slackChannelId: "C_LEAD_CHANNEL",
907
+ slackThreadTs: "1111111111.000000",
908
+ slackUserId: "U_LEAD_USER",
909
+ });
910
+ startTask(task.id);
911
+
912
+ // Simulate what the fixed request-human-input tool does:
913
+ // sourceTaskId from header is missing, so fall back to agent's current task
914
+ const headerSourceTaskId: string | undefined = undefined;
915
+ let sourceTaskId = headerSourceTaskId;
916
+ if (!sourceTaskId) {
917
+ const currentTask = getAgentCurrentTask(agent.id);
918
+ if (currentTask) {
919
+ sourceTaskId = currentTask.id;
920
+ }
921
+ }
922
+
923
+ const approval = createApprovalRequest(makeApprovalData({ sourceTaskId }));
924
+ expect(approval.sourceTaskId).toBe(task.id);
925
+ });
926
+ });
927
+
928
+ describe("updateApprovalRequestNotifications", () => {
929
+ test("stores messageTs back in notification channels", () => {
930
+ const channels = [
931
+ { channel: "slack", target: "C12345" },
932
+ { channel: "email", target: "user@example.com" },
933
+ ];
934
+ const approval = createApprovalRequest(makeApprovalData({ notificationChannels: channels }));
935
+ expect(approval.notificationChannels).toEqual(channels);
936
+
937
+ const updatedChannels = [
938
+ { channel: "slack", target: "C12345", messageTs: "1234567890.123456" },
939
+ { channel: "email", target: "user@example.com" },
940
+ ];
941
+ updateApprovalRequestNotifications(approval.id, updatedChannels);
942
+
943
+ const fetched = getApprovalRequestById(approval.id);
944
+ expect(fetched).not.toBeNull();
945
+ expect(fetched!.notificationChannels).toEqual(updatedChannels);
946
+ });
947
+ });
735
948
  });
@@ -97,7 +97,7 @@ describe("syncSkillsToFilesystem", () => {
97
97
  });
98
98
 
99
99
  test("skips complex skills", () => {
100
- const result = syncSkillsToFilesystem(agentId, "claude", FAKE_HOME);
100
+ const _result = syncSkillsToFilesystem(agentId, "claude", FAKE_HOME);
101
101
 
102
102
  const complexDir = join(FAKE_HOME, ".claude", "skills", "complex-skill");
103
103
  expect(existsSync(complexDir)).toBe(false);
@@ -145,9 +145,10 @@ describe("buildProgressBlocks", () => {
145
145
  });
146
146
 
147
147
  expect(blocks.length).toBe(2);
148
- // Single line: *Gamma* (`aabbccdd`): Analyzing codebase...
148
+ // Single line: *Gamma* (`aabbccdd`): Analyzing codebase...
149
+ // (no ⏳ prefix — progress strings now carry their own emoji)
149
150
  expect(blocks[0].type).toBe("section");
150
- expect(blocks[0].text.text).toContain("⏳");
151
+ expect(blocks[0].text.text).not.toContain("⏳");
151
152
  expect(blocks[0].text.text).toContain("Gamma");
152
153
  expect(blocks[0].text.text).toContain("aabbccdd");
153
154
  expect(blocks[0].text.text).toContain("Analyzing codebase...");
@@ -233,6 +233,7 @@ describe("AgentTaskConfigSchema — outputSchema", () => {
233
233
  test("accepts outputSchema in config", async () => {
234
234
  const { AgentTaskExecutor } = await import("../workflows/executors/agent-task");
235
235
  const executor = new AgentTaskExecutor({
236
+ // biome-ignore lint/suspicious/noExplicitAny: mock DB for test
236
237
  db: {} as any,
237
238
  eventBus: { emit: () => {}, on: () => {}, off: () => {} },
238
239
  interpolate: (t: string) => t,