feat(post-publisher): publish to seven social platforms via official APIs - #1
Merged
Merged
Conversation
…APIs The collection stopped at ./generated/ and three files said so on purpose. That boundary was defensible when drawn and had stopped being: the OAuth flows differ, but they differ in ways one adapter layer absorbs. common/runners/publishers/ is a sibling of providers/, not a subclass. A provider turns a prompt into bytes, costs money, and retries for free; a publisher takes bytes that exist and does something irreversible. So there is no estimate_cost() here, and in its place preflight() — concrete rather than abstract, so a subclass cannot skip the generic checks by forgetting super(). Dry-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. Platforms: telegram, threads, instagram, tiktok, x, youtube, linkedin. - TikTok defaults to the inbox. Direct posting needs an audited app; without one every post is forced to SELF_ONLY while the API reports success — the only failure mode in the set that looks like a success. - Instagram uses Instagram Login, not Facebook Login for Business, so no linked Page is required. Business/Creator still is; there is no API path to a personal account and the docs say so instead of implying one. - The browser fallback is instructions for Claude in Chrome, not code. A Playwright robot holding social session cookies would be worse than the problem it solves, and selectors rot on every redesign. - Meta drafts are genuinely two-step via --publish-container. tokens.py is a second store rather than an extension of keysfile.py: app credentials are long-lived and pasted, user tokens expire in hours. Merging them would have put expired social tokens in os.environ at every runner startup. Tokens are never loaded into the environment. posted.json receipts block a repeat publish, keyed on (platform, content hash) and on state — a draft does not block publishing it, which is the whole point of staging one. 144 new unit tests, none touching the network. Five pin bugs a review pass found: YouTube discarding an explicit --title on an empty caption (precedence), a TikTok chunk plan declaring a chunk larger than the file, Threads permanently unrefreshable via a hardcoded platform name in tokens.py, a one-shot OAuth listener a favicon request could consume, and a partial-alt warning the docs described but the code lacked. Also: scripts/validate.sh only resolved same-skill references/ links, so any cross-skill reference read as broken. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DUcstDF74VfBLh2e2g8g3i
Caught while deciding whether the release was safe to cut. Tagging v*.*.* triggers the Docker build, and the image would have gone out without post-publisher — along with 24 other skills. Dockerfile and package.json both listed the same seventeen directories: exactly the v1.x prose set, frozen since around v2.3 while twenty-five skills were added around them. skills.json advertised all forty-two, so install.sh running inside the container warned about twenty-five missing skills — inside the artifact meant to contain them. The Dockerfile's own header claimed it ships "all skill markdown", which had stopped being true. The cost was never size. These are markdown directories; the rebuilt image is 117 MB and the skills contribute almost nothing to that. The subset was drift, not a decision. Both lists regenerated from skills.json, and check-docs-consistency.sh gained gate 7 to compare them against it. That gate is the actual fix — the lists drifted for eighteen releases precisely because nothing compared them to anything. Verified it fails by removing a skill from each and watching it name both. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DUcstDF74VfBLh2e2g8g3i
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes the last mile. Every skill in the collection stopped at
./generated/; this one sends what is there and records that it did.Why now
The boundary was explicit in three places —
carousel-builder/SKILL.md:30,reel-builder/SKILL.md, anddocs/walkthroughs/research-to-carousel-reel.md:203("a deliberate boundary — each platform's API has different OAuth flows"). It was defensible when drawn and had stopped being: the flows differ, but they differ in ways one adapter layer absorbs.Shape
common/runners/publishers/is a sibling ofproviders/, not a subclass. A provider turns a prompt into bytes, costs money, and retries for free; a publisher takes bytes that exist and does something irreversible. So there is noestimate_cost(), and in its placepreflight()— concrete rather than abstract, so a subclass cannot skip the generic checks by forgettingsuper().Dry-run is the default;
--yesis what leaves it. That inverts the convention elsewhere in this repo, where--yesskips the cost prompt. The asymmetry is the point: a wasted generation costs cents, a wasted publish costs an audience. Each platform confirms separately.telegramthreadsinstagramtiktokxyoutubeprivatelinkedinFour decisions worth reviewing
SELF_ONLYwhile the API reports success — the only failure mode in the set that looks like a success.tokens.pyis a second store, not an extension ofkeysfile.py. Merging them would have put expired social tokens inos.environat every runner startup for all sixteen generation providers.posted.jsonreceipts block a repeat publish, keyed on (platform, content hash) and state — a draft does not block publishing it, which is the entire point of staging one.Bugs a review pass caught
144 new unit tests, none touching the network. Five pin defects found before merge:
--titlewhenever the caption was empty —(title or first_line) if text else "Untitled"binds the wrong way.tokens.pyhardcoded"instagram"as the one platform allowed to renew without a refresh token, and Threads renews identically./favicon.icocould consume it instead of the callback.Also:
scripts/validate.shonly resolved same-skillreferences/links, so any cross-skill reference read as broken.Known gap, not addressed here
package.jsonfilesandDockerfileboth ship the same 17-skill v1.x prose subset, so the npm and Docker artifacts are missing 25 skills —carousel-builder,reel-builder,skills-keysand nowpost-publisheramong them, whileskills.jsonadvertises all 42. Pre-existing since ~v2.3 and a distribution decision rather than a bug fix, so it is flagged rather than changed.Gates
make validate·make smoke(12/12) ·make test-unit(224) ·make check-docs— all green.shellcheckclean on the file touched.🤖 Generated with Claude Code
https://claude.ai/code/session_01DUcstDF74VfBLh2e2g8g3i