@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,93 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { mkdtemp, rm } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { buildPricingSeedRows } from "../be/seed-pricing";
6
+ import { CREDENTIAL_PROVIDER_CHECKERS } from "../commands/provider-credentials";
7
+ import { ProviderNameSchema } from "../types";
8
+
9
+ const entrypoint = await Bun.file(new URL("../../docker-entrypoint.sh", import.meta.url)).text();
10
+
11
+ const bootstrapStart = 'if [ "$HARNESS_PROVIDER" = "pi" ]; then';
12
+ const bootstrapEnd = "# ---- Verify provider binary is reachable ----";
13
+ const bootstrapBlock = entrypoint.slice(
14
+ entrypoint.indexOf(bootstrapStart),
15
+ entrypoint.indexOf(bootstrapEnd),
16
+ );
17
+ const codexHomeAssignment = 'WORKER_CODEX_HOME="/home/worker/.codex"';
18
+ const isolatedBootstrapBlock = bootstrapBlock.replace(
19
+ codexHomeAssignment,
20
+ 'WORKER_CODEX_HOME="$TEST_WORKER_CODEX_HOME"',
21
+ );
22
+ if (isolatedBootstrapBlock === bootstrapBlock) {
23
+ throw new Error(`Could not isolate entrypoint assignment: ${codexHomeAssignment}`);
24
+ }
25
+
26
+ const modelsDevFixture = {
27
+ anthropic: { models: { "claude-test": { cost: { input: 1, output: 2 } } } },
28
+ openai: { models: { "gpt-test": { cost: { input: 1, output: 2 } } } },
29
+ openrouter: { models: { "test/model": { cost: { input: 1, output: 2 } } } },
30
+ };
31
+
32
+ const ENTRYPOINT_EXEMPTIONS = new Set(["claude", "acp"]);
33
+ const ENTRYPOINT_OUTPUT: Record<string, string> = {
34
+ pi: "Warning: pi provider has no credentials yet",
35
+ opencode: "Warning: opencode provider has no credentials yet",
36
+ "claude-managed": "Warning: claude-managed provider missing:",
37
+ devin: "Warning: devin provider missing DEVIN_API_KEY / DEVIN_ORG_ID",
38
+ codex: "Warning: codex provider has no auth.json yet",
39
+ };
40
+
41
+ async function runCredentialBootstrap(provider: string): Promise<{
42
+ exitCode: number;
43
+ stdout: string;
44
+ }> {
45
+ const testRoot = await mkdtemp(join(tmpdir(), "provider-registration-"));
46
+ try {
47
+ const proc = Bun.spawn(["bash", "-c", isolatedBootstrapBlock], {
48
+ env: {
49
+ PATH: process.env.PATH ?? "",
50
+ HOME: join(testRoot, "home"),
51
+ TEST_WORKER_CODEX_HOME: join(testRoot, "codex"),
52
+ HARNESS_PROVIDER: provider,
53
+ API_KEY: "",
54
+ MCP_BASE_URL: "",
55
+ MCP_URL: "",
56
+ },
57
+ stdout: "pipe",
58
+ stderr: "pipe",
59
+ });
60
+ const [exitCode, stdout] = await Promise.all([proc.exited, new Response(proc.stdout).text()]);
61
+ return { exitCode, stdout: stdout.trim() };
62
+ } finally {
63
+ await rm(testRoot, { recursive: true, force: true });
64
+ }
65
+ }
66
+
67
+ describe("provider registration synchronization", () => {
68
+ test("credential dispatcher has a concrete handler for every provider", () => {
69
+ expect(Object.keys(CREDENTIAL_PROVIDER_CHECKERS).sort()).toEqual(
70
+ [...ProviderNameSchema.options].sort(),
71
+ );
72
+ });
73
+
74
+ test("pricing seeder emits rows for every provider except ACP", () => {
75
+ const registered = new Set(buildPricingSeedRows(modelsDevFixture).map((row) => row.provider));
76
+ const expected = ProviderNameSchema.options.filter((provider) => provider !== "acp");
77
+ expect([...registered].filter((provider) => expected.includes(provider)).sort()).toEqual(
78
+ [...expected].sort(),
79
+ );
80
+ });
81
+
82
+ test.skipIf(process.platform === "win32")(
83
+ "entrypoint executes a credential bootstrap for every non-exempt provider",
84
+ async () => {
85
+ for (const provider of ProviderNameSchema.options) {
86
+ if (ENTRYPOINT_EXEMPTIONS.has(provider)) continue;
87
+ const result = await runCredentialBootstrap(provider);
88
+ expect(result.exitCode).toBe(0);
89
+ expect(result.stdout).toContain(ENTRYPOINT_OUTPUT[provider]);
90
+ }
91
+ },
92
+ );
93
+ });
@@ -0,0 +1,53 @@
1
+ import { expect, test } from "bun:test";
2
+ import { closeSync, openSync } from "node:fs";
3
+ import { mkdtemp, rm } from "node:fs/promises";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+ import { type SwarmServer, waitForListen } from "./rbac-e2e-helpers";
7
+
8
+ test("waitForListen includes a bounded server log when the process exits early", async () => {
9
+ const dir = await mkdtemp(join(tmpdir(), "rbac-e2e-helper-"));
10
+ const logPath = join(dir, "server.log");
11
+ const knownLogLine = "known boot failure from the child process";
12
+ const discardedLogPrefix = "content before the bounded tail";
13
+ await Bun.write(logPath, `${discardedLogPrefix}${"x".repeat(8 * 1024)}\n`);
14
+ const logFd = openSync(logPath, "a");
15
+ let logFdOpen = true;
16
+
17
+ try {
18
+ const proc = Bun.spawn(["bun", "-e", `console.error(${JSON.stringify(knownLogLine)})`], {
19
+ stdout: logFd,
20
+ stderr: logFd,
21
+ });
22
+ await proc.exited;
23
+ closeSync(logFd);
24
+ logFdOpen = false;
25
+
26
+ const server: SwarmServer = {
27
+ proc,
28
+ port: 0,
29
+ base: "http://localhost:0",
30
+ dbPath: join(dir, "test.sqlite"),
31
+ logPath,
32
+ async stop() {
33
+ return proc.exitCode;
34
+ },
35
+ };
36
+
37
+ const earlyExitError = await waitForListen(server).catch((error: unknown) => error);
38
+ expect(earlyExitError).toBeInstanceOf(Error);
39
+ expect((earlyExitError as Error).message).toContain(knownLogLine);
40
+ expect((earlyExitError as Error).message).not.toContain(discardedLogPrefix);
41
+
42
+ const missingLogPath = join(dir, "missing.log");
43
+ const missingLogError = await waitForListen({ ...server, logPath: missingLogPath }).catch(
44
+ (error: unknown) => error,
45
+ );
46
+ expect(missingLogError).toBeInstanceOf(Error);
47
+ expect((missingLogError as Error).message).toContain(missingLogPath);
48
+ expect((missingLogError as Error).message).toContain("Could not read server log");
49
+ } finally {
50
+ if (logFdOpen) closeSync(logFd);
51
+ await rm(dir, { recursive: true, force: true });
52
+ }
53
+ });
@@ -22,6 +22,8 @@ export { getFreePort };
22
22
  export const REPO_ROOT = join(import.meta.dir, "../..");
