@desplega.ai/agent-swarm 1.173.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/{acp-adapter-ccm9xrep.js → acp-adapter-kr4abrsk.js} +17 -16
- package/dist/{actions-sgeyv08k.js → actions-eh4g6w2r.js} +39 -34
- package/dist/{amp-adapter-gpxp3kc9.js → amp-adapter-7h7wdt8q.js} +8 -7
- package/dist/{app-km9qnv2f.js → app-7k8s9w5p.js} +18 -14
- package/dist/{assistant-5r8tcvvd.js → assistant-wswt2y7p.js} +40 -35
- package/dist/{boot-reembed-16rv9843.js → boot-reembed-9apm9qw6.js} +10 -9
- package/dist/{boot-reembed-swwzdgd7.js → boot-reembed-whzbdthj.js} +10 -9
- package/dist/{boot-scrub-sweep-zkr6vkqb.js → boot-scrub-sweep-ndzmvmgt.js} +11 -10
- package/dist/{boot-seeds-d8sp49t2.js → boot-seeds-e9sta0yt.js} +16 -13
- package/dist/{claude-adapter-yzzzh8jp.js → claude-adapter-4mqd3r6r.js} +19 -18
- package/dist/{claude-managed-adapter-xgxhqrt2.js → claude-managed-adapter-9wswb52m.js} +7 -5
- package/dist/{claude-managed-setup-bnmjarjc.js → claude-managed-setup-tpe1effx.js} +6 -4
- package/dist/{claude-sdk-session-4whxere4.js → claude-sdk-session-q49ghmh9.js} +12402 -11850
- package/dist/{cli-ne7smj1b.js → cli-01xvj1rq.js} +10 -10
- package/dist/{cli-nwrdx840.js → cli-035vekcw.js} +3 -3
- package/dist/{cli-9xf7fdtp.js → cli-0pd8hzhc.js} +29 -6
- package/dist/{cli-tmhsxhb8.js → cli-0veb92a3.js} +28 -6
- package/dist/{cli-c3rk1v0x.js → cli-10jesr25.js} +3 -3
- package/dist/{cli-6thk4jz3.js → cli-1y7yywmj.js} +6 -6
- package/dist/{cli-6r2f7edk.js → cli-20krdhz5.js} +47 -19
- package/dist/{cli-rfs4vddv.js → cli-29rja109.js} +39 -20
- package/dist/cli-2m1ktfzz.js +17795 -0
- package/dist/{cli-na2pxe4f.js → cli-2pwa0q4d.js} +15 -4
- package/dist/{cli-7a93cv8c.js → cli-4mengw46.js} +441 -43
- package/dist/{cli-94b48jhk.js → cli-5twdke5x.js} +16 -14
- package/dist/{cli-7a0sde2x.js → cli-6cagzc96.js} +9 -0
- package/dist/cli-6cvfxs8r.js +47 -0
- package/dist/{cli-w1apjecb.js → cli-711vwbxn.js} +2 -2
- package/dist/{cli-r4fqfpwf.js → cli-78vv7446.js} +50 -27
- package/dist/{cli-srqq3gc5.js → cli-7n6s5man.js} +5 -5
- package/dist/{cli-m89t9jdf.js → cli-7x0g4sn1.js} +15416 -16024
- package/dist/{cli-2bw3w47j.js → cli-8wwxw8a3.js} +1 -1
- package/dist/{cli-yatxqynm.js → cli-9ha6hs2d.js} +1 -290
- package/dist/{cli-ywm1azg9.js → cli-9jzdj90z.js} +8 -4
- package/dist/cli-9traj3dn.js +638 -0
- package/dist/{cli-smcrrmhy.js → cli-agzebbd5.js} +9 -9
- package/dist/{cli-c5km5znr.js → cli-c8r3kzzb.js} +1 -1
- package/dist/cli-cvv9k0hh.js +10143 -0
- package/dist/{cli-anzdk0z4.js → cli-d9286m9h.js} +2 -2
- package/dist/cli-e3g5mbkz.js +776 -0
- package/dist/cli-f527ce74.js +781 -0
- package/dist/{cli-gmdajfw5.js → cli-gxzkg1de.js} +5020 -1729
- package/dist/{cli-451ks6aw.js → cli-h1arqn31.js} +1 -1
- package/dist/cli-hjwmhcg1.js +10622 -0
- package/dist/{cli-jpc4r224.js → cli-hqr88ew6.js} +19 -3
- package/dist/{cli-h7k0xtry.js → cli-hvqbtna4.js} +4 -1256
- package/dist/{cli-cd0hv9nh.js → cli-j67mh2vg.js} +2 -2
- package/dist/cli-jta2eyzc.js +619 -0
- package/dist/{cli-wd0hz6ah.js → cli-kbrt8gqa.js} +9 -91
- package/dist/cli-kceah1eh.js +92 -0
- package/dist/{cli-d603qf86.js → cli-ksphgh48.js} +10 -8
- package/dist/{cli-3nxdmjrc.js → cli-mgz4nbgg.js} +4 -4
- package/dist/{cli-saqyage0.js → cli-mz5as5ha.js} +179 -27
- package/dist/cli-n2wq4j7m.js +6985 -0
- package/dist/{cli-ppy4nnnh.js → cli-n8ndw8j7.js} +2 -2
- package/dist/{cli-1w0vj3gw.js → cli-naqzr17j.js} +7 -7
- package/dist/{cli-0a7mm3zp.js → cli-nm1p8jfr.js} +25 -21
- package/dist/{cli-wrh08zjd.js → cli-p3zbc1rw.js} +5 -18
- package/dist/{cli-hsem16hb.js → cli-pe05f8yk.js} +29 -6
- package/dist/{cli-5wpw4rab.js → cli-qmzsbkvy.js} +5 -5
- package/dist/{cli-bv2etw2y.js → cli-qnm4e9e4.js} +99 -590
- package/dist/{cli-1x7da0de.js → cli-qqy4q48b.js} +8 -8
- package/dist/{cli-r44t9wqc.js → cli-r106725k.js} +2 -2
- package/dist/{cli-5er5gjbm.js → cli-r35rw5de.js} +15 -15
- package/dist/{cli-v3zhnxsb.js → cli-r610p9bs.js} +2 -2
- package/dist/{cli-7jykv7ef.js → cli-rc6pf5kv.js} +1 -1
- package/dist/{cli-x119kdba.js → cli-rdxm77k3.js} +1 -1
- package/dist/cli-rhs9pz33.js +294 -0
- package/dist/{cli-0p3enk35.js → cli-s69nfyeh.js} +9 -1
- package/dist/{cli-0kxf81jj.js → cli-sawv63e8.js} +2 -2
- package/dist/{cli-e3pmyt39.js → cli-sg19gbf0.js} +1 -1
- package/dist/{cli-d161g69q.js → cli-shk7r5cs.js} +57 -6
- package/dist/{cli-7pz3b7x5.js → cli-sksetb2q.js} +36 -1
- package/dist/{cli-014vns2y.js → cli-stfs0k5s.js} +208 -525
- package/dist/{cli-db3m6zdt.js → cli-t02z59f2.js} +1 -1
- package/dist/{cli-b07hr3bt.js → cli-txqb9s38.js} +2 -2
- package/dist/{cli-s8z6x4b4.js → cli-v7g551te.js} +225 -113
- package/dist/{cli-rfz2ve55.js → cli-vgqzp4em.js} +3341 -955
- package/dist/{cli-7avy4v8w.js → cli-w1q7h2r2.js} +5 -3
- package/dist/{cli-rcp5aqdw.js → cli-w7m81tb7.js} +1 -1
- package/dist/cli-wfnemr1t.js +86 -0
- package/dist/cli-wmgrkscj.js +23254 -0
- package/dist/{cli-qm1448f9.js → cli-wms6x9db.js} +8 -8
- package/dist/{cli-tadfgd0t.js → cli-x1r545x8.js} +33 -4
- package/dist/{cli-xc7p7s3g.js → cli-xczsgshn.js} +6 -6
- package/dist/{cli-fmhj1ehj.js → cli-y3167wt1.js} +882 -271
- package/dist/{cli-r0byd61h.js → cli-yjmqpxt3.js} +17 -16
- package/dist/{cli-81r1wjyv.js → cli-z780ec7c.js} +1 -1
- package/dist/{cli-phcqp1b9.js → cli-zm9t2vde.js} +2 -2
- package/dist/cli.js +34 -49
- package/dist/{codex-adapter-r3dn4fp5.js → codex-adapter-4z16v9n0.js} +15 -14
- package/dist/{codex-login-841kv5fk.js → codex-login-pkepy2j1.js} +3 -2
- package/dist/{codex-session-runner-fwt793h4.js → codex-session-runner-rvms9y2x.js} +15 -14
- package/dist/{commands-z8fq9qna.js → commands-2a9rtyat.js} +11 -10
- package/dist/{controller-hq5ggpcp.js → controller-3k3madbn.js} +5 -4
- package/dist/{controllers-connect-3mf2btyj.js → controllers-connect-v9yn8zb9.js} +42 -37
- package/dist/{cursor-adapter-q8pgv5qa.js → cursor-adapter-98gbhyzn.js} +7 -6
- package/dist/{db-qfpq4bt7.js → db-eywwsnjw.js} +30 -9
- package/dist/{db-yyhqqrbf.js → db-ncypk9np.js} +12 -11
- package/dist/{devin-adapter-xgtj680j.js → devin-adapter-zeq8b9gz.js} +3 -2
- package/dist/{dispatcher-x0x7qfcq.js → dispatcher-w94kc575.js} +38 -33
- package/dist/{docker-f503k9p0.js → docker-b1e4cqab.js} +3 -2
- package/dist/{dsh-adapter-34etvb5m.js → dsh-adapter-47ztej36.js} +7 -6
- package/dist/{e2b-66xra4az.js → e2b-bbt8ey45.js} +4 -3
- package/dist/{e2b-xwhyj8cw.js → e2b-dp804sen.js} +4 -3
- package/dist/{event-bus-vjtv82gm.js → event-bus-j2jdg3qb.js} +3 -2
- package/dist/{external-channel-h1fjecwe.js → external-channel-zrkg8ar9.js} +11 -10
- package/dist/{google-generative-ai-szdxbpqg.js → google-generative-ai-g8db7x0f.js} +4 -2
- package/dist/{google-vertex-qzw3p36d.js → google-vertex-r2692q0r.js} +4 -2
- package/dist/{grok-adapter-e7mr287e.js → grok-adapter-s9ncp9vy.js} +18 -17
- package/dist/{handlers-znb9n7d4.js → handlers-kqftvjgw.js} +41 -36
- package/dist/{heartbeat-bm4qkn8d.js → heartbeat-9q96p8v9.js} +43 -38
- package/dist/{hook-254r4mza.js → hook-56qe10dp.js} +11 -23
- package/dist/{hook-xaa3q80s.js → hook-yy6rw42k.js} +12 -11
- package/dist/{http-3dgrgp8y.js → http-r2dnb8f8.js} +2411 -275
- package/dist/{index-fx3jqq1e.js → index-3gtmkhc6.js} +38 -33
- package/dist/{index-bdsqa799.js → index-3k5hkz7r.js} +43 -38
- package/dist/index-52j1aja4.js +3465 -0
- package/dist/{index-g2byveez.js → index-5e101240.js} +38 -33
- package/dist/{index-2ybk7khy.js → index-5wyfjc2c.js} +845 -683
- package/dist/index-9gw58n4k.js +193 -0
- package/dist/index-9j87mcm8.js +56305 -0
- package/dist/index-bsmcresk.js +79424 -0
- package/dist/index-mh85qhec.js +475 -0
- package/dist/{index-jd7sejm2.js → index-vxvhxxbn.js} +10 -9
- package/dist/index-we27kd31.js +12810 -0
- package/dist/{keepalive-6ywhpja1.js → keepalive-psj6j54c.js} +12 -11
- package/dist/{lead-cw45n21s.js → lead-hajz4688.js} +61 -56
- package/dist/{maintenance-ss1a1ge9.js → maintenance-ps02sb5m.js} +10 -9
- package/dist/{node-fkajaq96.js → node-mwb5r697.js} +2 -1
- package/dist/{oauth-refresh-sweep-3c6f8vyy.js → oauth-refresh-sweep-0284h93b.js} +11 -10
- package/dist/{onboard-8hzdhc1k.js → onboard-rbf6bg01.js} +11 -3
- package/dist/{opencode-adapter-qbchbcfz.js → opencode-adapter-13dcatgq.js} +18 -13
- package/dist/{otel-impl-e6mnvc4v.js → otel-impl-zvqcbhp7.js} +242 -868
- package/dist/{pi-mono-adapter-3c9g9snh.js → pi-mono-adapter-xxy47q4v.js} +244 -106
- package/dist/{pricing-refresh-gedkjdw0.js → pricing-refresh-kefx9v3t.js} +12 -11
- package/dist/{rbac-roles-dt4vh1yy.js → rbac-roles-3m36rxm2.js} +11 -10
- package/dist/{rbac-roles-eszdg6pm.js → rbac-roles-z76xv0hq.js} +12 -11
- package/dist/{render-v2-907cy6sa.js → render-v2-wz73bs25.js} +18 -14
- package/dist/{resume-42stcaz0.js → resume-k4wzge8w.js} +25 -21
- package/dist/{runtime-qfry88v2.js → runtime-sa2vr16g.js} +2 -2
- package/dist/{setup-adbc1c10.js → setup-2fp2pypp.js} +2 -2
- package/dist/{virtual-modules-rtmt494q.js → virtual-modules-9gkxqrap.js} +6 -5
- package/dist/{work-objects-wpj8ck95.js → work-objects-6k0ywz3m.js} +41 -36
- package/dist/{worker-bzdra9rf.js → worker-5v3rxsdh.js} +61 -56
- package/dist/{x-qfmygqqv.js → x-4mtdeym1.js} +3 -2
- package/openapi.json +1279 -329
- package/package.json +15 -15
- package/src/agentmail/handlers.ts +21 -11
- package/src/apps/definition.ts +16 -0
- package/src/azure-devops/api.ts +52 -2
- package/src/azure-devops/boards-sync.ts +50 -20
- package/src/azure-devops/index.ts +1 -0
- package/src/azure-devops/outbound.ts +95 -0
- package/src/be/boot-seeds.ts +11 -0
- package/src/be/config-refs.ts +60 -0
- package/src/be/db/model-profiles.ts +315 -0
- package/src/be/db/page-versions.ts +15 -0
- package/src/be/db/runtime.ts +35 -8
- package/src/be/db/skill-name-resolution.ts +36 -0
- package/src/be/db/tasks/read.ts +237 -0
- package/src/be/db/tasks/write.ts +7 -0
- package/src/be/db.ts +87 -36
- package/src/be/migrations/215_swarm_config_export_to_env.sql +34 -0
- package/src/be/migrations/216_task_resolved_effort.sql +5 -0
- package/src/be/migrations/217_chat_sdk_state.sql +104 -0
- package/src/be/migrations/219_relabel_pi_context_compactions.sql +22 -0
- package/src/be/migrations/220_model_profiles.sql +137 -0
- package/src/be/model-catalog-store.ts +5 -1
- package/src/be/model-tier-resolution.ts +155 -10
- package/src/be/model-validation.ts +12 -2
- package/src/be/models-catalog.ts +26 -1
- package/src/be/modelsdev-cache.json +198 -63
- package/src/be/modelsdev-cache.ts +9 -1
- package/src/be/script-credential-broker.ts +39 -2
- package/src/be/scripts/run-saved.ts +1 -0
- package/src/be/scripts/typecheck.ts +22 -8
- package/src/be/secret-registry.ts +6 -2
- package/src/be/seed/runner.ts +13 -1
- package/src/be/seed/state-db.ts +5 -0
- package/src/be/seed/types.ts +6 -0
- package/src/be/seed-model-profiles.ts +41 -0
- package/src/be/seed-pricing.ts +19 -2
- package/src/be/seed-scripts/catalog/heartbeat-jev-gate.ts +368 -0
- package/src/be/seed-scripts/catalog/ops-catalog-audit.inline.ts +1 -1
- package/src/be/seed-scripts/catalog/ops-catalog-audit.ts +1 -1
- package/src/be/seed-scripts/index.ts +9 -0
- package/src/be/seed-skills/bundled-files.generated.json +0 -18
- package/src/be/seed-skills/index.ts +104 -3
- package/src/be/swarm-config-guard.ts +53 -0
- package/src/cli.tsx +0 -16
- package/src/commands/onboard/non-interactive.ts +4 -2
- package/src/commands/provider-credentials.ts +52 -6
- package/src/commands/runner.ts +193 -71
- package/src/e2b/env.ts +5 -0
- package/src/extensions/catalog.generated.json +17 -0
- package/src/extensions/contract-types.generated.ts +1 -1
- package/src/extensions/contract.ts +45 -0
- package/src/extensions/dispatcher.ts +7 -0
- package/src/extensions/loader.ts +10 -5
- package/src/extensions/post-bridge.ts +40 -0
- package/src/github/handlers.ts +11 -4
- package/src/heartbeat/heartbeat.ts +2 -1
- package/src/heartbeat/templates.ts +8 -5
- package/src/hooks/hook.ts +0 -26
- package/src/http/agents.ts +49 -2
- package/src/http/all-routes.ts +1 -1
- package/src/http/apps.ts +30 -3
- package/src/http/config.ts +62 -19
- package/src/http/core.ts +8 -0
- package/src/http/index.ts +25 -3
- package/src/http/integrations.ts +88 -1
- package/src/http/mcp.ts +15 -5
- package/src/http/model-profiles.ts +303 -0
- package/src/http/page-proxy.ts +2 -4
- package/src/http/pages-public.ts +13 -7
- package/src/http/pages.ts +3 -13
- package/src/http/poll.ts +42 -71
- package/src/http/requested-by.ts +54 -0
- package/src/http/script-runs.ts +1 -1
- package/src/http/scripts.ts +26 -2
- package/src/http/stats.ts +1 -30
- package/src/http/tasks.ts +79 -5
- package/src/http/utils.ts +14 -0
- package/src/http/webhooks.ts +160 -3
- package/src/http/x.ts +1 -0
- package/src/integrations/chat-sdk/adapters.ts +129 -0
- package/src/integrations/chat-sdk/attachments.ts +232 -0
- package/src/integrations/chat-sdk/background.ts +36 -0
- package/src/integrations/chat-sdk/commands.ts +194 -0
- package/src/integrations/chat-sdk/config.ts +198 -0
- package/src/integrations/chat-sdk/discord-api.ts +63 -0
- package/src/integrations/chat-sdk/discord-gateway.ts +185 -0
- package/src/integrations/chat-sdk/handlers.ts +28 -0
- package/src/integrations/chat-sdk/inbound.ts +426 -0
- package/src/integrations/chat-sdk/limits.ts +48 -0
- package/src/integrations/chat-sdk/logger.ts +81 -0
- package/src/integrations/chat-sdk/outbound.ts +123 -0
- package/src/integrations/chat-sdk/relay.ts +172 -0
- package/src/integrations/chat-sdk/router.ts +169 -0
- package/src/integrations/chat-sdk/runtime.ts +351 -0
- package/src/integrations/chat-sdk/schema.ts +245 -0
- package/src/integrations/chat-sdk/state.ts +243 -0
- package/src/integrations/chat-sdk/status.ts +244 -0
- package/src/integrations/chat-sdk/steering.ts +58 -0
- package/src/integrations/chat-sdk/teams-origin.ts +77 -0
- package/src/integrations/chat-sdk/telegram-api.ts +56 -0
- package/src/integrations/chat-sdk/templates.ts +16 -0
- package/src/integrations/chat-sdk/test-hooks.ts +37 -0
- package/src/integrations/chat-sdk/thread-buffer.ts +100 -0
- package/src/integrations/chat-sdk/typing.ts +65 -0
- package/src/{artifact-sdk → pages}/feedback-overlay.ts +65 -6
- package/src/pages/version.ts +4 -5
- package/src/prompts/base-prompt.ts +111 -45
- package/src/providers/acp-target-catalog.ts +6 -0
- package/src/providers/acp-targets.ts +56 -3
- package/src/providers/amp-adapter.ts +1 -1
- package/src/providers/claude-adapter.ts +1 -1
- package/src/providers/claude-flag-settings.ts +17 -5
- package/src/providers/claude-managed-adapter.ts +1 -1
- package/src/providers/claude-session-events.ts +6 -1
- package/src/providers/dsh-adapter.ts +1 -1
- package/src/providers/opencode-adapter.ts +243 -26
- package/src/providers/opencode-swarm-plugin.ts +31 -0
- package/src/providers/pi-mono-adapter.ts +398 -153
- package/src/providers/pi-mono-extension.ts +17 -3
- package/src/providers/pricing-sources.md +33 -2
- package/src/providers/reasoning-effort.ts +37 -0
- package/src/providers/types.ts +6 -2
- package/src/rbac/legacy-policy.ts +16 -2
- package/src/rbac/permissions.ts +26 -0
- package/src/scheduler/deferred-task-waits.ts +18 -5
- package/src/scheduler/scheduler.ts +11 -3
- package/src/scripts-runtime/executors/quickjs-prelude.ts +75 -6
- package/src/scripts-runtime/executors/quickjs-runner.ts +85 -8
- package/src/scripts-runtime/executors/quickjs-web-shims.ts +1041 -0
- package/src/scripts-runtime/extract-args-schema.ts +9 -8
- package/src/scripts-runtime/loader.ts +19 -7
- package/src/scripts-runtime/quickjs-compat.ts +464 -0
- package/src/scripts-runtime/sdk-allowlist.ts +12 -6
- package/src/scripts-runtime/swarm-sdk.ts +0 -11
- package/src/scripts-runtime/types/stdlib.d.ts +39 -15
- package/src/scripts-runtime/types/swarm-extension.d.ts +66 -1
- package/src/scripts-runtime/types/swarm-sdk.d.ts +39 -15
- package/src/server.ts +62 -19
- package/src/slack/blocks.ts +4 -2
- package/src/slack/event-dedup.ts +1 -1
- package/src/slack/reaction-shortcode.ts +14 -0
- package/src/slack/render-v2.ts +12 -7
- package/src/slack/responses.ts +5 -4
- package/src/slack/thread-buffer.ts +41 -22
- package/src/tasks/additive-buffer.ts +4 -2
- package/src/tasks/context-key.ts +60 -1
- package/src/tasks/model-pin.ts +67 -0
- package/src/tasks/worker-follow-up.ts +15 -33
- package/src/tests/acp-adapter.test.ts +42 -0
- package/src/tests/agentmail-filters.test.ts +47 -0
- package/src/tests/api-drain-worker-e2e.test.ts +1 -6
- package/src/tests/apps-spike2.test.ts +35 -0
- package/src/tests/azure-devops-outbound.test.ts +420 -0
- package/src/tests/base-prompt.test.ts +58 -54
- package/src/tests/chat-sdk-commands.test.ts +191 -0
- package/src/tests/chat-sdk-config.test.ts +334 -0
- package/src/tests/chat-sdk-discord-api.test.ts +68 -0
- package/src/tests/chat-sdk-discord-gateway.test.ts +252 -0
- package/src/tests/chat-sdk-discord-webhook.test.ts +126 -0
- package/src/tests/chat-sdk-fields-sync.test.ts +50 -0
- package/src/tests/chat-sdk-harness.ts +146 -0
- package/src/tests/chat-sdk-inbound.test.ts +363 -0
- package/src/tests/chat-sdk-relay.test.ts +470 -0
- package/src/tests/chat-sdk-reload.test.ts +128 -0
- package/src/tests/chat-sdk-shutdown.test.ts +220 -0
- package/src/tests/chat-sdk-slack.test.ts +268 -0
- package/src/tests/chat-sdk-state.test.ts +163 -0
- package/src/tests/chat-sdk-status-route.test.ts +382 -0
- package/src/tests/chat-sdk-teams.test.ts +277 -0
- package/src/tests/chat-sdk-tools.test.ts +520 -0
- package/src/tests/chat-sdk-typing.test.ts +120 -0
- package/src/tests/chat-sdk-webhook-telegram.test.ts +200 -0
- package/src/tests/claude-adapter-binary.test.ts +9 -4
- package/src/tests/claude-flag-settings.test.ts +11 -3
- package/src/tests/claude-managed-adapter.test.ts +1 -1
- package/src/tests/claude-sdk-lifecycle.test.ts +9 -3
- package/src/tests/claude-thinking-tokens.test.ts +57 -0
- package/src/tests/claude-worker-parity.test.ts +2 -7
- package/src/tests/config-env-export-chat-sdk.test.ts +11 -0
- package/src/tests/config-refs.test.ts +66 -0
- package/src/tests/context-key.test.ts +19 -0
- package/src/tests/cors-allowlist.test.ts +25 -0
- package/src/tests/create-page-tool.test.ts +18 -0
- package/src/tests/credential-check.test.ts +139 -0
- package/src/tests/credentials.test.ts +21 -0
- package/src/tests/db-runtime.test.ts +61 -0
- package/src/tests/db-task-read-contract.test.ts +22 -0
- package/src/tests/db-task-write-contract.test.ts +11 -0
- package/src/tests/deferred-task-wake.test.ts +78 -1
- package/src/tests/docs-index-footprint.test.js +3 -3
- package/src/tests/dsh-adapter.test.ts +1 -1
- package/src/tests/enabled-capabilities.test.ts +20 -0
- package/src/tests/entrypoint-api-readiness.test.ts +14 -8
- package/src/tests/entrypoint-config-env-export.test.ts +5 -2
- package/src/tests/entrypoint-export-to-env.test.ts +78 -0
- package/src/tests/env-flag.test.ts +1 -0
- package/src/tests/explicit-model-validation.test.ts +105 -1
- package/src/tests/extensions-catalog.test.ts +1 -0
- package/src/tests/extensions-example-comms-adherence-gate.test.ts +413 -0
- package/src/tests/extensions-example-route-chat-channel.test.ts +154 -0
- package/src/tests/extensions-loader.test.ts +15 -0
- package/src/tests/extensions-post-bridge.test.ts +51 -0
- package/src/tests/fetch-resolved-env.test.ts +128 -0
- package/src/tests/fixtures/chat-sdk/telegram-private-text.json +10 -0
- package/src/tests/fixtures/extensions/route-chat-channel-to-agent/hooks.ts +27 -0
- package/src/tests/fixtures/extensions/route-chat-channel-to-agent/manifest.json +9 -0
- package/src/tests/fixtures/openai-compatible-registry-extension.child.ts +146 -0
- package/src/tests/fixtures/runner-legacy-resume-prompt.ts +122 -0
- package/src/tests/github-handlers.test.ts +35 -0
- package/src/tests/harness-provider-resolution.test.ts +12 -0
- package/src/tests/heartbeat-checklist.test.ts +3 -2
- package/src/tests/heartbeat-jev-gate-credentials.test.ts +168 -0
- package/src/tests/heartbeat-jev-gate.test.ts +385 -0
- package/src/tests/hook-runtime-close.test.ts +0 -2
- package/src/tests/http-api-integration.test.ts +0 -25
- package/src/tests/migration-046-budgets.test.ts +17 -16
- package/src/tests/migration-217-chat-sdk-state.test.ts +113 -0
- package/src/tests/model-profile-backcompat.test.ts +327 -0
- package/src/tests/model-profile-providers.test.ts +302 -0
- package/src/tests/model-profile-runtime-secrets.test.ts +223 -0
- package/src/tests/model-profiles-api.test.ts +614 -0
- package/src/tests/model-tier-resolution.test.ts +93 -0
- package/src/tests/models-catalog.test.ts +42 -1
- package/src/tests/openai-compatible-providers.test.ts +134 -0
- package/src/tests/openai-compatible-registry-extension.test.ts +42 -0
- package/src/tests/opencode-adapter.test.ts +216 -2
- package/src/tests/opencode-helmcode-sdk.test.ts +201 -0
- package/src/tests/opencode-shell-env.test.ts +145 -0
- package/src/tests/openrouter-base-url.test.ts +101 -151
- package/src/tests/page-proxy.test.ts +1 -1
- package/src/tests/pages-browser-sdk.test.ts +76 -0
- package/src/tests/pages-feedback-keys.test.ts +121 -0
- package/src/tests/pages-feedback.test.ts +4 -1
- package/src/tests/paused-tasks-requested-by.test.ts +102 -0
- package/src/tests/pi-context-compaction.test.ts +193 -0
- package/src/tests/pi-custom-provider-fake-server.test.ts +442 -0
- package/src/tests/pi-mono-adapter.test.ts +385 -5
- package/src/tests/pi-shell-env.test.ts +170 -0
- package/src/tests/profile-claim.test.ts +269 -0
- package/src/tests/prompt-template-remaining.test.ts +35 -0
- package/src/tests/quickjs-compat.test.ts +213 -0
- package/src/tests/rbac-engine.test.ts +32 -3
- package/src/tests/runner-exit-after-tasks.test.ts +0 -4
- package/src/tests/runner-legacy-resume-prompt.test.ts +64 -0
- package/src/tests/script-connections.test.ts +6 -0
- package/src/tests/script-executor-conformance.test.ts +252 -1
- package/src/tests/script-executor-quickjs.test.ts +96 -0
- package/src/tests/script-runs-http.test.ts +89 -0
- package/src/tests/scripts-http.test.ts +217 -1
- package/src/tests/scripts-only-gating.test.ts +2 -3
- package/src/tests/scripts-runtime.test.ts +58 -40
- package/src/tests/secret-scrubber.test.ts +96 -0
- package/src/tests/seed-model-profiles.test.ts +174 -0
- package/src/tests/seed-scripts.test.ts +2 -2
- package/src/tests/seed-skills-bundled-files.test.ts +45 -53
- package/src/tests/seed-skills-retire.test.ts +182 -0
- package/src/tests/send-task-requester-gate.test.ts +58 -0
- package/src/tests/session-cost-recompute-unit.test.ts +46 -0
- package/src/tests/skill-get-name-resolution.test.ts +137 -0
- package/src/tests/skill-invocations.test.ts +4 -2
- package/src/tests/slack-blocks.test.ts +36 -0
- package/src/tests/slack-inline-output.test.ts +14 -0
- package/src/tests/slack-reaction-config.test.ts +20 -0
- package/src/tests/slack-render-v2.test.ts +37 -0
- package/src/tests/slack-thread-buffer.test.ts +98 -1
- package/src/tests/sonnet-5-5-registry.test.ts +9 -1
- package/src/tests/swarm-config-export-to-env.test.ts +267 -0
- package/src/tests/swarm-config-reserved-keys.test.ts +16 -0
- package/src/tests/swarm-diff.test.ts +1 -1
- package/src/tests/task-model-effort-rerun.test.ts +164 -0
- package/src/tests/task-session-prompts.test.ts +132 -0
- package/src/tests/task-tool-manifest.test.ts +14 -0
- package/src/tests/task-tools-ctx.test.ts +2 -0
- package/src/tests/tool-annotations.test.ts +6 -6
- package/src/tests/ui-logs-parser.test.ts +43 -0
- package/src/tests/used-by-route.test.ts +6 -0
- package/src/tests/vercel-ai-gateway.test.ts +149 -0
- package/src/tests/workflow-llm.test.ts +61 -0
- package/src/tests/workflow-swarm-script.test.ts +38 -0
- package/src/tests/workflow-wait-filter.test.ts +5 -3
- package/src/tools/chat-access.ts +41 -0
- package/src/tools/chat-open-dm.ts +59 -0
- package/src/tools/chat-post.ts +74 -0
- package/src/tools/chat-read-thread.ts +100 -0
- package/src/tools/chat-reply.ts +75 -0
- package/src/tools/create-page.ts +3 -13
- package/src/tools/model-catalog/index.ts +2 -1
- package/src/tools/model-profiles/list-model-profiles.ts +54 -0
- package/src/tools/register-chat-channel.ts +139 -0
- package/src/tools/request-human-input.ts +11 -3
- package/src/tools/script-apis.ts +5 -1
- package/src/tools/script-upsert.ts +4 -1
- package/src/tools/send-task.ts +48 -1
- package/src/tools/skills/skill-get.ts +17 -9
- package/src/tools/swarm-config/get-config.ts +2 -1
- package/src/tools/swarm-config/list-config.ts +2 -1
- package/src/tools/swarm-config/set-config.ts +16 -3
- package/src/tools/system-one-decide.ts +104 -0
- package/src/tools/task-action.ts +22 -1
- package/src/tools/tool-config.ts +14 -6
- package/src/types.ts +48 -3
- package/src/utils/config-env-export.ts +74 -0
- package/src/utils/credentials.test.ts +92 -0
- package/src/utils/credentials.ts +52 -4
- package/src/utils/harness-provider.ts +15 -6
- package/src/utils/runtime-model-catalog.ts +11 -1
- package/src/utils/secret-scrubber.ts +86 -10
- package/src/utils/task-tool-manifest.ts +19 -2
- package/src/utils/vcs-repo-match.ts +29 -0
- package/src/workflows/executors/raw-llm.ts +8 -3
- package/src/workflows/executors/swarm-script.ts +1 -0
- package/src/workflows/executors/system-one-decision.ts +21 -11
- package/src/workflows/executors/workflow-llm.ts +22 -0
- package/src/workflows/index.ts +21 -12
- package/templates/extensions/README.md +1 -0
- package/templates/extensions/comms-adherence-gate/README.md +88 -0
- package/templates/extensions/comms-adherence-gate/hooks.ts +489 -0
- package/templates/extensions/comms-adherence-gate/manifest.json +8 -0
- package/templates/skills/agent-browser/SKILL.md +1 -1
- package/templates/skills/agent-browser/content.md +1 -1
- package/templates/skills/apps/SKILL.md +8 -1
- package/templates/skills/apps/content.md +8 -1
- package/templates/skills/chat-sdk/config.json +14 -0
- package/templates/skills/chat-sdk/content.md +84 -0
- package/templates/skills/composio-gmail/content.md +7 -0
- package/templates/skills/composio-google-docs/content.md +1 -1
- package/templates/skills/email-domain-health/config.json +14 -0
- package/templates/skills/email-domain-health/content.md +131 -0
- package/templates/skills/kv-storage/content.md +2 -2
- package/templates/skills/pages/content.md +8 -10
- package/templates/skills/swarm-extensions/content.md +2 -0
- package/templates/skills/swarm-scripts/SKILL.md +4 -2
- package/templates/skills/swarm-scripts/config.json +1 -1
- package/templates/skills/swarm-scripts/content.md +4 -2
- package/templates/skills/workflow-iterate/content.md +38 -0
- package/dist/artifact-eczsknc3.js +0 -3843
- package/dist/cli-42qhyvwx.js +0 -33
- package/dist/cli-bd957dgb.js +0 -153
- package/dist/cli-q51yprfq.js +0 -1104
- package/dist/cli-t1ze64h3.js +0 -811
- package/dist/cli-tcc4wd5d.js +0 -56471
- package/dist/index-9ch1n761.js +0 -1102
- package/src/artifact-sdk/index.ts +0 -2
- package/src/artifact-sdk/localtunnel.d.ts +0 -20
- package/src/artifact-sdk/port.ts +0 -12
- package/src/artifact-sdk/server.ts +0 -183
- package/src/artifact-sdk/tunnel.ts +0 -19
- package/src/commands/artifact.ts +0 -249
- package/src/http/ecosystem.ts +0 -77
- package/src/tests/artifact-commands.test.ts +0 -92
- package/src/tests/artifact-sdk.test.ts +0 -793
- package/src/tests/register-service-security.test.ts +0 -194
- package/src/tools/list-services.ts +0 -107
- package/src/tools/register-service.ts +0 -177
- package/src/tools/unregister-service.ts +0 -73
- package/src/tools/update-service-status.ts +0 -96
- package/templates/skills/artifacts/config.json +0 -14
- package/templates/skills/artifacts/content.md +0 -344
- package/templates/skills/artifacts/files/examples/approval-flow.ts +0 -34
- package/templates/skills/artifacts/files/examples/hono-dashboard.ts +0 -31
- package/templates/skills/artifacts/files/examples/multi-artifact.ts +0 -20
- package/templates/skills/artifacts/files/examples/static-report.sh +0 -17
- /package/src/{artifact-sdk → pages}/browser-sdk.ts +0 -0
|
@@ -3,45 +3,50 @@ import {
|
|
|
3
3
|
} from "./cli-ep19k3mb.js";
|
|
4
4
|
import {
|
|
5
5
|
agentFsProvisionSeeder
|
|
6
|
-
} from "./cli-
|
|
6
|
+
} from "./cli-zm9t2vde.js";
|
|
7
7
|
import {
|
|
8
8
|
extractArgsJsonSchema,
|
|
9
9
|
extractScriptSignature,
|
|
10
|
+
parseSkillContent,
|
|
10
11
|
require_dist,
|
|
11
12
|
typecheckScript,
|
|
12
13
|
validateScriptImports
|
|
13
|
-
} from "./cli-
|
|
14
|
+
} from "./cli-4mengw46.js";
|
|
14
15
|
import"./cli-epan13p1.js";
|
|
15
16
|
import"./cli-zt3w1cn1.js";
|
|
16
|
-
import"./cli-
|
|
17
|
+
import"./cli-711vwbxn.js";
|
|
17
18
|
import {
|
|
18
19
|
validateDefinition
|
|
19
|
-
} from "./cli-
|
|
20
|
-
import"./cli-
|
|
21
|
-
import"./cli-
|
|
22
|
-
import"./cli-b07hr3bt.js";
|
|
23
|
-
import"./cli-h6vtvt5c.js";
|
|
20
|
+
} from "./cli-20krdhz5.js";
|
|
21
|
+
import"./cli-1y7yywmj.js";
|
|
22
|
+
import"./cli-rc6pf5kv.js";
|
|
24
23
|
import"./cli-2x0e561b.js";
|
|
24
|
+
import"./cli-txqb9s38.js";
|
|
25
|
+
import"./cli-h6vtvt5c.js";
|
|
25
26
|
import"./cli-bsakbbkm.js";
|
|
26
27
|
import"./cli-zxg80zs9.js";
|
|
27
28
|
import"./cli-ydt3zkwv.js";
|
|
28
|
-
import"./cli-
|
|
29
|
-
import"./cli-
|
|
29
|
+
import"./cli-cvv9k0hh.js";
|
|
30
|
+
import"./cli-n2wq4j7m.js";
|
|
31
|
+
import"./cli-wmgrkscj.js";
|
|
30
32
|
import"./cli-3er6gcsn.js";
|
|
31
|
-
import"./cli-
|
|
32
|
-
import"./cli-
|
|
33
|
+
import"./cli-hjwmhcg1.js";
|
|
34
|
+
import"./cli-rhs9pz33.js";
|
|
35
|
+
import"./cli-9ha6hs2d.js";
|
|
36
|
+
import"./cli-r610p9bs.js";
|
|
33
37
|
import"./cli-61625k3x.js";
|
|
34
38
|
import"./cli-1m2e086m.js";
|
|
35
39
|
import"./cli-btbzvq95.js";
|
|
36
40
|
import"./cli-p7z6q2m6.js";
|
|
37
41
|
import"./cli-wnb9b2g3.js";
|
|
38
|
-
import"./cli-
|
|
42
|
+
import"./cli-f527ce74.js";
|
|
39
43
|
import"./cli-pzxba7gw.js";
|
|
40
44
|
import {
|
|
41
45
|
computeContentHash,
|
|
42
46
|
createScheduledTask,
|
|
43
47
|
createSkill,
|
|
44
48
|
createWorkflow,
|
|
49
|
+
deleteSkill,
|
|
45
50
|
deleteSkillFile,
|
|
46
51
|
getDbClient,
|
|
47
52
|
getScheduledTaskByName,
|
|
@@ -56,29 +61,30 @@ import {
|
|
|
56
61
|
updateWorkflow,
|
|
57
62
|
upsertScriptByName,
|
|
58
63
|
upsertSkillFiles
|
|
59
|
-
} from "./cli-
|
|
60
|
-
import"./cli-
|
|
61
|
-
import"./cli-
|
|
62
|
-
import"./cli-
|
|
63
|
-
import"./cli-7mm2r1kp.js";
|
|
64
|
+
} from "./cli-y3167wt1.js";
|
|
65
|
+
import"./cli-9jzdj90z.js";
|
|
66
|
+
import"./cli-pe05f8yk.js";
|
|
67
|
+
import"./cli-78vv7446.js";
|
|
64
68
|
import"./cli-dmtrxqhg.js";
|
|
69
|
+
import"./cli-7mm2r1kp.js";
|
|
70
|
+
import"./cli-x1r545x8.js";
|
|
65
71
|
import"./cli-mvzyxwg3.js";
|
|
66
|
-
import"./cli-tadfgd0t.js";
|
|
67
72
|
import"./cli-fdvbjtbw.js";
|
|
68
73
|
import"./cli-ywybs5as.js";
|
|
69
74
|
import"./cli-f9w1semm.js";
|
|
70
75
|
import"./cli-1a2j50m0.js";
|
|
71
76
|
import"./cli-a35hzdve.js";
|
|
72
|
-
import"./cli-
|
|
77
|
+
import"./cli-stfs0k5s.js";
|
|
73
78
|
import"./cli-sd0mxa3c.js";
|
|
74
79
|
import"./cli-w5e1d1vv.js";
|
|
75
80
|
import"./cli-p0sg24zp.js";
|
|
76
|
-
import"./cli-
|
|
81
|
+
import"./cli-r35rw5de.js";
|
|
77
82
|
import"./cli-zf8jjwex.js";
|
|
78
83
|
import"./cli-gd4favx9.js";
|
|
79
84
|
import"./cli-ej6gbxfb.js";
|
|
80
85
|
import"./cli-a1b9b32n.js";
|
|
81
|
-
import"./cli-
|
|
86
|
+
import"./cli-shk7r5cs.js";
|
|
87
|
+
import"./cli-e3g5mbkz.js";
|
|
82
88
|
import {
|
|
83
89
|
__toESM
|
|
84
90
|
} from "./cli-bc05qbss.js";
|
|
@@ -2585,6 +2591,377 @@ export default async function groupCount(args: any, _ctx: any) {
|
|
|
2585
2591
|
}
|
|
2586
2592
|
`;
|
|
2587
2593
|
|
|
2594
|
+
// src/be/seed-scripts/catalog/heartbeat-jev-gate.ts
|
|
2595
|
+
var heartbeat_jev_gate_default = `import { z } from "zod";
|
|
2596
|
+
|
|
2597
|
+
export const argsSchema = z.object({
|
|
2598
|
+
leadAgentId: z.string().optional().describe("Lead agent id; the heartbeat passes it"),
|
|
2599
|
+
dryRun: z
|
|
2600
|
+
.boolean()
|
|
2601
|
+
.optional()
|
|
2602
|
+
.describe(
|
|
2603
|
+
"Return the verdict, the compact state and the prompt it would create, without creating a prompt or writing KV",
|
|
2604
|
+
),
|
|
2605
|
+
threshold: z
|
|
2606
|
+
.number()
|
|
2607
|
+
.min(0)
|
|
2608
|
+
.max(1)
|
|
2609
|
+
.optional()
|
|
2610
|
+
.describe("Quiet when Jev's p(needs action) is below this (default 0.5)"),
|
|
2611
|
+
provider: z
|
|
2612
|
+
.enum(["auto", "typesafe", "openrouter"])
|
|
2613
|
+
.optional()
|
|
2614
|
+
.describe("Jev host; auto prefers TypeSafe (default auto)"),
|
|
2615
|
+
timeZone: z
|
|
2616
|
+
.string()
|
|
2617
|
+
.optional()
|
|
2618
|
+
.describe("IANA time zone for the state's current time and dates (default UTC)"),
|
|
2619
|
+
jevTimeoutMs: z
|
|
2620
|
+
.number()
|
|
2621
|
+
.int()
|
|
2622
|
+
.min(1_000)
|
|
2623
|
+
.max(45_000)
|
|
2624
|
+
.optional()
|
|
2625
|
+
.describe("Abandon the Jev call after this many ms (default 25000); the gate deadline is 60 s"),
|
|
2626
|
+
});
|
|
2627
|
+
|
|
2628
|
+
const KV_NAMESPACE = "heartbeat-jev-gate";
|
|
2629
|
+
const KV_KEY = "setup";
|
|
2630
|
+
// Measured per decision in the 2026-10-15 heartbeat replay (229 states).
|
|
2631
|
+
const COST_PER_DECISION = "$0.00004";
|
|
2632
|
+
|
|
2633
|
+
// Jev is called through \`ctx.swarm.systemOne_decide\`: the API server reads the
|
|
2634
|
+
// key and sends the request, so no key ever enters this script's realm. The
|
|
2635
|
+
// server also refuses OpenRouter when the effective OPENROUTER_BASE_URL (stored
|
|
2636
|
+
// config or deployment env) points at a gateway.
|
|
2637
|
+
const PROVIDERS = {
|
|
2638
|
+
typesafe: { keyName: "TYPESAFE_API_KEY", model: "jev-latest" },
|
|
2639
|
+
openrouter: { keyName: "OPENROUTER_API_KEY", model: "~typesafe/jev-latest" },
|
|
2640
|
+
} as const;
|
|
2641
|
+
type ProviderId = keyof typeof PROVIDERS;
|
|
2642
|
+
|
|
2643
|
+
// Worded exactly as in the replay that set the 0.5 threshold; changing it voids that result.
|
|
2644
|
+
const QUESTIONS = {
|
|
2645
|
+
act: {
|
|
2646
|
+
type: "noul",
|
|
2647
|
+
instructions:
|
|
2648
|
+
"This is a 30-minute heartbeat for the Lead agent of an AI agent swarm. Does this state require the Lead to take an action now: message a human, dispatch, resume or fix a task, or act on a tracked item that is due now? Answer false when nothing changed since the previous heartbeat and every failure is already triaged or owned.",
|
|
2649
|
+
criteria: {
|
|
2650
|
+
true: "A new untriaged failure, a tracked item or human ask due now and not yet done, or a problem with no owner",
|
|
2651
|
+
false:
|
|
2652
|
+
"Same state as the previous heartbeat, failures already triaged or owned, nothing due now",
|
|
2653
|
+
},
|
|
2654
|
+
},
|
|
2655
|
+
};
|
|
2656
|
+
|
|
2657
|
+
type SetupState = {
|
|
2658
|
+
status: "asked" | "approved" | "declined" | "expired";
|
|
2659
|
+
requestId?: string;
|
|
2660
|
+
url?: string;
|
|
2661
|
+
askedAt?: string;
|
|
2662
|
+
resolvedAt?: string;
|
|
2663
|
+
};
|
|
2664
|
+
|
|
2665
|
+
function rows(res: any): any[] {
|
|
2666
|
+
const p = res?.data ?? res;
|
|
2667
|
+
if (p?.error) throw new Error(\`db_query: \${String(p.error).slice(0, 200)}\`);
|
|
2668
|
+
if (!Array.isArray(p?.rows)) throw new Error("db_query returned no rows array");
|
|
2669
|
+
const cols: string[] = p.columns ?? [];
|
|
2670
|
+
return p.rows.map((r: any) =>
|
|
2671
|
+
Array.isArray(r) ? Object.fromEntries(cols.map((c, i) => [c, r[i]])) : r,
|
|
2672
|
+
);
|
|
2673
|
+
}
|
|
2674
|
+
|
|
2675
|
+
const oneLine = (s: unknown) => String(s ?? "").replace(/\\s+/g, " ");
|
|
2676
|
+
|
|
2677
|
+
/** \`YYYY-MM-DD HH:MM\` in the zone, falling back to UTC when the runtime has no Intl zone data. */
|
|
2678
|
+
function localStamp(ms: number, timeZone: string): { stamp: string; zone: string } {
|
|
2679
|
+
const utc = { stamp: new Date(ms).toISOString().slice(0, 16).replace("T", " "), zone: "UTC" };
|
|
2680
|
+
if (timeZone === "UTC") return utc;
|
|
2681
|
+
try {
|
|
2682
|
+
const parts = new Intl.DateTimeFormat("en-CA", {
|
|
2683
|
+
timeZone,
|
|
2684
|
+
year: "numeric",
|
|
2685
|
+
month: "2-digit",
|
|
2686
|
+
day: "2-digit",
|
|
2687
|
+
hour: "2-digit",
|
|
2688
|
+
minute: "2-digit",
|
|
2689
|
+
hourCycle: "h23",
|
|
2690
|
+
}).formatToParts(new Date(ms));
|
|
2691
|
+
const v = (t: string) => parts.find((p) => p.type === t)?.value ?? "";
|
|
2692
|
+
return { stamp: \`\${v("year")}-\${v("month")}-\${v("day")} \${v("hour")}:\${v("minute")}\`, zone: timeZone };
|
|
2693
|
+
} catch {
|
|
2694
|
+
return utc;
|
|
2695
|
+
}
|
|
2696
|
+
}
|
|
2697
|
+
|
|
2698
|
+
// The checklist task renders failures as these lines (gatherSystemStatus in
|
|
2699
|
+
// src/heartbeat/heartbeat.ts). The replay parsed them with this regex, so the
|
|
2700
|
+
// gate renders and parses the same way to keep its fields identical.
|
|
2701
|
+
const FAILURE_LINE_RE =
|
|
2702
|
+
/- \\[([0-9a-f]{8})\\] "([\\s\\S]*?)" — agent: ([0-9a-f]{8}), reason: ([\\s\\S]*?), at: (\\S+)/g;
|
|
2703
|
+
|
|
2704
|
+
function parseFailures(status: string): { id: string; what: string; reason: string }[] {
|
|
2705
|
+
const out: { id: string; what: string; reason: string }[] = [];
|
|
2706
|
+
for (const m of status.matchAll(FAILURE_LINE_RE)) {
|
|
2707
|
+
out.push({
|
|
2708
|
+
id: m[1] ?? "",
|
|
2709
|
+
what: oneLine(m[2]).slice(0, 90),
|
|
2710
|
+
reason: oneLine(m[4]).slice(0, 140),
|
|
2711
|
+
});
|
|
2712
|
+
}
|
|
2713
|
+
return out;
|
|
2714
|
+
}
|
|
2715
|
+
|
|
2716
|
+
function renderFailureLine(t: any): string {
|
|
2717
|
+
const agent = t.agentId ? String(t.agentId).slice(0, 8) : "unassigned";
|
|
2718
|
+
const reason = t.failureReason ? String(t.failureReason).slice(0, 100) : "no reason";
|
|
2719
|
+
return \`- [\${String(t.id).slice(0, 8)}] "\${String(t.task ?? "").slice(0, 50)}" — agent: \${agent}, reason: \${reason}, at: \${t.finishedAt}\`;
|
|
2720
|
+
}
|
|
2721
|
+
|
|
2722
|
+
/**
|
|
2723
|
+
* The compact state Jev scores: the fields of the 2026-10-15 replay, built from
|
|
2724
|
+
* live data because the checklist task body does not exist yet at gate time.
|
|
2725
|
+
*/
|
|
2726
|
+
async function buildState(ctx: any, leadId: string, nowMs: number, timeZone: string) {
|
|
2727
|
+
const q = async (sql: string, params: unknown[] = []) =>
|
|
2728
|
+
rows(await ctx.swarm.db_query({ sql, params }));
|
|
2729
|
+
const since6h = new Date(nowMs - 6 * 3600e3).toISOString();
|
|
2730
|
+
const since24h = new Date(nowMs - 24 * 3600e3).toISOString();
|
|
2731
|
+
// Sequential: db_query caps concurrent queries per caller, and parallel reads fail with
|
|
2732
|
+
// db_query_concurrency_cap on a busy swarm.
|
|
2733
|
+
const lead = await q("SELECT heartbeatMd FROM agents WHERE id = ?", [leadId]);
|
|
2734
|
+
const prev = await q(
|
|
2735
|
+
\`SELECT id, substr(output, 1, 400) AS output,
|
|
2736
|
+
substr(task, 1, instr(task, '## Your Standing Orders')) AS status
|
|
2737
|
+
FROM agent_tasks
|
|
2738
|
+
WHERE agentId = ? AND taskType = 'heartbeat-checklist' AND status = 'completed'
|
|
2739
|
+
ORDER BY createdAt DESC LIMIT 1\`,
|
|
2740
|
+
[leadId],
|
|
2741
|
+
);
|
|
2742
|
+
const failed = await q(
|
|
2743
|
+
\`SELECT id, agentId, substr(task, 1, 60) AS task, substr(failureReason, 1, 120) AS failureReason, finishedAt
|
|
2744
|
+
FROM agent_tasks WHERE status = 'failed' AND finishedAt > ?
|
|
2745
|
+
ORDER BY finishedAt DESC LIMIT 10\`,
|
|
2746
|
+
[since6h],
|
|
2747
|
+
);
|
|
2748
|
+
const overview = await q(
|
|
2749
|
+
\`SELECT SUM(status = 'in_progress') AS inProgress, SUM(status = 'pending') AS pending,
|
|
2750
|
+
SUM(status = 'unassigned') AS unassigned,
|
|
2751
|
+
SUM(status = 'completed' AND finishedAt > ?) AS completed24,
|
|
2752
|
+
SUM(status = 'failed' AND finishedAt > ?) AS failed24
|
|
2753
|
+
FROM agent_tasks\`,
|
|
2754
|
+
[since24h, since24h],
|
|
2755
|
+
);
|
|
2756
|
+
const agents = await q(
|
|
2757
|
+
"SELECT status, COUNT(*) AS n FROM agents WHERE COALESCE(role, '') != 'extension' GROUP BY status",
|
|
2758
|
+
);
|
|
2759
|
+
|
|
2760
|
+
const fails = parseFailures(failed.map(renderFailureLine).join("\\n"));
|
|
2761
|
+
const prevIds = new Set(parseFailures(String(prev[0]?.status ?? "")).map((f) => f.id));
|
|
2762
|
+
const newFails = fails.filter((f) => !prevIds.has(f.id));
|
|
2763
|
+
|
|
2764
|
+
const { stamp, zone } = localStamp(nowMs, timeZone);
|
|
2765
|
+
const mmdd = new Date(nowMs).toISOString().slice(5, 10);
|
|
2766
|
+
const mmddLocal = stamp.slice(5, 10);
|
|
2767
|
+
const due: string[] = [];
|
|
2768
|
+
for (const line of String(lead[0]?.heartbeatMd ?? "").split("\\n")) {
|
|
2769
|
+
const l = line.trim();
|
|
2770
|
+
if (!l) continue;
|
|
2771
|
+
const del = /DELETE <?(\\d\\d-\\d\\d)>?/.exec(l);
|
|
2772
|
+
if (l.includes(mmdd) || l.includes(mmddLocal) || (del?.[1] && del[1] <= mmddLocal)) {
|
|
2773
|
+
due.push(oneLine(l).slice(0, 200));
|
|
2774
|
+
}
|
|
2775
|
+
if (due.length >= 6) break;
|
|
2776
|
+
}
|
|
2777
|
+
|
|
2778
|
+
const o = overview[0] ?? {};
|
|
2779
|
+
const n = (v: unknown) => Number(v ?? 0) || 0;
|
|
2780
|
+
const count = (s: string) => n(agents.find((a) => a.status === s)?.n);
|
|
2781
|
+
const idle = count("idle");
|
|
2782
|
+
const busy = count("busy");
|
|
2783
|
+
|
|
2784
|
+
return {
|
|
2785
|
+
now: \`\${stamp} \${zone}\`,
|
|
2786
|
+
new_failures_since_previous_heartbeat: newFails.length
|
|
2787
|
+
? newFails.map((f) => \`\${f.id}: \${f.what} | \${f.reason}\`)
|
|
2788
|
+
: "none",
|
|
2789
|
+
tracked_items_mentioning_today_or_past_delete_date: due.length ? due : "none",
|
|
2790
|
+
previous_heartbeat_result: prev[0]?.output
|
|
2791
|
+
? oneLine(prev[0].output).slice(0, 350)
|
|
2792
|
+
: "unknown",
|
|
2793
|
+
failures_last_6h_total: fails.length,
|
|
2794
|
+
task_overview: \`- In Progress: \${n(o.inProgress)} - Pending: \${n(o.pending)} - Unassigned: \${n(o.unassigned)} - Completed (24h): \${n(o.completed24)} - Failed (24h): \${n(o.failed24)}\`,
|
|
2795
|
+
agents: \`- Online: \${idle + busy} (\${idle} idle, \${busy} busy), Offline: \${count("offline")}\`,
|
|
2796
|
+
};
|
|
2797
|
+
}
|
|
2798
|
+
|
|
2799
|
+
function setupPrompt(keys: string[]) {
|
|
2800
|
+
return {
|
|
2801
|
+
title: "Heartbeat Jev gate: add a Jev credential or decline",
|
|
2802
|
+
detached: true,
|
|
2803
|
+
questions: [
|
|
2804
|
+
{
|
|
2805
|
+
id: "enable",
|
|
2806
|
+
type: "approval",
|
|
2807
|
+
label: "Enable the Jev heartbeat gate?",
|
|
2808
|
+
description: [
|
|
2809
|
+
"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 Jev, a small decision model by TypeSafe, and asks whether the Lead needs to act. With \`HEARTBEAT_CHECKLIST_GATE_MODE=enforce\`, quiet ticks skip the Lead's checklist task.",
|
|
2810
|
+
"",
|
|
2811
|
+
\`Cost: about \${COST_PER_DECISION} per decision, one decision per checklist tick.\`,
|
|
2812
|
+
"",
|
|
2813
|
+
\`No Jev credential is configured. To enable, approve and add one of \${keys.map((k) => \`\\\`\${k}\\\`\`).join(" or ")} as a global secret on the Secrets page (Settings > Secrets). Do not paste the key here. The next tick picks it up.\`,
|
|
2814
|
+
"",
|
|
2815
|
+
"Reject to decline. The gate then stays inactive, with no further prompts, until a key is added.",
|
|
2816
|
+
].join("\\n"),
|
|
2817
|
+
},
|
|
2818
|
+
],
|
|
2819
|
+
};
|
|
2820
|
+
}
|
|
2821
|
+
|
|
2822
|
+
/**
|
|
2823
|
+
* Heartbeat checklist gate backed by Jev: asks Jev whether a recurring checklist
|
|
2824
|
+
* tick needs the Lead and returns { quiet, reason, summary }. With no Jev
|
|
2825
|
+
* credential it asks a human once, then stays a no-op until a key is added.
|
|
2826
|
+
*/
|
|
2827
|
+
export default async function heartbeatJevGate(args: any, ctx: any) {
|
|
2828
|
+
const parsed = argsSchema.safeParse(args ?? {});
|
|
2829
|
+
if (!parsed.success) throw new Error(\`invalid args: \${parsed.error.message.slice(0, 200)}\`);
|
|
2830
|
+
const a = parsed.data;
|
|
2831
|
+
const dryRun = a.dryRun === true;
|
|
2832
|
+
|
|
2833
|
+
const agentInfo: any = a.leadAgentId ? null : await ctx.swarm.agent_info();
|
|
2834
|
+
const leadId = String(a.leadAgentId ?? agentInfo?.data?.id ?? agentInfo?.id ?? "");
|
|
2835
|
+
if (!leadId) throw new Error("no leadAgentId");
|
|
2836
|
+
|
|
2837
|
+
// Values of secret rows come back masked: presence is all this script reads.
|
|
2838
|
+
const cfgRes: any = await ctx.swarm.config_get({ agentId: leadId });
|
|
2839
|
+
const configs: any[] = cfgRes?.data?.configs ?? cfgRes?.configs ?? [];
|
|
2840
|
+
const cfg = (key: string): string => {
|
|
2841
|
+
const value = configs.find((c) => c?.key === key)?.value;
|
|
2842
|
+
return typeof value === "string" ? value.trim() : "";
|
|
2843
|
+
};
|
|
2844
|
+
|
|
2845
|
+
const cfgThreshold = Number.parseFloat(cfg("HEARTBEAT_JEV_GATE_THRESHOLD"));
|
|
2846
|
+
const threshold =
|
|
2847
|
+
a.threshold ?? (cfgThreshold >= 0 && cfgThreshold <= 1 ? cfgThreshold : 0.5);
|
|
2848
|
+
const providerPref = a.provider ?? (cfg("HEARTBEAT_JEV_GATE_PROVIDER").toLowerCase() || "auto");
|
|
2849
|
+
const timeZone = a.timeZone ?? (cfg("HEARTBEAT_JEV_GATE_TIMEZONE") || "UTC");
|
|
2850
|
+
|
|
2851
|
+
const usable: Record<ProviderId, boolean> = {
|
|
2852
|
+
typesafe: cfg(PROVIDERS.typesafe.keyName) !== "",
|
|
2853
|
+
openrouter: cfg(PROVIDERS.openrouter.keyName) !== "",
|
|
2854
|
+
};
|
|
2855
|
+
const candidates: ProviderId[] =
|
|
2856
|
+
providerPref === "typesafe"
|
|
2857
|
+
? ["typesafe"]
|
|
2858
|
+
: providerPref === "openrouter"
|
|
2859
|
+
? ["openrouter"]
|
|
2860
|
+
: ["typesafe", "openrouter"];
|
|
2861
|
+
const provider = candidates.find((id) => usable[id]);
|
|
2862
|
+
|
|
2863
|
+
if (!provider) {
|
|
2864
|
+
const keys = candidates.map((id) => PROVIDERS[id].keyName);
|
|
2865
|
+
const inactive = (state: string, extra: Record<string, unknown> = {}) => ({
|
|
2866
|
+
quiet: false,
|
|
2867
|
+
reason: \`jev-gate inactive: \${state}\`.slice(0, 300),
|
|
2868
|
+
...(dryRun ? { dryRun: true, ...extra } : {}),
|
|
2869
|
+
});
|
|
2870
|
+
const entry: any = await ctx.swarm.kv_getOrNull({ namespace: KV_NAMESPACE, key: KV_KEY });
|
|
2871
|
+
const stored = entry?.value;
|
|
2872
|
+
let setup: SetupState | null = typeof stored === "string" ? JSON.parse(stored) : (stored ?? null);
|
|
2873
|
+
|
|
2874
|
+
if (!setup) {
|
|
2875
|
+
const prompt = setupPrompt(keys);
|
|
2876
|
+
if (dryRun) {
|
|
2877
|
+
const state = await buildState(ctx, leadId, Date.now(), timeZone);
|
|
2878
|
+
return inactive("no credential, would ask a human", { prompt, state });
|
|
2879
|
+
}
|
|
2880
|
+
const res: any = await ctx.swarm.request_humanInput(prompt);
|
|
2881
|
+
const requestId = res?.data?.requestId ?? res?.requestId;
|
|
2882
|
+
if (!requestId) throw new Error("request_humanInput returned no requestId");
|
|
2883
|
+
setup = {
|
|
2884
|
+
status: "asked",
|
|
2885
|
+
requestId,
|
|
2886
|
+
url: res?.data?.url ?? res?.url,
|
|
2887
|
+
askedAt: new Date().toISOString(),
|
|
2888
|
+
};
|
|
2889
|
+
await ctx.swarm.kv_set({ namespace: KV_NAMESPACE, key: KV_KEY, value: setup });
|
|
2890
|
+
return inactive(\`no credential, asked a human (approval request \${requestId})\`);
|
|
2891
|
+
}
|
|
2892
|
+
|
|
2893
|
+
if (setup.status === "asked" && setup.requestId) {
|
|
2894
|
+
const [req] = rows(
|
|
2895
|
+
await ctx.swarm.db_query({
|
|
2896
|
+
sql: "SELECT status FROM approval_requests WHERE id = ?",
|
|
2897
|
+
params: [setup.requestId],
|
|
2898
|
+
}),
|
|
2899
|
+
);
|
|
2900
|
+
const status = String(req?.status ?? "missing");
|
|
2901
|
+
if (status === "pending") {
|
|
2902
|
+
return inactive(\`no credential, awaiting answer on approval request \${setup.requestId}\`);
|
|
2903
|
+
}
|
|
2904
|
+
const next: SetupState["status"] =
|
|
2905
|
+
status === "approved" ? "approved" : status === "rejected" ? "declined" : "expired";
|
|
2906
|
+
setup = { ...setup, status: next, resolvedAt: new Date().toISOString() };
|
|
2907
|
+
if (!dryRun) await ctx.swarm.kv_set({ namespace: KV_NAMESPACE, key: KV_KEY, value: setup });
|
|
2908
|
+
}
|
|
2909
|
+
|
|
2910
|
+
const why =
|
|
2911
|
+
setup.status === "approved"
|
|
2912
|
+
? \`approved, waiting for \${keys.join(" or ")} on the Secrets page\`
|
|
2913
|
+
: setup.status === "declined"
|
|
2914
|
+
? \`declined; add \${keys.join(" or ")} to enable\`
|
|
2915
|
+
: \`setup request expired unanswered; add \${keys.join(" or ")} to enable\`;
|
|
2916
|
+
return inactive(why, { setup });
|
|
2917
|
+
}
|
|
2918
|
+
|
|
2919
|
+
const target = PROVIDERS[provider];
|
|
2920
|
+
const state = await buildState(ctx, leadId, Date.now(), timeZone);
|
|
2921
|
+
|
|
2922
|
+
// The server enforces the time budget. No retries, as in the replay.
|
|
2923
|
+
const res: any = await ctx.swarm.systemOne_decide({
|
|
2924
|
+
provider,
|
|
2925
|
+
model: target.model,
|
|
2926
|
+
state,
|
|
2927
|
+
questions: QUESTIONS,
|
|
2928
|
+
timeoutMs: a.jevTimeoutMs ?? 25_000,
|
|
2929
|
+
maxRetries: 0,
|
|
2930
|
+
});
|
|
2931
|
+
const body: any = res?.data ?? res;
|
|
2932
|
+
if (res?.success === false || body?.success === false) {
|
|
2933
|
+
throw new Error(\`jev \${provider}: \${String(body?.message ?? body?.error ?? \`HTTP \${res?.status}\`).slice(0, 250)}\`);
|
|
2934
|
+
}
|
|
2935
|
+
const p = body?.answers?.act?.noul;
|
|
2936
|
+
if (typeof p !== "number" || !Number.isFinite(p)) {
|
|
2937
|
+
throw new Error(\`jev \${provider}: response has no numeric answers.act.noul\`);
|
|
2938
|
+
}
|
|
2939
|
+
|
|
2940
|
+
const pText = p.toFixed(3);
|
|
2941
|
+
const quiet = p < threshold;
|
|
2942
|
+
const verdict: Record<string, unknown> = {
|
|
2943
|
+
quiet,
|
|
2944
|
+
reason: \`jev p(needs action)=\${pText} \${quiet ? "<" : ">="} \${threshold} via \${provider}\`,
|
|
2945
|
+
};
|
|
2946
|
+
if (!quiet) {
|
|
2947
|
+
const lines = [\`Jev p(needs action) = \${pText} (threshold \${threshold}, \${provider}).\`];
|
|
2948
|
+
const failures = state.new_failures_since_previous_heartbeat;
|
|
2949
|
+
const tracked = state.tracked_items_mentioning_today_or_past_delete_date;
|
|
2950
|
+
if (Array.isArray(failures)) {
|
|
2951
|
+
lines.push("", "New failures since the previous checklist:", ...failures.map((f) => \`- \${f}\`));
|
|
2952
|
+
}
|
|
2953
|
+
if (Array.isArray(tracked)) {
|
|
2954
|
+
lines.push("", "Tracked items due today or past their DELETE date:", ...tracked.map((t) => \`- \${t.replace(/^[-*] /, "")}\`));
|
|
2955
|
+
}
|
|
2956
|
+
if (!Array.isArray(failures) && !Array.isArray(tracked)) {
|
|
2957
|
+
lines.push("", "No new failures or due tracked items; Jev flagged the overall state.");
|
|
2958
|
+
}
|
|
2959
|
+
verdict.summary = lines.join("\\n").slice(0, 4000);
|
|
2960
|
+
}
|
|
2961
|
+
return dryRun ? { ...verdict, dryRun: true, provider, p, threshold, state } : verdict;
|
|
2962
|
+
}
|
|
2963
|
+
`;
|
|
2964
|
+
|
|
2588
2965
|
// src/be/seed-scripts/catalog/json-query.ts
|
|
2589
2966
|
var json_query_default = `import { z } from "zod";
|
|
2590
2967
|
|
|
@@ -3970,7 +4347,7 @@ const CONTRADICTORY_RE =
|
|
|
3970
4347
|
/\\b(do not|don't|never)\\b[\\s\\S]{0,240}\\b(always|must|required)\\b|\\b(always|must|required)\\b[\\s\\S]{0,240}\\b(do not|don't|never)\\b/i;
|
|
3971
4348
|
|
|
3972
4349
|
const CODE_REGISTRY_EVENTS =
|
|
3973
|
-
"agentmail.email.followup agentmail.email.mapped_lead agentmail.email.mapped_worker agentmail.email.no_agent agentmail.email.unmapped common.command_suggestions.github_comment_issue common.command_suggestions.github_comment_pr common.command_suggestions.github_issue common.command_suggestions.github_pr common.command_suggestions.gitlab_issue common.command_suggestions.gitlab_mr common.delegation_instruction common.delegation_instruction.gitlab github.check_run.failed github.check_suite.failed github.comment.mentioned github.issue.assigned github.issue.labeled github.issue.mentioned github.pull_request.assigned github.pull_request.closed github.pull_request.labeled github.pull_request.mentioned github.pull_request.review_requested github.pull_request.review_submitted github.pull_request.synchronize github.workflow_run.failed gitlab.comment.mentioned gitlab.issue.assigned gitlab.merge_request.opened gitlab.pipeline.failed heartbeat.boot-triage heartbeat.checklist jira.issue.assigned jira.issue.commented jira.issue.followup kapso.message.received linear.issue.assigned linear.issue.followup linear.issue.reassigned slack.assistant.greeting slack.assistant.offline slack.assistant.suggested_prompts slack.message.attachment_failed slack.message.thread_context system.agent.communication system.agent.lead system.agent.memory system.agent.memory.remote system.agent.outputs system.agent.outputs.no_agent_fs system.agent.repository system.agent.role system.agent.secrets system.agent.slack system.agent.tools_skills system.agent.worker system.agent.worker.remote system.agent.workspace system.agent.workspace.remote system.session.lead system.session.lead.managed system.session.worker system.session.worker.managed system.session.worker.remote task.budget.refused task.requester.profile task.worker.completed task.worker.failed".split(
|
|
4350
|
+
"agentmail.email.followup agentmail.email.mapped_lead agentmail.email.mapped_worker agentmail.email.no_agent agentmail.email.unmapped chat.message.thread_context common.command_suggestions.github_comment_issue common.command_suggestions.github_comment_pr common.command_suggestions.github_issue common.command_suggestions.github_pr common.command_suggestions.gitlab_issue common.command_suggestions.gitlab_mr common.delegation_instruction common.delegation_instruction.gitlab github.check_run.failed github.check_suite.failed github.comment.mentioned github.issue.assigned github.issue.labeled github.issue.mentioned github.pull_request.assigned github.pull_request.closed github.pull_request.labeled github.pull_request.mentioned github.pull_request.review_requested github.pull_request.review_submitted github.pull_request.synchronize github.workflow_run.failed gitlab.comment.mentioned gitlab.issue.assigned gitlab.merge_request.opened gitlab.pipeline.failed heartbeat.boot-triage heartbeat.checklist jira.issue.assigned jira.issue.commented jira.issue.followup kapso.message.received linear.issue.assigned linear.issue.followup linear.issue.reassigned slack.assistant.greeting slack.assistant.offline slack.assistant.suggested_prompts slack.message.attachment_failed slack.message.thread_context system.agent.communication system.agent.lead system.agent.memory system.agent.memory.remote system.agent.outputs system.agent.outputs.no_agent_fs system.agent.repository system.agent.role system.agent.secrets system.agent.slack system.agent.tools_skills system.agent.worker system.agent.worker.remote system.agent.workspace system.agent.workspace.remote system.session.lead system.session.lead.managed system.session.worker system.session.worker.managed system.session.worker.remote task.budget.refused task.requester.profile task.worker.completed task.worker.failed".split(
|
|
3974
4351
|
" ",
|
|
3975
4352
|
);
|
|
3976
4353
|
|
|
@@ -5536,6 +5913,12 @@ var SEED_SCRIPTS = [
|
|
|
5536
5913
|
intent: "Keep an app's synced rows fresh on a schedule or on demand; the script a targetType:'script' schedule points at to sync an app.",
|
|
5537
5914
|
source: asText(app_sync_run_default)
|
|
5538
5915
|
},
|
|
5916
|
+
{
|
|
5917
|
+
name: "heartbeat-jev-gate",
|
|
5918
|
+
description: "Heartbeat checklist gate backed by Jev (TypeSafe or OpenRouter decisions API): scores a compact swarm state and returns {quiet, reason, summary} for HEARTBEAT_CHECKLIST_GATE_SCRIPT.",
|
|
5919
|
+
intent: "Skip quiet Lead heartbeat checklist ticks with a cheap Jev decision; with no Jev credential it asks a human once, then stays a no-op until a key is added.",
|
|
5920
|
+
source: asText(heartbeat_jev_gate_default)
|
|
5921
|
+
},
|
|
5539
5922
|
{
|
|
5540
5923
|
name: "fetch-readable",
|
|
5541
5924
|
description: "Fetch a web page and extract clean readable article text — strips scripts, styles, nav, headers, footers, ads and HTML tags, and decodes entities.",
|
|
@@ -5737,7 +6120,7 @@ var config_default = `{
|
|
|
5737
6120
|
`;
|
|
5738
6121
|
|
|
5739
6122
|
// templates/skills/agent-browser/content.md
|
|
5740
|
-
var content_default = '# agent-browser\n\n`agent-browser` (vercel-labs) is the swarm\'s browser-automation CLI. It drives a headless Chromium over CDP and returns accessibility-tree snapshots with compact `@eN` refs. Use it for any browser task: verify a UI change, take a screenshot, walk a page, fill a form, extract page text, reproduce a frontend bug, or attach UI evidence to a PR, issue, Linear comment, or Slack thread.\n\nDo not use `qa-use`. It is not installed in the worker image any more.\n\n## Step 0: check that the CLI is present\n\nThe full worker image (`agent-swarm-worker:latest`) ships `agent-browser` and a headless Chromium. The slim image (`:slim`) has neither. The first command of any browser work is:\n\n```bash\ncommand -v agent-browser\n```\n\nIf it prints nothing, you are on the slim image. Say so in your progress report ("agent-browser is not available on this worker image") and fall back to manual QA: write the exact steps a human should run and mark the browser test cases as **Blocked**, not failed. Do not install a browser or the CLI yourself.\n\n## Step 1: load the version-matched guide\n\nThe CLI bundles its own usage guide. Load it once per task before the first browser command. Do not guess flags from memory.\n\n```bash\nagent-browser skills get core # overview + common patterns\nagent-browser skills get core --full # adds the full command reference\nagent-browser skills list # specialized guides (dogfood, electron, slack, ...)\n```\n\n## Step 2: the core loop\n\n```bash\nagent-browser open <url>\nagent-browser snapshot -i # accessibility tree, interactive elements only, @eN refs\nagent-browser click @e12 # act on refs from the snapshot\nagent-browser fill @e7 "text"\nagent-browser screenshot /tmp/<name>.png\nagent-browser close\n```\n\n- Re-run `snapshot` after every navigation or state change. Refs are only valid for the snapshot that produced them.\n- Run one browser session at a time. Call `store-progress` between page batches so the heartbeat watchdog sees you.\n- The browser is headless. Local servers inside the worker container are reachable at `http://localhost:<port>`.\n- The image launches Chromium with `--no-sandbox,--disable-dev-shm-usage` through `AGENT_BROWSER_ARGS`. If you pass your own `--args`, include `--no-sandbox` again or Chrome will not start.\n\n## Recording\n\nPass `record start` an absolute path (`/tmp/<topic>/raw.webm`). A relative path resolves against the agent-browser daemon\'s cwd, not yours, and fails with `ffmpeg write failed: Broken pipe` while `doctor` stays green.\n\nDefault: record with `--cursor`, drive every supported pointer action (`click`, `mouse move`, `drag`) with `--human`, and ship the **1.5x video**. Keep the 1x original only if a reviewer asks for it. Human pointer movement follows an eased curve; use `--seed <n>` on `mouse move` to make the take reproducible instead of using a bare move.\n\nOpen the page first, then record the interaction:\n\n```bash\nagent-browser record start ./in.mp4 --cursor\nagent-browser snapshot -i\nagent-browser mouse move 600 400 --human --seed 42 --duration 1400\nagent-browser click @e3 --human # use a ref from the current snapshot\n# Perform the remaining QA steps; use --human for drag actions too.\nagent-browser record stop\nffmpeg -y -i in.mp4 -filter:v "setpts=PTS/1.5,fps=30" -an \\\n -c:v libx264 -preset veryfast -crf 23 -pix_fmt yuv420p out-1.5x.mp4\n```\n\nSpeed-up is a post-process step: `record start` has no speed or human flag. Keep `fps=30` to preserve a steady 30 fps output rather than uneven frame drops at an inferred rate. `-an` disables audio because agent-browser recordings carry no audio track. The worker ships only `ffmpeg`, not `ffprobe`; inspect duration with `ffmpeg -i out-1.5x.mp4 2>&1 | grep Duration`.\n\nAdd `--contact-sheet` to the start command for a timestamped PNG summary of visual changes. Recording needs `ffmpeg` on PATH; check `agent-browser doctor` before starting. Stop the recording before closing the browser so the video is saved. Keep the 1.5x file on the same agent-fs QA path as screenshots below. In a PR, upload it as a GitHub user attachment and put the URL on its own line, so GitHub plays it inline.\n\n## Step 3: share the screenshot through agent-fs\n\nScreenshots on the worker disk disappear with the task. Upload them to agent-fs under the qa path convention: that copy is the durable original. Use `share-create` when a person needs to open the file. `--file` is binary-safe. `--content` is text-only and mangles PNGs.\n\n```bash\nagent-fs write thoughts/<agent-id>/qa/<topic>-screenshots/<name>.png \\\n --file /tmp/<name>.png -m "<what it shows>"\nagent-fs stat thoughts/<agent-id>/qa/<topic>-screenshots/<name>.png --json # confirm size > 0\nagent-fs share-create thoughts/<agent-id>/qa/<topic>-screenshots/<name>.png --json\n```\n\n- **GitHub PR body or comment**: upload the PNG, GIF or mp4 as a GitHub user attachment with the `github-attach` skill and embed ``. Attachments never expire and mp4s play inline. A UI PR puts a before (main) and after pair in the `## Before / after` table. Fallback for images only, when the GitHub token is an App token (`ghu_`/`ghs_`): `agent-fs signed-url <path> --inline --json` (expires in 7 days). Never a presigned URL for an mp4 or a doc: it downloads.\n- **Linear comment or Slack message**: paste the `share-create` link.\n- In `store-progress`, list the upload in the `attachments` field with `kind: "agent-fs"` and the path, and paste the `share-create` link for people who need to open the file.\n- If `agent-fs auth whoami` fails, report the local path, say the upload was skipped, and continue.\n\nThe `
|
|
6123
|
+
var content_default = '# agent-browser\n\n`agent-browser` (vercel-labs) is the swarm\'s browser-automation CLI. It drives a headless Chromium over CDP and returns accessibility-tree snapshots with compact `@eN` refs. Use it for any browser task: verify a UI change, take a screenshot, walk a page, fill a form, extract page text, reproduce a frontend bug, or attach UI evidence to a PR, issue, Linear comment, or Slack thread.\n\nDo not use `qa-use`. It is not installed in the worker image any more.\n\n## Step 0: check that the CLI is present\n\nThe full worker image (`agent-swarm-worker:latest`) ships `agent-browser` and a headless Chromium. The slim image (`:slim`) has neither. The first command of any browser work is:\n\n```bash\ncommand -v agent-browser\n```\n\nIf it prints nothing, you are on the slim image. Say so in your progress report ("agent-browser is not available on this worker image") and fall back to manual QA: write the exact steps a human should run and mark the browser test cases as **Blocked**, not failed. Do not install a browser or the CLI yourself.\n\n## Step 1: load the version-matched guide\n\nThe CLI bundles its own usage guide. Load it once per task before the first browser command. Do not guess flags from memory.\n\n```bash\nagent-browser skills get core # overview + common patterns\nagent-browser skills get core --full # adds the full command reference\nagent-browser skills list # specialized guides (dogfood, electron, slack, ...)\n```\n\n## Step 2: the core loop\n\n```bash\nagent-browser open <url>\nagent-browser snapshot -i # accessibility tree, interactive elements only, @eN refs\nagent-browser click @e12 # act on refs from the snapshot\nagent-browser fill @e7 "text"\nagent-browser screenshot /tmp/<name>.png\nagent-browser close\n```\n\n- Re-run `snapshot` after every navigation or state change. Refs are only valid for the snapshot that produced them.\n- Run one browser session at a time. Call `store-progress` between page batches so the heartbeat watchdog sees you.\n- The browser is headless. Local servers inside the worker container are reachable at `http://localhost:<port>`.\n- The image launches Chromium with `--no-sandbox,--disable-dev-shm-usage` through `AGENT_BROWSER_ARGS`. If you pass your own `--args`, include `--no-sandbox` again or Chrome will not start.\n\n## Recording\n\nPass `record start` an absolute path (`/tmp/<topic>/raw.webm`). A relative path resolves against the agent-browser daemon\'s cwd, not yours, and fails with `ffmpeg write failed: Broken pipe` while `doctor` stays green.\n\nDefault: record with `--cursor`, drive every supported pointer action (`click`, `mouse move`, `drag`) with `--human`, and ship the **1.5x video**. Keep the 1x original only if a reviewer asks for it. Human pointer movement follows an eased curve; use `--seed <n>` on `mouse move` to make the take reproducible instead of using a bare move.\n\nOpen the page first, then record the interaction:\n\n```bash\nagent-browser record start ./in.mp4 --cursor\nagent-browser snapshot -i\nagent-browser mouse move 600 400 --human --seed 42 --duration 1400\nagent-browser click @e3 --human # use a ref from the current snapshot\n# Perform the remaining QA steps; use --human for drag actions too.\nagent-browser record stop\nffmpeg -y -i in.mp4 -filter:v "setpts=PTS/1.5,fps=30" -an \\\n -c:v libx264 -preset veryfast -crf 23 -pix_fmt yuv420p out-1.5x.mp4\n```\n\nSpeed-up is a post-process step: `record start` has no speed or human flag. Keep `fps=30` to preserve a steady 30 fps output rather than uneven frame drops at an inferred rate. `-an` disables audio because agent-browser recordings carry no audio track. The worker ships only `ffmpeg`, not `ffprobe`; inspect duration with `ffmpeg -i out-1.5x.mp4 2>&1 | grep Duration`.\n\nAdd `--contact-sheet` to the start command for a timestamped PNG summary of visual changes. Recording needs `ffmpeg` on PATH; check `agent-browser doctor` before starting. Stop the recording before closing the browser so the video is saved. Keep the 1.5x file on the same agent-fs QA path as screenshots below. In a PR, upload it as a GitHub user attachment and put the URL on its own line, so GitHub plays it inline.\n\n## Step 3: share the screenshot through agent-fs\n\nScreenshots on the worker disk disappear with the task. Upload them to agent-fs under the qa path convention: that copy is the durable original. Use `share-create` when a person needs to open the file. `--file` is binary-safe. `--content` is text-only and mangles PNGs.\n\n```bash\nagent-fs write thoughts/<agent-id>/qa/<topic>-screenshots/<name>.png \\\n --file /tmp/<name>.png -m "<what it shows>"\nagent-fs stat thoughts/<agent-id>/qa/<topic>-screenshots/<name>.png --json # confirm size > 0\nagent-fs share-create thoughts/<agent-id>/qa/<topic>-screenshots/<name>.png --json\n```\n\n- **GitHub PR body or comment**: upload the PNG, GIF or mp4 as a GitHub user attachment with the `github-attach` skill and embed ``. Attachments never expire and mp4s play inline. A UI PR puts a before (main) and after pair in the `## Before / after` table. Fallback for images only, when the GitHub token is an App token (`ghu_`/`ghs_`): `agent-fs signed-url <path> --inline --json` (expires in 7 days). Never a presigned URL for an mp4 or a doc: it downloads.\n- **Linear comment or Slack message**: paste the `share-create` link.\n- In `store-progress`, list the upload in the `attachments` field with `kind: "agent-fs"` and the path, and paste the `share-create` link for people who need to open the file.\n- If `agent-fs auth whoami` fails, report the local path, say the upload was skipped, and continue.\n\nThe `agent-fs` skill holds the full agent-fs recipe.\n\n## Gotchas\n\n- `wait <selector>` waits for a VISIBLE match. When the first match is hidden it times out (about 25 s) even though the element exists. Wait on rendered text instead: `wait --text "<rendered text>"`.\n- Never run `pkill -f <pattern>` inside a Bash call: the pattern also matches the calling shell\'s command line and kills it. Record the PID when you start a background server (`echo $!`) and `kill <pid>`.\n';
|
|
5741
6124
|
|
|
5742
6125
|
// templates/skills/apps/config.json
|
|
5743
6126
|
var config_default2 = `{
|
|
@@ -5757,374 +6140,10 @@ var config_default2 = `{
|
|
|
5757
6140
|
`;
|
|
5758
6141
|
|
|
5759
6142
|
// templates/skills/apps/content.md
|
|
5760
|
-
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 and `artifacts` for custom server logic or a UI outside this catalog.\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-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 `POST /api/apps/<id>/actions/<name>` (a `script` action runs its script, a `task` action spawns a task, a `sync` action refreshes sources). Read a query's `$param` placeholders and an action's kind/args from the definition before calling — do not guess parameter names.\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\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 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`. Actions stay REST-only from scripts — there is no SDK method to invoke them.\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`, or `link`; `string` and `enum` render as text. 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\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? }`; `name` must be declared in definition `actions`. For a task-kind row action, pass whole-row context with `input: { issue: { \"$row\": \"\" } }`.\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";
|
|
5761
|
-
|
|
5762
|
-
// templates/skills/artifacts/config.json
|
|
5763
|
-
var config_default3 = `{
|
|
5764
|
-
"kind": "skill",
|
|
5765
|
-
"name": "artifacts",
|
|
5766
|
-
"displayName": "Artifacts",
|
|
5767
|
-
"slug": "artifacts",
|
|
5768
|
-
"title": "Artifacts",
|
|
5769
|
-
"description": "Store durable task outputs (reports, screenshots, exports, logs) in agent-fs or the shared workspace, and serve interactive web content to a public auth-protected URL via localtunnel. Use when asked to save evidence for a PR/ticket, share a file, \\"create an artifact for X\\", \\"host this for me\\", \\"spin up a web server\\", or \\"give me a live link\\". Covers agent-fs writes and verification, binary uploads, naming and attachment conventions, the \`agent-swarm artifact\` CLI, Hono apps, PM2 daemonization, HTTP Basic auth, and the injected browser SDK.",
|
|
5770
|
-
"version": "2.0.0",
|
|
5771
|
-
"category": "skills",
|
|
5772
|
-
"placeholders": [],
|
|
5773
|
-
"runAllSeedersCandidate": true,
|
|
5774
|
-
"systemDefault": false,
|
|
5775
|
-
"tags": ["artifacts", "evidence", "qa", "agent-fs", "serving", "tunnel"]
|
|
5776
|
-
}
|
|
5777
|
-
`;
|
|
5778
|
-
|
|
5779
|
-
// templates/skills/artifacts/content.md
|
|
5780
|
-
var content_default3 = `# Artifacts
|
|
5781
|
-
|
|
5782
|
-
An artifact is any output that should outlive the session — a report,
|
|
5783
|
-
screenshot, recording, log, data export, dashboard. There are two things you
|
|
5784
|
-
can do with one, and this skill covers both:
|
|
5785
|
-
|
|
5786
|
-
- **Store it** — write it to agent-fs or the shared workspace so humans and
|
|
5787
|
-
other agents can find it later. This is the common case.
|
|
5788
|
-
- **Serve it** — run a real web server behind a public, auth-protected URL so a
|
|
5789
|
-
human can *interact* with it (custom routes, form posts, live data).
|
|
5790
|
-
|
|
5791
|
-
| You need… | Use |
|
|
5792
|
-
|---|---|
|
|
5793
|
-
| A file, report, screenshot, or export someone will read later | **Store** (agent-fs) |
|
|
5794
|
-
| A file another agent needs during *this* session | **Store** (\`/workspace/shared/\`) |
|
|
5795
|
-
| A static HTML/JSON report shared by URL | The **\`pages\`** skill — cheaper, no process to babysit |
|
|
5796
|
-
| Custom routes, websockets, file uploads, per-request computation | **Serve** (below) |
|
|
5797
|
-
|
|
5798
|
-
Rule of thumb: if the content is a *snapshot*, store it or make a page. If it's
|
|
5799
|
-
a *running program*, serve it.
|
|
5800
|
-
|
|
5801
|
-
---
|
|
5802
|
-
|
|
5803
|
-
# Part 1 — Storing artifacts
|
|
5804
|
-
|
|
5805
|
-
Two stores: **agent-fs** (structured, searchable, shareable, survives restarts)
|
|
5806
|
-
and the **shared workspace filesystem** (\`/workspace/shared/\`, fast, same-session).
|
|
5807
|
-
|
|
5808
|
-
## When to create one
|
|
5809
|
-
|
|
5810
|
-
- Your task produces a deliverable humans should review.
|
|
5811
|
-
- Another agent or future session needs to pick up where you left off.
|
|
5812
|
-
- You want to attach evidence to a PR, Linear ticket, or Slack message.
|
|
5813
|
-
- The output is too large for \`store-progress.output\`.
|
|
5814
|
-
|
|
5815
|
-
## agent-fs (preferred for anything human-shareable)
|
|
5816
|
-
|
|
5817
|
-
\`\`\`bash
|
|
5818
|
-
# Write to your personal drive
|
|
5819
|
-
agent-fs write thoughts/research/2026-05-28-topic.md --content "..." -m "description"
|
|
5820
|
-
|
|
5821
|
-
# Write to the shared drive (humans + other agents can see it)
|
|
5822
|
-
agent-fs --org <org-id> write \\
|
|
5823
|
-
thoughts/<agent-id>/research/2026-05-28-topic.md \\
|
|
5824
|
-
--content "..." -m "research findings"
|
|
5825
|
-
\`\`\`
|
|
5826
|
-
|
|
5827
|
-
Your org and drive context is already configured — you only need \`--org\` when
|
|
5828
|
-
writing somewhere other than your default. Read the id from
|
|
5829
|
-
\`agent-fs stat <path> --json\` rather than hardcoding one you saw in an example.
|
|
5830
|
-
|
|
5831
|
-
Always verify the write landed — agent-fs writes can fail silently with an
|
|
5832
|
-
empty payload:
|
|
5833
|
-
|
|
5834
|
-
\`\`\`bash
|
|
5835
|
-
agent-fs stat <path> --json | jq '.size'
|
|
5836
|
-
# If size < 200 bytes on a non-trivial artifact, the write FAILED — re-do it.
|
|
5837
|
-
\`\`\`
|
|
5838
|
-
|
|
5839
|
-
### Sharing agent-fs files with humans
|
|
5840
|
-
|
|
5841
|
-
The deployment configures the live viewer host through \`AGENT_FS_LIVE_URL\`; the
|
|
5842
|
-
documented fallback is \`https://live.agent-fs.dev\`. Resolve it in the shell and
|
|
5843
|
-
combine it with the \`orgId\` and \`driveId\` returned by \`agent-fs stat\`:
|
|
5844
|
-
|
|
5845
|
-
\`\`\`bash
|
|
5846
|
-
FILE_PATH='thoughts/<agent-id>/research/<file>.md'
|
|
5847
|
-
AGENT_FS_HOST=\${AGENT_FS_LIVE_URL:-https://live.agent-fs.dev}
|
|
5848
|
-
FILE_STAT=$(agent-fs stat "$FILE_PATH" --json)
|
|
5849
|
-
ORG_ID=$(printf '%s' "$FILE_STAT" | jq -r '.orgId')
|
|
5850
|
-
DRIVE_ID=$(printf '%s' "$FILE_STAT" | jq -r '.driveId')
|
|
5851
|
-
SHARE_URL="\${AGENT_FS_HOST%/}/file/~/$ORG_ID/$DRIVE_ID/$FILE_PATH"
|
|
5852
|
-
printf '%s\\n' "$SHARE_URL"
|
|
5853
|
-
\`\`\`
|
|
5854
|
-
|
|
5855
|
-
Paste the printed concrete URL into Markdown or a message. Markdown does not
|
|
5856
|
-
expand environment variables.
|
|
5857
|
-
|
|
5858
|
-
## Shared filesystem
|
|
5859
|
-
|
|
5860
|
-
For non-text artifacts or files other agents need during the same session:
|
|
5861
|
-
|
|
5862
|
-
- \`/workspace/shared/downloads/<agent-id>/\` — downloaded files
|
|
5863
|
-
- \`/workspace/shared/misc/<agent-id>/\` — other shared files
|
|
5864
|
-
|
|
5865
|
-
It does **not** survive container restarts. Anything that must outlive the
|
|
5866
|
-
session goes to agent-fs.
|
|
5867
|
-
|
|
5868
|
-
## Binary artifacts (PNG, MP4)
|
|
5869
|
-
|
|
5870
|
-
\`agent-fs write --content\` is text-only and mangles binaries. Upload binaries
|
|
5871
|
-
with \`--file\` (or piped stdin), which uses the binary-safe raw route
|
|
5872
|
-
(agent-fs CLI >= 0.7.1):
|
|
5873
|
-
|
|
5874
|
-
\`\`\`bash
|
|
5875
|
-
agent-fs write thoughts/<agent-id>/qa/<topic>-screenshots/login.png \\
|
|
5876
|
-
--file /tmp/login.png -m "login page after fix"
|
|
5877
|
-
agent-fs stat thoughts/<agent-id>/qa/<topic>-screenshots/login.png --json | jq '.size'
|
|
5878
|
-
\`\`\`
|
|
5879
|
-
|
|
5880
|
-
Capture with \`agent-browser screenshot <path>\` when it is installed, otherwise
|
|
5881
|
-
Playwright, ffmpeg, or a system screenshot tool. The agent-fs copy is the
|
|
5882
|
-
durable original. Where each kind of content goes:
|
|
5883
|
-
|
|
5884
|
-
| Content | Use | Why |
|
|
5885
|
-
|---|---|---|
|
|
5886
|
-
| Image or GIF in GitHub | GitHub user attachment (\`github-attach\` skill) | Permanent, renders inline, short URL |
|
|
5887
|
-
| Video (mp4) in GitHub | GitHub user attachment, else an \`agent-fs share-create\` link | GitHub plays it inline. A presigned link downloads it |
|
|
5888
|
-
| Doc (plan, research, report) | \`agent-fs share-create <path> --expires-in 604800\` | Preview page, no download |
|
|
5889
|
-
| Durable pointer | \`live.agent-fs.dev\` path, in a PR only under \`## Swarm provenance\` | Share links die after 7 days, this one does not (needs login) |
|
|
5890
|
-
| Presigned \`signed-url\` | Image fallback only, with \`--inline\` | Downloads and expires after 7 days. Never for docs or mp4s |
|
|
5891
|
-
|
|
5892
|
-
\`\`\`bash
|
|
5893
|
-
agent-fs signed-url thoughts/<agent-id>/qa/<topic>-screenshots/login.png --inline --json # fallback only; 24h default, --expires-in up to 7d
|
|
5894
|
-
\`\`\`
|
|
5895
|
-
|
|
5896
|
-
## Naming conventions
|
|
5897
|
-
|
|
5898
|
-
Name paths predictably by task, date, and artifact type:
|
|
5899
|
-
|
|
5900
|
-
\`\`\`
|
|
5901
|
-
thoughts/<agent-id>/research/YYYY-MM-DD-<topic>.md
|
|
5902
|
-
thoughts/<agent-id>/plans/YYYY-MM-DD-<topic>.md
|
|
5903
|
-
thoughts/<agent-id>/qa/<topic>-screenshots/<filename>.png
|
|
5904
|
-
misc/<agent-id>/<task-id>-<description>.ext
|
|
5905
|
-
\`\`\`
|
|
5906
|
-
|
|
5907
|
-
## Attaching artifacts
|
|
5908
|
-
|
|
5909
|
-
- **PR body** — embed images and mp4s as GitHub user attachments
|
|
5910
|
-
(\`\`), and link docs
|
|
5911
|
-
with \`share-create\`. Never embed a \`share-create\` link as an image: it is an
|
|
5912
|
-
HTML preview page. Swarm task links and durable agent-fs paths go only under
|
|
5913
|
-
\`## Swarm provenance\`.
|
|
5914
|
-
- **Slack** — link the agent-fs URL (team members only, the live viewer needs a login). Use a \`share-create\` link (up to 7 days) for anyone else.
|
|
5915
|
-
- **\`store-progress\`** — use the \`attachments\` field with \`kind: "agent-fs"\` and the path.
|
|
5916
|
-
- **Linear comments** — paste the live URL in the comment body.
|
|
5917
|
-
|
|
5918
|
-
## What NOT to store
|
|
5919
|
-
|
|
5920
|
-
- Secrets, API keys, OAuth tokens
|
|
5921
|
-
- Raw customer data without approval
|
|
5922
|
-
- Oversized files without approval (check size before uploading)
|
|
5923
|
-
- Ephemeral progress notes — those go in \`store-progress.progress\`
|
|
5924
|
-
|
|
5925
|
-
## Trade-offs
|
|
5926
|
-
|
|
5927
|
-
**agent-fs vs shared filesystem** — agent-fs is persistent, versioned, and
|
|
5928
|
-
searchable across sessions. The shared filesystem is faster for same-session
|
|
5929
|
-
handoffs but doesn't survive container restarts. Use agent-fs for anything that
|
|
5930
|
-
needs to outlive the current session or be reviewed by a human.
|
|
5931
|
-
|
|
5932
|
-
---
|
|
5933
|
-
|
|
5934
|
-
# Part 2 — Serving interactive web content
|
|
5935
|
-
|
|
5936
|
-
Serve a directory or a Hono app to a public, auth-protected URL via
|
|
5937
|
-
localtunnel. Use this when a static page won't do: custom routes, form posts,
|
|
5938
|
-
uploads, per-request computation.
|
|
5939
|
-
|
|
5940
|
-
The CLI is a subcommand of \`agent-swarm\`. Always invoke it as
|
|
5941
|
-
**\`agent-swarm artifact <subcommand>\`** — there is no top-level \`artifact\`
|
|
5942
|
-
binary.
|
|
5943
|
-
|
|
5944
|
-
## Quick start
|
|
5945
|
-
|
|
5946
|
-
### Static content
|
|
5947
|
-
|
|
5948
|
-
\`\`\`bash
|
|
5949
|
-
# Create your content in a persisted directory
|
|
5950
|
-
mkdir -p /workspace/personal/artifacts/my-report
|
|
5951
|
-
echo '<h1>My Report</h1>' > /workspace/personal/artifacts/my-report/index.html
|
|
5952
|
-
|
|
5953
|
-
# Serve it (auto-assigns a free port, creates tunnel, registers in service registry)
|
|
5954
|
-
agent-swarm artifact serve /workspace/personal/artifacts/my-report --name my-report
|
|
5955
|
-
# -> Copy the deployment URL printed by the command; it is authoritative.
|
|
5956
|
-
\`\`\`
|
|
5957
|
-
|
|
5958
|
-
### Programmatic (custom Hono server)
|
|
5959
|
-
|
|
5960
|
-
\`\`\`typescript
|
|
5961
|
-
import { createArtifactServer } from '../artifact-sdk';
|
|
5962
|
-
import { Hono } from 'hono';
|
|
5963
|
-
|
|
5964
|
-
const app = new Hono();
|
|
5965
|
-
app.get('/', (c) => c.html('<h1>Dashboard</h1>'));
|
|
5966
|
-
|
|
5967
|
-
const server = createArtifactServer({ name: 'dashboard', app });
|
|
5968
|
-
await server.start();
|
|
5969
|
-
console.log(\`Live at: \${server.url}\`);
|
|
5970
|
-
\`\`\`
|
|
5971
|
-
|
|
5972
|
-
You can also run \`agent-swarm artifact serve ./server.ts --name dashboard\` if
|
|
5973
|
-
\`server.ts\` exports a Hono instance as its default export.
|
|
5974
|
-
|
|
5975
|
-
See the bundled \`examples/\` directory for complete working examples
|
|
5976
|
-
(\`static-report.sh\`, \`hono-dashboard.ts\`, \`approval-flow.ts\`,
|
|
5977
|
-
\`multi-artifact.ts\`).
|
|
5978
|
-
|
|
5979
|
-
## CLI commands
|
|
5980
|
-
|
|
5981
|
-
| Command | Description |
|
|
5982
|
-
|---|---|
|
|
5983
|
-
| \`agent-swarm artifact serve <path> --name <name> [--port <port>] [--no-auth] [--subdomain <sub>]\` | Start serving content. \`<path>\` is a directory (static) or a \`.ts\`/\`.js\` file exporting a default Hono app. |
|
|
5984
|
-
| \`agent-swarm artifact list\` | List active artifacts (name, agent, port, URL, status) from the service registry. |
|
|
5985
|
-
| \`agent-swarm artifact stop <name>\` | Stop an artifact: deletes the matching PM2 process and unregisters it from the service registry. See "Known limitation" below. |
|
|
5986
|
-
|
|
5987
|
-
Flags accepted by \`serve\`:
|
|
5988
|
-
|
|
5989
|
-
- \`--name <name>\` — defaults to the basename of \`<path>\`. Used for the subdomain and PM2 process name.
|
|
5990
|
-
- \`--port <port>\` — pin to a specific port. Default: auto-assigned ephemeral port.
|
|
5991
|
-
- \`--no-auth\` — disable HTTP Basic auth on the tunnel (DANGEROUS — anyone with the URL can access).
|
|
5992
|
-
- \`--subdomain <sub>\` — override the default \`\${agentId}-\${name}\` subdomain.
|
|
5993
|
-
|
|
5994
|
-
## Auth & URL pattern
|
|
5995
|
-
|
|
5996
|
-
Tunnels are protected by **HTTP Basic auth** by default:
|
|
5997
|
-
|
|
5998
|
-
- **Username:** \`hi\` (hardcoded MVP default in \`src/artifact-sdk/tunnel.ts\`)
|
|
5999
|
-
- **Password:** the agent's \`API_KEY\`
|
|
6000
|
-
|
|
6001
|
-
> **Never put the API key in the URL.** The Basic-auth password *is* the swarm
|
|
6002
|
-
> \`API_KEY\` — it authenticates against the whole swarm API, not just this
|
|
6003
|
-
> artifact. A \`https://user:key@host\` URL leaks that credential into shell
|
|
6004
|
-
> history, browser history, proxy and tunnel access logs, referrer headers, and
|
|
6005
|
-
> anything you paste it into. Treat a credential-bearing URL as a leaked key.
|
|
6006
|
-
|
|
6007
|
-
The installed \`@desplega.ai/localtunnel\` package currently defaults to
|
|
6008
|
-
\`lt.desplega.ai\`, but deployments may configure another tunnel host. The URL
|
|
6009
|
-
printed by \`agent-swarm artifact serve\` (or exposed as \`server.url\`) is
|
|
6010
|
-
authoritative. Save that plain URL as \`ARTIFACT_URL\`; a browser will prompt for
|
|
6011
|
-
the credentials.
|
|
6012
|
-
|
|
6013
|
-
For scripts and \`curl\`, pass the credential out-of-band instead of inlining it,
|
|
6014
|
-
and read it from the environment so it never appears as a literal:
|
|
6015
|
-
|
|
6016
|
-
\`\`\`bash
|
|
6017
|
-
# -u keeps the credential out of the URL; --netrc-file keeps it out of argv too.
|
|
6018
|
-
curl -u "hi:$API_KEY" "$ARTIFACT_URL"
|
|
6019
|
-
|
|
6020
|
-
# Better for anything long-lived or logged — argv is visible to other processes:
|
|
6021
|
-
ARTIFACT_HOST=\${ARTIFACT_URL#*://}
|
|
6022
|
-
ARTIFACT_HOST=\${ARTIFACT_HOST%%/*}
|
|
6023
|
-
printf 'machine %s login hi password %s\\n' \\
|
|
6024
|
-
"$ARTIFACT_HOST" "$API_KEY" > /tmp/artifact-netrc
|
|
6025
|
-
chmod 600 /tmp/artifact-netrc
|
|
6026
|
-
curl --netrc-file /tmp/artifact-netrc "$ARTIFACT_URL"
|
|
6027
|
-
\`\`\`
|
|
6028
|
-
|
|
6029
|
-
Do not echo the assembled command, and do not paste the credential into a task
|
|
6030
|
-
report, Slack message, PR body, or page — those are all persisted.
|
|
6031
|
-
|
|
6032
|
-
Use \`--no-auth\` only for genuinely public content. Anyone who learns the
|
|
6033
|
-
subdomain can read it.
|
|
6034
|
-
|
|
6035
|
-
## Running it as a daemon
|
|
6036
|
-
|
|
6037
|
-
\`agent-swarm artifact serve\` blocks on a never-resolving promise to stay alive —
|
|
6038
|
-
you cannot inline it in a script that needs to do other work. Pick one:
|
|
6039
|
-
|
|
6040
|
-
### Option A — \`nohup\` (quick, throwaway)
|
|
6041
|
-
|
|
6042
|
-
\`\`\`bash
|
|
6043
|
-
mkdir -p /workspace/personal/logs
|
|
6044
|
-
nohup agent-swarm artifact serve /workspace/personal/artifacts/my-report \\
|
|
6045
|
-
--name my-report \\
|
|
6046
|
-
> /workspace/personal/logs/my-report.out 2>&1 &
|
|
6047
|
-
echo $! > /workspace/personal/logs/my-report.pid
|
|
6048
|
-
|
|
6049
|
-
# Later, kill it manually:
|
|
6050
|
-
kill "$(cat /workspace/personal/logs/my-report.pid)"
|
|
6051
|
-
\`\`\`
|
|
6052
|
-
|
|
6053
|
-
### Option B — PM2 (recommended for anything you'll come back to)
|
|
6054
|
-
|
|
6055
|
-
PM2 gives you auto-restart on crash, a process name, log management, and —
|
|
6056
|
-
crucially — lets \`agent-swarm artifact stop <name>\` actually kill it.
|
|
6057
|
-
|
|
6058
|
-
\`\`\`bash
|
|
6059
|
-
pm2 start agent-swarm \\
|
|
6060
|
-
--name artifact-my-report \\
|
|
6061
|
-
-- artifact serve /workspace/personal/artifacts/my-report --name my-report
|
|
6062
|
-
|
|
6063
|
-
# Stop it cleanly later:
|
|
6064
|
-
agent-swarm artifact stop my-report
|
|
6065
|
-
\`\`\`
|
|
6066
|
-
|
|
6067
|
-
The PM2 process name **must** be \`artifact-<name>\` (matching \`--name\`) — that's
|
|
6068
|
-
exactly what \`artifact stop\` looks for.
|
|
6069
|
-
|
|
6070
|
-
### Known limitation — \`artifact stop\` only kills PM2-started processes
|
|
6071
|
-
|
|
6072
|
-
\`agent-swarm artifact stop <name>\` runs \`pm2 delete artifact-<name>\` and then
|
|
6073
|
-
unregisters the entry from the service registry. If you started the artifact
|
|
6074
|
-
with \`nohup\` (or \`&\`, or any non-PM2 launcher), \`pm2 delete\` silently fails and
|
|
6075
|
-
the server keeps running and serving — even though the command prints
|
|
6076
|
-
\`Artifact '<name>' stopped.\` Until that's fixed:
|
|
6077
|
-
|
|
6078
|
-
- Use **PM2** if you want \`artifact stop\` to do its job.
|
|
6079
|
-
- For \`nohup\`/foreground processes, kill the PID yourself
|
|
6080
|
-
(\`kill <pid>\` or \`pkill -f 'artifact serve.*<name>'\`) **and then** run
|
|
6081
|
-
\`agent-swarm artifact stop <name>\` to clear the registry row.
|
|
6082
|
-
|
|
6083
|
-
## Multiple artifacts
|
|
6084
|
-
|
|
6085
|
-
Each artifact gets its own port (auto-assigned) and subdomain
|
|
6086
|
-
(\`<agentId>-<name>\`). You can run several simultaneously — see
|
|
6087
|
-
\`examples/multi-artifact.ts\`.
|
|
6088
|
-
|
|
6089
|
-
## Browser SDK
|
|
6090
|
-
|
|
6091
|
-
Served HTML gets the same injected browser SDK that pages get. Load it and use
|
|
6092
|
-
the ready-made singleton:
|
|
6093
|
-
|
|
6094
|
-
\`\`\`html
|
|
6095
|
-
<script src="/@swarm/sdk.js"></script>
|
|
6096
|
-
<script>
|
|
6097
|
-
// \`window.swarmSdk\` is constructed for you — no \`new SwarmSDK()\` needed.
|
|
6098
|
-
const tasks = await window.swarmSdk.tasks.list({ status: 'in_progress' });
|
|
6099
|
-
const agents = await window.swarmSdk.agents.list();
|
|
6100
|
-
</script>
|
|
6101
|
-
\`\`\`
|
|
6102
|
-
|
|
6103
|
-
The SDK is **domain-grouped** (\`swarmSdk.tasks\`, \`.agents\`, \`.events\`,
|
|
6104
|
-
\`.memory\`, \`.repos\`, \`.schedules\`, \`.approvalRequests\`, \`.assets\`), each domain
|
|
6105
|
-
mapping 1:1 onto the public REST API. The **\`pages\`** skill documents the full
|
|
6106
|
-
method surface — it's the same object, so don't duplicate it here.
|
|
6107
|
-
|
|
6108
|
-
Calls route through the \`/@swarm/api/*\` proxy, which injects auth server-side.
|
|
6109
|
-
Browser code never sees the API key.
|
|
6110
|
-
|
|
6111
|
-
## Storage
|
|
6112
|
-
|
|
6113
|
-
Always store served content in persisted directories — the working dir is wiped
|
|
6114
|
-
between sessions:
|
|
6115
|
-
|
|
6116
|
-
- \`/workspace/personal/artifacts/\` — per-agent, persists across sessions (default)
|
|
6117
|
-
- \`/workspace/shared/artifacts/\` — shared across the swarm
|
|
6118
|
-
|
|
6119
|
-
## See also
|
|
6120
|
-
|
|
6121
|
-
- The **\`pages\`** skill — DB-backed static HTML/JSON, no process to run. Prefer
|
|
6122
|
-
it whenever the content is a snapshot.
|
|
6123
|
-
- The **\`kv-storage\`** skill — for the small state a served page needs.
|
|
6124
|
-
`;
|
|
6143
|
+
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-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 `POST /api/apps/<id>/actions/<name>` (a `script` action runs its script, a `task` action spawns a task, a `sync` action refreshes sources). Read a query's `$param` placeholders and an action's kind/args from the definition before calling — do not guess parameter names.\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\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 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`. Actions stay REST-only from scripts — there is no SDK method to invoke them.\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`, or `link`; `string` and `enum` render as text. 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\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? }`; `name` must be declared in definition `actions`. For a task-kind row action, pass whole-row context with `input: { issue: { \"$row\": \"\" } }`.\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";
|
|
6125
6144
|
|
|
6126
6145
|
// templates/skills/ask-user/config.json
|
|
6127
|
-
var
|
|
6146
|
+
var config_default3 = `{
|
|
6128
6147
|
"category": "skills",
|
|
6129
6148
|
"description": "Conventions for asking the user questions during a workflow via the AskUserQuestion tool. Other skills reference this to keep prompts consistent and structured.",
|
|
6130
6149
|
"displayName": "Ask User",
|
|
@@ -6145,7 +6164,7 @@ var config_default4 = `{
|
|
|
6145
6164
|
`;
|
|
6146
6165
|
|
|
6147
6166
|
// templates/skills/ask-user/content.md
|
|
6148
|
-
var
|
|
6167
|
+
var content_default3 = `# Ask User
|
|
6149
6168
|
|
|
6150
6169
|
Thin helper that codifies how to ask the user questions during a workflow. Other skills (planning, research, brainstorming, implementing) reference this skill instead of duplicating AskUserQuestion conventions.
|
|
6151
6170
|
|
|
@@ -6221,7 +6240,7 @@ options:
|
|
|
6221
6240
|
`;
|
|
6222
6241
|
|
|
6223
6242
|
// templates/skills/asset-namespaces/config.json
|
|
6224
|
-
var
|
|
6243
|
+
var config_default4 = `{
|
|
6225
6244
|
"kind": "skill",
|
|
6226
6245
|
"name": "asset-namespaces",
|
|
6227
6246
|
"displayName": "Asset Namespaces",
|
|
@@ -6238,10 +6257,10 @@ var config_default5 = `{
|
|
|
6238
6257
|
`;
|
|
6239
6258
|
|
|
6240
6259
|
// templates/skills/asset-namespaces/content.md
|
|
6241
|
-
var
|
|
6260
|
+
var content_default4 = "# Asset Namespaces\n\nUse an asset `key` as directory-like grouping metadata across tasks, workflows, schedules, pages, apps, scripts, and mapped provider files. Keep identity separate: entity IDs remain authoritative, task `contextKey` remains routing context, and many assets may intentionally share one key.\n\n## Choose a namespace\n\n- Use `shared/` for general work and stable descendants such as `shared/releases/` or `shared/platform/migrations/` for team-visible groupings.\n- Use `personal/<user-id>/` or a descendant only for work owned by the trusted resolved user in the current request or task context.\n- Prefer durable organizational paths over temporary status labels. Put lifecycle state in task or workflow status fields, not paths such as `shared/in-progress/`.\n- Supply canonical values: lowercase relative paths, forward slashes, and a trailing slash. The server trims, Unicode-normalizes, lowercases, and adds the trailing slash, but rejects empty segments, `.`, `..`, backslashes, absolute paths, NUL bytes, unknown roots, and values longer than 255 characters.\n\n`personal/` is a write-ownership convention, **not** a privacy or read-visibility boundary. Continue to rely on each entity's authorization rules, and never place sensitive data under `personal/` merely because of the key.\n\n## Set and inherit keys\n\nPass `key` when creating a task, workflow, schedule, or page. Omitting it creates a deterministic shared resource key such as `shared/task:<id>/`, `shared/workflow:<id>/`, `shared/schedule:<id>/`, `shared/page:<id>/`, or `shared/fs:agent-fs:<id>/`.\n\n- Tasks sent from another task inherit the source task's key unless explicitly overridden.\n- Tasks launched by a workflow inherit the workflow key unless the agent-task node supplies a key.\n- Tasks launched by a schedule inherit the schedule key.\n- Agent-fs task attachments receive logical file mappings and inherit their task key.\n\nUse only a canonical user-registry ID in `personal/<user-id>/`. Do not substitute an agent ID, email address, Slack ID, or unverified caller-provided value. The API authorizes the destination against trusted request/task context and rejects mismatches.\n\n## Find and manage assets\n\nUse entity list tools with `key` for an exact namespace or `keyPrefix` for a subtree where those filters are exposed. For a lightweight cross-entity view, use `GET /api/assets` or the public page SDK:\n\n```js\nconst result = await window.swarmSdk.assets.list({\n keyPrefix: \"shared/releases/\",\n types: \"task,workflow,schedule,page,app,script,file\",\n limit: 100,\n});\n```\n\nMove an asset logically without changing its ID:\n\n```js\nawait window.swarmSdk.assets.move(\"workflow\", workflowId, \"shared/platform/\");\n```\n\nThe full `swarmSdk.assets` domain is:\n\n| Method | Purpose |\n|---|---|\n| `list(filters?)` | Return lightweight cross-entity summaries. |\n| `move(entityType, id, key)` | Change local namespace metadata. |\n| `registerMapping(body)` | Operator-only registration of a provider object under a logical key. |\n| `audit()` | Operator-only structural and mapping-drift audit. |\n\nProvider-backed moves are metadata-only: do not claim that a provider file, drive object, or agent-fs path was renamed or moved remotely. Repair audit warnings before retrying a blocked move.\n\n## Before writing\n\n1. Confirm whether the grouping is shared or owned by a resolved user.\n2. Keep the key non-unique and separate from entity identity or routing context.\n3. Pass the key at the highest-level parent so descendants inherit it.\n4. Use exact and prefix filters instead of scanning unrelated records.\n5. Treat personal namespaces and provider mappings according to their real authorization and remote-move limits.\n";
|
|
6242
6261
|
|
|
6243
6262
|
// templates/skills/attio-interaction/config.json
|
|
6244
|
-
var
|
|
6263
|
+
var config_default5 = `{
|
|
6245
6264
|
"kind": "skill",
|
|
6246
6265
|
"name": "attio-interaction",
|
|
6247
6266
|
"displayName": "Attio Interaction",
|
|
@@ -6259,10 +6278,10 @@ var config_default6 = `{
|
|
|
6259
6278
|
`;
|
|
6260
6279
|
|
|
6261
6280
|
// templates/skills/attio-interaction/content.md
|
|
6262
|
-
var
|
|
6281
|
+
var content_default5 = '# Attio Interaction (Read + Write)\n\nUse this skill to read and write your Attio CRM through the REST API v2. Every read or write is a direct API call; agent-swarm does not maintain a separate Attio sync.\n\n## TL;DR\n\n1. Resolve `ATTIO_API_KEY` from swarm config before making calls.\n2. Base URL: `https://api.attio.com/v2/`\n3. Use `Authorization: Bearer $ATTIO_API_KEY`, `Content-Type: application/json`, and `Accept: application/json`.\n4. Prefer upsert over create for People, Companies, and Deals: `PUT /v2/objects/{slug}/records` with `matching_attribute`.\n5. Rate limits: 100 reads/sec, 25 writes/sec. Pace write bursts to roughly 15-20/sec.\n6. Attribute values are arrays, even for scalar values: `[{ "value": 42 }]`, never `42`.\n\n## Authentication\n\n```bash\nATTIO_API_KEY=$(get-config key="ATTIO_API_KEY" includeSecrets=true)\n\ncurl -sS "https://api.attio.com/v2/objects" \\\n -H "Authorization: Bearer $ATTIO_API_KEY" \\\n -H "Accept: application/json"\n```\n\nIf calls return `401`, re-fetch the key from config. If it still fails, notify the Lead; the key may need rotation. Do not retry silently.\n\n## Core object slugs\n\n| Object | API slug | Primary matching attribute |\n|---|---|---|\n| Companies | `companies` | `domains` |\n| People | `people` | `email_addresses` |\n| Deals | `deals` | Usually no global dedupe key; link to company/person records |\n\nCustom objects use their configured API slug. Discover them with `GET /v2/objects`.\n\n## Common operations\n\n### 1. Discover objects and slugs\n\n```bash\ncurl -sS "https://api.attio.com/v2/objects" \\\n -H "Authorization: Bearer $ATTIO_API_KEY" \\\n -H "Accept: application/json" \\\n | jq \'.data[] | {slug: .api_slug, name: .title}\'\n```\n\n### 2. Query records with filters and pagination\n\n```bash\ncurl -sS -X POST "https://api.attio.com/v2/objects/companies/records/query" \\\n -H "Authorization: Bearer $ATTIO_API_KEY" \\\n -H "Content-Type: application/json" \\\n -d \'{\n "filter": {\n "stage": { "$not_equal": "Won" }\n },\n "limit": 100,\n "offset": 0\n }\' | jq \'.data[] | {record_id: .id.record_id, name: .values.name[0].value}\'\n```\n\nPaginate by increasing `offset` until `data` is empty. For no filter, omit the `filter` key.\n\n### 3. Get a single record\n\n```bash\ncurl -sS "https://api.attio.com/v2/objects/companies/records/{RECORD_ID}" \\\n -H "Authorization: Bearer $ATTIO_API_KEY" \\\n -H "Accept: application/json" \\\n | jq \'.data.values\'\n```\n\n### 4. Upsert a company by domain\n\nUse `PUT` with `matching_attribute`. It creates if not found and updates if found, so it is safe to call repeatedly.\n\n```bash\ncurl -sS -X PUT "https://api.attio.com/v2/objects/companies/records" \\\n -H "Authorization: Bearer $ATTIO_API_KEY" \\\n -H "Content-Type: application/json" \\\n -d \'{\n "data": {\n "values": {\n "name": [{ "value": "Acme Corp" }],\n "domains": [{ "domain": "acme.com" }],\n "employee_count": [{ "value": 150 }]\n }\n },\n "matching_attribute": "domains"\n }\' | jq \'{record_id: .data.id.record_id}\'\n```\n\n### 5. Upsert a person by email\n\n```bash\ncurl -sS -X PUT "https://api.attio.com/v2/objects/people/records" \\\n -H "Authorization: Bearer $ATTIO_API_KEY" \\\n -H "Content-Type: application/json" \\\n -d \'{\n "data": {\n "values": {\n "name": [{ "first_name": "Jane", "last_name": "Doe" }],\n "email_addresses": [{ "email_address": "jane@acme.com" }],\n "job_title": [{ "value": "CTO" }]\n }\n },\n "matching_attribute": "email_addresses"\n }\' | jq \'{record_id: .data.id.record_id}\'\n```\n\nAn upsert returns HTTP `200` when it updates an existing record and `201` when it creates one. The response body has the same shape in both cases.\n\n### 6. Update specific attributes on an existing record\n\n```bash\ncurl -sS -X PATCH "https://api.attio.com/v2/objects/companies/records/{RECORD_ID}" \\\n -H "Authorization: Bearer $ATTIO_API_KEY" \\\n -H "Content-Type: application/json" \\\n -d \'{\n "data": {\n "values": {\n "icp_score": [{ "value": 85 }],\n "icp_tier": [{ "value": "Tier 1" }]\n }\n }\n }\'\n```\n\n### 7. Write a note to a record\n\nNotes are concise free-text entries linked to a record. Use them to preserve useful interaction context, enrichment findings, or an agent result that should remain visible in the CRM.\n\n```bash\ncurl -sS -X POST "https://api.attio.com/v2/notes" \\\n -H "Authorization: Bearer $ATTIO_API_KEY" \\\n -H "Content-Type: application/json" \\\n -d \'{\n "data": {\n "parent_object": "companies",\n "parent_record_id": "{RECORD_ID}",\n "title": "Enrichment - 2026-06-02",\n "content": "Employee count: 150. Funding stage: Series A. Tech stack: Node.js, React."\n }\n }\' | jq \'{note_id: .data.id.note_id}\'\n```\n\n### 8. Create a task linked to a record\n\n```bash\ncurl -sS "https://api.attio.com/v2/workspace_members" \\\n -H "Authorization: Bearer $ATTIO_API_KEY" \\\n -H "Accept: application/json" \\\n | jq \'.data[] | {member_id: .id.workspace_member_id, name: .name, email: .email_address}\'\n\ncurl -sS -X POST "https://api.attio.com/v2/tasks" \\\n -H "Authorization: Bearer $ATTIO_API_KEY" \\\n -H "Content-Type: application/json" \\\n -d \'{\n "data": {\n "content": "Follow up - no contact in 21 days",\n "is_completed": false,\n "assignees": [\n { "referenced_actor_type": "workspace-member", "referenced_actor_id": "{MEMBER_ID}" }\n ],\n "linked_records": [\n { "target_object": "deals", "target_record_id": "{DEAL_RECORD_ID}" }\n ]\n }\n }\' | jq \'{task_id: .data.id.task_id}\'\n```\n\n### 9. Post a comment on a record\n\n```bash\ncurl -sS -X POST "https://api.attio.com/v2/comments" \\\n -H "Authorization: Bearer $ATTIO_API_KEY" \\\n -H "Content-Type: application/json" \\\n -d \'{\n "data": {\n "record": { "target_object": "companies", "target_record_id": "{RECORD_ID}" },\n "content": [\n { "type": "text", "text": "Possible duplicate of acme-corp-old - please review and merge." }\n ]\n }\n }\' | jq \'{comment_id: .data.id.comment_id}\'\n```\n\n### 10. Query a list or pipeline view\n\n```bash\ncurl -sS "https://api.attio.com/v2/lists" \\\n -H "Authorization: Bearer $ATTIO_API_KEY" \\\n -H "Accept: application/json" \\\n | jq \'.data[] | {list_id: .id.list_id, name: .title}\'\n\ncurl -sS -X POST "https://api.attio.com/v2/lists/{LIST_ID}/entries/query" \\\n -H "Authorization: Bearer $ATTIO_API_KEY" \\\n -H "Content-Type: application/json" \\\n -d \'{ "limit": 100, "offset": 0 }\' \\\n | jq \'.data[] | {entry_id: .id.entry_id, record_id: .record_id}\'\n```\n\n### 11. Global search across objects\n\n```bash\ncurl -sS -X POST "https://api.attio.com/v2/records/search" \\\n -H "Authorization: Bearer $ATTIO_API_KEY" \\\n -H "Content-Type: application/json" \\\n -d \'{ "query": "acme", "limit": 10 }\' \\\n | jq \'.data[] | {object: .object_type, record_id: .id.record_id}\'\n```\n\n## Webhooks\n\nAttio delivers webhooks at least once. Payloads contain IDs only, so always re-fetch the full record via `GET` before acting.\n\nKey event types:\n\n- `record.created`, `record.updated`, `record.deleted`\n- `list-entry.created`, `list-entry.updated`, `list-entry.deleted`\n- `note.created`\n- `task.created`, `task.completed`\n\nWebhook timeout is 5 seconds. Respond `200` immediately and do async processing in a follow-up swarm task.\n\n## Rate limits\n\n| Operation | Hard limit | Safe working rate |\n|---|---|---|\n| Reads | 100 req/sec | ~80 req/sec |\n| Writes | 25 req/sec | ~15-20 req/sec |\n\nAdd `sleep 0.05` between write calls in loops. Attio does not provide a native batch endpoint for these operations.\n\n## Operational rules\n\n- Upsert first. Use `PUT /records` with `matching_attribute` for create-or-update. `POST /records` can create duplicates.\n- Re-fetch webhook records. Webhook payloads are event hints, not full source-of-truth records.\n- Values are arrays. Every attribute value must be wrapped in an array.\n- No merge endpoint. Attio has no API-level record merge; dedupe agents should flag duplicates as comments or tasks for human review.\n- Check config first. Fetch `ATTIO_API_KEY` via `get-config includeSecrets=true`; never hardcode it.\n\n## Error handling\n\n| Status | Likely cause | Action |\n|---|---|---|\n| 401 | API key invalid or expired | Re-fetch from config. If still failing, notify Lead for rotation. |\n| 403 | Key lacks permission | Check the Attio API key\'s workspace permissions. |\n| 404 | Wrong object slug or record ID | Re-discover slugs with `GET /v2/objects`. |\n| 400 | Malformed body | Ensure attribute values are wrapped in arrays. |\n| 422 | Validation or conflict error | Read the `errors` array for field-level details. |\n| 429 | Rate-limited | Back off and retry after `Retry-After` if provided. |\n\n## Worked example: stale deal reactivation\n\n```bash\nATTIO_API_KEY=$(get-config key="ATTIO_API_KEY" includeSecrets=true)\n\nDEALS=$(curl -sS -X POST "https://api.attio.com/v2/objects/deals/records/query" \\\n -H "Authorization: Bearer $ATTIO_API_KEY" \\\n -H "Content-Type: application/json" \\\n -d \'{"filter": {"stage": {"$not_equal": "Won"}}, "limit": 200}\')\n\necho "$DEALS" | jq -r \'.data[] | .id.record_id\' | while read -r RECORD_ID; do\n RECORD=$(curl -sS "https://api.attio.com/v2/objects/deals/records/$RECORD_ID" \\\n -H "Authorization: Bearer $ATTIO_API_KEY" \\\n -H "Accept: application/json")\n # Compute staleness from the relevant date attribute. If stale, create a task.\n sleep 0.05\ndone\n```\n\n## Related references\n\n- Official Attio REST API docs: https://developers.attio.com/reference\n- Official Attio MCP overview: https://docs.attio.com/mcp/overview\n';
|
|
6263
6282
|
|
|
6264
6283
|
// templates/skills/brainstorming/config.json
|
|
6265
|
-
var
|
|
6284
|
+
var config_default6 = `{
|
|
6266
6285
|
"category": "skills",
|
|
6267
6286
|
"description": "Interactive exploration of ideas through Socratic Q&A. Produces progressive documents that serve as lightweight pre-PRDs feeding into research.",
|
|
6268
6287
|
"displayName": "Brainstorming",
|
|
@@ -6283,7 +6302,7 @@ var config_default7 = `{
|
|
|
6283
6302
|
`;
|
|
6284
6303
|
|
|
6285
6304
|
// templates/skills/brainstorming/content.md
|
|
6286
|
-
var
|
|
6305
|
+
var content_default6 = `# Brainstorming
|
|
6287
6306
|
|
|
6288
6307
|
You are facilitating interactive exploration of ideas through Socratic Q&A. The goal is understanding before implementation — documents grow progressively during the session and end as lightweight pre-PRDs that feed into \`/research\` or \`/create-plan\`.
|
|
6289
6308
|
|
|
@@ -6425,6 +6444,26 @@ File-review is on by default:
|
|
|
6425
6444
|
- Process feedback with the \`file-review:process-review\` skill
|
|
6426
6445
|
`;
|
|
6427
6446
|
|
|
6447
|
+
// templates/skills/chat-sdk/config.json
|
|
6448
|
+
var config_default7 = `{
|
|
6449
|
+
"kind": "skill",
|
|
6450
|
+
"name": "chat-sdk",
|
|
6451
|
+
"displayName": "Chat SDK",
|
|
6452
|
+
"slug": "chat-sdk",
|
|
6453
|
+
"title": "Chat SDK",
|
|
6454
|
+
"description": "Chat threads from Telegram, Slack (Chat SDK), Discord, and Teams. Use when a task carries a task:chat-sdk contextKey or a user asked through Telegram, Slack, Discord, or Teams. Covers what the swarm posts for you, \`chat-reply\`, markdown and length per platform, attachments, \`chat-read-thread\` limits, and channel routes with \`register-chat-channel\`.",
|
|
6455
|
+
"version": "1.0.0",
|
|
6456
|
+
"category": "skills",
|
|
6457
|
+
"placeholders": [],
|
|
6458
|
+
"runAllSeedersCandidate": true,
|
|
6459
|
+
"systemDefault": false,
|
|
6460
|
+
"tags": ["chat", "telegram", "discord", "teams", "slack", "communication"]
|
|
6461
|
+
}
|
|
6462
|
+
`;
|
|
6463
|
+
|
|
6464
|
+
// templates/skills/chat-sdk/content.md
|
|
6465
|
+
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 arrive as task 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. You cannot send files back to the thread in this version. Share a link instead.\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";
|
|
6466
|
+
|
|
6428
6467
|
// templates/skills/code-quality/config.json
|
|
6429
6468
|
var config_default8 = `{
|
|
6430
6469
|
"kind": "skill",
|
|
@@ -6910,7 +6949,7 @@ var config_default12 = `{
|
|
|
6910
6949
|
`;
|
|
6911
6950
|
|
|
6912
6951
|
// templates/skills/composio-gmail/content.md
|
|
6913
|
-
var content_default12 = '# Composio · Gmail\n\nToolkit slug: **`gmail`**. Read the [[composio]] hub first for the call model\n(`agent-swarm x composio …`, user_id, connected accounts, the 4302 gotcha).\nTool `arguments` go inside the request body; `user_id` defaults to `"me"` (the\nauthorized account) — you usually don\'t need to set it.\n\n```bash\n# Direct execute (reliable path — pin the ACTIVE ca_… from the hub Recipe B)\nagent-swarm x composio POST /tools/execute/<SLUG> \\\n --body \'{"user_id":"<connected-account-email>","connected_account_id":"<active-connected-account-id>","arguments":{ … }}\'\n```\n\nFollow the hub\'s **Resolve the user and connected account** procedure, then\nselect the resolved user\'s `gmail` account whose status is `ACTIVE`.\n\n## Headline tools\n\n| Slug | What | Key args |\n|---|---|---|\n| `GMAIL_FETCH_EMAILS` | List/search emails | `query`, `max_results` (def **1** — set it!), `include_payload` (def true), `verbose` (def true), `ids_only`, `label_ids`, `page_token` |\n| `GMAIL_FETCH_MESSAGE_BY_MESSAGE_ID` | Full single message | `message_id`, `include_payload` |\n| `GMAIL_FETCH_MESSAGE_BY_THREAD_ID` | All messages in a thread | `thread_id` |\n| `GMAIL_LIST_THREADS` | List threads | `query`, `max_results`, `page_token` |\n| `GMAIL_SEND_EMAIL` | Send | `recipient_email`, `subject`, `body`, `is_html` (def false), `cc`, `bcc`, `extra_recipients`, `attachment` |\n| `GMAIL_REPLY_TO_THREAD` | Reply in-thread | `thread_id`, `message_body`, `recipient_email` |\n| `GMAIL_CREATE_EMAIL_DRAFT` / `GMAIL_SEND_DRAFT` | Draft then send | `body`, `subject`, `recipient_email` / `draft_id` |\n| `GMAIL_GET_PROFILE` | Whose mailbox is this? | — |\n| `GMAIL_LIST_LABELS` / `GMAIL_CREATE_LABEL` / `GMAIL_ADD_LABEL_TO_EMAIL` | Labels | `message_id`, `label_ids` (use LIST_LABELS for custom IDs) |\n| `GMAIL_GET_CONTACTS` / `GMAIL_SEARCH_PEOPLE` | Contacts | `query` |\n| `GMAIL_GET_ATTACHMENT` | Download attachment | `message_id`, `attachment_id` |\n\nFull set: 63 tools — list with\n`agent-swarm x composio GET "/tools?toolkit_slug=gmail&limit=100" | jq -r \'.items[]|"\\(.slug)\\t\\(.name)"\'`.\nAvoid the ones marked Deprecated (`GMAIL_LIST_MESSAGES`, `GMAIL_REMOVE_LABEL`).\n\n## Read recipe (metadata-first)\n\n```bash\nagent-swarm x composio POST /tools/execute/GMAIL_FETCH_EMAILS \\\n --body \'{"connected_account_id":"ca_…","arguments":{"max_results":5,"include_payload":false,"verbose":false}}\'\n```\n- **Always set `max_results`** — the default is `1`.\n- Use Gmail `query` syntax: `"is:unread"`, `"from:foo@bar.com newer_than:7d"`,\n `"subject:invoice has:attachment"`.\n- Keep `include_payload:false` + `verbose:false` unless the user needs full bodies\n (token-heavy). Use `ids_only:true` for the cheapest listing.\n\n## Send recipe\n\n```bash\nagent-swarm x composio POST /tools/execute/GMAIL_SEND_EMAIL \\\n --body \'{"connected_account_id":"ca_…","arguments":{\n "recipient_email":"someone@example.com",\n "subject":"Hello",\n "body":"<p>Hi there</p>",\n "is_html":true,\n "cc":["cc@example.com"]\n }}\'\n```\n- Set `is_html:true` when `body` contains HTML, otherwise it sends as literal text.\n- `recipient_email` is the primary; add more via `extra_recipients` / `cc` / `bcc`.\n- **Sending is a write action** — only do it when the task explicitly asks.\n\n## Reply in a thread\n\n```bash\nagent-swarm x composio POST /tools/execute/GMAIL_REPLY_TO_THREAD \\\n --body \'{"connected_account_id":"ca_…","arguments":{\n "thread_id":"<thread_id>","recipient_email":"someone@example.com","message_body":"thanks!"\n }}\'\n```\n\n## Gotchas\n\n- Default `max_results` is `1` — forgetting it makes "list my emails" return a\n single message.\n- Bodies/attachments are token-heavy and may contain secrets — default to\n metadata; the secret-scrubber doesn\'t run on Composio tool output.\n- If you get `ToolRouterV2_NoActiveConnection`, switch to direct execute with the\n pinned `connected_account_id` (hub Gotchas).\n';
|
|
6952
|
+
var content_default12 = '# Composio · Gmail\n\nToolkit slug: **`gmail`**. Read the [[composio]] hub first for the call model\n(`agent-swarm x composio …`, user_id, connected accounts, the 4302 gotcha).\nTool `arguments` go inside the request body; `user_id` defaults to `"me"` (the\nauthorized account) — you usually don\'t need to set it.\n\n```bash\n# Direct execute (reliable path — pin the ACTIVE ca_… from the hub Recipe B)\nagent-swarm x composio POST /tools/execute/<SLUG> \\\n --body \'{"user_id":"<connected-account-email>","connected_account_id":"<active-connected-account-id>","arguments":{ … }}\'\n```\n\nFollow the hub\'s **Resolve the user and connected account** procedure, then\nselect the resolved user\'s `gmail` account whose status is `ACTIVE`.\n\n## Headline tools\n\n| Slug | What | Key args |\n|---|---|---|\n| `GMAIL_FETCH_EMAILS` | List/search emails | `query`, `max_results` (def **1** — set it!), `include_payload` (def true), `verbose` (def true), `ids_only`, `label_ids`, `page_token` |\n| `GMAIL_FETCH_MESSAGE_BY_MESSAGE_ID` | Full single message | `message_id`, `include_payload` |\n| `GMAIL_FETCH_MESSAGE_BY_THREAD_ID` | All messages in a thread | `thread_id` |\n| `GMAIL_LIST_THREADS` | List threads | `query`, `max_results`, `page_token` |\n| `GMAIL_SEND_EMAIL` | Send | `recipient_email`, `subject`, `body`, `is_html` (def false), `cc`, `bcc`, `extra_recipients`, `attachment` |\n| `GMAIL_REPLY_TO_THREAD` | Reply in-thread | `thread_id`, `message_body`, `recipient_email` |\n| `GMAIL_CREATE_EMAIL_DRAFT` / `GMAIL_SEND_DRAFT` | Draft then send | `body`, `subject`, `recipient_email` / `draft_id` |\n| `GMAIL_GET_PROFILE` | Whose mailbox is this? | — |\n| `GMAIL_LIST_LABELS` / `GMAIL_CREATE_LABEL` / `GMAIL_ADD_LABEL_TO_EMAIL` | Labels | `message_id`, `label_ids` (use LIST_LABELS for custom IDs) |\n| `GMAIL_GET_CONTACTS` / `GMAIL_SEARCH_PEOPLE` | Contacts | `query` |\n| `GMAIL_GET_ATTACHMENT` | Download attachment | `message_id`, `attachment_id` |\n\nFull set: 63 tools — list with\n`agent-swarm x composio GET "/tools?toolkit_slug=gmail&limit=100" | jq -r \'.items[]|"\\(.slug)\\t\\(.name)"\'`.\nAvoid the ones marked Deprecated (`GMAIL_LIST_MESSAGES`, `GMAIL_REMOVE_LABEL`).\n\n## Read recipe (metadata-first)\n\n```bash\nagent-swarm x composio POST /tools/execute/GMAIL_FETCH_EMAILS \\\n --body \'{"connected_account_id":"ca_…","arguments":{"max_results":5,"include_payload":false,"verbose":false}}\'\n```\n- **Always set `max_results`** — the default is `1`.\n- Use Gmail `query` syntax: `"is:unread"`, `"from:foo@bar.com newer_than:7d"`,\n `"subject:invoice has:attachment"`.\n- Keep `include_payload:false` + `verbose:false` unless the user needs full bodies\n (token-heavy). Use `ids_only:true` for the cheapest listing.\n\n## Send recipe\n\n```bash\nagent-swarm x composio POST /tools/execute/GMAIL_SEND_EMAIL \\\n --body \'{"connected_account_id":"ca_…","arguments":{\n "recipient_email":"someone@example.com",\n "subject":"Hello",\n "body":"<p>Hi there</p>",\n "is_html":true,\n "cc":["cc@example.com"]\n }}\'\n```\n- Set `is_html:true` when `body` contains HTML, otherwise it sends as literal text.\n- `recipient_email` is the primary; add more via `extra_recipients` / `cc` / `bcc`.\n- **Sending is a write action** — only do it when the task explicitly asks.\n\n## Sender (From) and footer\n\n- A draft defaults to the connected account\'s primary address. Before you create drafts, list the account\'s verified send-as aliases and set From to the alias the requester prefers (requester profile or brief). Take the footer and signature domain from the same source.\n- Never set From to an alias that is not verified. Gmail rewrites it to the primary address.\n- After you create the drafts, check From as well as To and Subject on each one.\n- To fix existing drafts, update each draft in place with the alias. Draft ids stay the same, so there is no need to delete and recreate.\n\n## Reply in a thread\n\n```bash\nagent-swarm x composio POST /tools/execute/GMAIL_REPLY_TO_THREAD \\\n --body \'{"connected_account_id":"ca_…","arguments":{\n "thread_id":"<thread_id>","recipient_email":"someone@example.com","message_body":"thanks!"\n }}\'\n```\n\n## Gotchas\n\n- Default `max_results` is `1` — forgetting it makes "list my emails" return a\n single message.\n- Bodies/attachments are token-heavy and may contain secrets — default to\n metadata; the secret-scrubber doesn\'t run on Composio tool output.\n- If you get `ToolRouterV2_NoActiveConnection`, switch to direct execute with the\n pinned `connected_account_id` (hub Gotchas).\n';
|
|
6914
6953
|
|
|
6915
6954
|
// templates/skills/composio-google-calendar/config.json
|
|
6916
6955
|
var config_default13 = `{
|
|
@@ -6950,7 +6989,7 @@ var config_default14 = `{
|
|
|
6950
6989
|
`;
|
|
6951
6990
|
|
|
6952
6991
|
// templates/skills/composio-google-docs/content.md
|
|
6953
|
-
var content_default14 = '# Composio · Google Docs\n\nToolkit slug: **`googledocs`**. Read the [[composio]] hub first for the call model.\nA document is identified by its `document_id` (the id in the Docs URL).\n\n```bash\nagent-swarm x composio POST /tools/execute/<SLUG> \\\n --body \'{"user_id":"<connected-account-email>","connected_account_id":"<active-connected-account-id>","arguments":{ … }}\'\n```\n\nFollow the hub\'s **Resolve the user and connected account** procedure, then\nselect the resolved user\'s `googledocs` account whose status is `ACTIVE`.\n\n## Headline tools\n\n| Slug | What | Key args |\n|---|---|---|\n| `GOOGLEDOCS_SEARCH_DOCUMENTS` | Find docs (Drive search) | `query`, `max_results` (def 10), `order_by` (def `modifiedTime desc`), `modified_after`, `created_after`, `starred_only`, `shared_with_me`, `response_detail` (def `minimal`) |\n| `GOOGLEDOCS_GET_DOCUMENT_PLAINTEXT` | Read doc as text | **`document_id`**, `include_tables` (def true), `include_headers`, `include_footers`, `include_footnotes`, `include_tabs_content` |\n| `GOOGLEDOCS_GET_DOCUMENT_BY_ID` | Full structured doc JSON | `document_id` |\n| `GOOGLEDOCS_CREATE_DOCUMENT` | Create blank/with text | `title`, `text` |\n| `GOOGLEDOCS_CREATE_DOCUMENT_MARKDOWN` | Create from markdown | **`title`**, `markdown_text`, `image_assets` |\n| `GOOGLEDOCS_UPDATE_DOCUMENT_MARKDOWN` | Replace body with markdown | `document_id`, `markdown_text` |\n| `GOOGLEDOCS_INSERT_TEXT_ACTION` | Insert text at index | `document_id`, `text`, `index` |\n| `GOOGLEDOCS_REPLACE_ALL_TEXT` | Find & replace | `document_id`, `find`, `replace` |\n| `GOOGLEDOCS_COPY_DOCUMENT` | Duplicate a doc | `document_id`, `title` |\n| `GOOGLEDOCS_EXPORT_DOCUMENT_AS_PDF` | Export to PDF | `document_id` |\n\nFull set: 35 tools — `agent-swarm x composio GET "/tools?toolkit_slug=googledocs&limit=100" | jq -r \'.items[]|"\\(.slug)\\t\\(.name)"\'`.\nPrefer the `*_MARKDOWN` create/update tools for authoring; the granular\n`INSERT_*`/`DELETE_*`/table tools are for surgical structural edits.\n\n## Search recipe\n\n```bash\nagent-swarm x composio POST /tools/execute/GOOGLEDOCS_SEARCH_DOCUMENTS \\\n --body \'{"connected_account_id":"ca_…","arguments":{"query":"workshop","max_results":5}}\'\n# → results under .data.files[] (id, name, modifiedTime …)\n```\n- Results are Drive file entries at **`.data.files[]`** — grab `.id` to read.\n- `order_by` defaults to `modifiedTime desc` (most recent first).\n- Use `modified_after` / `shared_with_me` to narrow.\n\n## Read recipe\n\n```bash\nagent-swarm x composio POST /tools/execute/GOOGLEDOCS_GET_DOCUMENT_PLAINTEXT \\\n --body \'{"connected_account_id":"ca_…","arguments":{"document_id":"<id>","include_tables":true}}\'\n```\nUse `GET_DOCUMENT_PLAINTEXT` for reading content; only reach for\n`GET_DOCUMENT_BY_ID` when you need the structured JSON (styles, indices) for an\nedit.\n\n## Create-from-markdown recipe\n\n```bash\nagent-swarm x composio POST /tools/execute/GOOGLEDOCS_CREATE_DOCUMENT_MARKDOWN \\\n --body \'{"connected_account_id":"ca_…","arguments":{\n "title":"Weekly updates","markdown_text":"# Heading\\n\\n- point one\\n- point two"\n }}\'\n```\n\n## Google Doc to agent-fs\n\n1. Read the source with `GOOGLEDOCS_GET_DOCUMENT_PLAINTEXT` and retain its\n returned title and display URL as provenance.\n2. Write the converted content to your own agent-fs namespace with a short\n provenance header naming the source document and import method.\n3. Resolve the destination org and drive from the environment or\n `agent-fs stat <path> --json`; the `
|
|
6992
|
+
var content_default14 = '# Composio · Google Docs\n\nToolkit slug: **`googledocs`**. Read the [[composio]] hub first for the call model.\nA document is identified by its `document_id` (the id in the Docs URL).\n\n```bash\nagent-swarm x composio POST /tools/execute/<SLUG> \\\n --body \'{"user_id":"<connected-account-email>","connected_account_id":"<active-connected-account-id>","arguments":{ … }}\'\n```\n\nFollow the hub\'s **Resolve the user and connected account** procedure, then\nselect the resolved user\'s `googledocs` account whose status is `ACTIVE`.\n\n## Headline tools\n\n| Slug | What | Key args |\n|---|---|---|\n| `GOOGLEDOCS_SEARCH_DOCUMENTS` | Find docs (Drive search) | `query`, `max_results` (def 10), `order_by` (def `modifiedTime desc`), `modified_after`, `created_after`, `starred_only`, `shared_with_me`, `response_detail` (def `minimal`) |\n| `GOOGLEDOCS_GET_DOCUMENT_PLAINTEXT` | Read doc as text | **`document_id`**, `include_tables` (def true), `include_headers`, `include_footers`, `include_footnotes`, `include_tabs_content` |\n| `GOOGLEDOCS_GET_DOCUMENT_BY_ID` | Full structured doc JSON | `document_id` |\n| `GOOGLEDOCS_CREATE_DOCUMENT` | Create blank/with text | `title`, `text` |\n| `GOOGLEDOCS_CREATE_DOCUMENT_MARKDOWN` | Create from markdown | **`title`**, `markdown_text`, `image_assets` |\n| `GOOGLEDOCS_UPDATE_DOCUMENT_MARKDOWN` | Replace body with markdown | `document_id`, `markdown_text` |\n| `GOOGLEDOCS_INSERT_TEXT_ACTION` | Insert text at index | `document_id`, `text`, `index` |\n| `GOOGLEDOCS_REPLACE_ALL_TEXT` | Find & replace | `document_id`, `find`, `replace` |\n| `GOOGLEDOCS_COPY_DOCUMENT` | Duplicate a doc | `document_id`, `title` |\n| `GOOGLEDOCS_EXPORT_DOCUMENT_AS_PDF` | Export to PDF | `document_id` |\n\nFull set: 35 tools — `agent-swarm x composio GET "/tools?toolkit_slug=googledocs&limit=100" | jq -r \'.items[]|"\\(.slug)\\t\\(.name)"\'`.\nPrefer the `*_MARKDOWN` create/update tools for authoring; the granular\n`INSERT_*`/`DELETE_*`/table tools are for surgical structural edits.\n\n## Search recipe\n\n```bash\nagent-swarm x composio POST /tools/execute/GOOGLEDOCS_SEARCH_DOCUMENTS \\\n --body \'{"connected_account_id":"ca_…","arguments":{"query":"workshop","max_results":5}}\'\n# → results under .data.files[] (id, name, modifiedTime …)\n```\n- Results are Drive file entries at **`.data.files[]`** — grab `.id` to read.\n- `order_by` defaults to `modifiedTime desc` (most recent first).\n- Use `modified_after` / `shared_with_me` to narrow.\n\n## Read recipe\n\n```bash\nagent-swarm x composio POST /tools/execute/GOOGLEDOCS_GET_DOCUMENT_PLAINTEXT \\\n --body \'{"connected_account_id":"ca_…","arguments":{"document_id":"<id>","include_tables":true}}\'\n```\nUse `GET_DOCUMENT_PLAINTEXT` for reading content; only reach for\n`GET_DOCUMENT_BY_ID` when you need the structured JSON (styles, indices) for an\nedit.\n\n## Create-from-markdown recipe\n\n```bash\nagent-swarm x composio POST /tools/execute/GOOGLEDOCS_CREATE_DOCUMENT_MARKDOWN \\\n --body \'{"connected_account_id":"ca_…","arguments":{\n "title":"Weekly updates","markdown_text":"# Heading\\n\\n- point one\\n- point two"\n }}\'\n```\n\n## Google Doc to agent-fs\n\n1. Read the source with `GOOGLEDOCS_GET_DOCUMENT_PLAINTEXT` and retain its\n returned title and display URL as provenance.\n2. Write the converted content to your own agent-fs namespace with a short\n provenance header naming the source document and import method.\n3. Resolve the destination org and drive from the environment or\n `agent-fs stat <path> --json`; the `agent-fs` skill documents the canonical\n storage and share-link workflow. Never copy org or drive IDs from an example.\n4. Read the stored file back and assert several markers: the title, a middle\n phrase, and the final sentence. A successful byte-count response alone does\n not prove fidelity.\n\nPlaintext is intentionally lossy. Inline images may become bare `[IMAGE]`\nmarkers, and headings may be flattened into adjacent paragraphs. If those\nfeatures matter, use `GOOGLEDOCS_GET_DOCUMENT_BY_ID` for structured JSON or a\nGoogle Drive export, then preserve or reconstruct the structure explicitly.\nThere is no `GET_DOCUMENT_MARKDOWN` tool; markdown exists on create/update, not\non read.\n\n## Gotchas\n\n- Search returns Drive metadata, not document content — do a second\n `GET_DOCUMENT_PLAINTEXT` call with the `.id` to read.\n- Doc bodies can be long/token-heavy and may contain secrets — read only what you\n need (`include_headers/footers/footnotes` default off for a reason).\n- Create/update/replace are **write actions** — only on explicit request.\n';
|
|
6954
6993
|
|
|
6955
6994
|
// templates/skills/db-query-guidance/config.json
|
|
6956
6995
|
var config_default15 = `{
|
|
@@ -7143,8 +7182,159 @@ var config_default18 = `{
|
|
|
7143
7182
|
// templates/skills/download-task-attachment/content.md
|
|
7144
7183
|
var content_default18 = '# Download a Task Attachment\n\nTask attachments (files uploaded via the UI or API onto a task) are served\nthrough a single **provider-agnostic** REST route — regardless of whether the\nbacking storage is `local-fs` or `agent-fs`. You do not need to know or care\nwhich provider is active, and you do not need `agent-fs org`/`agent-fs drive`\ndiscovery.\n\n## The one-call recipe\n\n```bash\ncurl "$MCP_BASE_URL/api/fs/tasks/$AGENT_SWARM_TASK_ID/files/<attachmentId>/raw" \\\n -H "X-Agent-ID: $AGENT_ID" \\\n -H "Authorization: Bearer ${AGENT_SWARM_API_KEY:-$API_KEY}" \\\n --create-dirs -o "/tmp/attachments/<attachmentId>/<name>"\n```\n\n- `$MCP_BASE_URL`, `$AGENT_ID`, `$AGENT_SWARM_TASK_ID` (or `$AGENT_SWARM_AGENT_ID`/`TASK_FILE`-derived taskId), and `$AGENT_SWARM_API_KEY`/`$API_KEY` are already present in every worker container\'s env — no new plumbing needed.\n- `<attachmentId>` and `<name>`/`mimeType` come from the task\'s attachment list (see below).\n- One directory per attachment: two attachments can share a name (every pasted screenshot is `image.png`), and `/tmp/<name>` would let one overwrite the other.\n- The route resolves the active file-storage provider (`local-fs` in dev, `agent-fs` in prod) server-side and streams the raw bytes back with the correct `Content-Type` — you get one call no matter which provider is behind it.\n\n## Finding the attachment ID\n\nIf the dispatch prompt already injected an `## Attachments` section with the\ncurl command pre-filled, just run it — that\'s the fast path (see\n`buildAttachmentsSection()` in `src/commands/runner.ts`).\n\nOtherwise, pull it from the task:\n\n```\nget-task-details taskId=<your task id>\n```\n\nThe response includes an `attachments` array: `{id, name, mimeType, sizeBytes}`.\nUse `id` as `<attachmentId>` in the curl above.\n\n## Why not the `agent-fs` CLI directly?\n\n`agent-fs` is a general-purpose CLI for the swarm\'s own agent-fs filesystem —\nit requires you to already know the org/drive a file lives in, and task\nattachments don\'t always carry that (older rows, or attachments stored via\n`local-fs` in dev have no org/drive at all). Reaching for `agent-fs cat` /\n`agent-fs download` on a task attachment means guessing subcommand names,\ndiscovering the right org via `agent-fs org list`, and re-trying — several\ntool calls where one `curl` suffices. This was root-caused from a real\nsession that burned 7 tool calls (~64s) doing exactly that detour before\nsucceeding; see `runbooks/harness-providers.md` and PR that added\n`buildAttachmentsSection()` for the full trace.\n\n## Gotchas\n\n- The route requires `X-Agent-ID` + bearer auth like any other swarm API call — same headers you\'d use for any MCP-adjacent REST call.\n- `mimeType` on the attachment record reflects the real upload `Content-Type` (not a filename-extension guess) — trust it when deciding how to handle the downloaded bytes (image vs PDF vs text).\n- If you get a 404, double-check you\'re using the **attachment ID** (from `attachments[].id`), not the display `name`.\n';
|
|
7145
7184
|
|
|
7146
|
-
// templates/skills/
|
|
7185
|
+
// templates/skills/email-domain-health/config.json
|
|
7147
7186
|
var config_default19 = `{
|
|
7187
|
+
"kind": "skill",
|
|
7188
|
+
"name": "email-domain-health",
|
|
7189
|
+
"displayName": "Email Domain Health",
|
|
7190
|
+
"slug": "email-domain-health",
|
|
7191
|
+
"title": "Email Domain Health",
|
|
7192
|
+
"description": "Build a scheduled email domain health monitor: ask the user for domains, providers, test inbox, schedule and alert rules, then build a checks script (SPF, DKIM, DMARC, MX, provider records, blocklists with positive controls), a daily test send with score capture, bounce rate, run history in a Swarm App, and alerts only on a new red or a score drop. Orchestrates the swarm-scripts, apps, scheduling and composio-gmail skills.",
|
|
7193
|
+
"version": "1.0.0",
|
|
7194
|
+
"category": "skills",
|
|
7195
|
+
"placeholders": [],
|
|
7196
|
+
"runAllSeedersCandidate": true,
|
|
7197
|
+
"systemDefault": true,
|
|
7198
|
+
"tags": ["email", "deliverability", "dns", "monitoring", "apps"]
|
|
7199
|
+
}
|
|
7200
|
+
`;
|
|
7201
|
+
|
|
7202
|
+
// templates/skills/email-domain-health/content.md
|
|
7203
|
+
var content_default19 = `# Email Domain Health
|
|
7204
|
+
|
|
7205
|
+
Build a monitor that tells a person, every working day, whether mail from their
|
|
7206
|
+
domains authenticates, lands, and stays off blocklists. It has four parts: one
|
|
7207
|
+
saved script (checks + test send), a Swarm App with run history, one or two
|
|
7208
|
+
schedules, and an alert that fires only when something gets worse.
|
|
7209
|
+
|
|
7210
|
+
This skill decides **what** to build. The mechanics live in other skills; read
|
|
7211
|
+
them, do not re-derive them:
|
|
7212
|
+
|
|
7213
|
+
- [[swarm-scripts]]: authoring, \`script-upsert\`, scope, connections, secrets.
|
|
7214
|
+
- [[apps]]: models, named queries, pages, row writes.
|
|
7215
|
+
- [[scheduling]]: \`targetType: "script"\` schedules, cron and timezone.
|
|
7216
|
+
- [[composio-gmail]] (or the user's own mail integration): the test send and
|
|
7217
|
+
the bounce search.
|
|
7218
|
+
|
|
7219
|
+
Full design notes and pitfalls: the "Email domain health" playbook on the docs
|
|
7220
|
+
site.
|
|
7221
|
+
|
|
7222
|
+
## 1. Ask for requirements first
|
|
7223
|
+
|
|
7224
|
+
Ask before building (one batched question, see [[ask-user]]). Do not guess any
|
|
7225
|
+
of these; a wrong selector or From address makes every later check wrong.
|
|
7226
|
+
|
|
7227
|
+
| Ask | Why it matters |
|
|
7228
|
+
|---|---|
|
|
7229
|
+
| Domains to monitor, and which one is the envelope/bounce domain | One check set per domain; SPF is judged on the envelope domain. |
|
|
7230
|
+
| Sending providers per domain (Google Workspace, Microsoft 365, an ESP such as Resend, SES, Postmark) and each DKIM selector | DKIM is looked up at \`<selector>._domainkey.<domain>\`; there is no way to list selectors. |
|
|
7231
|
+
| The From address for the test send, and the integration that sends it (Composio Gmail, SMTP, ESP API) | The test must leave through the real path, alias included. |
|
|
7232
|
+
| Test inbox: mail-tester (free tier has a small daily cap) or an inbox the user owns | Decides how the score is captured. |
|
|
7233
|
+
| Schedule and timezone | Default: weekdays, morning, user's timezone. |
|
|
7234
|
+
| Alert channel and thresholds (score floor, bounce rate) | Defaults: score < 6/10, bounces >= 5% on >= 10 sends. |
|
|
7235
|
+
| History retention | Default 90 days of runs. |
|
|
7236
|
+
|
|
7237
|
+
Read the answers back in one short message before you write code.
|
|
7238
|
+
|
|
7239
|
+
## 2. Build the checks script
|
|
7240
|
+
|
|
7241
|
+
One saved script with modes \`send\`, \`check\`, and optionally \`backfill\`. Keep
|
|
7242
|
+
domains, selectors, thresholds and ids in a config block at the top or in
|
|
7243
|
+
\`args\`, never scattered through the code.
|
|
7244
|
+
|
|
7245
|
+
**DNS over HTTPS, two resolvers.** Query every record on two public DoH
|
|
7246
|
+
resolvers (for example dns.google and Cloudflare's JSON endpoint).
|
|
7247
|
+
NOERROR and NXDOMAIN are answers; SERVFAIL, REFUSED and timeouts are not.
|
|
7248
|
+
Same verdict on both: use it. Different verdicts: \`warn\` ("resolvers
|
|
7249
|
+
disagree", often propagation). One failed: use the other, note it. Both
|
|
7250
|
+
failed: \`unknown\`.
|
|
7251
|
+
|
|
7252
|
+
**Checks per domain:**
|
|
7253
|
+
|
|
7254
|
+
- SPF: exactly one \`v=spf1\` record, includes each provider, total DNS lookups
|
|
7255
|
+
<= 10 (recurse \`include:\`/\`redirect=\`; warn at 9), ends \`~all\` or \`-all\`.
|
|
7256
|
+
- DKIM: TXT at each selector (follow a CNAME), non-empty \`p=\` (empty means
|
|
7257
|
+
revoked), key length.
|
|
7258
|
+
- DMARC: exactly one \`v=DMARC1\` at \`_dmarc.<domain>\`, a \`p=\` tag, \`rua=\` (warn
|
|
7259
|
+
without it).
|
|
7260
|
+
- MX: present and pointing at the expected provider.
|
|
7261
|
+
- Provider records the ESP documents (bounce subdomain SPF and MX, DKIM
|
|
7262
|
+
CNAMEs).
|
|
7263
|
+
- Blocklists: domain lists such as Spamhaus DBL, SURBL, URIBL (details below).
|
|
7264
|
+
- Test-send result: score plus the SPF, DKIM and DMARC verdicts from the
|
|
7265
|
+
receiver's headers.
|
|
7266
|
+
- Bounce rate: sent vs bounced in the last 24h from the sending mailbox,
|
|
7267
|
+
excluding test sends.
|
|
7268
|
+
|
|
7269
|
+
**Verdicts:** \`ok\`, \`warn\`, \`red\`, \`unknown\`. Overall = worst. \`unknown\` means
|
|
7270
|
+
"could not read", and it is **never** reported as clean.
|
|
7271
|
+
|
|
7272
|
+
**Controls, not hope.** Every negative result needs a positive control in the
|
|
7273
|
+
same run:
|
|
7274
|
+
|
|
7275
|
+
- Blocklists: before trusting a list, query its documented test entry
|
|
7276
|
+
(\`dbltest.com\` on \`dbl.spamhaus.org\`, \`test.surbl.org\` on \`multi.surbl.org\`,
|
|
7277
|
+
\`test.uribl.com\` on \`multi.uribl.com\`) on each resolver. Use the list only on
|
|
7278
|
+
a resolver where the test entry answers "listed". Refusal codes
|
|
7279
|
+
(\`127.255.255.x\` from Spamhaus, \`127.0.0.1\` from SURBL and URIBL) mean the
|
|
7280
|
+
resolver is blocked: \`unknown\`.
|
|
7281
|
+
- Bounce rate: a whole-mailbox 24h count of 0 means the search failed:
|
|
7282
|
+
\`unknown\`, not "0 bounces".
|
|
7283
|
+
- Test result page without a score yet: \`unknown\` for the first hour after the
|
|
7284
|
+
send, then \`red\`.
|
|
7285
|
+
|
|
7286
|
+
Return a \`controls.ok\` flag with the result so a run that read nothing cannot
|
|
7287
|
+
pass as green.
|
|
7288
|
+
|
|
7289
|
+
**Send mode.** Send one realistic message (the same template and links real
|
|
7290
|
+
mail uses) to a fresh test address, through the user's integration. Read the
|
|
7291
|
+
sent message back and confirm the From header kept the address the user asked
|
|
7292
|
+
for. Store the pending test id in KV with a per-day key so a retry never
|
|
7293
|
+
sends twice.
|
|
7294
|
+
|
|
7295
|
+
## 3. Build the app
|
|
7296
|
+
|
|
7297
|
+
Use [[apps]]. Minimum models:
|
|
7298
|
+
|
|
7299
|
+
- \`run\`: one row per run (time, overall, red/warn/unknown counts, score,
|
|
7300
|
+
bounces, sent).
|
|
7301
|
+
- \`check\`: one row per check per run (history).
|
|
7302
|
+
- \`current\`: one row per \`domain|check\`, updated in place, with \`since\` = when
|
|
7303
|
+
the status last changed.
|
|
7304
|
+
|
|
7305
|
+
Optional per-domain views (\`domainRun\`, \`domainCheck\`) make tabs per domain
|
|
7306
|
+
cheap to render. Page: one tab per domain (problems first, raw record on
|
|
7307
|
+
hover), a score trend, and a collapsed "How it works". The script writes rows
|
|
7308
|
+
through the row endpoints; prune \`run\` and \`check\` rows older than the
|
|
7309
|
+
retention window.
|
|
7310
|
+
|
|
7311
|
+
## 4. Schedule it
|
|
7312
|
+
|
|
7313
|
+
Use [[scheduling]] with \`targetType: "script"\`, not an agent task that says
|
|
7314
|
+
"run the script". Two schedules: \`send\`, then \`check\` 10 to 15 minutes later
|
|
7315
|
+
in the user's timezone, so the test inbox has the message before the check
|
|
7316
|
+
reads it.
|
|
7317
|
+
|
|
7318
|
+
## 5. Alert only on change
|
|
7319
|
+
|
|
7320
|
+
Compare with the previous \`current\` rows. Alert when a check **turns** red, or
|
|
7321
|
+
the score drops below the floor after being above it. Never alert on a
|
|
7322
|
+
steady-state warn or an unknown. The message lists each new red with its raw
|
|
7323
|
+
detail and links the app. If the script's identity cannot post to the chosen
|
|
7324
|
+
channel, have it hand the message to an agent that can.
|
|
7325
|
+
|
|
7326
|
+
## Done when
|
|
7327
|
+
|
|
7328
|
+
- A manual \`check\` run returns every check, \`controls.ok: true\`, and writes
|
|
7329
|
+
rows the app renders.
|
|
7330
|
+
- Every blocklist shows either a passing control or \`unknown\`, never a bare
|
|
7331
|
+
"not listed".
|
|
7332
|
+
- One full scheduled send + check cycle has run and the score landed in \`run\`.
|
|
7333
|
+
- You reported the app link, script name and scope, and schedule ids.
|
|
7334
|
+
`;
|
|
7335
|
+
|
|
7336
|
+
// templates/skills/engineering-standards/config.json
|
|
7337
|
+
var config_default20 = `{
|
|
7148
7338
|
"category": "skills",
|
|
7149
7339
|
"description": "Code-shape policy for ALL code that gets written or reviewed — the senior-engineer bar for minimal complexity. Use whenever you are about to write code, design a module or abstraction, draft a plan phase that shapes code, or review a diff; also when the user asks \\"is this over-engineered\\", \\"can this be simpler\\", or pushes back on complexity. Sibling of desplega:delegate-work (which picks WHO executes; this skill defines WHAT good output looks like).",
|
|
7150
7340
|
"displayName": "Engineering Standards",
|
|
@@ -7165,7 +7355,7 @@ var config_default19 = `{
|
|
|
7165
7355
|
`;
|
|
7166
7356
|
|
|
7167
7357
|
// templates/skills/engineering-standards/content.md
|
|
7168
|
-
var
|
|
7358
|
+
var content_default20 = `# engineering-standards
|
|
7169
7359
|
|
|
7170
7360
|
The goal is always the same: **solve the problem with the least code and the least complexity that actually solves it.** Not the cleverest solution, not the most extensible one — the one a senior engineer would defend in review. Every rule below is phrased as a *checkable test*, not a slogan: if you can't run the test, the rule doesn't apply; if the test fails, change the code.
|
|
7171
7361
|
|
|
@@ -7236,7 +7426,7 @@ runbooks/<name>/<slug>.md # …or grouped by area when they multiply
|
|
|
7236
7426
|
`;
|
|
7237
7427
|
|
|
7238
7428
|
// templates/skills/heartbeat-runbook/config.json
|
|
7239
|
-
var
|
|
7429
|
+
var config_default21 = `{
|
|
7240
7430
|
"kind": "skill",
|
|
7241
7431
|
"name": "heartbeat-runbook",
|
|
7242
7432
|
"displayName": "Heartbeat Runbook",
|
|
@@ -7253,7 +7443,7 @@ var config_default20 = `{
|
|
|
7253
7443
|
`;
|
|
7254
7444
|
|
|
7255
7445
|
// templates/skills/heartbeat-runbook/content.md
|
|
7256
|
-
var
|
|
7446
|
+
var content_default21 = `# Heartbeat runbook
|
|
7257
7447
|
|
|
7258
7448
|
Lead only. The heartbeat runbook is your \`heartbeatMd\` profile field. The server reads it every 30 minutes.
|
|
7259
7449
|
|
|
@@ -7320,7 +7510,7 @@ Two kinds of sections:
|
|
|
7320
7510
|
`;
|
|
7321
7511
|
|
|
7322
7512
|
// templates/skills/implementing/config.json
|
|
7323
|
-
var
|
|
7513
|
+
var config_default22 = `{
|
|
7324
7514
|
"category": "skills",
|
|
7325
7515
|
"description": "Plan implementation skill. Executes approved technical plans phase by phase with verification checkpoints.",
|
|
7326
7516
|
"displayName": "Implementing",
|
|
@@ -7341,7 +7531,7 @@ var config_default21 = `{
|
|
|
7341
7531
|
`;
|
|
7342
7532
|
|
|
7343
7533
|
// templates/skills/implementing/content.md
|
|
7344
|
-
var
|
|
7534
|
+
var content_default22 = `# Implementing
|
|
7345
7535
|
|
|
7346
7536
|
You are implementing an approved technical plan, executing it phase by phase with verification at each step. Each phase is executed as a background sub-agent via \`desplega:phase-running\` — the main session acts as an orchestrator.
|
|
7347
7537
|
|
|
@@ -7534,7 +7724,7 @@ File-review is on by default (unless Autopilot):
|
|
|
7534
7724
|
`;
|
|
7535
7725
|
|
|
7536
7726
|
// templates/skills/improve-agents-md/config.json
|
|
7537
|
-
var
|
|
7727
|
+
var config_default23 = `{
|
|
7538
7728
|
"category": "skills",
|
|
7539
7729
|
"description": "Improve (or bootstrap) an AGENTS.md / CLAUDE.md file using \`<important if>\` conditional blocks so the agent actually attends to the right guidance at the right time. Use this skill whenever the user mentions AGENTS.md, CLAUDE.md, agent instructions, project rules for AI, \\"my claude config\\", onboarding docs for agents, or asks to tighten / shorten / audit / rewrite an existing one — even if they don't explicitly say the filename. Also use when the user complains that an agent keeps ignoring their project rules.",
|
|
7540
7730
|
"displayName": "Improve AGENTS.md",
|
|
@@ -7555,7 +7745,7 @@ var config_default22 = `{
|
|
|
7555
7745
|
`;
|
|
7556
7746
|
|
|
7557
7747
|
// templates/skills/improve-agents-md/content.md
|
|
7558
|
-
var
|
|
7748
|
+
var content_default23 = `# improve-agents-md
|
|
7559
7749
|
|
|
7560
7750
|
Progressively improve (or bootstrap from scratch) a project's agent-instruction file. Works on both \`AGENTS.md\` (vendor-neutral convention from OpenAI Codex, also read by Cursor, Claude Code, and others) and \`CLAUDE.md\` (Claude-specific). Keeps one canonical file on disk and symlinks the other, so every agent reads the same source of truth.
|
|
7561
7751
|
|
|
@@ -7975,7 +8165,7 @@ Run with \`turbo\` from the repo root.
|
|
|
7975
8165
|
`;
|
|
7976
8166
|
|
|
7977
8167
|
// templates/skills/kv-storage/config.json
|
|
7978
|
-
var
|
|
8168
|
+
var config_default24 = `{
|
|
7979
8169
|
"kind": "skill",
|
|
7980
8170
|
"name": "kv-storage",
|
|
7981
8171
|
"displayName": "KV Storage",
|
|
@@ -7992,7 +8182,7 @@ var config_default23 = `{
|
|
|
7992
8182
|
`;
|
|
7993
8183
|
|
|
7994
8184
|
// templates/skills/kv-storage/content.md
|
|
7995
|
-
var
|
|
8185
|
+
var content_default24 = `# KV Storage
|
|
7996
8186
|
|
|
7997
8187
|
Namespaced key/value store inside the swarm SQLite DB. Auto-scoped to your
|
|
7998
8188
|
calling context — same string used by \`agent_tasks.contextKey\`.
|
|
@@ -8281,15 +8471,15 @@ value so you can detect a clobber.
|
|
|
8281
8471
|
## See also
|
|
8282
8472
|
|
|
8283
8473
|
- The **\`pages\`** skill — companion skill for authed pages and \`swarmSdk.kv\`.
|
|
8284
|
-
- The **\`
|
|
8474
|
+
- The **\`agent-fs\`** skill — for anything too big or too binary for KV.
|
|
8285
8475
|
- \`src/be/migrations/061_kv_store.sql\` — schema (\`kv_entries\`)
|
|
8286
8476
|
- \`src/http/kv.ts\` — REST handler + namespace resolution
|
|
8287
8477
|
- \`src/tools/kv/*\` — MCP tool registrars
|
|
8288
|
-
- \`src/
|
|
8478
|
+
- \`src/pages/browser-sdk.ts\` — \`swarmSdk.kv\` for pages
|
|
8289
8479
|
`;
|
|
8290
8480
|
|
|
8291
8481
|
// templates/skills/learning/config.json
|
|
8292
|
-
var
|
|
8482
|
+
var config_default25 = `{
|
|
8293
8483
|
"category": "skills",
|
|
8294
8484
|
"description": "Compounding knowledge across projects and teams. Captures, searches, and promotes institutional learnings via tiered backends (local/qmd/agent-fs).",
|
|
8295
8485
|
"displayName": "Learning",
|
|
@@ -8310,7 +8500,7 @@ var config_default24 = `{
|
|
|
8310
8500
|
`;
|
|
8311
8501
|
|
|
8312
8502
|
// templates/skills/learning/content.md
|
|
8313
|
-
var
|
|
8503
|
+
var content_default25 = `# Learning
|
|
8314
8504
|
|
|
8315
8505
|
You are managing institutional knowledge — capturing insights, searching prior learnings, and promoting important patterns into CLAUDE.md for permanent reference.
|
|
8316
8506
|
|
|
@@ -8578,13 +8768,13 @@ The config file lives at \`~/.agentic-learnings.json\`:
|
|
|
8578
8768
|
`;
|
|
8579
8769
|
|
|
8580
8770
|
// templates/skills/memory/config.json
|
|
8581
|
-
var
|
|
8771
|
+
var config_default26 = '{\n "kind": "skill",\n "name": "memory",\n "displayName": "Memory",\n "slug": "memory",\n "title": "Memory",\n "description": "Remember, recall, learning, memory: how swarm memory works and how to store, search, edit, rate, and delete memories with `memory-store`, `memory-search`, `memory-get`, `memory-edit`, `memory_rate`, and `memory-delete`, plus logical paths (`key`, `newKey`, `keyPrefix`, the `/longterm` roots and the lead-only ones) and lead promotion with `inject-learning`. Use before you store, edit, or delete a memory.",\n "version": "1.1.0",\n "category": "skills",\n "placeholders": [],\n "runAllSeedersCandidate": true,\n "systemDefault": true,\n "tags": ["memory", "recall", "learning"]\n}\n';
|
|
8582
8772
|
|
|
8583
8773
|
// templates/skills/memory/content.md
|
|
8584
|
-
var
|
|
8774
|
+
var content_default26 = '# Memory\n\nSwarm memory is a store of short texts with embeddings, searchable by meaning and by keyword. Recall is automatic: the runner puts the best matches for your task in the task message under "Relevant Past Knowledge". Everything else is a tool call.\n\n## What is stored without you\n\n| Source | When | Scope |\n|---|---|---|\n| `task_completion` | `store-progress` with status `completed` or `failed` | `agent`, or `swarm` for research tasks and tasks tagged `knowledge` or `shared` |\n| `session_summary` | your session ends | `agent` |\n| `file_index` | a file written under `/workspace/personal/memory/` or `/workspace/shared/memory/<agentId>/` on a harness with the file hook (claude, pi, opencode) | by path |\n\nAutomatic tasks (schedules, heartbeat, monitors) skip the `task_completion` write unless `store-progress` gets `persistMemory: true`.\n\nPrefer `memory-store` over memory files. It works on every harness, including the remote ones.\n\n## Tools\n\n| Tool | Use |\n|---|---|\n| `memory-store` | create a memory: `content`, `name`, `scope`, optional `tags`, `taskId`, `intent`, `key` (a logical path, see Logical paths) |\n| `memory-search` | find memories: `query`, `scope` (`all`, `agent`, `swarm`), `limit`, optional `intent` (why you search), `source`, `keyPrefix` (only memories whose key starts with it) |\n| `memory-get` | the full content of one memory by ID |\n| `memory-edit` | change a memory in place: mode `replace` (whole content) or `exact` (one unique substring), `intent` required. `newKey` alone moves it to another logical path |\n| `memory-delete` | remove a memory by ID |\n| `memory_rate` | mark a memory you used in this task as useful or misleading |\n| `inject-learning` | lead only: push a learning into a worker\'s memory at swarm scope |\n\nSeed scripts (`script-run` with `name` and `args`):\n\n- `task-context-gathering` `{ taskId, queries: [...] }`: the task plus a deduplicated multi-query recall in one call.\n- `smart-recall` `{ queries: [...] }`: multi-query recall without the task.\n- `memory-dedup-check` `{ text, threshold? }`: near-duplicates of a candidate memory, default threshold 0.85.\n\n## What makes a good memory\n\n- One fact per memory: a fix, a pattern, a gotcha, a preference of a person, a fact about a repo or a host.\n- The context it applies to: repo, host, tool, version.\n- The evidence: what you saw, where.\n- A searchable `name`: "Linear API rejects issue updates without teamId", not "notes".\n- Under 2,000 characters stores as one chunk. Longer content splits on headings, one memory per chunk.\n\nSkip what a tool returns on demand (paths, tool lists, task status), what the repo already records (README, CLAUDE.md, git history), and what only mattered for this one task.\n\nA memory must not contain a token, password, key, or connection string. Remove the value and keep the reference ("the Linear token lives in config key LINEAR_API_KEY").\n\n## Scope\n\n- `agent` (default): only you recall it. Your own setup, your working notes, your mistakes.\n- `swarm`: every agent recalls it. Facts about shared repos, hosts, people, and processes. Choose `swarm` when a second agent would hit the same thing.\n\n## Logical paths (`key`)\n\nEvery memory has a `key`. By default it is an auto key (`<scope>/<source>/<id>`, or the file path for `file_index`), and the memory is **inbox material**: it keeps the lifecycle of its `source` and expires. Give it a path under `/longterm` and it becomes curated memory: findable by folder, ranked higher, and kept.\n\n`name` is a title. `key` is the address. Use a key only for what should outlive the task: a durable fact, a decision, a person or customer. Leave working notes and task results without one; expiring is the right outcome for them.\n\n### The `/longterm` roots\n\nThe list is closed. A key under `/longterm` whose second segment is not in it is refused, and the error lists the roots.\n\n| Root | Holds | Rank weight | Who may write |\n|---|---|---|---|\n| `/longterm/company-story` | what the company is and does | 1.4 | lead only |\n| `/longterm/entities/people/<slug>`, `/longterm/entities/customers/<slug>` | one doc per person or customer | 1.3 | lead only |\n| `/longterm/facts/<topic>/<slug>` | a durable fact about a repo, host, tool, or process | 1.2 | any agent |\n| `/longterm/decisions/<slug>` | a decision and why | 1.1 (0.3 with the tag `superseded`) | any agent |\n| `/longterm/workstreams/<slug>` | an ongoing effort | 1.0 | any agent |\n| `/longterm/timeline/<period>/<slug>` | dated events | 0.8 | lead only |\n\nAgents and repos are not entity types: agent profiles and `get-repos` own them, so `/longterm/entities/agents/...` is refused.\n\nKey shape: lowercase letters, digits, `-`, and `.` or `_` after the first segment; segments joined by `/`; starts with `/`; no trailing `/`; at most 200 characters. A key outside `/longterm` is not root-checked but must still have this shape.\n\n### Write, move, find\n\n```\nmemory-store key="/longterm/facts/swarm-runtime/sqlite-busy-retry" scope="swarm"\n name="SQLite BUSY_SNAPSHOT needs BEGIN IMMEDIATE" content="..."\n\nmemory-edit memoryId="<id>" newKey="/longterm/decisions/sqlite-busy-retry" intent="promote the fact to a decision"\n\nmemory-search query="busy retry" keyPrefix="/longterm/facts/swarm-runtime/"\n```\n\n- `memory-store`: one key per owner and scope. A second memory under a key you already use fails: `memory-edit` that one. If you pass no `key` and the `name` starts with `/longterm/`, the name is the key, so you set one field. An invalid name of that form is refused, never stored under an auto key.\n- `memory-edit` `newKey`: pass it alone for a pure move. Every chunk of the document moves in one step. The ID, usefulness rating, access counts, and author stay. A key that is already taken is refused. Address the memory with `memoryId`, or with `key` plus `scope`.\n- `memory-search` `keyPrefix`: a literal, case-sensitive prefix, applied before the top results are cut, not after. End a folder with `/` (`/longterm/entities/`, not `/longterm/entities`) to stay inside it.\n\n### What a `/longterm` key changes\n\n`source` stays provenance: there is no `longterm` source. The key is the tier. A memory under `/longterm` follows a `manual` memory\'s lifecycle whatever its `source`:\n\n- It never expires. Storing under a `/longterm` key sets no expiry. Moving a memory in with `newKey` clears the expiry it had, on every chunk. Moving it back out does not restore one.\n- It has no recency decay, and its source-quality multiplier is 1.5.\n- Automated cleanup skips it.\n\nA memory under any other key keeps its `source` lifecycle: `task_completion` expires after 7 days, `session_summary` after 3, `file_index` after 30, `manual` never.\n\nRanking multiplies similarity by recency decay, access boost, source quality, **path weight** (the table above, by longest matching root; any other key weighs 1.0), and usefulness. Path weight is one factor: a weak match under `/longterm/company-story` does not beat a strong match elsewhere on weight alone.\n\n### Lead-only roots\n\nOnly the lead may write or move a key under `/longterm/company-story`, `/longterm/entities`, or `/longterm/timeline`. The check is the permission `memory.write.consolidated`. It applies to a `key`, to a `newKey`, to a `/longterm/` name, and to a `sourcePath` sent to `POST /api/memory/index`, so there is no side door. A worker that tries gets an error naming the lead-only paths. A worker puts durable knowledge under `/longterm/facts` or `/longterm/decisions`, or leaves the key off, and the lead consolidates the rest.\n\n## Before you store\n\n1. Run `memory-dedup-check` with the text, or `memory-search` with one or two queries and `intent: "dedup before store"`.\n2. A near-duplicate exists: `memory-edit` it. Mode `replace` for a rewrite, mode `exact` for a one-line correction. Say why in `intent`.\n3. Nothing close exists: `memory-store`. For a keyed memory, `memory-search` with `keyPrefix` set to the folder you will write into shows what is already there.\n\n## Triage\n\n- A memory is wrong: `memory-edit` with the correction.\n- A memory is stale and nobody needs it: `memory-delete`.\n- A recalled memory helped or misled you: `memory_rate` with `useful` true or false and a short `note`. The call needs a task context and counts once per memory per task. Ratings move the memory up or down in future searches.\n\n## Lead: promote a learning\n\nWhen a worker\'s output or failure holds a lesson other workers need, call `inject-learning` with the worker\'s `agentId`, the `learning`, and a `category`: `mistake-pattern`, `best-practice`, `codebase-knowledge`, or `preference`. It lands at swarm scope and every agent recalls it. It is stored as `manual`, so it never expires, but it takes no key: it has no path weight and `keyPrefix` does not find it. To file a learning by path, `memory-store` it with a `/longterm` key, or `memory-edit` it with `newKey`.\n';
|
|
8585
8775
|
|
|
8586
8776
|
// templates/skills/one-shot/config.json
|
|
8587
|
-
var
|
|
8777
|
+
var config_default27 = `{
|
|
8588
8778
|
"category": "skills",
|
|
8589
8779
|
"description": "Small-scope plan+implement in one session. Assess the prompt, optionally one bundled round of questions, maintain a lightweight yolo plan while implementing, verify, quick code review, commit. Use when the user invokes /one-shot, or asks to \\"just build/fix/add\\" something small without the full create-plan → implement-plan pipeline. Small means ≤ ~3 phases inside one subsystem — anything bigger escalates to /desplega:create-plan.",
|
|
8590
8780
|
"displayName": "One Shot",
|
|
@@ -8605,7 +8795,7 @@ var config_default26 = `{
|
|
|
8605
8795
|
`;
|
|
8606
8796
|
|
|
8607
8797
|
// templates/skills/one-shot/content.md
|
|
8608
|
-
var
|
|
8798
|
+
var content_default27 = `# One-Shot
|
|
8609
8799
|
|
|
8610
8800
|
Plan and implement a small task in a single session. This is the lightweight sibling of the \`create-plan\` → \`implement-plan\` pipeline: same discipline (written plan, verification, code review), a fraction of the ceremony.
|
|
8611
8801
|
|
|
@@ -8674,7 +8864,7 @@ Commit with a concise message referencing the change (only if the user hasn't sa
|
|
|
8674
8864
|
`;
|
|
8675
8865
|
|
|
8676
8866
|
// templates/skills/pages/config.json
|
|
8677
|
-
var
|
|
8867
|
+
var config_default28 = `{
|
|
8678
8868
|
"kind": "skill",
|
|
8679
8869
|
"name": "pages",
|
|
8680
8870
|
"displayName": "Pages",
|
|
@@ -8697,11 +8887,11 @@ var config_default27 = `{
|
|
|
8697
8887
|
`;
|
|
8698
8888
|
|
|
8699
8889
|
// templates/skills/pages/content.md
|
|
8700
|
-
var
|
|
8890
|
+
var content_default28 = `# Pages — DB-backed Static Artifacts
|
|
8701
8891
|
|
|
8702
8892
|
DB-backed static content (HTML or JSON) served by the API directly. Cheap,
|
|
8703
|
-
versioned, share-able by URL.
|
|
8704
|
-
|
|
8893
|
+
versioned, share-able by URL. No server process, no tunnel, no port
|
|
8894
|
+
allocation.
|
|
8705
8895
|
|
|
8706
8896
|
A page should be a clean human-facing artifact, not a raw dump with a URL. This
|
|
8707
8897
|
skill covers both halves: **how to publish** a page, and **the taste baseline**
|
|
@@ -8712,7 +8902,7 @@ every page should meet.
|
|
|
8712
8902
|
> (e.g. \`CAPABILITIES=core,task-pool,pages\`). If the tool is missing from
|
|
8713
8903
|
> your MCP list, this is why.
|
|
8714
8904
|
|
|
8715
|
-
## When to use Pages vs
|
|
8905
|
+
## When to use Pages vs Apps
|
|
8716
8906
|
|
|
8717
8907
|
| You need… | Use |
|
|
8718
8908
|
|---|---|
|
|
@@ -8720,12 +8910,11 @@ every page should meet.
|
|
|
8720
8910
|
| A JSON status payload + a few buttons that call swarm APIs | **Pages** (\`contentType: 'application/json'\`) |
|
|
8721
8911
|
| To share an output via a URL with no server logic | **Pages** |
|
|
8722
8912
|
| Analysis that should be linkable and bookmarkable | **Pages** |
|
|
8723
|
-
|
|
|
8724
|
-
|
|
|
8913
|
+
| Persistent records, queries, and server-side actions | The **\`apps\`** skill |
|
|
8914
|
+
| Large or binary files (PNG, MP4, exports) | The **\`agent-fs\`** skill |
|
|
8725
8915
|
|
|
8726
8916
|
Rule of thumb: if the content is a snapshot (you can write the full HTML/JSON
|
|
8727
|
-
in a single call), use pages. If
|
|
8728
|
-
artifacts.
|
|
8917
|
+
in a single call), use pages. If it needs stored data and actions, use apps.
|
|
8729
8918
|
|
|
8730
8919
|
Do **not** use pages for:
|
|
8731
8920
|
|
|
@@ -9232,9 +9421,8 @@ human-facing summary.
|
|
|
9232
9421
|
|
|
9233
9422
|
## See Also
|
|
9234
9423
|
|
|
9235
|
-
- The **\`
|
|
9236
|
-
|
|
9237
|
-
servers, not static content.
|
|
9424
|
+
- The **\`apps\`** skill — schema-backed records, named queries, and actions.
|
|
9425
|
+
- The **\`agent-fs\`** skill — durable file storage and share links.
|
|
9238
9426
|
- The **\`kv-storage\`** skill — page-scoped state via \`swarmSdk.kv\`.
|
|
9239
9427
|
- \`runbooks/secret-scrubbing.md\` — egress scrubbing details.
|
|
9240
9428
|
- SPA listing: \`\${APP_URL}/pages\` — the same \`APP_URL\` used for share links above.
|
|
@@ -9389,7 +9577,7 @@ Channels cannot access internal workflow topics. The socket closes when its auth
|
|
|
9389
9577
|
`;
|
|
9390
9578
|
|
|
9391
9579
|
// templates/skills/phase-running/config.json
|
|
9392
|
-
var
|
|
9580
|
+
var config_default29 = `{
|
|
9393
9581
|
"category": "skills",
|
|
9394
9582
|
"description": "Execute individual plan phases as background sub-agents for context-efficient implementation.",
|
|
9395
9583
|
"displayName": "Phase Running",
|
|
@@ -9410,7 +9598,7 @@ var config_default28 = `{
|
|
|
9410
9598
|
`;
|
|
9411
9599
|
|
|
9412
9600
|
// templates/skills/phase-running/content.md
|
|
9413
|
-
var
|
|
9601
|
+
var content_default29 = `# Phase Running
|
|
9414
9602
|
|
|
9415
9603
|
You are executing a single phase of an implementation plan as an atomic background sub-agent. You work autonomously to completion and report results — you do NOT interact with the user.
|
|
9416
9604
|
|
|
@@ -9542,7 +9730,7 @@ Phase agents are atomic — they run to completion or stop:
|
|
|
9542
9730
|
`;
|
|
9543
9731
|
|
|
9544
9732
|
// templates/skills/planning/config.json
|
|
9545
|
-
var
|
|
9733
|
+
var config_default30 = `{
|
|
9546
9734
|
"category": "skills",
|
|
9547
9735
|
"description": "Implementation planning skill. Creates detailed technical plans through interactive research and iteration.",
|
|
9548
9736
|
"displayName": "Planning",
|
|
@@ -9563,7 +9751,7 @@ var config_default29 = `{
|
|
|
9563
9751
|
`;
|
|
9564
9752
|
|
|
9565
9753
|
// templates/skills/planning/content.md
|
|
9566
|
-
var
|
|
9754
|
+
var content_default30 = `# Planning
|
|
9567
9755
|
|
|
9568
9756
|
You create detailed implementation plans through an interactive, iterative process. Be skeptical, thorough, collaborative.
|
|
9569
9757
|
|
|
@@ -9638,7 +9826,7 @@ Canonical format and heading hierarchy lives in \`template.md\`. Structure valid
|
|
|
9638
9826
|
`;
|
|
9639
9827
|
|
|
9640
9828
|
// templates/skills/qa/config.json
|
|
9641
|
-
var
|
|
9829
|
+
var config_default31 = `{
|
|
9642
9830
|
"category": "skills",
|
|
9643
9831
|
"description": "Functional validation skill. Captures test evidence (screenshots, recordings, links) and produces QA reports in thoughts/*/qa/.",
|
|
9644
9832
|
"displayName": "QA",
|
|
@@ -9659,7 +9847,7 @@ var config_default30 = `{
|
|
|
9659
9847
|
`;
|
|
9660
9848
|
|
|
9661
9849
|
// templates/skills/qa/content.md
|
|
9662
|
-
var
|
|
9850
|
+
var content_default31 = `# QA
|
|
9663
9851
|
|
|
9664
9852
|
You are performing functional validation of a feature, bugfix, or deployment. Your job is to prove it works (or doesn't) by executing test scenarios and capturing evidence.
|
|
9665
9853
|
|
|
@@ -9845,7 +10033,7 @@ Based on the answer:
|
|
|
9845
10033
|
`;
|
|
9846
10034
|
|
|
9847
10035
|
// templates/skills/qa-loom/config.json
|
|
9848
|
-
var
|
|
10036
|
+
var config_default32 = `{
|
|
9849
10037
|
"category": "skills",
|
|
9850
10038
|
"description": "Narrated QA loom for UI work: record a UI QA pass with agent-browser and a timestamped step log, voice it with ElevenLabs, burn one-line subtitles with ffmpeg, and embed the narrated mp4 (plus an optional GIF preview) in the PR as GitHub user attachments. Use on UI-changing PRs and UI QA passes. Layers on the \`qa\` and \`agent-browser\` skills. Needs ELEVENLABS_API_KEY, ffmpeg with libass, and agent-browser.",
|
|
9851
10039
|
"displayName": "QA Loom",
|
|
@@ -9862,7 +10050,7 @@ var config_default31 = `{
|
|
|
9862
10050
|
`;
|
|
9863
10051
|
|
|
9864
10052
|
// templates/skills/qa-loom/content.md
|
|
9865
|
-
var
|
|
10053
|
+
var content_default32 = `# QA loom
|
|
9866
10054
|
|
|
9867
10055
|
A QA loom is a short screen recording of a real UI QA pass, with a voice-over and small one-line subtitles, embedded in the PR. The narrated mp4 plays inline on GitHub with audio. An optional subtitled GIF gives a silent preview.
|
|
9868
10056
|
|
|
@@ -10052,7 +10240,7 @@ A 4xx validation error is deterministic. Do not loop on it. Fix the input or fal
|
|
|
10052
10240
|
`;
|
|
10053
10241
|
|
|
10054
10242
|
// templates/skills/questioning/config.json
|
|
10055
|
-
var
|
|
10243
|
+
var config_default33 = `{
|
|
10056
10244
|
"category": "skills",
|
|
10057
10245
|
"description": "One-shot question answering using the research process. Answers inline without generating documents, then offers handoff to brainstorm or research.",
|
|
10058
10246
|
"displayName": "Questioning",
|
|
@@ -10073,7 +10261,7 @@ var config_default32 = `{
|
|
|
10073
10261
|
`;
|
|
10074
10262
|
|
|
10075
10263
|
// templates/skills/questioning/content.md
|
|
10076
|
-
var
|
|
10264
|
+
var content_default33 = `# Questioning
|
|
10077
10265
|
|
|
10078
10266
|
You are answering a question directly and concisely using the research process. No documents are created by default — the answer is the deliverable.
|
|
10079
10267
|
|
|
@@ -10184,7 +10372,7 @@ Each iteration is independent — no state accumulates between questions unless
|
|
|
10184
10372
|
`;
|
|
10185
10373
|
|
|
10186
10374
|
// templates/skills/researching/config.json
|
|
10187
|
-
var
|
|
10375
|
+
var config_default34 = `{
|
|
10188
10376
|
"category": "skills",
|
|
10189
10377
|
"description": "Comprehensive codebase research skill. Documents codebase as-is by spawning parallel sub-agents and synthesizing findings into research documents.",
|
|
10190
10378
|
"displayName": "Researching",
|
|
@@ -10205,7 +10393,7 @@ var config_default33 = `{
|
|
|
10205
10393
|
`;
|
|
10206
10394
|
|
|
10207
10395
|
// templates/skills/researching/content.md
|
|
10208
|
-
var
|
|
10396
|
+
var content_default34 = `# Researching
|
|
10209
10397
|
|
|
10210
10398
|
You are conducting comprehensive research across the codebase to answer questions by spawning parallel sub-agents and synthesizing their findings.
|
|
10211
10399
|
|
|
@@ -10374,7 +10562,7 @@ File-review is on by default (unless Autopilot):
|
|
|
10374
10562
|
`;
|
|
10375
10563
|
|
|
10376
10564
|
// templates/skills/review-offered-task/config.json
|
|
10377
|
-
var
|
|
10565
|
+
var config_default35 = `{
|
|
10378
10566
|
"kind": "skill",
|
|
10379
10567
|
"name": "review-offered-task",
|
|
10380
10568
|
"displayName": "Review Offered Task",
|
|
@@ -10391,10 +10579,10 @@ var config_default34 = `{
|
|
|
10391
10579
|
`;
|
|
10392
10580
|
|
|
10393
10581
|
// templates/skills/review-offered-task/content.md
|
|
10394
|
-
var
|
|
10582
|
+
var content_default35 = '# Review Offered Task\n\nYou have been offered a task. Your job is to review it and decide whether to accept or reject it based on your capabilities and current workload.\n\n## Workflow\n\n1. **Get task details**: Call the `get-task-details` tool with the provided `taskId` to understand what the task involves.\n\n2. **Evaluate the task**: Consider:\n - Does this task match your capabilities?\n - Do you have the necessary context or access to complete it?\n - Is the task description clear enough to proceed?\n\n3. **Make a decision**:\n - **Accept**: If you can complete this task, call `task-action` with `action: "accept"` and `taskId: "<taskId>"`. Then immediately use the `work-on-task` skill with the taskId to start working on it.\n - **Reject**: If you cannot complete this task, call `task-action` with `action: "reject"`, `taskId: "<taskId>"`, and provide a `reason` explaining why you\'re rejecting it (e.g., "Task requires Python expertise which I don\'t have", "Task description is too vague").\n\n## Example Accept Flow\n\n```\n1. get-task-details taskId="abc-123"\n2. [Review the task details]\n3. task-action action="accept" taskId="abc-123"\n4. /work-on-task abc-123\n```\n\n## Example Reject Flow\n\n```\n1. get-task-details taskId="abc-123"\n2. [Review the task details]\n3. task-action action="reject" taskId="abc-123" reason="Task requires access to production database which I don\'t have"\n4. Stop\n```\n\n## Important Notes\n\n- Always provide a clear reason when rejecting a task - this helps the lead agent reassign it appropriately\n- If you accept, you must immediately start working on the task using the `work-on-task` skill\n- If you reject, the task returns to the unassigned pool for reassignment\n';
|
|
10395
10583
|
|
|
10396
10584
|
// templates/skills/reviewing/config.json
|
|
10397
|
-
var
|
|
10585
|
+
var config_default36 = `{
|
|
10398
10586
|
"category": "skills",
|
|
10399
10587
|
"description": "Structured critique of research, plan, and brainstorm documents for completeness, gaps, and quality.",
|
|
10400
10588
|
"displayName": "Reviewing",
|
|
@@ -10415,7 +10603,7 @@ var config_default35 = `{
|
|
|
10415
10603
|
`;
|
|
10416
10604
|
|
|
10417
10605
|
// templates/skills/reviewing/content.md
|
|
10418
|
-
var
|
|
10606
|
+
var content_default36 = `# Reviewing
|
|
10419
10607
|
|
|
10420
10608
|
You are performing a structured critique of a document (research, plan, or brainstorm) to identify gaps, weaknesses, and quality issues. (For reviewing a code diff, use \`desplega:code-reviewing\` instead — this skill reviews documents.)
|
|
10421
10609
|
|
|
@@ -10667,7 +10855,7 @@ File-review is on by default (unless Autopilot):
|
|
|
10667
10855
|
`;
|
|
10668
10856
|
|
|
10669
10857
|
// templates/skills/scheduled-task-resilience/config.json
|
|
10670
|
-
var
|
|
10858
|
+
var config_default37 = `{
|
|
10671
10859
|
"kind": "skill",
|
|
10672
10860
|
"name": "scheduled-task-resilience",
|
|
10673
10861
|
"displayName": "Scheduled Task Resilience",
|
|
@@ -10684,7 +10872,7 @@ var config_default36 = `{
|
|
|
10684
10872
|
`;
|
|
10685
10873
|
|
|
10686
10874
|
// templates/skills/scheduled-task-resilience/content.md
|
|
10687
|
-
var
|
|
10875
|
+
var content_default37 = `# Scheduled Task Resilience
|
|
10688
10876
|
|
|
10689
10877
|
Use these rules whenever a scheduled or regular task polls or waits on a long-running operation. They prevent heartbeat collisions, lost work, duplicate delivery, and sessions that fail after holding an external job open too long.
|
|
10690
10878
|
|
|
@@ -10784,7 +10972,7 @@ When historical incident detail is useful, search the deployment's memory regist
|
|
|
10784
10972
|
`;
|
|
10785
10973
|
|
|
10786
10974
|
// templates/skills/scheduling/config.json
|
|
10787
|
-
var
|
|
10975
|
+
var config_default38 = `{
|
|
10788
10976
|
"kind": "skill",
|
|
10789
10977
|
"name": "scheduling",
|
|
10790
10978
|
"displayName": "Scheduling",
|
|
@@ -10801,10 +10989,10 @@ var config_default37 = `{
|
|
|
10801
10989
|
`;
|
|
10802
10990
|
|
|
10803
10991
|
// templates/skills/scheduling/content.md
|
|
10804
|
-
var
|
|
10992
|
+
var content_default38 = '# Scheduling\n\nUse a schedule when work repeats on a clock, or when it must run once at a later time. On each tick the schedule creates one run. The run is a workflow, a catalog script, or an agent task.\n\n## Tools\n\n`list-schedules`, `create-schedule`, `update-schedule`, `patch-schedule`, `delete-schedule`, `run-schedule-now`. They are deferred. Load them with your harness tool search before the first call.\n\n## Pick the target type\n\n| The tick should | `targetType` | Required fields |\n|---|---|---|\n| Start a workflow | `workflow` | `workflowId` |\n| Run a catalog script | `script` | `scriptName`, optional `scriptArgs` |\n| Put a reasoning agent in the loop | `agent-task` (default) | `taskTemplate` |\n\nChoose `agent-task` only when the run needs judgment, open-ended work, or tool orchestration that no workflow or script covers. An `agent-task` whose template says "trigger workflow X" or "run script Y" is wrong: use the direct target instead.\n\nThe agent-task fields `targetAgentId`, `model`, `modelTier`, `taskTemplate`, `priority`, and `tags` do not apply to workflow or script targets. Workflow cooldowns still gate workflow targets.\n\n## Pick the timing\n\n| Shape | Fields |\n|---|---|\n| Recurring on a cron | `scheduleType: "recurring"`, `cronExpression` (for example `0 9 * * 1-5`), `timezone` (default `UTC`) |\n| Recurring on an interval | `scheduleType: "recurring"`, `intervalMs` (for example `3600000` for hourly) |\n| Once, at a time | `scheduleType: "one_time"`, `runAt` (ISO datetime) |\n| Once, after a delay | `scheduleType: "one_time"`, `delayMs` |\n\nGive the schedule a unique `name` and a one-line `description` that says what a human gets from it.\n\n## Write the task template (agent-task target)\n\nThe template is the whole task the agent receives. It must state:\n\n- the goal and the inputs (IDs, repo, channel),\n- where the result goes (a page, agent-fs, a Slack thread, a task output),\n- what done looks like.\n\n`targetAgentId` pins the task to one agent. Omit it to use the pool. `modelTier` (`smol`, `regular`, `smart`, `ultra`) is the portable way to pick a model. `model` is a provider-specific override.\n\nTasks created by a schedule are automatic tasks. Their completed output is not stored as memory unless the agent calls `store-progress` with `persistMemory: true`.\n\n## Check back later\n\nA task whose answer needs time (a build, a deploy, a reply) calls `defer-task` with `delayMs` or `runAt`, a `summary` of what you did so far, and a `note` that says what is pending. The tool completes the task and creates the one-off schedule for you. The task reaches its final state (`completed`) on this call, and the `summary` becomes its output. The wake-up task carries the deferred task as its parent, so you receive that task\'s context. Add `checks` to list what to verify on wake-up. Do not hand-roll this with `create-schedule`.\n\nWhen waiting on tasks, include `wakeOn:{event:"settled",taskIds:["<id>"],mode:"all"}` alongside `delayMs` or `runAt`. The displayed time is a ceiling; matching task outcomes wake you sooner. The task whose outcome wakes you creates no separate follow-up for you; the wake-up task is where you review it. In `mode:"all"`, a member that settles while others are pending still creates its follow-up.\n\n## Secrets\n\nA `taskTemplate` and `scriptArgs` are stored as plain text and replayed on every run. They must not contain a token, password, or key. Call the external API from a script through a registered connection or a credential binding instead. See the `swarm-scripts` skill, section Secrets.\n\n## Verify before you leave\n\n1. `run-schedule-now` once and read the run result.\n2. `list-schedules` with `name` to confirm `nextRunAt` and `enabled`.\n3. For a script target, the script must exist at global scope: `script-search` by name.\n\n## Repair a failing schedule\n\nSchedules fail in two places. A schedule whose spawned tasks fail: `get-tasks` with `scheduleId` and `status: "failed"`. A schedule the scheduler could not dispatch at all: `list-schedules` with `lastRunStatus: "failed"` or `consecutiveErrorsMin: 1`; these errors back off and auto-disable after 5. The `list-schedules` filters never see spawned-task failures. Fix the cause, then `run-schedule-now` to confirm. A schedule you cannot fix now: `patch-schedule` with `enabled: false` and say so in your task output. A second schedule next to a broken one is not a fix.\n\n## Related skills\n\n- `scheduled-task-resilience`: waiting on CI, builds, deploys, or other slow jobs inside a scheduled task.\n- `swarm-scripts`: writing the script a `script` target runs.\n- `workflow-iterate`: building and testing the workflow a `workflow` target starts.\n';
|
|
10805
10993
|
|
|
10806
10994
|
// templates/skills/script-builder/config.json
|
|
10807
|
-
var
|
|
10995
|
+
var config_default39 = `{
|
|
10808
10996
|
"category": "skills",
|
|
10809
10997
|
"description": "Generate durable, re-runnable scripts from session intent — validation scripts (PASS/FAIL contract) AND gather/bulk scripts (many API/tool calls in code, one derived summary out). Supports TypeScript, Python, Bash with auto-detection, enforces context-optimal output, and auto-documents scripts in CLAUDE.md. Triggers on \\"turn this into a script\\", \\"I want to test/validate X end-to-end\\", \\"wrap this in a re-runnable script\\" — and, critically, whenever you are about to make (or just made) ~10+ similar tool/API calls or any bulk fan-out over a list: that mechanical middle belongs in a script, not in context.",
|
|
10810
10998
|
"displayName": "Script Builder",
|
|
@@ -10825,10 +11013,10 @@ var config_default38 = `{
|
|
|
10825
11013
|
`;
|
|
10826
11014
|
|
|
10827
11015
|
// templates/skills/script-builder/content.md
|
|
10828
|
-
var content_default38 = "# script-builder\n\nYou are converting session intent into a durable, re-runnable script committed to the target project's `scripts/` directory. Two script classes share one principle — **only the derived answer re-enters context; raw payloads never do**:\n\n- **Validation scripts** (`check-*`, `e2e-*`, `smoke-*`, …): a single PASS/FAIL line + `/tmp` log path on success, full verbose output redirected to a timestamped log file.\n- **Gather/bulk scripts** (`gather-*`, `bulk-*`): replace N similar tool/API calls with one script that loops, filters, and aggregates *in code* — stdout is one compact summary block (JSON or table), raw responses go to the `/tmp` log. Rubric (from production code-mode data): past ~10 items or any fan-out over a list, a script beats individual tool calls by ~100x on context and roughly halves end-to-end cost. Offer the script *before* the fan-out happens when you can see it coming, not just retrospectively.\n\nBoth classes get an `<important if>` block in `CLAUDE.md`/`AGENTS.md` so future agents discover the script when the intent recurs — durable scripts are reusable agent memory: import, don't re-derive.\n\n**Schema discovery happens in code, too**: when scripting against an unfamiliar API, grep/filter its OpenAPI spec or typed client programmatically to find the few relevant endpoints — never paste the full schema into context.\n\n## Working Agreement\n\nThese instructions establish a working agreement between you and the user. The key principles are:\n\n1. **AskUserQuestion is your primary communication tool** - Whenever you need to ask the user anything (clarifications, preferences, decisions, confirmations), use the **AskUserQuestion tool**. Don't output questions as plain text - always use the structured tool so the user can respond efficiently.\n\n2. **Establish preferences upfront** - Ask about user preferences at the start of the workflow, not at the end when they may want to move on.\n\n3. **Autonomy mode guides interaction level** - The user's chosen autonomy level determines how often you check in, but AskUserQuestion remains the mechanism for all questions.\n\n## When to Use\n\nThis skill activates when:\n- User invokes `/script-builder` command\n- Another skill references `**OPTIONAL SUB-SKILL:** desplega:script-builder`\n- A `planning`, `qa`, or `verifying` flow needs a re-runnable validation script that doesn't yet exist\n- The user expresses intent to \"turn X into a script\", \"wrap this validation into something reusable\", or \"I want to test X end-to-end\" with no existing script\n\n## Autonomy Mode\n\nAdapt your behavior based on the autonomy mode:\n\n| Mode | Behavior |\n|------|----------|\n| **Autopilot** | Detect mode, draft, syntax-check, document, and (if escalation tier) run + iterate without intermediate confirmations. Pause only at hard blockers or destructive side-effects. |\n| **Critical** (Default) | Confirm at each tier boundary (after draft, before doc edit, before run). Use AskUserQuestion for fix application during iterate loop. |\n| **Verbose** | Confirm before each step. Show every diff. Walk through detection reasoning out loud. |\n\nThe autonomy mode is passed by the invoking command. If not specified, default to **Critical**.\n\n## Process Steps\n\n### Step 1: Detect Mode (Retrospective vs Forward-Declared)\n\nDecide silently — **do not** ask the user \"which mode?\". Use the following heuristics:\n\n1. **Scan recent session tool-use history** (the last ~20 tool calls in the current conversation) for test/validation-shaped activity: `curl`/`fetch` calls, `bun run`/`python`/`pytest`, database queries, `agent-browser` actions, repeated `grep`/log inspection of a single endpoint or table. If ≥2 such actions targeting the same area exist → **retrospective mode**. Separately, if the history (or the task ahead) shows the *same call shape repeated ~10+ times or a fan-out over a list* → **gather-script mode**: propose replacing the repetition with one `gather-*` script before continuing.\n2. **Parse the user's invocation message** for cues:\n - Narrative cues → retrospective: *\"we just figured out\"*, *\"turn this into a script\"*, *\"wrap that in\"*, *\"that thing we just did\"*.\n - Intent cues → forward-declared: *\"I want to test\"*, *\"validate that\"*, *\"smoke check\"*, *\"check before deploy\"*.\n3. **Fallback**: when ambiguous, default to **forward-declared**.\n\n**Retrospective first action**: summarize the observed activity back to the user as plain text (≤6 lines: what was probed, against what, with what success signal), then go to Step 3 with a confirmation prompt instead of Q&A.\n\n**Forward first action**: skip directly to Step 3's Q&A.\n\n### Step 2: Scan Existing Scripts for Overlap\n\n**Resolve the scripts directory** in this order:\n1. Check `CLAUDE.md` for a `<!-- script-builder:dir=<path> -->` marker. If present, use that path.\n2. If `scripts/` exists at the repo root, use it.\n3. Use **AskUserQuestion** with options: `scripts/ (Recommended) | custom path | skip dir persistence for this run`.\n\n**Edge case — directory absent**: if the resolved path doesn't exist, skip the dedup scan, print a one-line note (`No existing scripts directory at <path> — proceeding to intent gathering`), and remember to optionally offer to create the directory before Step 5.\n\n**When the directory exists**:\n1. List files in the scripts directory (top-level only for v1).\n2. For each file, read the top of the file (header comment) and capture the file-name tokens.\n3. Read `CLAUDE.md`/`AGENTS.md` for `<important if=\"...\">` blocks that point at scripts in this directory and capture the trigger phrases.\n4. Fuzzy-match the current intent string against (file-name tokens ∪ header keywords ∪ trigger phrases). A \"plausible match\" is shared keyword + same area noun (e.g., `auth`, `health`, `webhook`).\n5. If ≥1 plausible match → use **AskUserQuestion** with options: `Reuse <name> as-is | Extend <name> | Generate new anyway`.\n\nNever silently skip dedup on borderline matches — surface and let the user decide. The **Extend** branch appends a sub-command/flag to the existing script (do not create a new file); document the new flag in the script's header comment and the existing `<important if>` block.\n\n### Step 3: Gather Intent\n\nBoth modes converge on the same internal intent structure: `{ what, success_signal, failure_signal, inputs, env, side_effects }`.\n\n**Forward mode** — use **AskUserQuestion** (one or two questions, not five):\n\n| Question | Options |\n|----------|---------|\n| \"What are we validating, and what's the success signal?\" | [Free text — collect what + success signal in one answer] |\n| \"Any required env vars, inputs, or side-effects to flag (e.g., writes to prod, costs money)?\" | [Free text — optional; skip if forward intent already specified them] |\n\n**Retrospective mode** — present the summary from Step 1 and use **AskUserQuestion** with: `That's the flow | Close but fix X | Start over`. On *Close but fix X*, ask for the correction inline; on *Start over*, fall through to forward-mode Q&A.\n\nPersist the resolved intent in working memory (do not write to disk yet).\n\n### Step 4: Detect Language\n\nPriority order — first match wins, with override paths:\n\n1. **TypeScript** — `package.json` + `tsconfig.json` exist.\n - **Bun** if `bun.lock` or `bunfig.toml` is present.\n - else **tsx** if `tsx` appears in `package.json` devDependencies.\n - else **compiled Node** (note in script header: requires `tsc` build).\n2. **Python** — `pyproject.toml` or `uv.lock` or `requirements.txt` exists.\n - **uv** if `uv.lock` exists or `pyproject.toml` has `[tool.uv]` configured (sets `{{UV_METADATA}}`).\n - else **vanilla python3** (`{{UV_METADATA}}` substituted with empty string).\n3. **Both TS and Python detected** — count source files in `src/`/top-level and ask **AskUserQuestion** tiebreaker with the dominant one Recommended.\n4. `Cargo.toml` / `go.mod` only → fall back to **bash** (note: TS/Python/Bash only in v1).\n5. Nothing matched → **bash**.\n\n**Task-driven override**: if the gathered intent is clearly shell-y (\"verify three docker containers respond\", \"tail this log file for an error\"), propose **bash** even in a TS/Python project.\n\nConfirm via **AskUserQuestion** with the detected language as the first option (Recommended). Skip in Autopilot.\n\n### Step 5: Draft the Script\n\n1. **Pick the template**: `templates/{typescript.ts.tmpl|python.py.tmpl|bash.sh.tmpl}`.\n2. **Resolve substitution markers** from gathered intent:\n - `{{SCRIPT_NAME}}` ← proposed file-name stem (see naming table).\n - `{{WHAT}}` / `{{WHEN}}` / `{{ENV}}` / `{{EXAMPLE}}` ← intent fields.\n - `{{UV_METADATA}}` ← per Step 4.\n3. **Generate `{{TEST_BODY}}`** from the intent. Keep it minimal: a single concrete probe + assertion, not a battery. Re-read the templates' README (`templates/README.md`) for the contract — the body must respect the PASS/FAIL surface. Throw/raise/`exit 1` on failure; let the template's outer try/trap convert it into the FAIL line.\n **Gather-class bodies** adapt the same template: the loop/aggregation replaces the probe, raw per-item responses go to the log only, and the final `console.log`/`print` emits one compact summary (JSON or aligned table) instead of the PASS line — exit non-zero only on operational failure (auth, network), not on \"found problems\" (problems ARE the output).\n4. **Propose a file name** matching the intent shape — see the prefix table below. Use **AskUserQuestion** with the proposed name first (Recommended) and `custom name` as the alternative.\n\n**Naming conventions** (advisory — skill proposes, user overrides):\n\n| Prefix | When | Example |\n|--------|------|---------|\n| `e2e-*` | End-to-end flows across multiple components | `e2e-auth-flow.ts` |\n| `check-*` | Idempotent single-probe verifications | `check-db-boundary.sh` |\n| `smoke-*` | Minimal-viability post-deploy checks | `smoke-prod-api.ts` |\n| `measure-*` | Performance / size / token measurements | `measure-tool-tokens.ts` |\n| `seed-*` / `generate-*` | Data seeding or artifact generation (rare for validation) | `seed-api-keys.sh` |\n| `gather-*` / `bulk-*` | Bulk data-gathering or bulk mutation replacing N tool calls; stdout = one summary block, not PASS/FAIL | `gather-workflow-health.ts` |\n\nIf the intent doesn't match any prefix cleanly, propose a free-form name like `validate-<area>.<ext>`.\n\n5. **Write the file** to the resolved scripts directory. Make it executable (`chmod +x`).\n\n### Step 6: Syntax/Type-Check\n\nRun the appropriate checker:\n\n| Language | Checker (in priority order) |\n|----------|-----------------------------|\n| TypeScript | `bunx tsc --noEmit <file>` if Bun present, else `npx tsc --noEmit <file>` |\n| Python | `python3 -m py_compile <file>` (always) + `ruff check <file>` if `ruff` is on PATH |\n| Bash | `shellcheck <file>` if available, else `bash -n <file>` |\n\n**On success**: log a single line (`syntax check OK`) and proceed to Step 7.\n\n**On failure**:\n1. Print the error (≤10 lines, not the full output).\n2. Propose a concrete one-edit fix (the exact `Edit` you'd apply).\n3. Use **AskUserQuestion** with `Apply fix | Investigate | Stop`.\n - **Apply fix** → edit the script and re-run the checker (loop, no hardcoded cap).\n - **Investigate** → drop into discussion with the user; do not auto-apply anything.\n - **Stop** → leave script in place at its path; print the path; do not delete.\n\n### Step 7: Document the Script\n\nAuto-edit the target project's `CLAUDE.md` and/or `AGENTS.md` so future agents discover this script when the matching intent recurs.\n\n**Block template** (the literal markdown the skill emits):\n\n```markdown\n<important if=\"[TRIGGER: e.g., you are testing the auth flow]\">\n\n## [Area] validation\n\nRun `scripts/<name>` to [one-liner]. Requires [env/deps]. Example: `<cmd>`. Full log at `/tmp/<name>-*.log`.\n\nGenerated/maintained via `/script-builder`.\n\n</important>\n```\n\n**Behavior**:\n\n1. **Target file selection**: edit `CLAUDE.md` and `AGENTS.md` if both exist; edit only what exists. **Never** create either file from scratch — if neither exists, print a one-line note (`No CLAUDE.md or AGENTS.md found — script generated but not documented`) and skip this step.\n2. **Placement heuristic**: search the file for the first heading matching `Test|Testing|Validation|Scripts` (case-insensitive). If found, append the new block within/after that section. Otherwise, append a new `## Scripts for testing & validation` section near the end of the file but **before** any heading matching `License|Acknowledg|Maintain|Contributors`.\n3. **Idempotency**: if a block referencing `scripts/<name>` already exists (i.e., a prior `/script-builder` run for the same script name), update it in place — replace the entire `<important if=...>...</important>` block, do not append a duplicate.\n4. **Scripts-dir marker**: if Step 2 resolved the scripts directory by user choice (not from an existing marker), insert `<!-- script-builder:dir=<path> -->` near the top of `CLAUDE.md` (after the title) so subsequent runs are silent. Skip if the marker already exists.\n5. **Scale gate — `scripts/index.md`**: if the scripts directory holds **>10 documented scripts**, per-script `<important if>` blocks become their own context bloat. Generate/maintain a `scripts/index.md` hub instead (one line + link per script, mirroring the `runbooks/` convention from `desplega:engineering-standards`), collapse the CLAUDE.md/AGENTS.md blocks into ONE pointer block referencing the hub, and add new scripts to the hub only.\n6. **Show the diff**: run the equivalent of `git diff CLAUDE.md AGENTS.md` and print a 5-line summary of what changed. **Never auto-stage** — the user commits.\n\n### Step 8: Offer Escalation\n\nAfter the doc edit, decide whether to proceed to the run-and-iterate tier.\n\n| Autonomy | Behavior |\n|----------|----------|\n| **Autopilot** | Auto-escalate to Step 9 unless the intent flagged side-effects (writes to prod, sends real money, mutates a shared resource). On flagged side-effects, fall through to Critical behavior. |\n| **Critical** (Default) | Use **AskUserQuestion**: `Run it now to confirm it works | I'll run it myself later | Just generate, don't run`. |\n| **Verbose** | Same as Critical, plus offer the proposed run command for review before executing. |\n\nIf the user picks \"I'll run it myself later\" or \"Just generate, don't run\" → skip directly to Step 10.\n\n### Step 9: Iterate on Failures (Escalated Tier Only)\n\nA bounded-by-the-human loop. **Never auto-apply a fix** outside Autopilot.\n\n1. **Run the script** with the proposed example invocation (no `--verbose`, no `--json`). Capture exit code and `tail -n 40` of the `/tmp` log.\n2. **If exit code 0** → report PASS (echo the script's PASS line) and proceed to Step 10.\n3. **If exit code non-zero**:\n - Summarize the failure in **1–3 lines**: error class (timeout / 4xx / 5xx / assertion / dependency-missing / etc.) + likely cause (grep the log for known signatures: `ECONNREFUSED`, `Traceback`, `non-zero exit`, `command not found`, etc.).\n - **Propose a concrete diff** — the exact `Edit` you would apply to the script. Show old → new.\n - Use **AskUserQuestion**: `Apply fix | Investigate differently | Stop`.\n - **Apply fix** → edit the script, log the change, loop back to (1).\n - **Investigate differently** → drop into discussion; do not auto-apply.\n - **Stop** → leave the script in place, print its path; do not revert.\n\n**No hardcoded retry cap** — the human is the implicit bound.\n\n**Side-effect flag**: if the intent declared side-effects (Step 3) and Autopilot is active, surface a one-line `WARNING: this script <does X> against <target>` before the first run and require explicit confirmation via **AskUserQuestion** even in Autopilot mode.\n\n### Step 10: Handoff\n\nHow the skill exits depends on how it was invoked:\n\n**Invoked as a sub-skill** (from `planning`, `qa`, `verifying`):\nReturn control to the parent skill with a structured summary: `{ script_path, status: \"pass\"|\"fail\"|\"unrun\", log_path?, doc_files_edited: [...] }`.\n\n**Invoked directly** (`/script-builder`):\nUse **AskUserQuestion**: `Run it with /qa | Run /verify-plan | Commit the script and doc changes | Done`.\n- **Run with /qa** → invoke `desplega:qa` with the script path as the source.\n- **Run /verify-plan** → invoke `desplega:verifying` if a plan path is in current context.\n- **Commit** → propose a commit message (`feat(scripts): add scripts/<name> for <area> validation`) and stage only the generated script + the CLAUDE.md/AGENTS.md edits. Do not auto-commit; show the proposed `git add` and `git commit` commands and require user confirmation.\n- **Done** → print the script path + log path (if escalation ran) and exit.\n\n**Abort path** (Stop selected at any earlier gate): leave the generated script in place, leave any CLAUDE.md/AGENTS.md edit in place if Step 7 ran, and print a one-liner: `Aborted. Script: <path>. Doc edits: <files or \"none\">. Re-run /script-builder or git checkout to discard.` Do not `git restore` on the user's behalf.\n\n## Learning Capture\n\n**OPTIONAL SUB-SKILL:** If significant insights, patterns, gotchas, or decisions emerged during this workflow, consider using `desplega:learning` to capture them via `/learning capture`. Focus on learnings that would help someone else in a future session.\n";
|
|
11016
|
+
var content_default39 = "# script-builder\n\nYou are converting session intent into a durable, re-runnable script committed to the target project's `scripts/` directory. Two script classes share one principle — **only the derived answer re-enters context; raw payloads never do**:\n\n- **Validation scripts** (`check-*`, `e2e-*`, `smoke-*`, …): a single PASS/FAIL line + `/tmp` log path on success, full verbose output redirected to a timestamped log file.\n- **Gather/bulk scripts** (`gather-*`, `bulk-*`): replace N similar tool/API calls with one script that loops, filters, and aggregates *in code* — stdout is one compact summary block (JSON or table), raw responses go to the `/tmp` log. Rubric (from production code-mode data): past ~10 items or any fan-out over a list, a script beats individual tool calls by ~100x on context and roughly halves end-to-end cost. Offer the script *before* the fan-out happens when you can see it coming, not just retrospectively.\n\nBoth classes get an `<important if>` block in `CLAUDE.md`/`AGENTS.md` so future agents discover the script when the intent recurs — durable scripts are reusable agent memory: import, don't re-derive.\n\n**Schema discovery happens in code, too**: when scripting against an unfamiliar API, grep/filter its OpenAPI spec or typed client programmatically to find the few relevant endpoints — never paste the full schema into context.\n\n## Working Agreement\n\nThese instructions establish a working agreement between you and the user. The key principles are:\n\n1. **AskUserQuestion is your primary communication tool** - Whenever you need to ask the user anything (clarifications, preferences, decisions, confirmations), use the **AskUserQuestion tool**. Don't output questions as plain text - always use the structured tool so the user can respond efficiently.\n\n2. **Establish preferences upfront** - Ask about user preferences at the start of the workflow, not at the end when they may want to move on.\n\n3. **Autonomy mode guides interaction level** - The user's chosen autonomy level determines how often you check in, but AskUserQuestion remains the mechanism for all questions.\n\n## When to Use\n\nThis skill activates when:\n- User invokes `/script-builder` command\n- Another skill references `**OPTIONAL SUB-SKILL:** desplega:script-builder`\n- A `planning`, `qa`, or `verifying` flow needs a re-runnable validation script that doesn't yet exist\n- The user expresses intent to \"turn X into a script\", \"wrap this validation into something reusable\", or \"I want to test X end-to-end\" with no existing script\n\n## Autonomy Mode\n\nAdapt your behavior based on the autonomy mode:\n\n| Mode | Behavior |\n|------|----------|\n| **Autopilot** | Detect mode, draft, syntax-check, document, and (if escalation tier) run + iterate without intermediate confirmations. Pause only at hard blockers or destructive side-effects. |\n| **Critical** (Default) | Confirm at each tier boundary (after draft, before doc edit, before run). Use AskUserQuestion for fix application during iterate loop. |\n| **Verbose** | Confirm before each step. Show every diff. Walk through detection reasoning out loud. |\n\nThe autonomy mode is passed by the invoking command. If not specified, default to **Critical**.\n\n## Process Steps\n\n### Step 1: Detect Mode (Retrospective vs Forward-Declared)\n\nDecide silently — **do not** ask the user \"which mode?\". Use the following heuristics:\n\n1. **Scan recent session tool-use history** (the last ~20 tool calls in the current conversation) for test/validation-shaped activity: `curl`/`fetch` calls, `bun run`/`python`/`pytest`, database queries, `agent-browser` actions, repeated `grep`/log inspection of a single endpoint or table. If ≥2 such actions targeting the same area exist → **retrospective mode**. Separately, if the history (or the task ahead) shows the *same call shape repeated ~10+ times or a fan-out over a list* → **gather-script mode**: propose replacing the repetition with one `gather-*` script before continuing.\n2. **Parse the user's invocation message** for cues:\n - Narrative cues → retrospective: *\"we just figured out\"*, *\"turn this into a script\"*, *\"wrap that in\"*, *\"that thing we just did\"*.\n - Intent cues → forward-declared: *\"I want to test\"*, *\"validate that\"*, *\"smoke check\"*, *\"check before deploy\"*.\n3. **Fallback**: when ambiguous, default to **forward-declared**.\n\n**Retrospective first action**: summarize the observed activity back to the user as plain text (≤6 lines: what was probed, against what, with what success signal), then go to Step 3 with a confirmation prompt instead of Q&A.\n\n**Forward first action**: skip directly to Step 3's Q&A.\n\n### Step 2: Scan Existing Scripts for Overlap\n\n**Resolve the scripts directory** in this order:\n1. Check `CLAUDE.md` for a `<!-- script-builder:dir=<path> -->` marker. If present, use that path.\n2. If `scripts/` exists at the repo root, use it.\n3. Use **AskUserQuestion** with options: `scripts/ (Recommended) | custom path | skip dir persistence for this run`.\n\n**Edge case — directory absent**: if the resolved path doesn't exist, skip the dedup scan, print a one-line note (`No existing scripts directory at <path> — proceeding to intent gathering`), and remember to optionally offer to create the directory before Step 5.\n\n**When the directory exists**:\n1. List files in the scripts directory (top-level only for v1).\n2. For each file, read the top of the file (header comment) and capture the file-name tokens.\n3. Read `CLAUDE.md`/`AGENTS.md` for `<important if=\"...\">` blocks that point at scripts in this directory and capture the trigger phrases.\n4. Fuzzy-match the current intent string against (file-name tokens ∪ header keywords ∪ trigger phrases). A \"plausible match\" is shared keyword + same area noun (e.g., `auth`, `health`, `webhook`).\n5. If ≥1 plausible match → use **AskUserQuestion** with options: `Reuse <name> as-is | Extend <name> | Generate new anyway`.\n\nNever silently skip dedup on borderline matches — surface and let the user decide. The **Extend** branch appends a sub-command/flag to the existing script (do not create a new file); document the new flag in the script's header comment and the existing `<important if>` block.\n\n### Step 3: Gather Intent\n\nBoth modes converge on the same internal intent structure: `{ what, success_signal, failure_signal, inputs, env, side_effects }`.\n\n**Forward mode** — use **AskUserQuestion** (one or two questions, not five):\n\n| Question | Options |\n|----------|---------|\n| \"What are we validating, and what's the success signal?\" | [Free text — collect what + success signal in one answer] |\n| \"Any required env vars, inputs, or side-effects to flag (e.g., writes to prod, costs money)?\" | [Free text — optional; skip if forward intent already specified them] |\n\n**Retrospective mode** — present the summary from Step 1 and use **AskUserQuestion** with: `That's the flow | Close but fix X | Start over`. On *Close but fix X*, ask for the correction inline; on *Start over*, fall through to forward-mode Q&A.\n\nPersist the resolved intent in working memory (do not write to disk yet).\n\n### Step 4: Detect Language\n\nPriority order — first match wins, with override paths:\n\n1. **TypeScript** — `package.json` + `tsconfig.json` exist.\n - **Bun** if `bun.lock` or `bunfig.toml` is present.\n - else **tsx** if `tsx` appears in `package.json` devDependencies.\n - else **compiled Node** (note in script header: requires `tsc` build).\n2. **Python** — `pyproject.toml` or `uv.lock` or `requirements.txt` exists.\n - **uv** if `uv.lock` exists or `pyproject.toml` has `[tool.uv]` configured (sets `{{UV_METADATA}}`).\n - else **vanilla python3** (`{{UV_METADATA}}` substituted with empty string).\n3. **Both TS and Python detected** — count source files in `src/`/top-level and ask **AskUserQuestion** tiebreaker with the dominant one Recommended.\n4. `Cargo.toml` / `go.mod` only → fall back to **bash** (note: TS/Python/Bash only in v1).\n5. Nothing matched → **bash**.\n\n**Task-driven override**: if the gathered intent is clearly shell-y (\"verify three docker containers respond\", \"tail this log file for an error\"), propose **bash** even in a TS/Python project.\n\nConfirm via **AskUserQuestion** with the detected language as the first option (Recommended). Skip in Autopilot.\n\n### Step 5: Draft the Script\n\n1. **Pick the template**: `templates/{typescript.ts.tmpl|python.py.tmpl|bash.sh.tmpl}`.\n2. **Resolve substitution markers** from gathered intent:\n - `{{SCRIPT_NAME}}` ← proposed file-name stem (see naming table).\n - `{{WHAT}}` / `{{WHEN}}` / `{{ENV}}` / `{{EXAMPLE}}` ← intent fields.\n - `{{UV_METADATA}}` ← per Step 4.\n3. **Generate `{{TEST_BODY}}`** from the intent. Keep it minimal: a single concrete probe + assertion, not a battery. Re-read the templates' README (`templates/README.md`) for the contract — the body must respect the PASS/FAIL surface. Throw/raise/`exit 1` on failure; let the template's outer try/trap convert it into the FAIL line.\n **Gather-class bodies** adapt the same template: the loop/aggregation replaces the probe, raw per-item responses go to the log only, and the final `console.log`/`print` emits one compact summary (JSON or aligned table) instead of the PASS line — exit non-zero only on operational failure (auth, network), not on \"found problems\" (problems ARE the output).\n4. **Propose a file name** matching the intent shape — see the prefix table below. Use **AskUserQuestion** with the proposed name first (Recommended) and `custom name` as the alternative.\n\n**Naming conventions** (advisory — skill proposes, user overrides):\n\n| Prefix | When | Example |\n|--------|------|---------|\n| `e2e-*` | End-to-end flows across multiple components | `e2e-auth-flow.ts` |\n| `check-*` | Idempotent single-probe verifications | `check-db-boundary.sh` |\n| `smoke-*` | Minimal-viability post-deploy checks | `smoke-prod-api.ts` |\n| `measure-*` | Performance / size / token measurements | `measure-tool-tokens.ts` |\n| `seed-*` / `generate-*` | Data seeding or artifact generation (rare for validation) | `seed-api-keys.sh` |\n| `gather-*` / `bulk-*` | Bulk data-gathering or bulk mutation replacing N tool calls; stdout = one summary block, not PASS/FAIL | `gather-workflow-health.ts` |\n\nIf the intent doesn't match any prefix cleanly, propose a free-form name like `validate-<area>.<ext>`.\n\n5. **Write the file** to the resolved scripts directory. Make it executable (`chmod +x`).\n\n### Step 6: Syntax/Type-Check\n\nRun the appropriate checker:\n\n| Language | Checker (in priority order) |\n|----------|-----------------------------|\n| TypeScript | `bunx tsc --noEmit <file>` if Bun present, else `npx tsc --noEmit <file>` |\n| Python | `python3 -m py_compile <file>` (always) + `ruff check <file>` if `ruff` is on PATH |\n| Bash | `shellcheck <file>` if available, else `bash -n <file>` |\n\n**On success**: log a single line (`syntax check OK`) and proceed to Step 7.\n\n**On failure**:\n1. Print the error (≤10 lines, not the full output).\n2. Propose a concrete one-edit fix (the exact `Edit` you'd apply).\n3. Use **AskUserQuestion** with `Apply fix | Investigate | Stop`.\n - **Apply fix** → edit the script and re-run the checker (loop, no hardcoded cap).\n - **Investigate** → drop into discussion with the user; do not auto-apply anything.\n - **Stop** → leave script in place at its path; print the path; do not delete.\n\n### Step 7: Document the Script\n\nAuto-edit the target project's `CLAUDE.md` and/or `AGENTS.md` so future agents discover this script when the matching intent recurs.\n\n**Block template** (the literal markdown the skill emits):\n\n```markdown\n<important if=\"[TRIGGER: e.g., you are testing the auth flow]\">\n\n## [Area] validation\n\nRun `scripts/<name>` to [one-liner]. Requires [env/deps]. Example: `<cmd>`. Full log at `/tmp/<name>-*.log`.\n\nGenerated/maintained via `/script-builder`.\n\n</important>\n```\n\n**Behavior**:\n\n1. **Target file selection**: edit `CLAUDE.md` and `AGENTS.md` if both exist; edit only what exists. **Never** create either file from scratch — if neither exists, print a one-line note (`No CLAUDE.md or AGENTS.md found — script generated but not documented`) and skip this step.\n2. **Placement heuristic**: search the file for the first heading matching `Test|Testing|Validation|Scripts` (case-insensitive). If found, append the new block within/after that section. Otherwise, append a new `## Scripts for testing & validation` section near the end of the file but **before** any heading matching `License|Acknowledg|Maintain|Contributors`.\n3. **Idempotency**: if a block referencing `scripts/<name>` already exists (i.e., a prior `/script-builder` run for the same script name), update it in place — replace the entire `<important if=...>...</important>` block, do not append a duplicate.\n4. **Scripts-dir marker**: if Step 2 resolved the scripts directory by user choice (not from an existing marker), insert `<!-- script-builder:dir=<path> -->` near the top of `CLAUDE.md` (after the title) so subsequent runs are silent. Skip if the marker already exists.\n5. **Scale gate — `scripts/index.md`**: if the scripts directory holds **>10 documented scripts**, per-script `<important if>` blocks become their own context bloat. Generate/maintain a `scripts/index.md` hub instead (one line + link per script, mirroring the `runbooks/` convention from `desplega:engineering-standards`), collapse the CLAUDE.md/AGENTS.md blocks into ONE pointer block referencing the hub, and add new scripts to the hub only.\n6. **Show the diff**: run the equivalent of `git diff CLAUDE.md AGENTS.md` and print a 5-line summary of what changed. **Never auto-stage** — the user commits.\n\n### Step 8: Offer Escalation\n\nAfter the doc edit, decide whether to proceed to the run-and-iterate tier.\n\n| Autonomy | Behavior |\n|----------|----------|\n| **Autopilot** | Auto-escalate to Step 9 unless the intent flagged side-effects (writes to prod, sends real money, mutates a shared resource). On flagged side-effects, fall through to Critical behavior. |\n| **Critical** (Default) | Use **AskUserQuestion**: `Run it now to confirm it works | I'll run it myself later | Just generate, don't run`. |\n| **Verbose** | Same as Critical, plus offer the proposed run command for review before executing. |\n\nIf the user picks \"I'll run it myself later\" or \"Just generate, don't run\" → skip directly to Step 10.\n\n### Step 9: Iterate on Failures (Escalated Tier Only)\n\nA bounded-by-the-human loop. **Never auto-apply a fix** outside Autopilot.\n\n1. **Run the script** with the proposed example invocation (no `--verbose`, no `--json`). Capture exit code and `tail -n 40` of the `/tmp` log.\n2. **If exit code 0** → report PASS (echo the script's PASS line) and proceed to Step 10.\n3. **If exit code non-zero**:\n - Summarize the failure in **1–3 lines**: error class (timeout / 4xx / 5xx / assertion / dependency-missing / etc.) + likely cause (grep the log for known signatures: `ECONNREFUSED`, `Traceback`, `non-zero exit`, `command not found`, etc.).\n - **Propose a concrete diff** — the exact `Edit` you would apply to the script. Show old → new.\n - Use **AskUserQuestion**: `Apply fix | Investigate differently | Stop`.\n - **Apply fix** → edit the script, log the change, loop back to (1).\n - **Investigate differently** → drop into discussion; do not auto-apply.\n - **Stop** → leave the script in place, print its path; do not revert.\n\n**No hardcoded retry cap** — the human is the implicit bound.\n\n**Side-effect flag**: if the intent declared side-effects (Step 3) and Autopilot is active, surface a one-line `WARNING: this script <does X> against <target>` before the first run and require explicit confirmation via **AskUserQuestion** even in Autopilot mode.\n\n### Step 10: Handoff\n\nHow the skill exits depends on how it was invoked:\n\n**Invoked as a sub-skill** (from `planning`, `qa`, `verifying`):\nReturn control to the parent skill with a structured summary: `{ script_path, status: \"pass\"|\"fail\"|\"unrun\", log_path?, doc_files_edited: [...] }`.\n\n**Invoked directly** (`/script-builder`):\nUse **AskUserQuestion**: `Run it with /qa | Run /verify-plan | Commit the script and doc changes | Done`.\n- **Run with /qa** → invoke `desplega:qa` with the script path as the source.\n- **Run /verify-plan** → invoke `desplega:verifying` if a plan path is in current context.\n- **Commit** → propose a commit message (`feat(scripts): add scripts/<name> for <area> validation`) and stage only the generated script + the CLAUDE.md/AGENTS.md edits. Do not auto-commit; show the proposed `git add` and `git commit` commands and require user confirmation.\n- **Done** → print the script path + log path (if escalation ran) and exit.\n\n**Abort path** (Stop selected at any earlier gate): leave the generated script in place, leave any CLAUDE.md/AGENTS.md edit in place if Step 7 ran, and print a one-liner: `Aborted. Script: <path>. Doc edits: <files or \"none\">. Re-run /script-builder or git checkout to discard.` Do not `git restore` on the user's behalf.\n\n## Learning Capture\n\n**OPTIONAL SUB-SKILL:** If significant insights, patterns, gotchas, or decisions emerged during this workflow, consider using `desplega:learning` to capture them via `/learning capture`. Focus on learnings that would help someone else in a future session.\n";
|
|
10829
11017
|
|
|
10830
11018
|
// templates/skills/script-workflows/config.json
|
|
10831
|
-
var
|
|
11019
|
+
var config_default40 = `{
|
|
10832
11020
|
"kind": "skill",
|
|
10833
11021
|
"name": "script-workflows",
|
|
10834
11022
|
"displayName": "Script Workflows",
|
|
@@ -10845,16 +11033,16 @@ var config_default39 = `{
|
|
|
10845
11033
|
`;
|
|
10846
11034
|
|
|
10847
11035
|
// templates/skills/script-workflows/content.md
|
|
10848
|
-
var
|
|
11036
|
+
var content_default40 = "Use this skill when a user asks to launch, monitor, inspect, or debug a durable script workflow run. This is for the Script Workflows v1 runtime: one-off TypeScript workflow source with journaled `swarm-script`, `raw-llm`, and `agent-task` steps.\n\n## Tool Flow\n\nLoad the script workflow tools with ToolSearch when they are not already visible:\n\n```text\nlaunch-script-run\nget-script-run\nlist-script-runs\n```\n\nUse `launch-script-run` to start a one-off run. It calls the same `/api/script-runs` API as the dashboard, preserves the invoking agent identity, and starts the run in the background.\n\nUse `get-script-run` to read terminal status and journal entries. Poll it when needed, but keep polling bounded and report progress for long runs.\n\nUse `list-script-runs` to find recent runs or filter by `status` / `agentId`.\n\nDo not hand-roll raw HTTP for this flow unless the tool itself is broken and you are explicitly debugging the API. The tool handles auth and `X-Agent-ID` like the existing inline `script-run` tool family.\n\n## Source Shape\n\nAuthor TypeScript workflow source as a default export. The runtime provides `args` and `ctx`.\n\n```ts\nexport default async function main(args, ctx) {\n const lookup = await ctx.step.swarmScript(\"lookup-data\", {\n scriptName: \"fetch-readable\",\n args: { url: args.url },\n });\n\n const summary = await ctx.step.rawLlm(\"summarize\", {\n prompt: `Summarize this for an operator:\\n${JSON.stringify(lookup)}`,\n });\n\n const task = await ctx.step.agentTask(\"operator-review\", {\n task: `Review this summary and flag risks:\\n${JSON.stringify(summary)}`,\n tags: [\"script-run\"],\n priority: 50,\n });\n\n return { lookup, summary, task };\n}\n```\n\n`ctx.step.agentTask` blocks until the dispatched task reaches a terminal status (default `waitForCompletion: true`, default `timeoutMs` 2h) and journals its real output — a sequential `plan → implement → review` chain will not fan out. Pass `waitForCompletion: false` for the legacy fire-and-poll-yourself shape, or `failOnTaskFailure: false` to receive `{taskId, status: \"failed\", error}` instead of a throw when the task fails/is cancelled.\n\n## Label Rules\n\nStep labels are durability keys. They must be stable and unique for each logical step. Do not reuse the same literal label inside a loop; launch will fail with `label_lint_violation`.\n\nFor looped work, include an item identifier in the label:\n\n```ts\nfor (const item of args.items) {\n await ctx.step.agentTask(`review-${item.id}`, { task: item.prompt });\n}\n```\n\n## Statuses\n\nTerminal statuses to surface clearly:\n\n- `completed` — run finished and `output` may be present.\n- `failed` — run ended with `error`.\n- `cancelled` — run was cancelled before completion.\n- `aborted_limit` — runtime guardrail stopped the run, usually step count, agent-task count, or wall-clock cap.\n- `label_lint_violation` — launch-time rejection, not a persisted run status.\n\nWhen a run is not terminal, report the current status, journal count, and latest heartbeat if present.\n";
|
|
10849
11037
|
|
|
10850
11038
|
// templates/skills/slack-interaction/config.json
|
|
10851
|
-
var
|
|
11039
|
+
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';
|
|
10852
11040
|
|
|
10853
11041
|
// templates/skills/slack-interaction/content.md
|
|
10854
|
-
var
|
|
11042
|
+
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';
|
|
10855
11043
|
|
|
10856
11044
|
// templates/skills/step-running/config.json
|
|
10857
|
-
var
|
|
11045
|
+
var config_default42 = `{
|
|
10858
11046
|
"category": "skills",
|
|
10859
11047
|
"description": "Execute a single DAG step as an autonomous background sub-agent. Sibling of phase-running for DAG plans produced by v-planning. Reads a step-<n>.md file directly, atomically claims it via frontmatter status, runs the three-bucket Success Criteria, and reports back. Spawned by v-implementing or by /run-step.",
|
|
10860
11048
|
"displayName": "Step Running",
|
|
@@ -10875,10 +11063,10 @@ var config_default41 = `{
|
|
|
10875
11063
|
`;
|
|
10876
11064
|
|
|
10877
11065
|
// templates/skills/step-running/content.md
|
|
10878
|
-
var
|
|
11066
|
+
var content_default42 = "# Step Running\n\nYou execute a single step of a DAG plan as an atomic background sub-agent. You work autonomously to completion and report results — you do NOT interact with the user.\n\nThis is the DAG sibling of `phase-running`. The execution model and atomicity contract are identical; the differences are:\n- You receive a **step file path** (`step-<n>.md`), not a plan path + phase number.\n- You **claim the step atomically** via the step's frontmatter `status` field before doing work, and release it on completion / failure. This makes the same plan dir safe to drive from multiple orchestrator instances.\n\n## Execution Model\n\nThis skill runs inside a background `Agent` (sub-agent). v-implementing (or a user invoking `/run-step`) spawns it via the Agent tool with `run_in_background: true`.\n\nThe step agent receives:\n- **Step path**: full path to `step-<n>.md`\n- **Plan dir**: full path to the parent plan directory (read `root.md` from here for plan-level context)\n- **Agent ID**: unique identifier for this sub-agent (e.g. orchestrator session ID + step ID + timestamp). Used for atomic claim + stale-claim detection.\n- **Relevant context**: any extra context from the caller\n\n## Concurrency: Atomic Claim\n\nBefore doing any work, claim the step:\n\n1. Read the step file's frontmatter.\n2. **If `status: done`** — report `Status: skipped` and exit. The step is already finished.\n3. **If `status: claimed`** and `assignee` differs from your agent ID:\n - Fresh claim (`claimed_at` within last hour): report `Status: blocked` (reason: `claimed by <other-id>`) and exit.\n - Stale claim (>1h): proceed and overwrite — the previous worker likely died.\n4. **Otherwise** (`status: ready`, or stale `claimed`): rewrite the frontmatter block in a single `Edit` so the write is atomic-enough for filesystem-based coordination. Set:\n - `status: claimed`\n - `assignee: <your-agent-id>`\n - `claimed_at: <ISO timestamp UTC>`\n5. Re-read the file. If `assignee` is not yours, another worker raced you — report `Status: blocked` (reason: `lost claim race`) and exit.\n\nOn terminal transitions, set frontmatter:\n- **Completed**: `status: done`, clear `assignee` and `claimed_at`.\n- **Blocked or failed (retry-able)**: `status: ready`, clear `assignee` and `claimed_at`. The orchestrator (or another worker) can retry.\n- **Failed (non-retry-able)**: leave `status: claimed` so the orchestrator can investigate; include the reason in the report.\n\nFor true cross-machine atomicity (NFS, etc.), the caller is responsible for rendezvous (e.g. a shared lock service). The single-Edit pattern is good-enough for local filesystem and well-behaved network filesystems.\n\n## Autonomy\n\nStep agents always run as **Autopilot** within the sub-agent. The calling context controls outer autonomy and human checkpoints.\n\n**CRITICAL**: Step agents do NOT use AskUserQuestion. If something is ambiguous, report `Status: blocked` with details. The caller handles all user interaction.\n\n## Process Steps\n\n### Step 1: Load Context\n\n1. **Claim the step** (see \"Concurrency: Atomic Claim\"). Stop here on any non-claim outcome.\n2. Read the full step file.\n3. Read `root.md` from the plan dir for plan-level context: Overview, Current State, Desired End State, Implementation Approach, Global Verification.\n4. Read all files mentioned in the step's \"Changes Required\" section.\n\n### Step 2: Pre-flight Check\n\n| Check | Action if failed |\n|-------|-----------------|\n| All `depends_on` steps have `status: done` | Scheduler bug — release claim, report `blocked` |\n| No merge conflicts in target files | Release claim, report `blocked` |\n| Files/dirs from depended-on steps exist | Release claim, report `blocked` |\n\n### Step 3: Execute Step\n\nImplement the changes described in the step's \"Changes Required\" section. Adapt to minor mismatches; report `blocked` for significant ones.\n\n### Step 4: Run Verification\n\nSame three-bucket pattern as `phase-running`:\n1. **Automated Verification** (runnable commands): run each, record pass/fail. On failure, attempt one fix-and-retry.\n2. **Automated QA** (agent-driven scenarios — browser-use, screenshot diff, CLI walkthrough): execute, record pass/fail per item.\n3. **Manual Verification**: leave unchecked. Caller handles with the user.\n4. **QA Spec (linked doc)**: if present, report `QA Doc: <path>`. Do not execute scenarios inline.\n\n### Step 5: Update Step File\n\n- Check off (`- [x]`) Automated Verification + Automated QA items that passed.\n- Do NOT check off Manual Verification items.\n- Update frontmatter `status` per \"Concurrency: Atomic Claim\" terminal-transition rules.\n- Update `last_updated` / `last_updated_by` frontmatter fields if present.\n\n### Step 6: Report Results\n\n**Completed:**\n```\nStatus: completed\nStep: step-N - <Name>\nFinal frontmatter status: done\nFiles changed: [list]\nAutomated Verification: N/M passed\n- [x] [Check 1] — passed\nAutomated QA: N/M passed\n- [x] [Scenario 1] — passed\nQA Doc: <path> | n/a\nManual verification needed:\n- [ ] [Manual check 1]\n```\n\n**Blocked / failed:** mirror `phase-running`'s shape, plus a `Final frontmatter status:` line so the orchestrator knows whether the step is released for retry (`ready`) or held for investigation (`claimed`).\n\n## Atomicity Contract\n\n- No interactive questions.\n- No partial states left unexplained.\n- Frontmatter `status` always reflects the final state at exit.\n- Report includes enough detail for the caller to decide next steps.\n\n## Context Handoff Pattern\n\n| Direction | Data |\n|-----------|------|\n| **Caller → Step Agent** | Step path + plan dir + agent ID + autonomy mode |\n| **Step Agent reads** | Step file + root.md + files in Changes Required |\n| **Step Agent writes** | Frontmatter status transitions + checkboxes + code changes |\n| **Step Agent → Caller** | Status + changed files + verification results + final frontmatter status |\n| **Caller handles** | Manual verification, cross-step coordination, QA doc execution, human checkpoints |\n";
|
|
10879
11067
|
|
|
10880
11068
|
// templates/skills/swarm-extensions/config.json
|
|
10881
|
-
var
|
|
11069
|
+
var config_default43 = `{
|
|
10882
11070
|
"kind": "skill",
|
|
10883
11071
|
"name": "swarm-extensions",
|
|
10884
11072
|
"displayName": "Swarm Extensions",
|
|
@@ -10895,7 +11083,7 @@ var config_default42 = `{
|
|
|
10895
11083
|
`;
|
|
10896
11084
|
|
|
10897
11085
|
// templates/skills/swarm-extensions/content.md
|
|
10898
|
-
var
|
|
11086
|
+
var content_default43 = `# Swarm Extensions
|
|
10899
11087
|
|
|
10900
11088
|
An extension is a trusted TypeScript hook bundle that runs inside the swarm API server. Use one when a rule must apply to the whole swarm: block or rewrite new tasks, route Slack channels, suppress follow-ups, change stalled-task remediation, guard tool calls, or react after a task or tool call finishes.
|
|
10901
11089
|
|
|
@@ -10944,10 +11132,12 @@ curl -s "$MCP_BASE_URL/api/extensions/type-defs" \\
|
|
|
10944
11132
|
| \`pre.task.create\` | before any task is stored (REST, MCP, Slack, schedule, workflow, webhook, follow-up) | \`block(reason)\`, \`modify({ priority, agentId, tags, description, ... })\`, or nothing |
|
|
10945
11133
|
| \`pre.task.followUp\` | before the lead follow-up task for a finished worker task | \`block\`, \`modify({ description, priority, agentId })\`, or nothing |
|
|
10946
11134
|
| \`pre.slack.route\` | before a Slack message becomes a task | \`modify({ target: { kind: "agent", agentId } })\`, \`{ kind: "lead" }\`, \`{ kind: "broadcast" }\` |
|
|
11135
|
+
| \`pre.chat.route\` | before buffered Chat SDK messages (Telegram, Slack, Discord, Teams) become a task | \`block(reason)\`, or \`modify({ target: { kind: "agent", agentId } })\`, \`{ kind: "lead" }\`, \`{ kind: "broadcast" }\`, \`{ kind: "workflow", workflowId }\` |
|
|
10947
11136
|
| \`pre.heartbeat.remediate\` | after the heartbeat sweep finds a stalled task | \`modify({ proposedAction: "record" | "fail" | "supersede-resume" })\` |
|
|
10948
11137
|
| \`pre.tool.call\` | before an agent MCP tool call runs | \`block(reason)\` or \`modify({ args })\` |
|
|
10949
11138
|
| \`post.task.created\`, \`post.task.completed\`, \`post.task.failed\`, \`post.task.cancelled\`, \`post.task.superseded\`, \`post.task.progress\` | after the change is committed | none |
|
|
10950
11139
|
| \`post.slack.message\`, \`post.tool.call\` | after the message or tool call finished | none |
|
|
11140
|
+
| \`post.chat.message\` | after a Chat SDK message is received (before buffering) or sent; \`direction\` is \`inbound\` or \`outbound\` | none |
|
|
10951
11141
|
| \`post.approval.resolved\` | after a person answers an approval request, workflow or standalone (not on timeout or cancel) | none |
|
|
10952
11142
|
| \`post.task.budgetRefused\` | after a task is refused by a daily spend budget | none |
|
|
10953
11143
|
| \`post.email.received\`, \`post.kapso.message\` | after an inbound AgentMail or Kapso message is received | none |
|
|
@@ -11074,14 +11264,14 @@ export default extension;
|
|
|
11074
11264
|
`;
|
|
11075
11265
|
|
|
11076
11266
|
// templates/skills/swarm-scripts/config.json
|
|
11077
|
-
var
|
|
11267
|
+
var config_default44 = `{
|
|
11078
11268
|
"kind": "skill",
|
|
11079
11269
|
"name": "swarm-scripts",
|
|
11080
11270
|
"displayName": "Swarm Scripts",
|
|
11081
11271
|
"slug": "swarm-scripts",
|
|
11082
11272
|
"title": "Swarm Scripts",
|
|
11083
11273
|
"description": "Bulk, repeat, fan-out, or data-heavy work: write and run swarm scripts (inline \`script-run\`, named \`script-upsert\`, durable \`launch-script-run\`). Covers the script-vs-tool rubric, the authoring contract (\`args\` first, \`ctx\` second), the seed catalog, connections and secrets, \`db_query\`, and exposing a script as an API.",
|
|
11084
|
-
"version": "1.
|
|
11274
|
+
"version": "1.2.0",
|
|
11085
11275
|
"category": "skills",
|
|
11086
11276
|
"placeholders": [],
|
|
11087
11277
|
"runAllSeedersCandidate": true,
|
|
@@ -11091,7 +11281,7 @@ var config_default43 = `{
|
|
|
11091
11281
|
`;
|
|
11092
11282
|
|
|
11093
11283
|
// templates/skills/swarm-scripts/content.md
|
|
11094
|
-
var
|
|
11284
|
+
var content_default44 = `# Swarm Scripts
|
|
11095
11285
|
|
|
11096
11286
|
A swarm script is TypeScript that runs out of process with a typed Swarm SDK. Only its return value enters your context. Use one when direct tool calls would repeat, flood your context, or need deterministic processing over many records.
|
|
11097
11287
|
|
|
@@ -11164,7 +11354,7 @@ export default async function (args: z.infer<typeof argsSchema>, ctx: ScriptCont
|
|
|
11164
11354
|
|
|
11165
11355
|
- \`ctx.swarm.*\`: the swarm SDK. \`task_get\`, \`task_send\`, \`task_storeProgress\`, \`task_action\`, \`task_list\`, \`slack_reply\`, \`memory_search\`, \`memory_store\`, \`kv_get\`, \`kv_getOrNull\`, \`kv_set\`, \`kv_delete\`, \`kv_incr\`, \`kv_list\`, \`swarm_get\`, \`agent_info\`, \`db_query\`, and more. \`kv_getOrNull\` returns the entry, \`null\` on a missing key, and throws on other errors.
|
|
11166
11356
|
- \`ctx.swarm.task_send\` requires \`routingReason\` and \`routingNote\` whenever \`agentId\` is present. The note must explain the worker choice in at least 10 characters after trimming whitespace (maximum 200 characters). Use \`human_pinned\` for an explicit/configured choice, \`skill\` for a role or specialization match, \`continuity\` for the same worker/session, \`reroute_fault\` for a fault handoff, or \`overflow\` for capacity/pool escalation. Omit both \`agentId\` and routing fields for the pool.
|
|
11167
|
-
- \`ctx.swarm.config\`: \`apiKey\`, \`agentId\`, \`mcpBaseUrl\`, and \`ctx.swarm.config.get("KEY")\` for
|
|
11357
|
+
- \`ctx.swarm.config\`: \`apiKey\`, \`agentId\`, \`mcpBaseUrl\`, and \`ctx.swarm.config.get("KEY")\` for swarm config values, resolved for the agent running the script (agent scope over global, the same rows \`get-config\` returns for that agent). A key that is not set returns \`undefined\`, so \`config.get("KEY") ?? fallback\` works. All are \`Redacted\` wrappers that stringify to \`<redacted>\`. Never unwrap one into a return value, a log line, or a request body you build by hand.
|
|
11168
11358
|
- \`ctx.api.<slug>\` and \`ctx.mcp.<slug>\`: typed clients for registered connections. They exist only for registered connections. Introspect with \`Object.keys(ctx.api ?? {})\` and \`Object.keys(ctx.mcp ?? {})\`.
|
|
11169
11359
|
- \`ctx.stdlib\`: \`fetch\`, \`fetchJson\` (retries, 30 s timeout), \`grep\`, \`glob\`, \`table\`, \`Redacted\`.
|
|
11170
11360
|
- \`ctx.logger\`: \`log\`, \`warn\`, \`error\`. Keep logs short.
|
|
@@ -11195,11 +11385,13 @@ export default async function main(args: { status?: string; limit?: number } | u
|
|
|
11195
11385
|
|
|
11196
11386
|
## Secrets
|
|
11197
11387
|
|
|
11388
|
+
A config row reaches your shell env only when its \`exportToEnv\` flag is on. If \`$KEY\` is empty in your shell but \`get-config\` or \`list-config\` shows the row marked \`not in agent env\`, the row stays server-side on purpose: do not copy the value into a file, an env var, or a command. Use it from a script instead, through one of the paths below. Harness, git, and agent-fs credentials stay exported. Keys only the API server reads (Slack app token and signing secret, GitHub App private key, webhook and OAuth client secrets) can never be exported. If a recipe genuinely needs the variable in a shell, ask the lead to turn \`exportToEnv\` on for that row (\`set-config\` with \`exportToEnv: true\`); it applies to your next task.
|
|
11389
|
+
|
|
11198
11390
|
Order of preference:
|
|
11199
11391
|
|
|
11200
11392
|
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.
|
|
11201
11393
|
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.
|
|
11202
|
-
3. \`ctx.swarm.config.get("KEY")\` for a config value you pass to \`ctx.stdlib.fetch\` as a header value.
|
|
11394
|
+
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.
|
|
11203
11395
|
|
|
11204
11396
|
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.
|
|
11205
11397
|
|
|
@@ -11258,7 +11450,7 @@ A named script can serve \`POST /api/x/script/<id>\` for callers outside the swa
|
|
|
11258
11450
|
`;
|
|
11259
11451
|
|
|
11260
11452
|
// templates/skills/tackle-gh-comments/config.json
|
|
11261
|
-
var
|
|
11453
|
+
var config_default45 = `{
|
|
11262
11454
|
"category": "skills",
|
|
11263
11455
|
"description": "Work through every review comment on a GitHub PR — fetch the threads, verify each claim against the code, fix what is real, then reply and mark each thread resolved before pushing. Use when the user says \\"tackle the PR comments\\", \\"address the review\\", \\"handle the bot comments\\", \\"check the comments on the PR\\", \\"resolve the review threads\\", or invokes /tackle-gh-comments. Project-agnostic; works in any repo with a GitHub remote.",
|
|
11264
11456
|
"displayName": "Tackle GH Comments",
|
|
@@ -11279,7 +11471,7 @@ var config_default44 = `{
|
|
|
11279
11471
|
`;
|
|
11280
11472
|
|
|
11281
11473
|
// templates/skills/tackle-gh-comments/content.md
|
|
11282
|
-
var
|
|
11474
|
+
var content_default45 = `# Tackle GitHub PR comments
|
|
11283
11475
|
|
|
11284
11476
|
Review threads and their resolved state live **only in GraphQL**. The REST API
|
|
11285
11477
|
cannot read \`isResolved\` and cannot resolve a thread, and \`gh pr view --comments\`
|
|
@@ -11398,7 +11590,7 @@ committed, and the branch is pushed.
|
|
|
11398
11590
|
`;
|
|
11399
11591
|
|
|
11400
11592
|
// templates/skills/taste-minimalist-skill/config.json
|
|
11401
|
-
var
|
|
11593
|
+
var config_default46 = `{
|
|
11402
11594
|
"kind": "skill",
|
|
11403
11595
|
"name": "taste-minimalist-skill",
|
|
11404
11596
|
"displayName": "Taste Minimalist Skill",
|
|
@@ -11420,7 +11612,7 @@ var config_default45 = `{
|
|
|
11420
11612
|
`;
|
|
11421
11613
|
|
|
11422
11614
|
// templates/skills/taste-minimalist-skill/content.md
|
|
11423
|
-
var
|
|
11615
|
+
var content_default46 = `> Vendored from [taste-skill](https://github.com/Leonxlnx/taste-skill) / [tasteskill.dev](https://www.tasteskill.dev/) at commit 06d6028b5c623016c59ce8536f578e5a1127b499.
|
|
11424
11616
|
> Upstream license: MIT, Copyright (c) 2026 Leonxlnx. See ../TASTE-SKILL-LICENSE.
|
|
11425
11617
|
|
|
11426
11618
|
# Protocol: Premium Utilitarian Minimalism UI Architect
|
|
@@ -11506,7 +11698,7 @@ When tasked with writing frontend code (HTML, React, Tailwind, Vue) or designing
|
|
|
11506
11698
|
`;
|
|
11507
11699
|
|
|
11508
11700
|
// templates/skills/tdd-planning/config.json
|
|
11509
|
-
var
|
|
11701
|
+
var config_default47 = `{
|
|
11510
11702
|
"category": "skills",
|
|
11511
11703
|
"description": "TDD-focused implementation planning. Creates plans with strict Red-Green-Commit/Rollback cycles for each step.",
|
|
11512
11704
|
"displayName": "TDD Planning",
|
|
@@ -11527,7 +11719,7 @@ var config_default46 = `{
|
|
|
11527
11719
|
`;
|
|
11528
11720
|
|
|
11529
11721
|
// templates/skills/tdd-planning/content.md
|
|
11530
|
-
var
|
|
11722
|
+
var content_default47 = `# TDD Planning
|
|
11531
11723
|
|
|
11532
11724
|
You are creating implementation plans that follow a strict Test-Driven Development approach. Every implementation step follows the TDD cycle: **RED → GREEN → COMMIT/ROLLBACK**.
|
|
11533
11725
|
|
|
@@ -11789,7 +11981,7 @@ Before finalizing any TDD plan, verify:
|
|
|
11789
11981
|
`;
|
|
11790
11982
|
|
|
11791
11983
|
// templates/skills/v-implementing/config.json
|
|
11792
|
-
var
|
|
11984
|
+
var config_default48 = `{
|
|
11793
11985
|
"category": "skills",
|
|
11794
11986
|
"description": "Parallel DAG-plan implementation skill. Reads a v-planning plan directory (root.md + step-<n>.md files), topologically schedules ready steps, and fans them out as parallel sub-agents. Use whenever the user invokes /v-implement, points at a plan directory produced by /v-plan, or asks to \\"run the parallel plan\\", \\"implement the DAG\\", or \\"fan out the steps\\" — even without those exact words. For linear plans (single .md file), use \`implementing\` instead.",
|
|
11795
11987
|
"displayName": "V-Implementing",
|
|
@@ -11810,10 +12002,10 @@ var config_default47 = `{
|
|
|
11810
12002
|
`;
|
|
11811
12003
|
|
|
11812
12004
|
// templates/skills/v-implementing/content.md
|
|
11813
|
-
var content_default47 = "# v-implementing\n\nYou are implementing an approved DAG-structured plan produced by `v-planning`. The plan is a directory with `root.md` + one `step-<n>.md` per node. Your job is to act as a **topological scheduler**: at each tick, find steps whose dependencies are all done, fan them out as parallel sub-agents (one per ready step), wait, repeat — until the DAG is drained. Then run Global Verification.\n\nThis is the parallel sibling of `implementing`. The orchestration model is the same (sub-agents do the work, main session coordinates), only the scheduler is different.\n\n## Working Agreement\n\n**All user-facing questions go through `AskUserQuestion`** — see `desplega:ask-user` for conventions. Never ask in chat as plain bullets.\n\n**All read/research/validation work goes through sub-agents** — keep raw tool output out of the main session. Default to `run_in_background: true`. The fan-out scheduler below is built on this.\n\nThe autonomy mode (below) controls how often you check in. AskUserQuestion is always the mechanism.\n\nFile-review is on by default — when significant changes land in a step (or wave), invoke `/file-review:file-review <path>` for inline feedback (skip only if Autopilot).\n\n## Autonomy Mode\n\n| Mode | Behavior |\n|------|----------|\n| **Autopilot** | Drain the DAG without pausing. Only stop on blocker / failed step. |\n| **Critical** (Default) | Pause when each *wave* of parallel steps completes; wait for manual verification before unlocking the next wave. |\n| **Verbose** | Pause after each individual step (even within a wave). |\n\n## Initial Setup Questions\n\nAfter understanding the plan and before the scheduler loop starts, gather implementation-specific details (skip in Autopilot):\n\n### 1. Branch / Worktree Setup\n\nCheck the current branch: `git branch --show-current`. Then check if the `wts` plugin is installed (look for `wts:wts` in available skills).\n\n**If wts is installed**, use **AskUserQuestion**:\n\n| Question | Options |\n|----------|---------|\n| \"You're on `<current-branch>`. Where would you like to implement?\" | 1. Continue on current branch, 2. Create a new branch, 3. Create a wts worktree |\n\n**If wts is not installed**, drop the worktree option.\n\n### 2. Commit Strategy\n\nUse **AskUserQuestion**:\n\n| Question | Options |\n|----------|---------|\n| \"How would you like to handle commits during parallel implementation?\" | 1. Commit after each step completes (Recommended), 2. Commit at the end (single commit), 3. Let me decide as I go |\n\nIf \"Commit after each step\" is selected, after a step's manual verification passes, create a commit: `[step-N] <step name>`.\n\n### 3. Orchestration Mode (only if the `Workflow` tool is available)\n\n| Question | Options |\n|----------|---------|\n| \"Run the DAG waves as Workflow scripts? (deterministic scheduling, parallel steps as agent() calls)\" | 1. Yes — Workflow waves, 2. No — Agent fan-out (Default) |\n\nA \"yes\" is the explicit opt-in the Workflow tool requires. If opted in, each wave of the scheduler loop below runs as one Workflow script — a `parallel()` of `agent()` calls, one per ready step, with `model`/`effort` per `desplega:delegate-work`. Everything else is unchanged: step agents still follow the `step-running` contract (atomic frontmatter claim, three-bucket verification), and autonomy-mode pause points sit **between** Workflow invocations, never inside one. In Autopilot these questions are skipped, so there is no opt-in — use Agent fan-out unless the user pre-authorized workflows.\n\n### 4. Code Review Mode\n\n| Question | Options |\n|----------|---------|\n| \"Run an automatic code review after each wave?\" | 1. Automatic two-axis review per wave (Recommended), 2. Only at the end (single review after the DAG drains), 3. Off — I'll review myself |\n\nIn **Autopilot**, skip the question and default to automatic per-wave review.\n\n## Getting Started\n\nGiven a plan directory path:\n\n1. **Read `root.md` fully** (no `limit`/`offset`). Capture: Overview, Current State, Desired End State, Implementation Approach, Global Verification.\n2. **Read every `step-<n>.md`'s frontmatter** to build the dependency graph (`id`, `depends_on`). You don't need to read step bodies up front — the sub-agents will do that.\n3. **Validate the DAG:**\n - No cycles\n - Every `depends_on` ID exists as a step file\n - At least one step has `depends_on: []` (otherwise nothing can start)\n4. **Set `root.md` frontmatter `status: in-progress`.**\n5. **Build a TodoWrite list** with one entry per step, marked `pending`.\n6. **Resume support:** if any step's body has `- [x]` boxes already (or its frontmatter says `status: completed` if the user added it), trust them and treat that step as `done`.\n\nIf no path given, ask via AskUserQuestion.\n\n## The Scheduler Loop\n\nThe scheduler reads each step's frontmatter `status` (`ready` | `claimed` | `done`) and `depends_on`. The frontmatter `status` field is the **single source of truth** — multiple orchestrator instances on the same plan dir coordinate through it (each `desplega:step-running` sub-agent atomically claims its step before doing work).\n\n```\nwhile any step has status != done:\n ready = [step for step in steps\n if step.status == \"ready\"\n and all(dep.status == \"done\" for dep in step.depends_on)]\n if not ready:\n # DAG drained, OR every remaining undone step is claimed by another worker\n if any step has status == claimed: wait for in-flight claims to resolve\n else: report stuck (likely cycle or unrecoverable failure)\n continue\n fan out each step in `ready` as a parallel `desplega:step-running` sub-agent\n wait for the wave to complete\n review reports; step-running has already updated frontmatter\n (status: done on success, status: ready on retry-able failure, status: claimed if held for investigation)\n if Critical mode: pause for manual verification of newly-completed steps\n if any failed: stop and ask user how to proceed\n```\n\n### Spawning a Step Sub-agent\n\nUse the `Agent` tool with `run_in_background: true`, invoking `desplega:step-running`. Pass:\n\n- **Step path**: full path to `step-<n>.md`\n- **Plan dir**: full path to the parent plan directory\n- **Agent ID**: unique ID for this sub-agent (e.g. orchestrator session ID + step ID + timestamp). step-running uses this for atomic claim + stale-claim detection.\n- **Plan-level context** (optional): a quick brief from `root.md`. step-running re-reads `root.md` itself, so this is just to reduce round-trips.\n\n`step-running` owns:\n- Atomic claim (rewrite frontmatter `status: ready` → `status: claimed, assignee: <agent-id>, claimed_at: <ts>`)\n- Three-bucket verification (Automated Verification, Automated QA, QA Doc identification)\n- Terminal status transition (`status: done` on success, `status: ready` on retry-able failure)\n- Reporting back\n\nThe orchestrator does NOT do step work itself — it delegates. See `desplega:step-running` for the full sub-agent contract.\n\n**Executor routing**: if the `desplega:delegate-work` skill is available, pick each step's executor per its routing matrix instead of defaulting to a `step-running` sub-agent — a step may route to a Codex variant (one worktree per parallel slice) or a specific Claude model tier. Only the executor choice changes; the scheduler, frontmatter claim protocol, and all other semantics here stay unchanged. When a step routes to Codex, the orchestrator owns the frontmatter bookkeeping that `step-running` would normally do (Codex must not edit plan files).\n\n### Wave Completion\n\nWhen a wave finishes:\n1. **Review each agent's report.** Mark steps `done` only if `completed` was reported.\n2. **Run the wave code review** (unless review mode is Off) — invoke `desplega:code-reviewing` on the wave's combined diff: Standards + Spec axes as parallel background sub-agents (routed per `desplega:delegate-work`), spec source = the completed steps' bodies and Success Criteria. Critical findings block those steps' commits — fix and re-verify first. If \"Only at the end\" was selected, run one review over the full diff after the DAG drains instead.\n3. **Handle `QA Doc: <path>`** — for each step that reported a QA doc, invoke `desplega:qa` against that path. The Automated QA bucket inside the step is already handled by the sub-agent; only the linked doc needs separate orchestration here. (`QA: n/a` → proceed normally.)\n4. **Manual verification (if not Autopilot)** — present manual verification items from each completed step's body. Wait for user confirmation. Don't tick manual boxes until confirmed.\n5. **Commits (if commit-per-step was selected)** — after a step's manual verification passes, create a commit: `[step-N] <step name>`.\n6. **Loop** — recompute `ready` and start the next wave.\n\n## Handling Failures and Mismatches\n\nIf a sub-agent reports `failed` or `blocked`, or you spot a mismatch between plan and reality:\n\n| Question | Options |\n|----------|---------|\n| \"[step-N] [issue]. How should I proceed?\" | 1. Adapt step (edit step-N.md and retry), 2. Retry as-is, 3. Skip and continue (mark step blocked), 4. Stop the run |\n\nIn Autopilot mode, use best judgment, document the decision in the step file, and continue if non-fatal.\n\n## After the DAG Drains\n\nWhen every step is `done`:\n\n1. Run **Global Verification** from `root.md`. Tick automated checks; surface manual checks to the user.\n2. Set `root.md` frontmatter `status: completed`.\n3. Offer post-implementation auditing: \"Would you like me to run `/verify-plan` and `/review` on the plan directory?\"\n4. If commit-per-step was off, offer to create a single bundled commit now.\n5. If the work ships as a GitHub PR: once review comments land, address them with `desplega:tackle-gh-comments` (fetch threads, verify each claim, fix, reply, resolve — push last).\n\n## Important Guidelines\n\n1. **One step = one sub-agent.** Don't bundle steps into a single agent even if they're in the same wave — fan-out is the whole point.\n2. **Plan-level context goes to every sub-agent.** Sibling step bodies do not.\n3. **The DAG is canonical via step frontmatter** — if `root.md`'s table is out of sync, trust the frontmatter.\n4. **Resume works step-granular.** If the user re-runs `/v-implement` mid-DAG, completed steps stay completed; only undone ones rerun.\n5. **Don't tick manual checkboxes** until the user confirms — automated boxes can be ticked by the sub-agent or the orchestrator.\n\n## Review Integration\n\nIf `file-review` is available and the user opted in:\n- After each step's significant code changes, invoke `/file-review:file-review <changed-file>`.\n- Process feedback with `file-review:process-review` before treating the step as done.\n- Skip in Autopilot.\n";
|
|
12005
|
+
var content_default48 = "# v-implementing\n\nYou are implementing an approved DAG-structured plan produced by `v-planning`. The plan is a directory with `root.md` + one `step-<n>.md` per node. Your job is to act as a **topological scheduler**: at each tick, find steps whose dependencies are all done, fan them out as parallel sub-agents (one per ready step), wait, repeat — until the DAG is drained. Then run Global Verification.\n\nThis is the parallel sibling of `implementing`. The orchestration model is the same (sub-agents do the work, main session coordinates), only the scheduler is different.\n\n## Working Agreement\n\n**All user-facing questions go through `AskUserQuestion`** — see `desplega:ask-user` for conventions. Never ask in chat as plain bullets.\n\n**All read/research/validation work goes through sub-agents** — keep raw tool output out of the main session. Default to `run_in_background: true`. The fan-out scheduler below is built on this.\n\nThe autonomy mode (below) controls how often you check in. AskUserQuestion is always the mechanism.\n\nFile-review is on by default — when significant changes land in a step (or wave), invoke `/file-review:file-review <path>` for inline feedback (skip only if Autopilot).\n\n## Autonomy Mode\n\n| Mode | Behavior |\n|------|----------|\n| **Autopilot** | Drain the DAG without pausing. Only stop on blocker / failed step. |\n| **Critical** (Default) | Pause when each *wave* of parallel steps completes; wait for manual verification before unlocking the next wave. |\n| **Verbose** | Pause after each individual step (even within a wave). |\n\n## Initial Setup Questions\n\nAfter understanding the plan and before the scheduler loop starts, gather implementation-specific details (skip in Autopilot):\n\n### 1. Branch / Worktree Setup\n\nCheck the current branch: `git branch --show-current`. Then check if the `wts` plugin is installed (look for `wts:wts` in available skills).\n\n**If wts is installed**, use **AskUserQuestion**:\n\n| Question | Options |\n|----------|---------|\n| \"You're on `<current-branch>`. Where would you like to implement?\" | 1. Continue on current branch, 2. Create a new branch, 3. Create a wts worktree |\n\n**If wts is not installed**, drop the worktree option.\n\n### 2. Commit Strategy\n\nUse **AskUserQuestion**:\n\n| Question | Options |\n|----------|---------|\n| \"How would you like to handle commits during parallel implementation?\" | 1. Commit after each step completes (Recommended), 2. Commit at the end (single commit), 3. Let me decide as I go |\n\nIf \"Commit after each step\" is selected, after a step's manual verification passes, create a commit: `[step-N] <step name>`.\n\n### 3. Orchestration Mode (only if the `Workflow` tool is available)\n\n| Question | Options |\n|----------|---------|\n| \"Run the DAG waves as Workflow scripts? (deterministic scheduling, parallel steps as agent() calls)\" | 1. Yes — Workflow waves, 2. No — Agent fan-out (Default) |\n\nA \"yes\" is the explicit opt-in the Workflow tool requires. If opted in, each wave of the scheduler loop below runs as one Workflow script — a `parallel()` of `agent()` calls, one per ready step, with `model`/`effort` per `desplega:delegate-work`. Everything else is unchanged: step agents still follow the `step-running` contract (atomic frontmatter claim, three-bucket verification), and autonomy-mode pause points sit **between** Workflow invocations, never inside one. In Autopilot these questions are skipped, so there is no opt-in — use Agent fan-out unless the user pre-authorized workflows.\n\n### 4. Code Review Mode\n\n| Question | Options |\n|----------|---------|\n| \"Run an automatic code review after each wave?\" | 1. Automatic two-axis review per wave (Recommended), 2. Only at the end (single review after the DAG drains), 3. Off — I'll review myself |\n\nIn **Autopilot**, skip the question and default to automatic per-wave review.\n\n## Getting Started\n\nGiven a plan directory path:\n\n1. **Read `root.md` fully** (no `limit`/`offset`). Capture: Overview, Current State, Desired End State, Implementation Approach, Global Verification.\n2. **Read every `step-<n>.md`'s frontmatter** to build the dependency graph (`id`, `depends_on`). You don't need to read step bodies up front — the sub-agents will do that.\n3. **Validate the DAG:**\n - No cycles\n - Every `depends_on` ID exists as a step file\n - At least one step has `depends_on: []` (otherwise nothing can start)\n4. **Set `root.md` frontmatter `status: in-progress`.**\n5. **Build a TodoWrite list** with one entry per step, marked `pending`.\n6. **Resume support:** if any step's body has `- [x]` boxes already (or its frontmatter says `status: completed` if the user added it), trust them and treat that step as `done`.\n\nIf no path given, ask via AskUserQuestion.\n\n## The Scheduler Loop\n\nThe scheduler reads each step's frontmatter `status` (`ready` | `claimed` | `done`) and `depends_on`. The frontmatter `status` field is the **single source of truth** — multiple orchestrator instances on the same plan dir coordinate through it (each `desplega:step-running` sub-agent atomically claims its step before doing work).\n\n```\nwhile any step has status != done:\n ready = [step for step in steps\n if step.status == \"ready\"\n and all(dep.status == \"done\" for dep in step.depends_on)]\n if not ready:\n # DAG drained, OR every remaining undone step is claimed by another worker\n if any step has status == claimed: wait for in-flight claims to resolve\n else: report stuck (likely cycle or unrecoverable failure)\n continue\n fan out each step in `ready` as a parallel `desplega:step-running` sub-agent\n wait for the wave to complete\n review reports; step-running has already updated frontmatter\n (status: done on success, status: ready on retry-able failure, status: claimed if held for investigation)\n if Critical mode: pause for manual verification of newly-completed steps\n if any failed: stop and ask user how to proceed\n```\n\n### Spawning a Step Sub-agent\n\nUse the `Agent` tool with `run_in_background: true`, invoking `desplega:step-running`. Pass:\n\n- **Step path**: full path to `step-<n>.md`\n- **Plan dir**: full path to the parent plan directory\n- **Agent ID**: unique ID for this sub-agent (e.g. orchestrator session ID + step ID + timestamp). step-running uses this for atomic claim + stale-claim detection.\n- **Plan-level context** (optional): a quick brief from `root.md`. step-running re-reads `root.md` itself, so this is just to reduce round-trips.\n\n`step-running` owns:\n- Atomic claim (rewrite frontmatter `status: ready` → `status: claimed, assignee: <agent-id>, claimed_at: <ts>`)\n- Three-bucket verification (Automated Verification, Automated QA, QA Doc identification)\n- Terminal status transition (`status: done` on success, `status: ready` on retry-able failure)\n- Reporting back\n\nThe orchestrator does NOT do step work itself — it delegates. See `desplega:step-running` for the full sub-agent contract.\n\n**Executor routing**: if the `desplega:delegate-work` skill is available, pick each step's executor per its routing matrix instead of defaulting to a `step-running` sub-agent — a step may route to a Codex variant (one worktree per parallel slice) or a specific Claude model tier. Only the executor choice changes; the scheduler, frontmatter claim protocol, and all other semantics here stay unchanged. When a step routes to Codex, the orchestrator owns the frontmatter bookkeeping that `step-running` would normally do (Codex must not edit plan files).\n\n### Wave Completion\n\nWhen a wave finishes:\n1. **Review each agent's report.** Mark steps `done` only if `completed` was reported.\n2. **Run the wave code review** (unless review mode is Off) — invoke `desplega:code-reviewing` on the wave's combined diff: Standards + Spec axes as parallel background sub-agents (routed per `desplega:delegate-work`), spec source = the completed steps' bodies and Success Criteria. Critical findings block those steps' commits — fix and re-verify first. If \"Only at the end\" was selected, run one review over the full diff after the DAG drains instead.\n3. **Handle `QA Doc: <path>`** — for each step that reported a QA doc, invoke `desplega:qa` against that path. The Automated QA bucket inside the step is already handled by the sub-agent; only the linked doc needs separate orchestration here. (`QA: n/a` → proceed normally.)\n4. **Manual verification (if not Autopilot)** — present manual verification items from each completed step's body. Wait for user confirmation. Don't tick manual boxes until confirmed.\n5. **Commits (if commit-per-step was selected)** — after a step's manual verification passes, create a commit: `[step-N] <step name>`.\n6. **Loop** — recompute `ready` and start the next wave.\n\n## Handling Failures and Mismatches\n\nIf a sub-agent reports `failed` or `blocked`, or you spot a mismatch between plan and reality:\n\n| Question | Options |\n|----------|---------|\n| \"[step-N] [issue]. How should I proceed?\" | 1. Adapt step (edit step-N.md and retry), 2. Retry as-is, 3. Skip and continue (mark step blocked), 4. Stop the run |\n\nIn Autopilot mode, use best judgment, document the decision in the step file, and continue if non-fatal.\n\n## After the DAG Drains\n\nWhen every step is `done`:\n\n1. Run **Global Verification** from `root.md`. Tick automated checks; surface manual checks to the user.\n2. Set `root.md` frontmatter `status: completed`.\n3. Offer post-implementation auditing: \"Would you like me to run `/verify-plan` and `/review` on the plan directory?\"\n4. If commit-per-step was off, offer to create a single bundled commit now.\n5. If the work ships as a GitHub PR: once review comments land, address them with `desplega:tackle-gh-comments` (fetch threads, verify each claim, fix, reply, resolve — push last).\n\n## Important Guidelines\n\n1. **One step = one sub-agent.** Don't bundle steps into a single agent even if they're in the same wave — fan-out is the whole point.\n2. **Plan-level context goes to every sub-agent.** Sibling step bodies do not.\n3. **The DAG is canonical via step frontmatter** — if `root.md`'s table is out of sync, trust the frontmatter.\n4. **Resume works step-granular.** If the user re-runs `/v-implement` mid-DAG, completed steps stay completed; only undone ones rerun.\n5. **Don't tick manual checkboxes** until the user confirms — automated boxes can be ticked by the sub-agent or the orchestrator.\n\n## Review Integration\n\nIf `file-review` is available and the user opted in:\n- After each step's significant code changes, invoke `/file-review:file-review <changed-file>`.\n- Process feedback with `file-review:process-review` before treating the step as done.\n- Skip in Autopilot.\n";
|
|
11814
12006
|
|
|
11815
12007
|
// templates/skills/v-planning/config.json
|
|
11816
|
-
var
|
|
12008
|
+
var config_default49 = `{
|
|
11817
12009
|
"category": "skills",
|
|
11818
12010
|
"description": "Vertical / parallel implementation planning skill. Creates DAG-structured plan directories where each step is an independent, QA-able vertical slice that sub-agents can pick up and implement in parallel. Use whenever the user wants a plan that fans out (multiple independent features), invokes /v-plan, or asks for a \\"parallel plan\\", \\"DAG plan\\", \\"vertical plan\\", or \\"plan that can be parallelized\\" — even if they don't say those exact words. Prefer the linear \`planning\` skill for strictly sequential work.",
|
|
11819
12011
|
"displayName": "V-Planning",
|
|
@@ -11834,7 +12026,7 @@ var config_default48 = `{
|
|
|
11834
12026
|
`;
|
|
11835
12027
|
|
|
11836
12028
|
// templates/skills/v-planning/content.md
|
|
11837
|
-
var
|
|
12029
|
+
var content_default49 = `# v-planning
|
|
11838
12030
|
|
|
11839
12031
|
You create implementation plans as a **DAG of vertical steps** — each step a complete, QA-able slice of value (DB + API + UI + tests for one feature). The DAG captures feature-level dependencies, so independent steps can be implemented in parallel by sub-agents.
|
|
11840
12032
|
|
|
@@ -11925,7 +12117,7 @@ Structure validation runs automatically (rule 9, Haiku sub-agent).
|
|
|
11925
12117
|
`;
|
|
11926
12118
|
|
|
11927
12119
|
// templates/skills/verifying/config.json
|
|
11928
|
-
var
|
|
12120
|
+
var config_default50 = `{
|
|
11929
12121
|
"category": "skills",
|
|
11930
12122
|
"description": "Post-implementation plan verification. Cross-references plans against actual changes for completeness and accuracy.",
|
|
11931
12123
|
"displayName": "Verifying",
|
|
@@ -11946,7 +12138,7 @@ var config_default49 = `{
|
|
|
11946
12138
|
`;
|
|
11947
12139
|
|
|
11948
12140
|
// templates/skills/verifying/content.md
|
|
11949
|
-
var
|
|
12141
|
+
var content_default50 = `# Verifying
|
|
11950
12142
|
|
|
11951
12143
|
You are performing a post-implementation audit of a plan, cross-referencing it against actual changes to ensure nothing was missed, nothing is stale, and the implementation matches what was planned.
|
|
11952
12144
|
|
|
@@ -12094,7 +12286,7 @@ File-review is on by default (unless Autopilot):
|
|
|
12094
12286
|
`;
|
|
12095
12287
|
|
|
12096
12288
|
// templates/skills/work-on-task/config.json
|
|
12097
|
-
var
|
|
12289
|
+
var config_default51 = `{
|
|
12098
12290
|
"kind": "skill",
|
|
12099
12291
|
"name": "work-on-task",
|
|
12100
12292
|
"displayName": "Work On Task",
|
|
@@ -12111,10 +12303,10 @@ var config_default50 = `{
|
|
|
12111
12303
|
`;
|
|
12112
12304
|
|
|
12113
12305
|
// templates/skills/work-on-task/content.md
|
|
12114
|
-
var
|
|
12306
|
+
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; 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";
|
|
12115
12307
|
|
|
12116
12308
|
// templates/skills/workflow-iterate/config.json
|
|
12117
|
-
var
|
|
12309
|
+
var config_default52 = `{
|
|
12118
12310
|
"kind": "skill",
|
|
12119
12311
|
"name": "workflow-iterate",
|
|
12120
12312
|
"displayName": "Workflow Iteration",
|
|
@@ -12136,7 +12328,7 @@ var config_default51 = `{
|
|
|
12136
12328
|
`;
|
|
12137
12329
|
|
|
12138
12330
|
// templates/skills/workflow-iterate/content.md
|
|
12139
|
-
var
|
|
12331
|
+
var content_default52 = `# Workflow Iteration
|
|
12140
12332
|
|
|
12141
12333
|
Use this skill when you need to change an existing workflow without breaking live runs. The goal is to make small, verified revisions: inspect the current workflow, diagnose the failing step, patch only the required node or edge, trigger a realistic run, and keep iterating until the run reaches the intended terminal state.
|
|
12142
12334
|
|
|
@@ -12311,6 +12503,44 @@ Afterwards, a \`warnings\` entry on \`create-workflow\` or a patch tool means th
|
|
|
12311
12503
|
- Do not set \`retry\` or \`validation.retry\` on a \`system-one-decision\` node. It retries connection errors, 408, 429, and 5xx itself (\`config.maxRetries\`, 0 to 3, default 2) inside \`config.timeoutMs\` (default 30000). A 401, a 422, or an answer that fails validation ends after one attempt.
|
|
12312
12504
|
- The node reads its provider's key (\`TYPESAFE_API_KEY\`, \`OPENROUTER_API_KEY\`, \`LAYA_API_KEY\`, \`OPENAI_DECISIONS_API_KEY\`, or \`CLOUDFLARE_API_TOKEN\`, plus \`LAYA_URL\` for \`laya\` and \`CLOUDFLARE_ACCOUNT_ID\` for \`cloudflare\`) server-side. The key never appears in the definition, the step output, or an error. A run whose key or \`LAYA_URL\` is missing fails before any node executes, and a 401 or 403 fails the step as \`<KEY> was rejected\`. An unresolved \`{{token}}\` fails the step before any request.
|
|
12313
12505
|
|
|
12506
|
+
#### Jev images (direct TypeSafe API)
|
|
12507
|
+
|
|
12508
|
+
\`system-one-decision\` cannot send images today: its strict config has no \`assets\` field, its wire sends only \`state\`, \`model\`, and \`questions\`, and it does not send the image opt-in header. A URL or base64 string in \`state\` stays text. Use a \`swarm-script\` calling \`POST https://api.typesafe.ai/v1/systemone\` with the credential kept server-side until the node supports assets.
|
|
12509
|
+
|
|
12510
|
+
- Send \`TypeSafe-Danger-Zone-Images: true\`. Both \`jev-1.13.0\` and \`jev-1.14.0\` accept images; Box questions require 1.14. Pin the model you tested.
|
|
12511
|
+
- Upload PNG or JPEG bytes, not a URL. Prefer multipart: first part \`request\` is the JSON with content type \`application/json\`; an asset such as \`assets[uuid] = { type: "image", part: "photo-file" }\` names its file part. For JSON-only calls, use the base64 shape below: standard padded base64, no whitespace or \`data:\` prefix.
|
|
12512
|
+
- Each image must be still, at most 16 MiB decoded, 256–1568 pixels on each side, and 78,400–2,458,624 pixels total (256×256 is too small). Request body, all processed images together, and the expanded request each have a 32 MiB limit. Limits reject the whole request; the API does not resize. Rotate pixels upright before sending because EXIF orientation is not applied.
|
|
12513
|
+
- Generate a fresh UUID reference per image. References are replaced wherever they occur in string values in \`state\`, \`instructions\`, or \`criteria\`; object keys are not replaced. Every asset must be referenced. Repeating the same reference inserts and bills another image copy.
|
|
12514
|
+
|
|
12515
|
+
\`\`\`json
|
|
12516
|
+
{
|
|
12517
|
+
"model": "jev-1.13.0",
|
|
12518
|
+
"state": { "photo": "d1369c6a-538e-4ab3-91cd-f76f2f67b651" },
|
|
12519
|
+
"assets": {
|
|
12520
|
+
"d1369c6a-538e-4ab3-91cd-f76f2f67b651": {
|
|
12521
|
+
"type": "image", "format": "png", "data_base64": "<encoded PNG bytes>"
|
|
12522
|
+
}
|
|
12523
|
+
},
|
|
12524
|
+
"questions": {
|
|
12525
|
+
"ready": { "type": "noul", "instructions": "Is the product in \`photo\` fully assembled?" }
|
|
12526
|
+
}
|
|
12527
|
+
}
|
|
12528
|
+
\`\`\`
|
|
12529
|
+
|
|
12530
|
+
Measured 2026-10-15 on \`jev-1.13.0\`, using an 850×850 RGB PNG with a red square on white. Cost is the complete request's \`usage.input_tokens × $0.042 / 1,000,000\`; output is free:
|
|
12531
|
+
|
|
12532
|
+
| Placement | Questions | Input tokens | Request cost (USD) |
|
|
12533
|
+
|---|---:|---:|---:|
|
|
12534
|
+
| Once in \`state\` | 1 | 2,273 | $0.000095466 |
|
|
12535
|
+
| Once in \`state\`, questions use the \`photo\` path | 2 | 2,289 | $0.000096138 |
|
|
12536
|
+
| Twice in \`state\` | 1 | 4,237 | $0.000177954 |
|
|
12537
|
+
| Once in each question's \`instructions\` | 2 | 4,203 | $0.000176526 |
|
|
12538
|
+
| Once in \`state\`, resized to 425×425 | 1 | 863 | $0.000036246 |
|
|
12539
|
+
|
|
12540
|
+
The roughly $0.0001 claim (about 10,000 one-image requests per dollar) holds for this example; the exact cost depends on image size and prompt. **Question count alone does not multiply a shared state image's tokens.** Put shared images in \`state\` once and have questions use its field path, not the asset UUID. Image duplication scales with repeated references, including one copy per question when embedded in each question's instructions. If several judgments form one classification, a single \`choice\` can avoid repeating question-local images. Downscale when the detail permits: halving both dimensions reduced this request's billed tokens from 2,273 to 863; recheck accuracy on your images.
|
|
12541
|
+
|
|
12542
|
+
Contract and current pricing: [TypeSafe images](https://docs.typesafe.ai/images), [models](https://docs.typesafe.ai/models). Re-measure before budgeting a different model, resolution, or prompt.
|
|
12543
|
+
|
|
12314
12544
|
### \`human-in-the-loop\` (how approvals render)
|
|
12315
12545
|
|
|
12316
12546
|
The same rules apply to a standalone \`request-human-input\` call.
|
|
@@ -12368,7 +12598,7 @@ The same rules apply to a standalone \`request-human-input\` call.
|
|
|
12368
12598
|
`;
|
|
12369
12599
|
|
|
12370
12600
|
// templates/skills/workflow-structured-output/config.json
|
|
12371
|
-
var
|
|
12601
|
+
var config_default53 = `{
|
|
12372
12602
|
"kind": "skill",
|
|
12373
12603
|
"name": "workflow-structured-output",
|
|
12374
12604
|
"displayName": "Workflow Structured Output",
|
|
@@ -12385,10 +12615,10 @@ var config_default52 = `{
|
|
|
12385
12615
|
`;
|
|
12386
12616
|
|
|
12387
12617
|
// templates/skills/workflow-structured-output/content.md
|
|
12388
|
-
var
|
|
12618
|
+
var content_default53 = '# Workflow Structured Output\n\n> **Companion skill — `workflow-iterate` (author-side).** This skill is for *workers* assigned a task spawned by an `agent-task` workflow node with an `outputSchema`. The author-side counterpart `workflow-iterate` covers how the workflow defines that schema, why gates downstream depend on it, and how to debug failed runs. If you ever need to understand *why* a particular schema is shaped the way it is — or you\'re switching from worker to author mode — read `workflow-iterate`.\n\n## Failure reason → fix (read this first)\n\nIf you see this failure reason on a task, the fix is always the same:\n\n| failureReason contains | What it means | Fix |\n|---|---|---|\n| `Task has an outputSchema but no output was provided` | You called `store-progress` without an `output` when the task required a JSON object matching `outputSchema` | Re-call `store-progress` with `status: "completed"` and `output` = a **stringified JSON** matching the schema exactly |\n| `Task output must be valid JSON` / `Task output does not match the outputSchema` | You passed invalid JSON, omitted required fields, or used the wrong types | Re-read the schema, include every `required` field with the exact key names, re-call `store-progress` |\n\n**You can re-call `store-progress` even after a rejection.** Your prior progress updates do not count as the final output. Fix the JSON and try again.\n\n## Pre-flight checklist (run through before calling store-progress)\n\n1. **Do the task details contain an actual `outputSchema`?** If yes, runtime\n enforcement is active: continue with that exact schema.\n2. **If there is no `outputSchema`, does the user provide an explicit JSON Output\n Format or interface?** Honor it as the requested output contract even though\n the runner is not enforcing an `outputSchema`.\n3. **Are workflow origin or tags such as `deterministic`, `litmus`, `validation`,\n or `context` the only signals?** They are heuristics only. Re-read the task\n details for an actual schema or explicit format. If neither exists, do not\n invent one; plain-text output is allowed.\n4. **Can I quote the exact required shape from the task details?** Use its exact\n keys and types, with every required field and no guessed renames.\n5. **Is my `output` a string (stringified JSON), not an object?** For a structured\n contract, `store-progress.output` should be a JSON string.\n6. **Only after the applicable contract is clear:** call `store-progress` with\n `status: "completed"` and the stringified JSON, or use plain text when no\n structured contract exists.\n\nIf the required shape is unclear, re-read the task details. Never synthesize a\nschema from tags or workflow origin.\n\n## Why this exists\n\nWhen a task has an actual `outputSchema`, the runner validates\n`store-progress.output` against it and rejects missing output, invalid JSON, or\nschema mismatches with one of the current messages listed above. A JSON Output\nFormat or interface without `task.outputSchema` remains a user instruction, but\nit is not the same runtime enforcement mechanism. Missing the distinction can\nturn completed work into a rejected terminal update, so check the task contract\nbefore reporting completion.\n\n## How to spot a structured-output task\n\nTreat these signals differently:\n\n- An actual `task.outputSchema` means JSON is runtime-enforced.\n- An explicit "Output Format", "Return a JSON object", or TypeScript/JSON\n interface is a user contract to return that shape, even without runtime\n enforcement.\n- Tags such as `deterministic`, `litmus`, `validation`, `releases`, or `context`,\n and `source = workflow`, only tell you to inspect the task details carefully.\n They do not prove a schema exists.\n\nWhen in doubt, re-read the task details and its `outputSchema`. Do not invent a\nschema or required fields.\n\n## How to complete correctly\n\n1. **Build the JSON object** that matches the actual `outputSchema` or explicit\n user-provided JSON format. Include every required field and use the exact key\n names from that contract.\n2. **Stringify it** — `store-progress.output` must be a string, not an object. Use `JSON.stringify(obj)` in your head.\n3. **Call store-progress** with `status: "completed"` and that JSON string as `output`.\n\n### Example — skip case\n\nTask has schema `{skip: bool, reason?: str, contextPath?: str, ...}` and you\'re skipping because the release already exists:\n\n```\nstore-progress(\n taskId,\n status="completed",\n output=\'{"skip":true,"reason":"Release already exists for this week"}\'\n)\n```\n\n### Example — full run\n\n```\nstore-progress(\n taskId,\n status="completed",\n output=\'{"skip":false,"contextPath":"release-runs/2026-04-20/context.json","commitCount":42,"repos":["example-repo"],"repoPatternsSource":"cache","dateRange":"2026-04-13 to 2026-04-20"}\'\n)\n```\n\n### Example — litmus / validation verdict\n\n```\nstore-progress(\n taskId,\n status="completed",\n output=\'{"verdict":"publish","reason":"5 user-facing changes; threshold met"}\'\n)\n```\n\n## Anti-patterns for an `outputSchema` task\n\n- `output: "Done. Context written to agent-fs at release-runs/2026-04-20/context.json"`\n- `output: "Published release notes for week of 2026-04-20"`\n- `output: "Verdict: publish"`\n- Calling `store-progress` with `status: "completed"` and no `output` at all\n- JSON that\'s missing any `required` field from the schema\n- JSON with extra keys instead of the schema\'s exact keys\n\n## Recovery if you realize mid-task\n\nIf you\'ve done all the work but forgot the JSON contract, just call `store-progress` again with the correct JSON string in `output`. Your prior progress updates don\'t count as the final output.\n\n## Verify before completing\n\nRead your task description once more. Find the schema. Build the JSON. Then complete. Takes 30 seconds. Saves a workflow re-run.\n\n## See also\n\n- **`workflow-iterate`** — the author-side counterpart of this skill. Read it if you\'re editing the workflow that produced this task, or want to understand why this particular schema is shaped the way it is (which gates downstream depend on it, etc.).\n';
|
|
12389
12619
|
|
|
12390
12620
|
// templates/skills/wts-expert/config.json
|
|
12391
|
-
var
|
|
12621
|
+
var config_default54 = `{
|
|
12392
12622
|
"category": "skills",
|
|
12393
12623
|
"description": "Git worktree management expert for @desplega.ai/wts. Use when the user asks about git worktrees, wts commands, worktree workflows, or wants help managing multiple branches simultaneously.",
|
|
12394
12624
|
"displayName": "WTS Expert",
|
|
@@ -12409,7 +12639,7 @@ var config_default53 = `{
|
|
|
12409
12639
|
`;
|
|
12410
12640
|
|
|
12411
12641
|
// templates/skills/wts-expert/content.md
|
|
12412
|
-
var
|
|
12642
|
+
var content_default54 = `# WTS Expert
|
|
12413
12643
|
|
|
12414
12644
|
You are an expert on \`@desplega.ai/wts\`, a CLI tool for managing Git worktrees with tmux integration, Claude Code launcher support, and GitHub PR creation.
|
|
12415
12645
|
|
|
@@ -12559,128 +12789,24 @@ For complete command documentation with all flags and options, see [COMMANDS.md]
|
|
|
12559
12789
|
|
|
12560
12790
|
// src/be/seed-skills/index.ts
|
|
12561
12791
|
init_db2();
|
|
12792
|
+
|
|
12793
|
+
// src/be/seed/state-db.ts
|
|
12794
|
+
init_db2();
|
|
12795
|
+
async function getSeedState(kind, key) {
|
|
12796
|
+
return getDbClient().get("SELECT kind, key, seededHash, seededAt FROM seed_state WHERE kind = ? AND key = ?", [kind, key]);
|
|
12797
|
+
}
|
|
12798
|
+
async function recordSeedState(kind, key, seededHash) {
|
|
12799
|
+
await getDbClient().run(`INSERT INTO seed_state (kind, key, seededHash, seededAt)
|
|
12800
|
+
VALUES (?, ?, ?, ?)
|
|
12801
|
+
ON CONFLICT(kind, key) DO UPDATE SET
|
|
12802
|
+
seededHash = excluded.seededHash,
|
|
12803
|
+
seededAt = excluded.seededAt`, [kind, key, seededHash, new Date().toISOString()]);
|
|
12804
|
+
}
|
|
12805
|
+
async function deleteSeedState(kind, key) {
|
|
12806
|
+
await getDbClient().run("DELETE FROM seed_state WHERE kind = ? AND key = ?", [kind, key]);
|
|
12807
|
+
}
|
|
12562
12808
|
// src/be/seed-skills/bundled-files.generated.json
|
|
12563
12809
|
var bundled_files_generated_default = {
|
|
12564
|
-
artifacts: [
|
|
12565
|
-
{
|
|
12566
|
-
path: "examples/approval-flow.ts",
|
|
12567
|
-
content: `import { createArtifactServer } from "../../src/artifact-sdk";
|
|
12568
|
-
import { Hono } from "hono";
|
|
12569
|
-
|
|
12570
|
-
const app = new Hono();
|
|
12571
|
-
app.get("/", (c) =>
|
|
12572
|
-
c.html(\`
|
|
12573
|
-
<!DOCTYPE html>
|
|
12574
|
-
<html>
|
|
12575
|
-
<head><title>Approval Required</title></head>
|
|
12576
|
-
<body>
|
|
12577
|
-
<h1>PR #42 — Review Required</h1>
|
|
12578
|
-
<p>Agent wants to merge this PR. Please review.</p>
|
|
12579
|
-
<button id="approve">Approve</button>
|
|
12580
|
-
<button id="reject">Reject</button>
|
|
12581
|
-
<script src="/@swarm/sdk.js"></script>
|
|
12582
|
-
<script>
|
|
12583
|
-
const swarm = new SwarmSDK();
|
|
12584
|
-
document.getElementById('approve').onclick = async () => {
|
|
12585
|
-
await swarm.createTask({ task: 'Merge PR #42 — human approved' });
|
|
12586
|
-
document.body.innerHTML = '<h1>Approved! Task created.</h1>';
|
|
12587
|
-
};
|
|
12588
|
-
document.getElementById('reject').onclick = async () => {
|
|
12589
|
-
await swarm.createTask({ task: 'PR #42 rejected by human — needs changes' });
|
|
12590
|
-
document.body.innerHTML = '<h1>Rejected. Agent notified.</h1>';
|
|
12591
|
-
};
|
|
12592
|
-
</script>
|
|
12593
|
-
</body>
|
|
12594
|
-
</html>
|
|
12595
|
-
\`),
|
|
12596
|
-
);
|
|
12597
|
-
|
|
12598
|
-
const server = createArtifactServer({ name: "approval-pr-42", app });
|
|
12599
|
-
await server.start();
|
|
12600
|
-
console.log(\`Approval artifact at: \${server.url}\`);
|
|
12601
|
-
`
|
|
12602
|
-
},
|
|
12603
|
-
{
|
|
12604
|
-
path: "examples/hono-dashboard.ts",
|
|
12605
|
-
content: `import { createArtifactServer } from "../../src/artifact-sdk";
|
|
12606
|
-
import { Hono } from "hono";
|
|
12607
|
-
|
|
12608
|
-
const app = new Hono();
|
|
12609
|
-
|
|
12610
|
-
app.get("/", (c) =>
|
|
12611
|
-
c.html(\`
|
|
12612
|
-
<!DOCTYPE html>
|
|
12613
|
-
<html>
|
|
12614
|
-
<head><title>Dashboard</title></head>
|
|
12615
|
-
<body>
|
|
12616
|
-
<h1>Agent Dashboard</h1>
|
|
12617
|
-
<div id="agents"></div>
|
|
12618
|
-
<script src="/@swarm/sdk.js"></script>
|
|
12619
|
-
<script>
|
|
12620
|
-
const swarm = new SwarmSDK();
|
|
12621
|
-
swarm.getSwarm().then(agents => {
|
|
12622
|
-
document.getElementById('agents').innerHTML =
|
|
12623
|
-
'<ul>' + agents.map(a => '<li>' + a.name + ' (' + a.status + ')</li>').join('') + '</ul>';
|
|
12624
|
-
});
|
|
12625
|
-
</script>
|
|
12626
|
-
</body>
|
|
12627
|
-
</html>
|
|
12628
|
-
\`),
|
|
12629
|
-
);
|
|
12630
|
-
|
|
12631
|
-
app.get("/api/status", (c) => c.json({ status: "ok", timestamp: Date.now() }));
|
|
12632
|
-
|
|
12633
|
-
const server = createArtifactServer({ name: "dashboard", app });
|
|
12634
|
-
await server.start();
|
|
12635
|
-
console.log(\`Dashboard live at: \${server.url}\`);
|
|
12636
|
-
`
|
|
12637
|
-
},
|
|
12638
|
-
{
|
|
12639
|
-
path: "examples/multi-artifact.ts",
|
|
12640
|
-
content: `import { createArtifactServer } from "../../src/artifact-sdk";
|
|
12641
|
-
import { Hono } from "hono";
|
|
12642
|
-
|
|
12643
|
-
// Artifact 1: Status page
|
|
12644
|
-
const statusApp = new Hono();
|
|
12645
|
-
statusApp.get("/", (c) => c.html("<h1>Status: All systems operational</h1>"));
|
|
12646
|
-
|
|
12647
|
-
// Artifact 2: Data viewer
|
|
12648
|
-
const dataApp = new Hono();
|
|
12649
|
-
dataApp.get("/", (c) => c.json({ items: [1, 2, 3], generated: new Date().toISOString() }));
|
|
12650
|
-
|
|
12651
|
-
const status = createArtifactServer({ name: "status", app: statusApp });
|
|
12652
|
-
const data = createArtifactServer({ name: "data", app: dataApp });
|
|
12653
|
-
|
|
12654
|
-
await status.start();
|
|
12655
|
-
await data.start();
|
|
12656
|
-
|
|
12657
|
-
console.log(\`Status page: \${status.url}\`);
|
|
12658
|
-
console.log(\`Data viewer: \${data.url}\`);
|
|
12659
|
-
console.log(\`Both artifacts running on different ports (\${status.port}, \${data.port})\`);
|
|
12660
|
-
`
|
|
12661
|
-
},
|
|
12662
|
-
{
|
|
12663
|
-
path: "examples/static-report.sh",
|
|
12664
|
-
content: `#!/bin/bash
|
|
12665
|
-
# Serve a static HTML report as an artifact
|
|
12666
|
-
ARTIFACT_DIR="/workspace/personal/artifacts/my-report"
|
|
12667
|
-
mkdir -p "$ARTIFACT_DIR"
|
|
12668
|
-
|
|
12669
|
-
cat > "$ARTIFACT_DIR/index.html" << 'HTML'
|
|
12670
|
-
<!DOCTYPE html>
|
|
12671
|
-
<html>
|
|
12672
|
-
<head><title>Agent Report</title></head>
|
|
12673
|
-
<body>
|
|
12674
|
-
<h1>Analysis Report</h1>
|
|
12675
|
-
<p>Generated by agent</p>
|
|
12676
|
-
</body>
|
|
12677
|
-
</html>
|
|
12678
|
-
HTML
|
|
12679
|
-
|
|
12680
|
-
agent-swarm artifact serve "$ARTIFACT_DIR" --name "my-report"
|
|
12681
|
-
`
|
|
12682
|
-
}
|
|
12683
|
-
],
|
|
12684
12810
|
brainstorming: [
|
|
12685
12811
|
{
|
|
12686
12812
|
path: "template.md",
|
|
@@ -15491,12 +15617,12 @@ var BUILT_IN_SKILL_SOURCES = [
|
|
|
15491
15617
|
{ config: config_default35, body: content_default35 },
|
|
15492
15618
|
{ config: config_default36, body: content_default36 },
|
|
15493
15619
|
{ config: config_default37, body: content_default37 },
|
|
15494
|
-
{ config: config_default42, body: content_default42 },
|
|
15495
15620
|
{ config: config_default38, body: content_default38 },
|
|
15621
|
+
{ config: config_default43, body: content_default43 },
|
|
15496
15622
|
{ config: config_default39, body: content_default39 },
|
|
15497
15623
|
{ config: config_default40, body: content_default40 },
|
|
15498
15624
|
{ config: config_default41, body: content_default41 },
|
|
15499
|
-
{ config:
|
|
15625
|
+
{ config: config_default42, body: content_default42 },
|
|
15500
15626
|
{ config: config_default44, body: content_default44 },
|
|
15501
15627
|
{ config: config_default45, body: content_default45 },
|
|
15502
15628
|
{ config: config_default46, body: content_default46 },
|
|
@@ -15506,7 +15632,8 @@ var BUILT_IN_SKILL_SOURCES = [
|
|
|
15506
15632
|
{ config: config_default50, body: content_default50 },
|
|
15507
15633
|
{ config: config_default51, body: content_default51 },
|
|
15508
15634
|
{ config: config_default52, body: content_default52 },
|
|
15509
|
-
{ config: config_default53, body: content_default53 }
|
|
15635
|
+
{ config: config_default53, body: content_default53 },
|
|
15636
|
+
{ config: config_default54, body: content_default54 }
|
|
15510
15637
|
];
|
|
15511
15638
|
function canonicalFiles(files) {
|
|
15512
15639
|
return [...files].sort((a, b) => a.path.localeCompare(b.path)).map((file) => `${file.path}
|
|
@@ -15596,6 +15723,42 @@ async function syncSeededSkillFiles(skillId, files) {
|
|
|
15596
15723
|
if (files.length > 0)
|
|
15597
15724
|
await upsertSkillFiles(skillId, files);
|
|
15598
15725
|
}
|
|
15726
|
+
var RETIRED_SEED_SKILLS = ["artifacts"];
|
|
15727
|
+
function metadataAsSeeded(skill, fileCount) {
|
|
15728
|
+
let seededDescription;
|
|
15729
|
+
try {
|
|
15730
|
+
seededDescription = parseSkillContent(skill.content).description;
|
|
15731
|
+
} catch {
|
|
15732
|
+
return false;
|
|
15733
|
+
}
|
|
15734
|
+
return skill.description === seededDescription && skill.type === "personal" && skill.isComplex === fileCount > 0 && skill.isEnabled && !skill.disableModelInvocation && skill.allowedTools === null && skill.model === null && skill.effort === null && skill.context === null && skill.agent === null && skill.sourceUrl === null && skill.sourceRepo === null && skill.sourcePath === null && skill.sourceBranch === "main" && skill.sourceHash === null && skill.lastFetchedAt === null;
|
|
15735
|
+
}
|
|
15736
|
+
async function retireSeedSkills(names = RETIRED_SEED_SKILLS) {
|
|
15737
|
+
const retired = [];
|
|
15738
|
+
for (const name of names) {
|
|
15739
|
+
const existing = await getSkillByName(name, "swarm");
|
|
15740
|
+
if (!existing)
|
|
15741
|
+
continue;
|
|
15742
|
+
const state = await getSeedState("skill", name);
|
|
15743
|
+
if (!state)
|
|
15744
|
+
continue;
|
|
15745
|
+
const liveFiles = (await getSkillFiles(existing.id)).map((file) => ({
|
|
15746
|
+
path: file.path,
|
|
15747
|
+
content: file.content
|
|
15748
|
+
}));
|
|
15749
|
+
const liveHash = skillSeedHash(existing.content, existing.systemDefault, liveFiles, existing.userInvocable);
|
|
15750
|
+
if (liveHash !== state.seededHash || !metadataAsSeeded(existing, liveFiles.length)) {
|
|
15751
|
+
console.warn(`[seed:skill] kept retired skill "${name}": edited since it was seeded`);
|
|
15752
|
+
continue;
|
|
15753
|
+
}
|
|
15754
|
+
await getDbClient().transaction(async () => {
|
|
15755
|
+
await deleteSkill(existing.id);
|
|
15756
|
+
await deleteSeedState("skill", name);
|
|
15757
|
+
});
|
|
15758
|
+
retired.push(name);
|
|
15759
|
+
}
|
|
15760
|
+
return retired;
|
|
15761
|
+
}
|
|
15599
15762
|
var skillsSeeder = {
|
|
15600
15763
|
kind: "skill",
|
|
15601
15764
|
async items() {
|
|
@@ -15646,22 +15809,10 @@ var skillsSeeder = {
|
|
|
15646
15809
|
});
|
|
15647
15810
|
await syncSeededSkillFiles(created.id, skill.files);
|
|
15648
15811
|
});
|
|
15649
|
-
}
|
|
15812
|
+
},
|
|
15813
|
+
retire: () => retireSeedSkills()
|
|
15650
15814
|
};
|
|
15651
15815
|
|
|
15652
|
-
// src/be/seed/state-db.ts
|
|
15653
|
-
init_db2();
|
|
15654
|
-
async function getSeedState(kind, key) {
|
|
15655
|
-
return getDbClient().get("SELECT kind, key, seededHash, seededAt FROM seed_state WHERE kind = ? AND key = ?", [kind, key]);
|
|
15656
|
-
}
|
|
15657
|
-
async function recordSeedState(kind, key, seededHash) {
|
|
15658
|
-
await getDbClient().run(`INSERT INTO seed_state (kind, key, seededHash, seededAt)
|
|
15659
|
-
VALUES (?, ?, ?, ?)
|
|
15660
|
-
ON CONFLICT(kind, key) DO UPDATE SET
|
|
15661
|
-
seededHash = excluded.seededHash,
|
|
15662
|
-
seededAt = excluded.seededAt`, [kind, key, seededHash, new Date().toISOString()]);
|
|
15663
|
-
}
|
|
15664
|
-
|
|
15665
15816
|
// src/be/seed/runner.ts
|
|
15666
15817
|
async function runSeeder(seeder, opts) {
|
|
15667
15818
|
const result = {
|
|
@@ -15670,6 +15821,7 @@ async function runSeeder(seeder, opts) {
|
|
|
15670
15821
|
updated: 0,
|
|
15671
15822
|
skippedUnchanged: 0,
|
|
15672
15823
|
skippedUserModified: 0,
|
|
15824
|
+
retired: 0,
|
|
15673
15825
|
failed: []
|
|
15674
15826
|
};
|
|
15675
15827
|
const items = await seeder.items();
|
|
@@ -15705,8 +15857,18 @@ async function runSeeder(seeder, opts) {
|
|
|
15705
15857
|
});
|
|
15706
15858
|
}
|
|
15707
15859
|
}
|
|
15860
|
+
if (seeder.retire) {
|
|
15861
|
+
try {
|
|
15862
|
+
result.retired = (await seeder.retire()).length;
|
|
15863
|
+
} catch (err) {
|
|
15864
|
+
result.failed.push({
|
|
15865
|
+
key: "(retire)",
|
|
15866
|
+
error: err instanceof Error ? err.message : String(err)
|
|
15867
|
+
});
|
|
15868
|
+
}
|
|
15869
|
+
}
|
|
15708
15870
|
if (!opts?.quiet) {
|
|
15709
|
-
console.log(`[seed:${result.kind}] created=${result.created} updated=${result.updated} ` + `unchanged=${result.skippedUnchanged} preserved=${result.skippedUserModified} ` + `failed=${result.failed.length}`);
|
|
15871
|
+
console.log(`[seed:${result.kind}] created=${result.created} updated=${result.updated} ` + `unchanged=${result.skippedUnchanged} preserved=${result.skippedUserModified} ` + `retired=${result.retired} failed=${result.failed.length}`);
|
|
15710
15872
|
for (const f of result.failed) {
|
|
15711
15873
|
console.error(`[seed:${result.kind}] FAILED ${f.key}: ${f.error}`);
|
|
15712
15874
|
}
|
|
@@ -15725,7 +15887,7 @@ async function runSeeders(seeders, opts) {
|
|
|
15725
15887
|
var import_cron_parser = __toESM(require_dist(), 1);
|
|
15726
15888
|
|
|
15727
15889
|
// templates/schedules/daily-blocker-digest/config.json
|
|
15728
|
-
var
|
|
15890
|
+
var config_default55 = `{
|
|
15729
15891
|
"kind": "schedule",
|
|
15730
15892
|
"name": "daily-blocker-digest",
|
|
15731
15893
|
"displayName": "Daily Blocker Digest",
|
|
@@ -15747,7 +15909,7 @@ var config_default54 = `{
|
|
|
15747
15909
|
`;
|
|
15748
15910
|
|
|
15749
15911
|
// templates/schedules/daily-blocker-digest/content.md
|
|
15750
|
-
var
|
|
15912
|
+
var content_default55 = `# Daily Blocker Digest
|
|
15751
15913
|
|
|
15752
15914
|
Delivery uses configured admin channels with an in-app fallback.
|
|
15753
15915
|
|
|
@@ -15916,7 +16078,7 @@ Call \`store-progress\` with status \`completed\` and an output paragraph coveri
|
|
|
15916
16078
|
`;
|
|
15917
16079
|
|
|
15918
16080
|
// templates/schedules/daily-compounding-reflection/config.json
|
|
15919
|
-
var
|
|
16081
|
+
var config_default56 = `{
|
|
15920
16082
|
"kind": "schedule",
|
|
15921
16083
|
"name": "daily-compounding-reflection",
|
|
15922
16084
|
"displayName": "Daily Evolution",
|
|
@@ -15939,7 +16101,7 @@ var config_default55 = `{
|
|
|
15939
16101
|
`;
|
|
15940
16102
|
|
|
15941
16103
|
// templates/schedules/daily-compounding-reflection/content.md
|
|
15942
|
-
var
|
|
16104
|
+
var content_default56 = `# Daily Compounding Reflection
|
|
15943
16105
|
|
|
15944
16106
|
Delivery uses configured admin channels with an in-app fallback.
|
|
15945
16107
|
|
|
@@ -16164,7 +16326,7 @@ If you have zero changes across all three folds and zero deferred items, somethi
|
|
|
16164
16326
|
`;
|
|
16165
16327
|
|
|
16166
16328
|
// templates/schedules/daily-hn-briefing/config.json
|
|
16167
|
-
var
|
|
16329
|
+
var config_default57 = `{
|
|
16168
16330
|
"kind": "schedule",
|
|
16169
16331
|
"name": "daily-hn-briefing",
|
|
16170
16332
|
"displayName": "Daily Hacker News Briefing",
|
|
@@ -16181,7 +16343,7 @@ var config_default56 = `{
|
|
|
16181
16343
|
`;
|
|
16182
16344
|
|
|
16183
16345
|
// templates/schedules/daily-hn-briefing/content.md
|
|
16184
|
-
var
|
|
16346
|
+
var content_default57 = `# Daily Hacker News Briefing
|
|
16185
16347
|
|
|
16186
16348
|
Template parameters: {{REPORT_EMAIL}}
|
|
16187
16349
|
|
|
@@ -16283,7 +16445,7 @@ IMPORTANT:
|
|
|
16283
16445
|
`;
|
|
16284
16446
|
|
|
16285
16447
|
// templates/schedules/daily-status-report/config.json
|
|
16286
|
-
var
|
|
16448
|
+
var config_default58 = `{
|
|
16287
16449
|
"kind": "schedule",
|
|
16288
16450
|
"name": "daily-status-report",
|
|
16289
16451
|
"displayName": "Daily Status Report",
|
|
@@ -16305,7 +16467,7 @@ var config_default57 = `{
|
|
|
16305
16467
|
`;
|
|
16306
16468
|
|
|
16307
16469
|
// templates/schedules/daily-status-report/content.md
|
|
16308
|
-
var
|
|
16470
|
+
var content_default58 = `# Daily Status Report
|
|
16309
16471
|
|
|
16310
16472
|
Give the operator one daily read on whether the swarm is healthy: what's running, what's failed, what's waiting on them.
|
|
16311
16473
|
|
|
@@ -16401,7 +16563,7 @@ More than 3 qualify ⇒ post the 3 that matter and put the rest in the task outp
|
|
|
16401
16563
|
`;
|
|
16402
16564
|
|
|
16403
16565
|
// templates/schedules/daily-swarm-update-check/config.json
|
|
16404
|
-
var
|
|
16566
|
+
var config_default59 = `{
|
|
16405
16567
|
"kind": "schedule",
|
|
16406
16568
|
"name": "daily-swarm-update-check",
|
|
16407
16569
|
"displayName": "Daily Agent-Swarm Update Check",
|
|
@@ -16423,7 +16585,7 @@ var config_default58 = `{
|
|
|
16423
16585
|
`;
|
|
16424
16586
|
|
|
16425
16587
|
// templates/schedules/daily-swarm-update-check/content.md
|
|
16426
|
-
var
|
|
16588
|
+
var content_default59 = `# Daily Agent-Swarm Update Check
|
|
16427
16589
|
|
|
16428
16590
|
Zero-config auto-update watchdog: compares the swarm's own running version against the latest published GitHub release of \`desplega-ai/agent-swarm\`, and flags the operator when an upgrade is available. No placeholders and no integration required — every install gets this by default.
|
|
16429
16591
|
|
|
@@ -16475,7 +16637,7 @@ Do not attempt the upgrade yourself — this schedule only detects and reports.
|
|
|
16475
16637
|
`;
|
|
16476
16638
|
|
|
16477
16639
|
// templates/schedules/daily-workflow-health-audit/config.json
|
|
16478
|
-
var
|
|
16640
|
+
var config_default60 = `{
|
|
16479
16641
|
"kind": "schedule",
|
|
16480
16642
|
"name": "daily-workflow-health-audit",
|
|
16481
16643
|
"displayName": "Daily Workflow Health Audit",
|
|
@@ -16497,7 +16659,7 @@ var config_default59 = `{
|
|
|
16497
16659
|
`;
|
|
16498
16660
|
|
|
16499
16661
|
// templates/schedules/daily-workflow-health-audit/content.md
|
|
16500
|
-
var
|
|
16662
|
+
var content_default60 = `# Daily Workflow Health Audit
|
|
16501
16663
|
|
|
16502
16664
|
Delivery uses configured admin channels with an in-app fallback.
|
|
16503
16665
|
|
|
@@ -16691,7 +16853,7 @@ Omit any section whose count is 0. **HARD CEILING 700 characters — not the 400
|
|
|
16691
16853
|
`;
|
|
16692
16854
|
|
|
16693
16855
|
// templates/schedules/gtm-weekly-review/config.json
|
|
16694
|
-
var
|
|
16856
|
+
var config_default61 = `{
|
|
16695
16857
|
"kind": "schedule",
|
|
16696
16858
|
"name": "gtm-weekly-review",
|
|
16697
16859
|
"displayName": "Weekly GTM Metrics Review",
|
|
@@ -16708,7 +16870,7 @@ var config_default60 = `{
|
|
|
16708
16870
|
`;
|
|
16709
16871
|
|
|
16710
16872
|
// templates/schedules/gtm-weekly-review/content.md
|
|
16711
|
-
var
|
|
16873
|
+
var content_default61 = `Template parameters: {{REPO_URL}}, {{GSC_PROPERTY}}
|
|
16712
16874
|
|
|
16713
16875
|
# Weekly GTM Metrics Review
|
|
16714
16876
|
|
|
@@ -16789,7 +16951,7 @@ This is part of your team's GTM goal; update the goal statement before enabling
|
|
|
16789
16951
|
`;
|
|
16790
16952
|
|
|
16791
16953
|
// templates/schedules/weekly-code-health-reports/config.json
|
|
16792
|
-
var
|
|
16954
|
+
var config_default62 = `{
|
|
16793
16955
|
"kind": "schedule",
|
|
16794
16956
|
"name": "weekly-code-health-reports",
|
|
16795
16957
|
"displayName": "Weekly Code Health Reports",
|
|
@@ -16806,7 +16968,7 @@ var config_default61 = `{
|
|
|
16806
16968
|
`;
|
|
16807
16969
|
|
|
16808
16970
|
// templates/schedules/weekly-code-health-reports/content.md
|
|
16809
|
-
var
|
|
16971
|
+
var content_default62 = `# Weekly Code Health Reports
|
|
16810
16972
|
|
|
16811
16973
|
Template parameters: {{REPO_URL}}, {{BRANCH}}, {{SCOPE_PATH}}, {{REPORT_NAME}}, {{PAGE_ID}}
|
|
16812
16974
|
|
|
@@ -16871,7 +17033,7 @@ Call \`store-progress\` with status \`completed\` and an output summary that inc
|
|
|
16871
17033
|
`;
|
|
16872
17034
|
|
|
16873
17035
|
// templates/schedules/weekly-dependabot-triage/config.json
|
|
16874
|
-
var
|
|
17036
|
+
var config_default63 = `{
|
|
16875
17037
|
"kind": "schedule",
|
|
16876
17038
|
"name": "weekly-dependabot-triage",
|
|
16877
17039
|
"displayName": "Weekly Dependency Triage",
|
|
@@ -16888,7 +17050,7 @@ var config_default62 = `{
|
|
|
16888
17050
|
`;
|
|
16889
17051
|
|
|
16890
17052
|
// templates/schedules/weekly-dependabot-triage/content.md
|
|
16891
|
-
var
|
|
17053
|
+
var content_default63 = `# Weekly Dependency Triage
|
|
16892
17054
|
|
|
16893
17055
|
Template parameters: {{SLACK_CHANNEL_ID}}, {{REPO_URL}}, {{TIMEZONE}}
|
|
16894
17056
|
|
|
@@ -16942,7 +17104,7 @@ Call \`store-progress\` with status \`completed\` and an output summary listing
|
|
|
16942
17104
|
`;
|
|
16943
17105
|
|
|
16944
17106
|
// templates/schedules/weekly-dora-metrics/config.json
|
|
16945
|
-
var
|
|
17107
|
+
var config_default64 = `{
|
|
16946
17108
|
"kind": "schedule",
|
|
16947
17109
|
"name": "weekly-dora-metrics",
|
|
16948
17110
|
"displayName": "Weekly DORA Metrics",
|
|
@@ -16959,7 +17121,7 @@ var config_default63 = `{
|
|
|
16959
17121
|
`;
|
|
16960
17122
|
|
|
16961
17123
|
// templates/schedules/weekly-dora-metrics/content.md
|
|
16962
|
-
var
|
|
17124
|
+
var content_default64 = `# Weekly DORA Metrics
|
|
16963
17125
|
|
|
16964
17126
|
Template parameters: {{REPO_URL}}, {{BRANCH}}, {{TAG_PATTERN}}, {{REPORT_NAME}}, {{PAGE_ID}}
|
|
16965
17127
|
|
|
@@ -17025,7 +17187,7 @@ Call \`store-progress\` with status \`completed\` and an output summary that inc
|
|
|
17025
17187
|
`;
|
|
17026
17188
|
|
|
17027
17189
|
// templates/schedules/weekly-harness-upgrade-check/config.json
|
|
17028
|
-
var
|
|
17190
|
+
var config_default65 = `{
|
|
17029
17191
|
"kind": "schedule",
|
|
17030
17192
|
"name": "weekly-harness-upgrade-check",
|
|
17031
17193
|
"displayName": "Weekly Harness Upgrade Check",
|
|
@@ -17042,7 +17204,7 @@ var config_default64 = `{
|
|
|
17042
17204
|
`;
|
|
17043
17205
|
|
|
17044
17206
|
// templates/schedules/weekly-harness-upgrade-check/content.md
|
|
17045
|
-
var
|
|
17207
|
+
var content_default65 = '# Weekly Harness Upgrade Check\n\nThis is a topology-preserving, anonymized port of the live Sunday schedule. It keeps its bundle, deduplication, baseline, partial-fallback, and CI acceptance behavior.\n\n```json\n{"cron":"0 16 * * 0","timezone":"UTC","agentRole":"lead","enabled":true}\n```\n\n**Weekly Harness Upgrade — self-hosted repository Dockerfile.worker**\n\nRepo: `{{REPO_URL}}` (working dir: `/workspace/repo`)\nFile: `Dockerfile.worker`\n\nThe worker image pins these harnesses via `ARG`. Each week, check upstream for new versions and open ONE PR bundling ALL outdated harness bumps together ({{PR_REVIEWER}}\'s preference — bump ALL in one PR per run, do NOT stagger one-per-run).\n\n| ARG | Source | Sync requirement |\n|---|---|---|\n| `CLAUDE_CODE_VERSION` | npm `@anthropic-ai/claude-code` | none |\n| `PI_CODING_AGENT_VERSION` | npm `@earendil-works/pi-coding-agent` | MUST match `@earendil-works/pi-agent-core`, `@earendil-works/pi-ai`, and `@earendil-works/pi-coding-agent` in `package.json` (family-pin, all same version) — bump all together |\n| `CODEX_VERSION` | npm `@openai/codex` | MUST match direct `@openai/codex` in `package.json` (enforced by `scripts/check-codex-default-model.sh`) |\n| `OPENCODE_VERSION` | npm `@opencode/sdk` | Installs both `@opencode/cli` and `@opencode/sdk`. MUST match `@opencode/sdk` and `@opencode/plugin` in `package.json` devDependencies. OpenCode 2 ships under the `@opencode/*` scope; never take a version from `opencode-ai`, `@opencode-ai/sdk`, or the `sst/opencode` GitHub releases (still the 1.x line, which would be a downgrade) |\n\n## Steps\n\n1. `cd /workspace/repo && git fetch origin && git checkout main && git pull --ff-only`\n2. Read current pinned versions from `Dockerfile.worker` (the `ARG` lines) and from `package.json` (for the pi family + `@openai/codex` + `@opencode/sdk`).\n3. For each harness, fetch the latest version:\n - npm claude-code: `npm view @anthropic-ai/claude-code version`\n - npm pi: `npm view @earendil-works/pi-coding-agent version` (use the SAME version for `@earendil-works/pi-agent-core` and `@earendil-works/pi-ai` — they\'re a family pin)\n - npm codex: `npm view @openai/codex version`\n - npm opencode: `npm view @opencode/sdk version` (one version for `@opencode/cli`, `@opencode/sdk`, `@opencode/plugin`)\n4. Check for an existing open bundled PR — `gh pr list --search "harness upgrade in:title" --state open --repo {{REPO_URL}}`. Look at the branch prefix `harness-upgrade/`. If one is already open AND was created within the last 48h (sibling schedule may have just opened it), skip and complete with `"Skipped: sibling harness upgrade PR already open"` + the existing PR URL.\n5. Collect ALL outdated harnesses. If none are outdated, complete with output `"All harnesses up to date as of YYYY-MM-DD"` (no PR).\n6. Branch: `harness-upgrade/weekly-YYYY-MM-DD` (e.g. `harness-upgrade/weekly-2026-06-03`).\n7. Edit `Dockerfile.worker` — bump every outdated ARG. Also update `package.json`:\n - Codex: bump direct `@openai/codex` to match `CODEX_VERSION`.\n - Pi family: bump `@earendil-works/pi-agent-core`, `@earendil-works/pi-ai`, and `@earendil-works/pi-coding-agent` to the same new version as `PI_CODING_AGENT_VERSION`.\n - Opencode: bump `@opencode/sdk` and `@opencode/plugin` (devDependencies) to match `OPENCODE_VERSION`.\n Then re-run `bun install` so `bun.lock` updates.\n8. Local verification (all must pass before pushing):\n ```bash\n bun install --frozen-lockfile\n bun run lint\n bun run tsc:check\n bun test\n bash scripts/check-db-boundary.sh\n ```\n For Codex bumps: also run `bash scripts/check-codex-default-model.sh`.\n Note: do NOT run `docker build -f Dockerfile.worker .` locally — the runner lacks docker. CI\'s `docker-worker-build` job is the acceptance gate.\n9. If any local check fails, apply the **partial-bundle fallback** before giving up — one harness must never block the others:\n **BASELINE FIRST — do this before ANY isolation work.** A red gate does NOT mean the bumps broke anything. Re-run the EXACT failing command on a pristine `origin/main` worktree with zero bumps applied (`git worktree add /tmp/hb-baseline origin/main && cd /tmp/hb-baseline && bun install --frozen-lockfile`) and compare. If the SAME checks fail with the SAME assertions and the same pass/skip/fail totals, the failure is **PRE-EXISTING and unrelated to the bumps** ⇒ it does NOT block this PR, and you must NOT attempt isolation for it. Proceed to step 10, open the PR, and add a `## Pre-existing failures (also fail on origin/main)` section quoting the verbatim error plus both totals (baseline vs branch) so the reviewer can see they match. Only a failure that is RED on your branch and GREEN on baseline was caused by a bump — apply a–d below to that failure ONLY. A gate that is red on origin/main is never a reason to skip the weekly bump.\n a. Isolate which harness\'s bump caused it. The error usually names the package; if not, revert bumps one at a time until the step-8 gate passes.\n b. Revert ONLY that harness — its `Dockerfile.worker` ARG **and** its `package.json` pins (respect the family/sync requirements in the table above) — then re-run `bun install` and the FULL step-8 gate.\n c. Gate green ⇒ open the PR with the remaining harnesses and add a `## Held back` section naming the excluded harness, current → attempted version, and the VERBATIM error. Name it in your `store-progress` output too.\n d. Gate still red with that harness reverted, OR the failure cannot be isolated to one harness ⇒ do NOT open a PR; report via `store-progress` with `failureReason`.\n Migrating an upstream API break (a removed option, a renamed export, a moved type) is still OUT OF SCOPE for this routine — hold that harness back per (c) rather than changing code here.\n10. Push the branch and open a PR:\n - Title: `chore(docker): bump harnesses (weekly YYYY-MM-DD)` — or list the bumps, e.g. `chore(docker): bump claude-code, pi, opencode (weekly YYYY-MM-DD)`.\n - Body: include a table of current → new versions for every harness bumped, link to each upstream changelog (e.g. https://github.com/anthropics/claude-code/releases/tag/v<X.Y.Z>), and the local verification output (or "all green").\n - Add `{{PR_REVIEWER}}` as a reviewer.\n - DO NOT merge — {{PR_REVIEWER}} reviews and merges.\n11. Verify CI on the PR — wait for the merge-gate workflow to start, confirm at least the `docker-worker-build` job is queued/passing. If CI fails, comment on the PR with the failure log and leave the branch for human review.\n\n## Constraints\n\n- **ONE bundled PR per run.** Bump every outdated harness in the same PR — do not stagger one-per-run. The ONLY permitted exclusion is a harness held back by the step-9 partial-bundle fallback, which must be declared in the PR body.\n- **No `--no-verify`, no force pushes.**\n- If a harness has a major-version bump (e.g. 2.x → 3.x), still bundle it but flag clearly in the PR description that this is a major bump and may need extra review.\n- Pi family-pin: all three pi packages MUST be the same version. If they drift in the lockfile, fix the drift in the same PR.\n\n## Sibling-schedule coordination\nThis schedule has a sibling — `weekly-harness-upgrade-check-wed` (Wednesday 05:00 BCN). If a sibling-opened PR is OPEN and `< 48h` old, step 4 skips. This is the dedup gate. Don\'t change branch-naming heuristics in a way that breaks the dedup.\n\n## Output\n\n`store-progress` with `status: "completed"` and `output` containing:\n- The PR URL (if one was opened) + the table of harnesses bumped, OR\n- "No-op: all harnesses up to date as of YYYY-MM-DD" with the version table, OR\n- "Skipped: sibling harness upgrade PR already open" with the existing PR URL.\n';
|
|
17046
17208
|
|
|
17047
17209
|
// src/be/seed/schedules-seeder.ts
|
|
17048
17210
|
init_db2();
|
|
@@ -17064,7 +17226,7 @@ function resolveSeededEnabled(params) {
|
|
|
17064
17226
|
}
|
|
17065
17227
|
|
|
17066
17228
|
// templates/workflows/alerts-triage/config.json
|
|
17067
|
-
var
|
|
17229
|
+
var config_default66 = `{
|
|
17068
17230
|
"kind": "workflow",
|
|
17069
17231
|
"name": "alerts-triage",
|
|
17070
17232
|
"displayName": "Alerts Triage",
|
|
@@ -17081,7 +17243,7 @@ var config_default65 = `{
|
|
|
17081
17243
|
`;
|
|
17082
17244
|
|
|
17083
17245
|
// templates/workflows/alerts-triage/content.md
|
|
17084
|
-
var
|
|
17246
|
+
var content_default66 = `# Alerts Triage
|
|
17085
17247
|
|
|
17086
17248
|
The Slack handler emits \`slack.message\`; this workflow keeps the event-driven path and deduplicates work after the channel filter.
|
|
17087
17249
|
|
|
@@ -17101,7 +17263,7 @@ The Slack handler emits \`slack.message\`; this workflow keeps the event-driven
|
|
|
17101
17263
|
`;
|
|
17102
17264
|
|
|
17103
17265
|
// templates/workflows/autopilot/config.json
|
|
17104
|
-
var
|
|
17266
|
+
var config_default67 = `{
|
|
17105
17267
|
"kind": "workflow",
|
|
17106
17268
|
"name": "autopilot",
|
|
17107
17269
|
"displayName": "Autopilot Feature Pipeline",
|
|
@@ -17118,7 +17280,7 @@ var config_default66 = `{
|
|
|
17118
17280
|
`;
|
|
17119
17281
|
|
|
17120
17282
|
// templates/workflows/autopilot/content.md
|
|
17121
|
-
var
|
|
17283
|
+
var content_default67 = `# Autopilot Feature Pipeline
|
|
17122
17284
|
|
|
17123
17285
|
Template parameter: {{REPO_URL}}
|
|
17124
17286
|
|
|
@@ -17130,7 +17292,7 @@ Use this workflow as a canonical feature delivery pipeline. Customize roles and
|
|
|
17130
17292
|
`;
|
|
17131
17293
|
|
|
17132
17294
|
// templates/workflows/claude-code-changelog-watch/config.json
|
|
17133
|
-
var
|
|
17295
|
+
var config_default68 = `{
|
|
17134
17296
|
"kind": "workflow",
|
|
17135
17297
|
"name": "claude-code-changelog-watch",
|
|
17136
17298
|
"displayName": "Claude Code Changelog Watch",
|
|
@@ -17147,10 +17309,10 @@ var config_default67 = `{
|
|
|
17147
17309
|
`;
|
|
17148
17310
|
|
|
17149
17311
|
// templates/workflows/claude-code-changelog-watch/content.md
|
|
17150
|
-
var
|
|
17312
|
+
var content_default68 = '# Claude Code Changelog Watch\n\nThis topology-preserving self-hosted port removes installation-specific identities, endpoints, and credentials while retaining the live workflow\'s nodes, edges, input mappings, guards, retries, cooldown, and triggers. Configure the declared template parameters and integrations before enabling it.\n\nPrerequisite: the `check` and `commit` nodes run the global script `claude-code-changelog-check`, which is not seeded. Author it before enabling the workflow, or every run fails at the first node.\n\n```json\n{"name":"claude-code-changelog-watch","description":"Changelog watcher with deduplication: fetch, gate on a new version, publish an in-app receipt plus any configured delivery providers, then commit the version marker.","nodes":[{"id":"check","type":"swarm-script","label":"Fetch changelog + compare vs KV","config":{"scriptName":"claude-code-changelog-check","scope":"global","args":{"mode":"check"}},"next":"gate"},{"id":"gate","type":"property-match","label":"Gate: only continue on a new version","config":{"mode":"all","conditions":[{"field":"check.result.isNew","op":"eq","value":true}]},"next":{"true":"post"}},{"id":"post","type":"agent-task","label":"Post new version to Slack","config":{"model":"sonnet","tags":["changelog","claude-code","workflow"],"priority":50,"template":"**Claude Code new version announcement**\\n\\nA new Claude Code version was detected by the claude-code-changelog-watch workflow.\\n\\nNew version: {{check.result.latestVersion}} (released {{check.result.releaseDate}}, previous known: {{check.result.lastKnown}})\\n\\nRaw release notes:\\n{{check.result.section}}\\n\\nSteps:\\n1. ONE `slack-post` to the configured Slack notification target, **max 400 chars**:\\n\\n```\\n:rocket: *Claude Code {{check.result.latestVersion}}* — <the one change that matters to us, one line>\\n<https://code.claude.com/docs/en/changelog|changelog>\\n```\\n\\nRules:\\n- **One line.** Pick the single item that affects how we run the swarm (a fix we were hitting, a flag we use, a breaking change). Not 3-5 bullets.\\n- If a second item genuinely matters, one more bullet. Never more than two.\\n- **Routine release with nothing relevant to us ⇒ still post, but the one line is just the headline feature.** Never paste the release notes.\\n- No preamble, no \\"is out!\\", no per-surface breakdown (VSCode/CLI/SDK), no closing line.\\n\\n2. Complete with output \\"Posted Claude Code {{check.result.latestVersion}} to in-app activity and configured delivery providers.\\" plus the charCount.\\n\\nDo NOT update any version state — the workflow\'s commit node handles that after you complete. Do NOT post more than once."},"next":"commit","inputs":{"check":"check"}},{"id":"commit","type":"swarm-script","label":"Commit new version to KV","config":{"scriptName":"claude-code-changelog-check","scope":"global","args":{"mode":"commit","version":"{{check.result.latestVersion}}"}},"inputs":{"check":"check"}}],"onNodeFailure":"fail"}\n```\n\n\n\n\n';
|
|
17151
17313
|
|
|
17152
17314
|
// templates/workflows/competitor-radar/config.json
|
|
17153
|
-
var
|
|
17315
|
+
var config_default69 = `{
|
|
17154
17316
|
"kind": "workflow",
|
|
17155
17317
|
"name": "competitor-radar",
|
|
17156
17318
|
"displayName": "Competitor Radar",
|
|
@@ -17167,7 +17329,7 @@ var config_default68 = `{
|
|
|
17167
17329
|
`;
|
|
17168
17330
|
|
|
17169
17331
|
// templates/workflows/competitor-radar/content.md
|
|
17170
|
-
var
|
|
17332
|
+
var content_default69 = `# Competitor Radar
|
|
17171
17333
|
|
|
17172
17334
|
This self-hosted port retains the scheduled, fixture, extraction, gap-analysis, litmus, cooldown, and bounded-report behavior. Its pinned GSC helper is embedded in the workflow, so it does not fetch executable code from Desplega infrastructure.
|
|
17173
17335
|
|
|
@@ -17177,7 +17339,7 @@ This self-hosted port retains the scheduled, fixture, extraction, gap-analysis,
|
|
|
17177
17339
|
`;
|
|
17178
17340
|
|
|
17179
17341
|
// templates/workflows/docs-site-releases/config.json
|
|
17180
|
-
var
|
|
17342
|
+
var config_default70 = `{
|
|
17181
17343
|
"kind": "workflow",
|
|
17182
17344
|
"name": "docs-site-releases",
|
|
17183
17345
|
"displayName": "Documentation Release Notes",
|
|
@@ -17194,7 +17356,7 @@ var config_default69 = `{
|
|
|
17194
17356
|
`;
|
|
17195
17357
|
|
|
17196
17358
|
// templates/workflows/docs-site-releases/content.md
|
|
17197
|
-
var
|
|
17359
|
+
var content_default70 = `# Documentation Release Notes
|
|
17198
17360
|
|
|
17199
17361
|
This topology-preserving self-hosted port removes installation-specific identities, endpoints, and credentials while retaining the live workflow's nodes, edges, input mappings, guards, retries, cooldown, and triggers. Configure the declared template parameters and integrations before enabling it.
|
|
17200
17362
|
|
|
@@ -17209,7 +17371,7 @@ This topology-preserving self-hosted port removes installation-specific identiti
|
|
|
17209
17371
|
`;
|
|
17210
17372
|
|
|
17211
17373
|
// templates/workflows/gsc-topic-miner/config.json
|
|
17212
|
-
var
|
|
17374
|
+
var config_default71 = `{
|
|
17213
17375
|
"kind": "workflow",
|
|
17214
17376
|
"name": "gsc-topic-miner",
|
|
17215
17377
|
"displayName": "Search Topic Miner",
|
|
@@ -17226,7 +17388,7 @@ var config_default70 = `{
|
|
|
17226
17388
|
`;
|
|
17227
17389
|
|
|
17228
17390
|
// templates/workflows/gsc-topic-miner/content.md
|
|
17229
|
-
var
|
|
17391
|
+
var content_default71 = `# Search Topic Miner
|
|
17230
17392
|
|
|
17231
17393
|
This template uses only the configured GSC and agent-fs integrations. The deferred seeder creates the weekly schedule binding; no installation-specific schedule ID is shipped.
|
|
17232
17394
|
|
|
@@ -17236,7 +17398,7 @@ This template uses only the configured GSC and agent-fs integrations. The deferr
|
|
|
17236
17398
|
`;
|
|
17237
17399
|
|
|
17238
17400
|
// templates/workflows/linear-drain-loop/config.json
|
|
17239
|
-
var
|
|
17401
|
+
var config_default72 = `{
|
|
17240
17402
|
"kind": "workflow",
|
|
17241
17403
|
"name": "linear-drain-loop",
|
|
17242
17404
|
"displayName": "Linear Drain Loop",
|
|
@@ -17258,7 +17420,7 @@ var config_default71 = `{
|
|
|
17258
17420
|
`;
|
|
17259
17421
|
|
|
17260
17422
|
// templates/workflows/linear-drain-loop/content.md
|
|
17261
|
-
var
|
|
17423
|
+
var content_default72 = `# Linear Drain Loop
|
|
17262
17424
|
|
|
17263
17425
|
Template parameter: {{LINEAR_PROJECT_ID}}
|
|
17264
17426
|
|
|
@@ -17303,7 +17465,7 @@ Blocked and ambiguous items are explicitly left alone — the dispatch node surf
|
|
|
17303
17465
|
`;
|
|
17304
17466
|
|
|
17305
17467
|
// templates/workflows/llm-safe-release-context/config.json
|
|
17306
|
-
var
|
|
17468
|
+
var config_default73 = `{
|
|
17307
17469
|
"kind": "workflow",
|
|
17308
17470
|
"name": "llm-safe-release-context",
|
|
17309
17471
|
"displayName": "LLM-Safe Release Context",
|
|
@@ -17320,7 +17482,7 @@ var config_default72 = `{
|
|
|
17320
17482
|
`;
|
|
17321
17483
|
|
|
17322
17484
|
// templates/workflows/llm-safe-release-context/content.md
|
|
17323
|
-
var
|
|
17485
|
+
var content_default73 = `# LLM-Safe Release Context
|
|
17324
17486
|
|
|
17325
17487
|
Template parameters: {{ORG_ID}}, {{REPO_URL}}
|
|
17326
17488
|
|
|
@@ -17394,7 +17556,7 @@ This avoids the common failure mode where a large \`context.json\` fills the mod
|
|
|
17394
17556
|
`;
|
|
17395
17557
|
|
|
17396
17558
|
// templates/workflows/pr-review-status-sweep/config.json
|
|
17397
|
-
var
|
|
17559
|
+
var config_default74 = `{
|
|
17398
17560
|
"kind": "workflow",
|
|
17399
17561
|
"name": "pr-review-status-sweep",
|
|
17400
17562
|
"displayName": "PR Review Status Sweep",
|
|
@@ -17411,7 +17573,7 @@ var config_default73 = `{
|
|
|
17411
17573
|
`;
|
|
17412
17574
|
|
|
17413
17575
|
// templates/workflows/pr-review-status-sweep/content.md
|
|
17414
|
-
var
|
|
17576
|
+
var content_default74 = `# PR Review Status Sweep
|
|
17415
17577
|
|
|
17416
17578
|
This topology-preserving self-hosted port removes installation-specific identities, endpoints, and credentials while retaining the live workflow's nodes, edges, input mappings, guards, retries, cooldown, and triggers. Configure the declared template parameters and integrations before enabling it.
|
|
17417
17579
|
|
|
@@ -17426,7 +17588,7 @@ Prerequisite: the \`sweep\` node runs the agent-scoped script \`pr-review-status
|
|
|
17426
17588
|
`;
|
|
17427
17589
|
|
|
17428
17590
|
// templates/workflows/ralph-loop/config.json
|
|
17429
|
-
var
|
|
17591
|
+
var config_default75 = `{
|
|
17430
17592
|
"kind": "workflow",
|
|
17431
17593
|
"name": "ralph-loop",
|
|
17432
17594
|
"displayName": "Iterative Review Loop",
|
|
@@ -17443,7 +17605,7 @@ var config_default74 = `{
|
|
|
17443
17605
|
`;
|
|
17444
17606
|
|
|
17445
17607
|
// templates/workflows/ralph-loop/content.md
|
|
17446
|
-
var
|
|
17608
|
+
var content_default75 = `# Iterative Review Loop
|
|
17447
17609
|
|
|
17448
17610
|
Template parameter: {{REPO_URL}}
|
|
17449
17611
|
|
|
@@ -17494,19 +17656,19 @@ The \`maxIterations\` parameter is declarative: the reviewer signals PASS when d
|
|
|
17494
17656
|
init_db2();
|
|
17495
17657
|
var asText2 = (value) => value;
|
|
17496
17658
|
var BUILT_IN_WORKFLOW_SOURCES = [
|
|
17497
|
-
{ config: asText2(config_default65), content: asText2(content_default65) },
|
|
17498
17659
|
{ config: asText2(config_default66), content: asText2(content_default66) },
|
|
17660
|
+
{ config: asText2(config_default67), content: asText2(content_default67) },
|
|
17499
17661
|
{
|
|
17500
|
-
config: asText2(
|
|
17501
|
-
content: asText2(
|
|
17662
|
+
config: asText2(config_default68),
|
|
17663
|
+
content: asText2(content_default68)
|
|
17502
17664
|
},
|
|
17503
|
-
{ config: asText2(config_default68), content: asText2(content_default68) },
|
|
17504
17665
|
{ config: asText2(config_default69), content: asText2(content_default69) },
|
|
17505
17666
|
{ config: asText2(config_default70), content: asText2(content_default70) },
|
|
17506
17667
|
{ config: asText2(config_default71), content: asText2(content_default71) },
|
|
17507
17668
|
{ config: asText2(config_default72), content: asText2(content_default72) },
|
|
17508
17669
|
{ config: asText2(config_default73), content: asText2(content_default73) },
|
|
17509
|
-
{ config: asText2(config_default74), content: asText2(content_default74) }
|
|
17670
|
+
{ config: asText2(config_default74), content: asText2(content_default74) },
|
|
17671
|
+
{ config: asText2(config_default75), content: asText2(content_default75) }
|
|
17510
17672
|
];
|
|
17511
17673
|
function sortJson(value) {
|
|
17512
17674
|
if (Array.isArray(value))
|
|
@@ -17622,34 +17784,34 @@ var workflowsSeeder = createWorkflowsSeeder();
|
|
|
17622
17784
|
// src/be/seed/schedules-seeder.ts
|
|
17623
17785
|
var asText3 = (value) => value;
|
|
17624
17786
|
var BUILT_IN_SCHEDULE_SOURCES = [
|
|
17625
|
-
{ config: asText3(
|
|
17787
|
+
{ config: asText3(config_default55), content: asText3(content_default55) },
|
|
17626
17788
|
{
|
|
17627
|
-
config: asText3(
|
|
17628
|
-
content: asText3(
|
|
17789
|
+
config: asText3(config_default56),
|
|
17790
|
+
content: asText3(content_default56)
|
|
17629
17791
|
},
|
|
17630
|
-
{ config: asText3(config_default56), content: asText3(content_default56) },
|
|
17631
17792
|
{ config: asText3(config_default57), content: asText3(content_default57) },
|
|
17632
|
-
{
|
|
17633
|
-
config: asText3(config_default58),
|
|
17634
|
-
content: asText3(content_default58)
|
|
17635
|
-
},
|
|
17793
|
+
{ config: asText3(config_default58), content: asText3(content_default58) },
|
|
17636
17794
|
{
|
|
17637
17795
|
config: asText3(config_default59),
|
|
17638
17796
|
content: asText3(content_default59)
|
|
17639
17797
|
},
|
|
17640
|
-
{ config: asText3(config_default60), content: asText3(content_default60) },
|
|
17641
17798
|
{
|
|
17642
|
-
config: asText3(
|
|
17643
|
-
content: asText3(
|
|
17799
|
+
config: asText3(config_default60),
|
|
17800
|
+
content: asText3(content_default60)
|
|
17644
17801
|
},
|
|
17802
|
+
{ config: asText3(config_default61), content: asText3(content_default61) },
|
|
17645
17803
|
{
|
|
17646
17804
|
config: asText3(config_default62),
|
|
17647
17805
|
content: asText3(content_default62)
|
|
17648
17806
|
},
|
|
17649
|
-
{ config: asText3(config_default63), content: asText3(content_default63) },
|
|
17650
17807
|
{
|
|
17651
|
-
config: asText3(
|
|
17652
|
-
content: asText3(
|
|
17808
|
+
config: asText3(config_default63),
|
|
17809
|
+
content: asText3(content_default63)
|
|
17810
|
+
},
|
|
17811
|
+
{ config: asText3(config_default64), content: asText3(content_default64) },
|
|
17812
|
+
{
|
|
17813
|
+
config: asText3(config_default65),
|
|
17814
|
+
content: asText3(content_default65)
|
|
17653
17815
|
}
|
|
17654
17816
|
];
|
|
17655
17817
|
function parseScheduleSource(source) {
|