@desplega.ai/agent-swarm 1.131.1 → 1.133.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (250) hide show
  1. package/README.md +2 -2
  2. package/dist/{actions-jgcy477k.js → actions-cskpayd7.js} +11 -9
  3. package/dist/{anthropic-messages-rjmd2e7n.js → anthropic-messages-tpxtbx7b.js} +28 -12
  4. package/dist/{app-5da78bd4.js → app-ang77nbb.js} +8 -6
  5. package/dist/{assistant-f2vkk1hj.js → assistant-b4exebdw.js} +16 -13
  6. package/dist/{azure-openai-responses-c0yjmbfd.js → azure-openai-responses-m3921t4y.js} +8 -8
  7. package/dist/{boot-reembed-q7mp8hcr.js → boot-reembed-ps2aftpb.js} +9 -7
  8. package/dist/{boot-reembed-xmw8wcx6.js → boot-reembed-znyk3nxb.js} +10 -8
  9. package/dist/{boot-scrub-logs-dsqrv79n.js → boot-scrub-logs-2ax3xgz1.js} +7 -5
  10. package/dist/{claude-adapter-q6x7yspx.js → claude-adapter-3ct8kgxw.js} +13 -9
  11. package/dist/{claude-managed-adapter-wsg37bt8.js → claude-managed-adapter-akx4ntf2.js} +2 -2
  12. package/dist/{cli-114fvtmj.js → cli-13w2hwkh.js} +1 -1
  13. package/dist/{cli-4rkfg1wx.js → cli-1532szb1.js} +8 -6
  14. package/dist/{cli-g74vpjp2.js → cli-23d3rseb.js} +33 -17
  15. package/dist/{cli-3ty0t082.js → cli-2a75z9c5.js} +196 -7
  16. package/dist/{cli-pxfxkx5m.js → cli-2t30rj0d.js} +1 -1
  17. package/dist/{cli-rgrfzg8z.js → cli-3c3tdgab.js} +10 -10
  18. package/dist/{cli-6dp3ydxa.js → cli-3zzxa9jz.js} +15 -11
  19. package/dist/{cli-2f07m0xr.js → cli-4k6233gc.js} +1 -1
  20. package/dist/{cli-t8d349cy.js → cli-5p2tz05r.js} +2 -2
  21. package/dist/{cli-w2wxd56v.js → cli-5z2yhzvd.js} +127 -49
  22. package/dist/{cli-kd1ck4rx.js → cli-6zfq31nj.js} +9 -3
  23. package/dist/{cli-er3a4bb0.js → cli-8m9de10a.js} +6 -1
  24. package/dist/{cli-gm9cbhbq.js → cli-8t14vg9w.js} +27 -8
  25. package/dist/{cli-ccf9vy7q.js → cli-9nfc2es8.js} +6 -6
  26. package/dist/{cli-ycrnynpw.js → cli-9ta3pwms.js} +11 -4
  27. package/dist/{cli-7dm2zqy2.js → cli-avr0ns7z.js} +2 -2
  28. package/dist/{cli-n6h8qt8n.js → cli-c897wyhw.js} +12 -7
  29. package/dist/{cli-gecy3z2s.js → cli-dgk20ft7.js} +4 -4
  30. package/dist/cli-et1bbwqf.js +13 -0
  31. package/dist/{cli-bqw0mt1n.js → cli-f5w27pj3.js} +3 -3
  32. package/dist/{cli-2wadx7p2.js → cli-f8dre4bz.js} +2 -2
  33. package/dist/{cli-6pcz9h15.js → cli-ftwpv451.js} +170 -387
  34. package/dist/{cli-nrkkknat.js → cli-g854s2dw.js} +1 -1
  35. package/dist/{cli-793jvbsh.js → cli-ggmdxeqa.js} +13 -8
  36. package/dist/cli-hkwfe3qj.js +530 -0
  37. package/dist/{cli-5dsbcjjw.js → cli-jqwnqx9m.js} +1 -1
  38. package/dist/{cli-9dr5qzc0.js → cli-ny122vh4.js} +21 -10
  39. package/dist/{cli-sy7jk3tx.js → cli-pvzqhbs9.js} +2 -2
  40. package/dist/cli-pzq5rjym.js +15 -0
  41. package/dist/{cli-xpxde7rm.js → cli-qes8ef67.js} +48 -30
  42. package/dist/{cli-sajgpkqg.js → cli-qs9hhsvb.js} +1723 -325
  43. package/dist/cli-qys3rhhz.js +27 -0
  44. package/dist/{cli-se2p6rkb.js → cli-r6tpm37h.js} +2 -2
  45. package/dist/{cli-g1re8r05.js → cli-rch80c76.js} +2 -2
  46. package/dist/{cli-0258203w.js → cli-rpp0x4zy.js} +137 -17
  47. package/dist/{cli-5n6e5yfn.js → cli-s1k0wxhe.js} +12 -7
  48. package/dist/{cli-aca5pz9f.js → cli-sfd9v7sj.js} +1 -1
  49. package/dist/{cli-2rk0cx7s.js → cli-sh3ytsyj.js} +49 -39
  50. package/dist/{cli-zsspvvm2.js → cli-w1mp9jy7.js} +1 -1
  51. package/dist/{cli-6dpytdr2.js → cli-xdb4mpnp.js} +2 -2
  52. package/dist/{cli-57khth04.js → cli-xmyqv00z.js} +120 -4
  53. package/dist/{cli-jbcva0xj.js → cli-xv8t8p3c.js} +56 -31
  54. package/dist/{cli-f1am9szv.js → cli-xxv5pn4f.js} +1 -1
  55. package/dist/{cli-jevgrb4w.js → cli-y08jvxzx.js} +601 -140
  56. package/dist/{cli-50vbt00w.js → cli-zfbf3srj.js} +650 -245
  57. package/dist/{cli-ygrbmjja.js → cli-zmq89k34.js} +6 -6
  58. package/dist/cli.js +22 -22
  59. package/dist/{codex-adapter-htjkn22b.js → codex-adapter-bef93pq4.js} +9 -7
  60. package/dist/{codex-hook-hq54t9ft.js → codex-hook-e8dba65p.js} +3 -2
  61. package/dist/{codex-session-runner-wjfc7smz.js → codex-session-runner-5pkxys7x.js} +9 -7
  62. package/dist/{commands-xhrfwqsk.js → commands-e23s1bff.js} +9 -7
  63. package/dist/{db-anvwb22f.js → db-m3nabatn.js} +17 -5
  64. package/dist/{google-generative-ai-hysrd8qx.js → google-generative-ai-8maw55xh.js} +11 -8
  65. package/dist/{google-vertex-p5arrga0.js → google-vertex-8vjqmx8g.js} +11 -8
  66. package/dist/{handlers-b9kbc621.js → handlers-akazedvr.js} +16 -13
  67. package/dist/{hook-xkvn6c5h.js → hook-ze4cpf0v.js} +55 -41
  68. package/dist/{hook-cgk4wxmx.js → hook-zfqyyenw.js} +10 -7
  69. package/dist/{http-9xh6ta4r.js → http-sja0n6kw.js} +608 -147
  70. package/dist/index-0gr8vd6k.js +39 -0
  71. package/dist/{index-eaxyqcmj.js → index-354vkhdv.js} +154 -74
  72. package/dist/{index-070v4eyb.js → index-5qthek5t.js} +2 -3
  73. package/dist/{index-25wjsdv2.js → index-95t3cqq5.js} +16 -14
  74. package/dist/{index-9znhar3x.js → index-sea66619.js} +3 -4
  75. package/dist/{index-ywds4812.js → index-vpn36mma.js} +15 -13
  76. package/dist/{keepalive-vc7s2aa6.js → keepalive-3sp3meqj.js} +16 -10
  77. package/dist/{lead-r2yqfmx7.js → lead-mefeewkf.js} +39 -36
  78. package/dist/{maintenance-mw70k468.js → maintenance-8tbrbjfg.js} +10 -8
  79. package/dist/mistral-conversations-00zrp7g4.js +717 -0
  80. package/dist/{oauth-refresh-sweep-yecgjpk4.js → oauth-refresh-sweep-srjwjz6m.js} +8 -6
  81. package/dist/{onboard-xj0f73f9.js → onboard-mt2tgyjg.js} +7 -7
  82. package/dist/{openai-codex-responses-5g27pd6f.js → openai-codex-responses-map32h2s.js} +18 -18
  83. package/dist/{openai-completions-3vkmfz2a.js → openai-completions-w871b1cq.js} +12 -11
  84. package/dist/{openai-responses-dg4t7j7a.js → openai-responses-b0j7xnw8.js} +15 -12
  85. package/dist/{opencode-adapter-1gm38aba.js → opencode-adapter-b39jefcj.js} +5 -3
  86. package/dist/{openrouter-images-g3gm8931.js → openrouter-images-srfkywy0.js} +4 -4
  87. package/dist/{otel-impl-nxg0n0b3.js → otel-impl-z6pxt3j9.js} +224 -9
  88. package/dist/{pi-mono-adapter-4g5kby31.js → pi-mono-adapter-btvqmj58.js} +1138 -408
  89. package/dist/{pricing-refresh-2cn6effr.js → pricing-refresh-kyq8cezh.js} +9 -7
  90. package/dist/{rbac-roles-1hcxdtg7.js → rbac-roles-3ryfrfna.js} +8 -6
  91. package/dist/{rbac-roles-vynday5g.js → rbac-roles-6qfvbee1.js} +9 -7
  92. package/dist/{render-v2-tc0bh8kk.js → render-v2-p61ppjey.js} +8 -6
  93. package/dist/{seed-pricing-8kceb43f.js → seed-pricing-j56bbj4y.js} +8 -6
  94. package/dist/{setup-46xtgq3c.js → setup-tpr6c1kn.js} +2 -2
  95. package/dist/{worker-qrbz6hmv.js → worker-9w1v0mbb.js} +39 -36
  96. package/openapi.json +542 -16
  97. package/package.json +6 -6
  98. package/src/be/budget-refusal-notify.ts +14 -7
  99. package/src/be/db-queries/task-artifacts.ts +182 -0
  100. package/src/be/db.ts +783 -169
  101. package/src/be/events.ts +5 -0
  102. package/src/be/migrations/131_session_logs_created_at_index.sql +2 -0
  103. package/src/be/migrations/132_multi_runtime_instances.sql +44 -0
  104. package/src/be/migrations/133_workflow_run_trigger_type.sql +12 -0
  105. package/src/be/migrations/135_backfill_task_pull_request_attachments.sql +228 -0
  106. package/src/be/migrations/136_task_requester_provenance.sql +37 -0
  107. package/src/be/migrations/137_memory_retrieval_composite_index.sql +1 -0
  108. package/src/be/multi-runtime.ts +430 -0
  109. package/src/be/scripts/extract-schema.ts +3 -1
  110. package/src/be/scripts/typecheck.ts +3 -2
  111. package/src/be/seed-skills/index.ts +7 -0
  112. package/src/be/steering.ts +1 -1
  113. package/src/be/swarm-config-guard.ts +18 -0
  114. package/src/commands/credential-wait.ts +54 -0
  115. package/src/commands/onboard/steps/core-credentials.tsx +1 -1
  116. package/src/commands/onboard/steps/custom-templates.tsx +1 -1
  117. package/src/commands/onboard/steps/harness-credentials.tsx +1 -1
  118. package/src/commands/onboard/steps/health-check.tsx +1 -1
  119. package/src/commands/onboard/steps/start.tsx +1 -1
  120. package/src/commands/profile-sync-audit.ts +56 -0
  121. package/src/commands/profile-sync.ts +287 -15
  122. package/src/commands/provider-credentials.ts +46 -19
  123. package/src/commands/runner.ts +196 -33
  124. package/src/commands/templates.ts +22 -0
  125. package/src/github/handlers.ts +55 -9
  126. package/src/heartbeat/heartbeat.ts +71 -12
  127. package/src/hooks/hook.ts +49 -33
  128. package/src/http/active-sessions.ts +11 -1
  129. package/src/http/agents.ts +261 -30
  130. package/src/http/all-routes.ts +1 -0
  131. package/src/http/config.ts +13 -2
  132. package/src/http/core.ts +102 -4
  133. package/src/http/db-query-bounded.ts +313 -0
  134. package/src/http/db-query-shared.ts +249 -0
  135. package/src/http/db-query.ts +54 -42
  136. package/src/http/events.ts +2 -0
  137. package/src/http/index.ts +22 -2
  138. package/src/http/mcp-bridge.ts +37 -3
  139. package/src/http/mcp.ts +12 -0
  140. package/src/http/memory.ts +13 -2
  141. package/src/http/openapi.ts +1 -0
  142. package/src/http/poll.ts +70 -27
  143. package/src/http/route-def.ts +43 -9
  144. package/src/http/schedules.ts +2 -2
  145. package/src/http/scripts.ts +10 -1
  146. package/src/http/session-data.ts +44 -0
  147. package/src/http/tasks.ts +2 -2
  148. package/src/http/users.ts +74 -52
  149. package/src/http/webhooks.ts +5 -2
  150. package/src/http/workflows.ts +1 -4
  151. package/src/integrations/kapso/inbound.ts +1 -1
  152. package/src/jira/outbound.ts +39 -8
  153. package/src/jira/webhook-lifecycle.ts +13 -2
  154. package/src/linear/outbound.ts +43 -10
  155. package/src/oauth/keepalive.ts +13 -2
  156. package/src/prompts/base-prompt.ts +1 -1
  157. package/src/prompts/session-templates.ts +45 -7
  158. package/src/providers/claude-adapter.ts +6 -0
  159. package/src/providers/codex-adapter.ts +6 -0
  160. package/src/providers/opencode-adapter.ts +6 -0
  161. package/src/providers/pi-mono-adapter.ts +8 -0
  162. package/src/providers/pricing-sources.md +24 -0
  163. package/src/queue-stall-alarm.ts +216 -0
  164. package/src/scheduler/scheduler.ts +7 -0
  165. package/src/script-workflows/executor.ts +2 -1
  166. package/src/scripts-runtime/executors/native.ts +2 -1
  167. package/src/scripts-runtime/executors/types.ts +5 -0
  168. package/src/scripts-runtime/loader.ts +5 -0
  169. package/src/scripts-runtime/swarm-config.ts +8 -0
  170. package/src/scripts-runtime/swarm-sdk.ts +7 -0
  171. package/src/scripts-runtime/types/stdlib.d.ts +1 -0
  172. package/src/scripts-runtime/types/swarm-sdk.d.ts +1 -0
  173. package/src/tests/budget-claim-gate.test.ts +20 -0
  174. package/src/tests/claude-adapter.test.ts +32 -0
  175. package/src/tests/codex-adapter.test.ts +30 -0
  176. package/src/tests/create-task-validation.test.ts +128 -0
  177. package/src/tests/credential-wait.test.ts +67 -1
  178. package/src/tests/db-query.test.ts +711 -2
  179. package/src/tests/http-api-integration.test.ts +81 -0
  180. package/src/tests/http-users.test.ts +166 -1
  181. package/src/tests/identity-field-budget.test.ts +176 -0
  182. package/src/tests/mcp-tools-user.test.ts +34 -0
  183. package/src/tests/mcp-user-route.test.ts +17 -0
  184. package/src/tests/metrics-http.test.ts +44 -0
  185. package/src/tests/multi-runtime-registration.test.ts +2703 -0
  186. package/src/tests/openapi-response-contract.test.ts +24 -0
  187. package/src/tests/openapi-route-headers.test.ts +83 -0
  188. package/src/tests/opencode-adapter.test.ts +40 -0
  189. package/src/tests/outbound-listener-rejection.test.ts +138 -0
  190. package/src/tests/pi-mono-adapter.test.ts +98 -0
  191. package/src/tests/profile-sync-warning.test.ts +277 -0
  192. package/src/tests/profile-sync.test.ts +232 -6
  193. package/src/tests/prompt-template-session.test.ts +32 -3
  194. package/src/tests/queue-stall-alarm.test.ts +226 -0
  195. package/src/tests/requester-comms.test.ts +32 -0
  196. package/src/tests/runner-requester-profile.test.ts +38 -0
  197. package/src/tests/schedule-target-type.test.ts +38 -0
  198. package/src/tests/script-connections.test.ts +13 -1
  199. package/src/tests/scripts-runtime-identity.test.ts +375 -0
  200. package/src/tests/scripts-runtime.test.ts +4 -3
  201. package/src/tests/sdk-allowlist.test.ts +47 -0
  202. package/src/tests/send-task-requested-by.test.ts +9 -0
  203. package/src/tests/session-costs.test.ts +506 -1
  204. package/src/tests/shutdown-error-scrubbing.test.ts +92 -0
  205. package/src/tests/steering-core.test.ts +28 -0
  206. package/src/tests/task-artifacts.test.ts +386 -0
  207. package/src/tests/task-attachments-schema.test.ts +17 -1
  208. package/src/tests/task-completion-idempotency.test.ts +57 -0
  209. package/src/tests/task-pull-request-backfill-migration.test.ts +321 -0
  210. package/src/tests/update-profile-api.test.ts +67 -39
  211. package/src/tests/update-profile-auth.test.ts +48 -4
  212. package/src/tests/user-attribution-migration.test.ts +54 -0
  213. package/src/tests/user-identity.test.ts +72 -0
  214. package/src/tests/workflow-async-v2.test.ts +39 -1
  215. package/src/tests/workflow-http-v2.test.ts +3 -2
  216. package/src/tests/workflow-mcp-trigger-schema.test.ts +52 -3
  217. package/src/tests/workflow-retry-v2.test.ts +45 -1
  218. package/src/tools/db-query.ts +18 -8
  219. package/src/tools/get-task-details.ts +24 -3
  220. package/src/tools/manage-user.ts +36 -3
  221. package/src/tools/poll-task.ts +93 -14
  222. package/src/tools/script-common.ts +5 -0
  223. package/src/tools/send-task.ts +1 -4
  224. package/src/tools/store-progress.ts +13 -2
  225. package/src/tools/swarm-config/delete-config.ts +9 -0
  226. package/src/tools/swarm-config/set-config.ts +3 -2
  227. package/src/tools/task-action.ts +30 -0
  228. package/src/tools/task-tool-ctx.ts +8 -1
  229. package/src/tools/update-profile.ts +16 -21
  230. package/src/tools/utils.ts +5 -0
  231. package/src/tools/workflows/trigger-workflow.ts +7 -1
  232. package/src/types.ts +165 -1
  233. package/src/utils/github-pull-request.ts +76 -0
  234. package/src/utils/identity-field-budget.ts +73 -0
  235. package/src/utils/multi-runtime.ts +20 -0
  236. package/src/utils/requester-comms.ts +24 -0
  237. package/src/workflows/engine.ts +22 -2
  238. package/src/workflows/resume.ts +19 -11
  239. package/src/workflows/retry-poller.ts +1 -0
  240. package/src/workflows/triggers.ts +4 -0
  241. package/templates/skills/db-query-guidance/config.json +14 -0
  242. package/templates/skills/db-query-guidance/content.md +57 -0
  243. package/dist/cli-3te4jdkm.js +0 -225
  244. package/dist/cli-bmze986d.js +0 -7
  245. package/dist/cli-kh3ba2zf.js +0 -607
  246. package/dist/index-vg4snay4.js +0 -36
  247. package/dist/mistral-conversations-81fwav0j.js +0 -30578
  248. package/dist/otel-nqa12wf3.js +0 -3149
  249. package/dist/{cli-wz9115x0.js → cli-zr9q011v.js} +6 -6
  250. package/dist/{pi-messages-tqj8q6b9.js → pi-messages-sssy32x4.js} +3 -3
