@desplega.ai/agent-swarm 1.145.0 → 1.147.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 (221) hide show
  1. package/README.md +2 -1
  2. package/dist/{acp-adapter-580t41c0.js → acp-adapter-9n319wqc.js} +61 -6
  3. package/dist/{actions-m662tjng.js → actions-2vxqvpr9.js} +9 -8
  4. package/dist/{app-zb3rndm8.js → app-w066xfy0.js} +6 -5
  5. package/dist/assistant-kt31dj2h.js +236 -0
  6. package/dist/{boot-reembed-072x00vn.js → boot-reembed-az5rassp.js} +7 -6
  7. package/dist/{boot-reembed-65zz2smt.js → boot-reembed-j3mm3rfz.js} +8 -7
  8. package/dist/{boot-scrub-logs-yz0ams04.js → boot-scrub-logs-b6h54817.js} +4 -4
  9. package/dist/{claude-adapter-paj270y2.js → claude-adapter-w1z04ann.js} +8 -6
  10. package/dist/{claude-managed-setup-w619jncn.js → claude-managed-setup-f5pmsyrp.js} +24 -3
  11. package/dist/{claude-sdk-session-swt80j9z.js → claude-sdk-session-f0cc1kec.js} +8 -6
  12. package/dist/{cli-dt553ktb.js → cli-0b6y7kxm.js} +1 -1
  13. package/dist/{cli-bxdhw615.js → cli-12fz972k.js} +4 -4
  14. package/dist/{cli-s2f20r5w.js → cli-1th1728g.js} +2 -2
  15. package/dist/{cli-rs6ec3b9.js → cli-3tgwnf43.js} +3 -3
  16. package/dist/{cli-gxnzc6wh.js → cli-4nr988h6.js} +1 -1
  17. package/dist/{cli-axs8e6d4.js → cli-53s590z8.js} +1 -1
  18. package/dist/{cli-ygbjp3z8.js → cli-54wwve05.js} +5 -4
  19. package/dist/{cli-v0asr5yv.js → cli-719k9j2c.js} +2 -2
  20. package/dist/{cli-anktweqr.js → cli-89xfd7f9.js} +21 -47
  21. package/dist/{cli-q34a0e10.js → cli-8v0g1yc8.js} +1 -1
  22. package/dist/cli-9d8ntetj.js +42 -0
  23. package/dist/{cli-z9fprcrf.js → cli-atcve9y8.js} +881 -738
  24. package/dist/{cli-jmb7g47h.js → cli-b5bq9crk.js} +14 -31
  25. package/dist/cli-d8brzxjb.js +190 -0
  26. package/dist/{cli-gzky0gkr.js → cli-de14znh8.js} +2 -2
  27. package/dist/{cli-xytwff7f.js → cli-dttph1bw.js} +2 -2
  28. package/dist/cli-ep19k3mb.js +19 -0
  29. package/dist/{cli-m1xa8be7.js → cli-f146mn5s.js} +7 -50
  30. package/dist/{cli-1fj0chdk.js → cli-f7dsy61y.js} +5 -3
  31. package/dist/{cli-f567d5c1.js → cli-ftht3zzj.js} +2782 -2589
  32. package/dist/{cli-a0g1gbt5.js → cli-hbv0kq6w.js} +2 -2
  33. package/dist/{cli-fhsz3twr.js → cli-jsp14b6c.js} +171 -1
  34. package/dist/{cli-7fnkj1hj.js → cli-kpv03zhj.js} +21 -17
  35. package/dist/{cli-m4j6trn4.js → cli-m8yrsg97.js} +2 -2
  36. package/dist/cli-m9sxbkhm.js +779 -0
  37. package/dist/cli-mmemxxdc.js +823 -0
  38. package/dist/{cli-tqtvj5pt.js → cli-n8508nre.js} +1 -1
  39. package/dist/{cli-z96t0thj.js → cli-p1d5b073.js} +37 -2
  40. package/dist/{cli-gh8y8f36.js → cli-rw51bq3j.js} +13 -10
  41. package/dist/{cli-59ej1c07.js → cli-smby5m17.js} +7 -6
  42. package/dist/{cli-65vqzz3v.js → cli-tzvk9haz.js} +94 -372
  43. package/dist/{cli-dc9k2ba9.js → cli-w9kb3bk1.js} +1 -1
  44. package/dist/{cli-kpev657q.js → cli-y6b6fb76.js} +25 -3
  45. package/dist/{cli-123q2get.js → cli-yz3djrm0.js} +9 -9
  46. package/dist/{cli-wvp6bvy4.js → cli-zhjmqxzh.js} +1 -1
  47. package/dist/cli.js +13 -13
  48. package/dist/{codex-adapter-k98vaw2r.js → codex-adapter-zw4dwsx7.js} +2 -2
  49. package/dist/{codex-hook-9pf19cks.js → codex-hook-bw8p5nh4.js} +2 -2
  50. package/dist/{codex-session-runner-tngtphx7.js → codex-session-runner-k981s65v.js} +2 -2
  51. package/dist/{commands-y8nbr3fv.js → commands-sykrxx7e.js} +4 -4
  52. package/dist/{db-30p4h2t1.js → db-sfbsc8wt.js} +6 -4
  53. package/dist/handlers-0aspcejm.js +603 -0
  54. package/dist/{hook-dcw4jfbj.js → hook-4nvqj2sb.js} +7 -5
  55. package/dist/{hook-21jexvfx.js → hook-hxccs7m5.js} +24 -49
  56. package/dist/{http-r4kq5g8d.js → http-y06z517d.js} +230 -94
  57. package/dist/{index-q3qja6ta.js → index-hssww7kj.js} +15 -14
  58. package/dist/{index-gay66b9w.js → index-k8znfx0z.js} +13 -12
  59. package/dist/{index-53zyht4z.js → index-tce1rss4.js} +371 -110
  60. package/dist/{index-nqvf26za.js → index-x5j894zf.js} +14 -13
  61. package/dist/{keepalive-xs8fa55y.js → keepalive-w733ax66.js} +6 -6
  62. package/dist/{lead-d9m2t4s0.js → lead-75bjqa9d.js} +29 -27
  63. package/dist/{maintenance-3zh82g6z.js → maintenance-w4f1zjck.js} +8 -7
  64. package/dist/{oauth-refresh-sweep-03snsye5.js → oauth-refresh-sweep-wdetky26.js} +5 -5
  65. package/dist/{onboard-0cg1925x.js → onboard-bb5net8s.js} +2 -2
  66. package/dist/{otel-impl-0ab95z27.js → otel-impl-0w7c14ap.js} +1 -1
  67. package/dist/{pi-mono-adapter-dtkz4e7j.js → pi-mono-adapter-2has4gnp.js} +12 -2
  68. package/dist/{pricing-refresh-e5jhzz4j.js → pricing-refresh-9nzsrgcq.js} +6 -6
  69. package/dist/{rbac-roles-p1mf40z3.js → rbac-roles-r3hqjp09.js} +5 -5
  70. package/dist/{rbac-roles-2w3cmrkc.js → rbac-roles-z1jrffbt.js} +4 -4
  71. package/dist/{registry-9kdck5ye.js → registry-v5wytg0s.js} +1 -2
  72. package/dist/{render-v2-xhawry3k.js → render-v2-vbr89fnb.js} +6 -5
  73. package/dist/{seed-pricing-t223sw91.js → seed-pricing-g62hy1pk.js} +7 -5
  74. package/dist/{setup-zcs0rce7.js → setup-gnfqnptp.js} +2 -2
  75. package/dist/{worker-qnp2ka62.js → worker-kznxp4qm.js} +29 -27
  76. package/openapi.json +263 -12
  77. package/package.json +1 -1
  78. package/plugin/commands/start-worker.md +4 -9
  79. package/plugin/pi-skills/start-worker/SKILL.md +4 -9
  80. package/src/be/db/agents.ts +556 -0
  81. package/src/be/db/context-versions.ts +117 -0
  82. package/src/be/db/runtime.ts +406 -0
  83. package/src/be/db/tasks/read.ts +1167 -0
  84. package/src/be/db/tasks/write.ts +953 -0
  85. package/src/be/db.ts +584 -3429
  86. package/src/be/migrations/147_scheduled_tasks_parent_task.sql +7 -0
  87. package/src/be/migrations/148_pages_svg.sql +123 -0
  88. package/src/be/migrations/149_session_tokens.sql +24 -0
  89. package/src/be/scripts/typecheck.ts +1 -0
  90. package/src/be/seed-pricing.ts +13 -8
  91. package/src/be/seed-scripts/catalog/ops-catalog-audit.inline.ts +1 -1
  92. package/src/be/seed-scripts/catalog/ops-catalog-audit.ts +1 -1
  93. package/src/be/seed-scripts/catalog/star-history-refresh.ts +101 -0
  94. package/src/be/seed-scripts/catalog/star-history-renderer.ts +119 -0
  95. package/src/be/seed-scripts/index.ts +17 -0
  96. package/src/be/seed-skills/index.ts +14 -0
  97. package/src/be/swarm-config-guard.ts +5 -2
  98. package/src/be/task-attachment-store.ts +70 -0
  99. package/src/be/users.ts +77 -0
  100. package/src/commands/attachments-section.ts +37 -0
  101. package/src/commands/claude-managed-setup.ts +45 -3
  102. package/src/commands/claude-md-session.ts +408 -0
  103. package/src/commands/context-preamble.ts +15 -3
  104. package/src/commands/profile-sync.ts +205 -5
  105. package/src/commands/provider-credentials.ts +60 -27
  106. package/src/commands/runner.ts +63 -58
  107. package/src/commands/templates.ts +25 -2
  108. package/src/hooks/hook.ts +26 -67
  109. package/src/http/agents.ts +40 -1
  110. package/src/http/auth.ts +13 -1
  111. package/src/http/fs.ts +21 -51
  112. package/src/http/index.ts +2 -0
  113. package/src/http/mcp.ts +32 -0
  114. package/src/http/pages-public.ts +20 -4
  115. package/src/http/pages.ts +11 -3
  116. package/src/http/poll.ts +4 -0
  117. package/src/http/repos.ts +3 -3
  118. package/src/http/sessions.ts +82 -0
  119. package/src/http/tasks.ts +10 -2
  120. package/src/http/utils.ts +79 -1
  121. package/src/prompts/session-templates.ts +12 -8
  122. package/src/providers/acp-adapter.ts +62 -8
  123. package/src/providers/pi-mono-adapter.ts +23 -6
  124. package/src/providers/pricing-sources.md +11 -0
  125. package/src/scheduler/scheduler.test.ts +49 -2
  126. package/src/scheduler/scheduler.ts +4 -0
  127. package/src/scripts-runtime/sdk-allowlist.ts +1 -0
  128. package/src/scripts-runtime/types/stdlib.d.ts +8 -0
  129. package/src/scripts-runtime/types/swarm-sdk.d.ts +8 -0
  130. package/src/server.ts +2 -0
  131. package/src/slack/assistant.ts +87 -46
  132. package/src/slack/handlers.test.ts +1 -3
  133. package/src/slack/handlers.ts +102 -87
  134. package/src/slack/inbound-files.ts +466 -0
  135. package/src/slack/templates.ts +14 -0
  136. package/src/tasks/task-terminal-effects.ts +158 -0
  137. package/src/tests/acp-adapter.test.ts +369 -117
  138. package/src/tests/budget-claim-gate.test.ts +1 -0
  139. package/src/tests/build-pi-skills.test.ts +5 -7
  140. package/src/tests/claude-managed-setup.test.ts +52 -4
  141. package/src/tests/claude-md-session.test.ts +603 -0
  142. package/src/tests/codex-skill-resolver.test.ts +19 -0
  143. package/src/tests/context-versioning.test.ts +107 -0
  144. package/src/tests/cors-allowlist.test.ts +309 -0
  145. package/src/tests/credential-check.test.ts +157 -14
  146. package/src/tests/db-agents.test.ts +91 -0
  147. package/src/tests/db-context-versions.test.ts +37 -0
  148. package/src/tests/db-extraction-contract.test.ts +45 -0
  149. package/src/tests/db-runtime.test.ts +70 -0
  150. package/src/tests/db-task-read-contract.test.ts +624 -0
  151. package/src/tests/db-task-write-contract.test.ts +653 -0
  152. package/src/tests/db-tasks-read.test.ts +183 -0
  153. package/src/tests/db-tasks-write.test.ts +230 -0
  154. package/src/tests/defer-task.test.ts +389 -0
  155. package/src/tests/docs-index-footprint.test.js +2 -2
  156. package/src/tests/e2e-visuals-stage.test.ts +151 -0
  157. package/src/tests/file-lock.test.ts +144 -0
  158. package/src/tests/fixtures/file-lock-holder.ts +13 -0
  159. package/src/tests/fixtures/hold-file-lock.ts +25 -0
  160. package/src/tests/heartbeat-expiry.test.ts +188 -0
  161. package/src/tests/http-api-integration.test.ts +78 -1
  162. package/src/tests/pages-public-html.test.ts +43 -0
  163. package/src/tests/pages-svg-migration.test.ts +42 -0
  164. package/src/tests/preload.ts +4 -1
  165. package/src/tests/profile-sync-warning.test.ts +16 -0
  166. package/src/tests/profile-sync.test.ts +126 -0
  167. package/src/tests/prompt-template-session.test.ts +20 -4
  168. package/src/tests/provider-registration.test.ts +93 -0
  169. package/src/tests/rbac-e2e-helpers.test.ts +53 -0
  170. package/src/tests/rbac-e2e-helpers.ts +19 -2
  171. package/src/tests/reload-config.test.ts +26 -0
  172. package/src/tests/routing-affinity-inventory.test.ts +1 -1
  173. package/src/tests/runner-attachments-section.test.ts +22 -2
  174. package/src/tests/runner-context-preamble.test.ts +30 -0
  175. package/src/tests/runner-deployment-resume.test.ts +107 -0
  176. package/src/tests/runner-repo-autostash.test.ts +12 -0
  177. package/src/tests/script-connections.test.ts +4 -0
  178. package/src/tests/seed-scripts.test.ts +2 -2
  179. package/src/tests/session-tokens.test.ts +279 -0
  180. package/src/tests/slack-file-tools-attachments.test.ts +408 -0
  181. package/src/tests/slack-inbound-files.test.ts +847 -0
  182. package/src/tests/star-history-refresh.test.ts +102 -0
  183. package/src/tests/swarm-config-reserved-keys.test.ts +21 -0
  184. package/src/tests/swarm-repos.test.ts +20 -0
  185. package/src/tests/tool-annotations.test.ts +4 -3
  186. package/src/tests/ui-logs-parser.test.ts +176 -0
  187. package/src/tools/create-page.ts +4 -2
  188. package/src/tools/defer-task.ts +239 -0
  189. package/src/tools/repos/update-repo.ts +2 -2
  190. package/src/tools/schedules/create-schedule.ts +1 -0
  191. package/src/tools/schedules/list-schedules.ts +1 -0
  192. package/src/tools/schedules/patch-schedule.ts +1 -0
  193. package/src/tools/schedules/update-schedule.ts +1 -0
  194. package/src/tools/slack-download-file.ts +60 -29
  195. package/src/tools/slack-read.ts +73 -32
  196. package/src/tools/store-progress.ts +16 -146
  197. package/src/tools/tool-config.ts +1 -0
  198. package/src/tools/utils.ts +21 -0
  199. package/src/types.ts +37 -4
  200. package/src/utils/acp-session-token.ts +56 -0
  201. package/src/utils/file-lock.ts +111 -0
  202. package/src/utils/heartbeat-expiry.ts +78 -0
  203. package/src/utils/request-auth-context.ts +2 -0
  204. package/src/utils/task-attachment-links.ts +22 -0
  205. package/templates/skills/code-quality/content.md +8 -5
  206. package/templates/skills/download-task-attachment/content.md +2 -1
  207. package/{plugin/pi-skills → templates/skills}/review-offered-task/SKILL.md +5 -5
  208. package/templates/skills/review-offered-task/config.json +14 -0
  209. package/{plugin/commands/review-offered-task.md → templates/skills/review-offered-task/content.md} +3 -8
  210. package/templates/skills/scheduling/content.md +4 -0
  211. package/templates/skills/slack-interaction/content.md +2 -2
  212. package/templates/skills/work-on-task/SKILL.md +16 -0
  213. package/templates/skills/work-on-task/config.json +14 -0
  214. package/templates/skills/work-on-task/content.md +11 -0
  215. package/dist/assistant-0qt42nzg.js +0 -212
  216. package/dist/cli-fs1kkqgr.js +0 -317
  217. package/dist/cli-jnf5zghe.js +0 -269
  218. package/dist/cli-n1q8q14d.js +0 -177
  219. package/dist/handlers-yfp1qn81.js +0 -592
  220. package/plugin/commands/work-on-task.md +0 -39
  221. package/plugin/pi-skills/work-on-task/SKILL.md +0 -39
