@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,200 @@
1
+ /**
2
+ * fal.ai `fal_generate_image` (capability `fal-ai`): two defects reproduced on
3
+ * the founder's pod, 2026-10-27, through `synap_run_capability`.
4
+ *
5
+ * 1. `numImages: "3"` returned `imageCount: 3` but ONE `imageUrl` — the other
6
+ * two images were generated, billed and dropped.
7
+ * 2. `model: "fal-ai/nano-banana/edit"` with reference images got fal's 422:
8
+ * the code built a fixed `{ prompt, image_size, num_images }` body, so the
9
+ * model's required `image_urls` never reached fal.
10
+ *
11
+ * SEAM: the verb's REAL `code` body, run the way the pod's executor wraps it
12
+ * (`async (args, context) => { <code> }` with `callProvider` / `propose` in
13
+ * scope), against a fal response shaped like fal's documented output. The
14
+ * request body asserted is exactly what `callProvider` would send to fal.run.
15
+ *
16
+ * NOT covered: fal's live behaviour per model (payloads are hand-written), the
17
+ * pod's propose-moment arg check (backend `validate-verb-parameters`) and the
18
+ * IS isolate (Node's AsyncFunction is used).
19
+ */
20
+ import { readFileSync } from "node:fs";
21
+ import { dirname, join } from "node:path";
22
+ import { fileURLToPath } from "node:url";
23
+ import { describe, expect, it } from "vitest";
24
+
25
+ const HERE = dirname(fileURLToPath(import.meta.url));
26
+ const manifest = JSON.parse(readFileSync(join(HERE, "../capabilities/fal-ai.capability.json"), "utf8"));
27
+ type Skill = { name: string; code?: string; parameters?: Record<string, string> };
28
+ const verb = (manifest.spec.skills as Skill[]).find((s) => s.name === "fal_generate_image")!;
29
+
30
+ const AsyncFunction = Object.getPrototypeOf(async () => {}).constructor as new (
31
+ ...args: string[]
32
+ ) => (...a: unknown[]) => Promise<unknown>;
33
+ const body = new AsyncFunction("args", "context", "callProvider", "propose", verb.code!);
34
+
35
+ type Call = { tool: string; method: string; path: string; input: Record<string, unknown> };
36
+ async function run(args: Record<string, unknown>, images = 1) {
37
+ const calls: Call[] = [];
38
+ const writes: Array<{ title: string; properties: Record<string, unknown> }> = [];
39
+ const callProvider = async (tool: string, method: string, path: string, input: Record<string, unknown>) => {
40
+ calls.push({ tool, method, path, input });
41
+ return {
42
+ status: 200,
43
+ headers: {},
44
+ body: {
45
+ images: Array.from({ length: images }, (_, i) => ({ url: `https://v3.fal.media/files/out-${i + 1}.png`, width: 1024, height: 1024 })),
46
+ seed: 42,
47
+ },
48
+ };
49
+ };
50
+ const propose = { entity: async (x: { title: string; properties: Record<string, unknown> }) => (writes.push(x), { status: "proposed" }) };
51
+ const raw = (await body(args, {}, callProvider, propose)) as Record<string, any>;
52
+ return { raw, calls, writes };
53
+ }
54
+
55
+ const REF = "https://v3.fal.media/files/brand-logo.png";
56
+
57
+ describe("fal_generate_image — every generated image comes back", () => {
58
+ it("numImages 3 returns all three URLs, keeps imageUrl as the first, files one bookmark each", async () => {
59
+ const { raw, calls, writes } = await run({ prompt: "a red fox", numImages: "3" }, 3);
60
+ expect(calls[0].input.num_images).toBe(3);
61
+ expect(raw.imageUrls).toEqual([
62
+ "https://v3.fal.media/files/out-1.png",
63
+ "https://v3.fal.media/files/out-2.png",
64
+ "https://v3.fal.media/files/out-3.png",
65
+ ]);
66
+ expect(raw.imageUrl).toBe("https://v3.fal.media/files/out-1.png");
67
+ expect(raw.imageCount).toBe(3);
68
+ expect(writes.map((w) => w.properties.url)).toEqual(raw.imageUrls);
69
+ });
70
+ });
71
+
72
+ /**
73
+ * Third defect (2026-10-27): the bookmark title was the truncated prompt alone,
74
+ * identical for every variant and every re-run of a prompt. The duplicate door's
75
+ * name leg filed each image after the first as POSSIBLE_DUPLICATE (pending
76
+ * proposals 7a1ad4a2, 9568bd99, …) until the agent hit its pending cap, and the
77
+ * cap's 403 thrown by propose.entity then failed a run whose images were already
78
+ * generated and billed.
79
+ */
80
+ describe("fal_generate_image — bookmarks never pile up as duplicates or sink the run", () => {
81
+ it("every image's bookmark title carries that image's own fal file id, so no two images share one", async () => {
82
+ const prompt = "Square editorial illustration, flat gouache style with soft risograph grain on warm cream paper";
83
+ const a = await run({ prompt, numImages: "3" }, 3);
84
+ const titles = a.writes.map((w) => w.title);
85
+ expect(new Set(titles).size).toBe(3);
86
+ expect(titles.map((t) => t.split(" #").pop())).toEqual(["out-1", "out-2", "out-3"]);
87
+ for (const t of titles) expect(t.length).toBeLessThanOrEqual(110);
88
+ });
89
+
90
+ it("a refused bookmark write is reported beside the URLs and the run still succeeds", async () => {
91
+ const callProvider = async () => ({ status: 200, body: { images: [{ url: "https://v3.fal.media/files/a.png" }, { url: "https://v3.fal.media/files/b.png" }] } });
92
+ let n = 0;
93
+ const propose = {
94
+ entity: async () => {
95
+ if (++n === 2) throw new Error("propose.entity failed: Hub API error: 403 Forbidden — Agent proposal limit reached (15 pending)");
96
+ return { status: "proposed", proposalId: "p1", proposedEntityId: "e1" };
97
+ },
98
+ };
99
+ const raw = (await body({ prompt: "x", numImages: "2" }, {}, callProvider, propose)) as Record<string, any>;
100
+ expect(raw.success).toBe(true);
101
+ expect(raw.imageUrls).toEqual(["https://v3.fal.media/files/a.png", "https://v3.fal.media/files/b.png"]);
102
+ expect(raw.bookmarkFailures).toBe(1);
103
+ expect(raw.bookmarks[0]).toMatchObject({ status: "proposed", proposalId: "p1" });
104
+ expect(raw.bookmarks[1]).toMatchObject({ url: "https://v3.fal.media/files/b.png", status: "failed" });
105
+ expect(raw.bookmarks[1].error).toMatch(/proposal limit reached/);
106
+ });
107
+ });
108
+
109
+ describe("fal_generate_image — declared model inputs reach fal, nothing else does", () => {
110
+ it("declares imageUrls / aspectRatio / seed beside the original args", () => {
111
+ expect(verb.parameters).toEqual({
112
+ prompt: "string",
113
+ model: "string?",
114
+ imageSize: "string?",
115
+ numImages: "string?",
116
+ imageUrls: "array?",
117
+ images: "file[]:url?",
118
+ aspectRatio: "string?",
119
+ seed: "string?",
120
+ });
121
+ });
122
+
123
+ it("an edit model gets image_urls, aspect_ratio and seed in fal's own names, and no default image_size", async () => {
124
+ const { calls, raw } = await run({
125
+ prompt: "put the logo on a mug",
126
+ model: "fal-ai/nano-banana/edit",
127
+ imageUrls: [REF],
128
+ aspectRatio: "16:9",
129
+ seed: "7",
130
+ });
131
+ expect(calls[0].path).toBe("/fal-ai/nano-banana/edit");
132
+ expect(calls[0].input).toEqual({
133
+ prompt: "put the logo on a mug",
134
+ num_images: 1,
135
+ image_urls: [REF],
136
+ aspect_ratio: "16:9",
137
+ seed: 7,
138
+ });
139
+ expect(raw.success).toBe(true);
140
+ });
141
+
142
+ it("a plain text-to-image call is unchanged: prompt, num_images, image_size square_hd", async () => {
143
+ const { calls } = await run({ prompt: "a red fox" });
144
+ expect(calls[0]).toMatchObject({ tool: "fal_api", method: "POST", path: "/fal-ai/flux/schnell" });
145
+ expect(calls[0].input).toEqual({ prompt: "a red fox", num_images: 1, image_size: "square_hd" });
146
+ });
147
+
148
+ it("undeclared keys are never forwarded to fal", async () => {
149
+ const { calls } = await run({ prompt: "x", apiKey: "sk-secret", sync_mode: true, enable_safety_checker: false });
150
+ expect(Object.keys(calls[0].input).sort()).toEqual(["image_size", "num_images", "prompt"]);
151
+ });
152
+
153
+ it.each([
154
+ [{ image_urls: [REF] }, /pass imageUrls, not image_urls/],
155
+ [{ aspect_ratio: "16:9" }, /pass aspectRatio, not aspect_ratio/],
156
+ [{ imageUrls: REF }, /imageUrls must be an array/],
157
+ [{ imageUrls: [] }, /imageUrls is empty/],
158
+ [{ imageUrls: Array(9).fill(REF) }, /at most 8/],
159
+ [{ imageUrls: ["data:image/png;base64,AAAA"] }, /public http\(s\) URLs/],
160
+ [{ imageUrls: ["https://user:pass@example.com/a.png"] }, /without credentials/],
161
+ [{ imageUrls: ["https://example.com/" + "a".repeat(2100)] }, /public http\(s\) URLs/],
162
+ [{ aspectRatio: "wide" }, /aspectRatio must be/],
163
+ [{ seed: "-1" }, /seed must be a whole number/],
164
+ [{ seed: "1.5" }, /seed must be a whole number/],
165
+ [{ model: "../../admin" }, /model must be a fal model id/],
166
+ ])("refuses %j before any billed call", async (extra, message) => {
167
+ const calls: unknown[] = [];
168
+ const callProvider = async (...a: unknown[]) => (calls.push(a), { status: 200, body: { images: [] } });
169
+ await expect(body({ prompt: "x", ...extra }, {}, callProvider, { entity: async () => ({}) })).rejects.toThrow(message);
170
+ expect(calls).toHaveLength(0);
171
+ });
172
+ });
173
+
174
+ /**
175
+ * Phase 1c (pod files to tools by URL, backend #469): reference images can be
176
+ * POD FILES. `images` is declared "file[]:url?"; the caller passes file entity
177
+ * ids and the pod swaps them for fresh signed URLs at run time (6 h, the verb
178
+ * is async). They join imageUrls in fal's image_urls, and the signed URLs are
179
+ * never written into a bookmark.
180
+ */
181
+ describe("fal_generate_image — reference images from the pod", () => {
182
+ const SIGNED = "https://s3.example.synap.live/pod/logo.png?X-Amz-Signature=secret123";
183
+
184
+ it("sends the pod's signed URLs to fal beside public ones, 8 at most together", async () => {
185
+ const { calls, writes } = await run({ prompt: "logo on a mug", model: "fal-ai/nano-banana/edit", imageUrls: [REF], images: [SIGNED] });
186
+ expect(calls[0].input.image_urls).toEqual([REF, SIGNED]);
187
+ expect(writes[0].properties.referenceUrls).toEqual([REF]);
188
+ expect(writes[0].properties.referencePodFiles).toBe(1);
189
+ expect(JSON.stringify(writes)).not.toContain("X-Amz-Signature");
190
+ });
191
+
192
+ it("refuses an unswapped id and more than 8 images in all, before any billed call", async () => {
193
+ const calls: unknown[] = [];
194
+ const callProvider = async (...a: unknown[]) => (calls.push(a), { status: 200, body: { images: [] } });
195
+ const propose = { entity: async () => ({}) };
196
+ await expect(body({ prompt: "x", images: ["8d2f4c1e-0b7a-4c55-9a7e-3f1f0d1c2b3a"] }, {}, callProvider, propose)).rejects.toThrow(/did not arrive as signed pod file URLs/);
197
+ await expect(body({ prompt: "x", imageUrls: Array(5).fill(REF), images: Array(4).fill(SIGNED) }, {}, callProvider, propose)).rejects.toThrow(/at most 8 images together/);
198
+ expect(calls).toHaveLength(0);
199
+ });
200
+ });
@@ -0,0 +1,180 @@
1
+ /**
2
+ * fal Media Lab (capability `fal-media-lab`): its image inputs can be POD FILES
3
+ * (phase 1c of pod files to tools by URL, backend #469). `images` (edit) and
4
+ * `image` / `endImage` (Seedance) are declared `file:url`; the caller passes
5
+ * file entity ids and the pod swaps them for fresh signed URLs at run time
6
+ * (6 h: both verbs are async, and the Seedance queue fetches its frames
7
+ * later). Public URLs keep working; a signed URL is never stored in the
8
+ * bookmark the verb proposes.
9
+ *
10
+ * SEAM: each verb's REAL `code` body, wrapped the way the pod's executor wraps
11
+ * it, against fal responses shaped like fal's documented output. NOT covered:
12
+ * the pod's swap itself (backend file-parameters.ts) and fal live.
13
+ */
14
+ import { readFileSync } from "node:fs";
15
+ import { dirname, join } from "node:path";
16
+ import { fileURLToPath } from "node:url";
17
+ import { describe, expect, it } from "vitest";
18
+
19
+ const HERE = dirname(fileURLToPath(import.meta.url));
20
+ const manifest = JSON.parse(
21
+ readFileSync(
22
+ join(HERE, "../capabilities/fal-media-lab.capability.json"),
23
+ "utf8"
24
+ )
25
+ );
26
+ type Skill = {
27
+ name: string;
28
+ code: string;
29
+ parameters: Record<string, string>;
30
+ executionMode?: string;
31
+ metadata?: Record<string, unknown>;
32
+ };
33
+ const verb = (name: string) =>
34
+ (manifest.spec.skills as Skill[]).find((s) => s.name === name)!;
35
+ const AsyncFunction = Object.getPrototypeOf(async () => {}).constructor as new (
36
+ ...args: string[]
37
+ ) => (...a: unknown[]) => Promise<unknown>;
38
+ const bodyOf = (name: string) =>
39
+ new AsyncFunction(
40
+ "args",
41
+ "context",
42
+ "callProvider",
43
+ "propose",
44
+ verb(name).code
45
+ );
46
+
47
+ const PUBLIC = "https://v3.fal.media/files/ref.png";
48
+ const SIGNED =
49
+ "https://s3.example.synap.live/pod/frame.png?X-Amz-Signature=secret123";
50
+ const FILE_ID = "8d2f4c1e-0b7a-4c55-9a7e-3f1f0d1c2b3a";
51
+
52
+ async function run(name: string, args: Record<string, unknown>) {
53
+ const calls: Array<{ path: string; input: Record<string, any> }> = [];
54
+ const writes: Array<{ properties: Record<string, unknown> }> = [];
55
+ const callProvider = async (
56
+ _t: string,
57
+ _m: string,
58
+ path: string,
59
+ input: Record<string, any>
60
+ ) => {
61
+ calls.push({ path, input });
62
+ return {
63
+ status: 200,
64
+ body: {
65
+ images: [{ url: "https://v3.fal.media/files/out.png" }],
66
+ request_id: "req-1",
67
+ status_url: "https://queue.fal.run/x/status",
68
+ },
69
+ };
70
+ };
71
+ const propose = {
72
+ entity: async (x: { properties: Record<string, unknown> }) => (
73
+ writes.push(x),
74
+ { status: "proposed" }
75
+ ),
76
+ };
77
+ const raw = (await bodyOf(name)(args, {}, callProvider, propose)) as Record<
78
+ string,
79
+ any
80
+ >;
81
+ return { raw, calls, writes };
82
+ }
83
+
84
+ describe("fal-media-lab — pod files are declared, by URL, on async verbs", () => {
85
+ it("declares images / image / endImage as file:url, never on a read-only verb", () => {
86
+ expect(verb("fal_edit_reference_image").parameters).toMatchObject({
87
+ imageUrls: "string?",
88
+ images: "file[]:url?",
89
+ });
90
+ expect(verb("fal_submit_seedance_video").parameters).toMatchObject({
91
+ imageUrl: "string?",
92
+ image: "file:url?",
93
+ endImage: "file:url?",
94
+ });
95
+ for (const n of ["fal_edit_reference_image", "fal_submit_seedance_video"]) {
96
+ expect(verb(n).executionMode).toBe("async");
97
+ expect(verb(n).metadata?.readOnly).toBeUndefined();
98
+ }
99
+ });
100
+ });
101
+
102
+ describe("fal_edit_reference_image — reference images from the pod", () => {
103
+ it("sends pod and public references together; the bookmark keeps only the public ones", async () => {
104
+ const { calls, writes, raw } = await run("fal_edit_reference_image", {
105
+ prompt: "swap the mug",
106
+ imageUrls: PUBLIC,
107
+ images: [SIGNED],
108
+ });
109
+ expect(calls[0].input.image_urls).toEqual([PUBLIC, SIGNED]);
110
+ expect(writes[0].properties).toMatchObject({
111
+ referenceUrls: [PUBLIC],
112
+ referencePodFiles: 1,
113
+ });
114
+ expect(JSON.stringify([writes, raw])).not.toContain("X-Amz-Signature");
115
+ });
116
+
117
+ it("a pod image alone is enough; no image at all and an unswapped id are refused before any call", async () => {
118
+ expect(
119
+ (await run("fal_edit_reference_image", { prompt: "x", images: [SIGNED] }))
120
+ .calls
121
+ ).toHaveLength(1);
122
+ await expect(
123
+ run("fal_edit_reference_image", { prompt: "x" })
124
+ ).rejects.toThrow(/at least one reference image/);
125
+ await expect(
126
+ run("fal_edit_reference_image", { prompt: "x", images: [FILE_ID] })
127
+ ).rejects.toThrow(/did not arrive as signed pod file URLs/);
128
+ });
129
+ });
130
+
131
+ describe("fal_submit_seedance_video — first and last frame from the pod", () => {
132
+ it("queues the pod's signed frames; the bookmark records that they came from the pod, not their URLs", async () => {
133
+ const { calls, writes } = await run("fal_submit_seedance_video", {
134
+ prompt: "a slow push-in",
135
+ image: SIGNED,
136
+ endImage: SIGNED,
137
+ });
138
+ expect(calls[0].input).toMatchObject({
139
+ image_url: SIGNED,
140
+ end_image_url: SIGNED,
141
+ });
142
+ expect(writes[0].properties).toMatchObject({
143
+ imageUrl: null,
144
+ endImageUrl: null,
145
+ imageFromPod: true,
146
+ endImageFromPod: true,
147
+ });
148
+ expect(JSON.stringify(writes)).not.toContain("X-Amz-Signature");
149
+ });
150
+
151
+ it("a public first frame works as before", async () => {
152
+ const { calls, writes } = await run("fal_submit_seedance_video", {
153
+ prompt: "x",
154
+ imageUrl: PUBLIC,
155
+ });
156
+ expect(calls[0].input.image_url).toBe(PUBLIC);
157
+ expect(calls[0].input.end_image_url).toBeUndefined();
158
+ expect(writes[0].properties).toMatchObject({
159
+ imageUrl: PUBLIC,
160
+ imageFromPod: false,
161
+ });
162
+ });
163
+
164
+ it.each([
165
+ [{ prompt: "x" }, /the first frame is required/],
166
+ [{ prompt: "x", imageUrl: PUBLIC, image: SIGNED }, /not both/],
167
+ [
168
+ { prompt: "x", image: SIGNED, endImageUrl: PUBLIC, endImage: SIGNED },
169
+ /not both/,
170
+ ],
171
+ [
172
+ { prompt: "x", image: FILE_ID },
173
+ /did not arrive as a signed pod file URL/,
174
+ ],
175
+ ])("refuses %j before any call", async (args, message) => {
176
+ await expect(run("fal_submit_seedance_video", args)).rejects.toThrow(
177
+ message
178
+ );
179
+ });
180
+ });
package/src/finance.yaml CHANGED
@@ -12,6 +12,12 @@ meta:
12
12
  color: "#16A34A"