@@ -0,0 +1,386 @@
1
+ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
2
+ import {
3
+ closeDb,
4
+ completeTask,
5
+ createAgent,
6
+ createTaskExtended,
7
+ getDb,
8
+ getTaskAttachments,
9
+ initDb,
10
+ insertTaskAttachment,
11
+ startTask,
12
+ updateTaskVcs,
13
+ } from "../be/db";
14
+ import { getTaskShippingEvidence, taskShippingEvidenceSql } from "../be/db-queries/task-artifacts";
15
+ import { extractGitHubPullRequestUrls } from "../utils/github-pull-request";
16
+
17
+ const TEST_DB_PATH = "./test-task-artifacts.sqlite";
18
+ let agentId: string;
19
+
20
+ const ECMASCRIPT_WHITESPACE_CODE_POINTS = [
21
+ 9, 10, 11, 12, 13, 32, 160, 5760, 8192, 8193, 8194, 8195, 8196, 8197, 8198, 8199, 8200, 8201,
22
+ 8202, 8232, 8233, 8239, 8287, 12288, 65279,
23
+ ];
24
+
25
+ beforeAll(async () => {
26
+ for (const suffix of ["", "-wal", "-shm"]) {
27
+ try {
28
+ await Bun.file(`${TEST_DB_PATH}${suffix}`).delete();
29
+ } catch {}
30
+ }
31
+ initDb(TEST_DB_PATH);
32
+ agentId = createAgent({ name: "Task artifact test agent", isLead: false, status: "idle" }).id;
33
+ });
34
+
35
+ afterAll(async () => {
36
+ closeDb();
37
+ for (const suffix of ["", "-wal", "-shm"]) {
38
+ try {
39
+ await Bun.file(`${TEST_DB_PATH}${suffix}`).delete();
40
+ } catch {}
41
+ }
42
+ });
43
+
44
+ function inProgressTask(name: string) {
45
+ const task = createTaskExtended(name, { agentId, source: "api" });
46
+ startTask(task.id);
47
+ return task;
48
+ }
49
+
50
+ describe("GitHub pull-request extraction", () => {
51
+ test("canonicalizes, deduplicates, and strips URL suffixes", () => {
52
+ expect(
53
+ extractGitHubPullRequestUrls(
54
+ "See https://github.com/Owner/repo.name/pull/42/files and " +
55
+ "https://github.com/Owner/repo.name/pull/42#discussion plus " +
56
+ "github.com/other/repo/pull/7).",
57
+ ),
58
+ ).toEqual([
59
+ {
60
+ url: "https://github.com/Owner/repo.name/pull/42",
61
+ owner: "Owner",
62
+ repo: "repo.name",
63
+ number: 42,
64
+ },
65
+ {
66
+ url: "https://github.com/other/repo/pull/7",
67
+ owner: "other",
68
+ repo: "repo",
69
+ number: 7,
70
+ },
71
+ ]);
72
+ expect(extractGitHubPullRequestUrls("https://notgithub.com/o/r/pull/8")).toEqual([]);
73
+ });
74
+
75
+ test("requires GitHub to be the host or a bare token", () => {
76
+ for (const fixture of [
77
+ "https://evil.example/?next=github.com/o/r/pull/8",
78
+ "xhttps://github.com/o/r/pull/1",
79
+ "https://evil.example/github.com/o/r/pull/8",
80
+ "https://github.com/o/r/pull/123.foo",
81
+ "https://github.com/o/r/pull/123,abc",
82
+ "https://github.com/o/./pull/1",
83
+ "https://github.com/../r/pull/1",
84
+ ]) {
85
+ expect(extractGitHubPullRequestUrls(fixture)).toEqual([]);
86
+ }
87
+ for (const fixture of [
88
+ "https://github.com/o/r/pull/8/files",
89
+ "github.com/o/r/pull/8",
90
+ "Shipped https://github.com/o/r/pull/8, with follow-up prose.",
91
+ ]) {
92
+ expect(extractGitHubPullRequestUrls(fixture)).toEqual([
93
+ { url: "https://github.com/o/r/pull/8", owner: "o", repo: "r", number: 8 },
94
+ ]);
95
+ }
96
+ });
97
+ });
98
+
99
+ describe("automatic task pull-request attachments", () => {
100
+ test("records every PR from successful task completion", () => {
101
+ const task = inProgressTask("completion records PRs");
102
+ completeTask(
103
+ task.id,
104
+ "Shipped https://github.com/desplega-ai/agent-swarm/pull/1200 and " +
105
+ "https://github.com/desplega-ai/docs/pull/81).",
106
+ );
107
+
108
+ expect(
109
+ getTaskAttachments(task.id).map((attachment) => ({
110
+ name: attachment.name,
111
+ url: attachment.url,
112
+ intent: attachment.intent,
113
+ providerId: attachment.providerId,
114
+ providerKey: attachment.providerKey,
115
+ })),
116
+ ).toEqual([
117
+ {
118
+ name: "GitHub pull request #1200",
119
+ url: "https://github.com/desplega-ai/agent-swarm/pull/1200",
120
+ intent: "task-deliverable",
121
+ providerId: "github",
122
+ providerKey: "https://github.com/desplega-ai/agent-swarm/pull/1200",
123
+ },
124
+ {
125
+ name: "GitHub pull request #81",
126
+ url: "https://github.com/desplega-ai/docs/pull/81",
127
+ intent: "task-deliverable",
128
+ providerId: "github",
129
+ providerKey: "https://github.com/desplega-ai/docs/pull/81",
130
+ },
131
+ ]);
132
+ });
133
+
134
+ test("does not duplicate an existing URL attachment with a different name", () => {
135
+ const task = inProgressTask("completion preserves caller attachment");
136
+ const url = "http://GitHub.com/desplega-ai/agent-swarm/pull/1201/files";
137
+ insertTaskAttachment({
138
+ taskId: task.id,
139
+ agentId,
140
+ name: "Review this change",
141
+ kind: "url",
142
+ url,
143
+ intent: "review",
144
+ });
145
+
146
+ completeTask(task.id, "Done: https://github.com/desplega-ai/agent-swarm/pull/1201");
147
+ const attachments = getTaskAttachments(task.id);
148
+ expect(attachments).toHaveLength(1);
149
+ expect(attachments[0]?.name).toBe("Review this change");
150
+ });
151
+
152
+ test("records deterministic GitHub VCS discovery idempotently", () => {
153
+ const task = createTaskExtended("VCS discovery records PR", { agentId, source: "api" });
154
+ const vcs = {
155
+ vcsProvider: "github" as const,
156
+ vcsRepo: "desplega-ai/agent-swarm",
157
+ vcsNumber: 1202,
158
+ vcsUrl: "https://github.com/desplega-ai/agent-swarm/pull/1202",
159
+ };
160
+ updateTaskVcs(task.id, vcs);
161
+ updateTaskVcs(task.id, vcs);
162
+
163
+ const attachments = getTaskAttachments(task.id);
164
+ expect(attachments).toHaveLength(1);
165
+ expect(attachments[0]?.url).toBe(vcs.vcsUrl);
166
+ });
167
+
168
+ test("reconciles generated VCS attachments while preserving caller-authored rows", () => {
169
+ const task = createTaskExtended("VCS discovery replaces generated PR", {
170
+ agentId,
171
+ source: "api",
172
+ });
173
+ const callerUrl = "https://github.com/caller/owned/pull/77";
174
+ insertTaskAttachment({
175
+ taskId: task.id,
176
+ agentId,
177
+ name: "Caller-owned evidence",
178
+ kind: "url",
179
+ url: callerUrl,
180
+ providerId: "github",
181
+ intent: "task-deliverable",
182
+ description: "Pull request shipped by this task",
183
+ });
184
+
185
+ updateTaskVcs(task.id, {
186
+ vcsProvider: "github",
187
+ vcsRepo: "owner/repo",
188
+ vcsNumber: 1,
189
+ vcsUrl: "https://github.com/owner/repo/pull/1",
190
+ });
191
+ updateTaskVcs(task.id, {
192
+ vcsProvider: "github",
193
+ vcsRepo: "owner/repo",
194
+ vcsNumber: 2,
195
+ vcsUrl: "https://github.com/owner/repo/pull/2",
196
+ });
197
+
198
+ expect(
199
+ getTaskAttachments(task.id).map(({ name, url, providerId }) => ({ name, url, providerId })),
200
+ ).toEqual([
201
+ { name: "Caller-owned evidence", url: callerUrl, providerId: "github" },
202
+ {
203
+ name: "GitHub pull request #2",
204
+ url: "https://github.com/owner/repo/pull/2",
205
+ providerId: "github",
206
+ },
207
+ ]);
208
+
209
+ updateTaskVcs(task.id, {
210
+ vcsProvider: "gitlab",
211
+ vcsRepo: "owner/repo",
212
+ vcsNumber: 3,
213
+ vcsUrl: "https://gitlab.com/owner/repo/-/merge_requests/3",
214
+ });
215
+ expect(getTaskAttachments(task.id).map(({ name, url }) => ({ name, url }))).toEqual([
216
+ { name: "Caller-owned evidence", url: callerUrl },
217
+ ]);
218
+ });
219
+ });
220
+
221
+ describe("attachment-first task shipping evidence", () => {
222
+ const pullRequestFixtures = [
223
+ "https://github.com/desplega-ai/agent-swarm/pull/1207",
224
+ "github.com/desplega-ai/docs/pull/9/files",
225
+ "https://github.com/org/repo/tree/pull/123",
226
+ "https://github.com/org/repo/issues/1/pull/2",
227
+ "https://notgithub.com/o/r/pull/8",
228
+ "https://_github.com/o/r/pull/8",
229
+ "https://evil.example/github.com/o/r/pull/8",
230
+ "https://evil.example/?next=github.com/o/r/pull/8",
231
+ "xhttps://github.com/o/r/pull/1",
232
+ "https://github.com/o/r/pull/123abc",
233
+ "https://github.com/o/r/pull/123.foo",
234
+ "https://github.com/o/r/pull/123,abc",
235
+ "https://github.com/o/./pull/1",
236
+ "https://github.com/../r/pull/1",
237
+ "https://github.com/o/r/pull/123/files",
238
+ "https://github.com/o/r/pull/8/files",
239
+ "github.com/o/r/pull/8",
240
+ "https://github.com/o/r/pull/123.",
241
+ "Reject https://github.com/org/repo/tree/pull/123, then accept github.com/o/r/pull/8",
242
+ ...ECMASCRIPT_WHITESPACE_CODE_POINTS.map(
243
+ (codePoint) => `done${String.fromCodePoint(codePoint)}https://github.com/o/r/pull/8`,
244
+ ),
245
+ ];
246
+
247
+ test("falls back to legacy output even when a non-PR attachment exists", () => {
248
+ const task = createTaskExtended("legacy output fallback", { agentId, source: "api" });
249
+ insertTaskAttachment({
250
+ taskId: task.id,
251
+ agentId,
252
+ name: "Notes",
253
+ kind: "url",
254
+ url: "https://example.com/notes",
255
+ });
256
+ getDb()
257
+ .prepare("UPDATE agent_tasks SET output = ?, status = 'completed' WHERE id = ?")
258
+ .run("Legacy https://github.com/desplega-ai/agent-swarm/pull/1203", task.id);
259
+
260
+ expect(getTaskShippingEvidence(task.id)).toEqual({
261
+ hasArtifact: true,
262
+ hasPullRequest: true,
263
+ pullRequestUrls: ["https://github.com/desplega-ai/agent-swarm/pull/1203"],
264
+ pullRequestSource: "output-fallback",
265
+ });
266
+ });
267
+
268
+ test("prefers attachment PR evidence over legacy output", () => {
269
+ const task = createTaskExtended("attachment wins", { agentId, source: "api" });
270
+ insertTaskAttachment({
271
+ taskId: task.id,
272
+ agentId,
273
+ name: "Shipped PR",
274
+ kind: "url",
275
+ url: "https://github.com/desplega-ai/agent-swarm/pull/1204",
276
+ });
277
+ getDb()
278
+ .prepare("UPDATE agent_tasks SET output = ? WHERE id = ?")
279
+ .run("Old https://github.com/desplega-ai/agent-swarm/pull/999", task.id);
280
+
281
+ expect(getTaskShippingEvidence(task.id)).toEqual({
282
+ hasArtifact: true,
283
+ hasPullRequest: true,
284
+ pullRequestUrls: ["https://github.com/desplega-ai/agent-swarm/pull/1204"],
285
+ pullRequestSource: "attachment",
286
+ });
287
+ });
288
+
289
+ test("provides aggregate SQL predicates without multiplying task rows", () => {
290
+ const task = createTaskExtended("aggregate predicates", { agentId, source: "api" });
291
+ insertTaskAttachment({
292
+ taskId: task.id,
293
+ agentId,
294
+ name: "First",
295
+ kind: "url",
296
+ url: "https://github.com/desplega-ai/agent-swarm/pull/1205",
297
+ });
298
+ insertTaskAttachment({
299
+ taskId: task.id,
300
+ agentId,
301
+ name: "Second",
302
+ kind: "url",
303
+ url: "https://example.com/report",
304
+ });
305
+ const sql = taskShippingEvidenceSql("t");
306
+ const row = getDb()
307
+ .prepare<{ hasArtifact: number; hasPullRequest: number }, [string]>(
308
+ `SELECT ${sql.hasArtifact} AS hasArtifact, ${sql.hasPullRequest} AS hasPullRequest
309
+ FROM agent_tasks t WHERE t.id = ?`,
310
+ )
311
+ .get(task.id);
312
+ expect(row).toEqual({ hasArtifact: 1, hasPullRequest: 1 });
313
+ expect(() => taskShippingEvidenceSql("t; DROP TABLE agent_tasks")).toThrow(
314
+ "Invalid task SQL alias",
315
+ );
316
+ });
317
+
318
+ test("keeps aggregate and single-task evidence aligned for alternate URL forms", () => {
319
+ const task = createTaskExtended("aggregate parity", { agentId, source: "api" });
320
+ insertTaskAttachment({
321
+ taskId: task.id,
322
+ agentId,
323
+ name: "Alternate PR URL",
324
+ kind: "url",
325
+ url: "http://GitHub.com/desplega-ai/agent-swarm/pull/1206/files",
326
+ });
327
+ const sql = taskShippingEvidenceSql("t");
328
+ const row = getDb()
329
+ .prepare<{ hasPullRequest: number }, [string]>(
330
+ `SELECT ${sql.hasPullRequest} AS hasPullRequest FROM agent_tasks t WHERE t.id = ?`,
331
+ )
332
+ .get(task.id);
333
+
334
+ expect(row?.hasPullRequest).toBe(1);
335
+ expect(getTaskShippingEvidence(task.id)?.hasPullRequest).toBe(true);
336
+ });
337
+
338
+ test("rejects lookalike domains in both aggregate and single-task fallback evidence", () => {
339
+ const task = createTaskExtended("fallback negative control", { agentId, source: "api" });
340
+ getDb()
341
+ .prepare("UPDATE agent_tasks SET output = ? WHERE id = ?")
342
+ .run("Not a PR: https://notgithub.com/o/r/pull/8", task.id);
343
+ const sql = taskShippingEvidenceSql("t");
344
+ const row = getDb()
345
+ .prepare<{ hasPullRequest: number }, [string]>(
346
+ `SELECT ${sql.hasPullRequest} AS hasPullRequest FROM agent_tasks t WHERE t.id = ?`,
347
+ )
348
+ .get(task.id);
349
+
350
+ expect(row?.hasPullRequest).toBe(0);
351
+ expect(getTaskShippingEvidence(task.id)?.hasPullRequest).toBe(false);
352
+ });
353
+
354
+ test("keeps aggregate SQL and TypeScript extraction aligned for URL shapes", () => {
355
+ const sql = taskShippingEvidenceSql("t");
356
+
357
+ for (const evidenceSource of ["output", "attachment"] as const) {
358
+ for (const fixture of pullRequestFixtures) {
359
+ const task = createTaskExtended(`${evidenceSource} fixture: ${fixture}`, {
360
+ agentId,
361
+ source: "api",
362
+ });
363
+ if (evidenceSource === "output") {
364
+ getDb().prepare("UPDATE agent_tasks SET output = ? WHERE id = ?").run(fixture, task.id);
365
+ } else {
366
+ insertTaskAttachment({
367
+ taskId: task.id,
368
+ agentId,
369
+ name: "Fixture URL",
370
+ kind: "url",
371
+ url: fixture,
372
+ });
373
+ }
374
+ const aggregate = getDb()
375
+ .prepare<{ hasPullRequest: number }, [string]>(
376
+ `SELECT ${sql.hasPullRequest} AS hasPullRequest FROM agent_tasks t WHERE t.id = ?`,
377
+ )
378
+ .get(task.id);
379
+ const expected = extractGitHubPullRequestUrls(fixture).length > 0;
380
+
381
+ expect(Boolean(aggregate?.hasPullRequest)).toBe(expected);
382
+ expect(getTaskShippingEvidence(task.id)?.hasPullRequest).toBe(expected);
383
+ }
384
+ }
385
+ });
386
+ });
@@ -11,7 +11,11 @@ import {
11
11
  insertTaskAttachment,
12
12
  replaceTaskAttachment,
13
13
  } from "../be/db";