@@ -0,0 +1,102 @@
1
+ import { afterAll, beforeAll, expect, test } from "bun:test";
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import type { ScriptContext } from "swarm-sdk";
4
+ import { closeDb, getPageBySlug, initDb } from "../be/db";
5
+ import refresh from "../be/seed-scripts/catalog/star-history-refresh";
6
+ import { renderChart } from "../be/seed-scripts/catalog/star-history-renderer";
7
+ import { registerCreatePageTool } from "../tools/create-page";
8
+
9
+ const agentId = crypto.randomUUID();
10
+ const server = new McpServer({ name: "star-history-test", version: "1" });
11
+ registerCreatePageTool(server);
12
+ const tool = (server as any)._registeredTools.create_page;
13
+ beforeAll(() => initDb(":memory:"));
14
+ afterAll(() => closeDb());
15
+
16
+ function fixture() {
17
+ let cache: unknown;
18
+ let fail = false;
19
+ let calls = 0;
20
+ const ctx = {
21
+ stdlib: {
22
+ fetch: async (url: string, init: RequestInit) => {
23
+ calls++;
24
+ if (fail) return new Response("", { status: 503 });
25
+ const page = Number(new URL(url).searchParams.get("page"));
26
+ if ((init.headers as Record<string, string>)["If-None-Match"]) {
27
+ return new Response(null, { status: 304 });
28
+ }
29
+ return Response.json(
30
+ Array.from({ length: page === 1 ? 100 : 2 }, (_, i) => ({
31
+ starred_at: new Date(Date.UTC(2026, 0, 1) + (page * 100 + i) * 86400000).toISOString(),
32
+ })),
33
+ { headers: { ETag: `"page-${page}"` } },
34
+ );
35
+ },
36
+ },
37
+ swarm: {
38
+ kv_getOrNull: async () => (cache ? { value: cache } : null),
39
+ kv_set: async ({ value }: { value: unknown }) => {
40
+ cache = value;
41
+ },
42
+ page_create: async (args: unknown) => {
43
+ const res = await tool.handler(args, {
44
+ sessionId: "stars",
45
+ requestInfo: { headers: { "x-agent-id": agentId } },
46
+ });
47
+ return res.structuredContent;
48
+ },
49
+ },
50
+ } as unknown as ScriptContext;
51
+ return {
52
+ ctx,
53
+ fail: () => {
54
+ fail = true;
55
+ },
56
+ calls: () => calls,
57
+ };
58
+ }
59
+
60
+ test("paginated refresh, ETags, stable MCP page IDs/URLs, and cached fallback", async () => {
61
+ const f = fixture();
62
+ const first = await refresh({}, f.ctx);
63
+ expect(first.stars).toBe(102);
64
+ expect(f.calls()).toBe(2);
65
+ expect(first.stale).toBe(false);
66
+ const second = await refresh({}, f.ctx);
67
+ expect(second.notModified).toBe(2);
68
+ expect(second.pages).toEqual(first.pages);
69
+ for (const page of second.pages) {
70
+ const stored = await getPageBySlug(agentId, page.slug);
71
+ expect(stored?.contentType).toBe("image/svg+xml");
72
+ expect(stored?.authMode).toBe("public");
73
+ expect(stored?.body).toContain("102 stars");
74
+ expect(stored?.body).not.toContain("NaN");
75
+ }
76
+ f.fail();
77
+ const fallback = await refresh({}, f.ctx);
78
+ expect(fallback.stale).toBe(true);
79
+ expect(fallback.stars).toBe(102);
80
+ expect(fallback.pages).toEqual(first.pages);
81
+ expect(fallback.fetchedAt).toBe(second.fetchedAt);
82
+ });
83
+
84
+ test("a failed first fetch refuses to publish an empty chart", async () => {
85
+ const f = fixture();
86
+ f.fail();
87
+ await expect(refresh({}, f.ctx)).rejects.toThrow("no cached series");
88
+ });
89
+
90
+ test("dry run renders both themes without publishing", async () => {
91
+ const f = fixture();
92
+ const result = await refresh({ dryRun: true, authenticated: false }, f.ctx);
93
+ expect(result.pages).toHaveLength(2);
94
+ expect(result.pages.every((page) => "bytes" in page && page.bytes > 1000)).toBe(true);
95
+ });
96
+
97
+ test("renderer escapes repository text and handles one star", async () => {
98
+ const svg = renderChart("owner/<repo>", [Date.UTC(2026, 0, 1)], "dark");
99
+ expect(svg).toContain("owner/&lt;repo&gt;");
100
+ expect(svg).toContain("1 stars");
101
+ expect(svg).not.toContain("NaN");
102
+ });
@@ -134,6 +134,27 @@ describe("swarm-config reserved keys guard", () => {
134
134
  await unlink(`${TEST_DB_PATH}-shm`).catch(() => {});
135
135
  });
