# X To Skill

> Collects a person's public X/Twitter posts into a provenance-annotated corpus and converts their recurring ideas into a structured agent skill. Use when the user wants to learn from a specific account ("turn @karpathy into a skill", "extract X's thinking"), study a practitioner who publishes on X rather than in books, or build a citable knowledge base from someone's posts.

- Skill: `virgiliojr94/x-to-skill` (Agent Skill, multi-file: 4 files)
- Install (CLI): `npx skillmds@latest add virgiliojr94/x-to-skill`
- Raw SKILL.md: https://api.skillmd.com/api/skills/virgiliojr94/x-to-skill/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: AI & ML
- Author: virgiliojr94 (https://skillmd.com/u/virgiliojr94)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/virgiliojr94/x-to-skill

---


<!--
Cross-agent notes (informational; ignored by host agents):
  - Companion to book-to-skill. Same output contract, different source.
  - Needs: shell (to run opencli + scripts/collect.py), file read/write.
  - Argument hint: <@username-or-x-url> [skill-name-slug] [--limit N]
  - OpenCode slash command: ~/.config/opencode/command/x-to-skill.md
    Other hosts have no slash command — invoke by name (see Invocation).
-->

# X-to-Skill Converter

Turn an account's public posting history into a toolkit of their frameworks —
with a URL behind every claim.

## Requirement: opencli

**Hard dependency.** `opencli` is the only sanctioned way this skill reads x.com.
It drives a real browser reusing your existing Chrome login session, so there are
no API keys and no credentials in this repo.

```bash
npm install -g @jackwener/opencli    # verified against v1.8.6
opencli twitter whoami -f json       # confirm the session works
```

`scripts/collect.py` aborts with install instructions if `opencli` is missing.
**Do not substitute a scraper or an API client.** The safety guards in Step 0
are built on `opencli`'s output shape (`author` per post, `whoami`); a different
backend silently voids them.

Optional: `agent-reach doctor --json` reports backend health if that skill is
installed. Not required — `opencli twitter whoami` is the direct check.

## Invocation

| Host | How |
|---|---|
| OpenCode | `/x-to-skill https://x.com/<user>` |
| Claude Code, Copilot CLI, Amp, other | "run x-to-skill on https://x.com/\<user\>" |
| Shell only | `python3 scripts/collect.py https://x.com/<user>`, then Steps 3–4 by hand |

Every host accepts a full URL or a bare `@handle`. The slash command is an
OpenCode convenience wrapper around the same steps — nothing below depends on it.

## Philosophy

A book is edited, ordered, and argued once. A timeline is none of those things:
it is thousands of fragments, written across years, in which the author changed
their mind, joked, retweeted others, and occasionally published something that
would have been a chapter. The value is real but it is **unsorted and unsigned**.

So this skill inverts book-to-skill's problem. There, structure exists and must
be extracted. Here, structure must be *discovered* — and the main risk is not
missing an idea, it is **inventing one**.

Three rules follow:

**Every claim cites a post.** A framework attributed to the author without a
post URL is fabrication. If you cannot point to the post, drop the claim.

**Repetition is the signal.** In a book, one mention makes a chapter. In a
timeline, one mention makes a passing thought. An idea earns a place in the
skill when the author returns to it — across multiple posts, ideally across
months. A single viral post is a data point, not a framework.

**Quoted words are not the author's words.** Retweets are excluded outright.
Quote-tweets keep the quoted block only as context for the author's reply.
Never attribute the quoted text to the author.

---

## Modes of Operation

### 1. Full Conversion (Default)
**Trigger:** User provides an @handle or x.com URL
**Action:** Steps 0–4
**Output:** Local corpus directory + generated skill

### 2. Collect Only
**Trigger:** "just collect", "get the posts", "I want the corpus"
**Action:** Steps 0–2. Stop.
**Output:** `<username>-corpus/` with manifest, raw source, compact corpus,
sample, and monthly chunks

### 3. Build From Existing Corpus
**Trigger:** User points at a prior `<username>-corpus/`
**Action:** Skip collection. Verify `manifest.json`, start from `sample.md`, then
read only the monthly chunks relevant to candidate themes.
**Output:** Generated or refreshed skill; no X request, no re-ingestion bill

### 4. Refresh (Existing Skill)
**Trigger:** User wants posts newer than an existing corpus window
**Action:** Collect only the uncovered date window into a separate corpus
snapshot, analyze it, then fold verified changes into the skill. Preserve the
old corpus — never overwrite provenance.
**Output:** Updated skill + new dated corpus snapshot.

---

## Step 0 — Out-of-scope check

If no handle is provided, stop:

> "x-to-skill requires an X handle or URL, e.g. `https://x.com/<user>` or `@<user>`."

### What this skill reads — and what it never reads

**Reads:** the public posts of the *named* account, and nothing else.

**Never reads:** the operator's own account. Not their timeline, DMs, bookmarks,
notifications, likes, or following list. OpenCLI runs on the operator's logged-in
browser session, so the operator's own data is technically within reach — that is
precisely why the boundary is enforced in code, not left to intent:

- `parse_handle()` **aborts** on an empty or unparseable handle. It never falls
  through to a bare `opencli twitter tweets`, which silently returns the
  logged-in user's timeline.
- `main()` compares the target against `opencli twitter whoami` and **refuses**
  when they match, unless `--allow-self` is passed explicitly.
- `fetch()` verifies every returned post is authored by the requested handle and
  aborts if anything else appears.

Run `python3 scripts/test_collect.py` to verify these guards (10 accepted forms,
12 rejected).

**The target account must be public.** The session is used to read x.com as a
logged-in browser would; it is not a key to protected accounts. If the account is
protected, collection returns nothing — report that and stop. Do not attempt
another auth path, and never touch, rotate, or inspect the session credentials.

### Refuse and explain when

- **The target is protected or private.** Only public posts are in scope.
- **The target is a private individual with no public teaching intent.** This
  skill is for practitioners who publish ideas — not for profiling people.
- **The user wants a dossier on someone.** Building a behavioral profile of a
  person is out of scope regardless of how the request is phrased.
- **The request is to imitate someone's voice to post as them.** Extracting how
  someone thinks is fine; manufacturing their persona is not.

---

## Step 1 — Verify the backend

```bash
opencli twitter whoami -f json    # expect {"logged_in": true, "username": "..."}
```

Two things must hold before collecting:

- **`opencli` is installed.** If not, stop and give the install line from
  *Requirement: opencli* above. `collect.py` enforces this too, but failing here
  is cheaper than failing mid-run.
- **The session is logged in.** If `logged_in` is false, stop and ask the
  operator to log in to x.com in Chrome. Do not attempt another auth path and do
  not scrape x.com directly.

Note the `username` it returns — that is the operator's own account, and Step 0's
self-guard refuses to collect it.

Never touch, rotate, or inspect the session credentials themselves.

(`agent-reach doctor --json` gives the same signal plus other platforms, if that
skill happens to be installed. Optional.)

---

## Step 1.5 — Intake

Ask these questions in one turn before collecting. Defaults let a user skip
choices without blocking the workflow.

| Question | Choices | Default | What it changes |
|---|---|---|---|
| **Profile type** | Technical · Philosophical · Idealistic · Mixed / custom | Mixed / custom | The analytical lens and candidate-theme vocabulary — **never** the collected posts |
| **Time window** | Last 3 · 6 · 12 · 24 months · all available · custom dates | **Default: last 12 months** | `--since` / `--until` and expected collection size |
| **Purpose and depth** | Reference economic · Study deep | **Reference economic** | Whether analysis stays at `sample.md` + selective chunks, or reads all relevant chunks |

**Profile type is a lens, not a label or a restriction.** A Karpathy corpus may
be technical; a Cortella corpus may be philosophical; either can contain the
other themes. Use the type to form hypotheses, then prove or discard them from
posts. For `Mixed / custom`, ask for one sentence about what the user wants to
learn, e.g. "management principles" or "faith and ethics."

**Time window is a request, not a promise.** The collector records both requested
and actual windows in `manifest.json`; X pagination can return less history than
asked. Never represent the actual window as complete history.

### Intake examples

```text
/x-to-skill https://x.com/karpathy
→ technical · last 12 months · reference economic

/x-to-skill https://x.com/<cortella-handle>
→ philosophical · last 24 months · study deep
```

---

## Step 2 — Collect the corpus

```bash
python3 scripts/collect.py https://x.com/<user> --since 2025-08-26 --until 2026-08-26 \
  --limit 400 -o <user>-2025-08-26_2026-08-26
```

The argument may be a full URL or a bare `@handle` — `parse_handle()` accepts
`x.com`, `twitter.com`, `www.`/`mobile.` prefixes, trailing slashes, query
strings, and `/status/...` subpaths. It aborts on anything else rather than
guessing.

If the user takes the defaults, omit dates: the collector uses the prior 365
days through today. `--limit` caps **fetched** posts, not kept posts. The date
filter is inclusive and local, so fetch enough to cover the requested period.

| Request | Command shape | Cost implication |
|---|---|---|
| Default: 12 months | `--limit 400` | Good first pass for active accounts |
| Short recent view | `--since YYYY-MM-DD --limit 200` | Fewer fetched posts / chunks |
| 24 months | `--since YYYY-MM-DD --limit 800` | More history; collect once, reuse corpus |
| All available | omit `--since`, raise `--limit` deliberately | X may still stop short; can be expensive |
| Exact custom range | both `--since` and `--until` | Bounded and reproducible |

The collector excludes retweets, filters by date, and writes `<outdir>/`:

| File | Purpose | Read by default? |
|---|---|---|
| `manifest.json` | Counts, requested/actual windows, format, chunk list, sample scoring | Yes |
| `raw.jsonl` | Canonical structured source; quoted external text appears **only** here | No |
| `sample.md` | Discovery sample: up to 3 posts/month, globally capped at 18,000 chars | Yes |
| `chunks/YYYY-MM.md` | **Canonical rendered source**, one compact monthly slice; a heavy month splits into ordered parts `YYYY-MMa`, `YYYY-MMb`, … | Only when a candidate theme needs evidence |
| `corpus.md` | Optional one-file export, emitted only with `--full-corpus` | Never by default |

Use `--format txt` for plain text, `--chunk none` only for a tiny corpus, and
`--sample-per-month N` only when the default sample is too thin.
`--chunk-max-bytes N` caps a chunk file (default: 40,000). A prolific month is
split chronologically into ordered parts rather than becoming one oversized file
— a monthly chunk stops being a cost boundary the moment it exceeds the budget.
Read `chunk_index` in the manifest for each month's ordered parts; filename sort
is wrong past `z` (`2026-08aa` sorts before `2026-08b`).
`--sample-max-chars N` controls the full discovery budget (default: 18,000). A
candidate that would exceed it is skipped, never truncated; the manifest records
the requested and actual sample size. Use `--full-corpus` only when a portable
one-file export is explicitly needed. Every block has a date and URL; quote-tweets
identify the other author but never reproduce their text outside `raw.jsonl`.

**Read `manifest.json` before continuing.** It is the evidence of what was
actually obtained. A skill must name that actual window — never claim to describe
"how the author thinks" beyond it.

X's timeline API can stop well short of full history, and a prolific account
exhausts `--limit` long before the requested period. When the delivered window
covers less than 40% of the request, the collector prints a warning and records
`window_warning` in the manifest; a `null` there means the check ran and passed.
Never treat a collapsed window as the requested one — see *Handle a collapsed
window before generating* in Step 3.

---

## Token economy — collect once, read selectively

A corpus is a local source artifact, like an extracted book. The expensive
network collection happens once; reasoning reads only the evidence needed for
the chosen depth. Reuse it instead of collecting again for another skill,
question, or refinement.

**Reference economic (default):**

1. Read `manifest.json` — establish counts and actual window.
2. Read `sample.md` — up to 3 posts/month, selected by deterministic local
   signal score and capped at 18,000 chars across the full sample.
3. Form candidate themes from that sample and the chosen profile lens.
4. For each candidate, only then read the relevant `chunks/YYYY-MM.md` files to
   count recurrence, check spread, recover exact language, and test for
   contradiction.
5. Open `raw.jsonl` only when quote context or full structured source data is
   necessary.

**Do not load a full corpus by default.** Monthly chunks are the rendered source
and cost-control boundary. `corpus.md` exists only if the user explicitly passed
`--full-corpus`; it is a portability export, never a second canonical source.

**Study deep:** start the same way, then read all monthly chunks that plausibly
contain the final themes. Do not treat "deep" as permission to ignore the actual
window or provenance.

The output stays book-to-skill shaped — structured patterns, not a timeline
summary — but the corpus replaces a book's extracted text and the sample/chunks
replace a blind full-context read.

### Reuse a snapshot instead of re-collecting

```bash
python3 scripts/collect.py https://x.com/<user> --reuse <user>-corpus
```

`--reuse` answers from an existing local snapshot and never contacts X. It loads
that directory's `manifest.json` and refuses unless the snapshot answers exactly
this request: same collector schema, same target, same requested window, same
format, chunking, and sample settings, with `raw.jsonl` present and parseable.
Any mismatch names the offending field and stops — a near-miss snapshot is a
different corpus, and silently reusing it would answer the question from the
wrong data.

`manifest.json` records a `collection_key`: a deterministic hash of the request
(schema, target, window, limit, format, chunking, sample settings). Equal keys
mean equal *requests*, never a guarantee that X returned identical or complete
history. The actual window remains the honest scope.

### Report what was actually spent

Every run prints, and the manifest records, measured figures rather than
estimates of money:

- `economy.x_fetches_this_run` and `posts_fetched_this_run` — 0 on reuse
- `measured_bytes` — real source, chunk, and sample sizes on disk
- `token_estimates.reference_path` — sample plus one average chunk, the expected
  cost of the default reading path
- `token_estimates.all_chunks` — what a full chunk sweep would cost instead

State these when reporting to the user. Do not convert them into currency: the
collector uses a browser session, so there is no per-request price to quote.

---

## Step 3 — Find what repeats

This is the step with no book-to-skill equivalent, and the one that decides
whether the output is a skill or a scrapbook.

Read the sample first, then cluster posts by recurring idea. Use selective monthly
chunks to prove each candidate. For each candidate:

1. **Count the posts, against the window's bar.** The threshold is not fixed —
   it scales with how much history you actually have. See *Calibrate the
   recurrence bar* below. Below the bar → observation, or drop it.
2. **Check the spread.** Three posts in one week is a mood; three posts across
   a year is a conviction. Prefer themes the author returns to.
3. **Take the author's own words.** If they named the idea, keep the name
   exactly — the same precision rule as book-to-skill ("The 5 Whys" is not
   "ask why a few times").
4. **Record every source URL.** These become the citations in the final skill.
5. **Log the contradictions.** People change their minds in public, and the
   timeline preserves both sides. When early and late posts disagree, keep
   both, dated. An evolving position is more useful than a flattened one.
6. **Record the evidence files.** A claim's URLs tell *what* supports it; list
   the monthly chunks read so another agent can reproduce the route without
   loading the whole corpus.

Produce a clustering report before generating anything:

```markdown
## Theme: <name in the author's words>
Posts: <N> across <first date> – <last date>
Sources: <url>, <url>, <url>
Evidence files: sample.md; chunks/YYYY-MM.md, chunks/YYYY-MM.md
Claim: <one sentence the posts actually support>
Evolution: <only if the position visibly changed>
```

Discard themes that cannot fill this template honestly.

### Calibrate the recurrence bar to the actual window

"Three posts" only means conviction when there was enough time for the author to
return to the idea. Read `actual_window` from `manifest.json` and apply:

| Actual window | Bar for a framework | Otherwise |
|---|---|---|
| < 90 days | ≥3 posts **and** spread across ≥3 distinct calendar months, or a theme the author explicitly names as a principle | Label it an **observation**, not a framework |
| ≥ 90 days | ≥3 posts, spread across ≥2 distinct months | Observation |

A short window cannot distinguish a conviction from a phase. On a five-week
corpus, near-daily repetition of one topic is evidence of *what the author is
working on now*, not of a durable framework — say so instead of promoting it.

### Handle a collapsed window before generating

The collector warns when the returned window covers far less than requested and
records `window_warning` in `manifest.json`. When that warning is present:

1. **Tell the user before generating**: state requested vs actual span and that
   high-volume accounts exhaust `--limit` long before the requested period.
2. **Offer the fix**: re-collect with a higher `--limit`, or narrow `--since` to
   match what is actually reachable and accept the smaller scope honestly.
3. **If they proceed anyway**, apply the short-window bar above, and put the
   actual span in the generated skill's description — not just a footnote.

Never present a collapsed window as if it were the requested one.

### Weigh quote-tweet evidence lower

A quote tweet proves the author endorsed someone else's framing. It does not
give you their words. When a theme rests mainly on quote tweets:

- attribute the framework to the endorsement, never to the quoted author;
- prefer a citation where the author states the idea in their own words;
- if every citation for a theme is a quote tweet, label it an observation.

---

## Step 4 — Generate the skill

Write the skill directly from the Step 3 clustering report, following
book-to-skill's **output contract** (SKILL.md + `chapters/` + supporting files).

Do not shell out to `book-to-skill <corpus>.md`. Verified 2026-08-26: its CLI
takes a *document path* and re-derives structure from the text — it has no flag
for supplying a prior analysis, so Mode 3 ("Generate from Prior Analysis") is a
workflow the agent performs, not a command the CLI exposes. Running it on the
corpus would discard the theme clustering that Step 3 exists to produce, and
re-derive chapters from raw chronology.

Keep source evidence **outside** the generated skill, in its collected
`<user>-corpus/` directory. The skill points to that immutable snapshot; it does
not copy it.

- Record `source_corpus_path`, `manifest.json` actual window, and the source
  corpus collection timestamp in the generated skill's front matter or Sources
  section.
- Record source URLs and the `chunks/YYYY-MM.md` files read for every framework.
- Keep `analysis.md` only in `<user>-corpus/`, including dropped themes and
  contradictions. Do not copy it into the skill.
- If the source snapshot must travel with the skill, package the two directories
  together unchanged; do not duplicate `raw.jsonl`, chunks, analysis, or an
  optional `corpus.md` inside the skill directory.

Two adjustments to book-to-skill's output contract:

- **Chapters are themes, not dates.** Chronological chapters would reproduce
  the timeline, which is the thing being escaped.
- **Every framework carries its post URLs.** book-to-skill cites chapters; here
  the citation is the permalink.

The generated skill's front-matter description must name the account and the
date window, e.g. *"Working patterns from @karpathy's X posts (2024-06 –
2026-08)."* This keeps the scope honest at load time.

---

## Anti-patterns

**Summarizing the timeline.** Chronological recap is the failure mode. Nobody
needs "in March they posted about X." Structure by idea or don't ship.

**Promoting one viral post to a framework.** High engagement measures reach,
not conviction. The engagement counts in the corpus are context, not ranking.

**Smoothing the contradictions.** A corpus spanning years contains genuine
reversals. Presenting a single coherent position from contradictory posts is
fabrication by omission.

**Filling gaps with the plausible.** If the author never addressed something,
the skill says nothing about it. Do not complete their worldview for them.

**Mistaking jokes for doctrine.** Timelines are largely social. Irony, replies,
and throwaway lines are not teaching material.

---

## Scope

Public posts only. Extracts ideas, not identity. Produces a citable knowledge
base from someone's published thinking — not a profile of a person, not a voice
clone, and not a claim to represent them.

