@synap-core/workspace-templates 0.12.0 → 0.24.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.
Files changed (237) hide show
  1. package/README.md +509 -0
  2. package/automations/bookmark-enrichment.automation.json +321 -0
  3. package/automations/github-pr-events.automation.json +505 -0
  4. package/automations/mail-feed.automation.json +86 -0
  5. package/automations/ship-a-change-checks-green.automation.json +106 -0
  6. package/automations/ship-a-change-checks-red.automation.json +106 -0
  7. package/automations/ship-a-change-deployed.automation.json +56 -0
  8. package/automations/ship-a-change-pr-merged.automation.json +110 -0
  9. package/automations/ship-a-change-pr-opened.automation.json +104 -0
  10. package/automations/url-bookmark-capture.automation.json +132 -0
  11. package/capabilities/agency-skills.capability.json +157 -0
  12. package/capabilities/apify.capability.json +265 -0
  13. package/capabilities/apollo.capability.json +120 -0
  14. package/capabilities/arch-backend.capability.json +97 -0
  15. package/capabilities/arch-client-intelligence.capability.json +401 -0
  16. package/capabilities/backblaze-b2.capability.json +205 -0
  17. package/capabilities/cal-com.capability.json +266 -0
  18. package/capabilities/canva.capability.json +274 -0
  19. package/capabilities/claude-code-routines.capability.json +172 -0
  20. package/capabilities/claude-managed-agents.capability.json +215 -0
  21. package/capabilities/client-comms-report.capability.json +246 -0
  22. package/capabilities/content-idea-to-production.capability.json +29 -0
  23. package/capabilities/datagouv.capability.json +315 -0
  24. package/capabilities/discord-bot.capability.json +157 -0
  25. package/capabilities/dropbox.capability.json +281 -0
  26. package/capabilities/exa.capability.json +109 -0
  27. package/capabilities/fal-ai.capability.json +83 -0
  28. package/capabilities/fal-media-lab.capability.json +158 -0
  29. package/capabilities/fireflies.capability.json +191 -0
  30. package/capabilities/freellmapi.capability.json +114 -0
  31. package/capabilities/generic-apikey.capability.json +92 -0
  32. package/capabilities/github-merge.capability.json +99 -0
  33. package/capabilities/github-read.capability.json +325 -0
  34. package/capabilities/governance-tighten-recommender.capability.json +81 -0
  35. package/capabilities/jev.capability.json +226 -0
  36. package/capabilities/lead-conversion-enrollment.capability.json +246 -0
  37. package/capabilities/mail-feed.capability.json +277 -0
  38. package/capabilities/mailgun.capability.json +52 -0
  39. package/capabilities/nango-google.capability.json +639 -0
  40. package/capabilities/notion.capability.json +417 -0
  41. package/capabilities/omniroute.capability.json +137 -0
  42. package/capabilities/posthog-analytics.capability.json +258 -0
  43. package/capabilities/postiz.capability.json +467 -0
  44. package/capabilities/proactive-digest.capability.json +190 -0
  45. package/capabilities/proton.capability.json +288 -0
  46. package/capabilities/replicate-rvm.capability.json +97 -0
  47. package/capabilities/research-methods.capability.json +57 -0
  48. package/capabilities/shopify.capability.json +395 -0
  49. package/capabilities/slack.capability.json +369 -0
  50. package/capabilities/stripe.capability.json +1496 -0
  51. package/capabilities/telegram-bridge.capability.json +56 -0
  52. package/capabilities/treg.capability.json +102 -0
  53. package/capabilities/unipile-linkedin.capability.json +494 -0
  54. package/capabilities/vercel.capability.json +424 -0
  55. package/dist/define-DqoItRiR.d.ts +1370 -0
  56. package/dist/index.d.ts +174 -900
  57. package/dist/index.js +26447 -24075
  58. package/dist/index.js.map +1 -1
  59. package/dist/wire.d.ts +222 -0
  60. package/dist/wire.js +77 -0
  61. package/dist/wire.js.map +1 -0
  62. package/package.json +27 -4
  63. package/retired.yaml +136 -0
  64. package/skills/app-factory-build-slice/SKILL.md +70 -0
  65. package/skills/app-factory-build-slice.catalog.json +17 -0
  66. package/skills/app-factory-design-canvas/SKILL.md +96 -0
  67. package/skills/app-factory-design-canvas.catalog.json +17 -0
  68. package/skills/app-factory-design-review/SKILL.md +105 -0
  69. package/skills/app-factory-design-review.catalog.json +18 -0
  70. package/skills/app-factory-research/SKILL.md +123 -0
  71. package/skills/app-factory-research.catalog.json +17 -0
  72. package/skills/bookmark-enrichment/SKILL.md +40 -0
  73. package/skills/carousel-format/carousel-format.skill.json +18 -0
  74. package/skills/collage-broll/collage-broll.skill.json +20 -0
  75. package/skills/content-taste/content-taste.skill.json +18 -0
  76. package/skills/creative-director/creative-director.skill.json +18 -0
  77. package/skills/design-critique/SKILL.md +138 -0
  78. package/skills/design-critique.catalog.json +12 -0
  79. package/skills/designing-with-real-components/SKILL.md +140 -0
  80. package/skills/designing-with-real-components.catalog.json +12 -0
  81. package/skills/edit-loop/edit-loop.skill.json +25 -0
  82. package/skills/facecam-edit/facecam-edit.skill.json +24 -0
  83. package/skills/mapping-the-server/SKILL.md +54 -0
  84. package/skills/math-explainer-manimgx/math-explainer-manimgx.skill.json +20 -0
  85. package/skills/motion-quality-gates/motion-quality-gates.skill.json +20 -0
  86. package/skills/open-design-edit/open-design-edit.skill.json +24 -0
  87. package/skills/reading-client-progress/SKILL.md +65 -0
  88. package/skills/routing-to-channels/SKILL.md +54 -0
  89. package/skills/stellar-scf-grants-advisor/ASSOCIATE-CHEATSHEET.md +133 -0
  90. package/skills/stellar-scf-grants-advisor/ASSOCIATE-ONBOARDING-GUIDE.md +761 -0
  91. package/skills/stellar-scf-grants-advisor/CLIENT-FAQ.md +181 -0
  92. package/skills/stellar-scf-grants-advisor/PARTNER-HANDOVER.md +130 -0
  93. package/skills/stellar-scf-grants-advisor/SKILL.md +102 -0
  94. package/skills/stellar-scf-grants-advisor/examples/README.md +43 -0
  95. package/skills/stellar-scf-grants-advisor/examples/chat-summary-templates.md +158 -0
  96. package/skills/stellar-scf-grants-advisor/examples/corpus-comparable-rows.md +136 -0
  97. package/skills/stellar-scf-grants-advisor/examples/q10-team-rewrites.md +109 -0
  98. package/skills/stellar-scf-grants-advisor/examples/q2-description-rewrites.md +88 -0
  99. package/skills/stellar-scf-grants-advisor/examples/q6-integration-rewrites.md +107 -0
  100. package/skills/stellar-scf-grants-advisor/examples/tranche-structures.md +176 -0
  101. package/skills/stellar-scf-grants-advisor/reference/01-methodology.md +96 -0
  102. package/skills/stellar-scf-grants-advisor/reference/02-scoring-framework.md +199 -0
  103. package/skills/stellar-scf-grants-advisor/reference/03-rejection-patterns.md +241 -0
  104. package/skills/stellar-scf-grants-advisor/reference/04-winner-patterns.md +120 -0
  105. package/skills/stellar-scf-grants-advisor/reference/05-comeback-patterns.md +92 -0
  106. package/skills/stellar-scf-grants-advisor/reference/06-budget-benchmarks.md +125 -0
  107. package/skills/stellar-scf-grants-advisor/reference/07-integration-list.md +147 -0
  108. package/skills/stellar-scf-grants-advisor/reference/08-abstract-fields.md +258 -0
  109. package/skills/stellar-scf-grants-advisor/reference/09-build-fields.md +259 -0
  110. package/skills/stellar-scf-grants-advisor/reference/10-corpus-winners.md +135 -0
  111. package/skills/stellar-scf-grants-advisor/reference/11-corpus-rejects.md +267 -0
  112. package/skills/stellar-scf-grants-advisor/reference/12-client-communication.md +131 -0
  113. package/skills/stellar-scf-grants-advisor/reference/13-excel-output-spec.md +199 -0
  114. package/skills/stellar-scf-grants-advisor/reference/14-worked-examples.md +160 -0
  115. package/skills/stellar-scf-grants-advisor/templates/abstract-review-builder.py +253 -0
  116. package/skills/stellar-scf-grants-advisor/templates/build-review-builder.py +213 -0
  117. package/skills/video-direction/video-direction.skill.json +20 -0
  118. package/skills/video-style-vocabulary/video-style-vocabulary.skill.json +20 -0
  119. package/skills/video-toolbox/video-toolbox.skill.json +20 -0
  120. package/src/__fixtures__/lifecycle-presets.flat-before.json +205 -0
  121. package/src/agency-os.yaml +122 -0
  122. package/src/agent-os-skill-slugs.test.ts +65 -0
  123. package/src/apify-social-reader.test.ts +317 -0
  124. package/src/base.yaml +85 -39
  125. package/src/blockchain-ecosystem.yaml +1 -0
  126. package/src/brand-library.yaml +525 -14
  127. package/src/builder-dev-fold.template.test.ts +113 -0
  128. package/src/builder.yaml +779 -91
  129. package/src/business-model-catalog-description.test.ts +47 -0
  130. package/src/business-model-grp.template.test.ts +214 -0
  131. package/src/business-model.yaml +296 -49
  132. package/src/catalog-safety.test.ts +198 -0
  133. package/src/catalog.template-items.test.ts +75 -0
  134. package/src/catalog.ts +66 -2
  135. package/src/concept-guard.test.ts +28 -8
  136. package/src/concept-guard.ts +2 -2
  137. package/src/content-creation.template.test.ts +52 -0
  138. package/src/content-creation.yaml +192 -0
  139. package/src/content-os.edit-loop.template.test.ts +210 -0
  140. package/src/content-os.publish-due-posts.template.test.ts +261 -0
  141. package/src/content-os.template.test.ts +349 -0
  142. package/src/content-os.video-takes.template.test.ts +338 -0
  143. package/src/content-os.work-a-take.template.test.ts +125 -0
  144. package/src/content-os.yaml +1873 -39
  145. package/src/content-studio-retired.template.test.ts +82 -0
  146. package/src/corpus-flow-validity.test.ts +4 -5
  147. package/src/crm-absorbs-outreach.template.test.ts +117 -0
  148. package/src/crm-build-plan.template.test.ts +128 -0
  149. package/src/crm-lists.template.test.ts +96 -0
  150. package/src/crm-messages-cadence.template.test.ts +246 -0
  151. package/src/crm-one-send-door.template.test.ts +65 -0
  152. package/src/crm.yaml +1563 -181
  153. package/src/decisions-wave.template.test.ts +186 -0
  154. package/src/define.ts +146 -17
  155. package/src/design-to-implementation.template.test.ts +94 -0
  156. package/src/domain-template-type.test.ts +75 -0
  157. package/src/ecosystem.yaml +27 -66
  158. package/src/enterprise-os.yaml +10 -134
  159. package/src/facecam-edit.audio-studio.test.ts +188 -0
  160. package/src/fal-generate-image.test.ts +200 -0
  161. package/src/fal-media-lab-pod-files.test.ts +180 -0
  162. package/src/finance.yaml +254 -1
  163. package/src/foundation.yaml +78 -38
  164. package/src/frame-host-modules.d.mts +6 -0
  165. package/src/frame-host-modules.mjs +58 -0
  166. package/src/generate.mjs +9 -3
  167. package/src/github-merge.test.ts +210 -0
  168. package/src/grants.yaml +46 -17
  169. package/src/hr.yaml +39 -6
  170. package/src/index.ts +36 -1
  171. package/src/intent.test.ts +161 -0
  172. package/src/internal-runbook.yaml +19 -1
  173. package/src/jev.test.ts +356 -0
  174. package/src/legal.yaml +33 -1
  175. package/src/life-os.yaml +51 -240
  176. package/src/lifecycle-coverage.test.ts +124 -0
  177. package/src/lifecycle-declarations.test.ts +252 -0
  178. package/src/lifecycle-presets.test.ts +468 -0
  179. package/src/lifecycle-presets.ts +494 -0
  180. package/src/lifecycle-stuck.test.ts +46 -0
  181. package/src/lifecycle-waits.parity.test.ts +319 -0
  182. package/src/marketing.yaml +108 -4
  183. package/src/meetings-knowledge.yaml +18 -0
  184. package/src/networking.yaml +23 -2
  185. package/src/operations.yaml +304 -4
  186. package/src/pipeline-views.template.test.ts +88 -0
  187. package/src/playbook-goal-placeholders.test.ts +57 -0
  188. package/src/postiz-contract.test.ts +134 -0
  189. package/src/presentation-declarations.test.ts +174 -0
  190. package/src/process-contract.validate.test.ts +352 -0
  191. package/src/project-management.yaml +39 -740
  192. package/src/publish-body.d.mts +29 -0
  193. package/src/publish-body.mjs +74 -0
  194. package/src/publish-body.test.ts +74 -0
  195. package/src/publish-official-lib.test.ts +176 -0
  196. package/src/replicate-rvm.test.ts +504 -0
  197. package/src/research-base.yaml +119 -20
  198. package/src/research-decision.yaml +60 -3
  199. package/src/reserved-profile-slugs.test.ts +81 -0
  200. package/src/retired-goal-kinds.test.ts +52 -0
  201. package/src/role-reference-and-step-domain.test.ts +192 -0
  202. package/src/seed-obligation.test.ts +1 -6
  203. package/src/send-message-maps.test.ts +227 -0
  204. package/src/shared-role-ssot.test.ts +40 -2
  205. package/src/ship-a-change.template.test.ts +177 -0
  206. package/src/skills.test.ts +170 -0
  207. package/src/social.yaml +85 -27
  208. package/src/space-packages.freshness.test.ts +97 -0
  209. package/src/space-packages.generated.ts +40 -0
  210. package/src/space-packages.test.ts +125 -0
  211. package/src/space-packages.ts +73 -0
  212. package/src/start.test.ts +188 -0
  213. package/src/stripe-capability.test.ts +192 -0
  214. package/src/task-intents.test.ts +293 -0
  215. package/src/template-documents.test.ts +158 -0
  216. package/src/templates.ts +20907 -21464
  217. package/src/types.ts +475 -61
  218. package/src/unipile-linkedin-find-profile.test.ts +183 -0
  219. package/src/unipile-publish-post.test.ts +162 -0
  220. package/src/validate-flow.drift.test.ts +2 -3
  221. package/src/validate-flow.ts +22 -2
  222. package/src/validate-onboarding.test.ts +59 -0
  223. package/src/validate.test.ts +71 -4
  224. package/src/validate.ts +1416 -7
  225. package/src/wire-core.ts +231 -0
  226. package/src/wire.test.ts +159 -0
  227. package/src/wire.ts +107 -0
  228. package/views/bookmark-views-pack.view.json +39 -0
  229. package/views/event-views-pack.view.json +45 -0
  230. package/views/task-views-pack.view.json +53 -0
  231. package/src/agent-fleet.yaml +0 -1645
  232. package/src/business-developer.template.test.ts +0 -351
  233. package/src/business-developer.yaml +0 -860
  234. package/src/content-studio.yaml +0 -361
  235. package/src/dev-dashboard.yaml +0 -990
  236. package/src/outreach-comms.yaml +0 -486
  237. package/src/the-arch.yaml +0 -201
