@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,322 @@
1
+ /**
2
+ * Content OS pin strategy — Produce Content is pinned; the weekly cron that
3
+ * runs it ships paused; Plan Next Content's workers (weekly and by hand) run
4
+ * once over every post and propose. Not a YAML snapshot.
5
+ */
6
+ import { readFileSync } from "fs";
7
+ import { dirname, join } from "path";
8
+ import { fileURLToPath } from "url";
9
+ import { load } from "js-yaml";
10
+ import { describe, expect, it } from "vitest";
11
+ import { getWorkspaceTemplate } from "./templates.js";
12
+ import type { TemplatePlaybook, WorkspaceYaml } from "./types.js";
13
+
14
+ const template = (() => {
15
+ const resolved = getWorkspaceTemplate("content-os");
16
+ if (!resolved) throw new Error("content-os template is not registered");
17
+ return resolved;
18
+ })();
19
+
20
+ type Strategy = NonNullable<TemplatePlaybook["inputStrategy"]>;
21
+
22
+ function strategyKind(strategy: Strategy): string {
23
+ return typeof strategy === "string" ? strategy : strategy.kind;
24
+ }
25
+
26
+ describe("Content OS pin strategy", () => {
27
+ it("keeps the legacy string form and the pinned object", () => {
28
+ expect(strategyKind("query")).toBe("query");
29
+ expect(strategyKind({ kind: "pinned" })).toBe("pinned");
30
+ // @ts-expect-error pinned entity ids are not part of the strategy
31
+ const rejected: Strategy = { kind: "pinned", pinnedEntityIds: ["post-1"] };
32
+ expect(rejected).toBeDefined();
33
+ });
34
+
35
+ it("pins Produce Content; Plan Next Content runs weekly and by hand, once over every post, proposing", () => {
36
+ const produce = template.playbooks?.find((p) => p.name === "Produce Content");
37
+ const plan = template.playbooks?.find((p) => p.name === "Plan Next Content");
38
+
39
+ expect(produce?.inputStrategy).toEqual({
40
+ kind: "pinned",
41
+ fallback: {
42
+ filter: { "post-status": "Idea" },
43
+ orderBy: "createdAt",
44
+ orderDir: "asc",
45
+ },
46
+ });
47
+ expect(produce).not.toHaveProperty("schedule");
48
+ expect(produce?.inputStrategy).not.toHaveProperty("pinnedEntityIds");
49
+
50
+ // A subject-bound playbook's `schedule:` fans out one run per post: the
51
+ // plan's clock is a per_moment activator instead, never that field.
52
+ expect(plan).not.toHaveProperty("schedule");
53
+ expect(plan).not.toHaveProperty("inputStrategy");
54
+ expect(plan?.subjectProfile).toEqual({ profileSlug: "post" });
55
+ expect(plan?.stages ?? []).toEqual([]);
56
+ expect(plan?.activators).toEqual([
57
+ { on: "schedule", cron: "0 9 * * 1", mode: "propose" },
58
+ { on: "manual", size: "per_moment", mode: "propose" },
59
+ ]);
60
+ // The plan reads its numbers from the pod, and files dates as proposals.
61
+ expect(plan?.goalTemplate).toContain("platform-posting-freq");
62
+ expect(plan?.goalTemplate).toContain("never apply a date yourself");
63
+ });
64
+
65
+ it("ships one paused playbook_run of Produce Content, with no subject binding", () => {
66
+ // Two automations: this weekly run, and Publish due posts (content-os.publish-due-posts.template.test.ts).
67
+ expect(template.automations?.map((a) => a.key)).toEqual(["content-os-weekly-produce", "content-os-publish-due-posts"]);
68
+ const automation = template.automations?.find((a) => a.key === "content-os-weekly-produce");
69
+ expect(automation).toMatchObject({
70
+ key: "content-os-weekly-produce",
71
+ triggerType: "cron",
72
+ status: "paused",
73
+ triggerConfig: { expression: "0 9 * * 1" },
74
+ });
75
+
76
+ const nodes = (automation?.flow.nodes ?? []) as Array<{
77
+ id?: string;
78
+ type?: string;
79
+ data?: Record<string, unknown>;
80
+ }>;
81
+ const runs = nodes.filter((n) => n.type === "playbook_run");
82
+ expect(runs).toHaveLength(1);
83
+ expect(runs[0]?.data).toMatchObject({ playbookName: "Produce Content" });
84
+ expect(runs[0]?.data).not.toHaveProperty("paramsMapping");
85
+ expect(runs[0]?.data).not.toHaveProperty("subject");
86
+ expect(runs[0]?.data).not.toHaveProperty("playbookId");
87
+
88
+ expect(automation?.flow.edges).toEqual([
89
+ { id: "e-trigger-produce", source: "trigger", target: "produce" },
90
+ ]);
91
+ });
92
+ });
93
+
94
+ /**
95
+ * Content OS is THE Content space (one space per domain), so it — not the
96
+ * superseded content-studio — must declare the video/visual kinds the browser
97
+ * Content app reads (`CONTENT_KINDS`). Without them "New video" fails on a pod
98
+ * with only Content OS: `Profile not found: content-composition`.
99
+ */
100
+ describe("Content OS owns the video / visual pipeline", () => {
101
+ const profile = (slug: string) => template.profiles.find((p) => p.slug === slug);
102
+
103
+ it("declares content-composition, content-asset and render-job (workspace scope)", () => {
104
+ for (const slug of ["content-composition", "content-asset", "render-job"]) {
105
+ const p = profile(slug);
106
+ expect(p, `content-os must declare ${slug}`).toBeDefined();
107
+ expect(p?.scope ?? "workspace", `${slug} scope`).not.toBe("shared");
108
+ }
109
+ // The fields the Content app and the composition detail read.
110
+ expect(profile("render-job")?.properties.map((d) => d.slug)).toEqual(
111
+ expect.arrayContaining(["job-status", "render-progress", "composition-ref"])
112
+ );
113
+ expect(profile("content-asset")?.properties.map((d) => d.slug)).toContain("asset-kind");
114
+ });
115
+
116
+ it("re-declares brand-template as shared (the grant) and requires brand-library (the SSOT)", () => {
117
+ expect(profile("brand-template")?.scope).toBe("shared");
118
+ expect(
119
+ (template.dependencies ?? []).some(
120
+ (d) => d.slug === "brand-library" && (d.relation ?? "require") === "require"
121
+ )
122
+ ).toBe(true);
123
+ });
124
+
125
+ it("ships the pipeline views and links", () => {
126
+ const viewScopes = (template.views ?? []).map((v) => v.scopeProfileSlug);
127
+ expect(viewScopes).toEqual(
128
+ expect.arrayContaining(["content-composition", "content-asset", "render-job"])
129
+ );
130
+ const links = (template.entityLinks ?? []).map(
131
+ (l) => `${l.sourceProfileSlug}-${l.type}->${l.targetProfileSlug}`
132
+ );
133
+ expect(links).toEqual(
134
+ expect.arrayContaining([
135
+ "render-job-renders->content-composition",
136
+ "content-asset-used_in->content-composition",
137
+ "content-asset-used_in->render-job",
138
+ "content-composition-made_for->post",
139
+ ])
140
+ );
141
+ });
142
+
143
+ it("keeps its own home: the content-home cell", () => {
144
+ expect(template.app).toMatchObject({ kind: "stored", cellKey: "content-home" });
145
+ });
146
+ });
147
+
148
+ // A stage's `subjectStatus` is written when the run ENTERS it, and the pod holds
149
+ // it only behind a HUMAN gate (a check gate judges the stage being left, so it
150
+ // passes on entry). A status that claims an outside act — the post is live, the
151
+ // issue went out — or that starts ANOTHER process must therefore sit behind a
152
+ // person's approval, or the record says it happened before anyone confirmed it
153
+ // (review M6: Repurpose fired on a post that never shipped).
154
+ describe("Content OS never writes an outside act before a person confirms it", () => {
155
+ const playbooks = (template.playbooks ?? []) as TemplatePlaybook[];
156
+ type Stage = NonNullable<TemplatePlaybook["stages"]>[number];
157
+ const bound: Array<{ playbook: string; stage: Stage }> = playbooks.flatMap((pb) =>
158
+ (pb.stages ?? [])
159
+ .filter((s) => typeof s.subjectStatus === "string")
160
+ .map((stage) => ({ playbook: pb.name, stage })),
161
+ );
162
+ // Values another playbook's activator listens for, per subject kind.
163
+ const activatedElsewhere = (owner: string, kind: string | undefined, status: string) =>
164
+ playbooks.some(
165
+ (pb) =>
166
+ pb.name !== owner &&
167
+ pb.subjectProfile?.profileSlug === kind &&
168
+ (pb.activators ?? []).some((a) => a.on === "enters_status" && a.status === status),
169
+ );
170
+ const kindOf = (name: string) => playbooks.find((p) => p.name === name)?.subjectProfile?.profileSlug;
171
+
172
+ const mustBeHuman = bound.filter(
173
+ ({ playbook, stage }) =>
174
+ stage.category === "completed" ||
175
+ activatedElsewhere(playbook, kindOf(playbook), stage.subjectStatus as string),
176
+ );
177
+
178
+ it("finds the stages it guards (non-vacuity)", () => {
179
+ expect(mustBeHuman.map(({ playbook, stage }) => `${playbook}/${stage.key}`).sort()).toEqual([
180
+ "Newsletter issue/sent",
181
+ "Produce Content/publish",
182
+ ]);
183
+ });
184
+
185
+ it("gates each of them on a person", () => {
186
+ for (const { playbook, stage } of mustBeHuman) {
187
+ expect({ at: `${playbook}/${stage.key}`, gate: stage.gate }).toEqual({
188
+ at: `${playbook}/${stage.key}`,
189
+ gate: { kind: "human" },
190
+ });
191
+ }
192
+ });
193
+
194
+ it("keeps a recorded-evidence criterion on each (URL / date)", () => {
195
+ for (const { playbook, stage } of mustBeHuman) {
196
+ const evidence = (stage.criteria ?? [])
197
+ .map((c) => (c as { check?: { kind?: string; evidenceKey?: string } }).check)
198
+ .filter((c) => c?.kind === "evidence")
199
+ .map((c) => c!.evidenceKey);
200
+ expect({ at: `${playbook}/${stage.key}`, has: evidence.length > 0 }).toEqual({
201
+ at: `${playbook}/${stage.key}`,
202
+ has: true,
203
+ });
204
+ }
205
+ });
206
+ });
207
+
208
+ // The script step (D4, 2026-10-12): a post is WRITTEN (script / outline) and a
209
+ // person approves that before anything is generated. Read from the YAML SOURCE,
210
+ // not the generated templates.ts, so the assertion sees this file's edit
211
+ // without a rebuild.
212
+ describe("Content OS scripts a post before generating it", () => {
213
+ const source = load(
214
+ readFileSync(join(dirname(fileURLToPath(import.meta.url)), "content-os.yaml"), "utf8"),
215
+ ) as WorkspaceYaml;
216
+ const post = source.profiles.find((p) => p.slug === "post");
217
+ const prop = (slug: string) => post?.properties.find((d) => d.slug === slug);
218
+ const produce = (source.playbooks ?? []).find((p) => p.name === "Produce Content") as
219
+ | TemplatePlaybook
220
+ | undefined;
221
+ const stage = (key: string) => produce?.stages?.find((s) => s.key === key);
222
+
223
+ it("puts Scripted between Briefed and In Draft, in the enum and the lifecycle", () => {
224
+ const order = ["Idea", "Briefed", "Scripted", "In Draft", "Review", "Scheduled", "Published", "Archived"];
225
+ expect(prop("post-status")?.enumValues).toEqual(order);
226
+ expect(post?.lifecycle?.options.map((o) => o.key)).toEqual(order);
227
+ expect(post?.lifecycle?.options.find((o) => o.key === "Scripted")?.category).toBe("active");
228
+ });
229
+
230
+ it("declares post-goal with the shared goal enum", () => {
231
+ expect(prop("post-goal")).toMatchObject({ inputType: "select" });
232
+ expect(prop("post-goal")?.enumValues).toEqual([
233
+ "sell",
234
+ "hot-take",
235
+ "tutorial",
236
+ "product-demo",
237
+ "announcement",
238
+ "proof",
239
+ "behind-the-scenes",
240
+ "community",
241
+ ]);
242
+ });
243
+
244
+ it("runs a script stage at Scripted, right after the brief, producing a Script document", () => {
245
+ expect(produce?.stages?.map((s) => s.key)).toEqual([
246
+ "brief",
247
+ "script",
248
+ "draft",
249
+ "review",
250
+ "schedule",
251
+ "publish",
252
+ ]);
253
+ expect(stage("script")?.subjectStatus).toBe("Scripted");
254
+ expect(stage("script")?.expectedOutputs).toEqual([
255
+ expect.objectContaining({ kind: "document", label: "Script" }),
256
+ ]);
257
+ });
258
+
259
+ it("holds the draft behind a person's approval of the script, reworking the script on reject", () => {
260
+ expect(stage("draft")?.gate).toEqual({ kind: "human" });
261
+ expect(stage("draft")?.onFail).toEqual({ toStage: "script" });
262
+ });
263
+ });
264
+
265
+ // Decision B (review 22fec5ee, 3 Nov 2026): an idea is its own kind, linked
266
+ // many-to-many to posts by `feeds`, and "Package ideas" is the door that
267
+ // proposes posts from the bank without writing them.
268
+ describe("Content OS ideas: their own kind, feeding posts", () => {
269
+ const idea = (template.profiles ?? []).find((p) => p.slug === "idea");
270
+
271
+ it("declares the idea kind with one lifecycle and no post fields", () => {
272
+ expect(idea?.lifecycle?.property).toBe("idea-status");
273
+ expect(idea?.lifecycle?.options.map((o) => `${o.key}:${o.category}`)).toEqual([
274
+ "New:open",
275
+ "Shaped:active",
276
+ "Used:done",
277
+ "Dropped:lost",
278
+ ]);
279
+ const fields = (idea?.properties ?? []).map((p) => p.slug);
280
+ expect(fields).toEqual(expect.arrayContaining(["idea-status", "content", "idea-pillar", "idea-stage", "idea-tags"]));
281
+ // An idea has no format and no date (that is what makes it not a post).
282
+ expect(fields.some((f) => f.startsWith("post-") || f === "publish-date")).toBe(false);
283
+ });
284
+
285
+ it("links an idea to the posts it feeds, many-to-many", () => {
286
+ const links = (template.entityLinks ?? []).map((l) => `${l.sourceProfileSlug}-${l.type}->${l.targetProfileSlug}`);
287
+ expect(links).toEqual(expect.arrayContaining(["idea-feeds->post", "idea-belongs_to_pillar->pillar"]));
288
+ });
289
+
290
+ it("packages ideas through a bank-wide playbook that proposes posts, never writes them", () => {
291
+ const pkg = template.playbooks?.find((p) => p.name === "Package ideas");
292
+ expect(pkg?.subjectProfile).toBeUndefined();
293
+ expect(pkg?.expectedOutputs).toEqual([expect.objectContaining({ kind: "post", relationToSubject: "none" })]);
294
+ expect(pkg?.goalTemplate).toMatch(/idea feeds post/);
295
+ expect(pkg?.goalTemplate).toMatch(/ONE synap_capture graph/);
296
+ expect(pkg?.goalTemplate).toMatch(/Never a script/);
297
+ });
298
+ });
299
+
300
+ // A post sent for another account (L9: one post per extra account, `post-copy-of`)
301
+ // enters Published like its original. Repurpose must still propose once per
302
+ // ORIGINAL: its activator leaves out a post that carries post-copy-of
303
+ // (backend#766 `unless`), and that property is one the post kind declares.
304
+ describe("Content OS Repurpose proposes once per original, never for a copy", () => {
305
+ const playbooks = (template.playbooks ?? []) as TemplatePlaybook[];
306
+ const repurpose = playbooks.find((p) => p.name === "Repurpose");
307
+
308
+ it("finds Repurpose and its Published activator (non-vacuity)", () => {
309
+ expect(repurpose?.subjectProfile?.profileSlug).toBe("post");
310
+ expect(repurpose?.activators?.filter((a) => a.on === "enters_status" && a.status === "Published")).toHaveLength(1);
311
+ });
312
+
313
+ it("leaves out a post that carries post-copy-of", () => {
314
+ const published = repurpose?.activators?.find((a) => a.on === "enters_status" && a.status === "Published");
315
+ expect(published?.unless).toEqual({ property: "post-copy-of", present: true });
316
+ });
317
+
318
+ it("names a property the post kind declares", () => {
319
+ const post = template.profiles.find((p) => p.slug === "post");
320
+ expect(post?.properties.map((d) => d.slug)).toContain("post-copy-of");
321
+ });
322
+ });
@@ -0,0 +1,338 @@
1
+ /**
2
+ * "Record once, publish many" (founder decisions D3 and D6, 2026-10-27), as
3
+ * catalog config — read from the YAML SOURCE so an edit is seen without a
4
+ * rebuild.
5
+ *
6
+ * - Takes enter Produce Content as a `takes` param, and their presence picks
7
+ * the RE-EDIT video route. Said in the Draft goal as well as the playbook
8
+ * goal: a template update leaves an owner-edited field alone, so `stages`
9
+ * is what still reaches a pod whose goal was edited.
10
+ * - Per-pass approval is ONE person-owned slot per video pass, declared by
11
+ * the agent as each pass's preview exists — never a stage per pass, and
12
+ * never a stage-level slot (all owed on entry, by every format).
13
+ * - A take's transcript is ONE Content OS content-asset (Kind script, File
14
+ * ID = the take's file), the shape Content Studio already writes: its body
15
+ * holds the readable text and a word-level JSON block in HyperFrames'
16
+ * `[{text,start,end}]` shape. Never a second, document-per-take shape.
17
+ * (Founder interview stays a pod-local playbook: changed on the pod, not here.)
18
+ */
19
+ import { readFileSync } from "fs";
20
+ import { dirname, join } from "path";
21
+ import { fileURLToPath } from "url";
22
+ import { load } from "js-yaml";
23
+ import { describe, expect, it } from "vitest";
24
+ import type { TemplatePlaybook, WorkspaceYaml } from "./types.js";
25
+
26
+ const source = load(
27
+ readFileSync(join(dirname(fileURLToPath(import.meta.url)), "content-os.yaml"), "utf8"),
28
+ ) as WorkspaceYaml;
29
+ const playbook = (name: string) =>
30
+ (source.playbooks ?? []).find((p) => p.name === name) as TemplatePlaybook | undefined;
31
+
32
+ /**
33
+ * The five passes, in build order (D3; founder, 2 Nov: background and colour right after the cut, then captions and
34
+ * graphics on top). The keys keep their `layer-` names: Content Studio reads them, in the same order.
35
+ */
36
+ const LAYER_KEYS = ["layer-cut", "layer-background", "layer-colour", "layer-captions", "layer-graphics"];
37
+
38
+ /** D6: the transcript convention, as every process that reads or writes one states it. */
39
+ const TRANSCRIPT_SHAPE = '[{"text","start","end"}]';
40
+
41
+ describe("Produce Content takes footage", () => {
42
+ const produce = playbook("Produce Content");
43
+
44
+ it("declares takes as an optional text param the goal reads", () => {
45
+ expect(produce?.params?.find((p) => p.name === "takes")).toEqual({
46
+ name: "takes",
47
+ type: "text",
48
+ label: "Takes (file ids)",
49
+ });
50
+ expect(produce?.goalTemplate).toContain("takes ({takes}) mean RE-EDIT");
51
+ });
52
+
53
+ it("routes takes and states the transcript convention in the Draft goal", () => {
54
+ const goal = (produce?.stages?.find((s) => s.key === "draft")?.goal ?? "").replace(/\s+/g, " ");
55
+ expect(goal).toContain("the run's takes param, on the run session at metadata.params.takes");
56
+ expect(goal).toContain("mean the RE-EDIT video route");
57
+ expect(goal).toContain(TRANSCRIPT_SHAPE);
58
+ });
59
+
60
+ it("keeps a take's transcript in ONE shape: the content-asset Content Studio writes, found before it is made", () => {
61
+ const goal = (produce?.stages?.find((s) => s.key === "draft")?.goal ?? "").replace(/\s+/g, " ");
62
+ expect(goal).toContain("ONE content-asset, Kind script, whose File ID is the take's file");
63
+ expect(goal).toContain('the readable text under a "## Transcript" heading');
64
+ expect(goal).toContain('"## Words" heading followed by exactly one ```json fenced block');
65
+ expect(goal).toContain("Find it before transcribing");
66
+ expect(goal).toContain("reuse it and only fill the word timings");
67
+ expect(goal).toContain("with a references relation");
68
+ // The second shape this replaced: a document per take.
69
+ expect(goal).not.toMatch(/transcript is a pod document/i);
70
+ expect(goal).not.toContain("one per take");
71
+ });
72
+ });
73
+
74
+ describe("Produce Content approves a video pass by pass", () => {
75
+ const produce = playbook("Produce Content");
76
+ const draft = produce?.stages?.find((s) => s.key === "draft");
77
+
78
+ it("adds no stage for a pass", () => {
79
+ expect(produce?.stages?.map((s) => s.key)).toEqual(["brief", "script", "draft", "review", "schedule", "publish"]);
80
+ });
81
+
82
+ it("declares no pass slot on any stage (a stage's slots are owed on entry, by every format)", () => {
83
+ const declared = (produce?.stages ?? []).flatMap((s) => s.expectedOutputs ?? []);
84
+ for (const o of declared) {
85
+ expect(LAYER_KEYS.some((k) => JSON.stringify(o).includes(k))).toBe(false);
86
+ }
87
+ expect(produce?.expectedOutputs ?? []).toEqual([]);
88
+ });
89
+
90
+ it("calls a video editing step a pass, never a layer (founder 2026-10-28)", () => {
91
+ // Slot keys keep their `layer-` names (Content Studio reads them); the static passes (edit loop, 2 Nov) too.
92
+ const text = [produce?.goalTemplate ?? "", draft?.goal ?? ""]
93
+ .join(" ")
94
+ .replace(/layer-(cut|captions|graphics|background|colour|structure|visual|polish|<pass>)/g, "");
95
+ expect(text).not.toMatch(/\blayers?\b/i);
96
+ expect(text).toMatch(/\bpass(es)?\b/);
97
+ });
98
+
99
+ it("fixes the five person-owned slots, in build order, in the Draft goal", () => {
100
+ const goal = draft?.goal ?? "";
101
+ const at = LAYER_KEYS.map((k) => goal.indexOf(k));
102
+ expect(at.every((i) => i >= 0)).toBe(true);
103
+ expect([...at].sort((a, b) => a - b)).toEqual(at);
104
+ for (const part of [
105
+ "owner human",
106
+ "ask choose with allowOther",
107
+ "Approve (recommended",
108
+ "Rework",
109
+ "completeOutput",
110
+ "answered as the edit-loop skill spells out",
111
+ ]) {
112
+ expect(goal.replace(/\s+/g, " ")).toContain(part);
113
+ }
114
+ // The rest of the slot's shape lives once, in the edit-loop skill (2 Nov).
115
+ const loop = JSON.parse(
116
+ readFileSync(join(dirname(fileURLToPath(import.meta.url)), "../skills/edit-loop/edit-loop.skill.json"), "utf8"),
117
+ ) as { spec: { body: string } };
118
+ expect(loop.spec.body).toContain("owner `human`, blockedReason `decision`, why `Approve the <pass> on its preview`");
119
+ });
120
+ });
121
+
122
+
123
+ describe("Produce Content starts the Background and Colour passes from the booth look", () => {
124
+ const produce = playbook("Produce Content");
125
+ const goal = (produce?.stages?.find((s) => s.key === "draft")?.goal ?? "").replace(/\s+/g, " ");
126
+ const lookAt = goal.indexOf("A LOOK given");
127
+
128
+ it("declares look as an optional text param (Content Studio sends it only when declared)", () => {
129
+ expect(produce?.params?.find((p) => p.name === "look")).toEqual({
130
+ name: "look",
131
+ type: "text",
132
+ label: "Look (JSON)",
133
+ });
134
+ });
135
+
136
+ it("names the matte and the grading block, and still asks each look pass on its own slot", () => {
137
+ expect(lookAt).toBeGreaterThan(0);
138
+ const look = goal.slice(lookAt);
139
+ expect(look).toContain("the run's look param, at metadata.params.look");
140
+ expect(look).toContain("npx hyperframes remove-background");
141
+ expect(look).toContain("grading.lut");
142
+ expect(look).toContain('{"mode":"blur"}');
143
+ expect(look).toContain("Each is still its own pass, with its own slot (layer-background, layer-colour) and its own question");
144
+ expect(look).toContain("Remove this pass");
145
+ expect(look).toContain("The raw take is never overwritten");
146
+ });
147
+
148
+ it("leaves the path without a look as it was: the look wording follows the pass wording, which is unchanged", () => {
149
+ // The answers' details moved to the edit-loop skill (2 Nov); the goal keeps the slot and names the skill.
150
+ const passes =
151
+ "ask choose with allowOther, options Approve (recommended), Rework and Use my edit, answered as the edit-loop " +
152
+ "skill spells out (Approve claims the slot: completeOutput). Build the next pass only after the answer.";
153
+ expect(goal).toContain(passes);
154
+ expect(goal.indexOf(passes)).toBeLessThan(lookAt);
155
+ // Nothing before the look sentence mentions it: the old path reads exactly as before.
156
+ expect(goal.slice(0, lookAt)).not.toMatch(/\blook\b|remove-background|grading/i);
157
+ });
158
+ });
159
+
160
+ /**
161
+ * The Background pass cuts the person out with THEIR matting tool (intent
162
+ * `edit_video`, founder decisions be0deab7 and ac1ba2b6, 2026-10-28), and the
163
+ * local HyperFrames matte is only the fallback: it squashes each frame to
164
+ * 320x320 with no memory between frames (research 2e187277). The chain never
165
+ * breaks the video: tool → local matte over the real room → raw take.
166
+ */
167
+ describe("Produce Content's Background pass uses the person's edit_video tool first", () => {
168
+ const produce = playbook("Produce Content");
169
+ const goal = (produce?.stages?.find((s) => s.key === "draft")?.goal ?? "").replace(/\s+/g, " ");
170
+ const look = goal.slice(goal.indexOf("A LOOK given"));
171
+ const skill = JSON.parse(
172
+ readFileSync(join(dirname(fileURLToPath(import.meta.url)), "../skills/facecam-edit/facecam-edit.skill.json"), "utf8"),
173
+ ) as { spec: { body: string } };
174
+ const body = skill.spec.body;
175
+ const pass = body.slice(body.indexOf("**Background pass.**"), body.indexOf("**Colour pass.**"));
176
+
177
+ it("the Draft goal finds the tool by intent and hands it the take's pod file id, signed by the pod", () => {
178
+ expect(look).toContain("the person's edit_video tool (synap_list_capabilities with intent edit_video)");
179
+ expect(look).toContain('take\'s pod file id ({"videoFile": id}; the pod signs it at run time)');
180
+ });
181
+
182
+ it("the Draft goal gates the matte and finishes the composite, in order", () => {
183
+ const steps = ["gate the matte", "frame count and duration match the take", "no flicker", "defocus the background once", "choke, feather, light wrap", "grain on the background and a light match on the person"];
184
+ const at = steps.map((s) => look.indexOf(s));
185
+ expect(at.every((i) => i >= 0)).toBe(true);
186
+ expect([...at].sort((a, b) => a - b)).toEqual(at);
187
+ });
188
+
189
+ it("the Draft goal falls back tool → local matte over the chosen background → raw take marked background not applied", () => {
190
+ const tool = look.indexOf("edit_video tool");
191
+ const local = look.indexOf("npx hyperframes remove-background instead and composite the same way over the same background");
192
+ const raw = look.indexOf('says "background not applied"');
193
+ expect(tool).toBeGreaterThanOrEqual(0);
194
+ expect(local).toBeGreaterThan(tool);
195
+ expect(raw).toBeGreaterThan(local);
196
+ expect(look.indexOf("No edit_video tool, a failed run or a failed gate")).toBeLessThan(local);
197
+ });
198
+
199
+ it("the skill runs the tool with the video alone, waits through next, and never re-runs the billed verb", () => {
200
+ expect(pass).toContain('synap_list_capabilities({ intent: "edit_video" })');
201
+ expect(pass).toContain('RVM takes `{ "videoFile": <file entity id> }`');
202
+ expect(pass).toContain("never fetch a signed URL yourself for it");
203
+ expect(pass).toContain("`GET /api/hub/files/<fileId>/url` just before the run");
204
+ expect(pass).toContain("run its `next`");
205
+ expect(pass).toContain("Never run the matte verb again for the same take");
206
+ expect(pass).toContain("Download every URL at once");
207
+ // A tool with no pod-file parameter still gets a URL, fetched just before the run.
208
+ expect(pass).toContain("A tool that declares no pod-file parameter takes `{ \"video\": <https URL> }`");
209
+ });
210
+
211
+ it("the skill gates frames and flicker before use, then composites and treats the layers apart", () => {
212
+ for (const part of ["nb_read_frames", "tblend=all_mode=difference", "never drops under half its median", "gblur=sigma=12", "alphamerge", '"details":{"grain"', "`adjust` section"]) {
213
+ expect(pass).toContain(part);
214
+ }
215
+ expect(pass.indexOf("**Gate the matte**")).toBeLessThan(pass.indexOf("**Defocus the background once.**"));
216
+ expect(pass.indexOf("**Defocus the background once.**")).toBeLessThan(pass.indexOf("**Composite the person with ffmpeg**"));
217
+ });
218
+
219
+ it("the skill's fallback order is local matte over the chosen background, then the raw take, and Remove this pass keeps working", () => {
220
+ const fallback = pass.slice(pass.indexOf("**Fallback, in order.**"));
221
+ const local = fallback.indexOf("npx hyperframes remove-background");
222
+ const raw = fallback.indexOf('"background not applied"');
223
+ expect(local).toBeGreaterThan(0);
224
+ expect(raw).toBeGreaterThan(local);
225
+ // 31 Oct (founder: "still not able to easily set up adding a background"):
226
+ // the local matte goes over the background the look chose, never the room by force.
227
+ expect(fallback).toContain("over the background the look chose");
228
+ expect(fallback).not.toContain("whatever the look asked");
229
+ expect(pass).toContain("Remove this pass: put the raw take back in the cut");
230
+ });
231
+
232
+ it("the Colour pass adds the LUT to an element the Background pass treated, never replaces its grain or light match", () => {
233
+ const colour = body.slice(body.indexOf("**Colour pass.**"), body.indexOf("## 7."));
234
+ expect(colour).toContain("send them again with `lut` in the same `--grading` object");
235
+ });
236
+ });
237
+
238
+ /**
239
+ * The video page (D4-D7, 29 Oct): the person sees which pass the agent is on,
240
+ * can do a pass themselves in HyperFrames Studio and keep it ("Use my edit"),
241
+ * and opens Studio from the app through the address the run records. The
242
+ * cut is a HyperFrames project from the first pass (D7), so Studio shows it.
243
+ */
244
+ describe("Produce Content's passes: agent working, Use my edit, Studio", () => {
245
+ const produce = playbook("Produce Content");
246
+ const goal = (produce?.stages?.find((s) => s.key === "draft")?.goal ?? "").replace(/\s+/g, " ");
247
+ const skill = JSON.parse(
248
+ readFileSync(join(dirname(fileURLToPath(import.meta.url)), "../skills/facecam-edit/facecam-edit.skill.json"), "utf8"),
249
+ ) as { meta: { description: string }; spec: { body: string; description: string } };
250
+ const body = skill.spec.body;
251
+
252
+ it("declares a pass's slot as the agent's before building it, then hands the same key to the person", () => {
253
+ const working = goal.indexOf("When a pass starts, declare its slot as yours first (an outcome with the pass's key, kind file, no owner, no ask)");
254
+ const handed = goal.indexOf("After each pass, render its preview into the pod and hand the person ONE slot for it");
255
+ expect(working).toBeGreaterThan(0);
256
+ expect(handed).toBeGreaterThan(working);
257
+ });
258
+
259
+ it("offers Use my edit on every pass and never overwrites the person's edit", () => {
260
+ expect(goal).toContain("Use my edit: their edit is the pass; never overwrite or redo their changes");
261
+ expect(body).toContain("**Use my edit** (an answer on a pass's question)");
262
+ expect(body).toContain("keep it as the pass and change nothing of it");
263
+ });
264
+
265
+ it("records the Studio address as the run's `studio` outcome on the fixed edit/ project, and retires it", () => {
266
+ for (const part of [
267
+ "~/synap-work/<this run's session id>/edit",
268
+ "whose address is the run's studio outcome",
269
+ "retireOutput studio",
270
+ ]) {
271
+ expect(goal).toContain(part);
272
+ }
273
+ // The commands moved out of the goal (2 Nov, edit loop): the skill holds them.
274
+ expect(body).toContain("npx hyperframes preview --background --port <a free port, from 3002 up>");
275
+ expect(body).toContain("`npx hyperframes preview --stop`");
276
+ // The skill says the same key and URL the app reads.
277
+ expect(body).toContain('{ key: "studio", kind: "link", label: "HyperFrames Studio", required: false, ref: { url: "http://localhost:<port>/#project/edit" } }');
278
+ expect(body).toContain("`~/synap-work/<run session id>/edit`");
279
+ expect(body).toContain('retireOutput: "studio"');
280
+ });
281
+
282
+ it("keeps one history turn per pass so the person's Studio edits are never undone", () => {
283
+ for (const part of ["history begin --who", "history --since mine", "history end", "history undo --who"]) {
284
+ expect(body).toContain(part);
285
+ }
286
+ });
287
+ });
288
+
289
+ /** D7 (29 Oct, supersedes 19ee2ca2): HyperFrames is the editor of record for the cut. */
290
+ describe("facecam-edit cuts in HyperFrames", () => {
291
+ const read = (rel: string) =>
292
+ JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), rel), "utf8")) as {
293
+ meta: { description: string };
294
+ spec: { body: string; description: string };
295
+ };
296
+ const skill = read("../skills/facecam-edit/facecam-edit.skill.json");
297
+ const body = skill.spec.body;
298
+ const cut = body.slice(body.indexOf("## 4. Apply the cut"), body.indexOf("## 5."));
299
+
300
+ it("names HyperFrames as the editor of record and lays the cut as clips, never a baked file", () => {
301
+ expect(cut).toContain("**Editor of record: HyperFrames.**");
302
+ expect(cut).toContain("never a baked file");
303
+ expect(cut).toContain("`data-media-start`");
304
+ expect(cut).toContain("transcript-cut.mjs --plan");
305
+ expect(cut).toContain("lay them as clips, never encode them");
306
+ });
307
+
308
+ it("no longer sends the cut to OpenChatCut, in the skill or in the toolbox", () => {
309
+ const toolbox = read("../skills/video-toolbox/video-toolbox.skill.json").spec.body;
310
+ for (const text of [body, skill.spec.description, skill.meta.description, toolbox]) {
311
+ expect(text).not.toContain("OpenChatCut");
312
+ }
313
+ expect(toolbox).toContain("**Work a take**");
314
+ });
315
+
316
+ it("keeps the invisible-cut rule (founder, 29 Oct)", () => {
317
+ expect(body).toContain("**Invisible cuts (picture continuity).**");
318
+ expect(body).toContain("Never move an edge into speech");
319
+ });
320
+ });
321
+
322
+ /** Finding f36420d5: the pilot post stayed at Idea while its run reached Draft. */
323
+ describe("Produce Content moves the post's status with its stages", () => {
324
+ const produce = playbook("Produce Content");
325
+ const goalOf = (key: string) => (produce?.stages?.find((s) => s.key === key)?.goal ?? "").replace(/\s+/g, " ");
326
+
327
+ it("moves an Idea post to Briefed when the run starts, through the status door", () => {
328
+ expect(goalOf("brief")).toContain(
329
+ "if the post still reads Idea, move it to Briefed first (synap_update_entity, post-status Briefed, expectedStatus Idea)",
330
+ );
331
+ expect(goalOf("brief")).toContain("never skipping one, takes included");
332
+ });
333
+
334
+ it("makes a take's cut plan the script, and says the Draft gate's approval writes In Draft", () => {
335
+ expect(goalOf("script")).toContain("With takes (the RE-EDIT route), the script is the cut plan");
336
+ expect(goalOf("draft").startsWith("Entering Draft waits on the person: this stage's gate asks them to approve the script, and that approval moves the post to In Draft. Build nothing before it passes.")).toBe(true);
337
+ });
338
+ });