@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.
- package/README.md +483 -0
- package/automations/bookmark-enrichment.automation.json +321 -0
- package/automations/github-pr-events.automation.json +505 -0
- package/automations/mail-feed.automation.json +86 -0
- package/automations/ship-a-change-checks-green.automation.json +106 -0
- package/automations/ship-a-change-checks-red.automation.json +106 -0
- package/automations/ship-a-change-deployed.automation.json +56 -0
- package/automations/ship-a-change-pr-merged.automation.json +110 -0
- package/automations/ship-a-change-pr-opened.automation.json +104 -0
- package/automations/url-bookmark-capture.automation.json +132 -0
- package/capabilities/agency-skills.capability.json +157 -0
- package/capabilities/apify.capability.json +265 -0
- package/capabilities/apollo.capability.json +120 -0
- package/capabilities/arch-backend.capability.json +97 -0
- package/capabilities/arch-client-intelligence.capability.json +401 -0
- package/capabilities/backblaze-b2.capability.json +205 -0
- package/capabilities/cal-com.capability.json +266 -0
- package/capabilities/canva.capability.json +274 -0
- package/capabilities/claude-code-routines.capability.json +172 -0
- package/capabilities/claude-managed-agents.capability.json +215 -0
- package/capabilities/client-comms-report.capability.json +246 -0
- package/capabilities/content-idea-to-production.capability.json +29 -0
- package/capabilities/datagouv.capability.json +315 -0
- package/capabilities/discord-bot.capability.json +157 -0
- package/capabilities/dropbox.capability.json +281 -0
- package/capabilities/exa.capability.json +109 -0
- package/capabilities/fal-ai.capability.json +83 -0
- package/capabilities/fal-media-lab.capability.json +158 -0
- package/capabilities/fireflies.capability.json +191 -0
- package/capabilities/freellmapi.capability.json +114 -0
- package/capabilities/generic-apikey.capability.json +92 -0
- package/capabilities/github-merge.capability.json +99 -0
- package/capabilities/github-read.capability.json +325 -0
- package/capabilities/governance-tighten-recommender.capability.json +81 -0
- package/capabilities/jev.capability.json +226 -0
- package/capabilities/lead-conversion-enrollment.capability.json +246 -0
- package/capabilities/mail-feed.capability.json +277 -0
- package/capabilities/mailgun.capability.json +52 -0
- package/capabilities/nango-google.capability.json +639 -0
- package/capabilities/notion.capability.json +417 -0
- package/capabilities/omniroute.capability.json +137 -0
- package/capabilities/posthog-analytics.capability.json +258 -0
- package/capabilities/postiz.capability.json +467 -0
- package/capabilities/proactive-digest.capability.json +190 -0
- package/capabilities/proton.capability.json +288 -0
- package/capabilities/replicate-rvm.capability.json +97 -0
- package/capabilities/research-methods.capability.json +57 -0
- package/capabilities/shopify.capability.json +395 -0
- package/capabilities/slack.capability.json +369 -0
- package/capabilities/stripe.capability.json +420 -0
- package/capabilities/telegram-bridge.capability.json +56 -0
- package/capabilities/treg.capability.json +102 -0
- package/capabilities/unipile-linkedin.capability.json +471 -0
- package/capabilities/vercel.capability.json +424 -0
- package/dist/define-CPS0n8BV.d.ts +1331 -0
- package/dist/index.d.ts +174 -900
- package/dist/index.js +26148 -23931
- package/dist/index.js.map +1 -1
- package/dist/wire.d.ts +222 -0
- package/dist/wire.js +77 -0
- package/dist/wire.js.map +1 -0
- package/package.json +27 -4
- package/packs/bookmark-views-pack.json +33 -0
- package/packs/crm-pack.json +76 -0
- package/packs/customer-profile.json +28 -0
- package/packs/deep-research.playbook.json +30 -0
- package/packs/event-views-pack.json +33 -0
- package/packs/habit-profile.json +27 -0
- package/packs/hr-pack.json +54 -0
- package/packs/invoice-profile.json +28 -0
- package/packs/knowledge-base-pack.json +56 -0
- package/packs/meeting-profile.json +29 -0
- package/packs/product-profile.json +30 -0
- package/packs/project-management-pack.json +49 -0
- package/packs/recipe-profile.json +29 -0
- package/packs/task-views-pack.json +39 -0
- package/retired.yaml +136 -0
- package/skills/app-factory-build-slice/SKILL.md +70 -0
- package/skills/app-factory-build-slice.catalog.json +17 -0
- package/skills/app-factory-design-canvas/SKILL.md +96 -0
- package/skills/app-factory-design-canvas.catalog.json +17 -0
- package/skills/app-factory-design-review/SKILL.md +99 -0
- package/skills/app-factory-design-review.catalog.json +18 -0
- package/skills/app-factory-research/SKILL.md +123 -0
- package/skills/app-factory-research.catalog.json +17 -0
- package/skills/bookmark-enrichment/SKILL.md +40 -0
- package/skills/carousel-format/carousel-format.skill.json +18 -0
- package/skills/collage-broll/collage-broll.skill.json +20 -0
- package/skills/content-taste/content-taste.skill.json +18 -0
- package/skills/creative-director/creative-director.skill.json +18 -0
- package/skills/design-critique/SKILL.md +135 -0
- package/skills/design-critique.catalog.json +12 -0
- package/skills/designing-with-real-components/SKILL.md +140 -0
- package/skills/designing-with-real-components.catalog.json +12 -0
- package/skills/edit-loop/edit-loop.skill.json +25 -0
- package/skills/facecam-edit/facecam-edit.skill.json +24 -0
- package/skills/mapping-the-server/SKILL.md +54 -0
- package/skills/math-explainer-manimgx/math-explainer-manimgx.skill.json +20 -0
- package/skills/motion-quality-gates/motion-quality-gates.skill.json +20 -0
- package/skills/open-design-edit/open-design-edit.skill.json +24 -0
- package/skills/reading-client-progress/SKILL.md +65 -0
- package/skills/routing-to-channels/SKILL.md +54 -0
- package/skills/stellar-scf-grants-advisor/ASSOCIATE-CHEATSHEET.md +133 -0
- package/skills/stellar-scf-grants-advisor/ASSOCIATE-ONBOARDING-GUIDE.md +761 -0
- package/skills/stellar-scf-grants-advisor/CLIENT-FAQ.md +181 -0
- package/skills/stellar-scf-grants-advisor/PARTNER-HANDOVER.md +130 -0
- package/skills/stellar-scf-grants-advisor/SKILL.md +102 -0
- package/skills/stellar-scf-grants-advisor/examples/README.md +43 -0
- package/skills/stellar-scf-grants-advisor/examples/chat-summary-templates.md +158 -0
- package/skills/stellar-scf-grants-advisor/examples/corpus-comparable-rows.md +136 -0
- package/skills/stellar-scf-grants-advisor/examples/q10-team-rewrites.md +109 -0
- package/skills/stellar-scf-grants-advisor/examples/q2-description-rewrites.md +88 -0
- package/skills/stellar-scf-grants-advisor/examples/q6-integration-rewrites.md +107 -0
- package/skills/stellar-scf-grants-advisor/examples/tranche-structures.md +176 -0
- package/skills/stellar-scf-grants-advisor/reference/01-methodology.md +96 -0
- package/skills/stellar-scf-grants-advisor/reference/02-scoring-framework.md +199 -0
- package/skills/stellar-scf-grants-advisor/reference/03-rejection-patterns.md +241 -0
- package/skills/stellar-scf-grants-advisor/reference/04-winner-patterns.md +120 -0
- package/skills/stellar-scf-grants-advisor/reference/05-comeback-patterns.md +92 -0
- package/skills/stellar-scf-grants-advisor/reference/06-budget-benchmarks.md +125 -0
- package/skills/stellar-scf-grants-advisor/reference/07-integration-list.md +147 -0
- package/skills/stellar-scf-grants-advisor/reference/08-abstract-fields.md +258 -0
- package/skills/stellar-scf-grants-advisor/reference/09-build-fields.md +259 -0
- package/skills/stellar-scf-grants-advisor/reference/10-corpus-winners.md +135 -0
- package/skills/stellar-scf-grants-advisor/reference/11-corpus-rejects.md +267 -0
- package/skills/stellar-scf-grants-advisor/reference/12-client-communication.md +131 -0
- package/skills/stellar-scf-grants-advisor/reference/13-excel-output-spec.md +199 -0
- package/skills/stellar-scf-grants-advisor/reference/14-worked-examples.md +160 -0
- package/skills/stellar-scf-grants-advisor/templates/abstract-review-builder.py +253 -0
- package/skills/stellar-scf-grants-advisor/templates/build-review-builder.py +213 -0
- package/skills/video-direction/video-direction.skill.json +20 -0
- package/skills/video-style-vocabulary/video-style-vocabulary.skill.json +20 -0
- package/skills/video-toolbox/video-toolbox.skill.json +20 -0
- package/src/__fixtures__/lifecycle-presets.flat-before.json +205 -0
- package/src/agency-os.yaml +122 -0
- package/src/agent-os-skill-slugs.test.ts +65 -0
- package/src/apify-social-reader.test.ts +317 -0
- package/src/base.yaml +85 -39
- package/src/blockchain-ecosystem.yaml +1 -0
- package/src/brand-library.yaml +525 -14
- package/src/builder-dev-fold.template.test.ts +113 -0
- package/src/builder.yaml +779 -91
- package/src/business-model-catalog-description.test.ts +47 -0
- package/src/business-model-grp.template.test.ts +214 -0
- package/src/business-model.yaml +296 -49
- package/src/catalog-safety.test.ts +198 -0
- package/src/catalog.template-items.test.ts +75 -0
- package/src/catalog.ts +66 -2
- package/src/concept-guard.test.ts +28 -8
- package/src/concept-guard.ts +2 -2
- package/src/content-creation.template.test.ts +52 -0
- package/src/content-creation.yaml +192 -0
- package/src/content-os.edit-loop.template.test.ts +210 -0
- package/src/content-os.publish-due-posts.template.test.ts +261 -0
- package/src/content-os.template.test.ts +322 -0
- package/src/content-os.video-takes.template.test.ts +338 -0
- package/src/content-os.work-a-take.template.test.ts +125 -0
- package/src/content-os.yaml +1830 -39
- package/src/content-studio-retired.template.test.ts +82 -0
- package/src/corpus-flow-validity.test.ts +4 -5
- package/src/crm-absorbs-outreach.template.test.ts +117 -0
- package/src/crm-build-plan.template.test.ts +128 -0
- package/src/crm-lists.template.test.ts +96 -0
- package/src/crm-messages-cadence.template.test.ts +164 -0
- package/src/crm-one-send-door.template.test.ts +65 -0
- package/src/crm.yaml +1456 -181
- package/src/decisions-wave.template.test.ts +186 -0
- package/src/define.ts +130 -17
- package/src/design-to-implementation.template.test.ts +94 -0
- package/src/domain-template-type.test.ts +75 -0
- package/src/ecosystem.yaml +27 -66
- package/src/enterprise-os.yaml +10 -134
- package/src/facecam-edit.audio-studio.test.ts +188 -0
- package/src/fal-generate-image.test.ts +200 -0
- package/src/fal-media-lab-pod-files.test.ts +180 -0
- package/src/finance.yaml +254 -1
- package/src/foundation.yaml +78 -38
- package/src/frame-host-modules.d.mts +6 -0
- package/src/frame-host-modules.mjs +58 -0
- package/src/generate.mjs +9 -3
- package/src/github-merge.test.ts +210 -0
- package/src/grants.yaml +46 -17
- package/src/hr.yaml +39 -6
- package/src/index.ts +36 -1
- package/src/intent.test.ts +161 -0
- package/src/internal-runbook.yaml +19 -1
- package/src/jev.test.ts +356 -0
- package/src/legal.yaml +33 -1
- package/src/life-os.yaml +51 -240
- package/src/lifecycle-coverage.test.ts +124 -0
- package/src/lifecycle-declarations.test.ts +252 -0
- package/src/lifecycle-presets.test.ts +468 -0
- package/src/lifecycle-presets.ts +494 -0
- package/src/lifecycle-stuck.test.ts +46 -0
- package/src/lifecycle-waits.parity.test.ts +319 -0
- package/src/marketing.yaml +108 -4
- package/src/meetings-knowledge.yaml +18 -0
- package/src/networking.yaml +23 -2
- package/src/operations.yaml +304 -4
- package/src/pipeline-views.template.test.ts +88 -0
- package/src/playbook-goal-placeholders.test.ts +57 -0
- package/src/postiz-contract.test.ts +134 -0
- package/src/presentation-declarations.test.ts +174 -0
- package/src/process-contract.validate.test.ts +352 -0
- package/src/project-management.yaml +39 -740
- package/src/publish-body.d.mts +29 -0
- package/src/publish-body.mjs +74 -0
- package/src/publish-body.test.ts +74 -0
- package/src/publish-official-lib.test.ts +176 -0
- package/src/replicate-rvm.test.ts +504 -0
- package/src/research-base.yaml +119 -20
- package/src/research-decision.yaml +60 -3
- package/src/reserved-profile-slugs.test.ts +83 -0
- package/src/retired-goal-kinds.test.ts +58 -0
- package/src/role-reference-and-step-domain.test.ts +192 -0
- package/src/seed-obligation.test.ts +1 -6
- package/src/send-message-maps.test.ts +227 -0
- package/src/shared-role-ssot.test.ts +40 -2
- package/src/ship-a-change.template.test.ts +177 -0
- package/src/skills.test.ts +170 -0
- package/src/social.yaml +85 -27
- package/src/space-packages.freshness.test.ts +97 -0
- package/src/space-packages.generated.ts +40 -0
- package/src/space-packages.test.ts +125 -0
- package/src/space-packages.ts +73 -0
- package/src/start.test.ts +188 -0
- package/src/task-intents.test.ts +293 -0
- package/src/templates.ts +20851 -21496
- package/src/types.ts +443 -60
- package/src/unipile-publish-post.test.ts +162 -0
- package/src/validate-flow.drift.test.ts +2 -3
- package/src/validate-flow.ts +22 -2
- package/src/validate-onboarding.test.ts +59 -0
- package/src/validate.test.ts +71 -4
- package/src/validate.ts +1335 -7
- package/src/wire-core.ts +231 -0
- package/src/wire.test.ts +159 -0
- package/src/wire.ts +107 -0
- package/src/agent-fleet.yaml +0 -1645
- package/src/business-developer.template.test.ts +0 -351
- package/src/business-developer.yaml +0 -860
- package/src/content-studio.yaml +0 -361
- package/src/dev-dashboard.yaml +0 -990
- package/src/outreach-comms.yaml +0 -486
- 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
|
-
|
|
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 }
|