@synap-core/workspace-templates 0.11.0 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (249) hide show
  1. package/README.md +483 -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 +420 -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 +471 -0
  54. package/capabilities/vercel.capability.json +424 -0
  55. package/dist/define-CPS0n8BV.d.ts +1331 -0
  56. package/dist/index.d.ts +189 -900
  57. package/dist/index.js +24654 -21639
  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 +41 -27
  63. package/packs/bookmark-views-pack.json +33 -0
  64. package/packs/crm-pack.json +76 -0
  65. package/packs/customer-profile.json +28 -0
  66. package/packs/deep-research.playbook.json +30 -0
  67. package/packs/event-views-pack.json +33 -0
  68. package/packs/habit-profile.json +27 -0
  69. package/packs/hr-pack.json +54 -0
  70. package/packs/invoice-profile.json +28 -0
  71. package/packs/knowledge-base-pack.json +56 -0
  72. package/packs/meeting-profile.json +29 -0
  73. package/packs/product-profile.json +30 -0
  74. package/packs/project-management-pack.json +49 -0
  75. package/packs/recipe-profile.json +29 -0
  76. package/packs/task-views-pack.json +39 -0
  77. package/retired.yaml +136 -0
  78. package/skills/app-factory-build-slice/SKILL.md +70 -0
  79. package/skills/app-factory-build-slice.catalog.json +17 -0
  80. package/skills/app-factory-design-canvas/SKILL.md +96 -0
  81. package/skills/app-factory-design-canvas.catalog.json +17 -0
  82. package/skills/app-factory-design-review/SKILL.md +99 -0
  83. package/skills/app-factory-design-review.catalog.json +18 -0
  84. package/skills/app-factory-research/SKILL.md +123 -0
  85. package/skills/app-factory-research.catalog.json +17 -0
  86. package/skills/bookmark-enrichment/SKILL.md +40 -0
  87. package/skills/carousel-format/carousel-format.skill.json +18 -0
  88. package/skills/collage-broll/collage-broll.skill.json +20 -0
  89. package/skills/content-taste/content-taste.skill.json +18 -0
  90. package/skills/creative-director/creative-director.skill.json +18 -0
  91. package/skills/design-critique/SKILL.md +135 -0
  92. package/skills/design-critique.catalog.json +12 -0
  93. package/skills/designing-with-real-components/SKILL.md +140 -0
  94. package/skills/designing-with-real-components.catalog.json +12 -0
  95. package/skills/edit-loop/edit-loop.skill.json +25 -0
  96. package/skills/facecam-edit/facecam-edit.skill.json +24 -0
  97. package/skills/mapping-the-server/SKILL.md +54 -0
  98. package/skills/math-explainer-manimgx/math-explainer-manimgx.skill.json +20 -0
  99. package/skills/motion-quality-gates/motion-quality-gates.skill.json +20 -0
  100. package/skills/open-design-edit/open-design-edit.skill.json +24 -0
  101. package/skills/reading-client-progress/SKILL.md +65 -0
  102. package/skills/routing-to-channels/SKILL.md +54 -0
  103. package/skills/stellar-scf-grants-advisor/ASSOCIATE-CHEATSHEET.md +133 -0
  104. package/skills/stellar-scf-grants-advisor/ASSOCIATE-ONBOARDING-GUIDE.md +761 -0
  105. package/skills/stellar-scf-grants-advisor/CLIENT-FAQ.md +181 -0
  106. package/skills/stellar-scf-grants-advisor/PARTNER-HANDOVER.md +130 -0
  107. package/skills/stellar-scf-grants-advisor/SKILL.md +102 -0
  108. package/skills/stellar-scf-grants-advisor/examples/README.md +43 -0
  109. package/skills/stellar-scf-grants-advisor/examples/chat-summary-templates.md +158 -0
  110. package/skills/stellar-scf-grants-advisor/examples/corpus-comparable-rows.md +136 -0
  111. package/skills/stellar-scf-grants-advisor/examples/q10-team-rewrites.md +109 -0
  112. package/skills/stellar-scf-grants-advisor/examples/q2-description-rewrites.md +88 -0
  113. package/skills/stellar-scf-grants-advisor/examples/q6-integration-rewrites.md +107 -0
  114. package/skills/stellar-scf-grants-advisor/examples/tranche-structures.md +176 -0
  115. package/skills/stellar-scf-grants-advisor/reference/01-methodology.md +96 -0
  116. package/skills/stellar-scf-grants-advisor/reference/02-scoring-framework.md +199 -0
  117. package/skills/stellar-scf-grants-advisor/reference/03-rejection-patterns.md +241 -0
  118. package/skills/stellar-scf-grants-advisor/reference/04-winner-patterns.md +120 -0
  119. package/skills/stellar-scf-grants-advisor/reference/05-comeback-patterns.md +92 -0
  120. package/skills/stellar-scf-grants-advisor/reference/06-budget-benchmarks.md +125 -0
  121. package/skills/stellar-scf-grants-advisor/reference/07-integration-list.md +147 -0
  122. package/skills/stellar-scf-grants-advisor/reference/08-abstract-fields.md +258 -0
  123. package/skills/stellar-scf-grants-advisor/reference/09-build-fields.md +259 -0
  124. package/skills/stellar-scf-grants-advisor/reference/10-corpus-winners.md +135 -0
  125. package/skills/stellar-scf-grants-advisor/reference/11-corpus-rejects.md +267 -0
  126. package/skills/stellar-scf-grants-advisor/reference/12-client-communication.md +131 -0
  127. package/skills/stellar-scf-grants-advisor/reference/13-excel-output-spec.md +199 -0
  128. package/skills/stellar-scf-grants-advisor/reference/14-worked-examples.md +160 -0
  129. package/skills/stellar-scf-grants-advisor/templates/abstract-review-builder.py +253 -0
  130. package/skills/stellar-scf-grants-advisor/templates/build-review-builder.py +213 -0
  131. package/skills/video-direction/video-direction.skill.json +20 -0
  132. package/skills/video-style-vocabulary/video-style-vocabulary.skill.json +20 -0
  133. package/skills/video-toolbox/video-toolbox.skill.json +20 -0
  134. package/src/__fixtures__/lifecycle-presets.flat-before.json +205 -0
  135. package/src/agency-os.yaml +122 -0
  136. package/src/agent-os-skill-slugs.test.ts +65 -0
  137. package/src/apify-social-reader.test.ts +317 -0
  138. package/src/base.template.test.ts +25 -19
  139. package/src/base.yaml +95 -39
  140. package/src/blockchain-ecosystem.yaml +1 -0
  141. package/src/brand-library.yaml +525 -14
  142. package/src/builder-dev-fold.template.test.ts +113 -0
  143. package/src/builder.yaml +798 -92
  144. package/src/business-model-catalog-description.test.ts +47 -0
  145. package/src/business-model-grp.template.test.ts +214 -0
  146. package/src/business-model.yaml +598 -0
  147. package/src/catalog-safety.test.ts +198 -0
  148. package/src/catalog.template-items.test.ts +75 -0
  149. package/src/catalog.ts +85 -2
  150. package/src/civic-france.yaml +102 -0
  151. package/src/concept-guard.test.ts +28 -8
  152. package/src/concept-guard.ts +2 -2
  153. package/src/content-creation.template.test.ts +52 -0
  154. package/src/content-creation.yaml +192 -0
  155. package/src/content-os.edit-loop.template.test.ts +210 -0
  156. package/src/content-os.publish-due-posts.template.test.ts +261 -0
  157. package/src/content-os.template.test.ts +322 -0
  158. package/src/content-os.video-takes.template.test.ts +338 -0
  159. package/src/content-os.work-a-take.template.test.ts +125 -0
  160. package/src/content-os.yaml +1830 -39
  161. package/src/content-studio-retired.template.test.ts +82 -0
  162. package/src/corpus-flow-validity.test.ts +4 -5
  163. package/src/crm-absorbs-outreach.template.test.ts +117 -0
  164. package/src/crm-build-plan.template.test.ts +128 -0
  165. package/src/crm-lists.template.test.ts +96 -0
  166. package/src/crm-messages-cadence.template.test.ts +164 -0
  167. package/src/crm-one-send-door.template.test.ts +65 -0
  168. package/src/crm.yaml +1456 -181
  169. package/src/decisions-wave.template.test.ts +186 -0
  170. package/src/define.ts +130 -17
  171. package/src/design-to-implementation.template.test.ts +94 -0
  172. package/src/dist-freshness.test.ts +97 -0
  173. package/src/domain-template-type.test.ts +75 -0
  174. package/src/ecosystem.yaml +27 -66
  175. package/src/enterprise-os.yaml +10 -134
  176. package/src/facecam-edit.audio-studio.test.ts +188 -0
  177. package/src/fal-generate-image.test.ts +200 -0
  178. package/src/fal-media-lab-pod-files.test.ts +180 -0
  179. package/src/finance.yaml +254 -1
  180. package/src/foundation.yaml +130 -28
  181. package/src/frame-host-modules.d.mts +6 -0
  182. package/src/frame-host-modules.mjs +58 -0
  183. package/src/generate.mjs +9 -3
  184. package/src/github-merge.test.ts +210 -0
  185. package/src/grants.yaml +57 -17
  186. package/src/hr.yaml +39 -6
  187. package/src/index.ts +36 -1
  188. package/src/intent.test.ts +161 -0
  189. package/src/internal-runbook.yaml +19 -1
  190. package/src/jev.test.ts +356 -0
  191. package/src/legal.yaml +33 -1
  192. package/src/life-os.yaml +65 -240
  193. package/src/lifecycle-coverage.test.ts +124 -0
  194. package/src/lifecycle-declarations.test.ts +252 -0
  195. package/src/lifecycle-presets.test.ts +468 -0
  196. package/src/lifecycle-presets.ts +494 -0
  197. package/src/lifecycle-stuck.test.ts +46 -0
  198. package/src/lifecycle-waits.parity.test.ts +319 -0
  199. package/src/marketing.yaml +108 -4
  200. package/src/meetings-knowledge.yaml +18 -0
  201. package/src/networking.yaml +23 -2
  202. package/src/operations.yaml +304 -4
  203. package/src/pipeline-views.template.test.ts +88 -0
  204. package/src/playbook-goal-placeholders.test.ts +57 -0
  205. package/src/postiz-contract.test.ts +134 -0
  206. package/src/presentation-declarations.test.ts +174 -0
  207. package/src/process-contract.validate.test.ts +352 -0
  208. package/src/project-management.yaml +39 -740
  209. package/src/publish-body.d.mts +29 -0
  210. package/src/publish-body.mjs +74 -0
  211. package/src/publish-body.test.ts +74 -0
  212. package/src/publish-official-lib.test.ts +176 -0
  213. package/src/replicate-rvm.test.ts +504 -0
  214. package/src/research-base.yaml +119 -20
  215. package/src/research-decision.yaml +60 -3
  216. package/src/reserved-profile-slugs.test.ts +83 -0
  217. package/src/retired-goal-kinds.test.ts +58 -0
  218. package/src/role-reference-and-step-domain.test.ts +192 -0
  219. package/src/seed-obligation.test.ts +176 -0
  220. package/src/send-message-maps.test.ts +227 -0
  221. package/src/shared-role-ssot.test.ts +42 -1
  222. package/src/ship-a-change.template.test.ts +177 -0
  223. package/src/skills.test.ts +170 -0
  224. package/src/social.yaml +85 -27
  225. package/src/space-packages.freshness.test.ts +97 -0
  226. package/src/space-packages.generated.ts +40 -0
  227. package/src/space-packages.test.ts +125 -0
  228. package/src/space-packages.ts +73 -0
  229. package/src/start.test.ts +188 -0
  230. package/src/task-intents.test.ts +293 -0
  231. package/src/templates.ts +21639 -21522
  232. package/src/types.ts +443 -60
  233. package/src/unipile-publish-post.test.ts +162 -0
  234. package/src/validate-flow.drift.test.ts +2 -3
  235. package/src/validate-flow.ts +32 -2
  236. package/src/validate-onboarding.test.ts +59 -0
  237. package/src/validate.test.ts +156 -3
  238. package/src/validate.ts +1384 -7
  239. package/src/wire-core.ts +231 -0
  240. package/src/wire.test.ts +159 -0
  241. package/src/wire.ts +107 -0
  242. package/src/__fixtures__/report-automation-flow.json +0 -393
  243. package/src/agent-fleet.yaml +0 -1645
  244. package/src/business-developer.template.test.ts +0 -351
  245. package/src/business-developer.yaml +0 -860
  246. package/src/content-studio.yaml +0 -361
  247. package/src/dev-dashboard.yaml +0 -990
  248. package/src/outreach-comms.yaml +0 -486
  249. package/src/the-arch.yaml +0 -201
@@ -2,9 +2,16 @@
2
2
  # The editorial engine — turn thinking into a published content system.
3
3
  # View-driven workspace (app: stored / entity-detail), modeled on marketing.yaml.
4
4
  #
5
- # This is the EDITORIAL half that used to be absorbed inside content-studio.yaml.
6
- # content-studio stays video/render-only; Content OS owns the publishing model:
7
- # posts, pillars, platforms, and newsletters.
5
+ # Content OS is THE Content space — one space per domain (root CLAUDE.md, "One
6
+ # space per domain"), so this ONE template carries every format: the editorial
7
+ # model (posts, pillars, platforms, newsletters) AND the video/visual pipeline
8
+ # (`content-composition`, `content-asset`, `render-job`, plus the shared
9
+ # `brand-template` grant). The browser Content app (`CONTENT_KINDS` in
10
+ # browser/.../apps/content/content-model.ts) reads all of them from this one
11
+ # space — before this template declared them, "New video" failed with
12
+ # `Profile not found: content-composition` on a pod with only Content OS.
13
+ # content-studio.yaml was SUPERSEDED (its kinds were re-declared here verbatim)
14
+ # and is retired into this template (retired.yaml, 2026-11-03).
8
15
 
9
16
  meta:
10
17
  slug: content-os
@@ -15,6 +22,34 @@ meta:
15
22
  color: "#6366F1"
16
23
  tags: [content, editorial, publishing, creator, newsletter]
17
24
  isPublic: true
25
+ # Onboarding start (meta.start): the "content" card.
26
+ start:
27
+ order: 2
28
+ label: Content
29
+ promise: Every post from idea to published, on one calendar.
30
+ suggestFor: [writer, creator, marketer, musician, photographer]
31
+ profile: post
32
+ views: [content-posts, content-calendar]
33
+ example:
34
+ - { title: "Why we left spreadsheets", values: { post-status: Idea, post-format: Article } }
35
+ - { title: "Three mistakes in our first launch", values: { post-status: Briefed, post-format: Thread } }
36
+ - { title: "Behind the scenes: build week", values: { post-status: "In Draft", post-format: "Short Video" } }
37
+ - { title: "Customer story: the Lyon bakery", values: { post-status: Review, post-format: Carousel } }
38
+ - { title: "Our pricing, explained", values: { post-status: Published, post-format: Article } }
39
+ firstSession:
40
+ title: Your content pipeline
41
+ goal: "Add the 3 to 5 posts you are working on now, each with a format and a status, and set a publish date on the next one."
42
+ starters:
43
+ - "I publish a weekly newsletter and a few social posts"
44
+ - "I make short videos for Instagram and YouTube"
45
+ - "I write articles to attract clients"
46
+ # What this space PRODUCES (homes): the purpose word, then its kinds/roles.
47
+ intent:
48
+ label: Content
49
+ produces:
50
+ - profile: post
51
+ - profile: newsletter
52
+ - profile: campaign
18
53
 
19
54
  # The editorial `post`/`newsletter` kinds REFERENCE profiles owned elsewhere:
20
55
  # • `campaign` — the marketing-campaign template's kind. It ships pod-wide as a
@@ -53,6 +88,91 @@ dependencies:
53
88
  kind: workspace
54
89
  relation: require
55
90
  reason: "Provides the campaign kind (and its SSOT property body) this workspace's posts/newsletters link to."
91
+ # Same reason as foundation above, for the video pipeline: `brand-template` is
92
+ # Brand Library's pod-wide `scope: shared` kind and is re-declared below with
93
+ # only the properties used here. Requiring brand-library installs it FIRST, so
94
+ # its FULL body is the SSOT base and our subset never takes the pod-wide seat.
95
+ # It is also what the Produce Content playbook already reads (the brand kit,
96
+ # GET /api/hub/brand/kit). Brand is a separate domain, so this is a second
97
+ # space by design, not a second Content space.
98
+ - slug: brand-library
99
+ kind: workspace
100
+ relation: require
101
+ reason: "Provides the brand kit and the shared brand-template kind compositions start from."
102
+ # Bottom-up: the skill is its OWN standalone package (skills/creative-director.skill.json),
103
+ # published before this template, and installed here by slug. Resolved in Step 0 of
104
+ # materialization — BEFORE the skills linker — so the `skills:` link below finds a
105
+ # pod-wide approved row and reports `linked` rather than `unresolved`.
106
+ - slug: creative-director
107
+ kind: skill
108
+ relation: require
109
+ reason: "The creative director routes every content ask to the brand's approved brand-template, references and assets."
110
+ # Same bottom-up shape for the taste skill (skills/content-taste/): linked
111
+ # on-demand below, for the readiness gaps the director routes to it.
112
+ - slug: content-taste
113
+ kind: skill
114
+ relation: require
115
+ reason: "Turns references into approved guidelines and the asset kit a post is made from, when the readiness check finds them missing."
116
+ - slug: carousel-format
117
+ kind: skill
118
+ relation: require
119
+ reason: "The carousel module: hook slide, one idea per slide, text density, last-slide CTA, 4:5, slot use, built as a layered composition on the post."
120
+ # The edit loop (founder D0-D2, 2026-11-02): one contract every engine
121
+ # shares (the layer-<pass> slots, studio, needs-tool-<tool>, the three
122
+ # answers), and the Open Design engine for decks and PDF documents.
123
+ - slug: edit-loop
124
+ kind: skill
125
+ relation: require
126
+ reason: "The one edit-run contract every engine shares: pass slots, the three answers, a version before every pass, work from the diff."
127
+ - slug: open-design-edit
128
+ kind: skill
129
+ relation: require
130
+ reason: "Decks, PDF documents and presentations built in Open Design on the person's Mac, filed back to the pod."
131
+ # The video toolbox (founder decision 2026-10-13, pod decision "Video toolbox:
132
+ # one tool per job"): one routing skill plus one skill per job. Same bottom-up
133
+ # shape as the skills above: each is its own package under skills/, installed
134
+ # here and linked on-demand in `skills:` below.
135
+ - slug: video-toolbox
136
+ kind: skill
137
+ relation: require
138
+ reason: "Routes every video ask to the one tool for its job: HyperFrames to create, Cap + OpenChatCut to edit footage, ManimGX for math, fal.ai to generate."
139
+ - slug: video-direction
140
+ kind: skill
141
+ relation: require
142
+ reason: "Directs a short film from brief to delivery: story, treatment, look, sound, camera, delivery."
143
+ - slug: video-style-vocabulary
144
+ kind: skill
145
+ relation: require
146
+ reason: "43 visual styles and the per-film style sheet the build follows, under the brand's palette and fonts."
147
+ - slug: motion-quality-gates
148
+ kind: skill
149
+ relation: require
150
+ reason: "The checks a HyperFrames cut passes before anyone sees it: determinism, reading time, seams, loudness, rhythm."
151
+ - slug: math-explainer-manimgx
152
+ kind: skill
153
+ relation: require
154
+ reason: "Math, equation, graph and algorithm explainer clips with ManimGX, handed to HyperFrames as media."
155
+ - slug: facecam-edit
156
+ kind: skill
157
+ relation: require
158
+ reason: "Face-camera footage to a finished cut: transcript-based cuts, captions and graphics timed to speech, QA."
159
+ - slug: collage-broll
160
+ kind: skill
161
+ relation: require
162
+ reason: "Generated b-roll through fal.ai in three approval gates (idea, still, video), saved to the pod for reuse."
163
+
164
+ # ── taskIntents — WHAT THIS WORKSPACE NEEDS TO FUNCTION ─────────────────────
165
+ # Carried over from content-studio with the video pipeline it describes:
166
+ # generate_media (the render), manage_file (zip / mp4 / stills land as pod
167
+ # files), capture_into_pod (a finished render becomes pod entities),
168
+ # search_external (asset gathering upstream of composition). A declaration, not
169
+ # a snapshot of what is installed — validated by `validateTemplate`
170
+ # (`task-intent-unknown`) and src/task-intents.test.ts.
171
+ taskIntents:
172
+ - generate_media
173
+ - manage_file
174
+ - capture_into_pod
175
+ - search_external
56
176
 
57
177
  workspace:
58
178
  name: Content OS
@@ -69,18 +189,48 @@ workspace:
69
189
  - content.pillars
70
190
  - content.calendar
71
191
  - content.newsletter
192
+ # The video pipeline's family (`CONTENT_CAPABILITIES`, @synap-core/workspace-directory),
193
+ # carried over from content-studio. `content.pipeline` is the anchor the
194
+ # browser resolves a `workspaceType: content` scope against
195
+ # (useSurfaceReadiness `capabilityForKind`). `content.templates` is not
196
+ # claimed: templates are Brand Library's `brand-template` (brand.templates).
197
+ - content.pipeline
198
+ - content.compose
199
+ - content.render
200
+ - content.assets
72
201
  sourceRoles:
73
202
  brand: consumer
74
203
  marketing: consumer
75
204
  content: provider
76
205
 
77
206
  profiles:
207
+ # `post` is the ONE pod-wide post kind (`scope: shared`, the `platform`
208
+ # pattern below): Social re-declares it to get the grant instead of owning a
209
+ # `social-post` twin (folded in by the w11.post.* manifest ops, plan D2).
210
+ # Shares the DEFINITION, not the entities — no `entityScope: pod`.
78
211
  - slug: post
79
212
  displayName: Post
80
213
  icon: file-edit
81
214
  color: "#6366F1"
82
215
  description: "Your atomic content unit. One idea, multiple platform formats — tracked from spark to publication."
83
- scope: WORKSPACE
216
+ scope: shared
217
+ # The ONE lifecycle (work model L1): every option's category.
218
+ lifecycle:
219
+ property: post-status
220
+ options:
221
+ - { key: Idea, category: open }
222
+ - { key: Briefed, category: active }
223
+ # Added 2026-10-12 (D4): the written version (script / outline) exists
224
+ # and waits for a person's approval before anything is generated.
225
+ - { key: Scripted, category: active }
226
+ - { key: "In Draft", category: active }
227
+ - { key: Review, category: active }
228
+ - { key: Scheduled, category: active }
229
+ - { key: Published, category: done }
230
+ - { key: Archived, category: lost, humanOnly: true }
231
+ # How it renders (entity rendering, decision 8): @synap-core/types/presentation.
232
+ presentation:
233
+ family: work
84
234
  properties:
85
235
  - slug: post-status
86
236
  label: Status
@@ -90,6 +240,7 @@ profiles:
90
240
  [
91
241
  "Idea",
92
242
  "Briefed",
243
+ "Scripted",
93
244
  "In Draft",
94
245
  "Review",
95
246
  "Scheduled",
@@ -113,6 +264,24 @@ profiles:
113
264
  "Podcast Episode",
114
265
  "Other",
115
266
  ]
267
+ # What the post is FOR — the axis that picks its narrative frame (an
268
+ # approved `content-guideline` of type=frame for this goal, Brand
269
+ # Library). Same enum as `guideline-goal`; never rename a value.
270
+ - slug: post-goal
271
+ label: Goal
272
+ valueType: string
273
+ inputType: select
274
+ enumValues:
275
+ [
276
+ "sell",
277
+ "hot-take",
278
+ "tutorial",
279
+ "product-demo",
280
+ "announcement",
281
+ "proof",
282
+ "behind-the-scenes",
283
+ "community",
284
+ ]
116
285
  - slug: post-platform
117
286
  label: Platform
118
287
  valueType: string
@@ -225,12 +394,177 @@ profiles:
225
394
  valueType: entity_id
226
395
  targetProfileSlug: post
227
396
  inputType: entity_select
397
+ # The person's own words, verbatim (B7). Stored by capture before it was
398
+ # declared; declared so it can be queried and validated.
399
+ - slug: content
400
+ label: Original words
401
+ description: "The person's own words, verbatim, in whatever language they were written."
402
+ valueType: string
403
+ inputType: textarea
228
404
  - slug: post-url
229
405
  label: Published URL
230
406
  valueType: string
231
407
  inputType: text
232
408
  placeholder: "https://..."
233
409
  constraints: { format: url }
410
+ # The publishing ACCOUNT (a `platform` entity) the post goes out on —
411
+ # distinct from `post-platform`, the network label above.
412
+ - slug: post-account
413
+ label: Account
414
+ valueType: entity_id
415
+ targetProfileSlug: platform
416
+ inputType: entity_select
417
+ # ── Publish outcome (plan D12) ─────────────────────────────────────────
418
+ # `failed` is what happened to a publish ATTEMPT, not a lifecycle stage:
419
+ # a post whose publish failed is still Scheduled (or wherever it was).
420
+ # `queued`: approved and armed, Publish due posts sends it on its day;
421
+ # `sending`: claimed by the send (the automation, or Content Studio's
422
+ # "Post now"), so it is never sent twice (decision A, 3 Nov);
423
+ # `waiting`: a repost armed at approval that waits for its original to
424
+ # go out (Publish due posts queues it then, with the original's id).
425
+ - slug: publish-outcome
426
+ label: Publish outcome
427
+ valueType: string
428
+ inputType: select
429
+ enumValues: [succeeded, failed, queued, sending, waiting]
430
+ - slug: publish-error
431
+ label: Publish error
432
+ valueType: string
433
+ inputType: textarea
434
+ - slug: published-at
435
+ label: Published At
436
+ valueType: date
437
+ inputType: date
438
+ # ── What goes out, frozen when the post is approved (decision A, 3 Nov) ──
439
+ # Approving a post (Review → Scheduled) freezes what "Publish due posts"
440
+ # sends on its day, so the automation never reads documents: the words,
441
+ # the Unipile account, the pictures, and the moment it may go.
442
+ - slug: publish-text
443
+ label: Words to send
444
+ description: "The approved caption or text, frozen when the post was approved. Publish due posts sends exactly this."
445
+ valueType: string
446
+ inputType: textarea
447
+ - slug: publish-account-id
448
+ label: Unipile account ID
449
+ description: "The Unipile account it goes out on, copied from its platform's unipile-account-id when it was approved."
450
+ valueType: string
451
+ inputType: text
452
+ - slug: publish-media
453
+ label: Files to send
454
+ description: "The pictures or video sent with it, as pod file ids in order (at most 10), frozen when it was approved."
455
+ valueType: array
456
+ # Epoch milliseconds: the automation's query compares properties
457
+ # numerically only, so the moment is a number (09:00 Europe/Paris on the
458
+ # publish date when Content Studio arms it).
459
+ - slug: publish-at-ms
460
+ label: Sends after (ms)
461
+ description: "The moment Publish due posts may send it, in milliseconds since 1970 (UTC)."
462
+ valueType: number
463
+ inputType: number
464
+ # ── Where it goes (accounts and destinations, founder 4 Nov, L9) ──────
465
+ # A post goes out on ONE account (`post-account`) and may also go out on
466
+ # others: the same post sent again from each account in `post-copy-to`,
467
+ # or a native repost of it from each account in `post-reshare-on`
468
+ # (LinkedIn only: Unipile's quoted_post_id; Instagram has no repost).
469
+ # Approving the post writes one post per extra account (`post-copy-of`
470
+ # pointing back here, `post-send-mode` copy | reshare), each with its own
471
+ # frozen send and its own outcome, so Publish due posts still sends one
472
+ # post per run and a failed repost never hides a sent original.
473
+ - slug: post-copy-to
474
+ label: Also posted on
475
+ description: "The other accounts (platform ids) that get the same post, sent again from each."
476
+ valueType: array
477
+ - slug: post-reshare-on
478
+ label: Reposted by
479
+ description: "The other accounts (platform ids) that repost it once it is out (LinkedIn)."
480
+ valueType: array
481
+ - slug: post-copy-of
482
+ label: Sent for
483
+ description: "The post this one sends again or reposts on another account."
484
+ valueType: entity_id
485
+ targetProfileSlug: post
486
+ inputType: entity_select
487
+ - slug: post-send-mode
488
+ label: Sent as
489
+ valueType: string
490
+ inputType: select
491
+ enumValues: [copy, reshare]
492
+ # Frozen with the rest at approval, for a LinkedIn company page: the
493
+ # page's id (its platform's linkedin-organization-id), sent as post_as.
494
+ - slug: publish-as-organization
495
+ label: Posts as page
496
+ description: "The LinkedIn company page it is posted as (Unipile post_as), frozen when it was approved."
497
+ valueType: string
498
+ inputType: text
499
+ # A repost's original, written when the original went out: Unipile's id
500
+ # of that post, sent as quoted_post_id.
501
+ - slug: publish-quote-id
502
+ label: Reposts post
503
+ description: "Unipile's id of the post this repost shares, written when the original went out."
504
+ valueType: string
505
+ inputType: text
506
+ # Unipile's id of this post once it is out (what a repost of it quotes).
507
+ - slug: publish-post-id
508
+ label: Unipile post ID
509
+ description: "Unipile's id of this post, written when it went out."
510
+ valueType: string
511
+ inputType: text
512
+ # ── Postiz (the `postiz` capability) ─────────────────────────────────
513
+ # The post's id in the person's Postiz, recorded when it is queued there
514
+ # (postiz_schedule_post returns it). Postiz's webhook finds the post by it.
515
+ - slug: postiz-post-id
516
+ label: Postiz post ID
517
+ valueType: string
518
+ inputType: text
519
+
520
+ # An idea is its OWN kind, not a post at Idea (founder decision B, 3 Nov
521
+ # 2026, review 22fec5ee): it has no format and no date, and "one idea feeds
522
+ # several posts, several ideas feed one post" is a relation, `idea --feeds-->
523
+ # post` (entityLinks below). Packaging ideas into posts is the "Package ideas"
524
+ # playbook. The posts at Idea that predate this kind convert through the
525
+ # backend conversions manifest, never by hand.
526
+ - slug: idea
527
+ displayName: Idea
528
+ icon: lightbulb
529
+ color: "#F59E0B"
530
+ description: "A spark worth keeping: a claim, a story or a question, in your own words. It feeds one or more posts."
531
+ # The ONE lifecycle (work model L1). Used = it feeds a post. No playbook
532
+ # binds this lifecycle, so no option is humanOnly (an agent's move into
533
+ # done or lost proposes anyway).
534
+ lifecycle:
535
+ property: idea-status
536
+ options:
537
+ - { key: New, category: open }
538
+ - { key: Shaped, category: active }
539
+ - { key: Used, category: done }
540
+ - { key: Dropped, category: lost, tone: textMuted }
541
+ properties:
542
+ - slug: idea-status
543
+ label: Status
544
+ valueType: string
545
+ inputType: select
546
+ enumValues: ["New", "Shaped", "Used", "Dropped"]
547
+ constraints: { defaultValue: New }
548
+ # The person's own words, verbatim (same field as on `post`).
549
+ - slug: content
550
+ label: Original words
551
+ description: "The person's own words, verbatim, in whatever language they were written."
552
+ valueType: string
553
+ inputType: textarea
554
+ - slug: idea-pillar
555
+ label: Pillar
556
+ valueType: entity_id
557
+ targetProfileSlug: pillar
558
+ inputType: entity_select
559
+ - slug: idea-stage
560
+ label: Journey Stage
561
+ valueType: entity_id
562
+ targetProfileSlug: journey-stage
563
+ inputType: entity_select
564
+ # Where it came from (`source:dump`, `source:interview`, …), as on `post-tags`.
565
+ - slug: idea-tags
566
+ label: Tags
567
+ valueType: array
234
568
 
