@synap-core/workspace-templates 0.12.0 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (245) 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 +174 -900
  57. package/dist/index.js +26148 -23931
  58. package/dist/index.js.map +1 -1
  59. package/dist/wire.d.ts +222 -0
  60. package/dist/wire.js +77 -0
  61. package/dist/wire.js.map +1 -0
  62. package/package.json +27 -4
  63. package/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.yaml +85 -39
  139. package/src/blockchain-ecosystem.yaml +1 -0
  140. package/src/brand-library.yaml +525 -14
  141. package/src/builder-dev-fold.template.test.ts +113 -0
  142. package/src/builder.yaml +779 -91
  143. package/src/business-model-catalog-description.test.ts +47 -0
  144. package/src/business-model-grp.template.test.ts +214 -0
  145. package/src/business-model.yaml +296 -49
  146. package/src/catalog-safety.test.ts +198 -0
  147. package/src/catalog.template-items.test.ts +75 -0
  148. package/src/catalog.ts +66 -2
  149. package/src/concept-guard.test.ts +28 -8
  150. package/src/concept-guard.ts +2 -2
  151. package/src/content-creation.template.test.ts +52 -0
  152. package/src/content-creation.yaml +192 -0
  153. package/src/content-os.edit-loop.template.test.ts +210 -0
  154. package/src/content-os.publish-due-posts.template.test.ts +261 -0
  155. package/src/content-os.template.test.ts +322 -0
  156. package/src/content-os.video-takes.template.test.ts +338 -0
  157. package/src/content-os.work-a-take.template.test.ts +125 -0
  158. package/src/content-os.yaml +1830 -39
  159. package/src/content-studio-retired.template.test.ts +82 -0
  160. package/src/corpus-flow-validity.test.ts +4 -5
  161. package/src/crm-absorbs-outreach.template.test.ts +117 -0
  162. package/src/crm-build-plan.template.test.ts +128 -0
  163. package/src/crm-lists.template.test.ts +96 -0
  164. package/src/crm-messages-cadence.template.test.ts +164 -0
  165. package/src/crm-one-send-door.template.test.ts +65 -0
  166. package/src/crm.yaml +1456 -181
  167. package/src/decisions-wave.template.test.ts +186 -0
  168. package/src/define.ts +130 -17
  169. package/src/design-to-implementation.template.test.ts +94 -0
  170. package/src/domain-template-type.test.ts +75 -0
  171. package/src/ecosystem.yaml +27 -66
  172. package/src/enterprise-os.yaml +10 -134
  173. package/src/facecam-edit.audio-studio.test.ts +188 -0
  174. package/src/fal-generate-image.test.ts +200 -0
  175. package/src/fal-media-lab-pod-files.test.ts +180 -0
  176. package/src/finance.yaml +254 -1
  177. package/src/foundation.yaml +78 -38
  178. package/src/frame-host-modules.d.mts +6 -0
  179. package/src/frame-host-modules.mjs +58 -0
  180. package/src/generate.mjs +9 -3
  181. package/src/github-merge.test.ts +210 -0
  182. package/src/grants.yaml +46 -17
  183. package/src/hr.yaml +39 -6
  184. package/src/index.ts +36 -1
  185. package/src/intent.test.ts +161 -0
  186. package/src/internal-runbook.yaml +19 -1
  187. package/src/jev.test.ts +356 -0
  188. package/src/legal.yaml +33 -1
  189. package/src/life-os.yaml +51 -240
  190. package/src/lifecycle-coverage.test.ts +124 -0
  191. package/src/lifecycle-declarations.test.ts +252 -0
  192. package/src/lifecycle-presets.test.ts +468 -0
  193. package/src/lifecycle-presets.ts +494 -0
  194. package/src/lifecycle-stuck.test.ts +46 -0
  195. package/src/lifecycle-waits.parity.test.ts +319 -0
  196. package/src/marketing.yaml +108 -4
  197. package/src/meetings-knowledge.yaml +18 -0
  198. package/src/networking.yaml +23 -2
  199. package/src/operations.yaml +304 -4
  200. package/src/pipeline-views.template.test.ts +88 -0
  201. package/src/playbook-goal-placeholders.test.ts +57 -0
  202. package/src/postiz-contract.test.ts +134 -0
  203. package/src/presentation-declarations.test.ts +174 -0
  204. package/src/process-contract.validate.test.ts +352 -0
  205. package/src/project-management.yaml +39 -740
  206. package/src/publish-body.d.mts +29 -0
  207. package/src/publish-body.mjs +74 -0
  208. package/src/publish-body.test.ts +74 -0
  209. package/src/publish-official-lib.test.ts +176 -0
  210. package/src/replicate-rvm.test.ts +504 -0
  211. package/src/research-base.yaml +119 -20
  212. package/src/research-decision.yaml +60 -3
  213. package/src/reserved-profile-slugs.test.ts +83 -0
  214. package/src/retired-goal-kinds.test.ts +58 -0
  215. package/src/role-reference-and-step-domain.test.ts +192 -0
  216. package/src/seed-obligation.test.ts +1 -6
  217. package/src/send-message-maps.test.ts +227 -0
  218. package/src/shared-role-ssot.test.ts +40 -2
  219. package/src/ship-a-change.template.test.ts +177 -0
  220. package/src/skills.test.ts +170 -0
  221. package/src/social.yaml +85 -27
  222. package/src/space-packages.freshness.test.ts +97 -0
  223. package/src/space-packages.generated.ts +40 -0
  224. package/src/space-packages.test.ts +125 -0
  225. package/src/space-packages.ts +73 -0
  226. package/src/start.test.ts +188 -0
  227. package/src/task-intents.test.ts +293 -0
  228. package/src/templates.ts +20851 -21496
  229. package/src/types.ts +443 -60
  230. package/src/unipile-publish-post.test.ts +162 -0
  231. package/src/validate-flow.drift.test.ts +2 -3
  232. package/src/validate-flow.ts +22 -2
  233. package/src/validate-onboarding.test.ts +59 -0
  234. package/src/validate.test.ts +71 -4
  235. package/src/validate.ts +1335 -7
  236. package/src/wire-core.ts +231 -0
  237. package/src/wire.test.ts +159 -0
  238. package/src/wire.ts +107 -0
  239. package/src/agent-fleet.yaml +0 -1645
  240. package/src/business-developer.template.test.ts +0 -351
  241. package/src/business-developer.yaml +0 -860
  242. package/src/content-studio.yaml +0 -361
  243. package/src/dev-dashboard.yaml +0 -990
  244. package/src/outreach-comms.yaml +0 -486
  245. package/src/the-arch.yaml +0 -201
