Skip to content

feat(post-publisher): publish to seven social platforms via official APIs - #1

Merged
Mikefluff merged 2 commits into
mainfrom
feat/post-publisher
Aug 3, 2026
Merged

Mikefluff merged 2 commits into
mainfrom
feat/post-publisher

Conversation

@Mikefluff

Copy link
Copy Markdown
Owner

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, and docs/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 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(), 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; --yes is what leaves it. That inverts the convention elsewhere in this repo, where --yes skips the cost prompt. The asymmetry is the point: a wasted generation costs cents, a wasted publish costs an audience. Each platform confirms separately.

Platform Draft The thing that bites
telegram — Caption on media caps at 1024 (text gets 4096). Only one testable end-to-end without OAuth.
threads container 500 chars. Text-only needs no S3.
instagram container Business/Creator + S3 mandatory. Video publishes as a Reel. 25/24h.
tiktok inbox Direct posting needs an audited app.
x — 280 chars, ~17/24h free tier. Threads publish incrementally.
youtube private 1600 quota units/upload of 10000/day ≈ 6, drafts included.
linkedin — Member posts only; company pages are partner-gated.

Four decisions worth reviewing

  • TikTok defaults to the inbox, not to publishing. An unaudited app has every direct post 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. Business/Creator still required; there is no API path to a personal account and the docs say so rather than implying one.
  • The browser fallback is instructions, not code. A Playwright robot holding social session cookies would be worse than the problem it solves, and selectors rot on every redesign. It drives Claude in Chrome against the user's own logged-in browser and stores nothing.
  • tokens.py is a second store, not an extension of keysfile.py. Merging them would have put expired social tokens in os.environ at every runner startup for all sixteen generation providers.

posted.json receipts 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:

  1. YouTube discarded an explicit --title whenever the caption was empty — (title or first_line) if text else "Untitled" binds the wrong way.
  2. TikTok declared a 20 MB chunk for a 6 MB file; TikTok rejects a chunk larger than the file.
  3. Threads was permanently unrefreshable — tokens.py hardcoded "instagram" as the one platform allowed to renew without a refresh token, and Threads renews identically.
  4. The OAuth listener handled exactly one request, so a browser's /favicon.ico could consume it instead of the callback.
  5. A partial-alt-text warning the documentation described and the code did not implement.

Also: scripts/validate.sh only resolved same-skill references/ links, so any cross-skill reference read as broken.

Known gap, not addressed here

package.json files and Dockerfile both ship the same 17-skill v1.x prose subset, so the npm and Docker artifacts are missing 25 skills — carousel-builder, reel-builder, skills-keys and now post-publisher among them, while skills.json advertises 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. shellcheck clean on the file touched.

🤖 Generated with Claude Code

https://claude.ai/code/session_01DUcstDF74VfBLh2e2g8g3i

Mikefluff and others added 2 commits August 3, 2026 12:23
…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
@Mikefluff
Mikefluff merged commit 4a66c70 into main Aug 3, 2026
3 checks passed
@Mikefluff
Mikefluff deleted the feat/post-publisher branch August 3, 2026 15:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant