@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,349 @@
1
+ /**
2
+ * Content OS pin strategy — Produce Content is pinned; the weekly cron that
3
+ * runs it ships paused; Plan Next Content's workers (weekly and by hand) run
4
+ * once over every post and propose. Not a YAML snapshot.
5
+ */
6
+ import { readFileSync } from "fs";
7
+ import { dirname, join } from "path";
8
+ import { fileURLToPath } from "url";
9
+ import { load } from "js-yaml";
10
+ import { describe, expect, it } from "vitest";
11
+ import { getWorkspaceTemplate } from "./templates.js";
12
+ import type { TemplatePlaybook, WorkspaceYaml } from "./types.js";
13
+
14
+ const template = (() => {
15
+ const resolved = getWorkspaceTemplate("content-os");
16
+ if (!resolved) throw new Error("content-os template is not registered");
17
+ return resolved;
18
+ })();
19
+
20
+ type Strategy = NonNullable<TemplatePlaybook["inputStrategy"]>;
21
+
22
+ function strategyKind(strategy: Strategy): string {
23
+ return typeof strategy === "string" ? strategy : strategy.kind;
24
+ }
25
+
26
+ describe("Content OS pin strategy", () => {
27
+ it("keeps the legacy string form and the pinned object", () => {
28
+ expect(strategyKind("query")).toBe("query");
29
+ expect(strategyKind({ kind: "pinned" })).toBe("pinned");
30
+ // @ts-expect-error pinned entity ids are not part of the strategy
31
+ const rejected: Strategy = { kind: "pinned", pinnedEntityIds: ["post-1"] };
32
+ expect(rejected).toBeDefined();
33
+ });
34
+
35
+ it("pins Produce Content; Plan Next Content runs weekly and by hand, once over every post, proposing", () => {
36
+ const produce = template.playbooks?.find((p) => p.name === "Produce Content");
37
+ const plan = template.playbooks?.find((p) => p.name === "Plan Next Content");
38
+
39
+ expect(produce?.inputStrategy).toEqual({
40
+ kind: "pinned",
41
+ fallback: {
42
+ filter: { "post-status": "Idea" },
43
+ orderBy: "createdAt",
44
+ orderDir: "asc",
45
+ },
46
+ });
47
+ expect(produce).not.toHaveProperty("schedule");
48
+ expect(produce?.inputStrategy).not.toHaveProperty("pinnedEntityIds");
49
+
50
+ // A subject-bound playbook's `schedule:` fans out one run per post: the
51
+ // plan's clock is a per_moment activator instead, never that field.
52
+ expect(plan).not.toHaveProperty("schedule");
53
+ expect(plan).not.toHaveProperty("inputStrategy");
54
+ expect(plan?.subjectProfile).toEqual({ profileSlug: "post" });
55
+ expect(plan?.stages ?? []).toEqual([]);
56
+ expect(plan?.activators).toEqual([
57
+ { on: "schedule", cron: "0 9 * * 1", mode: "propose" },
58
+ { on: "manual", size: "per_moment", mode: "propose" },
59
+ ]);
60
+ // The plan reads its numbers from the pod, and files dates as proposals.
61
+ expect(plan?.goalTemplate).toContain("platform-posting-freq");
62
+ expect(plan?.goalTemplate).toContain("never apply a date yourself");
63
+ });
64
+
65
+ it("ships one paused playbook_run of Produce Content, with no subject binding", () => {
66
+ // Two automations: this weekly run, and Publish due posts (content-os.publish-due-posts.template.test.ts).
67
+ expect(template.automations?.map((a) => a.key)).toEqual(["content-os-weekly-produce", "content-os-publish-due-posts"]);
68
+ const automation = template.automations?.find((a) => a.key === "content-os-weekly-produce");
69
+ expect(automation).toMatchObject({
70
+ key: "content-os-weekly-produce",
71
+ triggerType: "cron",
72
+ status: "paused",
73
+ triggerConfig: { expression: "0 9 * * 1" },
74
+ });
75
+
76
+ const nodes = (automation?.flow.nodes ?? []) as Array<{
77
+ id?: string;
78
+ type?: string;
79
+ data?: Record<string, unknown>;
80
+ }>;
81
+ const runs = nodes.filter((n) => n.type === "playbook_run");
82
+ expect(runs).toHaveLength(1);
83
+ expect(runs[0]?.data).toMatchObject({ playbookName: "Produce Content" });
84
+ expect(runs[0]?.data).not.toHaveProperty("paramsMapping");
85
+ expect(runs[0]?.data).not.toHaveProperty("subject");
86
+ expect(runs[0]?.data).not.toHaveProperty("playbookId");
87
+
88
+ expect(automation?.flow.edges).toEqual([
89
+ { id: "e-trigger-produce", source: "trigger", target: "produce" },
90
+ ]);
91
+ });
92
+ });
93
+
94
+ /**
95
+ * Content OS is THE Content space (one space per domain), so it — not the
96
+ * superseded content-studio — must declare the video/visual kinds the browser
97
+ * Content app reads (`CONTENT_KINDS`). Without them "New video" fails on a pod
98
+ * with only Content OS: `Profile not found: content-composition`.
99
+ */
100
+ describe("Content OS owns the video / visual pipeline", () => {
101
+ const profile = (slug: string) => template.profiles.find((p) => p.slug === slug);
102
+
103
+ it("declares content-composition, content-asset and render-job (workspace scope)", () => {
104
+ for (const slug of ["content-composition", "content-asset", "render-job"]) {
105
+ const p = profile(slug);
106
+ expect(p, `content-os must declare ${slug}`).toBeDefined();
107
+ expect(p?.scope ?? "workspace", `${slug} scope`).not.toBe("shared");
108
+ }
109
+ // The fields the Content app and the composition detail read.
110
+ expect(profile("render-job")?.properties.map((d) => d.slug)).toEqual(
111
+ expect.arrayContaining(["job-status", "render-progress", "composition-ref"])
112
+ );
113
+ expect(profile("content-asset")?.properties.map((d) => d.slug)).toContain("asset-kind");
114
+ });
115
+
116
+ it("re-declares brand-template as shared (the grant) and requires brand-library (the SSOT)", () => {
117
+ expect(profile("brand-template")?.scope).toBe("shared");
118
+ expect(
119
+ (template.dependencies ?? []).some(
120
+ (d) => d.slug === "brand-library" && (d.relation ?? "require") === "require"
121
+ )
122
+ ).toBe(true);
123
+ });
124
+
125
+ it("ships the pipeline views and links", () => {
126
+ const viewScopes = (template.views ?? []).map((v) => v.scopeProfileSlug);
127
+ expect(viewScopes).toEqual(
128
+ expect.arrayContaining(["content-composition", "content-asset", "render-job"])
129
+ );
130
+ const links = (template.entityLinks ?? []).map(
131
+ (l) => `${l.sourceProfileSlug}-${l.type}->${l.targetProfileSlug}`
132
+ );
133
+ expect(links).toEqual(
134
+ expect.arrayContaining([
135
+ "render-job-renders->content-composition",
136
+ "content-asset-used_in->content-composition",
137
+ "content-asset-used_in->render-job",
138
+ "content-composition-made_for->post",
139
+ ])
140
+ );
141
+ });
142
+
143
+ it("keeps its own home: the content-home cell", () => {
144
+ expect(template.app).toMatchObject({ kind: "stored", cellKey: "content-home" });
145
+ });
146
+ });
147
+
148
+ // A stage's `subjectStatus` is written when the run ENTERS it, and the pod holds
149
+ // it only behind a HUMAN gate (a check gate judges the stage being left, so it
150
+ // passes on entry). A status that claims an outside act — the post is live, the
151
+ // issue went out — or that starts ANOTHER process must therefore sit behind a
152
+ // person's approval, or the record says it happened before anyone confirmed it
153
+ // (review M6: Repurpose fired on a post that never shipped).
154
+ describe("Content OS never writes an outside act before a person confirms it", () => {
155
+ const playbooks = (template.playbooks ?? []) as TemplatePlaybook[];
156
+ type Stage = NonNullable<TemplatePlaybook["stages"]>[number];
157
+ const bound: Array<{ playbook: string; stage: Stage }> = playbooks.flatMap((pb) =>
158
+ (pb.stages ?? [])
159
+ .filter((s) => typeof s.subjectStatus === "string")
160
+ .map((stage) => ({ playbook: pb.name, stage })),
161
+ );
162
+ // Values another playbook's activator listens for, per subject kind.
163
+ const activatedElsewhere = (owner: string, kind: string | undefined, status: string) =>
164
+ playbooks.some(
165
+ (pb) =>
166
+ pb.name !== owner &&
167
+ pb.subjectProfile?.profileSlug === kind &&
168
+ (pb.activators ?? []).some((a) => a.on === "enters_status" && a.status === status),
169
+ );
170
+ const kindOf = (name: string) => playbooks.find((p) => p.name === name)?.subjectProfile?.profileSlug;
171
+
172
+ const mustBeHuman = bound.filter(
173
+ ({ playbook, stage }) =>
174
+ stage.category === "completed" ||
175
+ activatedElsewhere(playbook, kindOf(playbook), stage.subjectStatus as string),
176
+ );
177
+
178
+ it("finds the stages it guards (non-vacuity)", () => {
179
+ expect(mustBeHuman.map(({ playbook, stage }) => `${playbook}/${stage.key}`).sort()).toEqual([
180
+ "Newsletter issue/sent",
181
+ "Produce Content/publish",
182
+ ]);
183
+ });
184
+
185
+ it("gates each of them on a person", () => {
186
+ for (const { playbook, stage } of mustBeHuman) {
187
+ expect({ at: `${playbook}/${stage.key}`, gate: stage.gate }).toEqual({
188
+ at: `${playbook}/${stage.key}`,
189
+ gate: { kind: "human" },
190
+ });
191
+ }
192
+ });
193
+
194
+ it("keeps a recorded-evidence criterion on each (URL / date)", () => {
195
+ for (const { playbook, stage } of mustBeHuman) {
196
+ const evidence = (stage.criteria ?? [])
197
+ .map((c) => (c as { check?: { kind?: string; evidenceKey?: string } }).check)
198
+ .filter((c) => c?.kind === "evidence")
199
+ .map((c) => c!.evidenceKey);
200
+ expect({ at: `${playbook}/${stage.key}`, has: evidence.length > 0 }).toEqual({
201
+ at: `${playbook}/${stage.key}`,
202
+ has: true,
203
+ });
204
+ }
205
+ });
206
+ });
207
+
208
+ // The script step (D4, 2026-10-12): a post is WRITTEN (script / outline) and a
209
+ // person approves that before anything is generated. Read from the YAML SOURCE,
210
+ // not the generated templates.ts, so the assertion sees this file's edit
211
+ // without a rebuild.
212
+ describe("Content OS scripts a post before generating it", () => {
213
+ const source = load(
214
+ readFileSync(join(dirname(fileURLToPath(import.meta.url)), "content-os.yaml"), "utf8"),
215
+ ) as WorkspaceYaml;
216
+ const post = source.profiles.find((p) => p.slug === "post");
217
+ const prop = (slug: string) => post?.properties.find((d) => d.slug === slug);
218
+ const produce = (source.playbooks ?? []).find((p) => p.name === "Produce Content") as
219
+ | TemplatePlaybook
220
+ | undefined;
221
+ const stage = (key: string) => produce?.stages?.find((s) => s.key === key);
222
+
223
+ it("puts Scripted between Briefed and In Draft, in the enum and the lifecycle", () => {
224
+ const order = ["Idea", "Briefed", "Scripted", "In Draft", "Review", "Scheduled", "Published", "Archived"];
225
+ expect(prop("post-status")?.enumValues).toEqual(order);
226
+ expect(post?.lifecycle?.options.map((o) => o.key)).toEqual(order);
227
+ expect(post?.lifecycle?.options.find((o) => o.key === "Scripted")?.category).toBe("active");
228
+ });
229
+
230
+ it("declares post-goal with the shared goal enum", () => {
231
+ expect(prop("post-goal")).toMatchObject({ inputType: "select" });
232
+ expect(prop("post-goal")?.enumValues).toEqual([
233
+ "sell",
234
+ "hot-take",
235
+ "tutorial",
236
+ "product-demo",
237
+ "announcement",
238
+ "proof",
239
+ "behind-the-scenes",
240
+ "community",
241
+ ]);
242
+ });
243
+
244
+ it("runs a script stage at Scripted, right after the brief, producing a Script document", () => {
245
+ expect(produce?.stages?.map((s) => s.key)).toEqual([
246
+ "brief",
247
+ "script",
248
+ "draft",
249
+ "review",
250
+ "schedule",
251
+ "publish",
252
+ ]);
253
+ expect(stage("script")?.subjectStatus).toBe("Scripted");
254
+ expect(stage("script")?.expectedOutputs).toEqual([
255
+ expect.objectContaining({ kind: "document", label: "Script" }),
256
+ ]);
257
+ });
258
+
259
+ it("holds the draft behind a person's approval of the script, reworking the script on reject", () => {
260
+ expect(stage("draft")?.gate).toEqual({ kind: "human" });
261
+ expect(stage("draft")?.onFail).toEqual({ toStage: "script" });
262
+ });
263
+ });
264
+
265
+ // Decision B (review 22fec5ee, 3 Nov 2026): an idea is its own kind, linked
266
+ // many-to-many to posts by `feeds`, and "Package ideas" is the door that
267
+ // proposes posts from the bank without writing them.
268
+ describe("Content OS ideas: their own kind, feeding posts", () => {
269
+ const idea = (template.profiles ?? []).find((p) => p.slug === "idea");
270
+
271
+ it("declares the idea kind with one lifecycle and no post fields", () => {
272
+ expect(idea?.lifecycle?.property).toBe("idea-status");
273
+ expect(idea?.lifecycle?.options.map((o) => `${o.key}:${o.category}`)).toEqual([
274
+ "New:open",
275
+ "Shaped:active",
276
+ "Used:done",
277
+ "Dropped:lost",
278
+ ]);
279
+ const fields = (idea?.properties ?? []).map((p) => p.slug);
280
+ expect(fields).toEqual(expect.arrayContaining(["idea-status", "content", "idea-pillar", "idea-stage", "idea-tags"]));
281
+ // An idea has no format and no date (that is what makes it not a post).
282
+ expect(fields.some((f) => f.startsWith("post-") || f === "publish-date")).toBe(false);
283
+ });
284
+
285
+ it("links an idea to the posts it feeds, many-to-many", () => {
286
+ const links = (template.entityLinks ?? []).map((l) => `${l.sourceProfileSlug}-${l.type}->${l.targetProfileSlug}`);
287
+ expect(links).toEqual(expect.arrayContaining(["idea-feeds->post", "idea-belongs_to_pillar->pillar"]));
288
+ });
289
+
290
+ it("packages ideas through a bank-wide playbook that proposes posts, never writes them", () => {
291
+ const pkg = template.playbooks?.find((p) => p.name === "Package ideas");
292
+ expect(pkg?.subjectProfile).toBeUndefined();
293
+ expect(pkg?.expectedOutputs).toEqual([expect.objectContaining({ kind: "post", relationToSubject: "none" })]);
294
+ expect(pkg?.goalTemplate).toMatch(/idea feeds post/);
295
+ expect(pkg?.goalTemplate).toMatch(/ONE synap_capture graph/);
296
+ expect(pkg?.goalTemplate).toMatch(/Never a script/);
297
+ });
298
+ });
299
+
300
+ // A post sent for another account (L9: one post per extra account, `post-copy-of`)
301
+ // enters Published like its original. Repurpose must still propose once per
302
+ // ORIGINAL: its activator leaves out a post that carries post-copy-of
303
+ // (backend#766 `unless`), and that property is one the post kind declares.
304
+ describe("Content OS Repurpose proposes once per original, never for a copy", () => {
305
+ const playbooks = (template.playbooks ?? []) as TemplatePlaybook[];
306
+ const repurpose = playbooks.find((p) => p.name === "Repurpose");
307
+
308
+ it("finds Repurpose and its Published activator (non-vacuity)", () => {
309
+ expect(repurpose?.subjectProfile?.profileSlug).toBe("post");
310
+ expect(repurpose?.activators?.filter((a) => a.on === "enters_status" && a.status === "Published")).toHaveLength(1);
311
+ });
312
+
313
+ it("leaves out a post that carries post-copy-of", () => {
314
+ const published = repurpose?.activators?.find((a) => a.on === "enters_status" && a.status === "Published");
315
+ expect(published?.unless).toEqual({ property: "post-copy-of", present: true });
316
+ });
317
+
318
+ it("names a property the post kind declares", () => {
319
+ const post = template.profiles.find((p) => p.slug === "post");
320
+ expect(post?.properties.map((d) => d.slug)).toContain("post-copy-of");
321
+ });
322
+ });
323
+
324
+ // "Write the caption" (founder, 5 Nov): Content Studio's "Write it for me" runs it on ONE post. It drafts, never
325
+ // decides: it writes the script's "## Caption draft" section, never the "## Caption" on its own; the person's yes to ONE
326
+ // owed confirm ask (or their Save in the app) makes it the caption. It binds no stage and moves no status.
327
+ describe("Content OS Write the caption drafts one post's caption, never the person's", () => {
328
+ const playbooks = (template.playbooks ?? []) as TemplatePlaybook[];
329
+ const caption = playbooks.find((p) => p.name === "Write the caption");
330
+
331
+ it("is a post's process, run by hand on one post (non-vacuity)", () => {
332
+ expect(caption?.subjectProfile?.profileSlug).toBe("post");
333
+ expect(caption?.params).toEqual([expect.objectContaining({ name: "post", type: "entity", required: true })]);
334
+ expect(caption?.goalTemplate).toContain("{post}");
335
+ expect(caption?.status).toBe("active");
336
+ });
337
+
338
+ it("writes the draft section only, never the saved caption, and never moves the status", () => {
339
+ expect(caption?.goalTemplate).toMatch(/"## Caption draft" section/);
340
+ expect(caption?.goalTemplate).toMatch(/Never write or change the "## Caption" section on\s+your own/);
341
+ // The person's approval is ONE owed confirm ask; only their yes makes the draft the caption.
342
+ expect(caption?.goalTemplate).toMatch(/ONE confirm ask on this session/);
343
+ expect(caption?.goalTemplate).toMatch(/On yes, put the draft in\s+the "## Caption" section/);
344
+ expect(caption?.goalTemplate).toMatch(/Never move the post's status/);
345
+ expect(caption?.stages ?? []).toEqual([]);
346
+ expect(caption?.activators ?? []).toEqual([]);
347
+ expect(caption?.expectedOutputs).toEqual([expect.objectContaining({ kind: "document", label: "Caption draft", relationToSubject: "none" })]);
348
+ });
349
+ });