@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.
- package/README.md +509 -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 +1496 -0
- package/capabilities/telegram-bridge.capability.json +56 -0
- package/capabilities/treg.capability.json +102 -0
- package/capabilities/unipile-linkedin.capability.json +494 -0
- package/capabilities/vercel.capability.json +424 -0
- package/dist/define-DqoItRiR.d.ts +1370 -0
- package/dist/index.d.ts +174 -900
- package/dist/index.js +26447 -24075
- 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/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 +105 -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 +138 -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 +349 -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 +1873 -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 +246 -0
- package/src/crm-one-send-door.template.test.ts +65 -0
- package/src/crm.yaml +1563 -181
- package/src/decisions-wave.template.test.ts +186 -0
- package/src/define.ts +146 -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 +81 -0
- package/src/retired-goal-kinds.test.ts +52 -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/stripe-capability.test.ts +192 -0
- package/src/task-intents.test.ts +293 -0
- package/src/template-documents.test.ts +158 -0
- package/src/templates.ts +20907 -21464
- package/src/types.ts +475 -61
- package/src/unipile-linkedin-find-profile.test.ts +183 -0
- 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 +1416 -7
- package/src/wire-core.ts +231 -0
- package/src/wire.test.ts +159 -0
- package/src/wire.ts +107 -0
- package/views/bookmark-views-pack.view.json +39 -0
- package/views/event-views-pack.view.json +45 -0
- package/views/task-views-pack.view.json +53 -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,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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
+
});
|