@desplega.ai/agent-swarm 1.172.0 → 2.0.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.
- package/README.md +1 -1
- package/dist/{acp-adapter-fpecx1g1.js → acp-adapter-sj2g0m84.js} +18 -17
- package/dist/{actions-pw87a1a8.js → actions-fegk44gr.js} +65 -35
- package/dist/{amp-adapter-8jem2bc0.js → amp-adapter-ad8ft89x.js} +8 -7
- package/dist/{anthropic-messages-8ee2f22q.js → anthropic-messages-r6czkjv1.js} +7 -7
- package/dist/{app-wmf0m843.js → app-kfvgbvzy.js} +19 -13
- package/dist/{assistant-emp1e108.js → assistant-z7zdy9ja.js} +65 -38
- package/dist/{boot-reembed-1de4f4aj.js → boot-reembed-5c2z0mbe.js} +11 -9
- package/dist/{boot-reembed-4ymn9atn.js → boot-reembed-j8xa181e.js} +11 -9
- package/dist/{boot-scrub-sweep-adhk1s8c.js → boot-scrub-sweep-hnkn3rgt.js} +12 -10
- package/dist/{boot-seeds-8q637aew.js → boot-seeds-kwjq4d2z.js} +15 -13
- package/dist/{claude-adapter-t26cv8a4.js → claude-adapter-9vmvpvgd.js} +20 -19
- package/dist/{claude-managed-adapter-xgxhqrt2.js → claude-managed-adapter-k9qae05j.js} +7 -5
- package/dist/{claude-managed-setup-bnmjarjc.js → claude-managed-setup-q54zx5q2.js} +6 -4
- package/dist/{claude-sdk-session-gsfsh662.js → claude-sdk-session-9jye6cxr.js} +12698 -11995
- package/dist/{cli-k46r9hmk.js → cli-03gedhng.js} +16 -14
- package/dist/{cli-kb4ehzx5.js → cli-1e4b6bzh.js} +7 -7
- package/dist/{cli-d603qf86.js → cli-29jc5q7w.js} +10 -8
- package/dist/cli-2m1ktfzz.js +17795 -0
- package/dist/{cli-8vs9trd4.js → cli-3dj8k4vy.js} +1 -1
- package/dist/{cli-zxm9p3x4.js → cli-3fbmmawm.js} +5 -5
- package/dist/{cli-gmdajfw5.js → cli-4jxm2qtz.js} +4987 -1708
- package/dist/{cli-tadfgd0t.js → cli-5hkm7whv.js} +33 -4
- package/dist/{cli-6jb2rf77.js → cli-5t4f29ez.js} +3 -3
- package/dist/{cli-7a0sde2x.js → cli-6cagzc96.js} +9 -0
- package/dist/cli-6cvfxs8r.js +47 -0
- package/dist/{cli-pj6x9qce.js → cli-6jyj59nb.js} +2 -2
- package/dist/{cli-388egyf6.js → cli-6razj6t4.js} +2 -2
- package/dist/{cli-qsztpfry.js → cli-74ahz61x.js} +2 -2
- package/dist/{cli-4nz8p1p5.js → cli-7x0g4sn1.js} +15474 -16082
- package/dist/{cli-yatxqynm.js → cli-9ha6hs2d.js} +1 -290
- package/dist/{cli-81r1wjyv.js → cli-9nfx44eh.js} +1 -1
- package/dist/cli-9traj3dn.js +638 -0
- package/dist/{cli-smcrrmhy.js → cli-agzebbd5.js} +9 -9
- package/dist/{cli-bf7485pm.js → cli-apx87jne.js} +2 -2
- package/dist/{cli-ztrv9y21.js → cli-bzszjf2p.js} +3689 -1081
- package/dist/{cli-c5km5znr.js → cli-c8r3kzzb.js} +1 -1
- package/dist/{cli-hsem16hb.js → cli-cgtksawx.js} +7 -4
- package/dist/{cli-db3m6zdt.js → cli-d2qr7aj4.js} +1 -1
- package/dist/{cli-1hgat6pa.js → cli-d5f9ed5c.js} +5 -5
- package/dist/cli-e3g5mbkz.js +776 -0
- package/dist/{cli-7jykv7ef.js → cli-e6mpjhed.js} +1 -1
- package/dist/{cli-j6kpj9az.js → cli-ee9gtz70.js} +1553 -68
- package/dist/{cli-e0a9sk00.js → cli-eejsgvm4.js} +4 -4
- package/dist/{cli-tgpfnm6r.js → cli-ef1hg1wv.js} +518 -59
- package/dist/cli-f527ce74.js +781 -0
- package/dist/{cli-ppy4nnnh.js → cli-f7kr7svz.js} +2 -2
- package/dist/cli-hjwmhcg1.js +10622 -0
- package/dist/{cli-2bw3w47j.js → cli-hnhtk6cj.js} +1 -1
- package/dist/{cli-ywm1azg9.js → cli-hnmhtzvh.js} +6 -4
- package/dist/{cli-4fnb6884.js → cli-hvqbtna4.js} +7 -1259
- package/dist/{cli-pstq9888.js → cli-j0pfptdp.js} +220 -103
- package/dist/cli-jta2eyzc.js +619 -0
- package/dist/{cli-esnpehf5.js → cli-k382tshb.js} +12 -13
- package/dist/cli-k3dp5tpf.js +263 -0
- package/dist/{cli-wd0hz6ah.js → cli-kbrt8gqa.js} +9 -91
- package/dist/cli-kceah1eh.js +92 -0
- package/dist/{cli-prrt9fa0.js → cli-ke896fcf.js} +10 -10
- package/dist/{cli-nv4zd0b9.js → cli-kt6zqg0e.js} +6 -6
- package/dist/{cli-g6t9texm.js → cli-m50t4309.js} +5 -18
- package/dist/cli-n2wq4j7m.js +6985 -0
- package/dist/{cli-c2pq2rjb.js → cli-n3hzg89b.js} +46 -15
- package/dist/{cli-tmhsxhb8.js → cli-n5vwg842.js} +23 -6
- package/dist/{cli-5rv1j9ga.js → cli-ne236afy.js} +53 -7
- package/dist/cli-p0sg24zp.js +16 -0
- package/dist/{cli-d161g69q.js → cli-p1x85d2t.js} +50 -2
- package/dist/{cli-7pz3b7x5.js → cli-przpmdgr.js} +25 -0
- package/dist/{cli-g4f5pj26.js → cli-q0ke1yww.js} +111 -25
- package/dist/{cli-e3pmyt39.js → cli-q50942rs.js} +1 -1
- package/dist/{cli-r44t9wqc.js → cli-r106725k.js} +2 -2
- package/dist/{cli-gj81e47t.js → cli-r2pnvj8g.js} +2 -2
- package/dist/{cli-r8smj8b8.js → cli-r62a35ea.js} +19 -3
- package/dist/{cli-cd0hv9nh.js → cli-ra0xzggj.js} +2 -2
- package/dist/cli-rhs9pz33.js +294 -0
- package/dist/{cli-nyhb1r6x.js → cli-s2sp957d.js} +52 -29
- package/dist/{cli-1x7da0de.js → cli-s4setq91.js} +8 -8
- package/dist/{cli-xczpdp9e.js → cli-se8w6wny.js} +6 -6
- package/dist/cli-skxvvpap.js +10140 -0
- package/dist/{cli-014vns2y.js → cli-stfs0k5s.js} +208 -525
- package/dist/{cli-0p3enk35.js → cli-tqaazemc.js} +9 -1
- package/dist/{cli-cf69s3ew.js → cli-tr4sxp68.js} +3 -3
- package/dist/{cli-8hzfb6xz.js → cli-tsde1hdr.js} +15 -15
- package/dist/{cli-cceskcvt.js → cli-txqb9s38.js} +8 -8
- package/dist/{cli-aspcaef3.js → cli-v1yb0rqb.js} +6 -6
- package/dist/{cli-j1pmc07z.js → cli-vatyhshy.js} +268 -40
- package/dist/{cli-04emfe2q.js → cli-vdach5a9.js} +5 -5
- package/dist/{cli-7avy4v8w.js → cli-w1q7h2r2.js} +5 -3
- package/dist/{cli-rcp5aqdw.js → cli-w7m81tb7.js} +1 -1
- package/dist/{cli-hwwvy36w.js → cli-wayw93nt.js} +3 -3
- package/dist/cli-wmgrkscj.js +23254 -0
- package/dist/{cli-gp1rtyvk.js → cli-z5jjpaxw.js} +29 -6
- package/dist/{cli-451ks6aw.js → cli-zkpks3r3.js} +1 -1
- package/dist/cli.js +32 -48
- package/dist/{codex-adapter-m4tgdysy.js → codex-adapter-y81sssk0.js} +16 -15
- package/dist/{codex-login-841kv5fk.js → codex-login-f2rcfpp9.js} +3 -2
- package/dist/{codex-session-runner-ymwzm6ea.js → codex-session-runner-8vx4j6a9.js} +16 -15
- package/dist/{commands-n0hshcvs.js → commands-kcpvpcrr.js} +21 -10
- package/dist/{controller-hq5ggpcp.js → controller-ywrmt6yd.js} +5 -4
- package/dist/{controllers-connect-y07cqcvy.js → controllers-connect-kwpvj3rf.js} +45 -38
- package/dist/{cursor-adapter-q8pgv5qa.js → cursor-adapter-cehv1qns.js} +7 -6
- package/dist/{db-hkqvrdcb.js → db-k8ankj20.js} +13 -11
- package/dist/{db-4w7tmt1g.js → db-ww5d7jq3.js} +29 -9
- package/dist/{devin-adapter-xgtj680j.js → devin-adapter-x1022qb1.js} +3 -2
- package/dist/{dispatcher-5sd69y14.js → dispatcher-e8gq5m68.js} +41 -34
- package/dist/{docker-f503k9p0.js → docker-ff6y1190.js} +3 -2
- package/dist/{dsh-adapter-2h8m4cab.js → dsh-adapter-3epjm3af.js} +7 -6
- package/dist/{e2b-66xra4az.js → e2b-8ygwhg0f.js} +4 -3
- package/dist/{e2b-xwhyj8cw.js → e2b-swhnpw3n.js} +4 -3
- package/dist/{event-bus-vjtv82gm.js → event-bus-6axnngdj.js} +3 -2
- package/dist/external-channel-h3359cwn.js +52 -0
- package/dist/{google-generative-ai-7pavrbv7.js → google-generative-ai-g8db7x0f.js} +5 -3
- package/dist/{google-vertex-3f1jk41f.js → google-vertex-r2692q0r.js} +5 -3
- package/dist/{grok-adapter-8m9yqzbq.js → grok-adapter-0d4x7gkx.js} +19 -18
- package/dist/{handlers-s20xr10r.js → handlers-an1gzv21.js} +44 -37
- package/dist/{heartbeat-3k31q4g2.js → heartbeat-6dakeb61.js} +46 -39
- package/dist/{hook-gdn6a2fn.js → hook-6v6cv6d5.js} +12 -24
- package/dist/{hook-ygwerrdh.js → hook-sa5xye4y.js} +13 -12
- package/dist/{http-jdteeg81.js → http-r7f3eky0.js} +1598 -279
- package/dist/index-52j1aja4.js +3465 -0
- package/dist/{index-e0snym4t.js → index-7ep4gzdk.js} +41 -34
- package/dist/index-9gw58n4k.js +193 -0
- package/dist/index-9j87mcm8.js +56305 -0
- package/dist/index-bsmcresk.js +79424 -0
- package/dist/index-mh85qhec.js +475 -0
- package/dist/{index-vm3bm9t7.js → index-rmpw9a8q.js} +41 -34
- package/dist/{index-6ksewd5y.js → index-s2wxgzr0.js} +1015 -732
- package/dist/{index-tsszt7g9.js → index-v1v6n7hg.js} +46 -39
- package/dist/index-we27kd31.js +12810 -0
- package/dist/{index-ffqzqgwb.js → index-zk96aq1k.js} +11 -9
- package/dist/{keepalive-jekc896t.js → keepalive-be9vwy5h.js} +13 -11
- package/dist/{lead-4k9s9szx.js → lead-0861cwrf.js} +64 -57
- package/dist/{maintenance-shazch4n.js → maintenance-mzzgc8bn.js} +11 -9
- package/dist/node-c9we95xf.js +1 -1
- package/dist/{node-fkajaq96.js → node-mn1zs7an.js} +2 -1
- package/dist/{oauth-refresh-sweep-5gb36kn8.js → oauth-refresh-sweep-cpdnfwdj.js} +12 -10
- package/dist/{onboard-ydrbnvww.js → onboard-fh8qs4r3.js} +11 -3
- package/dist/{opencode-adapter-5zh0he20.js → opencode-adapter-nm42wwj8.js} +19 -14
- package/dist/{otel-impl-6pf5cv86.js → otel-impl-x6c1c9g4.js} +248 -869
- package/dist/{pi-mono-adapter-ef9wwx9m.js → pi-mono-adapter-9n2ya240.js} +245 -107
- package/dist/{pricing-refresh-s52nsa5q.js → pricing-refresh-qb0rv0ap.js} +13 -11
- package/dist/{rbac-roles-aq3msh4e.js → rbac-roles-1gzdwbh1.js} +12 -10
- package/dist/{rbac-roles-q5gb5e8v.js → rbac-roles-3mraey52.js} +13 -11
- package/dist/{render-v2-pd1rg54q.js → render-v2-agpa45zz.js} +19 -13
- package/dist/{resume-gw4a2c6v.js → resume-x0ydy9s9.js} +27 -21
- package/dist/{runtime-qfry88v2.js → runtime-sa2vr16g.js} +2 -2
- package/dist/{setup-jda1ksdh.js → setup-as9ga0d6.js} +2 -2
- package/dist/{virtual-modules-b3ga2m65.js → virtual-modules-9gkxqrap.js} +7 -6
- package/dist/{work-objects-tb7ycye8.js → work-objects-y5g1y7ev.js} +49 -37
- package/dist/{worker-njjccz5d.js → worker-860c3e38.js} +64 -57
- package/dist/{x-qfmygqqv.js → x-m477fr0h.js} +3 -2
- package/openapi.json +951 -356
- package/package.json +12 -13
- package/src/apps/catalog.generated.json +1309 -53
- package/src/apps/page-validator.ts +93 -31
- package/src/azure-devops/api.ts +94 -18
- package/src/azure-devops/auth.ts +32 -0
- package/src/azure-devops/boards-sync.ts +708 -0
- package/src/azure-devops/handlers.ts +52 -8
- package/src/azure-devops/html.ts +26 -0
- package/src/azure-devops/index.ts +15 -0
- package/src/azure-devops/templates.ts +86 -0
- package/src/azure-devops/types.ts +65 -1
- package/src/be/config-refs.ts +60 -0
- package/src/be/db/page-versions.ts +15 -0
- package/src/be/db/runtime.ts +35 -8
- package/src/be/db/skill-name-resolution.ts +36 -0
- package/src/be/db/tasks/read.ts +230 -0
- package/src/be/db/tasks/write.ts +7 -0
- package/src/be/db.ts +74 -13
- package/src/be/migrations/213_script_connections_sql.sql +79 -0
- package/src/be/migrations/215_swarm_config_export_to_env.sql +34 -0
- package/src/be/migrations/216_task_resolved_effort.sql +5 -0
- package/src/be/migrations/217_chat_sdk_state.sql +104 -0
- package/src/be/model-catalog-store.ts +5 -1
- package/src/be/model-tier-resolution.ts +12 -1
- package/src/be/model-validation.ts +5 -2
- package/src/be/models-catalog.ts +26 -1
- package/src/be/modelsdev-cache.json +198 -63
- package/src/be/modelsdev-cache.ts +9 -1
- package/src/be/script-connections.ts +47 -8
- package/src/be/script-credential-broker.ts +39 -2
- package/src/be/scripts/run-saved.ts +1 -0
- package/src/be/scripts/typecheck.ts +10 -8
- package/src/be/seed/runner.ts +13 -1
- package/src/be/seed/state-db.ts +5 -0
- package/src/be/seed/types.ts +6 -0
- package/src/be/seed-pricing.ts +19 -2
- package/src/be/seed-scripts/catalog/ops-catalog-audit.inline.ts +1 -1
- package/src/be/seed-scripts/catalog/ops-catalog-audit.ts +1 -1
- package/src/be/seed-skills/bundled-files.generated.json +10 -18
- package/src/be/seed-skills/index.ts +107 -3
- package/src/be/sql-connections.ts +118 -0
- package/src/be/swarm-config-guard.ts +53 -0
- package/src/cli.tsx +0 -16
- package/src/commands/onboard/non-interactive.ts +4 -2
- package/src/commands/profile-sync.ts +5 -3
- package/src/commands/provider-credentials.ts +52 -6
- package/src/commands/runner.ts +196 -56
- package/src/e2b/env.ts +5 -0
- package/src/extensions/contract-types.generated.ts +1 -1
- package/src/extensions/contract.ts +45 -0
- package/src/extensions/dispatcher.ts +7 -0
- package/src/extensions/post-bridge.ts +40 -0
- package/src/github/handlers.ts +11 -4
- package/src/heartbeat/templates.ts +8 -5
- package/src/hooks/hook.ts +0 -26
- package/src/http/all-routes.ts +0 -1
- package/src/http/config.ts +36 -17
- package/src/http/core.ts +4 -0
- package/src/http/index.ts +14 -2
- package/src/http/integrations.ts +88 -1
- package/src/http/mcp-oauth.ts +6 -13
- package/src/http/mcp.ts +15 -5
- package/src/http/page-proxy.ts +2 -4
- package/src/http/pages-public.ts +6 -5
- package/src/http/pages.ts +5 -14
- package/src/http/poll.ts +28 -68
- package/src/http/requested-by.ts +54 -0
- package/src/http/route-def.ts +7 -6
- package/src/http/script-connection-proxy.ts +51 -4
- package/src/http/script-connections.ts +5 -2
- package/src/http/scripts.ts +1 -0
- package/src/http/session-cost-recompute.ts +12 -2
- package/src/http/session-data.ts +4 -0
- package/src/http/stats.ts +1 -30
- package/src/http/tasks.ts +61 -5
- package/src/http/utils.ts +14 -0
- package/src/http/webhooks.ts +183 -2
- package/src/http/x.ts +1 -0
- package/src/integrations/chat-sdk/adapters.ts +129 -0
- package/src/integrations/chat-sdk/attachments.ts +232 -0
- package/src/integrations/chat-sdk/background.ts +36 -0
- package/src/integrations/chat-sdk/commands.ts +194 -0
- package/src/integrations/chat-sdk/config.ts +198 -0
- package/src/integrations/chat-sdk/discord-api.ts +63 -0
- package/src/integrations/chat-sdk/discord-gateway.ts +185 -0
- package/src/integrations/chat-sdk/handlers.ts +28 -0
- package/src/integrations/chat-sdk/inbound.ts +426 -0
- package/src/integrations/chat-sdk/limits.ts +48 -0
- package/src/integrations/chat-sdk/logger.ts +81 -0
- package/src/integrations/chat-sdk/outbound.ts +123 -0
- package/src/integrations/chat-sdk/relay.ts +172 -0
- package/src/integrations/chat-sdk/router.ts +169 -0
- package/src/integrations/chat-sdk/runtime.ts +351 -0
- package/src/integrations/chat-sdk/schema.ts +245 -0
- package/src/integrations/chat-sdk/state.ts +243 -0
- package/src/integrations/chat-sdk/status.ts +244 -0
- package/src/integrations/chat-sdk/steering.ts +58 -0
- package/src/integrations/chat-sdk/teams-origin.ts +77 -0
- package/src/integrations/chat-sdk/telegram-api.ts +56 -0
- package/src/integrations/chat-sdk/templates.ts +16 -0
- package/src/integrations/chat-sdk/test-hooks.ts +37 -0
- package/src/integrations/chat-sdk/thread-buffer.ts +100 -0
- package/src/integrations/chat-sdk/typing.ts +65 -0
- package/src/jira/adf.ts +3 -1
- package/src/kv-reserved-namespaces.ts +2 -0
- package/src/oauth/mcp-wrapper.ts +10 -6
- package/src/otel-impl.ts +2 -1
- package/src/pages/version.ts +4 -5
- package/src/prompts/base-prompt.ts +111 -45
- package/src/providers/acp-target-catalog.ts +6 -0
- package/src/providers/acp-targets.ts +56 -3
- package/src/providers/amp-adapter.ts +1 -1
- package/src/providers/claude-adapter.ts +27 -2
- package/src/providers/claude-flag-settings.ts +133 -0
- package/src/providers/claude-managed-adapter.ts +1 -1
- package/src/providers/claude-sdk-session.ts +7 -0
- package/src/providers/claude-session-events.ts +6 -1
- package/src/providers/dsh-adapter.ts +1 -1
- package/src/providers/opencode-adapter.ts +268 -37
- package/src/providers/opencode-billing.ts +86 -0
- package/src/providers/opencode-swarm-plugin.ts +51 -0
- package/src/providers/pi-mono-adapter.ts +398 -153
- package/src/providers/pricing-sources.md +37 -3
- package/src/providers/reasoning-effort.ts +27 -0
- package/src/providers/types.ts +11 -2
- package/src/rbac/legacy-policy.ts +8 -2
- package/src/rbac/permissions.ts +21 -0
- package/src/scheduler/scheduler.ts +4 -2
- package/src/scripts-runtime/api-client.ts +15 -1
- package/src/scripts-runtime/api-types.ts +9 -1
- package/src/scripts-runtime/ctx.ts +1 -1
- package/src/scripts-runtime/loader.ts +19 -7
- package/src/scripts-runtime/mcp-client.ts +30 -19
- package/src/scripts-runtime/sdk-allowlist.ts +8 -6
- package/src/scripts-runtime/swarm-sdk.ts +0 -11
- package/src/scripts-runtime/types/stdlib.d.ts +27 -15
- package/src/scripts-runtime/types/swarm-extension.d.ts +63 -1
- package/src/scripts-runtime/types/swarm-sdk.d.ts +27 -15
- package/src/server.ts +52 -18
- package/src/slack/actions.ts +29 -1
- package/src/slack/app.ts +14 -2
- package/src/slack/assistant.ts +36 -2
- package/src/slack/channel-join.ts +3 -16
- package/src/slack/commands.ts +9 -1
- package/src/slack/event-dedup.ts +1 -1
- package/src/slack/external-channel-egress.ts +113 -0
- package/src/slack/external-channel.ts +309 -0
- package/src/slack/handlers.ts +61 -0
- package/src/slack/render-v2.ts +40 -47
- package/src/slack/work-objects.ts +5 -0
- package/src/tasks/additive-buffer.ts +4 -2
- package/src/tasks/context-key.ts +94 -2
- package/src/tasks/model-pin.ts +67 -0
- package/src/tasks/worker-follow-up.ts +4 -31
- package/src/telemetry.ts +2 -1
- package/src/tests/acp-adapter.test.ts +42 -0
- package/src/tests/api-drain-worker-e2e.test.ts +1 -6
- package/src/tests/apps-catalog-feedback.test.ts +310 -0
- package/src/tests/apps-catalog-inputs.test.ts +70 -0
- package/src/tests/apps-catalog-table.test.ts +113 -0
- package/src/tests/azure-devops-boards-sync.test.ts +584 -0
- package/src/tests/azure-devops-html.test.ts +13 -0
- package/src/tests/azure-devops-webhook-malformed.test.ts +4 -0
- package/src/tests/base-prompt.test.ts +58 -54
- package/src/tests/chat-sdk-commands.test.ts +191 -0
- package/src/tests/chat-sdk-config.test.ts +334 -0
- package/src/tests/chat-sdk-discord-api.test.ts +68 -0
- package/src/tests/chat-sdk-discord-gateway.test.ts +252 -0
- package/src/tests/chat-sdk-discord-webhook.test.ts +126 -0
- package/src/tests/chat-sdk-fields-sync.test.ts +50 -0
- package/src/tests/chat-sdk-harness.ts +146 -0
- package/src/tests/chat-sdk-inbound.test.ts +363 -0
- package/src/tests/chat-sdk-relay.test.ts +470 -0
- package/src/tests/chat-sdk-reload.test.ts +128 -0
- package/src/tests/chat-sdk-shutdown.test.ts +220 -0
- package/src/tests/chat-sdk-slack.test.ts +268 -0
- package/src/tests/chat-sdk-state.test.ts +163 -0
- package/src/tests/chat-sdk-status-route.test.ts +382 -0
- package/src/tests/chat-sdk-teams.test.ts +277 -0
- package/src/tests/chat-sdk-tools.test.ts +520 -0
- package/src/tests/chat-sdk-typing.test.ts +120 -0
- package/src/tests/chat-sdk-webhook-telegram.test.ts +200 -0
- package/src/tests/claude-adapter-binary.test.ts +144 -0
- package/src/tests/claude-flag-settings.test.ts +114 -0
- package/src/tests/claude-managed-adapter.test.ts +1 -1
- package/src/tests/claude-sdk-lifecycle.test.ts +148 -1
- package/src/tests/claude-sdk-process.fixture.ts +7 -0
- package/src/tests/claude-thinking-tokens.test.ts +57 -0
- package/src/tests/claude-worker-parity.test.ts +2 -7
- package/src/tests/config-env-export-chat-sdk.test.ts +11 -0
- package/src/tests/config-refs.test.ts +66 -0
- package/src/tests/context-key.test.ts +51 -0
- package/src/tests/cors-allowlist.test.ts +25 -0
- package/src/tests/create-page-tool.test.ts +18 -0
- package/src/tests/credential-check.test.ts +139 -0
- package/src/tests/credentials.test.ts +21 -0
- package/src/tests/db-runtime.test.ts +61 -0
- package/src/tests/db-task-read-contract.test.ts +20 -0
- package/src/tests/db-task-write-contract.test.ts +10 -0
- package/src/tests/deferred-task-wake.test.ts +52 -4
- package/src/tests/docs-index-footprint.test.js +3 -3
- package/src/tests/dsh-adapter.test.ts +1 -1
- package/src/tests/enabled-capabilities.test.ts +20 -0
- package/src/tests/entrypoint-api-readiness.test.ts +14 -8
- package/src/tests/entrypoint-config-env-export.test.ts +5 -2
- package/src/tests/entrypoint-export-to-env.test.ts +78 -0
- package/src/tests/explicit-model-validation.test.ts +105 -1
- package/src/tests/extensions-example-route-chat-channel.test.ts +154 -0
- package/src/tests/extensions-post-bridge.test.ts +51 -0
- package/src/tests/fetch-resolved-env.test.ts +128 -0
- package/src/tests/fixtures/azure-boards/workitem.commented.json +89 -0
- package/src/tests/fixtures/azure-boards/workitem.created.json +81 -0
- package/src/tests/fixtures/azure-boards/workitem.deleted.json +70 -0
- package/src/tests/fixtures/azure-boards/workitem.restored.json +82 -0
- package/src/tests/fixtures/azure-boards/workitem.updated.json +130 -0
- package/src/tests/fixtures/catalog-feedback-definition.json.txt +860 -0
- package/src/tests/fixtures/catalog-inputs-definition.json.txt +910 -0
- package/src/tests/fixtures/catalog-table-definition.json.txt +468 -0
- package/src/tests/fixtures/chat-sdk/telegram-private-text.json +10 -0
- package/src/tests/fixtures/extensions/route-chat-channel-to-agent/hooks.ts +27 -0
- package/src/tests/fixtures/extensions/route-chat-channel-to-agent/manifest.json +9 -0
- package/src/tests/fixtures/openai-compatible-registry-extension.child.ts +146 -0
- package/src/tests/fixtures/runner-identity-refresh.ts +3 -0
- package/src/tests/fixtures/runner-legacy-resume-prompt.ts +122 -0
- package/src/tests/github-handlers.test.ts +35 -0
- package/src/tests/harness-provider-resolution.test.ts +12 -0
- package/src/tests/heartbeat-checklist.test.ts +3 -2
- package/src/tests/hook-runtime-close.test.ts +0 -2
- package/src/tests/http-api-integration.test.ts +0 -25
- package/src/tests/migration-046-budgets.test.ts +17 -16
- package/src/tests/migration-217-chat-sdk-state.test.ts +113 -0
- package/src/tests/model-tier-resolution.test.ts +93 -0
- package/src/tests/models-catalog.test.ts +42 -1
- package/src/tests/node-env-runtime.test.ts +98 -0
- package/src/tests/openai-compatible-providers.test.ts +134 -0
- package/src/tests/openai-compatible-registry-extension.test.ts +42 -0
- package/src/tests/opencode-adapter.test.ts +348 -3
- package/src/tests/opencode-billing.test.ts +28 -0
- package/src/tests/opencode-helmcode-sdk.test.ts +201 -0
- package/src/tests/opencode-shell-env.test.ts +145 -0
- package/src/tests/openrouter-base-url.test.ts +101 -151
- package/src/tests/page-proxy.test.ts +1 -1
- package/src/tests/pages-browser-sdk.test.ts +76 -0
- package/src/tests/pages-feedback.test.ts +1 -1
- package/src/tests/paused-tasks-requested-by.test.ts +102 -0
- package/src/tests/pi-custom-provider-fake-server.test.ts +442 -0
- package/src/tests/pi-mono-adapter.test.ts +385 -5
- package/src/tests/pi-shell-env.test.ts +170 -0
- package/src/tests/prompt-template-remaining.test.ts +35 -0
- package/src/tests/rbac-engine.test.ts +11 -3
- package/src/tests/runner-exit-after-tasks.test.ts +0 -4
- package/src/tests/runner-identity-refresh.test.ts +3 -0
- package/src/tests/runner-legacy-resume-prompt.test.ts +64 -0
- package/src/tests/script-connections-sql.test.ts +675 -0
- package/src/tests/script-connections.test.ts +7 -1
- package/src/tests/scripts-http.test.ts +201 -1
- package/src/tests/scripts-only-gating.test.ts +2 -3
- package/src/tests/scripts-runtime.test.ts +58 -40
- package/src/tests/secret-scrubber.test.ts +86 -0
- package/src/tests/seed-skills-bundled-files.test.ts +45 -53
- package/src/tests/seed-skills-retire.test.ts +182 -0
- package/src/tests/send-task-requester-gate.test.ts +58 -0
- package/src/tests/session-cost-recompute-unit.test.ts +94 -0
- package/src/tests/skill-get-name-resolution.test.ts +137 -0
- package/src/tests/slack-channel-join.test.ts +4 -1
- package/src/tests/slack-external-channel-config.test.ts +65 -0
- package/src/tests/slack-external-channel-egress.test.ts +237 -0
- package/src/tests/slack-external-channel-guard.test.ts +280 -0
- package/src/tests/slack-external-channel-tools.test.ts +173 -0
- package/src/tests/slack-external-channel.test.ts +370 -0
- package/src/tests/slack-render-v2.test.ts +86 -10
- package/src/tests/sonnet-5-5-registry.test.ts +9 -1
- package/src/tests/spec-base-url.test.ts +16 -1
- package/src/tests/swarm-config-export-to-env.test.ts +267 -0
- package/src/tests/swarm-config-reserved-keys.test.ts +16 -0
- package/src/tests/swarm-diff.test.ts +1 -1
- package/src/tests/task-model-effort-rerun.test.ts +164 -0
- package/src/tests/task-session-prompts.test.ts +132 -0
- package/src/tests/task-tool-manifest.test.ts +14 -0
- package/src/tests/task-tools-ctx.test.ts +2 -0
- package/src/tests/tool-annotations.test.ts +6 -6
- package/src/tests/ui-logs-parser.test.ts +43 -0
- package/src/tests/used-by-route.test.ts +6 -0
- package/src/tests/vendored-openapi.test.ts +16 -1
- package/src/tests/vercel-ai-gateway.test.ts +149 -0
- package/src/tests/workflow-swarm-script.test.ts +38 -0
- package/src/tests/workflow-wait-filter.test.ts +5 -3
- package/src/tools/chat-access.ts +41 -0
- package/src/tools/chat-open-dm.ts +59 -0
- package/src/tools/chat-post.ts +74 -0
- package/src/tools/chat-read-thread.ts +100 -0
- package/src/tools/chat-reply.ts +75 -0
- package/src/tools/create-page.ts +3 -13
- package/src/tools/defer-task.ts +17 -0
- package/src/tools/model-catalog/index.ts +2 -1
- package/src/tools/register-chat-channel.ts +139 -0
- package/src/tools/send-task.ts +24 -0
- package/src/tools/skills/skill-get.ts +17 -9
- package/src/tools/slack-post.ts +6 -0
- package/src/tools/slack-read.ts +8 -0
- package/src/tools/slack-reply.ts +6 -0
- package/src/tools/slack-start-thread.ts +6 -0
- package/src/tools/slack-update.ts +6 -0
- package/src/tools/slack-upload-file.ts +6 -0
- package/src/tools/swarm-config/get-config.ts +2 -1
- package/src/tools/swarm-config/list-config.ts +2 -1
- package/src/tools/swarm-config/set-config.ts +16 -3
- package/src/tools/tool-config.ts +8 -6
- package/src/types.ts +12 -2
- package/src/utils/config-env-export.ts +74 -0
- package/src/utils/credentials.test.ts +92 -0
- package/src/utils/credentials.ts +52 -4
- package/src/utils/harness-provider.ts +15 -6
- package/src/utils/node-env.ts +24 -0
- package/src/utils/runtime-model-catalog.ts +11 -1
- package/src/utils/secret-scrubber.ts +71 -2
- package/src/utils/task-tool-manifest.ts +19 -2
- package/src/workflows/executors/swarm-script.ts +1 -0
- package/templates/skills/agent-browser/SKILL.md +1 -1
- package/templates/skills/agent-browser/content.md +1 -1
- package/templates/skills/apps/SKILL.md +749 -0
- package/templates/skills/apps/content.md +60 -22
- package/templates/skills/chat-sdk/config.json +14 -0
- package/templates/skills/chat-sdk/content.md +84 -0
- package/templates/skills/composio-gmail/content.md +7 -0
- package/templates/skills/composio-google-docs/content.md +1 -1
- package/templates/skills/email-domain-health/config.json +14 -0
- package/templates/skills/email-domain-health/content.md +131 -0
- package/templates/skills/kv-storage/content.md +2 -2
- package/templates/skills/pages/content.md +8 -10
- package/templates/skills/qa-loom/config.json +14 -0
- package/templates/skills/qa-loom/content.md +187 -0
- package/templates/skills/qa-loom/files/build-loom.mjs +221 -0
- package/templates/skills/qa-loom/files/run-qa.sh +57 -0
- package/templates/skills/slack-interaction/content.md +4 -0
- package/templates/skills/swarm-extensions/content.md +2 -0
- package/templates/skills/swarm-scripts/SKILL.md +4 -2
- package/templates/skills/swarm-scripts/config.json +1 -1
- package/templates/skills/swarm-scripts/content.md +4 -2
- package/templates/skills/workflow-iterate/content.md +38 -0
- package/dist/artifact-eczsknc3.js +0 -3843
- package/dist/cli-9xsmdpdt.js +0 -56377
- package/dist/cli-bd957dgb.js +0 -153
- package/dist/cli-q51yprfq.js +0 -1104
- package/dist/cli-t1ze64h3.js +0 -811
- package/dist/index-9ch1n761.js +0 -1102
- package/src/artifact-sdk/index.ts +0 -2
- package/src/artifact-sdk/localtunnel.d.ts +0 -20
- package/src/artifact-sdk/port.ts +0 -12
- package/src/artifact-sdk/server.ts +0 -183
- package/src/artifact-sdk/tunnel.ts +0 -19
- package/src/commands/artifact.ts +0 -249
- package/src/http/ecosystem.ts +0 -77
- package/src/tests/artifact-commands.test.ts +0 -92
- package/src/tests/artifact-sdk.test.ts +0 -793
- package/src/tests/register-service-security.test.ts +0 -194
- package/src/tools/list-services.ts +0 -107
- package/src/tools/register-service.ts +0 -177
- package/src/tools/unregister-service.ts +0 -73
- package/src/tools/update-service-status.ts +0 -96
- package/templates/skills/artifacts/config.json +0 -14
- package/templates/skills/artifacts/content.md +0 -344
- package/templates/skills/artifacts/files/examples/approval-flow.ts +0 -34
- package/templates/skills/artifacts/files/examples/hono-dashboard.ts +0 -31
- package/templates/skills/artifacts/files/examples/multi-artifact.ts +0 -20
- package/templates/skills/artifacts/files/examples/static-report.sh +0 -17
- package/dist/{azure-openai-responses-ap1gwxgr.js → azure-openai-responses-ygfreq4t.js} +5 -5
- package/dist/{cli-5rqrj7q0.js → cli-pw7ebjdt.js} +0 -0
- package/dist/{openai-codex-responses-33qhkkdy.js → openai-codex-responses-3bk0grnr.js} +4 -4
- package/dist/{openai-completions-s88xm3ns.js → openai-completions-5d4jpakc.js} +9 -9
- package/dist/{openai-responses-7xd2f2e2.js → openai-responses-krb51f59.js} +5 -5
- package/dist/{pi-messages-xszk613n.js → pi-messages-x9gzynm1.js} +3 -3
- /package/src/{artifact-sdk → pages}/browser-sdk.ts +0 -0
- /package/src/{artifact-sdk → pages}/feedback-overlay.ts +0 -0
|
@@ -0,0 +1,708 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Azure Boards (work item) service-hook handlers.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors src/jira/sync.ts:
|
|
5
|
+
* - A work item assigned to the bot, or a bot mention in a description or
|
|
6
|
+
* comment, creates a swarm task linked to the work item via `tracker_sync`
|
|
7
|
+
* - `createTrackerSyncIfAbsent` claims the work item before the task exists,
|
|
8
|
+
* so concurrent deliveries cannot create two tasks
|
|
9
|
+
* - Deliveries are deduplicated on the service-hook notification id: an
|
|
10
|
+
* in-flight claim blocks concurrent retries, and a durable receipt is written
|
|
11
|
+
* only after the delivery was handled, so a failed one stays retryable
|
|
12
|
+
* - Work-item ids are organization-local, so tracker keys carry the organization
|
|
13
|
+
* - Events from an organization other than the configured one are dropped
|
|
14
|
+
* before any credentialed request; only actors mapped to a swarm user (or
|
|
15
|
+
* listed in AZURE_DEVOPS_ALLOWED_ACTORS) can create tasks
|
|
16
|
+
* - Deleting a work item cancels its task; restoring it re-engages the swarm
|
|
17
|
+
*
|
|
18
|
+
* Payload facts (verified against real deliveries, resourceVersion 1.0):
|
|
19
|
+
* - Identities in work-item fields are strings: "Display Name <unique@name>"
|
|
20
|
+
* - `workitem.updated`: `resource.id` is the update id, the work item is
|
|
21
|
+
* `resource.workItemId`, and the hook also fires on comments, so assignment
|
|
22
|
+
* is detected from the `System.AssignedTo` field change
|
|
23
|
+
* - `workitem.commented`: the comment HTML is `fields["System.History"]`
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { cancelTask, getKv, getTaskById, upsertKv } from "../be/db";
|
|
27
|
+
import {
|
|
28
|
+
createTrackerSyncIfAbsent,
|
|
29
|
+
deleteTrackerSync,
|
|
30
|
+
getTrackerSyncByExternalId,
|
|
31
|
+
hasTrackerDelivery,
|
|
32
|
+
markTrackerDelivery,
|
|
33
|
+
updateTrackerSyncSwarmId,
|
|
34
|
+
} from "../be/db-queries/tracker";
|
|
35
|
+
import { resolveTemplate } from "../prompts/resolver";
|
|
36
|
+
import { azureDevOpsWorkItemContextKey } from "../tasks/context-key";
|
|
37
|
+
import { createTaskWithSiblingAwareness } from "../tasks/sibling-awareness";
|
|
38
|
+
import type { TrackerSync } from "../tracker/types";
|
|
39
|
+
import { isTerminalTaskStatus } from "../types";
|
|
40
|
+
import { resolveAzureDevOpsBotIdentity } from "./api";
|
|
41
|
+
import {
|
|
42
|
+
AZURE_DEVOPS_BOT_NAME,
|
|
43
|
+
getAzureDevOpsOrgUrl,
|
|
44
|
+
isTrustedOrgUrl,
|
|
45
|
+
normalizeOrgUrl,
|
|
46
|
+
orgKeyOf,
|
|
47
|
+
} from "./auth";
|
|
48
|
+
import {
|
|
49
|
+
authorizeAzureDevOpsActor,
|
|
50
|
+
detectMention,
|
|
51
|
+
escapeRegExp,
|
|
52
|
+
findLeadAgent,
|
|
53
|
+
renderAzureDevOpsIdentity,
|
|
54
|
+
warnMalformed,
|
|
55
|
+
} from "./handlers";
|
|
56
|
+
import { htmlToText } from "./html";
|
|
57
|
+
// Side-effect import: registers all Azure DevOps event templates in the in-memory registry
|
|
58
|
+
import "./templates";
|
|
59
|
+
import type {
|
|
60
|
+
AzureDevOpsIdentityRef,
|
|
61
|
+
AzureDevOpsWorkItem,
|
|
62
|
+
AzureDevOpsWorkItemFields,
|
|
63
|
+
AzureDevOpsWorkItemUpdate,
|
|
64
|
+
WorkItemCommentedEvent,
|
|
65
|
+
WorkItemCreatedEvent,
|
|
66
|
+
WorkItemDeletedEvent,
|
|
67
|
+
WorkItemRestoredEvent,
|
|
68
|
+
WorkItemUpdatedEvent,
|
|
69
|
+
} from "./types";
|
|
70
|
+
|
|
71
|
+
type HandlerResult = { created: boolean; taskId?: string };
|
|
72
|
+
|
|
73
|
+
const PROVIDER = "azure-devops";
|
|
74
|
+
const DELETED_REASON = "Azure DevOps work item deleted";
|
|
75
|
+
const DESCRIPTION_LIMIT = 4000;
|
|
76
|
+
|
|
77
|
+
// ── Payload guards ──
|
|
78
|
+
|
|
79
|
+
function hasFields(value: unknown): value is AzureDevOpsWorkItemFields {
|
|
80
|
+
return typeof value === "object" && value !== null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** The work item of a created / commented / deleted / restored event, or null when malformed. */
|
|
84
|
+
export function workItemOf(
|
|
85
|
+
event:
|
|
86
|
+
| WorkItemCreatedEvent
|
|
87
|
+
| WorkItemCommentedEvent
|
|
88
|
+
| WorkItemDeletedEvent
|
|
89
|
+
| WorkItemRestoredEvent
|
|
90
|
+
| null
|
|
91
|
+
| undefined,
|
|
92
|
+
): AzureDevOpsWorkItem | null {
|
|
93
|
+
const resource = event?.resource;
|
|
94
|
+
if (typeof event?.id !== "string" || typeof resource?.id !== "number") return null;
|
|
95
|
+
return hasFields(resource.fields) ? resource : null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** The update of a `workitem.updated` event, or null when malformed. */
|
|
99
|
+
export function updatedPayloadOf(
|
|
100
|
+
event: WorkItemUpdatedEvent | null | undefined,
|
|
101
|
+
): AzureDevOpsWorkItemUpdate | null {
|
|
102
|
+
const resource = event?.resource;
|
|
103
|
+
if (typeof event?.id !== "string" || typeof resource?.workItemId !== "number") return null;
|
|
104
|
+
return hasFields(resource.revision?.fields) ? resource : null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ── Identity and mention helpers ──
|
|
108
|
+
|
|
109
|
+
/** Parse `"Display Name <unique@name>"`; a bare string is a display name. */
|
|
110
|
+
export function parseIdentityString(value: unknown): AzureDevOpsIdentityRef | null {
|
|
111
|
+
if (typeof value !== "string" || !value.trim()) return null;
|
|
112
|
+
const match = value.match(/^(.*?)\s*<([^>]+)>\s*$/);
|
|
113
|
+
const displayName = (match?.[1] || value).trim();
|
|
114
|
+
const uniqueName = match?.[2]?.trim();
|
|
115
|
+
return { id: uniqueName ?? displayName, displayName, uniqueName };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
interface Bot {
|
|
119
|
+
id: string | null;
|
|
120
|
+
uniqueName: string | null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Throws AzureDevOpsLookupError on a transient failure, so the delivery is retried. */
|
|
124
|
+
async function resolveBot(orgUrl: string | null): Promise<Bot> {
|
|
125
|
+
return await resolveAzureDevOpsBotIdentity(orgUrl);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function isBotIdentity(identity: AzureDevOpsIdentityRef | null, bot: Bot): boolean {
|
|
129
|
+
if (!identity) return false;
|
|
130
|
+
if (bot.id && identity.id.toLowerCase() === bot.id.toLowerCase()) return true;
|
|
131
|
+
const name = identity.uniqueName?.toLowerCase();
|
|
132
|
+
return !!name && !!bot.uniqueName && name === bot.uniqueName.toLowerCase();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function isBotString(value: unknown, bot: Bot): boolean {
|
|
136
|
+
return isBotIdentity(parseIdentityString(value), bot);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** `<a data-vss-mention="version:2.0,{GUID}">@Name</a>`, as the identity picker writes it. */
|
|
140
|
+
function pickerMentionPattern(botId: string): RegExp {
|
|
141
|
+
return new RegExp(
|
|
142
|
+
`<a\\b[^>]*data-vss-mention="version:[\\d.]+,${escapeRegExp(botId)}"[^>]*>.*?</a>`,
|
|
143
|
+
"gi",
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Plain-text patterns: `@AZURE_DEVOPS_BOT_NAME` and `@<local part of the bot sign-in name>`. */
|
|
148
|
+
function plainMentionPatterns(bot: Bot): RegExp[] {
|
|
149
|
+
const names = [AZURE_DEVOPS_BOT_NAME];
|
|
150
|
+
const local = bot.uniqueName?.split("@")[0];
|
|
151
|
+
if (local) names.push(local);
|
|
152
|
+
return names.map((name) => new RegExp(`@${escapeRegExp(name)}(?![\\w-])`, "gi"));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Whether `html` mentions the bot, and the text with the mention removed. */
|
|
156
|
+
function inspectMention(
|
|
157
|
+
html: string | null | undefined,
|
|
158
|
+
bot: Bot,
|
|
159
|
+
): { mentioned: boolean; text: string } {
|
|
160
|
+
if (!html) return { mentioned: false, text: "" };
|
|
161
|
+
let withoutPicker = html;
|
|
162
|
+
let mentioned = false;
|
|
163
|
+
if (bot.id) {
|
|
164
|
+
const re = pickerMentionPattern(bot.id);
|
|
165
|
+
mentioned = re.test(html);
|
|
166
|
+
withoutPicker = html.replace(re, "");
|
|
167
|
+
}
|
|
168
|
+
let text = htmlToText(withoutPicker);
|
|
169
|
+
const plain = plainMentionPatterns(bot);
|
|
170
|
+
if (plain.some((re) => re.test(text)) || detectMention(text, null)) {
|
|
171
|
+
mentioned = true;
|
|
172
|
+
}
|
|
173
|
+
for (const re of plain) text = text.replace(re, "");
|
|
174
|
+
return { mentioned, text: text.trim() };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ── Work item location ──
|
|
178
|
+
|
|
179
|
+
interface Org {
|
|
180
|
+
/** Trusted organization URL, no trailing slash. */
|
|
181
|
+
url: string;
|
|
182
|
+
/** Canonical, lowercased organization identity, e.g. `dev.azure.com/acme`. */
|
|
183
|
+
key: string;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* The organization a delivery belongs to, or null when it must be dropped.
|
|
188
|
+
* Payload URLs are attacker-influenced, so they only select among trusted
|
|
189
|
+
* organizations: with AZURE_DEVOPS_ORG_URL set they must match it, otherwise
|
|
190
|
+
* they must be public Azure DevOps HTTPS URLs. Nothing credentialed runs
|
|
191
|
+
* before this check.
|
|
192
|
+
*/
|
|
193
|
+
function orgFor(event: {
|
|
194
|
+
resource?: { url?: string };
|
|
195
|
+
resourceContainers?: {
|
|
196
|
+
account?: { baseUrl?: string };
|
|
197
|
+
collection?: { baseUrl?: string };
|
|
198
|
+
};
|
|
199
|
+
}): Org | null {
|
|
200
|
+
const containers = event.resourceContainers;
|
|
201
|
+
const fromUrl = event.resource?.url?.split("/_apis/")[0];
|
|
202
|
+
const candidates = [
|
|
203
|
+
containers?.account?.baseUrl,
|
|
204
|
+
containers?.collection?.baseUrl,
|
|
205
|
+
fromUrl?.includes("://") ? fromUrl : undefined,
|
|
206
|
+
]
|
|
207
|
+
.map((url) => normalizeOrgUrl(url))
|
|
208
|
+
.filter((url): url is string => !!url);
|
|
209
|
+
const configured = getAzureDevOpsOrgUrl();
|
|
210
|
+
const url = candidates[0] ?? configured;
|
|
211
|
+
if (!url || !isTrustedOrgUrl(url)) return null;
|
|
212
|
+
// Every payload source must agree on one organization.
|
|
213
|
+
const key = orgKeyOf(url);
|
|
214
|
+
if (!key || candidates.some((c) => orgKeyOf(c) !== key)) return null;
|
|
215
|
+
return { url, key };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function dropUntrustedOrg(eventType: string): HandlerResult {
|
|
219
|
+
console.warn(`[AzureDevOps] Dropping ${eventType} event from an untrusted organization`);
|
|
220
|
+
return { created: false };
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** Tracker `externalId`: work-item ids repeat across organizations. */
|
|
224
|
+
function trackerKey(org: Org, workItemId: number): string {
|
|
225
|
+
return `${org.key}#${workItemId}`;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** Organization name: the path segment of `dev.azure.com/{org}`, or the host label of `{org}.visualstudio.com`. */
|
|
229
|
+
function organizationOf(orgUrl: string | null): string {
|
|
230
|
+
if (!orgUrl) return "unknown";
|
|
231
|
+
try {
|
|
232
|
+
const url = new URL(orgUrl);
|
|
233
|
+
const segment = url.pathname.split("/").find(Boolean);
|
|
234
|
+
return (segment ?? url.hostname.split(".")[0] ?? "unknown").replace(/:/g, "_");
|
|
235
|
+
} catch {
|
|
236
|
+
return "unknown";
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
interface WorkItemContext {
|
|
241
|
+
eventId: string;
|
|
242
|
+
eventType: string;
|
|
243
|
+
workItemId: number;
|
|
244
|
+
fields: AzureDevOpsWorkItemFields;
|
|
245
|
+
org: Org;
|
|
246
|
+
/** Person that caused the event, as an identity object. */
|
|
247
|
+
actor: AzureDevOpsIdentityRef | null;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function workItemUrl(ctx: WorkItemContext): string {
|
|
251
|
+
const project = ctx.fields["System.TeamProject"];
|
|
252
|
+
const base = ctx.org.url;
|
|
253
|
+
return project
|
|
254
|
+
? `${base}/${encodeURIComponent(project)}/_workitems/edit/${ctx.workItemId}`
|
|
255
|
+
: `${base}/_workitems/edit/${ctx.workItemId}`;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// ── Dedup and serialization ──
|
|
259
|
+
|
|
260
|
+
const RECEIPT_NAMESPACE = "integration:azure-devops:deliveries";
|
|
261
|
+
const RECEIPT_TTL_MS = 7 * 24 * 60 * 60 * 1000;
|
|
262
|
+
const inFlight = new Map<string, Promise<HandlerResult>>();
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Run `fn` once per `key`. A concurrent delivery with the same key shares the
|
|
266
|
+
* first attempt's promise, so it observes that attempt's success or failure
|
|
267
|
+
* instead of being acknowledged early. The durable receipt is only written after
|
|
268
|
+
* `fn` returned, so a thrown error leaves every waiting delivery retryable.
|
|
269
|
+
*/
|
|
270
|
+
function withReceipt(key: string, fn: () => Promise<HandlerResult>): Promise<HandlerResult> {
|
|
271
|
+
const pending = inFlight.get(key);
|
|
272
|
+
// A waiter rethrows the first attempt's failure; on success it is a duplicate, so it creates nothing.
|
|
273
|
+
if (pending) return pending.then(() => ({ created: false }));
|
|
274
|
+
const attempt = (async (): Promise<HandlerResult> => {
|
|
275
|
+
if ((await getKv(RECEIPT_NAMESPACE, key)) || (await hasTrackerDelivery(PROVIDER, key))) {
|
|
276
|
+
return { created: false };
|
|
277
|
+
}
|
|
278
|
+
const result = await fn();
|
|
279
|
+
await upsertKv({
|
|
280
|
+
namespace: RECEIPT_NAMESPACE,
|
|
281
|
+
key,
|
|
282
|
+
value: { handledAt: new Date().toISOString() },
|
|
283
|
+
valueType: "json",
|
|
284
|
+
expiresAt: Date.now() + RECEIPT_TTL_MS,
|
|
285
|
+
});
|
|
286
|
+
return result;
|
|
287
|
+
})().finally(() => {
|
|
288
|
+
inFlight.delete(key);
|
|
289
|
+
});
|
|
290
|
+
inFlight.set(key, attempt);
|
|
291
|
+
return attempt;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const workItemLocks = new Map<string, Promise<unknown>>();
|
|
295
|
+
|
|
296
|
+
/** Serialize handling per work item (the API server is the single DB owner, so in-process is enough). */
|
|
297
|
+
async function withWorkItemLock<T>(key: string, fn: () => Promise<T>): Promise<T> {
|
|
298
|
+
const run = (workItemLocks.get(key) ?? Promise.resolve()).then(fn);
|
|
299
|
+
const tail = run.catch(() => {});
|
|
300
|
+
workItemLocks.set(key, tail);
|
|
301
|
+
try {
|
|
302
|
+
return await run;
|
|
303
|
+
} finally {
|
|
304
|
+
if (workItemLocks.get(key) === tail) workItemLocks.delete(key);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/** Gate, receipt and lock shared by every work-item event. */
|
|
309
|
+
async function handleDelivery(
|
|
310
|
+
org: Org,
|
|
311
|
+
eventId: string,
|
|
312
|
+
workItemId: number,
|
|
313
|
+
fn: () => Promise<HandlerResult>,
|
|
314
|
+
extraReceiptKey?: string,
|
|
315
|
+
): Promise<HandlerResult> {
|
|
316
|
+
const locked = () => withWorkItemLock(trackerKey(org, workItemId), fn);
|
|
317
|
+
return await withReceipt(eventId, () =>
|
|
318
|
+
extraReceiptKey ? withReceipt(extraReceiptKey, locked) : locked(),
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// ── Task creation ──
|
|
323
|
+
|
|
324
|
+
type Trigger = "assigned" | "mentioned_description" | "mentioned_comment" | "restored";
|
|
325
|
+
|
|
326
|
+
const TRIGGER_LABEL: Record<Trigger, string> = {
|
|
327
|
+
assigned: "work item assigned to the bot",
|
|
328
|
+
mentioned_description: "bot mentioned in the description",
|
|
329
|
+
mentioned_comment: "bot mentioned in a comment",
|
|
330
|
+
restored: "work item restored",
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
async function createWorkItemTask(
|
|
334
|
+
ctx: WorkItemContext,
|
|
335
|
+
trigger: Trigger,
|
|
336
|
+
opts: {
|
|
337
|
+
followup: boolean;
|
|
338
|
+
requestedByUserId: string;
|
|
339
|
+
commentText?: string;
|
|
340
|
+
parentTaskId?: string;
|
|
341
|
+
existingTaskId?: string;
|
|
342
|
+
},
|
|
343
|
+
): Promise<string | null> {
|
|
344
|
+
const { fields } = ctx;
|
|
345
|
+
const description = htmlToText(fields["System.Description"]).slice(0, DESCRIPTION_LIMIT);
|
|
346
|
+
const common = {
|
|
347
|
+
work_item_id: ctx.workItemId,
|
|
348
|
+
work_item_type: fields["System.WorkItemType"] ?? "Work item",
|
|
349
|
+
title: fields["System.Title"] ?? "(no title)",
|
|
350
|
+
state: fields["System.State"] ?? "unknown",
|
|
351
|
+
project: fields["System.TeamProject"] ?? "unknown",
|
|
352
|
+
work_item_url: workItemUrl(ctx),
|
|
353
|
+
username: ctx.actor ? await renderAzureDevOpsIdentity(ctx.actor) : "UNKNOWN",
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
const templateName = opts.followup
|
|
357
|
+
? "azure-devops.work_item.followup"
|
|
358
|
+
: trigger === "mentioned_comment"
|
|
359
|
+
? "azure-devops.work_item.commented"
|
|
360
|
+
: "azure-devops.work_item.assigned";
|
|
361
|
+
const variables =
|
|
362
|
+
templateName === "azure-devops.work_item.followup"
|
|
363
|
+
? { ...common, trigger: TRIGGER_LABEL[trigger], user_message: opts.commentText ?? "" }
|
|
364
|
+
: templateName === "azure-devops.work_item.commented"
|
|
365
|
+
? {
|
|
366
|
+
...common,
|
|
367
|
+
context: opts.commentText ?? "",
|
|
368
|
+
existing_task_note: opts.existingTaskId
|
|
369
|
+
? `\n\n_Note: There's an active task (${opts.existingTaskId}) for this work item._`
|
|
370
|
+
: "",
|
|
371
|
+
}
|
|
372
|
+
: {
|
|
373
|
+
...common,
|
|
374
|
+
trigger: TRIGGER_LABEL[trigger],
|
|
375
|
+
description_section: description ? `Description:\n${description}\n\n` : "",
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
const result = resolveTemplate(templateName, variables);
|
|
379
|
+
if (result.skipped) return null;
|
|
380
|
+
|
|
381
|
+
const lead = await findLeadAgent();
|
|
382
|
+
const task = await createTaskWithSiblingAwareness(
|
|
383
|
+
result.text,
|
|
384
|
+
{
|
|
385
|
+
agentId: lead?.id ?? null,
|
|
386
|
+
routingReason: lead ? "skill" : undefined,
|
|
387
|
+
routingSource: lead ? "engine_default" : undefined,
|
|
388
|
+
source: "azure-devops",
|
|
389
|
+
taskType: "azure-devops-work-item",
|
|
390
|
+
requestedByUserId: opts.requestedByUserId,
|
|
391
|
+
parentTaskId: opts.parentTaskId,
|
|
392
|
+
contextKey: azureDevOpsWorkItemContextKey({
|
|
393
|
+
organization: organizationOf(ctx.org.url),
|
|
394
|
+
workItemId: ctx.workItemId,
|
|
395
|
+
}),
|
|
396
|
+
},
|
|
397
|
+
{ origin: "webhook" },
|
|
398
|
+
);
|
|
399
|
+
return task.id;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Only actors mapped to a swarm user (or allowlisted) may create tasks: the
|
|
404
|
+
* task text is untrusted work-item content handed to a PAT-enabled agent.
|
|
405
|
+
*/
|
|
406
|
+
async function authorizeActor(ctx: WorkItemContext, trigger: Trigger): Promise<string | null> {
|
|
407
|
+
const userId = ctx.actor
|
|
408
|
+
? await authorizeAzureDevOpsActor(ctx.actor, "work_item", `${trigger} #${ctx.workItemId}`)
|
|
409
|
+
: null;
|
|
410
|
+
if (!userId) {
|
|
411
|
+
console.warn(
|
|
412
|
+
`[AzureDevOps] Ignoring ${TRIGGER_LABEL[trigger]} on work item #${ctx.workItemId}: actor is not a known swarm user`,
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
return userId;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Claim the work item in `tracker_sync`, then create or follow up a task:
|
|
420
|
+
* - new link → initial task
|
|
421
|
+
* - linked task still active → assignment is ignored; a mention creates a
|
|
422
|
+
* child task and leaves the link on the original task
|
|
423
|
+
* - linked task terminal (or orphan link) → follow-up task, link repointed
|
|
424
|
+
*
|
|
425
|
+
* Callers hold the work-item lock, so a `pending:` link seen here is an
|
|
426
|
+
* abandoned claim from a crashed delivery, not one in flight.
|
|
427
|
+
*/
|
|
428
|
+
async function routeToTask(
|
|
429
|
+
ctx: WorkItemContext,
|
|
430
|
+
trigger: Trigger,
|
|
431
|
+
commentText?: string,
|
|
432
|
+
): Promise<HandlerResult> {
|
|
433
|
+
const requestedByUserId = await authorizeActor(ctx, trigger);
|
|
434
|
+
if (!requestedByUserId) return { created: false };
|
|
435
|
+
|
|
436
|
+
const externalId = trackerKey(ctx.org, ctx.workItemId);
|
|
437
|
+
const claim = await createTrackerSyncIfAbsent({
|
|
438
|
+
provider: PROVIDER,
|
|
439
|
+
entityType: "task",
|
|
440
|
+
providerEntityType: "WorkItem",
|
|
441
|
+
// Unique placeholder (UNIQUE(provider, entityType, swarmId)), replaced once the task exists.
|
|
442
|
+
swarmId: `pending:${PROVIDER}:${externalId}`,
|
|
443
|
+
externalId,
|
|
444
|
+
externalIdentifier: `${ctx.fields["System.TeamProject"] ?? "unknown"}#${ctx.workItemId}`,
|
|
445
|
+
externalUrl: workItemUrl(ctx),
|
|
446
|
+
lastSyncOrigin: "external",
|
|
447
|
+
syncDirection: "inbound",
|
|
448
|
+
});
|
|
449
|
+
const mentioned = trigger === "mentioned_comment" || trigger === "mentioned_description";
|
|
450
|
+
|
|
451
|
+
if (claim.inserted) {
|
|
452
|
+
let taskId: string | null = null;
|
|
453
|
+
try {
|
|
454
|
+
taskId = await createWorkItemTask(ctx, trigger, {
|
|
455
|
+
followup: false,
|
|
456
|
+
requestedByUserId,
|
|
457
|
+
commentText,
|
|
458
|
+
});
|
|
459
|
+
if (taskId) {
|
|
460
|
+
await updateTrackerSyncSwarmId(claim.sync.id, taskId);
|
|
461
|
+
await markTrackerDelivery(PROVIDER, "task", externalId, ctx.eventId);
|
|
462
|
+
}
|
|
463
|
+
} finally {
|
|
464
|
+
// No task: release the claim so a retry starts clean.
|
|
465
|
+
if (!taskId) await deleteTrackerSync(claim.sync.id);
|
|
466
|
+
}
|
|
467
|
+
return taskId ? { created: true, taskId } : { created: false };
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
const prior =
|
|
471
|
+
claim.sync.swarmId && !claim.sync.swarmId.startsWith("pending:")
|
|
472
|
+
? await getTaskById(claim.sync.swarmId)
|
|
473
|
+
: null;
|
|
474
|
+
if (prior && !isTerminalTaskStatus(prior.status)) {
|
|
475
|
+
if (!mentioned) {
|
|
476
|
+
console.log(
|
|
477
|
+
`[AzureDevOps] Work item #${ctx.workItemId} re-triggered but task ${prior.id} is ${prior.status}, ignoring`,
|
|
478
|
+
);
|
|
479
|
+
return { created: false };
|
|
480
|
+
}
|
|
481
|
+
const taskId = await createWorkItemTask(ctx, trigger, {
|
|
482
|
+
followup: false,
|
|
483
|
+
requestedByUserId,
|
|
484
|
+
commentText,
|
|
485
|
+
parentTaskId: prior.id,
|
|
486
|
+
existingTaskId: prior.id,
|
|
487
|
+
});
|
|
488
|
+
if (taskId) await markTrackerDelivery(PROVIDER, "task", externalId, ctx.eventId);
|
|
489
|
+
return taskId ? { created: true, taskId } : { created: false };
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
const taskId = await createWorkItemTask(ctx, trigger, {
|
|
493
|
+
followup: true,
|
|
494
|
+
requestedByUserId,
|
|
495
|
+
commentText,
|
|
496
|
+
});
|
|
497
|
+
if (!taskId) return { created: false };
|
|
498
|
+
await updateTrackerSyncSwarmId(claim.sync.id, taskId);
|
|
499
|
+
await markTrackerDelivery(PROVIDER, "task", externalId, ctx.eventId);
|
|
500
|
+
return { created: true, taskId };
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/** Assigned to the bot, else the description mentions it. */
|
|
504
|
+
async function triggerFromFields(
|
|
505
|
+
fields: AzureDevOpsWorkItemFields,
|
|
506
|
+
bot: Bot,
|
|
507
|
+
): Promise<Trigger | null> {
|
|
508
|
+
if (isBotString(fields["System.AssignedTo"], bot)) return "assigned";
|
|
509
|
+
return inspectMention(fields["System.Description"], bot).mentioned
|
|
510
|
+
? "mentioned_description"
|
|
511
|
+
: null;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// ── Event handlers ──
|
|
515
|
+
|
|
516
|
+
/** `workitem.created`: assigned to the bot, or the description mentions it. */
|
|
517
|
+
export async function handleWorkItemCreated(event: WorkItemCreatedEvent): Promise<HandlerResult> {
|
|
518
|
+
const item = workItemOf(event);
|
|
519
|
+
if (!item) {
|
|
520
|
+
warnMalformed(event);
|
|
521
|
+
return { created: false };
|
|
522
|
+
}
|
|
523
|
+
const org = orgFor(event);
|
|
524
|
+
if (!org) return dropUntrustedOrg(event.eventType);
|
|
525
|
+
|
|
526
|
+
return await handleDelivery(org, event.id, item.id, async () => {
|
|
527
|
+
const bot = await resolveBot(org.url);
|
|
528
|
+
const actor = parseIdentityString(item.fields["System.ChangedBy"]);
|
|
529
|
+
if (isBotIdentity(actor, bot)) return { created: false };
|
|
530
|
+
|
|
531
|
+
const trigger = await triggerFromFields(item.fields, bot);
|
|
532
|
+
if (!trigger) return { created: false };
|
|
533
|
+
|
|
534
|
+
return await routeToTask(
|
|
535
|
+
{
|
|
536
|
+
eventId: event.id,
|
|
537
|
+
eventType: event.eventType,
|
|
538
|
+
workItemId: item.id,
|
|
539
|
+
fields: item.fields,
|
|
540
|
+
org,
|
|
541
|
+
actor,
|
|
542
|
+
},
|
|
543
|
+
trigger,
|
|
544
|
+
);
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
/**
|
|
549
|
+
* `workitem.updated`: only a change of `System.AssignedTo` to the bot triggers.
|
|
550
|
+
* The hook also fires on comments and every other edit, so the field diff is
|
|
551
|
+
* the filter, not the subscription.
|
|
552
|
+
*/
|
|
553
|
+
export async function handleWorkItemUpdated(event: WorkItemUpdatedEvent): Promise<HandlerResult> {
|
|
554
|
+
const update = updatedPayloadOf(event);
|
|
555
|
+
if (!update) {
|
|
556
|
+
warnMalformed(event);
|
|
557
|
+
return { created: false };
|
|
558
|
+
}
|
|
559
|
+
const change = update.fields?.["System.AssignedTo"];
|
|
560
|
+
if (!change) return { created: false };
|
|
561
|
+
const org = orgFor(event);
|
|
562
|
+
if (!org) return dropUntrustedOrg(event.eventType);
|
|
563
|
+
|
|
564
|
+
return await handleDelivery(org, event.id, update.workItemId, async () => {
|
|
565
|
+
const bot = await resolveBot(org.url);
|
|
566
|
+
if (!isBotString(change.newValue, bot) || isBotString(change.oldValue, bot)) {
|
|
567
|
+
return { created: false };
|
|
568
|
+
}
|
|
569
|
+
if (isBotIdentity(update.revisedBy ?? null, bot)) return { created: false };
|
|
570
|
+
|
|
571
|
+
return await routeToTask(
|
|
572
|
+
{
|
|
573
|
+
eventId: event.id,
|
|
574
|
+
eventType: event.eventType,
|
|
575
|
+
workItemId: update.workItemId,
|
|
576
|
+
fields: update.revision.fields,
|
|
577
|
+
org,
|
|
578
|
+
actor: update.revisedBy ?? null,
|
|
579
|
+
},
|
|
580
|
+
"assigned",
|
|
581
|
+
);
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/** `workitem.commented`: a bot mention in a newly published comment. */
|
|
586
|
+
export async function handleWorkItemCommented(
|
|
587
|
+
event: WorkItemCommentedEvent,
|
|
588
|
+
): Promise<HandlerResult> {
|
|
589
|
+
const item = workItemOf(event);
|
|
590
|
+
if (!item) {
|
|
591
|
+
warnMalformed(event);
|
|
592
|
+
return { created: false };
|
|
593
|
+
}
|
|
594
|
+
// An edit re-fires the event with a higher comment version; only the first
|
|
595
|
+
// version triggers, so fixing a typo in a mention does not spawn a second task.
|
|
596
|
+
const ref = item.commentVersionRef;
|
|
597
|
+
if (ref && ref.version > 1) return { created: false };
|
|
598
|
+
const org = orgFor(event);
|
|
599
|
+
if (!org) return dropUntrustedOrg(event.eventType);
|
|
600
|
+
|
|
601
|
+
const commentKey = ref ? `comment:${trackerKey(org, item.id)}:${ref.commentId}` : undefined;
|
|
602
|
+
return await handleDelivery(
|
|
603
|
+
org,
|
|
604
|
+
event.id,
|
|
605
|
+
item.id,
|
|
606
|
+
async () => {
|
|
607
|
+
const bot = await resolveBot(org.url);
|
|
608
|
+
const actor = parseIdentityString(item.fields["System.ChangedBy"]);
|
|
609
|
+
if (isBotIdentity(actor, bot)) return { created: false };
|
|
610
|
+
|
|
611
|
+
const { mentioned, text } = inspectMention(item.fields["System.History"], bot);
|
|
612
|
+
if (!mentioned) return { created: false };
|
|
613
|
+
|
|
614
|
+
return await routeToTask(
|
|
615
|
+
{
|
|
616
|
+
eventId: event.id,
|
|
617
|
+
eventType: event.eventType,
|
|
618
|
+
workItemId: item.id,
|
|
619
|
+
fields: item.fields,
|
|
620
|
+
org,
|
|
621
|
+
actor,
|
|
622
|
+
},
|
|
623
|
+
"mentioned_comment",
|
|
624
|
+
text,
|
|
625
|
+
);
|
|
626
|
+
},
|
|
627
|
+
commentKey,
|
|
628
|
+
);
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
/** `workitem.deleted`: cancel the linked task if it is still active. The link stays for a restore. */
|
|
632
|
+
export async function handleWorkItemDeleted(event: WorkItemDeletedEvent): Promise<HandlerResult> {
|
|
633
|
+
const item = workItemOf(event);
|
|
634
|
+
if (!item) {
|
|
635
|
+
warnMalformed(event);
|
|
636
|
+
return { created: false };
|
|
637
|
+
}
|
|
638
|
+
const org = orgFor(event);
|
|
639
|
+
if (!org) return dropUntrustedOrg(event.eventType);
|
|
640
|
+
|
|
641
|
+
return await handleDelivery(org, event.id, item.id, async () => {
|
|
642
|
+
const externalId = trackerKey(org, item.id);
|
|
643
|
+
const sync = await getTrackerSyncByExternalId(PROVIDER, "task", externalId);
|
|
644
|
+
if (!sync) return { created: false };
|
|
645
|
+
|
|
646
|
+
const task =
|
|
647
|
+
sync.swarmId && !sync.swarmId.startsWith("pending:") ? await getTaskById(sync.swarmId) : null;
|
|
648
|
+
if (task && !isTerminalTaskStatus(task.status)) {
|
|
649
|
+
await cancelTask(task.id, DELETED_REASON);
|
|
650
|
+
console.log(`[AzureDevOps] Cancelled task ${task.id} (work item #${item.id} deleted)`);
|
|
651
|
+
}
|
|
652
|
+
await markTrackerDelivery(PROVIDER, "task", externalId, event.id);
|
|
653
|
+
return { created: false };
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
/**
|
|
658
|
+
* `workitem.restored`: the inverse of deleted.
|
|
659
|
+
* - Linked, and the task was cancelled by the delete: re-engage with a
|
|
660
|
+
* follow-up task when the work item is still assigned to the bot.
|
|
661
|
+
* - Linked otherwise: nothing to undo.
|
|
662
|
+
* - Never linked: handled like `workitem.created`.
|
|
663
|
+
*/
|
|
664
|
+
export async function handleWorkItemRestored(event: WorkItemRestoredEvent): Promise<HandlerResult> {
|
|
665
|
+
const item = workItemOf(event);
|
|
666
|
+
if (!item) {
|
|
667
|
+
warnMalformed(event);
|
|
668
|
+
return { created: false };
|
|
669
|
+
}
|
|
670
|
+
const org = orgFor(event);
|
|
671
|
+
if (!org) return dropUntrustedOrg(event.eventType);
|
|
672
|
+
|
|
673
|
+
return await handleDelivery(org, event.id, item.id, async () => {
|
|
674
|
+
const bot = await resolveBot(org.url);
|
|
675
|
+
const actor = parseIdentityString(item.fields["System.ChangedBy"]);
|
|
676
|
+
const ctx: WorkItemContext = {
|
|
677
|
+
eventId: event.id,
|
|
678
|
+
eventType: event.eventType,
|
|
679
|
+
workItemId: item.id,
|
|
680
|
+
fields: item.fields,
|
|
681
|
+
org,
|
|
682
|
+
actor,
|
|
683
|
+
};
|
|
684
|
+
|
|
685
|
+
const externalId = trackerKey(org, item.id);
|
|
686
|
+
const sync: TrackerSync | null = await getTrackerSyncByExternalId(PROVIDER, "task", externalId);
|
|
687
|
+
if (!sync) {
|
|
688
|
+
if (isBotIdentity(actor, bot)) return { created: false };
|
|
689
|
+
const trigger = await triggerFromFields(item.fields, bot);
|
|
690
|
+
return trigger ? await routeToTask(ctx, trigger) : { created: false };
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
const task =
|
|
694
|
+
sync.swarmId && !sync.swarmId.startsWith("pending:") ? await getTaskById(sync.swarmId) : null;
|
|
695
|
+
const cancelledByDelete = task?.status === "cancelled" && task.failureReason === DELETED_REASON;
|
|
696
|
+
if (!cancelledByDelete || !isBotString(item.fields["System.AssignedTo"], bot)) {
|
|
697
|
+
await markTrackerDelivery(PROVIDER, "task", externalId, event.id);
|
|
698
|
+
return { created: false };
|
|
699
|
+
}
|
|
700
|
+
const requestedByUserId = await authorizeActor(ctx, "restored");
|
|
701
|
+
if (!requestedByUserId) return { created: false };
|
|
702
|
+
const taskId = await createWorkItemTask(ctx, "restored", { followup: true, requestedByUserId });
|
|
703
|
+
if (!taskId) return { created: false };
|
|
704
|
+
await updateTrackerSyncSwarmId(sync.id, taskId);
|
|
705
|
+
await markTrackerDelivery(PROVIDER, "task", externalId, event.id);
|
|
706
|
+
return { created: true, taskId };
|
|
707
|
+
});
|
|
708
|
+
}
|