13
13
  tags: [finance, accounting, revenue]
14
14
  isPublic: true
15
+ # What this space PRODUCES (homes): the purpose word, then its kinds/roles.
16
+ intent:
17
+ label: Finance
18
+ produces:
19
+ - profile: invoice
20
+ - profile: expense
15
21
 
16
22
  workspace:
17
23
  name: Finance
@@ -19,7 +25,10 @@ workspace:
19
25
  proposalId: finance-v1
20
26
  subtype: finance
21
27
  visibility: members
22
- workspaceType: operational
28
+ # A DOMAIN workspace that holds entities — NOT `operational`. `operational`
29
+ # marks an admin/system surface: `isDomainHomeWorkspace` refuses filing
30
+ # entities into it and excludes it from auto-placement (2026-09-25).
31
+ workspaceType: personal
23
32
  capabilities:
24
33
  - finance.accounts
25
34
  - finance.revenue
@@ -76,6 +85,15 @@ profiles:
76
85
  icon: receipt
77
86
  color: blue
78
87
  description: An accounts-receivable invoice — money customers owe us.
88
+ # The ONE lifecycle (work model L1): every option's category.
89
+ lifecycle:
90
+ property: status
91
+ options:
92
+ - { key: draft, category: open }
93
+ - { key: sent, category: active }
94
+ - { key: paid, category: done }
95
+ - { key: overdue, category: active }
96
+ - { key: void, category: lost, humanOnly: true }
79
97
  properties:
