@desplega.ai/agent-swarm 2.7.5 → 2.7.7
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/dist/{acp-adapter-g3ads55s.js → acp-adapter-7b4vk3h5.js} +5 -4
- package/dist/{actions-zwfykqqk.js → actions-3tt153af.js} +18 -17
- package/dist/{amp-adapter-z3s0ssdt.js → amp-adapter-njpts0yr.js} +1 -1
- package/dist/{anthropic-messages-x4q6rfq5.js → anthropic-messages-xey2skwx.js} +7 -7
- package/dist/{app-vmp4fgep.js → app-nky4378f.js} +3 -3
- package/dist/{assistant-vwde653f.js → assistant-jnb669pa.js} +24 -22
- package/dist/{azure-openai-responses-s1w5n70z.js → azure-openai-responses-26t7j9ra.js} +5 -5
- package/dist/{boot-reembed-375t7tbk.js → boot-reembed-9mgvaztt.js} +1 -1
- package/dist/{boot-reembed-gq9rcpnj.js → boot-reembed-qs2djdej.js} +1 -1
- package/dist/{boot-scrub-sweep-nd938kyf.js → boot-scrub-sweep-yjpdj7s1.js} +3 -3
- package/dist/{boot-seeds-aykx51v3.js → boot-seeds-5cm6ambd.js} +4 -4
- package/dist/{boxd-k1j26g3h.js → boxd-nv507va8.js} +5 -5
- package/dist/{claude-adapter-8j4hm099.js → claude-adapter-fn287zmm.js} +5 -4
- package/dist/{claude-managed-setup-0wyynpn1.js → claude-managed-setup-wtdz2mj2.js} +1 -1
- package/dist/{claude-sdk-session-q6wj5hmy.js → claude-sdk-session-x91kqyhm.js} +9677 -9514
- package/dist/{cli-j3jvnpe8.js → cli-08bmz9z9.js} +1 -1
- package/dist/{cli-gjr2y2xk.js → cli-0agcv1v5.js} +4 -1
- package/dist/{cli-y24sc29k.js → cli-1c6tzt8g.js} +30 -8
- package/dist/{cli-mjzynrse.js → cli-35rsp8ye.js} +1 -1
- package/dist/{cli-rpm2sgv0.js → cli-3fg0bdcv.js} +19 -2
- package/dist/{cli-nn4d8dne.js → cli-5b456bpf.js} +2 -2
- package/dist/{cli-bhjt02p8.js → cli-5jnhv72d.js} +1 -1
- package/dist/{cli-fqzx7gwa.js → cli-5zx699j9.js} +2 -2
- package/dist/{cli-hf6zt5m6.js → cli-69szn7w7.js} +17 -1
- package/dist/{cli-vqypxzh4.js → cli-6g0hb4cp.js} +8 -8
- package/dist/{cli-tqgg40ek.js → cli-6pfd54vx.js} +9 -8
- package/dist/{cli-bsn8nhrq.js → cli-6vcp76qk.js} +3 -3
- package/dist/{cli-zppwg9p9.js → cli-72v799xn.js} +1 -1
- package/dist/cli-82j36f2m.js +45 -0
- package/dist/{cli-hn9hxmz9.js → cli-8305gbq0.js} +29 -16
- package/dist/{cli-z8eh3ex8.js → cli-8sckt74b.js} +8 -8
- package/dist/{cli-bnz7a7qf.js → cli-95jzxcp3.js} +1 -1
- package/dist/{cli-0bxqnvq9.js → cli-adeegh7d.js} +29 -7
- package/dist/{cli-g61qxmge.js → cli-afkqkrtw.js} +3 -3
- package/dist/{cli-7a42nfy0.js → cli-ayayvfg4.js} +24 -4
- package/dist/{cli-bqt1mptt.js → cli-az48n0nr.js} +4 -4
- package/dist/{cli-z6jkx23q.js → cli-bfv7vdg6.js} +9 -9
- package/dist/{cli-pywf3eh5.js → cli-caj33vcq.js} +5 -5
- package/dist/{cli-5pag6rmn.js → cli-cb2a8pna.js} +1 -1
- package/dist/{cli-wvaawacd.js → cli-cppkf404.js} +5236 -72
- package/dist/{cli-mry0qzym.js → cli-ddaanqkd.js} +123 -10
- package/dist/{cli-0gey5fzy.js → cli-e0kmphd6.js} +2 -2
- package/dist/{cli-zh3fp8v2.js → cli-e46hg0xw.js} +1 -1
- package/dist/{cli-jm2vfpfd.js → cli-e5bzrn01.js} +1 -1
- package/dist/{cli-qxnf8z2z.js → cli-excnnjg2.js} +165 -153
- package/dist/{cli-71ptav7x.js → cli-ferkkrg9.js} +1 -1
- package/dist/{cli-vpf89f34.js → cli-fmjd0t1y.js} +3 -3
- package/dist/{cli-rm6hqj3z.js → cli-fyt2g4wd.js} +33 -139
- package/dist/cli-g08zdaec.js +55 -0
- package/dist/{cli-ehq736cn.js → cli-g7s47cc5.js} +4 -4
- package/dist/{cli-82ae98pz.js → cli-gz6q4eve.js} +3 -3
- package/dist/{cli-82jg7fp9.js → cli-he5nxaa8.js} +1 -1
- package/dist/{cli-rqkxrmgw.js → cli-je518txm.js} +39 -14
- package/dist/{cli-8hs41zy2.js → cli-jtjtabxe.js} +1 -1
- package/dist/{cli-611a1294.js → cli-k0wwfqbg.js} +18 -1
- package/dist/{cli-nzxr556s.js → cli-k1nje3v3.js} +38 -43
- package/dist/{cli-3h4nhjn8.js → cli-k8196sh2.js} +9 -7
- package/dist/{cli-afrrppsw.js → cli-k899ehvh.js} +2 -2
- package/dist/{cli-0731hvbw.js → cli-m0da8j35.js} +23 -9
- package/dist/cli-m2z8jagn.js +12078 -0
- package/dist/{cli-8veytvbh.js → cli-mkk9rnzz.js} +5 -5
- package/dist/{cli-7wspm5gm.js → cli-n6bv12jn.js} +528 -6
- package/dist/{cli-spknvxbq.js → cli-nmzhp1b5.js} +14 -12
- package/dist/{cli-p599f1qm.js → cli-p82cnnjq.js} +1 -1
- package/dist/{cli-xtjhfmpc.js → cli-pc6n7fre.js} +1 -1
- package/dist/{cli-vbyfwh92.js → cli-pjctyea3.js} +2 -2
- package/dist/{cli-nmbmy64z.js → cli-pq73h5mz.js} +3 -3
- package/dist/{cli-aa6yxc7m.js → cli-pz4rk021.js} +1 -1
- package/dist/{cli-7wcyk2s5.js → cli-q3tkegby.js} +1 -1
- package/dist/cli-qghe0h66.js +67 -0
- package/dist/{cli-6s7vdcta.js → cli-qh4d4tvk.js} +4 -4
- package/dist/{cli-mmwm7zbf.js → cli-rtt8g4v9.js} +1 -1
- package/dist/{cli-kg8hchyg.js → cli-sa02jsqt.js} +1 -1
- package/dist/{cli-1h3wcpbw.js → cli-snpj5zbj.js} +2 -2
- package/dist/{cli-a7v854rf.js → cli-tgrv9cjx.js} +263 -12
- package/dist/{cli-a47xy8eq.js → cli-vz22fyqv.js} +186 -54
- package/dist/{cli-1ayk1eas.js → cli-wk5yrfwd.js} +285 -42
- package/dist/{cli-1338796d.js → cli-ws19768n.js} +1755 -119
- package/dist/{cli-70r77n04.js → cli-xae4g51r.js} +1 -1
- package/dist/{cli-gf5hbmdr.js → cli-yjdgea32.js} +3 -3
- package/dist/{cli-xbkdm710.js → cli-yndmfxb3.js} +3 -3
- package/dist/{cli-rhw40jgb.js → cli-yyj3j5m4.js} +2 -2
- package/dist/cli.js +26 -25
- package/dist/{cloudflare-workers-ai-system-one-bm60fjm0.js → cloudflare-workers-ai-system-one-ef2a980q.js} +2 -2
- package/dist/{codex-adapter-q6k3gnyg.js → codex-adapter-cf2yxe9m.js} +3 -2
- package/dist/{codex-hook-9efm2x1m.js → codex-hook-a0c6g6t6.js} +1 -1
- package/dist/{codex-session-runner-3mwmk3d9.js → codex-session-runner-6vfzdgbd.js} +3 -2
- package/dist/{commands-evrenjn9.js → commands-t3mfy7vn.js} +9 -3
- package/dist/{controller-xjp8vswh.js → controller-q7xrbaz0.js} +8 -3
- package/dist/{controllers-connect-pp5jnxvc.js → controllers-connect-c6nm70kg.js} +20 -19
- package/dist/{cursor-adapter-yf47w4t0.js → cursor-adapter-ftd81ga8.js} +1 -1
- package/dist/{db-zxpw6aht.js → db-dk572x25.js} +3 -3
- package/dist/{db-rgfsh91f.js → db-vemtpmcc.js} +14 -4
- package/dist/{devtools-82fzqra8.js → devtools-qxfgbsqz.js} +1 -1
- package/dist/{dispatcher-smasgws7.js → dispatcher-kh7ecxh9.js} +17 -16
- package/dist/{docker-rrjhaqt9.js → docker-c1a3gqmf.js} +89 -22
- package/dist/{dsh-adapter-ftyy8dnp.js → dsh-adapter-v8628r89.js} +1 -1
- package/dist/{e2b-w1p0bnpm.js → e2b-fr12yccx.js} +5 -5
- package/dist/{e2b-qcaj675y.js → e2b-x6ywzak3.js} +3 -3
- package/dist/{easter-egg-3d-yyts7p16.js → easter-egg-3d-nenw4x63.js} +1 -1
- package/dist/{embedding-space-1z0mbg95.js → embedding-space-0jepb239.js} +6 -6
- package/dist/{embeddings-ygeg7tm7.js → embeddings-xxt49yxg.js} +1 -1
- package/dist/{execute-14qhqp4d.js → execute-1mhw6e4x.js} +1 -1
- package/dist/{external-channel-ttgrs3sx.js → external-channel-55v12e60.js} +2 -2
- package/dist/{fx-adapter-gt6pncj2.js → fx-adapter-bs8wqrk5.js} +6 -5
- package/dist/{google-generative-ai-yvmdxyxa.js → google-generative-ai-v94hr7h0.js} +5 -5
- package/dist/{google-vertex-jrtdh9gs.js → google-vertex-g7vr6cf8.js} +5 -5
- package/dist/{grok-adapter-sm4m5k4g.js → grok-adapter-gpjqmkcq.js} +6 -5
- package/dist/handlers-zxmp1ta4.js +32 -0
- package/dist/{heartbeat-qahz4p62.js → heartbeat-k42svnr0.js} +20 -19
- package/dist/{hook-4fd5wq5w.js → hook-114m7hap.js} +4 -3
- package/dist/{hook-kj3d398m.js → hook-6nphweh0.js} +24 -9
- package/dist/{http-1ted0j1k.js → http-r8cc4wz7.js} +1196 -211
- package/dist/index-a8phxqnk.js +37 -0
- package/dist/{index-q4b917zs.js → index-e3s8wmxy.js} +1 -1
- package/dist/{index-6rq3r5zb.js → index-fpk4bcd9.js} +1 -1
- package/dist/{index-5rk6rfzr.js → index-fvzke5kx.js} +1 -1
- package/dist/index-k6h00t4c.js +30 -0
- package/dist/{index-7v51awbn.js → index-nh3wyqdt.js} +1027 -577
- package/dist/{index-yqhz88yy.js → index-pxn30mse.js} +8 -8
- package/dist/index-xrz7te8h.js +33 -0
- package/dist/{keepalive-6cf311t5.js → keepalive-x060nt9p.js} +3 -3
- package/dist/lead-956sd2yc.js +71 -0
- package/dist/{maintenance-a4bm1bmt.js → maintenance-2xxfexhf.js} +1 -1
- package/dist/{mistral-conversations-4pmwj1c0.js → mistral-conversations-3gjpdb4b.js} +4 -4
- package/dist/{oauth-refresh-sweep-3h0dfw1k.js → oauth-refresh-sweep-fckc4ar5.js} +2 -2
- package/dist/{onboard-myn5hraa.js → onboard-zc48j237.js} +5 -5
- package/dist/{openai-codex-responses-44j573nm.js → openai-codex-responses-8pbxq9h5.js} +5 -5
- package/dist/{openai-completions-a4gee2a8.js → openai-completions-rm00s912.js} +4 -4
- package/dist/{openai-decisions-w4qkrcar.js → openai-decisions-8v957r2r.js} +1 -1
- package/dist/{openai-responses-rbp6h5kr.js → openai-responses-agxpf96k.js} +5 -5
- package/dist/{opencode-adapter-p2nkb5am.js → opencode-adapter-r690h1cr.js} +4 -3
- package/dist/{otel-impl-a1a0ef6b.js → otel-impl-r5xbh81q.js} +4 -4
- package/dist/{pi-mono-adapter-rzmpb72a.js → pi-mono-adapter-c7v8sdec.js} +7 -6
- package/dist/{pricing-refresh-83drrnxj.js → pricing-refresh-3pmre11v.js} +3 -3
- package/dist/{rbac-roles-s8y6ecb5.js → rbac-roles-3vbshwsb.js} +1 -1
- package/dist/{rbac-roles-0fb6e7pj.js → rbac-roles-910mmkgz.js} +2 -2
- package/dist/{render-v2-twmbgrz4.js → render-v2-pxykkbab.js} +3 -3
- package/dist/{resume-zfqc36xj.js → resume-adda0bwy.js} +6 -6
- package/dist/{runtime-qwggdpqr.js → runtime-fnbm7stk.js} +1 -1
- package/dist/{session-access-sc9pn9da.js → session-access-2fvamrdc.js} +4 -4
- package/dist/{session-source-backfill-qh07xr8g.js → session-source-backfill-08xvv0sm.js} +2 -2
- package/dist/{setup-4fr0m7a1.js → setup-1aqmvmh1.js} +3 -3
- package/dist/{typesafe-system-one-5mavm259.js → typesafe-system-one-0bmqcxfg.js} +2 -2
- package/dist/{virtual-modules-fh4ha15b.js → virtual-modules-d2pjk9j1.js} +5 -5
- package/dist/{work-objects-0wr8zxct.js → work-objects-9s0yrm5j.js} +20 -19
- package/dist/{worker-follow-up-ktyyazyp.js → worker-follow-up-3q78fqmr.js} +17 -16
- package/dist/worker-y15hy08h.js +71 -0
- package/openapi.json +289 -4
- package/package.json +9 -8
- package/src/apps/definition.ts +6 -5
- package/src/be/db/page-theme-tokens.ts +12 -0
- package/src/be/db/script-runs.ts +23 -0
- package/src/be/db/tasks/read.ts +131 -4
- package/src/be/db/tasks/write.ts +19 -0
- package/src/be/db-client.ts +114 -11
- package/src/be/db.ts +49 -6
- package/src/be/memory/providers/sqlite-store.ts +49 -12
- package/src/be/migrations/260_runtime_pool_cache.sql +20 -0
- package/src/be/migrations/262_session_visibility_indexes.sql +18 -0
- package/src/be/migrations/263_task_list_filter_index.sql +11 -0
- package/src/be/migrations/264_slack_thread_cover_index.sql +9 -0
- package/src/be/migrations/266_script_runs_source_task.sql +11 -0
- package/src/be/migrations/269_pages_design_system.sql +23 -0
- package/src/be/runtime-pools/cache-cleanup.ts +109 -0
- package/src/be/runtime-pools/db.ts +65 -6
- package/src/be/runtime-pools/remote-controller.ts +5 -0
- package/src/be/runtime-pools/supervisor.ts +33 -2
- package/src/be/scripts/typecheck.ts +22 -0
- package/src/be/seed/schedules-seeder.ts +0 -10
- package/src/be/seed-scripts/catalog/catalog-report.inline.ts +2 -0
- package/src/be/seed-scripts/catalog/catalog-report.ts +2 -0
- package/src/be/seed-scripts/catalog/memory-eval.ts +2 -0
- package/src/be/seed-scripts/catalog/swarm-update-check.ts +150 -0
- package/src/be/seed-scripts/catalog/task-failure-audit.ts +5 -1
- package/src/be/seed-scripts/index.ts +9 -0
- package/src/be/seed-skills/bundled-files.generated.json +3 -3
- package/src/be/swarm-config-guard.ts +32 -0
- package/src/be/task-auto-title-listener.ts +127 -0
- package/src/commands/controller.ts +4 -0
- package/src/commands/entrypoint-api-readiness.ts +74 -0
- package/src/commands/onboard/agent-fs-version.ts +1 -1
- package/src/commands/profile-sync.ts +151 -16
- package/src/commands/runner.ts +15 -7
- package/src/hooks/hook.ts +20 -9
- package/src/http/index.ts +4 -1
- package/src/http/list-total-cache.ts +14 -7
- package/src/http/memory.ts +38 -9
- package/src/http/pages-public.ts +93 -129
- package/src/http/pages.ts +94 -21
- package/src/http/script-runs.ts +174 -5
- package/src/http/status.ts +10 -2
- package/src/http/tasks.ts +4 -2
- package/src/pages/browser-sdk.ts +48 -21
- package/src/pages/color-literals.ts +115 -0
- package/src/pages/page-head.ts +766 -0
- package/src/pages/version.ts +3 -0
- package/src/providers/amp-adapter.ts +1 -1
- package/src/providers/codex-adapter.ts +1 -0
- package/src/providers/codex-app-server.ts +27 -1
- package/src/providers/fx-adapter.ts +1 -1
- package/src/providers/pi-mono-extension.ts +30 -15
- package/src/runtime-providers/controller-protocol.ts +14 -1
- package/src/runtime-providers/controller.ts +5 -0
- package/src/runtime-providers/docker.ts +108 -23
- package/src/runtime-providers/types.ts +16 -0
- package/src/script-workflows/workflow-ctx.ts +8 -0
- package/src/scripts-runtime/types/stdlib.d.ts +22 -0
- package/src/scripts-runtime/types/swarm-sdk.d.ts +22 -0
- package/src/server-user.ts +1 -0
- package/src/slack/commands.ts +5 -1
- package/src/slack/handlers.ts +37 -2
- package/src/slack/left-threads.ts +27 -4
- package/src/tasks/follow-up-gate.ts +318 -0
- package/src/tasks/follow-up-redelegation-guard.ts +155 -0
- package/src/tasks/worker-follow-up.ts +21 -1
- package/src/tests/claude-artifacts-read-script.test.ts +209 -0
- package/src/tests/codex-app-server.test.ts +59 -0
- package/src/tests/codex-gateway-route.test.ts +50 -1
- package/src/tests/create-page-tool.test.ts +501 -0
- package/src/tests/db-client-busy-retry.test.ts +81 -0
- package/src/tests/db-client.test.ts +21 -0
- package/src/tests/db-task-read-contract.test.ts +9 -2
- package/src/tests/db-task-write-contract.test.ts +7 -1
- package/src/tests/db-tasks-read.test.ts +21 -0
- package/src/tests/entrypoint-api-readiness.test.ts +416 -210
- package/src/tests/explicit-model-validation.test.ts +14 -8
- package/src/tests/fixtures/pages/head-v1.html +56 -0
- package/src/tests/follow-up-gate-shadow.test.ts +513 -0
- package/src/tests/follow-up-redelivery-guard.test.ts +432 -202
- package/src/tests/identity-field-budget.test.ts +1 -1
- package/src/tests/internal-ai/credentials.test.ts +62 -0
- package/src/tests/internal-ai/task-title.test.ts +283 -0
- package/src/tests/list-total-cache.test.ts +27 -2
- package/src/tests/memory-fts-populate.test.ts +11 -9
- package/src/tests/memory-retrieval-purge.test.ts +80 -0
- package/src/tests/merge-gate-detect-changes.test.ts +145 -0
- package/src/tests/migration-260-runtime-pool-cache.test.ts +118 -0
- package/src/tests/page-proxy.test.ts +1 -1
- package/src/tests/page-session-dev-cookie.test.ts +188 -0
- package/src/tests/pages-frame-ancestors.test.ts +28 -1
- package/src/tests/pages-head-v2.test.ts +534 -0
- package/src/tests/pages-http.test.ts +330 -0
- package/src/tests/pages-public-html.test.ts +43 -0
- package/src/tests/pages-storage.test.ts +182 -0
- package/src/tests/pages-versioning.test.ts +72 -0
- package/src/tests/profile-sync-stale-writeback.test.ts +334 -0
- package/src/tests/profile-sync.test.ts +11 -15
- package/src/tests/query-audit-plans.test.ts +27 -0
- package/src/tests/runtime-cache-cleanup.test.ts +367 -0
- package/src/tests/runtime-e2b-pools.test.ts +41 -0
- package/src/tests/runtime-pools-db.test.ts +98 -0
- package/src/tests/runtime-pools-http.test.ts +29 -0
- package/src/tests/runtime-providers-docker.test.ts +253 -0
- package/src/tests/runtime-remote-controller.test.ts +66 -2
- package/src/tests/runtime-supervisor.test.ts +66 -2
- package/src/tests/scheduled-task-required-params-migration.test.ts +3 -1
- package/src/tests/script-connections.test.ts +13 -0
- package/src/tests/script-runs-http.test.ts +684 -2
- package/src/tests/seed-registry.test.ts +28 -6
- package/src/tests/seed-schedules.test.ts +61 -8
- package/src/tests/seed-scripts.test.ts +168 -2
- package/src/tests/slack-leave-thread.test.ts +145 -5
- package/src/tests/slack-render-v2-query-plans.test.ts +24 -2
- package/src/tests/status.test.ts +11 -0
- package/src/tests/swarm-diff.test.ts +121 -2
- package/src/tests/task-auto-title-config-guard.test.ts +37 -0
- package/src/tests/task-auto-title-listener.test.ts +165 -0
- package/src/tests/tasks-readable-row.test.ts +155 -0
- package/src/tests/theme-import.test.ts +758 -0
- package/src/tests/themes-page-theme.test.ts +155 -0
- package/src/tests/themes-presets.test.ts +172 -0
- package/src/tests/workflow-script-principal.test.ts +2 -1
- package/src/themes/classics.ts +949 -0
- package/src/themes/import.ts +420 -0
- package/src/themes/page-fields.ts +40 -0
- package/src/themes/page-theme.ts +102 -0
- package/src/themes/presets.ts +561 -0
- package/src/themes/tokens.ts +218 -0
- package/src/tools/app-upsert.ts +1 -1
- package/src/tools/create-page.ts +118 -12
- package/src/tools/send-task.ts +35 -47
- package/src/tools/slack-leave-thread.ts +1 -1
- package/src/tools/task-dedup.ts +45 -21
- package/src/tools/utils.ts +33 -0
- package/src/types.ts +83 -3
- package/src/utils/constants.ts +19 -0
- package/src/utils/follow-up-gate-schedule-ids.ts +13 -0
- package/src/utils/internal-ai/credentials.ts +146 -93
- package/src/utils/internal-ai/index.ts +10 -0
- package/src/utils/internal-ai/models.ts +18 -0
- package/src/utils/internal-ai/task-title.ts +282 -0
- package/src/utils/page-session.ts +37 -0
- package/templates/schedules/daily-status-report/content.md +88 -2
- package/templates/schedules/daily-swarm-update-check/content.md +16 -40
- package/templates/skills/apps/SKILL.md +1 -1
- package/templates/skills/apps/content.md +1 -1
- package/templates/skills/composio-google-docs/content.md +10 -0
- package/templates/skills/heartbeat-runbook/content.md +1 -1
- package/templates/skills/pages/config.json +1 -1
- package/templates/skills/pages/content.md +539 -86
- package/templates/skills/pages/files/examples/annotated-pr.html +21 -12
- package/templates/skills/pages/files/examples/multiplayer-board.html +41 -41
- package/templates/skills/pages/files/examples/report-page.html +125 -124
- package/templates/skills/script-workflows/content.md +19 -1
- package/templates/skills/swarm-docs/content.md +1 -0
- package/templates/skills/swarm-scripts/SKILL.md +2 -0
- package/templates/skills/swarm-scripts/content.md +2 -0
- package/dist/cli-2pnswgh8.js +0 -189
- package/dist/cli-dznrgaep.js +0 -30
- package/dist/cli-w3hcrxht.js +0 -6985
- package/dist/handlers-vwmba1m2.js +0 -31
- package/dist/index-5r71rf98.js +0 -32
- package/dist/index-anmwxs87.js +0 -29
- package/dist/index-spp0ra5c.js +0 -36
- package/dist/lead-fjy418cw.js +0 -69
- package/dist/worker-0c5tyzqe.js +0 -69
- package/templates/schedules/daily-workflow-health-audit/config.json +0 -19
- package/templates/schedules/daily-workflow-health-audit/content.md +0 -191
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
export const argsSchema = z.object({
|
|
4
|
+
dryRun: z
|
|
5
|
+
.boolean()
|
|
6
|
+
.optional()
|
|
7
|
+
.describe("Compute the verdict but do not notify the lead when an update is available"),
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
const RELEASES_URL = "https://api.github.com/repos/desplega-ai/agent-swarm/releases/latest";
|
|
11
|
+
|
|
12
|
+
/** Parse `major.minor.patch` (a leading `v` and a pre-release/build suffix are ignored). */
|
|
13
|
+
export function parseVersion(value: unknown): [number, number, number] | null {
|
|
14
|
+
if (typeof value !== "string") return null;
|
|
15
|
+
const m = value
|
|
16
|
+
.trim()
|
|
17
|
+
.replace(/^v/i, "")
|
|
18
|
+
.match(/^(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/);
|
|
19
|
+
if (!m) return null;
|
|
20
|
+
return [Number(m[1]), Number(m[2]), Number(m[3])];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Numeric compare of two parsed versions: negative, zero or positive. Never a string compare. */
|
|
24
|
+
export function compareVersions(a: [number, number, number], b: [number, number, number]): number {
|
|
25
|
+
for (let i = 0; i < 3; i++) {
|
|
26
|
+
const diff = (a[i] as number) - (b[i] as number);
|
|
27
|
+
if (diff !== 0) return diff;
|
|
28
|
+
}
|
|
29
|
+
return 0;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function errorText(error: unknown): string {
|
|
33
|
+
return String((error as any)?.message ?? error).slice(0, 200);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** The version the API server is running, from its own unauthenticated `/health` route. */
|
|
37
|
+
async function runningVersion(ctx: any): Promise<string | null> {
|
|
38
|
+
try {
|
|
39
|
+
const base = ctx.stdlib.Redacted.value(ctx.swarm.config.mcpBaseUrl).replace(/\/+$/, "");
|
|
40
|
+
const res: any = await ctx.stdlib.fetch(base + "/health", { retries: 1, timeoutMs: 10_000 });
|
|
41
|
+
if (!res.ok) return null;
|
|
42
|
+
const body: any = await res.json();
|
|
43
|
+
return typeof body?.version === "string" && body.version.trim() ? body.version.trim() : null;
|
|
44
|
+
} catch {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function resolveLeadId(ctx: any): Promise<string | null> {
|
|
50
|
+
const res: any = await ctx.swarm.swarm_get({});
|
|
51
|
+
const data = res?.data ?? res;
|
|
52
|
+
const agents: any[] = Array.isArray(data?.agents) ? data.agents : Array.isArray(data) ? data : [];
|
|
53
|
+
const lead = agents.find((agent) => agent?.isLead === true || agent?.isLead === 1);
|
|
54
|
+
return typeof lead?.id === "string" ? lead.id : null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Compare the running agent-swarm version with the latest published GitHub
|
|
59
|
+
* release and, only when a newer release exists, hand the lead one task to
|
|
60
|
+
* tell the operator through its admin delivery channel.
|
|
61
|
+
*/
|
|
62
|
+
export default async function swarmUpdateCheck(args: any, ctx: any) {
|
|
63
|
+
const parsed = argsSchema.safeParse(args ?? {});
|
|
64
|
+
if (!parsed.success) return { error: "invalid args: " + parsed.error.message };
|
|
65
|
+
const dryRun = parsed.data.dryRun === true;
|
|
66
|
+
|
|
67
|
+
const running = await runningVersion(ctx);
|
|
68
|
+
const runningParsed = parseVersion(running);
|
|
69
|
+
if (!running || !runningParsed) {
|
|
70
|
+
// Unknown is not "up to date": say so and stop, never guess a version.
|
|
71
|
+
return {
|
|
72
|
+
checked: false,
|
|
73
|
+
status: "unknown",
|
|
74
|
+
running,
|
|
75
|
+
message: `Unable to determine running version: ${running ? `unparseable "${running}"` : "not reported by /health"}.`,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
let release: any;
|
|
80
|
+
try {
|
|
81
|
+
// One attempt: a failed GitHub call is reported, never retried in a loop.
|
|
82
|
+
const res: any = await ctx.stdlib.fetch(RELEASES_URL, {
|
|
83
|
+
retries: 1,
|
|
84
|
+
timeoutMs: 15_000,
|
|
85
|
+
headers: { Accept: "application/vnd.github+json", "User-Agent": "agent-swarm-update-check" },
|
|
86
|
+
});
|
|
87
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
88
|
+
release = await res.json();
|
|
89
|
+
} catch (error) {
|
|
90
|
+
return {
|
|
91
|
+
checked: false,
|
|
92
|
+
status: "github-unreachable",
|
|
93
|
+
running,
|
|
94
|
+
message: `Could not reach GitHub releases API: ${errorText(error)}`,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const latest = typeof release?.tag_name === "string" ? release.tag_name.replace(/^v/i, "") : null;
|
|
99
|
+
const latestParsed = parseVersion(latest);
|
|
100
|
+
if (!latest || !latestParsed) {
|
|
101
|
+
return {
|
|
102
|
+
checked: false,
|
|
103
|
+
status: "github-unreachable",
|
|
104
|
+
running,
|
|
105
|
+
message: `Could not read a version from the latest release tag: ${JSON.stringify(release?.tag_name ?? null)}.`,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
const releaseUrl =
|
|
109
|
+
typeof release?.html_url === "string"
|
|
110
|
+
? release.html_url
|
|
111
|
+
: `https://github.com/desplega-ai/agent-swarm/releases/tag/v${latest}`;
|
|
112
|
+
|
|
113
|
+
if (compareVersions(latestParsed, runningParsed) <= 0) {
|
|
114
|
+
return {
|
|
115
|
+
checked: true,
|
|
116
|
+
status: "up-to-date",
|
|
117
|
+
running,
|
|
118
|
+
latest,
|
|
119
|
+
message: `Up to date: running ${running}, latest published ${latest}.`,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const message = `Update available: running ${running}, latest ${latest} (${releaseUrl})`;
|
|
124
|
+
if (dryRun) {
|
|
125
|
+
return { checked: true, status: "update-available", running, latest, releaseUrl, notified: false, message };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const leadId = await resolveLeadId(ctx);
|
|
129
|
+
// A detected update that reaches nobody is a failed run, not a quiet success.
|
|
130
|
+
if (!leadId) throw new Error(`${message}, but the swarm has no lead to notify.`);
|
|
131
|
+
const sent: any = await ctx.swarm.task_send({
|
|
132
|
+
agentId: leadId,
|
|
133
|
+
routingReason: "skill",
|
|
134
|
+
routingNote: "Lead owns the operator's admin delivery channel for release notices.",
|
|
135
|
+
tags: ["swarm-update-check"],
|
|
136
|
+
task: [
|
|
137
|
+
`A newer agent-swarm release is published: running ${running}, latest ${latest}.`,
|
|
138
|
+
`Release: ${releaseUrl}`,
|
|
139
|
+
"",
|
|
140
|
+
"Post one short message to your configured admin delivery channel (the in-app fallback if none is configured) naming the running version, the new version and the release URL. Then complete with that message as the output.",
|
|
141
|
+
"Do not attempt the upgrade yourself: this notice only detects and reports.",
|
|
142
|
+
].join("\n"),
|
|
143
|
+
});
|
|
144
|
+
const taskId = sent?.data?.task?.id ?? null;
|
|
145
|
+
if (sent?.success === false || sent?.data?.success === false || !taskId) {
|
|
146
|
+
const reason = sent?.data?.message ?? sent?.data?.error ?? `status ${sent?.status}`;
|
|
147
|
+
throw new Error(`${message}, but notifying the lead failed: ${errorText(reason)}`);
|
|
148
|
+
}
|
|
149
|
+
return { checked: true, status: "update-available", running, latest, releaseUrl, notified: true, leadTaskId: taskId, message };
|
|
150
|
+
}
|
|
@@ -30,7 +30,11 @@ const REASON_PATTERNS: any[] = [
|
|
|
30
30
|
{ key: "context-window", re: /context (window|limit|saturat)|peakcontext|compact/i },
|
|
31
31
|
{ key: "reboot-sweep", re: /reboot sweep/i },
|
|
32
32
|
{ key: "not-found", re: /not found|404|missing|no such/i },
|
|
33
|
-
{
|
|
33
|
+
{
|
|
34
|
+
key: "output-limit",
|
|
35
|
+
re: /\bmax_(?:output_)?tokens\b|\b(?:maximum|max) output (?:length|tokens)\b|\boutput (?:token|length) limit\b/i,
|
|
36
|
+
},
|
|
37
|
+
{ key: "auth/credentials", re: /unauthorized|401|403|credential|\btoken\b|forbidden/i },
|
|
34
38
|
{ key: "ci/checks-failed", re: /\bci\b|check.?s? fail|lint|tsc|test.?s? fail/i },
|
|
35
39
|
{ key: "network", re: /network|econn|fetch failed|socket|dns|502|503|504/i },
|
|
36
40
|
{ key: "cancelled", re: /cancel|aborted/i },
|
|
@@ -60,6 +60,7 @@ import starHistoryRefreshSrc from "./catalog/star-history-refresh.ts" with { typ
|
|
|
60
60
|
// @ts-expect-error Bun text imports synthesize a default string for this helper.
|
|
61
61
|
import starHistoryRendererSrc from "./catalog/star-history-renderer.ts" with { type: "text" };
|
|
62
62
|
import swarmOverviewSrc from "./catalog/swarm-overview.ts" with { type: "text" };
|
|
63
|
+
import swarmUpdateCheckSrc from "./catalog/swarm-update-check.ts" with { type: "text" };
|
|
63
64
|
import taskContextGatheringSrc from "./catalog/task-context-gathering.ts" with { type: "text" };
|
|
64
65
|
import taskFailureAuditSrc from "./catalog/task-failure-audit.ts" with { type: "text" };
|
|
65
66
|
import textDiffSrc from "./catalog/text-diff.ts" with { type: "text" };
|
|
@@ -217,6 +218,14 @@ export const SEED_SCRIPTS: SeedScript[] = [
|
|
|
217
218
|
"Task onboarding: one call instead of task_get plus multiple memory_search calls. Pass the task description split into 2-4 natural-language queries.",
|
|
218
219
|
source: asText(taskContextGatheringSrc),
|
|
219
220
|
},
|
|
221
|
+
{
|
|
222
|
+
name: "swarm-update-check",
|
|
223
|
+
description:
|
|
224
|
+
"Compare the running agent-swarm version (API /health) with the latest GitHub release of desplega-ai/agent-swarm as numeric major.minor.patch, and hand the lead one notify task only when a newer release exists.",
|
|
225
|
+
intent:
|
|
226
|
+
"Daily zero-config upgrade watchdog behind the daily-swarm-update-check schedule; dryRun reports the verdict without notifying.",
|
|
227
|
+
source: asText(swarmUpdateCheckSrc),
|
|
228
|
+
},
|
|
220
229
|
{
|
|
221
230
|
name: "schedule-health",
|
|
222
231
|
description:
|
|
@@ -36,15 +36,15 @@
|
|
|
36
36
|
"pages": [
|
|
37
37
|
{
|
|
38
38
|
"path": "examples/annotated-pr.html",
|
|
39
|
-
"content": "<!doctype html>\n<!--\n Annotated-PR page using the auto-injected <swarm-diff> web component.\n\n No <script> import and no bundling: <swarm-diff>, <swarm-diff-jumps> and\n window.swarmUi.renderDiff are injected into every HTML page automatically.\n Each hunk gets id=\"swarm-diff-<file
|
|
39
|
+
"content": "<!doctype html>\n<!--\n Annotated-PR page using the auto-injected <swarm-diff> web component.\n\n No <script> import and no bundling: <swarm-diff>, <swarm-diff-jumps> and\n window.swarmUi.renderDiff are injected into every HTML page automatically.\n\n Each \"text\" is the code line WITHOUT its +, - or space marker: the\n component draws the marker in its own column. old_lines counts the context\n and deleted lines, and new_lines counts the context and added lines. Inside\n the JSON, write < as \\u003c and & as \\u0026.\n\n Each hunk gets id=\"swarm-diff-<slug>\". The slug is <file>-<old_start> in\n lowercase, with each run of characters other than a-z and 0-9 changed to\n one \"-\". So src/foo.ts at old_start 10 gets id=\"swarm-diff-src-foo-ts-10\".\n <swarm-diff-jumps> builds its jump list from these ids, and external links\n can deep-link a specific hunk.\n-->\n<html lang=\"en\">\n<head>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n <title>PR #1234: console.log cleanup</title>\n</head>\n<body>\n <h1>PR #1234: cleanup raw <code>console.log</code> calls</h1>\n <p>Replaces ad-hoc logging with the project logger.</p>\n\n <swarm-diff-jumps></swarm-diff-jumps>\n\n <swarm-diff file=\"src/foo.ts\" base-sha=\"abc123\" head-sha=\"def456\">\n { \"hunks\": [\n { \"old_start\": 10, \"old_lines\": 2, \"new_start\": 10, \"new_lines\": 3,\n \"lines\": [\n { \"type\": \"context\", \"text\": \"export function load(id) {\" },\n { \"type\": \"del\", \"text\": \" console.log(id);\" },\n { \"type\": \"add\", \"text\": \" logger.info({ id });\" },\n { \"type\": \"add\", \"text\": \" return cache.get(id);\" }\n ],\n \"annotations\": [\n { \"line\": 12, \"severity\": \"warn\", \"text\": \"Returns undefined on a cache miss\" }\n ]\n }\n ] }\n </swarm-diff>\n\n <swarm-diff file=\"src/bar.ts\" base-sha=\"abc123\" head-sha=\"def456\">\n { \"hunks\": [\n { \"old_start\": 5, \"old_lines\": 1, \"new_start\": 5, \"new_lines\": 1,\n \"lines\": [\n { \"type\": \"del\", \"text\": \"console.error('boom');\" },\n { \"type\": \"add\", \"text\": \"logger.error('boom');\" }\n ]\n }\n ] }\n </swarm-diff>\n</body>\n</html>\n"
|
|
40
40
|
},
|
|
41
41
|
{
|
|
42
42
|
"path": "examples/multiplayer-board.html",
|
|
43
|
-
"content": "<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n <title>Shared board</title>\n <style>\n :root {\n color: #252522;\n background: #f7f6f3;\n font-family: \"SF Pro Display\", \"Helvetica Neue\", ui-sans-serif, system-ui, sans-serif;\n font-synthesis: none;\n }\n\n * { box-sizing: border-box; }\n\n body {\n margin: 0;\n min-width: 320px;\n padding: 0;\n background: #f7f6f3;\n color: #252522;\n font-family: \"SF Pro Display\", \"Helvetica Neue\", ui-sans-serif, system-ui, sans-serif;\n }\n\n button, input, textarea { font: inherit; }\n\n button {\n border: 1px solid #252522;\n border-radius: 8px;\n background: #252522;\n color: #fff;\n cursor: pointer;\n padding: 10px 14px;\n }\n\n button:hover { background: #3c3c37; }\n button:focus-visible, input:focus-visible, textarea:focus-visible { outline: 3px solid #e1a52c; outline-offset: 2px; }\n button:disabled { cursor: not-allowed; opacity: .5; }\n\n .page { max-width: 1120px; margin: 0 auto; padding: 32px 24px 56px; }\n\n header {\n display: flex;\n align-items: flex-start;\n justify-content: space-between;\n gap: 24px;\n border-bottom: 1px solid #d9d7d0;\n padding-bottom: 24px;\n }\n\n h1, h2, p { margin-top: 0; }\n h1 { font-size: clamp(2rem, 5vw, 3rem); letter-spacing: -.04em; margin-bottom: 8px; }\n h2 { font-size: 1.15rem; letter-spacing: -.02em; margin-bottom: 16px; }\n .lede, .muted { color: #67665f; }\n .lede { max-width: 46rem; margin-bottom: 0; }\n\n .viewer {\n min-width: 170px;\n border: 1px solid #d9d7d0;\n border-radius: 8px;\n background: #fff;\n padding: 12px;\n }\n\n .eyebrow {\n color: #67665f;\n display: block;\n font-size: .72rem;\n font-weight: 700;\n letter-spacing: .08em;\n margin-bottom: 4px;\n text-transform: uppercase;\n }\n\n .viewer-name { font-weight: 700; overflow-wrap: anywhere; }\n\n .notice {\n border: 1px solid #d9d7d0;\n border-left: 4px solid #e1a52c;\n border-radius: 8px;\n background: #fffdf8;\n margin: 24px 0;\n min-height: 47px;\n padding: 12px 14px;\n }\n\n .notice[data-tone=\"error\"] { border-left-color: #a34736; background: #fff9f7; }\n .notice[data-tone=\"stale\"] { border-left-color: #e1a52c; background: #fffdf8; }\n .notice:empty { display: none; }\n\n .grid { display: grid; gap: 24px; grid-template-columns: minmax(0, 1.65fr) minmax(260px, .85fr); }\n .panel { border: 1px solid #d9d7d0; border-radius: 12px; background: #fff; padding: 20px; }\n .panel + .panel { margin-top: 24px; }\n\n .add-row { display: grid; gap: 8px; grid-template-columns: minmax(0, 1fr) auto; margin-bottom: 16px; }\n input, textarea {\n border: 1px solid #bdbbb4;\n border-radius: 8px;\n background: #fff;\n color: inherit;\n padding: 10px 12px;\n width: 100%;\n }\n\n textarea { min-height: 94px; resize: vertical; }\n label { display: block; font-size: .88rem; font-weight: 650; margin-bottom: 8px; }\n\n .cards, .peers { list-style: none; margin: 0; padding: 0; }\n .cards { border-top: 1px solid #e7e5df; }\n .card {\n align-items: center;\n border-bottom: 1px solid #e7e5df;\n display: flex;\n gap: 12px;\n justify-content: space-between;\n padding: 13px 0;\n }\n\n .card-title { font-weight: 650; overflow-wrap: anywhere; }\n .card-status {\n background: #fff;\n border-color: #bdbbb4;\n color: #252522;\n flex: 0 0 auto;\n font-size: .82rem;\n padding: 7px 10px;\n }\n\n .card-status[data-done=\"true\"] { background: #f6efd9; border-color: #c89429; }\n .empty { color: #67665f; padding: 16px 0 4px; }\n .agent-note { background: #f7f6f3; border-radius: 8px; min-height: 46px; padding: 12px; white-space: pre-wrap; overflow-wrap: anywhere; }\n .agent-note:empty::before { color: #67665f; content: \"No note yet. A server-side agent can write one here.\"; }\n\n .peers { display: grid; gap: 8px; }\n .peer {\n border: 1px solid #e7e5df;\n border-radius: 8px;\n display: grid;\n gap: 3px;\n padding: 10px;\n }\n\n .peer-name { font-weight: 650; overflow-wrap: anywhere; }\n .room-meta { color: #67665f; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: .75rem; margin: 0 0 16px; }\n .save-state { color: #67665f; font-size: .82rem; margin: 8px 0 0; min-height: 1.2em; }\n\n @media (max-width: 720px) {\n .page { padding: 24px 16px 40px; }\n header { align-items: stretch; flex-direction: column; }\n .viewer { min-width: 0; }\n .grid { grid-template-columns: 1fr; }\n .add-row { grid-template-columns: 1fr; }\n }\n </style>\n </head>\n <body>\n <swarm-cursors room=\"match-42\"></swarm-cursors>\n <main class=\"page\">\n <header>\n <div>\n <h1>Shared board</h1>\n <p class=\"lede\">A small board shared by everyone in match-42. Changes appear as peers make them.</p>\n </div>\n <section class=\"viewer\" aria-label=\"Current viewer\">\n <span class=\"eyebrow\">Connected as</span>\n <div class=\"viewer-name\" id=\"viewer-name\">Connecting…</div>\n </section>\n </header>\n\n <div id=\"notice\" class=\"notice\" role=\"status\" aria-live=\"polite\"></div>\n\n <div class=\"grid\">\n <section class=\"panel\" aria-labelledby=\"board-heading\">\n <h2 id=\"board-heading\">Match board</h2>\n <p class=\"room-meta\">Room: match-42 · Schema: 1</p>\n <form id=\"add-card-form\" class=\"add-row\">\n <label for=\"card-title\" class=\"sr-only\" hidden>New card title</label>\n <input id=\"card-title\" name=\"title\" maxlength=\"120\" placeholder=\"Write a card title\" required>\n <button type=\"submit\">Add card</button>\n </form>\n <ul id=\"cards\" class=\"cards\" aria-live=\"polite\"></ul>\n </section>\n\n <aside>\n <section class=\"panel\" aria-labelledby=\"peers-heading\">\n <h2 id=\"peers-heading\">Live peers</h2>\n <p class=\"muted\">Pointers use document-normalized coordinates and line up across viewports.</p>\n <ul id=\"peers\" class=\"peers\" aria-live=\"polite\"></ul>\n </section>\n\n <section class=\"panel\" aria-labelledby=\"agent-note-heading\">\n <h2 id=\"agent-note-heading\">Agent note</h2>\n <div id=\"agent-note\" class=\"agent-note\" aria-live=\"polite\"></div>\n </section>\n </aside>\n </div>\n\n <section class=\"panel\" aria-labelledby=\"lobby-heading\">\n <h2 id=\"lobby-heading\">Lobby</h2>\n <p class=\"room-meta\">Room: lobby · Schema: 1</p>\n <form id=\"lobby-form\">\n <label for=\"lobby-message\">Shared lobby message</label>\n <textarea id=\"lobby-message\" maxlength=\"1000\" placeholder=\"Leave a message for everyone in the lobby\"></textarea>\n <button type=\"submit\">Save lobby message</button>\n <p id=\"lobby-save-state\" class=\"save-state\" aria-live=\"polite\"></p>\n </form>\n </section>\n </main>\n\n <script>\n (() => {\n const ui = {\n viewerName: document.querySelector('#viewer-name'),\n notice: document.querySelector('#notice'),\n cards: document.querySelector('#cards'),\n peers: document.querySelector('#peers'),\n agentNote: document.querySelector('#agent-note'),\n addCardForm: document.querySelector('#add-card-form'),\n cardTitle: document.querySelector('#card-title'),\n lobbyForm: document.querySelector('#lobby-form'),\n lobbyMessage: document.querySelector('#lobby-message'),\n lobbySaveState: document.querySelector('#lobby-save-state')\n };\n\n let board;\n let lobby;\n let peers = [];\n let lobbyDirty = false;\n\n function message(error) {\n return error && error.message ? error.message : String(error);\n }\n\n function showNotice(text, tone) {\n ui.notice.textContent = text || '';\n ui.notice.dataset.tone = tone || '';\n }\n\n function markStale(room, roomName) {\n if (!room.stale) return false;\n showNotice(`${roomName} uses a different schema version. Its saved state remains visible, but this page cannot write changes.`, 'stale');\n return true;\n }\n\n function renderBoard(state) {\n const cards = state && state.cards && typeof state.cards === 'object' ? state.cards : {};\n ui.cards.replaceChildren();\n const entries = Object.entries(cards);\n\n if (!entries.length) {\n const empty = document.createElement('li');\n empty.className = 'empty';\n empty.textContent = 'No cards yet. Add the first card.';\n ui.cards.append(empty);\n }\n\n for (const [id, card] of entries) {\n const item = document.createElement('li');\n item.className = 'card';\n const title = document.createElement('span');\n title.className = 'card-title';\n title.textContent = card && typeof card.title === 'string' ? card.title : 'Untitled card';\n const toggle = document.createElement('button');\n const done = Boolean(card && card.done);\n toggle.className = 'card-status';\n toggle.dataset.done = String(done);\n toggle.type = 'button';\n toggle.textContent = done ? 'Done' : 'Open';\n toggle.setAttribute('aria-label', `Mark ${title.textContent} as ${done ? 'open' : 'done'}`);\n toggle.addEventListener('click', () => toggleCard(id));\n item.append(title, toggle);\n ui.cards.append(item);\n }\n\n ui.agentNote.textContent = state && typeof state.agentNote === 'string' ? state.agentNote : '';\n }\n\n function renderPeers(nextPeers) {\n peers = Array.isArray(nextPeers) ? nextPeers : [];\n ui.peers.replaceChildren();\n\n if (!peers.length) {\n const empty = document.createElement('li');\n empty.className = 'empty';\n empty.textContent = 'No other peers are connected.';\n ui.peers.append(empty);\n return;\n }\n\n for (const peer of peers) {\n const item = document.createElement('li');\n item.className = 'peer';\n const name = document.createElement('span');\n name.className = 'peer-name';\n name.textContent = peer.name || 'Guest';\n item.append(name);\n ui.peers.append(item);\n }\n }\n\n function renderLobby(state) {\n if (lobbyDirty) return;\n ui.lobbyMessage.value = state && typeof state.message === 'string' ? state.message : '';\n }\n\n async function addCard(title) {\n if (markStale(board, 'match-42')) return;\n try {\n await board.change(state => {\n state.cards ??= {};\n state.cards[crypto.randomUUID()] = { title, done: false };\n });\n ui.cardTitle.value = '';\n showNotice('Card added.', 'success');\n } catch (error) {\n showNotice(`Could not add the card: ${message(error)}`, 'error');\n }\n }\n\n async function toggleCard(id) {\n if (markStale(board, 'match-42')) return;\n try {\n await board.change(state => {\n const card = state.cards && state.cards[id];\n if (card) card.done = !card.done;\n });\n } catch (error) {\n showNotice(`Could not update the card: ${message(error)}`, 'error');\n }\n }\n\n async function saveLobbyMessage() {\n if (markStale(lobby, 'lobby')) return;\n try {\n const value = ui.lobbyMessage.value;\n await lobby.change(state => {\n state.message = value;\n });\n lobbyDirty = false;\n ui.lobbySaveState.textContent = 'Saved to the lobby room.';\n } catch (error) {\n ui.lobbySaveState.textContent = `Could not save: ${message(error)}`;\n showNotice(`Could not save the lobby message: ${message(error)}`, 'error');\n }\n }\n\n async function initialize() {\n try {\n board = await window.swarmSdk.room('match-42', { schemaVersion: 1 });\n lobby = await window.swarmSdk.room('lobby', { schemaVersion: 1 });\n\n const viewer = board.me || lobby.me || {};\n ui.viewerName.textContent = viewer.name || 'Guest';\n\n board.on('change', renderBoard);\n board.on('presence', renderPeers);\n board.on('error', error => showNotice(`match-42 error: ${message(error)}`, 'error'));\n board.on('reset', () => showNotice('match-42 was reset by another participant. The board has resynchronized.', 'stale'));\n lobby.on('change', renderLobby);\n lobby.on('error', error => showNotice(`lobby error: ${message(error)}`, 'error'));\n lobby.on('reset', () => showNotice('lobby was reset by another participant. The lobby has resynchronized.', 'stale'));\n\n renderBoard(board.state);\n renderPeers(board.presence.peers);\n renderLobby(lobby.state);\n markStale(board, 'match-42');\n markStale(lobby, 'lobby');\n\n if (!board.stale) {\n await board.change(state => {\n state.cards ??= {};\n state.agentNote ??= '';\n });\n }\n if (!lobby.stale) {\n await lobby.change(state => {\n state.message ??= '';\n state.activeMatch ??= 'match-42';\n });\n }\n } catch (error) {\n showNotice(`Could not connect to the shared rooms: ${message(error)}`, 'error');\n ui.viewerName.textContent = 'Unavailable';\n }\n }\n\n ui.addCardForm.addEventListener('submit', event => {\n event.preventDefault();\n const title = ui.cardTitle.value.trim();\n if (title) addCard(title);\n });\n ui.lobbyMessage.addEventListener('input', () => { lobbyDirty = true; });\n ui.lobbyForm.addEventListener('submit', event => {\n event.preventDefault();\n saveLobbyMessage();\n });\n window.addEventListener('pagehide', () => {\n if (board) board.close();\n if (lobby) lobby.close();\n });\n\n initialize();\n })();\n </script>\n </body>\n</html>\n"
|
|
43
|
+
"content": "<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n <title>Shared board</title>\n <style>\n :root {\n color: var(--color-foreground);\n background: var(--color-background);\n font-family: var(--font-sans);\n font-synthesis: none;\n }\n\n * { box-sizing: border-box; }\n\n body {\n margin: 0;\n min-width: 320px;\n padding: 0;\n background: var(--color-background);\n color: var(--color-foreground);\n font-family: var(--font-sans);\n }\n\n button, input, textarea { font: inherit; }\n\n button {\n border: 1px solid var(--color-primary);\n border-radius: var(--radius-md);\n background: var(--color-primary);\n color: var(--color-primary-foreground);\n cursor: pointer;\n padding: 10px 14px;\n }\n\n button:hover { background: color-mix(in oklab, var(--color-primary) 90%, transparent); }\n button:focus-visible, input:focus-visible, textarea:focus-visible { outline: 3px solid var(--color-ring); outline-offset: 2px; }\n button:disabled { cursor: not-allowed; opacity: .5; }\n\n .page { max-width: 1120px; margin: 0 auto; padding: 32px 24px 56px; }\n\n header {\n display: flex;\n align-items: flex-start;\n justify-content: space-between;\n gap: 24px;\n border-bottom: 1px solid var(--color-border);\n padding-bottom: 24px;\n }\n\n h1, h2, p { margin-top: 0; }\n h1 { font-size: clamp(2rem, 5vw, 3rem); letter-spacing: -.04em; margin-bottom: 8px; }\n h2 { font-size: 1.15rem; letter-spacing: -.02em; margin-bottom: 16px; }\n .lede, .muted { color: var(--color-muted-foreground); }\n .lede { max-width: 46rem; margin-bottom: 0; }\n\n .viewer {\n min-width: 170px;\n border: 1px solid var(--color-border);\n border-radius: var(--radius-md);\n background: var(--color-card);\n padding: 12px;\n }\n\n .eyebrow {\n color: var(--color-muted-foreground);\n display: block;\n font-size: .72rem;\n font-weight: 700;\n letter-spacing: .08em;\n margin-bottom: 4px;\n text-transform: uppercase;\n }\n\n .viewer-name { font-weight: 700; overflow-wrap: anywhere; }\n\n .notice {\n border: 1px solid var(--color-border);\n border-left: 4px solid var(--color-primary);\n border-radius: var(--radius-md);\n background: var(--color-card);\n margin: 24px 0;\n min-height: 47px;\n padding: 12px 14px;\n }\n\n .notice[data-tone=\"error\"] { border-left-color: var(--color-status-error); background: color-mix(in oklab, var(--color-status-error) 10%, var(--color-card)); }\n .notice[data-tone=\"stale\"] { border-left-color: var(--color-status-warning); background: color-mix(in oklab, var(--color-status-warning) 10%, var(--color-card)); }\n .notice:empty { display: none; }\n\n .grid { display: grid; gap: 24px; grid-template-columns: minmax(0, 1.65fr) minmax(260px, .85fr); }\n .panel { border: 1px solid var(--color-border); border-radius: var(--radius-xl); background: var(--color-card); padding: 20px; }\n .panel + .panel { margin-top: 24px; }\n\n .add-row { display: grid; gap: 8px; grid-template-columns: minmax(0, 1fr) auto; margin-bottom: 16px; }\n input, textarea {\n border: 1px solid var(--color-input);\n border-radius: var(--radius-md);\n background: var(--color-card);\n color: inherit;\n padding: 10px 12px;\n width: 100%;\n }\n\n textarea { min-height: 94px; resize: vertical; }\n label { display: block; font-size: .88rem; font-weight: 650; margin-bottom: 8px; }\n\n .cards, .peers { list-style: none; margin: 0; padding: 0; }\n .cards { border-top: 1px solid var(--color-border-subtle); }\n .card {\n align-items: center;\n border-bottom: 1px solid var(--color-border-subtle);\n display: flex;\n gap: 12px;\n justify-content: space-between;\n padding: 13px 0;\n }\n\n .card-title { font-weight: 650; overflow-wrap: anywhere; }\n .card-status {\n background: var(--color-card);\n border-color: var(--color-input);\n color: var(--color-foreground);\n flex: 0 0 auto;\n font-size: .82rem;\n padding: 7px 10px;\n }\n\n .card-status[data-done=\"true\"] { background: var(--color-status-success); border-color: var(--color-status-success-strong); color: var(--color-status-success-foreground); }\n .empty { color: var(--color-muted-foreground); padding: 16px 0 4px; }\n .agent-note { background: var(--color-muted); border-radius: var(--radius-md); min-height: 46px; padding: 12px; white-space: pre-wrap; overflow-wrap: anywhere; }\n .agent-note:empty::before { color: var(--color-muted-foreground); content: \"No note yet. A server-side agent can write one here.\"; }\n\n .peers { display: grid; gap: 8px; }\n .peer {\n border: 1px solid var(--color-border-subtle);\n border-radius: var(--radius-md);\n display: grid;\n gap: 3px;\n padding: 10px;\n }\n\n .peer-name { font-weight: 650; overflow-wrap: anywhere; }\n .room-meta { color: var(--color-muted-foreground); font-family: var(--font-mono); font-size: .75rem; margin: 0 0 16px; }\n .save-state { color: var(--color-muted-foreground); font-size: .82rem; margin: 8px 0 0; min-height: 1.2em; }\n\n @media (max-width: 720px) {\n .page { padding: 24px 16px 40px; }\n header { align-items: stretch; flex-direction: column; }\n .viewer { min-width: 0; }\n .grid { grid-template-columns: 1fr; }\n .add-row { grid-template-columns: 1fr; }\n }\n </style>\n </head>\n <body>\n <swarm-cursors room=\"match-42\"></swarm-cursors>\n <main class=\"page\">\n <header>\n <div>\n <h1>Shared board</h1>\n <p class=\"lede\">A small board shared by everyone in match-42. Changes appear as peers make them.</p>\n </div>\n <section class=\"viewer\" aria-label=\"Current viewer\">\n <span class=\"eyebrow\">Connected as</span>\n <div class=\"viewer-name\" id=\"viewer-name\">Connecting…</div>\n </section>\n </header>\n\n <div id=\"notice\" class=\"notice\" role=\"status\" aria-live=\"polite\"></div>\n\n <div class=\"grid\">\n <section class=\"panel\" aria-labelledby=\"board-heading\">\n <h2 id=\"board-heading\">Match board</h2>\n <p class=\"room-meta\">Room: match-42 · Schema: 1</p>\n <form id=\"add-card-form\" class=\"add-row\">\n <label for=\"card-title\" class=\"sr-only\" hidden>New card title</label>\n <input id=\"card-title\" name=\"title\" maxlength=\"120\" placeholder=\"Write a card title\" required>\n <button type=\"submit\">Add card</button>\n </form>\n <ul id=\"cards\" class=\"cards\" aria-live=\"polite\"></ul>\n </section>\n\n <aside>\n <section class=\"panel\" aria-labelledby=\"peers-heading\">\n <h2 id=\"peers-heading\">Live peers</h2>\n <p class=\"muted\">Pointers use document-normalized coordinates and line up across viewports.</p>\n <ul id=\"peers\" class=\"peers\" aria-live=\"polite\"></ul>\n </section>\n\n <section class=\"panel\" aria-labelledby=\"agent-note-heading\">\n <h2 id=\"agent-note-heading\">Agent note</h2>\n <div id=\"agent-note\" class=\"agent-note\" aria-live=\"polite\"></div>\n </section>\n </aside>\n </div>\n\n <section class=\"panel\" aria-labelledby=\"lobby-heading\">\n <h2 id=\"lobby-heading\">Lobby</h2>\n <p class=\"room-meta\">Room: lobby · Schema: 1</p>\n <form id=\"lobby-form\">\n <label for=\"lobby-message\">Shared lobby message</label>\n <textarea id=\"lobby-message\" maxlength=\"1000\" placeholder=\"Leave a message for everyone in the lobby\"></textarea>\n <button type=\"submit\">Save lobby message</button>\n <p id=\"lobby-save-state\" class=\"save-state\" aria-live=\"polite\"></p>\n </form>\n </section>\n </main>\n\n <script>\n (() => {\n const ui = {\n viewerName: document.querySelector('#viewer-name'),\n notice: document.querySelector('#notice'),\n cards: document.querySelector('#cards'),\n peers: document.querySelector('#peers'),\n agentNote: document.querySelector('#agent-note'),\n addCardForm: document.querySelector('#add-card-form'),\n cardTitle: document.querySelector('#card-title'),\n lobbyForm: document.querySelector('#lobby-form'),\n lobbyMessage: document.querySelector('#lobby-message'),\n lobbySaveState: document.querySelector('#lobby-save-state')\n };\n\n let board;\n let lobby;\n let peers = [];\n let lobbyDirty = false;\n\n function message(error) {\n return error && error.message ? error.message : String(error);\n }\n\n function showNotice(text, tone) {\n ui.notice.textContent = text || '';\n ui.notice.dataset.tone = tone || '';\n }\n\n function markStale(room, roomName) {\n if (!room.stale) return false;\n showNotice(`${roomName} uses a different schema version. Its saved state remains visible, but this page cannot write changes.`, 'stale');\n return true;\n }\n\n function renderBoard(state) {\n const cards = state && state.cards && typeof state.cards === 'object' ? state.cards : {};\n ui.cards.replaceChildren();\n const entries = Object.entries(cards);\n\n if (!entries.length) {\n const empty = document.createElement('li');\n empty.className = 'empty';\n empty.textContent = 'No cards yet. Add the first card.';\n ui.cards.append(empty);\n }\n\n for (const [id, card] of entries) {\n const item = document.createElement('li');\n item.className = 'card';\n const title = document.createElement('span');\n title.className = 'card-title';\n title.textContent = card && typeof card.title === 'string' ? card.title : 'Untitled card';\n const toggle = document.createElement('button');\n const done = Boolean(card && card.done);\n toggle.className = 'card-status';\n toggle.dataset.done = String(done);\n toggle.type = 'button';\n toggle.textContent = done ? 'Done' : 'Open';\n toggle.setAttribute('aria-label', `Mark ${title.textContent} as ${done ? 'open' : 'done'}`);\n toggle.addEventListener('click', () => toggleCard(id));\n item.append(title, toggle);\n ui.cards.append(item);\n }\n\n ui.agentNote.textContent = state && typeof state.agentNote === 'string' ? state.agentNote : '';\n }\n\n function renderPeers(nextPeers) {\n peers = Array.isArray(nextPeers) ? nextPeers : [];\n ui.peers.replaceChildren();\n\n if (!peers.length) {\n const empty = document.createElement('li');\n empty.className = 'empty';\n empty.textContent = 'No other peers are connected.';\n ui.peers.append(empty);\n return;\n }\n\n for (const peer of peers) {\n const item = document.createElement('li');\n item.className = 'peer';\n const name = document.createElement('span');\n name.className = 'peer-name';\n name.textContent = peer.name || 'Guest';\n item.append(name);\n ui.peers.append(item);\n }\n }\n\n function renderLobby(state) {\n if (lobbyDirty) return;\n ui.lobbyMessage.value = state && typeof state.message === 'string' ? state.message : '';\n }\n\n async function addCard(title) {\n if (markStale(board, 'match-42')) return;\n try {\n await board.change(state => {\n state.cards ??= {};\n state.cards[crypto.randomUUID()] = { title, done: false };\n });\n ui.cardTitle.value = '';\n showNotice('Card added.', 'success');\n } catch (error) {\n showNotice(`Could not add the card: ${message(error)}`, 'error');\n }\n }\n\n async function toggleCard(id) {\n if (markStale(board, 'match-42')) return;\n try {\n await board.change(state => {\n const card = state.cards && state.cards[id];\n if (card) card.done = !card.done;\n });\n } catch (error) {\n showNotice(`Could not update the card: ${message(error)}`, 'error');\n }\n }\n\n async function saveLobbyMessage() {\n if (markStale(lobby, 'lobby')) return;\n try {\n const value = ui.lobbyMessage.value;\n await lobby.change(state => {\n state.message = value;\n });\n lobbyDirty = false;\n ui.lobbySaveState.textContent = 'Saved to the lobby room.';\n } catch (error) {\n ui.lobbySaveState.textContent = `Could not save: ${message(error)}`;\n showNotice(`Could not save the lobby message: ${message(error)}`, 'error');\n }\n }\n\n async function initialize() {\n try {\n board = await window.swarmSdk.room('match-42', { schemaVersion: 1 });\n lobby = await window.swarmSdk.room('lobby', { schemaVersion: 1 });\n\n const viewer = board.me || lobby.me || {};\n ui.viewerName.textContent = viewer.name || 'Guest';\n\n board.on('change', renderBoard);\n board.on('presence', renderPeers);\n board.on('error', error => showNotice(`match-42 error: ${message(error)}`, 'error'));\n board.on('reset', () => showNotice('match-42 was reset by another participant. The board has resynchronized.', 'stale'));\n lobby.on('change', renderLobby);\n lobby.on('error', error => showNotice(`lobby error: ${message(error)}`, 'error'));\n lobby.on('reset', () => showNotice('lobby was reset by another participant. The lobby has resynchronized.', 'stale'));\n\n renderBoard(board.state);\n renderPeers(board.presence.peers);\n renderLobby(lobby.state);\n markStale(board, 'match-42');\n markStale(lobby, 'lobby');\n\n if (!board.stale) {\n await board.change(state => {\n state.cards ??= {};\n state.agentNote ??= '';\n });\n }\n if (!lobby.stale) {\n await lobby.change(state => {\n state.message ??= '';\n state.activeMatch ??= 'match-42';\n });\n }\n } catch (error) {\n showNotice(`Could not connect to the shared rooms: ${message(error)}`, 'error');\n ui.viewerName.textContent = 'Unavailable';\n }\n }\n\n ui.addCardForm.addEventListener('submit', event => {\n event.preventDefault();\n const title = ui.cardTitle.value.trim();\n if (title) addCard(title);\n });\n ui.lobbyMessage.addEventListener('input', () => { lobbyDirty = true; });\n ui.lobbyForm.addEventListener('submit', event => {\n event.preventDefault();\n saveLobbyMessage();\n });\n window.addEventListener('pagehide', () => {\n if (board) board.close();\n if (lobby) lobby.close();\n });\n\n initialize();\n })();\n </script>\n </body>\n</html>\n"
|
|
44
44
|
},
|
|
45
45
|
{
|
|
46
46
|
"path": "examples/report-page.html",
|
|
47
|
-
"content": "<!doctype html>\n<!--\n Reference report page
|
|
47
|
+
"content": "<!doctype html>\n<!--\n Reference report page for the swarm design system (designVersion 2).\n\n Copy this as the starting point for a dense report or dashboard page, then\n replace the content. It uses only the swarm-* primitives, the theme tokens,\n and a few Tailwind layout utilities. It has zero color literals, so it\n follows the viewer's theme and dark mode inside the dashboard.\n\n Publish with:\n create_page({\n title: \"Q2 SEO Performance\",\n slug: \"q2-seo-performance\",\n description: \"Human-readable SEO performance report.\",\n contentType: \"text/html\",\n authMode: \"authed\",\n body: <this file's contents>,\n })\n-->\n<html lang=\"en\">\n<head>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n <title>Q2 SEO Performance</title>\n</head>\n<body>\n <main class=\"swarm-page\">\n <header>\n <p class=\"swarm-eyebrow\">Search Console · Generated 2026-06-04</p>\n <h1>Q2 SEO Performance</h1>\n <p class=\"swarm-lede\">Organic clicks grew 50% this quarter. The next gains depend on fixing thin page metadata and on three high-intent pillar pages.</p>\n <span class=\"swarm-badge\" data-tone=\"success\" data-dot>On track</span>\n </header>\n\n <section class=\"swarm-metrics\" aria-label=\"Key metrics\">\n <div class=\"swarm-metric\">\n <div class=\"swarm-metric-value\">18.6k</div>\n <div class=\"swarm-metric-label\">Organic clicks</div>\n </div>\n <div class=\"swarm-metric\" data-tone=\"success\">\n <div class=\"swarm-metric-value\">+50%</div>\n <div class=\"swarm-metric-label\">Quarter over quarter</div>\n </div>\n <div class=\"swarm-metric\" data-tone=\"warning\">\n <div class=\"swarm-metric-value\">23</div>\n <div class=\"swarm-metric-label\">Metadata fixes</div>\n </div>\n <div class=\"swarm-metric\">\n <div class=\"swarm-metric-value\">3</div>\n <div class=\"swarm-metric-label\">Priority pages</div>\n </div>\n </section>\n\n <section class=\"swarm-section\" aria-labelledby=\"findings-heading\">\n <h2 id=\"findings-heading\">Findings</h2>\n <div class=\"swarm-callout\" data-tone=\"warning\">\n <strong>Thin metadata caps growth.</strong> 23 ranking pages share a generic title and description. Fix them before new content ships.\n </div>\n <div class=\"swarm-callout\">\n <strong>One post drives the growth.</strong> The agent swarms post brought about a quarter of the new clicks.\n </div>\n </section>\n\n <section class=\"swarm-section\" aria-labelledby=\"pages-heading\">\n <h2 id=\"pages-heading\">Top pages</h2>\n <div class=\"swarm-toolbar\">\n <input class=\"swarm-input\" id=\"page-filter\" type=\"search\" placeholder=\"Filter pages\" aria-label=\"Filter pages\">\n <div class=\"swarm-segmented\" role=\"group\" aria-label=\"Status\">\n <button type=\"button\" data-status-filter=\"all\" aria-pressed=\"true\">All</button>\n <button type=\"button\" data-status-filter=\"fix\" aria-pressed=\"false\">Needs fix</button>\n </div>\n <button class=\"swarm-button\" id=\"filter-reset\" type=\"button\">Reset</button>\n </div>\n <div class=\"swarm-table-wrap\">\n <table class=\"swarm-table\" id=\"pages-table\">\n <thead>\n <tr><th>Page</th><th>Status</th><th class=\"num\">Q1 clicks</th><th class=\"num\">Q2 clicks</th><th class=\"num\">Change</th></tr>\n </thead>\n <tbody>\n <tr data-status=\"ok\"><td class=\"nowrap\">/pricing</td><td><span class=\"swarm-badge\" data-tone=\"success\">Healthy</span></td><td class=\"num\">4,100</td><td class=\"num\">6,900</td><td class=\"num\">+68%</td></tr>\n <tr data-status=\"fix\"><td class=\"nowrap\">/docs/install</td><td><span class=\"swarm-badge\" data-tone=\"warning\">Thin title</span></td><td class=\"num\">3,200</td><td class=\"num\">3,350</td><td class=\"num\">+5%</td></tr>\n <tr data-status=\"ok\"><td class=\"nowrap\">/blog/agent-swarms</td><td><span class=\"swarm-badge\" data-tone=\"success\">Healthy</span></td><td class=\"num\">1,500</td><td class=\"num\">3,100</td><td class=\"num\">+107%</td></tr>\n <tr data-status=\"fix\"><td class=\"nowrap\">/integrations/slack</td><td><span class=\"swarm-badge\" data-tone=\"error\">No description</span></td><td class=\"num\">1,900</td><td class=\"num\">1,700</td><td class=\"num text-status-error-strong\">-11%</td></tr>\n <tr data-status=\"review\"><td class=\"nowrap\">/changelog</td><td><span class=\"swarm-badge\">Not reviewed</span></td><td class=\"num\">400</td><td class=\"num\">1,150</td><td class=\"num\">+188%</td></tr>\n </tbody>\n </table>\n </div>\n <p class=\"text-muted-foreground\" id=\"filter-empty\" hidden>No pages match the filter.</p>\n </section>\n\n <section class=\"swarm-section\" aria-labelledby=\"actions-heading\">\n <h2 id=\"actions-heading\">Next actions</h2>\n <div class=\"grid gap-4 md:grid-cols-2\">\n <article class=\"swarm-panel\">\n <h3>Fix thin metadata</h3>\n <dl class=\"swarm-kv\">\n <dt>Owner</dt><dd>Content team</dd>\n <dt>Pages</dt><dd>23</dd>\n <dt>Due</dt><dd>2026-07-15</dd>\n </dl>\n </article>\n <article class=\"swarm-panel\">\n <h3>Publish three pillar pages</h3>\n <dl class=\"swarm-kv\">\n <dt>Owner</dt><dd>Growth team</dd>\n <dt>Topics</dt><dd>Agent orchestration, task routing, swarm costs</dd>\n <dt>Due</dt><dd>2026-08-01</dd>\n </dl>\n </article>\n </div>\n <div class=\"mt-4\">\n <a class=\"swarm-button\" data-variant=\"primary\" href=\"https://search.google.com/search-console\">Open Search Console</a>\n </div>\n </section>\n\n <details>\n <summary>Raw evidence</summary>\n <pre>{ \"source\": \"search-console\", \"range\": \"2026-04-01/2026-06-30\" }</pre>\n </details>\n </main>\n\n <script>\n (() => {\n const filter = document.getElementById(\"page-filter\");\n const reset = document.getElementById(\"filter-reset\");\n const empty = document.getElementById(\"filter-empty\");\n const statusButtons = document.querySelectorAll(\"[data-status-filter]\");\n const rows = document.querySelectorAll(\"#pages-table tbody tr\");\n let status = \"all\";\n\n function applyFilter() {\n const query = filter.value.trim().toLowerCase();\n let shown = 0;\n for (const row of rows) {\n const visible =\n row.textContent.toLowerCase().includes(query) &&\n (status === \"all\" || row.dataset.status === status);\n row.style.display = visible ? \"\" : \"none\";\n if (visible) shown += 1;\n }\n empty.hidden = shown > 0;\n }\n\n function setStatus(next) {\n status = next;\n for (const button of statusButtons) {\n button.setAttribute(\"aria-pressed\", String(button.dataset.statusFilter === next));\n }\n applyFilter();\n }\n\n for (const button of statusButtons) {\n button.addEventListener(\"click\", () => setStatus(button.dataset.statusFilter));\n }\n filter.addEventListener(\"input\", applyFilter);\n reset.addEventListener(\"click\", () => {\n filter.value = \"\";\n setStatus(\"all\");\n });\n })();\n </script>\n</body>\n</html>\n"
|
|
48
48
|
}
|
|
49
49
|
],
|
|
50
50
|
"planning": [
|
|
@@ -12,6 +12,11 @@ import {
|
|
|
12
12
|
import { envExportRestrictedError, isEnvExportRestrictedKey } from "../utils/config-env-export";
|
|
13
13
|
import { validateEgressAllowlist } from "../utils/egress-guard";
|
|
14
14
|
import { parseEnabledTools } from "../utils/enabled-tools";
|
|
15
|
+
import {
|
|
16
|
+
isFollowUpGateScheduleId,
|
|
17
|
+
parseFollowUpGateScheduleIds,
|
|
18
|
+
} from "../utils/follow-up-gate-schedule-ids";
|
|
19
|
+
import { kindForModelString } from "../utils/internal-ai/models";
|
|
15
20
|
import { normalizeOrgWebsite } from "../utils/org-website";
|
|
16
21
|
import { parseTaskToolManifest } from "../utils/task-tool-manifest";
|
|
17
22
|
import { isTierConfigKey, validateTierConfigValue } from "./model-tier-keys";
|
|
@@ -306,6 +311,24 @@ export function validateRuntimeTokenTtlHours(value: unknown): string | null {
|
|
|
306
311
|
: "Invalid RUNTIME_TOKEN_TTL_HOURS (must be greater than zero and at most 168)";
|
|
307
312
|
}
|
|
308
313
|
|
|
314
|
+
/**
|
|
315
|
+
* `TASK_AUTO_TITLE_MODEL` pins the task-title model: `provider/model-id` for a
|
|
316
|
+
* provider the internal-ai layer can call, or a bare `claude -p` alias. Empty
|
|
317
|
+
* clears the pin. The model id itself is checked at call time (an unknown id
|
|
318
|
+
* logs and falls through to the next candidate).
|
|
319
|
+
*/
|
|
320
|
+
export function validateTaskAutoTitleModel(value: unknown): string | null {
|
|
321
|
+
if (typeof value !== "string") return "Invalid TASK_AUTO_TITLE_MODEL (must be a string)";
|
|
322
|
+
const model = value.trim();
|
|
323
|
+
if (!model) return null;
|
|
324
|
+
if (/\s/.test(model) || model.endsWith("/")) {
|
|
325
|
+
return "Invalid TASK_AUTO_TITLE_MODEL (use provider/model-id or a claude -p alias)";
|
|
326
|
+
}
|
|
327
|
+
return kindForModelString(model)
|
|
328
|
+
? null
|
|
329
|
+
: "Invalid TASK_AUTO_TITLE_MODEL (provider must be openrouter, anthropic, openai, or openai-codex)";
|
|
330
|
+
}
|
|
331
|
+
|
|
309
332
|
export const validateControllerTokenTtlDays = boundedIntegerValidators(
|
|
310
333
|
["RUNTIME_CONTROLLER_TOKEN_TTL_DAYS"],
|
|
311
334
|
1,
|
|
@@ -428,6 +451,13 @@ const VALIDATED_KEYS: Record<string, ConfigValidator> = {
|
|
|
428
451
|
// model's weekly window (Fable/Opus/Sonnet). fallback: legacy random pick.
|
|
429
452
|
...enumValidator("MODEL_WINDOW_EXHAUSTED_POLICY", ["fail", "fallback"]),
|
|
430
453
|
...enumValidator("HEARTBEAT_CHECKLIST_GATE_MODE", ["shadow", "enforce"]),
|
|
454
|
+
// `enforce` is deliberately absent: the follow-up gate only records decisions.
|
|
455
|
+
...enumValidator("FOLLOW_UP_GATE_MODE", ["off", "shadow"]),
|
|
456
|
+
FOLLOW_UP_GATE_SCHEDULE_IDS: (value: unknown) => {
|
|
457
|
+
const message = "Invalid FOLLOW_UP_GATE_SCHEDULE_IDS (must be comma-separated schedule UUIDs)";
|
|
458
|
+
if (typeof value !== "string") return message;
|
|
459
|
+
return parseFollowUpGateScheduleIds(value).every(isFollowUpGateScheduleId) ? null : message;
|
|
460
|
+
},
|
|
431
461
|
...enumValidator("SYSTEM_ONE_DEFAULT_PROVIDER", [...SYSTEM_ONE_DEFAULT_PROVIDER_OPTIONS]),
|
|
432
462
|
// Codex credits-exhausted cooldown (ms). Permissive on range here (positive
|
|
433
463
|
// integer) — the worker clamps to [5m, 7d] via resolveCodexCreditsExhaustedCooldownMs.
|
|
@@ -525,7 +555,9 @@ const VALIDATED_KEYS: Record<string, ConfigValidator> = {
|
|
|
525
555
|
"MODEL_AUTO_UPGRADE",
|
|
526
556
|
"COMB_ENABLED",
|
|
527
557
|
"AGENT_FS_PROXY",
|
|
558
|
+
"TASK_AUTO_TITLE_ENABLED",
|
|
528
559
|
]),
|
|
560
|
+
TASK_AUTO_TITLE_MODEL: validateTaskAutoTitleModel,
|
|
529
561
|
...enumValidator("SLACK_MODE", ["socket", "http"]),
|
|
530
562
|
...enumValidator("SLACK_THREAD_STEERING", ["off", "lead", "all"]),
|
|
531
563
|
...enumValidator("SLACK_THREAD_STEERING_MODE", ["steer", "queue"]),
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Automatic task titles.
|
|
3
|
+
*
|
|
4
|
+
* Listens for `task.created` (emitted after commit by `createTaskExtended`),
|
|
5
|
+
* asks the cheapest configured model for a short title, and stores it on the
|
|
6
|
+
* task when nobody set one first. The sessions sidebar and task lists read
|
|
7
|
+
* `title` and fall back to the prompt, so a failed or skipped generation
|
|
8
|
+
* changes nothing visible.
|
|
9
|
+
*
|
|
10
|
+
* `TASK_AUTO_TITLE_ENABLED=false` turns the listener off at the next event;
|
|
11
|
+
* `TASK_AUTO_TITLE_MODEL` pins one model (see `buildTaskTitlePlan`).
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { getApiKey } from "../utils/api-key";
|
|
15
|
+
import { getMcpBaseUrl } from "../utils/constants";
|
|
16
|
+
import { parseEnvFlag } from "../utils/env-flag";
|
|
17
|
+
import { generateTaskTitle, TASK_TITLE_TIMEOUT_MS } from "../utils/internal-ai/task-title";
|
|
18
|
+
import { scrubSecrets } from "../utils/secret-scrubber";
|
|
19
|
+
import { workflowEventBus } from "../workflows/event-bus";
|
|
20
|
+
import { getTaskById } from "./db/tasks/read";
|
|
21
|
+
import { setTaskTitleIfUnset } from "./db/tasks/write";
|
|
22
|
+
|
|
23
|
+
/** Titles generated at once. Bursts (a workflow fan-out) queue behind this. */
|
|
24
|
+
export const TASK_AUTO_TITLE_CONCURRENCY = 3;
|
|
25
|
+
|
|
26
|
+
export function isTaskAutoTitleEnabled(env: NodeJS.ProcessEnv = process.env): boolean {
|
|
27
|
+
return parseEnvFlag(env.TASK_AUTO_TITLE_ENABLED, true);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface TaskAutoTitleListenerOptions {
|
|
31
|
+
/** Defaults to `process.env`; read on every event so a config reload applies. */
|
|
32
|
+
env?: NodeJS.ProcessEnv;
|
|
33
|
+
/** Test injection: replaces the model call. */
|
|
34
|
+
generate?: typeof generateTaskTitle;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface TaskAutoTitleListener {
|
|
38
|
+
stop(): void;
|
|
39
|
+
/** Resolves once every queued task has been processed. */
|
|
40
|
+
idle(): Promise<void>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function startTaskAutoTitleListener(
|
|
44
|
+
options: TaskAutoTitleListenerOptions = {},
|
|
45
|
+
): TaskAutoTitleListener {
|
|
46
|
+
const env = options.env ?? process.env;
|
|
47
|
+
const generate = options.generate ?? generateTaskTitle;
|
|
48
|
+
const queue: string[] = [];
|
|
49
|
+
let active = 0;
|
|
50
|
+
let idleWaiters: Array<() => void> = [];
|
|
51
|
+
|
|
52
|
+
const settleIdle = () => {
|
|
53
|
+
if (active > 0 || queue.length > 0) return;
|
|
54
|
+
const waiters = idleWaiters;
|
|
55
|
+
idleWaiters = [];
|
|
56
|
+
for (const resolve of waiters) resolve();
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const pump = () => {
|
|
60
|
+
while (active < TASK_AUTO_TITLE_CONCURRENCY) {
|
|
61
|
+
const taskId = queue.shift();
|
|
62
|
+
if (!taskId) break;
|
|
63
|
+
active++;
|
|
64
|
+
void titleTask(taskId, env, generate)
|
|
65
|
+
.catch((error) => console.error("[task-title] Failed:", scrubSecrets(String(error))))
|
|
66
|
+
.finally(() => {
|
|
67
|
+
active--;
|
|
68
|
+
pump();
|
|
69
|
+
settleIdle();
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const onTaskCreated = (data: unknown) => {
|
|
75
|
+
if (!isTaskAutoTitleEnabled(env)) return;
|
|
76
|
+
const taskId = (data as { taskId?: unknown }).taskId;
|
|
77
|
+
if (typeof taskId !== "string" || !taskId) return;
|
|
78
|
+
queue.push(taskId);
|
|
79
|
+
pump();
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
workflowEventBus.on("task.created", onTaskCreated);
|
|
83
|
+
return {
|
|
84
|
+
stop: () => workflowEventBus.off("task.created", onTaskCreated),
|
|
85
|
+
idle: () =>
|
|
86
|
+
new Promise<void>((resolve) => {
|
|
87
|
+
idleWaiters.push(resolve);
|
|
88
|
+
settleIdle();
|
|
89
|
+
}),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function titleTask(
|
|
94
|
+
taskId: string,
|
|
95
|
+
env: NodeJS.ProcessEnv,
|
|
96
|
+
generate: typeof generateTaskTitle,
|
|
97
|
+
): Promise<void> {
|
|
98
|
+
const task = await getTaskById(taskId);
|
|
99
|
+
if (!task || task.title) return;
|
|
100
|
+
const shortId = taskId.slice(0, 8);
|
|
101
|
+
const startedAt = Date.now();
|
|
102
|
+
const result = await generate({
|
|
103
|
+
task: task.task,
|
|
104
|
+
env,
|
|
105
|
+
// Loopback context for the codex OAuth probe; only used when no API key is set.
|
|
106
|
+
apiUrl: getMcpBaseUrl(),
|
|
107
|
+
apiKey: getApiKey(),
|
|
108
|
+
callerTag: `task-title:${task.source}`,
|
|
109
|
+
signal: AbortSignal.timeout(TASK_TITLE_TIMEOUT_MS),
|
|
110
|
+
});
|
|
111
|
+
if (!result.ok) {
|
|
112
|
+
if (result.reason !== "short-task") {
|
|
113
|
+
console.log(`[task-title] ${shortId}: no title (${result.reason})`);
|
|
114
|
+
}
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
const updated = await setTaskTitleIfUnset(taskId, result.title);
|
|
118
|
+
const elapsed = Date.now() - startedAt;
|
|
119
|
+
// The title derives from task text, so a pasted token could reach the log.
|
|
120
|
+
console.log(
|
|
121
|
+
scrubSecrets(
|
|
122
|
+
`[task-title] ${shortId}: "${result.title}" via ${result.model} in ${elapsed}ms${
|
|
123
|
+
updated ? "" : " (kept the existing title)"
|
|
124
|
+
}`,
|
|
125
|
+
),
|
|
126
|
+
);
|
|
127
|
+
}
|
|
@@ -15,6 +15,10 @@ async function executeCommand(
|
|
|
15
15
|
command: ControllerCommand,
|
|
16
16
|
): Promise<unknown> {
|
|
17
17
|
switch (command.op) {
|
|
18
|
+
case "release-cache":
|
|
19
|
+
if (!controller.releaseCache) throw new Error("Controller does not support cache cleanup");
|
|
20
|
+
await controller.releaseCache(command.payload);
|
|
21
|
+
return {};
|
|
18
22
|
case "provision":
|
|
19
23
|
return await controller.provision(command.payload);
|
|
20
24
|
case "terminate":
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/** Worker boot step; deliberately standalone so both worker images can run it before setup. */
|
|
2
|
+
export interface ApiReadinessDependencies {
|
|
3
|
+
fetch: (url: string, init: RequestInit) => Promise<Response>;
|
|
4
|
+
now: () => number;
|
|
5
|
+
sleep: (ms: number) => Promise<void>;
|
|
6
|
+
log: (message: string) => void;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const defaults: ApiReadinessDependencies = {
|
|
10
|
+
fetch: (url, init) => fetch(url, init),
|
|
11
|
+
now: () => performance.now(),
|
|
12
|
+
sleep: (ms) => Bun.sleep(ms),
|
|
13
|
+
log: (message) => console.log(message),
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/** Poll the unauthenticated health endpoint before any provider reads configuration. */
|
|
17
|
+
export async function waitForApiReady(
|
|
18
|
+
baseUrl: string,
|
|
19
|
+
timeoutValue = "90",
|
|
20
|
+
dependencies: Partial<ApiReadinessDependencies> = {},
|
|
21
|
+
): Promise<boolean> {
|
|
22
|
+
const { fetch: probe, now, sleep, log } = { ...defaults, ...dependencies };
|
|
23
|
+
const raw = timeoutValue || "90";
|
|
24
|
+
const seconds = Number(raw);
|
|
25
|
+
if (!/^\d+$/.test(raw) || !Number.isSafeInteger(seconds) || seconds <= 0) {
|
|
26
|
+
log(
|
|
27
|
+
`[entrypoint] FATAL: WORKER_API_READY_TIMEOUT_SECONDS must be a positive integer, got '${raw}'; exiting.`,
|
|
28
|
+
);
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const url = `${baseUrl.replace(/\/$/, "")}/health`;
|
|
33
|
+
const deadline = now() + seconds * 1000;
|
|
34
|
+
log(`[entrypoint] Waiting for API readiness at ${url} (timeout ${raw}s)...`);
|
|
35
|
+
while (true) {
|
|
36
|
+
const controller = new AbortController();
|
|
37
|
+
const timer = setTimeout(
|
|
38
|
+
() => controller.abort(),
|
|
39
|
+
Math.min(3000, Math.max(1, deadline - now())),
|
|
40
|
+
);
|
|
41
|
+
try {
|
|
42
|
+
const response = await probe(url, { redirect: "manual", signal: controller.signal });
|
|
43
|
+
// curl -o /dev/null waited for the whole response, without retaining its body.
|
|
44
|
+
if (response.body) {
|
|
45
|
+
for await (const _chunk of response.body) {
|
|
46
|
+
// Discard health output, including error bodies, within the request timeout.
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
// Match curl -f without -L: redirects are successful and are not followed.
|
|
50
|
+
if (response.status > 0 && response.status < 400 && now() <= deadline) {
|
|
51
|
+
log(`[entrypoint] API ready at ${url}`);
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
} catch {
|
|
55
|
+
// The API can still be booting; retry connection, HTTP and timeout failures alike.
|
|
56
|
+
} finally {
|
|
57
|
+
clearTimeout(timer);
|
|
58
|
+
}
|
|
59
|
+
if (now() >= deadline) {
|
|
60
|
+
log(`[entrypoint] FATAL: API readiness timed out after ${raw}s waiting for ${url}; exiting.`);
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
await sleep(Math.min(1000, deadline - now()));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (import.meta.main) {
|
|
68
|
+
process.exitCode = (await waitForApiReady(
|
|
69
|
+
Bun.argv[2] ?? "http://host.docker.internal:3013",
|
|
70
|
+
Bun.argv[3],
|
|
71
|
+
))
|
|
72
|
+
? 0
|
|
73
|
+
: 1;
|
|
74
|
+
}
|