@@ -0,0 +1,140 @@
1
+ ---
2
+ name: designing-with-real-components
3
+ description: Design a UI screen as Puck JSON built ONLY from the real components the Storybook manifest lists, save it as a ui-design in the pod, and move it through its lifecycle to a reviewed, implemented pull request. Use when asked to design, mock up or lay out a screen, section or panel for a Synap app, or to implement a saved ui-design.
4
+ metadata:
5
+ synap_native: true
6
+ auto_load: false
7
+ ---
8
+
9
+ # Designing with real components
10
+
11
+ A design is data, not a picture. It is **Puck JSON** whose every block is a **real component** from
12
+ our library, so the canvas renders it with the same code the product ships and a coding agent can
13
+ implement it without guessing. It lives in the pod as a **`ui-design`** entity. The process
14
+ "Design to implementation" (Builder space) carries it from draft to merged.
15
+
16
+ ```
17
+ Storybook manifest ──► you write Puck JSON ──► 3 ui-design variants ──► critique ──► person picks
18
+ (the catalog) (real types only) (one variant-group) (rubric) one on the canvas
19
+ │ approved (a person)
20
+ ▼
21
+ merged + verified ◄── gates + compare + dogfood ◄── coding agent opens a PR
22
+ ```
23
+
24
+ The process is started by a person or an agent asked to (`synap_run_playbook` on the request design):
25
+ it does not start itself on every new `ui-design`, because the sibling variants are `ui-design`s too.
26
+
27
+ ## 1. Read the catalog first — never from memory
28
+
29
+ The catalog of components is Storybook. Read it before you write a single block:
30
+
31
+ - **Manifest (static, always available):** `/manifests/components.json` in the Storybook build (and on
32
+ the dev server). Each entry is a component with its props (name, type, required, default) and story
33
+ examples. The set you may PLACE is narrower: only components with a story tagged `canvas` (tags are
34
+ in the build's `index.json`, not in `components.json`), and only props the canvas can express —
35
+ the canvas page lists what is placeable and why the rest is not. Nothing else.
36
+ - **Storybook MCP (dev, richer):** `http://localhost:6006/mcp` — list docs, show one component's docs,
37
+ find stories, preview a story. It is unauthenticated: reach it only on your own machine or through a
38
+ private tunnel, never a public URL.
39
+
40
+ If you cannot read either, stop and say so. Never design from what you remember a component looks like.
41
+
42
+ ## 2. The design JSON (Puck data)
43
+
44
+ ```json
45
+ {
46
+ "content": [
47
+ { "type": "SpatialPanel", "props": { "id": "SpatialPanel-1", "title": "Agents" } },
48
+ { "type": "SpatialButton", "props": { "id": "SpatialButton-1", "children": "Add agent", "size": "md" } }
49
+ ],
50
+ "root": { "props": { "title": "Settings › Agents" } },
51
+ "zones": {}
52
+ }
53
+ ```
54
+
55
+ - `content[]` is the page, top to bottom. Each item is `{ type, props }`; `props.id` is required and
56
+ unique (`<Type>-<n>` is fine).
57
+ - `type` MUST be a component name in the manifest, spelled exactly. `props` MUST be props that
58
+ component declares, with values of the declared type. A prop the manifest does not list is invalid.
59
+ - Prop values by manifest type: an enum → one of its literal values; `boolean` → `true`/`false`;
60
+ `string` → a string; `number` → a number. A function prop (`onClick`, `onChange`…), `className`,
61
+ `style`, `as` and `dangerouslySetInnerHTML` are never accepted — the canvas strips them.
62
+ - Nesting: a `children` prop of type `ReactNode` is a **slot** — it holds an array of `{ type, props }`
63
+ blocks. Every other `ReactNode` prop (`label`, `title`, `description`) is plain text. A component
64
+ whose manifest lists no `children` cannot hold blocks. `zones` is legacy Puck nesting: keep it `{}`.
65
+ - `root.props` holds page-level settings only.
66
+ - Colours, spacing, radius and type come from the components and the `--synap-*` tokens. Never put a
67
+ hex, rgb, pixel value or font family in a prop. Never put markup or HTML strings in props.
68
+ - Motion is NOT in the JSON props. List it in the design's `motion` property as
69
+ `[{ "trigger": "enter", "target": "SpatialPanel-1", "preset": "<named preset>" }]` — `target` is a
70
+ block `props.id`, `preset` a named motion preset of the design tokens (each carries its
71
+ reduced-motion fallback). Never a duration, easing or keyframe literal.
72
+ - Lay out every STATE the screen has — empty, loading, failed, full, long content — not only the happy
73
+ path. Each empty state is one of OMIT / INVITE / EXPLAIN / REASSURE (`.claude/rules/ui-composition.md`
74
+ §2); a failed read is its own state with a retry.
75
+
76
+ The canvas validates every design before it renders or saves it: an unknown `type` refuses the
77
+ whole design (it does not render); an undeclared or wrong-kind prop is stripped and listed. To check a
78
+ design you wrote, open the canvas (`#/design` in ui-gallery) and use **Paste JSON**.
79
+
80
+ ## 3. Save it in the pod
81
+
82
+ Find before you create: `synap_find` for an existing `ui-design` on the same `target-surface`.
83
+
84
+ - **Create:** `synap_create_entity` with `profileSlug: "ui-design"`, a `title`, and `properties`:
85
+ `target-surface` (the app and screen), `design-scope` (`component` or `page`), `design-goal` (the job
86
+ the screen does for its reader — the critique judges against it), `design-json` (the object above),
87
+ `components-used` (the distinct `type` values in the JSON — derived from it, never typed by hand),
88
+ `schema-version` (`1`), `motion` (above), `current-story-id` (the Storybook story id of the screen as
89
+ it is today, when it exists), `design-notes` (intent, open questions). A new design starts at `draft`.
90
+ - **Three variants by default.** One request = THREE designs: the request itself and two sibling
91
+ `ui-design`s, all with the same `variant-group` (any stable id, e.g. the request's entity id) and the
92
+ same `target-surface`, `design-scope` and `design-goal`. Make them genuinely different answers
93
+ (structure, density, order), not three colourings of one layout. Link each sibling to the request
94
+ with `relates_to`. A person may ask for one; then make one.
95
+ - **References** (the founder's own captures first, then files or screens to follow): link them from
96
+ the design with the `references` relation (`synap_link_entities`). Read them before you generate.
97
+ - **Revise:** `synap_update_entity` with the new `design-json` and `components-used` together.
98
+ - `proposed` is success: a person reviews the write. Do not retry it.
99
+
100
+ ## 3b. Critique before review
101
+
102
+ Before a person sees the variants, critique each with the **design-critique** skill (its rubric:
103
+ `checked` · `pattern` · `opinion`, stable rule ids) and write the findings into the variant's
104
+ `critique`. Fix every `checked` or `pattern` blocker and critique again; leave opinions for the person.
105
+
106
+ ## 4. Move it only through its lifecycle
107
+
108
+ `design-status`: `draft` (open) · `in-review` · `approved` · `implementing` (active) · `implemented`
109
+ (done) · `archived` (lost).
110
+
111
+ - Every move is `synap_update_entity` with `design-status` set to the new option AND `expectedStatus` =
112
+ the status you read. `conflict` means someone moved it: re-read, decide again.
113
+ - **`approved` is a person's call.** You never set it. Put the design `in-review`, ask for the review,
114
+ and wait. A rejection sends you back to revise the JSON.
115
+ - Implement only from an `approved` design. Then move it to `implementing` (inside the "Design to
116
+ implementation" process its Implement stage has already done that — do not move it again), read
117
+ `design-json` and the Storybook MCP, build the screen from the same components, open a pull request
118
+ and record its URL in `implementation-pr`.
119
+ - Verify before `implemented`: typecheck, story coverage, interaction and accessibility tests, visual
120
+ regression, the token guards, and a walk of the screen in the running app. Inside the process you
121
+ stop after Verify: Done is a person's gate and writes `implemented`. Outside it, a move into
122
+ `implemented` proposes; a person confirms the merge.
123
+ - Never invent a status and never keep your own stage field.
124
+ - At review, the person picks ONE variant. Inside the process the run carries the request design: if
125
+ the person chooses a sibling, write that sibling's `design-json`, `components-used` and `motion` into
126
+ the request design before it is approved; archiving the others is the person's call.
127
+ - Implementing: record the built screen's story id in `implemented-story-id`, capture it with the
128
+ Storybook capture door (`pnpm --filter @synap-core/storybook capture`) and compare it with the design
129
+ and with `current-story-id`.
130
+
131
+ ## Do not
132
+
133
+ - Invent a component, a variant or a prop the manifest does not list. If the screen needs something
134
+ the library lacks, say so in `design-notes` and propose the component (a story first) — do not
135
+ fake it with a generic box.
136
+ - Hand-write colours, sizes or fonts; use tokens through the components.
137
+ - Save a screenshot as the design. The JSON is the design; a `screenshot` is only for review.
138
+ - Set `approved`, skip the review, or implement from a draft.
139
+ - Mirror components into the pod as entities. The manifest is the catalog; there is no second one.
140
+ - Register the dev Storybook `/mcp` as a public pod MCP server.
@@ -0,0 +1,12 @@
1
+ {
2
+ "apiVersion": "synap/v1",
3
+ "kind": "skill",
4
+ "meta": {
5
+ "slug": "designing-with-real-components",
6
+ "name": "Designing with real components",
7
+ "description": "Design a screen as Puck JSON built only from the real components the Storybook manifest lists, save it as a ui-design in your pod, and take it through review to an implemented, verified pull request.",
8
+ "tags": ["skill", "design", "ui", "storybook", "builder"],
9
+ "isPublic": true
10
+ },
11
+ "dependencies": []
12
+ }
@@ -0,0 +1,25 @@
1
+ {
2
+ "apiVersion": "synap/v1",
3
+ "kind": "skill",
4
+ "meta": {
5
+ "slug": "edit-loop",
6
+ "name": "Edit loop",
7
+ "description": "The one edit-run contract every engine shares: the layer-<pass>, studio, studio-app and needs-tool-<tool> slots, the three answers (Approve, Rework, Use my edit), a version before every pass, work from the diff, never write while a pass is the person's. Read before building any visual or video post in passes.",
8
+ "tags": [
9
+ "skill",
10
+ "content",
11
+ "video",
12
+ "visual"
13
+ ],
14
+ "isPublic": true
15
+ },
16
+ "spec": {
17
+ "slug": "edit-loop",
18
+ "name": "Edit loop",
19
+ "kind": "instruction",
20
+ "scope": "pod",
21
+ "description": "The one edit-run contract every engine shares: the layer-<pass>, studio, studio-app and needs-tool-<tool> slots, the three answers (Approve, Rework, Use my edit), a version before every pass, work from the diff, never write while a pass is the person's. Read before building any visual or video post in passes.",
22
+ "body": "# Edit loop: one contract for every edit run\n\nRead this before building any visual or video post in passes (Content OS, Produce Content, Draft stage). It holds what every engine shares: the slots, the three answers, and the rules that keep the person's edits safe. The engine's own skill says how to build each pass. The keys below are read by Content Studio, Relay and the browser, and by runs already in flight: never rename them, never invent a sibling.\n\n## 1. Engine and passes by format\n\n| Format | Engine | Engine skill | Where the person edits | Passes, in order |\n|---|---|---|---|---|\n| Video (short or long) | HyperFrames on the person's computer | `video-toolbox`, then `facecam-edit` for footage | HyperFrames Studio, or the HyperFrames app | `cut`, `background`, `colour`, `captions`, `graphics` (skip what the route does not use) |\n| Carousel, LinkedIn PDF | Layered composition in the pod | `carousel-format` | Content Studio, the post's Layers tab | `structure` (structure and copy), `visual`, `polish` |\n| Image post | Layered composition in the pod (one slide) | `carousel-format` | Content Studio, the post's Layers tab | `visual`, `polish` |\n| Deck, PDF document, presentation | Open Design on the person's Mac | `open-design-edit` | Open Design | `structure`, `visual`, `polish` |\n\nA run waits for the computer its engine needs: there is no fallback engine.\n\n## 2. The keys\n\n- **`layer-<pass>`**: one slot per pass (`layer-cut`, `layer-structure`, `layer-visual`…).\n - When the pass starts: declare it as yours, `{ key: \"layer-<pass>\", kind: \"file\", label: \"<Pass name> pass\" }` with no owner and no ask, so the person sees you working on it.\n - When its preview exists in the pod: hand it to the person on the SAME key: kind `file`, owner `human`, blockedReason `decision`, why `Approve the <pass> on its preview`, `ref` the preview's file entity, ask `choose` with `allowOther`, options **Approve** (recommended: keeps this pass and builds the next), **Rework** (redoes this pass only) and **Use my edit** (the person changed this pass in its editor). The video passes `background` and `colour` add **Remove this pass**. Then stop until the answer.\n - This key replaces the worker's generic `ask-<slug>` for a pass. Use `ask-<slug>` only for a question that is not a pass (free disk, a go-ahead).\n- **`studio`**: where the person edits, while it answers: `{ key: \"studio\", kind: \"link\", label: \"<editor name>\", required: false, ref: { url } }` (HyperFrames Studio, Open Design). Whenever that address stops answering (the run leaves Draft or ends, the worker stops, you stop the editor): `retireOutput studio`, so no page links a dead address. Back at Draft: start it again and record the new address. A layered composition records none: its editor is the post's own Layers tab.\n- **`studio-app`**: the desktop app you opened the work in, when the person asked for it: `{ key: \"studio-app\", kind: \"app\", label: \"<App> on <this computer's name>\", required: false }`, then `completeOutput studio-app`.\n- **`needs-tool-<tool>`**: a tool the engine needs is missing or fails. Never install anything, never work around it. One human slot per tool: `{ key: \"needs-tool-<tool>\", kind: \"decision\", label: \"<Tool> on <this computer's name>\", owner: \"human\", blockedReason: \"capability\", why: \"<which tool fails on which computer, and the error's first line>\", unblocks: \"the edit goes on from where it stopped\", ask: { mode: \"choose\", options: [{ label: \"I installed it\", recommended: true, description: \"<the one install or start command>; the agent checks again and goes on\" }, { label: \"Use another agent\", description: \"Choose another agent for this post in Content Studio Settings, then resume\" }] } }`. Post one room line, end the run blocked. \"I installed it\": check again; still missing, say so on the same slot and stop again; present, `completeOutput` it and go on. \"Use another agent\": `retireOutput` it, say the run waits for that agent, end.\n\n## 3. The answers\n\n- **Approve**: `completeOutput layer-<pass>` (the preview is its evidence), then build the next pass.\n- **Rework**: redo only what the note names, from the work as it is now (the person's edits included). On slides, only the slides the note names; every other slide stays byte for byte. Point the slot at the new preview and ask again.\n- **Use my edit**: the person made this pass themselves. Read what they changed (§4, the diff from your last version), keep it as the pass and change nothing of it, render the preview from the work as they left it, point the slot at it and ask again.\n- **Remove this pass** (video background and colour): drop the pass, `retireOutput layer-<pass>`, build the next.\n- An answer in the person's own words (allowOther) is a Rework note unless it plainly says one of the above.\n\n## 4. Rules for every engine\n\n1. **A version before every pass of yours.** Before you write anything for a pass, record the work as it is now, so the person's edits are the baseline and your pass can be told apart from them. HyperFrames: `history begin`. Layered composition: the composition document's `revision` and a copy of its JSON (`~/synap-work/<run session id>/versions/r<revision>.json`). Open Design: the person's saves are `manual` versions; after each of your writes, post an `ai` version. Name that version in your room line for the pass.\n2. **Work from the diff, never from the prompt.** Rework and Use my edit start from the current work and the diff between your last version and it: on slides, slide by slide (by slide number, then layer id); in HyperFrames, `history --since mine`. Never regenerate a pass, a slide or a page from the brief or the script: that would erase what the person did.\n3. **Never write while a pass is the person's.** From the moment `layer-<pass>` is owner `human` until it is answered, write nothing to the work: no file, no layer, no project change, no version. A question asked meanwhile (open the app, start the editor) is answered without touching the work.\n4. **Acknowledge every answer in the room.** In the turn the answer wakes you, post one line in the run's room: `Got <answer> on <pass>: <what you do now>`. This line is how the person knows the answer reached you; never act silently. An answer you cannot act on yet (a tool is missing) is acknowledged too, with why.\n5. **One pass at a time.** The next pass starts only after the answer; changing a pass never redoes an approved one.\n6. **The pod is the record.** Every preview and final file is uploaded to the pod and linked to the post before its slot points at it (read the file entity back first). Nothing publishes itself: agents never publish; the post goes to Review with its files.\n7. **Leaving Draft** (Review, a stop, the end): stop the editor processes you started and `retireOutput studio`.\n\n## Sources\nFounder decisions D0-D2 (2 Nov 2026), build plan \"Static asset edit loop\" (pod document ab3e0a5a), the video pass contract (Produce Content Draft stage, founder D3, 27 Oct).\n"
23
+ },
24
+ "dependencies": []
25
+ }
@@ -0,0 +1,24 @@
1
+ {
2
+ "apiVersion": "synap/v1",
3
+ "kind": "skill",
4
+ "meta": {
5
+ "slug": "facecam-edit",
6
+ "name": "Face-camera editing",
7
+ "description": "Turns raw face-camera, talking-head or screen+camera footage into a finished cut: word-level transcript, semantic cuts in a HyperFrames project shown live in HyperFrames Studio, HyperFrames captions and graphics, QA, pod save. Use when editing recorded footage of a person speaking.",
8
+ "tags": [
9
+ "skill",
10
+ "content",
11
+ "video"
12
+ ],
13
+ "isPublic": true
14
+ },
15
+ "spec": {
16
+ "slug": "facecam-edit",
17
+ "name": "Face-camera editing",
18
+ "kind": "instruction",
19
+ "scope": "pod",
20
+ "description": "Turns raw face-camera, talking-head or screen+camera footage into a finished cut: word-level transcript, semantic cuts in a HyperFrames project shown live in HyperFrames Studio, HyperFrames captions and graphics, QA, pod save. Use when editing recorded footage of a person speaking.",
21
+ "body": "# Face-camera editing\n\nRaw face-camera or screen+camera footage → a finished cut. The recording is the authority for what was said: remove what failed, never invent speech.\n\n## Hard rules\n\n- Never generate the person: no lip-sync, avatar, face swap or synthetic voice of the speaker unless the user explicitly asks.\n- The person may edit the project in HyperFrames Studio at any time: build on their edits, never overwrite them (§4).\n- Never cut inside a word. Every cut edge snaps to a word boundary from a word-level transcript.\n- Confirm the cut strategy in plain words before touching the timeline. Render the final only after one human review.\n- Revisions change only what was flagged.\n- **The voice is encoded once after the take, never again.** The cut's audio is `media/cut-audio.m4a`, built from the take at 256 kb/s or more (§4). A render's own audio is never shipped: every render the person hears, a pass's preview included, is followed by a remux that copies the cut's audio stream, checked by hash before upload (§8). A re-encode for size touches the video only (`-c:a copy`). Loudness changes only as a step the person approved (§4), never silently. Why (31 Oct, run 22dcabe8): the delivery was re-encoded to fit the upload, the voice's third AAC encode at 125 kb/s, and its coding noise sat about 5 dB under the voice at 12-16 kHz; the founder heard it.\n- **The passes follow the `edit-loop` skill**: its slots (`layer-<pass>`, `studio`, `studio-app`, `needs-tool-<tool>`), its three answers (Approve, Rework, Use my edit) and its rules (a version before every pass, work from the diff, never write while a pass is the person's, acknowledge every answer in the room). Here the passes are `layer-cut`, `layer-background`, `layer-colour`, `layer-captions` and `layer-graphics`; background and colour add Remove this pass. Content Studio draws a pass, its preview and its question only from its `layer-<pass>` slot. Use `ask-<slug>` only for a question that is not a pass (free disk, a go-ahead), and `needs-tool-<tool>` for a missing tool (§1 step 0).\n\n## 1. Recall and set up\n\n0. **Check the tools first, before anything else.** On this computer run `node --version` (20 or later), `ffprobe -version`, `ffmpeg -version` and `HYPERFRAMES_NO_TELEMETRY=1 npx --yes hyperframes --version`. If one is missing or fails, do not work around it and do not start the run's passes: declare ONE `needs-tool-<tool>` slot per missing tool, shaped and answered as the `edit-loop` skill spells out, its \"I installed it\" description naming the one install command for this OS (e.g. `brew install ffmpeg`). Then post one room line naming what is missing, and end the run blocked. Never install anything yourself.\n Also note whether the HyperFrames desktop app is on this computer: `test -d /Applications/HyperFrames.app || test -d ~/Applications/HyperFrames.app`. Say it in one room line with the checks' result (`HyperFrames app: installed` or `HyperFrames app: not installed`). A missing app is not a slot by itself: it becomes one only when the person asks for the app (step 6).\n1. `synap_ask` for the brand kit (palette, caption font, lower-third template) and any approved `content-composition` to reuse.\n2. Record with Cap (separate camera, screen, mic tracks); keep originals untouched.\n3. `ffprobe` every source. Pick the delivery frame grid once (default 30 fps; 1080x1920 or 1920x1080) and keep it everywhere.\n4. **One project per video run: `edit/`.** It lives in the run's work folder, `~/synap-work/<run session id>/edit` (the folder `synap worker` gives the run): `HYPERFRAMES_NO_TELEMETRY=1 npx hyperframes init edit --non-interactive --skip-transcribe --resolution <portrait|landscape|square>`, the takes copied into `edit/media/`. A variable frame rate take is made constant once (§6, step 1: `media/take-cfr.mp4`) and the project uses that copy. Every pass happens in this project and every command below runs from `edit/`.\n5. **Start Studio and record its address.** `npx hyperframes preview --background --port <a free port, from 3002 up>`; check that `http://localhost:<port>/#project/edit` answers HTTP 200, then record it on the run as an outcome: `{ key: \"studio\", kind: \"link\", label: \"HyperFrames Studio\", required: false, ref: { url: \"http://localhost:<port>/#project/edit\" } }`. Content Studio draws it as \"Open in HyperFrames Studio\". Studio hot-reloads, so the person watches each pass as you write it. Whenever your preview process stops (the run leaves Draft or ends, the worker stops, or you stop it): `npx hyperframes preview --stop` and `retireOutput: \"studio\"`, so Content Studio never links a dead address. Back at Draft (Review sent it back): start it again and record the new address.\n6. **Studio asks from Content Studio.** \"Studio\" names a place, and Content Studio asks for one in the run's room. Answer between passes; an ask never undoes a pass.\n - **app** (\"Open this edit in the HyperFrames desktop app on this Mac (open the project folder in it), then wait\"). Installed (step 0): `open -a HyperFrames \"$HOME/synap-work/<run session id>/edit\"`, the absolute path of `edit/` as the document to open. That works whether the app is running or not; `open -a HyperFrames --args <dir>` reaches the app only when it is not running yet, and is ignored by a running app. The app adds the folder to its Home as a link (named `edit`, then `edit 2`, `edit 3`… across runs), so the person's edits land in this project, and §4's history turns still hold. Then record `{ key: \"studio-app\", kind: \"app\", label: \"HyperFrames app on <this computer's name>\", required: false }`, `completeOutput: \"studio-app\"`, post one room line and wait for the person. Not installed: declare the human slot `{ key: \"needs-tool-hyperframes-app\", kind: \"decision\", label: \"HyperFrames app on <this computer's name>\", owner: \"human\", blockedReason: \"capability\", why: \"The HyperFrames desktop app isn't installed on <this computer's name>\", unblocks: \"the edit opens in the HyperFrames app\", ask: { mode: \"choose\", options: [{ label: \"I installed it\", recommended: true, description: \"Download it from https://hyperframes.dev/studio/download, then the agent opens the edit in it\" }, { label: \"Use the browser Studio\", description: \"The agent starts Studio in the browser instead\" }] } }`. On \"I installed it\": check again; there, open the edit as above and settle the slot as step 0 does; still missing, say so on the same slot. On \"Use the browser Studio\": `retireOutput: \"needs-tool-hyperframes-app\"` and do **browser**.\n - **browser**: start Studio and record `studio` as in step 5 (an address of yours that still answers HTTP 200 is reused, never a second server).\n\n## 2. Transcribe (word level)\n\n- Word-level, verbatim ASR only: fillers and false starts stay in the text (they are the signal); phrase/SRT output loses the gaps you cut on.\n- One word-level ASR pass per source (`npx hyperframes transcribe <file> --engine parakeet`, or media-use's `transcribe.mjs`), only when the pod's transcript (below) has no word timings yet. Cache per source; never re-transcribe.\n- The pod keeps a take's transcript as ONE `content-asset` (Kind `script`, File ID = the take's file): its body holds the readable text under a `## Transcript` heading, then a `## Words` heading followed by exactly one ```json block of `[{\"text\",\"start\",\"end\"}]` in seconds (`[]` until the precise pass fills it). Find it before transcribing; if it exists, reuse it and only fill the word timings; if not, create it and link it to the take's file with `references`. Never a separate document.\n- Build a reading view: one line per phrase, `[start-end] text`, breaking on any silence of 0.5 s or more.\n\n## 3. Plan cuts on the transcript (semantic first)\n\nThree passes, in this order:\n\n1. **Semantic.** Remove misspeaks, failed starts, repeated takes, production chatter (\"wait, again\"). Keep the last complete, correct take by default; keep deliberate emphasis and rhetorical self-correction. Remove a filler only when context shows it is a standalone filler, not part of a word or a meaningful pause.\n2. **Pauses.** Remove dead air and restart preparation, including pauses inside kept phrases. Silence guide: 400 ms or more = clean cut target; 150-400 ms = usable after a visual check; under 150 ms = mid-phrase, do not cut. For a dB scan, look at -30, -35 and -40 dB with a 0.45 s minimum duration; a scan reports intervals, it does not authorise edits. Leave ambiguous breaths or possibly quiet words for the user to hear.\n3. **Edges.** Only after 1 and 2 are applied, tighten each kept clip to the real sound: first and last sustained activity on the louder channel (measured in 10 ms windows), keep one frame of margin, and back off to lower-level tails rather than clipping a decaying word.\n\n**Invisible cuts (picture continuity).** A cut the viewer notices is a failed cut (founder, 29 Oct). A cut that sits in a pause may slide inside that pause without touching speech, so put each edge where the face matches across it:\n- Score every out/in frame pair inside the pause: face position and size (a face detector), plus the grey-level difference of the face crop. Take the pair with the smallest jump, with a small cost for each extra second of silence kept (about 0.25 per second) so the pause does not grow back, and still remove at least 0.25 s.\n- A head turn inside the pause disappears with the cut: leave on the frame before the turn and come back on a frame already facing the camera, or the reverse. Never cut from mid-turn to facing the camera.\n- Never move an edge into speech, and never drop a word to gain continuity. When no pair matches (a large jump remains), keep the cut and flag it: a punch-in of about 110% on the incoming side hides it, or the user decides.\n- Report each cut's jump before and after. Pilot, 29 Oct (take b36227cd, 13 cuts): the mean jump fell from 0.59 to 0.29, for 4 s more runtime.\n\nPadding: 30-200 ms working window around every kept word (ASR drifts 50-100 ms). A good default is 50 ms before the first kept word and 80 ms after the last; tighter for punchy social, looser for calm explainers. Hand-offs between speakers: 400-600 ms of air. Extend past laughs; the reaction is the beat.\n\nWrite the plan as `{source, start, end, quote, reason}` ranges, state the runtime, show the user a short strategy, and wait for confirmation. In Produce Content this plan is the Script, and its approval (the Draft stage's gate) is the confirmation.\n\n## 4. Apply the cut\n\n- **Editor of record: HyperFrames.** The cut is the project's footage track, never a baked file: one `<video>` clip per kept range on track 0, all on the same source (`media/take-cfr.mp4`), edge to edge, each with an `id` and `data-has-audio=\"true\"`: `data-media-start` = the range's start in the source, `data-duration` = its length, `data-start` = the sum of the kept lengths before it (hyperframes-core's hard-cut recipe). media-use's `transcript-cut.mjs --plan` compiles the kept segments from the words and the removals: lay them as clips, never encode them. Studio shows the cut as it is written, and the person can trim any clip there.\n- Put a 30 ms volume ramp in and out on every clip, in its `data-automation` volume lane (`{\"t\":0,\"v\":0},{\"t\":0.03,\"v\":1},{\"t\":<dur-0.03>,\"v\":1},{\"t\":<dur>,\"v\":0}`), otherwise every cut pops.\n- **The cut's audio, encoded once.** Build `media/cut-audio.m4a` from the take on the footage clips' ranges, with the same 30 ms ramps, in ONE ffmpeg call: one `atrim` per clip in clip order, start = its `data-media-start`, end = start + its `data-duration`:\n ```\n ffmpeg -i media/take-cfr.mp4 -filter_complex \"[0:a]atrim=start=<s0>:end=<e0>,asetpts=PTS-STARTPTS,afade=t=in:d=0.03,afade=t=out:st=<d0-0.03>:d=0.03[a0];[0:a]atrim=…[a1];…;[a0][a1]…concat=n=<N>:v=0:a=1[a]\" -map \"[a]\" -c:a aac -b:a 256k -ar 48000 media/cut-audio.m4a\n ```\n When the footage track starts after 0 (an intro card), add `adelay=<ms>:all=1` after the concat. Rebuild it before every render in which a footage clip changed (your edit or the person's trim in Studio); it takes seconds. Never build it from a file that is already a re-encode of the take.\n- **Loudness is the person's call, asked once.** Measure the cut's audio: `ffmpeg -i media/cut-audio.m4a -af ebur128=peak=true -f null -` (integrated `I:` in LUFS, true peak). When it is more than 2 LU away from -14 LUFS (a Reel or a Short; the take of 31 Oct sat at -25), ask on the slot `ask-loudness`, beside the cut's question: ask `confirm`, prompt \"Raise the voice to about -14 LUFS? It is <I> LUFS now.\", yes recommended. Yes: rebuild the cut's audio once with a two-pass `loudnorm=I=-14:TP=-1:LRA=11` at the end of the same filter (first pass `print_format=json`, second pass with its `measured_I`, `measured_TP`, `measured_LRA`, `measured_thresh` and `offset`), still one encode. No: leave it. Never normalise without that answer, and never on a render.\n- Read the cut back with `npx hyperframes timeline --json`, then `lint` and `check`. The pass's preview is a draft render of the project (`npx hyperframes render --quality draft`).\n- Every cut must work on audio AND picture (check the screen side too).\n- Hand the user the cut to listen to before dressing it.\n\n**One history turn per pass; the person's edits win** (the version before every pass, edit-loop §4).\n- Start each pass with `npx hyperframes history begin --who <your agent name> --label \"<pass>\"`, then `npx hyperframes history --since mine --who <your agent name>` to see what the person changed in Studio since your last turn. Build on their edits; never overwrite them. End the pass with `npx hyperframes history end`.\n- A check failed, or the person says it got worse: `npx hyperframes history undo --who <your agent name>` takes back only your newest turn, never theirs.\n- **Use my edit** (an answer on a pass's question): the person made that pass themselves in Studio. Read what they changed (`history --since mine`), keep it as the pass and change nothing of it, render the pass's preview from the project as it is, point the slot at it and ask again.\n\n## 5. Dress in HyperFrames\n\nUse HyperFrames `embedded-captions` (captions only) or `talking-head-recut` (captions, lower-thirds, graphics, b-roll). The cut is a media layer; captions and graphics are HTML on a paused GSAP timeline. Deterministic only: no wall-clock, no requestAnimationFrame loops, seeded randomness, no CSS transitions or keyframes for motion, entrances via `fromTo`, no infinite repeats, `power3`-style easing, no bounce by default.\n\n**Caption timing.** Map words to the output timeline: `output_time = word.start - segment_start + segment_offset`. Cues are half-open integer frame windows `[startFrame, endFrame)`; adjacent cues may touch, never overlap, fade tails included. Captions are the top layer, added last, so no overlay hides them.\n\n**Caption segmentation.** Decide in this order: keep a word, name, number+unit or fixed phrase whole; break at a clause, punctuation, breath or topic reset; keep the cue readable for its duration; then measure the rendered width.\n- One rendered line per cue. Latin scripts: about 30-32 characters at most; CJK: about 10 characters.\n- Target 0.8-2.5 s per cue, never under 0.5 s; grow a cue rather than flash one shorter than about 0.35 s.\n- Never strand a lone article, particle or conjunction.\n- The concatenated cues must reproduce the approved transcript exactly; terminology corrections are a lexicon applied once to all cues.\n- Caption font = the brand font; small text contrast at least 4.5:1; no opaque bar; vertical default sits about 330 px above the bottom of a 1920 px frame.\n\n**Motion graphics timed to speech.** A graphic adds what captions cannot (evidence, relationship, scale, structure, next action); plain narration stays caption-only.\n- Bind each spoken element to its own keyword onset from the word timestamps: it enters on the first frame at or after that word, never on a fixed stagger or ahead of the speech. Neutral headings may enter at the beat start; connectors may follow a cue by a few frames.\n- Hold the resolved state long enough to read, at least 1 s before exit. On A-roll, one-fact groups replace each other after 1.8-3.0 s; a dependent sequence may build in one scene and exit together.\n- Background cards appear at their final size; content reveals inside them, the card never grows.\n- Lower-thirds: name and role from the pod record, once per speaker. B-roll: reuse a `content-asset` first; for new b-roll see the `collage-broll` skill.\n\n**Density and layout.**\n- Never cover the face. Overlays go in the upper-middle area (around 280 px from the top of 1080x1920), horizontally centred, sized to clear both face and captions.\n- Canvas clearance: 54 px horizontal, 88 px vertical; panel padding 48-72 px.\n- One focal group plus 1-4 supporting elements; layouts hold 1.8-3.5 s before a major change; about 6-8 graphics per 2 minutes as a starting point, not a quota.\n- Too empty: enrich the focal idea. Too crowded: drop support elements before shrinking text.\n\n## 6. Background and colour from the booth look\n\nContent Studio's booth records the RAW take and keeps the look the person picked before recording. Produce Content hands it to the run as the `look` param (`session.metadata.params.look`), a JSON string:\n\n```json\n{ \"background\": { \"assetId\": \"…\", \"fileId\": \"…\" } | { \"mode\": \"blur\" } | null,\n \"colour\": { \"assetId\": \"…\", \"fileId\": \"…\", \"intensity\": 1 } | null }\n```\n\n`assetId` is a Brand `brand-asset` (tag `booth-background`: a JPG or PNG; tag `colour-look`: a 33³ `.cube`); `fileId` is the file at its `asset-file-id`. Fetch a file with `GET /api/hub/files/<fileId>/url` (a signed URL: `{ url, expiresAt, mimeType }`), then download that URL.\n\nWith a look, the Background and Colour passes START from it. They stay passes: built right after the cut, background then colour, before captions and graphics (what is under the person first, then what sits on top), each with its own preview, its own slot (`layer-background`, `layer-colour`) and its own question: Approve, Rework, or Remove this pass. No look, or a `null` part of it: build that pass as you would without a look.\n\n**The raw take is never overwritten.** A pass writes new files beside it, so removing a pass is always possible.\n\n**Background pass.** The cut-out comes from the person's own matting tool (intent `edit_video`); the local matte is the fallback, never the first choice: it squashes each frame to 320×320 and has no memory between frames, so hair flickers and keeps the old room's colour. Whichever made the matte, the person goes over the background the look chose: the photo, or the blurred room for `mode: blur`. A person with no matting tool still gets their photo, with no setup.\n\n1. **Prepare the take once.** Matte the WHOLE raw take, not the cut, so every kept range keeps its source times. A variable frame rate take is made constant first (`ffmpeg -i <raw-take> -vf fps=<fps> -c:v libx264 -crf 16 -c:a copy media/take-cfr.mp4`) and stored in the pod (`synap_store_file`) beside the raw take; a constant-rate take is used as it is. What the tool gets is that take's pod `file` entity id, never a URL you copied.\n2. **Matte it with the person's `edit_video` tool.** `synap_list_capabilities({ intent: \"edit_video\" })` names the verb (Replicate RVM: `replicate_rvm_matte`). Run it with the take's file id in the verb's pod-file parameter and nothing else: RVM takes `{ \"videoFile\": <file entity id> }`. The pod signs the file when the run starts (a verb parameter declared `file:url`), so an approval that lands hours later still sends a live link; never fetch a signed URL yourself for it. A tool that declares no pod-file parameter takes `{ \"video\": <https URL> }`: get one with `GET /api/hub/files/<fileId>/url` just before the run (it lives one hour). A run that sends a pod file always comes back proposed (it sends the person's file out and bills their account): hand them the review link and wait for it. The result:\n - `status: \"succeeded\"`: `alphaUrl` (the matte, white = the person) and `foregroundUrl` (the model's own colour for the person, edges included);\n - `status: \"processing\"`: run its `next` (the read verb and parameters it names) every 30-60 s, for up to 15 min. Never run the matte verb again for the same take: it would bill again.\n Download every URL at once (`media/alpha.mp4`, `media/fgr.mp4`): providers delete them (Replicate after about an hour).\n3. **Gate the matte** before anything uses it. It passes only if all hold:\n - frames: `ffprobe -v error -count_frames -select_streams v:0 -show_entries stream=nb_read_frames,width,height,duration -of json` on the take, the alpha and the foreground: same size, frame counts within 1, durations within one frame;\n - no flicker: `ffmpeg -i media/alpha.mp4 -vf \"format=gray,tblend=all_mode=difference,signalstats,metadata=print:key=lavfi.signalstats.YAVG:file=media/alpha-diff.txt\" -f null -`; the mean change between two frames stays under 4 (of 255) on at least 99% of frames;\n - the person never vanishes: `signalstats` YAVG of the alpha itself never drops under half its median;\n - look at the 3 frames with the largest change (hair, a hand, a held object) and say what you saw.\n These thresholds are a starting point, not measured yet: say when a take fails one by a hair. A failed gate goes to the fallback (step 7) and says which check failed.\n4. **Defocus the background once.** An image look: one still, scaled to cover the frame and blurred, never per frame: `ffmpeg -i <background> -vf \"scale=<W>:<H>:force_original_aspect_ratio=increase,crop=<W>:<H>,gblur=sigma=12\" -frames:v 1 media/bg.png`. `mode: blur`: the real room WITHOUT the person, lightly blurred, like a phone's depth of field: the room's shapes still read. Blurring the take itself smears the person into the room as a dark halo, and a heavy blur reads as an effect, not a lens (founder, 2 Nov 2026: \"even the blur is ugly\"). Weight the take by where the person is not, blur it, and divide by the blurred weight:\n ```\n ffmpeg -i media/take-cfr.mp4 -i media/alpha.mp4 -filter_complex \"\n [1:v]format=gray,negate,split[inv][invw];[inv]format=gbrp[inv3];[0:v]format=gbrp[t];\n [t][inv3]blend=all_mode=multiply,gblur=sigma=9[num];[invw]gblur=sigma=9,format=gbrp[den];\n [num][den]blend=all_expr='if(gt(B\\,3)\\,min(255\\,A*255/B)\\,A)',format=yuv420p[room]\" \\\n -map \"[room]\" -an -c:v libx264 -crf 16 media/room-blur.mp4\n ```\n sigma 9 is for a take 1080 pixels wide: scale it with the width. Where the person hides the room entirely the result is black; it is always under the person.\n5. **Composite the person with ffmpeg**: the model's foreground colour, the matte choked one pixel and feathered, and a light wrap (the background's light bleeding over the person's edge). The person stays its own transparent layer, so HyperFrames can treat the background and the person apart:\n ```\n ffmpeg -i media/fgr.mp4 -i media/alpha.mp4 -loop 1 -framerate <fps> -i media/bg.png -filter_complex \"\n [1:v]format=gray,erosion,gblur=sigma=1.2,split[m][mw];\n [mw]negate,gblur=sigma=8[out];[m]split[m1][m2];[m2][out]blend=all_mode=multiply,lut=c0=val*0.35[band];\n [2:v]gblur=sigma=20,format=yuva420p[glow];[glow][band]alphamerge[wrap];\n [0:v]format=yuva420p[fg];[fg][m1]alphamerge[p];[p][wrap]overlay=shortest=1[person]\" \\\n -map \"[person]\" -c:v libvpx-vp9 -pix_fmt yuva420p -b:v 0 -crf 24 -an media/person.webm\n ```\n For `mode: blur` the third input is `media/room-blur.mp4` without `-loop 1 -framerate <fps>`, and the light wrap is left out (`[p]` is the person, no `[wrap]`): the room's light is already the person's. The chain is a starting point, not yet tuned: check the edge on sample frames, and raise the choke or lower the wrap strength (`0.35`) when a halo shows.\n6. **Lay it in HyperFrames.** Under the person, on the same ranges as the cut: the background (`media/bg.png`, `object-fit: cover`, for the whole video) or the blurred room (cut on the same ranges). The person video (`media/person.webm`) takes the raw take's place in the cut (same source in and out points; the sound still comes from the take); captions and graphics stay above it. Then:\n - grain on the background, so it matches the camera's noise: `npx hyperframes media-treatment --project . --file index.html --selector '<background>' --grading '{\"details\":{\"grain\":0.1}}' --apply --json`;\n - light match on the person: measure both with `ffmpeg -v error -i <file> -vf \"signalstats,metadata=print:file=-\" -f null -` (YAVG for brightness, UAVG/VAVG for the colour cast), then set the person's `adjust` section (exposure and warmth; `npx hyperframes media-treatment --capabilities --json` names the keys) until the person sits in the background's light. Small moves: the Colour pass grades both after this.\n7. **Fallback, in order.** The video is never broken:\n - no `edit_video` tool, a refused or failed run, still processing after 15 min, or a failed gate: matte locally with `npx hyperframes remove-background media/take-cfr.mp4 -o media/person-rb.webm --json` (hyperframes 0.8.211: a transparent VP9 WebM, its alpha in a side stream; the model, u2net_human_seg ~168 MB, downloads once to `~/.cache/hyperframes/background-removal/models/` and the first run loads it for about a minute with no output: it is not hung; then about 0.23 s per 1080×1440 frame on CoreML). Take its matte out as a video: `ffmpeg -c:v libvpx-vp9 -i media/person-rb.webm -vf alphaextract,format=gray -c:v libx264 -crf 12 -pix_fmt yuv420p media/alpha.mp4` (`-c:v libvpx-vp9` goes BEFORE `-i`: ffmpeg's own VP9 decoder drops the alpha). Run the gate (step 3) on it and put its result in the pass's question; a failed gate does not stop this path, the person judges the preview. Then steps 4 to 6 as for the tool's matte, with `media/take-cfr.mp4` as the foreground (in place of `media/fgr.mp4`), over the background the look chose: the photo, or the blurred room for `mode: blur`. Its edge can carry the old room's colour and keep an object beside the person (a lamp, a mic stand): say what you saw on sample frames (step 8). The person can answer Rework (for example \"use the blurred room\") or Remove this pass;\n - that failing too: the raw take stays in the cut, and the pass's preview and question say \"background not applied\" and why.\n Say in the pass's question which path made the preview.\n8. Sample frames before the preview: a segmenter can drop an object the person holds or keep a large one beside them. Say what you saw.\n9. Remove this pass: put the raw take back in the cut and drop the underlay; keep the matte files.\n\n**Colour pass.**\n1. Ingest the `.cube` (never read it into context): `npx hyperframes media-use resolve --type lut --from <downloaded>.cube --project .` validates it and freezes a copy as `.media/luts/lut_NNN.cube` (the `path` it prints).\n2. Grade every footage element of the frame (the person, and the background image or the blurred room) with the grading block's `lut`, keys `src` and `intensity` (0 to 1, the look's intensity): `npx hyperframes media-treatment --project . --file index.html --selector '<element>' --grading '{\"lut\":{\"src\":\".media/luts/lut_NNN.cube\",\"intensity\":<intensity>}}' --apply --json`. It writes `data-color-grading` on that element. An element the Background pass already treated (grain, light match) keeps those sections: send them again with `lut` in the same `--grading` object, so the LUT is added, not swapped in.\n3. Remove this pass: `npx hyperframes media-treatment --project . --file index.html --selector '<element>' --clear` on each element it graded.\n\n## 7. QA before showing anyone\n\n1. `npx hyperframes lint` and `npx hyperframes check` clean.\n2. Snapshot every graphic in its complete state (after its last reveal, before its exit) with `npx hyperframes snapshot` and open the images: whole phrases, no orphan words, no overflow, centred, clear of captions and face.\n3. Snapshot both sides of every cut (cut -0.1 s and +0.2 s): no visual jump, a persistent element sits at the same position on both sides.\n4. Measure the audio: `ffmpeg -i out.mp4 -af ebur128=peak=true -f null -`: the integrated loudness is the cut's audio's (the level the person chose in §4), true peak at or below -1 dBTP; look for spikes at seams. Another level means the audio was mixed or re-encoded after the cut: find where.\n5. `ffprobe` the output: duration matches the plan, audio and video present, stream starts within max(0.1 s, 2/fps).\n6. Confirm the brand font actually loaded (fallbacks fail silently).\nCap self-fix at 3 passes, then report what remains.\n\n## 8. Review, render, save\n\n1. One human review of a preview. Then render with `npx hyperframes render`. Its audio is a fresh AAC encode, and `render` has no flag to keep the source's.\n2. **Put the cut's audio back, then check it**, after every render the person hears, a pass's preview included: `ffmpeg -i <render>.mp4 -i media/cut-audio.m4a -map 0:v:0 -map 1:a:0 -c copy -movflags +faststart <name>-final.mp4`. The decoded audio of the two must hash the same: `ffmpeg -v error -i <name>-final.mp4 -map 0:a:0 -f md5 -`, and the same on `media/cut-audio.m4a`. Different hashes: do not upload; find the step that re-encoded. One exception: a project that adds sound of its own (music, a sound effect, b-roll with sound) ships the render's mix as rendered, said in the pass's question, and that mix is never re-encoded again either.\n3. **Too big to upload?** Re-encode the video only: `ffmpeg -i <name>-final.mp4 -map 0 -c:v libx264 -crf <a higher CRF> -preset slow -c:a copy -movflags +faststart <name>-upload.mp4`, then the hash check again. Never an audio encoder (a `-c:a` other than `copy`) on a render or a delivery.\n4. Upload the final MP4 to the pod (`synap_store_file`) and record it: the composition as `content-composition`, the render as `render-job` / `content-asset`, linked to the content piece. Save reusable pieces (lower-third, caption style) as assets.\n\n## Revisions\n\nStart from the delivered version. Change only what was flagged; keep every settled decision (wording, splits, placement, size, fps). A late cut is one frame-aligned range applied to video, audio, captions and graphics together. Fix a repeated defect across every graphic that shares it, then re-snapshot only those.\n\n## Sources\n\nAdapted from video-use SKILL.md (MIT, © browser-use) and cut-motion docs (Apache-2.0, cut-motion by Endless1936, © 2026 prototech).\n"
22
+ },
23
+ "dependencies": []
24
+ }
@@ -0,0 +1,54 @@
1
+ ---
2
+ name: mapping-the-server
3
+ description: Understands a Discord server's structure (categories, channels, members) and proposes ONE reviewable CRM graph — clients, partners, companies, people, channel bindings, and the relations between them — for the operator to accept in a single move. The ambient "make sense of my server" onboarding.
4
+ metadata:
5
+ synap_native: true
6
+ auto_load: false
7
+ ---
8
+
9
+ You are the server-mapping agent. The operator pointed you at their Discord server and wants you to **understand it and propose the whole CRM graph in one move** — so they can review it, accept it, and watch everything link up. Your job is to make them feel *understood*: nothing important left out, sensible structure, theirs to approve.
10
+
11
+ You receive the server structure as input: `categories` (each with its `channels`), `members`, and optionally recent channel activity. You also have read access to the pod.
12
+
13
+ ## 1. Orient before proposing
14
+ - Call `list_profiles` / `orient` to learn which entity types exist (e.g. `client`, `partner`, `company`, `person`, `contact`) and read the bento schema so you propose entities with the RIGHT profile + properties.
15
+ - Look at what's **already in the pod**. Never propose a duplicate of an entity or binding that already exists — match by name/domain first and *link to the existing one* instead.
16
+
17
+ ## 2. Read the structure — COMPANY-FIRST
18
+ - **A category is usually a "room" for ONE organization** (a client or a partner). Its name is your strongest signal.
19
+ - **The model is company-first:** every client/partner organization is a **`company`** entity. The client/partner is a SEPARATE entity that is **linked to** that company — so the pod can list all companies, all clients, and all partners independently. A single company can be BOTH a client and a partner.
20
+ - **Channel firewall role:** `client-comms` / `tg-*` / `comms` / a mirrored external chat → `branchPurpose: 'client-comms'` (mirrored to the client — internal-only!). `team` / `internal` / `notes` → `branchPurpose: 'team'`.
21
+ - **TEAM THREADS (forums):** the input has a `forums[]` array of forum POSTS (threads) — these are usually the INTERNAL **team** thread for each client (e.g. an "important" forum with one thread per client). A client almost always has TWO surfaces: a mirrored comms channel (`tg-acme`, `client-comms`) AND a team thread (`acme`, `team`). **PAIR them by name** (`tg-acme` ↔ forum thread `acme`) and bind BOTH to the SAME client/company entity — the comms channel as `client-comms`, the team thread as `team`. This pairing is what lets digests + notices land in the team thread and NEVER the client channel.
22
+ - **People → contacts:** each channel carries a `participants` list — the recent message senders. For a client channel these are the **mirrored Telegram people** (the company's real contacts), not Discord members. Non-team participants become **`contact`** entities linked to that room's company.
23
+ - **Skip generic channels** (`general`, `welcome`, `help`, `rules`, voice).
24
+
25
+ ## 3. Build ONE proposed graph — company-first chain
26
+ Call `propose_entity_graph` ONCE. For EACH organization (room), build this chain:
27
+ 1. **`company`** entity (ref e.g. `acme-co`) — the organization itself. Reuse an existing company via `existingEntityId` (match by name) instead of duplicating.
28
+ 2. **`client`** or **`partner`** entity (ref e.g. `acme-client`) — the relationship — and a relation linking it to the company: `{ sourceRef: "acme-client", targetRef: "acme-co", type: "belongs_to" }`.
29
+ 3. **`contact`** entities for each non-team `participant` in the room's channels, each linked to the company: `{ sourceRef: "<contactRef>", targetRef: "acme-co", type: "works_at" }`.
30
+ 4. **Team members** (your own people) → **`person`** entities (NOT `contact` — the bridge's `/link-client --assigned` dedup looks up team members in the `person` profile only; using `contact` would create duplicates). Reuse existing. Add an `{ sourceRef: "<personRef>", targetRef: "acme-client", type: "assigned_to" }` relation when a team member clearly owns the room.
31
+ 5. **`bindings`**: each real channel → the company's `entityRef`, with `branchPurpose` (`client-comms` for the mirrored channel, `team` for the internal one). When a client has BOTH a comms channel and a paired forum team thread, emit TWO bindings to the same `entityRef` — one `client-comms`, one `team` — so the firewall has a safe place (the team thread) to surface digests.
32
+
33
+ So per org the graph is: **`contact → company ← client/partner`**, with channels bound and firewall roles set. Propose ALL orgs as **one** `propose_entity_graph` call — never dozens of single proposals.
34
+
35
+ (Missing `team` threads — rooms with a `client-comms` channel but no `team` channel — note them in your summary. After the graph is accepted, ensure the sibling team channel for each such client with `channel.ensure({ contextObjectType: 'entity', contextObjectId: <clientEntityId>, branchPurpose: 'team' })` so `channel.resolve(channelType: 'team')` has a firewall-safe target for digests + review flags. See the `routing-to-channels` skill for the full bind/ensure lifecycle.)
36
+
37
+ ## 4. The firewall is absolute
38
+ NEVER propose a write, binding, post, or thread that would put bot/team activity into a `client-comms` channel — those mirror to the client. All collaboration lands in `team` channels/threads. When in doubt about a channel's role, propose it as `client-comms` (the safe default) and say so.
39
+
40
+ ## 5. Be conservative + honest
41
+ - Only propose what the structure supports. Mark low-confidence inferences explicitly ("likely a partner — confirm?").
42
+ - If a room is ambiguous (is it a client, a partner, or a topic channel?), include your best guess but flag it for the operator's review rather than guessing silently.
43
+ - Do not delete or rename anything. You only ever ADD structure as a proposal.
44
+
45
+ ## 6. Close with reassurance, not a data dump
46
+ Return a short, human summary that makes the operator feel covered:
47
+ - What you found: "**Q companies → N clients, M partners, P contacts** across your rooms."
48
+ - The shape: a few highlights ("**Acme** → company + client, 3 contacts, comms + team channels bound").
49
+ - **Show bindings with channel mentions** so the operator can verify what maps to what — write each bound channel as `<#CHANNELID>` (Discord renders these as clickable channel links), e.g. "Acme → `<#123…>` (client-comms), `<#456…>` (team)".
50
+ - Anything you flagged for their judgment.
51
+ - **The review link:** pass the link from the tool's result through **verbatim** — do NOT rewrite it into document/cell syntax like `[[open:side|…]]` (that's plain text in Discord and can't be clicked). Keep it exactly as the tool returned it.
52
+ - **The next step:** end by telling them that once they accept the graph, they can run **`/digest-all`** to pull the contacts, notes and links out of each channel's existing history (the channels are only bound on accept, so this is the moment to backfill them).
53
+
54
+ You are the support layer that already gets it. Map the server, propose the graph, hand them one confident decision.
@@ -0,0 +1,20 @@
1
+ {
2
+ "apiVersion": "synap/v1",
3
+ "kind": "skill",
4
+ "meta": {
5
+ "slug": "math-explainer-manimgx",
6
+ "name": "Math & diagram explainers (ManimGX)",
7
+ "description": "Produce math, equation, graph, algorithm or 3D explainer clips with ManimGX (Manim CE API) and hand the MP4 to a HyperFrames composition; use when the motion itself explains a formula or structure.",
8
+ "tags": ["skill", "content", "video"],
9
+ "isPublic": true
10
+ },
11
+ "spec": {
12
+ "slug": "math-explainer-manimgx",
13
+ "name": "Math & diagram explainers (ManimGX)",
14
+ "kind": "instruction",
15
+ "scope": "pod",
16
+ "description": "Produce math, equation, graph, algorithm or 3D explainer clips with ManimGX (Manim CE API) and hand the MP4 to a HyperFrames composition; use when the motion itself explains a formula or structure.",
17
+ "body": "# Math & diagram explainers (ManimGX)\n\nMake an animated math, equation, graph, algorithm or 3D explainer clip with **ManimGX**, then place the MP4 into a **HyperFrames** composition as media. ManimGX runs the Manim Community Edition Python API on a Rust/wgpu core: `import manimgx as m`, never `import manim`.\n\n## When to use it, and when not\n- **Use it** when the motion IS the explanation: an equation that rearranges term by term (`TransformMatchingTex`), a curve drawn on axes, a tangent sliding along a function, a matrix moving a grid, an algorithm stepping through a data structure, a rotating 3D surface.\n- **Use HyperFrames + GSAP instead** for titles, kinetic type, product UI, cards, charts with a few bars, logos, captions. They render in the composition directly; a ManimGX clip adds a render loop and a seam.\n- **Use a static diagram** (an image in a HyperFrames frame) when nothing changes over time. If the viewer only needs to read it, do not animate it.\n\n## Setup (where it runs)\nManimGX renders on the machine where the agent runs: the user's computer or the team's Linux devbox. It does not run inside the pod today; only the finished files go to the pod.\n1. Python 3.13 or 3.14 venv: `python3.13 -m venv .venv && .venv/bin/pip install manimgx` (about 200 MB with bundled Noto fonts). `uv tool install --python 3.14 manimgx` also works. Do not install `manim`, LaTeX or ffmpeg: math is typeset by Typst (LaTeX input via mitex) and the H.264 encoder is inside the wheel.\n2. Wheels exist for macOS arm64 and Linux x86_64/aarch64. Linux without a GPU renders on the CPU through bundled lavapipe Vulkan (slower; not yet tested on the devbox).\n3. `manimgx --version`. A `no GPU adapter` error means a sandbox hides the GPU: run the command outside it.\n\n## Plan before code\nWrite `plan.md` first: the one idea each scene teaches, the narration line it sits under, its duration, its layout, and which colour means which concept. Then:\n- **Geometry before algebra**: show the shape, then the formula.\n- **One new idea per scene**; at most 5 or 6 elements visible at once.\n- **Colour = meaning**: if `x` is ochre in scene 1, it stays ochre everywhere.\n- **Transform, don't replace**: carry a term from one line to the next with `TransformMatchingTex` / `ReplacementTransform`; never `Write` a new note on top of an old one.\n\n## Scene conventions\n- One file `scene.py`, one class per scene, each renderable alone (`manimgx render scene.py SceneName`). Use `m.Scene`, or `m.ThreeDScene` for 3D.\n- Time passes only in `self.play(...)` (1 s unless `run_time=`) and `self.wait(s)`. Derive updater state from time or other objects, never a frame counter.\n- **Frame budget**: 14.2 x 8 units. Keep content inside x in [-6.5, 6.5], y in [-3.5, 3.5]; title zone y 2.5..3.5, note zone y -3.5..-2.5. Edge text uses `buff >= 0.5`. Leave about 15% of the frame empty. If the clip will sit in a panel, design for the panel's aspect (`-r 1080x1080` for square).\n- **Opacity layering**: primary element 1.0, context 0.4, structure (axes, grids) 0.15. Never everything at full brightness.\n- **Text**: math in `MathTex(r\"...\")` raw strings; words in `Text`. Minimum font size 18 at 1080p; when more than 4 text items are visible, drop to about 20. Cap long lines: `if t.width > 13: t.set_width(13)`.\n- **Hold times**: after a key reveal wait 2 s; after a transform 1.5 s; a supporting label 0.5 s; the \"aha\" reveal 3 s. End the scene with a clean exit (`FadeOut` of everything) unless HyperFrames cuts away on a hold.\n- Captions: `add_subcaption` / `self.say` produce an `.srt`, but in this pipeline captions are drawn in HyperFrames. Keep them out of the clip.\n\n## Brand setup\nRead the palette and fonts from the pod's Brand Library first (`synap_ask \"brand kit palette fonts\"`). Put them as constants at the top of the file; never inline a hex in a scene. Synap's own dark palette, as an example:\n\n```python\nimport manimgx as m\nGROUND = \"#1D1812\"; INK = \"#F4ECDC\"; OCHRE = \"#C28A4A\"; SAGE = \"#D0DFBA\"\nAI = \"#FFDB23\" # AI provenance only, never decoration\nm.config.background_color = GROUND\nBRAND_FONT = \"Geist\"; FONTS = [\"fonts\"]\ntitle = m.Text(\"Why it converges\", font=BRAND_FONT, font_paths=FONTS, color=INK, font_size=48)\n```\n\n**Font gotcha (verified):** `font=\"Geist\"` with a font that is not installed **silently** falls back to a default serif, with no warning. Ship the brand fonts as TTF/OTF in a `fonts/` directory and pass `font_paths=[\"fonts\"]` (searched recursively). Web WOFF2 files must be converted first:\n`python -c \"from fontTools.ttLib import TTFont; t=TTFont('Geist.woff2'); t.flavor=None; t.save('fonts/Geist.ttf')\"`. Then confirm on the storyboard PNG that the letterforms are the brand's.\n\n## The loop\n1. **Write** the scene.\n2. **Inspect**: `manimgx inspect scene.py` prints the timeline (each play with its code line), reports 2D layout problems (e.g. text over text) and writes `<Scene>.storyboard.png` with problems outlined and numbered. It exits 1 when problems exist. Open the PNG and look. For a specific moment: `manimgx inspect scene.py -t 1.5,end`. 3D scenes get storyboards but no layout checks: inspect those frames by eye.\n3. **Fix** and inspect again until \"no problems\" and the storyboard reads well.\n4. **Draft render**: `manimgx render scene.py SceneName -r 1280x720 --fps 30 --preset ultrafast -o draft.mp4`. Watch it for timing.\n5. **Final render** at the composition's exact size and fps: `manimgx render scene.py SceneName -r 1920x1080 --fps 60 -o explainer.mp4` (defaults are 1920x1080 at 60 fps). Render writes the MP4, a storyboard and a layout report.\n\n## Handoff to HyperFrames\n- **Match** the composition's resolution and fps exactly; do not let the player rescale or resample.\n- **No alpha**: output is H.264 MP4 only. Render on the exact ground colour of the composition (`background_color` = the frame's background) and place the clip as a full-frame or panel media clip. Never try to key it out or overlay it on a different background.\n- **Time it to the narration**: take the VO timestamps for the beat, set `run_time`/`wait` so the reveal lands on its word, and leave the planned silences. Trim or hold in HyperFrames rather than re-rendering for a few frames.\n- **Captions, titles and lower-thirds stay in HyperFrames**, in the brand font, so they match the rest of the video.\n- Check both sides of each seam with `npx hyperframes snapshot` (cut -0.1 s and +0.2 s): the ground colour must be identical, no flash.\n\n## Save to the pod\nAfter approval, upload the MP4 and its storyboard PNG to the pod as a `content-asset` in the Content space (`synap_store_file`, then link it to the `content-composition` it serves). Record the scene source, ManimGX version, resolution and fps on the asset so it can be re-rendered, not regenerated. Before making a new clip, `synap_ask` for an existing approved one.\n\n## Checklist before showing anyone\n- [ ] Storyboard shows the brand fonts, not a fallback serif.\n- [ ] `manimgx inspect` reports no layout problems (3D: frames checked by eye).\n- [ ] No overlapping text; edge buffers >= 0.5; nothing outside the frame budget.\n- [ ] Every reveal has a readable hold; nothing is on screen for less than its reading time.\n- [ ] Opacity layering applied; colour constants only; AI-yellow only for AI provenance.\n- [ ] Resolution, fps and ground colour match the HyperFrames composition.\n- [ ] MP4 + storyboard saved as a `content-asset`.\n\n## Licence\nManimGX's own code is MIT, but the distributed binary wheels statically include x264, so the wheel is GPL-3.0-or-later as distributed. Using it as a tool is fine and rendered videos are unaffected. Do not bake or redistribute the binary inside a product image without GPL compliance.\n\n## Sources\nProduction method adapted from browser-use/video-use `skills/manim-video` (MIT, (c) 2026 Browser Use). ManimGX CLI and API from academa-labs/manimgx README and llms.txt (MIT).\n"
18
+ },
19
+ "dependencies": []
20
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "apiVersion": "synap/v1",
3
+ "kind": "skill",
4
+ "meta": {
5
+ "slug": "motion-quality-gates",
6
+ "name": "Motion quality gates",
7
+ "description": "Checks a HyperFrames cut before anyone sees it: determinism, reading time, contact sheets, seams, rhythm and carry, cue alignment, not-the-template, loudness and blank frames, with exact commands. Use before every preview, review or final render.",
8
+ "tags": ["skill", "content", "video"],
9
+ "isPublic": true
10
+ },
11
+ "spec": {
12
+ "slug": "motion-quality-gates",
13
+ "name": "Motion quality gates",
14
+ "kind": "instruction",
15
+ "scope": "pod",
16
+ "description": "Checks a HyperFrames cut before anyone sees it: determinism, reading time, contact sheets, seams, rhythm and carry, cue alignment, not-the-template, loudness and blank frames, with exact commands. Use before every preview, review or final render.",
17
+ "body": "# Motion quality gates\n\nCheck a HyperFrames cut **before anyone sees it**: before the storyboard preview, before the human review, before the final render. Every gate below ends in PASS, FAIL or NOT CHECKED. **NOT CHECKED is never a pass**: a check that could not run (no text inventory, no audio, a crashed command) is reported as such and blocks delivery until it runs. Record the results on the `render-job` record in the pod's Content space.\n\nOrder: 1 → 2 run on the source while you build; 3 → 9 on a low-resolution draft; 10 on the final file only.\n\n## 1. Engine gates (always first)\n\n```sh\nnpx hyperframes lint # while authoring\nnpx hyperframes check --snapshots --at-transitions # required gate; includes lint + WCAG contrast\n```\n\nBoth must exit 0. `check` audits runtime errors, failed requests, layout overlap and contrast; treat every persistent finding as a defect unless a snapshot proves it intentional.\n\n## 2. Determinism\n\nEvery frame is a seek of **one paused GSAP timeline**: the picture at time t depends on t only.\n\n1. Grep the composition; every hit is a FAIL unless seeded or justified:\n ```sh\n grep -rnE 'Math\\.random|Date\\.now|performance\\.now|requestAnimationFrame|setInterval|setTimeout|repeat: *-1|@keyframes|transition *:' --include='*.html' --include='*.js' --include='*.css' .\n ```\n2. Randomness comes from a seeded generator (e.g. mulberry32 with a fixed seed), never `Math.random`.\n3. Entrances use `fromTo`, not `from`, so a frame rendered alone has its start state. No state carried from the previous frame.\n4. **Any frame renders alone**: capture `npx hyperframes snapshot --at 7.3`, then `--at 2,11,7.3`; the two 7.3 s images must be identical (`cmp` or ImageMagick `compare -metric AE a.png b.png null:` → 0).\n5. Two draft renders decode to the same frames: `ffmpeg -i a.mp4 -map 0:v -f framemd5 a.md5` for both, then `diff a.md5 b.md5` is empty.\n\n## 3. Reading time\n\nInventory every on-screen text block (titles, labels, UI copy, numbers) with the time it is **fully inside the frame and fully opaque** and the time it starts to leave. Get it by seeking the paused timeline in 0.04 s steps (preview page or a headless script) and reading each text element's bounding box and computed opacity. Rules:\n\n- On-screen text holds ≥ **max(1.5 s, Latin chars ÷ 15 + CJK chars ÷ 4.5 + 1.5 s)**, counting non-whitespace characters, measured from the first frame it is complete and in frame, over one continuous run.\n- A typewriter reveal counts the **full** string from its first frame; the hold starts when the last character lands.\n- Subtitles hold ≥ **max(1.8 s, spoken line + 0.6 s)**.\n- Title cards hold ≥ **4 s**.\n- Text that is never fully inside the frame (cropped, scrolled out) is a FAIL.\n- No inventory, or an inventory with zero text in a film that has text = NOT CHECKED.\n\nFix by cutting words, not by speeding up.\n\n## 4. Contact sheet and action strips\n\n1. Contact sheet, one frame every 1–2 s: `npx hyperframes snapshot --at 0,1.5,3,...` or from the draft `ffmpeg -i draft.mp4 -vf \"fps=1/1.5,scale=480:-1,tile=6x5\" -frames:v 1 sheet.png`. Make two full passes over it.\n2. Strip at 0.2 s for every key action (a click, a hand-off, the reveal, the signature move): `ffmpeg -ss <t> -t 1.2 -i draft.mp4 -vf \"fps=5,scale=480:-1,tile=6x1\" -frames:v 1 strip-<t>.png`.\n3. Look for: subject size (≥ ⅓ frame height at key moments), subject against the edge, text over busy areas, subtitles covering action, colour on same colour, off-brand colour (a dark brand rendered light), AI-yellow used for anything but AI provenance, captions not in the brand font.\n\n## 5. Seams\n\nFor **every** cut and section boundary at time t: `npx hyperframes snapshot --at <t-0.1>,<t+0.2>`.\n\n- A persistent element (logo, card, receipt, header) sits at the **identical** position and size on both sides. Crop both with `snapshot --zoom \"<selector>\"` and compare; any shift is a FAIL.\n- Push or zoom transitions move whole frames and double a persistent element: use a crossfade or a hard cut on continuity seams.\n- No element starts its own move inside the transition overlap.\n- No blank or half-built frame inside a transition.\n\n## 6. Rhythm, carry and framing\n\nCheck the cut against the treatment's beat sheet:\n\n- Shot lengths vary widely (short bursts and long holds); at least one still hold ≥ 2 s. Even pacing = FAIL.\n- Hard cuts are concentrated in bursts, not spread evenly.\n- **Carry**: at every section boundary something on screen survives and moves or scales into the next beat. A clean swap of whole scenes = FAIL (reads as a slideshow).\n- **Cause**: every reaction has a visible subject causing it.\n- The camera never loses the subject; follows and zooms track it after every camera move.\n\n## 7. Cue alignment\n\n1. `npx hyperframes beats` (music `<audio>` marked `data-timeline-role=\"music\"`) writes the beat grid.\n2. For every cut, key action and subtitle start in the cue map, the nearest beat must be within **1 frame** (33 ms at 30 fps). List offsets; any larger = FAIL unless the treatment marks it off-grid on purpose.\n3. Every row of the sound table is audible in the mix; at least two silences exist where planned.\n\n## 8. Not the template\n\nCompare the cut with the example the HyperFrames workflow ships (and with the last film made from the same template). Six axes: **structure, opening, signature shot, camera path, score shape, ending**. At least **4 of 6 must differ**; otherwise rethink the matching ones before review.\n\n## 9. Draft first\n\nIterate on `npx hyperframes render --quality draft` (low resolution). Show the preview (`npx hyperframes preview --background`) and get **one human acceptance** before the final render. Checks passing is not approval.\n\n## 10. Final file\n\nRun on the delivered MP4:\n\n```sh\n# Loudness: integrated ≈ −14 LUFS (±1), true peak ≤ −1 dBTP\nffmpeg -hide_banner -nostats -i final.mp4 -af ebur128=peak=true -f null - 2>&1 | tail -12\n# Black frames (any hit inside the film is a FAIL; a planned fade-out at the end is not)\nffmpeg -hide_banner -i final.mp4 -vf blackdetect=d=0.04:pix_th=0.10 -an -f null - 2>&1 | grep black_start\n# White/blank frames\nffmpeg -hide_banner -i final.mp4 -vf negate,blackdetect=d=0.04:pix_th=0.05 -an -f null - 2>&1 | grep black_start\n# Frozen stretches you did not plan\nffmpeg -hide_banner -i final.mp4 -vf freezedetect=n=0.001:d=3 -an -f null - 2>&1 | grep freeze_start\n# Duration and fps match the composition\nffprobe -v error -show_entries stream=codec_type,r_frame_rate,duration -of compact final.mp4\n```\n\nFix loudness with a two-pass `loudnorm=I=-14:TP=-1:LRA=11`. Also: no `NaN` in the browser console during `check` (a NaN transform renders as a missing element), every voice line transcribes back to the script, the `.srt` matches the burned captions.\n\n## Report\n\nList each gate with PASS / FAIL / NOT CHECKED and the evidence (command output, sheet path). Upload the contact sheet and strips to the pod as `content-asset`. Never call a cut ready with a FAIL or NOT CHECKED open.\n\n## Sources\n\nAdapted from lemo-opuscar TECHNIQUE.md, DIRECTOR.md and core/render/readcheck.mjs (MIT, © 2026 LemoLab). Seam and brand checks from Synap launch video #1.\n"
18
+ },
19
+ "dependencies": []
20
+ }
@@ -0,0 +1,24 @@
1
+ {
2
+ "apiVersion": "synap/v1",
3
+ "kind": "skill",
4
+ "meta": {
5
+ "slug": "open-design-edit",
6
+ "name": "Open Design edit",
7
+ "description": "Build a deck, PDF document or presentation in Open Design on the person's Mac, our worker the only writer: privacy check, project location in the run's work folder, one HTML file of slide sections, ai versions after each write, the person's manual versions read as a diff, PDF/PPTX/PNG export filed to the pod.",
8
+ "tags": [
9
+ "skill",
10
+ "content",
11
+ "visual"
12
+ ],
13
+ "isPublic": true
14
+ },
15
+ "spec": {
16
+ "slug": "open-design-edit",
17
+ "name": "Open Design edit",
18
+ "kind": "instruction",
19
+ "scope": "pod",
20
+ "description": "Build a deck, PDF document or presentation in Open Design on the person's Mac, our worker the only writer: privacy check, project location in the run's work folder, one HTML file of slide sections, ai versions after each write, the person's manual versions read as a diff, PDF/PPTX/PNG export filed to the pod.",
21
+ "body": "# Open Design edit: decks, PDF documents and presentations\n\nLoad this when the post's format is a deck, a PDF document or a presentation. Carousels, LinkedIn PDFs and image posts are NOT built here: they are layered compositions (`carousel-format`). The run follows the `edit-loop` skill (slots, answers, versions, room lines): read it first. This skill is the engine part only.\n\nOpen Design (desktop app, pinned 0.26.1) runs on the person's Mac. Our worker is the only agent that writes into the project (\"Path B\"): Open Design's own agent is never asked to generate.\n\n## 0. Before anything: privacy, app, daemon\n\n1. **Privacy, every run.** Open Design's data folder is `~/Library/Application Support/Open Design/namespaces/release-stable/data` (`<data>` below). Read `<data>/app-config.json`: `telemetry` must be `{ \"metrics\": false, \"content\": false }`. Read `<data>/diagnostics/consent.json`: `enabled` must be `false`. Anything else (a missing file included): stop, write nothing, and ask the person on a slot `ask-open-design-privacy` (confirm: \"Open Design would send usage or content out. Turn sharing off in its settings, then answer yes.\"). Never change those files yourself.\n2. **The daemon's port is dynamic.** Find it each run: `lsof -nP -iTCP -sTCP:LISTEN | grep -i \"open design\"`, then `GET http://127.0.0.1:<port>/api/health` must answer `{ \"ok\": true }`. No listening port or no health: the app is not running, so declare `needs-tool-open-design` (edit-loop §2; the install-or-start line: \"Open the Open Design app on <this computer's name>\") and end blocked.\n3. Record the editor: `{ key: \"studio\", kind: \"link\", label: \"Open Design\", required: false, ref: { url: \"http://127.0.0.1:<port>/\" } }`, and retire it as edit-loop §2 says.\n\n## 1. The project\n\n- **One project per run, in the run's work folder**: `~/synap-work/<run session id>/` (the folder `synap worker` gives the run). Write `.open-design/project.json` there: `{ \"schemaVersion\": 1, \"id\": \"<run session id>\", \"name\": \"<post title> (<format>)\", \"createdAt\": <ms epoch>, \"updatedAt\": <ms epoch>, \"designSystemId\": \"user:<brand design system slug>\" }`.\n- **Register `~/synap-work` as a project location**, once per Mac: `PUT /api/project-locations` with that folder, then `POST /api/project-locations/scan` so the project shows. Never `POST /api/import/folder`: it needs a desktop token we do not have.\n- **One HTML file** for the whole piece, one `<section class=\"slide\">` per page, each at the platform's size (LinkedIn document 1080 × 1350; a 16:9 deck 1920 × 1080; A4 portrait 794 × 1123 CSS px). Styles come from the design system's `tokens.css`; never hard-code a colour or a font the tokens hold.\n- **Never write into `out/` or `dist/`**: Open Design does not watch them, so the person would never see that work.\n\n## 2. The brand as a design system\n\nThe brand lives in the pod (`GET /api/hub/brand/kit`). Open Design reads it as a user design system in `<data>/design-systems/<slug>/`:\n- `manifest.json`: `schemaVersion` `od-design-system-project/v1`, `id` = the slug, `name`, `category`, `description`, `source` (where each value came from in the pod).\n- `DESIGN.md`: at least 7 `##` sections (voice, colour, type, layout and grid, components, imagery, do and don't), from the brand kit's approved rows only.\n- `tokens.css`: all 56 tokens of Open Design's schema, every one set; a value the kit does not give is chosen from the nearest approved one and listed in `DESIGN.md` as guessed.\nAn existing folder for this brand is reused and updated, never duplicated.\n\n## 3. Each pass\n\nPasses: `structure` (pages, order, headlines and copy), `visual` (layout, imagery, brand assets), `polish` (type, spacing, contrast, page numbers).\n1. **Version first** (edit-loop §4.1): `GET /api/projects/<id>/files/<path>/versions` and read the newest `manual` versions: they are the person's edits since your last `ai` version (`GET …/versions/<vid>` for each body). Diff them against your last `ai` version, page by page (by `<section class=\"slide\">` order), and keep every change they made.\n2. Write the pass into the HTML file: only the pages the pass or the Rework note touches.\n3. **Record your write**: `POST /api/projects/<id>/files/<path>/versions` with `{ \"source\": \"ai\", \"label\": \"<pass> v<n>\", \"prompt\": \"<one line: what this pass changed>\" }`.\n4. **Preview**: `POST /api/projects/<id>/export/pdf-image` with `{ \"fileName\": \"<post slug>-<pass>-v<n>.pdf\", \"deck\": true, \"width\": <page width>, \"height\": <page height> }` (needs the desktop app running; unreachable → `needs-tool-open-design`). Upload it to the pod and link it to the post: `synap upload <file> --title \"<post title>: <pass> v<n>\" --workspace <Content OS space id> --attach <post id>` (the post references the file). Then hand `layer-<pass>` to the person (edit-loop §2).\n5. **Use my edit**: the person's `manual` versions ARE the pass. Do not touch the file; export the preview from it as it is, point the slot at it, ask again.\n\n## 4. Finals\n\nAfter the last pass is approved, export what the format ships: `pdf-image` for a PDF document or a LinkedIn-style document, `pptx` for a presentation the person will present elsewhere, `image` for page stills (same body as above). Upload each the same way (`synap upload <file> --workspace <Content OS space id> --attach <post id>`), record a `render-job` (`output-format` pdf, pptx or png, `output-file` the upload) and a `content-composition` (`render-engine` open-design) made for the post, then move the post to Review. Nothing publishes itself.\n\n## Traps\n- Open Design frames every project as a slide deck and may inject speaker notes into a file the person opened: remove them in the polish pass unless the person kept them.\n- Its hand editor cannot add an element or an asset: when the person needs one, add it in a Rework from their note.\n- Exports need the desktop app running, not only the daemon.\n\n## Sources\nW0 pilot (pod session 932dce9c, 2 Nov 2026), build plan \"Static asset edit loop\" (pod document ab3e0a5a), founder decision D1 (Open Design for decks, PDF documents and presentations).\n"
22
+ },
23
+ "dependencies": []
24
+ }
@@ -0,0 +1,65 @@
1
+ ---
2
+ name: reading-client-progress
3
+ description: Methodology for reading a Stellar grant client's most recent messages and determining exactly where they stand in the Stellar Community Fund (SCF) process. Use when an extraction step needs to classify a client's latest CLIENT-COMMS messages into a strict {stage, grantStatus, excelUrl, advancement} JSON result. Triggers include syncing client progress, advancing a Stellar grant session from comms, or reading a company's recent messages to infer pipeline stage and health.
4
+ ---
5
+
6
+ # Reading Client Progress
7
+
8
+ You read a Stellar grant client's most recent messages and decide where they are in the SCF process. You output a single strict JSON object — nothing else. This skill is the reference an extraction `command` node embeds; it is consumed by an automation, not a human, so prose, markdown fences, and commentary are forbidden in the output.
9
+
10
+ ## Input
11
+
12
+ You receive the client's recent messages as an array of `{ role, content, authorName, createdAt }` (most recent last). These come from the client's CLIENT-COMMS channel — the external thread where the client shares their project, drafts, links, and replies. Read them in chronological order to understand the trajectory; weight the most recent messages most heavily for the current stage.
13
+
14
+ ## Output — STRICT JSON, nothing else
15
+
16
+ Respond with NOTHING but a single JSON object, no prose, no markdown fences, exactly this shape:
17
+
18
+ ```
19
+ { "stage": "scope|abstract|build|awaiting-stellar|closed", "grantStatus": "not-responding|waiting-on-client|in-review|submitted|passed", "excelUrl": string|null, "advancement": string }
20
+ ```
21
+
22
+ If you cannot determine a field, use the most conservative value: `stage` defaults to `"scope"`, `grantStatus` defaults to `"waiting-on-client"`, `excelUrl` defaults to `null`. Never invent a value.
23
+
24
+ ## `stage` — pipeline position (where the work is)
25
+
26
+ `stage` is the client's position in the ordered SCF pipeline. Pick the FURTHEST stage the messages clearly support:
27
+
28
+ - **`scope`** — Default. The client has not yet shared a concrete project, or is still explaining what they're building. Use until they share the project (site / GitHub / deck / clear description) and the engagement starts working on the Abstract.
29
+ - **`abstract`** — The client is actively working the SCF **Project Abstract** (the 12-field abstract / interest form). Messages reference the Abstract, abstract fields, the interest form, or a draft Abstract being reviewed.
30
+ - **`build`** — The client is working the SCF **Build dossier** (Code URL, Video, Tech Architecture, 3-tranche budget). Messages reference the Build submission, the build dossier, tranches, the technical architecture, or the demo video. Build comes AFTER abstract.
31
+ - **`awaiting-stellar`** — The client has **submitted** to SCF and is waiting on the panel. Messages say they submitted, sent it in, or are waiting on Stellar / the panel / the vote. No further drafting.
32
+ - **`closed`** — The engagement is complete: the client was **awarded** (won the grant) or **rejected**. Messages confirm an award or a rejection decision.
33
+
34
+ Stages are monotonic — don't regress to an earlier stage on the strength of an old message if a later message shows they've moved on.
35
+
36
+ ## `grantStatus` — comms / health sub-status (how it's going)
37
+
38
+ `grantStatus` is orthogonal to `stage`. It captures the health of the engagement from the comms, not the pipeline position:
39
+
40
+ - **`not-responding`** — The client has gone quiet; we're waiting and they haven't replied to our last asks.
41
+ - **`waiting-on-client`** — The ball is in the client's court (we asked for something — a draft, a link, info — and await their reply).
42
+ - **`in-review`** — We are actively reviewing / working the client's material right now.
43
+ - **`submitted`** — The dossier has been submitted to SCF (pairs naturally with stage `awaiting-stellar`).
44
+ - **`passed`** — The grant was awarded (pairs naturally with stage `closed`).
45
+
46
+ ## `excelUrl` — the client's working dossier link
47
+
48
+ `excelUrl` is the client's working Excel / Google Sheet / dossier URL if one appears anywhere in the messages, else `null`. Look for:
49
+
50
+ - `.xlsx` file links
51
+ - Google Sheets links (`docs.google.com/spreadsheets/...`)
52
+ - Google Drive links (`drive.google.com/...`) to a sheet or dossier
53
+ - Any link the client calls their "abstract", "build", "dossier", "submission sheet", or "grant sheet"
54
+
55
+ If several appear, prefer the most recent one. If none appear, return `null` — do not guess or fabricate a URL.
56
+
57
+ ## `advancement` — one-sentence note
58
+
59
+ `advancement` is a single plain-language sentence summarizing where the client is and what's next (e.g. "Client shared their GitHub and pitch deck; ready to start the Abstract." or "Abstract draft delivered for review; waiting on client to confirm the budget."). Keep it to one sentence. If genuinely unknown, use a short honest note like "No recent client activity to assess."
60
+
61
+ ## Reminders
62
+
63
+ - Output ONLY the JSON object. No prose, no markdown fences, no leading or trailing text.
64
+ - Omit nothing — always return all four keys. Use the conservative defaults / `null` when unknown.
65
+ - `stage` = pipeline position; `grantStatus` = comms/health. They move independently.
@@ -0,0 +1,54 @@
1
+ ---
2
+ name: routing-to-channels
3
+ description: Routes a message to the right channel and creates a channel when warranted. Use when a message belongs in a different channel, or a new room or thread should be created.
4
+ metadata:
5
+ synap_native: true
6
+ auto_load: false
7
+ ---
8
+
9
+ Route messages to the right channel. Understand channel types (personal, group, external).
10
+
11
+ ## CHANNEL ROUTING
12
+ When you detect the user is talking about a specific entity (project, task, person, etc.) that has or should have its own channel, use `route_to_channel` to continue the conversation there. This gives you access to the entity's full context and history.
13
+
14
+ Examples of when to route:
15
+ - User asks about "the marketing project" → route to the marketing project's channel
16
+ - User discusses a specific task in depth → route to that task's channel
17
+ - User mentions a person and wants to plan something → route to that person's channel
18
+
19
+ Always explain WHY you're routing: "Let me continue this in the Marketing Project channel where I have the full project context."
20
+
21
+ When routing, ALWAYS set `forwardMessage` to the user's original message so the target channel has full context. This prevents "what were we talking about?" moments — the user's question appears in the target channel as a forwarded message.
22
+
23
+ Do NOT route when:
24
+ - The user asks a quick question you can answer from memory
25
+ - The user is browsing/exploring across multiple entities
26
+ - You're in a general brainstorming conversation
27
+ - The entity doesn't benefit from dedicated context (simple notes, one-off bookmarks)
28
+
29
+ Route when the conversation will clearly benefit from entity-specific context and history.
30
+
31
+ ## BINDING AN EXTERNAL CHANNEL TO A CLIENT ENTITY
32
+
33
+ A channel is *bound* when `channels.contextObjectId` points at the entity it belongs to. Once bound, every inbound message on it arrives entity-resolved for free: `external_message.received` carries `subjectId` = the bound entity. Automations (e.g. proactive grant review) rely on this — they resolve the client's **team** channel and dossier from `subjectId`, and skip entirely when a channel is unbound.
34
+
35
+ ### First contact on an UNBOUND external channel (runtime, you the agent)
36
+ A channel is still unbound when it has no `contextObjectId` — the tell is that the message's `subjectId` **equals the channel id** (the resolver falls back to the channel id when there is nothing bound). When you see that on an external/inbound channel:
37
+
38
+ 1. **Match an existing client** — `entity.query({ profileSlug: 'client', filter: { ... } })` on the sender's name/handle/domain. Never create a duplicate: bind to the match if one exists.
39
+ 2. **Create one if there is no match** — a `client` entity (the create-then-configure minimum: just enough to exist).
40
+ 3. **Bind the channel to it** — `channel.bind({ channelId, contextObjectType: 'entity', contextObjectId: <clientEntityId>, branchPurpose: 'client-comms' })`. The inbound channel mirrors to the client, so it is `client-comms`.
41
+
42
+ After binding, you are done for every future message: they all arrive entity-resolved, and the team-channel resolution the automations need starts working. Explain the bind briefly, the way you would when keeping notes ("Binding this channel to Acme so everything they send lands on their record.").
43
+
44
+ **FIREWALL:** binding a channel `client-comms` marks it as mirrored to the client. NEVER post bot/team output to a `client-comms` channel — collaboration goes to the sibling `team` channel (see below). When unsure of a channel's role, bind it `client-comms` (the safe default).
45
+
46
+ ### Onboarding a KNOWN client (you already have, or are creating, the entity)
47
+ When you create or onboard a client, wire its channels in the same move so the firewall has both surfaces from day one:
48
+
49
+ 1. **Client-comms channel** — `channel.ensure({ contextObjectType: 'entity', contextObjectId: <clientEntityId>, branchPurpose: 'client-comms' })` (find-or-create). If the client's external channel already exists, `channel.bind(...)` it instead of ensuring a new one.
50
+ 2. **Team sibling channel** — `channel.ensure({ contextObjectType: 'entity', contextObjectId: <clientEntityId>, branchPurpose: 'team' })`. This is the channel every automation resolves via `channel.resolve({ ..., channelType: 'team' })`. Without it, team-facing posts (grant review flags, digests) have nowhere firewall-safe to land.
51
+
52
+ Both point at the SAME client entity — one `client-comms`, one `team`. That pairing is what lets bot/team output surface on the team channel and NEVER leak into the client channel.
53
+
54
+ > Population model: onboarding binds the clients you already know; on-the-fly binding (above) catches the unknown ones on their first inbound message. Together they cover the whole roster.