@desplega.ai/agent-swarm 1.102.0 → 1.103.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 (107) hide show
  1. package/dist/{actions-j9399ktm.js → actions-7txktrdj.js} +6 -6
  2. package/dist/{app-tnq1pz27.js → app-xm3xsdye.js} +3 -3
  3. package/dist/{assistant-c7y92gkw.js → assistant-qpv63wk6.js} +9 -9
  4. package/dist/{boot-reembed-k7e6fc1p.js → boot-reembed-9cxqka3e.js} +3 -3
  5. package/dist/{boot-reembed-b97r699t.js → boot-reembed-chxhrwv8.js} +4 -4
  6. package/dist/{boot-scrub-logs-14kbc61y.js → boot-scrub-logs-8qtfkcz8.js} +2 -2
  7. package/dist/{claude-managed-adapter-g07dbz3b.js → claude-managed-adapter-z29hjgzz.js} +3 -3
  8. package/dist/{cli-tsz1q7gk.js → cli-2307phk1.js} +375 -23
  9. package/dist/{cli-g5px3d7x.js → cli-2957pqpj.js} +24 -1
  10. package/dist/{cli-9qascwj7.js → cli-2qynerth.js} +1 -1
  11. package/dist/{cli-k1ghyj8b.js → cli-4fnp2tc3.js} +1 -1
  12. package/dist/{cli-hc4rd6sv.js → cli-85pf4z38.js} +1 -1
  13. package/dist/{cli-zk4gn40w.js → cli-8hyv6c2v.js} +6 -14
  14. package/dist/{cli-k81y1cjb.js → cli-8w95v19d.js} +1 -1
  15. package/dist/{cli-7xdq3jg3.js → cli-92fqyjcv.js} +1 -1
  16. package/dist/{cli-zctkpdmd.js → cli-dz3d6zjg.js} +1 -1
  17. package/dist/{cli-d0t4854c.js → cli-fedn86nz.js} +213 -476
  18. package/dist/{cli-g861fwjp.js → cli-fwxdt7kt.js} +3 -3
  19. package/dist/{cli-2aaa9mqr.js → cli-fygaw191.js} +2 -2
  20. package/dist/{cli-qbc6mncp.js → cli-gzepjz54.js} +2 -2
  21. package/dist/{cli-r68m8arr.js → cli-jt9d9fjw.js} +1 -1
  22. package/dist/{cli-axtg1zaa.js → cli-khe4x17a.js} +1 -1
  23. package/dist/{cli-sygq3fhn.js → cli-kk51f3tf.js} +5 -5
  24. package/dist/{cli-dgab5x2h.js → cli-krvxq8bc.js} +1 -1
  25. package/dist/{cli-xde8vhhp.js → cli-ksq38x4n.js} +2 -2
  26. package/dist/{cli-stqqf5kh.js → cli-m8f6qzck.js} +1 -1
  27. package/dist/{cli-ap4md0gc.js → cli-ns0r7mkr.js} +30 -5
  28. package/dist/{cli-t2jk33ta.js → cli-rttgde5f.js} +18 -6
  29. package/dist/{cli-mhj07xbq.js → cli-rvae030h.js} +3 -3
  30. package/dist/{cli-q6jzg6wj.js → cli-s14sb64w.js} +4 -4
  31. package/dist/{cli-zmf7refz.js → cli-v9283nma.js} +1 -1
  32. package/dist/{cli-fjx4x7nn.js → cli-xmtsan9k.js} +1 -1
  33. package/dist/{cli-vgck9mqq.js → cli-yeemwevj.js} +155 -37
  34. package/dist/{cli-5bqa41fd.js → cli-yfwwjeft.js} +1 -1
  35. package/dist/{cli-s8jw5bmg.js → cli-yp2krx5r.js} +2 -2
  36. package/dist/cli.js +9 -9
  37. package/dist/{codex-adapter-wf119mnw.js → codex-adapter-w0xharsg.js} +3 -3
  38. package/dist/{codex-session-runner-1gknw7mh.js → codex-session-runner-kzbjcm2k.js} +3 -3
  39. package/dist/{commands-2wyn3swb.js → commands-aqn63mcw.js} +2 -2
  40. package/dist/{db-567ntpxv.js → db-azrvwsnj.js} +2 -2
  41. package/dist/{handlers-wvwpk2kj.js → handlers-hf61har3.js} +9 -9
  42. package/dist/{hook-fatkywc0.js → hook-306p3yz4.js} +2 -2
  43. package/dist/{http-eq6wxve6.js → http-zts4haq7.js} +81 -62
  44. package/dist/{index-400x6p3j.js → index-29vg51x6.js} +6 -6
  45. package/dist/{index-9wqh5352.js → index-3c651yfk.js} +10 -10
  46. package/dist/{index-6aj8966p.js → index-bfh9hgek.js} +5 -5
  47. package/dist/{index-d1rm4ea3.js → index-h2yqjyk0.js} +8 -8
  48. package/dist/{keepalive-6x9nx92g.js → keepalive-a0dg5pds.js} +4 -4
  49. package/dist/{lead-5fgqwe13.js → lead-7mcxedkc.js} +21 -21
  50. package/dist/{maintenance-eajx5k1k.js → maintenance-mrzqd53t.js} +4 -4
  51. package/dist/{onboard-jc1w1edg.js → onboard-4dkpv9wa.js} +2 -2
  52. package/dist/{otel-impl-8k375amf.js → otel-impl-jt7gpyp9.js} +1 -1
  53. package/dist/{pi-mono-adapter-hf8sw7hs.js → pi-mono-adapter-zf7dt1wg.js} +1 -1
  54. package/dist/{pricing-refresh-zed6916t.js → pricing-refresh-xqnzh8dt.js} +4 -4
  55. package/dist/{seed-pricing-5805qzkz.js → seed-pricing-56tjm1fj.js} +3 -3
  56. package/dist/{setup-wz05vxh0.js → setup-tbwk8j10.js} +2 -2
  57. package/dist/{worker-j359graf.js → worker-e03za92t.js} +21 -21
  58. package/openapi.json +70 -1
  59. package/package.json +1 -1
  60. package/src/be/db.ts +83 -13
  61. package/src/be/migrations/098_repo_hooks_config.sql +3 -0
  62. package/src/be/task-lifecycle-events.ts +42 -0
  63. package/src/commands/runner.ts +174 -25
  64. package/src/github/task-reactions.ts +21 -0
  65. package/src/hooks/tool-loop-detection.test.ts +40 -0
  66. package/src/hooks/tool-loop-detection.ts +37 -1
  67. package/src/http/agents.ts +7 -0
  68. package/src/http/index.ts +16 -2
  69. package/src/http/prompt-templates.ts +1 -1
  70. package/src/http/repos.ts +5 -1
  71. package/src/http/schedules.ts +1 -1
  72. package/src/http/session-data.ts +2 -0
  73. package/src/http/tasks.ts +2 -1
  74. package/src/http/workflows.ts +1 -0
  75. package/src/prompts/resolver.ts +1 -1
  76. package/src/scheduler/scheduler.ts +25 -2
  77. package/src/server-runtime-counters.ts +12 -0
  78. package/src/server-user.ts +1 -2
  79. package/src/server.ts +5 -0
  80. package/src/telemetry.ts +18 -1
  81. package/src/tests/http-api-integration.test.ts +4 -0
  82. package/src/tests/model-control.test.ts +6 -6
  83. package/src/tests/swarm-repos.test.ts +25 -2
  84. package/src/tests/task-lifecycle-events.test.ts +53 -0
  85. package/src/tests/task-lifecycle-telemetry.test.ts +37 -1
  86. package/src/tests/telemetry-init.test.ts +32 -0
  87. package/src/tools/prompt-templates/preview.ts +1 -1
  88. package/src/tools/repos/update-repo.ts +6 -1
  89. package/src/tools/schedules/create-schedule.ts +1 -1
  90. package/src/tools/schedules/update-schedule.ts +1 -1
  91. package/src/tools/send-task.ts +7 -2
  92. package/src/tools/task-action.ts +6 -2
  93. package/src/types.ts +155 -1
  94. package/src/utils/template.ts +127 -0
  95. package/src/workflows/engine.ts +7 -0
  96. package/src/workflows/executors/agent-task.ts +1 -2
  97. package/src/workflows/template.ts +12 -121
  98. package/src/workflows/triggers.ts +6 -2
  99. package/templates/community/code-health-reports/PLAYBOOK.md +355 -0
  100. package/templates/community/code-health-reports/README.md +38 -0
  101. package/templates/community/code-health-reports/lead-prompt.md +52 -0
  102. package/templates/community/code-health-reports/report.mjs +378 -0
  103. package/templates/community/code-health-reports/run.sh +143 -0
  104. package/templates/schedules/weekly-code-health-reports/config.json +13 -0
  105. package/templates/schedules/weekly-code-health-reports/content.md +60 -0
  106. package/src/model-tiers.ts +0 -140
  107. package/templates/community/.gitkeep +0 -0