23
23
  export const E2E_API_KEY = "rbac-e2e-key";
24
24
 
25
+ const SERVER_LOG_TAIL_BYTES = 8 * 1024;
26
+
25
27
  export const LEAD = "11111111-1111-4111-8111-111111111111";
26
28
  export const WORKER_A = "22222222-2222-4222-8222-222222222222";
27
29
  export const WORKER_B = "33333333-3333-4333-8333-333333333333";
@@ -115,7 +117,7 @@ export async function waitForListen(server: SwarmServer, deadlineMs = 90_000): P
115
117
  while (Date.now() - start < deadlineMs) {
116
118
  if (server.proc.exitCode !== null) {
117
119
  throw new Error(
118
- `server exited with code ${server.proc.exitCode} before listening — see ${server.logPath}`,
120
+ `server exited with code ${server.proc.exitCode} before listening — see ${server.logPath}${await serverLogTail(server.logPath)}`,
119
121
  );
120
122
  }
121
123
  try {
@@ -126,7 +128,22 @@ export async function waitForListen(server: SwarmServer, deadlineMs = 90_000): P
126
128
  }
127
129
  await Bun.sleep(200);
128
130
  }
129
- throw new Error(`server did not listen within ${deadlineMs}ms — see ${server.logPath}`);
131
+ throw new Error(
132
+ `server did not listen within ${deadlineMs}ms — see ${server.logPath}${await serverLogTail(server.logPath)}`,
133
+ );
134
+ }
135
+
136
+ async function serverLogTail(logPath: string): Promise<string> {
137
+ try {
138
+ const log = Bun.file(logPath);
139
+ if (!(await log.exists())) throw new Error("file does not exist");
140
+ const size = log.size;
141
+ const tail = await log.slice(Math.max(0, size - SERVER_LOG_TAIL_BYTES), size).text();
142
+ return `\n\nLog tail (up to ${SERVER_LOG_TAIL_BYTES} bytes):\n${tail}`;
143
+ } catch (error) {
144
+ const reason = error instanceof Error ? error.message : String(error);
145
+ return `\n\nCould not read server log at ${logPath}: ${reason}`;
146
+ }
130
147
  }