136
136
 
137
+ for (const key of ["CORS_ALLOW_ANY_ORIGIN", "cors_allow_any_origin"]) {
138
+ test(`rejects deployment-only ${key} through DB, HTTP, and MCP`, async () => {
139
+ const config = { scope: "global" as const, key, value: "true" };
140
+ await expect(upsertSwarmConfig(config)).rejects.toThrow(EXPECTED_MESSAGE(key));
141
+ const res = await fetch(`${baseUrl}/api/config`, {
142
+ method: "PUT",
143
+ headers: { "Content-Type": "application/json" },
144
+ body: JSON.stringify(config),
145
+ });
146
+ expect(res.status).toBe(400);
147
+ expect(await res.json()).toMatchObject({ error: EXPECTED_MESSAGE(key) });
148
+ const handler = mcpServer.handlers.get("set-config")!;
149
+ const result = (await handler(config, makeRequestInfo())) as {
150
+ structuredContent: { success: boolean; message: string };
151
+ };
152
+ expect(result.structuredContent.success).toBe(false);
153
+ expect(result.structuredContent.message).toBe(EXPECTED_MESSAGE(key));
154
+ expect(await getSwarmConfigs({ key })).toHaveLength(0);
155
+ });
156
+ }
157
+
137
158
  // ─── Helper predicate ─────────────────────────────────────────────────────