14
- import { AttachmentInputSchema, TaskAttachmentSchema } from "../types";
14
+ import {
15
+ AttachmentInputSchema,
16
+ SERVER_GENERATED_ATTACHMENT_CAPABILITY,
17
+ TaskAttachmentSchema,
18
+ } from "../types";
15
19
 
16
20
  const TEST_DB_PATH = "./test-task-attachments-schema.sqlite";
17
21
 
@@ -154,4 +158,16 @@ describe("task_attachments provider-agnostic metadata", () => {
154
158
  });
155
159
  expect(parsed.success).toBe(true);
156
160
  });
161
+
162
+ test("AttachmentInputSchema rejects the server-generated provenance marker", () => {
163
+ const parsed = AttachmentInputSchema.safeParse({
164
+ kind: "url",
165
+ name: "spoofed generated attachment",
166
+ url: "https://github.com/owner/repo/pull/1",
167
+ capabilities: {
168
+ [SERVER_GENERATED_ATTACHMENT_CAPABILITY]: "task-pull-request-recorder",
169
+ },
170
+ });
171
+ expect(parsed.success).toBe(false);
172
+ });
157
173
  });
@@ -13,8 +13,10 @@ import {
13
13
  getDb,
14
14
  getLeadAgent,
15
15
  getLogsByTaskId,
16
+ getTaskAttachments,
16
17
  getTaskById,
17
18
  initDb,
19
+ insertTaskAttachment,
18
20
  startTask,
19
21
  } from "../be/db";
