publish
Skill1 starsChanged 3 months agoApache-2.0View on GitHub
Posts finished episode files to Threads, Instagram, Facebook and YouTube. Use when the user asks to "게시해", "올려줘", "SNS 게시", "publish to social", "스레드/인스타/페북/유튜브에 올려", or once production is done. Takes the per-platform content under data/[channel]/episodes/[topic]/output/ through account checks and public media hosting, stops for the user's publishing authorization, publishes through the social-flow platform tools (Threads, Instagram and Facebook post at once; YouTube uploads private and flips public with youtube_update), and records the permalinks in publish-log.md.
60 other projects use a version of this.
Adopted by
Most recently taken up by
- jskswamy/skillstheir own version, first seen Nov 8, 2026
- ai-search-guru/getcito-worlds-first-open-source-aio-aeo-or-geo-tooltheir own version, first seen Nov 8, 2026
- Mharbulous/probanda-devtheir own version, first seen Nov 8, 2026
- hyperstructure/sandboxtheir own version, first seen Nov 8, 2026
- will-17173/telegram-clitheir own version, first seen Nov 8, 2026
- pche-broken-artist/forge-of-thoughttheir own version, first seen Nov 6, 2026
Used with
Projects with it also use create, render and 9 more.
Other files people use alongside this one.
Files found in the same public projects as a version of this one, more often than they turn up anywhere else. Each project counts once, large collections count for less, and this file's own repository is left out.
MicrosoftSkillcreate
Also in 4 projects that use this
suxrobGMSkillrender
Generate raster images (PNG, JPEG, WebP) from natural language prompts using Python rendering engines. Covers graphics, charts, layouts, and procedural art. Not for SVG output.
Also in 3 projects that use this
genomewalkerSkillexplore
RLM-style recursive memory exploration - dynamically navigate the memory graph
Also in 4 projects that use this
GoogleSkilltranslate
Translates a common phrase into one or more languages.
Also in 3 projects that use this
VercelSkillsetup
Set up Vercel CLI and project configuration. Use when the user says "set up Vercel", "configure Vercel", "link to Vercel", or "vercel init".
Also in 14 projects that use this
aakash-dharSkilloptimize
Analyzes code for performance issues including slow algorithms, memory leaks, unnecessary re-renders, database query problems, and resource bottlenecks. Use when the user says "this is slow", "optimize this", "performance issue", "why is this taking so long?", or "make this faster".
Also in 3 projects that use this
Other versions60 projects use a version of this.
Word for word · 0
None found.
Edited · 0
None found.
Wrote their own version · 60
- code-yeongyu/oh-my-opencode★ 71ksame name
- code-yeongyu/oh-my-openagent★ 71ksame name
- garrytan/gbrain★ 32ksame name
- TencentCloud/Octop★ 12ksame name
- harbor-framework/harbor★ 6.9ksame name
- laude-institute/harbor★ 6.9ksame name
- Q00/ouroboros★ 6.3ksame name
- paean-ai/deeptide★ 1.1ksame name
- ZimoLiao/scholaraio★ 585same name
- ai-search-guru/getcito-worlds-first-open-source-aio-aeo-or-geo-tool★ 432same name
50 more not listed
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository30 other files it gives its agents.
- AGENTS.mdAGENTS.md
- CLAUDE.mdCLAUDE.md
- autoproduceSkill
- brandingSkill
- channelSkill
- compose-blenderSkill
- datagoSkill
- edit-speechSkill
- generate-cut-imagesSkill
- generate-cut-videosSkill
- grow-instagramSkill
- grow-threadsSkill
- grow-youtubeSkill
- ingestSkill
- inspect-contentSkill
- introSkill
- platform-guideSkill
- produceSkill
- project-initSkill
- review-cameraSkill
- review-contextSkill
- review-cut-imagesSkill
- review-cut-videosSkill
- review-recentSkill
- setup-instagramSkill
- setup-threadsSkill
- setup-youtubeSkill
- storyboardSkill
- topic-scoutSkill
- upscale-videoSkill
The file
Show its rules and text
We split this skill into the rules it gives an agent
29 rules. 0 are common, 12 are distinctive, and 17 are very specific to this project.
0
Common
Most of the 49 edited versions we read keep these.
12
Distinctive
This author's own choices. Worth a look.
17
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 20
Use local episode files under data/<channel>/episodes/<topic>/.
- Very specific: line 63
No link in the body either — the first two link-in-body posts got 9 and 21 views, a tenth of the link-free posts from the same day …
- Very specific: line 79
Call youtube_publish with privacyStatus: "private", attach everything the cover needs, and only then flip to public with youtube_update.
- Distinctive: line 108
If it needs the user (a login, say), ask right then — don't defer it.
- Very specific: line 158
Every format uses the clean master.
- Very specific: line 187
Call sns_account_check with channel: <channel-slug> to check this channel's per-platform token status — confirm and report which account is …
- Distinctive: line 190
If the account name in the check doesn't match the channel brand, stop publishing and confirm with the user.
- Distinctive: line 200
Run the style checker once per surface before approval (Bash, not an LLM call) — after publishing you can't swap the IG image or video, so …
- Very specific: line 245
Only a fallback episode that can't carry a video uses a reply link, and there the link slot is still the <IG_REELS_URL> placeholder at this …
- Distinctive: line 258
Use the same subtitle-free master on every platform.
- Distinctive: line 271
Use the hosting method named in profile.md §4.
- Very specific: line 273
If you used a temporary tunnel (python3 -m http.server + cloudflared tunnel): (1) serve from a dedicated directory holding only the publish …
- Very specific: line 278
Check 200 and MIME with curl -sI <URL> before publishing.
- Very specific: line 284
Put channel: <channel-slug> on every publish and comment tool call below (rule 7).
- Distinctive: line 289
Only the fallback (an episode that can't carry a video) puts IG reels before Threads, because the link in the Threads reply is then the IG …
- Very specific: line 299
captionTracks: instead, listing the requested subs.<lang>.srt with its BCP-47 code ([{filePath: …/subs.srt, language: "ko"}, {filePath: … - Distinctive: line 346
If a login is needed, ask the user right then — don't defer it as an open item and report completion.
- Very specific: line 352
Threads: before calling, record the actual §1 human approval in data/<channel>/episodes/<topic>/threads-publish-approval.json.
- Very specific: line 361
Call threads_publish(channel, episodeRef: <topic>, ...) — the approved body (no link) as caption and the public URL of the clean …
- Distinctive: line 367
No link in the body, same as before — the first two link-in-body posts got 9 and 21 views, a tenth of the link-free posts from the same day …
- Very specific: line 395
Call youtube_publish with privacyStatus: "private".
- Very specific: line 397
Only when YouTube subtitles are requested, add captionFilePath=subs.srt or captionTracks for the requested languages.
- Distinctive: line 410
If any one of them fails, tell the user while it's still private — that stage exists so the fix lands before anyone has seen it.
- Very specific: line 481
When you reply to comments after publishing (sns_comment_reply), check the copy first as well.
- Distinctive: line 497
Use a growth-post-reviewer read when it helps resolve wording or context concerns.
- Distinctive: line 498
Keep the original comment and post body available.
- Very specific: line 510
Update storyboard.md to status: published.
- Distinctive: line 511
If you used a temporary tunnel, verify the teardown per §1, then give the final report as a platform/permalink table.
- Very specific: line 523
Read the episode settings snapshot when present.
“Common” is measured: kept by at least 6 in 10 of the 49 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Automatic execution policy: Read automation guidance first. Creative rules and review findings guide improvements; scores, review/hash/freeze records, CTA choices and intermediate approval steps do not block authorized work. Preserve actual external-action authorization and stop at the artifact the user requested. This policy supersedes older quality-gate wording below.
Use local episode files under data/<channel>/episodes/<topic>/.
HITL records: Record the user's platform approval separately from actual publication outcomes. Preserve actual provenance in local files and reuse existing authorization. Publishing needs the user's authorization; quality reviews add no gate.
Platform publishing — after the user's approval
Publishes the finished artifacts in output/ with the per-platform publish tools.
Threads, Instagram and Facebook post the moment the tool is called; YouTube uploads
private (rule 9). The user's approval (§1) and the native permission prompt that fires on
every publish call (never pre-approved) are the two lines of defense. Don't nudge the user
into promoting that prompt to "always allow".
Owner directive (2026-10-17): upload the clean, subtitle-free video on every platform, even when characters speak. Never select a stale video-sub/reel-sub file. Separate YouTube subtitle tracks are optional when the user requests them; do not infer consent from profile languages or an SRT already on disk. This overrides older subtitle recipes.
Absolute rules (violation = stop immediately)
- User approval before publishing is mandatory (HITL) — before any call, present the full final text for each platform with AskUserQuestion and get explicit approval. IG can't swap the image after publishing, and every platform pins it to a brand account (irreversible).
- No cross-post copy-paste — the same sentence must not go out on two platforms verbatim.
- Never expose a token in plain text — handle tokens only as files under
~/.config/social-flow/<channel-slug>/. Even when debugging with curl, use inline$(cat …)references only; never print token values or the access_token field into the conversation. - No distorting the facts — publish only within the approved artifacts. If a caption needs fixing on the spot, take the corrected version back through the approval gate.
- Respect the limits — Threads 250/24h, IG 100/24h, YouTube uploads 100/day (the videos.insert-only Video Uploads bucket, 1 unit per call — no reason to ration episode publishes).
- Threads carries the video on the post, FB puts the link in the first comment —
a Threads video episode goes out in one call: the casual spoken-style body (no link)
as
captionplus the public URL of the clean subtitle-free master asvideoUrl(user directive 2026-08-19). Don't attach the video as a reply. Only an episode that can't carry a video falls back to a self-reply link. No link in the body either — the first two link-in-body posts got 9 and 21 views, a tenth of the link-free posts from the same day (measured 2026-08-15). FB: no link in the body →facebook_commentfirst comment, and the FB publish is complete only once that comment is up too. - Every publish and comment tool call takes
channel: <channel-slug>— use this skill's first argument (data/<channel-slug>) as is. With a channel given, only that channel's token is used, with no fallback to the default token (prevents publishing from the wrong account — a server contract). If you get an error saying the channel has no token, walk the user through the channel-directory procedure inreferences/token-setup.md. - Every platform gets
video.mp4, with no subtitles inside the video. Instagram and Threads have no separate subtitle parameter; publish the clean master there too. When the user requests YouTube subtitles, passcaptionFilePathorcaptionTracksfor separate viewer-selectable tracks. Otherwise omit caption inputs. Existing sidecars and legacy burned-in files do not change this choice. Every YouTube upload starts private — no exceptions (user directive 2026-08-19). Call
youtube_publishwithprivacyStatus: "private", attach everything the cover needs, and only then flip to public withyoutube_update. The server uploads private by default and rejects apublicrequest (unlistedstays allowed); growth loops run the same flip withyoutube_updateunder their approved plan. Publishing straight to public exposes a mid-sentence frame as the first thing anyone sees while you are still attaching the cover. This holds for short-form and long-form alike; the long-form specifics are rule 10.What
thumbnailFilePathchanges is the landscape surface only — search results, share previews, embeds. The portrait frame that the shorts feed and the channel shorts tab actually show is separate, and left unset a random mid-video frame lands as the first frame — an expression mid-sentence instead of our cover.The portrait frame is set in a browser (measured 2026-08-19) —
studio.youtube.com/video/<id>/edit→ the⋮over the portrait image in the thumbnail box → "Select from video" → the cover frame → Done → Save. Drive it with ego lite (the only browser lane — no Chrome MCP fallback); the native app is not needed. Don't pickChange(file upload) in that same menu — it only swaps the landscape surface. If the picker is missing or the cover isn't among the candidates, fall back to the emulator procedure inreferences/shorts-surface-adb.md.The verdict is not
oardefault.jpg— it stays 404 even after the frame is set, andoar2.jpganswers 200 with a cached older frame. Read the tile'simgsrc from the channel shorts tab DOM (youtube.com/@<handle>/shorts), download it, and confirm with your eyes that it's the cover. This check only works once the video is public — private videos don't appear in the shorts tab. So the frame gets set while private, and the check happens right after going public.Don't skip this step and report "published" (completion criteria: §4). If it needs the user (a login, say), ask right then — don't defer it.
- Long-form publishes in two stages — upload the 8–15 minute video as
privacyStatus: "private", have a human check the encode, subtitles and chapters on the watch page, then flip it to public withyoutube_update. Going straight public like short-form means the viewer sees the failure first — a 12-minute video takes a few minutes to process, and a low-quality copy is exposed meanwhile. Procedure in §3-2. Don't add#Shorts— on landscape long-form it gets misfiled onto the shorts surface. Rule 9 (portrait first frame) doesn't apply to long-form. Don't touch a video while it is spreading — the metadata freezes the moment the §4 checklist passes. From there: no re-upload, no delete, and no title, description or thumbnail change without a reason you can write into the log. Re-editing a video while it is gaining views resets the momentum it had built. That grade is creator-observed — YouTube documents nothing here, so it stands as our operating rule rather than [official]. The mechanics point the same way:
youtube_updateoverwrites instead of patching (§3-2 step 3) and one casual title edit re-sends the description and thumbnail at whatever they currently are.What this doesn't restrict. Everything rule 9 attaches while the video is still private — thumbnail, subtitle tracks, playlist, portrait first frame — and the private→public flip itself, which is the publish completing. The portrait-surface check after going public reads the tile and changes nothing. Finishing an attachment that never landed still belongs to the publish: the subtitle track a publish-only scope rejected (§3 step 1), an FB
captionWarning(§3 step 4), a first comment that failed. And replying to comments is the post-publish work we want — §5 checks that copy before it goes out.A factual error in what went out is the one discretionary fix worth the reset. Take it to the user first, then correct it and record what changed and why.
Procedure
0. Pre-check (once per session)
Read the format first —
window.FORMATinscenes.jsdecides the publish path.PG=${CLAUDE_PLUGIN_ROOT}/skills/platform-guide/references node $PG/format-resolve.js storyboard/scenes.js --json | python3 -c \ 'import json,sys; f=json.load(sys.stdin); print(f["format"], f["platforms"], f["hashtags"])'Short-form 9:16 YouTube long-form 16:9 Where it goes four platforms YouTube alone Files needed video.mp4; optional requested YouTube SRTvideo.mp4·chapters.txt; optional requested YouTube SRTHashtags 3–5 including #Shortsno #Shorts— it gets misfiled as a shortFirst publish upload private, set the portrait frame, then public (rule 9) upload private, check, then public (§3-2) Portrait surface oar*required (rule 9)n/a — for landscape the thumbnail is the surface Every format uses the clean master. When requested, YouTube receives a separate
subs.srttrack that viewers can enable or disable.Confirm the episode is actually finished, not just marked finished.
REF=${CLAUDE_PLUGIN_ROOT}/skills/autoproduce/references node $REF/episode-state.js . # from the episode directory · exit 1 = a delivery blockerThe check also verifies
output/video/delivery-proof.jsonagainst the delivered files and source storyboard. A separate export without current assembly provenance must be rebuilt through the common builder and final pace pass. For a large final speech payload, the embedded chapter manifest must cover every original audio sample and every chapter join. The same preflight rejects a missing/unverified chapter, boundary gap, changed ordering or stale audio/script; a source scene PASS cannot replace this proof.Exit 1 here means the directory promised something it never delivered — a video with no per-platform text, a
queue_*: readymarker with no video behind it, anoutput/still holding the pre-pass build (the pace pass in produce §7.5 writes the── speedup x…line tooutput/video/build-report.txt, then aPASS final speech rateline for a requested sidecar or thesubtitle-based rate check skippedmarker without one — the default, subtitle-free delivery). These blockers are technical, not quality verdicts. Publishing is the irreversible step, so resolve or explain one to the user before §1 rather than working around it.- Confirm the
output/artifacts exist and thatstoryboard.mdsaysstatus: produced— otherwise point them at/social-flow:producefirst. Both formats requireoutput/video/video.mp4, the subtitle-free master. A missing burned-in copy or SRT is not a defect. Include an SRT only for a requested separate YouTube track; retain other historical assets without selecting them. - Call
sns_account_checkwithchannel: <channel-slug>to check this channel's per-platform token status — confirm and report which account is publishing from that result (the account comes from that channel token's /me, not from a setting). If the account name in the check doesn't match the channel brand, stop publishing and confirm with the user. Drop invalid platforms from the target list and report why. If the publish tool isn't in ListTools at all, the token file isn't set up — walk them through the channel directory (~/.config/social-flow/<channel-slug>/) procedure inreferences/token-setup.md. Meta tokens are the 60-day refreshing kind — recommend a refresh once the file is over 45 days old.
1. HITL approval gate (before hosting — unapproved content doesn't go on a public URL)
Run the style checker once per surface before approval (Bash, not an LLM call) — after publishing you can't swap the IG image or video, so this is the last chance.
CS=${CLAUDE_PLUGIN_ROOT}/skills/platform-guide/references/check-style.py
python3 "$CS" --selftest >/dev/null 2>&1 \
|| echo "gate_exit=3 (checker missing, corrupted, or rules red — every result below is unverified)"
for P in threads:output/threads/post.md ig:output/instagram/caption.md \
fb:output/facebook/post.md yt:output/youtube/meta.md; do
python3 $CS --surface ${P%%:*} ${P#*:}; echo "[${P%%:*}] gate_exit=$?"
done
node ${CLAUDE_PLUGIN_ROOT}/skills/platform-guide/references/check-meta.js output/youtube/meta.md; echo "[meta] exit=$?"
# Don't append | head to shorten the output — $? becomes that command's and a FAIL reads as 0check-meta.js exit 2 (a meta.md outside the playbook §6 layout, an angle bracket in
the title, a preset hashtag missing, or COMPREHENSION.answer copied into the title or
description) and exit 1 both go into the approval prompt as findings for the user to
weigh — but only when the output ends with a CHECK_META: exit=… line. A broken
install exits 1 too, with a node stack and no tail, and that is unchecked, not one
warning.
On exit 4 the copy isn't Korean and the checker declined to judge it — that surface is unchecked, not clean. Say so in the approval prompt by name, so the person approving knows they are the only reader it got.
Exit 2 (S1) and exit 1 (warning) both go into the approval prompt as suggestions, with
the flagged lines quoted, and the user judges; a style finding never sends the copy back
to produce on its own. A fix the user wants is applied and the copy re-presented (rule 4).
If the header line has quote-exempt N, list those N separately in the prompt —
they're violations the checker excluded from the verdict without knowing whether the
source is genuine, so say whether each one quotes a real source or is our own sentence
in quotes. exit 3 means the checker didn't run — fix the path and run it again, and
if you can't, state "style unverified" in the prompt. If the checker file itself is
missing, python returns 2, so the existence check line above tells them apart.
Present the final version for each platform with AskUserQuestion — the full body/caption,
the media local paths (every platform gets video.mp4; YouTube gets a separate
SRT only when requested), the hashtags, the FB first-comment copy, all of it; the account it
will publish to (from the account check); and the style-check results (per-surface exit
and score, number of quote exemptions). Options: [publish everything / some platforms
only / revise and re-present / stop]. Passing this gate is the approval to actually
publish. If they ask for changes, apply them and gate again.
Threads carries the video on the post (§2 table), so there is no link placeholder.
Only a fallback episode that can't carry a video uses a reply link, and there the link
slot is still the <IG_REELS_URL> placeholder at this point (IG has to be published
first for the permalink to exist). Write "the IG reels permalink goes here" into the
prompt, and filling in that slot alone in §3 stays inside the approval — it isn't
editing a sentence, so rule 4's re-approval doesn't apply.
2. Getting public media URLs (required for IG and Threads · FB images/video)
The imageUrl/videoUrl on the publish tools must be a publicly reachable HTTPS
URL — the platform crawls it, so local paths and authenticated URLs won't do
(YouTube is the one exception: it uploads the local file, so no hosting needed).
Subtitle files are passed as local paths on every platform — they aren't hosted.
Use the same subtitle-free master on every platform.
| Platform | Video | Separate subtitles |
| YouTube | video.mp4 local path | Only when requested: captionFilePath or captionTracks |
video.mp4 public URL | Omit caption inputs unless separately requested | |
video.mp4 public URL | None; do not burn subtitles into the video | |
| Threads | video.mp4 public URL | None; do not burn subtitles into the video |
Host only the selected clean master and verify its public URL. Never choose a legacy
video-sub.mp4 because it happens to exist. Caption text in an Instagram/Threads post
is platform copy, not permission to draw subtitles into the video.
- Use the hosting method named in profile.md §4. If none is set, hold the publish and ask the user which hosting to use.
- If you used a temporary tunnel (
python3 -m http.server+cloudflared tunnel): (1) serve from a dedicated directory holding only the publish files; (2) after publishing, kill both processes and report completion only afterpgrep -fl "cloudflared|http.server"prints nothing; (3) run the same command at the start of the session to check for a leftover tunnel from an earlier one. - Check 200 and MIME with
curl -sI <URL>before publishing. - If the hosted file differs from the file approved in §1, or the URL changes after approval (a tunnel restart, say), go through the §1 gate again.
3. Publish (approved platforms only)
Put channel: <channel-slug> on every publish and comment tool call below (rule 7).
There is no ordering constraint — now that Threads carries its own video, the dependency on the IG permalink is gone. Publish in any order.
Only the fallback (an episode that can't carry a video) puts IG reels before Threads, because the link in the Threads reply is then the IG permalink. If the IG publish fails there, hold Threads and ask the user for a replacement link (the YouTube permalink, say).
YouTube:
youtube_publish— title/description fromoutput/youtube/meta.md,videoFilePath=output/video/video.mp4 (the clean copy),thumbnailFilePath=cover.jpg required (leave it out and a random frame becomes the thumbnail). Only for requested separate YouTube subtitles, passcaptionFilePath=output/video/subs.srt; for requested multiple languages passcaptionTracksinstead, listing the requestedsubs.<lang>.srtwith its BCP-47 code ([{filePath: …/subs.srt, language: "ko"}, {filePath: …/subs.en.srt, language: "en"}, …]; the two arguments are mutually exclusive). AthumbnailWarningorcaptionWarningmeans the publish succeeded — report what the warning says. Subtitle upload needs theyoutube.force-sslscope (the publish-onlyyoutube.uploadgets rejected). With a publish-only token, a scope error on the first call is normal, and at that point the video is already up, so don't republish — reissue the token and upload just the subtitles separately (references/token-setup.md). The quota differs too — the upload is 1 unit but the subtitles are 400 per track (three languages = 1,200), so upload each language once per episode.captionLanguagesin the response says which tracks went up, and a per-languagecaptionWarning("en: …") means only that language failed — upload just it in YouTube Studio. No standalone existing-video caption-upload MCP tool is exposed here; never call youtube_publish again merely to add or repair a track. Don't setcontainsSyntheticMedia(it defaults to true). This pipeline uses generated video and Lyria music, so it falls under AI disclosure, and YouTube states that disclosure doesn't affect reach or monetization eligibility while warning that habitual non-disclosure brings forced labels, removal, and YPP suspension. Turning it off requires the user to confirm an exemption reason (the exemption list: script/title/thumbnail/subtitle/idea generation, cloning your own voice, non-realistic animation, color grading and filters). The YouTube publish isn't done until the shorts portrait first frame is set (absolute rule 9 — not skippable). WhatthumbnailFilePathchanges is only the landscape surface (search results, share previews, embeds); the portrait frame used by the shorts feed and the channel shorts tab is separate. Leave it unset and a random mid-video frame becomes the shorts first frame.Set it in a browser while the video is still private —
studio.youtube.com/video/<id>/edit→ the⋮over the portrait image in the thumbnail box → "Select from video" → the cover frame → Done → Save. Drive it with ego lite (the only browser lane — no Chrome MCP fallback).Changein that same menu is file upload and only swaps the landscape surface. The emulator route inreferences/shorts-surface-adb.mdis the fallback for when the picker is missing or the cover isn't among the candidates.The target frame is the moment the cover is complete — use the timestamp where
producepulledcover.jpg(the cover transition completion time inbuild-report.txt; around 6 seconds in this pipeline). At the frame picker's first slot (t=0) the hero stat isn't up yet.The verdict is not
oardefault.jpg— it stays 404 after the frame is set, andoar2.jpgserves a cached older frame with a 200. Read the tile'simgsrc from the channel shorts tab DOM (youtube.com/@<handle>/shorts), download it, and confirm by eye that it's the cover. This only works after the video is public, so the order is: set the frame while private → flip to public → check the shorts tab.If a login is needed, ask the user right then — don't defer it as an open item and report completion. Only if the user explicitly says they'll do it later do you record it as open, along with the videoId.
- Instagram:
instagram_publish—videoUrlis the public URL of the clean subtitle-free master (video.mp4) + platform caption text. No subtitles are baked into the picture. Threads: before calling, record the actual §1 human approval in
data/<channel>/episodes/<topic>/threads-publish-approval.json. Use JSON withapproved: true, the exact approvedcaption, and every suppliedimageUrl,videoUrl,linkUrl,selfReply, orreplyToId. Omit unused fields. Write this record only after approval, and update it only after approval for changed copy or media. This local record is an attestation, not server authentication of the human. The server requires the real episode directory,storyboard/scenes.js, and an approval record matching every publishing field.Call
threads_publish(channel, episodeRef: <topic>, ...)— the approved body (no link) ascaptionand the public URL of the clean subtitle-free master (video.mp4) asvideoUrl. One call finishes it — no reply. The video container transcodes, so the tool waits up to 2 minutes for FINISHED.No link in the body, same as before — the first two link-in-body posts got 9 and 21 views, a tenth of the link-free posts from the same day (measured 2026-08-15). Carrying the video on the post means nothing links out, which sidesteps this.
Fallback — only when there's no video file or hosting is blocked: publish the body plus the approved IG permalink
selfReplyin onethreads_publishcall withepisodeRef. The approval record must contain both the exactcaptionandselfReply(one line like "full video here →" plus the link). The server posts the reply only after the root succeeds. A self-reply failure returns the root ID and a warning: record the partial result and do not republish the root. Threads is complete only when both posts succeed.- Facebook:
facebook_publish—videoUrlis the public URL of the clean copy (video.mp4). Omit subtitle parameters by default. Only if separate Facebook subtitles were also requested, usecaptionFilePath=output/video/subs.srt orcaptionFilesfor requested languages, default locale first:[{filePath: …/subs.srt, locale: "ko_KR"}, {filePath: …/subs.en.srt, locale: "en_US"}, …]; FB locales are the underscore form, not BCP-47. Include the body → then publish the first comment (source/related link) immediately withfacebook_commentusing the postId from the response. AcaptionWarningdoesn't invalidate the publish — FB processes video asynchronously, so it may have caught it mid-processing. Don't republish; upload just the subtitles again a moment later (the postId in the response is the video_id).
3-2. Long-form (youtube-long-16x9) — upload private, check, then go public
For landscape long-form, step 1 above is called differently. The other platforms
aren't touched at all (preset platforms: ["youtube"]).
- Call
youtube_publishwithprivacyStatus: "private".videoFilePath=video.mp4(clean) ·thumbnailFilePath=cover.jpg· Only when YouTube subtitles are requested, addcaptionFilePath=subs.srtorcaptionTracksfor the requested languages; otherwise omit them.caption(the description) carries the chapter timestamps —output/video/chapters.txtis that list, built from the builder's measured times, with00:00as the first line and three or more entries. Add hashtags if you like, but never#Shorts. - Open
https://www.youtube.com/watch?v=<videoId>and look at four things — (1) has the quality come up to the final resolution (if it's still processing, wait), (2) if requested, does the separate subtitle track turn on, (3) do the description's chapters draw as segments on the scrubber, (4) is the thumbnail our cover. - Once all four pass, flip it to
privacyStatus: "public"withyoutube_update. On a video you're touching for the first time, call it withdryRun: truefirst and checkwouldSend— this tool overwrites rather than patching, so it re-sends the fields you didn't pass at their current values. - If any one of them fails, tell the user while it's still private — that stage exists so the fix lands before anyone has seen it. Don't re-upload; re-uploading restarts the view count at zero. A subtitle track can still be replaced later without touching anything else (§2 table), but a description or thumbnail change once the video is public falls under rule 11.
Sometimes the chapters don't draw. Break the requirements (first line 0:00 · three or
more · at least 10 seconds apart) and our list is ignored in favor of auto chapters —
the builder checks those three first, so a chapters.txt that exists has passed them;
if they still don't show, check whether the lines broke when they were copied into the
description.
3-3. Multi-language audio (dubbing) — what the API can and cannot do
The Data API has no endpoint for extra audio tracks — captions.insert covers
subtitles only, so this pipeline cannot upload dubs. Two real paths:
- YouTube auto-dubbing — on channels where YouTube has enabled the multi-language feature (https://support.google.com/youtube/answer/13338784) YouTube generates dubbed audio itself. Subtitle tracks the user requested and this skill uploaded seed that translation quality; a track is never added for this reason without the request.
- Custom dub tracks go up by hand in YouTube Studio (up to 30 languages; an audio-only file about the length of the video). The long-form private stage (§3-2) is the moment — attach the tracks while the video is private, then flip public.
Neither blocks a publish. When the user asks about multi-language reach, report these two paths along with the multi-language subtitle tracks that did go up.
Handling failures: report the error as it came and don't blindly retry the same call (the publish APIs aren't idempotent — after a timeout, check for a duplicate first via the permalink or a recent-media query). If only the FB first comment failed, retry just the comment call; if it keeps failing, write down the postId and the reason and explain the manual fix. Threads is a single call, so it has no partial failure — if it failed, nothing was published, so check the permalink before calling again.
4. Completion checklist (don't say "published" until every box is ticked)
A 200 from the API is not completion. Walk this list, and if anything is open, name the open item and say what's left. Reporting completion with work still outstanding is the most frequent failure in this skill.
- YouTube — the landscape thumbnail is our cover. Download
i.ytimg.com/vi/<videoId>/maxresdefault.jpgand compare it tocover.jpgby eye. AthumbnailWarningmeans it did not attach — don't just report the warning and move on. - YouTube — the portrait first frame is set. Web Studio frame picker (rule 9).
thumbnailFilePathdoes not change this. Skip it and a mid-sentence expression is the first thing the shorts feed shows. - YouTube — the portrait surface was checked after going public. Read the tile's
imgsrc from the channel shorts tab DOM and confirm it's the cover (notoardefault.jpg). - YouTube — private actually became public. Read
privacyStatusback from theyoutube_updateresponse. Don't flip it while anything above is still open. - YouTube — every requested subtitle track attached (skip when none requested). No
captionWarning,captionLanguagesin the response lists every language you passed, and CC shows on the watch page. - Threads — the video is on the post (rule 6). Open the permalink and confirm it plays. A body-only post is incomplete.
- Facebook — the first comment is up (rule 6).
- Instagram — a permalink came back. A 200 on the container is not a publish.
- Any temporary tunnel is torn down —
pgrep -fl "cloudflared|http.server"prints nothing. - The metadata is final (rule 11). Every box above is ticked, so nothing further gets edited on this video. If a growth loop is watching it, note here when the watch started.
Anything that needs the user (a login, say) gets asked for right then. Don't defer it and report the rest as done.
5. Record and wrap up
When you reply to comments after publishing (sns_comment_reply), check the copy
first as well. For THREADS, create a surface: "reply" draft with its parent
comment ID as replyToId (threads_draft_create returns draftId + bodyHash), then
pass that draftId with channel to sns_comment_reply. The server verifies only that
the posted body hashes to the draft's hash, the channel matches and the reply target
matches; the draft needs no review. threads_review_submit records reviews as hash-bound
evidence and never blocks, and check-style.py findings come back as suggestions.
The episode fallback self-reply above is already handled in its approved call.
A reply is person-to-person talk, which is where AI phrasing gets
spotted fastest — golden-hour response isn't a reason to skip the check.
printf '%s\n' "$reply_copy" | \
python3 ${CLAUDE_PLUGIN_ROOT}/skills/platform-guide/references/check-style.py --surface reply -Use a growth-post-reviewer read when it helps resolve wording or context concerns. Keep the original comment and post body available. A score or fixed review-round count does not decide whether to send; the actual reply authorization and runtime inputs do. Record unresolved findings without claiming a passing review.
- Record in
data/<channel>/episodes/<topic>/output/publish-log.md: a table of timestamp, platform, post id, permalink, caption summary, and the approver's decision. Write the §4 checklist beside it, item by item, as O/X — an open item has to survive in the log for the next person to finish it. - A change someone wants after publishing goes into the log, not into the video (rule 11) — write what they asked for and why under the episode's row, and the next episode carries the fix. Comment replies are the exception; they're the post-publish work, and the copy check above covers them.
- Update
storyboard.mdtostatus: published. - If you used a temporary tunnel, verify the teardown per §1, then give the final report as a platform/permalink table.
Additional Resources
Reference Files
references/token-setup.md— per-platform credential issuance, file conventions, refresh procedures (Threads/IG 60 days · FB indefinite · YouTube OAuth)references/shorts-surface-adb.md— setting the YouTube shorts portrait surface (oar*) frame: the channel-dedicated AVD + adb procedure, its traps, and the oardefault verdict
Project SNS preferences
Read the episode settings snapshot when present. Its sns records identify target
platforms and the expected account IDs; explicit current publishing instructions
still govern scope. Recheck live identity with sns_account_check before upload.
Resolve an account-ID mismatch with the user before publishing to another account.
Credentials stay in the external channel token directory; credentialRef never
contains a token. After verification, refresh the workspace SNS record through
project_manage update if it targets this channel, preserving other platforms and
all existing episode snapshots. Pending connection state does not authorize upload.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