235
569
  - slug: pillar
236
570
  displayName: Pillar
@@ -495,6 +829,42 @@ profiles:
495
829
  - slug: platform-tags
496
830
  label: Tags
497
831
  valueType: array
832
+ # ── Postiz channel (the `postiz` capability) ─────────────────────────
833
+ # Which Postiz channel this account is, both read from
834
+ # postiz_list_integrations: its id, and its provider (the `identifier`
835
+ # a post's `settings.__type` names: x, linkedin, instagram…). A post
836
+ # queued on this account passes both to postiz_schedule_post.
837
+ - slug: postiz-integration-id
838
+ label: Postiz channel ID
839
+ valueType: string
840
+ inputType: text
841
+ - slug: postiz-provider
842
+ label: Postiz provider
843
+ valueType: string
844
+ inputType: text
845
+ # The Unipile account this platform posts from (unipile_list_accounts
846
+ # `id`, e.g. acc_…): copied onto a post as publish-account-id when it is
847
+ # approved, so Publish due posts sends it from this account.
848
+ - slug: unipile-account-id
849
+ label: Unipile account ID
850
+ valueType: string
851
+ inputType: text
852
+ # Whose account it is (founder, 4 Nov): one row per account, so a
853
+ # network can hold a personal and a company account side by side. A new
854
+ # post goes to the personal account of its network by default.
855
+ - slug: account-owner
856
+ label: Owner
857
+ valueType: string
858
+ inputType: select
859
+ enumValues: [Personal, Company]
860
+ # A LinkedIn company page posts through the personal account that
861
+ # administers it: same unipile-account-id, plus the page's id here
862
+ # (linkedin_list_company_pages), copied onto a post as
863
+ # publish-as-organization and sent as Unipile's post_as.
864
+ - slug: linkedin-organization-id
865
+ label: LinkedIn page ID
866
+ valueType: string
867
+ inputType: text
498
868
 
499
869
  - slug: newsletter
500
870
  displayName: Newsletter
@@ -502,6 +872,16 @@ profiles:
502
872
  color: "#10B981"
503
873
  description: "A newsletter issue — your direct channel to an owned audience. No algorithm, no platform fee, no intermediary."
504
874
  scope: WORKSPACE
875
+ # The ONE lifecycle (work model L1): every option's category.
876
+ lifecycle:
877
+ property: newsletter-status
878
+ options:
879
+ - { key: Idea, category: open }
880
+ - { key: Drafting, category: active }
881
+ - { key: Editing, category: active }
882
+ - { key: Scheduled, category: active }
883
+ - { key: Sent, category: done }
884
+ - { key: Archived, category: lost, humanOnly: true }
505
885
  properties:
506
886
  - slug: newsletter-status
507
887
  label: Status
@@ -578,6 +958,15 @@ profiles:
578
958
  color: pink
579
959
  description: "A marketing campaign — email, social, event, content, or partnership"
580
960
  semanticSlug: marketing.campaign
961
+ # The ONE lifecycle (work model L1): every option's category.
962
+ lifecycle:
963
+ property: campaign-status
964
+ options:
965
+ - { key: planning, category: open }
966
+ - { key: active, category: active }
967
+ - { key: paused, category: paused, humanOnly: true }
968
+ - { key: completed, category: done }
969
+ - { key: cancelled, category: lost, humanOnly: true }
581
970
  properties:
582
971
  - slug: campaign-status
583
972
  label: Status
@@ -627,7 +1016,7 @@ profiles:
627
1016
  # `campaign-channel` (the delivery medium). scoping/outreach/live have no
628
1017
  # channel equivalent, so folding them into `campaign-channel` would lose
629
1018
  # them — they get their own def. Kept verbatim across the three campaign
630
- # declarations (business-developer / marketing / here) so there is no drift.
1019
+ # declarations (crm / marketing / here) so there is no drift.
631
1020
  - slug: campaign-type
632
1021
  label: Type
633
1022
  valueType: string
@@ -793,6 +1182,30 @@ profiles:
793
1182
  - usage-based
794
1183
  - free
795
1184
  - other
1185
+ # "Starting at" list price. The deal/contract stays authoritative for what
1186
+ # a buyer actually pays; these three only answer "what do we advertise".
1187
+ - slug: offer-list-price
1188
+ label: Starting at
1189
+ valueType: number
1190
+ inputType: number
1191
+ placeholder: "List price, before any negotiation"
1192
+ - slug: offer-currency
1193
+ label: Currency
1194
+ valueType: string
1195
+ inputType: select
1196
+ enumValues:
1197
+ - EUR
1198
+ - USD
1199
+ - GBP
1200
+ - CHF
1201
+ constraints: { defaultValue: EUR }
1202
+ - slug: offer-billing
1203
+ label: Billing
1204
+ valueType: string
1205
+ inputType: select
1206
+ enumValues:
1207
+ - one-off
1208
+ - monthly
796
1209
  - slug: offer-audience
797
1210
  label: Audience
798
1211
  valueType: entity_id
@@ -804,6 +1217,334 @@ profiles:
804
1217
  targetProfileSlug: positioning
805
1218
  inputType: entity_select
806
1219
 
