@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,138 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: design-critique
|
|
3
|
+
description: Critique a ui-design (each variant of a request) or a built screen's Storybook story with a three-tier rubric of stable rule ids — checked (a tool decides), pattern (you classify, a fixed house rule judges), opinion (a named heuristic, low confidence, never applied on its own) — and write the findings into the design's critique. Use in the Critique stage of "Design to implementation", or when asked to review, critique or audit a design or a screen.
|
|
4
|
+
metadata:
|
|
5
|
+
synap_native: true
|
|
6
|
+
auto_load: false
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Design critique
|
|
10
|
+
|
|
11
|
+
A critique is a list of **findings**, each tied to a **rule id** below. No rule id, no finding. The
|
|
12
|
+
rubric has three tiers, and they are not equally trusted:
|
|
13
|
+
|
|
14
|
+
| Tier | Who decides | May block | Applied automatically |
|
|
15
|
+
| --------- | ------------------------------------------------------------ | --------- | ---------------------------- |
|
|
16
|
+
| `checked` | a tool or a measurement | yes | the generator fixes it |
|
|
17
|
+
| `pattern` | you CLASSIFY what is on screen; the fixed house rule decides | yes | the generator fixes it |
|
|
18
|
+
| `opinion` | your judgement, tied to a named heuristic | never | **never** — a person decides |
|
|
19
|
+
|
|
20
|
+
You do not invent rules. Every `checked` and `pattern` rule comes from a house rule file (cited); every
|
|
21
|
+
`opinion` names a published heuristic. If something bothers you and no rule fits, it is an `opinion`.
|
|
22
|
+
|
|
23
|
+
## Findings (the `critique` property of the ui-design)
|
|
24
|
+
|
|
25
|
+
```json
|
|
26
|
+
[
|
|
27
|
+
{
|
|
28
|
+
"id": "f-1",
|
|
29
|
+
"tier": "pattern",
|
|
30
|
+
"rule": "pat.empty-answer",
|
|
31
|
+
"severity": "major",
|
|
32
|
+
"confidence": "high",
|
|
33
|
+
"region": { "nodeId": "SpatialPanel-3" },
|
|
34
|
+
"evidence": "Section 'Recent runs' renders a grey caption 'Nothing here' when empty. Classified: bare caption (none of OMIT/INVITE/EXPLAIN/REASSURE).",
|
|
35
|
+
"suggestion": "Only an agent fills it: EXPLAIN with why it is empty and a door to ask the AI.",
|
|
36
|
+
"verdict": null
|
|
37
|
+
}
|
|
38
|
+
]
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
- `id` — unique within this critique (`f-1`, `f-2`, …). `rule` — a rule id from the tables below, exactly.
|
|
42
|
+
- `severity` — `blocker` | `major` | `minor`. `confidence` — `high` | `medium` | `low`.
|
|
43
|
+
- `region.nodeId` — the Puck block `props.id` the finding is about (a design), or a DOM/story locator (a
|
|
44
|
+
built screen). Omit `nodeId` only for a page-wide finding — never for an opinion.
|
|
45
|
+
- `evidence` — what you saw, with the measured value or your classification. `suggestion` — optional.
|
|
46
|
+
- `verdict` — `null` while open; `fixed` (the generator changed the design) or `dismissed` (a person
|
|
47
|
+
decided; put the reason in `evidence`). Only a person dismisses.
|
|
48
|
+
|
|
49
|
+
Write the whole list with `synap_update_entity` on each variant (`critique`), and post a short
|
|
50
|
+
critique document relating to the design (the stage's expected output): per variant, the counts by
|
|
51
|
+
tier and severity and the open blockers.
|
|
52
|
+
|
|
53
|
+
## Tier 1 — `checked` (a tool decides)
|
|
54
|
+
|
|
55
|
+
Run the tool; never estimate. If you cannot run it, the finding is `confidence: low` with what is
|
|
56
|
+
missing — never a pass.
|
|
57
|
+
|
|
58
|
+
| Rule id | Rule | How to check | Source |
|
|
59
|
+
| -------------------- | ------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- |
|
|
60
|
+
| `chk.a11y-axe` | No axe violation | the story's axe run (`pnpm --filter @synap-core/storybook test:a11y`), or axe on the canvas render | ui-rules "Shared interface style" (WCAG) |
|
|
61
|
+
| `chk.contrast-text` | Text ≥ 4.5:1 against its background; a colour fill's text picked by `onAccent` | measure with `contrastRatio` (`@synap-core/headless/theme`) | ui-rules "Shared interface style" (WCAG 1.4.3); ui-composition §1 (ink vs fill) |
|
|
62
|
+
| `chk.target-size` | Every interactive target ≥ 24px; buttons on the size scale (xs 28 · sm 34 · md 40 · lg 46) | the component's `size` prop / measured box | ui-rules "Shared interface style" (WCAG 2.2 SC 2.5.8) |
|
|
63
|
+
| `chk.tokens-only` | No hex, rgb, px, ratio, weight or font-family literal in a prop or style | scan the design JSON / `pnpm check:hex` + `check:tokens` on a build | ui-rules "Cardinal rules" (tokens only) |
|
|
64
|
+
| `chk.manifest-only` | Every `type` is in the Storybook manifest and placeable; every prop is declared with its type | compare with `/manifests/components.json` (the canvas Paste JSON reports it) | designing-with-real-components §1–2 |
|
|
65
|
+
| `chk.reduced-motion` | Every `motion` entry uses a named motion preset, and each preset has its reduced-motion fallback | read `motion` against the design tokens' presets | founder decision 2026-10-13 (named presets, fallback required) |
|
|
66
|
+
|
|
67
|
+
A `checked` failure is a `blocker`, except `chk.contrast-text` on decorative text and an axe
|
|
68
|
+
`minor`-impact rule, which are `major`.
|
|
69
|
+
|
|
70
|
+
## Tier 2 — `pattern` (you classify; the house rule decides)
|
|
71
|
+
|
|
72
|
+
For each rule: classify every instance on the screen into the listed classes, write the class in
|
|
73
|
+
`evidence`, then apply the rule. The rule is fixed; your only judgement is the classification — so
|
|
74
|
+
make it explicit, and lower `confidence` when the class is ambiguous.
|
|
75
|
+
|
|
76
|
+
| Rule id | Classify each… into | The rule (a failure is the finding) | Source |
|
|
77
|
+
| ----------------------- | ----------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------- |
|
|
78
|
+
| `pat.state-mark` | displayed state → `mark` (tone + glyph) or `sentence` | a state shown only as prose is a defect | ui-composition §1 |
|
|
79
|
+
| `pat.empty-answer` | section's empty state → `OMIT` · `INVITE` · `EXPLAIN` · `REASSURE` · `failed` · `loading` · `bare caption` | a bare caption is a defect; OMIT renders no heading; INVITE has an action that fills it; EXPLAIN says why + an escape hatch; a failed read is a retry state, never calm empty copy | ui-composition §2 |
|
|
80
|
+
| `pat.door` | each named object, count, chip, provenance line → `door` · `caption` · `scroll-anchor` | a named object that is not a door to its own page is a defect | ui-composition §3 |
|
|
81
|
+
| `pat.list-cap` | each list → `capped + show all` · `unbounded`; shape `row` · `column` | an unbounded list in a detail screen is a defect; a column of browseable peers should be a row | ui-composition §4 |
|
|
82
|
+
| `pat.no-carousel` | each carousel/deck → `unique content` · `duplicates the page` | a carousel over content the page already has is a defect | ui-composition §5 |
|
|
83
|
+
| `pat.order` | sections → `outcome` · `process` · `context` | an outcome below process or context is a defect | ui-composition §6 |
|
|
84
|
+
| `pat.primary-action` | actions per section → `primary` · `secondary` · `inline` | more than one primary action in a section, a primary not at `md`, or `xs` outside inline/row/toolbar is a defect | ui-composition §7; ui-rules "Shared interface style" |
|
|
85
|
+
| `pat.ai-colour` | each use of the AI colour / `tone="ai"` → `AI provenance` · `AI action` · `control state` · `decoration` | the AI colour on anything but AI provenance or an action that starts AI work is a defect | ui-rules "Cardinal rules" + "Shared interface style" |
|
|
86
|
+
| `pat.no-warning-button` | each risky action → `danger button` · `warning button` · `banner` | a warning-styled button is a defect | ui-rules "Shared interface style" |
|
|
87
|
+
| `pat.creation` | object creation → `minimum then detail page` · `wizard` | a multi-step wizard before the object exists is a defect | ui-rules "Cardinal rules" (object creation) |
|
|
88
|
+
| `pat.raw-token` | each label naming a kind, action or status → `words` · `raw token` | a raw token (`entity.create`, `in_review`) shown to a person is a defect | vocabulary.md "Cardinal rules" |
|
|
89
|
+
| `pat.states-covered` | screen states → which of `empty` · `loading` · `failed` · `full` · `long` the screen has, and which the design lays out | a state the screen has and the design omits is a defect | "Design to implementation", Generate |
|
|
90
|
+
| `pat.filter-row` | each filter → `one chip + menu` · `row of chips`; the controls → `one row` · `several rows` | a filter drawn as a row of chips (one per value), or filters, tabs and search spread over more than one row, is a defect | page-grammar §9 (decision afab5911) |
|
|
91
|
+
| `pat.disclosure` | each choice → `current choice + one door` · `every option shown`; each default → `declared` · `first item` | every option shown at once, a later step drawn before its earlier one is made, an option that cannot apply offered, or a default nobody declared is a defect | ui-composition §12 |
|
|
92
|
+
| `pat.status-follows` | each action, label, section → `fits the status` · `contradicts the status` | anything that contradicts the object's status (offers to make what exists, offers a step already done, primary not the next move) is a defect | ui-composition §13 |
|
|
93
|
+
|
|
94
|
+
A `pattern` failure is `major` by default; it is a `blocker` when it hides a failure from the reader
|
|
95
|
+
(`pat.empty-answer` folding a failed read into empty) or leaves the reader no way to act
|
|
96
|
+
(`pat.door` on the screen's main object).
|
|
97
|
+
|
|
98
|
+
## Tier 3 — `opinion` (a person decides)
|
|
99
|
+
|
|
100
|
+
An opinion names ONE heuristic id, a `region.nodeId`, and `confidence: low`. It is never `blocker`,
|
|
101
|
+
never applied by the generator, and never blocks the stage. Use it for what the rules above cannot see:
|
|
102
|
+
hierarchy, density, rhythm, wording.
|
|
103
|
+
|
|
104
|
+
| Rule id | Heuristic (Nielsen, 10 usability heuristics) |
|
|
105
|
+
| ---------------- | ------------------------------------------------------ |
|
|
106
|
+
| `op.nielsen-h1` | Visibility of system status |
|
|
107
|
+
| `op.nielsen-h2` | Match between the system and the real world |
|
|
108
|
+
| `op.nielsen-h3` | User control and freedom |
|
|
109
|
+
| `op.nielsen-h4` | Consistency and standards |
|
|
110
|
+
| `op.nielsen-h5` | Error prevention |
|
|
111
|
+
| `op.nielsen-h6` | Recognition rather than recall |
|
|
112
|
+
| `op.nielsen-h7` | Flexibility and efficiency of use |
|
|
113
|
+
| `op.nielsen-h8` | Aesthetic and minimalist design |
|
|
114
|
+
| `op.nielsen-h9` | Help users recognise, diagnose and recover from errors |
|
|
115
|
+
| `op.nielsen-h10` | Help and documentation |
|
|
116
|
+
|
|
117
|
+
If an opinion is really a house rule above, file it under that rule instead.
|
|
118
|
+
|
|
119
|
+
## The gate
|
|
120
|
+
|
|
121
|
+
The Critique stage passes when, on every variant going to review: the `critique` is written, and no
|
|
122
|
+
`checked` or `pattern` finding with `severity: blocker` has `verdict: null`. Opinions never decide it.
|
|
123
|
+
A failing gate sends the run back to Generate, which fixes the blockers and marks them `fixed`; you
|
|
124
|
+
then critique again from scratch (a fresh list, new ids) — never edit a finding to make it pass.
|
|
125
|
+
|
|
126
|
+
## Critiquing a built screen
|
|
127
|
+
|
|
128
|
+
The same rubric runs on the implemented story (`implemented-story-id`) in Verify: capture it with the
|
|
129
|
+
Storybook capture door (`pnpm --filter @synap-core/storybook capture`), run the `checked` tier on the
|
|
130
|
+
real render, and compare it with the design and the `current-story-id` capture. `region` is then a
|
|
131
|
+
locator in the story, not a Puck node id.
|
|
132
|
+
|
|
133
|
+
## Do not
|
|
134
|
+
|
|
135
|
+
- Invent a rule, a rule id or a threshold. Missing rule → `opinion`, or propose the rule to a person.
|
|
136
|
+
- Pass a `checked` rule you did not run. Unmeasured is `confidence: low`, never clean.
|
|
137
|
+
- Apply an opinion, or raise one to `blocker`.
|
|
138
|
+
- Rewrite or delete findings to clear the gate. Fix the design, or ask a person to dismiss.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"apiVersion": "synap/v1",
|
|
3
|
+
"kind": "skill",
|
|
4
|
+
"meta": {
|
|
5
|
+
"slug": "design-critique",
|
|
6
|
+
"name": "Design critique",
|
|
7
|
+
"description": "Critique a ui-design (or a built screen's story) with a three-tier rubric of stable rule ids: checked rules a tool decides, pattern rules the agent classifies and a fixed house rule judges, and opinions tied to a named heuristic that are never applied on their own.",
|
|
8
|
+
"tags": ["skill", "design", "ui", "critique", "accessibility", "builder"],
|
|
9
|
+
"isPublic": true
|
|
10
|
+
},
|
|
11
|
+
"dependencies": []
|
|
12
|
+
}
|
|
@@ -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
|
+
}
|