@@ -0,0 +1,288 @@
1
+ {
2
+ "apiVersion": "synap/v1",
3
+ "kind": "capability",
4
+ "meta": {
5
+ "slug": "proton",
6
+ "name": "Proton Mail (self-hosted Bridge)",
7
+ "description": "Reads your full Proton Mail inbox through a Proton Bridge on your own server, so mail is decrypted locally. Includes a draft-reply playbook (you approve every send) and optional triage for unknown senders. Needs your Proton login.",
8
+ "tags": [
9
+ "capability"
10
+ ],
11
+ "isPublic": true,
12
+ "syncByDefault": true
13
+ },
14
+ "spec": {
15
+ "key": "proton",
16
+ "name": "Proton Mail (self-hosted bridge)",
17
+ "description": "SOVEREIGN self-host path for Proton Mail: a locally-run Proton Bridge (headless, `synap proton-setup`-provisioned) decrypts Proton's end-to-end-encrypted mail and exposes it as plain IMAP/SMTP on localhost, which this connector polls. This is the alternative to the `mailgun` template's Proton-forward path (a Proton auto-forward rule to a Mailgun catch-all inbound address) — that path is simpler to provision but round-trips mail through Mailgun's servers and only forwards what the Proton account chooses to forward. This path keeps mail local (the bridge decrypts on the user's own host) and reads the full mailbox, at the cost of requiring the bridge process to stay running. MUTUAL EXCLUSIVITY: do NOT run both a Proton Bridge connector (this template) and a Proton-forward-to-Mailgun rule (the `mailgun` template's Proton coverage) for the SAME mailbox — the same message would land twice, once via each pipeline (double-ingest into the channel/message substrate). Pick exactly one path per Proton mailbox. SELF-SERVE CREDENTIAL MODEL: enter your normal proton.me sign-in email + password (and a 2FA/TOTP seed if you have one enabled) — the headless bridge auto-logs-in with these on boot, no manual bridge login required. This is your regular account login — it is NOT the Organization key/name/password found under Settings → Organization (that is Business-admin-only machinery for activating/recovering admin status and is never used to log a mail client in). If your account uses two-password mode (a separate mailbox password on top of the login password), also provide `protonMailboxPassword`. Accounts secured with a FIDO2/security-key as their only 2FA method cannot be headless-automated — switch that account to TOTP first, or do one interactive bridge login. If Proton throws a one-time human-verification challenge (more likely on datacenter/VPS IPs than a home connection), a URL appears in the bridge logs (`pm2 logs proton-bridge`); click it once in a browser and the bridge finishes logging in — this is a one-time step, not a recurring one. Pass the account credentials as `protonEmail` / `protonPassword` / `protonTotpSeed` / `protonMailboxPassword`.",
18
+ "metadata": {
19
+ "mode": "standing"
20
+ },
21
+ "params": [
22
+ {
23
+ "name": "protonEmail",
24
+ "label": "Proton account email",
25
+ "type": "text",
26
+ "required": true,
27
+ "description": "The Proton ACCOUNT email address (the one you sign in to proton.me with) — NOT a bridge-generated value. Used by the headless bridge to auto-log-in on boot."
28
+ },
29
+ {
30
+ "name": "protonPassword",
31
+ "label": "Proton account password",
32
+ "type": "password",
33
+ "required": true,
34
+ "description": "Your normal proton.me sign-in password — NOT the Bridge-generated IMAP/SMTP password, and NOT the Organization key/password (Settings → Organization, Business-admin-only, never used to log a mail client in). Two-password-mode accounts: this is the LOGIN password; provide the separate mailbox password via `protonMailboxPassword`. Stored server-encrypted in the vault; never persisted in the template. Used by the headless bridge to auto-log-in on boot."
35
+ },
36
+ {
37
+ "name": "protonTotpSeed",
38
+ "label": "Proton 2FA/TOTP seed (optional)",
39
+ "type": "password",
40
+ "required": false,
41
+ "description": "Base32 TOTP/2FA secret seed from your Proton account's two-factor authenticator setup (Settings → Account and password → Two-factor authentication) — leave blank if 2FA is off. Lets the bridge compute the current 2FA code itself on auto-login instead of prompting a human each time. Not usable for FIDO2/security-key-only 2FA — switch that account to TOTP first, or do one interactive bridge login."
42
+ },
43
+ {
44
+ "name": "protonMailboxPassword",
45
+ "label": "Proton mailbox password (optional)",
46
+ "type": "password",
47
+ "required": false,
48
+ "description": "ONLY for accounts with two-password mode enabled (Settings → Account and password) — a separate second password protecting the mailbox itself, distinct from the account login password. Leave blank for single-password accounts (the default)."
49
+ }
50
+ ],
51
+ "vault": [
52
+ {
53
+ "ref": "proton-account",
54
+ "name": "{{name}} Proton account credentials",
55
+ "value": "{{protonCredentialsJson}}",
56
+ "type": "api_key",
57
+ "service": "proton",
58
+ "description": "Proton ACCOUNT credentials (email/password/totpSeed/mailboxPassword, JSON-encoded; last two optional) used by the headless bridge to auto-log-in on boot. Not the Bridge-generated IMAP/SMTP password."
59
+ }
60
+ ],
61
+ "tools": [
62
+ {
63
+ "name": "proton",
64
+ "kind": "external",
65
+ "description": "Proton Mail via a locally-run Proton Bridge (self-host path). The bridge decrypts Proton's end-to-end-encrypted mail and exposes plain IMAP/SMTP on localhost; this tool's credentialRef is remapped to the runtime vault://<id> at apply time so the Proton account credentials resolve server-side and the bridge auto-logs-in with them on boot. Inbound is polled from the bridge's IMAP mailbox and landed via the same inbound-recorder substrate as other channels (v1 is inbound-only; SMTP send is a future outbound capability). Connection host/ports and poll cadence are bridge-local env (the bridge talks to a fixed 127.0.0.1 Proton Bridge) — only mailbox/backfill/sender-filters here are pod-editable behavioral config. MUTUAL EXCLUSIVITY: do not also run a Proton-forward-to-Mailgun rule for the same mailbox (see template description) — that double-ingests.",
66
+ "credentialRef": "proton-account",
67
+ "executor": "external-agent",
68
+ "config": {
69
+ "transport": "bridge",
70
+ "externalSource": "proton"
71
+ },
72
+ "metadata": {
73
+ "proton": {
74
+ "mailbox": "INBOX",
75
+ "backfillDays": 7,
76
+ "senderAllow": [],
77
+ "senderDeny": [],
78
+ "configSchema": [
79
+ {
80
+ "key": "mailbox",
81
+ "label": "Canonical mailbox to poll",
82
+ "valueType": "string"
83
+ },
84
+ {
85
+ "key": "backfillDays",
86
+ "label": "Initial backfill window (days)",
87
+ "valueType": "number"
88
+ },
89
+ {
90
+ "key": "senderAllow",
91
+ "label": "Sender allowlist (emails/domains)",
92
+ "valueType": "json"
93
+ },
94
+ {
95
+ "key": "senderDeny",
96
+ "label": "Sender blocklist (emails/domains)",
97
+ "valueType": "json"
98
+ }
99
+ ]
100
+ }
101
+ }
102
+ }
103
+ ],
104
+ "skills": [],
105
+ "provides": [],
106
+ "playbooks": [
107
+ {
108
+ "name": "Proton — Draft Reply",
109
+ "description": "Draft a reply to a client's most recent inbound Proton email and FILE IT AS A PROPOSAL for the human to approve — never auto-sent. Runs as an is-agent so the send routes through the governed external-send door (the client-comms firewall guarantees an agent reply to a client channel is PROPOSED, not mirrored). On-demand for any client with a bound Proton channel; approving the proposal delivers the reply out over the bridge's SMTP with the correct RFC reply headers. This is the sovereign, proposal-gated counterpart to a hosted mail client's 'AI draft' — the draft is byte-identical to what sends on approval.",
110
+ "goalTemplate": "Draft a reply to the most recent inbound Proton email from the client {client}, then send it — your external-send tool FILES IT AS A PROPOSAL for a human to review and approve; it is NEVER auto-sent. Optional steer: {guidance}.\n\n1. Find the client's bound Proton email channel and read the latest inbound thread (the subject and the specific message you are replying to). If the client has no Proton channel or no inbound mail, STOP and say so — never invent a thread.\n2. Draft a reply IN THE CLIENT'S LANGUAGE and register/tone, grounded ONLY in the thread and the client's known context. Address what they actually asked. Never fabricate commitments, dates, numbers, or facts you cannot verify — leave those for the human to fill.\n3. Send the drafted reply on that Proton channel using your external-send tool. This FILES A PROPOSAL — a 'proposed' result is SUCCESS, awaiting the human's approval; do NOT claim the email was sent. On approval the bridge delivers it over SMTP.\n4. If anything is ambiguous or high-stakes, note it for the human rather than guessing.",
111
+ "params": [
112
+ {
113
+ "name": "client",
114
+ "label": "Client",
115
+ "type": "entity",
116
+ "required": true,
117
+ "description": "The client entity whose latest inbound Proton email you are replying to (the session subject). Already resolved — never created here."
118
+ },
119
+ {
120
+ "name": "guidance",
121
+ "label": "Reply guidance",
122
+ "type": "text",
123
+ "required": false,
124
+ "description": "Optional steer for the reply (angle, key points, tone). Leave blank to let the agent draft from the thread alone."
125
+ }
126
+ ],
127
+ "subjectProfile": {
128
+ "profileSlug": "company"
129
+ },
130
+ "channelSpec": {
131
+ "type": "THREAD",
132
+ "aiReactionMode": "only_mentioned"
133
+ },
134
+ "executor": "is-agent",
135
+ "status": "active"
136
+ }
137
+ ],
138
+ "automations": [
139
+ {
140
+ "name": "Proton — Inbox Triage (unbound mail)",
141
+ "description": "EVENT-DRIVEN. When a NEW Proton email lands on an UNBOUND channel (a sender not yet linked to a client), classify it with ONE AI pass and post a one-line digest to this automation's OWN run channel so untracked mail is visible instead of silent. Gated to provider=proton AND unbound (entityId empty), so it never overlaps the BOUND-client re-synthesis that arch-client-intelligence already runs on the same event (no double-triage). COMMON CASE NEEDS NOTHING: a client whose email is on file binds automatically (the `email` identity signal is derived from the entity's email property), so their mail is BOUND and handled by arch — this catches only the not-yet-linked tail. Seeded DRAFT: it runs a paid AI pass per unbound Proton message, so activate it after a dogfood run (same convention as the arch/Stellar AI automations). NOT a second inbound door or triage engine — it reads the landed message off the shared `external_message.received` event, the same spine every channel uses.",
142
+ "triggerType": "event",
143
+ "triggerConfig": {
144
+ "eventPattern": "external_message.received.completed"
145
+ },
146
+ "status": "draft",
147
+ "metadata": {
148
+ "kind": "proton-inbox-triage",
149
+ "pairsWith": "proton"
150
+ },
151
+ "flowDefinition": {
152
+ "nodes": [
153
+ {
154
+ "id": "trigger",
155
+ "type": "trigger",
156
+ "position": {
157
+ "x": 0,
158
+ "y": 0
159
+ },
160
+ "data": {
161
+ "triggerType": "event",
162
+ "label": "New inbound external message",
163
+ "config": {
164
+ "eventPattern": "external_message.received.completed"
165
+ }
166
+ }
167
+ },
168
+ {
169
+ "id": "proton-gate",
170
+ "type": "condition",
171
+ "position": {
172
+ "x": 0,
173
+ "y": 120
174
+ },
175
+ "data": {
176
+ "label": "Only Proton mail",
177
+ "expression": "trigger.payload.data.provider === \"proton\"",
178
+ "_note": "`external_message.received` fires for EVERY inbound provider (Discord/Unipile/Proton). The event data carries `provider` (= the connector's provider string, 'proton' here), so a bare-path + operator + quoted-literal condition keeps this automation to Proton mail only. triggerConfig.filters is equality-only on top-level keys and cannot reach into `data`, so the provider gate lives here in-flow. Chained (not `&&`-combined with the unbound gate) because the condition grammar is single-comparison — two condition nodes in series is the supported shape."
179
+ }
180
+ },
181
+ {
182
+ "id": "unbound-gate",
183
+ "type": "condition",
184
+ "position": {
185
+ "x": 0,
186
+ "y": 240
187
+ },
188
+ "data": {
189
+ "label": "Only UNBOUND senders (not yet linked to a client)",
190
+ "expression": "trigger.payload.data.entityId === \"\"",
191
+ "_note": "`data.entityId` is the bound client entity (null/absent when the channel isn't client-bound). resolveTemplate renders an absent path to \"\", so `=== \"\"` is true for unbound mail and false for a bound client. This is the EXACT COMPLEMENT of arch-client-intelligence's matched-client gate (`entityId !== \"\"`): arch handles bound clients, this handles the unbound tail — no message is triaged twice."
192
+ }
193
+ },
194
+ {
195
+ "id": "assess",
196
+ "type": "capability",
197
+ "position": {
198
+ "x": 0,
199
+ "y": 360
200
+ },
201
+ "data": {
202
+ "label": "Classify the unbound email → JSON",
203
+ "verbId": "ai.generate",
204
+ "inputMapping": {
205
+ "json": "true",
206
+ "maxTokens": "300",
207
+ "system": "You are a strict data-extraction function that triages a single inbound email from an unrecognized sender. Respond with STRICT JSON and NOTHING else. Never fabricate.",
208
+ "prompt": "An email just arrived from a sender not yet linked to any client.\n\nFrom: {{trigger.payload.data.participantName}}\nBody:\n{{trigger.payload.data.content}}\n\nReturn STRICT JSON with EXACTLY these keys and nothing else:\n{\n \"category\": \"one of: client-inquiry, vendor, newsletter, personal, spam, other\",\n \"summary\": \"one plain-language sentence on what this email is\",\n \"relevant\": \"true or false (a STRING, not a boolean): 'true' only when a real person or business appears to be reaching out and a human should look; 'false' for bulk mail, newsletters, and spam\"\n}\nAlways return ALL keys. Use conservative defaults (relevant 'false') when unsure."
209
+ },
210
+ "errorHandling": {
211
+ "continueOnError": true
212
+ },
213
+ "_note": "SINGLE-SUBJECT from the event (no loop node) — the prompt reads the one message off the trigger payload (content is bounded to 4k at the emit site). `relevant` is a STRING ('true'/'false') so the downstream `condition` can test it with an explicit `=== \"true\"` — a bare truthiness check would read the string 'false' as truthy (the documented gate-fail-open trap). Mirrors arch-client-intelligence's `assess` node (ai.generate json)."
214
+ }
215
+ },
216
+ {
217
+ "id": "relevant-gate",
218
+ "type": "condition",
219
+ "position": {
220
+ "x": 0,
221
+ "y": 480
222
+ },
223
+ "data": {
224
+ "label": "Only post when the email looks worth a human's attention",
225
+ "expression": "steps.assess.output.relevant === \"true\"",
226
+ "_note": "Bare-path + operator + quoted literal (the correct, non-fail-open condition grammar). Newsletters/spam ('false') are silently dropped — that IS the triage; the digest stays signal, not noise."
227
+ }
228
+ },
229
+ {
230
+ "id": "post",
231
+ "type": "output",
232
+ "position": {
233
+ "x": 0,
234
+ "y": 600
235
+ },
236
+ "data": {
237
+ "label": "Post one line into the run channel (TARGETLESS)",
238
+ "outputType": "channel_message",
239
+ "config": {
240
+ "content": "**{{steps.assess.output.category}}** — {{steps.assess.output.summary}}\nFrom {{trigger.payload.data.participantName}} · not yet linked to a client",
241
+ "metadata": {
242
+ "automationMessage": true,
243
+ "kind": "proton-inbox-triage"
244
+ }
245
+ },
246
+ "errorHandling": {
247
+ "continueOnError": true
248
+ },
249
+ "_note": "TARGETLESS on purpose (no channelId/channelType): the executor posts to this automation's OWN run channel, so an unbound-mail digest never lands in a client-facing or team channel by accident. Same posture as mail-feed's shadow post. NOT a reply and NOT a client-facing message — it is an internal triage line for the operator."
250
+ }
251
+ }
252
+ ],
253
+ "edges": [
254
+ {
255
+ "id": "e-trigger-proton",
256
+ "source": "trigger",
257
+ "target": "proton-gate"
258
+ },
259
+ {
260
+ "id": "e-proton-unbound",
261
+ "source": "proton-gate",
262
+ "target": "unbound-gate",
263
+ "sourceHandle": "yes"
264
+ },
265
+ {
266
+ "id": "e-unbound-assess",
267
+ "source": "unbound-gate",
268
+ "target": "assess",
269
+ "sourceHandle": "yes"
270
+ },
271
+ {
272
+ "id": "e-assess-relevant",
273
+ "source": "assess",
274
+ "target": "relevant-gate"
275
+ },
276
+ {
277
+ "id": "e-relevant-post",
278
+ "source": "relevant-gate",
279
+ "target": "post",
280
+ "sourceHandle": "yes"
281
+ }
282
+ ]
283
+ }
284
+ }
285
+ ]
286
+ },
287
+ "dependencies": []
288
+ }
@@ -0,0 +1,97 @@
1
+ {
2
+ "apiVersion": "synap/v1",
3
+ "kind": "capability",
4
+ "meta": {
5
+ "slug": "replicate-rvm",
6
+ "name": "Replicate — Video Background Removal",
7
+ "description": "Cut the person out of a recorded video with Robust Video Matting on Replicate: returns the matte and the person's own colours as videos, for putting a new background behind them. Each run bills your Replicate account. Replicate deletes the results after about an hour, so download them. Needs a Replicate API token.",
8
+ "tags": ["capability"],
9
+ "isPublic": true,
10
+ "syncByDefault": true
11
+ },
12
+ "spec": {
13
+ "key": "replicate-rvm",
14
+ "name": "Replicate Video Background Removal",
15
+ "description": "Serves the edit_video intent with Robust Video Matting (RVM, arielreplicate/robust_video_matting, version 73d2128a pinned) on Replicate's predictions API (base https://api.replicate.com/v1, Authorization: Bearer <token>). RVM is a recurrent model built for video: it keeps memory between frames, so the matte does not flicker the way a per-frame image model does. Its weights are GPL-3.0; here they run only as Replicate's hosted service, nothing is distributed. Stores the Replicate token in the vault, registers one HTTP API tool, and seeds two code verbs. replicate_rvm_matte (edit_video, a write that bills the account) takes { video: https URL } or { videoFile: a pod file id, signed by the pod at run time (file:url) }, plus outputs?: ['alpha','foreground','green-screen'] (default alpha + foreground) and starts one prediction per output: the alpha matte (white = the person) and the foreground (the model's own colour for the person, edges included, to composite without the old room's colour in the hair). replicate_rvm_result (fetch_record, read-only) reads them by id. The matte verb declares inputMap { video: $.video }, so a pod app's edit_video action (one input per intent) reaches it with the default outputs. GOTCHA: a skill isolate has no timer, so a verb cannot poll: the matte verb starts every prediction but the last without waiting and the last with Prefer: wait=60 (Replicate's maximum), so they run side by side, then reads the others once. A take that is not done by then comes back { status: 'processing', predictionIds, next: { verbId: 'replicate_rvm_result', parameters } }: run next every 30-60 s, never start the matte again. GOTCHA: Replicate deletes outputs about an hour after a prediction ends; download them at once. The video URL must be reachable by Replicate (a signed pod file URL is). PRECONDITION: a Replicate account with billing and an API token (https://replicate.com/account/api-tokens). Pass it as `replicateApiToken`.",
16
+ "params": [
17
+ {
18
+ "name": "replicateApiToken",
19
+ "label": "Replicate API token",
20
+ "type": "text",
21
+ "required": true,
22
+ "description": "The Replicate API token (https://replicate.com/account/api-tokens). Stored server-encrypted in the vault; never persisted in the template."
23
+ }
24
+ ],
25
+ "vault": [
26
+ {
27
+ "ref": "replicateApiTokenSecret",
28
+ "name": "{{name}} Replicate API token",
29
+ "value": "{{replicateApiToken}}",
30
+ "type": "api_key",
31
+ "description": "Replicate API token for the replicate_api connector."
32
+ }
33
+ ],
34
+ "tools": [
35
+ {
36
+ "name": "replicate_api",
37
+ "kind": "api",
38
+ "description": "Authenticated HTTP access to Replicate's predictions API (https://api.replicate.com/v1). The applier injects the vaulted token into the Authorization header as `Bearer <token>`; its credentialRef is remapped to the runtime vault://<id> at apply time.",
39
+ "credentialRef": "replicateApiTokenSecret",
40
+ "executor": "is-agent",
41
+ "config": {
42
+ "baseUrl": "https://api.replicate.com/v1",
43
+ "auth": {
44
+ "in": "header",
45
+ "name": "Authorization",
46
+ "prefix": "Bearer "
47
+ }
48
+ }
49
+ }
50
+ ],
51
+ "skills": [
52
+ {
53
+ "name": "replicate_rvm_matte",
54
+ "intent": "edit_video",
55
+ "kind": "code",
56
+ "scope": "pod",
57
+ "description": "Cuts the person out of a video with Robust Video Matting on Replicate (bills the Replicate account). Args: { video?: string (https URL of the video, no credentials) OR videoFile?: a pod file entity id (the pod signs it at run time; pass exactly one), outputs?: string[] (any of 'alpha', 'foreground', 'green-screen'; default ['alpha', 'foreground']) }. Only these args reach Replicate. Returns { status: 'succeeded' | 'processing', alphaUrl?, foregroundUrl?, greenScreenUrl?, predictions: [{ output, id, status, url, error }], predictionIds, next }. status 'processing' means a prediction was still running after about a minute: run next (replicate_rvm_result with the same predictionIds) every 30-60 s; never run this verb again for the same video. A failed prediction fails the run with its error. Replicate deletes the videos about an hour after they finish: download them at once.",
58
+ "requires": ["replicate_api"],
59
+ "executionMode": "async",
60
+ "timeoutSeconds": 150,
61
+ "metadata": {
62
+ "verbType": "write"
63
+ },
64
+ "parameters": {
65
+ "video": "string?",
66
+ "videoFile": "file:url?",
67
+ "outputs": "array?"
68
+ },
69
+ "inputMap": {
70
+ "params": {
71
+ "video": "$.video"
72
+ }
73
+ },
74
+ "code": "// Cut the person out of a video with Robust Video Matting on Replicate\n// (side-effecting write: starts billed predictions on the person's Replicate\n// account). Executor wraps this body as (async (args, context) => { <code> });\n// use args directly and `return`. callProvider(tool, method, path, body?, opts?)\n// resolves the replicate_api tool by NAME, injects the vaulted token as\n// Authorization: Bearer <token>, and uses the tool's config.baseUrl =\n// https://api.replicate.com/v1.\n//\n// LONG RUNS. The isolate has no timer, so this verb cannot poll. It starts one\n// prediction per output; all but the last start without waiting, the last\n// starts with `Prefer: wait=60` (Replicate holds that request up to 60 s, its\n// maximum), so the predictions run side by side while it waits. Then it reads\n// the earlier ones once. Whatever is not finished comes back as status\n// 'processing' with `next`: the read verb replicate_rvm_result and its\n// parameters, to run later. Never start the predictions again to wait.\n//\n// INPUTS: only `video` OR `videoFile`, and `outputs`, are read; nothing else\n// reaches Replicate. `video` is an https URL (the edit_video intent's input).\n// `videoFile` is a pod file: the caller passes its file entity id and the pod\n// swaps it for a fresh signed URL at run time (`file:url`, valid 6 h for an\n// async verb, so a cold Replicate start can still fetch it). That URL is never\n// echoed in an error or returned. The model version is pinned, so a run is\n// reproducible and a caller cannot point the token at another model.\nconst VERSION = '73d2128a371922d5d1abf0712a1d974be0e4e2358cc1218e4e34714767232bac';\nconst OUTPUT_TYPES = { alpha: 'alpha-mask', foreground: 'foreground-mask', 'green-screen': 'green-screen' };\nconst URL_KEYS = { alpha: 'alphaUrl', foreground: 'foregroundUrl', 'green-screen': 'greenScreenUrl' };\nif (args && args.input_video !== undefined) throw new Error('replicate_rvm_matte: pass video, not input_video');\nif (args && args.output_type !== undefined) throw new Error('replicate_rvm_matte: pass outputs, not output_type');\nconst HTTPS = /^https:\\/\\/[^\\s\\/?#@]+([\\/?#][^\\s]*)?$/i;\nconst given = String(args?.video ?? '').trim();\nconst fromPod = args?.videoFile === undefined || args?.videoFile === null ? '' : String(args.videoFile).trim();\nif (given && fromPod) throw new Error('replicate_rvm_matte: pass video (an https URL) or videoFile (a pod file), not both');\nif (!given && !fromPod) throw new Error('replicate_rvm_matte: video (an https URL) or videoFile (a pod file) is required');\n// Public https only, no userinfo (user:pass@), no data:/file:/vault: refs.\nif (given && (given.length > 2048 || !HTTPS.test(given))) {\n throw new Error('replicate_rvm_matte: video must be an https URL without credentials (got ' + given.slice(0, 80) + ')');\n}\n// The pod signs videoFile; anything but a signed https URL means it was not swapped.\nif (fromPod && (fromPod.length > 4096 || !HTTPS.test(fromPod))) {\n throw new Error('replicate_rvm_matte: videoFile did not arrive as a signed pod file URL; pass the take\\'s file entity id');\n}\nconst video = given || fromPod;\nconst source = given ? 'url' : 'pod-file';\nlet outputs = ['alpha', 'foreground'];\nif (args?.outputs !== undefined) {\n if (!Array.isArray(args.outputs) || args.outputs.length === 0) throw new Error('replicate_rvm_matte: outputs must be a non-empty array of alpha, foreground, green-screen');\n outputs = [];\n for (const o of args.outputs) {\n const k = String(o ?? '').trim();\n if (!Object.prototype.hasOwnProperty.call(OUTPUT_TYPES, k)) throw new Error('replicate_rvm_matte: unknown output ' + k.slice(0, 40) + ' (use alpha, foreground or green-screen)');\n if (!outputs.includes(k)) outputs.push(k);\n }\n}\n\nconst FAILED = ['failed', 'canceled', 'aborted'];\nconst ID_RE = /^[a-z0-9]{8,64}$/;\nconst shape = (output, p) => {\n const status = String(p?.status ?? '');\n const out = Array.isArray(p?.output) ? p.output[0] : p?.output;\n if (status === 'succeeded') {\n if (typeof out !== 'string' || !/^https:\\/\\//i.test(out)) return { output, id: p.id, status: 'failed', url: null, error: 'the prediction succeeded with no video URL' };\n return { output, id: p.id, status: 'succeeded', url: out, error: null };\n }\n if (FAILED.includes(status)) return { output, id: p.id, status: 'failed', url: null, error: String(p?.error ?? status).slice(0, 300) };\n return { output, id: p.id, status: 'processing', url: null, error: null };\n};\nconst ok = (res) => typeof res?.status === 'number' && res.status >= 200 && res.status < 300;\n\nconst predictions = [];\nfor (let i = 0; i < outputs.length; i++) {\n const last = i === outputs.length - 1;\n const res = await callProvider(\n 'replicate_api',\n 'POST',\n '/predictions',\n { version: VERSION, input: { input_video: video, output_type: OUTPUT_TYPES[outputs[i]] } },\n last ? { headers: { Prefer: 'wait=60' } } : undefined,\n );\n // The capability-execution gate may return a PROPOSAL envelope instead of\n // executing: that proposal IS the record. Report what already started.\n if (res?.proposed === true) {\n return { success: false, proposed: true, proposalId: res?.proposalId ?? null, started: predictions.map((p) => p.id) };\n }\n const body = res?.body ?? {};\n if (!ok(res) || typeof body.id !== 'string' || !ID_RE.test(body.id)) {\n const detail = typeof body?.detail === 'string' ? ': ' + body.detail.slice(0, 200) : '';\n throw new Error('replicate_rvm_matte: Replicate refused the ' + outputs[i] + ' prediction (status ' + (res?.status ?? 'unknown') + ')' + detail);\n }\n predictions.push(shape(outputs[i], body));\n}\n// The earlier predictions ran while the last one waited: read each once.\nfor (let i = 0; i < predictions.length - 1; i++) {\n if (predictions[i].status !== 'processing') continue;\n const res = await callProvider('replicate_api', 'GET', '/predictions/' + predictions[i].id);\n if (ok(res) && res.body?.id === predictions[i].id) predictions[i] = shape(predictions[i].output, res.body);\n}\n\nconst failed = predictions.filter((p) => p.status === 'failed');\nif (failed.length) {\n throw new Error('replicate_rvm_matte: ' + failed.map((p) => p.output + ' prediction ' + p.id + ' failed: ' + p.error).join('; '));\n}\nconst predictionIds = predictions.map((p) => p.id);\nconst status = predictions.some((p) => p.status === 'processing') ? 'processing' : 'succeeded';\nconst urls = {};\nfor (const p of predictions) if (p.url) urls[URL_KEYS[p.output]] = p.url;\nreturn {\n success: true,\n proposed: false,\n source,\n status,\n ...urls,\n predictions,\n predictionIds,\n next: status === 'processing' ? { verbId: 'replicate_rvm_result', parameters: { predictionIds } } : null,\n note: 'Replicate deletes prediction outputs about an hour after they finish: download the videos now.',\n};"
75
+ },
76
+ {
77
+ "name": "replicate_rvm_result",
78
+ "intent": "fetch_record",
79
+ "kind": "code",
80
+ "scope": "pod",
81
+ "description": "Reads Robust Video Matting predictions that replicate_rvm_matte started (read-only: nothing is started or billed). Args: { predictionIds: string[] (1-3 ids from replicate_rvm_matte) }. Returns the same shape: { status: 'succeeded' | 'processing', alphaUrl?, foregroundUrl?, greenScreenUrl?, predictions, predictionIds, next }. A failed prediction fails the run with its error; an id that is not an RVM prediction is refused.",
82
+ "requires": ["replicate_api"],
83
+ "executionMode": "sync",
84
+ "timeoutSeconds": 30,
85
+ "metadata": {
86
+ "readOnly": true
87
+ },
88
+ "parameters": {
89
+ "predictionIds": "array"
90
+ },
91
+ "code": "// Read Robust Video Matting predictions started by replicate_rvm_matte (a\n// read: GET only, nothing is billed or started). Executor wraps this body as\n// (async (args, context) => { <code> }). callProvider resolves the\n// replicate_api tool by NAME and injects the vaulted token.\n//\n// Run it while replicate_rvm_matte's result says status 'processing', with the\n// predictionIds it returned (its `next`), every 30-60 s. Only predictions of\n// the pinned RVM version are read: another id on the same token is refused.\nconst VERSION = '73d2128a371922d5d1abf0712a1d974be0e4e2358cc1218e4e34714767232bac';\nconst OUTPUTS = { 'alpha-mask': 'alpha', 'foreground-mask': 'foreground', 'green-screen': 'green-screen' };\nconst URL_KEYS = { alpha: 'alphaUrl', foreground: 'foregroundUrl', 'green-screen': 'greenScreenUrl' };\nif (args && args.prediction_ids !== undefined) throw new Error('replicate_rvm_result: pass predictionIds, not prediction_ids');\nif (!Array.isArray(args?.predictionIds) || args.predictionIds.length === 0) {\n throw new Error('replicate_rvm_result: predictionIds is required (the ids replicate_rvm_matte returned)');\n}\nconst ids = [];\nfor (const raw of args.predictionIds) {\n const id = String(raw ?? '').trim();\n if (!/^[a-z0-9]{8,64}$/.test(id)) throw new Error('replicate_rvm_result: ' + id.slice(0, 40) + ' is not a Replicate prediction id');\n if (!ids.includes(id)) ids.push(id);\n}\nif (ids.length > 3) throw new Error('replicate_rvm_result: predictionIds takes at most 3 ids');\n\nconst FAILED = ['failed', 'canceled', 'aborted'];\nconst predictions = [];\nfor (const id of ids) {\n const res = await callProvider('replicate_api', 'GET', '/predictions/' + id);\n const p = res?.body ?? {};\n if (!(typeof res?.status === 'number' && res.status >= 200 && res.status < 300) || p.id !== id) {\n throw new Error('replicate_rvm_result: could not read prediction ' + id + ' (status ' + (res?.status ?? 'unknown') + ')');\n }\n if (p.version !== VERSION) throw new Error('replicate_rvm_result: prediction ' + id + ' is not a Robust Video Matting prediction');\n const output = OUTPUTS[p?.input?.output_type ?? 'green-screen'] ?? null;\n const status = String(p.status ?? '');\n const out = Array.isArray(p.output) ? p.output[0] : p.output;\n if (status === 'succeeded' && typeof out === 'string' && /^https:\\/\\//i.test(out)) predictions.push({ output, id, status: 'succeeded', url: out, error: null });\n else if (status === 'succeeded') predictions.push({ output, id, status: 'failed', url: null, error: 'the prediction succeeded with no video URL' });\n else if (FAILED.includes(status)) predictions.push({ output, id, status: 'failed', url: null, error: String(p.error ?? status).slice(0, 300) });\n else predictions.push({ output, id, status: 'processing', url: null, error: null });\n}\n\nconst failed = predictions.filter((p) => p.status === 'failed');\nif (failed.length) {\n throw new Error('replicate_rvm_result: ' + failed.map((p) => p.output + ' prediction ' + p.id + ' failed: ' + p.error).join('; '));\n}\nconst status = predictions.some((p) => p.status === 'processing') ? 'processing' : 'succeeded';\nconst urls = {};\nfor (const p of predictions) if (p.url && p.output) urls[URL_KEYS[p.output]] = p.url;\nreturn {\n success: true,\n status,\n ...urls,\n predictions,\n predictionIds: ids,\n next: status === 'processing' ? { verbId: 'replicate_rvm_result', parameters: { predictionIds: ids } } : null,\n note: 'Replicate deletes prediction outputs about an hour after they finish: download the videos now.',\n};"
92
+ }
93
+ ],
94
+ "provides": ["edit_video", "fetch_record"]
95
+ },
96
+ "dependencies": []
97
+ }
@@ -0,0 +1,57 @@
1
+ {
2
+ "apiVersion": "synap/v1",
3
+ "kind": "capability",
4
+ "meta": {
5
+ "slug": "research-methods",
6
+ "name": "Research — Method Skills",
7
+ "description": "Four research methods for agents: rank sources, weigh evidence into a finding, map findings onto a concept graph, and structure open questions as decisions. Used by the Research and Decision Analysis workspaces; pairs with Exa for web search.",
8
+ "tags": [
9
+ "capability"
10
+ ],
11
+ "isPublic": true,
12
+ "syncByDefault": true
13
+ },
14
+ "spec": {
15
+ "key": "research-methods",
16
+ "name": "Research — Method Skills",
17
+ "description": "The research method bundle: four instruction skills (prompt/doc skills injected into the agent's turn — no sandboxed code, no connector calls) that teach the agent HOW to do research well, not just gather links. source-triage builds the full ranked evidence surface and keeps the rejected sources (nothing lost); evidence-synthesis weighs supporting vs contradicting sources into a calibrated finding; concept-cartography places findings onto the field/concept knowledge map and dedups; ibis-decision-analysis structures an open question as a decision with its arguments and roads-not-taken preserved. These are pure know-how — they shape WHAT the agent writes into the research graph. Every write stays proposal-gated and reversible. Granted by the research-base and research-decision workspace templates; pairs with a web-search tool (exa) for source gathering.",
18
+ "params": [],
19
+ "vault": [],
20
+ "tools": [],
21
+ "skills": [
22
+ {
23
+ "name": "source-triage",
24
+ "kind": "instruction",
25
+ "scope": "pod",
26
+ "description": "Gather sources widely, rank them by credibility/quality, and KEEP the rejected ones with a reason (nothing lost). Feeds supports/contradicts links to a hypothesis. Pairs with a web-search tool.",
27
+ "requires": [
28
+ "exa_search"
29
+ ],
30
+ "code": "# Source Triage\n\nYou are gathering and ranking the sources for a piece of research. The point is not to find \"the answer\" and stop — it is to build the FULL evidence surface and rank it honestly, so the reasoning that follows can be trusted and re-checked later. A researcher who keeps only the sources that agreed with them has hidden their work.\n\n## Inputs you are given\n- The question or hypothesis under investigation.\n- Whatever the agent can retrieve (web search results, provided documents, the pod's existing sources).\n\n## How to triage\n1. **Gather widely first, judge second.** Pull more candidate sources than you'll cite — including ones you expect to disagree with. Breadth before verdict.\n2. **Classify the source TYPE** honestly: paper, dataset, book, article, blog, forum, video, other. Type shapes how much weight it can bear.\n3. **Assign a credibility band** — high / medium / low / unknown — from concrete signals, not vibes: primary vs secondary, peer review, author expertise, recency, methodology, corroboration by independent sources, and conflicts of interest. State the signal, don't just assert the band.\n4. **KEEP everything, mark the rejects.** A source you considered and decided NOT to rely on is still recorded — as a source with `status: rejected` and a one-line `whyRejected` (out of date, unsupported claim, primary source contradicts it, low-quality outlet). Never silently drop it. The rejected pile is part of the evidence.\n5. **Cited = the ones you actually rely on** (`status: cited`); **considered = still in play** but not yet load-bearing.\n\n## Hard rules\n- Never inflate a blog or a single forum post to \"high\" credibility because it's convenient.\n- When two credible sources conflict, keep BOTH and record the conflict — do not pick one and delete the other.\n- Never invent a source, a URL, or an author. If you can't verify it, band is `unknown`.\n\n## Output\nFor each source, propose a `source` entity with: title, url, author, sourceType, credibilityBand, an optional qualityScore (0–100), status (cited | considered | rejected), and whyRejected when rejected. Link cited/considered sources to the hypothesis they bear on with `supports` or `contradicts`. Every write is proposal-gated."
31
+ },
32
+ {
33
+ "name": "evidence-synthesis",
34
+ "kind": "instruction",
35
+ "scope": "pod",
36
+ "description": "Weigh supporting vs contradicting sources into a finding with honestly calibrated confidence, confronting the contradictions rather than hiding them. Pure know-how.",
37
+ "code": "# Evidence Synthesis\n\nYou are turning a ranked set of sources into a FINDING — a validated (or provisional) claim with calibrated confidence. This is the step where evidence becomes knowledge. The danger is false certainty: a confident finding built on weak or one-sided evidence is worse than an honest \"we don't know yet\".\n\n## Inputs you are given\n- A question or hypothesis.\n- The triaged sources bearing on it — those that support it and those that contradict it, with their credibility bands.\n\n## How to synthesize\n1. **Weigh, don't count.** Three low-credibility blogs do not outweigh one strong primary source. Give weight by credibility band and directness of evidence, not by headcount.\n2. **Confront the contradictions.** Explicitly account for the sources that disagree. A finding that ignores its counter-evidence isn't validated — it's asserted. If the contradiction is unresolved, say so and lower confidence.\n3. **Separate what's established from what's inferred.** State the claim at the strength the evidence actually supports — not stronger. Prefer a narrow claim you can stand behind over a broad one you can't.\n4. **Calibrate confidence honestly** (0–1): high (>0.8) only with strong, corroborated, uncontested evidence; provisional (~0.4–0.7) when evidence is real but partial or mixed; low (<0.4) when it's suggestive at best. If the evidence is too thin, do NOT emit a finding — record what's still needed instead.\n5. **Cite your grounds.** Every finding links to the specific sources it rests on with `supported_by`, and to the research it came from with `derived_from`.\n\n## Hard rules\n- Never round confidence up to make the finding feel conclusive.\n- Never drop the dissenting evidence to make the story clean.\n- A finding with no source link is an opinion, not a finding — don't emit it.\n\n## Output\nPropose a `finding` entity: claim (stated at the supported strength), confidence (0–1), evidenceSummary (how the sources were weighed, including the contradictions), status (validated | provisional | retracted). Link it `supported_by` its sources and `derived_from` the research, and `about` the concept(s) it concerns. Proposal-gated."
38
+ },
39
+ {
40
+ "name": "concept-cartography",
41
+ "kind": "instruction",
42
+ "scope": "pod",
43
+ "description": "Place findings/concepts onto the field/concept knowledge map (part_of, overlaps_with, prerequisite_of), resolving duplicates before creating. Pure know-how.",
44
+ "code": "# Concept Cartography\n\nYou are placing what was learned onto the knowledge MAP — the graph of fields and concepts — so a finding isn't an island but a located point a researcher can navigate to and build from. Good cartography is what turns a pile of notes into a second brain: it reveals what connects to what, and what to learn next.\n\n## Inputs you are given\n- A concept, field, or finding to place.\n- The existing fields and concepts already in the pod (always check before creating).\n\n## How to map\n1. **Resolve before you create.** Search existing `field` and `concept` entities first — \"Bayesian inference\", \"gradient descent\", \"qualia\" should exist ONCE. Match on the real idea, not the exact spelling, before minting a duplicate.\n2. **Locate the concept in its field(s)** with `part_of` — and note when a concept genuinely spans several fields (e.g. \"information theory\" is part_of both mathematics and AI). Cross-field concepts are the interesting ones; don't force a single home.\n3. **Wire the field overlaps.** When two fields share territory, link them `overlaps_with` (AI overlaps_with mathematics, philosophy, cognitive science). This is the map's connective tissue — the thing that lets the system suggest \"you're studying X; here's its neighbour Y\".\n4. **Chain the prerequisites.** When understanding concept A genuinely requires concept B first, link `prerequisite_of` (linear algebra prerequisite_of gradient descent). Use it for real dependencies, not loose association — that's what `relates_to` is for.\n5. **Attach the finding.** Link the finding `about` the concept(s) it concerns, so the map shows not just ideas but what we've actually established about them.\n\n## Hard rules\n- Never create a second entity for a concept that already exists — extend the existing one.\n- Don't invent a prerequisite chain to look rigorous; only link dependencies you can justify.\n- A field/concept with no links is barely on the map — always place at least one edge.\n\n## Output\nPropose `field`/`concept` entities (only when they don't already exist) and the edges above: `part_of`, `overlaps_with`, `prerequisite_of`, `relates_to`, `about`. Proposal-gated."
45
+ },
46
+ {
47
+ "name": "ibis-decision-analysis",
48
+ "kind": "instruction",
49
+ "scope": "pod",
50
+ "description": "Structure an open question as a decision via IBIS: options as hypotheses, arguments as supporting/contradicting sources, and a decision that preserves the roads-not-taken and why. Pure know-how.",
51
+ "code": "# Decision Analysis (IBIS)\n\nYou are structuring an open question as a DECISION using the IBIS method (Issue → Positions → Arguments) so the reasoning behind the recommendation is fully captured — not just the answer. The value is that six months later, anyone can see which options were considered, what argued for and against each, and why the chosen one won. A recommendation with no visible argument is just an assertion.\n\n## Inputs you are given\n- The question / issue to decide.\n- The research so far — hypotheses (candidate positions) and the sources bearing on them.\n\n## How to analyze\n1. **State the issue sharply.** One decision, framed so the options are genuinely mutually exclusive. A fuzzy issue produces a fuzzy decision.\n2. **Enumerate the real options** as `hypothesis` entities — including the status-quo / do-nothing option. Two options is often a false binary; look for the third.\n3. **Argue each side with EVIDENCE, not preference.** For every option, gather the sources that `support` it and those that `contradict` it (use Source Triage — keep the rejected ones too). An argument with no source behind it is weak and should be marked as such.\n4. **Weigh against explicit criteria.** Name the criteria that matter for THIS decision (cost, reversibility, risk, time, evidence strength) before scoring — so the weighing is honest, not retrofitted to a preferred answer.\n5. **Record the decision, not just the winner.** The recommendation becomes a `decision` entity whose `rationale` explains why it won and whose `alternatives` field preserves the options that lost and WHY. The losing options stay linked — the reasoning is the deliverable.\n6. **State what would change your mind.** Note the evidence that, if it appeared, would flip the decision. This is what makes it revisitable rather than final.\n\n## Hard rules\n- Never present one option as obvious if the evidence is genuinely mixed — surface the trade-off.\n- Never discard the rejected options or the counter-arguments; `alternatives` must record them.\n- Confidence in the recommendation is bounded by the weakest link in its evidence — don't overstate it.\n\n## Output\nPropose: `hypothesis` entities for the options (with `stance`), `source` links (`supports`/`contradicts`) for the arguments, and a `decision` entity with summary, rationale, alternatives (the roads not taken + why), and decisionStatus. Link the decision to the question it resolves. Proposal-gated."
52
+ }
53
+ ],
54
+ "provides": []
55
+ },
56
+ "dependencies": []
57
+ }