1220
+ # ── The video / visual pipeline (moved from content-studio.yaml) ──────────
1221
+ # HyperFrames-first: a composition IS a HyperFrames project — brief +
1222
+ # storyboard documents, the project zip, the rendered MP4 and its key-frame
1223
+ # stills, and the hash of the brand kit it was made with. Remotion is RETIRED
1224
+ # (Content x Brand plan, 2026-10-05); live rows are retired through
1225
+ # conversions/manifest.ts (`w11.content.*`), never by dropping a field here
1226
+ # (the reconciler is ADD-ONLY).
1227
+ #
1228
+ # SCOPE: workspace (omitted), exactly as content-studio declared them. Only
1229
+ # the ONE Content space uses these kinds, and `shared` would bind the whole
1230
+ # corpus to this body forever (the shared-kind SSOT rule) — the one-way door
1231
+ # the `journey-stage` note above declines for the same reason. Bodies are
1232
+ # VERBATIM from content-studio.yaml so a pod that ever installed it converges
1233
+ # on one body (same-actor reuse in resolve-profile-for-apply.ts), no drift.
1234
+ # `recipe-ref` / `composition-ref` stay free text verbatim: a pod's live rows
1235
+ # carry them, and `composition-ref` is what the Content app joins renders on.
1236
+ # `recipe-ref` was `pipeline-ref` until 2026-10-11 ("pipeline" is now only the
1237
+ # view); live rows move via conversions `w12.content.pipeline-ref-to-recipe-ref`.
1238
+ - slug: content-composition
1239
+ displayName: Composition
1240
+ icon: clapperboard
1241
+ color: blue
1242
+ description: "A HyperFrames project: brief + storyboard, the project zip, the rendered MP4, its key-frame stills, and the brand kit it used."
1243
+ semanticSlug: content.composition
1244
+ # How it renders (entity rendering, decision 8): @synap-core/types/presentation.
1245
+ presentation:
1246
+ family: document
1247
+ properties:
1248
+ - slug: composition-id
1249
+ label: Composition ID
1250
+ valueType: string
1251
+ inputType: text
1252
+ description: Internal id for this composition record.
1253
+ # HyperFrames renders video; `layered` is a carousel / image built as a
1254
+ # layered composition in the pod, `open-design` a deck or PDF document
1255
+ # built in Open Design (founder D1, 2 Nov; skills carousel-format and
1256
+ # open-design-edit). `remotion` / `hybrid` are retired (reconcile is
1257
+ # add-only — live enums keep them until retired by hand).
1258
+ - slug: render-engine
1259
+ label: Render engine
1260
+ valueType: string
1261
+ inputType: select
1262
+ enumValues: [hyperframes, layered, open-design]
1263
+ constraints: { defaultValue: hyperframes }
1264
+ - slug: aspect-ratio
1265
+ label: Aspect Ratio
1266
+ valueType: string
1267
+ inputType: select
1268
+ enumValues: ["9:16", "16:9", "1:1", "4:5"]
1269
+ constraints: { defaultValue: "16:9" }
1270
+ - slug: duration-seconds
1271
+ label: Duration (seconds)
1272
+ valueType: number
1273
+ inputType: number
1274
+ # The HyperFrames workflow's own documents (pod document ids, the same
1275
+ # spelling Brand Library uses for `asset-document-id`).
1276
+ - slug: brief-document-id
1277
+ label: Brief
1278
+ valueType: string
1279
+ inputType: text
1280
+ description: Document id of the creative brief.
1281
+ - slug: storyboard-document-id
1282
+ label: Storyboard
1283
+ valueType: string
1284
+ inputType: text
1285
+ description: Document id of the storyboard / script.
1286
+ # Outputs are pod FILES (uploaded through the one upload door), linked
1287
+ # so each is a door to its own page.
1288
+ - slug: project-zip
1289
+ label: Project (zip)
1290
+ valueType: entity_id
1291
+ targetProfileSlug: file
1292
+ inputType: entity_select
1293
+ description: The HyperFrames project folder, zipped — "edit as code".
1294
+ - slug: render-mp4
1295
+ label: Render (MP4)
1296
+ valueType: entity_id
1297
+ targetProfileSlug: file
1298
+ inputType: entity_select
1299
+ description: The rendered video.
1300
+ - slug: key-frame-stills
1301
+ label: Key-frame stills
1302
+ valueType: array
1303
+ description: File entity ids of key-frame stills — what relay approves from.
1304
+ - slug: brand-kit-hash
1305
+ label: Brand kit hash
1306
+ valueType: string
1307
+ inputType: text
1308
+ description: Content hash of the brand kit export this was made with (GET /api/hub/brand/kit). A different current hash = stale brand.
1309
+ - slug: hyperframes-meta
1310
+ label: HyperFrames JSON
1311
+ valueType: string
1312
+ inputType: textarea
1313
+ - slug: composition-template
1314
+ label: Template
1315
+ valueType: entity_id
1316
+ targetProfileSlug: brand-template
1317
+ inputType: entity_select
1318
+ description: The reusable brand-template shell this started from.
1319
+ - slug: recipe-ref
1320
+ label: Recipe
1321
+ valueType: string
1322
+ inputType: text
1323
+
1324
+ - slug: content-asset
1325
+ displayName: Asset
1326
+ icon: image
1327
+ color: green
1328
+ description: "Media asset: image, video clip, audio track, font, or script."
1329
+ semanticSlug: content.asset
1330
+ # How it renders (entity rendering, decision 8): @synap-core/types/presentation.
1331
+ presentation:
1332
+ family: media
1333
+ media:
1334
+ source: { property: document-id }
1335
+ properties:
1336
+ # `recording` (D1, 29 Oct): a TAKE recorded in Content Studio's booth, or
1337
+ # a PIECE cut from one into its own new file (D2). A piece is told apart
1338
+ # by `source-take`; one Kind keeps one player and one transcript door.
1339
+ - slug: asset-kind
1340
+ label: Kind
1341
+ valueType: string
1342
+ inputType: select
1343
+ enumValues: [image, video, audio, font, script, recording, other]
1344
+ - slug: source-kind
1345
+ label: Source
1346
+ valueType: string
1347
+ inputType: select
1348
+ enumValues: [uploaded, brand, generated]
1349
+ constraints: { defaultValue: uploaded }
1350
+ - slug: source-url
1351
+ label: URL
1352
+ valueType: string
1353
+ inputType: url
1354
+ - slug: brand-asset-ref
1355
+ label: Brand Asset Ref
1356
+ valueType: string
1357
+ inputType: text
1358
+ - slug: file-id
1359
+ label: File ID
1360
+ valueType: string
1361
+ inputType: text
1362
+ - slug: document-id
1363
+ label: Document ID
1364
+ valueType: string
1365
+ inputType: text
1366
+ # ── A recording: a take, or a piece cut from one (D1, D2, 29 Oct) ──────
1367
+ # `file-id` is the raw take's file, or the piece's OWN new file. The raw
1368
+ # take is never overwritten. The facts come from the device that
1369
+ # recorded it (a browser WebM carries no duration), else from ffprobe.
1370
+ - slug: duration-seconds
1371
+ label: Duration (seconds)
1372
+ valueType: number
1373
+ inputType: number
1374
+ - slug: frame-width
1375
+ label: Width (px)
1376
+ valueType: number
1377
+ inputType: number
1378
+ - slug: frame-height
1379
+ label: Height (px)
1380
+ valueType: number
1381
+ inputType: number
1382
+ - slug: frame-rate
1383
+ label: Frame rate (fps)
1384
+ valueType: number
1385
+ inputType: number
1386
+ - slug: recorded-at
1387
+ label: Recorded at
1388
+ valueType: date
1389
+ inputType: date
1390
+ - slug: poster-file-id
1391
+ label: Poster
1392
+ valueType: string
1393
+ inputType: text
1394
+ description: File id of one still frame of the recording.
1395
+ # The take's own facts. The week is derived from `recorded-at`.
1396
+ - slug: take-kind
1397
+ label: Take
1398
+ valueType: string
1399
+ inputType: select
1400
+ enumValues: [answer, free]
1401
+ description: "answer: it answers an interview question; free: a free take."
1402
+ - slug: take-topic
1403
+ label: Question or topic
1404
+ valueType: string
1405
+ inputType: text
1406
+ description: The question the take answers, or the free take's topic.
1407
+ - slug: take-look
1408
+ label: Look (JSON)
1409
+ valueType: string
1410
+ inputType: textarea
1411
+ description: 'The look picked in the booth before recording, the JSON Produce Content takes as its look param: {"background": …, "colour": …}.'
1412
+ # A piece's lineage (G12). Seconds are in the TAKE's time.
1413
+ - slug: source-take
1414
+ label: Cut from take
1415
+ valueType: entity_id
1416
+ targetProfileSlug: content-asset
1417
+ inputType: entity_select
1418
+ - slug: source-start
1419
+ label: Starts at (s)
1420
+ valueType: number
1421
+ inputType: number
1422
+ - slug: source-end
1423
+ label: Ends at (s)
1424
+ valueType: number
1425
+ inputType: number
1426
+ - slug: piece-hook
1427
+ label: Hook
1428
+ valueType: string
1429
+ inputType: text
1430
+ description: The piece's opening line, in the person's exact words.
1431
+ - slug: piece-format
1432
+ label: Suggested format
1433
+ valueType: string
1434
+ inputType: select
1435
+ enumValues: ["Short Video", "Long Video"]
1436
+ # A re-cut is a NEW file: `file-id` moves to it and the old file id is
1437
+ # appended here, oldest first. Nothing is deleted.
1438
+ - slug: earlier-file-ids
1439
+ label: Earlier cuts
1440
+ valueType: array
1441
+ description: File ids of this piece's earlier cuts, oldest first.
1442
+
1443
+ - slug: render-job
1444
+ displayName: Render Job
1445
+ icon: play
1446
+ color: amber
1447
+ description: "One render run of a composition — on the user's own agent (HyperFrames, local) — queued, rendering, done, or failed."
1448
+ semanticSlug: content.render
1449
+ # How it renders (entity rendering, decision 8): @synap-core/types/presentation.
1450
+ presentation:
1451
+ family: media
1452
+ media:
1453
+ source: { entityRef: output-file }
1454
+ properties:
1455
+ - slug: job-status
1456
+ label: Status
1457
+ valueType: string
1458
+ inputType: select
1459
+ enumValues: [queued, rendering, done, failed]
1460
+ constraints: { defaultValue: queued }
1461
+ - slug: render-progress
1462
+ label: Progress (%)
1463
+ valueType: number
1464
+ inputType: number
1465
+ description: 0-100 while rendering, when the agent reports it.
1466
+ - slug: output-format
1467
+ label: Format
1468
+ valueType: string
1469
+ inputType: select
1470
+ enumValues: [mp4, gif, webm, mp3, pdf, png, pptx]
1471
+ constraints: { defaultValue: mp4 }
1472
+ - slug: output-file
1473
+ label: Output
1474
+ valueType: entity_id
1475
+ targetProfileSlug: file
1476
+ inputType: entity_select
1477
+ - slug: output-url
1478
+ label: Output URL
1479
+ valueType: string
1480
+ inputType: url
1481
+ - slug: render-duration-ms
1482
+ label: Render Duration (ms)
1483
+ valueType: number
1484
+ inputType: number
1485
+ - slug: error-message
1486
+ label: Error
1487
+ valueType: string
1488
+ inputType: textarea
1489
+ - slug: composition-ref
1490
+ label: Composition
1491
+ valueType: string
1492
+ inputType: text
1493
+
1494
+ # ── brand-template (shared SSOT is brand-library.yaml) ──────────────────
1495
+ # RE-DECLARED, not owned — and not twinned: `scope: shared` dedups to Brand
1496
+ # Library's ONE pod-wide row (installed first by the `require: brand-library`
1497
+ # dependency). The declaration is what writes THIS workspace's
1498
+ # `profile_workspace_access` grant; without it `composition-template`'s
1499
+ # `targetProfileSlug: brand-template` resolves to a NULL target (see the meta
1500
+ # header). Only the properties this workspace uses are re-declared (as the
1501
+ # retired content-studio did); bodies must match brand-library's (guarded
1502
+ # by shared-role-ssot.test.ts).
1503
+ - slug: brand-template
1504
+ displayName: Brand Template
1505
+ icon: layout-template
1506
+ color: pink
1507
+ description: Reusable output template for artboards, decks, carousels, banners, documents, video, or generated HTML.
1508
+ semanticSlug: brand.template
1509
+ scope: shared
1510
+ # MIRROR of brand-library's lifecycle — every declaration of a shared kind's
1511
+ # lifecycle must be identical (validate: lifecycle-diverges).
1512
+ lifecycle:
1513
+ property: template-status
1514
+ options:
1515
+ - { key: draft, category: open }
1516
+ - { key: approved, category: done }
1517
+ - { key: deprecated, category: lost }
1518
+ # How it renders (entity rendering, decision 8): @synap-core/types/presentation.
1519
+ presentation:
1520
+ family: document
1521
+ properties:
1522
+ - slug: template-kind
1523
+ label: Kind
1524
+ valueType: string
1525
+ inputType: select
1526
+ enumValues:
1527
+ [artboard, deck, carousel, banner, document, html, social, video, other]
1528
+ - slug: template-status
1529
+ label: Status
1530
+ valueType: string
1531
+ inputType: select
1532
+ enumValues: [draft, approved, deprecated]
1533
+ constraints: { defaultValue: draft }
1534
+ # `layered` = a carousel / image built as a layered composition in the
1535
+ # pod, `open-design` = a deck or PDF document built in Open Design: the
1536
+ # words content-os `render-engine` uses (static edit loop, founder D1,
1537
+ # 2 Nov 2026). Additive only: the reconciler never drops a value.
1538
+ - slug: template-format
1539
+ label: Format
1540
+ valueType: string
1541
+ inputType: select
1542
+ enumValues: [tldraw, html, json, markdown, image, mixed, hyperframes, layered, open-design]
1543
+ - slug: template-config
1544
+ label: Config JSON
1545
+ valueType: string
1546
+ inputType: textarea
1547
+
807
1548
  # CURATED views — the editorial spine
808
1549
  views:
809
1550
  - name: Posts
@@ -886,11 +1627,34 @@ views:
886
1627
  type: list
887
1628
  scopeProfileSlug: series
888
1629
  description: The curriculums you're running — each an ordered arc through one pillar.
1630
+ # The video / visual pipeline (moved from content-studio.yaml). Not in the
1631
+ # sidebar: the Content home (`content-home`) is where videos are made and
1632
+ # read; these are the raw collections behind it. No Templates view — the
1633
+ # templates live in Brand Library's own Templates view (one owner).
1634
+ - name: Compositions
1635
+ slug: content-compositions
1636
+ type: grid
1637
+ scopeProfileSlug: content-composition
1638
+ description: Video compositions ready for rendering.
1639
+ - name: Assets
1640
+ slug: content-assets
1641
+ type: grid
1642
+ scopeProfileSlug: content-asset
1643
+ description: Media assets uploaded or generated.
1644
+ - name: Render Jobs
1645
+ slug: render-jobs
1646
+ type: table
1647
+ scopeProfileSlug: render-job
1648
+ description: Render queue and history.
889
1649
 
890
- # App reference — view-driven, no custom app (mirrors marketing.yaml)
1650
+ # The workspace HOME is THE Content surface (plan W5, founder D1 + D11 — the
1651
+ # CRM precedent): the `content-home` cell, the lens page at this space's scope
1652
+ # (Needs you · Happening · Produced · Data with the coverage map). The same
1653
+ # cell is content-studio's home, so the two templates share ONE surface.
891
1654
  app:
892
1655
  kind: stored
893
- cellKey: entity-detail
1656
+ cellKey: content-home
1657
+ title: Content
894
1658
 
895
1659
  # Layout config for browser sidebar
896
1660
  layout:
@@ -929,8 +1693,8 @@ layout:
929
1693
  label: Newsletters
930
1694
  icon: mail
931
1695
 
932
- # Bento dashboard = the STUDIO HOME (the browser landing surface — `defaultView`
933
- # is inert in browser/, so the workspace opens on this bento, not on a view).
1696
+ # Bento dashboard — the space's editable DASHBOARD (pinned `dashboard`). The
1697
+ # landing surface is the `content-home` cell above (app:), not this bento.
934
1698
  # Ordered so a FRESH workspace still says something: the Coverage grid leads
935
1699
  # (the "where am I thin" read — a custom cell, `content-coverage-map`, since a
936
1700
  # generic matrix can't render an entity_id×entity_id grid's empty cells), then
@@ -1004,58 +1768,1027 @@ entityLinks:
1004
1768
  targetProfileSlug: post
1005
1769
  type: derived_from
1006
1770
  label: Repurposed From
1771
+ # Many-to-many: one idea feeds several posts, several ideas feed one post
1772
+ # (decision B). The "Package ideas" playbook writes it.
1773
+ - sourceProfileSlug: idea
1774
+ targetProfileSlug: post
1775
+ type: feeds
1776
+ label: Feeds
1777
+ - sourceProfileSlug: idea
1778
+ targetProfileSlug: pillar
1779
+ type: belongs_to_pillar
1780
+ label: Pillar
1781
+ # The video pipeline edges (moved from content-studio.yaml). content-studio's
1782
+ # `made_with_recipe` edge is NOT carried: its recipe kind was folded into
1783
+ # Brand Library's `brand-template` (decision D2, 2026-10-12), which the
1784
+ # composition already names through `composition-template`. The `post` is
1785
+ # this space's unit of work, and Produce Content already says to "record a
1786
+ # content-composition linked to this post" — so the composition points at its
1787
+ # post instead.
1788
+ - sourceProfileSlug: content-composition
1789
+ targetProfileSlug: post
1790
+ type: made_for
1791
+ label: Post
1792
+ - sourceProfileSlug: render-job
1793
+ targetProfileSlug: content-composition
1794
+ type: renders
1795
+ label: Composition
1796
+ - sourceProfileSlug: content-asset
1797
+ targetProfileSlug: render-job
1798
+ type: used_in
1799
+ label: Render Job
1800
+ - sourceProfileSlug: content-asset
1801
+ targetProfileSlug: content-composition
1802
+ type: used_in
1803
+ label: Composition
1007
1804
 
1008
1805
  # Playbooks — session templates seeded with the workspace.
1009
- # `subjectProfile` binds this playbook to the `post` kind so the Capture → Session
1010
- # matcher (playbooks.subject_profile->>'profileSlug') can offer it when a post is
1011
- # captured. The captured post is passed as the `post` entity param and substituted
1012
- # into the goalTemplate; the agent carries it from raw idea to a scheduled draft.
1806
+ # `subjectProfile` binds a playbook to a kind so the Capture → Session matcher
1807
+ # (playbooks.subject_profile->>'profileSlug') can offer it when one is captured.
1808
+ #
1809
+ # THE PROCESS CONTRACT (lifecycle-bound playbooks). `subjectProfile.statusProperty`
1810
+ # names the kind's lifecycle select (`post-status`); each stage's `subjectStatus`
1811
+ # is the value that stage IS — advancing into the stage writes it to the subject
1812
+ # (governed), and the subject entering it advances an open run to the stage.
1813
+ # `humanOnlyStatuses` lists the values no stage owns because a person sets them.
1814
+ # `activators` are DECLARATIONS the pod compiles into governed propose-mode rules
1815
+ # at install — never hand-written `automations:`. `relationToSubject` is the edge
1816
+ # written output → subject when a slot is satisfied. Validated by validate.ts
1817
+ # (`checkProcessContract`) and lifecycle-coverage.test.ts.
1013
1818
  playbooks:
1014
1819
  - name: Produce Content
1015
- description: Take a captured post idea and carry it from spark to a scheduled, platform-ready draft.
1820
+ # ONE playbook for every format (plan W3.3): text / visual / video are
1821
+ # BRANCHES of it, not sibling playbooks — never a second top-level
1822
+ # "Produce video". Its only home is this template (decision D6: the
1823
+ # content-idea-to-production capability no longer carries a copy).
1824
+ description: Take a briefed post from brief to script to published in any format — text, visual or video (HyperFrames) — from the pod's brand kit, one stage per post status, with a person approving the script before anything is generated.
1016
1825
  goalTemplate: >-