80
98
  - slug: invoice-number
81
99
  label: Invoice #
@@ -119,6 +137,15 @@ profiles:
119
137
  icon: credit-card
120
138
  color: orange
121
139
  description: An accounts-payable expense or spend commitment.
140
+ # The ONE lifecycle (work model L1): every option's category.
141
+ lifecycle:
142
+ property: status
143
+ options:
144
+ - { key: pending, category: open }
145
+ - { key: approved, category: active }
146
+ - { key: paid, category: done }
147
+ - { key: reimbursed, category: done, humanOnly: true }
148
+ - { key: rejected, category: lost, humanOnly: true }
122
149
  properties:
123
150
  - slug: category
124
151
  label: Category
@@ -393,6 +420,16 @@ entityLinks:
393
420
  targetProfileSlug: account
394
421
  type: deposits_to
395
422
  label: Deposits to
423
+ # Who a recurring stream bills — read by "Bill a recurring fee" to address
424
+ # the month's invoice. A stream can bill a company or a person (a solo client).
425
+ - sourceProfileSlug: revenue-stream
426
+ targetProfileSlug: company
427
+ type: billed_to
428
+ label: Billed to
429
+ - sourceProfileSlug: revenue-stream
430
+ targetProfileSlug: person
431
+ type: billed_to
432
+ label: Billed to
396
433
 