20
22
  import { createWorkerTaskFollowUp } from "../tasks/worker-follow-up";
@@ -523,6 +525,61 @@ describe("store-progress terminal result reporting", () => {
523
525
  }
524
526
  });
525
527
 
528
+ test("force reconciles generated PR attachments without deleting caller attachments", async () => {
529
+ const agent = createAgent({
530
+ name: "terminal-handler-force-attachments",
531
+ isLead: false,
532
+ status: "idle",
533
+ capabilities: [],
534
+ });
535
+ const task = createTaskExtended("terminal forced attachment correction", {
536
+ agentId: agent.id,
537
+ });
538
+ startTask(task.id);
539
+ completeTask(task.id, "Original https://github.com/owner/repo/pull/1");
540
+ const callerUrl = "https://github.com/caller/owned/pull/77";
541
+ insertTaskAttachment({
542
+ taskId: task.id,
543
+ agentId: agent.id,
544
+ name: "Caller-owned evidence",
545
+ kind: "url",
546
+ url: callerUrl,
547
+ providerId: "github",
548
+ intent: "task-deliverable",
549
+ description: "Pull request shipped by this task",
550
+ });
551
+ const handler = buildStoreProgressHandler();
552
+
553
+ const changed = (await handler.handler(
554
+ {
555
+ taskId: task.id,
556
+ output: "Corrected https://github.com/owner/repo/pull/2",
557
+ force: true,
558
+ },
559
+ storeProgressMeta(agent.id),
560
+ )) as StoreProgressResult;
561
+ expect(changed.structuredContent.wasForcedOverwrite).toBe(true);
562
+ expect(
563
+ getTaskAttachments(task.id).map(({ name, url, providerId }) => ({ name, url, providerId })),
564
+ ).toEqual([
565
+ { name: "Caller-owned evidence", url: callerUrl, providerId: "github" },
566
+ {
567
+ name: "GitHub pull request #2",
568
+ url: "https://github.com/owner/repo/pull/2",
569
+ providerId: "github",
570
+ },
571
+ ]);
572
+
573
+ const removed = (await handler.handler(
574
+ { taskId: task.id, output: "Corrected with no pull request", force: true },
575
+ storeProgressMeta(agent.id),
576
+ )) as StoreProgressResult;
577
+ expect(removed.structuredContent.wasForcedOverwrite).toBe(true);
578
+ expect(getTaskAttachments(task.id).map(({ name, url }) => ({ name, url }))).toEqual([
579
+ { name: "Caller-owned evidence", url: callerUrl },
580
+ ]);
581
+ });
582
+
526
583
  test("force preserves outputSchema validation before overwriting terminal output", async () => {
527
584
  const agent = createAgent({
528
585
  name: "terminal-handler-output-schema",