1017
- Produce content for {platform} from this idea ({post}). Take the raw idea,
1018
- shape it into a {format} draft with a scroll-stopping hook and a clear CTA,
1019
- keep it aligned to the post's pillar and the operator's brand voice, then
1020
- move it to Scheduled with a publish date.
1826
+ Produce content for {platform} from this idea ({post}) in the {format} format.
1827
+ Read the brand first from the pod's brand kit (GET /api/hub/brand/kit) — never
1828
+ re-derive it from a website capture — and load the creative-director skill
1829
+ (linked to this space) to pick the approved brand-template (layout and
1830
+ recipe), references and assets for this format and style; drafts never feed
1831
+ generation. Work the stages in order; each stage moves the post's status.
1832
+ SCRIPT before anything is made: write the script or outline for the format
1833
+ (carousel slide outline, deck outline, video script and storyboard, or the
1834
+ text draft) by filling the narrative frame for the post's goal — the
1835
+ approved content-guideline of type frame for that goal, else the default
1836
+ frame (sell: story, example, lesson, link; hot-take: claim, tension, reason,
1837
+ invitation; tutorial: promise, steps, result, next; product-demo: problem,
1838
+ real screen, outcome, try it). A person approves the script before the
1839
+ draft; nothing is generated from an unapproved script. At the draft, run the
1840
+ creative-director readiness check first (approved references, an approved
1841
+ base guideline, an approved brand-template for this format); if anything is
1842
+ missing, stop and route to the content-taste skill or brand onboarding
1843
+ instead of improvising. Then branch on the format. TEXT
1844
+ (article, thread, newsletter section, caption): draft it with a
1845
+ scroll-stopping hook and a clear CTA. VISUAL: build it pass by pass through
1846
+ the edit-loop skill, a person answering each pass — a carousel, LinkedIn
1847
+ PDF or image post as a layered composition saved on the post
1848
+ (carousel-format), a deck, PDF document or presentation in Open Design
1849
+ (open-design-edit). VIDEO (short or
1850
+ long video): first pick the route — FROM SCRATCH (script, storyboard,
1851
+ frames), FROM A VOICE (an existing voice-over or talk: transcribe it, cut on
1852
+ its beats, build the frames under it) or RE-EDIT (existing footage plus a
1853
+ voice: cut, caption, add music and motion); takes ({takes}) mean RE-EDIT,
1854
+ and the Draft stage says how takes, transcripts and passes are handled.
1855
+ Then run the HyperFrames
1856
+ workflow on your own agent with HYPERFRAMES_NO_TELEMETRY=1, from the kit's
1857
+ frame.md (format=frame-md), with GSAP vendored locally — never `hyperframes
1858
+ publish`, never a cloud render. For a visual or video format, record a
1859
+ content-composition made for this post (for a video: the brief and storyboard
1860
+ documents, the project zip, the MP4, key-frame stills and the brand-kit hash).
1861
+ Keep every format on the post's pillar and the brand voice. A human approves
1862
+ the script before the draft, each pass as it is built, and the
1863
+ draft on the preview at Review. At
1864
+ Schedule, a post whose account is a Postiz channel is queued through the
1865
+ capability that serves publish_post (postiz_schedule_post); that queueing is
1866
+ itself a proposal a person approves. Nothing publishes itself, and the post
1867
+ reads Published only once it went live: reported by Postiz, or confirmed
1868
+ by a person at Publish.
1021
1869
  subjectProfile:
1022
1870
  profileSlug: post
1871
+ statusProperty: post-status
1872
+ # Idea is where a capture lands and Archived is a person's call — no stage
1873
+ # owns either. The activator below starts the run once the post is Briefed.
1874
+ humanOnlyStatuses: [Idea, Archived]
1875
+ activators:
1876
+ - { on: enters_status, status: Briefed, mode: propose }
1023
1877
  params:
1024
1878
  - { name: post, type: entity, label: Post, required: true }
1025
1879
  - { name: platform, type: text, label: Platform, required: true }
1026
1880
  - { name: format, type: text, label: Format }
1881
+ # Recorded footage, as `file` entity ids (comma-separated). A TEXT param,
1882
+ # not `type: entity`: the pod's param contract has no list and no kind
1883
+ # filter (`PlaybookParamType`, @synap/playbooks), and the reader drops any
1884
+ # other key without a word, so "entity, file, multi" cannot be declared.
1885
+ # Present → the video route is RE-EDIT.
1886
+ - { name: takes, type: text, label: Takes (file ids) }
1887
+ # The look picked in Content Studio's booth before recording (founder,
1888
+ # 2026-10-28), as ONE JSON string: {"background": {"assetId","fileId"} |
1889
+ # {"mode":"blur"} | null, "colour": {"assetId","fileId","intensity"} |
1890
+ # null}. A TEXT param for the same reason as `takes`: the param contract
1891
+ # has no object type, and a `text` value must be a string. Content Studio
1892
+ # sends it only to a process that declares it. Present → the Background
1893
+ # and Colour passes start from it; absent → they are built as before.
1894
+ - { name: look, type: text, label: Look (JSON) }
1027
1895
  executor: is-agent
1028
1896
  status: active
