@synap-core/workspace-templates 0.12.0 → 0.23.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 (245) hide show
  1. package/README.md +483 -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 +420 -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 +471 -0
  54. package/capabilities/vercel.capability.json +424 -0
  55. package/dist/define-CPS0n8BV.d.ts +1331 -0
  56. package/dist/index.d.ts +174 -900
  57. package/dist/index.js +26148 -23931
  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/packs/bookmark-views-pack.json +33 -0
  64. package/packs/crm-pack.json +76 -0
  65. package/packs/customer-profile.json +28 -0
  66. package/packs/deep-research.playbook.json +30 -0
  67. package/packs/event-views-pack.json +33 -0
  68. package/packs/habit-profile.json +27 -0
  69. package/packs/hr-pack.json +54 -0
  70. package/packs/invoice-profile.json +28 -0
  71. package/packs/knowledge-base-pack.json +56 -0
  72. package/packs/meeting-profile.json +29 -0
  73. package/packs/product-profile.json +30 -0
  74. package/packs/project-management-pack.json +49 -0
  75. package/packs/recipe-profile.json +29 -0
  76. package/packs/task-views-pack.json +39 -0
  77. package/retired.yaml +136 -0
  78. package/skills/app-factory-build-slice/SKILL.md +70 -0
  79. package/skills/app-factory-build-slice.catalog.json +17 -0
  80. package/skills/app-factory-design-canvas/SKILL.md +96 -0
  81. package/skills/app-factory-design-canvas.catalog.json +17 -0
  82. package/skills/app-factory-design-review/SKILL.md +99 -0
  83. package/skills/app-factory-design-review.catalog.json +18 -0
  84. package/skills/app-factory-research/SKILL.md +123 -0
  85. package/skills/app-factory-research.catalog.json +17 -0
  86. package/skills/bookmark-enrichment/SKILL.md +40 -0
  87. package/skills/carousel-format/carousel-format.skill.json +18 -0
  88. package/skills/collage-broll/collage-broll.skill.json +20 -0
  89. package/skills/content-taste/content-taste.skill.json +18 -0
  90. package/skills/creative-director/creative-director.skill.json +18 -0
  91. package/skills/design-critique/SKILL.md +135 -0
  92. package/skills/design-critique.catalog.json +12 -0
  93. package/skills/designing-with-real-components/SKILL.md +140 -0
  94. package/skills/designing-with-real-components.catalog.json +12 -0
  95. package/skills/edit-loop/edit-loop.skill.json +25 -0
  96. package/skills/facecam-edit/facecam-edit.skill.json +24 -0
  97. package/skills/mapping-the-server/SKILL.md +54 -0
  98. package/skills/math-explainer-manimgx/math-explainer-manimgx.skill.json +20 -0
  99. package/skills/motion-quality-gates/motion-quality-gates.skill.json +20 -0
  100. package/skills/open-design-edit/open-design-edit.skill.json +24 -0
  101. package/skills/reading-client-progress/SKILL.md +65 -0
  102. package/skills/routing-to-channels/SKILL.md +54 -0
  103. package/skills/stellar-scf-grants-advisor/ASSOCIATE-CHEATSHEET.md +133 -0
  104. package/skills/stellar-scf-grants-advisor/ASSOCIATE-ONBOARDING-GUIDE.md +761 -0
  105. package/skills/stellar-scf-grants-advisor/CLIENT-FAQ.md +181 -0
  106. package/skills/stellar-scf-grants-advisor/PARTNER-HANDOVER.md +130 -0
  107. package/skills/stellar-scf-grants-advisor/SKILL.md +102 -0
  108. package/skills/stellar-scf-grants-advisor/examples/README.md +43 -0
  109. package/skills/stellar-scf-grants-advisor/examples/chat-summary-templates.md +158 -0
  110. package/skills/stellar-scf-grants-advisor/examples/corpus-comparable-rows.md +136 -0
  111. package/skills/stellar-scf-grants-advisor/examples/q10-team-rewrites.md +109 -0
  112. package/skills/stellar-scf-grants-advisor/examples/q2-description-rewrites.md +88 -0
  113. package/skills/stellar-scf-grants-advisor/examples/q6-integration-rewrites.md +107 -0
  114. package/skills/stellar-scf-grants-advisor/examples/tranche-structures.md +176 -0
  115. package/skills/stellar-scf-grants-advisor/reference/01-methodology.md +96 -0
  116. package/skills/stellar-scf-grants-advisor/reference/02-scoring-framework.md +199 -0
  117. package/skills/stellar-scf-grants-advisor/reference/03-rejection-patterns.md +241 -0
  118. package/skills/stellar-scf-grants-advisor/reference/04-winner-patterns.md +120 -0
  119. package/skills/stellar-scf-grants-advisor/reference/05-comeback-patterns.md +92 -0
  120. package/skills/stellar-scf-grants-advisor/reference/06-budget-benchmarks.md +125 -0
  121. package/skills/stellar-scf-grants-advisor/reference/07-integration-list.md +147 -0
  122. package/skills/stellar-scf-grants-advisor/reference/08-abstract-fields.md +258 -0
  123. package/skills/stellar-scf-grants-advisor/reference/09-build-fields.md +259 -0
  124. package/skills/stellar-scf-grants-advisor/reference/10-corpus-winners.md +135 -0
  125. package/skills/stellar-scf-grants-advisor/reference/11-corpus-rejects.md +267 -0
  126. package/skills/stellar-scf-grants-advisor/reference/12-client-communication.md +131 -0
  127. package/skills/stellar-scf-grants-advisor/reference/13-excel-output-spec.md +199 -0
  128. package/skills/stellar-scf-grants-advisor/reference/14-worked-examples.md +160 -0
  129. package/skills/stellar-scf-grants-advisor/templates/abstract-review-builder.py +253 -0
  130. package/skills/stellar-scf-grants-advisor/templates/build-review-builder.py +213 -0
  131. package/skills/video-direction/video-direction.skill.json +20 -0
  132. package/skills/video-style-vocabulary/video-style-vocabulary.skill.json +20 -0
  133. package/skills/video-toolbox/video-toolbox.skill.json +20 -0
  134. package/src/__fixtures__/lifecycle-presets.flat-before.json +205 -0
  135. package/src/agency-os.yaml +122 -0
  136. package/src/agent-os-skill-slugs.test.ts +65 -0
  137. package/src/apify-social-reader.test.ts +317 -0
  138. package/src/base.yaml +85 -39
  139. package/src/blockchain-ecosystem.yaml +1 -0
  140. package/src/brand-library.yaml +525 -14
  141. package/src/builder-dev-fold.template.test.ts +113 -0
  142. package/src/builder.yaml +779 -91
  143. package/src/business-model-catalog-description.test.ts +47 -0
  144. package/src/business-model-grp.template.test.ts +214 -0
  145. package/src/business-model.yaml +296 -49
  146. package/src/catalog-safety.test.ts +198 -0
  147. package/src/catalog.template-items.test.ts +75 -0
  148. package/src/catalog.ts +66 -2
  149. package/src/concept-guard.test.ts +28 -8
  150. package/src/concept-guard.ts +2 -2
  151. package/src/content-creation.template.test.ts +52 -0
  152. package/src/content-creation.yaml +192 -0
  153. package/src/content-os.edit-loop.template.test.ts +210 -0
  154. package/src/content-os.publish-due-posts.template.test.ts +261 -0
  155. package/src/content-os.template.test.ts +322 -0
  156. package/src/content-os.video-takes.template.test.ts +338 -0
  157. package/src/content-os.work-a-take.template.test.ts +125 -0
  158. package/src/content-os.yaml +1830 -39
  159. package/src/content-studio-retired.template.test.ts +82 -0
  160. package/src/corpus-flow-validity.test.ts +4 -5
  161. package/src/crm-absorbs-outreach.template.test.ts +117 -0
  162. package/src/crm-build-plan.template.test.ts +128 -0
  163. package/src/crm-lists.template.test.ts +96 -0
  164. package/src/crm-messages-cadence.template.test.ts +164 -0
  165. package/src/crm-one-send-door.template.test.ts +65 -0
  166. package/src/crm.yaml +1456 -181
  167. package/src/decisions-wave.template.test.ts +186 -0
  168. package/src/define.ts +130 -17
  169. package/src/design-to-implementation.template.test.ts +94 -0
  170. package/src/domain-template-type.test.ts +75 -0
  171. package/src/ecosystem.yaml +27 -66
  172. package/src/enterprise-os.yaml +10 -134
  173. package/src/facecam-edit.audio-studio.test.ts +188 -0
  174. package/src/fal-generate-image.test.ts +200 -0
  175. package/src/fal-media-lab-pod-files.test.ts +180 -0
  176. package/src/finance.yaml +254 -1
  177. package/src/foundation.yaml +78 -38
  178. package/src/frame-host-modules.d.mts +6 -0
  179. package/src/frame-host-modules.mjs +58 -0
  180. package/src/generate.mjs +9 -3
  181. package/src/github-merge.test.ts +210 -0
  182. package/src/grants.yaml +46 -17
  183. package/src/hr.yaml +39 -6
  184. package/src/index.ts +36 -1
  185. package/src/intent.test.ts +161 -0
  186. package/src/internal-runbook.yaml +19 -1
  187. package/src/jev.test.ts +356 -0
  188. package/src/legal.yaml +33 -1
  189. package/src/life-os.yaml +51 -240
  190. package/src/lifecycle-coverage.test.ts +124 -0
  191. package/src/lifecycle-declarations.test.ts +252 -0
  192. package/src/lifecycle-presets.test.ts +468 -0
  193. package/src/lifecycle-presets.ts +494 -0
  194. package/src/lifecycle-stuck.test.ts +46 -0
  195. package/src/lifecycle-waits.parity.test.ts +319 -0
  196. package/src/marketing.yaml +108 -4
  197. package/src/meetings-knowledge.yaml +18 -0
  198. package/src/networking.yaml +23 -2
  199. package/src/operations.yaml +304 -4
  200. package/src/pipeline-views.template.test.ts +88 -0
  201. package/src/playbook-goal-placeholders.test.ts +57 -0
  202. package/src/postiz-contract.test.ts +134 -0
  203. package/src/presentation-declarations.test.ts +174 -0
  204. package/src/process-contract.validate.test.ts +352 -0
  205. package/src/project-management.yaml +39 -740
  206. package/src/publish-body.d.mts +29 -0
  207. package/src/publish-body.mjs +74 -0
  208. package/src/publish-body.test.ts +74 -0
  209. package/src/publish-official-lib.test.ts +176 -0
  210. package/src/replicate-rvm.test.ts +504 -0
  211. package/src/research-base.yaml +119 -20
  212. package/src/research-decision.yaml +60 -3
  213. package/src/reserved-profile-slugs.test.ts +83 -0
  214. package/src/retired-goal-kinds.test.ts +58 -0
  215. package/src/role-reference-and-step-domain.test.ts +192 -0
  216. package/src/seed-obligation.test.ts +1 -6
  217. package/src/send-message-maps.test.ts +227 -0
  218. package/src/shared-role-ssot.test.ts +40 -2
  219. package/src/ship-a-change.template.test.ts +177 -0
  220. package/src/skills.test.ts +170 -0
  221. package/src/social.yaml +85 -27
  222. package/src/space-packages.freshness.test.ts +97 -0
  223. package/src/space-packages.generated.ts +40 -0
  224. package/src/space-packages.test.ts +125 -0
  225. package/src/space-packages.ts +73 -0
  226. package/src/start.test.ts +188 -0
  227. package/src/task-intents.test.ts +293 -0
  228. package/src/templates.ts +20851 -21496
  229. package/src/types.ts +443 -60
  230. package/src/unipile-publish-post.test.ts +162 -0
  231. package/src/validate-flow.drift.test.ts +2 -3
  232. package/src/validate-flow.ts +22 -2
  233. package/src/validate-onboarding.test.ts +59 -0
  234. package/src/validate.test.ts +71 -4
  235. package/src/validate.ts +1335 -7
  236. package/src/wire-core.ts +231 -0
  237. package/src/wire.test.ts +159 -0
  238. package/src/wire.ts +107 -0
  239. package/src/agent-fleet.yaml +0 -1645
  240. package/src/business-developer.template.test.ts +0 -351
  241. package/src/business-developer.yaml +0 -860
  242. package/src/content-studio.yaml +0 -361
  243. package/src/dev-dashboard.yaml +0 -990
  244. package/src/outreach-comms.yaml +0 -486
  245. package/src/the-arch.yaml +0 -201