138
159
  describe("isReservedConfigKey helper", () => {
139
160
  test("recognizes API_KEY", () => {
@@ -11,6 +11,11 @@ import {
11
11
  initDb,
12
12
  updateSwarmRepo,
13
13
  } from "../be/db";
14
+ import {
15
+ REPO_GUIDELINES_MAX_ENTRIES,
16
+ RepoGuidelinesInputSchema,
17
+ RepoGuidelinesSchema,
18
+ } from "../types";
14
19
 
15
20
  const TEST_DB_PATH = "./test-swarm-repos.sqlite";
16
21
 
@@ -255,6 +260,21 @@ describe("Swarm Repos", () => {
255
260
  expect(updated?.guidelines).toBeNull();
256
261
  });
257
262
 
263
+ test("write schema caps each guideline list; read schema stays unbounded", () => {
264
+ const atCap = Array.from({ length: REPO_GUIDELINES_MAX_ENTRIES }, (_, i) => `check ${i}`);
265
+ const overCap = [...atCap, "one more"];
266
+ const base = { prChecks: [], mergeChecks: [], allowMerge: false, review: [] };
267
+
268
+ expect(RepoGuidelinesInputSchema.safeParse({ ...base, prChecks: atCap }).success).toBe(true);
269
+ for (const key of ["prChecks", "mergeChecks", "review"] as const) {
270
+ expect(RepoGuidelinesInputSchema.safeParse({ ...base, [key]: overCap }).success).toBe(
271
+ false,
272
+ );
273
+ // A stored row above the cap must still read back.
274
+ expect(RepoGuidelinesSchema.safeParse({ ...base, [key]: overCap }).success).toBe(true);
275
+ }
276
+ });
277
+
258
278
  test("should round-trip null vs configured distinction", async () => {
259
279
  const withGuidelines = await getSwarmRepoByName("guidelines-repo");
260
280
  const withoutGuidelines = await getSwarmRepoByName("no-guidelines-repo");
@@ -199,8 +199,8 @@ describe("Tool Annotations & Classification", () => {
199
199
  expect(overlap).toEqual([]);
200
200
  });
201
201
 
202
- test("CORE_TOOLS contains exactly 25 tools", () => {
203
- expect(CORE_TOOLS.size).toBe(25);
202
+ test("CORE_TOOLS contains exactly 26 tools", () => {
203
+ expect(CORE_TOOLS.size).toBe(26);
204
204
  });
205
205
 
206
206
  test("ALL_TOOLS equals CORE_TOOLS union DEFERRED_TOOLS", () => {
@@ -251,6 +251,7 @@ describe("Tool Annotations & Classification", () => {
251
251
  "task-action",
252
252
  "send-task",
253
253
  "get-tasks",
254
+ "defer-task",
254
255
  ];
255
256
  for (const tool of lifecycleTools) {
256
257
  expect(CORE_TOOLS.has(tool)).toBe(true);
@@ -377,7 +378,7 @@ describe("Tool Annotations & Classification", () => {
377
378
  // Includes 11 skill tools, 7 MCP server tools, reusable script tools, and the
378
379
  // native Kapso/WhatsApp tools (register/unregister number + send/reply message).
379
380
  expect(count).toBeGreaterThanOrEqual(45);
380
- expect(count).toBeLessThanOrEqual(140);
381
+ expect(count).toBeLessThanOrEqual(141);
381
382
  });
382
383
 
383
384
  test("core tools are fewer than deferred tools", () => {
@@ -786,6 +786,182 @@ describe("ui logs parser", () => {
786
786
  ]);
787
787
  });
788
788
 
789
+ test("surfaces originalType and a value summary for codex item.type unknown", () => {
790
+ const messages = parseSessionLogs([
791
+ log("sleep", "codex", 1, {
792
+ type: "item.completed",
793
+ item: {
794
+ id: "call-1",
795
+ type: "unknown",
796
+ originalType: "sleep",
797
+ value: { durationMs: 30000 },
798
+ },
799
+ }),
800
+ log("image", "codex", 2, {
801
+ type: "item.completed",
802
+ item: {
803
+ id: "exec-1",
804
+ type: "unknown",
805
+ originalType: "imageView",
806
+ value: { path: "/workspace/personal/meme/spongebob.jpg" },
807
+ },
808
+ }),
809
+ log("subagent", "codex", 3, {
810
+ type: "item.completed",
811
+ item: {
812
+ id: "subagent-1",
813
+ type: "unknown",
814
+ originalType: "subAgentActivity",
815
+ value: { kind: "completed", agentPath: "/root/standards_review" },
816
+ },
817
+ }),
818
+ log("collab", "codex", 4, {
819
+ type: "item.completed",
820
+ item: {
821
+ id: "collab-1",
822
+ type: "unknown",
823
+ originalType: "collabAgentToolCall",
824
+ value: { tool: "wait", status: "completed" },
825
+ },
826
+ }),
827
+ ]);
828
+
829
+ expect(messages.map((message) => message.content[0])).toEqual([
830
+ expect.objectContaining({
831
+ kind: "unknown",
832
+ data: expect.objectContaining({
833
+ type: "item.completed · sleep",
834
+ itemType: "sleep",
835
+ detail: "sleep 30s",
836
+ }),
837
+ }),
838
+ expect.objectContaining({
839
+ kind: "unknown",
840
+ data: expect.objectContaining({
841
+ type: "item.completed · imageView",
842
+ itemType: "imageView",
843
+ detail: "spongebob.jpg",
844
+ }),
845
+ }),
846
+ expect.objectContaining({
847
+ kind: "unknown",
848
+ data: expect.objectContaining({
849
+ type: "item.completed · subAgentActivity",
850
+ itemType: "subAgentActivity",
851
+ detail: "completed · /root/standards_review",
852
+ }),
853
+ }),
854
+ expect.objectContaining({
855
+ kind: "unknown",
856
+ data: expect.objectContaining({
857
+ type: "item.completed · collabAgentToolCall",
858
+ itemType: "collabAgentToolCall",
859
+ detail: "wait · completed",
860
+ }),
861
+ }),
862
+ ]);
863
+ });
864
+
865
+ test("groups a codex unknown item's started and completed events into one row", () => {
866
+ const items = normalizeSessionLogs([
867
+ log(
868
+ "sleep-start",
869
+ "codex",
870
+ 1,
871
+ {
872
+ type: "item.started",
873
+ item: {
874
+ id: "sleep-1",
875
+ type: "unknown",
876
+ originalType: "sleep",
877
+ value: { durationMs: 45000 },
878
+ },
879
+ },
880
+ "2026-06-01T09:32:48.000Z",
881
+ ),
882
+ log(
883
+ "sleep-done",
884
+ "codex",
885
+ 2,
886
+ {
887
+ type: "item.completed",
888
+ item: {
889
+ id: "sleep-1",
890
+ type: "unknown",
891
+ originalType: "sleep",
892
+ value: { durationMs: 45000 },
893
+ },
894
+ },
895
+ "2026-06-01T09:33:33.000Z",
896
+ ),
897
+ ]).items;
898
+
899
+ expect(items.filter((item) => item.kind === "unknown")).toHaveLength(1);
900
+ expect(items[0]).toEqual(
901
+ expect.objectContaining({ kind: "unknown", status: "completed", durationMs: 45000 }),
902
+ );
903
+ });
904
+
905
+ test("shows a codex unknown item as running before its terminal event arrives", () => {
906
+ const items = normalizeSessionLogs([
907
+ log("collab-start", "codex", 1, {
908
+ type: "item.started",
909
+ item: {
910
+ id: "collab-1",
911
+ type: "unknown",
912
+ originalType: "collabAgentToolCall",
913
+ value: { tool: "wait", status: "pending" },
914
+ },
915
+ }),
916
+ ]).items;
917
+
918
+ expect(items).toHaveLength(1);
919
+ expect(items[0]).toEqual(expect.objectContaining({ kind: "unknown", status: "running" }));
920
+ expect(items[0].durationMs).toBeUndefined();
921
+ });
922
+
923
+ test("marks a codex unknown item failed when its completed event carries a failure signal", () => {
924
+ const items = normalizeSessionLogs([
925
+ log("collab-start", "codex", 1, {
926
+ type: "item.started",
927
+ item: {
928
+ id: "collab-1",
929
+ type: "unknown",
930
+ originalType: "collabAgentToolCall",
931
+ value: { tool: "wait", status: "pending" },
932
+ },
933
+ }),
934
+ log("collab-done", "codex", 2, {
935
+ type: "item.completed",
936
+ item: {
937
+ id: "collab-1",
938
+ type: "unknown",
939
+ originalType: "collabAgentToolCall",
940
+ status: "failed",
941
+ value: { tool: "wait", status: "failed" },
942
+ },
943
+ }),
944
+ ]).items;
945
+
946
+ expect(items).toHaveLength(1);
947
+ expect(items[0]).toEqual(expect.objectContaining({ kind: "unknown", status: "failed" }));
948
+ });
949
+
950
+ test("keeps id-less codex unknown items as separate one-off rows", () => {
951
+ const items = normalizeSessionLogs([
952
+ log("no-id-start", "codex", 1, {
953
+ type: "item.started",
954
+ item: { type: "unknown", originalType: "sleep", value: { durationMs: 1000 } },
955
+ }),
956
+ log("no-id-done", "codex", 2, {
957
+ type: "item.completed",
958
+ item: { type: "unknown", originalType: "sleep", value: { durationMs: 1000 } },
959
+ }),
960
+ ]).items;
961
+
962
+ expect(items.filter((item) => item.kind === "unknown")).toHaveLength(2);
963
+ });
964
+
789
965
  test("normalizes claude-managed raw SSE events without unknown noise", () => {
790
966
  const result = normalizeSessionLogs([
791
967
  log("status", "claude-managed", 1, {
@@ -83,9 +83,11 @@ export const registerCreatePageTool = (server: McpServer) => {
83
83
  body: z
84
84
  .string()
85
85
  .min(1)
86
- .describe("Full page body (HTML document or JSON-render spec, per contentType)."),
86
+ .describe(
87
+ "Full page body (HTML document, SVG image, or JSON-render spec, per contentType).",
88
+ ),
87
89
  contentType: PageContentTypeSchema.describe(
88
- "'text/html' renders directly at /p/:id; 'application/json' is rendered by the SPA.",
90
+ "'text/html' and 'image/svg+xml' render directly at /p/:id; 'application/json' is rendered by the SPA.",
89
91
  ),
90
92
  authMode: PageAuthModeSchema.default("authed").describe(
91
93
  "'authed' — requires page-session cookie (default); 'public' — no gate and must be explicit; 'password' — requires key.",
@@ -0,0 +1,239 @@
1
+ import { ensure } from "@desplega.ai/business-use";
2
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import * as z from "zod";
4
+ import { resolveTaskAuditUserId } from "@/be/audit-user";
5
+ import {
6
+ completeTask,
7
+ createScheduledTask,
8
+ getAgentById,
9
+ getDbClient,
10
+ getTaskById,
11
+ updateAgentStatusFromCapacity,
12
+ } from "@/be/db";
13
+ import { runTaskTerminalEffects } from "@/tasks/task-terminal-effects";
14
+ import { getTaskOutputValidationError } from "@/tasks/terminal-result-guard";
15
+ import { assertOwnsTask, ownerCtx } from "@/tools/task-tool-ctx";
16
+ import { createToolRegistrar, swarmToolOutputSchema, toolErr, toolOk } from "@/tools/utils";
17
+ import { isTerminalTaskStatus } from "@/types";
18
+
19
+ /** Thrown inside the transaction to abort and roll back the schedule INSERT. */
20
+ class DeferAbortedError extends Error {}
21
+
22
+ /** Render `note` + optional `checks` as the plain-text tail both texts share. */
23
+ function renderChecks(checks?: string[]): string {
24
+ if (!checks || checks.length === 0) return "";
25
+ return `\n\nChecks:\n${checks.map((c) => `- ${c}`).join("\n")}`;
26
+ }
27
+
28
+ export const registerDeferTaskTool = (server: McpServer) => {
29
+ createToolRegistrar(server)(
30
+ "defer-task",
31
+ {
32
+ title: "Defer Task",
33
+ annotations: { destructiveHint: false, idempotentHint: false },
34
+ description:
35
+ "Completes this task now with status `completed` and books a wake-up for you. Use when the result needs time: a build, a deploy, a reply. The task reaches its final state on this call; the lead sees your summary as its output. A one-off schedule wakes you up later with a child task that carries this task as its parent. Provide delayMs or runAt, a summary of what you did, and a note that says what is pending and what to check.",
36
+ inputSchema: z.object({
37
+ taskId: z.string().describe("The ID of the task you are working on."),
38
+ delayMs: z
39
+ .number()
40
+ .int()
41
+ .positive()
42
+ .optional()
43
+ .describe("Wake up after this many milliseconds (e.g. 1800000 for 30 min)."),
44
+ runAt: z
45
+ .string()
46
+ .datetime()
47
+ .optional()
48
+ .describe("Wake up at this ISO datetime (e.g. '2026-03-06T15:00:00Z'). Must be future."),
49
+ summary: z
50
+ .string()
51
+ .min(1)
52
+ .max(4000)
53
+ .describe(
54
+ "What you did so far and where things stand. This becomes the task's output; the lead and your wake-up run both read it.",
55
+ ),
56
+ note: z
57
+ .string()
58
+ .min(1)
59
+ .max(2000)
60
+ .describe("What is pending, and what to check on wake-up."),
61
+ checks: z
62
+ .array(z.string().min(1))
63
+ .max(20)
64
+ .optional()
65
+ .describe("Concrete things to verify on wake-up, one per entry."),
66
+ }),
67
+ outputSchema: swarmToolOutputSchema({
68
+ yourAgentId: z.string().optional(),
69
+ taskId: z.string().optional(),
70
+ scheduleId: z.string().optional(),
71
+ nextRunAt: z.string().optional(),
72
+ }),
73
+ },
74
+ async ({ taskId, delayMs, runAt, summary, note, checks }, requestInfo, _meta) => {
75
+ if (!requestInfo.agentId) {
76
+ return toolErr('Agent ID not found. Set the "X-Agent-ID" header.');
77
+ }
78
+ const agent = await getAgentById(requestInfo.agentId);
79
+ if (!agent) {
80
+ return toolErr(`Agent not found: ${requestInfo.agentId}`);
81
+ }
82
+
83
+ const task = await getTaskById(taskId);
84
+ if (!task) {
85
+ return toolErr(`Task with ID "${taskId}" not found.`);
86
+ }
87
+
88
+ const forbidden = assertOwnsTask(ownerCtx(requestInfo), task);
89
+ if (forbidden) return forbidden;
90
+
91
+ // A deferral completes the task and books a wake-up for the CALLER, so
92
+ // only the assignee may defer. `assertOwnsTask` above is the RBAC
93
+ // chokepoint; it does not (today) constrain agent-to-agent access.
94
+ if (task.agentId !== requestInfo.agentId) {
95
+ return toolErr(`Task "${taskId}" is not assigned to you.`);
96
+ }
97
+
98
+ if (isTerminalTaskStatus(task.status)) {
99
+ return toolErr(`Task ${taskId} is already ${task.status}; nothing to defer.`);
100
+ }
101
+
102
+ // A workflow step's completion drives `src/workflows/resume.ts` to advance
103
+ // `next` nodes immediately using this call's output. The scheduled wake-up
104
+ // task runs outside that workflow run and has no way to feed its eventual
105
+ // result back into the step, so the workflow would advance on a deferral
106
+ // note instead of the real result. Not supported: fail the step normally
107
+ // (or use a workflow-native wait) instead of deferring it.
108
+ if (task.workflowRunId) {
109
+ return toolErr(
110
+ `Task ${taskId} is owned by workflow run ${task.workflowRunId}; workflow-owned tasks cannot be deferred.`,
111
+ );
112
+ }
113
+
114
+ if (!delayMs && !runAt) {
115
+ return toolErr("Provide either delayMs or runAt.");
116
+ }
117
+ if (delayMs && runAt) {
118
+ return toolErr("Provide either delayMs or runAt, not both.");
119
+ }
120
+ if (runAt && new Date(runAt).getTime() <= Date.now()) {
121
+ return toolErr("runAt must be in the future.");
122
+ }
123
+ const nextRunAt = delayMs ? new Date(Date.now() + delayMs).toISOString() : runAt!;
124
+
125
+ const checksBlock = renderChecks(checks);
126
+ const taskTemplate = `Resume task ${taskId}: ${note}${checksBlock}`;
127
+ const createdBy =
128
+ (await resolveTaskAuditUserId(requestInfo.sourceTaskId, requestInfo.agentId)) ?? undefined;
129
+
130
+ // The deferral note is written as the task's FINAL output — a task with
131
+ // an outputSchema must satisfy it on completion (store-progress enforces
132
+ // the same rule). Validate before creating anything: the real schedule
133
+ // id isn't known yet, but it never changes whether this prose is valid
134
+ // JSON against the schema, so a placeholder stands in for it here.
135
+ const previewOutput = `${summary}\n\nDeferred until ${nextRunAt} (schedule pending). Pending: ${note}${checksBlock}`;
136
+ const outputValidationError = getTaskOutputValidationError(task.outputSchema, previewOutput);
137
+ if (outputValidationError) {
138
+ return toolErr(
139
+ `Task ${taskId} has an outputSchema; its terminal output must satisfy it, but a deferral note cannot. ${outputValidationError}`,
140
+ );
141
+ }
142
+
143
+ try {
144
+ const committed = await getDbClient().transaction(async () => {
145
+ const schedule = await createScheduledTask({
146
+ // Unique name (`getScheduledTaskByName` is a unique lookup). The UUID
147
+ // prevents concurrent deferrals of the same task from colliding.
148
+ name: `deferred-${taskId.slice(0, 8)}-${Date.now()}-${crypto.randomUUID()}`,
149
+ description: note,
150
+ taskTemplate,
151
+ targetType: "agent-task",
152
+ scheduleType: "one_time",
153
+ nextRunAt,
154
+ targetAgentId: requestInfo.agentId,
155
+ createdByAgentId: requestInfo.agentId,
156
+ taskType: "deferred",
157
+ tags: ["deferred"],
158
+ priority: task.priority,
159
+ // No `model`: a concrete provider model pinned now can be
160
+ // incompatible with the assignee/provider at wake-up, especially
161
+ // after a delay. Only the portable modelTier travels — mirrors
162
+ // the non-inheriting continuation path in `src/be/db.ts`.
163
+ modelTier: task.modelTier,
164
+ parentTaskId: taskId,
165
+ createdBy,
166
+ });
167
+
168
+ const output = `${summary}\n\nDeferred until ${nextRunAt} (schedule ${schedule.id}). Pending: ${note}${checksBlock}`;
169
+
170
+ // Deliberately NOT running `getTaskOutputValidationError` again here:
171
+ // already validated above against a placeholder id; re-run would be
172
+ // redundant since the id never affects JSON-shape validity.
173
+ const completed = await completeTask(taskId, output);
174
+ if (!completed) {
175
+ // Another writer terminally completed/failed/cancelled this task
176
+ // between our early check and this transaction's write. Abort:
177
+ // rolling back here discards the schedule INSERT so no orphan
178
+ // wake-up is committed, and no terminal effects fire for a
179
+ // completion that didn't happen on this call.
180
+ throw new DeferAbortedError(
181
+ `Task ${taskId} reached a terminal state before this deferral committed.`,
182
+ );
183
+ }
184
+
185
+ // afterCommit: the transaction can still roll back; business-use must
186
+ // not be told the task completed for a write that never landed.
187
+ getDbClient().afterCommit(() => {
188
+ ensure({
189
+ id: "completed",
190
+ flow: "task",
191
+ runId: taskId,
192
+ depIds: task.wasPaused ? ["started", "resumed"] : ["started"],
193
+ data: {
194
+ taskId,
195
+ agentId: task.agentId,
196
+ previousStatus: task.status,
197
+ hasOutput: true,
198
+ },
199
+ validator: (data) => data.previousStatus === "in_progress",
200
+ // biome-ignore lint/correctness/noEmptyPattern: data unused, ctx needed
201
+ filter: ({}, ctx) => ctx.deps.length > 0,
202
+ conditions: [{ timeout_ms: 3_600_000 }], // 1 hour
203
+ });
204
+ });
205
+
206
+ if (task.agentId) {
207
+ await updateAgentStatusFromCapacity(task.agentId);
208
+ }
209
+
210
+ return { scheduleId: schedule.id, output, completed };
211
+ });
212
+
213
+ await runTaskTerminalEffects({
214
+ task: committed.completed,
215
+ status: "completed",
216
+ output: committed.output,
217
+ agentId: requestInfo.agentId,
218
+ });
219
+
220
+ return toolOk(
221
+ `Task ${taskId} completed and deferred. Wake-up at ${nextRunAt} (schedule ${committed.scheduleId}). This task is final; the wake-up task continues the work.`,
222
+ {
223
+ data: {
224
+ yourAgentId: requestInfo.agentId,
225
+ taskId,
226
+ scheduleId: committed.scheduleId,
227
+ nextRunAt,
228
+ },
229
+ },
230
+ );
231
+ } catch (error) {
232
+ const message = error instanceof Error ? error.message : "Unknown error";
233
+ return toolErr(`Failed to defer task: ${message}`, {
234
+ data: { yourAgentId: requestInfo.agentId, taskId },
235
+ });
236
+ }
237
+ },
238
+ );
239
+ };
@@ -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, swarmToolOutputSchema, toolErr, toolOk } from "@/tools/utils";
5
- import { RepoGuidelinesSchema, RepoHooksSchema } from "@/types";
5
+ import { RepoGuidelinesInputSchema, RepoHooksSchema } from "@/types";
6
6
 
7
7
  const swarmRepoOutputShape = z.looseObject({
8
8
  id: z.string().optional(),
@@ -46,7 +46,7 @@ export const registerUpdateRepoTool = (server: McpServer) => {
46
46
  .describe(
47
47
  "Repository hook install config. Set { enabled: true } to opt into best-effort worker hook installation, or null to disable.",
48
48
  ),
49
- guidelines: RepoGuidelinesSchema.nullable()
49
+ guidelines: RepoGuidelinesInputSchema.nullable()
50
50
  .optional()
51
51
  .describe(
52
52
  "Repository guidelines: prChecks (commands before PR), mergeChecks (conditions before merge), allowMerge (default false), review (guidance for reviewers). Pass null to clear.",
@@ -120,6 +120,7 @@ const scheduleDataShape = {
120
120
  lastRunAt: z.string().optional(),
121
121
  nextRunAt: z.string().optional(),
122
122
  createdByAgentId: z.string().optional(),
123
+ parentTaskId: z.string().optional(),
123
124
  timezone: z.string().optional(),
124
125
  model: z.string().optional(),
125
126
  modelTier: ModelTierSchema.optional(),
@@ -22,6 +22,7 @@ const scheduleRowShape = {
22
22
  lastRunAt: z.string().optional(),
23
23
  nextRunAt: z.string().optional(),
24
24
  createdByAgentId: z.string().optional(),
25
+ parentTaskId: z.string().optional(),
25
26
  timezone: z.string().optional(),
26
27
  consecutiveErrors: z.number().optional(),
27
28
  lastErrorAt: z.string().optional(),
@@ -89,6 +89,7 @@ const scheduleDataShape = {
89
89
  lastRunAt: z.string().optional(),
90
90
  nextRunAt: z.string().optional(),
91
91
  createdByAgentId: z.string().optional(),
92
+ parentTaskId: z.string().optional(),
92
93
  timezone: z.string().optional(),
93
94
  model: z.string().optional(),
94
95
  modelTier: ModelTierSchema.optional(),