@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,187 @@
|
|
|
1
|
+
# QA loom
|
|
2
|
+
|
|
3
|
+
A QA loom is a short screen recording of a real UI QA pass, with a voice-over and small one-line subtitles, embedded in the PR. The narrated mp4 plays inline on GitHub with audio. An optional subtitled GIF gives a silent preview.
|
|
4
|
+
|
|
5
|
+
Two bundled files ship next to this skill:
|
|
6
|
+
|
|
7
|
+
- `run-qa.sh` records the QA pass with `agent-browser` and writes `steps.json`.
|
|
8
|
+
- `build-loom.mjs` turns `steps.json` and the raw recording into `qa-loom.mp4`.
|
|
9
|
+
|
|
10
|
+
Copy both into your working directory before you edit them.
|
|
11
|
+
|
|
12
|
+
## When to use
|
|
13
|
+
|
|
14
|
+
- On every PR that changes visible UI, and on every UI QA pass.
|
|
15
|
+
- Run your normal QA with the `qa` skill. Drive the browser with the `agent-browser` skill. This skill adds recording, narration and delivery on top.
|
|
16
|
+
- Skip it when a PR has no visible UI change.
|
|
17
|
+
|
|
18
|
+
## Requirements
|
|
19
|
+
|
|
20
|
+
Check these first. If one is missing, use the fallback in section 7.
|
|
21
|
+
|
|
22
|
+
| Requirement | Check |
|
|
23
|
+
|---|---|
|
|
24
|
+
| `ELEVENLABS_API_KEY` env var | `test -n "$ELEVENLABS_API_KEY"` (never echo the value) |
|
|
25
|
+
| `agent-browser` with recording | `command -v agent-browser`; on errors, `agent-browser doctor` |
|
|
26
|
+
| `ffmpeg` built with libass | `ffmpeg -hide_banner -filters 2>/dev/null \| grep -w ass` |
|
|
27
|
+
| Node 18+ or Bun | `node --version` (the script needs global `fetch` and top-level await) |
|
|
28
|
+
| `jq` and GNU `date` | `jq --version`; `date +%N` prints digits, not `N` |
|
|
29
|
+
| A subtitle font | `fc-list \| grep -i "DejaVu Sans"`, or set `FONT` to an installed font |
|
|
30
|
+
| `GITHUB_TOKEN` for delivery | a token that can upload user attachments to the repo (section 6) |
|
|
31
|
+
|
|
32
|
+
Portability gotchas:
|
|
33
|
+
|
|
34
|
+
- Some ffmpeg builds ship without `ffprobe`. Probe with `ffmpeg -hide_banner -i f.mp4 2>&1 | grep -E "Duration|Stream"`.
|
|
35
|
+
- Some ffmpeg builds have no `drawtext` filter. Burn subtitles with the libass `ass` filter, as `build-loom.mjs` does.
|
|
36
|
+
- libass ignores the alpha byte of `BackColour` for `BorderStyle=3`. The subtitle box is solid dark grey, not translucent.
|
|
37
|
+
- Minimal containers may lack `bc`. Do time arithmetic in `jq`, as `run-qa.sh` does.
|
|
38
|
+
|
|
39
|
+
## Style rules
|
|
40
|
+
|
|
41
|
+
- **Speed 1.33x** on the raw footage (`SPEED=1.33`). 2x is too fast to follow.
|
|
42
|
+
- **One short sentence per step**, present tense, describing what the viewer sees: "The editor opens a shared file." Aim for 6 to 12 words.
|
|
43
|
+
- **Subtitles: one line, small, bottom centre.** 38 px on a 1080p frame, dark box, `MarginV 30`. Cues hold at most 6 words, split on punctuation, with no orphan cue under 3 words.
|
|
44
|
+
- **Voice and subtitles start when the screen renders**, not when the click fires. That is why the step log carries `visualAt`.
|
|
45
|
+
- **Letterbox, never stretch.** Mobile-viewport steps are fitted into 1920x1080 with padding.
|
|
46
|
+
- **Keep it short.** 4 to 6 steps that prove the change, about 20 to 30 seconds final.
|
|
47
|
+
|
|
48
|
+
## Context hygiene
|
|
49
|
+
|
|
50
|
+
The pipeline handles large binary payloads. Keep them out of your context:
|
|
51
|
+
|
|
52
|
+
- Run every ffmpeg call with `-loglevel error`, or send its stderr to a file and `tail -3` it only on failure.
|
|
53
|
+
- Never print an ElevenLabs response. `audio_base64` is hundreds of KB. `build-loom.mjs` writes it straight to disk.
|
|
54
|
+
- Never `cat` or read an mp4 or GIF. To look at the result, extract one PNG frame and view that.
|
|
55
|
+
|
|
56
|
+
## 1. Record the QA pass with a step log
|
|
57
|
+
|
|
58
|
+
Each step logs:
|
|
59
|
+
|
|
60
|
+
- `start`: just before the action fires (`act`).
|
|
61
|
+
- `visualAt`: after the resulting screen has rendered, following an `agent-browser wait <selector>` (`step`).
|
|
62
|
+
- `verdict` and one `narration` sentence.
|
|
63
|
+
- `end`: the next step's `start`. The last step ends when recording stops.
|
|
64
|
+
|
|
65
|
+
All times are seconds from recording start. Replace the example steps in `run-qa.sh` with your QA steps, then run it:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
BASE_URL=http://localhost:5274 OUT=./loom bash run-qa.sh
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Notes:
|
|
72
|
+
|
|
73
|
+
- Log in and seed data **before** `record start`, so the loom shows only the change.
|
|
74
|
+
- For a mobile check, run `agent-browser set viewport 390 844` inside a step. The build letterboxes it.
|
|
75
|
+
- After each step, wait about 800 ms so the viewer sees the result before the next action.
|
|
76
|
+
- Take your normal QA screenshots as well. They are the fallback.
|
|
77
|
+
|
|
78
|
+
## 2. Build the narrated mp4
|
|
79
|
+
|
|
80
|
+
`build-loom.mjs` reads `steps.json` and the raw recording. For each step it:
|
|
81
|
+
|
|
82
|
+
1. Calls ElevenLabs `POST /v1/text-to-speech/{voice}/with-timestamps` once and caches the mp3 and character alignment under `work/`. A re-run after a style tweak costs no characters.
|
|
83
|
+
2. Cuts the step's segment, speeds it up, and letterboxes it to 1920x1080 at 30 fps.
|
|
84
|
+
3. Freezes the last frame when the narration outlasts the segment.
|
|
85
|
+
4. Builds subtitle cues from the same character alignment, offset by `visualAt`, so voice and subtitles cannot drift.
|
|
86
|
+
|
|
87
|
+
Then it concatenates the segments with the concat demuxer, delays each step's audio to where its screen renders, mixes with `amix normalize=0`, burns the ASS subtitles, and writes `qa-loom.mp4` and `qa-loom.ass`.
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
node build-loom.mjs loom/steps.json loom/raw.webm loom/out > loom/build.log 2>&1 || tail -5 loom/build.log
|
|
91
|
+
cat loom/build.log # one JSON line: {"durationS":..,"steps":..,"cues":..,"chars":..}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Optional env: `SPEED` (default `1.33`), `VOICE` (default `21m00Tcm4TlvDq8ikWAM`, an ElevenLabs premade voice), `MODEL` (default `eleven_turbo_v2_5`), `FONT` (default `DejaVu Sans`).
|
|
95
|
+
|
|
96
|
+
## 3. Check the result
|
|
97
|
+
|
|
98
|
+
Look at one frame per cue, as PNG:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
cd loom/out
|
|
102
|
+
ffmpeg -hide_banner -i qa-loom.mp4 2>&1 | grep -E "Duration|Stream" # 1920x1080, 30 fps, h264 + aac
|
|
103
|
+
grep Dialogue qa-loom.ass # one short line per cue
|
|
104
|
+
ffmpeg -hide_banner -i qa-loom.mp4 -vn -af volumedetect -f null - 2>&1 | grep mean_volume # about -91 dB means silent
|
|
105
|
+
ffmpeg -y -loglevel error -ss 2 -i qa-loom.mp4 -frames:v 1 check-2s.png # view it: the subtitle matches the screen
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
If the loom feels slow or fast, change `SPEED` and re-run. TTS is cached, so the re-run is free.
|
|
109
|
+
|
|
110
|
+
## 4. Optional GIF preview
|
|
111
|
+
|
|
112
|
+
GitHub plays an mp4 inline only when it is a GitHub user attachment (section 6). The GIF is an optional silent preview with the subtitles already burned in.
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
ffmpeg -y -loglevel error -i qa-loom.mp4 \
|
|
116
|
+
-vf "fps=10,scale=960:-1:flags=lanczos,split[a][b];[a]palettegen=stats_mode=diff[p];[b][p]paletteuse=dither=bayer:bayer_scale=5:diff_mode=rectangle" \
|
|
117
|
+
-loop 0 qa-loom.gif 2>gif.err || tail -3 gif.err
|
|
118
|
+
ffmpeg -hide_banner -i qa-loom.gif 2>&1 | grep -E "Duration|Stream" # 960x540, 10 fps
|
|
119
|
+
head -c 6 qa-loom.gif; echo # GIF89a
|
|
120
|
+
ffmpeg -y -loglevel error -ss 5 -i qa-loom.gif -frames:v 1 gif-check.png # view it: subtitle readable
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
At these settings a GIF costs about 30 KB per second of loom. Keep it under 5 MB. If it is bigger, drop to `fps=8`, then to `scale=720:-1`. Below 720 px wide the subtitles become unreadable.
|
|
124
|
+
|
|
125
|
+
## 5. Keep the originals
|
|
126
|
+
|
|
127
|
+
Store `qa-loom.mp4` and `qa-loom.gif` wherever your team keeps QA evidence, next to the QA screenshots, under the same topic name. The GitHub attachment is the embed; your store is the durable original.
|
|
128
|
+
|
|
129
|
+
## 6. Attach to the PR
|
|
130
|
+
|
|
131
|
+
Upload both files to GitHub as user attachments. This uses GitHub's user-attachments upload endpoint, the one the web editor uses. It is not part of the documented REST API.
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
REPO=<owner>/<repo>
|
|
135
|
+
REPO_ID=$(gh api "repos/$REPO" --jq .id)
|
|
136
|
+
attach() { # $1 local file, $2 content type; prints https://github.com/user-attachments/assets/<uuid>
|
|
137
|
+
q=$(jq -rn --arg name "$(basename "$1")" --arg content_type "$2" --arg repository_id "$REPO_ID" \
|
|
138
|
+
'$ARGS.named | to_entries | map("\(.key)=\(.value | @uri)") | join("&")')
|
|
139
|
+
curl --fail-with-body -sS -X POST "https://uploads.github.com/user-attachments/assets?$q" \
|
|
140
|
+
-H "Authorization: Bearer $GITHUB_TOKEN" -H 'Content-Type: application/octet-stream' \
|
|
141
|
+
--data-binary "@$1" | jq -er .url
|
|
142
|
+
}
|
|
143
|
+
MP4_URL=$(attach qa-loom.mp4 video/mp4)
|
|
144
|
+
GIF_URL=$(attach qa-loom.gif image/gif)
|
|
145
|
+
echo "$MP4_URL $GIF_URL"
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Verification rules:
|
|
149
|
+
|
|
150
|
+
- Both URLs must match `https://github.com/user-attachments/assets/<uuid>`.
|
|
151
|
+
- A 401 means the token type is not accepted (GitHub App installation tokens are rejected; a personal access token works). A 404 means a wrong repository ID or a token without write access to the repo.
|
|
152
|
+
- Do not embed presigned storage URLs. A presigned mp4 usually downloads instead of playing, and it expires.
|
|
153
|
+
|
|
154
|
+
Put the loom in the PR body's `## Before / after` section. Write the section to a file:
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
cat > loom-section.md <<EOF
|
|
158
|
+
QA loom: <one-line summary>. <n> steps, <verdicts>.
|
|
159
|
+
|
|
160
|
+
$MP4_URL
|
|
161
|
+
|
|
162
|
+