397
434
  onboarding:
398
435
  goal: Capture the company's real money model — where cash sits, what revenue comes in, what it burns on, and its current runway — so the workspace reflects the actual financial position, not a generic ledger.
@@ -444,10 +481,12 @@ onboarding:
444
481
  distinct from collected cash, and at least a rough gross margin and LTV:CAC in view.
445
482
  collect:
446
483
  - profileSlug: account
484
+ min: 1
447
485
  what: The bank and credit accounts holding company cash, with current balances.
448
486
  cardinality: few
449
487
  keyFields: [title, account-type, balance]
450
488
  - profileSlug: revenue-stream
489
+ min: 1
451
490
  what: How the company makes money — each recurring or one-off revenue stream and its MRR/ARR.
452
491
  cardinality: several
453
492
  keyFields: [title, stream-type, mrr]
@@ -456,6 +495,7 @@ onboarding:
456
495
  cardinality: several
457
496
  keyFields: [title, category, amount]
458
497
  - profileSlug: financial-metric
498
+ min: 1
459
499
  what: The current vital signs, above all RUNWAY (months of cash left) plus burn rate and MRR.
460
500
  cardinality: few
461
501
  keyFields: [metric-type, value, unit]
@@ -524,3 +564,216 @@ bento:
524
564
  - kind: view
