Codex runtime
The bundled recent-project collector reads Claude history. In Codex, use the
current repository's git history and projects named by the user when it finds
nothing; do not interpret absent Claude history as no recent work. Optional
claude -p/Anthropic judge scripts still require their own CLI or API credentials.
External judges are optional diagnostics. The mandatory review in
references/post-review.md runs in this session on either host; unavailable or
mock judges never count as a passing review. Never claim an external score.
When running in Codex, invoke this skill as $ghostwriter-x. Resolve scripts, assets,
and references from the directory containing this SKILL.md, regardless of the
current working directory. Existing ~/.claude/ personal-data paths remain valid
and are still used by the bundled scripts; they do not require Claude to run.
Map Read/Write/Edit/Bash to the available file and shell tools, and
WebSearch/WebFetch to available web tools. For AskUserQuestion, use an
available question tool or a concise chat question; wait for answers that gate
action. Use Codex's delegation tools for required subagents when available;
otherwise disclose that independent execution is unavailable. Discover connected
apps by capability rather than assuming Claude MCP tool names exist.
X Ghostwriter
In Claude Code, load /press; in Codex, load $press; then follow the shared PRESS terminal/UI contract from brand/agent-ui.md. Do not copy or override that contract here.
Draft X posts and threads that sound like the user, then publish to their own account
through the Typefully API (free plan — X's own API has no free tier anymore) — only
after they approve the draft. Never auto-publish.
The repo root is the directory containing this skill's scripts/, voice/, and drafts/
folders. All commands below are run from that repo root.
Personal data lives in ~/.claude/ghostwriter-x/, not the repo. The voice profile
(voice/voice-profile.md, voice-notes.md, interests.md), the brand guide
(assets/diagram.css), and the Typefully credentials (.env) are read from
~/.claude/ghostwriter-x/{voice,assets,.env} — the same location whether the skill is running
from this repo, an installed Claude Code plugin, or Claude Desktop, so editing your voice or
brand once is visible everywhere. voice/algorithm.md (X reach tuning) stays bundled in the
repo — it's shipped, identical content, not personal. data/, drafts/, images/, scripts/
also stay repo-local since they're tied to running the actual publish flow from one place.
Sibling skill: the LinkedIn ghostwriter skill shares this architecture but not this
platform. If the user asks for a LinkedIn post, that skill handles it, not this one.
Decide which mode you're in
- Setup —
~/.claude/ghostwriter-x/.env has no TYPEFULLY_API_KEY, or
~/.claude/ghostwriter-x/voice/voice-profile.md is missing, or the user says "set up",
"configure", "connect my X account". → Run Setup.
- Generate — the user wants a post or thread (the common case). → Run Generate.
- Publish — the user approves a draft you already showed. → Run Publish.
- Check in — the user asks how a post or their posts did ("how did my posts do?", "how did
that thread go?"). → Run just the Outcome check-in at the top of Generate, record the
answer, and stop. Don't slide into drafting a new post unless they ask.
Before generating, quietly confirm setup is done: ~/.claude/ghostwriter-x/voice/voice-profile.md
exists and ~/.claude/ghostwriter-x/.env contains TYPEFULLY_API_KEY + TYPEFULLY_SOCIAL_SET_ID.
If not, switch to Setup.
Keep this invisible. Do the setup check (and any other bookkeeping — idea-board/radar
freshness, directory orientation) in as few, terse tool calls as possible: one chained
existence/content check, not a parade of separate Bash calls with printed section headers.
Skip exploratory commands that don't feed an immediate decision (a bare pwd, an ls "just to
look around"). The first thing the user should see is your one-sentence status line, not a
scroll of raw command output. This doesn't apply to the real research in Generate step 2 (the
HN check, radar read, recent_projects.py) — that work produces content the user actually sees
reflected in the menu.
Mode: Setup
Walk the user through this once. Do the steps you can; hand them the steps only they can do.
- Typefully account. Publishing goes through Typefully's API because X's own API has
no free tier (pay-per-use since Feb 2026). Ask them to sign up at
https://typefully.com, connect their X account in Typefully's UI, and create an
API key (Settings → API). The free plan covers 1 social set and ~15 posts/month —
plenty for a personal cadence; mention the cap if they post daily.
- .env. Run
mkdir -p ~/.claude/ghostwriter-x && cp .env.example ~/.claude/ghostwriter-x/.env,
then write their API key into ~/.claude/ghostwriter-x/.env as TYPEFULLY_API_KEY
(edit the file; never echo the key back in chat).
- Connect. Run
python3 scripts/typefully_post.py --connect — it fetches their
Typefully social sets and stores TYPEFULLY_SOCIAL_SET_ID in .env. One-time; no
OAuth dance, no token expiry.
- Voice corpus — pick whichever the user can do fastest:
- X archive (best). Request it from X (Settings → Your account → Download an
archive of your data; it can take a day to arrive). Drop the archive's
data/tweets.js
into data/, then run python3 scripts/extract_tweets.py and do the Voice Profile
step below on data/my_posts.md.
- Seed from the LinkedIn ghostwriter (fast start). If
~/.claude/ghostwriter/voice/voice-profile.md exists, offer to derive the X profile from
it: keep the voice DNA (tone, vocabulary, never-do list) but shift the register for X —
shorter sentences, hook-first, no corporate polish, fragments welcome. Show the user the
derived profile and confirm before saving. Mark it as seeded so a later archive import
can replace it.
- Interview (no data at all). Ask about tone, the 3–5 topics they're known for,
formatting habits (emoji? all-lowercase? threads or singles?), and what they never want
to sound like.
- Interests & voice notes. If they don't exist yet (e.g. a fresh clone), seed them from
the templates:
mkdir -p ~/.claude/ghostwriter-x/voice && cp voice/interests.example.md ~/.claude/ghostwriter-x/voice/interests.md and cp voice/voice-notes.example.md ~/.claude/ghostwriter-x/voice/voice-notes.md. Then help them fill in
~/.claude/ghostwriter-x/voice/interests.md (interview them if it's empty; if the LinkedIn
ghostwriter's interests.md exists, offer to copy it as the starting point — interests
usually transfer even when register doesn't). voice-notes.md ships with sensible
defaults; append the user's own feedback to it as it comes up.
Voice Profile (the heart of "sounds like me")
Read data/my_posts.md in full, then write ~/.claude/ghostwriter-x/voice/voice-profile.md
(mkdir -p ~/.claude/ghostwriter-x/voice first if it doesn't exist yet) capturing:
- Voice & tone — e.g. direct, contrarian, warm, wry. Quote 2–3 tweets that exemplify it.
- Sentence rhythm — one-liners? multi-clause? fragments for emphasis? all-lowercase?
- Openers — how do their best tweets hook in the first line? (bold claim, number, story
cold-open, question). List the patterns they actually use.
- Closers — do they end threads with a takeaway, a question, a link reply, nothing?
- Structure — singles vs threads; numbered threads (
3/7) or free-form; line breaks
within tweets.
- Vocabulary & tics — recurring phrases, signature words, how they swear or don't.
- Emoji & hashtags — none / sparing / heavy; which ones; where. (Most strong X accounts:
no hashtags.)
- Topics they own — the themes they return to.
- Never do — anti-patterns to avoid (engagement bait, "🧵👇", corporate buzzwords,
em-dash overuse, fake vulnerability, generic AI-slop phrasing). Be specific to this person.
Keep it concrete and example-driven — it's a generation guide, not an essay.
Mode: Generate
Draft display is gated. Before exposing post copy anywhere, complete
the mandatory post review. Topic menus may describe
angles and evidence, but cannot preview an unreviewed hook or post excerpt.
Posture: propose, don't interrogate. The default is you surface concrete, already-real
ideas and the user taps one — not a blank "what do you want to post about?" The picked idea is the
post's real anchor, so there's no generic interview.
Outcome check-in (max one, fast — the feedback loop). Before anything else, run
python3 scripts/post_outcome.py --check-due. It prints none, or the one record a check-in
is owed on. Do not hand-roll this from the log: the script picks the oldest unscored post at
least 2 days old, which is the whole point. (The pre-0.2.0 rule asked about the newest record,
so posting twice in one day left the newest record same-day forever, the gate never opened, and
ripe older posts were never offered — the loop sat dead through three published posts. If you
re-derive the rule by hand you will reintroduce that bug.)
If a record comes back, ask ONE check-in question — "How did '' do?" with options
great / normal / flopped (notes via "Other") — then record it:
python3 scripts/post_outcome.py --due --outcome <answer> --notes "<notes>". One dialog to
start: if the idea menu (step 2) is also due, the check-in and the menu ride in the SAME single
AskUserQuestion call — the check-in takes the first question slot and the flat idea question
(step 2) takes the second — still one dialog, one round trip, never two sequential question
dialogs to get a session moving. Only when no menu is due (the topic came in concrete)
may the check-in be its own question. Never ask more
than once per session; none → skip silently, don't mention it.
Posts older than 30 days are never asked about — nobody honestly remembers, and a guess is
worse than a gap. python3 scripts/post_outcome.py --retire-stale marks those unrecalled so the
backlog stops growing; run it when --check-due has been returning none for a while.
Using the outcomes — and the sample-size bar. This is a self-reported signal on a handful of
posts with no control group. Treat it as a record you can show the user, not a measurement you can
optimize against. The confounds here are unidentifiable, not merely noisy: every post varies
lane, hour, format, image and hook at once, so there are more free variables than observations,
and format is collinear with content by construction because the agent picks thread-vs-single
from the content's shape — "threads do better" may only mean "meatier topics do better".
- Fewer than 5 scored posts → display only. You may show the user their own numbers and use
them to avoid repeating a topic. Zero influence on ranking.
- ≥5 in an arm → you may say what you see, with the N attached ("3 of your 4 threads beat
your 2 singles, small sample"). No behaviour change.
- ≥8 in each of two arms AND a ≥2× gap → move a lane by one position, stating the N.
Never drop a lane entirely.
- Single vs thread is never outcome-driven. Content shape decides, always.
- Posting time is never outcome-driven. The per-hour sample will not exist for years.
- Compare medians, not means — one amplified post swamps a small account's average — and
ignore anything older than 90 days; X's ranking and the follower base both move.
Anti-ratchet — the loop's real hazard. Outcomes must never override
voice-notes.md, the substance bar, or the source gate. They sit below those in precedence,
exactly as algorithm.md does. A flopped on an honest, well-sourced post is not a reason to
change the voice; chasing a reach number on a personal account is precisely the pressure that
bends a technical voice toward the engagement bait this skill forbids. If the data ever seems to
argue for a banned tactic, the data loses.
This is the only free compliant performance signal available. Be precise about why: real
analytics are paywalled, not forbidden. Typefully's list_social_set_analytics_posts and
get_social_set_analytics_followers are official, COMPLIANCE-clean endpoints that return
403 MONETIZATION_ERROR on the free plan (verified 2026-07-27) — don't re-probe them every
session. If the user ever wants real numbers sooner, the zero-cost path is for them to read
their own x.com analytics and paste the figures in; the agent never fetches x.com, so that is not
scraping. Revisit the whole loop if the account upgrades.
Short-circuit if the topic is already concrete. If the user named a specific topic, pointed
you at a source, or said "draft a post from item N in the radar," skip the menu and go straight
to grounding + drafting (step 3). The menu below is the default only for an open-ended "write me
a post."
No topic given → ONE flat idea question, pick and go. Gather concrete, ready-to-write
ideas from the four lanes below yourself, then flatten them into a single ranked list
(lane priority order below, bent by outcome history) and present the top 3 as ONE
single-select AskUserQuestion — options are the 3 ideas plus a 4th, "Show more
ideas." Never ask one question per lane. Rules of the question:
- Every idea option carries a
preview (≤ ~9 lines so the pane never clips): a topic
summary rather than draft copy, the suggested angle in one sentence (assume a single
unless the user asked for a thread), and a source-freshness line prefixed with its
lane (e.g. Trending · HN 612 pts / 340 comments · Jul 18, Radar · Jul 17 · anthropic.com). A user should be able to pick on the preview alone.
- Picking a real idea goes straight to grounding + draft (step 3) — nothing else to answer
or dismiss. The auto "Other" on the question takes a typed topic directly (same
short-circuit as step 1).
- Picking "Show more ideas" asks exactly ONE follow-up single-select question with the
next batch (the remaining candidates, up to 3 + auto "Other"), same preview format.
- One provenance line total in chat, not per lane (radar date + job health, live-search
date, repo names) — don't dump a duplicate board into chat; the question options carry the
ideas.
- When the outcome check-in is due it rides as the first question in the SAME call (see
above); the flat idea question is the second. Still one dialog, one round trip.
The four lanes, in priority order (used to rank the flattened list, not to structure separate
questions):
- Trending now (live, run-day — VERIFIED trending, not vibes). "Trending" means you can
point at the surge, not that a web search returned articles; vendor blogs and SEO listicles
are not trending signals. Check measurable surfaces directly, TODAY: Hacker News via
python3 scripts/hn_trending.py (defaults to the last 2 days over 150 points; --days,
--min-points, --json if you need them). Use the script, not a hand-written curl — the
Algolia filter needs points>150,created_at_i>… percent-encoded, and in a shell the >
redirects to a file and the query silently returns nothing (this cost a wasted round trip on a
real run). Then top posts this week in the relevant subreddits, and news coverage from the last
~48 h (search with explicit recency). Filter through the trending areas in
~/.claude/ghostwriter-x/voice/interests.md, propose 2–3 topics, each with the specific
angle the user could own (a trending topic without their angle is just news), and put the
ACTUAL signal in the preview's source line. X moves faster than LinkedIn: a surge older
than ~24 h is usually already picked over — prefer today's signal, and say so when an item
is borderline stale. No citable signal → the item doesn't go in the lane; fewer real
trending items beat padded ones.
- Release radar — current through TODAY, not through the last digest. Read the newest
research/release-radar-*.md and, if it exists, the tail of research/.radar.log, and
state provenance in the board ("Jul 24 radar, job ran clean"). The log is only created once
the launchd job has run on this machine — absent means "job never ran here", which is not
the same as "job failed"; say which one you actually know and don't spend a second call
hunting for it. If the digest is older than today, top the lane
up: one quick live search for AI releases since the digest date — label digest items
radar · <date> and top-ups live · today. Reuse digest items' title + "suggested angle"
(already source-backed; the twice-weekly scripts/release_radar.sh job scans the broader AI
industry, not just Anthropic). Never add experience claims the digest didn't establish; the
digest's Discussion radar items feed opinion/hot-take slots the same way. Skip items
already published (check published.jsonl). Radar stale (>4 days) or missing → say so,
note whether the log shows the job failing, and run the lane fully live; if the job is broken
(e.g. exit 127 — usually the repo moved), offer to repair it: bash scripts/install_radar.sh
re-renders the launchd agent against the repo's current path.
- Interests & hot takes (1–3 entries). Read
~/.claude/ghostwriter-x/voice/interests.md —
core themes, the "Strong opinions" list, and the story bank — for specific angles not
covered recently (check published.jsonl and recent drafts). A strong uncovered story-bank
item beats a generic theme; hot takes are X's home turf, so weight this lane a notch higher
than the LinkedIn sibling does; label each interests · <theme or story>.
- Your recent Claude projects (2–3 entries). Run
python3 scripts/recent_projects.py and
take the top 2–3 repos with recent Claude Code sessions; for each, read the recent git log
- last session summary for the one real thing shipped (that's the anchor). Respect
~/.claude/ghostwriter-x/voice/interests.md → Off-limits: never surface or post anything
work-confidential; personal/OSS repos only.
Build the list fast and honestly. Gather all four lanes in parallel (the HN check, the
radar read + top-up, interests, recent_projects.py) so the question is the first thing the
user waits on. An idea appears in exactly ONE lane — highest-signal lane wins. Filter every
candidate against published.jsonl and recent drafts/ so nothing already covered
resurfaces. Rank the flattened list by lane priority and the outcome history, and say so in
the provenance line when it bends the order.
Persist the full list — research the user paid for doesn't evaporate. Whether or not it
was shown, write research/idea-board-YYYY-MM-DD.md: every idea gathered (not just the 3
surfaced) with its lane, signal, angle, and status. Status is one of published · <url>,
drafted · <slug>, or on deck — never a bare picked, which records an intention
rather than an outcome and goes stale the moment the session ends. (A real board carried three
items marked picked that were never drafted, so the next run could not tell covered ground
from abandoned ground.) Reconcile before you trust a board: an item is only published if
published.jsonl has it, and only drafted if the file is in drafts/; downgrade anything
else back to on deck. On the next open-ended run, read the newest board (≤7 days old) and
fold still-good on deck ideas back into the flattened ranking labeled on deck · <date> —
re-verify a trending idea's signal before reusing it, and drop anything that went stale.
After the pick: lock it in, zero extra dialogs. Echo a compact brief and go —
Locked in: <idea> · <lane>, then one line each for the angle, the real anchor, the form
(a single unless the user asked for a thread), and the sources you'll verify against. Then straight to
grounding + draft (step 3); no second drill.
Confirm the anchor, then draft. Every post still needs one concrete, real, first-person
anchor — the actual tool, a real number, a specific decision, a thing that actually happened
(see voice-notes.md → Substance bar + Authenticity). The menu pick normally is that anchor.
Only the personal-project lane sometimes needs a single sharp follow-up to nail the specific
detail — ask one AskUserQuestion, never a generic multi-question interview. Never
fabricate a detail to clear this bar. If there's genuinely no real anchor, say so rather than
shipping a generic post.
3a. Run it before you write about it — the highest-leverage step in this skill. A release
how-to assembled from documentation is a recap anyone could write. The same post built on a
real run is an artifact only this user has. Before drafting, spend the two or three calls to
check whether the thing is runnable from this session:
- Is the tool reachable?
uvx <pkg>@<version>, npx -y <pkg>@<version>, pipx, docker run, an already-installed binary, or an MCP tool the session has. Pin the exact version the
post is about, and the version before it when the post is about a change.
- Verify the mechanism against the real binary, not the docs. Run
--help, check that the
flags and defaults you're about to publish actually exist. Docs drift; a shipped post that
names a flag that isn't there is the most embarrassing failure mode this skill has.
- Measure a real before/after when there is a legitimate subject.
scripts/recent_projects.py
lists the user's own repos; running an analysis command over one turns "the default set grew"
into "9 errors became 138 on the same code." That number is the post.
- Read-only, always. Analysis and reporting commands only — linters,
--help, --dry-run,
--check, read queries. Never a command that writes, installs into, or mutates the user's
repos, and never anything in a work-confidential repo (interests.md → Off-limits).
- Capture what you ran to
images/<slug>.source.txt even when no card is planned; it is
the evidence behind the numbers and card_lint.py verifies cards against it.
- When it isn't runnable, say so and write generically — steps in the second person, no
invented results. A how-to that can't be run is still fine; a fabricated run is not.
3b. Whose run was it? — the disclosure rule. Measurements from step 3a were produced by
you, in this session, not by the user at their keyboard. First-person framing ("I ran…", "my
repo", "my CI never moved") is only allowed when both hold: the command ran against the
user's own machine or repos, and you state the provenance plainly at the approval step
("these are real measurements, but I ran them in this session — you didn't"). Let the user
decide; some will happily own it, some won't. Default to second person ("run this on your
repo") whenever you haven't disclosed it, and never use first person for a run on a machine
or repo that isn't theirs. Everything else in Authenticity still applies: no invented numbers,
ever.
Draft against the voice profile. Read ~/.claude/ghostwriter-x/voice/voice-notes.md,
~/.claude/ghostwriter-x/voice/voice-profile.md, AND voice/algorithm.md (bundled,
repo-relative) first, every time (voice-notes.md holds direct user feedback and takes
priority; algorithm.md is reach optimization and must never override voice). If a voice file
is missing — e.g. a fresh setup — copy voice/voice-notes.example.md to
~/.claude/ghostwriter-x/voice/voice-notes.md and proceed with what you have. Write to match
them — their openers, rhythm, emoji habits, thread style. If the optional skill_memory
tool is connected, follow local-memory.md for bounded,
privately opted-in X hashtag recall; otherwise retain the original source workflow.
Read Compose before polishing in references/post-review.md:
start from one supported observation and its value to the reader, then compare
a direct opening and a focused edit. Do not add a question, lesson, or framework
just to pursue engagement. Preserve warmth and material qualifications.
Apply the Engagement craft
rules below AND the reach rules in voice/algorithm.md. Format rules:
- A SINGLE tweet is the default form. Draft a thread only when the user asks for one.
Material with five beats is a signal to find the sharpest beat and cut the other four,
not to thread it; a punchy single is the house style, and the beats that don't survive
the cut are exactly what a card is for (step 8). When the user does ask for a thread:
complete-thought tweets, ≤7 by default, never a sentence split across tweets.
- Draft file format: one tweet, or thread tweets separated by lines containing only
---. This is exactly what scripts/typefully_post.py parses.
- Write each tweet to fit 280 weighted characters (URLs count 23, most emoji/CJK count
- — check as you go with
python3 scripts/x_len.py --file <draft> --thread; don't write
long and trim at the gate.
- No external links in tweet 1 — a needed link goes in the last reply tweet (see
voice/algorithm.md).
Never fabricate or exaggerate details that aren't true to the user's real experience —
authenticity over drama (see voice-notes.md).
Save the draft to drafts/ as YYYY-MM-DD-slug.md (ask the user for today's date if you
don't have it; do not invent one).
Research & fact-check — every external claim must be backed by ≥3 real, live sources (the post
is generated from sources). Do this after Save (you need the slug) and before showing the
draft. List every external/world claim the draft makes — a vendor shipped X, a research
finding, a statistic, a definition; anything about the outside world, not the user's own
first-person experience. For each, research it (WebSearch / firecrawl / WebFetch) and
actually read the source to confirm it supports the claim — a live URL is not enough, the
content has to back the statement. Prefer primary/authoritative sources (official docs,
release notes, the vendor's own announcement, standards bodies, reputable engineering writing);
skip SEO/hype blogs. Radar-lane posts: reuse the digest's source URLs. Then write a sidecar
drafts/YYYY-MM-DD-slug.sources.json pairing each claim to its URL(s) — every claim needs ≥1
source, and the post needs ≥3 distinct live source hosts overall — and run
python3 scripts/verify_sources.py --file drafts/YYYY-MM-DD-slug.md until it passes. The sources
live only in the sidecar; never put sources, links, or a "Sources" section in the post
body (links in tweet 1 also crush reach — see voice/algorithm.md; if the user wants the
link public, it becomes the final reply tweet at publish time, called out in the preview).
If a claim can't reach ≥3 reputable sources, cut it or don't ship the post — never fabricate
a citation or a fact.
- Pure first-person posts (no external claims — e.g. a personal story or hot take about
the user's own work) make no outside-world assertion. Write a sidecar declaring
{"external_claims": false, "claims": []}; the gate passes trivially. Be honest: if the
post mixes a real external claim into a personal story, it is not
external_claims:false.
- Narrate the gate — it's the slow step; never go silent through it. Emit one short
status line as claims resolve, without quoting unreviewed draft text. Close
with the result, for example
3 claims · 5 distinct hosts · gate passed.
- The narration floor applies to every slow stretch, not just this one. From the live run
(3a) through the source gate, the render cycles, and publishing, never go more than ~2 tool
calls without a user-facing line. Say what you're doing and what came back:
ran ruff 0.15.5 vs 0.16.0 on local-fitness → 9 errors became 138, render 2 failed clip-overflow at 688px, cutting a row. Silence during a long stretch reads as a hang, and it hides the
decisions the user would most want to interrupt.
- Re-verify on edit. The show→edit→re-show loop below can add a claim after the sidecar was
written. Whenever an edit adds or changes an external claim, re-run this step and update the
sidecar before publishing.
Complete the mandatory post review before showing any draft. Read
references/post-review.md and follow its full
rubric and private revision loop, using one fresh editor subagent when the host
supports delegation (otherwise label the in-session review honestly). Prepare drafts/<slug>.review.json with
scripts/post_review.py prepare, then complete every editorial check against
the user's current voice files, 2–3 real samples, and source evidence. Complete
both private comparisons (opening and compression), and justify every question's
purpose. A blanket pass or an overall score cannot replace these decisions.
The ending stops on the last real point; voice, naturalness, substance,
clarity, hook, credibility, restraint, originality and platform fit must also
pass. Resolve every warning with a specific contextual reason or rewrite it.
Re-run the gate after every edit; missing, stale, skipped, or mock reviews
block display. After at most three private revision rounds, report the blocker
without showing failed copy. Never open a failed draft or quote it in status
updates, idea previews, approval choices, or a final response.
Run python3 scripts/post_review.py check --file drafts/<slug>.md --show.
Only exit 0 permits display. No averaged score can overrule a failed check.
The first tweet must stand alone as the hook; every reply earns its place.
Fix what fails, then show the full draft in the X-true format — numbered tweets, each in
its own fenced block, each headed by its live weighted count in the form [n/N · used/280]
(from x_len.py, not estimated):
- A single post is
[1/1 · 243/280] + the tweet.
- One metadata line under the last block:
single|thread of N · save: <the thing a reader keeps> · lane: <lane> (+ link rides in final reply: <url> when applicable).
- Review line:
Review passed · voice, substance, clarity, credibility and platform checks.
- Re-shows lead with the delta: after any edit, the first line is
Changed: <one-line summary>, then the full draft in the same format — the user should never
re-read the whole thread hunting for the edit.
Then ask with a single AskUserQuestion carrying TWO questions in the one call — and wait
for the answer:
- Q1 the text — options Publish / Edit (the auto "Other" takes typed edit
instructions directly) / Scrap.
- Q2 the visual — the step-8 choice, asked here rather than in a second dialog, since the
recommendation and its ASCII previews are already derivable from the approved text. If the
user picks Edit or Scrap, the visual answer is simply discarded; that waste is
cheaper than a guaranteed extra round trip on every post.
- Add Q3 only when step 3a produced first-person measurements — the disclosure required by
3b, as its own question ("these are real numbers, but I ran them this session, not you: keep
the 'I ran…' framing, or switch to 'run this on your repo'?"). Never bury that in prose the
user might skim past.
The Publish tap immediately after seeing the exact full text is the explicit approval; an
edited draft is re-shown and re-asked the same way. Do not publish unprompted.
Any voice/style feedback the user gives — append it to
~/.claude/ghostwriter-x/voice/voice-notes.md in the same turn, BEFORE redrafting, and say
you did ("added to voice notes"). Fixing only the draft loses the correction and the user has
to repeat it next session. For privately opted-in writing-x.hashtags corrections,
the owner bridge in local-memory.md replaces this
manual append: never write both. Confirm the source save before dependent redrafting;
an attempted bridge failure must not silently fall back to appending.
The visual choice — asked in step 7's dialog, built only after the pick. This is Q2 of the
single approval call above, never a separate dialog: text-only / single card (name the
Press hero component you'd compose around, e.g. "a duel" or "a ledger") / image carousel (a
4-image post, or one image per tweet on a thread) — with your recommendation first, chosen from the
post's shape and the outcome history: how-to or anything technical → a single 16:9 card
carrying the steps the single tweet had to cut; personal story or hot take → text-only
(X is text-native; a strong text post beats a weak image); carousels only on a
user-requested thread. Give every option an
ASCII preview sketch of what THIS post would get: the card option sketches the actual
proposed Press composition as labeled blocks with this post's real headline; the carousel
option sketches the image strip (cover → point → point → recap, using this post's real
slide titles); text-only previews tweet 1 verbatim. Sketches are text in the question, not
builds — authoring still waits for the pick. Only after the pick do you author and render
(see Visuals); never render a form the user didn't choose. Cards are composed, not
templated: read assets/card-language.md, check images/card-history.jsonl, and differ
from the last 3 cards on ≥2 variation axes.
If the post is about the user's own agent, CLI, or code — any visual that would show
its output (a hero term, code, or claude card) — settle the output source in the
SAME single question, via the option descriptions: you capture it live (run their CLI /
call their MCP tool from this session), they paste or screenshot a real session, or —
only if neither is possible — compose from facts already in the draft. One question
total, never a second round-trip. See Real-output cards below.
How-to posts (technical, from AI releases)
The priority lane, and the one radar items feed directly. When the anchor is a recent AI release,
write a genuine how-to — not a news recap.
- Shape: a single tweet, with the steps on the card. The tweet carries the implication
(what the reader can now do) and the sharpest number; the concrete steps, real commands
and the gotcha go into the composed Press card, which is where a how-to earns its
bookmarks. Prescriptive, for the reader (voice-notes → Framing & audience). Only draft the
step-per-tweet thread version when the user explicitly asks for a thread.
- Real technical meat, accessible entry. Use real commands, real config, real names — a
curious non-expert can follow the tweet, an engineer still learns the mechanism from the
card. This is what earns bookmarks and quote-posts.
- Authenticity — how-to ≠ "I did this." A release how-to makes external/world claims, so it
is exactly the case the source gate is for: the
*.sources.json sidecar + verify_sources.py
step (step 6) is mandatory. Never fabricate or imply the user personally ran a release
they haven't — write the steps generically, not as a first-person story.
- Default visual: a composed Press card on tweet 1 — usually built around a ledger
(numbered steps + the real command in a
.cmdbar) or tiles. Compose it fresh per
assets/card-language.md and vary against images/card-history.jsonl.
Visuals (optional — diagrams & cards)
If the user chooses a graphic generated or edited with Codex imagegen, read
references/visual-composition.md to plan a picture-led explanation, then
references/visual-review.md and pass all 12 checks before presentation. Register
its origin, inspect the actual pixels, and run visual_review.py check before
showing it. The publisher enforces that review, including dry-run and draft-only
creation. This gate applies only to Codex imagegen assets; the native screenshot
and Claude/legacy render workflow below stays unchanged.
Only when the user opts in. Requires the diagram dependency (see README; if render_image.py
reports Playwright/Chromium is missing, point them at the install step and stop).
Brand guide (per-user). Styling + byline live in ~/.claude/ghostwriter-x/assets/diagram.css —
the user's personal brand guide, shared across every install of the skill. On first use, if it
doesn't exist, copy it from the template: mkdir -p ~/.claude/ghostwriter-x/assets && cp assets/diagram.css.example ~/.claude/ghostwriter-x/assets/diagram.css, then set their --byline,
their Press identity (--press-sig signature color + --stamp monogram initials), and tweak the
palette. Cards use <div class="footer brand"></div> to pull the byline automatically — don't
hardcode it. If the user already has a LinkedIn ghostwriter brand guide at
~/.claude/ghostwriter/assets/diagram.css, copy its personalization vars — same brand, new
geometry.
- The Press system (THE brand — default for every card). Editorial-poster identity: warm
paper canvas, huge black type, serif standfirst, ONE loud signature accent, heavy ink rules,
giant numerals, an issue-numbered masthead with the personal monogram stamp. Cards are
landscape 16:9 (1200×675) — X's timeline crop — and composed, not templated: read
assets/card-language.md (the component vocabulary, composition rules, and variation axes),
pick the 1–2 body components that prove the post's point (a duel proves a decision, a
ledger proves a method, a big stat proves a claim, a terminal proves it's real), and author
a bespoke images/<slug>.html. assets/card-template-press.html is one example composition,
not the shape. Landscape wants hero-left / support-right two-column arrangements, not
stacked bands. Anti-sameness contract: before authoring, read
images/card-history.jsonl and differ from the last 3 approved cards on ≥2 variation
axes (hero component, headline treatment, density, numeral presence, support texture);
after the user approves the render, append the card's fingerprint line to that file.
- Real-output cards (the fidelity contract). Whenever a card shows the output of the
user's own agent, tool, or code — a hero
term component, a code card, a claude
session card — the terminal content is a transcription of a real session, not an
invention. A round of "make it look like my actual agent" is a defect: get the ground
truth before authoring, not after the user complains.
- Capture first. In preference order: run it yourself (the user's CLI or MCP tool
is often reachable from this session — call it and capture real output); else take the
user's paste or screenshot (offered in the step-8 question). Save the raw capture —
transcribing a screenshot fa
…(truncated)
1---2name: ghostwriter-x3description: Write sharp X (Twitter) posts and threads in the user's own voice and publish them through the free Typefully API after they approve. Use when the user wants to draft, write, or post something to X or Twitter, asks for a "tweet", a "thread", or an "X post", or wants to set up X posting. Enforces X's 280-weighted-character limit per tweet, formats threads natively, and never publishes without explicit approval.4---56## Codex runtime78The bundled recent-project collector reads Claude history. In Codex, use the9current repository's git history and projects named by the user when it finds10nothing; do not interpret absent Claude history as no recent work. Optional11`claude -p`/Anthropic judge scripts still require their own CLI or API credentials.12External judges are optional diagnostics. The mandatory review in13`references/post-review.md` runs in this session on either host; unavailable or14mock judges never count as a passing review. Never claim an external score.1516When running in Codex, invoke this skill as `$ghostwriter-x`. Resolve scripts, assets,17and references from the directory containing this SKILL.md, regardless of the18current working directory. Existing `~/.claude/` personal-data paths remain valid19and are still used by the bundled scripts; they do not require Claude to run.20Map `Read`/`Write`/`Edit`/`Bash` to the available file and shell tools, and21`WebSearch`/`WebFetch` to available web tools. For `AskUserQuestion`, use an22available question tool or a concise chat question; wait for answers that gate23action. Use Codex's delegation tools for required subagents when available;24otherwise disclose that independent execution is unavailable. Discover connected25apps by capability rather than assuming Claude MCP tool names exist.2627# X Ghostwriter2829<!-- press:runtime -->30In Claude Code, load `/press`; in Codex, load `$press`; then follow the shared PRESS terminal/UI contract from `brand/agent-ui.md`. Do not copy or override that contract here.31<!-- press:runtime -->3233Draft X posts and threads that sound like the user, then publish to their own account34through the Typefully API (free plan — X's own API has no free tier anymore) — **only35after they approve the draft**. Never auto-publish.3637The repo root is the directory containing this skill's `scripts/`, `voice/`, and `drafts/`38folders. All commands below are run from that repo root.3940**Personal data lives in `~/.claude/ghostwriter-x/`, not the repo.** The voice profile41(`voice/voice-profile.md`, `voice-notes.md`, `interests.md`), the brand guide42(`assets/diagram.css`), and the Typefully credentials (`.env`) are read from43`~/.claude/ghostwriter-x/{voice,assets,.env}` — the same location whether the skill is running44from this repo, an installed Claude Code plugin, or Claude Desktop, so editing your voice or45brand once is visible everywhere. `voice/algorithm.md` (X reach tuning) stays bundled in the46repo — it's shipped, identical content, not personal. `data/`, `drafts/`, `images/`, `scripts/`47also stay repo-local since they're tied to running the actual publish flow from one place.4849**Sibling skill:** the LinkedIn `ghostwriter` skill shares this architecture but not this50platform. If the user asks for a LinkedIn post, that skill handles it, not this one.5152## Decide which mode you're in5354- **Setup** — `~/.claude/ghostwriter-x/.env` has no `TYPEFULLY_API_KEY`, or55 `~/.claude/ghostwriter-x/voice/voice-profile.md` is missing, or the user says "set up",56 "configure", "connect my X account". → Run **Setup**.57- **Generate** — the user wants a post or thread (the common case). → Run **Generate**.58- **Publish** — the user approves a draft you already showed. → Run **Publish**.59- **Check in** — the user asks how a post or their posts did ("how did my posts do?", "how did60 that thread go?"). → Run just the **Outcome check-in** at the top of Generate, record the61 answer, and stop. Don't slide into drafting a new post unless they ask.6263Before generating, quietly confirm setup is done: `~/.claude/ghostwriter-x/voice/voice-profile.md`64exists and `~/.claude/ghostwriter-x/.env` contains `TYPEFULLY_API_KEY` + `TYPEFULLY_SOCIAL_SET_ID`.65If not, switch to Setup.6667**Keep this invisible.** Do the setup check (and any other bookkeeping — idea-board/radar68freshness, directory orientation) in as few, terse tool calls as possible: one chained69existence/content check, not a parade of separate `Bash` calls with printed section headers.70Skip exploratory commands that don't feed an immediate decision (a bare `pwd`, an `ls` "just to71look around"). The first thing the user should see is your one-sentence status line, not a72scroll of raw command output. This doesn't apply to the real research in Generate step 2 (the73HN check, radar read, `recent_projects.py`) — that work produces content the user actually sees74reflected in the menu.7576---7778## Mode: Setup7980Walk the user through this once. Do the steps you can; hand them the steps only they can do.81821. **Typefully account.** Publishing goes through Typefully's API because X's own API has83 no free tier (pay-per-use since Feb 2026). Ask them to sign up at84 <https://typefully.com>, **connect their X account** in Typefully's UI, and create an85 API key (**Settings → API**). The free plan covers 1 social set and ~15 posts/month —86 plenty for a personal cadence; mention the cap if they post daily.872. **.env.** Run `mkdir -p ~/.claude/ghostwriter-x && cp .env.example ~/.claude/ghostwriter-x/.env`,88 then write their API key into `~/.claude/ghostwriter-x/.env` as `TYPEFULLY_API_KEY`89 (edit the file; never echo the key back in chat).903. **Connect.** Run `python3 scripts/typefully_post.py --connect` — it fetches their91 Typefully social sets and stores `TYPEFULLY_SOCIAL_SET_ID` in `.env`. One-time; no92 OAuth dance, no token expiry.934. **Voice corpus — pick whichever the user can do fastest:**94 - **X archive (best).** Request it from X (Settings → *Your account* → *Download an95 archive of your data*; it can take a day to arrive). Drop the archive's `data/tweets.js`96 into `data/`, then run `python3 scripts/extract_tweets.py` and do the **Voice Profile**97 step below on `data/my_posts.md`.98 - **Seed from the LinkedIn ghostwriter (fast start).** If99 `~/.claude/ghostwriter/voice/voice-profile.md` exists, offer to derive the X profile from100 it: keep the voice DNA (tone, vocabulary, never-do list) but shift the register for X —101 shorter sentences, hook-first, no corporate polish, fragments welcome. Show the user the102 derived profile and confirm before saving. Mark it as seeded so a later archive import103 can replace it.104 - **Interview (no data at all).** Ask about tone, the 3–5 topics they're known for,105 formatting habits (emoji? all-lowercase? threads or singles?), and what they never want106 to sound like.1075. **Interests & voice notes.** If they don't exist yet (e.g. a fresh clone), seed them from108 the templates: `mkdir -p ~/.claude/ghostwriter-x/voice && cp voice/interests.example.md109 ~/.claude/ghostwriter-x/voice/interests.md` and `cp voice/voice-notes.example.md110 ~/.claude/ghostwriter-x/voice/voice-notes.md`. Then help them fill in111 `~/.claude/ghostwriter-x/voice/interests.md` (interview them if it's empty; if the LinkedIn112 ghostwriter's `interests.md` exists, offer to copy it as the starting point — interests113 usually transfer even when register doesn't). `voice-notes.md` ships with sensible114 defaults; append the user's own feedback to it as it comes up.115116### Voice Profile (the heart of "sounds like me")117118Read `data/my_posts.md` in full, then write `~/.claude/ghostwriter-x/voice/voice-profile.md`119(`mkdir -p ~/.claude/ghostwriter-x/voice` first if it doesn't exist yet) capturing:120121- **Voice & tone** — e.g. direct, contrarian, warm, wry. Quote 2–3 tweets that exemplify it.122- **Sentence rhythm** — one-liners? multi-clause? fragments for emphasis? all-lowercase?123- **Openers** — how do their best tweets hook in the first line? (bold claim, number, story124 cold-open, question). List the patterns they actually use.125- **Closers** — do they end threads with a takeaway, a question, a link reply, nothing?126- **Structure** — singles vs threads; numbered threads (`3/7`) or free-form; line breaks127 within tweets.128- **Vocabulary & tics** — recurring phrases, signature words, how they swear or don't.129- **Emoji & hashtags** — none / sparing / heavy; which ones; where. (Most strong X accounts:130 no hashtags.)131- **Topics they own** — the themes they return to.132- **Never do** — anti-patterns to avoid (engagement bait, "🧵👇", corporate buzzwords,133 em-dash overuse, fake vulnerability, generic AI-slop phrasing). Be specific to *this* person.134135Keep it concrete and example-driven — it's a generation guide, not an essay.136137---138139## Mode: Generate140141**Draft display is gated.** Before exposing post copy anywhere, complete142[the mandatory post review](references/post-review.md). Topic menus may describe143angles and evidence, but cannot preview an unreviewed hook or post excerpt.144145**Posture: propose, don't interrogate.** The default is *you* surface concrete, already-real146ideas and the user taps one — not a blank "what do you want to post about?" The picked idea is the147post's real anchor, so there's no generic interview.148149**Outcome check-in (max one, fast — the feedback loop).** Before anything else, run150**`python3 scripts/post_outcome.py --check-due`**. It prints `none`, or the one record a check-in151is owed on. Do not hand-roll this from the log: the script picks the **oldest unscored post at152least 2 days old**, which is the whole point. (The pre-0.2.0 rule asked about the *newest* record,153so posting twice in one day left the newest record same-day forever, the gate never opened, and154ripe older posts were never offered — the loop sat dead through three published posts. If you155re-derive the rule by hand you will reintroduce that bug.)156157If a record comes back, ask ONE check-in question — *"How did '<first_line>' do?"* with options158great / normal / flopped (notes via "Other") — then record it:159`python3 scripts/post_outcome.py --due --outcome <answer> --notes "<notes>"`. **One dialog to160start: if the idea menu (step 2) is also due, the check-in and the menu ride in the SAME single161`AskUserQuestion` call** — the check-in takes the first question slot and the flat idea question162(step 2) takes the second — still one dialog, one round trip, never two sequential question163dialogs to get a session moving. Only when no menu is due (the topic came in concrete)164may the check-in be its own question. Never ask more165than once per session; `none` → skip silently, don't mention it.166167Posts older than **30 days are never asked about** — nobody honestly remembers, and a guess is168worse than a gap. `python3 scripts/post_outcome.py --retire-stale` marks those `unrecalled` so the169backlog stops growing; run it when `--check-due` has been returning `none` for a while.170171**Using the outcomes — and the sample-size bar.** This is a self-reported signal on a handful of172posts with no control group. Treat it as a record you can show the user, not a measurement you can173optimize against. The confounds here are *unidentifiable*, not merely noisy: every post varies174lane, hour, format, image and hook at once, so there are more free variables than observations,175and format is collinear with content **by construction** because the agent picks thread-vs-single176from the content's shape — "threads do better" may only mean "meatier topics do better".177178- **Fewer than 5 scored posts → display only.** You may show the user their own numbers and use179 them to avoid repeating a topic. **Zero influence on ranking.**180- **≥5 in an arm** → you may *say* what you see, with the N attached ("3 of your 4 threads beat181 your 2 singles, small sample"). No behaviour change.182- **≥8 in each of two arms AND a ≥2× gap** → move a lane by **one** position, stating the N.183 Never drop a lane entirely.184- **Single vs thread is never outcome-driven.** Content shape decides, always.185- **Posting time is never outcome-driven.** The per-hour sample will not exist for years.186- Compare **medians, not means** — one amplified post swamps a small account's average — and187 ignore anything older than **90 days**; X's ranking and the follower base both move.188189**Anti-ratchet — the loop's real hazard.** Outcomes must **never** override190`voice-notes.md`, the substance bar, or the source gate. They sit *below* those in precedence,191exactly as `algorithm.md` does. A `flopped` on an honest, well-sourced post is not a reason to192change the voice; chasing a reach number on a personal account is precisely the pressure that193bends a technical voice toward the engagement bait this skill forbids. If the data ever seems to194argue for a banned tactic, the data loses.195196This is the only **free** compliant performance signal available. Be precise about why: real197analytics are **paywalled, not forbidden**. Typefully's `list_social_set_analytics_posts` and198`get_social_set_analytics_followers` are official, COMPLIANCE-clean endpoints that return199`403 MONETIZATION_ERROR` on the free plan (verified 2026-07-27) — don't re-probe them every200session. If the user ever wants real numbers sooner, the zero-cost path is for **them** to read201their own x.com analytics and paste the figures in; the agent never fetches x.com, so that is not202scraping. Revisit the whole loop if the account upgrades.2032041. **Short-circuit if the topic is already concrete.** If the user named a specific topic, pointed205 you at a source, or said "draft a post from item N in the radar," skip the menu and go straight206 to grounding + drafting (step 3). The menu below is the default only for an open-ended "write me207 a post."2082. **No topic given → ONE flat idea question, pick and go.** Gather concrete, ready-to-write209 ideas from the four lanes below *yourself*, then **flatten them into a single ranked list**210 (lane priority order below, bent by outcome history) and present the **top 3** as **ONE211 single-select `AskUserQuestion`** — options are the 3 ideas plus a 4th, **"Show more212 ideas."** Never ask one question per lane. Rules of the question:213 - **Every idea option carries a `preview`** (≤ ~9 lines so the pane never clips): a topic214 summary rather than draft copy, the suggested angle in one sentence (assume a single215 unless the user asked for a thread), and a source-freshness line prefixed with its216 lane (e.g. `Trending · HN 612 pts / 340 comments · Jul 18`, `Radar · Jul 17 ·217 anthropic.com`). A user should be able to pick on the preview alone.218 - **Picking a real idea goes straight to grounding + draft (step 3) — nothing else to answer219 or dismiss.** The auto "Other" on the question takes a typed topic directly (same220 short-circuit as step 1).221 - **Picking "Show more ideas" asks exactly ONE follow-up single-select question** with the222 next batch (the remaining candidates, up to 3 + auto "Other"), same preview format.223 - **One provenance line total in chat**, not per lane (radar date + job health, live-search224 date, repo names) — don't dump a duplicate board into chat; the question options carry the225 ideas.226 - **When the outcome check-in is due** it rides as the first question in the SAME call (see227 above); the flat idea question is the second. Still one dialog, one round trip.228229 The four lanes, in priority order (used to rank the flattened list, not to structure separate230 questions):231 - **Trending now (live, run-day — VERIFIED trending, not vibes).** "Trending" means you can232 point at the surge, not that a web search returned articles; vendor blogs and SEO listicles233 are not trending signals. Check measurable surfaces directly, TODAY: **Hacker News via234 `python3 scripts/hn_trending.py`** (defaults to the last 2 days over 150 points; `--days`,235 `--min-points`, `--json` if you need them). **Use the script, not a hand-written curl** — the236 Algolia filter needs `points>150,created_at_i>…` percent-encoded, and in a shell the `>`237 redirects to a file and the query silently returns nothing (this cost a wasted round trip on a238 real run). Then **top posts this week** in the relevant subreddits, and **news coverage from the last239 ~48 h** (search with explicit recency). Filter through the trending areas in240 `~/.claude/ghostwriter-x/voice/interests.md`, propose **2–3 topics**, each with the specific241 angle the user could own (a trending topic without their angle is just news), and put the242 ACTUAL signal in the preview's source line. X moves faster than LinkedIn: a surge older243 than ~24 h is usually already picked over — prefer today's signal, and say so when an item244 is borderline stale. No citable signal → the item doesn't go in the lane; fewer real245 trending items beat padded ones.246 - **Release radar — current through TODAY, not through the last digest.** Read the newest247 `research/release-radar-*.md` and, **if it exists**, the tail of `research/.radar.log`, and248 state provenance in the board ("Jul 24 radar, job ran clean"). The log is only created once249 the launchd job has run on this machine — **absent means "job never ran here", which is not250 the same as "job failed"**; say which one you actually know and don't spend a second call251 hunting for it. **If the digest is older than today, top the lane252 up**: one quick live search for AI releases since the digest date — label digest items253 `radar · <date>` and top-ups `live · today`. Reuse digest items' title + "suggested angle"254 (already source-backed; the twice-weekly `scripts/release_radar.sh` job scans the broader AI255 industry, not just Anthropic). Never add experience claims the digest didn't establish; the256 digest's **Discussion radar** items feed opinion/hot-take slots the same way. Skip items257 already published (check `published.jsonl`). **Radar stale (>4 days) or missing** → say so,258 note whether the log shows the job failing, and run the lane fully live; if the job is broken259 (e.g. exit 127 — usually the repo moved), offer to repair it: `bash scripts/install_radar.sh`260 re-renders the launchd agent against the repo's current path.261 - **Interests & hot takes (1–3 entries).** Read `~/.claude/ghostwriter-x/voice/interests.md` —262 core themes, the "Strong opinions" list, and the story bank — for specific angles not263 covered recently (check `published.jsonl` and recent drafts). A strong uncovered story-bank264 item beats a generic theme; hot takes are X's home turf, so weight this lane a notch higher265 than the LinkedIn sibling does; label each `interests · <theme or story>`.266 - **Your recent Claude projects (2–3 entries).** Run `python3 scripts/recent_projects.py` and267 take the top 2–3 repos with recent Claude Code sessions; for each, read the recent `git log`268 + last session summary for the **one real thing shipped** (that's the anchor). Respect269 `~/.claude/ghostwriter-x/voice/interests.md` → **Off-limits**: never surface or post anything270 work-confidential; personal/OSS repos only.271272 **Build the list fast and honestly.** Gather all four lanes in parallel (the HN check, the273 radar read + top-up, interests, `recent_projects.py`) so the question is the first thing the274 user waits on. An idea appears in exactly ONE lane — highest-signal lane wins. Filter every275 candidate against `published.jsonl` and recent `drafts/` so nothing already covered276 resurfaces. Rank the flattened list by lane priority and the outcome history, and say so in277 the provenance line when it bends the order.278279 **Persist the full list — research the user paid for doesn't evaporate.** Whether or not it280 was shown, write `research/idea-board-YYYY-MM-DD.md`: every idea gathered (not just the 3281 surfaced) with its lane, signal, angle, and status. Status is one of **`published · <url>`**,282 **`drafted · <slug>`**, or **`on deck`** — never a bare `picked`, which records an intention283 rather than an outcome and goes stale the moment the session ends. (A real board carried three284 items marked `picked` that were never drafted, so the next run could not tell covered ground285 from abandoned ground.) **Reconcile before you trust a board**: an item is only `published` if286 `published.jsonl` has it, and only `drafted` if the file is in `drafts/`; downgrade anything287 else back to `on deck`. On the next open-ended run, read the newest board (≤7 days old) and288 fold still-good `on deck` ideas back into the flattened ranking labeled `on deck · <date>` —289 re-verify a trending idea's signal before reusing it, and drop anything that went stale.290291 **After the pick: lock it in, zero extra dialogs.** Echo a compact brief and go —292 `Locked in: <idea> · <lane>`, then one line each for the angle, the real anchor, the form293 (a single unless the user asked for a thread), and the sources you'll verify against. Then straight to294 grounding + draft (step 3); no second drill.2953. **Confirm the anchor, then draft.** Every post still needs **one concrete, real, first-person296 anchor** — the actual tool, a real number, a specific decision, a thing that actually happened297 (see voice-notes.md → Substance bar + Authenticity). The menu pick normally *is* that anchor.298 Only the personal-project lane sometimes needs a single sharp follow-up to nail the specific299 detail — ask **one** `AskUserQuestion`, never a generic multi-question interview. **Never300 fabricate a detail to clear this bar.** If there's genuinely no real anchor, say so rather than301 shipping a generic post.302303 **3a. Run it before you write about it — the highest-leverage step in this skill.** A release304 how-to assembled from documentation is a recap anyone could write. The same post built on a305 real run is an artifact only this user has. Before drafting, spend the two or three calls to306 check whether the thing is *runnable from this session*:307 - **Is the tool reachable?** `uvx <pkg>@<version>`, `npx -y <pkg>@<version>`, `pipx`, `docker308 run`, an already-installed binary, or an MCP tool the session has. Pin the exact version the309 post is about, and the version *before* it when the post is about a change.310 - **Verify the mechanism against the real binary, not the docs.** Run `--help`, check that the311 flags and defaults you're about to publish actually exist. Docs drift; a shipped post that312 names a flag that isn't there is the most embarrassing failure mode this skill has.313 - **Measure a real before/after when there is a legitimate subject.** `scripts/recent_projects.py`314 lists the user's own repos; running an analysis command over one turns "the default set grew"315 into "9 errors became 138 on the same code." That number is the post.316 - **Read-only, always.** Analysis and reporting commands only — linters, `--help`, `--dry-run`,317 `--check`, read queries. Never a command that writes, installs into, or mutates the user's318 repos, and never anything in a work-confidential repo (interests.md → Off-limits).319 - **Capture what you ran** to `images/<slug>.source.txt` even when no card is planned; it is320 the evidence behind the numbers and `card_lint.py` verifies cards against it.321 - **When it isn't runnable, say so and write generically** — steps in the second person, no322 invented results. A how-to that can't be run is still fine; a fabricated run is not.323324 **3b. Whose run was it? — the disclosure rule.** Measurements from step 3a were produced by325 *you*, in this session, not by the user at their keyboard. First-person framing ("I ran…", "my326 repo", "my CI never moved") is **only** allowed when both hold: the command ran against the327 user's own machine or repos, **and** you state the provenance plainly at the approval step328 ("these are real measurements, but I ran them in this session — you didn't"). Let the user329 decide; some will happily own it, some won't. **Default to second person** ("run this on your330 repo") whenever you haven't disclosed it, and **never** use first person for a run on a machine331 or repo that isn't theirs. Everything else in Authenticity still applies: no invented numbers,332 ever.3334. **Draft against the voice profile.** Read `~/.claude/ghostwriter-x/voice/voice-notes.md`,334 `~/.claude/ghostwriter-x/voice/voice-profile.md`, AND `voice/algorithm.md` (bundled,335 repo-relative) first, every time (voice-notes.md holds direct user feedback and takes336 priority; algorithm.md is reach optimization and must never override voice). If a voice file337 is missing — e.g. a fresh setup — copy `voice/voice-notes.example.md` to338 `~/.claude/ghostwriter-x/voice/voice-notes.md` and proceed with what you have. Write to match339 them — their openers, rhythm, emoji habits, thread style. If the optional `skill_memory`340 tool is connected, follow [local-memory.md](references/local-memory.md) for bounded,341 privately opted-in X hashtag recall; otherwise retain the original source workflow.342 Read **Compose before polishing** in [references/post-review.md](references/post-review.md):343 start from one supported observation and its value to the reader, then compare344 a direct opening and a focused edit. Do not add a question, lesson, or framework345 just to pursue engagement. Preserve warmth and material qualifications.346 Apply the **Engagement craft**347 rules below AND the reach rules in `voice/algorithm.md`. Format rules:348 - **A SINGLE tweet is the default form. Draft a thread only when the user asks for one.**349 Material with five beats is a signal to find the sharpest beat and cut the other four,350 not to thread it; a punchy single is the house style, and the beats that don't survive351 the cut are exactly what a card is for (step 8). When the user does ask for a thread:352 complete-thought tweets, ≤7 by default, never a sentence split across tweets.353 - **Draft file format:** one tweet, or thread tweets separated by lines containing only354 `---`. This is exactly what `scripts/typefully_post.py` parses.355 - **Write each tweet to fit 280 weighted characters** (URLs count 23, most emoji/CJK count356 2) — check as you go with `python3 scripts/x_len.py --file <draft> --thread`; don't write357 long and trim at the gate.358 - **No external links in tweet 1** — a needed link goes in the last reply tweet (see359 `voice/algorithm.md`).360 **Never fabricate or exaggerate** details that aren't true to the user's real experience —361 authenticity over drama (see voice-notes.md).3625. **Save the draft** to `drafts/` as `YYYY-MM-DD-slug.md` (ask the user for today's date if you363 don't have it; do not invent one).3646. **Research & fact-check — every external claim must be backed by ≥3 real, live sources (the post365 is *generated from* sources).** Do this after Save (you need the slug) and before showing the366 draft. List every **external/world claim** the draft makes — a vendor shipped X, a research367 finding, a statistic, a definition; anything about the outside world, not the user's own368 first-person experience. For each, **research it** (WebSearch / firecrawl / WebFetch) and369 **actually read the source to confirm it supports the claim** — a live URL is not enough, the370 content has to back the statement. Prefer **primary/authoritative** sources (official docs,371 release notes, the vendor's own announcement, standards bodies, reputable engineering writing);372 skip SEO/hype blogs. Radar-lane posts: reuse the digest's source URLs. Then write a sidecar373 `drafts/YYYY-MM-DD-slug.sources.json` pairing each claim to its URL(s) — **every claim needs ≥1374 source, and the post needs ≥3 distinct live source hosts overall** — and run375 `python3 scripts/verify_sources.py --file drafts/YYYY-MM-DD-slug.md` until it passes. The sources376 live **only** in the sidecar; **never put sources, links, or a "Sources" section in the post377 body** (links in tweet 1 also crush reach — see `voice/algorithm.md`; if the user wants the378 link public, it becomes the final reply tweet at publish time, called out in the preview).379 If a claim can't reach ≥3 reputable sources, **cut it or don't ship the post — never fabricate380 a citation or a fact.**381 - **Pure first-person posts** (no external claims — e.g. a personal story or hot take about382 the user's own work) make no outside-world assertion. Write a sidecar declaring383 `{"external_claims": false, "claims": []}`; the gate passes trivially. Be honest: if the384 post mixes a real external claim into a personal story, it is *not*385 `external_claims:false`.386 - **Narrate the gate — it's the slow step; never go silent through it.** Emit one short387 status line as claims resolve, without quoting unreviewed draft text. Close388 with the result, for example `3 claims · 5 distinct hosts · gate passed`.389 - **The narration floor applies to every slow stretch, not just this one.** From the live run390 (3a) through the source gate, the render cycles, and publishing, **never go more than ~2 tool391 calls without a user-facing line.** Say what you're doing and what came back: `ran ruff392 0.15.5 vs 0.16.0 on local-fitness → 9 errors became 138`, `render 2 failed clip-overflow at393 688px, cutting a row`. Silence during a long stretch reads as a hang, and it hides the394 decisions the user would most want to interrupt.395 - **Re-verify on edit.** The show→edit→re-show loop below can add a claim after the sidecar was396 written. **Whenever an edit adds or changes an external claim, re-run this step** and update the397 sidecar before publishing.3987. **Complete the mandatory post review before showing any draft.** Read399 [references/post-review.md](references/post-review.md) and follow its full400 rubric and private revision loop, using one fresh editor subagent when the host401 supports delegation (otherwise label the in-session review honestly). Prepare `drafts/<slug>.review.json` with402 `scripts/post_review.py prepare`, then complete every editorial check against403 the user's current voice files, 2–3 real samples, and source evidence. Complete404 both private comparisons (opening and compression), and justify every question's405 purpose. A blanket pass or an overall score cannot replace these decisions.406 The ending stops on the last real point; voice, naturalness, substance,407 clarity, hook, credibility, restraint, originality and platform fit must also408 pass. Resolve every warning with a specific contextual reason or rewrite it.409 **Re-run the gate after every edit**; missing, stale, skipped, or mock reviews410 block display. After at most three private revision rounds, report the blocker411 without showing failed copy. Never open a failed draft or quote it in status412 updates, idea previews, approval choices, or a final response.413 Run `python3 scripts/post_review.py check --file drafts/<slug>.md --show`.414 **Only exit 0 permits display.** No averaged score can overrule a failed check.415 The first tweet must stand alone as the hook; every reply earns its place.416 Fix what fails, then **show the full draft in the X-true format** — numbered tweets, each in417 its own fenced block, each headed by its live weighted count in the form `[n/N · used/280]`418 (from `x_len.py`, not estimated):419 - A single post is `[1/1 · 243/280]` + the tweet.420 - One metadata line under the last block: `single|thread of N · save: <the thing a reader421 keeps> · lane: <lane>` (+ `link rides in final reply: <url>` when applicable).422 - Review line: `Review passed · voice, substance, clarity, credibility and platform checks`.423 - **Re-shows lead with the delta:** after any edit, the first line is424 `Changed: <one-line summary>`, then the full draft in the same format — the user should never425 re-read the whole thread hunting for the edit.426 Then ask with a single `AskUserQuestion` **carrying TWO questions in the one call** — and wait427 for the answer:428 - **Q1 the text** — options **Publish** / **Edit** (the auto "Other" takes typed edit429 instructions directly) / **Scrap**.430 - **Q2 the visual** — the step-8 choice, asked here rather than in a second dialog, since the431 recommendation and its ASCII previews are already derivable from the approved text. If the432 user picks **Edit** or **Scrap**, the visual answer is simply discarded; that waste is433 cheaper than a guaranteed extra round trip on every post.434 - **Add Q3 only when step 3a produced first-person measurements** — the disclosure required by435 3b, as its own question ("these are real numbers, but I ran them this session, not you: keep436 the 'I ran…' framing, or switch to 'run this on your repo'?"). Never bury that in prose the437 user might skim past.438439 The Publish tap immediately after seeing the exact full text is the explicit approval; an440 edited draft is re-shown and re-asked the same way. Do not publish unprompted.441 **Any voice/style feedback the user gives — append it to442 `~/.claude/ghostwriter-x/voice/voice-notes.md` in the same turn, BEFORE redrafting,** and say443 you did ("added to voice notes"). Fixing only the draft loses the correction and the user has444 to repeat it next session. For privately opted-in `writing-x.hashtags` corrections,445 the owner bridge in [local-memory.md](references/local-memory.md) **replaces** this446 manual append: never write both. Confirm the source save before dependent redrafting;447 an attempted bridge failure must not silently fall back to appending.4488. **The visual choice — asked in step 7's dialog, built only after the pick.** This is Q2 of the449 single approval call above, never a separate dialog: **text-only** / **single card** (name the450 Press hero component you'd compose around, e.g. "a duel" or "a ledger") / **image carousel** (a451 4-image post, or one image per tweet on a thread) — with your recommendation first, chosen from the452 post's shape and the outcome history: how-to or anything technical → a single 16:9 card453 carrying the steps the single tweet had to cut; personal story or hot take → text-only454 (X is text-native; a strong text post beats a weak image); carousels only on a455 user-requested thread. **Give every option an456 ASCII `preview` sketch of what THIS post would get:** the card option sketches the actual457 proposed Press composition as labeled blocks with this post's real headline; the carousel458 option sketches the image strip (`cover → point → point → recap`, using this post's real459 slide titles); text-only previews tweet 1 verbatim. Sketches are text in the question, not460 builds — authoring still waits for the pick. Only after the pick do you author and render461 (see **Visuals**); never render a form the user didn't choose. Cards are **composed, not462 templated**: read `assets/card-language.md`, check `images/card-history.jsonl`, and differ463 from the last 3 cards on ≥2 variation axes.464 **If the post is about the user's own agent, CLI, or code** — any visual that would show465 its output (a hero `term`, `code`, or `claude` card) — settle the output source in the466 SAME single question, via the option descriptions: you capture it live (run their CLI /467 call their MCP tool from this session), they paste or screenshot a real session, or —468 only if neither is possible — compose from facts already in the draft. One question469 total, never a second round-trip. See **Real-output cards** below.470471### How-to posts (technical, from AI releases)472473The priority lane, and the one radar items feed directly. When the anchor is a recent AI release,474write a genuine how-to — not a news recap.475476- **Shape: a single tweet, with the steps on the card.** The tweet carries the implication477 (what the reader can now *do*) and the sharpest number; the concrete steps, real commands478 and the gotcha go into the composed Press card, which is where a how-to earns its479 bookmarks. Prescriptive, for the reader (voice-notes → Framing & audience). Only draft the480 step-per-tweet thread version when the user explicitly asks for a thread.481- **Real technical meat, accessible entry.** Use real commands, real config, real names — a482 curious non-expert can follow the tweet, an engineer still learns the mechanism from the483 card. This is what earns bookmarks and quote-posts.484- **Authenticity — how-to ≠ "I did this."** A release how-to makes external/world claims, so it485 is exactly the case the source gate is for: the `*.sources.json` sidecar + `verify_sources.py`486 step (step 6) is mandatory. **Never fabricate** or imply the user personally ran a release487 they haven't — write the steps generically, not as a first-person story.488- **Default visual: a composed Press card on tweet 1** — usually built around a **ledger**489 (numbered steps + the real command in a `.cmdbar`) or **tiles**. Compose it fresh per490 `assets/card-language.md` and vary against `images/card-history.jsonl`.491492### Visuals (optional — diagrams & cards)493494If the user chooses a graphic generated or edited with Codex imagegen, read495`references/visual-composition.md` to plan a picture-led explanation, then496`references/visual-review.md` and pass all 12 checks before presentation. Register497its origin, inspect the actual pixels, and run `visual_review.py check` before498showing it. The publisher enforces that review, including dry-run and draft-only499creation. This gate applies only to Codex imagegen assets; the native screenshot500and Claude/legacy render workflow below stays unchanged.501502Only when the user opts in. Requires the diagram dependency (see README; if `render_image.py`503reports Playwright/Chromium is missing, point them at the install step and stop).504505**Brand guide (per-user).** Styling + byline live in `~/.claude/ghostwriter-x/assets/diagram.css` —506the user's personal brand guide, shared across every install of the skill. On first use, if it507doesn't exist, copy it from the template: `mkdir -p ~/.claude/ghostwriter-x/assets && cp508assets/diagram.css.example ~/.claude/ghostwriter-x/assets/diagram.css`, then set their `--byline`,509their Press identity (`--press-sig` signature color + `--stamp` monogram initials), and tweak the510palette. Cards use `<div class="footer brand"></div>` to pull the byline automatically — don't511hardcode it. If the user already has a LinkedIn ghostwriter brand guide at512`~/.claude/ghostwriter/assets/diagram.css`, copy its personalization vars — same brand, new513geometry.514515- **The Press system (THE brand — default for every card).** Editorial-poster identity: warm516 paper canvas, huge black type, serif standfirst, ONE loud signature accent, heavy ink rules,517 giant numerals, an issue-numbered masthead with the personal monogram stamp. Cards are518 **landscape 16:9 (1200×675)** — X's timeline crop — and **composed, not templated**: read519 `assets/card-language.md` (the component vocabulary, composition rules, and variation axes),520 pick the 1–2 body components that *prove the post's point* (a duel proves a decision, a521 ledger proves a method, a big stat proves a claim, a terminal proves it's real), and author522 a bespoke `images/<slug>.html`. `assets/card-template-press.html` is one example composition,523 not the shape. Landscape wants **hero-left / support-right** two-column arrangements, not524 stacked bands. **Anti-sameness contract:** before authoring, read525 `images/card-history.jsonl` and differ from the last 3 approved cards on **≥2 variation526 axes** (hero component, headline treatment, density, numeral presence, support texture);527 after the user approves the render, append the card's fingerprint line to that file.528- **Real-output cards (the fidelity contract).** Whenever a card shows the output of the529 user's own agent, tool, or code — a hero `term` component, a `code` card, a `claude`530 session card — the terminal content is a **transcription of a real session, not an531 invention**. A round of "make it look like my actual agent" is a defect: get the ground532 truth *before* authoring, not after the user complains.533 1. **Capture first.** In preference order: **run it yourself** (the user's CLI or MCP tool534 is often reachable from this session — call it and capture real output); else take the535 user's **paste or screenshot** (offered in the step-8 question). Save the raw capture —536 transcribing a screenshot fa537538…(truncated)