|
|
163
|
+
EOF
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
`$MP4_URL` must sit on its own line, so GitHub renders a player with audio. The GIF line is optional. Merge the section into the body (`gh pr view <n> --json body`, then `gh pr edit <n> --body-file <file>`) and leave every other line unchanged.
|
|
167
|
+
|
|
168
|
+
Then open the PR and confirm that the mp4 player and the GIF render. A successful upload response does not prove that the embed renders.
|
|
169
|
+
|
|
170
|
+
## 7. Fallback: never block the PR
|
|
171
|
+
|
|
172
|
+
If ElevenLabs fails (401, 429, quota, or a 5xx after one retry with backoff), ffmpeg fails, or a requirement is missing:
|
|
173
|
+
|
|
174
|
+
1. Post the QA screenshots in the PR as GitHub user attachments.
|
|
175
|
+
2. Add one line to the PR that says why there is no loom, for example "QA loom skipped: ElevenLabs 429 quota exceeded."
|
|
176
|
+
3. Ship the PR. The loom is evidence, never a merge gate.
|
|
177
|
+
|
|
178
|
+
A 4xx validation error is deterministic. Do not loop on it. Fix the input or fall back.
|
|
179
|
+
|
|
180
|
+
## Deliverable checklist
|
|
181
|
+
|
|
182
|
+
- [ ] `steps.json` has `start`, `visualAt`, `end`, `verdict` and `narration` for every step
|
|
183
|
+
- [ ] `qa-loom.mp4` is 1920x1080 at 30 fps with h264 and aac, is not silent, and shows one short subtitle line at the bottom
|
|
184
|
+
- [ ] `qa-loom.gif`, if made, is 960 px wide at 10 fps, under 5 MB, and starts with `GIF89a`
|
|
185
|
+
- [ ] The originals are stored with the QA screenshots
|
|
186
|
+
- [ ] The PR body's `## Before / after` section has `$MP4_URL` on its own line, and every URL is `https://github.com/user-attachments/assets/<uuid>`
|
|
187
|
+
- [ ] You opened the PR and saw the player render
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
// build-loom.mjs: steps.json + raw recording -> narrated qa-loom.mp4 (+ qa-loom.ass, work/).
|
|
2
|
+
// Usage: node build-loom.mjs <steps.json> <raw.webm> <outDir> (bun works too)
|
|
3
|
+
// Env: ELEVENLABS_API_KEY (required), SPEED=1.33, VOICE=21m00Tcm4TlvDq8ikWAM,
|
|
4
|
+
// MODEL=eleven_turbo_v2_5, FONT="DejaVu Sans"
|
|
5
|
+
// Needs: Node 18+ (global fetch, top-level await) and ffmpeg built with libass (`ass` filter).
|
|
6
|
+
import { execFileSync } from "node:child_process";
|
|
7
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
8
|
+
import { join, resolve } from "node:path";
|
|
9
|
+
|
|
10
|
+
const [stepsPath, raw, outArg] = process.argv.slice(2);
|
|
11
|
+
if (!stepsPath || !raw || !outArg) {
|
|
12
|
+
throw new Error("usage: node build-loom.mjs <steps.json> <raw.webm> <outDir>");
|
|
13
|
+
}
|
|
14
|
+
const out = resolve(outArg);
|
|
15
|
+
const SPEED = Number(process.env.SPEED ?? 1.33);
|
|
16
|
+
const VOICE = process.env.VOICE ?? "21m00Tcm4TlvDq8ikWAM";
|
|
17
|
+
const MODEL = process.env.MODEL ?? "eleven_turbo_v2_5";
|
|
18
|
+
const FONT = process.env.FONT ?? "DejaVu Sans";
|
|
19
|
+
const KEY = process.env.ELEVENLABS_API_KEY;
|
|
20
|
+
if (!KEY) throw new Error("ELEVENLABS_API_KEY is not set");
|
|
21
|
+
mkdirSync(join(out, "work"), { recursive: true });
|
|
22
|
+
const W = (f) => join(out, "work", f);
|
|
23
|
+
|
|
24
|
+
// -loglevel error keeps ffmpeg's banner and progress output out of the logs.
|
|
25
|
+
const ff = (...args) => {
|
|
26
|
+
try {
|
|
27
|
+
execFileSync("ffmpeg", ["-y", "-hide_banner", "-loglevel", "error", ...args], {
|
|
28
|
+
stdio: ["ignore", "ignore", "pipe"],
|
|
29
|
+
});
|
|
30
|
+
} catch (e) {
|
|
31
|
+
throw new Error(`ffmpeg failed: ${String(e.stderr).slice(0, 400)}`);
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
const ENC = [
|
|
35
|
+
"-c:v",
|
|
36
|
+
"libx264",
|
|
37
|
+
"-preset",
|
|
38
|
+
"veryfast",
|
|
39
|
+
"-crf",
|
|
40
|
+
"23",
|
|
41
|
+
"-pix_fmt",
|
|
42
|
+
"yuv420p",
|
|
43
|
+
"-r",
|
|
44
|
+
"30",
|
|
45
|
+
"-video_track_timescale",
|
|
46
|
+
"15360",
|
|
47
|
+
];
|
|
48
|
+
// Keep the aspect ratio: letterbox or pillarbox into 1920x1080, never stretch (mobile viewports).
|
|
49
|
+
const FIT =
|
|
50
|
+
"scale=1920:1080:force_original_aspect_ratio=decrease,pad=1920:1080:(ow-iw)/2:(oh-ih)/2:color=white";
|
|
51
|
+
|
|
52
|
+
const { steps } = JSON.parse(readFileSync(stepsPath, "utf8"));
|
|
53
|
+
const concat = [];
|
|
54
|
+
const cues = [];
|
|
55
|
+
const audio = [];
|
|
56
|
+
let T = 0; // running output timeline, in seconds
|
|
57
|
+
|
|
58
|
+
for (const [i, s] of steps.entries()) {
|
|
59
|
+
const visualAt = s.visualAt ?? s.start;
|
|
60
|
+
const segLen = (s.end - s.start) / SPEED;
|
|
61
|
+
const v = (visualAt - s.start) / SPEED; // voice and subtitles start when the screen renders
|
|
62
|
+
|
|
63
|
+
// 1. TTS with character timestamps, one request per step. Cached per narration text,
|
|
64
|
+
// so a re-run after a style tweak costs no ElevenLabs characters.
|
|
65
|
+
const mp3 = W(`audio-${i + 1}.mp3`);
|
|
66
|
+
const alPath = W(`audio-${i + 1}.json`);
|
|
67
|
+
let cached = existsSync(alPath) ? JSON.parse(readFileSync(alPath, "utf8")) : null;
|
|
68
|
+
if (cached?.text !== s.narration) {
|
|
69
|
+
const res = await fetch(
|
|
70
|
+
`https://api.elevenlabs.io/v1/text-to-speech/${VOICE}/with-timestamps`,
|
|
71
|
+
{
|
|
72
|
+
method: "POST",
|
|
73
|
+
headers: { "xi-api-key": KEY, "content-type": "application/json" },
|
|
74
|
+
body: JSON.stringify({ text: s.narration, model_id: MODEL }),
|
|
75
|
+
},
|
|
76
|
+
);
|
|
77
|
+
if (!res.ok) throw new Error(`ElevenLabs ${res.status}: ${(await res.text()).slice(0, 200)}`);
|
|
78
|
+
const tts = await res.json();
|
|
79
|
+
// audio_base64 is large: write it straight to disk, never print it.
|
|
80
|
+
writeFileSync(mp3, Buffer.from(tts.audio_base64, "base64"));
|
|
81
|
+
cached = { text: s.narration, alignment: tts.alignment };
|
|
82
|
+
writeFileSync(alPath, JSON.stringify(cached));
|
|
83
|
+
}
|
|
84
|
+
const al = cached.alignment;
|
|
85
|
+
const aDur = al.character_end_times_seconds.at(-1);
|
|
86
|
+
|
|
87
|
+
// 2. Sped-up segment. Freeze the last frame when the narration outlasts it.
|
|
88
|
+
const seg = W(`seg-${i + 1}.mp4`);
|
|
89
|
+
ff(
|
|
90
|
+
"-ss",
|
|
91
|
+
String(s.start),
|
|
92
|
+
"-t",
|
|
93
|
+
String(s.end - s.start),
|
|
94
|
+
"-i",
|
|
95
|
+
raw,
|
|
96
|
+
"-an",
|
|
97
|
+
"-vf",
|
|
98
|
+
`setpts=(PTS-STARTPTS)/${SPEED},${FIT},fps=30`,
|
|
99
|
+
...ENC,
|
|
100
|
+
seg,
|
|
101
|
+
);
|
|
102
|
+
concat.push(seg);
|
|
103
|
+
const need = v + aDur + 0.35;
|
|
104
|
+
let len = segLen;
|
|
105
|
+
if (need > segLen) {
|
|
106
|
+
const png = W(`last-${i + 1}.png`);
|
|
107
|
+
ff("-sseof", "-0.2", "-i", seg, "-update", "1", "-frames:v", "1", png);
|
|
108
|
+
const pad = W(`pad-${i + 1}.mp4`);
|
|
109
|
+
const padLen = (need - segLen).toFixed(3);
|
|
110
|
+
ff("-loop", "1", "-framerate", "30", "-t", padLen, "-i", png, "-vf", FIT, ...ENC, pad);
|
|
111
|
+
concat.push(pad);
|
|
112
|
+
len = need;
|
|
113
|
+
}
|
|
114
|
+
audio.push({ mp3, delayMs: Math.round((T + v) * 1000) });
|
|
115
|
+
|
|
116
|
+
// 3. Subtitle cues from the same alignment, so voice and subtitles cannot drift.
|
|
117
|
+
// At most 6 words per cue, break on punctuation, merge orphans under 3 words.
|
|
118
|
+
const words = [];
|
|
119
|
+
let cur = null;
|
|
120
|
+
al.characters.forEach((ch, k) => {
|
|
121
|
+
if (/\s/.test(ch)) {
|
|
122
|
+
if (cur) words.push(cur);
|
|
123
|
+
cur = null;
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
if (!cur) cur = { text: "", t0: al.character_start_times_seconds[k] };
|
|
127
|
+
cur.text += ch;
|
|
128
|
+
cur.t1 = al.character_end_times_seconds[k];
|
|
129
|
+
});
|
|
130
|
+
if (cur) words.push(cur);
|
|
131
|
+
const groups = [];
|
|
132
|
+
let g = [];
|
|
133
|
+
for (const w of words) {
|
|
134
|
+
g.push(w);
|
|
135
|
+
if (g.length >= 6 || /[.,;:!?]$/.test(w.text)) {
|
|
136
|
+
groups.push(g);
|
|
137
|
+
g = [];
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (g.length) groups.push(g);
|
|
141
|
+
for (let k = groups.length - 1; k > 0; k--) {
|
|
142
|
+
if (groups[k].length < 3) {
|
|
143
|
+
groups[k - 1].push(...groups[k]);
|
|
144
|
+
groups.splice(k, 1);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
for (const grp of groups) {
|
|
148
|
+
cues.push({
|
|
149
|
+
t0: T + v + grp[0].t0,
|
|
150
|
+
t1: T + v + grp.at(-1).t1 + 0.1,
|
|
151
|
+
text: grp.map((w) => w.text).join(" "),
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
T += len;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// 4. Concat with the concat demuxer. The concat filter in one big filtergraph can
|
|
158
|
+
// truncate the video stream.
|
|
159
|
+
writeFileSync(W("concat.txt"), concat.map((f) => `file '${f}'`).join("\n"));
|
|
160
|
+
ff("-f", "concat", "-safe", "0", "-i", W("concat.txt"), "-c", "copy", W("video.mp4"));
|
|
161
|
+
|
|
162
|
+
// 5. ASS subtitles: one small line, bottom centre, dark box. libass ignores the
|
|
163
|
+
// BackColour alpha with BorderStyle=3, so the box is solid #303030.
|
|
164
|
+
const ts = (t) => {
|
|
165
|
+
const cs = Math.round(t * 100);
|
|
166
|
+
const h = Math.floor(cs / 360000);
|
|
167
|
+
const m = String(Math.floor(cs / 6000) % 60).padStart(2, "0");
|
|
168
|
+
const sec = String(Math.floor(cs / 100) % 60).padStart(2, "0");
|
|
169
|
+
return `${h}:${m}:${sec}.${String(cs % 100).padStart(2, "0")}`;
|
|
170
|
+
};
|
|
171
|
+
for (let k = 0; k < cues.length - 1; k++) cues[k].t1 = Math.min(cues[k].t1, cues[k + 1].t0);
|
|
172
|
+
const dialogue = cues
|
|
173
|
+
.map((c) => `Dialogue: 0,${ts(c.t0)},${ts(c.t1)},Default,,0,0,0,,${c.text}`)
|
|
174
|
+
.join("\n");
|
|
175
|
+
const ass = `[Script Info]
|
|
176
|
+
ScriptType: v4.00+
|
|
177
|
+
PlayResX: 1920
|
|
178
|
+
PlayResY: 1080
|
|
179
|
+
|
|
180
|
+
[V4+ Styles]
|
|
181
|
+
Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
|
|
182
|
+
Style: Default,${FONT},38,&H00FFFFFF,&H00FFFFFF,&H303030,&H00000000,0,0,0,0,100,100,0,0,3,2,0,2,40,40,30,1
|
|
183
|
+
|
|
184
|
+
[Events]
|
|
185
|
+
Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
|
|
186
|
+
${dialogue}
|
|
187
|
+
`;
|
|
188
|
+
const assPath = join(out, "qa-loom.ass");
|
|
189
|
+
writeFileSync(assPath, ass);
|
|
190
|
+
|
|
191
|
+
// 6. Mux: delay each step's audio to where its screen renders, mix, burn subtitles.
|
|
192
|
+
// normalize=0 stops amix from dividing the volume by the number of inputs.
|
|
193
|
+
const inputs = audio.flatMap((a) => ["-i", a.mp3]);
|
|
194
|
+
const delays = audio.map((a, k) => `[${k + 1}:a]adelay=${a.delayMs}:all=1[a${k}]`).join(";");
|
|
195
|
+
const labels = audio.map((_, k) => `[a${k}]`).join("");
|
|
196
|
+
const mix = `${labels}amix=inputs=${audio.length}:normalize=0:duration=longest[a]`;
|
|
197
|
+
ff(
|
|
198
|
+
"-i",
|
|
199
|
+
W("video.mp4"),
|
|
200
|
+
...inputs,
|
|
201
|
+
"-filter_complex",
|
|
202
|
+
`${delays};${mix};[0:v]ass=${assPath}[v]`,
|
|
203
|
+
"-map",
|
|
204
|
+
"[v]",
|
|
205
|
+
"-map",
|
|
206
|
+
"[a]",
|
|
207
|
+
...ENC,
|
|
208
|
+
"-c:a",
|
|
209
|
+
"aac",
|
|
210
|
+
"-b:a",
|
|
211
|
+
"128k",
|
|
212
|
+
"-movflags",
|
|
213
|
+
"+faststart",
|
|
214
|
+
"-t",
|
|
215
|
+
T.toFixed(3),
|
|
216
|
+
join(out, "qa-loom.mp4"),
|
|
217
|
+
);
|
|
218
|
+
const chars = steps.reduce((n, s) => n + s.narration.length, 0);
|
|
219
|
+
console.log(
|
|
220
|
+
JSON.stringify({ durationS: +T.toFixed(2), steps: steps.length, cues: cues.length, chars }),
|
|
221
|
+
);
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# run-qa.sh: record a UI QA pass with agent-browser and write steps.json.
|
|
3
|
+
# Every step offset is in seconds from the moment recording starts.
|
|
4
|
+
#
|
|
5
|
+
# Usage: BASE_URL=http://localhost:5274 OUT=./loom bash run-qa.sh
|
|
6
|
+
# Needs: agent-browser (with `record`), jq, GNU date (%N). No bc: time math runs in jq.
|
|
7
|
+
#
|
|
8
|
+
# Replace the two example steps with the steps that prove your change.
|
|
9
|
+
set -euo pipefail
|
|
10
|
+
BASE_URL=${BASE_URL:-http://localhost:5274}
|
|
11
|
+
OUT=${OUT:-./loom}
|
|
12
|
+
mkdir -p "$OUT"
|
|
13
|
+
LOG="$OUT/steps.ndjson"
|
|
14
|
+
: >"$LOG"
|
|
15
|
+
|
|
16
|
+
now() { date +%s.%N; }
|
|
17
|
+
# act: call right before the action fires. It marks the step start.
|
|
18
|
+
act() { STEP_START=$(now); }
|
|
19
|
+
# step <action> <observed> <verdict> <narration>: call AFTER the resulting screen has
|
|
20
|
+
# rendered (after an `agent-browser wait <selector>`). That moment becomes visualAt.
|
|
21
|
+
step() {
|
|
22
|
+
jq -nc --arg a "$1" --arg o "$2" --arg v "$3" --arg n "$4" \
|
|
23
|
+
--arg t0 "$T0" --arg s "$STEP_START" --arg va "$(now)" \
|
|
24
|
+
'{action:$a, observed:$o, verdict:$v, narration:$n,
|
|
25
|
+
start:(($s|tonumber)-($t0|tonumber)), visualAt:(($va|tonumber)-($t0|tonumber))}' >>"$LOG"
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
# Log in and seed data BEFORE recording starts.
|
|
29
|
+
agent-browser set viewport 1920 1080 >/dev/null
|
|
30
|
+
agent-browser open "$BASE_URL" >/dev/null
|
|
31
|
+
agent-browser wait 1500 >/dev/null
|
|
32
|
+
agent-browser record start "$OUT/raw.webm" --cursor --fps 30 >/dev/null
|
|
33
|
+
T0=$(now)
|
|
34
|
+
|
|
35
|
+
act
|
|
36
|
+
agent-browser reload >/dev/null
|
|
37
|
+
agent-browser wait h1 >/dev/null
|
|
38
|
+
step "Load home page" "Heading and nav render" pass "The home page loads with its heading and nav."
|
|
39
|
+
agent-browser wait 800 >/dev/null # dwell so the viewer sees the result before the next action
|
|
40
|
+
|
|
41
|
+
act
|
|
42
|
+
agent-browser open "$BASE_URL/settings" >/dev/null
|
|
43
|
+
agent-browser wait h1 >/dev/null
|
|
44
|
+
step "Open settings" "Settings page renders" pass "The settings page opens."
|
|
45
|
+
agent-browser wait 800 >/dev/null
|
|
46
|
+
|
|
47
|
+
END=$(jq -n --arg a "$(now)" --arg b "$T0" '($a|tonumber)-($b|tonumber)')
|
|
48
|
+
agent-browser record stop >/dev/null
|
|
49
|
+
|
|
50
|
+
# Each step ends where the next one starts; the last one ends at record stop.
|
|
51
|
+
jq -s --argjson end "$END" '
|
|
52
|
+
{steps: ([to_entries[] | .value + {idx: (.key + 1)}]
|
|
53
|
+
| . as $s
|
|
54
|
+
| [range(0; length) as $i
|
|
55
|
+
| $s[$i] + {end: (if $i + 1 < ($s | length) then $s[$i + 1].start else $end end)}])}
|
|
56
|
+
' "$LOG" >"$OUT/steps.json"
|
|
57
|
+
jq -c '.steps[] | {idx, start, visualAt, end, verdict}' "$OUT/steps.json"
|
|
@@ -48,3 +48,7 @@ When your heartbeat runbook says so, check for unaddressed requests older than o
|
|
|
48
48
|
## Scripts-only mode
|
|
49
49
|
|
|
50
50
|
The named Slack tools are not registered. Use `script-run` with inline source: `ctx.swarm.slack_reply({ taskId, message })`. The task ID carries the thread context.
|
|
51
|
+
|
|
52
|
+
## Status and blocker questions
|
|
53
|
+
|
|
54
|
+
Answer for the scope of the place the question was asked. In a customer or project channel or thread, "current blockers" or "what's the status" means that customer's or project's work: its tracker, its PRs, its open asks. Answer swarm-wide only in a DM or an ops channel, or when the person asks for it. Name the scope you used in the first line.
|
|
@@ -47,10 +47,12 @@ curl -s "$MCP_BASE_URL/api/extensions/type-defs" \
|
|
|
47
47
|
| `pre.task.create` | before any task is stored (REST, MCP, Slack, schedule, workflow, webhook, follow-up) | `block(reason)`, `modify({ priority, agentId, tags, description, ... })`, or nothing |
|
|
48
48
|
| `pre.task.followUp` | before the lead follow-up task for a finished worker task | `block`, `modify({ description, priority, agentId })`, or nothing |
|
|
49
49
|
| `pre.slack.route` | before a Slack message becomes a task | `modify({ target: { kind: "agent", agentId } })`, `{ kind: "lead" }`, `{ kind: "broadcast" }` |
|
|
50
|
+
| `pre.chat.route` | before buffered Chat SDK messages (Telegram, Slack, Discord, Teams) become a task | `block(reason)`, or `modify({ target: { kind: "agent", agentId } })`, `{ kind: "lead" }`, `{ kind: "broadcast" }`, `{ kind: "workflow", workflowId }` |
|
|
50
51
|
| `pre.heartbeat.remediate` | after the heartbeat sweep finds a stalled task | `modify({ proposedAction: "record" | "fail" | "supersede-resume" })` |
|
|
51
52
|
| `pre.tool.call` | before an agent MCP tool call runs | `block(reason)` or `modify({ args })` |
|
|
52
53
|
| `post.task.created`, `post.task.completed`, `post.task.failed`, `post.task.cancelled`, `post.task.superseded`, `post.task.progress` | after the change is committed | none |
|
|
53
54
|
| `post.slack.message`, `post.tool.call` | after the message or tool call finished | none |
|
|
55
|
+
| `post.chat.message` | after a Chat SDK message is received (before buffering) or sent; `direction` is `inbound` or `outbound` | none |
|
|
54
56
|
| `post.approval.resolved` | after a person answers an approval request, workflow or standalone (not on timeout or cancel) | none |
|
|
55
57
|
| `post.task.budgetRefused` | after a task is refused by a daily spend budget | none |
|
|
56
58
|
| `post.email.received`, `post.kapso.message` | after an inbound AgentMail or Kapso message is received | none |
|
|
@@ -76,7 +76,7 @@ export default async function (args: z.infer<typeof argsSchema>, ctx: ScriptCont
|
|
|
76
76
|
|
|
77
77
|
- `ctx.swarm.*`: the swarm SDK. `task_get`, `task_send`, `task_storeProgress`, `task_action`, `task_list`, `slack_reply`, `memory_search`, `memory_store`, `kv_get`, `kv_getOrNull`, `kv_set`, `kv_delete`, `kv_incr`, `kv_list`, `swarm_get`, `agent_info`, `db_query`, and more. `kv_getOrNull` returns the entry, `null` on a missing key, and throws on other errors.
|
|
78
78
|
- `ctx.swarm.task_send` requires `routingReason` and `routingNote` whenever `agentId` is present. The note must explain the worker choice in at least 10 characters after trimming whitespace (maximum 200 characters). Use `human_pinned` for an explicit/configured choice, `skill` for a role or specialization match, `continuity` for the same worker/session, `reroute_fault` for a fault handoff, or `overflow` for capacity/pool escalation. Omit both `agentId` and routing fields for the pool.
|
|
79
|
-
- `ctx.swarm.config`: `apiKey`, `agentId`, `mcpBaseUrl`, and `ctx.swarm.config.get("KEY")` for
|
|
79
|
+
- `ctx.swarm.config`: `apiKey`, `agentId`, `mcpBaseUrl`, and `ctx.swarm.config.get("KEY")` for swarm config values, resolved for the agent running the script (agent scope over global, the same rows `get-config` returns for that agent). A key that is not set returns `undefined`, so `config.get("KEY") ?? fallback` works. All are `Redacted` wrappers that stringify to `<redacted>`. Never unwrap one into a return value, a log line, or a request body you build by hand.
|
|
80
80
|
- `ctx.api.<slug>` and `ctx.mcp.<slug>`: typed clients for registered connections. They exist only for registered connections. Introspect with `Object.keys(ctx.api ?? {})` and `Object.keys(ctx.mcp ?? {})`.
|
|
81
81
|
- `ctx.stdlib`: `fetch`, `fetchJson` (retries, 30 s timeout), `grep`, `glob`, `table`, `Redacted`.
|
|
82
82
|
- `ctx.logger`: `log`, `warn`, `error`. Keep logs short.
|
|
@@ -107,11 +107,13 @@ export default async function main(args: { status?: string; limit?: number } | u
|
|
|
107
107
|
|
|
108
108
|
## Secrets
|
|
109
109
|
|
|
110
|
+
A config row reaches your shell env only when its `exportToEnv` flag is on. If `$KEY` is empty in your shell but `get-config` or `list-config` shows the row marked `not in agent env`, the row stays server-side on purpose: do not copy the value into a file, an env var, or a command. Use it from a script instead, through one of the paths below. Harness, git, and agent-fs credentials stay exported. Keys only the API server reads (Slack app token and signing secret, GitHub App private key, webhook and OAuth client secrets) can never be exported. If a recipe genuinely needs the variable in a shell, ask the lead to turn `exportToEnv` on for that row (`set-config` with `exportToEnv: true`); it applies to your next task.
|
|
111
|
+
|
|
110
112
|
Order of preference:
|
|
111
113
|
|
|
112
114
|
1. A registered connection: `ctx.api.<slug>.<operationId>({ path, query, header, body })` (OpenAPI), `ctx.api.<slug>.graphql(query, variables)` (GraphQL), `ctx.mcp.<slug>.<toolName>(args)` (MCP, proxied server-side, returns the raw MCP envelope: read `res.structuredContent ?? res.content?.[0]?.text`). Credentials attach at egress. The script never sees them. OAuth tokens refresh on their own.
|
|
113
115
|
2. A credential binding placeholder in a hand-written request: `Authorization: Bearer [REDACTED:GITHUB_TOKEN]`. The runtime substitutes the value at egress, only for the binding's allowed hosts.
|
|
114
|
-
3. `ctx.swarm.config.get("KEY")` for a config value you pass to `ctx.stdlib.fetch` as a header value.
|
|
116
|
+
3. `ctx.swarm.config.get("KEY")` for a config value you pass to `ctx.stdlib.fetch` as a header value. Unwrap it with `ctx.stdlib.Redacted.value(...)` inside the header itself (a wrapped value sends the literal `<redacted>`), and never log it. Secret values are scrubbed from run output.
|
|
115
117
|
|
|
116
118
|
A raw secret, or the output of `get-config` with `includeSecrets: true`, must not appear in script source, script `args`, a schedule's `taskTemplate` or `scriptArgs`, or a task description. Those are stored as plain text and replayed on every run.
|
|
117
119
|
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"slug": "swarm-scripts",
|
|
6
6
|
"title": "Swarm Scripts",
|
|
7
7
|
"description": "Bulk, repeat, fan-out, or data-heavy work: write and run swarm scripts (inline `script-run`, named `script-upsert`, durable `launch-script-run`). Covers the script-vs-tool rubric, the authoring contract (`args` first, `ctx` second), the seed catalog, connections and secrets, `db_query`, and exposing a script as an API.",
|
|
8
|
-
"version": "1.
|
|
8
|
+
"version": "1.2.0",
|
|
9
9
|
"category": "skills",
|
|
10
10
|
"placeholders": [],
|
|
11
11
|
"runAllSeedersCandidate": true,
|
|
@@ -71,7 +71,7 @@ export default async function (args: z.infer<typeof argsSchema>, ctx: ScriptCont
|
|
|
71
71
|
|
|
72
72
|
- `ctx.swarm.*`: the swarm SDK. `task_get`, `task_send`, `task_storeProgress`, `task_action`, `task_list`, `slack_reply`, `memory_search`, `memory_store`, `kv_get`, `kv_getOrNull`, `kv_set`, `kv_delete`, `kv_incr`, `kv_list`, `swarm_get`, `agent_info`, `db_query`, and more. `kv_getOrNull` returns the entry, `null` on a missing key, and throws on other errors.
|
|
73
73
|
- `ctx.swarm.task_send` requires `routingReason` and `routingNote` whenever `agentId` is present. The note must explain the worker choice in at least 10 characters after trimming whitespace (maximum 200 characters). Use `human_pinned` for an explicit/configured choice, `skill` for a role or specialization match, `continuity` for the same worker/session, `reroute_fault` for a fault handoff, or `overflow` for capacity/pool escalation. Omit both `agentId` and routing fields for the pool.
|
|
74
|
-
- `ctx.swarm.config`: `apiKey`, `agentId`, `mcpBaseUrl`, and `ctx.swarm.config.get("KEY")` for
|
|
74
|
+
- `ctx.swarm.config`: `apiKey`, `agentId`, `mcpBaseUrl`, and `ctx.swarm.config.get("KEY")` for swarm config values, resolved for the agent running the script (agent scope over global, the same rows `get-config` returns for that agent). A key that is not set returns `undefined`, so `config.get("KEY") ?? fallback` works. All are `Redacted` wrappers that stringify to `<redacted>`. Never unwrap one into a return value, a log line, or a request body you build by hand.
|
|
75
75
|
- `ctx.api.<slug>` and `ctx.mcp.<slug>`: typed clients for registered connections. They exist only for registered connections. Introspect with `Object.keys(ctx.api ?? {})` and `Object.keys(ctx.mcp ?? {})`.
|
|
76
76
|
- `ctx.stdlib`: `fetch`, `fetchJson` (retries, 30 s timeout), `grep`, `glob`, `table`, `Redacted`.
|
|
77
77
|
- `ctx.logger`: `log`, `warn`, `error`. Keep logs short.
|
|
@@ -102,11 +102,13 @@ export default async function main(args: { status?: string; limit?: number } | u
|
|
|
102
102
|
|
|
103
103
|
## Secrets
|
|
104
104
|
|
|
105
|
+
A config row reaches your shell env only when its `exportToEnv` flag is on. If `$KEY` is empty in your shell but `get-config` or `list-config` shows the row marked `not in agent env`, the row stays server-side on purpose: do not copy the value into a file, an env var, or a command. Use it from a script instead, through one of the paths below. Harness, git, and agent-fs credentials stay exported. Keys only the API server reads (Slack app token and signing secret, GitHub App private key, webhook and OAuth client secrets) can never be exported. If a recipe genuinely needs the variable in a shell, ask the lead to turn `exportToEnv` on for that row (`set-config` with `exportToEnv: true`); it applies to your next task.
|
|
106
|
+
|
|
105
107
|
Order of preference:
|
|
106
108
|
|
|
107
109
|
1. A registered connection: `ctx.api.<slug>.<operationId>({ path, query, header, body })` (OpenAPI), `ctx.api.<slug>.graphql(query, variables)` (GraphQL), `ctx.mcp.<slug>.<toolName>(args)` (MCP, proxied server-side, returns the raw MCP envelope: read `res.structuredContent ?? res.content?.[0]?.text`). Credentials attach at egress. The script never sees them. OAuth tokens refresh on their own.
|
|
108
110
|
2. A credential binding placeholder in a hand-written request: `Authorization: Bearer [REDACTED:GITHUB_TOKEN]`. The runtime substitutes the value at egress, only for the binding's allowed hosts.
|
|
109
|
-
3. `ctx.swarm.config.get("KEY")` for a config value you pass to `ctx.stdlib.fetch` as a header value.
|
|
111
|
+
3. `ctx.swarm.config.get("KEY")` for a config value you pass to `ctx.stdlib.fetch` as a header value. Unwrap it with `ctx.stdlib.Redacted.value(...)` inside the header itself (a wrapped value sends the literal `<redacted>`), and never log it. Secret values are scrubbed from run output.
|
|
110
112
|
|
|
111
113
|
A raw secret, or the output of `get-config` with `includeSecrets: true`, must not appear in script source, script `args`, a schedule's `taskTemplate` or `scriptArgs`, or a task description. Those are stored as plain text and replayed on every run.
|
|
112
114
|
|
|
@@ -173,6 +173,44 @@ Afterwards, a `warnings` entry on `create-workflow` or a patch tool means the ke
|
|
|
173
173
|
- Do not set `retry` or `validation.retry` on a `system-one-decision` node. It retries connection errors, 408, 429, and 5xx itself (`config.maxRetries`, 0 to 3, default 2) inside `config.timeoutMs` (default 30000). A 401, a 422, or an answer that fails validation ends after one attempt.
|
|
174
174
|
- The node reads its provider's key (`TYPESAFE_API_KEY`, `OPENROUTER_API_KEY`, `LAYA_API_KEY`, `OPENAI_DECISIONS_API_KEY`, or `CLOUDFLARE_API_TOKEN`, plus `LAYA_URL` for `laya` and `CLOUDFLARE_ACCOUNT_ID` for `cloudflare`) server-side. The key never appears in the definition, the step output, or an error. A run whose key or `LAYA_URL` is missing fails before any node executes, and a 401 or 403 fails the step as `<KEY> was rejected`. An unresolved `{{token}}` fails the step before any request.
|
|
175
175
|
|
|
176
|
+
#### Jev images (direct TypeSafe API)
|
|
177
|
+
|
|
178
|
+
`system-one-decision` cannot send images today: its strict config has no `assets` field, its wire sends only `state`, `model`, and `questions`, and it does not send the image opt-in header. A URL or base64 string in `state` stays text. Use a `swarm-script` calling `POST https://api.typesafe.ai/v1/systemone` with the credential kept server-side until the node supports assets.
|
|
179
|
+
|
|
180
|
+
- Send `TypeSafe-Danger-Zone-Images: true`. Both `jev-1.13.0` and `jev-1.14.0` accept images; Box questions require 1.14. Pin the model you tested.
|
|
181
|
+
- Upload PNG or JPEG bytes, not a URL. Prefer multipart: first part `request` is the JSON with content type `application/json`; an asset such as `assets[uuid] = { type: "image", part: "photo-file" }` names its file part. For JSON-only calls, use the base64 shape below: standard padded base64, no whitespace or `data:` prefix.
|
|
182
|
+
- Each image must be still, at most 16 MiB decoded, 256–1568 pixels on each side, and 78,400–2,458,624 pixels total (256×256 is too small). Request body, all processed images together, and the expanded request each have a 32 MiB limit. Limits reject the whole request; the API does not resize. Rotate pixels upright before sending because EXIF orientation is not applied.
|
|
183
|
+
- Generate a fresh UUID reference per image. References are replaced wherever they occur in string values in `state`, `instructions`, or `criteria`; object keys are not replaced. Every asset must be referenced. Repeating the same reference inserts and bills another image copy.
|
|
184
|
+
|
|
185
|
+
```json
|
|
186
|
+
{
|
|
187
|
+
"model": "jev-1.13.0",
|
|
188
|
+
"state": { "photo": "d1369c6a-538e-4ab3-91cd-f76f2f67b651" },
|
|
189
|
+
"assets": {
|
|
190
|
+
"d1369c6a-538e-4ab3-91cd-f76f2f67b651": {
|
|
191
|
+
"type": "image", "format": "png", "data_base64": "<encoded PNG bytes>"
|
|
192
|
+
}
|
|
193
|
+
},
|
|
194
|
+
"questions": {
|
|
195
|
+
"ready": { "type": "noul", "instructions": "Is the product in `photo` fully assembled?" }
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Measured 2026-10-15 on `jev-1.13.0`, using an 850×850 RGB PNG with a red square on white. Cost is the complete request's `usage.input_tokens × $0.042 / 1,000,000`; output is free:
|
|
201
|
+
|
|
202
|
+
| Placement | Questions | Input tokens | Request cost (USD) |
|
|
203
|
+
|---|---:|---:|---:|
|
|
204
|
+
| Once in `state` | 1 | 2,273 | $0.000095466 |
|
|
205
|
+
| Once in `state`, questions use the `photo` path | 2 | 2,289 | $0.000096138 |
|
|
206
|
+
| Twice in `state` | 1 | 4,237 | $0.000177954 |
|
|
207
|
+
| Once in each question's `instructions` | 2 | 4,203 | $0.000176526 |
|
|
208
|
+
| Once in `state`, resized to 425×425 | 1 | 863 | $0.000036246 |
|
|
209
|
+
|
|
210
|
+
The roughly $0.0001 claim (about 10,000 one-image requests per dollar) holds for this example; the exact cost depends on image size and prompt. **Question count alone does not multiply a shared state image's tokens.** Put shared images in `state` once and have questions use its field path, not the asset UUID. Image duplication scales with repeated references, including one copy per question when embedded in each question's instructions. If several judgments form one classification, a single `choice` can avoid repeating question-local images. Downscale when the detail permits: halving both dimensions reduced this request's billed tokens from 2,273 to 863; recheck accuracy on your images.
|
|
211
|
+
|
|
212
|
+
Contract and current pricing: [TypeSafe images](https://docs.typesafe.ai/images), [models](https://docs.typesafe.ai/models). Re-measure before budgeting a different model, resolution, or prompt.
|
|
213
|
+
|
|
176
214
|
### `human-in-the-loop` (how approvals render)
|
|
177
215
|
|
|
178
216
|
The same rules apply to a standalone `request-human-input` call.
|