@synap-core/workspace-templates 0.12.0 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +509 -0
- package/automations/bookmark-enrichment.automation.json +321 -0
- package/automations/github-pr-events.automation.json +505 -0
- package/automations/mail-feed.automation.json +86 -0
- package/automations/ship-a-change-checks-green.automation.json +106 -0
- package/automations/ship-a-change-checks-red.automation.json +106 -0
- package/automations/ship-a-change-deployed.automation.json +56 -0
- package/automations/ship-a-change-pr-merged.automation.json +110 -0
- package/automations/ship-a-change-pr-opened.automation.json +104 -0
- package/automations/url-bookmark-capture.automation.json +132 -0
- package/capabilities/agency-skills.capability.json +157 -0
- package/capabilities/apify.capability.json +265 -0
- package/capabilities/apollo.capability.json +120 -0
- package/capabilities/arch-backend.capability.json +97 -0
- package/capabilities/arch-client-intelligence.capability.json +401 -0
- package/capabilities/backblaze-b2.capability.json +205 -0
- package/capabilities/cal-com.capability.json +266 -0
- package/capabilities/canva.capability.json +274 -0
- package/capabilities/claude-code-routines.capability.json +172 -0
- package/capabilities/claude-managed-agents.capability.json +215 -0
- package/capabilities/client-comms-report.capability.json +246 -0
- package/capabilities/content-idea-to-production.capability.json +29 -0
- package/capabilities/datagouv.capability.json +315 -0
- package/capabilities/discord-bot.capability.json +157 -0
- package/capabilities/dropbox.capability.json +281 -0
- package/capabilities/exa.capability.json +109 -0
- package/capabilities/fal-ai.capability.json +83 -0
- package/capabilities/fal-media-lab.capability.json +158 -0
- package/capabilities/fireflies.capability.json +191 -0
- package/capabilities/freellmapi.capability.json +114 -0
- package/capabilities/generic-apikey.capability.json +92 -0
- package/capabilities/github-merge.capability.json +99 -0
- package/capabilities/github-read.capability.json +325 -0
- package/capabilities/governance-tighten-recommender.capability.json +81 -0
- package/capabilities/jev.capability.json +226 -0
- package/capabilities/lead-conversion-enrollment.capability.json +246 -0
- package/capabilities/mail-feed.capability.json +277 -0
- package/capabilities/mailgun.capability.json +52 -0
- package/capabilities/nango-google.capability.json +639 -0
- package/capabilities/notion.capability.json +417 -0
- package/capabilities/omniroute.capability.json +137 -0
- package/capabilities/posthog-analytics.capability.json +258 -0
- package/capabilities/postiz.capability.json +467 -0
- package/capabilities/proactive-digest.capability.json +190 -0
- package/capabilities/proton.capability.json +288 -0
- package/capabilities/replicate-rvm.capability.json +97 -0
- package/capabilities/research-methods.capability.json +57 -0
- package/capabilities/shopify.capability.json +395 -0
- package/capabilities/slack.capability.json +369 -0
- package/capabilities/stripe.capability.json +1496 -0
- package/capabilities/telegram-bridge.capability.json +56 -0
- package/capabilities/treg.capability.json +102 -0
- package/capabilities/unipile-linkedin.capability.json +494 -0
- package/capabilities/vercel.capability.json +424 -0
- package/dist/define-DqoItRiR.d.ts +1370 -0
- package/dist/index.d.ts +174 -900
- package/dist/index.js +26447 -24075
- package/dist/index.js.map +1 -1
- package/dist/wire.d.ts +222 -0
- package/dist/wire.js +77 -0
- package/dist/wire.js.map +1 -0
- package/package.json +27 -4
- package/retired.yaml +136 -0
- package/skills/app-factory-build-slice/SKILL.md +70 -0
- package/skills/app-factory-build-slice.catalog.json +17 -0
- package/skills/app-factory-design-canvas/SKILL.md +96 -0
- package/skills/app-factory-design-canvas.catalog.json +17 -0
- package/skills/app-factory-design-review/SKILL.md +105 -0
- package/skills/app-factory-design-review.catalog.json +18 -0
- package/skills/app-factory-research/SKILL.md +123 -0
- package/skills/app-factory-research.catalog.json +17 -0
- package/skills/bookmark-enrichment/SKILL.md +40 -0
- package/skills/carousel-format/carousel-format.skill.json +18 -0
- package/skills/collage-broll/collage-broll.skill.json +20 -0
- package/skills/content-taste/content-taste.skill.json +18 -0
- package/skills/creative-director/creative-director.skill.json +18 -0
- package/skills/design-critique/SKILL.md +138 -0
- package/skills/design-critique.catalog.json +12 -0
- package/skills/designing-with-real-components/SKILL.md +140 -0
- package/skills/designing-with-real-components.catalog.json +12 -0
- package/skills/edit-loop/edit-loop.skill.json +25 -0
- package/skills/facecam-edit/facecam-edit.skill.json +24 -0
- package/skills/mapping-the-server/SKILL.md +54 -0
- package/skills/math-explainer-manimgx/math-explainer-manimgx.skill.json +20 -0
- package/skills/motion-quality-gates/motion-quality-gates.skill.json +20 -0
- package/skills/open-design-edit/open-design-edit.skill.json +24 -0
- package/skills/reading-client-progress/SKILL.md +65 -0
- package/skills/routing-to-channels/SKILL.md +54 -0
- package/skills/stellar-scf-grants-advisor/ASSOCIATE-CHEATSHEET.md +133 -0
- package/skills/stellar-scf-grants-advisor/ASSOCIATE-ONBOARDING-GUIDE.md +761 -0
- package/skills/stellar-scf-grants-advisor/CLIENT-FAQ.md +181 -0
- package/skills/stellar-scf-grants-advisor/PARTNER-HANDOVER.md +130 -0
- package/skills/stellar-scf-grants-advisor/SKILL.md +102 -0
- package/skills/stellar-scf-grants-advisor/examples/README.md +43 -0
- package/skills/stellar-scf-grants-advisor/examples/chat-summary-templates.md +158 -0
- package/skills/stellar-scf-grants-advisor/examples/corpus-comparable-rows.md +136 -0
- package/skills/stellar-scf-grants-advisor/examples/q10-team-rewrites.md +109 -0
- package/skills/stellar-scf-grants-advisor/examples/q2-description-rewrites.md +88 -0
- package/skills/stellar-scf-grants-advisor/examples/q6-integration-rewrites.md +107 -0
- package/skills/stellar-scf-grants-advisor/examples/tranche-structures.md +176 -0
- package/skills/stellar-scf-grants-advisor/reference/01-methodology.md +96 -0
- package/skills/stellar-scf-grants-advisor/reference/02-scoring-framework.md +199 -0
- package/skills/stellar-scf-grants-advisor/reference/03-rejection-patterns.md +241 -0
- package/skills/stellar-scf-grants-advisor/reference/04-winner-patterns.md +120 -0
- package/skills/stellar-scf-grants-advisor/reference/05-comeback-patterns.md +92 -0
- package/skills/stellar-scf-grants-advisor/reference/06-budget-benchmarks.md +125 -0
- package/skills/stellar-scf-grants-advisor/reference/07-integration-list.md +147 -0
- package/skills/stellar-scf-grants-advisor/reference/08-abstract-fields.md +258 -0
- package/skills/stellar-scf-grants-advisor/reference/09-build-fields.md +259 -0
- package/skills/stellar-scf-grants-advisor/reference/10-corpus-winners.md +135 -0
- package/skills/stellar-scf-grants-advisor/reference/11-corpus-rejects.md +267 -0
- package/skills/stellar-scf-grants-advisor/reference/12-client-communication.md +131 -0
- package/skills/stellar-scf-grants-advisor/reference/13-excel-output-spec.md +199 -0
- package/skills/stellar-scf-grants-advisor/reference/14-worked-examples.md +160 -0
- package/skills/stellar-scf-grants-advisor/templates/abstract-review-builder.py +253 -0
- package/skills/stellar-scf-grants-advisor/templates/build-review-builder.py +213 -0
- package/skills/video-direction/video-direction.skill.json +20 -0
- package/skills/video-style-vocabulary/video-style-vocabulary.skill.json +20 -0
- package/skills/video-toolbox/video-toolbox.skill.json +20 -0
- package/src/__fixtures__/lifecycle-presets.flat-before.json +205 -0
- package/src/agency-os.yaml +122 -0
- package/src/agent-os-skill-slugs.test.ts +65 -0
- package/src/apify-social-reader.test.ts +317 -0
- package/src/base.yaml +85 -39
- package/src/blockchain-ecosystem.yaml +1 -0
- package/src/brand-library.yaml +525 -14
- package/src/builder-dev-fold.template.test.ts +113 -0
- package/src/builder.yaml +779 -91
- package/src/business-model-catalog-description.test.ts +47 -0
- package/src/business-model-grp.template.test.ts +214 -0
- package/src/business-model.yaml +296 -49
- package/src/catalog-safety.test.ts +198 -0
- package/src/catalog.template-items.test.ts +75 -0
- package/src/catalog.ts +66 -2
- package/src/concept-guard.test.ts +28 -8
- package/src/concept-guard.ts +2 -2
- package/src/content-creation.template.test.ts +52 -0
- package/src/content-creation.yaml +192 -0
- package/src/content-os.edit-loop.template.test.ts +210 -0
- package/src/content-os.publish-due-posts.template.test.ts +261 -0
- package/src/content-os.template.test.ts +349 -0
- package/src/content-os.video-takes.template.test.ts +338 -0
- package/src/content-os.work-a-take.template.test.ts +125 -0
- package/src/content-os.yaml +1873 -39
- package/src/content-studio-retired.template.test.ts +82 -0
- package/src/corpus-flow-validity.test.ts +4 -5
- package/src/crm-absorbs-outreach.template.test.ts +117 -0
- package/src/crm-build-plan.template.test.ts +128 -0
- package/src/crm-lists.template.test.ts +96 -0
- package/src/crm-messages-cadence.template.test.ts +246 -0
- package/src/crm-one-send-door.template.test.ts +65 -0
- package/src/crm.yaml +1563 -181
- package/src/decisions-wave.template.test.ts +186 -0
- package/src/define.ts +146 -17
- package/src/design-to-implementation.template.test.ts +94 -0
- package/src/domain-template-type.test.ts +75 -0
- package/src/ecosystem.yaml +27 -66
- package/src/enterprise-os.yaml +10 -134
- package/src/facecam-edit.audio-studio.test.ts +188 -0
- package/src/fal-generate-image.test.ts +200 -0
- package/src/fal-media-lab-pod-files.test.ts +180 -0
- package/src/finance.yaml +254 -1
- package/src/foundation.yaml +78 -38
- package/src/frame-host-modules.d.mts +6 -0
- package/src/frame-host-modules.mjs +58 -0
- package/src/generate.mjs +9 -3
- package/src/github-merge.test.ts +210 -0
- package/src/grants.yaml +46 -17
- package/src/hr.yaml +39 -6
- package/src/index.ts +36 -1
- package/src/intent.test.ts +161 -0
- package/src/internal-runbook.yaml +19 -1
- package/src/jev.test.ts +356 -0
- package/src/legal.yaml +33 -1
- package/src/life-os.yaml +51 -240
- package/src/lifecycle-coverage.test.ts +124 -0
- package/src/lifecycle-declarations.test.ts +252 -0
- package/src/lifecycle-presets.test.ts +468 -0
- package/src/lifecycle-presets.ts +494 -0
- package/src/lifecycle-stuck.test.ts +46 -0
- package/src/lifecycle-waits.parity.test.ts +319 -0
- package/src/marketing.yaml +108 -4
- package/src/meetings-knowledge.yaml +18 -0
- package/src/networking.yaml +23 -2
- package/src/operations.yaml +304 -4
- package/src/pipeline-views.template.test.ts +88 -0
- package/src/playbook-goal-placeholders.test.ts +57 -0
- package/src/postiz-contract.test.ts +134 -0
- package/src/presentation-declarations.test.ts +174 -0
- package/src/process-contract.validate.test.ts +352 -0
- package/src/project-management.yaml +39 -740
- package/src/publish-body.d.mts +29 -0
- package/src/publish-body.mjs +74 -0
- package/src/publish-body.test.ts +74 -0
- package/src/publish-official-lib.test.ts +176 -0
- package/src/replicate-rvm.test.ts +504 -0
- package/src/research-base.yaml +119 -20
- package/src/research-decision.yaml +60 -3
- package/src/reserved-profile-slugs.test.ts +81 -0
- package/src/retired-goal-kinds.test.ts +52 -0
- package/src/role-reference-and-step-domain.test.ts +192 -0
- package/src/seed-obligation.test.ts +1 -6
- package/src/send-message-maps.test.ts +227 -0
- package/src/shared-role-ssot.test.ts +40 -2
- package/src/ship-a-change.template.test.ts +177 -0
- package/src/skills.test.ts +170 -0
- package/src/social.yaml +85 -27
- package/src/space-packages.freshness.test.ts +97 -0
- package/src/space-packages.generated.ts +40 -0
- package/src/space-packages.test.ts +125 -0
- package/src/space-packages.ts +73 -0
- package/src/start.test.ts +188 -0
- package/src/stripe-capability.test.ts +192 -0
- package/src/task-intents.test.ts +293 -0
- package/src/template-documents.test.ts +158 -0
- package/src/templates.ts +20907 -21464
- package/src/types.ts +475 -61
- package/src/unipile-linkedin-find-profile.test.ts +183 -0
- package/src/unipile-publish-post.test.ts +162 -0
- package/src/validate-flow.drift.test.ts +2 -3
- package/src/validate-flow.ts +22 -2
- package/src/validate-onboarding.test.ts +59 -0
- package/src/validate.test.ts +71 -4
- package/src/validate.ts +1416 -7
- package/src/wire-core.ts +231 -0
- package/src/wire.test.ts +159 -0
- package/src/wire.ts +107 -0
- package/views/bookmark-views-pack.view.json +39 -0
- package/views/event-views-pack.view.json +45 -0
- package/views/task-views-pack.view.json +53 -0
- package/src/agent-fleet.yaml +0 -1645
- package/src/business-developer.template.test.ts +0 -351
- package/src/business-developer.yaml +0 -860
- package/src/content-studio.yaml +0 -361
- package/src/dev-dashboard.yaml +0 -990
- package/src/outreach-comms.yaml +0 -486
- package/src/the-arch.yaml +0 -201
|
@@ -0,0 +1,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
|
+
});
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* "One take, several videos" (founder decisions D1, D2, D3, 29 Oct), as
|
|
3
|
+
* catalog config, read from the YAML SOURCE so an edit is seen without a
|
|
4
|
+
* rebuild.
|
|
5
|
+
*
|
|
6
|
+
* - D1: a take is a Content OS content-asset of Kind `recording`, holding
|
|
7
|
+
* its facts; its `file-id` is the raw take.
|
|
8
|
+
* - D2: a piece is a NEW video file cut from the take, recorded as the same
|
|
9
|
+
* Kind with `source-take`, `source-start` and `source-end`. The raw take
|
|
10
|
+
* is never overwritten; a re-cut is a new file, the old one kept.
|
|
11
|
+
* - D3: the process "Work a take" transcribes, finds the pieces, asks the
|
|
12
|
+
* person which to keep, cuts the kept ones and makes one post per piece.
|
|
13
|
+
*
|
|
14
|
+
* Content Studio and the other lanes read these exact keys, so they are pinned.
|
|
15
|
+
*/
|
|
16
|
+
import { readFileSync } from "fs";
|
|
17
|
+
import { dirname, join } from "path";
|
|
18
|
+
import { fileURLToPath } from "url";
|
|
19
|
+
import { load } from "js-yaml";
|
|
20
|
+
import { describe, expect, it } from "vitest";
|
|
21
|
+
import type { TemplatePlaybook, WorkspaceYaml } from "./types.js";
|
|
22
|
+
|
|
23
|
+
const source = load(
|
|
24
|
+
readFileSync(join(dirname(fileURLToPath(import.meta.url)), "content-os.yaml"), "utf8"),
|
|
25
|
+
) as WorkspaceYaml;
|
|
26
|
+
const asset = source.profiles.find((p) => p.slug === "content-asset");
|
|
27
|
+
const prop = (slug: string) => asset?.properties.find((d) => d.slug === slug);
|
|
28
|
+
const work = (source.playbooks ?? []).find((p) => p.name === "Work a take") as TemplatePlaybook | undefined;
|
|
29
|
+
const goalOf = (key: string) => (work?.stages?.find((s) => s.key === key)?.goal ?? "").replace(/\s+/g, " ");
|
|
30
|
+
|
|
31
|
+
/** The ONE transcript shape (D6 of 27 Oct), as every process that writes one states it. */
|
|
32
|
+
const TRANSCRIPT_SHAPE = '[{"text","start","end"}]';
|
|
33
|
+
|
|
34
|
+
describe("a take and a piece are content-assets of Kind recording", () => {
|
|
35
|
+
it("adds recording to the asset kinds, keeping every earlier value", () => {
|
|
36
|
+
expect(prop("asset-kind")?.enumValues).toEqual(["image", "video", "audio", "font", "script", "recording", "other"]);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("declares the recording facts with their types", () => {
|
|
40
|
+
for (const slug of ["duration-seconds", "frame-width", "frame-height", "frame-rate", "source-start", "source-end"]) {
|
|
41
|
+
expect({ slug, valueType: prop(slug)?.valueType }).toEqual({ slug, valueType: "number" });
|
|
42
|
+
}
|
|
43
|
+
for (const slug of ["poster-file-id", "take-topic", "take-look", "piece-hook"]) {
|
|
44
|
+
expect({ slug, valueType: prop(slug)?.valueType }).toEqual({ slug, valueType: "string" });
|
|
45
|
+
}
|
|
46
|
+
expect(prop("recorded-at")?.valueType).toBe("date");
|
|
47
|
+
expect(prop("take-kind")?.enumValues).toEqual(["answer", "free"]);
|
|
48
|
+
expect(prop("piece-format")?.enumValues).toEqual(["Short Video", "Long Video"]);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("gives a piece its lineage: the take it was cut from, and its earlier cuts", () => {
|
|
52
|
+
expect(prop("source-take")).toMatchObject({ valueType: "entity_id", targetProfileSlug: "content-asset" });
|
|
53
|
+
expect(prop("earlier-file-ids")?.valueType).toBe("array");
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe("Work a take finds the pieces in a take and makes them new files", () => {
|
|
58
|
+
it("is a process on the take, with no status of its own", () => {
|
|
59
|
+
expect(work?.subjectProfile).toEqual({ profileSlug: "content-asset" });
|
|
60
|
+
expect(work?.params).toEqual([{ name: "take", type: "entity", label: "Take", required: true }]);
|
|
61
|
+
expect(work?.executor).toBe("is-agent");
|
|
62
|
+
expect(work?.stages?.map((s) => s.key)).toEqual(["transcribe", "analyse", "keep", "cut", "posts"]);
|
|
63
|
+
for (const stage of work?.stages ?? []) {
|
|
64
|
+
expect({ stage: stage.key, subjectStatus: stage.subjectStatus }).toEqual({ stage: stage.key, subjectStatus: undefined });
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("asks the person about each piece on a slot, never through a stage gate or a stage-level slot", () => {
|
|
69
|
+
for (const stage of work?.stages ?? []) {
|
|
70
|
+
expect({ stage: stage.key, gate: stage.gate }).toEqual({ stage: stage.key, gate: undefined });
|
|
71
|
+
}
|
|
72
|
+
const declared = JSON.stringify([...(work?.expectedOutputs ?? []), ...(work?.stages ?? []).flatMap((s) => s.expectedOutputs ?? [])]);
|
|
73
|
+
for (const key of ["keep-piece-", "piece-<n>", "post-<n>"]) expect(declared).not.toContain(key);
|
|
74
|
+
const analyse = goalOf("analyse");
|
|
75
|
+
for (const part of [
|
|
76
|
+
"key keep-piece-<n>; kind decision; owner human; blockedReason decision",
|
|
77
|
+
"ask choose with allowOther, options Keep (recommended when it stands alone and fits its format), Drop, and Adjust",
|
|
78
|
+
"<start>-<end> in seconds",
|
|
79
|
+
]) {
|
|
80
|
+
expect(analyse).toContain(part);
|
|
81
|
+
}
|
|
82
|
+
expect(goalOf("keep")).toContain("a piece the person dropped is never cut");
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("transcribes into the ONE transcript shape and writes ONE analysis with a pieces block", () => {
|
|
86
|
+
const transcribe = goalOf("transcribe");
|
|
87
|
+
expect(transcribe).toContain("ONE content-asset, Kind script, whose File ID is the take's file");
|
|
88
|
+
expect(transcribe).toContain(TRANSCRIPT_SHAPE);
|
|
89
|
+
expect(transcribe).toContain("Never transcribe twice, never a separate document");
|
|
90
|
+
expect(goalOf("analyse")).toContain('"## Pieces" heading followed by exactly one ```json fenced block [{"n","start","end","title","hook","format","why"}]');
|
|
91
|
+
expect(work?.stages?.find((s) => s.key === "analyse")?.expectedOutputs).toEqual([
|
|
92
|
+
{ kind: "document", label: "Analysis", relationToSubject: "references" },
|
|
93
|
+
]);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("cuts each kept piece into a NEW file with ffmpeg and never touches the raw take", () => {
|
|
97
|
+
const cut = goalOf("cut");
|
|
98
|
+
for (const part of [
|
|
99
|
+
"Declare its slot as yours first (an outcome, key piece-<n>, kind content-asset, no owner, no ask)",
|
|
100
|
+
"from the raw take into a NEW file",
|
|
101
|
+
"~/synap-work/<this run's session id>/pieces/<n>.mp4",
|
|
102
|
+
"ffmpeg -ss <start> -to <end> -i <raw take>",
|
|
103
|
+
"Kind recording",
|
|
104
|
+
"source-take the take, source-start and source-end",
|
|
105
|
+
"append the old file id to earlier-file-ids; never delete or overwrite a file",
|
|
106
|
+
]) {
|
|
107
|
+
expect(cut).toContain(part);
|
|
108
|
+
}
|
|
109
|
+
// The piece gets its own transcript in the same shape, so Produce Content finds it by its file.
|
|
110
|
+
expect(cut).toContain("its own transcript in the same ONE shape (File ID = the piece's file)");
|
|
111
|
+
expect((work?.goalTemplate ?? "").replace(/\s+/g, " ")).toContain("The raw take is never overwritten");
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("makes one video post at Idea per piece and starts nothing unasked", () => {
|
|
115
|
+
const posts = goalOf("posts");
|
|
116
|
+
expect(posts).toContain("Make one video post per cut piece, at Idea");
|
|
117
|
+
expect(posts).toContain("linked to the piece with a references relation");
|
|
118
|
+
expect(posts).toContain("key post-<n>, kind post");
|
|
119
|
+
expect(posts).toContain("Start Produce Content for a post only if the person asks");
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("links ideas from the take back to it", () => {
|
|
123
|
+
expect(goalOf("analyse")).toContain("propose each as a post at Idea that quotes the person's sentence and links to the take with a references relation");
|
|
124
|
+
});
|
|
125
|
+
});
|