@@ -0,0 +1,504 @@
1
+ /**
2
+ * Replicate Robust Video Matting (capability `replicate-rvm`): the marketplace
3
+ * tool that serves `edit_video` (founder decisions be0deab7 and ac1ba2b6,
4
+ * 2026-10-28). The app's input is `{ video: <https URL> }`.
5
+ *
6
+ * SEAM: each verb's REAL `code` body, run the way the pod's executor wraps it
7
+ * (`async (args, context) => { <code> }` with `callProvider` in scope),
8
+ * against Replicate responses shaped like its documented prediction object
9
+ * (`{ id, version, input, status, output, error }`). The request asserted is
10
+ * exactly what `callProvider` would send to api.replicate.com/v1.
11
+ *
12
+ * The long-run design under test: an isolate has no timer, so the matte verb
13
+ * starts every prediction, waits on the LAST one only (`Prefer: wait=60`),
14
+ * reads the others once, and hands anything unfinished to the read verb
15
+ * through `next` instead of starting the predictions again.
16
+ *
17
+ * NOT covered: Replicate's live behaviour (payloads are hand-written from its
18
+ * API page), the pod's propose-moment arg check and the IS isolate (Node's
19
+ * AsyncFunction is used).
20
+ */
21
+ import { readFileSync } from "node:fs";
22
+ import { dirname, join } from "node:path";
23
+ import { fileURLToPath } from "node:url";
24
+ import { describe, expect, it } from "vitest";
25
+
26
+ const HERE = dirname(fileURLToPath(import.meta.url));
27
+ const manifest = JSON.parse(
28
+ readFileSync(
29
+ join(HERE, "../capabilities/replicate-rvm.capability.json"),
30
+ "utf8"
31
+ )
32
+ );
33
+ type Skill = {
34
+ name: string;
35
+ intent?: string;
36
+ code?: string;
37
+ parameters?: Record<string, string>;
38
+ metadata?: Record<string, unknown>;
39
+ };
40
+ const skills = manifest.spec.skills as Skill[];
41
+ const verb = (name: string) => skills.find((s) => s.name === name)!;
42
+
43
+ const AsyncFunction = Object.getPrototypeOf(async () => {}).constructor as new (
44
+ ...args: string[]
45
+ ) => (...a: unknown[]) => Promise<unknown>;
46
+ const matte = new AsyncFunction(
47
+ "args",
48
+ "context",
49
+ "callProvider",
50
+ verb("replicate_rvm_matte").code!
51
+ );
52
+ const result = new AsyncFunction(
53
+ "args",
54
+ "context",
55
+ "callProvider",
56
+ verb("replicate_rvm_result").code!
57
+ );
58
+
59
+ const VERSION =
60
+ "73d2128a371922d5d1abf0712a1d974be0e4e2358cc1218e4e34714767232bac";
61
+ const VIDEO =
62
+ "https://pod.example.synap.live/files/take-1.mp4?sig=abc&exp=1730000000";
63
+ const OUT = (t: string) => `https://replicate.delivery/xezq/${t}/output.mp4`;
64
+
65
+ type Call = {
66
+ tool: string;
67
+ method: string;
68
+ path: string;
69
+ body?: Record<string, any>;
70
+ opts?: Record<string, any>;
71
+ };
72
+ type Pred = {
73
+ id: string;
74
+ status: string;
75
+ output?: unknown;
76
+ error?: string | null;
77
+ type: string;
78
+ };
79
+
80
+ /** A fake Replicate: POST creates a prediction in the state `onCreate` says; GET returns `onGet`. */
81
+ function replicate(opts: {
82
+ onCreate: (type: string, waited: boolean) => Omit<Pred, "id" | "type">;
83
+ onGet?: (p: Pred) => Omit<Pred, "id" | "type">;
84
+ }) {
85
+ const calls: Call[] = [];
86
+ const preds = new Map<string, Pred>();
87
+ let n = 0;
88
+ const callProvider = async (
89
+ tool: string,
90
+ method: string,
91
+ path: string,
92
+ body?: Record<string, any>,
93
+ o?: Record<string, any>
94
+ ) => {
95
+ calls.push({ tool, method, path, body, opts: o });
96
+ if (method === "POST") {
97
+ const type = body!.input.output_type;
98
+ const p: Pred = {
99
+ id: `pred${++n}abcdefgh`,
100
+ type,
101
+ ...opts.onCreate(type, o?.headers?.Prefer === "wait=60"),
102
+ };
103
+ preds.set(p.id, p);
104
+ return {
105
+ status: 201,
106
+ headers: {},
107
+ body: {
108
+ id: p.id,
109
+ version: body!.version,
110
+ input: body!.input,
111
+ status: p.status,
112
+ output: p.output ?? null,
113
+ error: p.error ?? null,
114
+ },
115
+ };
116
+ }
117
+ const id = path.split("/").pop()!;
118
+ const p = preds.get(id) ?? {
119
+ id,
120
+ type: "alpha-mask",
121
+ status: "succeeded",
122
+ output: OUT(id),
123
+ };
124
+ const next = opts.onGet ? { ...p, ...opts.onGet(p) } : p;
125
+ preds.set(id, next);
126
+ return {
127
+ status: 200,
128
+ headers: {},
129
+ body: {
130
+ id,
131
+ version: VERSION,
132
+ input: { input_video: VIDEO, output_type: next.type },
133
+ status: next.status,
134
+ output: next.output ?? null,
135
+ error: next.error ?? null,
136
+ },
137
+ };
138
+ };
139
+ return { calls, preds, callProvider };
140
+ }
141
+
142
+ describe("replicate-rvm — the capability serves edit_video", () => {
143
+ it("declares edit_video on the matte verb, a read-only result verb, and provides exactly what the verbs declare", () => {
144
+ expect(verb("replicate_rvm_matte")).toMatchObject({
145
+ intent: "edit_video",
146
+ parameters: {
147
+ video: "string?",
148
+ videoFile: "file:url?",
149
+ outputs: "array?",
150
+ },
151
+ });
152
+ expect(verb("replicate_rvm_result")).toMatchObject({
153
+ intent: "fetch_record",
154
+ metadata: { readOnly: true },
155
+ parameters: { predictionIds: "array" },
156
+ });
157
+ expect([...manifest.spec.provides].sort()).toEqual([
158
+ "edit_video",
159
+ "fetch_record",
160
+ ]);
161
+ });
162
+
163
+ it("maps the edit_video input { video } onto the matte verb, so a pod app's edit_video action reaches it (0368 inputMap)", () => {
164
+ expect(
165
+ (verb("replicate_rvm_matte") as Skill & { inputMap?: unknown }).inputMap
166
+ ).toEqual({ params: { video: "$.video" } });
167
+ expect(
168
+ (verb("replicate_rvm_result") as Skill & { inputMap?: unknown }).inputMap
169
+ ).toBeUndefined();
170
+ });
171
+
172
+ it("keeps the token in the vault, sent as a Bearer header to api.replicate.com/v1 only", () => {
173
+ const tool = manifest.spec.tools[0];
174
+ expect(tool).toMatchObject({
175
+ name: "replicate_api",
176
+ kind: "api",
177
+ credentialRef: "replicateApiTokenSecret",
178
+ });
179
+ expect(tool.config).toEqual({
180
+ baseUrl: "https://api.replicate.com/v1",
181
+ auth: { in: "header", name: "Authorization", prefix: "Bearer " },
182
+ });
183
+ expect(manifest.spec.vault[0]).toMatchObject({
184
+ ref: "replicateApiTokenSecret",
185
+ value: "{{replicateApiToken}}",
186
+ });
187
+ expect(manifest.spec.params).toEqual([
188
+ expect.objectContaining({ name: "replicateApiToken", required: true }),
189
+ ]);
190
+ });
191
+ });
192
+
193
+ describe("replicate_rvm_matte — one call on a warm model", () => {
194
+ it("starts alpha then foreground on the pinned version, waits only on the last, and returns both URLs", async () => {
195
+ const r = replicate({
196
+ onCreate: (type, waited) =>
197
+ waited
198
+ ? { status: "succeeded", output: OUT(type) }
199
+ : { status: "starting" },
200
+ onGet: (p) => ({ status: "succeeded", output: OUT(p.type) }),
201
+ });
202
+ const raw = (await matte({ video: VIDEO }, {}, r.callProvider)) as Record<
203
+ string,
204
+ any
205
+ >;
206
+ const posts = r.calls.filter((c) => c.method === "POST");
207
+ expect(posts.map((c) => c.body)).toEqual([
208
+ {
209
+ version: VERSION,
210
+ input: { input_video: VIDEO, output_type: "alpha-mask" },
211
+ },
212
+ {
213
+ version: VERSION,
214
+ input: { input_video: VIDEO, output_type: "foreground-mask" },
215
+ },
216
+ ]);
217
+ expect(posts.map((c) => c.opts?.headers?.Prefer ?? null)).toEqual([
218
+ null,
219
+ "wait=60",
220
+ ]);
221
+ expect(
222
+ posts.every(
223
+ (c) => c.tool === "replicate_api" && c.path === "/predictions"
224
+ )
225
+ ).toBe(true);
226
+ // The first one ran while the second waited: read once.
227
+ expect(
228
+ r.calls.filter((c) => c.method === "GET").map((c) => c.path)
229
+ ).toEqual(["/predictions/pred1abcdefgh"]);
230
+ expect(raw).toMatchObject({
231
+ success: true,
232
+ status: "succeeded",
233
+ alphaUrl: OUT("alpha-mask"),
234
+ foregroundUrl: OUT("foreground-mask"),
235
+ next: null,
236
+ });
237
+ expect(raw.predictionIds).toEqual(["pred1abcdefgh", "pred2abcdefgh"]);
238
+ });
239
+
240
+ it("a single output waits on that one and reads nothing", async () => {
241
+ const r = replicate({
242
+ onCreate: () => ({ status: "succeeded", output: OUT("a") }),
243
+ });
244
+ const raw = (await matte(
245
+ { video: VIDEO, outputs: ["alpha", "alpha"] },
246
+ {},
247
+ r.callProvider
248
+ )) as Record<string, any>;
249
+ expect(
250
+ r.calls.map((c) => `${c.method} ${c.opts?.headers?.Prefer ?? ""}`)
251
+ ).toEqual(["POST wait=60"]);
252
+ expect(raw.alphaUrl).toBe(OUT("a"));
253
+ expect(raw.foregroundUrl).toBeUndefined();
254
+ });
255
+ });
256
+
257
+ describe("replicate_rvm_matte — a long take is handed to the read verb, never restarted", () => {
258
+ it("returns processing with next = replicate_rvm_result and the same ids", async () => {
259
+ const r = replicate({
260
+ onCreate: () => ({ status: "processing" }),
261
+ onGet: () => ({ status: "processing" }),
262
+ });
263
+ const raw = (await matte({ video: VIDEO }, {}, r.callProvider)) as Record<
264
+ string,
265
+ any
266
+ >;
267
+ expect(raw.status).toBe("processing");
268
+ expect(raw.alphaUrl).toBeUndefined();
269
+ expect(raw.next).toEqual({
270
+ verbId: "replicate_rvm_result",
271
+ parameters: { predictionIds: ["pred1abcdefgh", "pred2abcdefgh"] },
272
+ });
273
+ expect(r.calls.filter((c) => c.method === "POST")).toHaveLength(2);
274
+ });
275
+
276
+ it("the read verb finishes it: GET only, both URLs by output type", async () => {
277
+ let finished = false;
278
+ const r = replicate({
279
+ onCreate: () => ({ status: "processing" }),
280
+ onGet: (p) =>
281
+ finished
282
+ ? { status: "succeeded", output: OUT(p.type) }
283
+ : { status: "processing" },
284
+ });
285
+ const first = (await matte({ video: VIDEO }, {}, r.callProvider)) as Record<
286
+ string,
287
+ any
288
+ >;
289
+ finished = true;
290
+ const calls = r.calls.length;
291
+ const done = (await result(
292
+ first.next.parameters,
293
+ {},
294
+ async (...a: any[]) => {
295
+ // Only reads after this point.
296
+ expect(a[1]).toBe("GET");
297
+ return (r.callProvider as any)(...a);
298
+ }
299
+ )) as Record<string, any>;
300
+ expect(r.calls.length - calls).toBe(2);
301
+ expect(done).toMatchObject({
302
+ success: true,
303
+ status: "succeeded",
304
+ alphaUrl: OUT("alpha-mask"),
305
+ foregroundUrl: OUT("foreground-mask"),
306
+ next: null,
307
+ });
308
+ });
309
+
310
+ it("the read verb refuses an id that is not an RVM prediction", async () => {
311
+ const callProvider = async () => ({
312
+ status: 200,
313
+ body: {
314
+ id: "otherpred123",
315
+ version: "0".repeat(64),
316
+ input: {},
317
+ status: "succeeded",
318
+ output: OUT("x"),
319
+ },
320
+ });
321
+ await expect(
322
+ result({ predictionIds: ["otherpred123"] }, {}, callProvider)
323
+ ).rejects.toThrow(/not a Robust Video Matting prediction/);
324
+ });
325
+ });
326
+
327
+ describe("replicate_rvm_matte — failures are loud", () => {
328
+ it("a failed prediction fails the run with Replicate's error", async () => {
329
+ const r = replicate({
330
+ onCreate: (_t, waited) =>
331
+ waited
332
+ ? { status: "failed", error: "CUDA out of memory" }
333
+ : { status: "starting" },
334
+ onGet: () => ({ status: "processing" }),
335
+ });
336
+ await expect(matte({ video: VIDEO }, {}, r.callProvider)).rejects.toThrow(
337
+ /foreground prediction pred2abcdefgh failed: CUDA out of memory/
338
+ );
339
+ });
340
+
341
+ it("a refused create names the status and Replicate's detail", async () => {
342
+ const callProvider = async () => ({
343
+ status: 402,
344
+ body: { detail: "You have insufficient credit" },
345
+ });
346
+ await expect(matte({ video: VIDEO }, {}, callProvider)).rejects.toThrow(
347
+ /status 402\): You have insufficient credit/
348
+ );
349
+ });
350
+
351
+ it("a proposal envelope is returned as the record, nothing else runs", async () => {
352
+ const calls: string[] = [];
353
+ const callProvider = async (_t: string, m: string) => (
354
+ calls.push(m),
355
+ { proposed: true, proposalId: "p1" }
356
+ );
357
+ expect(await matte({ video: VIDEO }, {}, callProvider)).toEqual({
358
+ success: false,
359
+ proposed: true,
360
+ proposalId: "p1",
361
+ started: [],
362
+ });
363
+ expect(calls).toEqual(["POST"]);
364
+ });
365
+ });
366
+
367
+ describe("replicate_rvm_matte — strict inputs, nothing undeclared reaches Replicate", () => {
368
+ const ok = () =>
369
+ replicate({ onCreate: () => ({ status: "succeeded", output: OUT("a") }) });
370
+
371
+ it.each([
372
+ ["no video", {}],
373
+ ["http, not https", { video: "http://example.com/a.mp4" }],
374
+ [
375
+ "credentials in the URL",
376
+ { video: "https://user:pass@example.com/a.mp4" },
377
+ ],
378
+ ["a data URL", { video: "data:video/mp4;base64,AAAA" }],
379
+ ["a vault ref", { video: "vault://abc" }],
380
+ [
381
+ "a URL over the intent's 2048 characters",
382
+ { video: "https://example.com/" + "a".repeat(2029) },
383
+ ],
384
+ ["an unknown output", { video: VIDEO, outputs: ["depth"] }],
385
+ ["an empty outputs list", { video: VIDEO, outputs: [] }],
386
+ ["Replicate's own spelling", { input_video: VIDEO }],
387
+ ])("refuses %s before any call", async (_why, args) => {
388
+ const r = ok();
389
+ await expect(matte(args, {}, r.callProvider)).rejects.toThrow(
390
+ /replicate_rvm_matte:/
391
+ );
392
+ expect(r.calls).toEqual([]);
393
+ });
394
+
395
+ it("undeclared keys are never forwarded", async () => {
396
+ const r = ok();
397
+ await matte(
398
+ {
399
+ video: VIDEO,
400
+ version: "evil",
401
+ input: { x: 1 },
402
+ webhook: "https://evil.example",
403
+ apiKey: "sk",
404
+ },
405
+ {},
406
+ r.callProvider
407
+ );
408
+ for (const c of r.calls.filter((x) => x.method === "POST")) {
409
+ expect(Object.keys(c.body!).sort()).toEqual(["input", "version"]);
410
+ expect(c.body!.version).toBe(VERSION);
411
+ expect(Object.keys(c.body!.input).sort()).toEqual([
412
+ "input_video",
413
+ "output_type",
414
+ ]);
415
+ }
416
+ });
417
+
418
+ it("the read verb refuses a malformed id before any call", async () => {
419
+ const calls: unknown[] = [];
420
+ await expect(
421
+ result({ predictionIds: ["../account"] }, {}, async (...a: unknown[]) =>
422
+ calls.push(a)
423
+ )
424
+ ).rejects.toThrow(/not a Replicate prediction id/);
425
+ expect(calls).toEqual([]);
426
+ });
427
+ });
428
+
429
+ /**
430
+ * Phase 1c (pod files to tools by URL, backend #469): the take is a POD FILE.
431
+ * The caller passes its file entity id as `videoFile` (declared `file:url`);
432
+ * the pod mints a fresh signed URL at run time and swaps it in, so the skill
433
+ * receives an https URL. The verb is async so that URL lives 6 h (a cold
434
+ * Replicate start may fetch it late). The edit_video intent input stays an
435
+ * https `video`, mapped by inputMap.
436
+ */
437
+ describe("replicate_rvm_matte — a pod file in, signed by the pod", () => {
438
+ const SIGNED =
439
+ "https://s3.example.synap.live/pod/files/take-1.mp4?X-Amz-Signature=secret123&X-Amz-Expires=21600";
440
+
441
+ it("declares videoFile as a pod file by URL, async (6 h link), never read-only", () => {
442
+ const m = verb("replicate_rvm_matte") as Skill & { executionMode?: string };
443
+ expect(m.parameters?.videoFile).toBe("file:url?");
444
+ expect(m.executionMode).toBe("async");
445
+ expect(m.metadata?.readOnly).toBeUndefined();
446
+ expect((m as Skill & { inputMap?: unknown }).inputMap).toEqual({
447
+ params: { video: "$.video" },
448
+ });
449
+ });
450
+
451
+ it("sends the signed URL the pod swapped in as Replicate's input_video, and says the source", async () => {
452
+ const r = replicate({
453
+ onCreate: () => ({ status: "succeeded", output: OUT("a") }),
454
+ });
455
+ const raw = (await matte(
456
+ { videoFile: SIGNED },
457
+ {},
458
+ r.callProvider
459
+ )) as Record<string, any>;
460
+ const posts = r.calls.filter((c) => c.method === "POST");
461
+ expect(posts.map((c) => c.body!.input.input_video)).toEqual([
462
+ SIGNED,
463
+ SIGNED,
464
+ ]);
465
+ expect(raw.source).toBe("pod-file");
466
+ expect(JSON.stringify(raw)).not.toContain("X-Amz-Signature");
467
+ });
468
+
469
+ it("a URL caller is still served, marked as such", async () => {
470
+ const r = replicate({
471
+ onCreate: () => ({ status: "succeeded", output: OUT("a") }),
472
+ });
473
+ const raw = (await matte({ video: VIDEO }, {}, r.callProvider)) as Record<
474
+ string,
475
+ any
476
+ >;
477
+ expect(raw.source).toBe("url");
478
+ });
479
+
480
+ it("refuses both, neither, and an unswapped file id, without echoing a signed URL", async () => {
481
+ const r = replicate({
482
+ onCreate: () => ({ status: "succeeded", output: OUT("a") }),
483
+ });
484
+ await expect(
485
+ matte({ video: VIDEO, videoFile: SIGNED }, {}, r.callProvider)
486
+ ).rejects.toThrow(/not both/);
487
+ await expect(matte({}, {}, r.callProvider)).rejects.toThrow(
488
+ /video \(an https URL\) or videoFile \(a pod file\) is required/
489
+ );
490
+ const err = await matte(
491
+ { videoFile: "8d2f4c1e-0b7a-4c55-9a7e-3f1f0d1c2b3a" },
492
+ {},
493
+ r.callProvider
494
+ ).catch((e: Error) => e.message);
495
+ expect(err).toMatch(/did not arrive as a signed pod file URL/);
496
+ const http = await matte(
497
+ { videoFile: "http://s3.example/x.mp4?X-Amz-Signature=secret123" },
498
+ {},
499
+ r.callProvider
500
+ ).catch((e: Error) => e.message);
501
+ expect(http).not.toContain("secret123");
502
+ expect(r.calls).toEqual([]);
503
+ });
504
+ });
@@ -26,6 +26,12 @@ meta:
26
26
  color: "#8B5CF6"