131
148
 
132
149
  // ── HTTP helpers ─────────────────────────────────────────────────────────────
@@ -12,6 +12,7 @@ import {
12
12
  loadGlobalConfigsIntoEnv,
13
13
  scheduleIntegrationsReload,
14
14
  } from "../http/core";
15
+ import { isOriginAllowedForCredentials } from "../http/utils";
15
16
  import { listenOnFreePort } from "./test-net";
16
17
 
17
18
  const TEST_DB_PATH = "./test-reload-config.sqlite";
@@ -199,6 +200,31 @@ describe("reload-config", () => {
199
200
  expect(process.env.API_KEY).toBeUndefined();
200
201
  });
201
202
 
203
+ test("stored CORS bypass cannot enable or override deployment CORS at boot or reload", async () => {
204
+ const key = "CORS_ALLOW_ANY_ORIGIN";
205
+ const original = process.env[key];
206
+ const rowId = await insertLegacyReservedRow(key, "true");
207
+ try {
208
+ for (const override of [false, true]) {
209
+ delete process.env[key];
210
+ expect(await loadGlobalConfigsIntoEnv(override)).not.toContain(key);
211
+ expect(process.env[key]).toBeUndefined();
212
+ expect(isOriginAllowedForCredentials("https://attacker.invalid")).toBe(false);
213
+ process.env[key] = "false";
214
+ await loadGlobalConfigsIntoEnv(override);
215
+ expect(process.env[key]).toBe("false");
216
+ process.env[key] = "true";
217
+ await loadGlobalConfigsIntoEnv(override);
218
+ expect(process.env[key]).toBe("true");
219
+ expect(isOriginAllowedForCredentials("https://attacker.invalid")).toBe(false);
220
+ }
221
+ } finally {
222
+ await deleteSwarmConfig(rowId);
223
+ if (original === undefined) delete process.env[key];
224
+ else process.env[key] = original;
225
+ }
226
+ });
227
+
202
228
  test("loadGlobalConfigsIntoEnv skips unreadable reserved secret rows before decrypting them", async () => {
203
229
  const id = await insertUnreadableReservedSecretRow("SECRETS_ENCRYPTION_KEY");
204
230
 
@@ -40,7 +40,7 @@ const EXPECTED_INVENTORY: InventoryEntry[] = [
40
40
  kind: "consumer",
41
41
  },
42
42
  {
43
- file: "src/be/db.ts",
43
+ file: "src/be/db/tasks/read.ts",
44
44
  owner: "rowToAgentTask",
45
45
  syntax: "ShorthandPropertyAssignment",
46
46
  count: 1,
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, test } from "bun:test";
2
- import { buildAttachmentsSection } from "../commands/runner";
2
+ import { buildAttachmentsSection } from "../commands/attachments-section";
3
3
 
4
4
  describe("buildAttachmentsSection", () => {
5
5
  test("returns empty string when there are no attachments", () => {
@@ -20,7 +20,7 @@ describe("buildAttachmentsSection", () => {
20
20
  expect(section).toContain("IMG_1357.jpeg");
21
21
  expect(section).toContain("image/jpeg, 2816227 bytes");
22
22
  expect(section).toContain(
23
- '$MCP_BASE_URL/api/fs/tasks/task-123/files/att-1/raw" -o /tmp/IMG_1357.jpeg',
23
+ "$MCP_BASE_URL/api/fs/tasks/task-123/files/att-1/raw\" --create-dirs -o '/tmp/attachments/att-1/IMG_1357.jpeg'",
24
24
  );
25
25
  // No org/drive discovery should be required — the whole point of the fix.
26
26
  expect(section).not.toContain("agent-fs");
@@ -41,6 +41,26 @@ describe("buildAttachmentsSection", () => {
41
41
  expect(lines[1]).toContain("b.pdf");
42
42
  });
43
43
 
44
+ test("quotes the output path so names from users can't break the command", () => {
45
+ const section = buildAttachmentsSection("task-123", [
46
+ { id: "att-1", name: "Screenshot 2026-09-11 at 1.55 PM.png" },
47
+ { id: "att-2", name: "it's ../../etc/passwd" },
48
+ ]);
49
+
50
+ expect(section).toContain("-o '/tmp/attachments/att-1/Screenshot 2026-09-11 at 1.55 PM.png'");
51
+ expect(section).toContain("-o '/tmp/attachments/att-2/it'\\''s .._.._etc_passwd'");
52
+ });
53
+
54
+ test("gives every attachment its own download path, even when names repeat", () => {
55
+ const section = buildAttachmentsSection("task-123", [
56
+ { id: "att-1", name: "image.png" },
57
+ { id: "att-2", name: "image.png" },
58
+ ]);
59
+
60
+ expect(section).toContain("-o '/tmp/attachments/att-1/image.png'");
61
+ expect(section).toContain("-o '/tmp/attachments/att-2/image.png'");
62
+ });
63
+
44
64
  test("skips malformed attachment entries without throwing", () => {
45
65
  const section = buildAttachmentsSection("task-123", [
46
66
  { id: "att-1" }, // missing name — id used as fallback name
@@ -5,6 +5,7 @@ import {
5
5
  CONTEXT_PREAMBLE_MAX_CHARS,
6
6
  CONTEXT_PREAMBLE_MAX_TOKENS,
7
7
  fetchTaskContextForPreamble,
8
+ prependContextPreamble,
8
9
  type TaskContextForPreamble,
9
10
  } from "../commands/context-preamble";
10
11
  import { listenOnFreePort } from "./test-net";
@@ -51,6 +52,35 @@ function seedTask(task: TaskContextForPreamble): void {
51
52
  mockTasks[task.id] = task;
52
53
  }
53
54
 
55
+ describe("prependContextPreamble", () => {
56
+ const preamble = "\n---\n## Prior Conversation Context\nParent task evidence\n\n";
57
+
58
+ test.each([
59
+ "/work-on-task",
60
+ "/skill:work-on-task",
61
+ "/review-offered-task",
62
+ ])("preserves the leading %s invocation and the full task body", (command) => {
63
+ const prompt = `${command} task-id\n\nTask: follow up\nAttachments: artifact\n`;
64
+ expect(prependContextPreamble(prompt, preamble)).toBe(
65
+ `${command} task-id\n${preamble}\nTask: follow up\nAttachments: artifact\n`,
66
+ );
67
+ });
68
+
69
+ test("preserves a command-only prompt and leaves empty context unchanged", () => {
70
+ expect(prependContextPreamble("/work-on-task task-id", preamble)).toBe(
71
+ `/work-on-task task-id\n${preamble}`,
72
+ );
73
+ expect(prependContextPreamble("/work-on-task task-id\n\nTask", "")).toBe(
74
+ "/work-on-task task-id\n\nTask",
75
+ );
76
+ });
77
+
78
+ test("prepends context normally when there is no leading skill", () => {
79
+ const prompt = "Continue the task\n/work-on-task is quoted here";
80
+ expect(prependContextPreamble(prompt, preamble)).toBe(preamble + prompt);
81
+ });
82
+ });
83
+
54
84
  describe("fetchTaskContextForPreamble", () => {
55
85
  test("returns null on 404", async () => {
56
86
  const result = await fetchTaskContextForPreamble(API_URL, API_KEY, "missing-id");
@@ -0,0 +1,107 @@
1
+ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
2
+ import { rm } from "node:fs/promises";
3
+ import { createServer, type Server } from "node:http";
4
+ import {
5
+ closeDb,
6
+ createAgent,
7
+ createTaskExtended,
8
+ getDbClient,
9
+ initDb,
10
+ insertTaskAttachment,
11
+ } from "../be/db";
12
+ import { buildResumePrompt, getPausedTasksFromAPI } from "../commands/runner";
13
+ import { handleTasks } from "../http/tasks";
14
+ import { getPathSegments, parseQueryParams } from "../http/utils";
15
+ import { setRequestAuth } from "../utils/request-auth-context";
16
+ import { listenOnFreePort } from "./test-net";
17
+
18
+ const dbPath = `/tmp/test-deployment-resume-${crypto.randomUUID()}.sqlite`;
19
+ let server: Server;
20
+ let apiUrl: string;
21
+ let agentId: string;
22
+ const outputSchema = {
23
+ type: "object",
24
+ required: ["answer"],
25
+ properties: { answer: { type: "string" } },
26
+ };
27
+
28
+ beforeAll(async () => {
29
+ initDb(dbPath);
30
+ agentId = (await createAgent({ name: "resume-worker", isLead: false, status: "idle" })).id;
31
+ server = createServer(async (req, res) => {
32
+ setRequestAuth(req, { kind: "operator", fingerprint: "resume-test" });
33
+ if (
34
+ await handleTasks(
35
+ req,
36
+ res,
37
+ getPathSegments(req.url ?? ""),
38
+ parseQueryParams(req.url ?? ""),
39
+ req.headers["x-agent-id"] as string | undefined,
40
+ )
41
+ )
42
+ return;
43
+ res.writeHead(404).end();
44
+ });
45
+ const port = await listenOnFreePort(server);
46
+ apiUrl = `http://127.0.0.1:${port}`;
47
+ });
48
+
49
+ afterAll(async () => {
50
+ if (server) await new Promise<void>((resolve) => server.close(() => resolve()));
51
+ closeDb();
52
+ for (const suffix of ["", "-wal", "-shm"]) await rm(dbPath + suffix, { force: true });
53
+ });
54
+
55
+ describe("deployment resume context", () => {
56
+ for (const progress of [undefined, "Read the first page"]) {
57
+ test(`paused task retains its attachment and output schema (${progress ? "with" : "without"} progress)`, async () => {
58
+ const task = await createTaskExtended("Summarize the attached input", {
59
+ agentId,
60
+ outputSchema,
61
+ });
62
+ await getDbClient().run(
63
+ "UPDATE agent_tasks SET status = 'paused', progress = ? WHERE id = ?",
64
+ [progress ?? null, task.id],
65
+ );
66
+ const attachment = await insertTaskAttachment({
67
+ taskId: task.id,
68
+ kind: "url",
69
+ name: "input.txt",
70
+ url: "https://example.com/input.txt",
71
+ mimeType: "text/plain",
72
+ sizeBytes: 42,
73
+ });
74
+ const tasks = await getPausedTasksFromAPI({ apiUrl, apiKey: "test-key", agentId });
75
+ const paused = tasks.find((row) => row.id === task.id);
76
+ expect(paused).toBeDefined();
77
+ if (!paused) throw new Error("Paused task missing");
78
+ expect(paused.outputSchema).toEqual(outputSchema);
79
+ expect(paused.attachments).toContainEqual(
80
+ expect.objectContaining({ id: attachment.id, name: "input.txt" }),
81
+ );
82
+ const prompt = await buildResumePrompt(paused);
83
+ expect(prompt).toContain(`/work-on-task ${task.id}`);
84
+ expect(prompt).toContain(`/api/fs/tasks/${task.id}/files/${attachment.id}/raw`);
85
+ expect(prompt).toContain("input.txt");
86
+ expect(prompt).toContain("**Required Output Format**");
87
+ expect(prompt).toContain(JSON.stringify(outputSchema, null, 2));
88
+ expect(prompt).toContain(progress ?? "No progress was saved");
89
+ });
90
+ }
91
+
92
+ test("legacy tasks without a schema retain the generic completion instruction", async () => {
93
+ const prompt = await buildResumePrompt({ id: "task", task: "Continue" });
94
+ expect(prompt).toContain('status: "completed"');
95
+ expect(prompt).not.toContain("Required Output Format");
96
+ });
97
+
98
+ test("providers without MCP omit the MCP output instruction", async () => {
99
+ const prompt = await buildResumePrompt(
100
+ { id: "task", task: "Continue", outputSchema },
101
+ undefined,
102
+ { hasMcp: false },
103
+ );
104
+ expect(prompt).not.toContain("store-progress");
105
+ expect(prompt).not.toContain("/work-on-task");
106
+ });
107
+ });
@@ -248,6 +248,18 @@ describe("isFirstKickoffTask", () => {
248
248
  expect(await isFirstKickoffTask({ taskType: "resume" })).toBe(false);
249
249
  });
250
250
 
251
+ test("a deferred wake-up task is never a first kickoff", async () => {
252
+ // The `defer-task` parent is already `completed`, so the parent-status
253
+ // check alone would clear it for a destructive hard reset.
254
+ expect(await isFirstKickoffTask({ taskType: "deferred" })).toBe(false);
255
+ expect(
256
+ await isFirstKickoffTask(
257
+ { taskType: "deferred", parentTaskId: "parent-1" },
258
+ async () => "completed",
259
+ ),
260
+ ).toBe(false);
261
+ });
262
+
251
263
  test("a brand-new top-level task with no fields at all is a first kickoff", async () => {
252
264
  expect(await isFirstKickoffTask({})).toBe(true);
253
265
  });
@@ -562,6 +562,10 @@ describe("script connections", () => {
562
562
  markMigrationApplied(database, "142_workflow_automation_preflight.sql");
563
563
  // 143 backfills pricing, which this migration-112-only fixture does not create.
564
564
  markMigrationApplied(database, "143_backfill_gpt_6_astra_pricing.sql");
565
+ // 147 alters scheduled_tasks, which this migration-112-only fixture does not create.
566
+ markMigrationApplied(database, "147_scheduled_tasks_parent_task.sql");
567
+ // 148 rebuilds pages, which this migration-112-only fixture does not create.
568
+ markMigrationApplied(database, "148_pages_svg.sql");
565
569
 
566
570
  const id = crypto.randomUUID();
567
571
  const now = new Date().toISOString();
@@ -55,8 +55,8 @@ afterAll(async () => {
55
55
  });
56
56
 
57
57
  describe("seed-scripts catalog", () => {
58
- test("manifest holds 26 unique, well-described scripts", () => {
59
- expect(SEED_SCRIPTS.length).toBe(26);
58
+ test("manifest holds 27 unique, well-described scripts", () => {
59
+ expect(SEED_SCRIPTS.length).toBe(27);
60
60
  const names = SEED_SCRIPTS.map((s) => s.name);
61
61
  expect(new Set(names).size).toBe(names.length);
62
62
  for (const s of SEED_SCRIPTS) {