The production Twitter/X monitor reads accounts and search queries from
data/sources/twitter_accounts.json. The scheduled workflow should stay
deterministic: every run validates the manifest, builds a birdy multi-fetch
manifest, and fetches the exact reviewed account set.
The manifest groups handles into four categories:
| id | Beat |
|---|---|
labs |
AI labs and company accounts |
hyperscalers |
Hyperscalers, executives, and key insiders |
local-oss |
Local/on-device AI, quantization, OSS tooling, and the hardware to run it |
others |
Researchers, analysts, builders, and media |
Categories are organizational, not operational. build_fetch_manifest
iterates every category identically and tweets_per_account is manifest-global,
so moving a handle between categories changes nothing about what gets fetched.
They exist so the roster stays legible and so a future curation pass can reason
about coverage per beat.
Two couplings are load-bearing anyway, and both are easy to trip:
othersis hardcoded in two places, not one. It is the--default-categoryforpropose, andexplore_twitter_accounts.pystamps"category": "others"onto every candidate it discovers. Consequence: no category other thanothersever grows via the weekly explorer. A beat likelocal-ossonly gains handles through a manual curation pass.- Category ORDER is pinned by a test.
test_curate_twitter_accounts.pyasserts the first built fetch operation is@OpenAI, solabsmust stay first. Append a new category in the middle or at the end — never prepend one.
Use scripts/curate_twitter_accounts.py for account changes:
uv run python scripts/curate_twitter_accounts.py validate
uv run python scripts/curate_twitter_accounts.py build-fetch-manifest \
--output /tmp/manifest.json \
--concurrency 8To review candidates discovered by an agent or by manual research, put them in a JSON file:
[
{
"action": "add",
"handle": "example_ai",
"category": "others",
"score": 4,
"reason": "Repeatedly surfaced primary AI launch details before search lanes.",
"evidence": ["https://x.com/example_ai/status/123"]
},
{
"action": "remove",
"handle": "old_handle",
"reason": "No AI-relevant posts in recent monitored snapshots.",
"evidence": ["research/twitter/2026-06-17.md"]
}
]Generate a reviewable proposal:
uv run python scripts/curate_twitter_accounts.py propose \
--candidates /tmp/twitter-account-candidates.json \
--output /tmp/twitter-account-proposal.md \
--changes-output /tmp/twitter-account-changes.jsonAfter review, apply the approved changes file:
uv run python scripts/curate_twitter_accounts.py apply \
--changes /tmp/twitter-account-changes.jsonThe proposal step never mutates production state. The apply step preserves category order, appends additions to their target category, removes matching handles case-insensitively, and re-validates before writing.
.github/workflows/twitter-account-explorer.yml runs weekly and by manual
dispatch. It is an agent loop, but it is intentionally review-gated:
scripts/explore_twitter_accounts.pyruns broad bird searches and writes/tmp/twitter-account-candidates.jsonplus a Markdown evidence report. It scores candidates from three deliberately bounded signals so no single one can carry a watchlist change:- Trust-weighted mentions — the
goodtweetlesson applied to a trusted seed set. A mention-graph candidate must be vouched for by at least one account already in the manifest (a monitored citer); being cited only by other broad-search-surfaced accounts is not enough, which keeps crypto/spam rings out of the candidate pool. - AI topicality — a bonus for distinct AI-vocabulary terms actually present in the candidate's evidence text, so a viral but off-topic post (politics, astrology, crypto) cannot outrank a genuine AI source.
- Engagement — logarithmic and bounded, so it nudges ranking rather than
dominating it.
Exact weights and the
--min-scoredefault live in the script (the source of truth); this contract describes the mechanism, not the constants.
- Trust-weighted mentions — the
- A Claude explorer reads those candidates, the current manifest, recent Twitter outputs, and this curation contract.
- If no strong evidence exists, it prints
no account changes. - If evidence supports changes, it uses
curate_twitter_accounts.py proposeandapply, validates the manifest, runs focused tests, then pushes the branch and opens the PR againstmainfrom inside theclaude-code-actionstep (the PR is authored asapp/claude). PR creation must happen there, not in a downstreamrun:step: the defaultGITHUB_TOKENcannot open PRs in this repo (it fails with "GitHub Actions is not permitted to create or approve pull requests"). See CLAUDE.md's load-bearing rules.
The loop caps each run at five additions and two removals. Removals require clear observed evidence because losing a quiet but important source is costlier than adding a marginal candidate.
That cap governs the explorer loop only. A hand-curated batch that goes
through propose → human review → apply is a different path and is not bound
by it — the cap exists to stop an unattended agent from churning the roster,
not to limit reviewed human curation. Record the per-handle rationale in notes
and the verification in evidence so a later pass can re-adjudicate.