Repository navigation
Releases: Mikefluff/skills
Release list
v2.25.0 — 3D models, and voiced films directed in clay
Two modalities were missing or only half there. 3D had no skill at all, and
model-maker now returns a GLB from text or a photo. Video could illustrate a
voice but not direct it: reel-builder's film mode now records the take, draws a
storyboard, designs every camera move as a clay previs in Blender and fits the
voice back to the lips, all before or after the one paid step. Around them, two
gates that reported green without looking now look, thirteen prices no longer
understate the bill, and writer catches the props a book edit kept putting back.
Changed — film mode directs instead of illustrating
The first paid film had held seams and timings but no direction. It was a run of centred, static medium shots in grey, even light, with a picture that repeated every line. Its lips were also seconds off the take, and the cause was the prompt, not the model. Seedance re-performs the audio reference through a mouth on screen, and our prompt put lines over shots with no face in them. BytePlus's own 2.5 guide documents the fixes, and the chain now follows it.
- Director system prompt rewritten for the official 2.5 format. It writes a shot list with integer-second ranges and dual-contrast hard cuts. Lines go in verbatim in
{}, either on camera or(V.O.)with "his lips stay closed". The prompt also carries[Overall style], an audio policy and[Strictly exclude], and runs 350–700 words where it used to be capped at 330. The dramaturgy rules add counterpoint over illustration, a colour and light arc, acting through physical action, and a poster frame. reel_boardCLI: a storyboard before the video. Nano Banana Pro renders one keyframe per shot through OpenRouter (new provideropenrouter-nano-banana-pro, about $0.14 a frame). A Gemini critic scores each frame against its shot, and frames below 8/10 are re-rolled with the critic's fix. Approved frames go to Seedance as ordered keyframe references.reel_previsCLI. The camera is designed as a clay previs, and Blender renders it headless (common/runners/previs.pyholds the spec, checks and orbit expansion;common/runners/blender/previs_scene.pybuilds the scene). Each shot gets its clip as@Video1. On the trial shot Seedance followed the previs frame for frame, and motion went from 0.2–5 to 8–12. OpenRouter video references go up throughmedia_host(audio_hoststill works).reel --redo <labels>regenerates individual shots.- One generation per shot, the new default. The keyframed one-generation film measured as a slideshow, with near-zero motion for seconds at a time. Each shot is now generated from its keyframe with handles. The runner cuts shots frame-exact (
trim_start/trim_seconds), andlip_syncshots get their own slice of the master. New rules: every shot moves, cuts land on action, and the ending stays in a known space. reel_conformCLI: the salvage step for a picture that is already paid for. ElevenLabs speech-to-text reads what the picture's mouth says, and each phrase of the master take is moved there, with stretching capped at ±15%. On the trial film the median voice-to-lip offset dropped from 1.13 s to 0.25 s.- OpenRouter audio references go up through a one-hour Litterbox link, and only when the plan sets
audio_host: "litterbox", because OpenRouter refuses inline audio.
Added — film mode: voiced Seedance films that hold together at the seams
A voiced Seedance film came out with the picture jumping at the seams, the voice
breaking mid-word, events missing their lines, and about twenty staged events
in thirty seconds. Each of those has a cause in how the film was made, and each
cause now has a component.
reel_voiceCLI (python3 -m common.runners.cli.reel_voice): script →
ElevenLabs take with character timestamps (/with-timestamps, falling back
to/v1/forced-alignment, or--voice-filefor an existing take) → master
track on the film clock → cut plan →timing.json. Cuts are chosen by an
exact shortest-path over the pauses (common/runners/voice_timing.py):
never mid-word, never past the model's limit, no runt segment at the end.- Seedance providers:
seedance-2-5andseedance-2-0(BytePlus ModelArk,
ARK_API_KEY),fal-seedance-2-5(FAL_KEY). One kwarg contract on all
three, and reference order is preserved on the wire, because@Image1and
@Audio1are positional. 2.5 renders 4-30 s, so a ≤30 s film is one
generation with no seams. - Chained segments in
reel:continue_from_previoushands each segment
the frame its predecessor was cut on (as the last@Image, orfirst_frame
on BytePlus).trim_secondscuts frame-exact, andvoiceoverlays the one
master take over the join. Seedance's re-voiced audio is dropped. - Film system prompt (
common/video-prompt-library/film-system-prompt.md):
dramaturgy before prompts. It enforces one controlling image, at most one
event per 4 s, and a quiet turn. Hits land on word starts, speech goes only
on visible faces, and each prompt has role lines per reference, a timecoded
timeline, no quoted dialogue and no meta-commentary. - Designed soundtrack (
common/runners/soundtrack.py,reel_soundCLI,
eleven-sfxprovider): Seedance's audio is discarded. The chain writes music
cues (Eleven Music composition plans), effects and ambience beds
(/v1/sound-generation) plus their placements on the film clock. Kills,
dips and "distant" re-use are edits of one cue, not separate generations.
Effects markedsnapmove to the brightness jump where the rendered flash
actually is. The mix ducks music under the voice and is mastered to
−14 LUFS / −1 dBTP.reel_soundrenders it before the picture is paid for. reel-builder/references/film-mode.md: the pipeline, why each rule exists,
and a worked rewrite of a 1,800-word prompt into a 330-word one.
Fixed
- Reel music went silent half a second in.
MixOptions.music_filter
emittedafade=t=out:st=0, a fade-out starting at zero. Every reel and every
mixrun since the option-object refactor faded its music out at 0.5 s, and
the tests pinned the string. The fade now startsfade_outseconds before
the video ends, and is omitted when the end is unknown.
Changed
video-promptSeedance reference: 2.5 added, and the audio claims corrected
(native audio plus lip-sync to an@Audioreference, not "unconfirmed").
The--modellist now namesseedance-2-5/seedance-2-0instead of
seedance-1-pro.
The roadmap asked the next session to audit the structure. The audit found that
two of the nine documentation gates had never run, and that the numbers the
skills quote for what a run costs had drifted from the table that bills for it.
Added — writer learns the props a book edit kept reintroducing
A full proofread of a Russian non-fiction manuscript turned up more than a
hundred constructions the linter passed as clean, and most of them had been
inserted by the editor itself while fixing other hits. They share a shape: the
phrase announces a thought instead of stating it.
- 29. PROP_TURN — announcing props («самое интересное», «ирония в том, что»,
«откуда его меньше всего ждали», «стоит запомнить простую вещь», «и в этом весь
смысл»), bodily reactions used as drama («сказал вещь, от которой вздрогнули»),
figurative «держится на / держит на себе / удержаться от соблазна», and the
abstract middleman «разрыв между X и Y». - 30. ORDINAL_SCAFFOLD — a chapter read as a report: «Вторая сцена. … Третья
сцена», «Второй слой … Седьмой слой», «Дальше --- права».
Categories 26–28 (PERCEPT_METAPHOR, SYNTH_WORD, ANGLICISM) and 56 further
patterns had been living only in a local install and never reached the repo;
they are ported here with their catalogue sections. Six snapshots drift, each by
a true positive the old list missed («через призму», «как водится», «скилл»).
Fixed — two gates that reported green without looking at anything
check-docs-consistency.sh check 2 globbed */ at the repo root for
directories containing a SKILL.md. Nothing at the root has one; they all live
under skills/. The loop iterated an empty list and passed, every commit since
the move.
Check 5 had the same shape: it asked git for added */SKILL.md paths, then
discarded every result containing a slash as "nested". After the move that is
all of them, so post-publisher and schema-maker both shipped without the
gate ever opening the CHANGELOG.
Both now assert their scan found something. A gate that scans nothing prints the
same green as a gate that scans everything, which is the only reason these
survived two years of releases.
Fixed — thirteen prices that understated the bill
common/references/model-pricing.md is generated from cost.PRICE_TABLE and so
cannot lie. The skills quote prices somewhere else entirely — in decision tables,
worked examples, and the sentence that says whether a batch trips the budget —
and 153 such claims were hand-written. Thirteen were wrong:
nano-banana-proadvertised at $0.05 an image instyle-transfer, billed at
$0.134.thumbnail-makerquoted a nine-image batch at $0.45. It costs $1.21 — 2.7x,
and the doc was what a user read before deciding to run it.gif-makerpriced six video models per clip against a per-second table.logo-maker's $/image table had three of five rows wrong.lyria-3-clipat $0.05,eleven-ttsat $0.15 per 1000 characters, both stale.
Added — scripts/check-prices.py, gate 8
A line naming a model and quoting a dollar figure is a claim about that model.
It has to resolve to the unit price, or to the unit price times a batch size the
file declares with <!-- prices: batch=8 -->. Undeclared batch totals fail,
which is what makes the check worth running: $0.45 and $1.21 are both
plausible-looking numbers, and only one of them is nine times $0.134.
Fractional batches (batch=0.5) cover per-...
v2.24.0 — articles get a canonical, and pages get an answer-engine axis
The collection could make content and post it to social platforms. It could not
publish an article anywhere, and nothing in it knew that being quoted by an
answer engine is decided by different things than being ranked by a search
engine. Both are now covered, and both came with a correction to a premise.
Added — article syndication, canonical-first
Six article publishers alongside the seven social ones: devto, telegraph,
hashnode, tumblr, qiita, micropub. Post grew an article shape
(canonical_url, description, series) rather than a parallel system, so
preflight, receipts and the CLI all carry over.
The field that matters is --canonical. Syndicating without one is worse than
not syndicating: the same text on several domains makes search engines pick a
winner, and it is rarely the author's. Preflight warns when it is missing and
blocks a relative URL.
micropubis a protocol, not a vendor — one adapter reaches Micro.blog,
WordPress with the plugin, and anything else implementing the spec.devtois the anchor because it is the only platform in the set whose
outbound links are dofollow. It is also the URL Medium's "Import a Story"
pulls from, which is how a platform with no usable API still ends up pointing
home: Medium sets canonical back to the source itself.cli.origincloses the chain at the other end. For a static site there is
no publishing API, so it writes the markdown with frontmatter and prints the
URL the post will have after the rebuild — which is what every later
--canonicalneeds. Cyrillic titles transliterate; Jekyll's dated filenames
are a pattern flag. It writes a file and stops: committing somebody's blog
repository is not this layer's decision.- Submission packets for the platforms that have no API at all — Medium,
HackerNoon, Habr, VC.ru, Dzen — with the canonical instruction per platform
and what each audience actually rewards.
Added — AEO, kept off the prose verdict
writer --aeo is a third axis next to slop density and the hard-ban gate.
Mixing it into either would repeat the mistake Report.verdict() already
documents. A text can read beautifully and still be unquotable.
The rules encode how engines actually work: they do not read pages, they chunk
them into passages and cite the strongest one. So the checks are structural —
a direct answer inside the first 40-75 words, question-form headings, paragraphs
that survive chunking, a table on comparison pages. Offline, no model.
schema-maker (43rd skill) generates the JSON-LD. Question-form headings
become FAQPage pairs automatically, so the structure the linter asks for is
not authored twice. Google retired the FAQ rich result on 2026-05-07; the markup
outlived it and changed job — engines parse FAQPage first when choosing whose
answer to quote. The generator refuses to emit a statement inside a Question
node, or a headline past the documented 110-character limit.
essay-write gained the answer-first opening as a conditional mode, for web
publication only — a book chapter's first paragraph works on voice, and forcing
search discipline there would fight the skill's own guidance. viral-text says
plainly that a social post is not an AEO surface.
Added — distribution map
cli.distribute writes submission packets for directories and registries, with
project facts read from the repo so a packet cannot describe a stale version.
docs/distribution.md records where the project is listed, where it should be,
and what each listing is worth.
Fixed — npm was publishing a two-year-old version, and shipping bytecode
The registry showed 1.9.0 while the repo was on 2.23.0, because nothing tied
npm publish to cutting a release. make release now calls make publish-npm,
after the tag so a failed login leaves the tag intact and the step re-runnable.
The tarball was also shipping __pycache__/*.pyc: .npmignore never applied,
because package.json sets a files allow-list and that takes precedence.
Negation patterns fixed it — 656 files down to 552.
Fixed — new-skill.sh had been scaffolding into the wrong directory
Skills moved under skills/ in v2.22.0 and the scaffolder kept writing to the
repo root, so any new skill landed where validate.sh does not look. Found by
using it.
Honest about what does not work
- Substack has no official API. A third-party service sells one for $9/month
that authenticates with your browser session cookie — handing a live session
to someone else's server, against Substack's terms. Not wired, not recommended. - llms.txt is generated, with the caveat in the docs: a community convention
with no standards body, no vendor committed to reading it in production, and
no place in any documented citation pipeline. Cheap and harmless, not a lever. - Backlinks from these platforms are mostly nofollow. The mechanism that
works is canonical consolidation, not link equity. And ТИЦ, the metric that
prompted the question, was retired in 2018.
v2.23.0 — five model ids had stopped resolving, and nothing noticed
A vendor-drift release. Nothing in the collection knew the difference between a
model that works and a string that used to, so five models had quietly stopped
resolving and two flags had quietly stopped doing anything. The fixes matter
less than the guard added alongside them.
Fixed — three image models had been dead for five weeks
Google shut the Imagen 4 endpoints down on 2026-06-30. google_image.py went on
mapping imagen-4, imagen-4-ultra and imagen-4-fast to those exact retired
ids, priced them, documented them across thirty files, and listed them in
--help. Nothing in the suite could tell a live model from a string that used to
resolve, so the breakage survived twelve releases.
- Gemini image family replaces Imagen.
nano-banana-pro(gemini-3-pro-image),
nano-banana-2(gemini-3.1-flash-image) andnano-banana-2-lite
(gemini-3.1-flash-lite-image). Nano Banana Pro also loses its-preview
suffix, which was the GA id drifting out from under it. - The retired slugs still resolve.
config.register_deprecated()aliases each
one onto its successor and prints a single warning line. A vendor shutting a
model down does not get to break a user's script. nano-banana-prowas underpriced by 2.7×. The table said $0.05/image;
Google charges $0.134 at 1K/2K and $0.24 at 4K. Estimates below the real bill
are the one direction this table must never round, since they also decide
whether the confirmation prompt fires.estimate()now understands 4K tiers.
Added — a guard so this class of rot is loud next time
tests/unit/test_model_registry.py pins every vendor model id, asserts that each
registered provider is priced and each priced entry is reachable, checks that
deprecation aliases resolve, and fails after 120 days without a manual
re-verification. It found an unpinned lyria-3-pro id on its first run.
Added — install as a Claude Code plugin
.claude-plugin/marketplace.json + plugin.json, so
/plugin marketplace add Mikefluff/skills works alongside the curl installer.
tests/unit/test_plugin_manifest.py validates the schema, checks the declared
skill path actually holds skills, and pins the version across VERSION,
package.json, skills.json and both manifests — package.json had drifted to
1.9.0 while VERSION reached 2.22.0. scripts/bump.sh now syncs all of them.
Changed — Sora 2 is on a countdown
OpenAI announced on 2026-03-24 that the Videos API and both Sora slugs are
removed on 2026-09-24, with no successor. The providers warn on every call
with the days remaining, video-prompt carries a migrate-by-capability table,
and reel-builder routes long shots to Kling 3.0 (15s) instead.
Fixed — the music layer had the same rot, plus two live bugs
- Lyria pointed at a model id that does not exist.
lyria-3.0-pro-preview
vs the publishedlyria-3-pro-preview. Same failure shape as Imagen, caught by
the new registry guard the moment the provider was touched. Added
lyria-3-clip(lyria-3-clip-preview, 30-second tier) alongside it. - Eleven Music ignored
--duration. The provider sentduration_seconds;
the API field ismusic_length_ms. Every run came back at the vendor default
length regardless of what was asked for. - Eleven Music dropped lyrics. A top-level
lyricskey was sent, which is
not in the request schema, so "sing these words" produced instrumentals or
invented words. Lyrics are now folded into the prompt, which is how that API
takes them. Also defaults tomusic_v2(the API still defaults tomusic_v1)
and passesforce_instrumental. - Suno is honest now. There is still no public self-serve API — no console,
no key page, no endpoints. The provider pointed atapi.suno.comwhile the
docs promisedapi.suno.ai; neither answers.SUNO_API_URLis now documented
as required rather than optional, and the gate message nameseleven-musicand
lyria-3-proas the first-party alternatives. - Udio was listed as having an API. It never has. Documented as prompt-only,
along with the UMG/WMG walled-garden transition that gates exports. reel-builderno longer defaults music to Suno, since that default could not
run without third-party infrastructure.
Changed — audio stack caught up
- ElevenLabs TTS defaults to
eleven_v3(70+ languages) instead of
eleven_multilingual_v2. v3 rejects the v2similarity_boostknob, so
voice_settingsis now built per model family rather than sent blindly. gpt-4o-transcribeandgpt-4o-mini-transcribejoinwhisper-1. They are
more accurate and up to half the price, but emit json/text only — asking one
for SRT fails with a message instead of handingsubtitle-burnersomething it
cannot burn.
Changed — model registry refreshed
Veo 3.1 Lite ($0.08/sec), Runway Gen-4.5 (gen4.5), Ideogram 3 Flash. Docs
updated for Midjourney V8.1, Seedream 5.0 Pro/Lite (layered PNG output),
Ideogram 4.0 (open weights, JSON-first prompting — no v4 API endpoint yet, so
execution stays on v3), FLUX 3 and Muse Image (no public API, documented only).
docs/ROADMAP.md was twelve releases stale and claimed six shipped skills were
still planned.
v2.22.0 — the baseline reaches zero, and the repo grows a skills/ folder
Added — structural gates, and the publishing layer refactored to pass them
Two new smoke gates. check-code-quality.py enforces module ≤400 lines, function
≤50, branch complexity ≤12, parameters ≤5, plus two invariants: every registered
Publisher honours its ABC, and lower layers never import upper ones. The CLI
surface gate confirms all 26 CLI modules import and build a parser — those paths
have no unit tests, so it is the only thing between a refactor and a runner that
dies on invocation.
19k lines of Python predate the thresholds, so the gate runs against a frozen
baseline, the same shape check-launch-copy.py already uses here. Anything new
fails hard; the baseline may only shrink, and --freeze refuses to grow it. A
gate that fails the whole repo on day one gets disabled on day two.
The contract and layering checks have no baseline. They are invariants, not debt.
The publishing layer now has zero violations, which took four extractions,
each of which was worth doing on its own:
preflight()was a 102-line method checking ten unrelated things. It is now
publishers/rules.py— one function per rule, same signature, composed in a
list. A rule can be tested against a stub instead of a whole publisher.- Five publishers repeated the same forty lines of OAuth plumbing, and it had
already drifted — the "is there a refresh token?" guard existed in three of
them and not the other two.publishers/_oauth.pyholds the shape; subclasses
declare the endpoint and the payload. cli/publish.pywas a 423-line module whosemain()ran to 127 lines at
complexity 36. Working out what a directory contains moved topostsource.py
(domain logic, no argparse), rendering moved tocli/_publish_view.py, and the
per-platform decision sequence became one readable function — that order is
the safety contract, and it was hard to audit inside the loop.build_post()had grown to seven parameters that always travel together;
they became aPostOverridesdataclass.
Outside the publishing layer, the worst function in the repository is gone:
cli/reel.py:main() ran 260 lines at complexity 60. It was already a pipeline —
the code had comments labelling "Stage 1/2/3" — so it became one: load_plan,
prepare, estimate, run_shots, run_music, and an ffmpeg stage in
cli/_reel_stitch.py. Verified by re-running --cost-only, the stdin path, and
every plan-error case against the original exit codes.
The gate found three violations in itself on first run, which is the correct
outcome for a tool with any authority, and two bugs worth recording: --freeze
refused to create the initial baseline because "0 → 116" reads as growth, and a
failure printed all 116 findings, which is a wall nobody reads.
114 known violations remain, all pre-existing, all frozen and visible. The
largest cluster is the twenty -maker CLI main() functions, which share a
shape and want a common skeleton rather than twenty separate edits.
Changed — the baseline is empty
The 114 are gone. Twenty commits of extraction took the frozen baseline to zero,
so every threshold is now live: a new violation is a hard failure with nothing
to hide behind. The largest cluster went the way it was always going to — one
skeleton for the nine maker CLIs, one HTTP layer for the vendor adapters, one
procedure for the single-call tools.
Five bugs surfaced during those refactors, three of which no test could have
caught because nothing executed a main(). Each got its own commit and its own
test.
Changed — repository layout: the 42 skills live under skills/
The root listed fifty directories — forty-two skills and eight pieces of
machinery, undifferentiated. It now lists nine.
Nothing changes for anyone installing. install.sh, the npm package and the
Docker image all still produce the flat ~/.claude/skills/<name>/ layout, which
is what Claude Code expects; verified with a real install rather than a dry run.
Only the source tree moved.
What it buys: three manifests each enumerated all forty-two skills by hand, and
a gate existed purely to catch them drifting apart. The Dockerfile drops from 42
COPY lines to 1, package.json from 49 files entries to 8, and that gate
now checks the claim that makes one COPY safe instead of maintaining a list.
skills.json already carried a dir field distinct from name — precisely
this seam — so validate.sh needed no change at all.
If you reference repo paths directly (a submodule, a bookmarked
github.com/…/tree/main/viral-text), add skills/. CHANGELOG.md is
deliberately untouched: it records what was true when written.
Fixed — YouTube counted characters where the API counts bytes
snippet.description is documented as "a maximum length of 5000 bytes"
while snippet.title on the same page is 100 characters. Preflight counted
code points for both, so a 3000-character Russian description — 6000 bytes —
passed the gate and was rejected by the API. CJK is worse.
The send path was truncating by characters too, so a fix to preflight alone
would have left a description it had approved being cut to 5000 characters and
posted as 10000 bytes.
Fixed — TikTok photo posts have two caption fields, and neither is the video's
Photos are a different endpoint from video, with a different field table: a
title capped at 90 UTF-16 runes and a description capped at 4000, where
video has one title of 2200. One constant covered both, so the entire rendered
caption went into title for every post kind — and any carousel caption over 90
characters was a guaranteed rejection that preflight waved through at 2200.
The caption now goes in description; title becomes a headline taken from
--title or the first caption line. disable_duet / disable_stitch are
dropped from the photo body, being video-only fields.
Not fixed, and written down instead: TikTok documents brand_content_toggle and
brand_organic_toggle as required on both endpoints and this repo sends
neither. Whether the API actually refuses without them cannot be established
without publishing a real post, so it is recorded in
skills/post-publisher/references/platform-limits.md rather than guessed at.
Fixed — four defects found by reading, left alone mid-refactor
styles.anchor()swallowed an inline field. The walk stopped only on a
line starting with**and ending in:, so**Best for**: weddings
written under an anchor was absorbed into it. Users are invited to author
styles and the anchor goes into every generated prompt. The larger find was
thatsection()had the same stop condition copied out by hand — fixing the
helper would have left the copy broken. Measured across all 48 bundled styles
before and after:anchor()andvalidate_style()byte-identical,section()
different on every one (it had been silently running on; no callers).cover_imprints.apply_textconsumed the preset it read from. It filled the
sharedTypeLayoutin place, so a second cover in one process started from the
first cover's text — and because an empty author skipped the write, a book with
no author was published under the previous book's author.cli/gif.py --yeshad no confirmation to skip. Mode B calls a video
provider directly; veo-3-1 is $0.40 a second against a $0.10 threshold. Every
other generating CLI stops to ask. Now wired tocost_mod.confirm, exit 3 on
decline.subtitle._cmd_burn's unreachable branch was already deleted by the
_resolve_cuesextraction. No change needed; the test that documents it no
longer describes code that is gone.
Fixed — the batch manifest could lose the run it exists to protect
write_manifest used a plain write_text. The manifest is rewritten after every
item so that a crash is survivable, and a crash during that write left a
truncated file that the next --resume could not parse — the feature defeating
itself in the one situation it exists for. Now atomic, via a new
atomicfile.write_text that shares the replace-in-place guarantee without
forcing the 0600 the credential stores need.
Added — the twelve unverified platform limits, read
Rows in platform-limits.md marked ~ were ones nobody had found in the
vendor's own documentation. Ten are now verified and marked ✅ — Telegram's
4096/1024 split and 10 MB sendPhoto, X's 280 characters and 5 MB / 512 MB,
YouTube's 100-character title and 256 GB / 12 h, LinkedIn's 500 MB video and
4,086-character altText, Threads' 1 GB video, TikTok's 20 MB image.
Two survive as ~ with the reason recorded: LinkedIn documents neither a
commentary length nor an image file size anywhere in its API reference.
Four constraints that are documented, real and unenforced are now listed
rather than silently omitted — three video-duration limits and Telegram's photo
dimension ceiling. Duration is the one most likely to bite: a 3-minute reel is
well inside X's 512 MB and will still be rejected.
Added — tests for the eight modules that had none
The three refactors the paydown proved byte-identical with throwaway scripts now
carry those proofs as committed tests: compose_book_cover across all five
imprints, render_html across 36 language × theme × brand combinations, and
write_brief across 8. The cover hashes are recorded per Pillow major version
and skip on an unrecorded one — rasterisation is FreeType's, and a hash that
fails for somebody else's font renderer gets deleted rather than investigated.
Also: the proposal CLI driven end to end through both output modes (gate 14
proves it imports; it does not execute a line of main()), the brand palette
and font pickers from both sides of every threshold, the styles submit package
a human carries to GitHub, batch.py resume semantics, and the reel stitch
pipeline's three degradation points.
The suite goes from 403 to 599 tests.
v2.21.1 — the platform constants, actually verified
v2.21.0 shipped post-publisher with a note in its limits reference reading "last verified: not yet". This release is that verification.
Four numbers were wrong, each in the direction that hurts:
| Was | Actually | |
|---|---|---|
| Instagram publishing cap | 25 posts / 24h | 100 posts / 24h |
| Instagram Reels file size | 1 GB | 300 MB (3 s – 15 min) |
| YouTube upload allowance | 1600 units of 10,000/day ≈ 6 | 100 uploads/day, on their own allocation |
| LinkedIn API version | 202401 |
202607 — the old one had aged out |
The Instagram cap would have blocked three quarters of a legitimate posting day, phrased as certainty. The Reels limit would have let a 700 MB file pass preflight, stage to S3, get fetched by Meta, and fail only then. The YouTube figure was off by sixteen times, in the direction that makes people ration something they have plenty of.
Also corrected: Telegram photos are held to sendPhoto's 10 MB rather than the general 50 MB upload ceiling, and the X module no longer asserts a "17 posts per 24h free tier" that appears nowhere in X's documentation — the verifiable ceilings are 10,000/24h per app and 100/15min per user.
What was checked and found correct
Every endpoint path and request shape across all seven platforms. TikTok's chunking rules including the 5–64 MB range, the floor-division chunk count and the single-chunk-equals-file-size rule. X's POST /2/media/upload with INIT/APPEND/FINALIZE/STATUS on a Bearer token — not the legacy upload.twitter.com host. LinkedIn's content.multiImage.images[] shape with id + altText, its x-restli-id response header and both required version headers. Threads' container flow, 500-character limit, 2–20 carousel and 250/day cap. Instagram's 2200-character caption, 30 hashtags, 8 MB images, 10-item carousel and status_code polling field.
So it does not happen again
Thirteen tests pin the verified constants — a plausible-looking wrong number is invisible in code review. platform-limits.md now marks every row with whether it was read off live documentation or carried from a parameter table that could not be machine-read, lists the source document per platform, and each corrected constant in the code carries a comment naming its source and date.
Full detail: CHANGELOG
v2.21.0 — the collection can finally publish what it makes
Every skill in this collection stopped at ./generated/. Three files said so on purpose. This release closes that last mile.
New skill: post-publisher
Publishes finished assets to Instagram, Threads, TikTok, X, YouTube, Telegram and LinkedIn through the official APIs. It takes a carousel-builder or reel-builder output directory, reads captions.md, and sends it.
post-publisher ./generated/carousel/<slug>/ --platform threads,telegram # dry run
post-publisher ./generated/carousel/<slug>/ --platform instagram --draft --yes
python3 -m common.runners.cli.auth --platform threads # connect onceDry-run is the default and --yes is what leaves it — inverting the convention elsewhere in this repo, where --yes skips the cost prompt. A wasted generation costs cents; a wasted publish costs an audience. Each platform confirms separately, and posted.json receipts stop the same content going out twice.
Start with Telegram (no OAuth, five minutes) or Threads (simplest OAuth, text posts need no S3). Instagram is the worst place to start: Business account, registered app and an S3 bucket before the first post, because it fetches media from a URL rather than accepting bytes.
Setup per platform: post-publisher/references/oauth-setup.md · Walkthrough: publish-to-social.md
Worth knowing before you use it
- TikTok defaults to the inbox, not to publishing. Direct posting needs an audited app; without one every post is forced to
SELF_ONLYwhile the API reports success — the only failure mode in the set that looks like a success. - Instagram needs a Business or Creator account. No API publishes to personal accounts. The browser fallback covers that case honestly rather than implying an endpoint exists.
- Endpoints move. API versions are env-overridable (
INSTAGRAM_API_VERSION,LINKEDIN_API_VERSION,X_UPLOAD_URL, …) andplatform-limits.mdcarries a verification procedure. Check it against live vendor docs before relying on this in anger.
Fixed: npm and Docker shipped a 17-skill subset
Dockerfile and package.json both listed exactly the v1.x prose set, frozen since ~v2.3 while twenty-five skills were added around them — so install.sh inside the container warned about twenty-five missing skills, inside the artifact meant to contain them. Both lists regenerated from skills.json, and check-docs-consistency.sh gained a gate comparing them to it. The image is 117 MB; the subset was drift, not a decision.
If you use the Docker image or the npm package, this release is the one that actually contains the media and orchestrator skills.
Also in this release
Fixes that had accumulated unreleased: the writer linter's density verdict no longer counts hard bans (a document with forty-eight ordinary Russian em-dashes read as "neuroslop suspected"), exit code 3 no longer masks exit code 2 for the pre-commit hook, --quiet is quiet again, and the launch copy is both rewritten for what the project now is and actually linted by CI.
Full detail: CHANGELOG
Install
curl -fsSL https://raw.githubusercontent.com/Mikefluff/skills/main/install.sh | bashv2.20.0 — detection depth, release process, runner tests
Added — proposal-maker: raw offer → brand-faithful HTML proposal
Takes a telegram-style offer (client, line items with catalogue links, total) and produces a self-contained proposal.html whose visual style is copied from a brand's website.
Default flow is LLM-authored: a Python step assembles the brand kit (site screenshot, logo, accent and font tokens, per-item catalogue photos, BRIEF.md), then the skill writes bespoke HTML mirroring that brand. Line items missing a photo get an on-brand generated image. Prices and links stay exact — nothing about the commercial terms is generated. Prints to a link-preserving, Ghostscript-compressed PDF.
--quick skips the brand scrape entirely and renders one of three deterministic themes (editorial / invoice / dark), for offline use or clients with no site. Saved brand profiles live in proposal-maker/brands/ and are reusable across proposals for the same client.
Added — style-suggest: description or reference image → visual-style entry
Authors a new entry for the shared visual-prompt style library that every image-producing skill reads (carousel-builder, cover-maker, flyer-maker, quote-card-maker, banner-maker, logo-maker, thumbnail-maker, avatar-maker, meme-card-maker).
Takes free-form text, a reference image, or both. Checks the catalogue for a near-duplicate first (similarity ≥ 0.72) and says so rather than growing the library with variations of the same look. Otherwise emits a complete v2.15.0 entry — background, accents, elements, mood, accent_text_color, typography, composition_signature, when_to_use — into common/visual-prompt-library/styles/, usable immediately.
Distinct from skills-styles, which manages the carousel / video / music library by hand. This one writes the entry for you.
Detection and discipline pass over the prose stack, adapted from smixs/humanizer-ru (MIT), which in turn credits Vladimir-Human/humanizer-ru and petergyang/no-ai-slop (both MIT).
Added — class A copy-paste artifacts: one hit is the verdict
writer/scripts/lint.py now catches the service markers that reach a text only by copying out of a chat UI: :contentReference[oaicite:N], turn0search3, utm_source=chatgpt.com, referrer=grok.com, Gemini's [cite: 8] and grounding-api-redirect, 【12†source】, sandbox:/mnt/data/, </think>, ppl-ai-file-upload, unfilled placeholders, PUA glyphs.
No editor and no CMS produces these, so they need no corroborating cluster — a single hit settles the question. Reported as COPYPASTE_ARTIFACT at blocker severity. Two deliberate exemptions: URLs are scanned rather than stripped (that is where utm_source lives), and backticked spans never count, because quoting an artifact in documentation is not pasting one. Zero-width characters are class B (ZERO_WIDTH, caution) — newsletters inject them too — with ZWJ inside emoji sequences explicitly allowed. Registry: writer/references/copypaste-artifacts.md.
Added — a gate that is orthogonal to the density verdict
The linter now emits two independent results. verdict is unchanged (clean / borderline / neuroslop suspected) and stays a judgement call about slop density. gate is pass/fail on hard bans and is not a judgement call. A text can read clean and still fail the gate on one pasted turn0search3.
Hard bans wired as blocker: RU em-dash, math and code signs in prose (= → > < ≈ vs), completed negative parallelism («не только X, но и Y», «это не просто…»), chopped drama («Без X. Без Y.»), and class A artifacts. The blocker severity tier existed in the JSON schema but had no category assigned to it — it was dead code until now. New exit code 3 for a failed gate. New JSON fields gate and hard_bans.
--fiction demotes the em-dash ban to an advisory nit, matching the exception writer/references/typography.md already granted to book typesetting — without it every line of dialogue would fail.
Added — structural detectors regex cannot express
Five document-level checks, none of which are phrase lookups:
RHYTHM_MONOTONE/RHYTHM_NO_SHORT— mean difference between adjacent sentence lengths under 4 words, or no sentence under 9 words across 10+. Uniform sentence width is the LLM tell no word list catches.VERB_ECHO— the same verb stem in adjacent sentences. The repetition penalty makes models vary nouns (hence synonym cycling) while duplicating verbs into parallel constructions: «Сбербанк предлагает… Тинькофф предлагает…».BOLD_DENSITY— more than roughly one bold span per 200 words: formatting standing in for content.HEADING_ECHO— a short line after a heading that restates it. Stem-based, so it survives Russian inflection.HEDGE_CASCADE— three or more softeners in one sentence. One or two are ordinary careful speech and do not fire.
Added — three new categories, two of them regex-backed
THERAPEUTIC («и это нормально», «вы не одиноки», "and that's okay") — fake care, distinct from SELFHELP's fake drive. CALQUE_COLLOCATION («адресовать проблему», «доставить ценность») — every word Russian, the pairing English; distinct from the direct borrowings already in ru-calques.md. DANGLING_GERUND («используя метод, результаты улучшаются») — deliberately narrowed to a closed list of gerunds plus a required impersonal head, because a suffix guess would also match ordinary adjectives. FALSE_RANGE is documented but intentionally has no regex: legitimate ranges outnumber the slop.
Catalogue is now 25 RU categories + 18 EN signatures.
Added — forbidden substitutions: slop replaced by kindred slop is still slop
New writer/references/forbidden-substitutions.md. The failure mode it guards against is mechanical: a repetition penalty pushes the model toward synonym substitution instead of deletion, so «ключевой» becomes «важнейший» and «не только X, но и Y» becomes «как X, так и Y» — same cadence, new packaging, cleaning pass scored as a success.
Contains the treatment hierarchy (delete → replace with a fact from the original → rewrite simpler; a synonym is level zero and does not count), the substitution table, three rules for how a replacement knits into surrounding text, and the "do not touch" list.
Added — delete the water, not the function
The default treatment for slop is deletion, which is dangerous for text that has a job to do. A CTA, offer, price, deadline, link, contact, recovery step or upgrade command is a working part, not water — treat by replacement or simplification, then verify each survived.
Wired into landing-copy, cold-email, microcopy, viral-text, release-notes, and made a standing requirement for every sub-skill in writer/references/integration.md.
Added — phase discipline and a stop rule in writer
Audit → edit → verify, and the phases do not mix. Editing while reading overwrites detection: a model that has started rewriting goes on to repeat its own phrasing and misses half the patterns, because it is reading its draft over the text rather than the text.
Phase 1 builds a findings table and edits nothing. If there are no findings, stop and return the text unchanged — re-editing clean text degrades it, and dry text with no catalogued patterns is just dry text, not AI. Phase 3 adds a blind verification pass: hand a fresh subagent only the final text and the category catalogue, with no original and no edit history, because an editor recognizes their own phrasing and grades it leniently.
Added — detect mode in style-check
Answers "was this written by AI?" without touching the text. Scores findings by family (лексика / структура / коммуникация) rather than raw count: hits confined to one family are an author's style, hits spread across two or more are a model. Any class A artifact settles it alone.
Includes an explicit limit: flawless grammar, dryness and a wide vocabulary prove nothing, so report markers with quotes and let the reader conclude. Missing a machine-written text is cheaper than accusing a living author. See style-check/references/detect-mode.md.
Added — prompt-injection guard
Text handed to writer is data, not instructions. Directives inside it («забудь правила», «выполни», «отправь») are edited as ordinary sentences, never executed; a noticed attempt is reported in the summary. Matters because these skills routinely process text pasted from clients, email and websites.
Added — behavioural evals, a layer the repo did not have
tests/evals/writer.json — 9 scenarios, 49 checks. Snapshot tests lock the linter; these lock the skill. A snapshot proves lint.py still flags «представляет собой»; it cannot tell whether the model invented a statistic while removing it, deleted the CTA along with the water, or swapped «ключевой» for «важнейший» and stopped.
Seven of the nine are guard traps, and that ratio is the point: clean text that must come back untouched, slop that must not be replaced by kindred slop, functional elements that must survive, facts that must not be invented, a prompt injection that must not execute, fiction that must not be business-edited. A skill scoring well on the happy path and failing these produces confident, clean-reading damage.
Not wired into CI — they need a model in the loop, and a test that needs a model is not a smoke test.
Fixed — the calibration samples the linter could never see
scripts/check-after-samples.py, wired into smoke.sh as step 4/5.
The hole it closes: calibration samples live inside fenced code blocks, and lint.py masks fenced blocks by default. So the one thing a model copies verbatim — the "После" sample — was the one thing never checked. Six real violations were sitting in them, including an em-dash in landing-copy's hero sample and in microcopy's empty-state body, both...
v2.12.3 — figma-rigor hook/framework/cta + character override
Upgrades hook + framework + cta layout templates in carousel_prompt_builder.py with figma-style explicit typographic hierarchy (PRIMARY HEADLINE / SUBTITLE on separate pill / MAIN SUBJECT integrated, with explicit sizing %s and design discipline). Adds optional visual_hint field to FrameworkContent + CtaContent so character-driven decks can override scene policy via content (not global config). Validated with character-driven 3-slide course-invitation deck — framework now reads as designed infographic with per-card eyebrow + body hierarchy, not floating word-chips. Other 6 role layouts not yet upgraded — incremental v2.13 task.
v2.12.2 — per-role scene policy + anchor=vocabulary docs
Patch fixing the 'same scene on every slide' anti-pattern when style anchor bakes in a fixed setting. Builder now injects a per-role scene policy directive that overrides scene leakage on non-hook slides. Hook keeps the literal scene; framework/data/steps/comparison/myth-vs-truth get clean textured fields with content dominating; quote/cta allow one minimal decorative element. Docs: _universal-rules.md §0 (anchor=vocabulary) + §11 (scene policy table); dark-academia.md rewritten as model vocabulary anchor; SKILL.md anti-pattern constraint.
v2.12.1 — structured plans + stdin support
v2.12.1 — structured plans + stdin support across all 9 plan CLIs
UX patch — eliminates intermediate Python build-scripts AND disposable plan.json files.
Two problems with v2.12.0
- Per-test Python build-script overhead — using
carousel_prompt_builderrequired writing an intermediate Python script that imported the builder and dumped a plan.json. Bloat per iteration. - Disposable plan.json files — every test created
plan-v2.json/plan-v3.json/lattice-plan.jsonetc. — a graveyard of one-off artifacts.
Both fixed.
Structured-content items (carousel-builder)
skills.carousel.plan.v1 plan items now accept TWO shapes:
// Legacy (back-compat):
{"index": 1, "label": "...", "prompt": "<full prompt>", "kwargs": {...}}
// New — structured content:
{"index": 2, "label": "...", "role": "framework",
"content": {"framework_name": "...", "boxes": [...]}, "kwargs": {...}}When a plan item has role + content, the CLI internally calls carousel_prompt_builder.build_slide_prompt() at execution time. The plan also accepts top-level lang and slide_marker_style fields applied uniformly across slides.
No Python script needed — structured content goes straight in the JSON. All 9 roles supported (hook / point / framework / data / steps / comparison / quote / myth-vs-truth / cta), each routed to its matching dataclass.
--plan-file - reads from stdin (all 9 plan-driven CLIs)
carousel, cover, flyer, avatar, thumbnail, banner, meme, logo, quote, reel all support --plan-file -. Pipe a heredoc straight into the CLI — no intermediate file needed for one-off tests / iterations. Production workflows can still use file-based plans.
Full workflow now
Claude (in conversation) assembles structured plan
→ heredoc
→ stdin
→ CLI builder
→ batch generates
→ PNG files
Zero disposable artifacts on disk between the prompt and the deliverable.
Notes
- 39 skills total (unchanged). Pure UX patch.
- Other plan-driven skills (cover-maker, flyer-maker, avatar-maker, etc.) get stdin support but DON'T yet have structured-content mode — they still need full per-item prompts. Adding structured shape to them is a future enhancement.
- Validated end-to-end with a 3-slide SaaS promo deck (gradient-mesh-modern style, hook → framework → cta) generated via heredoc — no files created.
Generated with Claude Code.