package/src/http/tasks.ts CHANGED
@@ -23,7 +23,6 @@ import {
23
23
  updateTaskProgress,
24
24
  updateTaskVcs,
25
25
  } from "../be/db";
26
- import { ModelTierSchema, splitLegacyModelAlias } from "../model-tiers";
27
26
  import { createTaskWithSiblingAwareness } from "../tasks/sibling-awareness";
28
27
  import { createResumeFollowUp, createWorkerTaskFollowUp } from "../tasks/worker-follow-up";
29
28
  import {
@@ -32,8 +31,10 @@ import {
32
31
  type AgentTaskStatus,
33
32
  AgentTaskStatusSchema,
34
33
  isTerminalTaskStatus,
34
+ ModelTierSchema,
35
35
  ProviderNameSchema,
36
36
  ResumeReasonSchema,
37
+ splitLegacyModelAlias,
37
38
  } from "../types";
38
39
  import { getRequestAuth } from "../utils/request-auth-context";
39
40
  import { route } from "./route-def";
@@ -663,6 +663,7 @@ export async function handleWorkflows(
663
663
  let runId: string;
664
664
  try {
665
665
  runId = await startWorkflowExecution(workflow, body, getExecutorRegistry(), {
666
+ triggerType: "api",
666
667
  requestedByUserId: auth?.kind === "user" ? auth.userId : undefined,
667
668
  });
668
669
  } catch (err) {
@@ -8,7 +8,7 @@
8
8
  * workers which have no local database (architecture invariant: workers communicate via HTTP only).
9
9
  */
10
10
 
11
- import { interpolate } from "../workflows/template";
11
+ import { interpolate } from "../utils/template";
12
12
  import { getTemplateDefinition } from "./registry";
13
13
 
14
14
  // ─── DB Resolver Injection ──────────────────────────────────────────────────
@@ -3,6 +3,7 @@ import { CronExpressionParser } from "cron-parser";
3
3
  import { getDb, getDueScheduledTasks, getScheduledTaskById, updateScheduledTask } from "@/be/db";
4
4
  import { scheduleContextKey } from "@/tasks/context-key";
5
5
  import { createTaskWithSiblingAwareness } from "@/tasks/sibling-awareness";
6
+ import { telemetry } from "@/telemetry";
6
7
  import type { AgentTask, ScheduledTask } from "@/types";
7
8
  import type { ExecutorRegistry } from "@/workflows/executors/registry";
8
9
  import { handleScheduleTrigger } from "@/workflows/triggers";
@@ -48,9 +49,9 @@ async function recoverMissedSchedules(): Promise<void> {
48
49
  `(was due ${Math.round(missedBy / 1000)}s ago)`,
49
50
  );
50
51
 
52
+ let triggeredWorkflows = false;
51
53
  try {
52
54
  // Check if any workflows are linked to this schedule
53
- let triggeredWorkflows = false;
54
55
  if (executorRegistry) {
55
56
  const runIds = await handleScheduleTrigger(schedule.id, schedule, executorRegistry);
56
57
  if (runIds.length > 0) {
@@ -88,7 +89,18 @@ async function recoverMissedSchedules(): Promise<void> {
88
89
  if (schedule.scheduleType === "one_time") {
89
90
  console.log(`[Scheduler] One-time schedule "${schedule.name}" recovered and auto-disabled`);
90
91
  }
92
+ telemetry.schedule("executed", {
93
+ scheduleType: schedule.scheduleType,
94
+ triggeredWorkflows,
95
+ wasRecovered: true,
96
+ });
91
97
  } catch (err) {
98
+ telemetry.schedule("error", {
99
+ scheduleType: schedule.scheduleType,
100
+ triggeredWorkflows,
101
+ wasRecovered: true,
102
+ consecutiveErrors: schedule.consecutiveErrors ?? 0,
103
+ });
92
104
  console.error(`[Scheduler] Error recovering "${schedule.name}":`, err);
93
105
  }
94
106
  }
@@ -142,9 +154,9 @@ function getBackoffMs(consecutiveErrors: number): number {
142
154
  * Tracks consecutive errors and applies exponential backoff on failure.
143
155
  */
144
156
  async function executeSchedule(schedule: ScheduledTask): Promise<void> {
157
+ let triggeredWorkflows = false;
145
158
  try {
146
159
  // Check if any workflows are linked to this schedule
147
- let triggeredWorkflows = false;
148
160
  if (executorRegistry) {
149
161
  const runIds = await handleScheduleTrigger(schedule.id, schedule, executorRegistry);
150
162
  if (runIds.length > 0) {
@@ -187,6 +199,11 @@ async function executeSchedule(schedule: ScheduledTask): Promise<void> {
187
199
  });
188
200
  console.log(`[Scheduler] Executed schedule "${schedule.name}", next run: ${nextRun}`);
189
201
  }
202
+ telemetry.schedule("executed", {
203
+ scheduleType: schedule.scheduleType,
204
+ triggeredWorkflows,
205
+ wasRecovered: false,
206
+ });
190
207
  } catch (err) {
191
208
  const errorCount = (schedule.consecutiveErrors ?? 0) + 1;
192
209
  const now = new Date();
@@ -228,6 +245,12 @@ async function executeSchedule(schedule: ScheduledTask): Promise<void> {
228
245
  }
229
246
 
230
247
  updateScheduledTask(schedule.id, updates);
248
+ telemetry.schedule("error", {
249
+ scheduleType: schedule.scheduleType,
250
+ triggeredWorkflows,
251
+ wasRecovered: false,
252
+ consecutiveErrors: errorCount,
253
+ });
231
254
  }
232
255
  }
233
256
 
@@ -0,0 +1,12 @@
1
+ const runtimeCounters = globalThis as typeof globalThis & {
2
+ __agentSwarmServerSessionsProcessed?: number;
3
+ };
4
+
5
+ export function incrementServerSessionsProcessed(): void {
6
+ runtimeCounters.__agentSwarmServerSessionsProcessed =
7
+ (runtimeCounters.__agentSwarmServerSessionsProcessed ?? 0) + 1;
8
+ }
9
+
10
+ export function getServerSessionsProcessed(): number {
11
+ return runtimeCounters.__agentSwarmServerSessionsProcessed ?? 0;
12
+ }
@@ -1,7 +1,6 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import * as z from "zod";
3
3
  import pkg from "../package.json";
4
- import { ModelTierSchema } from "./model-tiers";
5
4
  import {
6
5
  cancelTaskHandler,
7
6
  cancelTaskInputSchema,
@@ -21,7 +20,7 @@ import {
21
20
  } from "./tools/task-action";
22
21
  import { userCtx } from "./tools/task-tool-ctx";
23
22
  import { createToolRegistrar } from "./tools/utils";
24
- import type { User } from "./types";
23
+ import { ModelTierSchema, type User } from "./types";
25
24
 
26
25
  const userSendTaskInputSchema = z.object({
27
26
  task: z.string().min(1).describe("The task description to send."),
package/src/server.ts CHANGED
@@ -3,6 +3,7 @@ import pkg from "../package.json";
3
3
  import { initDb } from "./be/db";
4
4
  import { startPricingRefreshLoop } from "./be/pricing-refresh";
5
5
  import { seedPricingFromModelsDev } from "./be/seed-pricing";
6
+ import { registerGithubTaskReactions } from "./github/task-reactions";
6
7
  import { registerCancelTaskTool } from "./tools/cancel-task";
7
8
  import { registerContextDiffTool } from "./tools/context-diff";
8
9
  import { registerContextHistoryTool } from "./tools/context-history";
@@ -175,6 +176,10 @@ export function createServer() {
175
176
  seedPricingFromModelsDev();
176
177
  startPricingRefreshLoop();
177
178
 
179
+ // Subscribe API-side integrations to task-lifecycle events. Idempotent.
180
+ // (Inverts the old be/db → github/task-reactions import; see cycle-break #4.)
181
+ registerGithubTaskReactions();
182
+
178
183
  const server = new McpServer(
179
184
  {
180
185
  name: pkg.name,
package/src/telemetry.ts CHANGED
@@ -7,6 +7,7 @@
7
7
  * - Importable from both API server and workers
8
8
  */
9
9
  import { randomUUID } from "node:crypto";
10
+ import pkg from "../package.json";
10
11
 
11
12
  const TELEMETRY_ENDPOINT = "https://proxy.desplega.sh/v1/events";
12
13
  const PRODUCT = "agent-swarm";
@@ -195,6 +196,7 @@ export function track(options: TrackOptions): void {
195
196
  // anonymous, and the boolean is sufficient to split cloud vs self-host.
196
197
  is_cloud: cachedIsCloud,
197
198
  is_e2b: cachedIsE2b,
199
+ swarmVersion: pkg.version,
198
200
  },
199
201
  metadata: {
200
202
  transport: "https",
@@ -238,7 +240,6 @@ export const telemetry = {
238
240
  props: {
239
241
  taskId: string;
240
242
  source?: string;
241
- tags?: string[];
242
243
  durationMs?: number;
243
244
  hasParent?: boolean;
244
245
  agentId?: string;
@@ -256,4 +257,20 @@ export const telemetry = {
256
257
  session(event: string, props: { agentId: string; taskId?: string; [k: string]: unknown }): void {
257
258
  track({ event: `session.${event}`, properties: props });
258
259
  },
260
+
261
+ schedule(event: string, props: Record<string, unknown>): void {
262
+ track({ event: `schedule.${event}`, properties: props });
263
+ },
264
+
265
+ workflow(event: string, props: Record<string, unknown>): void {
266
+ track({ event: `workflow.${event}`, properties: props });
267
+ },
268
+
269
+ agent(event: string, props: Record<string, unknown>): void {
270
+ track({ event: `agent.${event}`, properties: props });
271
+ },
272
+
273
+ compaction(event: string, props: Record<string, unknown>): void {
274
+ track({ event: `compaction.${event}`, properties: props });
275
+ },
259
276
  };
@@ -1187,6 +1187,10 @@ describe("Repos", () => {
1187
1187
  expect(status).toBe(201);
1188
1188
  expect(body.id).toBeDefined();
1189
1189
  expect(body.name).toBe("test-repo");
1190
+ expect(body.clonePath).toBe("/workspace/personal/repos/test-repo");
1191
+ expect(body.defaultBranch).toBe("main");
1192
+ expect(body.autoClone).toBe(true);
1193
+ expect(body.hooks).toEqual({ enabled: true });
1190
1194
  ids.repo = body.id;
1191
1195
  });
1192
1196
 
@@ -12,17 +12,17 @@ import {
12
12
  updateScheduledTask,
13
13
  upsertSwarmConfig,
14
14
  } from "../be/db";
15
- import {
16
- parseModelTier,
17
- resolveModelTier,
18
- resolveTaskModelSelection,
19
- splitLegacyModelAlias,
20
- } from "../model-tiers";
21
15
  import { runScheduleNow } from "../scheduler";
22
16
  import { createScheduleInputSchema } from "../tools/schedules/create-schedule";
23
17
  import { updateScheduleInputSchema } from "../tools/schedules/update-schedule";
24
18
  import { sendTaskInputSchema } from "../tools/send-task";
25
19
  import { taskActionInputSchema } from "../tools/task-action";
20
+ import {
21
+ parseModelTier,
22
+ resolveModelTier,
23
+ resolveTaskModelSelection,
24
+ splitLegacyModelAlias,
25
+ } from "../types";
26
26
 
27
27
  const TEST_DB_PATH = "./test-model-control.sqlite";
28
28
 
@@ -47,9 +47,10 @@ describe("Swarm Repos", () => {
47
47
  expect(repo.id).toBeDefined();
48
48
  expect(repo.url).toBe("https://github.com/desplega-ai/agent-swarm");
49
49
  expect(repo.name).toBe("agent-swarm");
50
- expect(repo.clonePath).toBe("/workspace/repos/agent-swarm");
50
+ expect(repo.clonePath).toBe("/workspace/personal/repos/agent-swarm");
51
51
  expect(repo.defaultBranch).toBe("main");
52
52
  expect(repo.autoClone).toBe(true);
53
+ expect(repo.hooks).toEqual({ enabled: true });
53
54
  expect(repo.createdAt).toBeDefined();
54
55
  expect(repo.lastUpdatedAt).toBeDefined();
55
56
  });
@@ -61,11 +62,23 @@ describe("Swarm Repos", () => {
61
62
  clonePath: "/workspace/custom/other",
62
63
  defaultBranch: "develop",
63
64
  autoClone: false,
65
+ hooks: { enabled: true },
64
66
  });
65
67
 
66
68
  expect(repo.clonePath).toBe("/workspace/custom/other");
67
69
  expect(repo.defaultBranch).toBe("develop");
68
70
  expect(repo.autoClone).toBe(false);
71
+ expect(repo.hooks).toEqual({ enabled: true });
72
+ });
73
+
74
+ test("should create a repo with explicitly disabled hooks", () => {
75
+ const repo = createSwarmRepo({
76
+ url: "https://github.com/desplega-ai/disabled-hooks-repo",
77
+ name: "disabled-hooks-repo",
78
+ hooks: { enabled: false },
79
+ });
80
+
81
+ expect(repo.hooks).toEqual({ enabled: false });
69
82
  });
70
83
 
71
84
  test("should list repos", () => {
@@ -122,14 +135,24 @@ describe("Swarm Repos", () => {
122
135
  const updated = updateSwarmRepo(repo!.id, {
123
136
  defaultBranch: "develop",
124
137
  autoClone: false,
138
+ hooks: { enabled: true },
125
139
  });
126
140
 
127
141
  expect(updated).not.toBeNull();
128
142
  expect(updated?.defaultBranch).toBe("develop");
129
143
  expect(updated?.autoClone).toBe(false);
144
+ expect(updated?.hooks).toEqual({ enabled: true });
130
145
  expect(updated?.lastUpdatedAt).not.toBe(repo?.lastUpdatedAt);
131
146
  });
132
147
 
148
+ test("should disable hooks by clearing hooks config", () => {
149
+ const repo = getSwarmRepoByName("agent-swarm");
150
+ expect(repo?.hooks).toEqual({ enabled: true });
151
+
152
+ const updated = updateSwarmRepo(repo!.id, { hooks: null });
153
+ expect(updated?.hooks).toEqual({ enabled: false });
154
+ });
155
+
133
156
  test("should update repo name and clonePath", () => {
134
157
  const repo = createSwarmRepo({
135
158
  url: "https://github.com/desplega-ai/temp-repo",
@@ -265,7 +288,7 @@ describe("Swarm Repos", () => {
265
288
  createSwarmRepo({
266
289
  url: "https://github.com/desplega-ai/unique-url-2",
267
290
  name: "unique-name",
268
- clonePath: "/workspace/repos/agent-swarm",
291
+ clonePath: "/workspace/personal/repos/agent-swarm",
269
292
  }),
270
293
  ).toThrow();
271
294
  });
@@ -0,0 +1,53 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { emitTaskStarted, onTaskStarted } from "../be/task-lifecycle-events";
3
+ import type { AgentTask } from "../types";
4
+
5
+ function makeTask(overrides: Partial<AgentTask> = {}): AgentTask {
6
+ return {
7
+ id: "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
8
+ agentId: "11111111-2222-3333-4444-555555555555",
9
+ task: "Test task",
10
+ status: "in_progress",
11
+ source: "github",
12
+ priority: 50,
13
+ tags: [],
14
+ dependsOn: [],
15
+ wasPaused: false,
16
+ createdAt: new Date().toISOString(),
17
+ lastUpdatedAt: new Date().toISOString(),
18
+ ...overrides,
19
+ } as AgentTask;
20
+ }
21
+
22
+ describe("task-lifecycle-events", () => {
23
+ test("emitTaskStarted invokes a registered onTaskStarted handler with the task", () => {
24
+ const received: AgentTask[] = [];
25
+ onTaskStarted((task) => {
26
+ received.push(task);
27
+ });
28
+
29
+ const task = makeTask();
30
+ emitTaskStarted(task);
31
+
32
+ expect(received).toContain(task);
33
+ });
34
+
35
+ test("a throwing handler does not break emit and later handlers still run", () => {
36
+ let laterRan = false;
37
+ onTaskStarted(() => {
38
+ throw new Error("boom");
39
+ });
40
+ onTaskStarted(() => {
41
+ laterRan = true;
42
+ });
43
+
44
+ expect(() => emitTaskStarted(makeTask())).not.toThrow();
45
+ expect(laterRan).toBe(true);
46
+ });
47
+
48
+ test("a promise-rejecting handler is fire-and-forget and does not throw", () => {
49
+ onTaskStarted(() => Promise.reject(new Error("async boom")));
50
+
51
+ expect(() => emitTaskStarted(makeTask())).not.toThrow();
52
+ });
53
+ });
@@ -9,6 +9,7 @@ import {
9
9
  failTask,
10
10
  getDb,
11
11
  initDb,
12
+ updateTaskClaudeSessionId,
12
13
  } from "../be/db";
13
14
  import { telemetry } from "../telemetry";
14
15
 
@@ -69,7 +70,6 @@ describe("task lifecycle telemetry", () => {
69
70
  props: {
70
71
  taskId: task.id,
71
72
  source: "mcp",
72
- tags: ["telemetry"],
73
73
  hasParent: false,
74
74
  priority: 60,
75
75
  },
@@ -150,4 +150,40 @@ describe("task lifecycle telemetry", () => {
150
150
  });
151
151
  expect(typeof calls[0]?.props.durationMs).toBe("number");
152
152
  });
153
+
154
+ test("emits structured task provider and harness context instead of tags", async () => {
155
+ const task = createTaskExtended("complete telemetry with harness context", {
156
+ agentId: WORKER_ID,
157
+ source: "mcp",
158
+ tags: ["telemetry"],
159
+ });
160
+ updateTaskClaudeSessionId(
161
+ task.id,
162
+ "provider-session-1",
163
+ "codex",
164
+ undefined,
165
+ "gpt-5.5",
166
+ "stock",
167
+ { version: "0.40.1" },
168
+ );
169
+ await flushMicrotasks();
170
+ calls = [];
171
+
172
+ completeTask(task.id, "done");
173
+ await flushMicrotasks();
174
+
175
+ expect(calls).toHaveLength(1);
176
+ expect(calls[0]).toMatchObject({
177
+ event: "completed",
178
+ props: {
179
+ taskId: task.id,
180
+ source: "mcp",
181
+ agentId: WORKER_ID,
182
+ provider: "codex",
183
+ harnessVariant: "stock",
184
+ harnessVersion: "0.40.1",
185
+ },
186
+ });
187
+ expect(calls[0]?.props.tags).toBeUndefined();
188
+ });
153
189
  });
@@ -1,4 +1,5 @@
1
1
  import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
+ import pkg from "../../package.json";
2
3
  import {
3
4
  _getInstallationIdForTests,
4
5
  _isE2bSandbox,
@@ -478,6 +479,37 @@ describe("initTelemetry", () => {
478
479
  });
479
480
  });
480
481
 
482
+ describe("track() ships swarmVersion in properties", () => {
483
+ const originalFetch = globalThis.fetch;
484
+ let captured: Record<string, unknown> | null = null;
485
+
486
+ beforeEach(() => {
487
+ captured = null;
488
+ globalThis.fetch = (async (_url: string, init?: { body?: string }) => {
489
+ captured = init?.body ? JSON.parse(init.body) : null;
490
+ return new Response(null, { status: 204 });
491
+ }) as typeof fetch;
492
+ });
493
+
494
+ afterEach(() => {
495
+ globalThis.fetch = originalFetch;
496
+ });
497
+
498
+ test("includes the package version on every event and ignores caller overrides", async () => {
499
+ await initTelemetry(
500
+ "worker",
501
+ async () => "install_version_test",
502
+ async () => {},
503
+ );
504
+
505
+ track({ event: "test.event", properties: { swarmVersion: "spoofed" } });
506
+ await new Promise((r) => setTimeout(r, 0));
507
+
508
+ const properties = (captured as { properties: Record<string, unknown> }).properties;
509
+ expect(properties.swarmVersion).toBe(pkg.version);
510
+ });
511
+ });
512
+
481
513
  describe("track() metadata.environment", () => {
482
514
  const originalFetch = globalThis.fetch;
483
515
  const originalNodeEnv = process.env.NODE_ENV;
@@ -2,7 +2,7 @@ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import * as z from "zod";
3
3
  import { getTemplateDefinition } from "@/prompts/registry";
4
4
  import { createToolRegistrar } from "@/tools/utils";
5
- import { interpolate } from "@/workflows/template";
5
+ import { interpolate } from "@/utils/template";
6
6
 
7
7
  export const registerPreviewPromptTemplateTool = (server: McpServer) => {
8
8
  createToolRegistrar(server)(
@@ -2,7 +2,7 @@ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import * as z from "zod";
3
3
  import { updateSwarmRepo } from "@/be/db";
4
4
  import { createToolRegistrar } from "@/tools/utils";
5
- import { RepoGuidelinesSchema, SwarmRepoSchema } from "@/types";
5
+ import { RepoGuidelinesSchema, RepoHooksSchema, SwarmRepoSchema } from "@/types";
6
6
 
7
7
  export const registerUpdateRepoTool = (server: McpServer) => {
8
8
  createToolRegistrar(server)(
@@ -20,6 +20,11 @@ export const registerUpdateRepoTool = (server: McpServer) => {
20
20
  clonePath: z.string().optional().describe("New clone path."),
21
21
  defaultBranch: z.string().optional().describe("New default branch."),
22
22
  autoClone: z.boolean().optional().describe("Whether to auto-clone."),
23
+ hooks: RepoHooksSchema.nullable()
24
+ .optional()
25
+ .describe(
26
+ "Repository hook install config. Set { enabled: true } to opt into best-effort worker hook installation, or null to disable.",
27
+ ),
23
28
  guidelines: RepoGuidelinesSchema.nullable()
24
29
  .optional()
25
30
  .describe(
@@ -5,7 +5,7 @@ import { resolveTaskAuditUserId } from "@/be/audit-user";
5
5
  import { createScheduledTask, getAgentById, getScheduledTaskByName } from "@/be/db";
6
6
  import { calculateNextRun } from "@/scheduler";
7
7
  import { createToolRegistrar } from "@/tools/utils";
8
- import { ModelTierSchema, splitLegacyModelAlias } from "../../model-tiers";
8
+ import { ModelTierSchema, splitLegacyModelAlias } from "../../types";
9
9
 
10
10
  export const createScheduleInputSchema = z.object({
11
11
  name: z.string().min(1).max(100).describe("Unique name for the schedule (e.g., 'daily-cleanup')"),
@@ -11,7 +11,7 @@ import {
11
11
  import { mergeScheduleTiming, validateRecurringTiming } from "@/be/schedules/validate";
12
12
  import { calculateNextRun } from "@/scheduler";
13
13
  import { createToolRegistrar } from "@/tools/utils";
14
- import { ModelTierSchema, splitLegacyModelAlias } from "../../model-tiers";
14
+ import { ModelTierSchema, splitLegacyModelAlias } from "../../types";
15
15
 
16
16
  export const updateScheduleInputSchema = z.object({
17
17
  scheduleId: z.string().uuid().optional().describe("Schedule ID to update"),
@@ -15,8 +15,13 @@ import { repointTrackerSyncBySwarmId } from "@/be/db-queries/tracker";
15
15
  import { findDuplicateTask } from "@/tools/task-dedup";
16
16
  import { ownerCtx, type ToolCtx } from "@/tools/task-tool-ctx";
17
17
  import { createToolRegistrar } from "@/tools/utils";
18
- import { type AgentTask, AgentTaskSchema, FollowUpConfigSchema } from "@/types";
19
- import { ModelTierSchema, splitLegacyModelAlias } from "../model-tiers";
18
+ import {
19
+ type AgentTask,
20
+ AgentTaskSchema,
21
+ FollowUpConfigSchema,
22
+ ModelTierSchema,
23
+ splitLegacyModelAlias,
24
+ } from "@/types";
20
25
 
21
26
  export const sendTaskInputSchema = z.object({
22
27
  agentId: z
@@ -27,8 +27,12 @@ import {
27
27
  } from "@/be/db";
28
28
  import { assertOwnsTask, ownerCtx, type ToolCtx } from "@/tools/task-tool-ctx";
29
29
  import { createToolRegistrar } from "@/tools/utils";
30
- import { AgentTaskSchema, BudgetRefusalCauseSchema } from "@/types";
31
- import { ModelTierSchema, splitLegacyModelAlias } from "../model-tiers";
30
+ import {
31
+ AgentTaskSchema,
32
+ BudgetRefusalCauseSchema,
33
+ ModelTierSchema,
34
+ splitLegacyModelAlias,
35
+ } from "@/types";
32
36
 
33
37
  export const TaskActionSchema = z.enum([
34
38
  "create",