marketing-machine
Health Warn
- License — License: MIT
- Description — Repository has a description
- Active repo — Last push 0 days ago
- Low visibility — Only 8 GitHub stars
Code Fail
- process.env — Environment variable access in lib/browser.mjs
- network request — Outbound network request in lib/browser.mjs
- exec() — Shell command execution in lib/detect.mjs
- exec() — Shell command execution in lib/html.mjs
- exec() — Shell command execution in lib/update.mjs
- network request — Outbound network request in lib/update.mjs
- network request — Outbound network request in marketing.config.example.json
Permissions Pass
- Permissions — No dangerous permissions requested
No AI report is available for this listing yet.
Claim-provenance and brand-compliance harness for marketing content: scans your codebase for product truth, lints copy against it, renders on-brand cards and video. Installs as a Claude Code plugin/skill, Cursor rule, or AGENTS.md section.
marketing-machine
A claim-provenance and brand-compliance harness for marketing content. It reads your codebase
to establish what the product actually does, enforces the rules mechanically, then renders cards
(PNG) and video (MP4) from a single HTML contract.
The pitch: your marketing copy cannot outrun what your product actually does, and the build checks.
Built for teams that already work with a coding agent — Claude Code, Cursor, or anything that readsAGENTS.md — and want the marketing it produces to survive an audit.
Requirements: Node 20+ (
recordneeds 22+ for the global WebSocket) and a Chrome/Edge/Chromium
found at runtime.ffmpeg-staticis an optional dependency, used by video only — linting, scanning
and card rendering have zero dependencies.
Quick start
git clone https://github.com/cagatayuncu/marketing-machine ../marketing-machine
cd your-repo
node ../marketing-machine/scripts/init.mjs --agent claude # or cursor, agents, all
Then, in your agent: setup — the skill completes its own configuration, installs the fonts
the renderer needs, proposes the brand-role mapping, and asks you only for the things a codebase
cannot answer (your one-liner, your stage, which claims you will defend).
From there you talk to it:
| You say | It does |
|---|---|
setup |
completes config, fonts and brand roles; offers the CI gate |
| "write a post about inventory turnover" | drafts to the anatomy, adds the mandatory verification table, lints |
| "make a card for post 12" | picks a format (default: no visual), renders a PNG in your brand, writes alt text |
| "render the intro video" | verifies a single frame first, then records the MP4 |
| "review this draft" | runs the linter, then the judgment passes a linter cannot do |
The skill lands wherever your agent reads instructions — .claude/skills/ for Claude Code,.cursor/rules/ for Cursor, an AGENTS.md section for everything else. Per-agent detail:
Installing it as an agent skill.
Other ways to get the toolchain:
- Claude Code plugin — no clone, no paths:
/plugin marketplace add cagatayuncu/marketing-machine, then/plugin install marketing-machine. The toolchain travels inside the plugin. - skills CLI —
npx skills add cagatayuncu/marketing-machine; the skill clones the toolchain itself on first use. - Vendor it — copy the directory into
tools/marketing-machine/. Best for teams: the installed path stays relative. - Submodule —
git submodule add https://github.com/cagatayuncu/marketing-machine tools/marketing-machineto pin a version and pull updates. - Fork it — add your own rule packs (
rules/lang.<code>.json) and card templates on top.
Prefer driving it by hand? Every step is a plain command:
node <marketing-machine>/scripts/doctor.mjs # environment and config
node <marketing-machine>/scripts/scan.mjs # product-facts draft
node <marketing-machine>/scripts/lint.mjs # enforce
node <marketing-machine>/scripts/render-card.mjs <name>
node <marketing-machine>/scripts/record.mjs <name>
npm link inside this directory gives you mkt as a shorthand for all of them.
What init derives from your codebase
| Value | Source |
|---|---|
| Product name | package.json name, or the .sln/.slnx name |
| Domain | homepage, or the first external link in the README |
| Brand colors | The stylesheet declaring the most of them — CSS custom properties, SCSS/Less variables, or a Tailwind theme. Prefixed token conventions (--color-bg, --c-primary, --theme-text) are understood |
| Font families | font-family declarations, matched to the packages that ship them — including a Google Fonts @import, which names the equivalent @fontsource package |
| Languages | The localisation directory, with a key count per language |
| Legal routes | Routes like /privacy or /terms found in the code — these become the linter's draft-link blocklist |
| Source roots | Directories that actually contain code |
Anything it cannot find is left as [VERIFY: ...]. doctor lists those, and unmapped brand roles
block rendering rather than falling back to someone else's palette.
Why
There is no shortage of tools that write marketing copy. What is missing is the layer underneath:
something that proves the copy stayed inside what the product actually does. That layer is the
whole of this package.
Four principles:
- Everything comes from the codebase it is installed into. Brand colors, font families, product
name, domain, languages, legal-page routes, source roots — all detected from the host repo. No
product's colors, name or fonts are baked into the package, and no value is inherited from the
example config. When detection fails the field staysnullwith a[VERIFY]note and
rendering is refused — publishing artwork in the wrong brand is worse than failing. - Product truth is derived from code and tiered by evidence. WORKS / PARTIAL / ABSENT / LIMITS,
every line carryingfile:line. - Nothing becomes claimable automatically. A scanner sees that a symbol exists; it cannot see
that the feature works. The draft waits for a human. - A rule is a build gate, not advice. Everything a machine can decide lives in the linter;
everything requiring judgment lives in the skill. The two are never mixed.
Adoption walkthrough
Two commands and a conversation. The friction is concentrated in one place, and it is deliberate.
1 · Get the directory
git clone https://github.com/cagatayuncu/marketing-machine ../marketing-machine
It does not have to live inside your repo — init writes an absolute path into the installed skill,
so the toolchain can sit anywhere. Vendoring it in (tools/marketing-machine/) also works if you
would rather pin a version.
Working in a team? Vendor it. An absolute path baked into a committed AGENTS.md or .claude/
skill runs on exactly one machine; a vendored toolchain keeps the path relative and the repo
self-contained. The alternative is each developer running init --refresh once after cloning —init defaults to that model when the toolchain sits outside the repo: the machine-specific skill
copies go into .gitignore and stay out of everyone's commits.
2 · Install and hand over
cd your-repo
node ../marketing-machine/scripts/init.mjs --agent claude
init also maintains a marked block in your .gitignore covering everything the toolchain
writes and can rewrite — rendered PNGs and MP4s, the scan draft, the _template-* starter files,
and (when the toolchain path is absolute) the machine-specific skill copies. Content stays in git:
config, context files, posts, card and scene HTML. That split is the collaboration model — a
render is reproducible from the committed content, while the committed content is exactly what
your team reviewed and your CI gate reads. Paths you already ignore in your own spelling are left
alone, and --no-gitignore skips the whole step.
Then tell your agent: setup. From here the skill drives, and it will ask you for exactly four
kinds of thing:
| It asks | Why it cannot answer this itself |
|---|---|
| Which language the copy is written in | It can read your locale files, but a product's interface languages and its marketing language are different decisions. A UI in three languages may market in one. |
| Who the audience is | Their words for the problem, what they already tried, the first objection, and who this is explicitly not for. None of that is in the code. |
| Which channel this is for | Sets card and video geometry, and the post anatomy |
| Your one-line product description | Positioning is a judgment, not a fact in the code |
| Your stage (pre-launch / beta / GA) | Same |
| A sanity check on the brand-role mapping | Role guessing is name-based and can be confidently wrong |
A .woff2 path, if your typeface is licensed |
Not resolvable from a public registry |
The language answer is not a single field: the rule pack, the verification-table heading and the
verify marker all move with it, which is why changing it goes throughinit --refresh --lang <code> rather than a hand edit. Anything you had already tuned survives that.
Audience answers go into marketing/context/audience.md, because an audience description is prose,
not configuration.
Everything else it handles: running doctor, installing the font package the renderer needs, writing
the role mapping, detecting a logo file if the repo ships one (brand.logo — cards then use the
image instead of the text wordmark), and offering to wire the CI gate.
3 · Establish product truth — the part that is not automated
Ask the skill to scan. It writes a draft where every line carries a file:line reference and
nothing is claimable yet, then walks it with you section by section asking "does this work end to
end, and what is the evidence?" It will not promote a line on its own.
Pay particular attention to the outbound-call inventory. If you intend to say anything about where
data goes, every row there has to be reviewed — one unreviewed call falsifies an absolute claim.
The same conversation fills claims.md. Its UNPROVABLE rows get written as rules inmarketing/rules/project.json, which is the moment they stop being advice and start failing builds.
4 · Produce
Ask for write, card, video, or review. Every output goes through the linter before you see it.
Everything written comes from your repository. Not from what products in the category usually
claim, not from your README's own marketing language. The chain is: the scan found it in the code → a
human confirmed it → it is WORKS in product-facts.md → it may appear in a sentence → the sentence
carries its file:line. Anything that cannot be traced back through that chain either gets a verify
marker or gets left out.
Time to first artwork
Steps 1, 2 and 4 are minutes. Step 3 is the real investment, and it is deliberately not
automatable: establishing what your product provably does, and which claims you will stand behind, is
the thing that makes everything downstream trustworthy. A scanner can hand you the candidates; only
you can confirm them.
Installing it as an agent skill
The instructions live in one place — skills/marketing-machine/SKILL.md — and init wraps that same body in whatever
shape your agent expects. Rewriting the body per target would guarantee the copies drift, so the
body is installed verbatim and only the wrapper differs.
node <marketing-machine>/scripts/init.mjs --agent claude # default
node <marketing-machine>/scripts/init.mjs --agent cursor
node <marketing-machine>/scripts/init.mjs --agent agents
node <marketing-machine>/scripts/init.mjs --agent all
node <marketing-machine>/scripts/init.mjs --agent claude,cursor
Re-running init --refresh reinstalls every target with current paths — that is also the fix when
a teammate clones a repo whose skill was installed from a different machine.
Claude Code — as a plugin (no clone)
/plugin marketplace add cagatayuncu/marketing-machine
/plugin install marketing-machine
The plugin carries the whole toolchain — scripts, rules, templates — inside the plugin directory,
and the skill finds it through $CLAUDE_PLUGIN_ROOT. Nothing to clone, no path to manage: open
your repo, say setup, and the skill creates marketing.config.json there. Updates arrive
through the plugin marketplace rather than git pull.
npx skills CLI
npx skills add cagatayuncu/marketing-machine
The skills CLI copies the skill into whichever agents it
manages — but only the skill, not the toolchain. The skill handles that itself: its first step on a
machine without a toolchain is to clone one (the resolution ladder is written into the skill). If
you would rather no agent cloned repos on your behalf, use any other route on this page.
Claude Code — via init
<your-repo>/.claude/skills/marketing-machine/
├── SKILL.md name + description frontmatter, tool allowlist
├── TOOLCHAIN where the scripts live
└── references/
└── video-guide.md
Claude Code discovers project skills in .claude/skills/ automatically — no registration step, no
settings change, and it works the same in the CLI, the desktop app and the IDE extensions. The
whole skills/marketing-machine/ directory travels, so the video guide's relative link resolves locally.
Invoke it by name:
/marketing-machine setup
/marketing-machine write post about inventory turnover
/marketing-machine card 12
or just describe the task — routing matches on the description frontmatter, so "write a LinkedIn
post about X", "make a card for this" or "review this copy" reach the skill without naming it.
One note on permissions: the frontmatter's allowed-tools (Read, Write, Edit, Glob, Grep, Bash) is
a ceiling, not a grant. Claude Code still asks before running toolchain commands unless you
allowlist them in your project settings.
Cursor
<your-repo>/.cursor/rules/marketing-machine.mdc
Installed as a project rule of the agent-requested kind — description in the frontmatter plusalwaysApply: false — so the agent pulls it in when the task looks like marketing work instead of
carrying it in every request. Verify it landed: Cursor Settings → Rules lists it.
To use it, open the agent panel and describe the task, or reference the rule explicitly
(@marketing-machine) when you want it loaded for certain. The rule tells the agent to run the
toolchain scripts from the repo root; Cursor shows each command for approval as usual.
Recent Cursor versions also read AGENTS.md, so --agent agents reaches Cursor too. Pick one
route, not both, or the same instructions land in context twice.
AGENTS.md (portable convention)
<your-repo>/AGENTS.md
The instructions are appended as a ## Marketing content (marketing-machine) section between<!-- marketing-machine:start --> / :end markers. Re-running init replaces what sits between
the markers and leaves the rest of your AGENTS.md alone.
This is the widest net: OpenAI Codex, GitHub Copilot's coding agent, Google Jules, Amp, Zed and a
growing list of tools read AGENTS.md as project instructions. The trade-off is the loading model.
An AGENTS.md section sits in every prompt, while a Claude skill or a Cursor rule loads on demand —
so if your AGENTS.md is already long, prefer the tool's own rules mechanism where one exists.
Everything else — manual paste
skills/marketing-machine/SKILL.md is plain Markdown with YAML frontmatter. Strip the frontmatter, replace<TOOLCHAIN> with the path to this directory, and paste the body wherever your tool reads project
instructions — that substitution is the only templating in the file. As of this writing:
| Tool | Where it reads project instructions |
|---|---|
| GitHub Copilot (VS Code / JetBrains chat) | .github/copilot-instructions.md |
| Windsurf | .windsurf/rules/ — save the body as a rule with a description trigger |
| Cline / Roo Code | .clinerules |
| Gemini CLI | GEMINI.md |
| Anything with a custom-instructions field | paste the body directly |
A first-class wrapper for a tool you use daily is a small, welcome PR — the cursor branch inscripts/init.mjs is the pattern to copy.
What the skill actually does
Six commands, in the order they get used:
| Ask | The skill does |
|---|---|
setup |
Installs itself, completes the config, installs the fonts the renderer needs, proposes the brand-role mapping, offers the CI gate — and asks you only for what code cannot answer |
scan |
Runs the scanner, then walks the draft with you asking "does this actually work, and what is the evidence" — it will not promote a line on its own |
write <channel> <topic> |
Loads the context files, writes to the anatomy, saves with a mandatory verification table, runs the linter, fixes violations |
card <post> |
Applies the decision table (default: no visual), builds from the template, renders, writes alt text |
video <scene> |
Checks a single frame first, then records; enforces the purity rules and the provenance declaration |
review <file> |
Runs the linter, then adds the four passes a linter cannot do: claim verification, limitation quality, audience, CTA fit |
Commands
| Command | What it does |
|---|---|
init |
Detect, write config and scaffolding, install the skill |
scan |
Scan the codebase, write a product-facts draft |
lint |
Check posts, cards and scenes against the rules |
card |
HTML → PNG |
record |
HTML scene → MP4 (or a single frame with --shot) |
doctor |
Browser, fonts, brand roles, ffmpeg, paths — plus freshness: is the installed skill copy in sync, is the toolchain behind upstream (--offline skips the network check) |
Useful flags: init --refresh (re-derive brand/fonts and reinstall every agent target found on
disk, keep the rest) · init --no-gitignore (leave the host .gitignore alone) ·lint --only post|card|video · lint --warnings-as-errors (for CI) · lint --json ·card --channel <name> · record --shot 6.5 · record --scale 2. Note on card --loose-fonts:
it only permits a source whose <!--FONTS--> marker is absent — it does not skip font
resolution when the marker is present.
One HTML contract
Cards and video go through the same pipeline. The only difference is that a scene declares a
timeline.
| Marker | What it does |
|---|---|
<!--FONTS--> |
Required. Fonts are inlined here as data URIs. Without it the page fetches fonts over the network and non-ASCII glyphs (ı ş ğ İ, ä ö ü ß, ą ę ł) break silently. The linter blocks its absence. |
<!--TOKENS--> |
Injects brand colors and font families from config |
{{product.name}}, {{brand.footerUrl}}, … |
Identity text from config. An unresolved placeholder fails the render. |
body { width/height } |
Output geometry. Change the aspect ratio in the source and the renderer follows. |
A scene adds four globals:
window.__duration = 15; // total seconds
window.__seek = (t) => { ... }; // a PURE function of t
window.__ready = true; // once fonts and layout have settled
window.__provenance = 'simulation'; // real-recording | driven-app | simulation
__seek must be pure. The recorder does not play the page and capture the screen; for every
frame it calls __seek(frame/fps) and screenshots. So: no CSS transition/animation, nothing
driving visuals from requestAnimationFrame, no state accumulating between calls.
The cost is more code. What it buys: the output of --shot 6.5 is byte-for-byte frame 195 at 30fps,
so you verify a frame instead of reviewing a video. Full contract:
skills/marketing-machine/references/video-guide.md.
Why a browser render, not an image model
Every card and every video frame is a screenshot of a page you can read. That is a deliberate
trade, and it buys four things a generative image or video model does not offer:
- Type and identity survive. The text on a card is text: your fonts inlined as data URIs,
your brand tokens injected from config. There is no diffusion pass to warp a glyph or melt a
wordmark — which is where generated imagery still fails most visibly, and exactly what a brand
asset cannot afford. - Output is deterministic. The same source renders the same pixels. A video frame at a given
second is exactly the frame the encoder saw, so one still can be reviewed instead of a whole
video re-watched, and an approval survives a re-render. - Revision is an edit, not a re-roll. No per-asset inference cost, no prompt roulette, no
almost-right variant you settle for. Change a line, render again; nothing else moves. - The output does not read machine-made. On the visual side there are none of the artifacts
that flag AI imagery. On the copy side the linter treats the tells of generated prose — the
in-sentence em dash, the not-only-but-also formula, the rule-of-three tic, hype vocabulary —
as violations, so what ships reads like it was written, because what survives the gate
effectively was.
The cost of the trade is honest too: a browser render cannot invent a photograph, an
illustration or a texture. When a post genuinely needs one, that asset comes from somewhere
else — this pipeline covers cards, product scenes and motion design, not photography.
The brand role contract
Templates style against six canonical roles: bg, bgDeep, fg, accent, accentSoft, muted.config.brand.roles binds each one to a hex value or to $name referencing an entry inconfig.brand.colors.
Without that indirection a template has to hardcode one project's color names (--navy), and a
project that calls the same color --surface falls silently through to the fallbacks: the artwork
looks fine and is in the wrong brand.
So the templates contain no hex value and no font name, and no fallbacks. Font families arrive
the same way (--mm-font-sans / --mm-font-mono) — a template naming a family literally would
inline that font and then never use it, quietly falling back to a system font. init guesses
the roles from the color names, leaves anything uncertain as null, and doctor reports it while
rendering refuses (--allow-fallback to override deliberately).
Rules are data, not code
Edit a rule file and what the build enforces changes; no code changes. A rule is either a regex or a
named builtin.
Three layers, general to specific:
| Layer | Where | What it holds |
|---|---|---|
core |
packaged (rules/core.json) |
Structural rules. Not one word of any human language: verification table, unsourced number, hashtag count, card word budget, provenance, leftover placeholder |
lang.<code> |
packaged (rules/lang.en.json, lang.tr.json) |
That language's vocabulary bans and language-specific mechanics |
| project rules | your repo (config.lint.projectRules) |
Claim bans specific to this product. init creates it empty |
The third layer is why the package can ship at all: "do not claim compliance for feature X" is a
sentence about one product, and baking it in would make every adopter inherit it. The UNPROVABLE rows
of your claims.md get written here by hand — a claim there is advice until it appears here, at
which point the build enforces it.
Language packs are not decoration. Turkish letters have no mathematical-bold counterpart, so a
bolded Turkish word renders half-bold and broken; that is a purely mechanical check no other tool
performs. For the same reason rule patterns avoid \w: it does not match ı ş ğ ç ö ü, so a\w-based pattern misses exactly the Turkish copy it was written for. Exclusion character classes
([^\s,.;:]+) are the portable form.
What the linter deliberately does not check
Out of scope on purpose: whether the limitation sentence is any good (its presence is checked, its
quality is not), whether a claim maps to a WORKS fact, audience coherence, hook strength, archetype
choice. Those need judgment and stay with the skill. Keep the split: putting a non-mechanical rule
in the linter produces false positives, and a linter people stop trusting enforces nothing.
CI
One command is the gate:
node <marketing-machine>/scripts/lint.mjs --warnings-as-errors
Exit 1 on any violation (and on any warning in strict mode). Wire it to whatever runs on changes
under your marketing/ paths. No workflow file ships with the package, because its shape depends on
your CI.
File layout
lib/
detect.mjs everything derived from the host codebase — shared by init and scan so they cannot disagree
config.mjs config discovery, validation, path resolution, CLI args
browser.mjs the ONE Chromium locator and the ONE CDP client
fonts.mjs three-tier font resolution (explicit paths -> host node_modules -> this package's)
html.mjs the single HTML contract: markers, geometry, role tokens, {{placeholders}}
scripts/ init · scan · lint · render-card · record · doctor · cli
rules/ core.json · lang.en.json · lang.tr.json
templates/ card (four archetypes in one file) + scene — neither contains a hex or a font name
skills/marketing-machine/ the agent skill + the video guide
Font resolution has three tiers so nothing is pinned to one repo: a project that already ships its
brand fonts points config.fonts.nodeModulesRoot at them and installs nothing here; a fresh project
installs them here (npm i @fontsource-variable/<family>; save it, because npm prunes unsaved
packages on the next install and a second font would silently delete the first). A missing font fails the
build — shipping a card in a fallback font is worse than not shipping it, because the breakage is
invisible until someone looks closely at published artwork.
Configuration reference
| Key | Notes |
|---|---|
product.name / oneLiner / stage |
Name is detected; the other two are judgments a human writes |
language.primary |
Inferred from the most complete locale dictionary |
language.ruleSets |
Which packaged rule files load, e.g. ["core", "lang.en"] |
paths.* |
Where context, posts, cards, scenes and output live |
brand.colors / colorsSource |
Raw inventory plus the file it came from |
brand.roles |
The six canonical roles — what templates consume |
brand.wordmark / footerUrl |
Identity text for {{...}} placeholders |
fonts.sans / mono / nodeModulesRoot / subsets |
Families, packages, where to resolve them |
channels.<name>.card / video / fps |
Output geometry per channel |
scan.* |
Include/exclude globs and the patterns for flags, limits and outbound calls |
lint.projectRules |
Path to this product's own claim bans |
lint.draftLegalPaths |
Policy routes a post must not link to yet |
lint.requireVerificationTable / verifyMarker |
Both appear inside content, so they follow the content language |
provenance.required / allowed |
Whether every scene must declare how its footage was made |
marketing.config.example.json is a filled-in example for a fictional product, there to show what populated fields look like. init never reads it and inherits nothing from it.
Verified state (2026-08-04)
Run end to end on a scratch repo with no relation to the tool's origin (nimbus: purple theme,
Inter + JetBrains Mono, source under app/, en+de localisation):
| Step | Result |
|---|---|
init --agent all |
Derived from the codebase: stack, product name nimbus, domain nimbus.example.co, 6 colors (app/ui/theme.css), Inter + JetBrains Mono, both font packages, en+de, source root app/**, legal routes /privacy /terms, content language en. All six brand roles mapped. Skill installed for Claude Code, Cursor and AGENTS.md |
doctor |
All green; fonts resolved from this package's own node_modules (the fresh-repo path) |
scan |
4 files; 1 outbound call, 2 limits, 6 colors, 2 languages — everything under the verify marker |
card |
1200×1200 PNG entirely in nimbus branding: purple background, purple accent, wordmark from package.json, footer from homepage, both fonts as detected from CSS |
| unmapped role | Setting accent to null refused the render, with the reason and the fix |
lint (English copy) |
13 violations + 4 warnings; lang.en vocabulary rules fired and draft-legal-link triggered on the /privacy route detected from the code |
lint (Turkish copy) |
16 violations + 2 warnings on a separate fixture, exit 1 |
provenance |
Declared → clean; declaration deleted → violation |
record --shot |
Three distinct frames, 1080×1080 |
record |
450 frames → 15s @ 30fps MP4 |
| language switch | init --refresh --lang tr moved the rule pack lang.en→lang.tr, the heading to Doğrulama notları and the marker to [DOĞRULA, while preserving a customised hashtagCount and the hand-written one-liner. The Turkish pack then fired on Turkish copy. |
| third fixture | A React repo using Google Fonts and --color-* tokens with no font package: all six roles mapped, @fontsource/manrope inferred from the @import, card rendered in its own palette |
Validated against a real production repo
The synthetic fixtures above prove the happy path. A .NET + React monorepo (891 files, three
interface languages, an oklch() design-token system, existing hand-written marketing content) proved
what the fixtures could not, and it found ten defects the fixtures never would have.
| Step | Outcome |
|---|---|
init |
Derived stack, product name, 59 colours from the app's own token file, both font families and their packages, three languages, source roots, four legal routes. Correctly reported the domain as not found — the repo's SITE_URL falls back to localhost and the real host arrives from a build-time env var, so it genuinely is not a code fact |
| brand roles | Five of six mapped from --c-* tokens; one left null. A palette-based proposal for the sixth was accepted, and the card then rendered in the product's actual light theme |
scan |
601 files → 51 outbound calls, 60 limits, 3 languages, all with file:line. After the fixes the egress inventory is real product code (OAuth clients, LLM provider, telemetry) rather than boilerplate |
write + lint |
A post whose only factual claim was a constant the scan found (PeekRowLimit = 50) passed with 0 violations, one warning for missing hashtags, and one note holding publication open because a verify marker was unresolved — exactly the intended behaviour |
| existing content | product-facts.md, claims.md and brand-voice.md were already written by hand in that repo. Adoption left them untouched |
Real defects found and fixed during that testing:
--only <value>was being mistaken for a filename\wdoes not match Turkish letters — the rule-of-three check never fired at all%94.failed to match the verification table because of the trailing period- A template's doc comment spelled out
<!--FONTS-->, which closed the HTML comment early and
printed the remaining doc text onto the card - Windows holds locks on the Chrome profile; cleanup was crashing the render
- Templates hardcoded one project's color names, so another repo fell silently through to the
fallbacks — this is why the role contract exists - The source-root threshold (3 files) missed small repos and fell back to a
src/**that did not
exist - Templates hardcoded font family names, so a detected font was inlined and then never used —
fixed by making families role variables too - The leftover-placeholder rule flagged config-bound placeholders as unfilled slots, so a correctly
rendered card failed the lint — it now resolves against config exactly as the renderer does - Role guessing did not understand prefixed token names (
--color-bg,--c-primary), which is the
most common convention in practice — three of six roles came back unmapped on a realistic repo,
meaning cards could not render at all doctortruncated failures to their first line, throwing away the fix instruction underneath —
the adopter saw "cannot resolve font package" without thenpm icommand that solves it
Found only by running against a real repo:
--forcedestroyed hand-written context files. It replaced 567 lines of verified product
facts, decided claims and a tuned voice with empty scaffolds, silently. Git had them, so nothing
was lost — but the most expensive content in a repo was one mistyped flag away from gone.--forcenow covers config, templates and the skill only; replacing hand-written content needs--force-context, and every kept file is named in the report- The locale search took the shallowest match, picking
i18noveri18n/localesand finding zero
languages in a repo that ships three - Only hex colours were recognised. Against an
oklch()token system that yielded five stray#ffffffvalues, mapped every role to white, and would have rendered a white-on-white card - Colour ranking picked a marketing brochure stylesheet over the application's own theme, because it
happened to declare more variables - Source roots included test, tooling and design directories, so the outbound-call inventory — the
section a data-locality claim depends on — was dominated by exported design-tool boilerplate - Variable-font family names never matched their package:
@fontsource-variable/*declares its CSS
family as " Variable" while the package is named after the plain family - An unresolved
[VERIFY]package name was interpolated into annpm icommand, producing an
instruction that could not possibly work - A
[VERIFY]value rendered onto the card as literal text. It is a non-null string, so it
counted as a value; a placeholder reaching published artwork is the exact failure this layer
exists to prevent - Domain detection ignored site-URL constants (
SITE_URL,metadataBase), which is where a web
product's canonical host actually lives - Comment lines matched the egress and limit patterns, filling the draft with prose a reviewer has
to reject line by line
Known gaps
| Not done | Why |
|---|---|
No claims.md → rule automation |
Claim wording is product-specific and gets written into the project rules by hand. Next step. |
| Copy writing is not in code | Writing needs judgment; skills/marketing-machine/SKILL.md runs it, not a script |
| No Remotion / React-video pipeline | The package stops at the plain HTML contract; a project that wants Remotion adds it and reuses the same linter |
| The verification tier is not implemented | Executing what a scene shows is product-specific. The contract is in the video guide; the adapter is yours to write. |
| No CI workflow file | Depends on your CI. lint --warnings-as-errors is the only command needed. |
| The human-confirmation conversation is untested | scan produces the draft and the skill knows to walk it, but nobody has yet sat through the confirm-each-fact pass on a real product. Until then product-facts.md stays empty and no claim is publishable — which is the correct failure, just not a finished workflow. |
| Video untested on a real product UI | Frames and a full MP4 render from the template scene. Reconstructing an actual product interface as a t-pure scene has not been attempted. |
Only en and tr rule packs exist |
A third language gets structural enforcement only until someone writes rules/lang.<code>.json. The skill says so rather than pretending otherwise. |
Tests
npm test
node:test only, no dependencies, no browser needed. The suite covers the detection heuristics
(locale layouts, role guessing from prefixed tokens, source-root ranking), the HTML contract
(role/placeholder resolution and its refusal paths) and the CLI edges that would silently weaken
the gate (--only typos, glob overmatch, malformed project rules) — each of the latter found by
running against real repos first, then pinned by a test.
License
MIT.
Reviews (0)
Sign in to leave a review.
Leave a reviewNo results found