1897
+ # What the process needs the pod to be able to do. Schedule queues the post
1898
+ # through whatever serves publish_post (the `postiz` capability's
1899
+ # postiz_schedule_post); a pod with nothing serving it reports the gap.
1900
+ requiredIntents: [publish_post]
1901
+ inputStrategy:
1902
+ kind: pinned
1903
+ fallback:
1904
+ filter:
1905
+ post-status: Idea
1906
+ orderBy: createdAt
1907
+ orderDir: asc
1908
+ stages:
1909
+ - key: brief
1910
+ name: Brief
1911
+ category: planned
1912
+ subjectStatus: Briefed
1913
+ # A run is BORN at its first stage, and nothing writes a stage's status
1914
+ # at birth (finding f36420d5: the pilot post stayed at Idea while its
1915
+ # run reached Draft). Said here, not only in goalTemplate: a template
1916
+ # update leaves an owner-edited goal alone.
1917
+ goal: >-
1918
+ Pin down the post's angle, platform, format, pillar and journey stage
1919
+ before anything is drafted. A run starts at this stage without moving
1920
+ the post: if the post still reads Idea, move it to Briefed first
1921
+ (synap_update_entity, post-status Briefed, expectedStatus Idea). Then
1922
+ advance the run through every stage in order (currentStage), never
1923
+ skipping one, takes included: entering a stage moves the post to its
1924
+ status, and a reply that says proposed is success.
1925
+ criteria:
1926
+ - key: brief-has-hook
1927
+ statement: The post has a hook or headline that states its one idea.
1928
+ check: { kind: judge }
1929
+ # One criterion for the whole placement (format, platform, pillar,
1930
+ # goal): the playbook sits at MAX_SESSION_CRITERIA (12) and the
1931
+ # script stage needed two.
1932
+ - key: brief-has-format-platform
1933
+ statement: The post's format, platform, pillar and goal are set.
1934
+ check: { kind: judge }
1935
+ # The written version, before anything is generated (D4, 2026-10-12).
1936
+ - key: script
1937
+ name: Script
1938
+ category: started
1939
+ subjectStatus: Scripted
1940
+ goal: >-
1941
+ Write the script or outline for the post's format — carousel slide
1942
+ outline, deck outline, video script and storyboard, or the text draft —
1943
+ by filling the narrative frame for the post's goal (the approved
1944
+ content-guideline of type frame for that goal, else the default frame).
1945
+ With takes (the RE-EDIT route), the script is the cut plan: what is
1946
+ kept of the takes' words, in order, and why (the facecam-edit skill's
1947
+ plan), so the person approves the cut before any pass is built.
1948
+ expectedOutputs:
1949
+ # A pod document. `relates_to` is the default relation (base.yaml):
1950
+ # the composition's brief/storyboard documents are stored as ids on
1951
+ # the composition, not as an edge, so there is no narrower one to reuse.
1952
+ - kind: document
1953
+ label: Script
1954
+ relationToSubject: relates_to
1955
+ criteria:
1956
+ - key: script-follows-frame
1957
+ statement: The script follows the narrative frame for the post's goal and ends on its call to action.
1958
+ check: { kind: judge }
1959
+ - key: script-fits-format
1960
+ statement: The script is shaped for the post's format (slides, scenes or sections) and the platform's limits.
1961
+ check: { kind: judge }
1962
+ - key: draft
1963
+ name: Draft
1964
+ category: started
1965
+ subjectStatus: In Draft
1966
+ # Per-pass approval (founder D3, 2026-10-27): ONE person-owned slot per
1967
+ # video pass, NOT a stage per pass (stages move post-status, and the
1968
+ # post lifecycle has no per-pass status). The slots are declared by
1969
+ # the agent as each pass's preview exists, never here in
1970
+ # `expectedOutputs`: a stage's slots are all owed on entry, by every
1971
+ # format (a text post would owe five video approvals, and the person
1972
+ # would be asked before anything is built), and an `optional` slot is
1973
+ # owed by nobody and cannot be turned owed again through the agent's
1974
+ # outcome door. The slot shape is fixed in the edit-loop skill (one
1975
+ # contract for every engine, 2 Nov) and named here, so every run asks
1976
+ # the same questions. Takes and transcripts are spelled here too,
1977
+ # not only in `goalTemplate`: a template update merges field by field
1978
+ # and leaves a field the owner edited alone (`threeWayMergeFields`), so
1979
+ # an install whose goal was edited on the pod still receives `stages`.
1980
+ # The pod caps a stage goal at 5,000 characters (backend
1981
+ # `playbook-stage.ts`); over it every install and update of Content OS
1982
+ # is refused (2026-11-02), and the catalog validator now refuses it too.
1983
+ goal: >-
1984
+ Entering Draft waits on the person: this stage's gate asks them to
1985
+ approve the script, and that approval moves the post to In Draft.
1986
+ Build nothing before it passes.
1987
+ Run the creative-director readiness check, then produce the draft in
1988
+ the post's format from the approved script, the brand kit and the
1989
+ approved brand-template (layout and recipe) for the format. A visual
1990
+ or video format is built pass by pass through the edit-loop skill and
1991
+ its engine's skill: carousel, LinkedIn PDF or image post, a layered
1992
+ composition saved on the post (carousel-format; passes
1993
+ layer-structure, layer-visual, layer-polish, an image the last two);
1994
+ deck, PDF document or presentation, Open Design (open-design-edit,
1995
+ same passes); video, HyperFrames.
1996
+ Takes given (the
1997
+ run's takes param, on the run session at metadata.params.takes:
1998
+ comma-separated file ids of recorded footage) mean the RE-EDIT video
1999
+ route, with the facecam-edit skill. A take's transcript is ONE
2000
+ content-asset, Kind script, whose File ID is the take's file; its
2001
+ body holds the readable text under a "## Transcript" heading, then a
2002
+ "## Words" heading followed by exactly one ```json fenced block with
2003
+ the word-level JSON in HyperFrames' shape [{"text","start","end"}]
2004
+ (seconds; [] until the precise pass fills it). Find it before transcribing: if it exists, reuse
2005
+ it and only fill the word timings; if not, create it and link it to
2006
+ the take's file with a references relation. Never a separate
2007
+ document per take.
2008
+ A VIDEO is built in passes, in this order, each one non-destructive (changing a pass never
2009
+ redoes an approved one): cut and order, background, colour,
2010
+ captions, graphics and animation. Skip a pass the route does not
2011
+ use. A run started in the older order builds the passes it has not done in this
2012
+ order, background first.
2013
+ When a pass starts, declare its slot as yours first (an outcome with
2014
+ the pass's key, kind file, no owner, no ask). After each pass, render
2015
+ its preview into the pod and hand the person ONE slot for it (an
2016
+ outcome) on the same key, layer-<pass> (a video: layer-cut,
2017
+ layer-background, layer-colour, layer-captions or layer-graphics):
2018
+ owner human, ref the preview, ask choose with allowOther, options
2019
+ Approve (recommended), Rework and Use my edit, answered as the
2020
+ edit-loop skill spells out (Approve claims the slot: completeOutput).
2021
+ Build the next pass only after the answer. Use my edit: their edit is
2022
+ the pass; never overwrite or redo their changes. Before each of your
2023
+ passes record a version, work from the diff to the current work (never
2024
+ regenerate from the prompt), never write while a pass is the person's,
2025
+ and acknowledge each answer in the room.
2026
+ With takes, every pass is built in ONE HyperFrames project, the folder
2027
+ edit in the run's work folder (~/synap-work/<this run's session
2028
+ id>/edit), shown live in HyperFrames Studio, whose address is the
2029
+ run's studio outcome, retired (retireOutput studio) whenever Studio
2030
+ stops, as the facecam-edit skill spells out.
2031
+ Every render the person hears, a preview included, carries the cut's
2032
+ audio copied (a remux, never the render's own encode), and a
2033
+ re-encode for size touches the video only, as the facecam-edit skill
2034
+ spells out.
2035
+ A LOOK given (the run's look param, at metadata.params.look: a JSON
2036
+ string {"background": {"assetId","fileId"} or {"mode":"blur"} or
2037
+ null, "colour": {"assetId","fileId","intensity"} or null}, picked in
2038
+ the booth before recording) means the Background pass and the Colour
2039
+ pass START from it, as the facecam-edit skill spells out. Background:
2040
+ matte the person from the raw take with the person's edit_video tool
2041
+ (synap_list_capabilities with intent edit_video), handing it the
2042
+ take's pod file id ({"videoFile": id}; the pod signs it at run
2043
+ time); gate the matte (its frame
2044
+ count and duration match the take, no flicker); defocus the
2045
+ background once; composite with ffmpeg (choke, feather, light wrap);
2046
+ then grain on the background and a light match on the person. The
2047
+ background is the background file (downloaded from the pod by its
2048
+ fileId), or, for mode blur, a blurred copy of the raw take. No edit_video tool, a failed run or a failed gate: matte with
2049
+ npx hyperframes remove-background instead and composite the same way
2050
+ over the same background; that failing
2051
+ too: the raw take, and the pass's question says "background not
2052
+ applied". Colour: download the
2053
+ .cube by its fileId and apply it as the grading block grading.lut
2054
+ (src the .cube, intensity the look's intensity). Each is still its
2055
+ own pass, with its own slot (layer-background, layer-colour) and its
2056
+ own question, asked like every other pass; for these two the options
2057
+ add Remove this pass (the raw take shows through: drop the pass,
2058
+ retire its slot with retireOutput, build the next). A null part of
2059
+ the look leaves its pass as it is without a look. The raw take is
2060
+ never overwritten: a pass writes new files beside it.
2061
+ # A person approves the written version before any generation: the
2062
+ # gate holds In Draft until it passes, and a rejection sends the run
2063
+ # back to rework the script.
2064
+ gate: { kind: human }
2065
+ onFail: { toStage: script }
2066
+ expectedOutputs:
2067
+ # Visual and video formats only: a text draft lives on the post itself.
2068
+ - kind: content-composition
2069
+ label: Composition (visual or video formats)
2070
+ relationToSubject: made_for
2071
+ criteria:
2072
+ - key: draft-brand-kit-read
2073
+ statement: The draft was built from the pod's brand kit and the approved brand-template for its format, not a re-derived brand.
2074
+ check: { kind: judge }
2075
+ - key: draft-matches-format
2076
+ statement: The draft follows the approved script, is in the post's format and fits the platform's constraints.
2077
+ check: { kind: judge }
2078
+ - key: review
2079
+ name: Review
2080
+ category: started
2081
+ subjectStatus: Review
2082
+ goal: Put the draft in front of a person on the preview, with a proposed publish date.
2083
+ gate: { kind: human }
2084
+ onFail: { toStage: draft }
2085
+ criteria:
2086
+ - key: review-date-proposed
2087
+ statement: A publish date is proposed.
2088
+ check: { kind: judge }
2089
+ - key: review-approved
2090
+ statement: A person approved the draft.
2091
+ check: { kind: human }
2092
+ - key: schedule
2093
+ name: Schedule
2094
+ category: started
2095
+ subjectStatus: Scheduled
2096
+ goal: >-
2097
+ Set the publish date and queue the post on its account: its
2098
+ post-account, else the Personal account (account-owner) of its
2099
+ network (founder, 4 Nov). When that platform carries
2100
+ unipile-account-id (Unipile, the default; decision A, 3 Nov:
2101
+ approving is the yes, nothing asks again), arm it for Publish due
2102
+ posts in one write: post-account set to that platform,
2103
+ publish-account-id to its unipile-account-id,
2104
+ publish-as-organization to its linkedin-organization-id when it is
2105
+ a LinkedIn company page, publish-text to the approved caption or
2106
+ text, publish-media to the approved pictures or video as pod file
2107
+ ids in order (at most 10), publish-at-ms to 09:00 Europe/Paris on
2108
+ the publish date in epoch milliseconds, and publish-outcome queued.
2109
+ Publish due posts sends it through unipile_publish_post on that day
2110
+ and writes the result back. For each other account in post-copy-to
2111
+ (the same post) or post-reshare-on (a LinkedIn repost), create one
2112
+ Scheduled post armed the same way on that account, with
2113
+ post-copy-of set to this post and post-send-mode copy or reshare; a
2114
+ repost has empty publish-text and no media, and waits
2115
+ (publish-outcome waiting) until this post is out.
2116
+ When the account is a Postiz channel (it carries
2117
+ postiz-integration-id and postiz-provider), queue it with
2118
+ postiz_schedule_post: type schedule, the publish date, the approved
2119
+ text, the media links in order, and postEntityId set to this post.
2120
+ When a person approves that run, the pod records the returned postId
2121
+ on the post as postiz-post-id. Any other account: queue it where the
2122
+ account lives and say how.
2123
+ criteria:
2124
+ - key: schedule-date-set
2125
+ statement: The post's publish date is set.
2126
+ check: { kind: evidence, evidenceKey: publish-date }
2127
+ # One judge criterion, not a postiz-post-id evidence check: an account
2128
+ # that is not on Postiz (a newsletter, a blog) has no Postiz id.
2129
+ - key: schedule-account-set
2130
+ statement: The post names the account it goes out on and is queued there; on a Unipile account it is armed (publish-account-id and publish-text set, publish-outcome queued); on a Postiz account its postiz-post-id is recorded.
2131
+ check: { kind: judge }
2132
+ - key: publish
2133
+ name: Publish
2134
+ category: completed
2135
+ subjectStatus: Published
2136
+ goal: >-
2137
+ The post is live at its URL. Armed for Unipile, Publish due posts
2138
+ sends it on its day and writes Published, post-url and
2139
+ published-at itself; a send that fails writes publish-outcome failed
2140
+ and publish-error and leaves the post at Scheduled, for a person to
2141
+ fix and arm again. Queued through Postiz, Postiz reports it (its
2142
+ webhook records the URL and Published); otherwise a person confirms
2143
+ it, and approving writes Published.
2144
+ # A HUMAN gate, not a check: the pod holds a gated stage's status until
2145
+ # the gate passes, and a check gate judges the stage being LEFT
2146
+ # (Schedule), so it would pass on entry and write Published before
2147
+ # anyone saw the post go out — firing Repurpose on a post that may
2148
+ # never have shipped. Only a person's approval writes Published.
2149
+ gate: { kind: human }
2150
+ onFail: { toStage: schedule }
2151
+ criteria:
2152
+ - key: publish-live-url
2153
+ statement: The post's published URL is recorded.
2154
+ check: { kind: evidence, evidenceKey: post-url }
2155
+ - key: publish-date-recorded
2156
+ statement: The post's publish date is set to the day it went live.
2157
+ check: { kind: evidence, evidenceKey: publish-date }
1029
2158
 
1030
- # The proactive half. Deliberately has NO `subjectProfile`: a subject profile
1031
- # binds a playbook to ONE entity, so pairing it with a cron fans out into one
1032
- # run PER EXISTING POST (capped at 100) — exactly the wrong shape for a
1033
- # question about the corpus as a whole. This one reasons over everything at
1034
- # once and proposes the NEXT piece, which is why it also takes no params.
1035
- # Weekly, because a content plan that is re-litigated daily is not a plan.
2159
+ # One take, several videos (D2, D3, 29 Oct). The take is a content-asset of
2160
+ # Kind recording; it has no lifecycle (its state is derived from what was
2161
+ # made from it), so this process declares no statusProperty and its stages
2162
+ # move no status. The person decides at Keep, through one question per
2163
+ # proposed piece (a slot ask: the door Content Studio and Relay answer), not
2164
+ # a stage gate: keeping a piece commits nothing outside the pod, and the raw
2165
+ # take is never touched. The per-piece slots are declared by the agent as
2166
+ # each piece exists, like Produce Content's pass slots: a stage's own slots
2167
+ # are owed on entry, and the number of pieces is not known before Analyse.
2168
+ - name: Work a take
2169
+ description: Find the videos inside one recorded take, cut the ones the person keeps into new files, and make one video post at Idea for each, on the person's computer, with the raw take never touched.
2170
+ goalTemplate: >-
2171
+ Work this take ({take}): a content-asset of Kind recording whose File ID
2172
+ is the raw take's file. Run on the person's computer, in this run's work
2173
+ folder (~/synap-work/<this run's session id>). Transcribe it precisely,
2174
+ find the pieces inside it (one or more videos that each stand alone),
2175
+ ask the person which to keep, cut each kept piece into its own new video
2176
+ file, and make one video post at Idea per piece. The raw take is never
2177
+ overwritten, moved or re-uploaded. Quote only words the person said;
2178
+ never invent a claim or a number. Nothing is published, and no Produce
2179
+ Content run starts unless the person asks for it.
2180
+ subjectProfile:
2181
+ profileSlug: content-asset
2182
+ params:
2183
+ - { name: take, type: entity, label: Take, required: true }
2184
+ executor: is-agent
2185
+ status: active
2186
+ stages:
2187
+ - key: transcribe
2188
+ name: Transcribe
2189
+ category: started
2190
+ goal: >-
2191
+ Download the take (GET /api/hub/files/<fileId>/url gives a signed
2192
+ URL) and ffprobe it. Fill the take's missing facts:
2193
+ duration-seconds, frame-width, frame-height, frame-rate, and a
2194
+ poster (one still frame as a JPEG, stored with synap_store_file,
2195
+ its id in poster-file-id). The take's transcript is ONE
2196
+ content-asset, Kind script, whose File ID is the take's file; its
2197
+ body holds the readable text under a "## Transcript" heading, then a
2198
+ "## Words" heading followed by exactly one ```json fenced block with
2199
+ the word-level JSON in HyperFrames' shape [{"text","start","end"}]
2200
+ (seconds). Find it before transcribing (Content Studio usually made
2201
+ it): reuse it and only fill the word timings (npx hyperframes
2202
+ transcribe, Parakeet on the Mac); if none exists, create it and link
2203
+ it to the take's file with a references relation. Never transcribe
2204
+ twice, never a separate document.
2205
+ criteria:
2206
+ - key: take-words-timed
2207
+ statement: The take's one transcript content-asset holds word timings for the whole take.
2208
+ check: { kind: judge }
2209
+ - key: take-facts-filled
2210
+ statement: The take records its duration, frame size, frame rate and a poster.
2211
+ check: { kind: judge }
2212
+ - key: analyse
2213
+ name: Analyse
2214
+ category: started
2215
+ goal: >-
2216
+ Read the transcript and find the pieces: each a run of the take that
2217
+ stands alone as one video, with a range whose edges sit on word
2218
+ boundaries (seconds, in the take's time), a working title, its hook
2219
+ (the person's exact opening sentence), a format (Short Video under
2220
+ about 90 seconds, else Long Video) and why it stands alone. A take
2221
+ that holds one video gives one piece. Write ONE document, the
2222
+ analysis, linked to the take with a references relation: the topics,
2223
+ the strong lines and a verdict on the hook, then a "## Pieces"
2224
+ heading followed by exactly one ```json fenced block
2225
+ [{"n","start","end","title","hook","format","why"}] (n from 1, in
2226
+ take order). Then ask about each piece: hand the person ONE slot per
2227
+ piece (an outcome), key keep-piece-<n>; kind decision; owner human;
2228
+ blockedReason decision; why the piece's range, title and hook; ref
2229
+ the analysis (kind document); ask
2230
+ choose with allowOther, options Keep (recommended when it stands
2231
+ alone and fits its format), Drop, and Adjust (the note gives the new
2232
+ range as <start>-<end> in seconds, or its first and last words).
2233
+ Lines worth a post that are not a piece: propose each as a post at
2234
+ Idea that quotes the person's sentence and links to the take with a
2235
+ references relation.
2236
+ expectedOutputs:
2237
+ - kind: document
2238
+ label: Analysis
2239
+ relationToSubject: references
2240
+ criteria:
2241
+ - key: pieces-ranged
2242
+ statement: Each piece has a word-boundary range, a working title, a hook in the person's exact words and a reason it stands alone.
2243
+ check: { kind: judge }
2244
+ - key: pieces-asked
2245
+ statement: Each piece has its own question to the person (keep-piece-n) with Keep, Drop and Adjust.
2246
+ check: { kind: judge }
2247
+ - key: keep
2248
+ name: Keep pieces
2249
+ category: started
2250
+ goal: >-
2251
+ Wait for the person's answer on every keep-piece-<n> question. Keep:
2252
+ the piece is cut as proposed. Drop: nothing is made from it. Adjust:
2253
+ take the range from the note, snap it to word boundaries, and treat
2254
+ the piece as kept. Claim each answered question (completeOutput).
2255
+ Cut only after every question is answered; a piece the person
2256
+ dropped is never cut.
2257
+ criteria:
2258
+ - key: every-piece-answered
2259
+ statement: The person kept, dropped or adjusted every proposed piece.
2260
+ check: { kind: judge }
2261
+ - key: none-cut-before-answers
2262
+ statement: No piece was cut before every keep-piece question was answered.
2263
+ check: { kind: judge }
2264
+ - key: cut
2265
+ name: Cut
2266
+ category: started
2267
+ goal: >-
2268
+ Cut each kept piece, one at a time. Declare its slot as yours first
2269
+ (an outcome, key piece-<n>, kind content-asset, no owner, no ask):
2270
+ that is how the person sees it being cut. Cut with ffmpeg, frame
2271
+ accurate, from the raw take into a NEW file,
2272
+ ~/synap-work/<this run's session id>/pieces/<n>.mp4: ffmpeg -ss
2273
+ <start> -to <end> -i <raw take> -vf fps=<frame rate> -c:v libx264
2274
+ -crf 18 -preset medium -c:a aac -b:a 192k -af
2275
+ "afade=t=in:st=0:d=0.03,afade=t=out:st=<end minus start minus
2276
+ 0.03>:d=0.03" -movflags +faststart <out> (the 30 ms fades keep the
2277
+ edges from popping). Check it with ffprobe (duration within one
2278
+ frame of end minus start, audio and video present). Upload it into
2279
+ this space (synap_request_upload, PUT the bytes, then
2280
+ synap_store_file with the uploadToken) as a file titled "Piece <n> ·
2281
+ <title> (m:ss–m:ss)". Record the piece: a content-asset, Kind
2282
+ recording, titled with its working title: file-id the new file,
2283
+ source-take the take, source-start and source-end, piece-hook,
2284
+ piece-format, duration-seconds, frame-width, frame-height,
2285
+ frame-rate, a poster (poster-file-id) and recorded-at copied from
2286
+ the take; link it to the take and to its file with references
2287
+ relations. Give it its own transcript in the same ONE shape (File
2288
+ ID = the piece's file): the take's words inside the range, re-timed
2289
+ to the piece (minus source-start). Point the slot at the piece
2290
+ (ref, kind entity) and claim it (completeOutput). The person may ask
2291
+ to adjust a piece after its cut: cut the new range into a NEW file
2292
+ the same way, set file-id to it and the new range, and append the
2293
+ old file id to earlier-file-ids; never delete or overwrite a file.
2294
+ criteria:
2295
+ - key: only-kept-cut
2296
+ statement: Only the kept pieces were cut, each into its own new file, and the raw take is unchanged.
2297
+ check: { kind: judge }
2298
+ - key: piece-lineage
2299
+ statement: Each piece records its take, its range and its own file, and references the take.
2300
+ check: { kind: judge }
2301
+ - key: posts
2302
+ name: Make the posts
2303
+ category: completed
2304
+ goal: >-
2305
+ Make one video post per cut piece, at Idea: post-format the piece's
2306
+ format, post-hook its hook, titled with its working title, linked to
2307
+ the piece with a references relation. Record each as an outcome, key
2308
+ post-<n>, kind post, ref the post (kind entity), and claim it
2309
+ (completeOutput). Start Produce Content for a post only if the
2310
+ person asks (its takes param is then the piece's file id).
2311
+ criteria:
2312
+ - key: one-post-per-piece
2313
+ statement: Each cut piece has one video post at Idea that references it, and no post moved past Idea by itself.
2314
+ check: { kind: judge }
2315
+ - key: nothing-started-unasked
2316
+ statement: Nothing was published, and no Produce Content run started unless the person asked for it.
2317
+ check: { kind: judge }
2318
+
2319
+ # The proactive half: the plan. It reasons over the corpus as a whole, so it
2320
+ # takes no params, and its WORKERS are per_moment: ONE run over every post,
2321
+ # never one run per post (a subject-bound playbook's `schedule:` field fans
2322
+ # out per entity; an activator's `schedule` and a per_moment `manual` do
2323
+ # not: api `run-playbook.ts` runWorkerBatch). The `subjectProfile` names the
2324
+ # kind those workers read; there are no stages and no `statusProperty`, so it
2325
+ # never moves a post's status.
2326
+ # Run by hand ("Re-plan" on Content Studio's calendar, or the pod's button)
2327
+ # and once a week (founder, report 22fec5ee §6, 3 Nov 2026). The weekly one
2328
+ # compiles to a governed rule the owner can pause on the pod; both propose
2329
+ # every write, so the dates arrive as ONE batch the person approves.
2330
+ # The plan's numbers are never in this text: cadence, goal mix, pillar
2331
+ # balance and the campaign are read from the pod, where the person edits them.
1036
2332
  - name: Plan Next Content
1037
- description: Audit the whole corpus against the journey, find the gap, and propose what to make next.
2333
+ description: Audit the whole corpus against the journey, find the gap, propose what to make next, and propose dates for the posts that have none or whose date has passed.
1038
2334
  goalTemplate: >-
1039
2335
  Audit the content corpus. Map every post onto its pillar and its journey
1040
2336
  stage, and list which pillar × stage cells are empty or thin. Check the
1041
2337
  running series for the chapter that is missing. If the pod has offers,
1042
2338
  note which ones no content teaches toward; if it has none, plan purely for
1043
2339
  audience-building and do not invent commerce. Then propose the next one or
1044
- two pieces — each as a post idea placed on a pillar, a stage, and a
1045
- platform, with the hook and the reason it is next.
2340
+ two pieces — each created as a post entity at the Idea status, placed on a
2341
+ pillar, a stage and a platform, with the hook and the reason it is next.
2342
+ Then re-plan the calendar. Read the plan's inputs from the pod, never from
2343
+ a number in this text: the active campaign whose dates cover the coming
2344
+ weeks (campaign-start-date, campaign-end-date, its notes), each publishing
2345
+ account's cadence (platform-posting-freq on its platform), each pillar's
2346
+ target (posting-frequency on the pillar) and the goal mix the person set
2347
+ in this space's brief. Propose a publish-date for every post past Idea
2348
+ that has none, and a new publish-date for every post whose date has
2349
+ passed while it is not Published, the longest-waiting first: follow the
2350
+ cadence, balance the pillars and the goals, keep a campaign's posts inside
2351
+ its dates, and never put two posts of one account on the same day. Never
2352
+ move a Published or Archived post, and never change a post's status. File
2353
+ every date as a proposal, all of them in this one run so the person
2354
+ approves them as one batch, each with a one-line reason (the cadence, the
2355
+ gap or the campaign it serves); never apply a date yourself. When an input
2356
+ is missing (no cadence, no goal mix), say which one in the run's summary
2357
+ and plan without it.
2358
+ subjectProfile:
2359
+ profileSlug: post
2360
+ activators:
2361
+ - { on: schedule, cron: "0 9 * * 1", mode: propose }
2362
+ - { on: manual, size: per_moment, mode: propose }
2363
+ # Typed output: each idea is a real `post` at Idea (the start of Produce
2364
+ # Content's lifecycle), not prose in a message. The run works the whole
2365
+ # set, so no edge to a subject.
2366
+ expectedOutputs:
2367
+ - { kind: post, label: Next post idea, relationToSubject: none }
1046
2368
  executor: is-agent
1047
- # Monday 09:00, shipped OFF — the operator opts in. `enabled` is read
1048
- # STRICTLY (`cron-automation.ts:43` is `s.enabled === true`, and `:194`
1049
- # skips a falsy one), so OMITTING the key does not mean "on by default":
1050
- # it means the backing cron automation is never created and this playbook
1051
- # silently never fires. Declaring it explicitly is the difference between
1052
- # an opt-in cadence and a dead one. Off is the right default for a
1053
- # `isPublic: true` template — every installer would otherwise be enrolled
1054
- # in a recurring agent run (real token cost, a queue of proposals) they
1055
- # never asked for. Matches `ecosystem.yaml:413,436`, same cadence.
1056
- schedule: { cron: "0 9 * * 1", enabled: false }
1057
2369
  status: active
1058
2370
 
2371
+ # The idea bank's door (decision B, review 22fec5ee §7): Content Studio's
2372
+ # "Package ideas" button runs it. Like Plan Next Content it reasons over the
2373
+ # whole bank at once, so it has NO `subjectProfile` and no params. It OPENS the
2374
+ # work: it proposes posts and their links in ONE capture the person approves,
2375
+ # and never writes the post. The `feeds` edge is the idea → post entityLink.
2376
+ - name: Package ideas
2377
+ description: Read the idea bank, group the ideas that belong together, and propose a set of posts, each linked to the ideas that feed it, as one batch to approve.
2378
+ goalTemplate: >-
2379
+ Package the idea bank of this space. Read every idea (kind idea) at New or
2380
+ Shaped: its words, its pillar, its journey stage and its age. Group the
2381
+ ideas that belong in one piece (the same claim, the same story, or the
2382
+ same question from the audience); one idea may feed several posts. Then
2383
+ propose one to five posts: each with a working title, a format, a pillar
2384
+ and the one sentence it is about, at the Idea status. Never a script, a
2385
+ hook to publish or a finished text: the person writes the post. File the
2386
+ whole set as ONE synap_capture graph (each post a new entity, each source
2387
+ idea by existingEntityId, linked idea feeds post), so the person approves
2388
+ it in one review. Every post has at least one idea; an idea with no cousin
2389
+ that is not a piece on its own is left out. Once the batch is approved,
2390
+ move each idea that feeds a post to Used (synap_update_entity,
2391
+ expectedStatus its status). Nothing is briefed, scheduled or published,
2392
+ and no Produce Content run starts.
2393
+ # Typed output: each packaged post is a real `post` at Idea, linked from its
2394
+ # ideas (`feeds`, written by the capture). No subject, so no edge to one.
2395
+ expectedOutputs:
2396
+ - { kind: post, label: Packaged post, relationToSubject: none }
2397
+ executor: is-agent
2398
+ status: active
2399
+
2400
+ # Once a post is Published, propose turning it into its siblings (thread →
2401
+ # video → newsletter section). It READS the post lifecycle (its activator) but
2402
+ # never moves it — the new pieces are new posts, linked `derived_from` (the
2403
+ # post → post edge above) — so it declares no stages and owns no status.
2404
+ - name: Repurpose
2405
+ description: Turn a published post into new posts for other formats and platforms, each linked to the original.
2406
+ goalTemplate: >-
2407
+ Repurpose this published post ({post}). Read what it says and how it
2408
+ performed, then propose one to three new pieces in other formats or on
2409
+ other platforms where the same idea travels (a thread from an article, a
2410
+ short video from a thread, a newsletter section from a series). Create each
2411
+ as a new post at the Idea status, on the same pillar, with its own hook,
2412
+ and link it derived_from the original. Never republish the original as is.
2413
+ subjectProfile:
2414
+ profileSlug: post
2415
+ statusProperty: post-status
2416
+ activators:
2417
+ # A copy or repost sent for another account (post-copy-of) is not repurposed: its original is.
2418
+ - { on: enters_status, status: Published, mode: propose, unless: { property: post-copy-of, present: true } }
2419
+ expectedOutputs:
2420
+ - { kind: post, label: Repurposed post, relationToSubject: derived_from }
2421
+ params:
2422
+ - { name: post, type: entity, label: Post, required: true }
2423
+ executor: is-agent
2424
+ status: active
2425
+
2426
+ # A newsletter issue from idea to sent, one stage per `newsletter-status`.
2427
+ # No schedule: a subject-bound playbook on a cron fans out one run per
2428
+ # existing newsletter, which is never what an issue cadence means.
2429
+ - name: Newsletter issue
2430
+ description: Take a newsletter issue from idea to sent — angle, draft, edit, schedule — with a person approving before it goes out.
2431
+ goalTemplate: >-
2432
+ Produce this newsletter issue ({newsletter}). Read the brand voice from the
2433
+ pod's brand kit, then work the stages in order: settle the issue's angle
2434
+ and the posts it draws on, draft it, edit it down with a subject line and
2435
+ preview text, and schedule the send. A person approves the final issue
2436
+ before it is sent, nothing sends itself, and the issue reads Sent only once
2437
+ a person confirms it went out.
2438
+ subjectProfile:
2439
+ profileSlug: newsletter
2440
+ statusProperty: newsletter-status
2441
+ humanOnlyStatuses: [Archived]
2442
+ params:
2443
+ - { name: newsletter, type: entity, label: Newsletter issue, required: true }
2444
+ executor: is-agent
2445
+ status: active
2446
+ stages:
2447
+ - key: idea
2448
+ name: Idea
2449
+ category: backlog
2450
+ subjectStatus: Idea
2451
+ goal: Settle the issue's one angle and the posts or sources it draws on.
2452
+ criteria:
2453
+ - key: issue-has-angle
2454
+ statement: The issue states its one angle in a sentence.
2455
+ check: { kind: judge }
2456
+ - key: issue-has-sources
2457
+ statement: The posts or sources the issue draws on are listed.
2458
+ check: { kind: judge }
2459
+ - key: drafting
2460
+ name: Drafting
2461
+ category: started
2462
+ subjectStatus: Drafting
2463
+ goal: Write the full issue in the brand voice.
2464
+ criteria:
2465
+ - key: draft-complete
2466
+ statement: Every section of the issue is written.
2467
+ check: { kind: judge }
2468
+ - key: draft-brand-voice
2469
+ statement: The issue reads in the brand voice from the kit.
2470
+ check: { kind: judge }
2471
+ - key: editing
2472
+ name: Editing
2473
+ category: started
2474
+ subjectStatus: Editing
2475
+ goal: Tighten the issue and write its subject line and preview text.
2476
+ criteria:
2477
+ - key: edit-subject-line
2478
+ statement: The issue has a subject line.
2479
+ check: { kind: judge }
2480
+ - key: edit-preview-text
2481
+ statement: The issue has preview text of about 45 characters.
2482
+ check: { kind: judge }
2483
+ - key: edit-links-work
2484
+ statement: Every link in the issue resolves.
2485
+ check: { kind: judge }
2486
+ - key: scheduled
2487
+ name: Scheduled
2488
+ category: started
2489
+ subjectStatus: Scheduled
2490
+ goal: Set the send date and queue the issue.
2491
+ criteria:
2492
+ - key: send-date-set
2493
+ statement: The issue's send date is set.
2494
+ check: { kind: evidence, evidenceKey: send-date }
2495
+ - key: send-approved
2496
+ statement: A person approved the final issue.
2497
+ check: { kind: human }
2498
+ - key: sent
2499
+ name: Sent
2500
+ category: completed
2501
+ subjectStatus: Sent
2502
+ goal: A person confirms the issue went out to the list; approving writes Sent.
2503
+ # A HUMAN gate: only a person's approval writes Sent, so the issue
2504
+ # never reads Sent before someone confirmed it went out.
2505
+ gate: { kind: human }
2506
+ onFail: { toStage: editing }
2507
+ criteria:
2508
+ - key: sent-confirmed
2509
+ statement: The issue was sent to the list.
2510
+ check: { kind: judge }
2511
+ - key: sent-date-set
2512
+ statement: The issue's send date is set to the day it went out.
2513
+ check: { kind: evidence, evidenceKey: send-date }
2514
+ - key: sent-numbers
2515
+ statement: Subscriber count at send is recorded.
2516
+ check: { kind: judge }
2517
+
2518
+ automations:
2519
+ - key: content-os-weekly-produce
2520
+ name: Weekly content production
2521
+ description: >
2522
+ Monday 09:00. Runs Produce Content once, with no subject. The playbook
2523
+ takes the first eligible pinned post, or the oldest post still in Idea
2524
+ when the pin list has nobody eligible. Shipped paused because Content OS
2525
+ is public: an active cron would spend a run for every installer. Turn
2526
+ it on to start the cycle. Produce Content's own schedule stays off.
2527
+ triggerType: cron
2528
+ triggerConfig:
2529
+ expression: "0 9 * * 1"
2530
+ status: paused
2531
+ flow:
2532
+ nodes:
2533
+ - id: trigger
2534
+ type: trigger
2535
+ position: { x: 0, y: 0 }
2536
+ data:
2537
+ label: Monday at 09:00
2538
+ triggerType: cron
2539
+ config:
2540
+ expression: "0 9 * * 1"
2541
+ - id: produce
2542
+ type: playbook_run
2543
+ position: { x: 0, y: 160 }
2544
+ data:
2545
+ label: Produce the next idea
2546
+ playbookName: Produce Content
2547
+ edges:
2548
+ - id: e-trigger-produce
2549
+ source: trigger
2550
+ target: produce
2551
+
2552
+ # Decision A (founder, 3 Nov): a Scheduled post goes out through Unipile on
2553
+ # its date, with no second approval. ONE post per run, at the top level: a
2554
+ # capability node inside a loop that fails aborts its item with no failure
2555
+ # branch, and the failure must be written on the post. The query compares
2556
+ # properties numerically only, hence publish-at-ms. Shipped paused, like
2557
+ # every catalog automation: the person turns it on, and lets
2558
+ # unipile_publish_post run without a second approval for it (a run the pod
2559
+ # would propose fails the send, and the post says so).
2560
+ - key: content-os-publish-due-posts
2561
+ name: Publish due posts
2562
+ description: >
2563
+ Every 15 minutes, sends the oldest approved post whose time has come
2564
+ through Unipile, with no second approval. A post is due when it is
2565
+ Scheduled, armed when it was approved (publish-outcome queued) and its
2566
+ publish-at-ms has passed. It is claimed first (publish-outcome
2567
+ sending), so it is never sent twice; then its frozen words, account and
2568
+ pictures (publish-text, publish-account-id, publish-media) go out with
2569
+ unipile_publish_post, as a LinkedIn company page when it names one
2570
+ (publish-as-organization) and as a repost when it is one
2571
+ (publish-quote-id). Sent: Published, published-at, post-url and its
2572
+ Unipile id (publish-post-id), and its reposts waiting for it are
2573
+ queued with that id. Failed: publish-outcome failed and publish-error,
2574
+ the post left at Scheduled. Shipped paused: turn it on to start.
2575
+ triggerType: cron
2576
+ triggerConfig:
2577
+ expression: "*/15 * * * *"
2578
+ status: paused
2579
+ flow:
2580
+ nodes:
2581
+ - id: trigger
2582
+ type: trigger
2583
+ position: { x: 0, y: 0 }
2584
+ data:
2585
+ label: Every 15 minutes
2586
+ triggerType: cron
2587
+ config:
2588
+ expression: "*/15 * * * *"
2589
+ - id: now
2590
+ type: transform
2591
+ position: { x: 0, y: 120 }
2592
+ data:
2593
+ label: When this run was due, in milliseconds
2594
+ expression: "{{trigger.payload.scheduledAt}} | to_ms"
2595
+ - id: due
2596
+ type: query
2597
+ position: { x: 0, y: 240 }
2598
+ data:
2599
+ label: The oldest armed post whose time has come
2600
+ profileSlug: post
2601
+ filter: '{"post-status": "Scheduled", "publish-outcome": "queued", "publish-at-ms": {"$lte": {{steps.now.output.result}}}}'
2602
+ orderBy: properties.publish-at-ms
2603
+ orderDir: asc
2604
+ limit: 1
2605
+ - id: any-due
2606
+ type: condition
2607
+ position: { x: 0, y: 360 }
2608
+ data:
2609
+ label: A post is due?
2610
+ expression: "steps.due.output.count > 0"
2611
+ - id: claim
2612
+ type: output
2613
+ position: { x: 0, y: 480 }
2614
+ data:
2615
+ label: Claim it, so it is never sent twice
2616
+ outputType: entity_update
2617
+ config:
2618
+ entityId: "{{steps.due.output.entities.0.id}}"
2619
+ properties:
2620
+ publish-outcome: sending
2621
+ # The claim must have LANDED: a claim the pod only proposed leaves the
2622
+ # post queued, and the next run would send it a second time.
2623
+ - id: claimed
2624
+ type: condition
2625
+ position: { x: 0, y: 540 }
2626
+ data:
2627
+ label: The claim is written?
2628
+ expression: "steps.claim.output.status === 'updated'"
2629
+ - id: send
2630
+ type: capability
2631
+ position: { x: 0, y: 600 }
2632
+ data:
2633
+ label: Post it through Unipile
2634
+ verbId: unipile_publish_post
2635
+ inputMapping:
2636
+ accountId: "{{steps.due.output.entities.0.properties.publish-account-id}}"
2637
+ text: "{{steps.due.output.entities.0.properties.publish-text}}"
2638
+ media: "{{steps.due.output.entities.0.properties.publish-media}}"
2639
+ # A LinkedIn company page (post_as) and a repost's original
2640
+ # (quoted_post_id); both empty for an ordinary post.
2641
+ postAs: "{{steps.due.output.entities.0.properties.publish-as-organization}}"
2642
+ quotedPostId: "{{steps.due.output.entities.0.properties.publish-quote-id}}"
2643
+ # Never retried (an outward post), and a failure goes on to the
2644
+ # failure branch instead of stopping the run.
2645
+ errorHandling:
2646
+ continueOnError: true
2647
+ - id: sent
2648
+ type: condition
2649
+ position: { x: 0, y: 720 }
2650
+ data:
2651
+ label: Unipile posted it?
2652
+ expression: "steps.send.output.error === ''"
2653
+ - id: published
2654
+ type: output
2655
+ position: { x: -160, y: 840 }
2656
+ data:
2657
+ label: Published, and when
2658
+ outputType: entity_update
2659
+ config:
2660
+ entityId: "{{steps.due.output.entities.0.id}}"
2661
+ properties:
2662
+ post-status: Published
2663
+ published-at: "{{trigger.payload.scheduledAt}}"
2664
+ publish-outcome: succeeded
2665
+ # Its own node: Unipile may return no link, and an empty post-url is
2666
+ # refused; the post is Published either way.
2667
+ - id: link
2668
+ type: output
2669
+ position: { x: -160, y: 960 }
2670
+ data:
2671
+ label: The live post's link
2672
+ outputType: entity_update
2673
+ config:
2674
+ entityId: "{{steps.due.output.entities.0.id}}"
2675
+ properties:
2676
+ post-url: "{{steps.send.output.shareUrl}}"
2677
+ errorHandling:
2678
+ continueOnError: true
2679
+ # Unipile's id of the post it just sent: what a repost of it quotes.
2680
+ # With no id there is nothing to quote, so its reposts stay waiting
2681
+ # (Content Studio says so on the post).
2682
+ - id: has-id
2683
+ type: condition
2684
+ position: { x: -160, y: 1080 }
2685
+ data:
2686
+ label: Unipile gave the post's id?
2687
+ expression: "steps.send.output.postId !== ''"
2688
+ - id: remember-id
2689
+ type: output
2690
+ position: { x: -160, y: 1200 }
2691
+ data:
2692
+ label: Keep the post's Unipile id
2693
+ outputType: entity_update
2694
+ config:
2695
+ entityId: "{{steps.due.output.entities.0.id}}"
2696
+ properties:
2697
+ publish-post-id: "{{steps.send.output.postId}}"
2698
+ errorHandling:
2699
+ continueOnError: true
2700
+ # Its reposts (written at approval, each waiting for this post), now
2701
+ # queued with the id they quote. Only writes run per item here: the
2702
+ # send itself is never inside a loop.
2703
+ - id: reposts
2704
+ type: query
2705
+ position: { x: -160, y: 1320 }
2706
+ data:
2707
+ label: The reposts waiting for this post
2708
+ profileSlug: post
2709
+ filter: '{"post-copy-of": "{{steps.due.output.entities.0.id}}", "publish-outcome": "waiting"}'
2710
+ limit: 20
2711
+ - id: each-repost
2712
+ type: loop
2713
+ position: { x: -160, y: 1440 }
2714
+ data:
2715
+ label: For each waiting repost
2716
+ iteratorExpression: steps.reposts.output.entities
2717
+ itemVariable: repost
2718
+ - id: arm-repost
2719
+ type: output
2720
+ position: { x: -160, y: 1560 }
2721
+ data:
2722
+ label: Queue it, with the post it reposts
2723
+ outputType: entity_update
2724
+ config:
2725
+ entityId: "{{loop.item.id}}"
2726
+ properties:
2727
+ publish-quote-id: "{{steps.send.output.postId}}"
2728
+ publish-outcome: queued
2729
+ - id: failed
2730
+ type: output
2731
+ position: { x: 160, y: 840 }
2732
+ data:
2733
+ label: It didn't go out, and why
2734
+ outputType: entity_update
2735
+ config:
2736
+ entityId: "{{steps.due.output.entities.0.id}}"
2737
+ properties:
2738
+ publish-outcome: failed
2739
+ publish-error: "{{steps.send.output.error}}"
2740
+ edges:
2741
+ - id: e-trigger-now
2742
+ source: trigger
2743
+ target: now
2744
+ - id: e-now-due
2745
+ source: now
2746
+ target: due
2747
+ - id: e-due-any
2748
+ source: due
2749
+ target: any-due
2750
+ - id: e-any-claim
2751
+ source: any-due
2752
+ target: claim
2753
+ sourceHandle: "yes"
2754
+ - id: e-claim-claimed
2755
+ source: claim
2756
+ target: claimed
2757
+ - id: e-claimed-send
2758
+ source: claimed
2759
+ target: send
2760
+ sourceHandle: "yes"
2761
+ - id: e-send-sent
2762
+ source: send
2763
+ target: sent
2764
+ - id: e-sent-published
2765
+ source: sent
2766
+ target: published
2767
+ sourceHandle: "yes"
2768
+ - id: e-published-link
2769
+ source: published
2770
+ target: link
2771
+ - id: e-link-has-id
2772
+ source: link
2773
+ target: has-id
2774
+ - id: e-has-id-remember
2775
+ source: has-id
2776
+ target: remember-id
2777
+ sourceHandle: "yes"
2778
+ - id: e-remember-reposts
2779
+ source: remember-id
2780
+ target: reposts
2781
+ - id: e-reposts-each
2782
+ source: reposts
2783
+ target: each-repost
2784
+ - id: e-each-arm
2785
+ source: each-repost
2786
+ target: arm-repost
2787
+ - id: e-sent-failed
2788
+ source: sent
2789
+ target: failed
2790
+ sourceHandle: "no"
2791
+
1059
2792
  onboarding:
1060
2793
  goal: >
1061
2794
  Stand up the editorial engine — the pillars you're known for, the journey a
@@ -1074,6 +2807,12 @@ onboarding:
1074
2807
  every piece lands on-strategy. Push for a cadence they can hold for a year,
1075
2808
  not a heroic month. Be concrete: real platforms, real formats, a real first
1076
2809
  post — a fuzzy "I'll post more" helps no one.
2810
+ READY BEFORE PRODUCING: a post is produced only from approved brand
2811
+ material — at least three approved references, an approved base content
2812
+ guideline, and an approved brand-template for each format the post uses.
2813
+ If any is missing, do not improvise a look: say which, and route to the
2814
+ content-taste skill (or Brand Library onboarding when there is no brand
2815
+ identity or voice yet) before the first post is produced.
1077
2816
  COMMERCE IS OPTIONAL AND YOU MUST TREAT IT THAT WAY. Ask ONCE whether they
1078
2817
  sell anything. If the answer is no, or vague, SKIP offers entirely, never
1079
2818
  mention them again, and build the journey as pure audience-building — a
@@ -1138,6 +2877,7 @@ onboarding:
1138
2877
  thin — so the coverage read is a habit, not a feature they never find.
1139
2878
  collect:
1140
2879
  - profileSlug: pillar
2880
+ min: 1
1141
2881
  what: The recurring themes you're known for — the 2–4 lenses your audience comes to you for, each pointed at the audience it speaks to and the goal it serves.
1142
2882
  cardinality: few
1143
2883
  keyFields: [pillar-status, pillar-audience-ref, pillar-goal]
@@ -1150,13 +2890,15 @@ onboarding:
1150
2890
  cardinality: one
1151
2891
  keyFields: [series-status, series-goal, series-pillar]
1152
2892
  - profileSlug: platform
2893
+ min: 1
1153
2894
  what: The channels you actually publish on — the social accounts, newsletter, or podcast — with their status and the cadence you can hold.
1154
2895
  cardinality: several
1155
2896
  keyFields: [platform-type, platform-status, platform-posting-freq]
1156
2897
  - profileSlug: post
1157
- what: A first post or two already in motion, each placed on a pillar and a platform at its true status (Idea through Published), and on a stage if the journey exists.
2898
+ min: 1
2899
+ what: A first post or two already in motion, each placed on a pillar and a platform at its true status (Idea through Published), with its goal (sell, hot-take, tutorial, product-demo…) so it gets the right narrative frame, and on a stage if the journey exists.
1158
2900
  cardinality: few
1159
- keyFields: [post-status, post-pillar, post-platform]
2901
+ keyFields: [post-status, post-pillar, post-platform, post-goal]
1160
2902
  - profileSlug: offer
1161
2903
  what: >-
1162
2904
  ONLY IF THEY SELL SOMETHING. Ask once; if they don't sell, or hedge,
@@ -1176,3 +2918,52 @@ onboarding:
1176
2918
  # one. Mirrored verbatim by standards/content-os.standard.yaml — the journey
1177
2919
  # layer lives in its `recommends`, not its `requires`.
1178
2920
  doneWhen: at least one pillar + one platform with a cadence + a first post placed on a pillar.
2921
+
2922
+ skills:
2923
+ # DECLARED, not asked for in prose: every AI working in Content OS is told this
2924
+ # skill exists, is prepended with it each turn (`always`), and finds it ranked
2925
+ # first — the producing space, where every content ask routes through it.
2926
+ #
2927
+ # A REF, not a copy: the skill ships as its own package
2928
+ # (`skills/creative-director.skill.json`) and is installed by the `require`
2929
+ # dependency below. This is the LINK; that is the INSTALL.
2930
+ - slug: creative-director
2931
+ when: Every content ask in this space — it picks the approved brand-template, references and assets
2932
+ mode: always
2933
+ # On-demand, not always: needed only when the readiness check finds no
2934
+ # approved references, base guideline or template for the format.
2935
+ - slug: content-taste
2936
+ when: Studying inspiration, writing content guidelines, building the asset kit
2937
+ mode: on-demand
2938
+ - slug: carousel-format
2939
+ when: Scripting or drafting a carousel, LinkedIn PDF or image post (the layered composition)
2940
+ mode: on-demand
2941
+ - slug: edit-loop
2942
+ when: Building any visual or video post pass by pass — the slots, answers and rules every engine shares
2943
+ mode: on-demand
2944
+ - slug: open-design-edit
2945
+ when: Building a deck, PDF document or presentation in Open Design
2946
+ mode: on-demand
2947
+ # Video: the toolbox first (which tool, where it renders, which skill next),
2948
+ # then the job skill it names.
2949
+ - slug: video-toolbox
2950
+ when: Any video or animation ask, before choosing a tool — making, editing or animating video
2951
+ mode: on-demand
2952
+ - slug: video-direction
2953
+ when: Planning a launch, promo or explainer film before building it — brief, story, treatment, storyboard
2954
+ mode: on-demand
2955
+ - slug: video-style-vocabulary
2956
+ when: Choosing the visual style of a video or writing its style sheet
2957
+ mode: on-demand
2958
+ - slug: motion-quality-gates
2959
+ when: Checking a HyperFrames draft before showing it or rendering the final
2960
+ mode: on-demand
2961
+ - slug: math-explainer-manimgx
2962
+ when: Animating math, equations, graphs, algorithms or 3D concepts for a video
2963
+ mode: on-demand
2964
+ - slug: facecam-edit
2965
+ when: Editing face-camera, interview or screen-recording footage — cuts, captions, graphics
2966
+ mode: on-demand
2967
+ - slug: collage-broll
2968
+ when: Generating b-roll clips or illustrative stills for a video through fal.ai
2969
+ mode: on-demand