27
27
  tags: [research, knowledge, graph, sources, evidence, learning]
28
28
  isPublic: true
29
+ # What this space PRODUCES (homes): the purpose word, then its kinds/roles.
30
+ intent:
31
+ label: Research
32
+ produces:
33
+ - profile: hypothesis
34
+ - profile: finding
29
35
 
30
36
  workspace:
31
37
  name: Research
@@ -178,6 +184,15 @@ profiles:
178
184
  icon: git-branch
179
185
  color: violet
180
186
  description: A candidate theory or answer under test — argued for and against by sources.
187
+ # The ONE lifecycle (work model L1): every option's category.
188
+ lifecycle:
189
+ property: stance
190
+ options:
191
+ - { key: proposed, category: open }
192
+ - { key: supported, category: done }
193
+ - { key: contested, category: active }
194
+ - { key: refuted, category: lost }
195
+ - { key: superseded, category: lost }
181
196
  properties:
182
197
  - slug: statement
183
198
  label: Statement
@@ -204,6 +219,13 @@ profiles:
204
219
  icon: check-circle
205
220
  color: "#10B981"
206
221
  description: A validated fact — the thing established between the question, the sources, and the reasoning. Carries calibrated confidence and links to its evidence.
222
+ # The ONE lifecycle (work model L1): every option's category.
223
+ lifecycle:
224
+ property: status
225
+ options:
226
+ - { key: validated, category: done }
227
+ - { key: provisional, category: open }
228
+ - { key: retracted, category: lost }
207
229
  properties:
208
230
  - slug: claim
209
231
  label: Claim
@@ -369,33 +391,39 @@ entityLinks:
369
391
  label: About,
370
392
  }
371
393
 
372
- # The one universal playbook — bound to `question`, so any question can launch it
373
- # from its detail page (see actionPlacements) or auto-match as a session. It runs
374
- # the full method: gather + rank sources (keep the rejects), form and weigh
375
- # hypotheses, emit findings with honest confidence, and place them on the map.
394
+ # "Research a Question" runs a question's lifecycle (open → exploring →
395
+ # answered): a new question proposes it, and any question can launch it from its
396
+ # detail page (see actionPlacements). It runs the full method: gather + rank
397
+ # sources (keep the rejects), form and weigh hypotheses, emit findings with
398
+ # honest confidence, and place them on the map. "Turn a finding into content"
399
+ # reads a finding's status: a validated finding proposes post ideas from it.
376
400
  playbooks:
377
401
  - name: Research a Question
378
402
  description: Investigate a question end-to-end — gather and rank sources (keeping the rejected ones), weigh hypotheses, emit findings with calibrated confidence, and map them onto the knowledge graph.
379
403
  goalTemplate: >-
380
- Investigate the question {question} and record the WHOLE investigation, not
381
- just the answer. Steps: (1) Use the source-triage skill to gather sources
382
- widely via web search, classify each by type and credibility, and create a
404
+ Investigate the question {question} and record the WHOLE investigation,
405
+ not just the answer; each stage moves the question's status. FRAME:
406
+ restate the question so it can be answered, and say what evidence would
407
+ answer it. GATHER: use the source-triage skill to gather sources widely
408
+ via web search, classify each by type and credibility, and create a
383
409
  `source` entity for every one — including the ones you rejected, marked
