@desplega.ai/agent-swarm 1.147.0 → 1.149.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 (185) hide show
  1. package/README.md +13 -1
  2. package/dist/{acp-adapter-9n319wqc.js → acp-adapter-4jncb126.js} +12 -5
  3. package/dist/{actions-2vxqvpr9.js → actions-31vp3ae7.js} +10 -9
  4. package/dist/{app-w066xfy0.js → app-1hh67x8w.js} +6 -6
  5. package/dist/{assistant-kt31dj2h.js → assistant-z66dmz6p.js} +13 -11
  6. package/dist/{boot-reembed-az5rassp.js → boot-reembed-fqzxxzjh.js} +7 -7
  7. package/dist/{boot-reembed-j3mm3rfz.js → boot-reembed-wxa8ya0s.js} +8 -8
  8. package/dist/{boot-scrub-logs-b6h54817.js → boot-scrub-logs-jnyndh79.js} +5 -5
  9. package/dist/{claude-adapter-w1z04ann.js → claude-adapter-3qpa9w0j.js} +9 -9
  10. package/dist/{claude-managed-adapter-sactwn31.js → claude-managed-adapter-qvsmh6mv.js} +2 -2
  11. package/dist/{claude-sdk-session-f0cc1kec.js → claude-sdk-session-2yxgjs5d.js} +9 -9
  12. package/dist/{cli-y6b6fb76.js → cli-08b07b4r.js} +1 -1
  13. package/dist/{cli-8fc8de14.js → cli-0z2v4nhw.js} +1 -1
  14. package/dist/{cli-de14znh8.js → cli-19f354qr.js} +2 -2
  15. package/dist/{cli-hbv0kq6w.js → cli-2qts1hys.js} +3 -3
  16. package/dist/{cli-kpv03zhj.js → cli-3apbzgyk.js} +27 -15
  17. package/dist/{cli-1th1728g.js → cli-3q5ejx9p.js} +2 -2
  18. package/dist/{cli-d8brzxjb.js → cli-4wtq5jjv.js} +2 -2
  19. package/dist/{cli-ct0et58h.js → cli-5my3bjsd.js} +1 -1
  20. package/dist/{cli-cn6mmc7f.js → cli-6664w7y4.js} +1 -1
  21. package/dist/{cli-yz3djrm0.js → cli-6pqcb4q0.js} +53 -13
  22. package/dist/{cli-719k9j2c.js → cli-7smrsr25.js} +3 -3
  23. package/dist/{cli-rw51bq3j.js → cli-9fy6nk6g.js} +1 -1
  24. package/dist/{cli-zhjmqxzh.js → cli-9hspd3dp.js} +2 -2
  25. package/dist/{cli-3tgwnf43.js → cli-aae6hz6a.js} +4 -4
  26. package/dist/{cli-89xfd7f9.js → cli-cr12paw2.js} +5 -5
  27. package/dist/{cli-b5bq9crk.js → cli-d8ftsp62.js} +8 -8
  28. package/dist/{cli-4nr988h6.js → cli-e95cx1eb.js} +3 -3
  29. package/dist/{cli-n8508nre.js → cli-gjhjfeeg.js} +1 -1
  30. package/dist/{cli-p1d5b073.js → cli-gv88e1vf.js} +1 -1
  31. package/dist/{cli-rvqz34y5.js → cli-jecc01cb.js} +1 -1
  32. package/dist/{cli-8v0g1yc8.js → cli-k0cr4kat.js} +1 -1
  33. package/dist/{cli-mmemxxdc.js → cli-kgz4np97.js} +4 -4
  34. package/dist/{cli-w9kb3bk1.js → cli-kjwt6hdf.js} +3 -1
  35. package/dist/{cli-f7dsy61y.js → cli-mnmsfd1w.js} +6 -2
  36. package/dist/{cli-54wwve05.js → cli-n3dz2942.js} +37 -26
  37. package/dist/{cli-12fz972k.js → cli-nye12xk5.js} +8 -5
  38. package/dist/{cli-tzvk9haz.js → cli-phsxnkp6.js} +20 -20
  39. package/dist/{cli-53s590z8.js → cli-q50ef8g0.js} +1 -1
  40. package/dist/{cli-f146mn5s.js → cli-qaacms9y.js} +7 -6
  41. package/dist/{cli-xqcq9y5e.js → cli-r2ap2czm.js} +1 -0
  42. package/dist/{cli-m8yrsg97.js → cli-sd5pv3b1.js} +3 -3
  43. package/dist/{cli-0b6y7kxm.js → cli-xm87hzbw.js} +1 -1
  44. package/dist/{cli-ftht3zzj.js → cli-yab68w40.js} +285 -11
  45. package/dist/{cli-atcve9y8.js → cli-yb9qhqam.js} +227 -59
  46. package/dist/{cli-smby5m17.js → cli-ygweqcn4.js} +133 -22
  47. package/dist/{cli-dttph1bw.js → cli-ykxrnd1q.js} +2 -2
  48. package/dist/{cli-m9sxbkhm.js → cli-zvz62chv.js} +3 -3
  49. package/dist/cli.js +16 -14
  50. package/dist/{codex-adapter-zw4dwsx7.js → codex-adapter-8meawydj.js} +4 -4
  51. package/dist/{codex-hook-bw8p5nh4.js → codex-hook-hfrt9shr.js} +2 -2
  52. package/dist/{codex-session-runner-k981s65v.js → codex-session-runner-gjgxyp5j.js} +4 -4
  53. package/dist/{commands-sykrxx7e.js → commands-hz3nb95f.js} +5 -5
  54. package/dist/{db-sfbsc8wt.js → db-ggz97zbm.js} +5 -5
  55. package/dist/{e2b-6hb103d8.js → e2b-p04vmtx0.js} +1 -1
  56. package/dist/{handlers-0aspcejm.js → handlers-nbzdzn9k.js} +14 -11
  57. package/dist/{hook-hxccs7m5.js → hook-bks2q1f6.js} +7 -7
  58. package/dist/{hook-4nvqj2sb.js → hook-kq9wp0ep.js} +8 -8
  59. package/dist/{http-y06z517d.js → http-hrf77v87.js} +96 -45
  60. package/dist/{index-tce1rss4.js → index-6nsbg88z.js} +443 -144
  61. package/dist/{index-x5j894zf.js → index-8hmeg8j1.js} +14 -14
  62. package/dist/{index-k8znfx0z.js → index-cqtebbhb.js} +13 -13
  63. package/dist/{index-hssww7kj.js → index-n28pw1zt.js} +15 -15
  64. package/dist/{keepalive-w733ax66.js → keepalive-dxc12gvg.js} +7 -7
  65. package/dist/{lead-75bjqa9d.js → lead-yckfn18d.js} +32 -32
  66. package/dist/{maintenance-w4f1zjck.js → maintenance-6kfb2m5e.js} +8 -8
  67. package/dist/{oauth-refresh-sweep-wdetky26.js → oauth-refresh-sweep-1x1ac722.js} +6 -6
  68. package/dist/{onboard-bb5net8s.js → onboard-dcyt1609.js} +4 -3
  69. package/dist/{opencode-adapter-fef1mrn2.js → opencode-adapter-dce4wskd.js} +2 -2
  70. package/dist/{otel-impl-0w7c14ap.js → otel-impl-tbk3e5dr.js} +2 -2
  71. package/dist/{pi-mono-adapter-2has4gnp.js → pi-mono-adapter-zvshk5cj.js} +2 -2
  72. package/dist/{pricing-refresh-9nzsrgcq.js → pricing-refresh-kaqg567e.js} +7 -7
  73. package/dist/{rbac-roles-z1jrffbt.js → rbac-roles-ammq6rd2.js} +5 -5
  74. package/dist/{rbac-roles-r3hqjp09.js → rbac-roles-cf97v1vd.js} +6 -6
  75. package/dist/{render-v2-vbr89fnb.js → render-v2-80w603vy.js} +6 -6
  76. package/dist/{seed-pricing-g62hy1pk.js → seed-pricing-zbrn83xv.js} +6 -6
  77. package/dist/{setup-gnfqnptp.js → setup-fe66kczs.js} +2 -2
  78. package/dist/{worker-kznxp4qm.js → worker-qmb2d1e2.js} +32 -32
  79. package/dist/{x-n2phr5vm.js → x-n3e0v3xa.js} +2 -2
  80. package/openapi.json +46 -4
  81. package/package.json +3 -1
  82. package/src/agentmail/handlers.ts +5 -0
  83. package/src/automation-preflight-alert.ts +41 -0
  84. package/src/be/automation-preflight.ts +3 -3
  85. package/src/be/budget-refusal-notify.ts +1 -0
  86. package/src/be/db/tasks/read.ts +5 -0
  87. package/src/be/db.ts +132 -2
  88. package/src/be/migrations/150_deferred_task_waits.sql +18 -0
  89. package/src/be/migrations/151_repair_scheduled_task_required_params.sql +39 -0
  90. package/src/be/migrations/152_routing_decisions.sql +44 -0
  91. package/src/be/scripts/typecheck.ts +33 -22
  92. package/src/be/seed-scripts/catalog/delegate.ts +5 -1
  93. package/src/be/seed-skills/bundled-files.generated.json +10 -0
  94. package/src/be/seed-skills/index.ts +3 -0
  95. package/src/be/steering.ts +1 -0
  96. package/src/be/swarm-config-guard.ts +11 -0
  97. package/src/commands/onboard/steps/post-task.tsx +1 -0
  98. package/src/github/handlers.ts +9 -0
  99. package/src/gitlab/handlers.ts +4 -0
  100. package/src/heartbeat/heartbeat.ts +8 -0
  101. package/src/http/approval-requests.ts +1 -0
  102. package/src/http/apps.ts +1 -0
  103. package/src/http/config.ts +15 -6
  104. package/src/http/mcp.ts +31 -4
  105. package/src/http/script-runs.ts +6 -0
  106. package/src/http/tasks.ts +52 -30
  107. package/src/integrations/kapso/inbound.ts +1 -0
  108. package/src/jira/sync.ts +2 -0
  109. package/src/linear/sync.ts +2 -0
  110. package/src/prompts/session-templates.ts +29 -15
  111. package/src/scheduler/deferred-task-waits.ts +123 -0
  112. package/src/scheduler/schedule-task.ts +42 -0
  113. package/src/scheduler/scheduler.ts +22 -31
  114. package/src/script-workflows/workflow-ctx.ts +2 -0
  115. package/src/scripts-runtime/types/stdlib.d.ts +38 -24
  116. package/src/scripts-runtime/types/swarm-sdk.d.ts +38 -24
  117. package/src/server.ts +10 -1
  118. package/src/slack/actions.ts +1 -0
  119. package/src/slack/assistant.ts +2 -0
  120. package/src/slack/handlers.ts +3 -0
  121. package/src/slack/thread-buffer.ts +2 -0
  122. package/src/tasks/worker-follow-up.ts +9 -0
  123. package/src/tests/acp-adapter.test.ts +35 -6
  124. package/src/tests/acp-session-token.test.ts +67 -0
  125. package/src/tests/asset-key-api.test.ts +15 -2
  126. package/src/tests/asset-key-mcp.test.ts +2 -0
  127. package/src/tests/automation-preflight-alert.test.ts +91 -0
  128. package/src/tests/claude-worker-parity.test.ts +3 -0
  129. package/src/tests/config-session-auth.test.ts +143 -0
  130. package/src/tests/defer-task.test.ts +117 -6
  131. package/src/tests/deferred-task-wake.test.ts +456 -0
  132. package/src/tests/heartbeat-reroute-decision.test.ts +1 -0
  133. package/src/tests/http-api-integration.test.ts +33 -4
  134. package/src/tests/mcp-input-ergonomics.test.ts +386 -0
  135. package/src/tests/model-control.test.ts +18 -5
  136. package/src/tests/opencode-adapter.test.ts +2 -2
  137. package/src/tests/promote-draft-task-route.test.ts +4 -0
  138. package/src/tests/prompt-template-session.test.ts +13 -4
  139. package/src/tests/rbac-wire-e2e.test.ts +2 -2
  140. package/src/tests/routing-decision-persistence.test.ts +520 -0
  141. package/src/tests/routing-reason-contract.test.ts +89 -0
  142. package/src/tests/routing-reason-inventory.test.ts +55 -0
  143. package/src/tests/schedule-target-type.test.ts +48 -1
  144. package/src/tests/scheduled-task-required-params-migration.test.ts +182 -0
  145. package/src/tests/scheduled-tasks.test.ts +9 -5
  146. package/src/tests/script-connections.test.ts +4 -0
  147. package/src/tests/script-runs-http.test.ts +31 -0
  148. package/src/tests/scripts-typecheck.test.ts +11 -0
  149. package/src/tests/secret-scrubber.test.ts +12 -0
  150. package/src/tests/send-task-output-schema.test.ts +66 -1
  151. package/src/tests/send-task-requested-by.test.ts +1 -0
  152. package/src/tests/send-task-slack-routing-guard.test.ts +2 -0
  153. package/src/tests/store-progress-blocked-waiting-gate.test.ts +151 -0
  154. package/src/tests/swarm-tool-result-gate.test.ts +24 -0
  155. package/src/tests/task-tool-manifest.test.ts +47 -0
  156. package/src/tests/task-tool-preload-mcp.test.ts +125 -0
  157. package/src/tests/task-tools-ctx.test.ts +2 -0
  158. package/src/tests/tool-output-agent-id.test.ts +1 -0
  159. package/src/tests/ui-followup-lead-delegation.test.ts +6 -1
  160. package/src/tests/workflow-agent-task.test.ts +24 -1
  161. package/src/tests/workflow-engine-v2.test.ts +58 -2
  162. package/src/tools/accept-steer.ts +0 -1
  163. package/src/tools/defer-task.ts +73 -21
  164. package/src/tools/memory-get.ts +10 -3
  165. package/src/tools/memory-rate.ts +19 -7
  166. package/src/tools/memory-search.ts +2 -1
  167. package/src/tools/memory-store.ts +25 -5
  168. package/src/tools/send-task.ts +40 -2
  169. package/src/tools/skills/skill-publish.ts +1 -0
  170. package/src/tools/store-progress.ts +78 -4
  171. package/src/tools/templates.ts +2 -1
  172. package/src/tools/utils.ts +33 -2
  173. package/src/types.ts +14 -1
  174. package/src/utils/acp-session-token.ts +14 -4
  175. package/src/utils/secret-scrubber.ts +2 -0
  176. package/src/utils/task-tool-manifest.ts +33 -0
  177. package/src/workflows/engine.ts +4 -1
  178. package/src/workflows/executors/agent-task.ts +7 -1
  179. package/templates/ai-toolbox.manifest.json +6 -1
  180. package/templates/skills/comms/config.json +18 -0
  181. package/templates/skills/comms/content.md +74 -0
  182. package/templates/skills/comms/files/references/ste-rules.md +73 -0
  183. package/templates/skills/comms/files/references/visual-shapes.md +112 -0
  184. package/templates/skills/swarm-scripts/SKILL.md +3 -2
  185. package/templates/skills/swarm-scripts/content.md +3 -2
