@desplega.ai/agent-swarm 2.5.4 → 2.7.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 +4 -2
- package/dist/{acp-adapter-ek38cwsv.js → acp-adapter-ma1js5n7.js} +9 -9
- package/dist/{actions-92mepq03.js → actions-1vhmtvg9.js} +21 -21
- package/dist/{amp-adapter-9gaxg015.js → amp-adapter-sxqqvmdk.js} +4 -4
- package/dist/{app-ctn8gsgx.js → app-mwx1d5a9.js} +3 -3
- package/dist/{assistant-zyr5kz47.js → assistant-yx1p6g82.js} +30 -26
- package/dist/{boot-reembed-w75maga2.js → boot-reembed-5h37a2c2.js} +1 -1
- package/dist/{boot-reembed-dwzfttdr.js → boot-reembed-7h78vmvv.js} +2 -1
- package/dist/{boot-scrub-sweep-e9kq43kf.js → boot-scrub-sweep-8mawd4gw.js} +4 -4
- package/dist/{boot-seeds-x28v3fhb.js → boot-seeds-ca84dhnn.js} +4 -4
- package/dist/{claude-adapter-f0afqnne.js → claude-adapter-wqt4hfkq.js} +9 -9
- package/dist/{claude-managed-adapter-x81a7r65.js → claude-managed-adapter-6fjqtr2z.js} +2 -2
- package/dist/{claude-managed-setup-bqzwjd9x.js → claude-managed-setup-rqv98sea.js} +10 -3
- package/dist/{claude-sdk-session-7b7whkaq.js → claude-sdk-session-f29f3b2x.js} +11 -11
- package/dist/{cli-sxgtf5fw.js → cli-07q03xm2.js} +1 -1
- package/dist/{cli-kxpfx2xr.js → cli-0z1xjzwa.js} +2 -2
- package/dist/{cli-g9vd9ece.js → cli-1hhbzkbb.js} +14 -16
- package/dist/{cli-qqzmjxkf.js → cli-1m9x8dr5.js} +11 -6
- package/dist/{cli-jbnn4vxb.js → cli-2pvzmas1.js} +3 -3
- package/dist/{cli-x77gen2p.js → cli-2y7v133v.js} +7 -7
- package/dist/{cli-dew6x255.js → cli-39aet655.js} +2 -2
- package/dist/{cli-g1cw7xtt.js → cli-39rrw8yp.js} +1 -1
- package/dist/{cli-jb30dzvk.js → cli-48gxjmcz.js} +530 -190
- package/dist/{cli-pzatvw94.js → cli-54sj64fc.js} +2 -2
- package/dist/{cli-bvpgr9ny.js → cli-60sty27p.js} +5 -5
- package/dist/{cli-asq5ydm4.js → cli-6a9qfa8b.js} +411 -69
- package/dist/{cli-277c31dw.js → cli-6mgdba42.js} +225 -3
- package/dist/{cli-19202kew.js → cli-6srv653c.js} +5 -5
- package/dist/{cli-n4c8e3s5.js → cli-708wdy32.js} +1 -1
- package/dist/{cli-pasyvjmm.js → cli-737pyqxr.js} +1 -1
- package/dist/{cli-6x9ma5w5.js → cli-803188pb.js} +1 -1
- package/dist/{cli-hh5we352.js → cli-86ehn4s5.js} +7 -7
- package/dist/{cli-9s05422w.js → cli-8mbc9za9.js} +1 -1
- package/dist/{cli-ff1tm9n9.js → cli-8wmj18cz.js} +1 -1
- package/dist/{cli-1r5ad7j5.js → cli-9gsm2rzp.js} +12 -12
- package/dist/{cli-mz9afxxf.js → cli-9pshx6rd.js} +17 -3
- package/dist/{cli-s682zgqz.js → cli-a6ds2epe.js} +1 -1
- package/dist/{cli-6a1xg1p7.js → cli-a89zdqz9.js} +2 -2
- package/dist/{cli-03vpq45j.js → cli-ay40dnj3.js} +2 -2
- package/dist/{cli-tsma7p8v.js → cli-aypd09nf.js} +2 -2
- package/dist/{cli-ethgy7ha.js → cli-b3yzyc57.js} +2 -2
- package/dist/{cli-bn0rewsy.js → cli-c081bj3b.js} +2 -2
- package/dist/{cli-mmhj8vd7.js → cli-c668vvcf.js} +11 -2
- package/dist/{cli-sbwsxvtd.js → cli-d20qvpr2.js} +2 -2
- package/dist/{cli-c952c7ac.js → cli-e1595bgx.js} +3 -3
- package/dist/{cli-621ce7kw.js → cli-f12mr20z.js} +3 -3
- package/dist/{cli-dqygeeh5.js → cli-f1e0ptjj.js} +10 -1
- package/dist/{cli-xzfq4at3.js → cli-f4dbh7a5.js} +16 -12
- package/dist/{cli-ha9rmwgq.js → cli-fb8m699c.js} +2 -2
- package/dist/{cli-5bnsc0e1.js → cli-gsfk58nw.js} +5 -5
- package/dist/cli-gt4h8cxw.js +895 -0
- package/dist/{cli-nwyw3zhg.js → cli-gxee0bqa.js} +1 -1
- package/dist/{cli-25n3mv5d.js → cli-h5jeg46p.js} +6 -6
- package/dist/{cli-gn5af0x5.js → cli-he9gbdx0.js} +62 -12
- package/dist/{cli-gbfxp48k.js → cli-j0ppywdr.js} +42 -35
- package/dist/{cli-k4tjsdx9.js → cli-j3y0x2n4.js} +277 -199
- package/dist/{cli-7p4jxgtt.js → cli-j9bpmykh.js} +75 -27
- package/dist/{cli-dabehxhp.js → cli-kt907da6.js} +1 -1
- package/dist/{cli-wx2nj19m.js → cli-kv7sbc2c.js} +6 -6
- package/dist/{cli-abs8ndtr.js → cli-m30k9777.js} +26 -26
- package/dist/{cli-cz5nmdxt.js → cli-m5e802r7.js} +2 -2
- package/dist/{cli-0shx9j93.js → cli-pfk92eqz.js} +2 -2
- package/dist/{cli-xtrz6bzs.js → cli-ptfq202x.js} +5 -3
- package/dist/{cli-6n8axjv1.js → cli-qj442fws.js} +2 -2
- package/dist/{cli-c61mbdds.js → cli-rd5m70gj.js} +4 -4
- package/dist/{cli-eftzyh9y.js → cli-s09j9vp5.js} +2 -2
- package/dist/{cli-m4ecdxvz.js → cli-s0mgx214.js} +7 -7
- package/dist/{cli-wghjkr0e.js → cli-sgqm2582.js} +10 -7
- package/dist/{cli-2655k16q.js → cli-sjctjr48.js} +2 -2
- package/dist/{cli-nzfnt2gs.js → cli-tf1n0r0b.js} +1 -1
- package/dist/{cli-x0f2v69c.js → cli-twctnkng.js} +12 -7
- package/dist/{cli-j6rktc91.js → cli-v6shg7na.js} +1 -1
- package/dist/{cli-gx4rm0qx.js → cli-vz78s5wm.js} +1 -1
- package/dist/{cli-gwhjmh8g.js → cli-wqr7cea4.js} +5 -5
- package/dist/{cli-e42vss7b.js → cli-x37r7qwf.js} +16 -8
- package/dist/{cli-4b2z6n0m.js → cli-yd2nreqw.js} +17 -8
- package/dist/{cli-64mh9mf3.js → cli-ymjpkxgn.js} +69 -17
- package/dist/{cli-5kqnemen.js → cli-yvy48785.js} +1 -1
- package/dist/{cli-9j129brq.js → cli-yxbsmx56.js} +6 -6
- package/dist/cli.js +22 -20
- package/dist/{codex-adapter-wch12q99.js → codex-adapter-zk59x2zh.js} +7 -7
- package/dist/{codex-hook-brpnycn5.js → codex-hook-xyjhvxwd.js} +1 -1
- package/dist/{codex-login-rn7c5gxt.js → codex-login-8cg0vhf2.js} +1 -1
- package/dist/{codex-session-runner-41pfcr95.js → codex-session-runner-kh6s4j3n.js} +8 -8
- package/dist/{commands-9849dkj6.js → commands-68et1qv2.js} +2 -2
- package/dist/{controller-ja0sd95p.js → controller-fyrbnzg4.js} +4 -4
- package/dist/{controllers-connect-xp05dj6x.js → controllers-connect-nep9h2xv.js} +23 -23
- package/dist/{cursor-adapter-623qxqew.js → cursor-adapter-yss3pg7t.js} +2 -2
- package/dist/{db-w759qmx0.js → db-28gjyvxj.js} +4 -2
- package/dist/{db-47vj40dy.js → db-8ftwvzba.js} +3 -3
- package/dist/{devin-adapter-4ycy4vrf.js → devin-adapter-byen2cxt.js} +1 -1
- package/dist/{dispatcher-7b8e4z2j.js → dispatcher-hayexmkr.js} +20 -20
- package/dist/{docker-s84hjs91.js → docker-fdzyewpz.js} +2 -2
- package/dist/{dsh-adapter-2ezw3xea.js → dsh-adapter-46wawff9.js} +2 -2
- package/dist/{e2b-9gkdhm3c.js → e2b-222hsajc.js} +2 -2
- package/dist/{e2b-cjtf8k1z.js → e2b-fn3zacvt.js} +2 -2
- package/dist/{easter-egg-3d-e4qv8dxj.js → easter-egg-3d-8d1e0h4m.js} +1 -1
- package/dist/{event-bus-3z78ep5n.js → event-bus-pghg8hqy.js} +1 -1
- package/dist/{execute-0xpz8950.js → execute-cjrfxfnm.js} +1 -1
- package/dist/{external-channel-e2s028da.js → external-channel-xbmxcg09.js} +2 -2
- package/dist/{grok-adapter-ys7gaqee.js → grok-adapter-ag4wks2a.js} +10 -10
- package/dist/handlers-tzmevqb5.js +41 -0
- package/dist/{heartbeat-w3mntghj.js → heartbeat-psfvcgwp.js} +23 -23
- package/dist/{hook-g7kvv6x9.js → hook-8pz04697.js} +6 -6
- package/dist/{hook-3jjywnpb.js → hook-cyq4mryn.js} +8 -8
- package/dist/{http-tkp7d4kb.js → http-haj1nd6k.js} +921 -169
- package/dist/{index-q7m26kvs.js → index-jwt2bj6f.js} +265 -97
- package/dist/{index-3n8m9ea2.js → index-q52f47pe.js} +1 -1
- package/dist/{index-31ceyf42.js → index-syvhnvtw.js} +1 -1
- package/dist/index-y1ajx04a.js +43 -0
- package/dist/{index-1ef1myq2.js → index-z5h22pes.js} +20 -20
- package/dist/{index-xdaahthb.js → index-zyq8ct52.js} +20 -20
- package/dist/{keepalive-v4ta8wgg.js → keepalive-xdvsamvj.js} +4 -4
- package/dist/lead-hqg57kdp.js +79 -0
- package/dist/{maintenance-84n1ex21.js → maintenance-4y2xzh3a.js} +1 -1
- package/dist/{oauth-refresh-sweep-vjnnandw.js → oauth-refresh-sweep-jd4r7zbs.js} +9 -4
- package/dist/{onboard-sfmpe4np.js → onboard-18447q9q.js} +6 -6
- package/dist/{opencode-adapter-jmpst4q4.js → opencode-adapter-26b2frdf.js} +7 -7
- package/dist/{otel-impl-v6cc0zve.js → otel-impl-f9tgr8yw.js} +2 -2
- package/dist/{pi-mono-adapter-fk8fkcf6.js → pi-mono-adapter-qn6qbga1.js} +38 -17
- package/dist/{pricing-refresh-fsk1e1zc.js → pricing-refresh-dw2tgb53.js} +3 -3
- package/dist/{rbac-roles-prmkdnkw.js → rbac-roles-aygm2760.js} +2 -2
- package/dist/{rbac-roles-zxmxqa39.js → rbac-roles-s4nwj2m3.js} +1 -1
- package/dist/render-v2-yfvdfr0x.js +16 -0
- package/dist/{resume-frdtgfa0.js → resume-1bmvjbf3.js} +9 -9
- package/dist/{runtime-bt5pkpm1.js → runtime-f7tpzedy.js} +1 -1
- package/dist/{session-access-fpez4y5q.js → session-access-r5garej6.js} +4 -4
- package/dist/session-source-backfill-424wzv0r.js +110 -0
- package/dist/{setup-mq6m3m8q.js → setup-8tygvxkq.js} +1 -1
- package/dist/{virtual-modules-mnfbh5mr.js → virtual-modules-xsw323b3.js} +2 -2
- package/dist/{work-objects-kfahhjsq.js → work-objects-ggnghpgf.js} +23 -23
- package/dist/worker-sqjd1nqf.js +79 -0
- package/dist/{x-pbpezr0s.js → x-wxrzvzx8.js} +2 -2
- package/openapi.json +1324 -14
- package/package.json +5 -3
- package/src/assets/source-key.ts +95 -0
- package/src/be/automation-preflight.ts +2 -0
- package/src/be/db/api-keys.ts +89 -0
- package/src/be/db/session-labels.ts +28 -0
- package/src/be/db/session-moves.ts +5 -6
- package/src/be/db/session-tree.ts +153 -14
- package/src/be/db/tasks/read.ts +11 -0
- package/src/be/db/tasks/write.ts +6 -1
- package/src/be/db/workflow-runs.ts +219 -6
- package/src/be/db-retention.ts +61 -6
- package/src/be/db.ts +135 -132
- package/src/be/memory/providers/openai-embedding.ts +5 -1
- package/src/be/memory/task-learning.ts +155 -0
- package/src/be/migrations/244_api_key_validation.sql +4 -0
- package/src/be/migrations/245_script_executor.sql +3 -0
- package/src/be/oauth-refresh-sweep.ts +2 -1
- package/src/be/onboarding.ts +4 -0
- package/src/be/scripts/boot-reembed.ts +2 -0
- package/src/be/scripts/db.ts +14 -4
- package/src/be/scripts/embeddings.ts +2 -0
- package/src/be/scripts/run-saved.ts +1 -0
- package/src/be/scripts/typecheck.ts +10 -4
- package/src/be/seed-skills/index.ts +7 -0
- package/src/be/session-source-backfill.ts +163 -0
- package/src/be/swarm-config-guard.ts +1 -0
- package/src/comb/review-batch.ts +28 -1
- package/src/comb/templates.ts +7 -2
- package/src/commands/claude-managed-setup.ts +7 -1
- package/src/commands/credential-refresh.ts +76 -1
- package/src/commands/credential-wait.ts +2 -2
- package/src/commands/onboard/compose-generator.ts +5 -3
- package/src/commands/provider-credentials.ts +212 -171
- package/src/commands/runner.ts +25 -4
- package/src/extensions/apply-task-create.ts +8 -1
- package/src/extensions/contract-types.generated.ts +1 -1
- package/src/extensions/contract.ts +5 -0
- package/src/heartbeat/heartbeat.ts +14 -1
- package/src/heartbeat/script-runner-probe.ts +47 -0
- package/src/http/all-routes.ts +2 -0
- package/src/http/api-keys.ts +77 -9
- package/src/http/assets.ts +15 -0
- package/src/http/codex-oauth-accounts.ts +191 -0
- package/src/http/comb.ts +15 -2
- package/src/http/config.ts +1 -1
- package/src/http/core.ts +6 -0
- package/src/http/fleet-benchmark.ts +5 -2
- package/src/http/idempotency-key.ts +1 -1
- package/src/http/index.ts +14 -3
- package/src/http/integrations.ts +5 -1
- package/src/http/kv.ts +8 -3
- package/src/http/onboarding.ts +6 -1
- package/src/http/pages-public.ts +38 -5
- package/src/http/pool-keys.ts +354 -0
- package/src/http/scripts.ts +27 -0
- package/src/http/sessions.ts +37 -12
- package/src/http/stats.ts +1 -0
- package/src/http/status.ts +26 -12
- package/src/http/tasks.ts +3 -0
- package/src/http/used-by.ts +7 -2
- package/src/http/workflows.ts +88 -9
- package/src/http/x.ts +5 -0
- package/src/integrations/chat-sdk/inbound.ts +27 -0
- package/src/kv-reserved-namespaces.ts +19 -0
- package/src/prompts/base-prompt.ts +21 -1
- package/src/prompts/session-templates.ts +17 -3
- package/src/providers/acp-targets.ts +1 -1
- package/src/providers/amp-adapter.ts +1 -1
- package/src/providers/claude-adapter.ts +5 -1
- package/src/providers/claude-managed-adapter.ts +16 -9
- package/src/providers/codex-adapter.ts +71 -11
- package/src/providers/dsh-adapter.ts +1 -1
- package/src/providers/pi-mono-adapter.ts +41 -11
- package/src/scheduler/scheduler.ts +5 -3
- package/src/scripts-runtime/eval-harness.ts +8 -1
- package/src/scripts-runtime/executors/native.ts +22 -16
- package/src/scripts-runtime/executors/quickjs-prelude.ts +103 -1
- package/src/scripts-runtime/executors/quickjs-runner.ts +2 -1
- package/src/scripts-runtime/executors/quickjs.ts +7 -0
- package/src/scripts-runtime/executors/registry.ts +32 -0
- package/src/scripts-runtime/executors/types.ts +7 -0
- package/src/scripts-runtime/host-validated-args.ts +133 -0
- package/src/scripts-runtime/loader.ts +30 -3
- package/src/scripts-runtime/swarm-sdk.ts +16 -3
- package/src/scripts-runtime/types/stdlib.d.ts +10 -2
- package/src/scripts-runtime/types/swarm-extension.d.ts +17 -8
- package/src/scripts-runtime/types/swarm-sdk.d.ts +10 -2
- package/src/slack/assistant.ts +7 -0
- package/src/slack/channel-labels.ts +74 -0
- package/src/slack/handlers.ts +8 -0
- package/src/slack/inbound-files.ts +5 -2
- package/src/slack/thread-buffer.ts +4 -0
- package/src/tasks/sibling-awareness.ts +12 -3
- package/src/telemetry-context.ts +5 -6
- package/src/telemetry-identity.ts +3 -3
- package/src/tests/api-key-status-by-suffix.test.ts +242 -0
- package/src/tests/api-keys-validation.test.ts +170 -0
- package/src/tests/base-prompt.test.ts +70 -2
- package/src/tests/claude-managed-adapter.test.ts +46 -0
- package/src/tests/claude-routes-e2e.test.ts +7 -2
- package/src/tests/codex-credential-pools.test.ts +120 -0
- package/src/tests/codex-gateway-route.test.ts +165 -0
- package/src/tests/codex-oauth-accounts.test.ts +357 -0
- package/src/tests/comb-review-batch.test.ts +63 -2
- package/src/tests/community-linear-delivery-loop.test.ts +2099 -0
- package/src/tests/config-pool-keys.test.ts +533 -0
- package/src/tests/credential-check.test.ts +294 -4
- package/src/tests/credential-refresh.test.ts +172 -1
- package/src/tests/db-retention.test.ts +9 -3
- package/src/tests/db-task-read-contract.test.ts +1 -0
- package/src/tests/db-task-write-contract.test.ts +1 -3
- package/src/tests/docs-index-footprint.test.js +3 -3
- package/src/tests/docs-index.test.ts +135 -0
- package/src/tests/dsh-adapter.test.ts +1 -1
- package/src/tests/extensions-pre-task.test.ts +53 -1
- package/src/tests/fetch-resolved-env.test.ts +16 -2
- package/src/tests/fixtures/extensions/record-task-create/hooks.ts +9 -0
- package/src/tests/fixtures/extensions/record-task-create/manifest.json +7 -0
- package/src/tests/fleet-benchmark.test.ts +8 -0
- package/src/tests/get-workflow-run-tool.test.ts +98 -0
- package/src/tests/heartbeat-checklist.test.ts +89 -0
- package/src/tests/heartbeat-script-runner-probe.test.ts +83 -0
- package/src/tests/integrations-http.test.ts +32 -2
- package/src/tests/internal-ai/complete-structured.test.ts +37 -1
- package/src/tests/internal-ai/credentials.test.ts +23 -0
- package/src/tests/kv-http.test.ts +32 -0
- package/src/tests/kv-tool.test.ts +25 -0
- package/src/tests/multi-runtime-registration.test.ts +12 -12
- package/src/tests/oauth-refresh-sweep.test.ts +34 -1
- package/src/tests/onboard-compose.test.ts +20 -0
- package/src/tests/onboarding.test.ts +108 -1
- package/src/tests/openai-embedding-credentials.test.ts +35 -0
- package/src/tests/opencode-adapter.test.ts +2 -1
- package/src/tests/openrouter-attribution-call-sites.test.ts +2 -2
- package/src/tests/org-website.test.ts +36 -0
- package/src/tests/pages-frame-ancestors.test.ts +162 -0
- package/src/tests/pi-codemode.test.ts +2 -1
- package/src/tests/pi-mono-adapter.test.ts +54 -0
- package/src/tests/pi-native-mcp.test.ts +13 -1
- package/src/tests/pi-tool-exposure.test.ts +8 -8
- package/src/tests/probe-tool-schema-tokens.test.ts +144 -0
- package/src/tests/prompt-template-session.test.ts +4 -3
- package/src/tests/provider-key-probes.test.ts +181 -0
- package/src/tests/rbac-e2e-helpers.test.ts +88 -1
- package/src/tests/rbac-e2e-helpers.ts +36 -11
- package/src/tests/request-composition-derive.test.ts +329 -0
- package/src/tests/script-connections.test.ts +4 -0
- package/src/tests/script-executor-conformance.test.ts +36 -1
- package/src/tests/script-executor-quickjs-pool.test.ts +50 -0
- package/src/tests/script-executor-quickjs.test.ts +140 -0
- package/src/tests/script-host-validated-args-native.test.ts +196 -0
- package/src/tests/script-per-script-executor.test.ts +415 -0
- package/src/tests/script-runs-retention.test.ts +212 -0
- package/src/tests/scripts-external-api.test.ts +45 -0
- package/src/tests/sdk-allowlist.test.ts +42 -2
- package/src/tests/self-improvement.test.ts +4 -4
- package/src/tests/session-source-backfill.test.ts +429 -0
- package/src/tests/session-source-keys.test.ts +236 -0
- package/src/tests/session-source-labels.test.ts +419 -0
- package/src/tests/sessions-detail-batch.test.ts +2 -0
- package/src/tests/sessions-move.test.ts +131 -0
- package/src/tests/sessions-tree.test.ts +299 -2
- package/src/tests/slack-inbound-files.test.ts +103 -23
- package/src/tests/status.test.ts +101 -18
- package/src/tests/store-progress-learning.test.ts +288 -0
- package/src/tests/tasks-workflow-run-filter.test.ts +146 -0
- package/src/tests/test-net.ts +31 -0
- package/src/tests/transcription.test.ts +8 -0
- package/src/tests/ui-logs-parser.test.ts +180 -0
- package/src/tests/used-by-route.test.ts +11 -0
- package/src/tests/workflow-cascade-cancel.test.ts +172 -0
- package/src/tests/workflow-engine-v2.test.ts +116 -1
- package/src/tests/workflow-http-v2.test.ts +131 -0
- package/src/tests/workflow-mcp-trigger-schema.test.ts +209 -0
- package/src/tests/workflow-run-finished-event.test.ts +263 -0
- package/src/tests/workflow-step-order.test.ts +92 -0
- package/src/tests/workflow-sub-workflow.test.ts +30 -0
- package/src/tests/workflow-swarm-script.test.ts +34 -0
- package/src/tools/get-tasks.ts +8 -0
- package/src/tools/kv/kv-read-auth.ts +4 -2
- package/src/tools/script-common.ts +1 -0
- package/src/tools/script-run.ts +7 -0
- package/src/tools/script-upsert.ts +7 -0
- package/src/tools/store-progress.ts +67 -1
- package/src/tools/task-action.ts +1 -1
- package/src/tools/workflows/get-workflow-run.ts +14 -4
- package/src/tools/workflows/trigger-workflow.ts +104 -10
- package/src/types.ts +36 -1
- package/src/utils/company-profile.ts +51 -1
- package/src/utils/credentials.ts +10 -4
- package/src/utils/internal-ai/complete-structured.ts +5 -0
- package/src/utils/internal-ai/credentials.ts +4 -3
- package/src/utils/provider-key-probes.ts +328 -0
- package/src/utils/transcription.ts +4 -1
- package/src/workflows/engine.ts +18 -16
- package/src/workflows/executors/sub-workflow.ts +8 -1
- package/src/workflows/executors/swarm-script.ts +5 -0
- package/src/workflows/foreach-driver.ts +5 -11
- package/src/workflows/node-timeout.ts +30 -0
- package/templates/ai-toolbox.manifest.json +2 -2
- package/templates/community/linear-delivery-loop/PLAYBOOK.md +323 -0
- package/templates/community/linear-delivery-loop/README.md +38 -0
- package/templates/community/linear-delivery-loop/config.example.json +31 -0
- package/templates/community/linear-delivery-loop/lead-prompt.md +34 -0
- package/templates/community/linear-delivery-loop/schedule.json +10 -0
- package/templates/community/linear-delivery-loop/scripts/linear-delivery-setup.ts +148 -0
- package/templates/community/linear-delivery-loop/scripts/linear-delivery-tick.ts +1781 -0
- package/templates/skills/researching/content.md +2 -0
- package/templates/skills/swarm-docs/config.json +14 -0
- package/templates/skills/swarm-docs/content.md +144 -0
- package/templates/skills/swarm-scripts/SKILL.md +1 -0
- package/templates/skills/swarm-scripts/content.md +1 -0
- package/dist/cli-7421jzsw.js +0 -84
- package/dist/cli-e45e4m6b.js +0 -413
- package/dist/cli-zw88zjba.js +0 -307
- package/dist/handlers-wy1dr6ja.js +0 -41
- package/dist/index-nmm79qgr.js +0 -43
- package/dist/lead-sawnyqxk.js +0 -79
- package/dist/render-v2-pjwxvnkw.js +0 -12
- package/dist/worker-v5e5rxj3.js +0 -79
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
2
|
+
import { mkdir, mkdtemp, rm } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import {
|
|
6
|
+
collectDocsPages,
|
|
7
|
+
coverageErrors,
|
|
8
|
+
INDEX_END,
|
|
9
|
+
INDEX_START,
|
|
10
|
+
renderDocsIndex,
|
|
11
|
+
replaceIndexBlock,
|
|
12
|
+
SKILL_CONTENT_PATH,
|
|
13
|
+
} from "../../scripts/build-docs-index";
|
|
14
|
+
import { loadSeedSkills } from "../be/seed-skills";
|
|
15
|
+
|
|
16
|
+
const page = (frontmatter: string, body = "Body.\n") => `---\n${frontmatter}\n---\n\n${body}`;
|
|
17
|
+
|
|
18
|
+
describe("docs index (real docs tree)", () => {
|
|
19
|
+
test("every playbook is in the index and the coverage rules pass", async () => {
|
|
20
|
+
const pages = await collectDocsPages();
|
|
21
|
+
expect(coverageErrors(pages)).toEqual([]);
|
|
22
|
+
|
|
23
|
+
const index = renderDocsIndex(pages);
|
|
24
|
+
const playbooks = pages.filter((p) => p.path.startsWith("playbooks/"));
|
|
25
|
+
expect(playbooks.length).toBeGreaterThan(10);
|
|
26
|
+
for (const playbook of playbooks) {
|
|
27
|
+
expect(index).toContain(`(https://docs.agent-swarm.dev/docs/${playbook.slug})`);
|
|
28
|
+
}
|
|
29
|
+
// Positive control: a known playbook carries its trigger.
|
|
30
|
+
expect(index).toContain(
|
|
31
|
+
"[Open Oncall](https://docs.agent-swarm.dev/docs/playbooks/open-oncall). When: ",
|
|
32
|
+
);
|
|
33
|
+
// Negative control: opted-out pages are reference only, release notes are skipped.
|
|
34
|
+
expect(index).toContain("(https://docs.agent-swarm.dev/docs/playbooks). Reference.");
|
|
35
|
+
expect(index).not.toContain("/docs/releases/");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("the committed skill body matches the generator", async () => {
|
|
39
|
+
const content = await Bun.file(SKILL_CONTENT_PATH).text();
|
|
40
|
+
const regenerated = replaceIndexBlock(content, renderDocsIndex(await collectDocsPages()));
|
|
41
|
+
expect(content).toBe(regenerated);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("the generator is deterministic", async () => {
|
|
45
|
+
const first = renderDocsIndex(await collectDocsPages());
|
|
46
|
+
const second = renderDocsIndex(await collectDocsPages());
|
|
47
|
+
expect(second).toBe(first);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("the seeded skill is a system default and carries the index", async () => {
|
|
51
|
+
const skill = (await loadSeedSkills()).find((s) => s.name === "swarm-docs");
|
|
52
|
+
expect(skill?.systemDefault).toBe(true);
|
|
53
|
+
expect(skill?.content).toContain("https://docs.agent-swarm.dev/docs/playbooks/open-oncall");
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe("docs index (fixture tree)", () => {
|
|
58
|
+
let root: string;
|
|
59
|
+
|
|
60
|
+
beforeAll(async () => {
|
|
61
|
+
root = await mkdtemp(join(tmpdir(), "docs-index-"));
|
|
62
|
+
await mkdir(join(root, "playbooks"), { recursive: true });
|
|
63
|
+
await mkdir(join(root, "guides"), { recursive: true });
|
|
64
|
+
await mkdir(join(root, "releases"), { recursive: true });
|
|
65
|
+
await Bun.write(join(root, "meta.json"), JSON.stringify({ pages: ["index", "playbooks"] }));
|
|
66
|
+
await Bun.write(join(root, "index.mdx"), page("title: Home"));
|
|
67
|
+
await Bun.write(
|
|
68
|
+
join(root, "playbooks", "meta.json"),
|
|
69
|
+
JSON.stringify({ pages: ["index", "b", "---Separator---", "a"] }),
|
|
70
|
+
);
|
|
71
|
+
await Bun.write(
|
|
72
|
+
join(root, "playbooks", "index.mdx"),
|
|
73
|
+
page("title: Playbooks\nagent:\n offer: false"),
|
|
74
|
+
);
|
|
75
|
+
await Bun.write(
|
|
76
|
+
join(root, "playbooks", "a.mdx"),
|
|
77
|
+
page('title: A\nagent:\n when: "Someone wants A"\n template: "community/a"'),
|
|
78
|
+
);
|
|
79
|
+
await Bun.write(join(root, "playbooks", "b.mdx"), page("title: B"));
|
|
80
|
+
await Bun.write(
|
|
81
|
+
join(root, "guides", "g.mdx"),
|
|
82
|
+
page("title: G\nagent:\n offer: true\n notWhen: never"),
|
|
83
|
+
);
|
|
84
|
+
await Bun.write(join(root, "releases", "r.mdx"), page("title: R"));
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
afterAll(async () => {
|
|
88
|
+
await rm(root, { recursive: true, force: true });
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("orders by meta.json, appends unlisted folders, and skips releases", async () => {
|
|
92
|
+
const pages = await collectDocsPages(root);
|
|
93
|
+
expect(pages.map((p) => p.path)).toEqual([
|
|
94
|
+
"index",
|
|
95
|
+
"playbooks/index",
|
|
96
|
+
"playbooks/b",
|
|
97
|
+
"playbooks/a",
|
|
98
|
+
"guides/g",
|
|
99
|
+
]);
|
|
100
|
+
expect(pages.map((p) => p.slug)).toEqual([
|
|
101
|
+
"",
|
|
102
|
+
"playbooks",
|
|
103
|
+
"playbooks/b",
|
|
104
|
+
"playbooks/a",
|
|
105
|
+
"guides/g",
|
|
106
|
+
]);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("a playbook without when, and an offered guide without when, fail coverage", async () => {
|
|
110
|
+
const errors = coverageErrors(await collectDocsPages(root));
|
|
111
|
+
expect(errors).toEqual([
|
|
112
|
+
"playbooks/b: playbook needs agent.when (or agent.offer: false)",
|
|
113
|
+
"guides/g: agent.offer is true but agent.when is missing",
|
|
114
|
+
]);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("renders triggers and templates for offered pages only", async () => {
|
|
118
|
+
const index = renderDocsIndex(await collectDocsPages(root));
|
|
119
|
+
expect(index).toContain(
|
|
120
|
+
"- [A](https://docs.agent-swarm.dev/docs/playbooks/a). When: Someone wants A. Template: `community/a`.",
|
|
121
|
+
);
|
|
122
|
+
expect(index).toContain(
|
|
123
|
+
"- [Playbooks](https://docs.agent-swarm.dev/docs/playbooks). Reference.",
|
|
124
|
+
);
|
|
125
|
+
expect(index).toContain("- [Home](https://docs.agent-swarm.dev/docs)");
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("replaceIndexBlock swaps only the marked block and rejects missing markers", () => {
|
|
129
|
+
const content = `before\n${INDEX_START}\nold\n${INDEX_END}\nafter\n`;
|
|
130
|
+
expect(replaceIndexBlock(content, `${INDEX_START}\nnew\n${INDEX_END}`)).toBe(
|
|
131
|
+
`before\n${INDEX_START}\nnew\n${INDEX_END}\nafter\n`,
|
|
132
|
+
);
|
|
133
|
+
expect(() => replaceIndexBlock("no markers", "x")).toThrow();
|
|
134
|
+
});
|
|
135
|
+
});
|
|
@@ -376,7 +376,7 @@ describe("dsh harness", () => {
|
|
|
376
376
|
await rename(join(config.cwd, "dsh"), join(config.cwd, "npx"));
|
|
377
377
|
config.env = { ...config.env, DSH_BINARY: "", PATH: config.cwd };
|
|
378
378
|
await expect(new DshAdapter().createSession(config)).rejects.toThrow(
|
|
379
|
-
"dsh CLI not found. Install @deepseek-ai/dsh@0.2.1-rc.
|
|
379
|
+
"dsh CLI not found. Install @deepseek-ai/dsh@0.2.1-rc.4",
|
|
380
380
|
);
|
|
381
381
|
expect(await Bun.file(join(config.cwd, "invocation.json")).exists()).toBe(false);
|
|
382
382
|
});
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
createWorkflowRunStep,
|
|
15
15
|
getDbClient,
|
|
16
16
|
getKv,
|
|
17
|
+
getMostRecentTaskInThread,
|
|
17
18
|
getTaskByWorkflowRunStepId,
|
|
18
19
|
initDb,
|
|
19
20
|
startTask,
|
|
@@ -22,7 +23,7 @@ import * as dbClient from "../be/db-client";
|
|
|
22
23
|
import type { InstallExtensionArgs as ExtensionInstallBody } from "../be/extensions/db";
|
|
23
24
|
import { installExtension, listExtensionRuns } from "../be/extensions/db";
|
|
24
25
|
import { getCatalogEntry } from "../extensions/catalog";
|
|
25
|
-
import type { TaskBlockedEvent } from "../extensions/contract";
|
|
26
|
+
import type { TaskBlockedEvent, TaskCreateEvent } from "../extensions/contract";
|
|
26
27
|
import { disableExtension, enableExtension, stopExtensionRuntime } from "../extensions/lifecycle";
|
|
27
28
|
import { initExtensionPostBridge, teardownExtensionPostBridge } from "../extensions/post-bridge";
|
|
28
29
|
import { handleTasks } from "../http/tasks";
|
|
@@ -551,6 +552,57 @@ export default extension;
|
|
|
551
552
|
);
|
|
552
553
|
});
|
|
553
554
|
|
|
555
|
+
test("Slack event metadata retains mixed-sender batches and rejects metadata rewrites", async () => {
|
|
556
|
+
await createAgent({ name: "slack-metadata-lead", isLead: true, status: "idle" });
|
|
557
|
+
await installSource(
|
|
558
|
+
"a-rewrite-slack-metadata",
|
|
559
|
+
`import type { SwarmExtension } from "swarm-extension";
|
|
560
|
+
const extension: SwarmExtension = (api) => {
|
|
561
|
+
api.on("pre.task.create", () => ({ action: "modify", data: {
|
|
562
|
+
priority: 7, slack: { trigger: "mention", messages: [] }
|
|
563
|
+
} as never }));
|
|
564
|
+
};
|
|
565
|
+
export default extension;
|
|
566
|
+
`,
|
|
567
|
+
);
|
|
568
|
+
const recorder = await installAndEnable("record-task-create");
|
|
569
|
+
const channelId = "C_EVENT_METADATA";
|
|
570
|
+
const threadTs = "3000.0001";
|
|
571
|
+
bufferThreadMessage(channelId, threadTs, "first request", "U_FIRST", "3000.0002");
|
|
572
|
+
bufferThreadMessage(channelId, threadTs, "second request", "U_SECOND", "3000.0003");
|
|
573
|
+
|
|
574
|
+
await instantFlush(`${channelId}:${threadTs}`);
|
|
575
|
+
|
|
576
|
+
const event = (await getKv(`task:agent:${recorder.agentId}`, "ext:record-task-create:last"))
|
|
577
|
+
?.value as TaskCreateEvent & { slack?: unknown };
|
|
578
|
+
expect(event.slack).toEqual({
|
|
579
|
+
trigger: "thread-buffer",
|
|
580
|
+
messages: [
|
|
581
|
+
{ ts: "3000.0002", userId: "U_FIRST", text: "first request" },
|
|
582
|
+
{ ts: "3000.0003", userId: "U_SECOND", text: "second request" },
|
|
583
|
+
],
|
|
584
|
+
});
|
|
585
|
+
expect(event.origin).toBe("slack");
|
|
586
|
+
expect(event.options).toMatchObject({
|
|
587
|
+
slackTriggerMessageTs: "3000.0003",
|
|
588
|
+
slackUserId: "U_FIRST",
|
|
589
|
+
priority: 7,
|
|
590
|
+
});
|
|
591
|
+
expect(event.options).not.toHaveProperty("slack");
|
|
592
|
+
const task = await getMostRecentTaskInThread(channelId, threadTs);
|
|
593
|
+
expect(task).toMatchObject({
|
|
594
|
+
priority: 7,
|
|
595
|
+
slackTriggerMessageTs: "3000.0003",
|
|
596
|
+
slackUserId: "U_FIRST",
|
|
597
|
+
});
|
|
598
|
+
expect(task).not.toHaveProperty("slack");
|
|
599
|
+
|
|
600
|
+
await createTaskWithSiblingAwareness("ordinary API task", { source: "api" });
|
|
601
|
+
const apiEvent = (await getKv(`task:agent:${recorder.agentId}`, "ext:record-task-create:last"))
|
|
602
|
+
?.value as TaskCreateEvent & { slack?: unknown };
|
|
603
|
+
expect(apiEvent).not.toHaveProperty("slack");
|
|
604
|
+
});
|
|
605
|
+
|
|
554
606
|
test("scheduler and workflow executor provide their task creation origins", async () => {
|
|
555
607
|
const extension = await installSource(
|
|
556
608
|
"record-task-origin",
|
|
@@ -41,8 +41,12 @@ const defaultMockResponse: MockResponse = {
|
|
|
41
41
|
const mockResponsesByAgentId = new Map<string, MockResponse>();
|
|
42
42
|
const requestedRepoIds = new Map<string, string | null>();
|
|
43
43
|
const requestedTaskIds = new Map<string, string | null>();
|
|
44
|
-
/**
|
|
44
|
+
/**
|
|
45
|
+
* Every `GET /api/keys/available` query string observed by the mock server,
|
|
46
|
+
* most recent last, without `keySuffixes` (recorded in `keysAvailableSuffixes`).
|
|
47
|
+
*/
|
|
45
48
|
const keysAvailableRequests: string[] = [];
|
|
49
|
+
const keysAvailableSuffixes: Array<string | null> = [];
|
|
46
50
|
|
|
47
51
|
beforeAll(() => {
|
|
48
52
|
server = Bun.serve({
|
|
@@ -63,7 +67,10 @@ beforeAll(() => {
|
|
|
63
67
|
}
|
|
64
68
|
|
|
65
69
|
if (url.pathname === "/api/keys/available") {
|
|
66
|
-
|
|
70
|
+
const params = new URLSearchParams(url.search);
|
|
71
|
+
keysAvailableSuffixes.push(params.get("keySuffixes"));
|
|
72
|
+
params.delete("keySuffixes");
|
|
73
|
+
keysAvailableRequests.push(`?${params}`);
|
|
67
74
|
const totalKeys = Number(url.searchParams.get("totalKeys") ?? "1");
|
|
68
75
|
const availableIndices = Array.from({ length: totalKeys }, (_, i) => i);
|
|
69
76
|
return Response.json({ success: true, availableIndices, totalKeys });
|
|
@@ -455,6 +462,13 @@ describe("fetchResolvedEnv", () => {
|
|
|
455
462
|
expect(keysAvailableRequests).toEqual([
|
|
456
463
|
"?keyType=CLAUDE_CODE_OAUTH_TOKEN&totalKeys=32&model=fable",
|
|
457
464
|
]);
|
|
465
|
+
// The pool's own suffixes, in order, so status is matched by key (issue #2425).
|
|
466
|
+
expect(keysAvailableSuffixes.at(-1)).toBe(
|
|
467
|
+
Object.values(envWithOauthPool(32))[0]!
|
|
468
|
+
.split(",")
|
|
469
|
+
.map((key) => key.slice(-5))
|
|
470
|
+
.join(","),
|
|
471
|
+
);
|
|
458
472
|
});
|
|
459
473
|
|
|
460
474
|
test("model: 'claude-fable-5-1' issues the same model=fable", async () => {
|
|
@@ -326,6 +326,14 @@ describe("GET /api/fleet-benchmark", () => {
|
|
|
326
326
|
expect(fetched).toHaveLength(1);
|
|
327
327
|
});
|
|
328
328
|
|
|
329
|
+
test("a use case the catalog does not list yet compares against the whole fleet", async () => {
|
|
330
|
+
await addTasks(10, "completed", 1);
|
|
331
|
+
await setProfile("2-10", "content");
|
|
332
|
+
const response = await buildFleetBenchmarkResponse(NOW);
|
|
333
|
+
expect(response).toMatchObject({ state: "ready", level: "full", peerGroup: null });
|
|
334
|
+
expect(fetched).toEqual(["http://proxy.test/v1/fleet/benchmarks?window=30d"]);
|
|
335
|
+
});
|
|
336
|
+
|
|
329
337
|
test("full profile tolerates absent or null peer_group from the proxy", async () => {
|
|
330
338
|
await addTasks(10, "completed", 1);
|
|
331
339
|
await setProfile();
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { afterAll, beforeAll, expect, test } from "bun:test";
|
|
2
|
+
import { unlink } from "node:fs/promises";
|
|
3
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4
|
+
import {
|
|
5
|
+
closeDb,
|
|
6
|
+
createWorkflow,
|
|
7
|
+
createWorkflowRun,
|
|
8
|
+
createWorkflowRunStep,
|
|
9
|
+
initDb,
|
|
10
|
+
updateWorkflowRun,
|
|
11
|
+
updateWorkflowRunStep,
|
|
12
|
+
} from "../be/db";
|
|
13
|
+
import { registerGetWorkflowRunTool } from "../tools/workflows/get-workflow-run";
|
|
14
|
+
|
|
15
|
+
const dbPath = `./test-get-workflow-run-tool-${process.pid}.sqlite`;
|
|
16
|
+
|
|
17
|
+
type ToolResult = {
|
|
18
|
+
isError?: boolean;
|
|
19
|
+
structuredContent: {
|
|
20
|
+
message: string;
|
|
21
|
+
run?: Record<string, unknown>;
|
|
22
|
+
steps?: Array<Record<string, unknown>>;
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
let handler: (args: unknown, extra: unknown) => Promise<ToolResult>;
|
|
26
|
+
let runId: string;
|
|
27
|
+
|
|
28
|
+
async function removeDb() {
|
|
29
|
+
for (const suffix of ["", "-wal", "-shm"]) await unlink(dbPath + suffix).catch(() => undefined);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
beforeAll(async () => {
|
|
33
|
+
await removeDb();
|
|
34
|
+
initDb(dbPath);
|
|
35
|
+
const workflow = await createWorkflow({
|
|
36
|
+
name: "get-workflow-run-tool",
|
|
37
|
+
definition: { nodes: [{ id: "poll", type: "notify", config: { template: "x" } }] },
|
|
38
|
+
});
|
|
39
|
+
runId = crypto.randomUUID();
|
|
40
|
+
await createWorkflowRun({ id: runId, workflowId: workflow.id });
|
|
41
|
+
const context = { poll: { attempts: 2 }, blob: "x".repeat(2_000) };
|
|
42
|
+
for (let i = 0; i < 2; i++) {
|
|
43
|
+
const step = await createWorkflowRunStep({
|
|
44
|
+
id: crypto.randomUUID(),
|
|
45
|
+
runId,
|
|
46
|
+
nodeId: "poll",
|
|
47
|
+
nodeType: "notify",
|
|
48
|
+
input: { ...context, iteration: i },
|
|
49
|
+
});
|
|
50
|
+
await updateWorkflowRunStep(step.id, {
|
|
51
|
+
status: "completed",
|
|
52
|
+
output: { iteration: i },
|
|
53
|
+
retryCount: i,
|
|
54
|
+
finishedAt: new Date().toISOString(),
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
await updateWorkflowRun(runId, { status: "completed", context });
|
|
58
|
+
|
|
59
|
+
const server = new McpServer({ name: "get-workflow-run-tool", version: "1.0.0" });
|
|
60
|
+
registerGetWorkflowRunTool(server);
|
|
61
|
+
handler = (server as unknown as { _registeredTools: Record<string, { handler: typeof handler }> })
|
|
62
|
+
._registeredTools["get-workflow-run"]!.handler;
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
afterAll(async () => {
|
|
66
|
+
closeDb();
|
|
67
|
+
await removeDb();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const extra = { sessionId: "get-workflow-run-tool", requestInfo: { headers: {} } };
|
|
71
|
+
|
|
72
|
+
test("defaults to the full run with context and step inputs", async () => {
|
|
73
|
+
const result = await handler({ id: runId }, extra);
|
|
74
|
+
expect(result.isError).not.toBe(true);
|
|
75
|
+
const { run, steps } = result.structuredContent;
|
|
76
|
+
expect(run?.context).toBeDefined();
|
|
77
|
+
expect(steps).toHaveLength(2);
|
|
78
|
+
for (const step of steps!) expect(step.input).toBeDefined();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("fields=slim drops context and step inputs, keeps outputs and retry fields", async () => {
|
|
82
|
+
const result = await handler({ id: runId, fields: "slim" }, extra);
|
|
83
|
+
expect(result.isError).not.toBe(true);
|
|
84
|
+
const { run, steps } = result.structuredContent;
|
|
85
|
+
expect(run?.status).toBe("completed");
|
|
86
|
+
expect(run?.context).toBeUndefined();
|
|
87
|
+
expect(steps).toHaveLength(2);
|
|
88
|
+
for (const step of steps!) {
|
|
89
|
+
expect(step.input).toBeUndefined();
|
|
90
|
+
expect(step.output).toBeDefined();
|
|
91
|
+
expect(typeof step.retryCount).toBe("number");
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("fields=slim reports a missing run as an error", async () => {
|
|
96
|
+
const result = await handler({ id: crypto.randomUUID(), fields: "slim" }, extra);
|
|
97
|
+
expect(result.isError).toBe(true);
|
|
98
|
+
});
|
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
upsertSwarmConfig,
|
|
25
25
|
} from "../be/db";
|
|
26
26
|
import { getScript, upsertScriptByName } from "../be/scripts/db";
|
|
27
|
+
import * as savedScriptRunner from "../be/scripts/run-saved";
|
|
27
28
|
import { runSavedScriptAsAgent } from "../be/scripts/run-saved";
|
|
28
29
|
import {
|
|
29
30
|
checkHeartbeatChecklist,
|
|
@@ -624,6 +625,66 @@ describe("Heartbeat Checklist", () => {
|
|
|
624
625
|
});
|
|
625
626
|
});
|
|
626
627
|
|
|
628
|
+
for (const quiet of [true, false]) {
|
|
629
|
+
test(`healthy probe preserves the saved gate quiet=${quiet} verdict`, async () => {
|
|
630
|
+
process.env.HEARTBEAT_CHECKLIST_GATE_MODE = "enforce";
|
|
631
|
+
await saveGlobalGateScript("quiet-gate", "export default () => ({ quiet: true });");
|
|
632
|
+
const saved = spyOn(savedScriptRunner, "runSavedScriptAsAgent").mockResolvedValue({
|
|
633
|
+
exitCode: 0,
|
|
634
|
+
result: { quiet, reason: "existing verdict" },
|
|
635
|
+
stdout: "",
|
|
636
|
+
stderr: "",
|
|
637
|
+
durationMs: 1,
|
|
638
|
+
truncated: { stdout: false, stderr: false },
|
|
639
|
+
});
|
|
640
|
+
try {
|
|
641
|
+
setChecklistGateRunnerForTests((name, agentId, signal) =>
|
|
642
|
+
defaultChecklistGateRunner(name, agentId, signal, { probe: async () => null }),
|
|
643
|
+
);
|
|
644
|
+
await seedLead();
|
|
645
|
+
await checkHeartbeatChecklist();
|
|
646
|
+
expect(saved).toHaveBeenCalledTimes(1);
|
|
647
|
+
const tasks = await checklistTasks();
|
|
648
|
+
expect(tasks).toHaveLength(quiet ? 0 : 1);
|
|
649
|
+
if (!quiet) expect(tasks[0]!.tags).toContain("heartbeat-gate:wake");
|
|
650
|
+
} finally {
|
|
651
|
+
saved.mockRestore();
|
|
652
|
+
}
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
for (const mode of ["shadow", "enforce"] as const) {
|
|
657
|
+
test(`${mode}: an unhealthy probe produces a wake checklist`, async () => {
|
|
658
|
+
process.env.HEARTBEAT_CHECKLIST_GATE_MODE = mode;
|
|
659
|
+
setChecklistGateRunnerForTests((name, agentId, signal) =>
|
|
660
|
+
defaultChecklistGateRunner(name, agentId, signal, {
|
|
661
|
+
probe: async () => ({ quiet: false, reason: "script-runner-unhealthy: timeout" }),
|
|
662
|
+
}),
|
|
663
|
+
);
|
|
664
|
+
await seedLead();
|
|
665
|
+
await checkHeartbeatChecklist();
|
|
666
|
+
const tasks = await checklistTasks();
|
|
667
|
+
expect(tasks).toHaveLength(1);
|
|
668
|
+
expect(tasks[0]!.tags).toContain("heartbeat-gate:wake");
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
test("a bug in the probe produces wake rather than error", async () => {
|
|
673
|
+
process.env.HEARTBEAT_CHECKLIST_GATE_MODE = "enforce";
|
|
674
|
+
setChecklistGateRunnerForTests((name, agentId, signal) =>
|
|
675
|
+
defaultChecklistGateRunner(name, agentId, signal, {
|
|
676
|
+
probe: async () => {
|
|
677
|
+
throw new Error("probe bug");
|
|
678
|
+
},
|
|
679
|
+
}),
|
|
680
|
+
);
|
|
681
|
+
await seedLead();
|
|
682
|
+
await checkHeartbeatChecklist();
|
|
683
|
+
const tasks = await checklistTasks();
|
|
684
|
+
expect(tasks).toHaveLength(1);
|
|
685
|
+
expect(tasks[0]!.tags).toContain("heartbeat-gate:wake");
|
|
686
|
+
});
|
|
687
|
+
|
|
627
688
|
test("shadow mode (default): a quiet verdict still creates the task, tagged quiet", async () => {
|
|
628
689
|
useGate({ quiet: true, reason: "no change", summary: "nothing moved" });
|
|
629
690
|
const lead = await seedLead();
|
|
@@ -703,6 +764,9 @@ describe("Heartbeat Checklist", () => {
|
|
|
703
764
|
test("enforce mode: a missing global script fails open through the real runner", async () => {
|
|
704
765
|
process.env.HEARTBEAT_CHECKLIST_GATE_MODE = "enforce";
|
|
705
766
|
process.env.HEARTBEAT_CHECKLIST_GATE_SCRIPT = "no-such-gate-script";
|
|
767
|
+
setChecklistGateRunnerForTests((name, agentId, signal) =>
|
|
768
|
+
defaultChecklistGateRunner(name, agentId, signal, { probe: async () => null }),
|
|
769
|
+
);
|
|
706
770
|
await seedLead();
|
|
707
771
|
|
|
708
772
|
await checkHeartbeatChecklist();
|
|
@@ -739,6 +803,7 @@ describe("Heartbeat Checklist", () => {
|
|
|
739
803
|
let run: Promise<unknown> | undefined;
|
|
740
804
|
setChecklistGateRunnerForTests((scriptName, leadAgentId, signal) => {
|
|
741
805
|
run = defaultChecklistGateRunner(scriptName, leadAgentId, signal, {
|
|
806
|
+
probe: async () => null,
|
|
742
807
|
buildCredentials: () => {
|
|
743
808
|
setupStarted.resolve();
|
|
744
809
|
return new Promise((resolve) => {
|
|
@@ -798,6 +863,9 @@ describe("Heartbeat Checklist", () => {
|
|
|
798
863
|
"export default async function run() { await new Promise((r) => setTimeout(r, 30_000)); return { quiet: true }; }",
|
|
799
864
|
);
|
|
800
865
|
setChecklistGateTimeoutMsForTests(1_500);
|
|
866
|
+
setChecklistGateRunnerForTests((name, agentId, signal) =>
|
|
867
|
+
defaultChecklistGateRunner(name, agentId, signal, { probe: async () => null }),
|
|
868
|
+
);
|
|
801
869
|
await seedLead();
|
|
802
870
|
|
|
803
871
|
const started = Date.now();
|
|
@@ -808,6 +876,27 @@ describe("Heartbeat Checklist", () => {
|
|
|
808
876
|
expect(tasks).toHaveLength(1);
|
|
809
877
|
expect(tasks[0]!.tags).toContain("heartbeat-gate:error");
|
|
810
878
|
});
|
|
879
|
+
|
|
880
|
+
test("enforce mode: the real probe passes with no swarm API key, using the Lead's run token", async () => {
|
|
881
|
+
process.env.HEARTBEAT_CHECKLIST_GATE_MODE = "enforce";
|
|
882
|
+
await saveGlobalGateScript("quiet-gate", "export default () => ({ quiet: true });");
|
|
883
|
+
const savedKeys = { swarm: process.env.AGENT_SWARM_API_KEY, legacy: process.env.API_KEY };
|
|
884
|
+
process.env.AGENT_SWARM_API_KEY = "";
|
|
885
|
+
process.env.API_KEY = "";
|
|
886
|
+
try {
|
|
887
|
+
await seedLead();
|
|
888
|
+
await checkHeartbeatChecklist();
|
|
889
|
+
expect(await checklistTasks()).toHaveLength(0);
|
|
890
|
+
} finally {
|
|
891
|
+
for (const [name, value] of [
|
|
892
|
+
["AGENT_SWARM_API_KEY", savedKeys.swarm],
|
|
893
|
+
["API_KEY", savedKeys.legacy],
|
|
894
|
+
] as const) {
|
|
895
|
+
if (value === undefined) delete process.env[name];
|
|
896
|
+
else process.env[name] = value;
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
});
|
|
811
900
|
});
|
|
812
901
|
|
|
813
902
|
// ==========================================================================
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { afterEach, describe, expect, mock, spyOn, test } from "bun:test";
|
|
2
|
+
import { probeScriptRunner } from "../heartbeat/script-runner-probe";
|
|
3
|
+
import type { RunScriptOutput } from "../scripts-runtime/loader";
|
|
4
|
+
|
|
5
|
+
const ok: RunScriptOutput = {
|
|
6
|
+
result: true,
|
|
7
|
+
stdout: "",
|
|
8
|
+
stderr: "",
|
|
9
|
+
truncated: { stdout: false, stderr: false },
|
|
10
|
+
durationMs: 1,
|
|
11
|
+
exitCode: 0,
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
afterEach(() => mock.restore());
|
|
15
|
+
|
|
16
|
+
describe("heartbeat script runner probe", () => {
|
|
17
|
+
test("success leaves the gate verdict unchanged and clears its deadline", async () => {
|
|
18
|
+
const clear = spyOn(globalThis, "clearTimeout");
|
|
19
|
+
const runner = mock(async () => ok);
|
|
20
|
+
expect(await probeScriptRunner("lead", runner)).toBeNull();
|
|
21
|
+
expect(runner).toHaveBeenCalledWith({
|
|
22
|
+
source: "export default async function () { return true; }",
|
|
23
|
+
agentId: "lead",
|
|
24
|
+
fsMode: "none",
|
|
25
|
+
timeoutMs: 10_000,
|
|
26
|
+
signal: expect.any(AbortSignal),
|
|
27
|
+
});
|
|
28
|
+
expect(clear).toHaveBeenCalledTimes(1);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("timeout wakes without waiting for a stalled runner and aborts it", async () => {
|
|
32
|
+
let expire: (() => void) | undefined;
|
|
33
|
+
spyOn(globalThis, "setTimeout").mockImplementation(((callback: () => void, ms: number) => {
|
|
34
|
+
expect(ms).toBe(10_000);
|
|
35
|
+
expire = callback;
|
|
36
|
+
return 1;
|
|
37
|
+
}) as typeof setTimeout);
|
|
38
|
+
const clear = spyOn(globalThis, "clearTimeout").mockImplementation(() => {});
|
|
39
|
+
let signal: AbortSignal | undefined;
|
|
40
|
+
let rejectRun: ((error: Error) => void) | undefined;
|
|
41
|
+
const result = probeScriptRunner("lead", async (input) => {
|
|
42
|
+
signal = input.signal;
|
|
43
|
+
return new Promise<RunScriptOutput>((_, reject) => {
|
|
44
|
+
rejectRun = reject;
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
expire!();
|
|
48
|
+
expect(await result).toEqual({
|
|
49
|
+
quiet: false,
|
|
50
|
+
reason: "script-runner-unhealthy: timeout after 10000ms",
|
|
51
|
+
});
|
|
52
|
+
expect(signal?.aborted).toBe(true);
|
|
53
|
+
expect(clear).toHaveBeenCalledTimes(1);
|
|
54
|
+
// A runner that rejects after the deadline remains handled by Promise.race.
|
|
55
|
+
rejectRun!(new Error("late rejection"));
|
|
56
|
+
await Promise.resolve();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("a thrown runner error wakes", async () => {
|
|
60
|
+
expect(
|
|
61
|
+
await probeScriptRunner("lead", async () => {
|
|
62
|
+
throw new Error("ECONNRESET");
|
|
63
|
+
}),
|
|
64
|
+
).toEqual({
|
|
65
|
+
quiet: false,
|
|
66
|
+
reason: "script-runner-unhealthy: ECONNRESET",
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
for (const [label, failure, reason] of [
|
|
71
|
+
["nonzero exit", { exitCode: 1 }, "script exited with code 1"],
|
|
72
|
+
["runner error", { error: "killed" }, "killed"],
|
|
73
|
+
["runtime error", { runtimeError: { message: "runtime failed" } }, "runtime failed"],
|
|
74
|
+
] as const) {
|
|
75
|
+
test(`${label} wakes`, async () => {
|
|
76
|
+
const result = await probeScriptRunner(
|
|
77
|
+
"lead",
|
|
78
|
+
async () => ({ ...ok, ...failure }) as RunScriptOutput,
|
|
79
|
+
);
|
|
80
|
+
expect(result).toEqual({ quiet: false, reason: `script-runner-unhealthy: ${reason}` });
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
});
|
|
@@ -21,6 +21,7 @@ const TEST_DB_PATH = "./test-integrations-http.sqlite";
|
|
|
21
21
|
|
|
22
22
|
interface FakeClientLog {
|
|
23
23
|
retrieveCalls: string[];
|
|
24
|
+
apiKeys: string[];
|
|
24
25
|
retrieveResult?: { name?: string | null; model?: string | null };
|
|
25
26
|
retrieveError?: Error;
|
|
26
27
|
}
|
|
@@ -60,7 +61,7 @@ async function clearManagedConfigRows() {
|
|
|
60
61
|
describe("POST /api/integrations/claude-managed/test", () => {
|
|
61
62
|
let server: Server;
|
|
62
63
|
let baseUrl = "";
|
|
63
|
-
const log: FakeClientLog = { retrieveCalls: [] };
|
|
64
|
+
const log: FakeClientLog = { retrieveCalls: [], apiKeys: [] };
|
|
64
65
|
const savedEnv = {
|
|
65
66
|
ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY,
|
|
66
67
|
MANAGED_AGENT_ID: process.env.MANAGED_AGENT_ID,
|
|
@@ -71,7 +72,10 @@ describe("POST /api/integrations/claude-managed/test", () => {
|
|
|
71
72
|
initDb(TEST_DB_PATH);
|
|
72
73
|
|
|
73
74
|
const handler = createIntegrationsHandler({
|
|
74
|
-
buildClient: () =>
|
|
75
|
+
buildClient: (apiKey) => {
|
|
76
|
+
log.apiKeys.push(apiKey);
|
|
77
|
+
return buildFakeClient(log);
|
|
78
|
+
},
|
|
75
79
|
});
|
|
76
80
|
|
|
77
81
|
server = createHttpServer(async (req, res) => {
|
|
@@ -101,6 +105,7 @@ describe("POST /api/integrations/claude-managed/test", () => {
|
|
|
101
105
|
|
|
102
106
|
beforeEach(async () => {
|
|
103
107
|
log.retrieveCalls = [];
|
|
108
|
+
log.apiKeys = [];
|
|
104
109
|
log.retrieveResult = undefined;
|
|
105
110
|
log.retrieveError = undefined;
|
|
106
111
|
await clearManagedConfigRows();
|
|
@@ -138,6 +143,31 @@ describe("POST /api/integrations/claude-managed/test", () => {
|
|
|
138
143
|
expect(log.retrieveCalls).toEqual(["agent_abc123"]);
|
|
139
144
|
});
|
|
140
145
|
|
|
146
|
+
test("a comma pool sends one key to Anthropic", async () => {
|
|
147
|
+
await upsertSwarmConfig({
|
|
148
|
+
scope: "global",
|
|
149
|
+
key: "ANTHROPIC_API_KEY",
|
|
150
|
+
value: "sk-ant-first,sk-ant-second",
|
|
151
|
+
isSecret: true,
|
|
152
|
+
});
|
|
153
|
+
await upsertSwarmConfig({
|
|
154
|
+
scope: "global",
|
|
155
|
+
key: "MANAGED_AGENT_ID",
|
|
156
|
+
value: "agent_abc123",
|
|
157
|
+
isSecret: false,
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
const res = await fetch(`${baseUrl}/api/integrations/claude-managed/test`, {
|
|
161
|
+
method: "POST",
|
|
162
|
+
headers: { "Content-Type": "application/json" },
|
|
163
|
+
body: "{}",
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
expect((await res.json()) as { ok: boolean }).toMatchObject({ ok: true });
|
|
167
|
+
expect(log.apiKeys).toHaveLength(1);
|
|
168
|
+
expect(["sk-ant-first", "sk-ant-second"]).toContain(log.apiKeys[0]);
|
|
169
|
+
});
|
|
170
|
+
|
|
141
171
|
test("missing-config path — returns ok:false with helpful error", async () => {
|
|
142
172
|
// No swarm_config rows, no env. resolveConfigValue should return null
|
|
143
173
|
// for both keys and short-circuit before calling Anthropic.
|