384
- rejected with a reason (never silently drop a source). (2) Form the
385
- candidate answers as `hypothesis` entities and link each to the question;
386
- link every source that bears on a hypothesis with `supports` or
387
- `contradicts`. (3) Use the evidence-synthesis skill to weigh the supporting
388
- vs contradicting sources and emit `finding` entities with honestly
389
- calibrated confidence, each `supported_by` its sources and `derived_from`
390
- this research. (4) Use the concept-cartography skill to place the findings
391
- onto the knowledge map — resolve or create the `field`/`concept` entities
392
- and wire `about` / `part_of` / `overlaps_with` / `prerequisite_of` edges,
393
- deduping against what already exists. (5) Create the `research` entity that
394
- `investigates` the question and `explored` the sources, write its
395
- conclusion and confidence, and set the question's status to answered. Every
396
- write is proposal-gated and reviewable.
410
+ rejected with a reason (never silently drop a source). WEIGH: form the
411
+ candidate answers as `hypothesis` entities that answer the question; link
412
+ every source that bears on a hypothesis with `supports` or
413
+ `contradicts`; use the evidence-synthesis skill to emit `finding`
414
+ entities with honestly calibrated confidence, each `supported_by` its
415
+ sources and `derived_from` this research; use the concept-cartography
416
+ skill to place the findings onto the knowledge map. ANSWER: create the
417
+ `research` entity that `investigates` the question and `explored` the
418
+ sources, write its conclusion and confidence, and set the question to
419
+ answered. Abandoning a question is a person's call. Every write is
420
+ proposal-gated and reviewable.
397
421
  subjectProfile:
398
422
  profileSlug: question
423
+ statusProperty: questionStatus
424
+ humanOnlyStatuses: [abandoned]
425
+ activators:
426
+ - { on: created, mode: propose }
399
427
  params:
400
428
  - { name: question, type: entity, label: Question, required: true }
401
429
  executor: is-agent
@@ -406,6 +434,68 @@ playbooks:
406
434
  - exa_search
407
435
  - entity.create
408
436
  status: active
437
+ requiredIntents: [search_external]
438
+ expectedOutputs:
439
+ - { kind: research, label: Research record, relationToSubject: investigates }
440
+ - { kind: hypothesis, label: Candidate answers, relationToSubject: answers }
441
+ stages:
442
+ - key: frame
443
+ name: Frame
444
+ category: planned
445
+ subjectStatus: open
446
+ goal: Restate the question so it can be answered and say what evidence would answer it.
447
+ criteria:
448
+ - key: question-answerable
449
+ statement: The question says what evidence would answer it.
450
+ check: { kind: judge }
451
+ - key: gather
452
+ name: Gather
453
+ category: started
454
+ subjectStatus: exploring
455
+ goal: Gather and classify sources, keeping every rejected source with its reason.
456
+ criteria:
457
+ - key: sources-kept
458
+ statement: Every source considered exists as a source entity, rejected ones with a reason.
459
+ check: { kind: judge }
460
+ - key: weigh
461
+ name: Weigh
462
+ category: started
463
+ goal: Form hypotheses, link evidence for and against, and emit findings with calibrated confidence.
464
+ criteria:
465
+ - key: findings-cited
466
+ statement: Every finding is supported by at least one source.
467
+ check: { kind: judge }
468
+ - key: counter-evidence
469
+ statement: Every hypothesis lists the sources that contradict it, or says none were found.
470
+ check: { kind: judge }
471
+ - key: answer
472
+ name: Answer
473
+ category: completed
474
+ subjectStatus: answered
475
+ goal: Record the research, its conclusion and confidence, and mark the question answered.
476
+ gate: { kind: check }
477
+ onFail: { toStage: weigh }
478
+
479
+ - name: Turn a finding into content
480
+ description: When a finding is validated, propose the posts that would share it, each linked to the finding.
481
+ goalTemplate: >-
482
+ Turn this validated finding ({finding}) into content. Read the finding,
483
+ its sources and the research it came from. Propose one to three posts
484
+ that share it with the audience — each created at the Idea status, on
485
+ the pillar it fits, with its hook and the reason it is worth making —
486
+ and link each one derived_from the finding. Quote only what the sources
487
+ support.
488
+ subjectProfile:
489
+ profileSlug: finding
490
+ statusProperty: status
491
+ activators:
492
+ - { on: enters_status, status: validated, mode: propose }
493
+ params:
494
+ - { name: finding, type: entity, label: Finding, required: true }
495
+ executor: is-agent
496
+ status: active
497
+ expectedOutputs:
498
+ - { kind: post, label: Post idea, relationToSubject: derived_from }
409
499
 
410
500
  # A "Research this" action on every question's detail page — the entity is the
411
501
  # starter of the flow, exactly as designed.
@@ -423,6 +513,15 @@ integrations:
423
513
  - templateKey: research-methods
424
514
  - templateKey: exa
425
515
 
516
+ # ── taskIntents — WHAT THIS WORKSPACE NEEDS TO FUNCTION ───────────────────
517
+ # Closed vocabulary: `@synap-core/types/capability-intents` (pod migrations
518
+ # 0283/0284). DERIVED from the declared intents of this template's own
519
+ # `integrations` capability packs — not hand-guessed. Checked at authoring
520
+ # time by `validateTemplate` (`task-intent-unknown`) and by
521
+ # `src/task-intents.test.ts`.
522
+ taskIntents:
523
+ - search_external
524
+
426
525
  onboarding:
427
526
  goal: >
428
527
  Seed the workspace with the first real questions worth researching and the