@@ -12,11 +12,22 @@
12
12
  * - HTTP route cross-field validation (create + update) for workflow/script targets.
13
13
  */
14
14
  import { Database } from "bun:sqlite";
15
- import { afterAll, beforeAll, describe, expect, test } from "bun:test";
15
+ import {
16
+ afterAll,
17
+ afterEach,
18
+ beforeAll,
19
+ beforeEach,
20
+ describe,
21
+ expect,
22
+ setSystemTime,
23
+ spyOn,
24
+ test,
25
+ } from "bun:test";
16
26
  import { unlink } from "node:fs/promises";
17
27
  import type { IncomingMessage, ServerResponse } from "node:http";
18
28
  import { Readable } from "node:stream";
19
29
  import { z } from "zod";
30
+ import * as preflightAlerts from "../automation-preflight-alert";
20
31
  import {
21
32
  type AutomationSetupStates,
22
33
  preflightAutomation,
@@ -88,6 +99,16 @@ class EchoExecutor extends BaseExecutor<typeof EchoExecutor.schema, typeof EchoE
88
99
 
89
100
  let savedEnv: NodeJS.ProcessEnv;
90
101
  let agentId: string;
102
+ let notify: ReturnType<typeof spyOn<typeof preflightAlerts, "notifyAutomationPreflightFailure">>;
103
+
104
+ beforeEach(() => {
105
+ notify = spyOn(preflightAlerts, "notifyAutomationPreflightFailure").mockResolvedValue(undefined);
106
+ });
107
+
108
+ afterEach(() => {
109
+ notify.mockRestore();
110
+ setSystemTime();
111
+ });
91
112
 
92
113
  async function removeDbFiles(): Promise<void> {
93
114
  for (const suffix of ["", "-wal", "-shm"]) {
@@ -617,6 +638,10 @@ describe("dispatchScheduleTarget — automation preflight", () => {
617
638
 
618
639
  await executeSchedule(schedule);
619
640
  const first = (await getScheduledTaskById(schedule.id))!;
641
+ expect(notify).toHaveBeenCalledTimes(1);
642
+ expect(notify).toHaveBeenCalledWith(
643
+ expect.objectContaining({ kind: "schedule", name: schedule.name, state: "needs_setup" }),
644
+ );
620
645
  await executeSchedule(first);
621
646
  const second = (await getScheduledTaskById(schedule.id))!;
622
647
  const after = await getDbClient().get<{ count: number }>(
@@ -624,6 +649,7 @@ describe("dispatchScheduleTarget — automation preflight", () => {
624
649
  );
625
650
 
626
651
  expect(after?.count).toBe(before?.count);
652
+ expect(notify).toHaveBeenCalledTimes(1);
627
653
  expect(second.lastErrorAt).toBe(first.lastErrorAt);
628
654
  expect(second.lastErrorMessage).toBe("needs_setup: params=[REPO_URL] integrations=[]");
629
655
  expect(second.consecutiveErrors).toBe(0);
@@ -632,6 +658,27 @@ describe("dispatchScheduleTarget — automation preflight", () => {
632
658
  expect(new Date(second.nextRunAt!).getTime()).toBeGreaterThan(Date.now());
633
659
  });
634
660
 
661
+ test("alerts once for concurrent schedule refusals and again after UTC midnight", async () => {
662
+ setSystemTime(new Date("2026-09-14T23:59:59Z"));
663
+ const schedule = await createScheduledTask({
664
+ name: `dispatch-daily-alert-${crypto.randomUUID()}`,
665
+ intervalMs: 60_000,
666
+ taskTemplate: "Review {{REPO_URL}}",
667
+ requiredParams: ["REPO_URL"],
668
+ });
669
+
670
+ const refusals = await Promise.allSettled([
671
+ dispatchScheduleTarget(schedule),
672
+ dispatchScheduleTarget(schedule),
673
+ ]);
674
+ expect(refusals.every((result) => result.status === "rejected")).toBe(true);
675
+ expect(notify).toHaveBeenCalledTimes(1);
676
+
677
+ setSystemTime(new Date("2026-09-15T00:00:00Z"));
678
+ await expect(dispatchScheduleTarget(schedule)).rejects.toThrow("needs_setup:");
679
+ expect(notify).toHaveBeenCalledTimes(2);
680
+ });
681
+
635
682
  test("advances a cron schedule while its required timezone is still unset", async () => {
636
683
  const schedule = await createScheduledTask({
637
684
  name: `dispatch-needs-timezone-${crypto.randomUUID()}`,
@@ -0,0 +1,182 @@
1
+ import { Database } from "bun:sqlite";
2
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
3
+ import { type AutomationSetupStates, preflightAutomation } from "../be/automation-preflight";
4
+ import { runMigrations } from "../be/migrations/runner";
5
+ import type { AutomationIntegrationId } from "../types";
6
+
7
+ const original = await Bun.file(
8
+ new URL("../be/migrations/141_scheduled_task_automation_preflight.sql", import.meta.url),
9
+ ).text();
10
+ const repair = await Bun.file(
11
+ new URL("../be/migrations/151_repair_scheduled_task_required_params.sql", import.meta.url),
12
+ ).text();
13
+
14
+ const canonicalNames = [
15
+ "daily-blocker-digest",
16
+ "daily-compounding-reflection",
17
+ "daily-status-report",
18
+ "daily-workflow-health-audit",
19
+ "weekly-harness-upgrade-check",
20
+ "weekly-dependabot-triage",
21
+ "weekly-code-health-reports",
22
+ "weekly-dora-metrics",
23
+ "daily-hn-briefing",
24
+ "gtm-weekly-review",
25
+ "dream-daily",
26
+ ];
27
+
28
+ interface ScheduleRow {
29
+ name: string;
30
+ taskTemplate: string | null;
31
+ timezone: string;
32
+ enabled: number;
33
+ params: string;
34
+ requiredParams: string;
35
+ requires: string;
36
+ }
37
+
38
+ const setup: AutomationSetupStates = {
39
+ slack: "verified",
40
+ github: "verified",
41
+ linear: "verified",
42
+ jira: "verified",
43
+ gsc: "verified",
44
+ agentmail: "verified",
45
+ agentfs: "verified",
46
+ };
47
+
48
+ function preflight(row: ScheduleRow) {
49
+ return preflightAutomation(
50
+ {
51
+ id: row.name,
52
+ name: row.name,
53
+ kind: "schedule",
54
+ params: JSON.parse(row.params),
55
+ requiredParams: JSON.parse(row.requiredParams),
56
+ requires: JSON.parse(row.requires) as AutomationIntegrationId[],
57
+ },
58
+ setup,
59
+ );
60
+ }
61
+
62
+ describe("scheduled task required params repair", () => {
63
+ let db: Database;
64
+
65
+ beforeEach(() => {
66
+ db = new Database(":memory:");
67
+ // Only the pre-141 columns consumed by the two data migrations.
68
+ db.exec(`CREATE TABLE scheduled_tasks (
69
+ name TEXT PRIMARY KEY, taskTemplate TEXT, timezone TEXT NOT NULL DEFAULT 'UTC',
70
+ enabled INTEGER NOT NULL DEFAULT 1
71
+ )`);
72
+ });
73
+
74
+ afterEach(() => db.close());
75
+
76
+ function insert(name: string, taskTemplate: string | null) {
77
+ db.run("INSERT INTO scheduled_tasks (name, taskTemplate) VALUES (?, ?)", [name, taskTemplate]);
78
+ }
79
+
80
+ function rows() {
81
+ return db.query<ScheduleRow, []>("SELECT * FROM scheduled_tasks ORDER BY name").all();
82
+ }
83
+
84
+ test("repairs concretized legacy schedules after 141 and is idempotent", () => {
85
+ for (const name of canonicalNames) {
86
+ insert(name, "Run the configured report for example/project and send it to ops@example.org.");
87
+ }
88
+ db.exec(original);
89
+ const before = rows();
90
+ expect(before.filter((row) => preflight(row).state === "needs_setup")).toHaveLength(6);
91
+ expect(before.find((row) => row.name === "weekly-dependabot-triage")?.timezone).toBe(
92
+ "{{TIMEZONE}}",
93
+ );
94
+
95
+ db.exec(repair);
96
+ expect(rows()).toEqual(
97
+ before.map((row) => ({ ...row, requiredParams: "[]", timezone: "UTC" })),
98
+ );
99
+ for (const row of rows()) expect(preflight(row).state).toBe("running");
100
+
101
+ const repaired = rows();
102
+ db.exec(repair);
103
+ expect(rows()).toEqual(repaired);
104
+ });
105
+
106
+ test("keeps every requirement on genuinely tokenized fresh templates", async () => {
107
+ for (const name of canonicalNames.filter((name) => name !== "dream-daily")) {
108
+ const template = new URL(`../../templates/schedules/${name}/content.md`, import.meta.url);
109
+ insert(name, await Bun.file(template).text());
110
+ }
111
+ db.exec(original);
112
+ const before = rows();
113
+
114
+ db.exec(repair);
115
+ expect(rows()).toEqual(before.map((row) => ({ ...row, timezone: "UTC" })));
116
+ expect(rows().map((row) => preflight(row).missing)).toEqual(
117
+ before.map((row) => preflight(row).missing),
118
+ );
119
+ });
120
+
121
+ test("keeps referenced keys in order and checks exact case-sensitive tokens", () => {
122
+ insert(
123
+ "weekly-code-health-reports",
124
+ "Report {{PAGE_ID}} for {{REPO_URL}}; {{BRANCH_SUFFIX}}, {{scope_path}}, REPORT_NAME",
125
+ );
126
+ db.exec(original);
127
+ db.exec(repair);
128
+ expect(rows()[0]?.requiredParams).toBe('["REPO_URL","PAGE_ID"]');
129
+ });
130
+
131
+ test("keeps timezone-only requirements when TIMEZONE has a binding", () => {
132
+ insert("weekly-dependabot-triage", null);
133
+ db.exec(original);
134
+ db.run("UPDATE scheduled_tasks SET params = ?", ['{"TIMEZONE":"Europe/Paris"}']);
135
+ db.exec(repair);
136
+ expect(rows()[0]).toMatchObject({
137
+ taskTemplate: null,
138
+ timezone: "{{TIMEZONE}}",
139
+ requiredParams: '["TIMEZONE"]',
140
+ params: '{"TIMEZONE":"Europe/Paris"}',
141
+ requires: '["github","slack"]',
142
+ });
143
+ expect(preflight(rows()[0]!).state).toBe("running");
144
+ });
145
+
146
+ test("restores an unbound timezone and removes unused requirements with a null body", () => {
147
+ insert("weekly-dependabot-triage", null);
148
+ db.exec(original);
149
+ db.run("UPDATE scheduled_tasks SET params = ?", ['{"TIMEZONE":null}']);
150
+ db.exec(repair);
151
+ expect(rows()[0]).toMatchObject({ timezone: "UTC", requiredParams: "[]" });
152
+ });
153
+
154
+ test("leaves manual repairs and schedules outside 141 unchanged", () => {
155
+ insert("weekly-dependabot-triage", "Review {{REPO_URL}}.");
156
+ insert("custom-schedule", null);
157
+ db.exec(original);
158
+ db.exec(`UPDATE scheduled_tasks SET
159
+ timezone = 'America/New_York', requiredParams = '[ "REPO_URL" ]',
160
+ params = '{"REPO_URL":"example/project"}', enabled = 0
161
+ WHERE name = 'weekly-dependabot-triage'`);
162
+ db.exec(`UPDATE scheduled_tasks SET
163
+ timezone = '{{TIMEZONE}}', requiredParams = '["TIMEZONE","CUSTOM"]'
164
+ WHERE name = 'custom-schedule'`);
165
+ const before = rows();
166
+ db.exec(repair);
167
+ expect(rows()).toEqual(before);
168
+ });
169
+ });
170
+
171
+ test("the migration runner applies the repair on a fresh database", () => {
172
+ const db = new Database(":memory:");
173
+ try {
174
+ runMigrations(db);
175
+ const applied = db.query("SELECT * FROM _migrations WHERE version = 151").get();
176
+ expect(applied).toMatchObject({ name: "151_repair_scheduled_task_required_params" });
177
+ runMigrations(db);
178
+ expect(db.query("SELECT * FROM _migrations WHERE version = 151").get()).toEqual(applied);
179
+ } finally {
180
+ db.close();
181
+ }
182
+ });
@@ -399,12 +399,13 @@ describe("Scheduled Tasks Integration", () => {
399
399
 
400
400
  // Find the created task
401
401
  const createdTask = (await getDbClient().get(
402
- "SELECT * FROM agent_tasks WHERE task = ? ORDER BY createdAt DESC LIMIT 1",
402
+ "SELECT *, routing_reason AS routingReason FROM agent_tasks WHERE task = ? ORDER BY createdAt DESC LIMIT 1",
403
403
  [schedule.taskTemplate!],
404
- )) as { agentId: string; status: string };
404
+ )) as { agentId: string; routingReason: string; status: string };
405
405
 
406
406
  expect(createdTask).toBeDefined();
407
407
  expect(createdTask.agentId).toBe(targetAgent.id);
408
+ expect(createdTask.routingReason).toBe("human_pinned");
408
409
  expect(createdTask.status).toBe("pending"); // Should be pending when assigned to agent
409
410
  });
410
411
 
@@ -420,12 +421,13 @@ describe("Scheduled Tasks Integration", () => {
420
421
 
421
422
  // Find the created task
422
423
  const createdTask = (await getDbClient().get(
423
- "SELECT * FROM agent_tasks WHERE task = ? ORDER BY createdAt DESC LIMIT 1",
424
+ "SELECT *, routing_reason AS routingReason FROM agent_tasks WHERE task = ? ORDER BY createdAt DESC LIMIT 1",
424
425
  [schedule.taskTemplate!],
425
- )) as { agentId: string | null; status: string };
426
+ )) as { agentId: string | null; routingReason: string | null; status: string };
426
427
 
427
428
  expect(createdTask).toBeDefined();
428
429
  expect(createdTask.agentId).toBeNull();
430
+ expect(createdTask.routingReason).toBeNull();
429
431
  expect(createdTask.status).toBe("unassigned");
430
432
  });
431
433
 
@@ -529,7 +531,7 @@ describe("Scheduled Tasks Integration", () => {
529
531
 
530
532
  // Find the created task
531
533
  const createdTask = (await getDbClient().get(
532
- "SELECT * FROM agent_tasks WHERE task = ? ORDER BY createdAt DESC LIMIT 1",
534
+ "SELECT *, routing_reason AS routingReason FROM agent_tasks WHERE task = ? ORDER BY createdAt DESC LIMIT 1",
533
535
  [schedule.taskTemplate!],
534
536
  )) as {
535
537
  task: string;
@@ -537,6 +539,7 @@ describe("Scheduled Tasks Integration", () => {
537
539
  tags: string;
538
540
  priority: number;
539
541
  agentId: string;
542
+ routingReason: string;
540
543
  creatorAgentId: string;
541
544
  };
542
545
 
@@ -544,6 +547,7 @@ describe("Scheduled Tasks Integration", () => {
544
547
  expect(createdTask.taskType).toBe("comprehensive");
545
548
  expect(createdTask.priority).toBe(90);
546
549
  expect(createdTask.agentId).toBe(testAgent.id);
550
+ expect(createdTask.routingReason).toBe("human_pinned");
547
551
  expect(createdTask.creatorAgentId).toBe(testAgent.id);
548
552
 
549
553
  const tags = JSON.parse(createdTask.tags);
@@ -566,6 +566,10 @@ describe("script connections", () => {
566
566
  markMigrationApplied(database, "147_scheduled_tasks_parent_task.sql");
567
567
  // 148 rebuilds pages, which this migration-112-only fixture does not create.
568
568
  markMigrationApplied(database, "148_pages_svg.sql");
569
+ // 151 repairs scheduled_tasks, which this migration-112-only fixture does not create.
570
+ markMigrationApplied(database, "151_repair_scheduled_task_required_params.sql");
571
+ // 152 indexes task routing context absent from this migration-112-only fixture.
572
+ markMigrationApplied(database, "152_routing_decisions.sql");
569
573
 
570
574
  const id = crypto.randomUUID();
571
575
  const now = new Date().toISOString();
@@ -401,6 +401,37 @@ describe("/api/script-runs HTTP", () => {
401
401
  expect(stepCount.c).toBe(1);
402
402
  });
403
403
 
404
+ test("agent-task dispatch preserves existing author pins and records human_pinned", async () => {
405
+ const created = await dispatch("/api/script-runs", {
406
+ method: "POST",
407
+ agentId,
408
+ body: createBody(),
409
+ });
410
+ const { id: runId } = (await created.json()) as { id: string };
411
+
412
+ const responsePromise = dispatch(`/api/internal/script-runs/${runId}/agent-task`, {
413
+ method: "POST",
414
+ agentId,
415
+ body: JSON.stringify({ stepKey: "pinned", task: "do pinned work", agentId }),
416
+ });
417
+
418
+ let dispatched = await getLatestScriptRunStepTaskByContextKey(`script-run:${runId}:pinned`);
419
+ for (let i = 0; i < 50 && !dispatched; i++) {
420
+ await Bun.sleep(20);
421
+ dispatched = await getLatestScriptRunStepTaskByContextKey(`script-run:${runId}:pinned`);
422
+ }
423
+ expect(dispatched).not.toBeNull();
424
+ await completeTask(dispatched!.id, "done");
425
+ const response = await responsePromise;
426
+ expect(response.status).toBe(200);
427
+ const { taskId } = (await response.json()) as { taskId: string };
428
+ expect(
429
+ await getDbClient().get("SELECT agentId, routing_reason FROM agent_tasks WHERE id = ?", [
430
+ taskId,
431
+ ]),
432
+ ).toEqual({ agentId, routing_reason: "human_pinned" });
433
+ });
434
+
404
435
  test("agent-task polling returns the step output when a newer completed follow-up shares its context key", async () => {
405
436
  const created = await dispatch("/api/script-runs", {
406
437
  method: "POST",
@@ -47,6 +47,17 @@ afterAll(async () => {
47
47
  });
48
48
 
49
49
  describe("typecheckScript", () => {
50
+ test("task_defer accepts task wakeOn with a required caller-supplied ceiling", async () => {
51
+ const result = await typecheckScript(`
52
+ import type { ScriptContext } from "swarm-sdk";
53
+ export default async function(args: unknown, ctx: ScriptContext) {
54
+ return ctx.swarm.task_defer({ taskId: "parent", delayMs: 60000,
55
+ wakeOn: { event: "settled", taskId: "producer" }, summary: "submitted", note: "check result" });
56
+ }
57
+ `);
58
+ expect(result).toEqual({ ok: true });
59
+ });
60
+
50
61
  test("accepts ES2022 globals: JSON, Math, Date, Number, String, Error, isFinite, encodeURIComponent, parseInt, parseFloat", async () => {
51
62
  const source = `
52
63
  export default async () => {
@@ -28,6 +28,18 @@ afterEach(() => {
28
28
  });
29
29
 
30
30
  describe("scrubSecrets — edge cases", () => {
31
+ test("redacts ACP session tokens in plain text and escaped session logs", () => {
32
+ const token = `aseph_${"aB3".repeat(11)}`;
33
+ const github = `ghp_${"gH4".repeat(12)}`;
34
+ for (const prefix of ["Bearer ", "\\n", "\\t", '"']) {
35
+ const input = `${prefix}${token} tail ${github}`;
36
+ const output = scrubSecrets(input);
37
+ expect(output).not.toContain(github);
38
+ expect(output).toBe(`${prefix}[REDACTED:acp_session_token] tail [REDACTED:github_token]`);
39
+ expect(scrubObject({ output: input })).toEqual({ output });
40
+ }
41
+ });
42
+
31
43
  test("empty string passes through", () => {
32
44
  expect(scrubSecrets("")).toBe("");
33
45
  });
@@ -2,7 +2,14 @@ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
2
2
  import { unlink } from "node:fs/promises";
3
3
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
4
4
  import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
5
- import { closeDb, createAgent, createUser, getTaskById, initDb } from "../be/db";
5
+ import {
6
+ closeDb,
7
+ createAgent,
8
+ createTaskExtended,
9
+ createUser,
10
+ getTaskById,
11
+ initDb,
12
+ } from "../be/db";
6
13
  import { createUserServer, userSendTaskInputSchema } from "../server-user";
7
14
  import { registerSendTaskTool, sendTaskInputSchema } from "../tools/send-task";
8
15
 
@@ -160,6 +167,7 @@ describe("send-task: outputSchema propagation", () => {
160
167
  expect(s.success).toBe(true);
161
168
  const created = await getTaskById(s.task!.id);
162
169
  expect(created?.outputSchema).toEqual(schema);
170
+ expect(created?.routingReason).toBeUndefined();
163
171
  });
164
172
 
165
173
  test("a provided outputSchema is persisted when the task is offered to an agent", async () => {
@@ -175,6 +183,7 @@ describe("send-task: outputSchema propagation", () => {
175
183
  {
176
184
  task: "offered task with a schema",
177
185
  agentId: worker.id,
186
+ routingReason: "human_pinned",
178
187
  offerMode: true,
179
188
  outputSchema: schema,
180
189
  allowDuplicate: true,
@@ -204,6 +213,7 @@ describe("send-task: outputSchema propagation", () => {
204
213
  {
205
214
  task: "directly assigned task with a schema",
206
215
  agentId: worker.id,
216
+ routingReason: "human_pinned",
207
217
  outputSchema: schema,
208
218
  allowDuplicate: true,
209
219
  },
@@ -213,6 +223,61 @@ describe("send-task: outputSchema propagation", () => {
213
223
  expect(s.success).toBe(true);
214
224
  const created = await getTaskById(s.task!.id);
215
225
  expect(created?.outputSchema).toEqual(schema);
226
+ expect(created?.routingReason).toBe("human_pinned");
227
+ });
228
+
229
+ test("implicit parent routing stamps continuity while pool creation omits a reason", async () => {
230
+ const worker = await createAgent({
231
+ name: "Continuity worker",
232
+ isLead: false,
233
+ status: "idle",
234
+ maxTasks: 2,
235
+ });
236
+ const parent = await createTaskExtended("continuity parent", {
237
+ agentId: worker.id,
238
+ routingReason: "human_pinned",
239
+ });
240
+ const continued = await callSendTask(
241
+ server,
242
+ {
243
+ task: "continue implicitly",
244
+ parentTaskId: parent.id,
245
+ allowDuplicate: true,
246
+ },
247
+ LEAD_ID,
248
+ );
249
+ const continuedTask = await getTaskById(structuredOf(continued).task!.id);
250
+ expect(continuedTask?.agentId).toBe(worker.id);
251
+ expect(continuedTask?.routingReason).toBe("continuity");
252
+
253
+ const pooled = await callSendTask(
254
+ server,
255
+ { task: "stay pooled", allowDuplicate: true },
256
+ LEAD_ID,
257
+ );
258
+ expect((await getTaskById(structuredOf(pooled).task!.id))?.routingReason).toBeUndefined();
259
+ });
260
+
261
+ test("direct routing note is persisted", async () => {
262
+ const worker = await createAgent({
263
+ name: "Noted worker",
264
+ isLead: false,
265
+ status: "idle",
266
+ maxTasks: 1,
267
+ });
268
+ const result = await callSendTask(
269
+ server,
270
+ {
271
+ task: "direct task with routing note",
272
+ agentId: worker.id,
273
+ routingReason: "skill",
274
+ routingNote: "matches the required runtime specialization",
275
+ allowDuplicate: true,
276
+ },
277
+ LEAD_ID,
278
+ );
279
+ const created = await getTaskById(structuredOf(result).task!.id);
280
+ expect(created?.routingNote).toBe("matches the required runtime specialization");
216
281
  });
217
282
  });
218
283
 
@@ -151,6 +151,7 @@ describe("send-task: requestedByUserId inheritance", () => {
151
151
  {
152
152
  task: "worker direct assign — inherit user",
153
153
  agentId: WORKER_ID,
154
+ routingReason: "human_pinned",
154
155
  allowDuplicate: true,
155
156
  },
156
157
  LEAD_ID,
@@ -275,6 +275,7 @@ describe("send-task: Slack-routing coherence guard", () => {
275
275
  {
276
276
  task: "direct assign with wrong channel",
277
277
  agentId: WORKER_ID,
278
+ routingReason: "human_pinned",
278
279
  slackChannelId: "C_WRONG_DIRECT",
279
280
  slackThreadTs: "555.555",
280
281
  allowDuplicate: true,
@@ -299,6 +300,7 @@ describe("send-task: Slack-routing coherence guard", () => {
299
300
  {
300
301
  task: "offer with wrong channel",
301
302
  agentId: WORKER_ID,
303
+ routingReason: "human_pinned",
302
304
  offerMode: true,
303
305
  slackChannelId: "C_WRONG_OFFER",
304
306
  slackThreadTs: "666.666",