525
565
  viewName: Budget vs Actual
526
566
  pos: { x: 6, y: 8, w: 6, h: 4 }
567
+
568
+ playbooks:
569
+ - name: Bill a client
570
+ description: Take an invoice from draft to paid — prepare it, send it with a person's approval, chase it when late, and confirm the payment.
571
+ goalTemplate: >-
572
+ Bill this invoice ({invoice}); each stage moves its status. PREPARE: set
573
+ the invoice number, customer, amount, currency, issue date and due date
574
+ from the engagement or deal it bills, and check the amount against what
575
+ was agreed. SEND: a person approves the invoice before it goes to the
576
+ client; record the issue date. DUE: wait for the payment; look for it in
577
+ the bank or payment provider (match the amount and the reference), and
578
+ move on once it is found or the due date has passed. PAID: its check
579
+ reads the payment; set the paid date and link the account it was
580
+ deposited to. When the due date passed unpaid, the check sends the run
581
+ to CHASE instead: the invoice becomes overdue and you draft a polite
582
+ reminder for a person to send; once the payment is found, move the run
583
+ back to Paid. Voiding an invoice is a person's call.
584
+ subjectProfile:
585
+ profileSlug: invoice
586
+ statusProperty: status
587
+ humanOnlyStatuses: [void]
588
+ activators:
589
+ - { on: created, mode: propose }
590
+ params:
591
+ - { name: invoice, type: entity, label: Invoice, required: true }
592
+ executor: is-agent
593
+ status: active
594
+ requiredIntents: [fetch_record, list_records, send_message]
595
+ expectedOutputs:
596
+ - { kind: document, label: Invoice sent to the client }
597
+ stages:
598
+ - key: prepare
599
+ name: Prepare
600
+ category: planned
601
+ subjectStatus: draft
602
+ goal: Fill every invoice field and check the amount against what was agreed.
603
+ criteria:
604
+ - key: invoice-fields-set
605
+ statement: The invoice number, customer, amount, currency, issue date and due date are set.
606
+ check: { kind: judge }
607
+ - key: invoice-amount-matches
608
+ statement: The amount matches the contract, engagement or deal it bills.
609
+ check: { kind: judge }
610
+ - key: send
611
+ name: Send
612
+ category: started
613
+ subjectStatus: sent
614
+ goal: A person approves the invoice before it goes to the client.
615
+ gate: { kind: human }
616
+ onFail: { toStage: prepare }
617
+ - key: due
618
+ name: Awaiting payment
619
+ category: started
620
+ goal: Wait for the payment and look for it; move on once it is found or the due date has passed.
621
+ indefinite: true
622
+ criteria:
623
+ - key: payment-matched
624
+ statement: A payment matching the invoice amount and reference was found.
625
+ check: { kind: evidence, evidenceKey: payment-reference }
626
+ - key: paid
627
+ name: Paid
628
+ category: completed
629
+ subjectStatus: paid
630
+ goal: Confirm the payment, set the paid date and link the account it was deposited to.
631
+ gate: { kind: check }
632
+ onFail: { toStage: chase }
633
+ - key: chase
634
+ name: Chase
635
+ category: started
636
+ subjectStatus: overdue
637
+ goal: The due date passed unpaid — draft a reminder for a person to send; once the payment is found, move the run back to Paid.
638
+ indefinite: true
639
+ criteria:
640
+ - key: late-payment-matched
641
+ statement: A payment matching the invoice amount and reference was found after the reminder.
642
+ check: { kind: evidence, evidenceKey: payment-reference }
643
+
644
+ # Recurring billing (founder decision 2026-10-12). Monthly, ONE run per
645
+ # active monthly revenue stream: the schedule is bound to the kind, so the
646
+ # pod's backing cron fans out query → loop → playbook_run over the streams
647
+ # the filter selects. The filter is a JSON STRING because the pod's cron door
648
+ # reads only a string (`readSubjectProfile`, cron-automation.ts) and refuses
649
+ # an unfiltered AI fan-out. The run only drafts the invoice: "Bill a client"
650
+ # takes it from there (its `created` activator). Writes are proposed, never
651
+ # applied unattended. Retainer specifics (quarterly/annual cadences, usage
652
+ # true-ups) are deliberately left for later.
653
+ - name: Bill a recurring fee
654
+ description: Once a month, draft the invoice for each active monthly revenue stream, for a person to approve; Bill a client then sends and collects it.
655
+ goalTemplate: >-
656
+ Draft this month's invoice for the recurring revenue stream ({stream}).
657
+ First look for an invoice already linked to this stream for this month;
658
+ if one exists, stop and say so. Otherwise create one invoice in Finance
659
+ at the draft status: the amount is the stream's MRR, the currency and
660
+ customer are those of the company or person it is billed to, the issue
661
+ date is today, and the due date follows the customer's usual terms (30
662
+ days when nothing says otherwise). Link it to the stream. When the
663
+ stream is billed to nobody, ask the person which client it bills
664
+ instead of guessing. Never send the invoice: Bill a client takes it
665
+ from the draft.
666
+ subjectProfile:
667
+ profileSlug: revenue-stream
668
+ filter: '{"properties.status":"active","properties.billing-cadence":"monthly"}'
669
+ params:
670
+ - { name: stream, type: entity, label: Revenue stream, required: true }
671
+ executor: is-agent
672
+ status: active
673
+ schedule: { cron: "0 8 1 * *", enabled: true }
674
+ metadata:
675
+ governance:
676
+ forceProposeWrites: true
677
+ requiredIntents: [list_records]
678
+ expectedOutputs:
679
+ - { kind: invoice, label: This month's invoice, relationToSubject: relates_to }
680
+
681
+ - name: Approve an expense
682
+ description: Review an expense against its budget, get a person's approval, and record the payment.
683
+ goalTemplate: >-
684
+ Handle this expense ({expense}); each stage moves its status. REVIEW:
685
+ check the vendor, amount, date, category and receipt, link the budget it
686
+ counts against and say how much of that budget is left after it. APPROVE:
687
+ a person approves or rejects the spend. PAY: a person confirms it was
688
+ paid; link the account it was paid from. Reimbursement and rejection are
689
+ a person's call.
690
+ subjectProfile:
691
+ profileSlug: expense
692
+ statusProperty: status
693
+ humanOnlyStatuses: [reimbursed, rejected]
694
+ activators:
695
+ - { on: created, mode: propose }
696
+ params:
697
+ - { name: expense, type: entity, label: Expense, required: true }
698
+ executor: is-agent
699
+ status: active
700
+ stages:
701
+ - key: review
702
+ name: Review
703
+ category: planned
704
+ subjectStatus: pending
705
+ goal: Check the expense's facts and receipt and link the budget it counts against.
706
+ criteria:
707
+ - key: expense-facts
708
+ statement: The vendor, amount, date and category are set.
709
+ check: { kind: judge }
710
+ - key: expense-budget-linked
711
+ statement: The expense is linked to the budget it counts against, with the remaining amount stated.
712
+ check: { kind: judge }
713
+ - key: approve
714
+ name: Approve
715
+ category: started
716
+ subjectStatus: approved
717
+ goal: A person approves the spend.
718
+ gate: { kind: human }
719
+ onFail: { toStage: review }
720
+ - key: pay
721
+ name: Pay
722
+ category: completed
723
+ subjectStatus: paid
724
+ goal: A person confirms the expense was paid; link the account it was paid from.
725
+ gate: { kind: human }
726
+ onFail: { toStage: approve }
727
+
728
+ - name: Close the month
729
+ description: Once a month, reconcile the accounts, chase what is owed, and record runway, burn and revenue for a person to review.
730
+ goalTemplate: >-
731
+ Close the month. RECONCILE: compare each account's balance with the bank
732
+ or payment provider and list every difference; when an account has no
733
+ bank or payment-provider connection, ask the person to `provide` it (a
734
+ connection, never a password in a form). RECEIVABLES: list unpaid
735
+ and overdue invoices and propose a reminder for each overdue one.
736
+ PAYABLES: list expenses still pending or approved but unpaid. METRICS:
737
+ record this month's cash in bank, net burn, runway in months and monthly
738
+ recurring revenue as financial metrics, each with its as-of date and how
739
+ it was computed. REVIEW: write a one-page month-end note for a person —
740
+ what changed, what is at risk, what to decide — and wait for their
741
+ approval before closing.
742
+ executor: is-agent
743
+ status: active
744
+ schedule: { cron: "0 9 1 * *", enabled: false, mode: appointment }
745
+ requiredIntents: [fetch_record, list_records]
746
+ expectedOutputs:
747
+ - { kind: financial-metric, label: Month-end metrics, relationToSubject: none }
748
+ - { kind: document, label: Month-end note, relationToSubject: none }
749
+ stages:
750
+ - key: reconcile
751
+ name: Reconcile
752
+ category: started
753
+ goal: Compare every account with the bank or payment provider and list differences.
754
+ criteria:
755
+ - key: accounts-reconciled
756
+ statement: Every active account has a balance compared with its source, and every difference is listed.
757
+ check: { kind: judge }
758
+ - key: receivables
759
+ name: Receivables
760
+ category: started
761
+ goal: List unpaid and overdue invoices and propose a reminder for each overdue one.
762
+ - key: payables
763
+ name: Payables
764
+ category: started
765
+ goal: List pending and approved-but-unpaid expenses.
766
+ - key: metrics
767
+ name: Metrics
768
+ category: started
769
+ goal: Record cash, net burn, runway and monthly recurring revenue as dated metrics.
770
+ criteria:
771
+ - key: metrics-recorded
772
+ statement: Cash in bank, net burn, runway and monthly recurring revenue are recorded with this month's as-of date.
773
+ check: { kind: judge }
774
+ - key: review
775
+ name: Review
776
+ category: completed
777
+ goal: A person reads the month-end note and approves the close.
778
+ gate: { kind: human }
779
+ onFail: { toStage: reconcile }