@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,82 @@
1
+ /**
2
+ * Static asset edit loop, W5 (build plan pod document ab3e0a5a §4, founder D0-D1, 2 Nov 2026).
3
+ * Pinned against the REAL YAML corpus and the real tombstone registry:
4
+ *
5
+ * 1. `content-studio` is no longer live, and `retired.yaml` points it at `content-os`, which
6
+ * declares every kind it held (content-composition, content-asset, render-job, the
7
+ * brand-template grant). No live template depends on it, and no standard targets it.
8
+ * 2. `brand-template`'s Format gains `layered` and `open-design` (the words content-os
9
+ * `render-engine` uses), additively, in Brand Library and in Content OS's mirror.
10
+ *
11
+ * Does NOT cover: the pod side (the resolver refuses a retired install and leaves an
12
+ * installed space alone, synap-backend resolve-workspace-template.ts), nor the CP row,
13
+ * which changes only when the catalog is published.
14
+ */
15
+
16
+ import { existsSync } from "fs";
17
+ import { dirname, join } from "path";
18
+ import { fileURLToPath } from "url";
19
+ import { describe, expect, it } from "vitest";
20
+ import { readYamlTemplates } from "./generate.mjs";
21
+ import type { WorkspaceYaml } from "./types.js";
22
+ import { loadRetired } from "../scripts/catalog-lib.mjs";
23
+
24
+ const here = dirname(fileURLToPath(import.meta.url));
25
+ const corpus = readYamlTemplates().map((t) => t.data as WorkspaceYaml);
26
+ const template = (slug: string) => corpus.find((t) => t.meta.slug === slug);
27
+ const prop = (tpl: WorkspaceYaml | undefined, profile: string, slug: string) =>
28
+ tpl?.profiles.find((p) => p.slug === profile)?.properties.find((d) => d.slug === slug) as
29
+ | { enumValues?: string[] }
30
+ | undefined;
31
+
32
+ const FORMATS_BEFORE = ["tldraw", "html", "json", "markdown", "image", "mixed", "hyperframes"];
33
+
34
+ describe("content-studio is retired into content-os", () => {
35
+ it("non-vacuity: the corpus is read and content-os is live", () => {
36
+ expect(corpus.length).toBeGreaterThanOrEqual(25);
37
+ expect(template("content-os")).toBeDefined();
38
+ });
39
+
40
+ it("is no longer live, and retired.yaml points it at content-os", async () => {
41
+ expect(corpus.map((t) => t.meta.slug)).not.toContain("content-studio");
42
+ const { retired, problems } = await loadRetired();
43
+ expect(problems).toEqual([]);
44
+ expect(retired.find((t) => t.slug === "content-studio")).toMatchObject({
45
+ kind: "workspace",
46
+ replacedBy: "content-os",
47
+ since: "2026-11-03",
48
+ });
49
+ });
50
+
51
+ it("content-os declares every kind content-studio held", () => {
52
+ const slugs = (template("content-os")?.profiles ?? []).map((p) => p.slug);
53
+ for (const kind of ["content-composition", "content-asset", "render-job", "brand-template"]) {
54
+ expect(slugs, `content-os must declare ${kind}`).toContain(kind);
55
+ }
56
+ });
57
+
58
+ it("nothing live depends on it, and no standard targets it", () => {
59
+ const dependents = corpus.filter((t) =>
60
+ ((t as { dependencies?: Array<{ slug: string }> }).dependencies ?? []).some((d) => d.slug === "content-studio")
61
+ );
62
+ expect(dependents.map((t) => t.meta.slug)).toEqual([]);
63
+ expect(existsSync(join(here, "../standards/content-studio.standard.yaml"))).toBe(false);
64
+ });
65
+ });
66
+
67
+ describe("brand-template gains the layered and open-design formats", () => {
68
+ it("uses the words content-os render-engine uses", () => {
69
+ const engines = prop(template("content-os"), "content-composition", "render-engine")?.enumValues ?? [];
70
+ expect(engines).toEqual(expect.arrayContaining(["layered", "open-design"]));
71
+ });
72
+
73
+ it("adds them without dropping a value, in Brand Library and Content OS's mirror", () => {
74
+ for (const slug of ["brand-library", "content-os"]) {
75
+ expect(prop(template(slug), "brand-template", "template-format")?.enumValues, slug).toEqual([
76
+ ...FORMATS_BEFORE,
77
+ "layered",
78
+ "open-design",
79
+ ]);
80
+ }
81
+ });
82
+ });
@@ -85,10 +85,9 @@ test("corpus: every shipped automation flow passes validateFlowDefinition", () =
85
85
  // These seeds ship automations verbatim into user pods, so they are held to the
86
86
  // same contract. Skips when the CP checkout is absent, like the drift guards.
87
87
  const HERE = dirname(fileURLToPath(import.meta.url));
88
- const CP_SEEDS = join(
89
- HERE,
90
- "../../../../synap-control-plane-api/src/seeds/capability-templates",
91
- );
88
+ // The catalog's capability manifests (`spec` = the CP seed JSON; the CP keeps a
89
+ // byte-checked mirror until its pin bump). Always present: no skip.
90
+ const CP_SEEDS = join(HERE, "../capabilities");
92
91
 
93
92
  test("corpus: every CP capability seed's automation flow passes validateFlowDefinition", (t) => {
94
93
  if (!existsSync(CP_SEEDS)) {
@@ -99,7 +98,7 @@ test("corpus: every CP capability seed's automation flow passes validateFlowDefi
99
98
  let flowsChecked = 0;
100
99
 
101
100
  for (const file of readdirSync(CP_SEEDS).filter((f) => f.endsWith(".json"))) {
102
- const seed = JSON.parse(readFileSync(join(CP_SEEDS, file), "utf8")) as {
101
+ const seed = (JSON.parse(readFileSync(join(CP_SEEDS, file), "utf8")) as { spec: unknown }).spec as {
103
102
  automations?: Array<{ key?: string; name?: string; flowDefinition?: unknown }>;
104
103
  };
105
104
  for (const automation of seed.automations ?? []) {
@@ -0,0 +1,117 @@
1
+ /**
2
+ * CRM absorbs Outreach & Comms (founder decision 2026-10-23: one space per
3
+ * domain; for a founder-led company outreach is part of the commercial job).
4
+ * Pinned against the REAL YAML corpus and the real tombstone registry:
5
+ *
6
+ * 1. `outreach-comms` is no longer live, and `retired.yaml` points it at `crm`;
7
+ * no live template still depends on it.
8
+ * 2. CRM owns `outreach-touch` with the SAME slug and lifecycle keys the CRM
9
+ * app reads, and `sent` is a person's move (the agent drafts, a person sends).
10
+ * 3. No twin: no template declares `outreach-campaign`; an outreach campaign is
11
+ * CRM's `campaign` (campaign-type outreach) carrying the angle's offer
12
+ * fields. Touch edges `sent_to` → person and `part_of` → campaign resolve.
13
+ * 4. No CRM process can send (no send grant, no send intent required) and none
14
+ * opens a deal from a reply; the reply reader proposes the estimate or a
15
+ * meeting and starts when a person marked the message sent (a touch from
16
+ * before the channels; a channel's replies are read by the follow-up).
17
+ *
18
+ * Does NOT cover: what an agent does at run time (the goals are prose), nor the
19
+ * pod applying the fold (no pod had outreach-comms installed; nothing migrates).
20
+ */
21
+
22
+ import { describe, expect, it } from "vitest";
23
+ import { readYamlTemplates } from "./generate.mjs";
24
+ import type { WorkspaceYaml } from "./types.js";
25
+ import { loadRetired } from "../scripts/catalog-lib.mjs";
26
+
27
+ const corpus = readYamlTemplates().map((t) => t.data as WorkspaceYaml);
28
+ const crm = corpus.find((t) => t.meta.slug === "crm")!;
29
+ const profileOf = (slug: string) => crm.profiles.find((p) => p.slug === slug);
30
+ const playbookOf = (name: string) => {
31
+ const pb = crm.playbooks?.find((p) => p.name === name);
32
+ if (!pb) throw new Error(`playbook "${name}" not in crm`);
33
+ return pb;
34
+ };
35
+ const outputsOf = (pb: ReturnType<typeof playbookOf>) => [
36
+ ...(pb.expectedOutputs ?? []),
37
+ ...(pb.stages ?? []).flatMap((s) => s.expectedOutputs ?? []),
38
+ ];
39
+ const SEND_GRANTS = ["unipile_send_message", "gmail_send", "linkedin_send_invite"];
40
+
41
+ describe("CRM absorbs Outreach & Comms", () => {
42
+ it("non-vacuity: the corpus is read and CRM has processes", () => {
43
+ expect(corpus.length).toBeGreaterThanOrEqual(25);
44
+ expect(crm).toBeDefined();
45
+ expect((crm.playbooks ?? []).length).toBeGreaterThanOrEqual(5);
46
+ });
47
+
48
+ it("outreach-comms is retired to crm, and nothing live depends on it", async () => {
49
+ expect(corpus.map((t) => t.meta.slug)).not.toContain("outreach-comms");
50
+ const { retired, problems } = await loadRetired();
51
+ expect(problems).toEqual([]);
52
+ expect(retired.find((t) => t.slug === "outreach-comms")).toMatchObject({
53
+ kind: "workspace",
54
+ replacedBy: "crm",
55
+ since: "2026-10-23",
56
+ });
57
+ const dependents = corpus.filter((t) =>
58
+ ((t as { dependencies?: Array<{ slug: string }> }).dependencies ?? []).some((d) => d.slug === "outreach-comms")
59
+ );
60
+ expect(dependents.map((t) => t.meta.slug)).toEqual([]);
61
+ });
62
+
63
+ it("CRM owns outreach-touch with the same lifecycle keys, and sending is a person's move", () => {
64
+ const touch = profileOf("outreach-touch");
65
+ expect(touch?.lifecycle?.property).toBe("status");
66
+ expect(touch?.lifecycle?.options.map((o) => [o.key, o.category, o.humanOnly ?? false])).toEqual([
67
+ ["drafted", "open", false],
68
+ ["sent", "active", true],
69
+ ["opened", "active", false],
70
+ ["replied", "done", false],
71
+ ["bounced", "lost", false],
72
+ ["no_response", "lost", false],
73
+ ]);
74
+ expect(crm.meta.intent?.produces?.map((p) => p.profile)).toContain("outreach-touch");
75
+ });
76
+
77
+ it("no outreach-campaign twin: the angle is CRM's campaign, and the touch edges resolve", () => {
78
+ for (const t of corpus) expect(t.profiles.map((p) => p.slug), t.meta.slug).not.toContain("outreach-campaign");
79
+ const campaign = profileOf("campaign");
80
+ const props = (campaign?.properties ?? []).map((p) => p.slug);
81
+ expect(props).toEqual(expect.arrayContaining(["campaign-value-prop", "campaign-call-to-action"]));
82
+ expect(campaign?.properties.find((p) => p.slug === "campaign-type")?.enumValues).toContain("outreach");
83
+ const touchLinks = (crm.entityLinks ?? []).filter((l) => l.sourceProfileSlug === "outreach-touch");
84
+ expect(touchLinks.map((l) => [l.type, l.targetProfileSlug])).toEqual([
85
+ ["sent_to", "person"],
86
+ ["part_of", "campaign"],
87
+ // Whose message it is: the lead, a person or a company (required, lane E2).
88
+ ["relates_to", "person"],
89
+ ["relates_to", "company"],
90
+ ]);
91
+ // A campaign reaches people through member_of, not a second `targets` edge.
92
+ expect((crm.entityLinks ?? []).filter((l) => l.sourceProfileSlug === "campaign")).toEqual([]);
93
+ });
94
+
95
+ it("no CRM process sends, and none opens a deal from a reply", () => {
96
+ for (const pb of crm.playbooks ?? []) {
97
+ const grants = (pb as { grants?: string[] }).grants ?? [];
98
+ expect(grants.filter((g) => SEND_GRANTS.includes(g)), pb.name).toEqual([]);
99
+ expect(pb.requiredIntents ?? [], pb.name).not.toContain("send_message");
100
+ }
101
+ const reply = playbookOf("Read the reply to an outreach message");
102
+ expect(reply.subjectProfile?.profileSlug).toBe("outreach-touch");
103
+ expect(reply.activators).toEqual([{ on: "enters_status", status: "sent", mode: "run" }]);
104
+ expect(outputsOf(reply).map((o) => o.kind)).not.toContain("deal");
105
+ expect(reply.goalTemplate).toMatch(/estimate/);
106
+ expect(reply.goalTemplate).toMatch(/meeting/);
107
+ expect(reply.goalTemplate).not.toMatch(/(?<!never )open a deal/i);
108
+ // The run starts at `sent`: no stage may move the message back to drafted or sent.
109
+ expect((reply.stages ?? []).map((s) => s.subjectStatus).filter(Boolean)).toEqual(["replied", "bounced", "no_response"]);
110
+
111
+ const outreach = playbookOf("Personalized Outreach");
112
+ expect(outreach.subjectProfile).toMatchObject({ profileSlug: "campaign", statusProperty: "campaign-status" });
113
+ expect(outputsOf(outreach)).toEqual([
114
+ { kind: "messaging", label: "Proposed first messages", relationToSubject: "none" },
115
+ ]);
116
+ });
117
+ });
@@ -0,0 +1,128 @@
1
+ /**
2
+ * The CRM build plan's catalog changes (pod document a684720a, section 3A,
3
+ * founder-approved 2026-10-22), pinned against the REAL YAML corpus and the
4
+ * real validator:
5
+ *
6
+ * A1 the lead role's leadSource names warm-network, research, import, inbound-form;
7
+ * A2 the deal's first stages read "Meeting booked" / "Proposal sent", and every
8
+ * stage still open is stuck after 14 days;
9
+ * A3 the deal carries the booked meeting's date;
10
+ * A5 "Build a prospect list from our foundation": ONE review per batch, and the
11
+ * next batch reads back the candidates a person dropped;
12
+ * A6 "Follow up on leads in outreach" is the one follow-up process and drafts
13
+ * proposed messages through the pod's send door (2026-11-02; outreach
14
+ * touches before); no outreach process opens a deal (Outreach & Comms'
15
+ * "Send an outreach message" is CRM's "Read the reply to an outreach
16
+ * message" since the fold of 2026-10-23, pinned in
17
+ * crm-absorbs-outreach.template.test.ts);
18
+ * A7 "Prepare a pre-sale estimate": the owner approves, then a SENT step records
19
+ * date and channel on the lead;
20
+ * A8 a lead's segment is a `targets` edge to an audience.
21
+ * A4 ("Qualify a lead" opens no deal) is pinned in decisions-wave.template.test.ts.
22
+ *
23
+ * Does NOT cover: what an agent does at run time (the goals are prose), and A9
24
+ * (CRM referencing `contact`), which waits on the pod granting a system role.
25
+ */
26
+
27
+ import { describe, expect, it } from "vitest";
28
+ import { readYamlTemplates } from "./generate.mjs";
29
+ import type { WorkspaceYaml } from "./types.js";
30
+ import { validateTemplate } from "./validate.js";
31
+
32
+ const corpus = readYamlTemplates().map((t) => t.data as WorkspaceYaml);
33
+ const tpl = (slug: string) => {
34
+ const t = corpus.find((x) => x.meta.slug === slug);
35
+ if (!t) throw new Error(`template ${slug} not in the corpus`);
36
+ return t;
37
+ };
38
+ const profileOf = (t: WorkspaceYaml, slug: string) => t.profiles.find((p) => p.slug === slug);
39
+ const propOf = (t: WorkspaceYaml, profile: string, prop: string) =>
40
+ profileOf(t, profile)?.properties.find((p) => p.slug === prop);
41
+ const playbookOf = (t: WorkspaceYaml, name: string) => {
42
+ const pb = t.playbooks?.find((p) => p.name === name);
43
+ if (!pb) throw new Error(`playbook "${name}" not in ${t.meta.slug}`);
44
+ return pb;
45
+ };
46
+ const outputsOf = (pb: ReturnType<typeof playbookOf>) => [
47
+ ...(pb.expectedOutputs ?? []),
48
+ ...(pb.stages ?? []).flatMap((s) => s.expectedOutputs ?? []),
49
+ ];
50
+
51
+ const crm = tpl("crm");
52
+
53
+ describe("CRM build plan — catalog changes", () => {
54
+ it("crm validates against the corpus", () => {
55
+ expect(validateTemplate(crm, { corpus }).errors).toEqual([]);
56
+ });
57
+
58
+ it("A1: the lead role's leadSource names the four new sources", () => {
59
+ const values = propOf(crm, "lead", "leadSource")?.enumValues ?? [];
60
+ for (const v of ["warm-network", "research", "import", "inbound-form"]) expect(values).toContain(v);
61
+ });
62
+
63
+ it("A2: deal stages read Meeting booked / Proposal sent, and every unfinished stage is stuck after 14 days", () => {
64
+ const options = profileOf(crm, "deal")?.lifecycle?.options ?? [];
65
+ expect(options.map((o) => [o.key, o.label, o.stuckAfterDays])).toEqual([
66
+ ["draft", "Meeting booked", 14],
67
+ ["proposed", "Proposal sent", 14],
68
+ ["negotiating", undefined, 14],
69
+ ["won", undefined, undefined],
70
+ ["lost", undefined, undefined],
71
+ ]);
72
+ });
73
+
74
+ it("A3: the deal records the booked meeting's date", () => {
75
+ expect(propOf(crm, "deal", "meetingDate")).toMatchObject({ valueType: "date", inputType: "date" });
76
+ });
77
+
78
+ it("A5: the prospect list is one review per batch, reads back dropped candidates, and creates leads only there", () => {
79
+ const pb = playbookOf(crm, "Build a prospect list from our foundation");
80
+ expect(pb.subjectProfile?.profileSlug).toBe("audience");
81
+ // No stage gate asks the person a second time: the batch's proposal is the review.
82
+ expect((pb.stages ?? []).filter((s) => s.gate)).toEqual([]);
83
+ expect(pb.goalTemplate).toMatch(/ONE proposal/);
84
+ expect(pb.goalTemplate).toMatch(/dropped/);
85
+ expect(pb.goalTemplate).toMatch(/lead source\s+research/);
86
+ expect(outputsOf(pb)).toEqual([{ kind: "company", label: "Prospect accounts", relationToSubject: "targets" }]);
87
+ });
88
+
89
+ it("A6: one follow-up process drafts proposed messages on weekdays; no outreach process opens a deal", () => {
90
+ const pb = playbookOf(crm, "Follow up on leads in outreach");
91
+ expect(pb.subjectProfile).toMatchObject({ profileSlug: "lead", statusProperty: "leadStage" });
92
+ expect(pb.activators).toContainEqual({ on: "schedule", cron: "0 7 * * 1-5", status: "outreach", mode: "propose" });
93
+ expect(outputsOf(pb).map((o) => o.kind)).toEqual(["messaging"]);
94
+ expect(pb.stages?.at(-1)).toMatchObject({ key: "send", gate: { kind: "human" } });
95
+
96
+ const followUps = corpus.flatMap((t) => (t.playbooks ?? []).filter((p) => /follow.?up/i.test(p.name)));
97
+ expect(followUps.map((p) => p.name)).toEqual(["Follow up on leads in outreach"]);
98
+
99
+ const reply = playbookOf(crm, "Read the reply to an outreach message");
100
+ expect(outputsOf(reply).map((o) => o.kind)).not.toContain("deal");
101
+ expect(reply.goalTemplate).not.toMatch(/(?<!never )open a deal/i);
102
+ });
103
+
104
+ it("A7: the estimate is approved by the owner, then a SENT step records date and channel on the lead", () => {
105
+ const pb = playbookOf(crm, "Prepare a pre-sale estimate");
106
+ expect(pb.subjectProfile?.profileSlug).toBe("lead");
107
+ expect((pb.stages ?? []).map((s) => [s.key, s.gate?.kind])).toEqual([
108
+ ["research", undefined],
109
+ ["draft", undefined],
110
+ ["approve", "human"],
111
+ ["sent", "human"],
112
+ ]);
113
+ // A reader: an estimate never moves the lead's stage.
114
+ expect((pb.stages ?? []).filter((s) => s.subjectStatus)).toEqual([]);
115
+ expect(propOf(crm, "lead", "estimateSentAt")).toMatchObject({ valueType: "date" });
116
+ expect(propOf(crm, "lead", "estimateSentChannel")?.inputType).toBe("select");
117
+ });
118
+
119
+ it("A8: a lead's segment is a targets edge from the person or company to an audience", () => {
120
+ const links = (crm.entityLinks ?? []).filter((l) => l.targetProfileSlug === "audience");
121
+ expect(links.map((l) => [l.sourceProfileSlug, l.type])).toEqual([
122
+ ["person", "targets"],
123
+ ["company", "targets"],
124
+ ]);
125
+ // A reference to foundation's shared kind, so the CRM space is granted it.
126
+ expect(profileOf(crm, "audience")).toMatchObject({ scope: "shared", entityScope: "pod", properties: [] });
127
+ });
128
+ });
@@ -0,0 +1,96 @@
1
+ /**
2
+ * CRM lists, signal-first scores, Claude Code drafts (contract of 2026-10-27,
3
+ * founder decisions in pod session ba6f39c8), pinned against the REAL YAML
4
+ * corpus and the real validator. The CRM app codes against these names:
5
+ *
6
+ * - the lead role carries buyingSignal / buyingSignalSource / scoredAt, and
7
+ * icpScore reads 1–5;
8
+ * - an outreach touch can be a first_message;
9
+ * - "Score a list" (subject audience) is the ONE home of the rubric;
10
+ * - "Draft a first message" (subject lead) drafts one touch and never sends;
11
+ * - "Build a prospect list from our foundation" takes pool public|network.
12
+ * "Qualify a lead" being manual only is pinned in decisions-wave.template.test.ts.
13
+ *
14
+ * Does NOT cover what an agent does at run time (the goals are prose).
15
+ */
16
+
17
+ import { describe, expect, it } from "vitest";
18
+ import { readYamlTemplates } from "./generate.mjs";
19
+ import type { WorkspaceYaml } from "./types.js";
20
+ import { validateTemplate } from "./validate.js";
21
+
22
+ const corpus = readYamlTemplates().map((t) => t.data as WorkspaceYaml);
23
+ const crm = corpus.find((x) => x.meta.slug === "crm")!;
24
+ const profileOf = (slug: string) => crm.profiles.find((p) => p.slug === slug);
25
+ const propOf = (profile: string, prop: string) => profileOf(profile)?.properties.find((p) => p.slug === prop);
26
+ const playbookOf = (name: string) => {
27
+ const pb = crm.playbooks?.find((p) => p.name === name);
28
+ if (!pb) throw new Error(`playbook "${name}" not in crm`);
29
+ return pb;
30
+ };
31
+ const paramsOf = (name: string) =>
32
+ (playbookOf(name).params ?? []).map((p) => [p.name, p.type, p.required ?? false, (p as { defaultValue?: unknown }).defaultValue]);
33
+
34
+ describe("CRM lists — catalog contract", () => {
35
+ it("crm validates against the corpus", () => {
36
+ expect(validateTemplate(crm, { corpus }).errors).toEqual([]);
37
+ });
38
+
39
+ it("the lead role carries the signal fields, and icpScore reads 1–5", () => {
40
+ expect(propOf("lead", "buyingSignal")).toMatchObject({ valueType: "string", inputType: "select", enumValues: ["hiring", "asking", "tooling", "none"] });
41
+ expect(propOf("lead", "buyingSignalSource")).toMatchObject({ valueType: "string", inputType: "url" });
42
+ expect(propOf("lead", "scoredAt")).toMatchObject({ valueType: "date", inputType: "date" });
43
+ expect(propOf("lead", "icpScore")?.label).toMatch(/1–5/);
44
+ });
45
+
46
+ it("an outreach touch can be a first message", () => {
47
+ expect(propOf("outreach-touch", "touchType")?.enumValues).toEqual(["first_message", "connection_request", "follow_up", "intro_email", "reply"]);
48
+ });
49
+
50
+ it("'Score a list' scores one audience's leads in one manual run and is the rubric's one home", () => {
51
+ const pb = playbookOf("Score a list");
52
+ expect(pb.subjectProfile).toEqual({ profileSlug: "audience" });
53
+ expect(pb.activators).toEqual([{ on: "manual", mode: "propose" }]);
54
+ expect(paramsOf("Score a list")).toEqual([
55
+ ["audience", "entity", true, undefined],
56
+ ["limit", "number", false, 50],
57
+ ["rescoreAfterDays", "number", false, 30],
58
+ ]);
59
+ // The audience has no lifecycle: no stage moves anything.
60
+ expect((pb.stages ?? []).filter((s) => s.subjectStatus || s.gate)).toEqual([]);
61
+ for (const part of [/SIGNAL, 0 to\s+2/, /FIT, 0\s+to 2/, /WARMTH, 0 or 1/, /within the last 90 days/])
62
+ expect(pb.goalTemplate).toMatch(part);
63
+ // ONE home: no other playbook restates the rubric.
64
+ const restating = corpus.flatMap((t) => (t.playbooks ?? []).filter((p) => p !== pb && /WARMTH, 0 or 1/.test(p.goalTemplate)));
65
+ expect(restating.map((p) => p.name)).toEqual([]);
66
+ });
67
+
68
+ it("'Draft a first message' drafts one proposed message through the send door for a person to approve, and reads the lead's lifecycle", () => {
69
+ const pb = playbookOf("Draft a first message");
70
+ expect(pb.subjectProfile).toEqual({ profileSlug: "lead" });
71
+ expect(pb.activators).toEqual([{ on: "manual", mode: "propose" }]);
72
+ expect(paramsOf("Draft a first message")).toEqual([
73
+ ["lead", "entity", true, undefined],
74
+ ["campaign", "entity", false, undefined],
75
+ ["channel", "choice", false, "linkedin"],
76
+ ]);
77
+ expect(pb.grants).toEqual(["entity.create"]);
78
+ expect((pb.stages ?? []).flatMap((s) => s.expectedOutputs ?? [])).toEqual([
79
+ { kind: "messaging", label: "Proposed first message", relationToSubject: "none" },
80
+ ]);
81
+ // A reader: no stage moves the lead; the person approves the send behind a human gate.
82
+ expect((pb.stages ?? []).filter((s) => s.subjectStatus)).toEqual([]);
83
+ expect(pb.stages?.at(-1)).toMatchObject({ key: "send", gate: { kind: "human" } });
84
+ expect(pb.goalTemplate).toMatch(/run messaging\.send with to = that person's entity id/);
85
+ expect(pb.goalTemplate).toMatch(/Never send\./);
86
+ expect(pb.goalTemplate).toMatch(/"Send the first message to\s+<name> on LinkedIn"/);
87
+ });
88
+
89
+ it("'Build a prospect list from our foundation' takes a pool, and the network pool makes warm-network leads", () => {
90
+ const pb = playbookOf("Build a prospect list from our foundation");
91
+ expect(pb.params?.find((p) => p.name === "pool")).toMatchObject({ type: "choice", options: ["public", "network"], defaultValue: "public" });
92
+ expect(pb.goalTemplate).toMatch(/\{pool\}/);
93
+ expect(pb.goalTemplate).toMatch(/lead source warm-network/);
94
+ expect(pb.goalTemplate).toMatch(/"Score\s+a list" process/);
95
+ });
96
+ });