@desplega.ai/agent-swarm 2.4.4 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -2
- package/dist/{acp-adapter-thq04adh.js → acp-adapter-11gp67nr.js} +8 -6
- package/dist/{actions-j6ywh6x5.js → actions-8p2kze8j.js} +24 -22
- package/dist/{amp-adapter-k8b5xmj8.js → amp-adapter-z4qvhacj.js} +2 -2
- package/dist/{anthropic-messages-g6pqmqk4.js → anthropic-messages-txs190dd.js} +17 -20
- package/dist/{app-rb7vc02s.js → app-0qp0a2gk.js} +3 -3
- package/dist/{assistant-ftta1zjf.js → assistant-jedd3ktm.js} +27 -25
- package/dist/{azure-openai-responses-9dbmj346.js → azure-openai-responses-9kfwhmn7.js} +18 -13
- package/dist/{boot-reembed-xns7h5h9.js → boot-reembed-4xwq5tg0.js} +1 -1
- package/dist/{boot-reembed-srp6v6af.js → boot-reembed-n4wq5dd2.js} +1 -1
- package/dist/{boot-scrub-sweep-3qqrh2mz.js → boot-scrub-sweep-69h90xcq.js} +4 -4
- package/dist/{boot-seeds-9ks8hce7.js → boot-seeds-3vn0m9sg.js} +4 -4
- package/dist/{claude-adapter-k5ksyres.js → claude-adapter-221mvad5.js} +8 -6
- package/dist/{claude-managed-adapter-h2y889ge.js → claude-managed-adapter-gt6pa35d.js} +2 -2
- package/dist/{claude-managed-setup-n5a5eqks.js → claude-managed-setup-c3hp36ph.js} +4 -4
- package/dist/{claude-sdk-session-2j3j4kwn.js → claude-sdk-session-8thjsk5e.js} +9729 -9628
- package/dist/{cli-b9fcxxev.js → cli-007cnhm1.js} +50 -4
- package/dist/{cli-j4rqdf2n.js → cli-03vpq45j.js} +2 -2
- package/dist/{cli-amyhm3gj.js → cli-0y6f0ckq.js} +1 -1
- package/dist/{cli-4dg55wsk.js → cli-0z3vjrpm.js} +1 -1
- package/dist/{cli-pccfmtb5.js → cli-179xcyky.js} +11 -11
- package/dist/{cli-3r8samv3.js → cli-1f0gp9vz.js} +3 -2
- package/dist/{cli-hj3debgw.js → cli-1pr672hz.js} +3 -3
- package/dist/{cli-0bqteszy.js → cli-2nf3b08t.js} +328 -25
- package/dist/{cli-t7cyef5g.js → cli-36j4jnk3.js} +4 -4
- package/dist/{cli-er83cjbn.js → cli-3qv8naq8.js} +527 -231
- package/dist/cli-3zwye9sn.js +1193 -0
- package/dist/{cli-60cgh1rv.js → cli-459fk97x.js} +7 -7
- package/dist/{cli-xj0re5tm.js → cli-4w24f7z3.js} +1 -1
- package/dist/{cli-v3yr2gbg.js → cli-5kzk3472.js} +2198 -968
- package/dist/{cli-h7gc0b4r.js → cli-5m84zd2k.js} +55 -18
- package/dist/{cli-w6nadkh4.js → cli-5rvg1ddk.js} +5 -5
- package/dist/{cli-dvkmp9bt.js → cli-6ctwrf1z.js} +20 -10
- package/dist/{cli-9qtjfd60.js → cli-6rg2jn2y.js} +3 -2
- package/dist/{cli-2v045xyf.js → cli-7bwtc6yx.js} +3 -3
- package/dist/{cli-3cgngyd8.js → cli-7g40g8hq.js} +35 -17
- package/dist/{cli-q6r7t299.js → cli-92mqenkc.js} +2379 -154
- package/dist/{cli-cynvc9fj.js → cli-9ce2xw98.js} +8 -8
- package/dist/{cli-gqvh8d8a.js → cli-bgdtv62r.js} +36 -22
- package/dist/{cli-wxbn3yfx.js → cli-cz5nmdxt.js} +2 -2
- package/dist/{cli-wt645v7q.js → cli-dabehxhp.js} +1 -1
- package/dist/{cli-vfha21c9.js → cli-e5bxnjps.js} +2 -2
- package/dist/{cli-p20r8w42.js → cli-fcc5qe3f.js} +763 -268
- package/dist/{cli-1ephgwnq.js → cli-fes51ya2.js} +3 -3
- package/dist/{cli-1d70crsy.js → cli-ftmq4nvk.js} +2 -2
- package/dist/{cli-qkhc5aqy.js → cli-fy1jyyym.js} +13 -13
- package/dist/{cli-s95dknxh.js → cli-g1cw7xtt.js} +1 -1
- package/dist/{cli-qje9k2nh.js → cli-g9ypd5en.js} +12 -2
- package/dist/{cli-74c1wpm0.js → cli-gpv60278.js} +1 -1
- package/dist/{cli-pemywhfq.js → cli-ha9rmwgq.js} +2 -1
- package/dist/{cli-bnz0r3kc.js → cli-j1eghqfm.js} +5 -5
- package/dist/{cli-2zqpkvtt.js → cli-jpxsrxcb.js} +1 -1
- package/dist/{cli-ejs3m1sx.js → cli-kxpfx2xr.js} +1 -1
- package/dist/{cli-37yb7307.js → cli-mbfj11fq.js} +1 -1
- package/dist/{cli-pnp2j5zg.js → cli-mmhj8vd7.js} +2 -2
- package/dist/{cli-xf234ws8.js → cli-n4c8e3s5.js} +2 -0
- package/dist/{cli-a0kcty51.js → cli-n6kmqngw.js} +2 -2
- package/dist/{cli-te6b5wvg.js → cli-nnvjb17s.js} +3 -3
- package/dist/{cli-1dmcq2eh.js → cli-nzfnt2gs.js} +1 -1
- package/dist/{cli-625tb7aq.js → cli-p859hcc8.js} +2 -2
- package/dist/{cli-smvcw9z5.js → cli-pasyvjmm.js} +1 -1
- package/dist/cli-pkaqk57w.js +128 -0
- package/dist/{cli-8nyhf7za.js → cli-qqzmjxkf.js} +129 -105
- package/dist/{cli-zrwpctp4.js → cli-r25qpaxt.js} +161 -63
- package/dist/{cli-nc35pxb7.js → cli-rh5smt8t.js} +19 -4
- package/dist/{cli-qwkcwjdn.js → cli-rycdaj0a.js} +1 -1
- package/dist/{cli-esxerzg7.js → cli-s4sntwt4.js} +2 -2
- package/dist/{cli-b8k7epj1.js → cli-segs7hx9.js} +8 -8
- package/dist/{cli-36ckw0wx.js → cli-stxdrqs2.js} +2 -2
- package/dist/{cli-vdm20wej.js → cli-sy8f9705.js} +4 -4
- package/dist/{cli-1m17dc9d.js → cli-ta8bd7th.js} +2 -2
- package/dist/{cli-2y91wnbh.js → cli-we0ra057.js} +222 -10
- package/dist/{cli-55qmbp6z.js → cli-x21dqn18.js} +5 -5
- package/dist/{cli-dqb4n629.js → cli-x6vf4sad.js} +11 -11
- package/dist/{cli-d2mb5s8a.js → cli-x77gen2p.js} +680 -440
- package/dist/{cli-vq97g1jq.js → cli-xawhpgc9.js} +95 -195
- package/dist/{cli-82d3nkc3.js → cli-xd4eqtr2.js} +2 -2
- package/dist/{cli-9fhkkpj5.js → cli-xt768r5m.js} +2 -2
- package/dist/{cli-zrk6jxv8.js → cli-y4x8ww6s.js} +2 -2
- package/dist/{cli-xc27fwzg.js → cli-y9jvxfhk.js} +3 -3
- package/dist/{cli-44159k28.js → cli-z63a269p.js} +2 -2
- package/dist/{cli-sm8rses9.js → cli-zfdp01hn.js} +8 -5
- package/dist/cli.js +31 -29
- package/dist/{cloudflare-workers-ai-system-one-ch2wxfcp.js → cloudflare-workers-ai-system-one-43v9qkjk.js} +7 -2
- package/dist/{codex-adapter-968xwfy7.js → codex-adapter-tcg1q3jk.js} +6 -4
- package/dist/{codex-login-m819c41r.js → codex-login-rn7c5gxt.js} +1 -1
- package/dist/{codex-session-runner-ht4z99fa.js → codex-session-runner-xfx4wd9x.js} +7 -5
- package/dist/{commands-pcymaeg9.js → commands-ew83nj70.js} +2 -2
- package/dist/{controller-fpjyk46r.js → controller-gnq1k1mf.js} +4 -4
- package/dist/{controllers-connect-mwzx1fh6.js → controllers-connect-tw1dan9r.js} +26 -24
- package/dist/{cursor-adapter-cp4s2e2f.js → cursor-adapter-yq5xgfcg.js} +1 -1
- package/dist/{db-xsjg9jqq.js → db-7gbc53fd.js} +14 -2
- package/dist/{db-sgd12bt9.js → db-t86xrnes.js} +3 -3
- package/dist/{devin-adapter-zgqadvnm.js → devin-adapter-4ycy4vrf.js} +1 -1
- package/dist/{dispatcher-x0jfw2c9.js → dispatcher-64bdyhps.js} +23 -21
- package/dist/{docker-fkgtr75t.js → docker-nfy2gc8t.js} +2 -2
- package/dist/{dsh-adapter-jrq7ma7h.js → dsh-adapter-tvhc4k2d.js} +1 -1
- package/dist/{e2b-16d6ch39.js → e2b-9gkdhm3c.js} +2 -2
- package/dist/{e2b-zn0b4w5w.js → e2b-cjtf8k1z.js} +2 -2
- package/dist/{easter-egg-3d-5cgynaja.js → easter-egg-3d-e4qv8dxj.js} +1 -1
- package/dist/{event-bus-9vpk8gmq.js → event-bus-3z78ep5n.js} +1 -1
- package/dist/{execute-t4e6m3rw.js → execute-0xpz8950.js} +85 -23
- package/dist/{external-channel-8njj4h3b.js → external-channel-e9nw24zv.js} +2 -2
- package/dist/{google-generative-ai-bmq8gkf3.js → google-generative-ai-mk9vcpe1.js} +15 -15
- package/dist/{google-vertex-b1rvp52s.js → google-vertex-q580kz8k.js} +20 -20
- package/dist/{grok-adapter-kkr97v2f.js → grok-adapter-v82srhrx.js} +9 -7
- package/dist/handlers-c9ajzhbn.js +41 -0
- package/dist/{heartbeat-wd03zhn0.js → heartbeat-2vqs1319.js} +26 -24
- package/dist/{hook-xzj2ts2p.js → hook-01nk13wx.js} +7 -5
- package/dist/{hook-f0vghj42.js → hook-jzpheegg.js} +9 -7
- package/dist/{http-4hwxv62p.js → http-fc1k6ec8.js} +859 -252
- package/dist/index-07ef7nkm.js +42 -0
- package/dist/{index-g6tbb6ve.js → index-31ceyf42.js} +1 -1
- package/dist/index-7yts8n9s.js +43 -0
- package/dist/{index-c4ykd0f1.js → index-dr94zbnh.js} +13348 -11611
- package/dist/index-q3t40ryv.js +46 -0
- package/dist/{index-ww2vznkp.js → index-sepja052.js} +1 -1
- package/dist/{index-97q4xn03.js → index-zdycv3ja.js} +135 -79
- package/dist/{index-g216q6jr.js → index-zsyx51ap.js} +1 -4
- package/dist/{keepalive-hrh7xnr9.js → keepalive-5nh2rhmr.js} +4 -4
- package/dist/lead-aayq64jn.js +79 -0
- package/dist/{llama-cpp-classify-vv43gswy.js → llama-cpp-classify-ee7vmd7q.js} +4 -1
- package/dist/{maintenance-e8wnwnqx.js → maintenance-hjhzdt3k.js} +1 -1
- package/dist/{mistral-conversations-1medfwv7.js → mistral-conversations-sd0xf1tn.js} +13 -13
- package/dist/{node-3v3f08f7.js → node-jzjj7r9x.js} +10 -9
- package/dist/{oauth-refresh-sweep-zm0yj4sj.js → oauth-refresh-sweep-y3n1knmd.js} +3 -3
- package/dist/{onboard-dtwcq603.js → onboard-1zq3m7db.js} +2 -2
- package/dist/{openai-codex-responses-de9pvs4d.js → openai-codex-responses-79s8xnbh.js} +29 -23
- package/dist/{openai-completions-mtqge3nf.js → openai-completions-tdbyts0x.js} +21 -20
- package/dist/{openai-decisions-ygk4kkxd.js → openai-decisions-bqhnjyfz.js} +2 -1
- package/dist/{openai-responses-dkyng71g.js → openai-responses-dm3t5vqp.js} +26 -17
- package/dist/{opencode-adapter-x4tqzhkh.js → opencode-adapter-e56086m1.js} +7 -5
- package/dist/{openrouter-images-wvv7camj.js → openrouter-images-ssttr1y4.js} +6 -3
- package/dist/{otel-impl-w3fbhpn0.js → otel-impl-h9yyza47.js} +2 -2
- package/dist/{pi-messages-nhh1pkz2.js → pi-messages-3nf8bq1a.js} +6 -4
- package/dist/{pi-mono-adapter-mcfkn9vf.js → pi-mono-adapter-tzps8f38.js} +9 -37
- package/dist/{pricing-refresh-j38dehfs.js → pricing-refresh-nevhat8h.js} +3 -3
- package/dist/{rbac-roles-ae8yttq4.js → rbac-roles-4y6ngvzq.js} +1 -1
- package/dist/{rbac-roles-ejxx9b35.js → rbac-roles-exbg8xb6.js} +2 -2
- package/dist/{registry-rxw10xzg.js → registry-ajhexe44.js} +1 -1
- package/dist/{render-v2-n16n0phe.js → render-v2-cfsh13z3.js} +3 -3
- package/dist/{resume-z0fgjc38.js → resume-5s2t0dbs.js} +10 -8
- package/dist/{runtime-xe3aqy4x.js → runtime-bt5pkpm1.js} +1 -1
- package/dist/{session-access-cv3mq6mc.js → session-access-dnbyp3cn.js} +6 -4
- package/dist/{setup-mafhj3yt.js → setup-1wj1y77j.js} +1 -1
- package/dist/{typesafe-system-one-2hgpayzy.js → typesafe-system-one-bre38hvs.js} +4 -2
- package/dist/{virtual-modules-4r3zkc20.js → virtual-modules-mnfbh5mr.js} +8 -2
- package/dist/{work-objects-8b86mgjp.js → work-objects-7mg6216g.js} +26 -24
- package/dist/worker-nwb789mg.js +79 -0
- package/dist/{x-y45mthwn.js → x-pbpezr0s.js} +2 -2
- package/openapi.json +793 -6
- package/package.json +11 -11
- package/src/apps/catalog.generated.json +206 -4
- package/src/apps/page-validator.ts +13 -1
- package/src/assets/key.ts +17 -0
- package/src/azure-devops/handlers.ts +1 -1
- package/src/be/chunking.ts +42 -9
- package/src/be/db/approvals.ts +18 -0
- package/src/be/db/session-moves.ts +164 -0
- package/src/be/db/session-tree.ts +384 -0
- package/src/be/db/tasks/draft-text.ts +15 -0
- package/src/be/db/tasks/read.ts +54 -5
- package/src/be/db/workflow-runs.ts +96 -0
- package/src/be/db.ts +85 -237
- package/src/be/memory/index-content.ts +36 -17
- package/src/be/memory/providers/openai-embedding.ts +115 -61
- package/src/be/memory/providers/sqlite-store.ts +69 -23
- package/src/be/memory/reranker.ts +31 -1
- package/src/be/memory/search-candidates.ts +47 -0
- package/src/be/memory/types.ts +14 -0
- package/src/be/onboarding.ts +3 -0
- package/src/be/scripts/typecheck.ts +2 -0
- package/src/be/seed-scripts/catalog/heartbeat-jev-gate.ts +106 -54
- package/src/be/seed-scripts/index.ts +2 -2
- package/src/be/skill-sync.ts +43 -2
- package/src/be/swarm-config-guard.ts +25 -1
- package/src/be/task-attachment-store.ts +30 -0
- package/src/bitbucket/api.ts +81 -0
- package/src/bitbucket/auth.ts +64 -0
- package/src/bitbucket/handlers.ts +434 -0
- package/src/bitbucket/index.ts +28 -0
- package/src/bitbucket/templates.ts +134 -0
- package/src/bitbucket/types.ts +65 -0
- package/src/commands/runner.ts +55 -3
- package/src/e2b/env.ts +1 -0
- package/src/extensions/catalog.generated.json +4 -4
- package/src/extensions/contract-types.generated.ts +1 -1
- package/src/extensions/contract.ts +9 -2
- package/src/extensions/post-bridge.ts +1 -0
- package/src/fs/provider.ts +13 -1
- package/src/github/handlers.ts +6 -6
- package/src/gitlab/handlers.ts +5 -5
- package/src/heartbeat/heartbeat.ts +31 -11
- package/src/http/assets.ts +14 -2
- package/src/http/core.ts +12 -0
- package/src/http/fs.ts +54 -34
- package/src/http/index.ts +4 -0
- package/src/http/mcp-body.ts +37 -0
- package/src/http/mcp-user.ts +3 -5
- package/src/http/mcp.ts +6 -7
- package/src/http/memory.ts +26 -15
- package/src/http/onboarding.ts +1 -0
- package/src/http/page-proxy.ts +61 -5
- package/src/http/pages-public.ts +27 -44
- package/src/http/pages.ts +4 -4
- package/src/http/script-runs.ts +2 -0
- package/src/http/sessions.ts +366 -12
- package/src/http/skills.ts +48 -1
- package/src/http/tasks.ts +1 -1
- package/src/http/users.ts +13 -1
- package/src/http/webhooks.ts +141 -0
- package/src/integrations/chat-sdk/adapters.ts +24 -4
- package/src/integrations/chat-sdk/attachments.ts +90 -12
- package/src/integrations/chat-sdk/facts.ts +138 -0
- package/src/integrations/chat-sdk/inbound.ts +156 -8
- package/src/integrations/chat-sdk/limits.ts +24 -0
- package/src/integrations/chat-sdk/outbound-files.ts +186 -0
- package/src/integrations/chat-sdk/outbound.ts +272 -5
- package/src/integrations/chat-sdk/relay.ts +4 -1
- package/src/integrations/chat-sdk/router.ts +28 -2
- package/src/integrations/chat-sdk/steering.ts +9 -0
- package/src/integrations/chat-sdk/templates.ts +14 -0
- package/src/integrations/chat-sdk/thread-buffer.ts +4 -0
- package/src/mcp-limits.ts +20 -0
- package/src/pages/browser-sdk.ts +8 -3
- package/src/pages/feedback-overlay.ts +1 -1
- package/src/providers/acp-targets.ts +1 -1
- package/src/providers/amp-adapter.ts +1 -1
- package/src/providers/codex-adapter.ts +106 -119
- package/src/providers/codex-agents-md.ts +35 -229
- package/src/providers/cursor-adapter.ts +1 -1
- package/src/providers/pi-mono-adapter.ts +5 -53
- package/src/realtime/transport.ts +1 -2
- package/src/script-workflows/workflow-ctx.ts +1 -0
- package/src/scripts-runtime/types/stdlib.d.ts +2 -0
- package/src/scripts-runtime/types/swarm-extension.d.ts +14 -4
- package/src/scripts-runtime/types/swarm-sdk.d.ts +2 -0
- package/src/server-user.ts +2 -0
- package/src/server.ts +2 -0
- package/src/slack/assistant.ts +2 -2
- package/src/slack/files.ts +2 -0
- package/src/slack/handlers.test.ts +158 -0
- package/src/slack/handlers.ts +23 -32
- package/src/slack/inbound-files.ts +39 -6
- package/src/slack/thread-buffer.ts +2 -1
- package/src/tasks/context-key.ts +61 -1
- package/src/tasks/task-terminal-effects.ts +21 -17
- package/src/tests/apps-card-grid.test.ts +197 -0
- package/src/tests/apps-catalog-additions.test.ts +120 -0
- package/src/tests/apps-tree.test.ts +129 -0
- package/src/tests/bitbucket-auth.test.ts +156 -0
- package/src/tests/bitbucket-handlers.test.ts +425 -0
- package/src/tests/bitbucket-webhook.test.ts +220 -0
- package/src/tests/chat-sdk-attachments-reply.test.ts +52 -0
- package/src/tests/chat-sdk-config.test.ts +11 -0
- package/src/tests/chat-sdk-facts.test.ts +23 -0
- package/src/tests/chat-sdk-inbound.test.ts +597 -4
- package/src/tests/chat-sdk-outbound-files.test.ts +652 -0
- package/src/tests/chat-sdk-telegram-markdown.test.ts +92 -0
- package/src/tests/codex-adapter.test.ts +167 -242
- package/src/tests/config-api-only-keys.test.ts +9 -0
- package/src/tests/context-key.test.ts +50 -0
- package/src/tests/core-auth.test.ts +5 -0
- package/src/tests/cors-allowlist.test.ts +1 -1
- package/src/tests/db-task-read-contract.test.ts +7 -0
- package/src/tests/db-task-write-contract.test.ts +6 -0
- package/src/tests/docs-index-footprint.test.js +3 -3
- package/src/tests/extensions-example-slack-reply-gate.test.ts +262 -91
- package/src/tests/fixtures/bitbucket/pullrequest-comment-created-inline.json +174 -0
- package/src/tests/fixtures/bitbucket/pullrequest-comment-created.json +170 -0
- package/src/tests/fixtures/bitbucket/pullrequest-created-mention.json +132 -0
- package/src/tests/fixtures/bitbucket/pullrequest-created-reviewer.json +140 -0
- package/src/tests/fixtures/bitbucket/pullrequest-fulfilled.json +134 -0
- package/src/tests/fixtures/bitbucket/pullrequest-rejected.json +133 -0
- package/src/tests/generate-mcp-docs.test.ts +101 -0
- package/src/tests/gitlab-handlers.test.ts +3 -3
- package/src/tests/gitlab-vcs-db.test.ts +4 -4
- package/src/tests/heartbeat-checklist.test.ts +33 -0
- package/src/tests/heartbeat-jev-gate-credentials.test.ts +44 -0
- package/src/tests/heartbeat-jev-gate.test.ts +242 -1
- package/src/tests/integration-disable-flags.test.ts +36 -0
- package/src/tests/kv-page-proxy.test.ts +91 -5
- package/src/tests/launch-password-rejection.test.ts +2 -2
- package/src/tests/mcp-input-caps.test.ts +294 -0
- package/src/tests/mcp-tools.test.ts +1 -1
- package/src/tests/memory-chunked-search-starvation.test.ts +200 -0
- package/src/tests/memory-embedding-input-cap.test.ts +114 -0
- package/src/tests/memory.test.ts +14 -0
- package/src/tests/onboarding.test.ts +12 -0
- package/src/tests/page-guest-session.test.ts +19 -8
- package/src/tests/page-proxy-authed.test.ts +1 -1
- package/src/tests/page-proxy.test.ts +17 -10
- package/src/tests/page-session.test.ts +102 -1
- package/src/tests/pages-authed-mode.test.ts +82 -12
- package/src/tests/pages-password-mode.test.ts +5 -5
- package/src/tests/pi-codemode.test.ts +8 -2
- package/src/tests/preload-env.test.ts +57 -0
- package/src/tests/preload.ts +21 -0
- package/src/tests/prompt-template-remaining.test.ts +38 -0
- package/src/tests/realtime-transport.test.ts +13 -7
- package/src/tests/run-root-coverage.test.ts +39 -0
- package/src/tests/runner-bitbucket-vcs.test.ts +191 -0
- package/src/tests/runner-skills-refresh.test.ts +58 -0
- package/src/tests/script-runs-http.test.ts +84 -0
- package/src/tests/scripts-http.test.ts +10 -1
- package/src/tests/secret-scrubber.test.ts +15 -0
- package/src/tests/sessions-move.test.ts +576 -0
- package/src/tests/sessions-tree.test.ts +493 -0
- package/src/tests/skill-files-http.test.ts +50 -0
- package/src/tests/skills-bundle.test.ts +99 -0
- package/src/tests/swarm-config-export-to-env.test.ts +23 -3
- package/src/tests/task-audio-transcription.test.ts +299 -0
- package/src/tests/task-completion-memory-chunking.test.ts +434 -0
- package/src/tests/task-terminal-secret-scrub.test.ts +6 -1
- package/src/tests/transcription.test.ts +299 -0
- package/src/tests/vcs-bitbucket.test.ts +91 -0
- package/src/tests/vcs-provider-isolation.test.ts +123 -0
- package/src/tests/vcs-provider.test.ts +16 -1
- package/src/tests/vcs-tracking.test.ts +3 -3
- package/src/tests/workflow-foreach-bodies.test.ts +637 -0
- package/src/tests/workflow-foreach-concurrency.test.ts +342 -0
- package/src/tests/workflow-foreach-retry.test.ts +537 -0
- package/src/tests/workflow-foreach.test.ts +44 -23
- package/src/tests/workflow-step-claims.test.ts +6 -6
- package/src/tests/workflow-system-one-decision-providers.test.ts +98 -1
- package/src/tests/workflow-system-one-decision.test.ts +2 -1
- package/src/tests/workflow-tla-races.test.ts +2 -2
- package/src/tools/chat-access.ts +10 -0
- package/src/tools/chat-post.ts +31 -11
- package/src/tools/chat-reply.ts +32 -10
- package/src/tools/memory-search.ts +3 -1
- package/src/tools/script-delete.ts +1 -1
- package/src/tools/system-one-decide.ts +17 -4
- package/src/tools/workflows/create-workflow.ts +5 -3
- package/src/types.ts +9 -2
- package/src/utils/config-env-export.ts +4 -1
- package/src/utils/page-session.ts +133 -20
- package/src/utils/secret-scrubber.ts +4 -0
- package/src/utils/skills-refresh.ts +83 -43
- package/src/utils/transcription.ts +246 -0
- package/src/vcs/bitbucket.ts +58 -0
- package/src/vcs/index.ts +2 -0
- package/src/vcs/types.ts +2 -2
- package/src/workflows/approval-resolution.ts +4 -1
- package/src/workflows/checkpoint.ts +9 -1
- package/src/workflows/constants.ts +3 -0
- package/src/workflows/definition.ts +48 -17
- package/src/workflows/engine.ts +25 -15
- package/src/workflows/executors/foreach.ts +45 -99
- package/src/workflows/executors/system-one-decision.ts +28 -7
- package/src/workflows/executors/system-one-providers.ts +39 -4
- package/src/workflows/foreach-body.ts +146 -0
- package/src/workflows/foreach-driver.ts +552 -0
- package/src/workflows/foreach-join.ts +206 -7
- package/src/workflows/node-timeout.ts +21 -0
- package/src/workflows/recovery.ts +68 -3
- package/src/workflows/resume.ts +221 -43
- package/src/workflows/retry-poller.ts +39 -24
- package/src/workflows/step-input.ts +59 -0
- package/src/workflows/task-step-routing.ts +114 -56
- package/templates/extensions/slack-reply-gate/README.md +24 -22
- package/templates/extensions/slack-reply-gate/hooks.ts +146 -95
- package/templates/extensions/slack-reply-gate/manifest.json +2 -2
- package/templates/skills/apps/SKILL.md +28 -6
- package/templates/skills/apps/content.md +28 -6
- package/templates/skills/chat-sdk/content.md +6 -2
- package/templates/skills/code-quality/content.md +2 -1
- package/templates/skills/heartbeat-runbook/content.md +5 -5
- package/templates/skills/pages/content.md +6 -4
- package/templates/skills/slack-interaction/content.md +5 -0
- package/templates/skills/swarm-scripts/SKILL.md +2 -2
- package/templates/skills/swarm-scripts/content.md +2 -2
- package/templates/skills/work-on-task/SKILL.md +1 -1
- package/templates/skills/work-on-task/content.md +1 -1
- package/dist/cli-51y09c6p.js +0 -2657
- package/dist/cli-yw91rnmw.js +0 -13
- package/dist/handlers-0pvee9nw.js +0 -39
- package/dist/index-gdh8jrdm.js +0 -43
- package/dist/index-hg0x2cdv.js +0 -40
- package/dist/index-thm0y6a8.js +0 -41
- package/dist/lead-ee8vazz2.js +0 -77
- package/dist/worker-16s75hm5.js +0 -77
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
extractArgsJsonSchema,
|
|
6
6
|
typecheckScript,
|
|
7
7
|
parseSkillContent
|
|
8
|
-
} from "./cli-
|
|
8
|
+
} from "./cli-we0ra057.js";
|
|
9
9
|
import {
|
|
10
10
|
getDbClient2,
|
|
11
11
|
computeContentHash2,
|
|
@@ -26,26 +26,28 @@ import {
|
|
|
26
26
|
deleteSkill2,
|
|
27
27
|
getSkillByName2,
|
|
28
28
|
init_db3
|
|
29
|
-
} from "./cli-
|
|
29
|
+
} from "./cli-fcc5qe3f.js";
|
|
30
30
|
import"./cli-3ggwdtb0.js";
|
|
31
|
-
import"./cli-
|
|
31
|
+
import"./cli-s4sntwt4.js";
|
|
32
32
|
import {
|
|
33
33
|
agentFsProvisionSeeder
|
|
34
|
-
} from "./cli-
|
|
35
|
-
import"./cli-
|
|
36
|
-
import"./cli-
|
|
34
|
+
} from "./cli-1pr672hz.js";
|
|
35
|
+
import"./cli-fy1jyyym.js";
|
|
36
|
+
import"./cli-ftmq4nvk.js";
|
|
37
37
|
import {
|
|
38
38
|
validateDefinition
|
|
39
|
-
} from "./cli-
|
|
40
|
-
import"./cli-
|
|
39
|
+
} from "./cli-92mqenkc.js";
|
|
40
|
+
import"./cli-qqzmjxkf.js";
|
|
41
|
+
import"./cli-2nf3b08t.js";
|
|
41
42
|
import"./cli-kvzgak7c.js";
|
|
42
43
|
import"./cli-445c1vp7.js";
|
|
43
|
-
import"./cli-
|
|
44
|
+
import"./cli-kxpfx2xr.js";
|
|
44
45
|
import {
|
|
45
46
|
buildSkillContent
|
|
46
47
|
} from "./cli-691vz9mk.js";
|
|
47
48
|
import"./cli-13p25grh.js";
|
|
48
49
|
import"./cli-0adyhvq2.js";
|
|
50
|
+
import"./cli-rycdaj0a.js";
|
|
49
51
|
import {
|
|
50
52
|
__toESM
|
|
51
53
|
} from "./cli-242037k3.js";
|
|
@@ -2568,11 +2570,15 @@ export const argsSchema = z.object({
|
|
|
2568
2570
|
.min(0)
|
|
2569
2571
|
.max(1)
|
|
2570
2572
|
.optional()
|
|
2571
|
-
.describe(
|
|
2573
|
+
.describe(
|
|
2574
|
+
"Quiet when the model's p(needs action) is below this (default: the provider's measured threshold, 0.5 for Jev, 0.2 for Clef)",
|
|
2575
|
+
),
|
|
2572
2576
|
provider: z
|
|
2573
|
-
.enum(["auto", "typesafe", "openrouter"])
|
|
2577
|
+
.enum(["auto", "typesafe", "openrouter", "cloudflare"])
|
|
2574
2578
|
.optional()
|
|
2575
|
-
.describe(
|
|
2579
|
+
.describe(
|
|
2580
|
+
"Decision host: typesafe or openrouter (Jev), cloudflare (Clef). auto tries them in that order (default auto)",
|
|
2581
|
+
),
|
|
2576
2582
|
timeZone: z
|
|
2577
2583
|
.string()
|
|
2578
2584
|
.optional()
|
|
@@ -2583,25 +2589,56 @@ export const argsSchema = z.object({
|
|
|
2583
2589
|
.min(1_000)
|
|
2584
2590
|
.max(45_000)
|
|
2585
2591
|
.optional()
|
|
2586
|
-
.describe(
|
|
2592
|
+
.describe(
|
|
2593
|
+
"Abandon the decision call after this many ms (default 25000); the gate deadline is 60 s",
|
|
2594
|
+
),
|
|
2587
2595
|
});
|
|
2588
2596
|
|
|
2589
2597
|
const KV_NAMESPACE = "heartbeat-jev-gate";
|
|
2590
2598
|
const KV_KEY = "setup";
|
|
2591
|
-
//
|
|
2592
|
-
const
|
|
2593
|
-
|
|
2594
|
-
//
|
|
2595
|
-
//
|
|
2599
|
+
// Jev's cost, measured per decision in the 2026-10-15 heartbeat replay (229 states).
|
|
2600
|
+
const JEV_COST_PER_DECISION = "$0.00004";
|
|
2601
|
+
// Clef replayed on the same 229 states and labels (2026-10-20): 0.2 is the highest
|
|
2602
|
+
// threshold that misses none of the 9 ticks where the Lead acted (57 quiet ticks;
|
|
2603
|
+
// Jev at 0.5 has 100). Above 0.21 Clef starts missing real asks.
|
|
2604
|
+
const CLEF_THRESHOLD = 0.2;
|
|
2605
|
+
|
|
2606
|
+
// The model is called through \`ctx.swarm.systemOne_decide\`: the API server reads
|
|
2607
|
+
// the key and sends the request, so no key ever enters this script's realm. The
|
|
2596
2608
|
// server also refuses OpenRouter when the effective OPENROUTER_BASE_URL (stored
|
|
2597
|
-
// config or deployment env) points at a gateway
|
|
2609
|
+
// config or deployment env) points at a gateway; \`auto\` then tries the next
|
|
2610
|
+
// usable provider, since this script cannot read the deployment env.
|
|
2611
|
+
//
|
|
2612
|
+
// \`credentials\` lists what must be set: every group needs one of its keys.
|
|
2613
|
+
// \`threshold\` is the default the provider's own replay measured (see \`CLEF_THRESHOLD\`).
|
|
2598
2614
|
const PROVIDERS = {
|
|
2599
|
-
typesafe: {
|
|
2600
|
-
|
|
2615
|
+
typesafe: {
|
|
2616
|
+
model: "jev-latest",
|
|
2617
|
+
family: "jev",
|
|
2618
|
+
threshold: 0.5,
|
|
2619
|
+
credentials: [["TYPESAFE_API_KEY"]],
|
|
2620
|
+
setup: "\`TYPESAFE_API_KEY\` (Jev on TypeSafe)",
|
|
2621
|
+
},
|
|
2622
|
+
openrouter: {
|
|
2623
|
+
model: "~typesafe/jev-latest",
|
|
2624
|
+
family: "jev",
|
|
2625
|
+
threshold: 0.5,
|
|
2626
|
+
credentials: [["OPENROUTER_API_KEY"]],
|
|
2627
|
+
setup: "\`OPENROUTER_API_KEY\` (Jev on OpenRouter)",
|
|
2628
|
+
},
|
|
2629
|
+
cloudflare: {
|
|
2630
|
+
model: "clef",
|
|
2631
|
+
family: "clef",
|
|
2632
|
+
threshold: CLEF_THRESHOLD,
|
|
2633
|
+
credentials: [["CLOUDFLARE_API_TOKEN", "CLOUDFLARE_API_KEY"], ["CLOUDFLARE_ACCOUNT_ID"]],
|
|
2634
|
+
setup:
|
|
2635
|
+
"\`CLOUDFLARE_API_TOKEN\` (or \`CLOUDFLARE_API_KEY\`) as a secret plus the \`CLOUDFLARE_ACCOUNT_ID\` config value (Clef on Cloudflare Workers AI)",
|
|
2636
|
+
},
|
|
2601
2637
|
} as const;
|
|
2602
2638
|
type ProviderId = keyof typeof PROVIDERS;
|
|
2639
|
+
const AUTO_ORDER: ProviderId[] = ["typesafe", "openrouter", "cloudflare"];
|
|
2603
2640
|
|
|
2604
|
-
// Worded exactly as in the
|
|
2641
|
+
// Worded exactly as in the replays that set the thresholds; changing it voids those results.
|
|
2605
2642
|
const QUESTIONS = {
|
|
2606
2643
|
act: {
|
|
2607
2644
|
type: "noul",
|
|
@@ -2784,21 +2821,25 @@ async function buildState(ctx: any, leadId: string, nowMs: number, timeZone: str
|
|
|
2784
2821
|
};
|
|
2785
2822
|
}
|
|
2786
2823
|
|
|
2787
|
-
function setupPrompt(
|
|
2824
|
+
function setupPrompt(candidates: ProviderId[]) {
|
|
2825
|
+
const options = candidates.map((id) => \`- \${PROVIDERS[id].setup}\`);
|
|
2788
2826
|
return {
|
|
2789
|
-
title: "Heartbeat
|
|
2827
|
+
title: "Heartbeat gate: add a decision model credential or decline",
|
|
2790
2828
|
detached: true,
|
|
2791
2829
|
questions: [
|
|
2792
2830
|
{
|
|
2793
2831
|
id: "enable",
|
|
2794
2832
|
type: "approval",
|
|
2795
|
-
label: "Enable the
|
|
2833
|
+
label: "Enable the heartbeat checklist gate?",
|
|
2796
2834
|
description: [
|
|
2797
|
-
"Before each recurring heartbeat checklist, the \`heartbeat-jev-gate\` script sends a short summary of swarm state (about 1,500 characters: new failures, due tracked items, task and agent counts) to
|
|
2835
|
+
"Before each recurring heartbeat checklist, the \`heartbeat-jev-gate\` script sends a short summary of swarm state (about 1,500 characters: new failures, due tracked items, task and agent counts) to a small SystemOne decision model (Jev by TypeSafe, or Clef on Cloudflare Workers AI) and asks whether the Lead needs to act. With \`HEARTBEAT_CHECKLIST_GATE_MODE=enforce\`, quiet ticks skip the Lead's checklist task.",
|
|
2798
2836
|
"",
|
|
2799
|
-
\`Cost:
|
|
2837
|
+
\`Cost: one decision per checklist tick, about \${JEV_COST_PER_DECISION} each with Jev.\`,
|
|
2800
2838
|
"",
|
|
2801
|
-
|
|
2839
|
+
"No credential for the gate is configured. To enable, approve and add one of these as a global value on the Secrets page (Settings > Secrets):",
|
|
2840
|
+
...options,
|
|
2841
|
+
"",
|
|
2842
|
+
"Do not paste a key here. The next tick picks it up.",
|
|
2802
2843
|
"",
|
|
2803
2844
|
"Reject to decline. The gate then stays inactive, with no further prompts, until a key is added.",
|
|
2804
2845
|
].join("\\n"),
|
|
@@ -2808,9 +2849,10 @@ function setupPrompt(keys: string[]) {
|
|
|
2808
2849
|
}
|
|
2809
2850
|
|
|
2810
2851
|
/**
|
|
2811
|
-
* Heartbeat checklist gate backed by
|
|
2812
|
-
* tick needs the Lead and returns
|
|
2813
|
-
* credential it asks a human once, then
|
|
2852
|
+
* Heartbeat checklist gate backed by a SystemOne decision model (Jev or Clef):
|
|
2853
|
+
* asks whether a recurring checklist tick needs the Lead and returns
|
|
2854
|
+
* { quiet, reason, summary }. With no credential it asks a human once, then
|
|
2855
|
+
* stays a no-op until a key is added.
|
|
2814
2856
|
*/
|
|
2815
2857
|
export default async function heartbeatJevGate(args: any, ctx: any) {
|
|
2816
2858
|
const parsed = argsSchema.safeParse(args ?? {});
|
|
@@ -2831,25 +2873,21 @@ export default async function heartbeatJevGate(args: any, ctx: any) {
|
|
|
2831
2873
|
};
|
|
2832
2874
|
|
|
2833
2875
|
const cfgThreshold = Number.parseFloat(cfg("HEARTBEAT_JEV_GATE_THRESHOLD"));
|
|
2834
|
-
const threshold =
|
|
2835
|
-
a.threshold ?? (cfgThreshold >= 0 && cfgThreshold <= 1 ? cfgThreshold : 0.5);
|
|
2836
2876
|
const providerPref = a.provider ?? (cfg("HEARTBEAT_JEV_GATE_PROVIDER").toLowerCase() || "auto");
|
|
2837
2877
|
const timeZone = a.timeZone ?? (cfg("HEARTBEAT_JEV_GATE_TIMEZONE") || "UTC");
|
|
2838
2878
|
|
|
2839
|
-
const usable:
|
|
2840
|
-
|
|
2841
|
-
openrouter: cfg(PROVIDERS.openrouter.keyName) !== "",
|
|
2842
|
-
};
|
|
2879
|
+
const usable = (id: ProviderId) =>
|
|
2880
|
+
PROVIDERS[id].credentials.every((group) => group.some((key) => cfg(key) !== ""));
|
|
2843
2881
|
const candidates: ProviderId[] =
|
|
2844
|
-
providerPref
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2882
|
+
Object.hasOwn(PROVIDERS, providerPref) ? [providerPref as ProviderId] : AUTO_ORDER;
|
|
2883
|
+
const usableCandidates = candidates.filter(usable);
|
|
2884
|
+
|
|
2885
|
+
if (usableCandidates.length === 0) {
|
|
2886
|
+
const keys = candidates.map((id) =>
|
|
2887
|
+
PROVIDERS[id].credentials
|
|
2888
|
+
.map((group) => (group.length > 1 ? \`(\${group.join(" or ")})\` : group[0]))
|
|
2889
|
+
.join(" + "),
|
|
2890
|
+
);
|
|
2853
2891
|
const inactive = (state: string, extra: Record<string, unknown> = {}) => ({
|
|
2854
2892
|
quiet: false,
|
|
2855
2893
|
reason: \`jev-gate inactive: \${state}\`.slice(0, 300),
|
|
@@ -2860,7 +2898,7 @@ export default async function heartbeatJevGate(args: any, ctx: any) {
|
|
|
2860
2898
|
let setup: SetupState | null = typeof stored === "string" ? JSON.parse(stored) : (stored ?? null);
|
|
2861
2899
|
|
|
2862
2900
|
if (!setup) {
|
|
2863
|
-
const prompt = setupPrompt(
|
|
2901
|
+
const prompt = setupPrompt(candidates);
|
|
2864
2902
|
if (dryRun) {
|
|
2865
2903
|
const state = await buildState(ctx, leadId, Date.now(), timeZone);
|
|
2866
2904
|
return inactive("no credential, would ask a human", { prompt, state });
|
|
@@ -2904,35 +2942,51 @@ export default async function heartbeatJevGate(args: any, ctx: any) {
|
|
|
2904
2942
|
return inactive(why, { setup });
|
|
2905
2943
|
}
|
|
2906
2944
|
|
|
2907
|
-
const target = PROVIDERS[provider];
|
|
2908
2945
|
const state = await buildState(ctx, leadId, Date.now(), timeZone);
|
|
2909
2946
|
|
|
2910
|
-
// The server enforces the time budget. No retries, as in the replay.
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2947
|
+
// The server enforces the time budget. No retries, as in the replay. A provider
|
|
2948
|
+
// this deployment refuses (OpenRouter behind a gateway) sends nothing, so the
|
|
2949
|
+
// next usable one is tried; only an explicit choice or the last one throws.
|
|
2950
|
+
let provider = usableCandidates[0] as ProviderId;
|
|
2951
|
+
let body: any;
|
|
2952
|
+
const skipped: string[] = [];
|
|
2953
|
+
for (const [i, id] of usableCandidates.entries()) {
|
|
2954
|
+
provider = id;
|
|
2955
|
+
const res: any = await ctx.swarm.systemOne_decide({
|
|
2956
|
+
provider: id,
|
|
2957
|
+
model: PROVIDERS[id].model,
|
|
2958
|
+
state,
|
|
2959
|
+
questions: QUESTIONS,
|
|
2960
|
+
timeoutMs: a.jevTimeoutMs ?? 25_000,
|
|
2961
|
+
maxRetries: 0,
|
|
2962
|
+
});
|
|
2963
|
+
body = res?.data ?? res;
|
|
2964
|
+
if (res?.success !== false && body?.success !== false) break;
|
|
2965
|
+
const why = String(body?.message ?? body?.error ?? \`HTTP \${res?.status}\`).slice(0, 250);
|
|
2966
|
+
if (body?.providerUnavailable === true && i < usableCandidates.length - 1) {
|
|
2967
|
+
skipped.push(\`\${id}: \${why}\`);
|
|
2968
|
+
continue;
|
|
2969
|
+
}
|
|
2970
|
+
throw new Error(\`\${PROVIDERS[id].family} \${id}: \${why}\`);
|
|
2922
2971
|
}
|
|
2972
|
+
const target = PROVIDERS[provider];
|
|
2973
|
+
// A configured threshold applies to every provider; unset, each uses its measured one.
|
|
2974
|
+
const threshold =
|
|
2975
|
+
a.threshold ?? (cfgThreshold >= 0 && cfgThreshold <= 1 ? cfgThreshold : target.threshold);
|
|
2923
2976
|
const p = body?.answers?.act?.noul;
|
|
2924
2977
|
if (typeof p !== "number" || !Number.isFinite(p)) {
|
|
2925
|
-
throw new Error(
|
|
2978
|
+
throw new Error(\`\${target.family} \${provider}: response has no numeric answers.act.noul\`);
|
|
2926
2979
|
}
|
|
2927
2980
|
|
|
2928
2981
|
const pText = p.toFixed(3);
|
|
2929
2982
|
const quiet = p < threshold;
|
|
2930
2983
|
const verdict: Record<string, unknown> = {
|
|
2931
2984
|
quiet,
|
|
2932
|
-
reason:
|
|
2985
|
+
reason: \`\${target.family} p(needs action)=\${pText} \${quiet ? "<" : ">="} \${threshold} via \${provider}\`,
|
|
2933
2986
|
};
|
|
2934
2987
|
if (!quiet) {
|
|
2935
|
-
const
|
|
2988
|
+
const model = target.family === "jev" ? "Jev" : "Clef";
|
|
2989
|
+
const lines = [\`\${model} p(needs action) = \${pText} (threshold \${threshold}, \${provider}).\`];
|
|
2936
2990
|
const failures = state.new_failures_since_previous_heartbeat;
|
|
2937
2991
|
const tracked = state.tracked_items_mentioning_today_or_past_delete_date;
|
|
2938
2992
|
if (Array.isArray(failures)) {
|
|
@@ -2942,11 +2996,11 @@ export default async function heartbeatJevGate(args: any, ctx: any) {
|
|
|
2942
2996
|
lines.push("", "Tracked items due today or past their DELETE date:", ...tracked.map((t) => \`- \${t.replace(/^[-*] /, "")}\`));
|
|
2943
2997
|
}
|
|
2944
2998
|
if (!Array.isArray(failures) && !Array.isArray(tracked)) {
|
|
2945
|
-
lines.push("",
|
|
2999
|
+
lines.push("", \`No new failures or due tracked items; \${model} flagged the overall state.\`);
|
|
2946
3000
|
}
|
|
2947
3001
|
verdict.summary = lines.join("\\n").slice(0, 4000);
|
|
2948
3002
|
}
|
|
2949
|
-
return dryRun ? { ...verdict, dryRun: true, provider, p, threshold, state } : verdict;
|
|
3003
|
+
return dryRun ? { ...verdict, dryRun: true, provider, skipped, p, threshold, state } : verdict;
|
|
2950
3004
|
}
|
|
2951
3005
|
`;
|
|
2952
3006
|
|
|
@@ -5904,8 +5958,8 @@ var SEED_SCRIPTS = [
|
|
|
5904
5958
|
},
|
|
5905
5959
|
{
|
|
5906
5960
|
name: "heartbeat-jev-gate",
|
|
5907
|
-
description: "Heartbeat checklist gate backed by Jev
|
|
5908
|
-
intent: "Skip quiet Lead heartbeat checklist ticks with a cheap Jev decision; with no
|
|
5961
|
+
description: "Heartbeat checklist gate backed by a SystemOne decision model (Jev on TypeSafe or OpenRouter, Clef on Cloudflare Workers AI): scores a compact swarm state and returns {quiet, reason, summary} for HEARTBEAT_CHECKLIST_GATE_SCRIPT.",
|
|
5962
|
+
intent: "Skip quiet Lead heartbeat checklist ticks with a cheap Jev or Clef decision; with no credential it asks a human once, then stays a no-op until a key is added.",
|
|
5909
5963
|
source: asText(heartbeat_jev_gate_default)
|
|
5910
5964
|
},
|
|
5911
5965
|
{
|
|
@@ -6129,7 +6183,7 @@ var config_default2 = `{
|
|
|
6129
6183
|
`;
|
|
6130
6184
|
|
|
6131
6185
|
// templates/skills/apps/content.md
|
|
6132
|
-
var content_default2 = "# Swarm Apps\n\nSwarm Apps are persistent, agent-authored internal applications: a schema-backed row store, named queries, custom script/task actions, and a validated json-render interface served at `/apps/<id>`. Use an app when people need to view and change live records; use `create_page` for a static snapshot.\n\n> **Capability gate**: app tools are available when the server's `CAPABILITIES` includes `pages` (for example `CAPABILITIES=core,task-pool,pages`). There is no separate `apps` capability. If these tools are absent from your MCP list, this is why.\n\n## Tools and iteration loop\n\n| Tool | Use it for |\n|---|---|\n| `app-list` | Discover apps by id, name, description, and timestamps. Definitions are omitted. |\n| `app-get` | Read one complete stored definition before changing it. |\n| `app-upsert` | Create an app, or replace an existing app's entire definition when you intentionally have the full desired state. |\n| `app-patch` | Make a focused change while preserving everything not mentioned in the patch. Prefer this for iteration. |\n| `app-query` | Run one declared named query and return its rows. Use this when an agent or saved script needs to consume app data rather than render the app UI. |\n| `app-rows-list` | List rows with typed equality filters, sorting, and a limit. Returns `{ rows, total }`. |\n| `app-row-get` | Read a row by `rowId`. Returns `{ row }`. |\n| `app-row-create` | Create a row from `values`. Returns `{ row }`. |\n| `app-rows-create` | Create 1 to 500 rows from a `rows: values[]` array, within 10 MiB. Returns `{ rows }`. |\n| `app-row-patch` | Patch a row by `rowId` with `values`. Returns `{ row }`. |\n| `app-row-delete` | Delete a row by `rowId`. Returns `{ ok: true }`. |\n| `app-rows-delete` | Delete matching rows with a non-empty `equals` filter. Returns `{ deleted }`. |\n| `app-action-run` | Run a declared script, task, or sync action with `{ appId, action, input? }`. Agents can invoke it through MCP only. |\n| `app-history` | List definition snapshots and their compact model/column digests before choosing a restore point. |\n| `app-diff` | Show a unified definition diff; defaults to newest snapshot → CURRENT. |\n| `app-rollback` | Restore a snapshot through the normal schema-migration engine. |\n\nFor every edit: `app-get` -> modify the smallest coherent subtree -> `app-patch` -> if rejected, fix every returned `issues[]` entry (`path` + `message`) and retry. Validation happens before storage, so a rejected update leaves the saved app unchanged. Do not guess at the current definition or use `app-upsert` with a partial definition.\n\n`app-patch` can also change `name`; omit `description` to preserve it or pass `description: null` to clear it.\n\n### Reading an app's callable surface\n\nOne `app-get` returns everything needed to drive an app you did not write: `definition.queries` are runnable with `app-query` (supply every `$param` the query references), and `definition.actions` are invocable with `app-action-run` or `POST /api/apps/<id>/actions/<name>` (a `script` action runs its script, a `task` action spawns a task, a `sync` action refreshes sources). Read the query parameters and action arguments from the definition before invocation. Do not guess parameter names.\n\n### Verified action caller\n\nScript actions receive `caller` as a reserved, server-stamped argument. The server overwrites `caller` from definition defaults and request input.\n\n```typescript\ntype AppActionCaller = {\n kind: \"user\" | \"agent\" | \"operator\" | \"guest\";\n id: string | null;\n name?: string;\n pageId?: string;\n};\n```\n\nThe caller identifies the authenticated user or agent. Operator and guest callers have `id: null`. A signed page session adds `pageId`.\nWith the swarm key, a registered `X-Agent-ID` produces an agent caller and `agent:<id>` row provenance. Missing or unknown agent IDs remain operator callers.\nTask actions include the same caller as server-verified JSON in the task prompt. Script execution still uses the saved script owner's bindings.\n\n### Verified action caller\n\nScript actions receive `caller` as a reserved, server-stamped argument. The server overwrites `caller` from definition defaults and request input.\n\n```typescript\ntype AppActionCaller = {\n kind: \"user\" | \"agent\" | \"operator\" | \"guest\";\n id: string | null;\n name?: string;\n pageId?: string;\n};\n```\n\nThe caller identifies the authenticated user or agent. Operator and guest callers have `id: null`. A signed page session adds `pageId`.\nWith the swarm key, a registered `X-Agent-ID` produces an agent caller and `agent:<id>` row provenance. Missing or unknown agent IDs remain operator callers.\nTask actions include the same caller as server-verified JSON in the task prompt. Script execution still uses the saved script owner's bindings.\n\n## Safe schema evolution and rollback\n\nPrefer **hiding** a column over deleting it. A hidden column's values stay on every existing row, so an unhide or rollback can make it visible again. Hiding is metadata-only: hidden columns cannot be used in queries, page bindings, `app.mutate` values, or new row writes, and `required` is ignored while hidden. Row GETs still expose the historical value for backward compatibility.\n\nDo not reuse a hidden column name. The name remains held until you either unhide it or explicitly purge its stored values. To permanently remove a column, delete its declaration with `null` and include `{ \"columnName\": { \"purge\": true } }` in the same `migration` object. That is deliberately one-shot and irreversible for row data.\n\nSchema-changing writes can carry this sibling `migration` object:\n\n```json\n{\n \"migration\": {\n \"priority\": { \"from\": \"flag\", \"map\": { \"urgent\": \"high\", \"watch\": \"low\" }, \"else\": \"none\" },\n \"status\": { \"from\": \"flag\", \"map\": { \"watch\": \"watching\", \"done\": \"done\" }, \"else\": \"open\" },\n \"oldColumn\": { \"purge\": true },\n \"count\": { \"coerce\": true, \"else\": 0 },\n \"owner\": { \"set\": \"unassigned\" }\n }\n}\n```\n\nUse `set` to backfill a changed column, `from` with optional `map`/`else` to derive it from one existing (including hidden) column, `coerce` for a kind change, and `purge` only for explicit destruction. Non-purge directives target a column changed in that same write. The server fails loudly before writing when a lossy change needs a directive: inspect every returned `issues[]` item, including its per-value row counts, then choose an explicit policy and retry. Never guess a migration just to get a 200.\n\n**Source lifecycle** rides the same engine with the same all-or-nothing guarantees. Free: adding a source, adding a NEW column with a `source` binding, binding an existing column that holds zero values, changing a binding's `field`/`transform`, and changing a source's `args`/`config`/`connection`/`scriptId` (window changes churn staleness on the next pass — expect `stale` flips when narrowing). Rejected with a path-bearing issue: changing `joinKey` (immutable — remove the source and add it again), changing `connector` while the source owns rows (row count in the message), and binding an existing column that already holds values (purge it — or add a fresh column — and bind that instead; a sync pass would otherwise overwrite the data). Removing a source **detaches** its rows: values survive as ordinary columns, the `source`/`syncedAt`/`stale` envelope is stripped, and the report counts them in `detachedRows`. A patch removing a source must drop its column bindings in the same call — a dangling `source.of` is rejected before anything is written.\n\nSync never adopts rows it does not own: a pulled record whose join key matches an unowned row creates a second, source-owned row. Adding a source over a join-key column that already holds values therefore duplicates those records on the first pass — and remove-source → re-add duplicates for the same reason, because detached rows keep their values. To convert a hand-maintained model into a synced one, purge the old rows first or use a fresh join-key column.\n\nFor example, split a legacy `flag` into `priority` and `status` without losing data in one patch: add the two columns, map both from `flag`, update every affected query/page binding, then hide `flag`.\n\n```json\n{\n \"appId\": \"<app-id>\",\n \"definition\": {\n \"models\": {\n \"ticket\": {\n \"columns\": {\n \"priority\": { \"kind\": \"enum\", \"enum\": [\"none\", \"low\", \"high\"] },\n \"status\": { \"kind\": \"enum\", \"enum\": [\"open\", \"watching\", \"done\"] },\n \"flag\": { \"kind\": \"string\", \"hidden\": true }\n }\n }\n }\n },\n \"migration\": {\n \"priority\": { \"from\": \"flag\", \"map\": { \"urgent\": \"high\", \"watch\": \"low\" }, \"else\": \"none\" },\n \"status\": { \"from\": \"flag\", \"map\": { \"watch\": \"watching\", \"done\": \"done\" }, \"else\": \"open\" }\n }\n}\n```\n\nEvery schema migration returns `{ scanned, backfilled, coerced, mapped, elsed, purgedValues, idxRebuilt, detachedRows, orphanFields }`. Read `orphanFields`: it reports extra row fields that predate or no longer appear in the definition; they are preserved until an explicit purge. `schemaVersion` is server-managed: never set it in an input patch or try to use it as application data.\n\nEvery successful definition write snapshots the previous definition. Use `app-history` to select a version, `app-diff({ appId, from: <version> })` to inspect it against CURRENT, then `app-rollback({ appId, version: <version>, migration? })` to restore it. Rollback is a forward migration over live rows, not row-level time travel: it creates a new pre-rollback snapshot and may reject a lossy restore with the same migration directives and counts. A rejected rollback changes nothing.\n\n### Per-user configuration\n\n`userConfig` declares up to 20 typed settings in the versioned definition; its values are deliberately stored outside the definition, separately for each user (and the operator dashboard). Each field is `{ \"kind\": \"string\" | \"number\" | \"boolean\" | \"date\" | \"enum\", \"default\"?: ..., \"enum\"?: [...], \"label\"?: \"...\" }`. Defaults follow column rules, including enum membership; `required` is not allowed. A missing or obsolete stored value reads as its default, or `null` when no default is declared.\n\nUse `{ \"$state\": \"/user/<field>\" }` on a page for the read-only current-user binding; the field must be declared and the path cannot continue below it. Pure and bound reusable elements cannot bind `/user` directly — pass the value through an element prop from the consuming page. An agent acting on an owned user-requested task reads and edits that requester's preferences. `userConfig` schema changes are always migration-compatible and appear in migration reports; no migration directives are needed. Rollback restores the historical schema but never alters saved per-user values, so values can resurface when a field returns.\n\n### Theming\n\nAn optional top-level `\"theme\"` sets the preset the dashboard applies to the app's rendered canvas — a lowercase slug validated by shape only. The preset catalog ships with the dashboard (`apps/ui/src/lib/themes.ts` + `theme-classics.ts` — update this list and the `app-upsert` tool description when it changes); current ids: `hive` (the stock look), the accent/field presets `meadow`, `iris`, `rose`, `cobalt`, `ember`, `carbon`, and the classic editor/platform presets `github`, `vscode`, `material`, `solarized`, `tokyo`, `monokai`, `gruvbox`, `catppuccin`. Action and destructive tones are never themed; status tones are themed ONLY by the classic presets (their identity includes a status palette — hue semantics stay fixed, success reads green and error reads red everywhere). An id the viewer's dashboard build does not know degrades to the dashboard's own theme, so unknown slugs are safe but pointless. Omit `theme` to inherit whatever theme the viewer runs; `\"hive\"` is an explicit reset to the stock look even inside a themed dashboard.\n\nViewers can override the preset per app from the app's settings drawer; the override rides the reserved `$theme` key of the user-config values (accepted on every app, even without a declared `userConfig` schema, and never visible at `/user/...`). A present override always beats the definition's `theme` — even an unrecognized one, which degrades to the dashboard theme — so treat `theme` as the app's default styling, not a guarantee.\n\nDistinguish the two rollback 400s. A lossy-migration 400 is fixable: copy the required `migration` entries from the message and retry. A target-snapshot validation 400 explicitly says that directives cannot repair that historical definition; use `app-history` and choose a different version.\n\nWhen a schema patch reports stale page/query bindings, repair them in that same patch. Page elements are atomic: replace the complete `pages.<page>.elements.<id>` declaration, removing any reference to a hidden or deleted column, instead of sending only the changed nested prop. This is also the repair move for older apps whose stored page already references an undeclared column; full-definition validation otherwise blocks every patch.\n\n## Definition reference\n\n```json\n{\n \"models\": {\n \"modelName\": {\n \"columns\": {\n \"externalId\": { \"kind\": \"string\" },\n \"title\": { \"kind\": \"string\" }\n }\n }\n },\n \"queries\": { \"queryName\": { \"model\": \"modelName\" } },\n \"actions\": { \"actionName\": { \"kind\": \"task\", \"prompt\": \"Review the current rows and report\" } },\n \"pages\": {\n \"main\": {\n \"root\": \"root\",\n \"elements\": { \"root\": { \"type\": \"Container\", \"props\": {} } }\n }\n },\n \"defaultPage\": \"main\",\n \"theme\": \"ember\"\n}\n```\n\nModel, reusable-element, element-prop, query, action, column, page, and page-param names start with a lowercase letter, contain only letters, numbers, or underscores, and are at most 40 characters. A definition has 0-10 models and at most 20 reusable elements; each declared model has 1-40 columns. Zero-model apps are valid for pure UI utilities as long as their pages and reusable elements validate.\n\n### Reusable elements\n\nTop-level `elements` are versioned inside the app definition and can be reused from the app's own pages or, when explicitly exported, from another app. They are private by default. Each entry declares a `mode`, optional typed `props`, one `root`, and a flat `elements` node map using the same validated tree vocabulary as a page:\n\n```json\n{\n \"elements\": {\n \"statCard\": {\n \"mode\": \"pure\",\n \"export\": true,\n \"props\": {\n \"label\": { \"kind\": \"string\", \"required\": true },\n \"value\": { \"kind\": \"number\", \"default\": 0 }\n },\n \"root\": \"card\",\n \"elements\": {\n \"card\": { \"type\": \"Card\", \"props\": {}, \"children\": [\"label\", \"slot\"] },\n \"label\": { \"type\": \"Metric\", \"props\": { \"label\": { \"$state\": \"/props/label\" }, \"value\": { \"$state\": \"/props/value\" } } },\n \"slot\": { \"type\": \"ElementSlot\", \"props\": {} }\n }\n }\n }\n}\n```\n\n- `pure` elements may read only their declared `/props/<name>` state; `$item` and `$index` are also legal inside repeated nodes. They cannot invoke any action step. A pure tree may contain at most one leaf `ElementSlot` where consumer children are inserted.\n- `bound` elements may additionally read the defining app's declared queries and actions and use its models. Those references are validated against the defining app, including hidden-column rules. Exported bound elements cannot use `app.navigate`; private bound elements may navigate within their defining app.\n- Prop kinds are `string`, `number`, `boolean`, `date`, or `enum`. An enum prop requires a non-empty `enum: [\"value\", ...]` values array. A literal default and each literal consumer value must match the kind and, for enums, be one of those values. A required prop without a default must be supplied.\n\nReference an element with an `ElementRef` node. Omit `app` for same-app reuse; cross-app references name the defining app id and require `export: true`. `instanceKey` is optional. Consumer `children` are accepted only when the target has an `ElementSlot`:\n\n```json\n{\n \"type\": \"ElementRef\",\n \"props\": {\n \"app\": \"<defining-app-id>\",\n \"element\": \"statCard\",\n \"props\": { \"label\": \"Open issues\", \"value\": 12 },\n \"instanceKey\": \"openIssues\"\n },\n \"children\": [\"details\"]\n}\n```\n\nElement references float to the defining app's current definition. `app` and `element` must be literal strings; dynamic references are not supported. The server rejects reference cycles and expansion deeper than five, missing/private targets, invalid props, and a breaking write to an exported element that other apps reference. Removing or unexporting an exported element, changing its mode, removing a declared prop, changing a prop kind, or adding a required prop without a default is breaking; publish the new contract under a new element name. If consumers are abandoned intentionally, retry the PUT, PATCH, or rollback with `\"forceElementBreak\": [\"elementName\"]`. The named consumers will then render an error once element rendering is available; deleting an entire app is deliberately not compatibility-gated.\n\n### Models\n\nEach column is `{ \"kind\": ..., \"required\"?: boolean, \"default\"?: ..., \"index\"?: boolean, \"enum\"?: string[], \"hidden\"?: boolean }`.\n\n| `kind` | Values and indexing |\n|---|---|\n| `string` | String value; set `index: true` only when equality lookups need it. |\n| `number` | Finite number; never indexed. |\n| `boolean` | Boolean; set `index: true` only when equality lookups need it. |\n| `date` | ISO-8601 string; never indexed. |\n| `enum` | String from the non-empty unique `enum` list; always indexed, without `index: true`. |\n| `json` | Any JSON value (object, array, nested) up to 256 KB serialized by default (`APPS_JSON_COLUMN_MAX_BYTES`); never indexed, and a named query cannot `filter` or `sort` on it. Not allowed for `userConfig` fields or element props. |\n\n`default` must match the column kind (and be one of the enum values). `required: true` rejects missing/null values unless a default supplies the value. `id`, `createdAt`, `updatedAt`, `createdBy`, `updatedBy` — and the sync envelope `source`, `syncedAt`, `stale` — are system columns and reserved names. An app created before the sync envelope that declared ordinary columns with these names surfaces `definitionError` after upgrade until they are renamed: patch in a fresh column with a `{from}` migration directive, then purge the old one. Rows always expose `id`/`createdAt`/`updatedAt`; `createdBy`/`updatedBy` record the acting principal (`user:<id>`, `agent:<id>`, `operator`, or `sync:<source>` for engine writes) and are system-managed on every row write.\n\n### Sources and sync\n\nA model may declare up to 4 named `sources` — one-way inbound projections of an external system into its rows. The `script` connector is THE way to bring external data in (GitHub, Linear, any API): a saved script owns the fetch, its auth placeholders, and its paging. `swarm-tasks` is the one native connector, projecting this swarm's own task pool (`config`: `status`/`tags` comma-lists, `agentId`, `assetKey` namespace prefix, `limit` up to 200 — default 100, `includeHeartbeat`). Synced rows are readable by EVERY principal with `app.use` on the app; an unscoped `swarm-tasks` source publishes task prompts (truncated, secret-scrubbed) to all of them, so scope it with `config.assetKey`/`config.agentId` unless that exposure is intended. A user-invoked sync only ever pulls the invoking user's own requested tasks. Script sources run with their run-as identity's credentials — an agent-owned script runs as its owner and only the owner may wire it; an owner-less catalog (global) script runs as the LEAD, so only the lead or the operator can wire or alter such a source (stored references stay valid with their args and connection pinned).\n\n```json\n{\n \"models\": {\n \"issue\": {\n \"columns\": {\n \"issueKey\": { \"kind\": \"string\" },\n \"title\": { \"kind\": \"string\", \"source\": { \"of\": \"gh\", \"field\": \"title\" } },\n \"openedAt\": { \"kind\": \"date\", \"source\": { \"of\": \"gh\", \"field\": \"created_at\", \"transform\": \"date-parse\" } },\n \"note\": { \"kind\": \"string\" }\n },\n \"sources\": {\n \"gh\": { \"connector\": \"script\", \"scriptId\": \"<script-uuid>\", \"joinKey\": \"issueKey\", \"args\": { \"repo\": \"owner/name\" }, \"connection\": \"github\" }\n }\n }\n }\n}\n```\n\n- **Join key**: each source names a `joinKey` — an owned, non-hidden `string` column carrying the external identity. It must not be `required`, must not declare a `default`, and cannot itself be source-bound. A row belongs to at most ONE source (`row.source`).\n- **Column bindings**: `{ \"of\": \"<sourceName>\", \"field\": \"user.login\", \"transform\"? }` projects a (dotted-path) field of the pulled record into the column on every pass. Transforms and their required column kinds: `slug`/`lower`/`upper` → `string`, `cents` → `number` (×100, rounded), `date-parse` → `date` (ISO). A failed transform nulls that one column and warns; it never fails the pass.\n- **Read-only contract**: source-bound columns and the join key reject every direct write (row create/patch/bulk) with a path-bearing issue — mutate them via the source or a sync refresh. Owned columns on the same model stay writable. Every `required` owned column on a sourced model must declare a `default`, because sync-created rows cannot supply it.\n- **Provenance**: synced rows carry `source` (owner), `syncedAt` (last confirmed presence — sortable), and `stale` (true when a complete pull no longer returned the record; clears on reappearance). `syncedAt` advances on every confirmed row WITHOUT touching `updatedAt`; `updatedAt` moves only when projected data actually changed.\n- **`connection`** (optional, script sources): names a connections-system slug the source depends on. It buys write-time validation plus a preflight before every pull; it does not by itself gate egress. Credentials resolve for the sync run-as identity — the script's owner when it has one, else the lead agent — so global connections always work and agent-scoped ones need matching script ownership. The slug is injected into the script's args as `connection`, letting one generic script serve N connections.\n\n**The canonical sync-script contract** (every sync script, always):\n\n```ts\nexport default async function run(args, ctx) {\n // args = { ...source.args, app: { id }, model: \"<model>\", source: \"<sourceName>\", connection?: \"<slug>\" }\n const records = [{ key: \"42\", fields: { title: \"…\", created_at: \"…\" } }];\n return { records, complete: true }; // or return the bare array — a bare array IS a complete snapshot\n}\n```\n\nReturn `Array<{ key, fields }>` or `{ records, complete? }`. `key` is the external identity the engine joins on `joinKey` (numbers are coerced to strings); `fields` is the bag bindings project from. Return `complete: false` whenever the pull window may have missed records — a paged fetch, a hit `limit`, or client-side filtering of a full page. An incomplete pull skips the stale sweep, so rows outside a narrow window are not falsely marked stale; compute `complete` BEFORE any client-side filtering. Hard cap 500 records per pull (over-cap pulls truncate and count as incomplete). An invalid return shape, thrown error, or non-zero exit fails the pass with zero row churn.\n\nThe seeded catalog script `github-issues-pull` is the worked example (placeholder auth via `[REDACTED:GITHUB_TOKEN]`, PR filtering, `complete` computed before filtering). For scheduled refreshes, point a `targetType: \"script\"` schedule at the seeded `app-sync-run` with `scriptArgs: { \"appId\": \"<id>\", \"model\"?, \"source\"? }`.\n\n**Three refresh doors, one engine**: `POST /api/apps/<id>/sync` (body `{ \"model\"?, \"source\"? }`), a `sync` action kind, and the `app-sync` MCP tool (`ctx.swarm.app_sync` from scripts). `{model?, source?}` fans out to every matching (model × source) pair; each pass reports `{ pulled, created, updated, refreshed, unchanged, markedStale, warnings }`. A pair already syncing in this API process short-circuits with `alreadyRunning: true` instead of pulling twice. Each pair's last-pass status (`lastFinishedAt`, counts, `error?`) comes back as `syncStatus` — keyed `\"<model>:<source>\"` — on `GET /api/apps/<id>` and in `app-get` data, so UI and agents can render \"last synced / last error\" without extra calls.\n\n### Freshness and the refresh button\n\nFreshness renders with the existing column kinds — no special components. Give the table `{ \"key\": \"syncedAt\", \"kind\": \"date\" }` and `{ \"key\": \"stale\", \"kind\": \"badge\", \"tones\": { \"true\": \"warning\" } }` columns, declare a `sync` action, and wire a Button to it:\n\n```json\n{\n \"actions\": { \"refresh\": { \"kind\": \"sync\" } },\n \"pages\": {\n \"main\": {\n \"root\": \"root\",\n \"elements\": {\n \"root\": { \"type\": \"Container\", \"props\": {}, \"children\": [\"refreshBtn\", \"issuesTable\"] },\n \"refreshBtn\": {\n \"type\": \"Button\",\n \"props\": { \"label\": \"Refresh\" },\n \"on\": { \"press\": [{ \"action\": \"app.action\", \"params\": { \"name\": \"refresh\" } }] }\n },\n \"issuesTable\": {\n \"type\": \"Table\",\n \"props\": {\n \"data\": { \"$state\": \"/queries/allIssues/data\" },\n \"columns\": [\n { \"key\": \"title\", \"label\": \"Issue\" },\n { \"key\": \"syncedAt\", \"label\": \"Synced\", \"kind\": \"date\" },\n { \"key\": \"stale\", \"label\": \"Stale\", \"kind\": \"badge\", \"tones\": { \"true\": \"warning\" } }\n ]\n }\n }\n }\n }\n }\n}\n```\n\nA `sync` action answers the script-action state shape (`/actions/refresh` → `running` → `ok`/`error`, result under `result.passes`), so the page's queries refetch when it completes. Use `app-query` to READ rows (named queries may filter on `stale`/`source` and sort by `syncedAt`); use `app-sync` to REFRESH sources; never poll a sync door in a loop — schedule it or give people the button.\n\n### Queries\n\nA named query is:\n\n```json\n{\n \"model\": \"idea\",\n \"filter\": { \"status\": \"open\" },\n \"sort\": { \"column\": \"createdAt\", \"dir\": \"desc\" },\n \"limit\": 100\n}\n```\n\n`filter` is a strict AND of equality checks. A filter may target a declared model column or a system column (`id`, `createdAt`, `updatedAt`, `createdBy`, `updatedBy`, and on synced models `source`, `syncedAt`, `stale`) — filtering on `id` is the universal way to select one row for a detail view, and `{ \"stale\": true }` selects rows a complete pull stopped confirming. A literal value must match the column's kind; date filters compare the stored raw ISO string, not parsed instants. Omit `limit` for the default 200 rows, or set an integer from 1 through 1000. `sort.column` is a non-`json` model column, `createdAt`, `updatedAt`, or `syncedAt`; `dir` is `asc` or `desc`. Query runtime state is `{ data, loading, error }` under `/queries/<queryName>`.\n\nA filter value may instead be exactly `{ \"$param\": \"<name>\" }`. At execution, the caller supplies that name and the server coerces its value to the filtered column's kind before equality matching:\n\n```json\n{\n \"queries\": {\n \"issueDetail\": { \"model\": \"issue\", \"filter\": { \"issueId\": { \"$param\": \"issueId\" } }, \"limit\": 1 }\n }\n}\n```\n\nThe placeholder object must contain only `$param`. Missing caller values fail loudly rather than returning unfiltered rows: HTTP reports 400 and `app-query` returns a tool error listing every missing name. Supplying a param name the query does not reference is also an error. The app UI supplies current declared route params automatically. Direct calls use `app-query({ appId, query: \"issueDetail\", params: { issueId: 42 } })` or `ctx.swarm.app_query({ appId, query: \"issueDetail\", params: { issueId: 42 } })`.\n\n### Custom actions\n\nActions are optional (maximum 20) and are invoked from the page with the `app.action` runtime action.\n\n```json\n{\n \"actions\": {\n \"recalculate\": {\n \"kind\": \"script\",\n \"scriptId\": \"00000000-0000-4000-8000-000000000000\",\n \"args\": { \"mode\": \"fast\" }\n },\n \"triage\": {\n \"kind\": \"task\",\n \"prompt\": \"Triage this idea and report a recommendation\"\n }\n }\n}\n```\n\n| Kind | Contract |\n|---|---|\n| `script` | `scriptId` must identify an existing script; optional `args` are defaults. Invocation input overrides same-named defaults, and the runtime also supplies `app: { id }`. |\n| `task` | `prompt` must be non-empty; omit `agentId` to use default (lead) assignment. Only set `agentId` to a real agent UUID. Invocation `input` is included as context for the task prompt. Spawned tasks carry asset key `shared/app:<appId>/action:<name>/`, so a `swarm-tasks` source with `config.assetKey: \"shared/app:<appId>/\"` pulls back the tasks this app spawned. |\n| `sync` | Optional `model` and `source` narrow which (model × source) pairs refresh; omitting both syncs every pair the app declares. At least one pair must exist. Answers the script-kind state shape with per-pass results under `result.passes`. |\n\n- Invocation state lands at `/actions/<name>` as `{ status, result?, error?, taskId?, taskStatus? }`, where `status` is `\"running\"`, `\"ok\"`, or `\"error\"`.\n\nUse a task action named Tackle when a person should hand one issue row to the swarm. Pass the complete current row under `input.issue`; `{ \"$row\": \"\" }` resolves to the whole row and that context lands in the task prompt.\n\n```json\n{\n \"actions\": {\n \"tackle\": { \"kind\": \"task\", \"prompt\": \"Tackle this issue. Inspect the supplied issue context, do the work, and report the outcome.\" }\n },\n \"pages\": {\n \"main\": {\n \"root\": \"issuesTable\",\n \"elements\": {\n \"issuesTable\": {\n \"type\": \"Table\",\n \"props\": {\n \"data\": { \"$state\": \"/queries/allIssues/data\" },\n \"columns\": [{ \"key\": \"title\", \"label\": \"Issue\" }],\n \"rowActions\": [{\n \"label\": \"Tackle\",\n \"actions\": [{\n \"action\": \"app.action\",\n \"params\": { \"name\": \"tackle\", \"input\": { \"issue\": { \"$row\": \"\" } } }\n }]\n }]\n }\n }\n }\n }\n },\n \"defaultPage\": \"main\"\n}\n```\n\nFor direct MCP calls, `app-query` accepts `{ appId, query, params? }` and returns rows from that declared named query; every supplied param must be referenced by a `$param` filter in that query. Use it when an agent needs to read app state without scraping the UI.\n\nSaved scripts use the generated SDK name `ctx.swarm.app_query({ appId, query, params? })`. Use `app_query` in scripts or workflows that turn current app rows into reports, digests, or follow-up work. Saved scripts also get generated per-app types automatically: each app contributes an `App_<Name>` namespace with one row interface per model (enum columns as literal unions) and a typed `app_query` overload per named query, all visible via `script-query-types`. Scripts can create, list, get, patch, and delete rows with the seven row tools. Actions remain REST/MCP-only and have no script SDK method.\n\n### Pages, routes, and page trees\n\n`pages` is a map of independently validated page trees and `defaultPage` must name one entry. Each page may also declare a display `title` and typed route `params`. On every non-default page the runtime automatically renders breadcrumbs (`<default page title> › <current page title>`, first crumb navigates back) — including in chromeless embeds — so a detail page does NOT need its own back button; give pages good `title`s instead. This complete pattern connects a table row to a typed detail route and its parameterized query:\n\n```json\n{\n \"models\": {\n \"issue\": {\n \"columns\": {\n \"issueId\": { \"kind\": \"number\" },\n \"title\": { \"kind\": \"string\" },\n \"status\": { \"kind\": \"string\" }\n }\n }\n },\n \"queries\": {\n \"allIssues\": { \"model\": \"issue\" },\n \"issueDetail\": {\n \"model\": \"issue\",\n \"filter\": { \"issueId\": { \"$param\": \"issueId\" } },\n \"limit\": 1\n }\n },\n \"pages\": {\n \"issues\": {\n \"title\": \"Issues\",\n \"root\": \"issuesTable\",\n \"elements\": {\n \"issuesTable\": {\n \"type\": \"Table\",\n \"props\": {\n \"data\": { \"$state\": \"/queries/allIssues/data\" },\n \"columns\": [\n { \"key\": \"title\", \"label\": \"Issue\" },\n { \"key\": \"status\", \"label\": \"Status\", \"kind\": \"badge\" }\n ],\n \"rowActions\": [{\n \"label\": \"Open\",\n \"actions\": [{\n \"action\": \"app.navigate\",\n \"params\": {\n \"page\": \"detail\",\n \"params\": { \"issueId\": { \"$row\": \"issueId\" } }\n }\n }]\n }]\n }\n }\n }\n },\n \"detail\": {\n \"title\": \"Issue detail\",\n \"params\": { \"issueId\": { \"kind\": \"number\", \"required\": true } },\n \"root\": \"detail\",\n \"elements\": {\n \"detail\": {\n \"type\": \"DetailList\",\n \"props\": {\n \"data\": { \"$state\": \"/queries/issueDetail/data/0\" },\n \"fields\": [{ \"key\": \"title\", \"label\": \"Issue\" }]\n }\n }\n }\n }\n },\n \"defaultPage\": \"issues\"\n}\n```\n\nDrawer variant: declare a route param on the containing page, set the Drawer `props.param` to that literal param name, and use the same `app.navigate` shape targeting that page with the row id assigned to the Drawer's param. The Drawer then opens from route state without a separate action contract.\n\nParam `kind` is `string` by default, or `number` / `boolean`; `required` defaults to false. The names `mode`, `apiUrl`, `apiKey`, `email`, and `name` are reserved. `/apps/<id>` renders `defaultPage`; `/apps/<id>/p/<page>?issueId=42` is the shareable deep link for another page. Browser navigation is client-side, so app state and polled data stay warm across page changes.\n\nWithin each page, `root` names one entry in its non-empty `elements` map. Elements are a flat, single-parent tree: ids are map keys, `children` contains ids (not nested elements), all elements must be reachable from `root`, and cycles, missing children, or shared children are invalid. Element and Form/UI ids are page-local. Element keys are `type`, `props`, `children`, `on`, `visible`, `repeat`, and `watch`; only components with a `default` child slot accept `children` (Stack, Grid, Split, Tabs, Container, Card, Section, and Drawer). `watch` maps state paths to an action step or chain.\n\n## Component catalog (all 33)\n\nProps reject unknown keys. A `{\"$state\":\"...\"}` binding may replace a literal prop value at any depth.\n\nIcons: every `icon` prop (and the `Icon` block) takes one name from a curated lucide set: `plus`, `refresh-cw`, `search`, `filter`, `download`, `upload`, `external-link`, `copy`, `check`, `x`, `trash-2`, `pencil`, `eye`, `calendar`, `clock`, `user`, `users`, `bot`, `git-pull-request`, `git-branch`, `github`, `slack`, `mail`, `link`, `file-text`, `folder`, `database`, `zap`, `sparkles`, `flag`, `star`, `tag`, `alert-triangle`, `info`, `circle-check`, `trending-up`, `trending-down`, `dollar-sign`, `activity`, `rocket`. Any other name is rejected.\n\nTones: Badge, Metric and Progress `tone` take `neutral`, `success`, `active`, `error`, `info`, `pending`, `warning`, or `paused`.\n\n| Component | Required props | Optional props / values |\n|---|---|---|\n| `Stack` | none | Primary layout; `direction: \"column\", \"row\"`, `gap`, `padding: \"none\", \"xs\", \"sm\", \"md\", \"lg\", \"xl\"`; `align: \"start\", \"center\", \"end\", \"stretch\"`, `justify: \"start\", \"center\", \"end\", \"between\"`, `wrap`; `motion: \"rise\"` (default), `\"fade\"`, or `\"none\"` (see Motion below); has the `default` child slot. |\n| `Grid` | none | Responsive `columns`: integer 1 through 6, or `{ base, sm, md, lg }` counts 1 through 6; `gap` uses the Stack spacing values; has the `default` child slot. |\n| `Split` | none | `ratio: \"1-1\", \"1-2\", \"2-1\", \"1-3\", \"3-1\"`, `gap`, `collapseBelow: \"sm\", \"md\", \"lg\"`, `reverse`; has the positional `default` child slot. |\n| `Divider` | none | `label`; no children. |\n| `Tabs` | `id`, `tabs` | `tabs` entries are `{ key, label?, icon?, count? }` (`count` is a number shown as a small pill); `defaultTab`; `variant: \"line\"` (default) or `\"segmented\"` (a compact segmented control, same state and warm panels); has the positional `default` child slot. |\n| `Container` | none | Legacy layout primitive; prefer Stack. `direction: \"row\", \"column\"`, `gap: \"none\", \"sm\", \"md\", \"lg\"`; has the `default` child slot. |\n| `Card` | none | `title`, `description`; has the `default` child slot. |\n| `Heading` | `text` | `level: \"h1\", \"h2\", or \"h3\"`. |\n| `Text` | `content` | `tone: \"default\" or \"muted\"`. |\n| `Markdown` | `content` | Rendered Markdown for help, instructions, and rich prose; no children. |\n| `SearchInput` | `id` | `placeholder`, `label`, `shortcut` (one key, usually `\"/\"`: shows a keycap and focuses the field); writes debounced text to `/ui/<id>/value`; no children. |\n| `Select` | `id`, `options` | `options` are strings or `{ value, label?, icon?, hint? }`; `placeholder`, `label`, `clearable`, `searchable` (type to filter; on by default past 8 options), `searchPlaceholder`, `multiple`; writes a string or null to `/ui/<id>/value`, or with `multiple` a string array (empty = nothing picked); no children. |\n| `Chips` | `id`, `options` | Toggle chips for quick filters. `options` are strings or `{ value, label?, icon? }`; `multiple`; `label` (group name for screen readers). Writes a string or null to `/ui/<id>/value`, or a string array with `multiple`; pressing the selected chip clears it. |\n| `Button` | `label` | `variant: \"default\", \"secondary\", \"outline\", \"ghost\", \"destructive\", or \"destructive-outline\"` (use `destructive-outline` for standalone deletes); `disabled` (constant or bound boolean); `busyWith: \"<actionName>\"` swaps the label for a spinner while that custom action's `/actions/<name>/status` is `\"running\"` (presses are ignored), then shows a check and `successLabel` for about 1.5s when it turns `\"ok\"`; `icon`, `iconPosition: \"start\" or \"end\"`; `size: \"sm\", \"default\", or \"lg\"`; `shortcut` (one key, shown as a keycap, presses the button); `hint` (tooltip); dispatch with element-level `on.press`. |\n| `Metric` | `label`, `value` | The dashboard stat tile. `value` is string or number (empty renders an em dash); bind `loading` to `/queries/<name>/loading` for a skeleton while the query loads; `icon` with `tone` tinting its tile; `detail` (a line under the value); `trend: { direction: \"up\", \"down\", or \"flat\", label }`; `info` (explanation tooltip next to the label). |\n| `Alert` | `message` | `title`, `tone: \"info\", \"success\", \"warning\", or \"error\"`, `icon` (overrides the tone icon), `actionLabel` (a button inside the callout that emits `press`; wire it with `on.press`). |\n| `Badge` | `text` | `text` is string or number; `tone`; `icon` replaces the dot; `variant: \"tag\"` (default uppercase chip), `\"status\"` (sentence-case pill), `\"count\"` (numeric pill), or `\"beta\"` (the BETA tag); `status` (a lifecycle value renders the dashboard status chip); `hint` (tooltip; the beta tag's explanation). |\n| `Icon` | `name` | `size: \"sm\", \"md\", or \"lg\"`, `tone: \"default\", \"muted\", \"success\", \"warning\", \"error\", or \"info\"`, `label` (accessible name; decorative without it). |\n| `Progress` | `value` | `max` (default 100), `tone`, `label`, `variant: \"bar\"` (default) or `\"ring\"` (the dashboard progress ring; ignores `tone`), `showValue` (default true: percent for a bar, `value / max` for a ring). |\n| `EmptyState` | `title` | `icon` (default inbox), `body`, `actionLabel` (a button that emits `press`; wire it with `on.press`). |\n| `CopyButton` | `value` | `value` is string or number; `label` is the visible text, icon-only without it. Renders nothing for an empty value. |\n| `Kbd` | `keys` | Keycaps, one per string (`[\"⌘\", \"K\"]`). Display only. |\n| `Skeleton` | `shape` | `shape: \"line\", \"block\", \"rows\", \"metric\", or \"card\"`; `count` (1-20: lines, rows, tiles, or cards); `height` (px, `block` only, default 128). Show it with `visible` bound to a query's `loading`. |\n| `Spinner` | none | `variant: \"ring\", \"ellipsis\", or \"bars\"` (default: the dashboard spinner), `size: \"sm\" or \"md\"`, `label` (visible text and accessible name). |\n| `AvatarStack` | `users` | `users` are display names (initials avatars); `max` (default 4) before a `+N` count. Hover lists every name. |\n| `Section` | `title` | `description`, `collapsible`, `defaultOpen` (default true); has the `default` child slot. |\n| `Table` | `columns` | `data`, `loading`, `error`, `emptyMessage`, `emptyState`, `rowActions`, `onRowClick`, `selectedId`, `rowId`, `mobile`, `mobileFields`, `search`, `filters`; see below. |\n| `Form` | `id`, `fields`, `onSubmit` | `title`, `submitLabel`; see below. |\n| `Drawer` | `param` | `title`, `description`, `side: \"right\" or \"left\"`, `size: \"sm\", \"md\" (default), \"lg\", or \"xl\"` (about 480/660/900/1010px at 1440px; grows with the viewport up to 640/960/1280/1600px; full width on phones), `actions` (sticky footer buttons; see below); has the `default` child slot. |\n| `DetailList` | `fields` | `data`, `emptyMessage`, `columns: 1 or 2`, `loading` (bind `/queries/<name>/loading` for skeleton fields while loading); renders one record without edit controls; see below for field kinds. |\n| `Kanban` | `columnField`, `columns`, `titleField` | `data`, `loading`, `cardFields`, `idField` (default `id`), `onMove`, `onCardClick`; see below. |\n| `Calendar` | `startField`, `titleField` | `data`, `loading`, `endField`, `idField`, `onSelect`; read-only month grid; see below. |\n| `ContributionGraph` | none | `data`, `loading`, `dateField` (default `date`), `countField`, `days` (7-730, default 365); read-only activity heatmap. |\n\nTable details:\n\n- `columns[]`: `{ key, label?, kind?, tones?: {value: badgeTone}, width?: number, pinned?: \"left\" or \"right\", align?: \"start\" or \"end\", truncate?: \"end\" or \"middle\", icon?, tooltip?: \"<row field>\", max?: number, onChange?: chain }`. `kind` is `text`, `string`, `enum`, `number`, `boolean`, `date`, `badge`, `status`, `user`, `agent`, `relative`, `duration`, `progress`, `copy`, `switch`, `link`, or `json`; `string` and `enum` render as text, and `json` renders a truncated single-line `JSON.stringify` preview. Pin id/actions columns so they survive horizontal scroll on narrow viewports.\n - `status`: lifecycle values (`pending`, `in_progress`, `running`, `completed`, `failed`, `cancelled`, `paused`, `reviewing`, ...) render the dashboard status chip with its icon; other values use `tones` like `badge`.\n - `user` / `agent`: avatar plus name. `relative` (and `date`): smart time, exact time on hover. `duration`: milliseconds as `1m 23s`. `progress`: a bar against `max` (default 100). `copy`: the value plus a copy button. `link`: an http(s) URL opening in a new tab (other values render as text).\n - `switch`: a toggle. `onChange` runs with `$row` = the row with this column already set to the new value, e.g. `[{ \"action\": \"app.mutate\", \"params\": { \"model\": \"m\", \"op\": \"update\", \"rowId\": { \"$row\": \"id\" }, \"values\": { \"enabled\": { \"$row\": \"enabled\" } } } }]`. Without `onChange` the switch is read-only.\n - Numbers, durations and progress right-align by default. `truncate: \"middle\"` keeps both ends of ids and paths. `icon` shows in the header; `tooltip` names a row field shown on hover.\n- `onRowClick`: action chain with `$row` / `$rowIndex`, run when a row is clicked (or Enter on a focused cell). Pair it with `app.navigate` that sets a Drawer param, and bind `selectedId` to `{ \"$state\": \"/route/params/<param>\" }` so the open row stays highlighted. `rowId` names the id field (default `id`).\n- Phones (below 768px): the table renders as a card list. `mobileFields: { title, subtitle?, meta?: string or string[], status? }` picks the columns; by default the first text column is the title and the first `status`/`badge` column the chip. `mobile: \"scroll\"` keeps the grid; it is the default when the table has `rowActions` but no `onRowClick`.\n- `emptyState: { icon?, title, body? }` replaces the plain empty message. A search or filter that hides every row shows its own \"No rows match\" state.\n- `pagination`: boolean; defaults to auto-enabling past 200 rows (set `false` to force one scroll region). `density: \"compact\"` tightens row height for dense readouts.\n- `rowActions[]`: `{ label, variant?, icon?, placement?: \"inline\" or \"menu\", confirm?, actions }`. `placement: \"menu\"` moves the action into the row's More menu. Variants add `destructive-outline` to the Button variants. `destructive` and `destructive-outline` confirm by default; customize with `{ \"title\": ..., \"description\": ..., \"confirmLabel\": ... }`, or use a bare `confirm` string as the dialog description (use `confirm: false` only for a reversible action).\n- `search`: optional string; case-insensitive substring matching across the listed string and number columns. Bind a SearchInput value for client-side search.\n- `filters`: optional record of per-column string, number, boolean, null, or array values. An array means \"the cell is one of these\" (what a `multiple` Chips or Select writes). Null, empty strings, empty arrays, or absent values disable one filter. Bind Select or Chips values for client-side filters.\n\nKanban details:\n\n- One card per `data` row, grouped by the row's `columnField` value. `columns[]` are plain strings or `{ id, label? }`, in display order; rows matching no column are hidden.\n- `cardFields[]` uses the DetailList field shape (`{ key, label?, kind?, tones? }`, same kinds) for extra lines under the title.\n- `onMove` is an action chain run when a card is dropped in another column. `$row` is the moved row with `columnField` already set to the new column: `[{ \"action\": \"app.mutate\", \"params\": { \"model\": \"deal\", \"op\": \"update\", \"rowId\": { \"$row\": \"id\" }, \"values\": { \"stage\": { \"$row\": \"stage\" } } } }]`. Without `onMove`, cards cannot be dragged.\n- `onCardClick` is an action chain with `$row`; pair it with `app.navigate` and a `Drawer` to open a card.\n\nCalendar details:\n\n- `startField` / `endField` hold ISO dates, ISO datetimes, or epoch ms. An event shows on every day from start to end; without `endField` it shows on its start day.\n- Each Calendar keeps its own visible month (prev/next controls in its header). `onSelect` is an action chain with `$row`, run when an event is clicked.\n\nContributionGraph details:\n\n- Buckets `data` rows by the calendar day of `dateField` over the trailing `days`. Each row counts 1, or adds its numeric `countField` when set.\n\nForm details:\n\n- `fields[]`: `{ name, label?, kind?, options?: string[], placeholder?, required? }`; `kind` is `string`, `text`, `number`, `boolean`, `date`, or `enum`, and enum fields need `options`.\n- Values live at `/forms/<formId>/<fieldName>`. `onSubmit` is an action chain.\n- The submit button shows a pending spinner while the chain runs, and `app.mutate` failures in the chain render inline under the fields (not the page banner) — no extra wiring needed.\n\nDrawer details:\n\n- `param` must name a param declared on the containing page. The drawer is open exactly when `/route/params/<param>` has a non-empty value, so its open state survives refresh and is shareable. Its children mount only while open; closing it clears that param with history replacement.\n- `actions[]`: `{ label, variant?, icon?, confirm?, actions }`, rendered as buttons in a sticky footer under the scrolling body. Confirmation follows Table `rowActions`: destructive variants confirm by default, and a chain with an `app.mutate` delete always confirms. Chains have no `$row`; address the open record with `{ \"$state\": \"/route/params/<param>\" }`, for example `{ \"action\": \"app.mutate\", \"params\": { \"model\": \"order\", \"op\": \"delete\", \"rowId\": { \"$state\": \"/route/params/panel\" } } }`. Prev/next record navigation is not available.\n\nDetailList details:\n\n- `fields[]`: `{ key, label?, kind?, tones? }`. Kinds reuse Table formatting (`text`, `string`, `number`, `boolean`, `date`, `badge`, `enum`) and the Table cell renderers (`status`: lifecycle chip or `tones` pill; `user`: avatar plus name; `relative`: smart time, exact on hover; `copy`: value plus copy button), and add `code` for a monospace block and `json` for a collapsible JSON tree (objects, or a string holding JSON). Bind `data` to one record, commonly `/queries/<detailQuery>/data/0`.\n\nMotion details:\n\n- Every `Stack` fades its direct children in and lifts them 4px over 200ms on first paint, staggered 30ms (capped at the 10th child). It never replays on the 5s data poll. A Stack nested inside another one does not add its own entrance. `motion: \"fade\"` drops the lift; `motion: \"none\"` turns it off. Viewers with reduced motion get a 150ms fade.\n\nShortcut details:\n\n- `Button.shortcut` and `SearchInput.shortcut` are one key. Letters and digits match without Shift; symbols such as `/` and `?` match as typed. They do nothing while focus is in a text field, while Ctrl, Cmd, or Alt is held, or while a dialog or menu is open (unless the control is inside it, such as a Button in an open Drawer).\n\n## Layout & interactivity\n\nUse `Stack` as the primary page and section layout; `Container` is the legacy two-prop primitive retained for existing pages. Stack supports vertical or horizontal flow, shared spacing, alignment, justification, wrapping, and padding; row Stacks (filter bars, button rows) should set `collapseBelow: \"sm\"|\"md\"|\"lg\"` so they stack vertically on phones. Use `Grid` for responsive card or metric strips: a bare column count is responsive shorthand (1 column on phones, 2 from `sm`, the count from `md` up), or pin breakpoints explicitly with `{ \"base\": 1, \"md\": 2, \"lg\": 3 }`. `Grid` and `Split` also accept `padding` on the shared spacing scale.\n\n`Split` and `Tabs` children are positional. For Split, `children[0]` is the first pane, `children[1]` is the second, and extra children append inside the second pane. Below `collapseBelow`, panes stack; `reverse` changes only that narrow-layout stacking order. For Tabs, `children[i]` is the body for `tabs[i]`; keep both arrays in the same order and with the same count. Inactive tab children stay mounted but hidden, so Tables keep polling. Use `Divider` to separate sections and `Markdown` for richer explanatory content.\n\nSearchInput, Select, and Chips are client-side controls: each needs a literal `id` and writes state under `/ui/<id>/value`; Tabs writes its active key to `/ui/<id>/tab`. Bind those values into a Table's `search` and `filters` props. This filters already-polled rows locally; it does not alter or re-run the named query.\n\n```json\n{\n \"filters\": { \"type\": \"Stack\", \"props\": { \"gap\": \"sm\" }, \"children\": [\"query\", \"status\"] },\n \"query\": { \"type\": \"SearchInput\", \"props\": { \"id\": \"ideaSearch\", \"placeholder\": \"Search ideas\" } },\n \"status\": { \"type\": \"Select\", \"props\": { \"id\": \"ideaStatus\", \"options\": [\"open\", \"done\"] } },\n \"ideas\": {\n \"type\": \"Table\",\n \"props\": {\n \"data\": { \"$state\": \"/queries/allIdeas/data\" },\n \"columns\": [{ \"key\": \"title\" }, { \"key\": \"status\" }],\n \"search\": { \"$state\": \"/ui/ideaSearch/value\" },\n \"filters\": { \"status\": { \"$state\": \"/ui/ideaStatus/value\" } }\n }\n }\n}\n```\n\n`visible` accepts a boolean, a `$state` truthiness binding, comparisons, and logical groups. Put exactly one comparison key in each condition object. The renderer evaluates the first matching key in fixed priority order (`eq`, `neq`, `gt`, `gte`, `lt`, `lte`; otherwise truthiness), so additional comparison keys in the same object are ignored. Use `$and` / `$or` arrays to combine conditions. `not` is a negation FLAG inside a condition object (`\"not\": true`), never a wrapper around one — `{ \"not\": { \"$state\": … } }` is invalid and the validator rejects it. The absent-record pattern (e.g. an alert when a detail query matched nothing) is `{ \"$state\": \"/queries/<q>/data/0/id\", \"not\": true }`:\n\n```json\n[\n { \"$state\": \"/route/params/issueId\", \"gt\": 0 },\n {\n \"$and\": [\n { \"$state\": \"/route/page\", \"eq\": \"detail\" },\n { \"$state\": \"/route/params/panel\", \"neq\": \"hidden\" }\n ]\n }\n]\n```\n\n## Bindings, sentinels, and action chains\n\nUse this exact query binding shape; do not invent `$query`, `$item`, or template strings:\n\n```json\n{\n \"data\": { \"$state\": \"/queries/allIdeas/data\" },\n \"loading\": { \"$state\": \"/queries/allIdeas/loading\" },\n \"error\": { \"$state\": \"/queries/allIdeas/error\" }\n}\n```\n\nValid `$state` roots are:\n\n- `/queries/<declaredQuery>/...`\n- `/forms/<formId>/...`, where a `Form` element has that literal `props.id`\n- `/ui/<id>/...`, where a SearchInput, Select, Chips, or Tabs element has that literal `props.id` (SearchInput, Select, and Chips use `/value`; Tabs uses `/tab`)\n- `/actions/<declaredAction>/...`\n- `/route/page` for the active page name\n- `/route/params/<declaredParam>` for a param declared on the current page\n\n`/route` is mirrored as `{ page, params }`. Only declared params appear. Values are coerced by their declared kind: `number` becomes a number when possible; boolean strings `true`/`1` become true; failed number coercion remains the raw string.\n\nOnly inside action-chain `params` (`on.<event>`, `Table.rowActions[].actions`, or `Form.onSubmit`) these scoped sentinels are valid, recursively inside objects and arrays:\n\n| Sentinel | Resolves to |\n|---|---|\n| `{ \"$row\": \"id\" }` | Current Table row's `id`; another column name selects that field. |\n| `{ \"$row\": \"\" }` | Entire current Table row. |\n| `{ \"$rowIndex\": true }` | Current Table row index. |\n| `{ \"$form\": \"title\" }` | One current Form value. |\n| `{ \"$form\": \"\" }` | All current Form values. |\n| `{ \"$state\": \"/queries/allIdeas/data\" }` | Current value at a valid app state path, including action state such as `/actions/refreshIssues/status`. |\n| `{ \"$chain\": \"leafTaskId\" }` | One chain-scope value, published by a component that runs a task chain; it carries `leafTaskId` and `newTaskId`. Where no chain scope is published, the sentinel is left in place and the action fails instead of silently dropping the parent. |\n| `{ \"$chain\": \"\" }` | The whole chain scope. |\n\nEach sentinel object must contain exactly the single key shown.\n\nAn action-chain step is `{ \"action\": \"<type>\", \"params\": {...} }`. Available action types:\n\n- `app.mutate`: `{ model, op, rowId?, values?, formId? }`, where `op` is `\"create\"`, `\"update\"`, or `\"delete\"`. `update`/`delete` require `rowId` (usually `{ \"$row\": \"id\" }`, or `{ \"$state\": \"/route/params/<param>\" }` in a Drawer footer); literal `values` keys must be model columns. Successful mutation refetches all queries on that model.\n- `app.refresh`: `{ query? }`; omit `query` to refetch all, or name a declared query.\n- `app.action`: `{ name, input?, parentTaskId? }`; `name` must be declared in definition `actions`. For a task-kind row action, pass whole-row context with `input: { issue: { \"$row\": \"\" } }`. `parentTaskId` chains the created task under an existing one for task-kind actions (typically `{ \"$chain\": \"leafTaskId\" }`; a literal or `$state` task id also works); a script or sync action rejects it with 400.\n- `app.navigate`: `{ page, params? }`; `page` must be declared, supplied keys must be params of the target page, and every required target param must be supplied. `params` replace the current route params wholesale and may contain action sentinels. Navigation pushes history, so browser Back returns to the previous page/params; only `mode` is preserved automatically.\n- `ui.toast`: `{ message, description?, tone?, undo? }`, where `tone` is `\"success\"`, `\"error\"`, `\"info\"` (default), or `\"warning\"`. `undo: { label?, actions }` adds an Undo button (default label `Undo`) that runs `actions` as a chain; its steps are validated like any chain. Put it after the step it confirms, e.g. a row delete followed by `{ \"action\": \"ui.toast\", \"params\": { \"message\": \"Order deleted\", \"undo\": { \"actions\": [{ \"action\": \"app.mutate\", \"params\": { \"model\": \"order\", \"op\": \"create\", \"values\": { \"title\": { \"$row\": \"title\" } } } }] } } }`. Undo params resolve `$state` when Undo is pressed; `$row` / `$form` resolve when the toast is shown.\n- `swarm.sdk`: `{ sdk, args? }`; invokes a catalog-supported Swarm browser SDK method with the viewer's bearer.\n- `swarm.call`: `{ method, endpoint: \"/api/...\", body? }`, where `method` is `\"GET\"`, `\"POST\"`, `\"PUT\"`, `\"DELETE\"`, or `\"PATCH\"`; raw authenticated API call.\n\n## Patch semantics\n\n`app-patch.definition` is RFC 7396 JSON Merge Patch applied to the stored definition:\n\n- omitted keys stay unchanged;\n- object keys merge recursively;\n- arrays and scalar values replace;\n- `null` deletes a key;\n- reusable-element disambiguation: a patch value for `elements.<name>` containing ONLY the `elements` key merges node-by-node; any other key present (`mode`, `root`, `props`, or `export`) makes it a full element replace — restate every field you want kept. In that node-by-node form, `elements.<name>.elements.<id>: null` deletes one node. Literal null nodes are rejected in a full element replace so they cannot be mistaken for deletions.\n- otherwise every supplied `pages.<page>.elements.<id>`, `pages.<page>.params.<param>`, `actions.<name>`, `models.<name>.columns.<col>`, and `models.<name>.sources.<src>` value is atomic and replaces that complete declaration; `null` deletes it (deleting a source detaches its rows — see the source lifecycle above).\n\n`pages.<page>: null` deletes a page, except the current `defaultPage` cannot be deleted. `root`, `title`, and top-level `defaultPage` use ordinary merge semantics. A top-level `page` patch is rejected with guidance to patch `pages.<name>` instead.\n\nBecause elements, param declarations, actions, and columns are atomic, include the complete desired object when changing one. For example, changing only a Table's `emptyMessage` requires sending its full `{ \"type\": \"Table\", \"props\": ... }` element. The merged result is validated as a whole; on failure, read `issues[]`, correct the paths, and retry without assuming anything was written.\n\n```json\n{\n \"appId\": \"<app-id>\",\n \"description\": null,\n \"definition\": {\n \"models\": {\n \"idea\": {\n \"columns\": {\n \"priority\": { \"kind\": \"enum\", \"enum\": [\"low\", \"high\"], \"default\": \"low\" }\n }\n }\n },\n \"queries\": { \"openIdeas\": null },\n \"pages\": {\n \"main\": {\n \"elements\": {\n \"title\": { \"type\": \"Heading\", \"props\": { \"text\": \"Prioritized ideas\", \"level\": \"h1\" } }\n }\n }\n }\n }\n}\n```\n\n## Worked example: ideas tracker\n\nCreate this with `app-upsert`:\n\n```json\n{\n \"name\": \"Ideas Tracker\",\n \"description\": \"Collect and manage product ideas\",\n \"definition\": {\n \"models\": {\n \"idea\": {\n \"columns\": {\n \"title\": { \"kind\": \"string\", \"required\": true },\n \"status\": { \"kind\": \"enum\", \"enum\": [\"open\", \"done\"], \"default\": \"open\" }\n }\n }\n },\n \"queries\": {\n \"allIdeas\": { \"model\": \"idea\", \"sort\": { \"column\": \"createdAt\", \"dir\": \"desc\" } }\n },\n \"actions\": {\n \"triage\": { \"kind\": \"task\", \"prompt\": \"Review current ideas and recommend the next one to pursue\" }\n },\n \"pages\": {\n \"main\": {\n \"root\": \"root\",\n \"elements\": {\n \"root\": {\n \"type\": \"Container\",\n \"props\": { \"direction\": \"column\", \"gap\": \"lg\" },\n \"children\": [\"heading\", \"formCard\", \"ideasCard\", \"triage\"]\n },\n \"heading\": { \"type\": \"Heading\", \"props\": { \"text\": \"Ideas\", \"level\": \"h1\" } },\n \"formCard\": {\n \"type\": \"Card\",\n \"props\": { \"title\": \"Add an idea\" },\n \"children\": [\"ideaForm\"]\n },\n \"ideaForm\": {\n \"type\": \"Form\",\n \"props\": {\n \"id\": \"newIdea\",\n \"fields\": [{ \"name\": \"title\", \"label\": \"Title\", \"required\": true }],\n \"submitLabel\": \"Add\",\n \"onSubmit\": [{\n \"action\": \"app.mutate\",\n \"params\": { \"model\": \"idea\", \"op\": \"create\", \"values\": { \"$form\": \"\" } }\n }]\n }\n },\n \"ideasCard\": {\n \"type\": \"Card\",\n \"props\": { \"title\": \"All ideas\" },\n \"children\": [\"ideasTable\"]\n },\n \"ideasTable\": {\n \"type\": \"Table\",\n \"props\": {\n \"data\": { \"$state\": \"/queries/allIdeas/data\" },\n \"loading\": { \"$state\": \"/queries/allIdeas/loading\" },\n \"error\": { \"$state\": \"/queries/allIdeas/error\" },\n \"columns\": [\n { \"key\": \"title\", \"label\": \"Title\" },\n { \"key\": \"status\", \"label\": \"Status\", \"kind\": \"badge\", \"tones\": { \"open\": \"info\", \"done\": \"success\" } }\n ],\n \"rowActions\": [{\n \"label\": \"Delete\",\n \"variant\": \"destructive-outline\",\n \"actions\": [{ \"action\": \"app.mutate\", \"params\": { \"model\": \"idea\", \"op\": \"delete\", \"rowId\": { \"$row\": \"id\" } } }]\n }]\n }\n },\n \"triage\": {\n \"type\": \"Button\",\n \"props\": { \"label\": \"Ask swarm to triage\", \"variant\": \"secondary\" },\n \"on\": { \"press\": [{ \"action\": \"app.action\", \"params\": { \"name\": \"triage\" } }] }\n }\n }\n }\n },\n \"defaultPage\": \"main\"\n }\n}\n```\n\nThe tool returns `{ appId, url: \"/apps/<id>\" }`. Open that URL to verify the live UI, then continue with the read-patch-validate loop above.\n\n## View and screenshot a live app\n\n- An app renders at `<dashboard>/apps/<id>`. Two view modes are set by query string: `?mode=full` (full-viewport overlay with a slim header) and `?mode=chromeless` (only the rendered page, the embed surface). `app.navigate` keeps only `?mode`.\n- Logged-in browser: a fresh dashboard has no connection and redirects to /setup. Seed the localStorage key `agent-swarm-connections` with `{connections:[{id,name,apiUrl,apiKey}],activeId}`. Build the value from an env var and pipe it to `agent-browser eval --stdin`, so the key never reaches a command line. Reload, then pick a user in the \"Who are you?\" dialog.\n- Apps scroll inside an inner canvas, so a full-page screenshot captures only the viewport. Scroll the canvas or raise the viewport height. For mobile, set a 375px-wide viewport before the screenshot.\n- App definitions: `GET /api/apps` and `GET /api/apps/<id>` with the API key as Bearer.\n- Follow-up edit to a live app: list in the brief the control numbers the edit must not move, and assert them from the backing script before publishing. Take a signed-in screenshot of each changed selector at 1440px wide, save the shots to agent-fs, critique, and repeat at least 3 times. If the backing script caches results, bump its version or force a fresh run so the shots show new data.\n\n\n### Rows from agents and scripts\n\nEvery row tool requires `appId` and `model`. Scripts use underscore names, such as `ctx.swarm.app_row_create` and `ctx.swarm.app_rows_list`.\nResults use the usual `data` wrapper, so scripts read `res?.data ?? res`. Rows contain system fields and declared columns at the top level.\nWrites record `agent:<agentId>` in `createdBy` and `updatedBy`. Source projections and join keys remain read-only. Each row input must fit within 1 MiB.\n\n`app-rows-list` accepts `equals`, `sort: \"<column>:asc|desc\"`, and `limit` from 1 to 1000 (default 200).\nEquality filters accept declared columns and system columns. Filter values must match the column kind and use strict equality.\n`app-rows-delete` requires at least one equality filter and accepts a limit from 1 to 1000 (default 1000).\n\n`app-action-run` returns the REST action body under `data`. Script actions run with the script owner's identity and receive the verified invoking agent as `caller`.\nScripts and extensions cannot invoke `app-action-run`.\n";
|
|
6186
|
+
var content_default2 = "# Swarm Apps\n\nSwarm Apps are persistent, agent-authored internal applications: a schema-backed row store, named queries, custom script/task actions, and a validated json-render interface served at `/apps/<id>`. Use an app when people need to view and change live records; use `create_page` for a static snapshot.\n\n> **Capability gate**: app tools are available when the server's `CAPABILITIES` includes `pages` (for example `CAPABILITIES=core,task-pool,pages`). There is no separate `apps` capability. If these tools are absent from your MCP list, this is why.\n\n## Tools and iteration loop\n\n| Tool | Use it for |\n|---|---|\n| `app-list` | Discover apps by id, name, description, and timestamps. Definitions are omitted. |\n| `app-get` | Read one complete stored definition before changing it. |\n| `app-upsert` | Create an app, or replace an existing app's entire definition when you intentionally have the full desired state. |\n| `app-patch` | Make a focused change while preserving everything not mentioned in the patch. Prefer this for iteration. |\n| `app-query` | Run one declared named query and return its rows. Use this when an agent or saved script needs to consume app data rather than render the app UI. |\n| `app-rows-list` | List rows with typed equality filters, sorting, and a limit. Returns `{ rows, total }`. |\n| `app-row-get` | Read a row by `rowId`. Returns `{ row }`. |\n| `app-row-create` | Create a row from `values`. Returns `{ row }`. |\n| `app-rows-create` | Create 1 to 500 rows from a `rows: values[]` array, within 10 MiB. Returns `{ rows }`. |\n| `app-row-patch` | Patch a row by `rowId` with `values`. Returns `{ row }`. |\n| `app-row-delete` | Delete a row by `rowId`. Returns `{ ok: true }`. |\n| `app-rows-delete` | Delete matching rows with a non-empty `equals` filter. Returns `{ deleted }`. |\n| `app-action-run` | Run a declared script, task, or sync action with `{ appId, action, input? }`. Agents can invoke it through MCP only. |\n| `app-history` | List definition snapshots and their compact model/column digests before choosing a restore point. |\n| `app-diff` | Show a unified definition diff; defaults to newest snapshot → CURRENT. |\n| `app-rollback` | Restore a snapshot through the normal schema-migration engine. |\n\nFor every edit: `app-get` -> modify the smallest coherent subtree -> `app-patch` -> if rejected, fix every returned `issues[]` entry (`path` + `message`) and retry. Validation happens before storage, so a rejected update leaves the saved app unchanged. Do not guess at the current definition or use `app-upsert` with a partial definition.\n\n`app-patch` can also change `name`; omit `description` to preserve it or pass `description: null` to clear it.\n\n### Reading an app's callable surface\n\nOne `app-get` returns everything needed to drive an app you did not write: `definition.queries` are runnable with `app-query` (supply every `$param` the query references), and `definition.actions` are invocable with `app-action-run` or `POST /api/apps/<id>/actions/<name>` (a `script` action runs its script, a `task` action spawns a task, a `sync` action refreshes sources). Read the query parameters and action arguments from the definition before invocation. Do not guess parameter names.\n\n### Verified action caller\n\nScript actions receive `caller` as a reserved, server-stamped argument. The server overwrites `caller` from definition defaults and request input.\n\n```typescript\ntype AppActionCaller = {\n kind: \"user\" | \"agent\" | \"operator\" | \"guest\";\n id: string | null;\n name?: string;\n pageId?: string;\n};\n```\n\nThe caller identifies the authenticated user or agent. Operator and guest callers have `id: null`. A signed page session adds `pageId`.\nWith the swarm key, a registered `X-Agent-ID` produces an agent caller and `agent:<id>` row provenance. Missing or unknown agent IDs remain operator callers.\nTask actions include the same caller as server-verified JSON in the task prompt. Script execution still uses the saved script owner's bindings.\n\n### Verified action caller\n\nScript actions receive `caller` as a reserved, server-stamped argument. The server overwrites `caller` from definition defaults and request input.\n\n```typescript\ntype AppActionCaller = {\n kind: \"user\" | \"agent\" | \"operator\" | \"guest\";\n id: string | null;\n name?: string;\n pageId?: string;\n};\n```\n\nThe caller identifies the authenticated user or agent. Operator and guest callers have `id: null`. A signed page session adds `pageId`.\nWith the swarm key, a registered `X-Agent-ID` produces an agent caller and `agent:<id>` row provenance. Missing or unknown agent IDs remain operator callers.\nTask actions include the same caller as server-verified JSON in the task prompt. Script execution still uses the saved script owner's bindings.\n\n## Safe schema evolution and rollback\n\nPrefer **hiding** a column over deleting it. A hidden column's values stay on every existing row, so an unhide or rollback can make it visible again. Hiding is metadata-only: hidden columns cannot be used in queries, page bindings, `app.mutate` values, or new row writes, and `required` is ignored while hidden. Row GETs still expose the historical value for backward compatibility.\n\nDo not reuse a hidden column name. The name remains held until you either unhide it or explicitly purge its stored values. To permanently remove a column, delete its declaration with `null` and include `{ \"columnName\": { \"purge\": true } }` in the same `migration` object. That is deliberately one-shot and irreversible for row data.\n\nSchema-changing writes can carry this sibling `migration` object:\n\n```json\n{\n \"migration\": {\n \"priority\": { \"from\": \"flag\", \"map\": { \"urgent\": \"high\", \"watch\": \"low\" }, \"else\": \"none\" },\n \"status\": { \"from\": \"flag\", \"map\": { \"watch\": \"watching\", \"done\": \"done\" }, \"else\": \"open\" },\n \"oldColumn\": { \"purge\": true },\n \"count\": { \"coerce\": true, \"else\": 0 },\n \"owner\": { \"set\": \"unassigned\" }\n }\n}\n```\n\nUse `set` to backfill a changed column, `from` with optional `map`/`else` to derive it from one existing (including hidden) column, `coerce` for a kind change, and `purge` only for explicit destruction. Non-purge directives target a column changed in that same write. The server fails loudly before writing when a lossy change needs a directive: inspect every returned `issues[]` item, including its per-value row counts, then choose an explicit policy and retry. Never guess a migration just to get a 200.\n\n**Source lifecycle** rides the same engine with the same all-or-nothing guarantees. Free: adding a source, adding a NEW column with a `source` binding, binding an existing column that holds zero values, changing a binding's `field`/`transform`, and changing a source's `args`/`config`/`connection`/`scriptId` (window changes churn staleness on the next pass — expect `stale` flips when narrowing). Rejected with a path-bearing issue: changing `joinKey` (immutable — remove the source and add it again), changing `connector` while the source owns rows (row count in the message), and binding an existing column that already holds values (purge it — or add a fresh column — and bind that instead; a sync pass would otherwise overwrite the data). Removing a source **detaches** its rows: values survive as ordinary columns, the `source`/`syncedAt`/`stale` envelope is stripped, and the report counts them in `detachedRows`. A patch removing a source must drop its column bindings in the same call — a dangling `source.of` is rejected before anything is written.\n\nSync never adopts rows it does not own: a pulled record whose join key matches an unowned row creates a second, source-owned row. Adding a source over a join-key column that already holds values therefore duplicates those records on the first pass — and remove-source → re-add duplicates for the same reason, because detached rows keep their values. To convert a hand-maintained model into a synced one, purge the old rows first or use a fresh join-key column.\n\nFor example, split a legacy `flag` into `priority` and `status` without losing data in one patch: add the two columns, map both from `flag`, update every affected query/page binding, then hide `flag`.\n\n```json\n{\n \"appId\": \"<app-id>\",\n \"definition\": {\n \"models\": {\n \"ticket\": {\n \"columns\": {\n \"priority\": { \"kind\": \"enum\", \"enum\": [\"none\", \"low\", \"high\"] },\n \"status\": { \"kind\": \"enum\", \"enum\": [\"open\", \"watching\", \"done\"] },\n \"flag\": { \"kind\": \"string\", \"hidden\": true }\n }\n }\n }\n },\n \"migration\": {\n \"priority\": { \"from\": \"flag\", \"map\": { \"urgent\": \"high\", \"watch\": \"low\" }, \"else\": \"none\" },\n \"status\": { \"from\": \"flag\", \"map\": { \"watch\": \"watching\", \"done\": \"done\" }, \"else\": \"open\" }\n }\n}\n```\n\nEvery schema migration returns `{ scanned, backfilled, coerced, mapped, elsed, purgedValues, idxRebuilt, detachedRows, orphanFields }`. Read `orphanFields`: it reports extra row fields that predate or no longer appear in the definition; they are preserved until an explicit purge. `schemaVersion` is server-managed: never set it in an input patch or try to use it as application data.\n\nEvery successful definition write snapshots the previous definition. Use `app-history` to select a version, `app-diff({ appId, from: <version> })` to inspect it against CURRENT, then `app-rollback({ appId, version: <version>, migration? })` to restore it. Rollback is a forward migration over live rows, not row-level time travel: it creates a new pre-rollback snapshot and may reject a lossy restore with the same migration directives and counts. A rejected rollback changes nothing.\n\n### Per-user configuration\n\n`userConfig` declares up to 20 typed settings in the versioned definition; its values are deliberately stored outside the definition, separately for each user (and the operator dashboard). Each field is `{ \"kind\": \"string\" | \"number\" | \"boolean\" | \"date\" | \"enum\", \"default\"?: ..., \"enum\"?: [...], \"label\"?: \"...\" }`. Defaults follow column rules, including enum membership; `required` is not allowed. A missing or obsolete stored value reads as its default, or `null` when no default is declared.\n\nUse `{ \"$state\": \"/user/<field>\" }` on a page for the read-only current-user binding; the field must be declared and the path cannot continue below it. Pure and bound reusable elements cannot bind `/user` directly — pass the value through an element prop from the consuming page. An agent acting on an owned user-requested task reads and edits that requester's preferences. `userConfig` schema changes are always migration-compatible and appear in migration reports; no migration directives are needed. Rollback restores the historical schema but never alters saved per-user values, so values can resurface when a field returns.\n\n### Theming\n\nAn optional top-level `\"theme\"` sets the preset the dashboard applies to the app's rendered canvas — a lowercase slug validated by shape only. The preset catalog ships with the dashboard (`apps/ui/src/lib/themes.ts` + `theme-classics.ts` — update this list and the `app-upsert` tool description when it changes); current ids: `hive` (the stock look), the accent/field presets `meadow`, `iris`, `rose`, `cobalt`, `ember`, `carbon`, and the classic editor/platform presets `github`, `vscode`, `material`, `solarized`, `tokyo`, `monokai`, `gruvbox`, `catppuccin`. Action and destructive tones are never themed; status tones are themed ONLY by the classic presets (their identity includes a status palette — hue semantics stay fixed, success reads green and error reads red everywhere). An id the viewer's dashboard build does not know degrades to the dashboard's own theme, so unknown slugs are safe but pointless. Omit `theme` to inherit whatever theme the viewer runs; `\"hive\"` is an explicit reset to the stock look even inside a themed dashboard.\n\nViewers can override the preset per app from the app's settings drawer; the override rides the reserved `$theme` key of the user-config values (accepted on every app, even without a declared `userConfig` schema, and never visible at `/user/...`). A present override always beats the definition's `theme` — even an unrecognized one, which degrades to the dashboard theme — so treat `theme` as the app's default styling, not a guarantee.\n\nDistinguish the two rollback 400s. A lossy-migration 400 is fixable: copy the required `migration` entries from the message and retry. A target-snapshot validation 400 explicitly says that directives cannot repair that historical definition; use `app-history` and choose a different version.\n\nWhen a schema patch reports stale page/query bindings, repair them in that same patch. Page elements are atomic: replace the complete `pages.<page>.elements.<id>` declaration, removing any reference to a hidden or deleted column, instead of sending only the changed nested prop. This is also the repair move for older apps whose stored page already references an undeclared column; full-definition validation otherwise blocks every patch.\n\n## Definition reference\n\n```json\n{\n \"models\": {\n \"modelName\": {\n \"columns\": {\n \"externalId\": { \"kind\": \"string\" },\n \"title\": { \"kind\": \"string\" }\n }\n }\n },\n \"queries\": { \"queryName\": { \"model\": \"modelName\" } },\n \"actions\": { \"actionName\": { \"kind\": \"task\", \"prompt\": \"Review the current rows and report\" } },\n \"pages\": {\n \"main\": {\n \"root\": \"root\",\n \"elements\": { \"root\": { \"type\": \"Container\", \"props\": {} } }\n }\n },\n \"defaultPage\": \"main\",\n \"theme\": \"ember\"\n}\n```\n\nModel, reusable-element, element-prop, query, action, column, page, and page-param names start with a lowercase letter, contain only letters, numbers, or underscores, and are at most 40 characters. A definition has 0-10 models and at most 20 reusable elements; each declared model has 1-40 columns. Zero-model apps are valid for pure UI utilities as long as their pages and reusable elements validate.\n\n### Reusable elements\n\nTop-level `elements` are versioned inside the app definition and can be reused from the app's own pages or, when explicitly exported, from another app. They are private by default. Each entry declares a `mode`, optional typed `props`, one `root`, and a flat `elements` node map using the same validated tree vocabulary as a page:\n\n```json\n{\n \"elements\": {\n \"statCard\": {\n \"mode\": \"pure\",\n \"export\": true,\n \"props\": {\n \"label\": { \"kind\": \"string\", \"required\": true },\n \"value\": { \"kind\": \"number\", \"default\": 0 }\n },\n \"root\": \"card\",\n \"elements\": {\n \"card\": { \"type\": \"Card\", \"props\": {}, \"children\": [\"label\", \"slot\"] },\n \"label\": { \"type\": \"Metric\", \"props\": { \"label\": { \"$state\": \"/props/label\" }, \"value\": { \"$state\": \"/props/value\" } } },\n \"slot\": { \"type\": \"ElementSlot\", \"props\": {} }\n }\n }\n }\n}\n```\n\n- `pure` elements may read only their declared `/props/<name>` state; `$item` and `$index` are also legal inside repeated nodes. They cannot invoke any action step. A pure tree may contain at most one leaf `ElementSlot` where consumer children are inserted.\n- `bound` elements may additionally read the defining app's declared queries and actions and use its models. Those references are validated against the defining app, including hidden-column rules. Exported bound elements cannot use `app.navigate`; private bound elements may navigate within their defining app.\n- Prop kinds are `string`, `number`, `boolean`, `date`, or `enum`. An enum prop requires a non-empty `enum: [\"value\", ...]` values array. A literal default and each literal consumer value must match the kind and, for enums, be one of those values. A required prop without a default must be supplied.\n\nReference an element with an `ElementRef` node. Omit `app` for same-app reuse; cross-app references name the defining app id and require `export: true`. `instanceKey` is optional. Consumer `children` are accepted only when the target has an `ElementSlot`:\n\n```json\n{\n \"type\": \"ElementRef\",\n \"props\": {\n \"app\": \"<defining-app-id>\",\n \"element\": \"statCard\",\n \"props\": { \"label\": \"Open issues\", \"value\": 12 },\n \"instanceKey\": \"openIssues\"\n },\n \"children\": [\"details\"]\n}\n```\n\nElement references float to the defining app's current definition. `app` and `element` must be literal strings; dynamic references are not supported. The server rejects reference cycles and expansion deeper than five, missing/private targets, invalid props, and a breaking write to an exported element that other apps reference. Removing or unexporting an exported element, changing its mode, removing a declared prop, changing a prop kind, or adding a required prop without a default is breaking; publish the new contract under a new element name. If consumers are abandoned intentionally, retry the PUT, PATCH, or rollback with `\"forceElementBreak\": [\"elementName\"]`. The named consumers will then render an error once element rendering is available; deleting an entire app is deliberately not compatibility-gated.\n\n### Models\n\nEach column is `{ \"kind\": ..., \"required\"?: boolean, \"default\"?: ..., \"index\"?: boolean, \"enum\"?: string[], \"hidden\"?: boolean }`.\n\n| `kind` | Values and indexing |\n|---|---|\n| `string` | String value; set `index: true` only when equality lookups need it. |\n| `number` | Finite number; never indexed. |\n| `boolean` | Boolean; set `index: true` only when equality lookups need it. |\n| `date` | ISO-8601 string; never indexed. |\n| `enum` | String from the non-empty unique `enum` list; always indexed, without `index: true`. |\n| `json` | Any JSON value (object, array, nested) up to 256 KB serialized by default (`APPS_JSON_COLUMN_MAX_BYTES`); never indexed, and a named query cannot `filter` or `sort` on it. Not allowed for `userConfig` fields or element props. |\n\n`default` must match the column kind (and be one of the enum values). `required: true` rejects missing/null values unless a default supplies the value. `id`, `createdAt`, `updatedAt`, `createdBy`, `updatedBy` — and the sync envelope `source`, `syncedAt`, `stale` — are system columns and reserved names. An app created before the sync envelope that declared ordinary columns with these names surfaces `definitionError` after upgrade until they are renamed: patch in a fresh column with a `{from}` migration directive, then purge the old one. Rows always expose `id`/`createdAt`/`updatedAt`; `createdBy`/`updatedBy` record the acting principal (`user:<id>`, `agent:<id>`, `operator`, or `sync:<source>` for engine writes) and are system-managed on every row write.\n\n### Sources and sync\n\nA model may declare up to 4 named `sources` — one-way inbound projections of an external system into its rows. The `script` connector is THE way to bring external data in (GitHub, Linear, any API): a saved script owns the fetch, its auth placeholders, and its paging. `swarm-tasks` is the one native connector, projecting this swarm's own task pool (`config`: `status`/`tags` comma-lists, `agentId`, `assetKey` namespace prefix, `limit` up to 200 — default 100, `includeHeartbeat`). Synced rows are readable by EVERY principal with `app.use` on the app; an unscoped `swarm-tasks` source publishes task prompts (truncated, secret-scrubbed) to all of them, so scope it with `config.assetKey`/`config.agentId` unless that exposure is intended. A user-invoked sync only ever pulls the invoking user's own requested tasks. Script sources run with their run-as identity's credentials — an agent-owned script runs as its owner and only the owner may wire it; an owner-less catalog (global) script runs as the LEAD, so only the lead or the operator can wire or alter such a source (stored references stay valid with their args and connection pinned).\n\n```json\n{\n \"models\": {\n \"issue\": {\n \"columns\": {\n \"issueKey\": { \"kind\": \"string\" },\n \"title\": { \"kind\": \"string\", \"source\": { \"of\": \"gh\", \"field\": \"title\" } },\n \"openedAt\": { \"kind\": \"date\", \"source\": { \"of\": \"gh\", \"field\": \"created_at\", \"transform\": \"date-parse\" } },\n \"note\": { \"kind\": \"string\" }\n },\n \"sources\": {\n \"gh\": { \"connector\": \"script\", \"scriptId\": \"<script-uuid>\", \"joinKey\": \"issueKey\", \"args\": { \"repo\": \"owner/name\" }, \"connection\": \"github\" }\n }\n }\n }\n}\n```\n\n- **Join key**: each source names a `joinKey` — an owned, non-hidden `string` column carrying the external identity. It must not be `required`, must not declare a `default`, and cannot itself be source-bound. A row belongs to at most ONE source (`row.source`).\n- **Column bindings**: `{ \"of\": \"<sourceName>\", \"field\": \"user.login\", \"transform\"? }` projects a (dotted-path) field of the pulled record into the column on every pass. Transforms and their required column kinds: `slug`/`lower`/`upper` → `string`, `cents` → `number` (×100, rounded), `date-parse` → `date` (ISO). A failed transform nulls that one column and warns; it never fails the pass.\n- **Read-only contract**: source-bound columns and the join key reject every direct write (row create/patch/bulk) with a path-bearing issue — mutate them via the source or a sync refresh. Owned columns on the same model stay writable. Every `required` owned column on a sourced model must declare a `default`, because sync-created rows cannot supply it.\n- **Provenance**: synced rows carry `source` (owner), `syncedAt` (last confirmed presence — sortable), and `stale` (true when a complete pull no longer returned the record; clears on reappearance). `syncedAt` advances on every confirmed row WITHOUT touching `updatedAt`; `updatedAt` moves only when projected data actually changed.\n- **`connection`** (optional, script sources): names a connections-system slug the source depends on. It buys write-time validation plus a preflight before every pull; it does not by itself gate egress. Credentials resolve for the sync run-as identity — the script's owner when it has one, else the lead agent — so global connections always work and agent-scoped ones need matching script ownership. The slug is injected into the script's args as `connection`, letting one generic script serve N connections.\n\n**The canonical sync-script contract** (every sync script, always):\n\n```ts\nexport default async function run(args, ctx) {\n // args = { ...source.args, app: { id }, model: \"<model>\", source: \"<sourceName>\", connection?: \"<slug>\" }\n const records = [{ key: \"42\", fields: { title: \"…\", created_at: \"…\" } }];\n return { records, complete: true }; // or return the bare array — a bare array IS a complete snapshot\n}\n```\n\nReturn `Array<{ key, fields }>` or `{ records, complete? }`. `key` is the external identity the engine joins on `joinKey` (numbers are coerced to strings); `fields` is the bag bindings project from. Return `complete: false` whenever the pull window may have missed records — a paged fetch, a hit `limit`, or client-side filtering of a full page. An incomplete pull skips the stale sweep, so rows outside a narrow window are not falsely marked stale; compute `complete` BEFORE any client-side filtering. Hard cap 500 records per pull (over-cap pulls truncate and count as incomplete). An invalid return shape, thrown error, or non-zero exit fails the pass with zero row churn.\n\nThe seeded catalog script `github-issues-pull` is the worked example (placeholder auth via `[REDACTED:GITHUB_TOKEN]`, PR filtering, `complete` computed before filtering). For scheduled refreshes, point a `targetType: \"script\"` schedule at the seeded `app-sync-run` with `scriptArgs: { \"appId\": \"<id>\", \"model\"?, \"source\"? }`.\n\n**Three refresh doors, one engine**: `POST /api/apps/<id>/sync` (body `{ \"model\"?, \"source\"? }`), a `sync` action kind, and the `app-sync` MCP tool (`ctx.swarm.app_sync` from scripts). `{model?, source?}` fans out to every matching (model × source) pair; each pass reports `{ pulled, created, updated, refreshed, unchanged, markedStale, warnings }`. A pair already syncing in this API process short-circuits with `alreadyRunning: true` instead of pulling twice. Each pair's last-pass status (`lastFinishedAt`, counts, `error?`) comes back as `syncStatus` — keyed `\"<model>:<source>\"` — on `GET /api/apps/<id>` and in `app-get` data, so UI and agents can render \"last synced / last error\" without extra calls.\n\n### Freshness and the refresh button\n\nFreshness renders with the existing column kinds — no special components. Give the table `{ \"key\": \"syncedAt\", \"kind\": \"date\" }` and `{ \"key\": \"stale\", \"kind\": \"badge\", \"tones\": { \"true\": \"warning\" } }` columns, declare a `sync` action, and wire a Button to it:\n\n```json\n{\n \"actions\": { \"refresh\": { \"kind\": \"sync\" } },\n \"pages\": {\n \"main\": {\n \"root\": \"root\",\n \"elements\": {\n \"root\": { \"type\": \"Container\", \"props\": {}, \"children\": [\"refreshBtn\", \"issuesTable\"] },\n \"refreshBtn\": {\n \"type\": \"Button\",\n \"props\": { \"label\": \"Refresh\" },\n \"on\": { \"press\": [{ \"action\": \"app.action\", \"params\": { \"name\": \"refresh\" } }] }\n },\n \"issuesTable\": {\n \"type\": \"Table\",\n \"props\": {\n \"data\": { \"$state\": \"/queries/allIssues/data\" },\n \"columns\": [\n { \"key\": \"title\", \"label\": \"Issue\" },\n { \"key\": \"syncedAt\", \"label\": \"Synced\", \"kind\": \"date\" },\n { \"key\": \"stale\", \"label\": \"Stale\", \"kind\": \"badge\", \"tones\": { \"true\": \"warning\" } }\n ]\n }\n }\n }\n }\n }\n}\n```\n\nA `sync` action answers the script-action state shape (`/actions/refresh` → `running` → `ok`/`error`, result under `result.passes`), so the page's queries refetch when it completes. Use `app-query` to READ rows (named queries may filter on `stale`/`source` and sort by `syncedAt`); use `app-sync` to REFRESH sources; never poll a sync door in a loop — schedule it or give people the button.\n\n### Queries\n\nA named query is:\n\n```json\n{\n \"model\": \"idea\",\n \"filter\": { \"status\": \"open\" },\n \"sort\": { \"column\": \"createdAt\", \"dir\": \"desc\" },\n \"limit\": 100\n}\n```\n\n`filter` is a strict AND of equality checks. A filter may target a declared model column or a system column (`id`, `createdAt`, `updatedAt`, `createdBy`, `updatedBy`, and on synced models `source`, `syncedAt`, `stale`) — filtering on `id` is the universal way to select one row for a detail view, and `{ \"stale\": true }` selects rows a complete pull stopped confirming. A literal value must match the column's kind; date filters compare the stored raw ISO string, not parsed instants. Omit `limit` for the default 200 rows, or set an integer from 1 through 1000. `sort.column` is a non-`json` model column, `createdAt`, `updatedAt`, or `syncedAt`; `dir` is `asc` or `desc`. Query runtime state is `{ data, loading, error }` under `/queries/<queryName>`.\n\nA filter value may instead be exactly `{ \"$param\": \"<name>\" }`. At execution, the caller supplies that name and the server coerces its value to the filtered column's kind before equality matching:\n\n```json\n{\n \"queries\": {\n \"issueDetail\": { \"model\": \"issue\", \"filter\": { \"issueId\": { \"$param\": \"issueId\" } }, \"limit\": 1 }\n }\n}\n```\n\nThe placeholder object must contain only `$param`. Missing caller values fail loudly rather than returning unfiltered rows: HTTP reports 400 and `app-query` returns a tool error listing every missing name. Supplying a param name the query does not reference is also an error. The app UI supplies current declared route params automatically. Direct calls use `app-query({ appId, query: \"issueDetail\", params: { issueId: 42 } })` or `ctx.swarm.app_query({ appId, query: \"issueDetail\", params: { issueId: 42 } })`.\n\n### Custom actions\n\nActions are optional (maximum 20) and are invoked from the page with the `app.action` runtime action.\n\n```json\n{\n \"actions\": {\n \"recalculate\": {\n \"kind\": \"script\",\n \"scriptId\": \"00000000-0000-4000-8000-000000000000\",\n \"args\": { \"mode\": \"fast\" }\n },\n \"triage\": {\n \"kind\": \"task\",\n \"prompt\": \"Triage this idea and report a recommendation\"\n }\n }\n}\n```\n\n| Kind | Contract |\n|---|---|\n| `script` | `scriptId` must identify an existing script; optional `args` are defaults. Invocation input overrides same-named defaults, and the runtime also supplies `app: { id }`. |\n| `task` | `prompt` must be non-empty; omit `agentId` to use default (lead) assignment. Only set `agentId` to a real agent UUID. Invocation `input` is included as context for the task prompt. Spawned tasks carry asset key `shared/app:<appId>/action:<name>/`, so a `swarm-tasks` source with `config.assetKey: \"shared/app:<appId>/\"` pulls back the tasks this app spawned. |\n| `sync` | Optional `model` and `source` narrow which (model × source) pairs refresh; omitting both syncs every pair the app declares. At least one pair must exist. Answers the script-kind state shape with per-pass results under `result.passes`. |\n\n- Invocation state lands at `/actions/<name>` as `{ status, result?, error?, taskId?, taskStatus? }`, where `status` is `\"running\"`, `\"ok\"`, or `\"error\"`.\n\nUse a task action named Tackle when a person should hand one issue row to the swarm. Pass the complete current row under `input.issue`; `{ \"$row\": \"\" }` resolves to the whole row and that context lands in the task prompt.\n\n```json\n{\n \"actions\": {\n \"tackle\": { \"kind\": \"task\", \"prompt\": \"Tackle this issue. Inspect the supplied issue context, do the work, and report the outcome.\" }\n },\n \"pages\": {\n \"main\": {\n \"root\": \"issuesTable\",\n \"elements\": {\n \"issuesTable\": {\n \"type\": \"Table\",\n \"props\": {\n \"data\": { \"$state\": \"/queries/allIssues/data\" },\n \"columns\": [{ \"key\": \"title\", \"label\": \"Issue\" }],\n \"rowActions\": [{\n \"label\": \"Tackle\",\n \"actions\": [{\n \"action\": \"app.action\",\n \"params\": { \"name\": \"tackle\", \"input\": { \"issue\": { \"$row\": \"\" } } }\n }]\n }]\n }\n }\n }\n }\n },\n \"defaultPage\": \"main\"\n}\n```\n\nFor direct MCP calls, `app-query` accepts `{ appId, query, params? }` and returns rows from that declared named query; every supplied param must be referenced by a `$param` filter in that query. Use it when an agent needs to read app state without scraping the UI.\n\nSaved scripts use the generated SDK name `ctx.swarm.app_query({ appId, query, params? })`. Use `app_query` in scripts or workflows that turn current app rows into reports, digests, or follow-up work. Saved scripts also get generated per-app types automatically: each app contributes an `App_<Name>` namespace with one row interface per model (enum columns as literal unions) and a typed `app_query` overload per named query, all visible via `script-query-types`. Scripts can create, list, get, patch, and delete rows with the seven row tools. Actions remain REST/MCP-only and have no script SDK method.\n\n### Pages, routes, and page trees\n\n`pages` is a map of independently validated page trees and `defaultPage` must name one entry. Each page may also declare a display `title` and typed route `params`. On every non-default page the runtime automatically renders breadcrumbs (`<default page title> › <current page title>`, first crumb navigates back) — including in chromeless embeds — so a detail page does NOT need its own back button; give pages good `title`s instead. This complete pattern connects a table row to a typed detail route and its parameterized query:\n\n```json\n{\n \"models\": {\n \"issue\": {\n \"columns\": {\n \"issueId\": { \"kind\": \"number\" },\n \"title\": { \"kind\": \"string\" },\n \"status\": { \"kind\": \"string\" }\n }\n }\n },\n \"queries\": {\n \"allIssues\": { \"model\": \"issue\" },\n \"issueDetail\": {\n \"model\": \"issue\",\n \"filter\": { \"issueId\": { \"$param\": \"issueId\" } },\n \"limit\": 1\n }\n },\n \"pages\": {\n \"issues\": {\n \"title\": \"Issues\",\n \"root\": \"issuesTable\",\n \"elements\": {\n \"issuesTable\": {\n \"type\": \"Table\",\n \"props\": {\n \"data\": { \"$state\": \"/queries/allIssues/data\" },\n \"columns\": [\n { \"key\": \"title\", \"label\": \"Issue\" },\n { \"key\": \"status\", \"label\": \"Status\", \"kind\": \"badge\" }\n ],\n \"rowActions\": [{\n \"label\": \"Open\",\n \"actions\": [{\n \"action\": \"app.navigate\",\n \"params\": {\n \"page\": \"detail\",\n \"params\": { \"issueId\": { \"$row\": \"issueId\" } }\n }\n }]\n }]\n }\n }\n }\n },\n \"detail\": {\n \"title\": \"Issue detail\",\n \"params\": { \"issueId\": { \"kind\": \"number\", \"required\": true } },\n \"root\": \"detail\",\n \"elements\": {\n \"detail\": {\n \"type\": \"DetailList\",\n \"props\": {\n \"data\": { \"$state\": \"/queries/issueDetail/data/0\" },\n \"fields\": [{ \"key\": \"title\", \"label\": \"Issue\" }]\n }\n }\n }\n }\n },\n \"defaultPage\": \"issues\"\n}\n```\n\nDrawer variant: declare a route param on the containing page, set the Drawer `props.param` to that literal param name, and use the same `app.navigate` shape targeting that page with the row id assigned to the Drawer's param. The Drawer then opens from route state without a separate action contract.\n\nParam `kind` is `string` by default, or `number` / `boolean`; `required` defaults to false. The names `mode`, `apiUrl`, `apiKey`, `email`, and `name` are reserved. `/apps/<id>` renders `defaultPage`; `/apps/<id>/p/<page>?issueId=42` is the shareable deep link for another page. Browser navigation is client-side, so app state and polled data stay warm across page changes.\n\nWithin each page, `root` names one entry in its non-empty `elements` map. Elements are a flat, single-parent tree: ids are map keys, `children` contains ids (not nested elements), all elements must be reachable from `root`, and cycles, missing children, or shared children are invalid. Element and Form/UI ids are page-local. Element keys are `type`, `props`, `children`, `on`, `visible`, `repeat`, and `watch`; only components with a `default` child slot accept `children` (Stack, Grid, Split, Tabs, Container, Card, Section, and Drawer). `watch` maps state paths to an action step or chain.\n\n## Component catalog (all 34)\n\nProps reject unknown keys. A `{\"$state\":\"...\"}` binding may replace a literal prop value at any depth.\n\nIcons: every `icon` prop (and the `Icon` block) takes one name from a curated lucide set: `plus`, `refresh-cw`, `search`, `filter`, `download`, `upload`, `external-link`, `copy`, `check`, `x`, `trash-2`, `pencil`, `eye`, `calendar`, `clock`, `user`, `users`, `bot`, `git-pull-request`, `git-branch`, `github`, `slack`, `mail`, `link`, `file-text`, `folder`, `database`, `zap`, `sparkles`, `flag`, `star`, `tag`, `alert-triangle`, `info`, `circle-check`, `trending-up`, `trending-down`, `dollar-sign`, `activity`, `rocket`. Any other name is rejected.\n\nTones: Badge, Metric and Progress `tone` take `neutral`, `success`, `active`, `error`, `info`, `pending`, `warning`, or `paused`.\n\n| Component | Required props | Optional props / values |\n|---|---|---|\n| `Stack` | none | Primary layout; `direction: \"column\", \"row\"`, `gap`, `padding: \"none\", \"xs\", \"sm\", \"md\", \"lg\", \"xl\"`; `align: \"start\", \"center\", \"end\", \"stretch\"`, `justify: \"start\", \"center\", \"end\", \"between\"`, `wrap`; `motion: \"rise\"` (default), `\"fade\"`, or `\"none\"` (see Motion below); `fill` (grow to fill a `fill`-aware parent; see Layout below); has the `default` child slot. |\n| `Grid` | none | Responsive `columns`: integer 1 through 6, or `{ base, sm, md, lg }` counts 1 through 6; `gap` uses the Stack spacing values; has the `default` child slot. |\n| `Split` | none | `ratio: \"1-1\", \"1-2\", \"2-1\", \"1-3\", \"3-1\"`, `gap`, `collapseBelow: \"sm\", \"md\", \"lg\"`, `reverse`, `fill` (same contract as Stack); has the positional `default` child slot. |\n| `Divider` | none | `label`; no children. |\n| `Tabs` | `id`, `tabs` | `tabs` entries are `{ key, label?, icon?, count? }` (`count` is a number shown as a small pill); `defaultTab`; `variant: \"line\"` (default) or `\"segmented\"` (a compact segmented control, same state and warm panels); has the positional `default` child slot. |\n| `Container` | none | Legacy layout primitive; prefer Stack. `direction: \"row\", \"column\"`, `gap: \"none\", \"sm\", \"md\", \"lg\"`; has the `default` child slot. |\n| `Card` | none | `title`, `description`; has the `default` child slot. |\n| `Heading` | `text` | `level: \"h1\", \"h2\", or \"h3\"`. |\n| `Text` | `content` | `tone: \"default\" or \"muted\"`. |\n| `Markdown` | `content` | Rendered Markdown for help, instructions, and rich prose; no children. |\n| `SearchInput` | `id` | `placeholder`, `label`, `shortcut` (one key, usually `\"/\"`: shows a keycap and focuses the field); writes debounced text to `/ui/<id>/value`; no children. |\n| `Select` | `id`, `options` | `options` are strings or `{ value, label?, icon?, hint? }`; `placeholder`, `label`, `clearable`, `searchable` (type to filter; on by default past 8 options), `searchPlaceholder`, `multiple`, `defaultValue` (seeds `/ui/<id>/value` once, before any interaction; a one-entry array with `multiple`); writes a string or null to `/ui/<id>/value`, or with `multiple` a string array (empty = nothing picked); no children. |\n| `NumberInput` | `id` | `label`, `placeholder`, `min`, `max`; a text field with a decimal keyboard that writes the parsed number to `/ui/<id>/value` on every change (no debounce), or null when the field is empty; non-numeric text (a bare `-` mid-edit, letters) leaves the stored value untouched; `min`/`max` clamp the stored value and the visible text snaps to it when the field loses focus; no children. |\n| `Chips` | `id`, `options` | Toggle chips for quick filters. `options` are strings or `{ value, label?, icon? }`; `multiple`; `label` (group name for screen readers). Writes a string or null to `/ui/<id>/value`, or a string array with `multiple`; pressing the selected chip clears it. |\n| `Button` | `label` | `variant: \"default\", \"secondary\", \"outline\", \"ghost\", \"destructive\", or \"destructive-outline\"` (use `destructive-outline` for standalone deletes); `disabled` (constant or bound boolean); `busyWith: \"<actionName>\"` swaps the label for a spinner while that custom action's `/actions/<name>/status` is `\"running\"` (presses are ignored), then shows a check and `successLabel` for about 1.5s when it turns `\"ok\"`; `icon`, `iconPosition: \"start\" or \"end\"`; `size: \"sm\", \"default\", or \"lg\"`; `shortcut` (one key, shown as a keycap, presses the button); `hint` (tooltip); dispatch with element-level `on.press`. |\n| `Metric` | `label`, `value` | The dashboard stat tile. `value` is string or number (empty renders an em dash); bind `loading` to `/queries/<name>/loading` for a skeleton while the query loads; `icon` with `tone` tinting its tile; `detail` (a line under the value); `trend: { direction: \"up\", \"down\", or \"flat\", label }`; `info` (explanation tooltip next to the label). |\n| `Alert` | `message` | `title`, `tone: \"info\", \"success\", \"warning\", or \"error\"`, `icon` (overrides the tone icon), `actionLabel` (a button inside the callout that emits `press`; wire it with `on.press`). |\n| `Badge` | `text` | `text` is string or number; `tone`; `icon` replaces the dot; `variant: \"tag\"` (default uppercase chip), `\"status\"` (sentence-case pill), `\"count\"` (numeric pill), or `\"beta\"` (the BETA tag); `status` (a lifecycle value renders the dashboard status chip); `hint` (tooltip; the beta tag's explanation). |\n| `Icon` | `name` | `size: \"sm\", \"md\", or \"lg\"`, `tone: \"default\", \"muted\", \"success\", \"warning\", \"error\", or \"info\"`, `label` (accessible name; decorative without it). |\n| `Progress` | `value` | `max` (default 100), `tone`, `label`, `variant: \"bar\"` (default) or `\"ring\"` (the dashboard progress ring; ignores `tone`), `showValue` (default true: percent for a bar, `value / max` for a ring). |\n| `EmptyState` | `title` | `icon` (default inbox), `body`, `actionLabel` (a button that emits `press`; wire it with `on.press`). |\n| `CopyButton` | `value` | `value` is string or number; `label` is the visible text, icon-only without it. Renders nothing for an empty value. |\n| `Kbd` | `keys` | Keycaps, one per string (`[\"⌘\", \"K\"]`). Display only. |\n| `Skeleton` | `shape` | `shape: \"line\", \"block\", \"rows\", \"metric\", or \"card\"`; `count` (1-20: lines, rows, tiles, or cards); `height` (px, `block` only, default 128). Show it with `visible` bound to a query's `loading`. |\n| `Spinner` | none | `variant: \"ring\", \"ellipsis\", or \"bars\"` (default: the dashboard spinner), `size: \"sm\" or \"md\"`, `label` (visible text and accessible name). |\n| `AvatarStack` | `users` | `users` are display names (initials avatars); `max` (default 4) before a `+N` count. Hover lists every name. |\n| `Section` | `title` | `description`, `collapsible`, `defaultOpen` (default true); has the `default` child slot. |\n| `Table` | `columns` | `data`, `loading`, `error`, `emptyMessage`, `emptyState`, `rowActions`, `onRowClick`, `selectedId`, `rowId`, `mobile`, `mobileFields`, `search`, `filters`; see below. |\n| `Form` | `id`, `fields`, `onSubmit` | `title`, `submitLabel`; see below. |\n| `Drawer` | `param` | `title`, `description`, `side: \"right\" or \"left\"`, `size: \"sm\", \"md\" (default), \"lg\", or \"xl\"` (about 480/660/900/1010px at 1440px; grows with the viewport up to 640/960/1280/1600px; full width on phones), `actions` (sticky footer buttons; see below); has the `default` child slot. |\n| `DetailList` | `fields` | `data`, `emptyMessage`, `columns: 1 or 2`, `loading` (bind `/queries/<name>/loading` for skeleton fields while loading); renders one record without edit controls; see below for field kinds. |\n| `CardGrid` | `titleKey` | `data`, `loading`, `error`, `emptyMessage`, `imageKey`, `fields`, `columns`, `onClick`; one card per row for record browsers; see below. |\n| `Kanban` | `columnField`, `columns`, `titleField` | `data`, `loading`, `cardFields`, `idField` (default `id`), `onMove`, `onCardClick`; see below. |\n| `Calendar` | `startField`, `titleField` | `data`, `loading`, `endField`, `idField`, `onSelect`; read-only month grid; see below. |\n| `ContributionGraph` | none | `data`, `loading`, `dateField` (default `date`), `countField`, `days` (7-730, default 365); read-only activity heatmap. |\n| `Tree` | `id`, `nodes` | `label`, `maxHeight` (default `480px`), `fill`; collapsible outline of a doc-shaped value that writes the clicked node key to `/ui/<id>/value`; see below. |\n\nTable details:\n\n- `columns[]`: `{ key, label?, kind?, tones?: {value: badgeTone}, width?: number, pinned?: \"left\" or \"right\", align?: \"start\" or \"end\", truncate?: \"end\" or \"middle\", icon?, tooltip?: \"<row field>\", max?: number, format?: \"<template>\", onChange?: chain }`. `kind` is `text`, `string`, `enum`, `number`, `boolean`, `date`, `badge`, `status`, `user`, `agent`, `relative`, `duration`, `progress`, `copy`, `switch`, `link`, or `json`; `string` and `enum` render as text, and `json` renders a truncated single-line `JSON.stringify` preview. Pin id/actions columns so they survive horizontal scroll on narrow viewports.\n - `status`: lifecycle values (`pending`, `in_progress`, `running`, `completed`, `failed`, `cancelled`, `paused`, `reviewing`, ...) render the dashboard status chip with its icon; other values use `tones` like `badge`.\n - `user` / `agent`: avatar plus name. `relative` (and `date`): smart time, exact time on hover. `duration`: milliseconds as `1m 23s`. `progress`: a bar against `max` (default 100). `copy`: the value plus a copy button. `link`: an http(s) URL opening in a new tab (other values render as text).\n - `switch`: a toggle. `onChange` runs with `$row` = the row with this column already set to the new value, e.g. `[{ \"action\": \"app.mutate\", \"params\": { \"model\": \"m\", \"op\": \"update\", \"rowId\": { \"$row\": \"id\" }, \"values\": { \"enabled\": { \"$row\": \"enabled\" } } } }]`. Without `onChange` the switch is read-only.\n - Numbers, durations and progress right-align by default. `truncate: \"middle\"` keeps both ends of ids and paths. `icon` shows in the header; `tooltip` names a row field shown on hover.\n - `format`: a `{{field}}` template that combines several row fields into one cell, e.g. `\"{{name}} ({{status}})\"`. Placeholder names match `[\\w.]+` (letters, digits, `_` and `.`; no hyphens or spaces). Each placeholder reads that row field as-is (a dotted name such as `{{owner.name}}` is a literal column name, not a nested path); a missing or null field becomes an empty string and an object or array becomes a JSON preview cut to 120 characters. The whole cell is capped at 500 characters. A templated column ignores `kind`, shows the same text in the phone list, and sorts by the composed text. `search` and `filters` still read the column's `key`.\n- `onRowClick`: action chain with `$row` / `$rowIndex`, run when a row is clicked (or Enter on a focused cell). Pair it with `app.navigate` that sets a Drawer param, and bind `selectedId` to `{ \"$state\": \"/route/params/<param>\" }` so the open row stays highlighted. `rowId` names the id field (default `id`).\n- Phones (below 768px): the table renders as a card list. `mobileFields: { title, subtitle?, meta?: string or string[], status? }` picks the columns; by default the first text column is the title and the first `status`/`badge` column the chip. `mobile: \"scroll\"` keeps the grid; it is the default when the table has `rowActions` but no `onRowClick`.\n- `emptyState: { icon?, title, body? }` replaces the plain empty message. A search or filter that hides every row shows its own \"No rows match\" state.\n- `pagination`: boolean; defaults to auto-enabling past 200 rows (set `false` to force one scroll region). `density: \"compact\"` tightens row height for dense readouts.\n- `rowActions[]`: `{ label, variant?, icon?, placement?: \"inline\" or \"menu\", confirm?, actions }`. `placement: \"menu\"` moves the action into the row's More menu. Variants add `destructive-outline` to the Button variants. `destructive` and `destructive-outline` confirm by default; customize with `{ \"title\": ..., \"description\": ..., \"confirmLabel\": ... }`, or use a bare `confirm` string as the dialog description (use `confirm: false` only for a reversible action).\n- `search`: optional string; case-insensitive substring matching across the listed string and number columns. Bind a SearchInput value for client-side search.\n- `filters`: optional record of per-column string, number, boolean, null, or array values. An array means \"the cell is one of these\" (what a `multiple` Chips or Select writes). Null, empty strings, empty arrays, or absent values disable one filter. Bind Select or Chips values for client-side filters.\n\nKanban details:\n\n- One card per `data` row, grouped by the row's `columnField` value. `columns[]` are plain strings or `{ id, label? }`, in display order; rows matching no column are hidden.\n- `cardFields[]` uses the DetailList field shape (`{ key, label?, kind?, tones? }`, same kinds) for extra lines under the title.\n- `onMove` is an action chain run when a card is dropped in another column. `$row` is the moved row with `columnField` already set to the new column: `[{ \"action\": \"app.mutate\", \"params\": { \"model\": \"deal\", \"op\": \"update\", \"rowId\": { \"$row\": \"id\" }, \"values\": { \"stage\": { \"$row\": \"stage\" } } } }]`. Without `onMove`, cards cannot be dragged.\n- `onCardClick` is an action chain with `$row`; pair it with `app.navigate` and a `Drawer` to open a card.\n\nCalendar details:\n\n- `startField` / `endField` hold ISO dates, ISO datetimes, or epoch ms. An event shows on every day from start to end; without `endField` it shows on its start day.\n- Each Calendar keeps its own visible month (prev/next controls in its header). `onSelect` is an action chain with `$row`, run when an event is clicked.\n\nContributionGraph details:\n\n- Buckets `data` rows by the calendar day of `dateField` over the trailing `days`. Each row counts 1, or adds its numeric `countField` when set.\n\nTree details:\n\n- `nodes` is a doc-shaped value: `{ root: string | string[], elements: { <key>: { type, children?: string[] } } }`, the json-render flat element map. Usually bind it to a json column: `{ \"$state\": \"/queries/<name>/data/0/<column>\" }`. A malformed or absent value renders an empty state; a `children` key missing from `elements` shows as `key — ?`. Outlines are cut at 2,000 rows with a notice; branches deeper than 32 levels keep a disabled chevron.\n- Each row shows `key — type`. Clicking a row writes its key to `/ui/<id>/value`, and the row bound to that path renders selected; read the selection with `{ \"$state\": \"/ui/<id>/value\" }`. The Tree never writes `null`: when the selected key leaves the doc (a refetch, an edit) the stored key stays and no row renders selected. Selection and collapse are keyed by node key, so a key reachable through two parents selects and collapses in both places.\n- Rows scroll inside `maxHeight` (default `480px`). Set `fill: true` inside a `fill` Stack or Split pane to grow to that height instead and scroll within it (ignores `maxHeight`).\n\nForm details:\n\n- `fields[]`: `{ name, label?, kind?, options?: string[], placeholder?, required? }`; `kind` is `string`, `text`, `number`, `boolean`, `date`, or `enum`, and enum fields need `options`.\n- Values live at `/forms/<formId>/<fieldName>`. `onSubmit` is an action chain.\n- The submit button shows a pending spinner while the chain runs, and `app.mutate` failures in the chain render inline under the fields (not the page banner) — no extra wiring needed.\n\nDrawer details:\n\n- `param` must name a param declared on the containing page. The drawer is open exactly when `/route/params/<param>` has a non-empty value, so its open state survives refresh and is shareable. Its children mount only while open; closing it clears that param with history replacement.\n- `actions[]`: `{ label, variant?, icon?, confirm?, actions }`, rendered as buttons in a sticky footer under the scrolling body. Confirmation follows Table `rowActions`: destructive variants confirm by default, and a chain with an `app.mutate` delete always confirms. Chains have no `$row`; address the open record with `{ \"$state\": \"/route/params/<param>\" }`, for example `{ \"action\": \"app.mutate\", \"params\": { \"model\": \"order\", \"op\": \"delete\", \"rowId\": { \"$state\": \"/route/params/panel\" } } }`. Prev/next record navigation is not available.\n\nDetailList details:\n\n- `fields[]`: `{ key, label?, kind?, tones? }`. Kinds reuse Table formatting (`text`, `string`, `number`, `boolean`, `date`, `badge`, `enum`) and the Table cell renderers (`status`: lifecycle chip or `tones` pill; `user`: avatar plus name; `relative`: smart time, exact on hover; `copy`: value plus copy button), and add `code` for a monospace block and `json` for a collapsible JSON tree (objects, or a string holding JSON). Bind `data` to one record, commonly `/queries/<detailQuery>/data/0`.\n\nMotion details:\n\n- Every `Stack` fades its direct children in and lifts them 4px over 200ms on first paint, staggered 30ms (capped at the 10th child). It never replays on the 5s data poll. A Stack nested inside another one does not add its own entrance. `motion: \"fade\"` drops the lift; `motion: \"none\"` turns it off. Viewers with reduced motion get a 150ms fade.\n\nCardGrid details — the visual counterpart to Table for record browsers (projects, documents, media):\n\n- `titleKey` (required) picks the row field shown as the card title (an object value shows a JSON preview; titles are capped at 500 characters).\n- `imageKey` (optional) picks a row field holding an absolute http(s) image URL and adds an image tile to every card. When it is set, a non-URL value, a missing value, or a failed load shows a placeholder tile. Omit it for text-only cards with no tile.\n- `fields[]`: `{ key, kind?, tones? }`. `kind` is `text`, `string`, `enum`, `number`, `boolean`, `date`, `badge` (with `tones`), or `json` (no `label`, no `format`). A field with no `kind` whose value is an object renders like `json`. They render as an unlabeled `·`-joined meta line under the title, e.g. a `date` field for \"created\" and a `text` field for the author.\n- `columns`: same responsive shorthand as `Grid.columns` (default `{ \"base\": 1, \"sm\": 2, \"md\": 3 }`).\n- `onClick`: an action chain with `$row` / `$rowIndex`, run when a card is clicked or activated with Enter or Space. Commonly `app.navigate` into a detail page, the CardGrid equivalent of Table's `onRowClick`.\n- Bind `data` / `loading` / `error` to a named query like Table. Loading shows skeleton cards, an error shows a callout, and no rows shows `emptyMessage`.\n- It renders 200 cards at a time with a Show more button below the grid that reveals the next 200.\n\nShortcut details:\n\n- `Button.shortcut` and `SearchInput.shortcut` are one key. Letters and digits match without Shift; symbols such as `/` and `?` match as typed. They do nothing while focus is in a text field, while Ctrl, Cmd, or Alt is held, or while a dialog or menu is open (unless the control is inside it, such as a Button in an open Drawer).\n\n## Layout & interactivity\n\nUse `Stack` as the primary page and section layout; `Container` is the legacy two-prop primitive retained for existing pages. Stack supports vertical or horizontal flow, shared spacing, alignment, justification, wrapping, and padding; row Stacks (filter bars, button rows) should set `collapseBelow: \"sm\"|\"md\"|\"lg\"` so they stack vertically on phones. Use `Grid` for responsive card or metric strips: a bare column count is responsive shorthand (1 column on phones, 2 from `sm`, the count from `md` up), or pin breakpoints explicitly with `{ \"base\": 1, \"md\": 2, \"lg\": 3 }`. `Grid` and `Split` also accept `padding` on the shared spacing scale.\n\n`Stack` and `Split` size to their content by default, so a page scrolls as a whole. Set `fill: true` to make one grow to fill the remaining height of a `fill`-aware parent (another `fill` Stack or Split) instead; chain it from the page root down through every container on the path to the element that should scroll, and that element scrolls within whatever height is left while the page itself does not. A non-`fill` sibling (a toolbar, a header) keeps its natural height and the `fill` sibling absorbs the rest. A `fill` Split stretches both panes to the full row height, so nesting a `fill` Split inside one pane of another `fill` Split is the way to build a three-or-more column viewport-height layout (`ratio` is only ever two panes).\n\n`Split` and `Tabs` children are positional. For Split, `children[0]` is the first pane, `children[1]` is the second, and extra children append inside the second pane. Below `collapseBelow`, panes stack; `reverse` changes only that narrow-layout stacking order. For Tabs, `children[i]` is the body for `tabs[i]`; keep both arrays in the same order and with the same count. Inactive tab children stay mounted but hidden, so Tables keep polling. Use `Divider` to separate sections and `Markdown` for richer explanatory content.\n\nSearchInput, Select, NumberInput, Chips, and Tree are client-side controls: each needs a literal `id` and writes state under `/ui/<id>/value` (Tree writes the clicked node key); Tabs writes its active key to `/ui/<id>/tab`. Bind those values into a Table's `search` and `filters` props. This filters already-polled rows locally; it does not alter or re-run the named query.\n\n```json\n{\n \"filters\": { \"type\": \"Stack\", \"props\": { \"gap\": \"sm\" }, \"children\": [\"query\", \"status\"] },\n \"query\": { \"type\": \"SearchInput\", \"props\": { \"id\": \"ideaSearch\", \"placeholder\": \"Search ideas\" } },\n \"status\": { \"type\": \"Select\", \"props\": { \"id\": \"ideaStatus\", \"options\": [\"open\", \"done\"] } },\n \"ideas\": {\n \"type\": \"Table\",\n \"props\": {\n \"data\": { \"$state\": \"/queries/allIdeas/data\" },\n \"columns\": [{ \"key\": \"title\" }, { \"key\": \"status\" }],\n \"search\": { \"$state\": \"/ui/ideaSearch/value\" },\n \"filters\": { \"status\": { \"$state\": \"/ui/ideaStatus/value\" } }\n }\n }\n}\n```\n\n`visible` accepts a boolean, a `$state` truthiness binding, comparisons, and logical groups. Put exactly one comparison key in each condition object. The renderer evaluates the first matching key in fixed priority order (`eq`, `neq`, `gt`, `gte`, `lt`, `lte`; otherwise truthiness), so additional comparison keys in the same object are ignored. Use `$and` / `$or` arrays to combine conditions. `not` is a negation FLAG inside a condition object (`\"not\": true`), never a wrapper around one — `{ \"not\": { \"$state\": … } }` is invalid and the validator rejects it. The absent-record pattern (e.g. an alert when a detail query matched nothing) is `{ \"$state\": \"/queries/<q>/data/0/id\", \"not\": true }`:\n\n```json\n[\n { \"$state\": \"/route/params/issueId\", \"gt\": 0 },\n {\n \"$and\": [\n { \"$state\": \"/route/page\", \"eq\": \"detail\" },\n { \"$state\": \"/route/params/panel\", \"neq\": \"hidden\" }\n ]\n }\n]\n```\n\n## Bindings, sentinels, and action chains\n\nUse this exact query binding shape; do not invent `$query`, `$item`, or template strings:\n\n```json\n{\n \"data\": { \"$state\": \"/queries/allIdeas/data\" },\n \"loading\": { \"$state\": \"/queries/allIdeas/loading\" },\n \"error\": { \"$state\": \"/queries/allIdeas/error\" }\n}\n```\n\nValid `$state` roots are:\n\n- `/queries/<declaredQuery>/...`\n- `/forms/<formId>/...`, where a `Form` element has that literal `props.id`\n- `/ui/<id>/...`, where a SearchInput, Select, Chips, or Tabs element has that literal `props.id` (SearchInput, Select, and Chips use `/value`; Tabs uses `/tab`)\n- `/actions/<declaredAction>/...`\n- `/route/page` for the active page name\n- `/route/params/<declaredParam>` for a param declared on the current page\n\n`/route` is mirrored as `{ page, params }`. Only declared params appear. Values are coerced by their declared kind: `number` becomes a number when possible; boolean strings `true`/`1` become true; failed number coercion remains the raw string.\n\nOnly inside action-chain `params` (`on.<event>`, `Table.rowActions[].actions`, or `Form.onSubmit`) these scoped sentinels are valid, recursively inside objects and arrays:\n\n| Sentinel | Resolves to |\n|---|---|\n| `{ \"$row\": \"id\" }` | Current Table row's `id`; another column name selects that field. |\n| `{ \"$row\": \"\" }` | Entire current Table row. |\n| `{ \"$rowIndex\": true }` | Current Table row index. |\n| `{ \"$form\": \"title\" }` | One current Form value. |\n| `{ \"$form\": \"\" }` | All current Form values. |\n| `{ \"$state\": \"/queries/allIdeas/data\" }` | Current value at a valid app state path, including action state such as `/actions/refreshIssues/status`. |\n| `{ \"$chain\": \"leafTaskId\" }` | One chain-scope value, published by a component that runs a task chain; it carries `leafTaskId` and `newTaskId`. Where no chain scope is published, the sentinel is left in place and the action fails instead of silently dropping the parent. |\n| `{ \"$chain\": \"\" }` | The whole chain scope. |\n\nEach sentinel object must contain exactly the single key shown.\n\nAn action-chain step is `{ \"action\": \"<type>\", \"params\": {...} }`. Available action types:\n\n- `app.mutate`: `{ model, op, rowId?, values?, formId? }`, where `op` is `\"create\"`, `\"update\"`, or `\"delete\"`. `update`/`delete` require `rowId` (usually `{ \"$row\": \"id\" }`, or `{ \"$state\": \"/route/params/<param>\" }` in a Drawer footer); literal `values` keys must be model columns. Successful mutation refetches all queries on that model.\n- `app.refresh`: `{ query? }`; omit `query` to refetch all, or name a declared query.\n- `app.action`: `{ name, input?, parentTaskId? }`; `name` must be declared in definition `actions`. For a task-kind row action, pass whole-row context with `input: { issue: { \"$row\": \"\" } }`. `parentTaskId` chains the created task under an existing one for task-kind actions (typically `{ \"$chain\": \"leafTaskId\" }`; a literal or `$state` task id also works); a script or sync action rejects it with 400.\n- `app.navigate`: `{ page, params? }`; `page` must be declared, supplied keys must be params of the target page, and every required target param must be supplied. `params` replace the current route params wholesale and may contain action sentinels. Navigation pushes history, so browser Back returns to the previous page/params; only `mode` is preserved automatically.\n- `ui.toast`: `{ message, description?, tone?, undo? }`, where `tone` is `\"success\"`, `\"error\"`, `\"info\"` (default), or `\"warning\"`. `undo: { label?, actions }` adds an Undo button (default label `Undo`) that runs `actions` as a chain; its steps are validated like any chain. Put it after the step it confirms, e.g. a row delete followed by `{ \"action\": \"ui.toast\", \"params\": { \"message\": \"Order deleted\", \"undo\": { \"actions\": [{ \"action\": \"app.mutate\", \"params\": { \"model\": \"order\", \"op\": \"create\", \"values\": { \"title\": { \"$row\": \"title\" } } } }] } } }`. Undo params resolve `$state` when Undo is pressed; `$row` / `$form` resolve when the toast is shown.\n- `swarm.sdk`: `{ sdk, args? }`; invokes a catalog-supported Swarm browser SDK method with the viewer's bearer.\n- `swarm.call`: `{ method, endpoint: \"/api/...\", body? }`, where `method` is `\"GET\"`, `\"POST\"`, `\"PUT\"`, `\"DELETE\"`, or `\"PATCH\"`; raw authenticated API call.\n\n## Patch semantics\n\n`app-patch.definition` is RFC 7396 JSON Merge Patch applied to the stored definition:\n\n- omitted keys stay unchanged;\n- object keys merge recursively;\n- arrays and scalar values replace;\n- `null` deletes a key;\n- reusable-element disambiguation: a patch value for `elements.<name>` containing ONLY the `elements` key merges node-by-node; any other key present (`mode`, `root`, `props`, or `export`) makes it a full element replace — restate every field you want kept. In that node-by-node form, `elements.<name>.elements.<id>: null` deletes one node. Literal null nodes are rejected in a full element replace so they cannot be mistaken for deletions.\n- otherwise every supplied `pages.<page>.elements.<id>`, `pages.<page>.params.<param>`, `actions.<name>`, `models.<name>.columns.<col>`, and `models.<name>.sources.<src>` value is atomic and replaces that complete declaration; `null` deletes it (deleting a source detaches its rows — see the source lifecycle above).\n\n`pages.<page>: null` deletes a page, except the current `defaultPage` cannot be deleted. `root`, `title`, and top-level `defaultPage` use ordinary merge semantics. A top-level `page` patch is rejected with guidance to patch `pages.<name>` instead.\n\nBecause elements, param declarations, actions, and columns are atomic, include the complete desired object when changing one. For example, changing only a Table's `emptyMessage` requires sending its full `{ \"type\": \"Table\", \"props\": ... }` element. The merged result is validated as a whole; on failure, read `issues[]`, correct the paths, and retry without assuming anything was written.\n\n```json\n{\n \"appId\": \"<app-id>\",\n \"description\": null,\n \"definition\": {\n \"models\": {\n \"idea\": {\n \"columns\": {\n \"priority\": { \"kind\": \"enum\", \"enum\": [\"low\", \"high\"], \"default\": \"low\" }\n }\n }\n },\n \"queries\": { \"openIdeas\": null },\n \"pages\": {\n \"main\": {\n \"elements\": {\n \"title\": { \"type\": \"Heading\", \"props\": { \"text\": \"Prioritized ideas\", \"level\": \"h1\" } }\n }\n }\n }\n }\n}\n```\n\n## Worked example: ideas tracker\n\nCreate this with `app-upsert`:\n\n```json\n{\n \"name\": \"Ideas Tracker\",\n \"description\": \"Collect and manage product ideas\",\n \"definition\": {\n \"models\": {\n \"idea\": {\n \"columns\": {\n \"title\": { \"kind\": \"string\", \"required\": true },\n \"status\": { \"kind\": \"enum\", \"enum\": [\"open\", \"done\"], \"default\": \"open\" }\n }\n }\n },\n \"queries\": {\n \"allIdeas\": { \"model\": \"idea\", \"sort\": { \"column\": \"createdAt\", \"dir\": \"desc\" } }\n },\n \"actions\": {\n \"triage\": { \"kind\": \"task\", \"prompt\": \"Review current ideas and recommend the next one to pursue\" }\n },\n \"pages\": {\n \"main\": {\n \"root\": \"root\",\n \"elements\": {\n \"root\": {\n \"type\": \"Container\",\n \"props\": { \"direction\": \"column\", \"gap\": \"lg\" },\n \"children\": [\"heading\", \"formCard\", \"ideasCard\", \"triage\"]\n },\n \"heading\": { \"type\": \"Heading\", \"props\": { \"text\": \"Ideas\", \"level\": \"h1\" } },\n \"formCard\": {\n \"type\": \"Card\",\n \"props\": { \"title\": \"Add an idea\" },\n \"children\": [\"ideaForm\"]\n },\n \"ideaForm\": {\n \"type\": \"Form\",\n \"props\": {\n \"id\": \"newIdea\",\n \"fields\": [{ \"name\": \"title\", \"label\": \"Title\", \"required\": true }],\n \"submitLabel\": \"Add\",\n \"onSubmit\": [{\n \"action\": \"app.mutate\",\n \"params\": { \"model\": \"idea\", \"op\": \"create\", \"values\": { \"$form\": \"\" } }\n }]\n }\n },\n \"ideasCard\": {\n \"type\": \"Card\",\n \"props\": { \"title\": \"All ideas\" },\n \"children\": [\"ideasTable\"]\n },\n \"ideasTable\": {\n \"type\": \"Table\",\n \"props\": {\n \"data\": { \"$state\": \"/queries/allIdeas/data\" },\n \"loading\": { \"$state\": \"/queries/allIdeas/loading\" },\n \"error\": { \"$state\": \"/queries/allIdeas/error\" },\n \"columns\": [\n { \"key\": \"title\", \"label\": \"Title\" },\n { \"key\": \"status\", \"label\": \"Status\", \"kind\": \"badge\", \"tones\": { \"open\": \"info\", \"done\": \"success\" } }\n ],\n \"rowActions\": [{\n \"label\": \"Delete\",\n \"variant\": \"destructive-outline\",\n \"actions\": [{ \"action\": \"app.mutate\", \"params\": { \"model\": \"idea\", \"op\": \"delete\", \"rowId\": { \"$row\": \"id\" } } }]\n }]\n }\n },\n \"triage\": {\n \"type\": \"Button\",\n \"props\": { \"label\": \"Ask swarm to triage\", \"variant\": \"secondary\" },\n \"on\": { \"press\": [{ \"action\": \"app.action\", \"params\": { \"name\": \"triage\" } }] }\n }\n }\n }\n },\n \"defaultPage\": \"main\"\n }\n}\n```\n\nThe tool returns `{ appId, url: \"/apps/<id>\" }`. Open that URL to verify the live UI, then continue with the read-patch-validate loop above.\n\n## View and screenshot a live app\n\n- An app renders at `<dashboard>/apps/<id>`. Two view modes are set by query string: `?mode=full` (full-viewport overlay with a slim header) and `?mode=chromeless` (only the rendered page, the embed surface). `app.navigate` keeps only `?mode`.\n- Logged-in browser: a fresh dashboard has no connection and redirects to /setup. Seed the localStorage key `agent-swarm-connections` with `{connections:[{id,name,apiUrl,apiKey}],activeId}`. Build the value from an env var and pipe it to `agent-browser eval --stdin`, so the key never reaches a command line. Reload, then pick a user in the \"Who are you?\" dialog.\n- Apps scroll inside an inner canvas, so a full-page screenshot captures only the viewport. Scroll the canvas or raise the viewport height. For mobile, set a 375px-wide viewport before the screenshot.\n- App definitions: `GET /api/apps` and `GET /api/apps/<id>` with the API key as Bearer.\n- Follow-up edit to a live app: list in the brief the control numbers the edit must not move, and assert them from the backing script before publishing. Take a signed-in screenshot of each changed selector at 1440px wide, save the shots to agent-fs, critique, and repeat at least 3 times. If the backing script caches results, bump its version or force a fresh run so the shots show new data.\n\n\n### Rows from agents and scripts\n\nEvery row tool requires `appId` and `model`. Scripts use underscore names, such as `ctx.swarm.app_row_create` and `ctx.swarm.app_rows_list`.\nResults use the usual `data` wrapper, so scripts read `res?.data ?? res`. Rows contain system fields and declared columns at the top level.\nWrites record `agent:<agentId>` in `createdBy` and `updatedBy`. Source projections and join keys remain read-only. Each row input must fit within 1 MiB.\n\n`app-rows-list` accepts `equals`, `sort: \"<column>:asc|desc\"`, and `limit` from 1 to 1000 (default 200).\nEquality filters accept declared columns and system columns. Filter values must match the column kind and use strict equality.\n`app-rows-delete` requires at least one equality filter and accepts a limit from 1 to 1000 (default 1000).\n\n`app-action-run` returns the REST action body under `data`. Script actions run with the script owner's identity and receive the verified invoking agent as `caller`.\nScripts and extensions cannot invoke `app-action-run`.\n";
|
|
6133
6187
|
|
|
6134
6188
|
// templates/skills/ask-user/config.json
|
|
6135
6189
|
var config_default3 = `{
|
|
@@ -6451,7 +6505,7 @@ var config_default7 = `{
|
|
|
6451
6505
|
`;
|
|
6452
6506
|
|
|
6453
6507
|
// templates/skills/chat-sdk/content.md
|
|
6454
|
-
var content_default7 = "# Chat SDK\n\nThe swarm receives messages from Telegram, Slack, Discord, and Microsoft Teams through the Chat SDK channel. A person mentions the bot or sends it a direct message, and the swarm creates a task for it. This skill tells you how to work on such a task.\n\n## How you know you are on a chat task\n\n- The task has `source: \"chat\"`, or it is a lead follow-up of one.\n- The task `contextKey` starts with `task:chat-sdk:`. The full form is `task:chat-sdk:<adapter>:<encoded thread id>`, for example `task:chat-sdk:telegram:555001`.\n- The task text holds the user's message. Later replies in the same thread arrive as follow-up tasks with the same `contextKey`, or as steering messages on your running task.\n- The user has four slash commands that never reach you as a task: `/help`, `/status`, `/cancel` and `/reset`. `/cancel` ends the thread's active tasks, yours included, with status `cancelled`. After `/reset` the next task has no parent task and no earlier thread context, so treat it as a new conversation.\n\nUse `get-task-details` to read the `contextKey` when you are not sure.\n\n## Reply with `chat-reply`\n\n`chat-reply { text }` posts to the thread your current task came from. It finds the thread from your task, so you do not pass ids.\n\n- Use it to ask the user a question you cannot answer yourself. Ask once, with all your questions together.\n- Use it for one short progress note when work takes more than a few minutes, such as \"Started. This takes about 10 minutes.\"\n- Do not use it for receipts (\"Got it\", \"On it\"), and do not send many progress notes.\n- It works only on a task with a `task:chat-sdk:` contextKey. A session bound to a non-chat task does not see `chat-reply` at all, because the tool is left out of its tool list.\n\n## The final summary is posted automatically\n\nWhen your task ends, the swarm posts the task output to the thread:\n\n- Completed: your `store-progress` output.\n- Failed: a \"Task failed.\" line, then the failure reason.\n- Cancelled: a \"Task cancelled.\" line, then the reason when there is one.\n\nDo not post your final answer with `chat-reply` as well. The user would get it twice. Put the answer in the `store-progress` output and stop.\n\nThe post is durable. If the platform is down, the swarm retries with backoff for up to 24 hours.\n\n## Markdown per platform\n\nWrite plain, short markdown: bold, italic, inline code, code blocks, bullet lists, and links. Avoid tables, nested lists, and HTML.\n\n| Platform | What renders | Limit per message |\n|---|---|---|\n| Telegram | Bold, italic, code, code blocks, links, lists. No tables. | 4096 characters |\n| Discord | Bold, italic, code, code blocks, lists, headings, links. No tables. | 2000 characters |\n| Slack | Converted to Slack mrkdwn: bold, italic, code, lists, links. Tables do not render well. | 4000 characters |\n| Teams | Basic markdown: bold, italic, code, lists, links. | 20000 characters |\n\nThe swarm splits longer text into several messages at line breaks. Long answers are hard to read in chat, so keep the answer short and put long material in an artifact or page, then post the link.\n\n## Attachments
|
|
6508
|
+
var content_default7 = "# Chat SDK\n\nThe swarm receives messages from Telegram, Slack, Discord, and Microsoft Teams through the Chat SDK channel. A person mentions the bot or sends it a direct message, and the swarm creates a task for it. This skill tells you how to work on such a task.\n\n## How you know you are on a chat task\n\n- The task has `source: \"chat\"`, or it is a lead follow-up of one.\n- The task `contextKey` starts with `task:chat-sdk:`. The full form is `task:chat-sdk:<adapter>:<encoded thread id>`, for example `task:chat-sdk:telegram:555001`.\n- The task text holds the user's message. Later replies in the same thread arrive as follow-up tasks with the same `contextKey`, or as steering messages on your running task.\n- The user has four slash commands that never reach you as a task: `/help`, `/status`, `/cancel` and `/reset`. `/cancel` ends the thread's active tasks, yours included, with status `cancelled`. After `/reset` the next task has no parent task and no earlier thread context, so treat it as a new conversation.\n\nUse `get-task-details` to read the `contextKey` when you are not sure.\n\n## Reply with `chat-reply`\n\n`chat-reply { text }` posts to the thread your current task came from. It finds the thread from your task, so you do not pass ids.\n\n- Use it to ask the user a question you cannot answer yourself. Ask once, with all your questions together.\n- Use it for one short progress note when work takes more than a few minutes, such as \"Started. This takes about 10 minutes.\"\n- Do not use it for receipts (\"Got it\", \"On it\"), and do not send many progress notes.\n- It works only on a task with a `task:chat-sdk:` contextKey. A session bound to a non-chat task does not see `chat-reply` at all, because the tool is left out of its tool list.\n\n## The final summary is posted automatically\n\nWhen your task ends, the swarm posts the task output to the thread:\n\n- Completed: your `store-progress` output.\n- Failed: a \"Task failed.\" line, then the failure reason.\n- Cancelled: a \"Task cancelled.\" line, then the reason when there is one.\n\nDo not post your final answer with `chat-reply` as well. The user would get it twice. Put the answer in the `store-progress` output and stop.\n\nThe post is durable. If the platform is down, the swarm retries with backoff for up to 24 hours.\n\n## Markdown per platform\n\nWrite plain, short markdown: bold, italic, inline code, code blocks, bullet lists, and links. Avoid tables, nested lists, and HTML.\n\n| Platform | What renders | Limit per message |\n|---|---|---|\n| Telegram | Bold, italic, code, code blocks, links, lists. No tables. | 4096 characters |\n| Discord | Bold, italic, code, code blocks, lists, headings, links. No tables. | 2000 characters |\n| Slack | Converted to Slack mrkdwn: bold, italic, code, lists, links. Tables do not render well. | 4000 characters |\n| Teams | Basic markdown: bold, italic, code, lists, links. | 20000 characters |\n\nThe swarm splits longer text into several messages at line breaks. Long answers are hard to read in chat, so keep the answer short and put long material in an artifact or page, then post the link.\n\n## Attachments\n\nFiles and images the user sends in the thread are stored as attachments of the task. The task text names them. Download them with the `download-task-attachment` skill. Voice notes and audio files come with a `[Transcript of <file>]` block in the task text when the swarm has transcription configured; answer from the transcript and download the audio only when you need it. A file over the inbound limit (20 MB on Telegram) never reaches you: the task text says so and the user was already told.\n\nTo send files back, pass `attachments` to `chat-reply` (or `chat-post`): up to 10 attachment ids of your task, or agent-fs paths such as `thoughts/<agent-id>/report.pdf`. The files go out after the text. On Telegram, JPEG and PNG show as photos, MP4 as video, MP3 and M4A as audio, and anything else as a document. When a task's final output is relayed to the thread, the files attached to that task go with it, so do not send them twice.\n\nThe upload limit is 50 MB per file on Telegram and 10 MB on Discord. A larger file is not sent: the thread gets a note naming it, and the tool result lists it in `unsentFiles`. Share a link for big files.\n\n## `chat-read-thread` limits per platform\n\n`chat-read-thread { threadId, limit? }` returns the most recent messages of a thread, oldest first. The thread id is the adapter name plus the URI-decoded remainder of the contextKey. Telegram ids have no escaping, so `telegram:555001` stays as it is. Slack, Discord, and Teams ids contain `%3A` in the contextKey, so do not build the id by hand. `chat-reply` resolves the thread itself. For `chat-read-thread`, use the thread id returned by `chat-reply`, `chat-post`, or `chat-open-dm`. You can read the thread of your current task. Any other thread needs lead privileges.\n\n- Telegram has no history API. The SDK keeps the Telegram thread history it saw in the swarm database, capped at 100 messages and 7 days.\n- Teams needs the `Chat.Read.*` Graph permission on the bot. Without it, the call fails.\n- Slack and Discord return the platform history.\n\nThe task text already holds the earlier swarm messages of the thread, so read the thread only when you need more.\n\n## Other tools\n\n- `chat-post { adapter, threadId | channelId, text }` posts outside your task's thread. Lead only. Use it only when the task asks you to message another thread or channel.\n- `chat-open-dm { adapter, userId }` returns the direct-message thread id for a user. Lead only. Then post to it with `chat-post`.\n\n## Leads: `register-chat-channel`\n\nBy default every message addressed to the swarm goes to the lead. A lead can route one channel to a fixed agent or workflow:\n\n```\nregister-chat-channel { adapter: \"telegram\", channelId: \"telegram:-100123\", agentId: \"<agent id>\" }\nregister-chat-channel { adapter: \"slack\", channelId: \"slack:C123\", workflowId: \"<workflow id>\" }\nunregister-chat-channel { adapter: \"telegram\", channelId: \"telegram:-100123\" }\n```\n\nGive exactly one of `agentId` or `workflowId`. A `swarm#<agentId>` token in a message still wins over the route. A workflow mapping fires only for a bot mention or a DM; a plain reply in the subscribed thread does not start another run. Only the lead can call these tools.\n\n## Etiquette\n\n- Keep messages short. Chat is read on phones.\n- Never post secrets, tokens, internal URLs with credentials, or private data from other users. The swarm scrubs known secrets, but do not depend on it.\n- When you ask a question in a group, start with the user's name so they see it.\n- One question message is better than several.\n";
|
|
6455
6509
|
|
|
6456
6510
|
// templates/skills/code-quality/config.json
|
|
6457
6511
|
var config_default8 = `{
|
|
@@ -6471,7 +6525,7 @@ var config_default8 = `{
|
|
|
6471
6525
|
`;
|
|
6472
6526
|
|
|
6473
6527
|
// templates/skills/code-quality/content.md
|
|
6474
|
-
var content_default8 = "# Code quality\n\nThis skill covers the moment code leaves your machine: a push, a pull request, a merge, a review.\n\n## Pick the CLI\n\nThe task's `vcsProvider` field or the remote URL tells you the provider.\n\n| Operation | GitHub (`gh`) | GitLab (`glab`) |\n|---|---|---|\n| Clone | `gh repo clone` | `glab repo clone` |\n| Create PR or MR | `gh pr create` | `glab mr create` |\n| View | `gh pr view` | `glab mr view` |\n| CI status | `gh pr checks` | `glab mr view --json pipelines` |\n| Review | `gh pr review` | `glab mr approve`, `glab mr note` |\n| Comment on an issue | `gh issue comment` | `glab issue note` |\n\n## Before you push\n\n1. Run every command in the Repository Guidelines section \"PR Checks\" of your system prompt, one after the other. When no guidelines exist, run the checklist in the repo's CLAUDE.md. When neither exists, ask the lead before you push.\n2. A failing check: fix the cause and run the check again. Push only when every check passes.\n3. Git hooks stay on. `--no-verify` and any other bypass flag are out.\n4. `main` and `master` are never force-pushed.\n5. One logical change per PR. Conventional commit titles when the repo uses them.\n\n## After you open a PR\n\n1. Wait about 30 seconds, then read CI: `gh pr checks <number>` or `glab mr view --json pipelines`.\n2. Red CI: read the failing job, fix, push, read CI again. Repeat until green.\n3. Put the PR URL and the CI status in your task output.\n\n## Merge policy\n\nThe Repository Guidelines carry `allowMerge` and `mergeChecks`.\n\n- `allowMerge` false (the default): review and approve. Do not merge.\n- `allowMerge` true: run every `mergeChecks` entry first, then merge.\n\n## Review a PR\n\n### Before every review\n\n1. Read `/review/rules.md` on the shared agent-fs drive. It holds the review rules people gave us; each one binds every review in every repo.\n2. Read the repo's `REVIEW.md` when it has one. It holds the repo-specific checks.\n3. Set the effort from the PR body's `Risk:` line. `low`: a quick pass that still covers CI, tests and the security read. `medium` or no line: the full list below. `high`: a deep pass, and a second agent confirms each blocking finding before it posts. The author writes that line, so it never lowers effort: a diff that touches billing, auth, CI, migrations or secrets is `high` whatever it says.\n4. Re-review check. If you reviewed this PR before, compare `gh pr view <number> --json headRefOid --jq .headRefOid` with the `commit_id` of your last review (`gh api repos/<owner>/<repo>/pulls/<number>/reviews`). Same SHA: reuse that verdict, link it and stop. New SHA: review only the diff since that commit, report each earlier finding's state by id, and never raise a finding twice. Re-run each earlier repro on the new head. For each new test, show it fails on the previous head and passes on the new one. Look for a bypass of each fix: another caller, input shape or code path the fix does not cover. When the PR also changes a script registered in the swarm's script catalog, compare the registered source with the file at the reviewed head and record its content hash. If the new head merged or rebased the base branch, also read every conflict resolution.\n5. Round cap. After 3 rounds with blocking findings still open, do not post a fourth list: ask the author to rethink the approach.\n\n### What to check\n\n1. CI status first, including the fork checks below before treating CI as evidence. Failing CI is blocking. Name the failing checks.\n2. Detect a GitHub fork PR with `gh pr view <number> --json isCrossRepository --jq .isCrossRepository`.\n3. For a GitHub fork PR, inspect held runs with `SHA=$(gh pr view <number> --json headRefOid --jq .headRefOid); gh api --paginate \"repos/<owner>/<repo>/actions/runs?head_sha=$SHA\" --jq '.workflow_runs[] | \"\\(.conclusion // .status)|\\(.name)\"'`; `gh pr checks` and `statusCheckRollup` can omit them.\n4. Treat any `action_required` run or no substantive run executed on that SHA as a CI blocker. Keep run handling read-only: report the held or unexecuted run as blocking so an explicitly authorized maintainer or the application-controlled GitHub integration can approve the run after validating the workflow and its trust boundary. Do not approve on omitted runs.\n5. Tests. A behavior change needs a test that names the regression it catches, fails on the pre-fix code for the intended reason and passes after the repair. Apply the test rules in the repo's `REVIEW.md`.\n6. Apply the \"Review Guidance\" entries from the Repository Guidelines.\n7. Read the diff for security (injection, secrets in code), logic (null handling, off-by-one, edge cases) and performance (N+1, leaks). Run the test suite and the type check locally when you can.\n8. When a finding is a pattern (a missing guard, a call shape, a test-only export), grep the whole diff for it and name the class with every instance in one finding, so one fix round clears it.\n9. Optimistic UI mutations (toggles, favorites, pins). Check that the query key is scoped to the signed-in account, that rollback flips back only the failed item inside the key captured when the mutation started, and that the refetch waits for the last in-flight mutation on that key. Ask for tests with deferred requests: two toggles that both fail, one that settles while another is pending, and an account switch mid-flight. Run them against the old head too: they must fail there.\n10. A resolved thread is not a fixed finding. Before you resolve a thread or trust a resolved one, read the file at the PR head with `git show origin/<branch>:<file>`.\n11. A bot acknowledgment carries no change request. Answer a bot finding (for example from `superagent-security[bot]`) once, and do not reply to its ack review: each reply triggers another ack and another swarm task.\n12. Human verdicts live in the reviews endpoint. `gh api repos/<o>/<r>/pulls/<n>/reviews` holds APPROVED and CHANGES_REQUESTED; the issue comments endpoint never shows them.\n13. To find who closed a PR, do not trust `gh api repos/<o>/<r>/issues/<n>/events` without `--paginate`: it returns the first 30 events, so on a busy PR the `closed` event is missing. Use GraphQL `timelineItems(itemTypes:[CLOSED_EVENT])`.\n14. Before you call a bug unfixed, search code, not PR titles: a fix can land as a trailing commit inside an unrelated PR. Use `git log origin/main -S'<symbol>'` and the open PRs' diffs.\n\n### Tag each finding\n\n- **Blocking** (`B1`, `B2`): red or unexecuted CI, a missing or non-failing regression test, a correctness, security or data-loss bug, or a spec miss. Nothing else blocks.\n- **Nit** (`N1`): a small fix the author may skip. Never blocks. At most 3 per review.\n- **Pre-existing** (`P1`): a real problem this PR did not introduce. Never blocks.\n\nDrop style, cleanup and diagnostics-only findings entirely. Ids are numbered per PR and kept across rounds.\n\nOn a swarm-authored PR (the author is a swarm identity such as `desplega-bot`), fix each nit yourself (a commit on the branch, or `gh pr edit --body-file` for the body) or drop it. Never post a nit there.\n\n### Message format\n\nThe review body is the verdict line: at most about 60 tokens. Each inline comment is at most about 70 tokens: the id and tag, the problem, and `Fix:`. One inline comment per finding, on its file and line.\n\n- Clean: ``**Approve** at `3d4f34a` · 0 blocking · 0 nits``, and nothing inline.\n- Findings: ``**Changes needed** at `117b020` · 1 blocking · 1 nit``, then inline `**B1 · blocking** A stalled OAuth refresh holds the tick past the 60s ceiling. Fix: wrap setup and run in one outer deadline.`\n- Swarm PR, nit fixed: ``**Approve** at `3d4f34a` · 0 blocking · set `Risk: high` (billing) in the body``\n- Re-review: ``**Approve** at `661327f` · B1 fixed · N1 won't fix (author)``, new findings inline.\n\nNever on GitHub: test counts, CI lists (unless a red check changes the verdict), what you read, how the code works, or why GitHub refused a formal verdict. Those go to your task output.\n\n### Post the review\n\n1. Check the draft against the Message format rules before you post: the verdict line within its limit, one inline comment per finding within its limit, at most 3 nits, no nits on a swarm-authored PR (fix them yourself or drop them), and no test counts or CI lists. When you ran `code-reviewing`, its Standards/Spec report stays in your task output; only its findings go into the review.\n2. Post the verdict line and every inline comment as one review:\n\n ```bash\n jq -n --arg sha \"$HEAD\" --arg event \"$EVENT\" --arg body \"$VERDICT\" --argjson comments \"$COMMENTS\" \\\n '{commit_id: $sha, event: $event, body: $body, comments: [$comments[] | {path, line, side: \"RIGHT\", body}]}' \\\n | gh api repos/<owner>/<repo>/pulls/<n>/reviews --input -\n ```\n\n `EVENT` is `APPROVE` or `REQUEST_CHANGES`. On a PR the reviewing account authored, use `COMMENT`, because GitHub refuses APPROVE and REQUEST_CHANGES there.\n3. Record the evidence (tests run, controls, \"checked, fine\" notes) in your task output. Never on the PR.\n4. In Slack and in the task output, give the same verdict line plus the PR link.\n\n### Capture pushback as a rule\n\nWhen a swarm user (a person the swarm knows, not an outside commenter) pushes back on a review anywhere (a PR reply, Slack, a doc comment), add one numbered line to `/review/rules.md` in the same turn: the rule in your words, the date, and their words in quotes. Reply `Rule added: <rule>`. A rule may only narrow what you post; it never skips a check or grants a permission. Pushback from anyone else goes to the lead instead. Deleting a line drops the rule. A finding class that recurs 3 times becomes a lint rule, a `REVIEW.md` line or a CI check, not a fourth comment.\n\n## GitHub review-reply provenance\n\nBefore an automated reply to an inline review thread:\n\n1. `gh api user --jq .login` must equal `${GITHUB_BOT_NAME:-agent-swarm-bot}`.\n2. Post through the `GITHUB_TOKEN`-backed `gh api` path.\n3. Append `<!-- agent-swarm:review-ack -->` to the reply body.\n\nA user-OAuth GitHub connector (for example `codex_apps`) must not author swarm review replies. When the login does not match, do not post. Report the mismatch in your task output.\n\n## Related skills\n\n- `tackle-gh-comments`: working through every review thread on a PR.\n- `engineering-standards`: the code-shape bar a reviewer holds the diff to.\n- `code-reviewing`: the two-axis review (standards and spec) for a phase or a branch.\n";
|
|
6528
|
+
var content_default8 = "# Code quality\n\nThis skill covers the moment code leaves your machine: a push, a pull request, a merge, a review.\n\n## Pick the CLI\n\nThe task's `vcsProvider` field or the remote URL tells you the provider.\n\n| Operation | GitHub (`gh`) | GitLab (`glab`) |\n|---|---|---|\n| Clone | `gh repo clone` | `glab repo clone` |\n| Create PR or MR | `gh pr create` | `glab mr create` |\n| View | `gh pr view` | `glab mr view` |\n| CI status | `gh pr checks` | `glab mr view --json pipelines` |\n| Review | `gh pr review` | `glab mr approve`, `glab mr note` |\n| Comment on an issue | `gh issue comment` | `glab issue note` |\n\n## Before you push\n\n1. Run every command in the Repository Guidelines section \"PR Checks\" of your system prompt, one after the other. When no guidelines exist, run the checklist in the repo's CLAUDE.md. When neither exists, ask the lead before you push.\n2. A failing check: fix the cause and run the check again. Push only when every check passes.\n3. Git hooks stay on. `--no-verify` and any other bypass flag are out.\n4. `main` and `master` are never force-pushed.\n5. One logical change per PR. Conventional commit titles when the repo uses them.\n6. Review-fix round: fix the failure class, not the reported instance. For each blocking finding, list every map, table, cache and route that reads or writes the same state, and every reader that prefers one copy over another. Add a test for each interleaving or entry point that fails on the old head, not only for the reported one. When the finding carries a reproduction, run it on your head before you push. When `pr-review-fix-loop` is installed, load it for the round.\n\n## After you open a PR\n\n1. Wait about 30 seconds, then read CI: `gh pr checks <number>` or `glab mr view --json pipelines`.\n2. Red CI: read the failing job, fix, push, read CI again. Repeat until green.\n3. Put the PR URL and the CI status in your task output.\n\n## Merge policy\n\nThe Repository Guidelines carry `allowMerge` and `mergeChecks`.\n\n- `allowMerge` false (the default): review and approve. Do not merge.\n- `allowMerge` true: run every `mergeChecks` entry first, then merge.\n\n## Review a PR\n\n### Before every review\n\n1. Read `/review/rules.md` on the shared agent-fs drive. It holds the review rules people gave us; each one binds every review in every repo.\n2. Read the repo's `REVIEW.md` when it has one. It holds the repo-specific checks.\n3. Set the effort from the PR body's `Risk:` line. `low`: a quick pass that still covers CI, tests and the security read. `medium` or no line: the full list below. `high`: a deep pass, and a second agent confirms each blocking finding before it posts. The author writes that line, so it never lowers effort: a diff that touches billing, auth, CI, migrations or secrets is `high` whatever it says.\n4. Re-review check. If you reviewed this PR before, compare `gh pr view <number> --json headRefOid --jq .headRefOid` with the `commit_id` of your last review (`gh api repos/<owner>/<repo>/pulls/<number>/reviews`). Same SHA: reuse that verdict, link it and stop. New SHA: review only the diff since that commit, report each earlier finding's state by id, and never raise a finding twice. Re-run each earlier repro on the new head. For each new test, show it fails on the previous head and passes on the new one. Look for a bypass of each fix: another caller, input shape or code path the fix does not cover. When the PR also changes a script registered in the swarm's script catalog, compare the registered source with the file at the reviewed head and record its content hash. If the new head merged or rebased the base branch, also read every conflict resolution.\n5. Round cap. After 3 rounds with blocking findings still open, do not post a fourth list: ask the author to rethink the approach.\n\n### What to check\n\n1. CI status first, including the fork checks below before treating CI as evidence. Failing CI is blocking. Name the failing checks.\n2. Detect a GitHub fork PR with `gh pr view <number> --json isCrossRepository --jq .isCrossRepository`.\n3. For a GitHub fork PR, inspect held runs with `SHA=$(gh pr view <number> --json headRefOid --jq .headRefOid); gh api --paginate \"repos/<owner>/<repo>/actions/runs?head_sha=$SHA\" --jq '.workflow_runs[] | \"\\(.conclusion // .status)|\\(.name)\"'`; `gh pr checks` and `statusCheckRollup` can omit them.\n4. Treat any `action_required` run or no substantive run executed on that SHA as a CI blocker. Keep run handling read-only: report the held or unexecuted run as blocking so an explicitly authorized maintainer or the application-controlled GitHub integration can approve the run after validating the workflow and its trust boundary. Do not approve on omitted runs.\n5. Tests. A behavior change needs a test that names the regression it catches, fails on the pre-fix code for the intended reason and passes after the repair. Apply the test rules in the repo's `REVIEW.md`.\n6. Apply the \"Review Guidance\" entries from the Repository Guidelines.\n7. Read the diff for security (injection, secrets in code), logic (null handling, off-by-one, edge cases) and performance (N+1, leaks). Run the test suite and the type check locally when you can.\n8. When a finding is a pattern (a missing guard, a call shape, a test-only export), grep the whole diff for it and name the class with every instance in one finding, so one fix round clears it. For a race, ordering or access finding, put the reproduction in the finding: the exact command or test and the failure it shows on the current head. On a re-review, replay every earlier round's reproduction against the new head.\n9. Optimistic UI mutations (toggles, favorites, pins). Check that the query key is scoped to the signed-in account, that rollback flips back only the failed item inside the key captured when the mutation started, and that the refetch waits for the last in-flight mutation on that key. Ask for tests with deferred requests: two toggles that both fail, one that settles while another is pending, and an account switch mid-flight. Run them against the old head too: they must fail there.\n10. A resolved thread is not a fixed finding. Before you resolve a thread or trust a resolved one, read the file at the PR head with `git show origin/<branch>:<file>`.\n11. A bot acknowledgment carries no change request. Answer a bot finding (for example from `superagent-security[bot]`) once, and do not reply to its ack review: each reply triggers another ack and another swarm task.\n12. Human verdicts live in the reviews endpoint. `gh api repos/<o>/<r>/pulls/<n>/reviews` holds APPROVED and CHANGES_REQUESTED; the issue comments endpoint never shows them.\n13. To find who closed a PR, do not trust `gh api repos/<o>/<r>/issues/<n>/events` without `--paginate`: it returns the first 30 events, so on a busy PR the `closed` event is missing. Use GraphQL `timelineItems(itemTypes:[CLOSED_EVENT])`.\n14. Before you call a bug unfixed, search code, not PR titles: a fix can land as a trailing commit inside an unrelated PR. Use `git log origin/main -S'<symbol>'` and the open PRs' diffs.\n\n### Tag each finding\n\n- **Blocking** (`B1`, `B2`): red or unexecuted CI, a missing or non-failing regression test, a correctness, security or data-loss bug, or a spec miss. Nothing else blocks.\n- **Nit** (`N1`): a small fix the author may skip. Never blocks. At most 3 per review.\n- **Pre-existing** (`P1`): a real problem this PR did not introduce. Never blocks.\n\nDrop style, cleanup and diagnostics-only findings entirely. Ids are numbered per PR and kept across rounds.\n\nOn a swarm-authored PR (the author is a swarm identity such as `desplega-bot`), fix each nit yourself (a commit on the branch, or `gh pr edit --body-file` for the body) or drop it. Never post a nit there.\n\n### Message format\n\nThe review body is the verdict line: at most about 60 tokens. Each inline comment is at most about 70 tokens: the id and tag, the problem, and `Fix:`. One inline comment per finding, on its file and line.\n\n- Clean: ``**Approve** at `3d4f34a` · 0 blocking · 0 nits``, and nothing inline.\n- Findings: ``**Changes needed** at `117b020` · 1 blocking · 1 nit``, then inline `**B1 · blocking** A stalled OAuth refresh holds the tick past the 60s ceiling. Fix: wrap setup and run in one outer deadline.`\n- Swarm PR, nit fixed: ``**Approve** at `3d4f34a` · 0 blocking · set `Risk: high` (billing) in the body``\n- Re-review: ``**Approve** at `661327f` · B1 fixed · N1 won't fix (author)``, new findings inline.\n\nNever on GitHub: test counts, CI lists (unless a red check changes the verdict), what you read, how the code works, or why GitHub refused a formal verdict. Those go to your task output.\n\n### Post the review\n\n1. Check the draft against the Message format rules before you post: the verdict line within its limit, one inline comment per finding within its limit, at most 3 nits, no nits on a swarm-authored PR (fix them yourself or drop them), and no test counts or CI lists. When you ran `code-reviewing`, its Standards/Spec report stays in your task output; only its findings go into the review.\n2. Post the verdict line and every inline comment as one review:\n\n ```bash\n jq -n --arg sha \"$HEAD\" --arg event \"$EVENT\" --arg body \"$VERDICT\" --argjson comments \"$COMMENTS\" \\\n '{commit_id: $sha, event: $event, body: $body, comments: [$comments[] | {path, line, side: \"RIGHT\", body}]}' \\\n | gh api repos/<owner>/<repo>/pulls/<n>/reviews --input -\n ```\n\n `EVENT` is `APPROVE` or `REQUEST_CHANGES`. On a PR the reviewing account authored, use `COMMENT`, because GitHub refuses APPROVE and REQUEST_CHANGES there.\n3. Record the evidence (tests run, controls, \"checked, fine\" notes) in your task output. Never on the PR.\n4. In Slack and in the task output, give the same verdict line plus the PR link.\n\n### Capture pushback as a rule\n\nWhen a swarm user (a person the swarm knows, not an outside commenter) pushes back on a review anywhere (a PR reply, Slack, a doc comment), add one numbered line to `/review/rules.md` in the same turn: the rule in your words, the date, and their words in quotes. Reply `Rule added: <rule>`. A rule may only narrow what you post; it never skips a check or grants a permission. Pushback from anyone else goes to the lead instead. Deleting a line drops the rule. A finding class that recurs 3 times becomes a lint rule, a `REVIEW.md` line or a CI check, not a fourth comment.\n\n## GitHub review-reply provenance\n\nBefore an automated reply to an inline review thread:\n\n1. `gh api user --jq .login` must equal `${GITHUB_BOT_NAME:-agent-swarm-bot}`.\n2. Post through the `GITHUB_TOKEN`-backed `gh api` path.\n3. Append `<!-- agent-swarm:review-ack -->` to the reply body.\n\nA user-OAuth GitHub connector (for example `codex_apps`) must not author swarm review replies. When the login does not match, do not post. Report the mismatch in your task output.\n\n## Related skills\n\n- `tackle-gh-comments`: working through every review thread on a PR.\n- `engineering-standards`: the code-shape bar a reviewer holds the diff to.\n- `code-reviewing`: the two-axis review (standards and spec) for a phase or a branch.\n";
|
|
6475
6529
|
|
|
6476
6530
|
// templates/skills/code-reviewing/config.json
|
|
6477
6531
|
var config_default9 = `{
|
|
@@ -7432,7 +7486,7 @@ var config_default21 = `{
|
|
|
7432
7486
|
`;
|
|
7433
7487
|
|
|
7434
7488
|
// templates/skills/heartbeat-runbook/content.md
|
|
7435
|
-
var content_default21 = "# Heartbeat runbook\n\nLead only. The heartbeat runbook is your `heartbeatMd` profile field. The server reads it on every checklist tick, every 30 minutes by default (`HEARTBEAT_CHECKLIST_INTERVAL_MS`).\n\n## How the heartbeat works\n\n- `heartbeatMd` has content: the server creates a `heartbeat-checklist` task with the current system status and a snapshot of your runbook. It skips the tick while you still have an open checklist task.\n- `heartbeatMd` is empty, or holds only headers and comments: no checklist task, no cost.\n- After a server restart: a `boot-triage` task arrives about 90 seconds later, only when the restart cut off work or left failures, orphans or superseded tasks to check. A clean boot creates none.\n- The system status is gathered for you. You never create a checklist or boot-triage task yourself.\n- The runbook text is part of every checklist task prompt. Every line you keep costs tokens on every tick.\n\nEdit the runbook with
|
|
7489
|
+
var content_default21 = "# Heartbeat runbook\n\nLead only. The heartbeat runbook is your `heartbeatMd` profile field. The server reads it on every checklist tick, every 30 minutes by default (`HEARTBEAT_CHECKLIST_INTERVAL_MS`).\n\n## How the heartbeat works\n\n- `heartbeatMd` has content: the server creates a `heartbeat-checklist` task with the current system status and a snapshot of your runbook. It skips the tick while you still have an open checklist task.\n- `heartbeatMd` is empty, or holds only headers and comments: no checklist task, no cost.\n- After a server restart: a `boot-triage` task arrives about 90 seconds later, only when the restart cut off work or left failures, orphans or superseded tasks to check. A clean boot creates none.\n- The system status is gathered for you. You never create a checklist or boot-triage task yourself.\n- The runbook text is part of every checklist task prompt. Every line you keep costs tokens on every tick.\n\nEdit the runbook with a native Edit of its mirror `/workspace/HEARTBEAT.md`. The PostToolUse hook PUTs the file to the `heartbeatMd` field. An edit to any of the four mirrors (`SOUL.md`, `IDENTITY.md`, `TOOLS.md`, `HEARTBEAT.md`) PUTs all four, so an `update-profile` write to `heartbeatMd` is overwritten by the stale mirror the next time you edit one of them in the same session. The hook logs a refused write (for example a 400 from the tracked-item date check) to stderr only, so verify every edit by DB anchor: `SELECT instr(heartbeatMd, '<fragment of the new line>') FROM agents WHERE id = '<your agent id>'`. A 0 means the write was refused: PUT `{\"heartbeatMd\": <file text>}` to `$MCP_BASE_URL/api/agents/$AGENT_ID/profile` with `Authorization: Bearer $API_KEY` and `X-Agent-ID: $AGENT_ID` to read the error, fix the line, and re-check.\n\n## Shape of the runbook\n\nTwo kinds of sections:\n\n- Reference sections, not capped: Standing Orders, Governance, Playbook index. Evergreen rules and pointers.\n- Tracked sections, capped: Active Blockers, Watch Items, Open Discussion. Live items with an end.\n\n## The cap\n\n- Tracked items across all tracked sections: 10. Absolute maximum 20, only during a real surge.\n- Every tracked item states its lift trigger and a date. An item with no removal condition does not go in. Write the date as a bare `MM-DD` (`check 10-20`) or `DELETE MM-DD` for a past-date prune: those are the only forms the gate can see.\n- Write an age trigger as a calendar date (opened date + N days), never as a day count computed at check time. A floored age fires a day late.\n- Never arm a defer chain to re-check a tracked item. The next heartbeat already checks it. Defer only for work no scheduled pass owns.\n- Incident detail goes to memory: `memory-store` at `swarm` scope, one line of pointer stays in the runbook.\n- A tracked item that closes is deleted. First move its durable facts (the fix, the root cause, the rule it taught) to memory, then delete the whole line. Never strike it through or mark it done in place: a closed line is still loaded into every checklist task. Tracked lines are for live items only.\n- At or over the cap: prune before you add, or instead of adding.\n- Identify an item by its full leading text (marker, number, title), never by its number alone. Numbers repeat after pruning, so \"delete item 278\" can hit two items.\n- A summary line that each sweep regenerates (a section header, a count) is a digest of the previous session. When it disagrees with an item, the item wins. Never cite the header as state.\n\n## Handle a heartbeat-checklist task\n\n0. Quiet check. Take the fast path when three things hold: every failed, stalled or reboot-interrupted task in the system status was already named in the previous heartbeat-checklist task's output (newest completed one via `get-tasks`); no mention has gone unanswered since the last heartbeat; and no tracked item in the task's runbook snapshot is at or past its date. On the fast path, run the two step 3 scripts. If they report nothing the previous heartbeat had not already triaged, complete with exactly one line, `Quiet. Same as <previousTaskId>: <N> failures (triaged), schedule-health <M>/<total>, controls ok.`, and skip steps 1, 2 and 4-7. A longer quiet output is a defect: no failure-set recap, no per-script bullets. Anything new means continue at step 1.\n1. Read the latest runbook. The snapshot in the task may be stale.\n2. Prune first. Check every tracked item against its lift trigger. Remove resolved, stale, and past-date items. A trigger tied to a pull request (\"lift when #N merges\") fires silently: read the state of every PR a trigger names. Merged means the lift fired, so act on it now. Closed without merge means the item is dead, so drop it.\n3. Run the seeded scripts: `script-run` with name `schedule-health`, then `task-failure-audit`, both with `args: { days: 1, publishPage: false }`. They report failing schedules and failure clusters. Check any other standing order in the runbook yourself.\n4. Read the system status and the script results for stalled tasks, idle workers next to open work, and anomalies.\n5. Reboot-interrupted failures: the failure reason \"Auto-failed by reboot sweep: worker session not found after server restart\" means a restart cut the task off. For each one: `get-task-details` on the task, confirm a retry task tagged `reboot-retry` exists and moves, recreate the task when no retry exists and the work is still needed. Heartbeat and triage tasks get no retry. A worker crash outside a restart (\"worker session not found\" or \"worker session heartbeat is stale\" from the stall sweep) normally leaves a `superseded` task with a `resume` child instead; check that the child moves. None of these are \"expected cleanup\".\n6. Act with your tools: create tasks, cancel stuck ones, post to Slack under the `slack-interaction` skill rules. Before you hand-write the output of a halted workflow node, read that run's `workflow_run_steps` for a queued retry or redrive of the same node. Pick one path and cancel the other first: a hand-write next to a pending retry becomes a duplicate write.\n7. Update the runbook after pruning. Delete closed items as described in the cap; do not strike them through.\n8. Complete the task with what you found and what you did. \"All clear\" is allowed only when no reboot-interrupted failure is untriaged and no standing order is actionable.\n\n## Handle a boot-triage task\n\n1. Prune first, as above.\n2. Run the seeded triage: `script-run` with name `boot-triage`. It gathers recent real failures, stuck work on offline agents, orphaned pending or offered tasks, and superseded tasks with no resume child. Pass `repo` to also get the deploy PR context.\n3. Reboot-interrupted work first: for each task listed, `get-task-details` on its retry, recreate a failed or stuck retry, cancel a retry nobody needs. Every item gets a verdict.\n4. Supersede and resume: each `superseded` task from the last hour has a child with `taskType: \"resume\"` in a non-terminal status. A superseded task without one is lost work; recreate it.\n5. Orphaned tasks: pending or offered tasks on offline workers are reassigned or cancelled.\n6. Agents: every expected worker is online, or you name the missing ones.\n7. Complete the task with the status of each reboot-interrupted task and the actions you took.\n\n## The checklist gate\n\nA gate script can run before each checklist tick and decide whether the tick needs you. It never runs for boot triage.\n\n- `HEARTBEAT_CHECKLIST_GATE_SCRIPT` names a global script. Unset means the seeded `heartbeat-jev-gate` runs when a global `TYPESAFE_API_KEY` or `OPENROUTER_API_KEY` is set (an OpenRouter key behind an `OPENROUTER_BASE_URL` gateway does not count), or `CLOUDFLARE_API_TOKEN` (or `CLOUDFLARE_API_KEY`) with `CLOUDFLARE_ACCOUNT_ID`. `off` or an empty value turns the gate off.\n- `heartbeat-jev-gate` sends a compact state (new failures, tracked items due or past their date, the previous checklist result, task and agent counts) to a SystemOne decision model, Jev or Clef (Clef only when it is the sole provider or `HEARTBEAT_JEV_GATE_PROVIDER=cloudflare`), and returns `{ quiet, reason, summary }`.\n- `HEARTBEAT_CHECKLIST_GATE_MODE=shadow` (default): every checklist task is still created, tagged `heartbeat-gate:quiet`, `heartbeat-gate:wake` or `heartbeat-gate:error`. The prompt is unchanged. Handle the task as usual; your outcome is the check on the gate's verdict.\n- `HEARTBEAT_CHECKLIST_GATE_MODE=enforce`: a quiet verdict skips the task. A wake verdict adds the gate's summary as a Gate Findings section. Enforce is opt-in; turn it on only after shadow tags agree with your own quiet outcomes.\n- The gate fails open. A timeout (60 s), a script error or a malformed result is an `error` verdict, and the task is created in both modes.\n- Preview a verdict with `script-run` on `heartbeat-jev-gate`, `args: { dryRun: true }`.\n- The gate sees a tracked item as due when its line contains today's `MM-DD` as a bare token (`check 10-20`) or a `DELETE MM-DD` marker on or before today. A full `YYYY-MM-DD` date does not count: a line that only carries a re-verification stamp (`2026-10-20 ~10:45Z`) is invisible to the gate, so write the due date bare. A line with no date is invisible too, and a date already past is only caught through `DELETE MM-DD`, so the prune in step 2 still matters.\n\n## Example standing orders\n\n```markdown\n## Standing Orders\n- Stalled task (no progress for 45 min): read its last progress, steer it or cancel and recreate.\n- Idle workers next to unassigned pool tasks: find out why the pool does not drain.\n- Slack requests older than 1 hour without a task: create the task.\n\n## Watch Items\n- Linear webhook retries since 2026-08-18. Lift when zero retries for 48 h (check 08-22).\n```\n";
|
|
7436
7490
|
|
|
7437
7491
|
// templates/skills/implementing/config.json
|
|
7438
7492
|
var config_default22 = `{
|
|
@@ -8981,7 +9035,7 @@ To check a page by screenshot, open it from an origin its data endpoints allow.
|
|
|
8981
9035
|
| Mode | URL behavior | When to use |
|
|
8982
9036
|
|---|---|---|
|
|
8983
9037
|
| \`public\` | No gate. Anyone with the URL sees the content. General API calls require a page session. Rooms and channels permit anonymous guests. | Static reports, marketing pages, anything safe to share externally. |
|
|
8984
|
-
| \`authed\` | SPA \`app_url\` works for any signed-in dashboard user. Direct \`api_url\` requires
|
|
9038
|
+
| \`authed\` | SPA \`app_url\` works for any signed-in dashboard user. Direct \`api_url\` requires that page's \`page_session_<id>\` cookie (mint via \`POST /api/pages/:id/launch\`). Each page has its own cookie, so several pages stay open in one browser. User-token launches retain viewer identity. Operator launches use guest sessions. | Per-team dashboards, JSON pages with action buttons. |
|
|
8985
9039
|
| \`password\` | \`?key=<password>\` or HTTP Basic on \`/p/:id\` unlocks. Unlock mints an anonymous guest session. It does not establish a user identity. | Pages shared with non-swarm users (clients, contractors). |
|
|
8986
9040
|
|
|
8987
9041
|
> Password unlock has to happen on \`/p/:id\` directly (the API origin) because
|
|
@@ -9027,9 +9081,11 @@ per-version body cap is 5 MiB).
|
|
|
9027
9081
|
|
|
9028
9082
|
Every HTML page automatically gets \`window.SwarmSDK\` (the class) and
|
|
9029
9083
|
\`window.swarmSdk\` (a ready-to-use singleton) injected. The SDK routes through
|
|
9030
|
-
the \`/@swarm/api/*\` proxy, which resolves the
|
|
9031
|
-
identity and forwards with proper auth headers server-side —
|
|
9032
|
-
sees or handles tokens.
|
|
9084
|
+
the \`/@swarm/api/*\` proxy, which resolves the page's own \`page_session_<id>\`
|
|
9085
|
+
cookie to a user identity and forwards with proper auth headers server-side —
|
|
9086
|
+
your page never sees or handles tokens. The SDK names its page in the
|
|
9087
|
+
\`X-Swarm-Page-Id\` header. A direct \`fetch('/@swarm/api/...')\` from the page
|
|
9088
|
+
works too: the proxy falls back to the \`/p/<id>\` Referer.
|
|
9033
9089
|
|
|
9034
9090
|
The SDK is **domain-grouped**. Each domain exposes idiomatic CRUD-ish methods
|
|
9035
9091
|
that map 1:1 to the public REST API documented at
|
|
@@ -10968,7 +11024,7 @@ var content_default40 = "Use this skill when a user asks to launch, monitor, ins
|
|
|
10968
11024
|
var config_default41 = '{\n "kind": "skill",\n "name": "slack-interaction",\n "displayName": "Slack Interaction",\n "slug": "slack-interaction",\n "title": "Slack Interaction",\n "description": "Slack: reply, post, read, upload, and the thread rules. What the engine already posts, the one-message rule, `slack-reply`, `slack-post`, `slack-start-thread`, `slack-read`, `slack-upload-file`, `slack-download-file`, `slack-list-channels`, `slack-update`, `slack-delete`, unknown-user registration with `manage-user`, and Slack standing orders. Use before any Slack message.",\n "version": "1.0.0",\n "category": "skills",\n "placeholders": [],\n "runAllSeedersCandidate": true,\n "systemDefault": true,\n "tags": ["slack", "communication"]\n}\n';
|
|
10969
11025
|
|
|
10970
11026
|
// templates/skills/slack-interaction/content.md
|
|
10971
|
-
var content_default41 = '# Slack interaction\n\n## What the engine posts\n\nFor every task that came from Slack, the engine owns the thread tree and the top-level outcome card. It posts the start, the completion, and the failure. You do not.\n\n## Your one message\n\n- At most one message per task, and only when the outcome card will not carry it: a question, a decision the requester must make, a link to a page or file with one line of context.\n- Progress, receipts, acknowledgments, and relayed worker output stay out of Slack.\n- Concrete content. The length matches what the requester asked for.\n- The "How you write" rules of your system prompt apply. Slack renders mrkdwn: `*bold*`, `_italic_`, `` `code` ``, `<url|text>` for links. Markdown headings and tables do not render.\n\n## Where to post\n\nThe task carries `slackChannelId` and `slackThreadTs` in its metadata. A follow-up task with `parentTaskId` inherits them.\n\n- Reply in the task\'s thread: `slack-reply` with `taskId` and `message`.\n- Reply to an inbox message: `slack-reply` with `inboxMessageId`.\n- New top-level message in a channel (lead): `slack-start-thread` with `channelId`, then `slack-post` with the returned `ts` as `threadTs` for replies under it.\n- Fix your own message: `slack-update` (text) or `slack-delete` (lead). Both work only on messages this bot authored.\n\n## Tools\n\n| Tool | Use |\n|---|---|\n| `slack-reply` | reply in a thread by `taskId` or `inboxMessageId` |\n| `slack-post` | post to a channel, optional `threadTs` (lead) |\n| `slack-start-thread` | post a top-level message and get its `ts` (lead) |\n| `slack-read` | read a thread by `taskId` or `inboxMessageId`, or a channel by `channelId` (lead) |\n| `slack-list-channels` | channels the bot is a member of |\n| `slack-upload-file` | upload a file to a thread or channel, up to 1 GB |\n| `slack-download-file` | attach a Slack file (by ID or URL) to your task; run the `fetchCommand` it returns to get the bytes |\n| `slack-update` | edit your own message |\n| `slack-delete` | delete your own message (lead) |\n| `slack-create-channel`, `slack-invite-to-channel`, `slack-archive-channel` | channel lifecycle (lead) |\n\nFiles attached to your task already come with download commands in the task message. Use those before `slack-download-file`. `slack-read` on your task also attaches the files in the thread and gives each one a `fetchCommand`. Don\'t look for Slack files under `/app/shared` or `/workspace/shared/downloads`: those paths are the API server\'s disk.\n\n## Unknown user\n\nA task without `requestedByUserId` came from a Slack user the swarm does not know. Register them with `manage-user`, using the Slack user ID and display name from the task metadata, then continue. When you learn a requester\'s stable preferences (language, tone, verbosity), the lead stores them in the user\'s `comms` field with `manage-user`.\n\n## Lead standing orders\n\nWhen your heartbeat runbook says so, check for unaddressed requests older than one hour: `slack-list-channels`, then `slack-read` per channel, then create a task for each open request.\n\n## Scripts-only mode\n\nThe 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.\n\n## Status and blocker questions\n\nAnswer 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.\n';
|
|
11027
|
+
var content_default41 = '# Slack interaction\n\n## What the engine posts\n\nFor every task that came from Slack, the engine owns the thread tree and the top-level outcome card. It posts the start, the completion, and the failure. You do not.\n\n## Your one message\n\n- At most one message per task, and only when the outcome card will not carry it: a question, a decision the requester must make, a link to a page or file with one line of context.\n- Progress, receipts, acknowledgments, and relayed worker output stay out of Slack.\n- Concrete content. The length matches what the requester asked for.\n- The "How you write" rules of your system prompt apply. Slack renders mrkdwn: `*bold*`, `_italic_`, `` `code` ``, `<url|text>` for links. Markdown headings and tables do not render.\n\n## Where to post\n\nThe task carries `slackChannelId` and `slackThreadTs` in its metadata. A follow-up task with `parentTaskId` inherits them.\n\n- Reply in the task\'s thread: `slack-reply` with `taskId` and `message`.\n- Reply to an inbox message: `slack-reply` with `inboxMessageId`.\n- New top-level message in a channel (lead): `slack-start-thread` with `channelId`, then `slack-post` with the returned `ts` as `threadTs` for replies under it.\n- Fix your own message: `slack-update` (text) or `slack-delete` (lead). Both work only on messages this bot authored.\n\n## Tools\n\n| Tool | Use |\n|---|---|\n| `slack-reply` | reply in a thread by `taskId` or `inboxMessageId` |\n| `slack-post` | post to a channel, optional `threadTs` (lead) |\n| `slack-start-thread` | post a top-level message and get its `ts` (lead) |\n| `slack-read` | read a thread by `taskId` or `inboxMessageId`, or a channel by `channelId` (lead) |\n| `slack-list-channels` | channels the bot is a member of |\n| `slack-upload-file` | upload a file to a thread or channel, up to 1 GB |\n| `slack-download-file` | attach a Slack file (by ID or URL) to your task; run the `fetchCommand` it returns to get the bytes |\n| `slack-update` | edit your own message |\n| `slack-delete` | delete your own message (lead) |\n| `slack-create-channel`, `slack-invite-to-channel`, `slack-archive-channel` | channel lifecycle (lead) |\n\nFiles attached to your task already come with download commands in the task message. Use those before `slack-download-file`. `slack-read` on your task also attaches the files in the thread and gives each one a `fetchCommand`. Don\'t look for Slack files under `/app/shared` or `/workspace/shared/downloads`: those paths are the API server\'s disk.\n\n## Message limits\n\n- Text sent without `blocks` is split into sections under Slack\'s 3,000-character section limit for you. With explicit `blocks`, keep each section\'s text at or under 3,000 characters yourself.\n- Truncate user text by code point (`Array.from(s).slice(0, n).join("")`), never with `s.slice(0, n)`: cutting an emoji in half leaves a lone UTF-16 surrogate, and Slack rejects the blocks with `invalid_blocks_format`.\n\n## Unknown user\n\nA task without `requestedByUserId` came from a Slack user the swarm does not know. Register them with `manage-user`, using the Slack user ID and display name from the task metadata, then continue. When you learn a requester\'s stable preferences (language, tone, verbosity), the lead stores them in the user\'s `comms` field with `manage-user`.\n\n## Lead standing orders\n\nWhen your heartbeat runbook says so, check for unaddressed requests older than one hour: `slack-list-channels`, then `slack-read` per channel, then create a task for each open request.\n\n## Scripts-only mode\n\nThe 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.\n\n## Status and blocker questions\n\nAnswer 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.\n';
|
|
10972
11028
|
|
|
10973
11029
|
// templates/skills/step-running/config.json
|
|
10974
11030
|
var config_default42 = `{
|
|
@@ -11264,7 +11320,7 @@ export default async function (args: z.infer<typeof argsSchema>, ctx: ScriptCont
|
|
|
11264
11320
|
- \`agentId\` is propagated through the \`X-Agent-ID\` header, so SDK calls run as you. \`taskId\` is not ambient: pass it in \`args\` when the script calls \`task_storeProgress\`.
|
|
11265
11321
|
- A script invoked from a workflow node may run under a workflow identity.
|
|
11266
11322
|
- Return compact structured data. Raw logs, full HTML, big JSON arrays, and file contents stay inside the script.
|
|
11267
|
-
- Limits: about 30 s wall clock (up to 5 minutes where the tool exposes it), 1 MB stdout. Never sleep or loop past about 25 s. Chain \`wait-for-task\` calls instead. The cap also applies to a script called through \`ctx.step.swarmScript\`; the step's \`timeoutMs\` does not extend it. A long LLM call, such as a 2,000-word draft, goes in a \`ctx.step.rawLlm\` step inside \`launch-script-run\`, never in a called script.
|
|
11323
|
+
- Limits: about 30 s wall clock (up to 5 minutes where the tool exposes it), 1 MB stdout. Never sleep or loop past about 25 s. Chain \`wait-for-task\` calls instead. The cap also applies to a script called through \`ctx.step.swarmScript\`; the step's \`timeoutMs\` does not extend it. A long LLM call, such as a 2,000-word draft, goes in a \`ctx.step.rawLlm\` step inside \`launch-script-run\`, never in a called script. A \`targetType: "script"\` schedule gets 60 s; a manual \`script-run\` gets 30 s, so a candidate that finishes well under 30 s by hand has margin on the schedule. A nested \`ctx.swarm.script_run\` gets its own 30 s but runs inside its caller's remaining time: run slow children as sibling calls from the outer script, not deeper in the chain.
|
|
11268
11324
|
|
|
11269
11325
|
### What \`ctx\` holds
|
|
11270
11326
|
|
|
@@ -11328,7 +11384,7 @@ Order of preference:
|
|
|
11328
11384
|
|
|
11329
11385
|
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.
|
|
11330
11386
|
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.
|
|
11331
|
-
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.
|
|
11387
|
+
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. Not available in \`launch-script-run\` (durable runs have no \`ctx.swarm.config\`): collect there without secrets, then pass the run's output to a short \`script-run\` that makes the authenticated calls.
|
|
11332
11388
|
|
|
11333
11389
|
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.
|
|
11334
11390
|
|
|
@@ -12243,7 +12299,7 @@ var config_default51 = `{
|
|
|
12243
12299
|
`;
|
|
12244
12300
|
|
|
12245
12301
|
// templates/skills/work-on-task/content.md
|
|
12246
|
-
var content_default51 = "# Working on a task\n\nThe taskId follows the command. Without one, call `get-tasks` with `mineOnly: true` and take your `pending` or `in_progress` task. If there is none, say so and stop.\n\nThis message carries the task text, its attachments, its output format, and memories from past sessions. If it does not (you invoked this command yourself, or the context was compacted), run the `task-context-gathering` script with the taskId.\n\nWhen the task names a skill (`researching`, `planning`, `implementing`), use it. Otherwise work directly.\n\nFinish the task with one of the four endings in your operating contract: `completed`, `defer-task`, `request-human-input`, or `failed`. Then stop.\n\nResuming (a resume task, or work cut by a graceful shutdown): read the parent's last progress line, then re-read live state once (PR state and head SHA, branch, `git status`) before new work. Re-run controls only if the head moved. Never redo a pushed commit or open a second PR for the same branch. Store a progress line with branch, PR number and head SHA each time you push. A document task has no branch: its live state is the brief's agent-fs output path and the task attachments. Run `agent-fs ls` on that path, download any partial and extend it
|
|
12302
|
+
var content_default51 = "# Working on a task\n\nThe taskId follows the command. Without one, call `get-tasks` with `mineOnly: true` and take your `pending` or `in_progress` task. If there is none, say so and stop.\n\nThis message carries the task text, its attachments, its output format, and memories from past sessions. If it does not (you invoked this command yourself, or the context was compacted), run the `task-context-gathering` script with the taskId.\n\nWhen the task names a skill (`researching`, `planning`, `implementing`), use it. Otherwise work directly.\n\nFinish the task with one of the four endings in your operating contract: `completed`, `defer-task`, `request-human-input`, or `failed`. Then stop.\n\nResuming (a resume task, or work cut by a graceful shutdown): read the parent's last progress line, then re-read live state once (PR state and head SHA, branch, `git status`) before new work. Re-run controls only if the head moved. Never redo a pushed commit or open a second PR for the same branch. Store a progress line with branch, PR number and head SHA each time you push. A document task has no branch: its live state is the brief's agent-fs output path and the task attachments. Run `agent-fs ls` on that path, download any partial and extend it. Then list the task's local work directory: a shutdown can leave the newest draft only there, or leave cache files half-written. Parse every cache file, delete the ones that fail, and re-run only the stages whose output is missing. Never re-run collection steps that already produced artifacts. If your task's `model` differs from the parent's, say so in the output, because the result's provenance changed.\n\nWrite times in replies, outputs and defer notes in the requester's timezone when the Requester Profile names one, never raw UTC.\n\nIf the user interrupts, follow their instructions. To resume, call `/work-on-task <taskId>` again.\n";
|
|
12247
12303
|
|
|
12248
12304
|
// templates/skills/workflow-iterate/config.json
|
|
12249
12305
|
var config_default52 = `{
|