Research crawl rss
Skill thangnguyenworkspace/industry-pulse/.claude/skills/research-crawl-rss
Watchlist-driven intelligence pipeline crawling LinkedIn, X, RSS, and email into one neutral signals brief.
npx -y skills add thangnguyenworkspace/industry-pulse --skill research-crawl-rssAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 4 stars4 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Batched RSS/Atom/RDF feed crawl from N feed-URLs via Python feedparser; per-feed markdown + manifest.json to caller dir; rolled-up + per-source summary.
SKILL.md
16.5 KB, as published. Nobody here has run it
Research Crawl RSS
Argument: $ARGUMENTS (required, see Runtime Inputs)
If $ARGUMENTS is empty or missing required fields, STOP and report which fields are absent.
Preamble
Runtime Inputs
Parse from $ARGUMENTS:
--feed-urls=[u1,u2,...] [JSON-style array of absolute feed URLs (RSS / Atom / RDF), non-empty; each must be an http(s) URL. Singular invocation expressed as a single-element array.]
--raw-output-dir={PATH} [absolute path to a directory where per-feed rendered markdown files + manifest.json will be written]
--max-items-per-feed={N} [positive integer per-feed item cap; MUST be ≥ 1, see §1.0 Step 3 + §6.0 row max-items-zero]
# Optional power params (omit when not needed):
--days={N} [positive integer recency window in days; default 1]
--max-body-chars={N} [positive integer per-item body truncation cap; default 8000]
Validation rules (before §1.0):
--feed-urls,--raw-output-dir, and--max-items-per-feedmandatory. If any missing, STOP and report which.--feed-urlsmust parse as a non-empty JSON array of strings. Else reject (feed-urls-malformed).- Each entry must be an absolute
http://orhttps://URL. First failing entry → STOP and report (feed-urls-malformed) with the offending value named. --raw-output-dirmust be absolute. Reject relative paths (caller owns path resolution).--max-items-per-feedmust parse as integer ≥ 1. Zero rejected per §6.0max-items-zero.--daysand--max-body-charsmust parse as positive integers when supplied; defaults 1 and 8000.
If any validation fails, STOP and report which field failed.
Global References
RSS_INGEST_SCRIPT = scripts/rss-ingest.py # feedparser-via-Bash leaf engine; one batched run over all feeds
Leaf does not read tool guides at runtime, the documented pitfalls are already encoded inside RSS_INGEST_SCRIPT (browser-UA default load-bearing, Accept-Encoding: identity + gzip fallback, HTML-strip before boundary-aware truncation, per-feed cap + newest-first sort-then-slice as the token governor, recency-window filtering on published/updated dates, dateless-item fallback + items_dropped_dateless surfacing, 403 gating classification cloudflare-waf vs cloudflare-js-challenge, bozo + content-type sniff + empty-body soft-failure surfacing, title-only body_sources:{none} provenance, est_tokens chars/4 budgeting heuristic). RSS fetch is free, $0 per run; the per-feed cap is a context/token governor, not a cost cap.
Caller Isolation
Pure primitive, no agent spawn, no pipeline agents, no telemetry. The skill runs the feedparser fetch + parse + render directly in whatever context invokes it; the caller absorbs the raw-feed context cost (raw feed items land in the caller's window). Callers crawling many feeds, or composing this leaf alongside other work, wrap the invocation in their own isolation:
Agent({subagent_type: "general-purpose", model: "sonnet", prompt: "Invoke /research-crawl-rss with <args>"})
Consumer skills document their own isolation choice. The run-pulse composer wraps each source-type leaf in one classifier sub-agent per source-type.
1.0 Context Capture
Step 1: Verify raw output directory parent exists
Check the parent directory of --raw-output-dir exists. If not, route to §6.0 (raw-output-parent-missing). Do not create parents, caller owns path resolution. The --raw-output-dir itself MAY or MAY NOT exist; §3.0 Step 1 creates it via mkdir -p before the run.
Step 2: Validate feed URLs and derive per-feed slugs
For each entry in --feed-urls: confirm it is an absolute http(s) URL. Build feeds[] preserving caller order. First entry that fails → route to §6.0 (feed-urls-malformed) with the offending value named.
Derive a deterministic slug per feed: lowercase the URL, drop the http:// / https:// scheme, replace each run of non-alphanumeric characters with a single -, strip leading/trailing -, truncate to 50 characters. If two feeds produce the same slug, append -{ordinal} to the later one so each feed maps to a distinct file. The slug becomes the feed's TSV name (and the script's output filename).
Step 3: Validate --max-items-per-feed lower bound
Confirm --max-items-per-feed ≥ 1. If --max-items-per-feed == 0, route to §6.0 (max-items-zero): pass a positive integer. The cap is the effective token governor, the script sorts in-window items newest-first then slices to this count, so a large feed backlog clamps to a bounded payload. RSS fetch is free; this is a context cap, not a cost cap.
Step 4: Build the feeds TSV
Write a tab-separated feed list for the script, one line per feed, {slug}<TAB>{url}<TAB> (empty domain column; the leaf does not classify or tag). Write it to a temp path via mktemp (e.g., RSS_FEEDS_TSV=$(mktemp /tmp/rss-feeds-XXXXXX), no suffix after the X's; BSD/macOS mktemp rejects a template-then-suffix form like -XXXXXX.tsv, and the script reads --feeds-file by path, not by extension). The window (--days), per-feed cap (--max-items-per-feed), and body cap (--max-body-chars) pass directly to the script; no per-feed query construction is needed (feedparser fetches the whole feed and the script windows + caps internally).
2.0 Plan & Confirm
Skill runs autonomously, no user pause. Pure primitive; executes directly in the caller's context. Composed leaves never gate parallelization; direct invocations log the execution config and proceed.
State the planned execution config in one structured log statement, then proceed directly to §3.0:
## /research-crawl-rss: executing crawl
feeds: {feeds[]} # N feed URLs in caller-supplied order
raw_output_dir: {--raw-output-dir}
max_items_per_feed: {--max-items-per-feed} # per-feed cap (token governor)
days: {--days} # default 1
max_body_chars: {--max-body-chars} # default 8000
engine: {RSS_INGEST_SCRIPT} # feedparser via Bash; $0/run
execution: direct (pure primitive, no sub-agent spawn); one batched script run over all feeds
3.0 Crawl
Execute the crawl directly, no sub-agent spawn. Run RSS_INGEST_SCRIPT once over all feeds (it fetches, windows, caps, and renders per-feed markdown), then parse its manifest to build the per-source return. This step writes raw bytes only, it does NOT classify (which domain, which author), summarize, template-fill, deduplicate across feeds, or follow item links to recover missing bodies; the composing router or domain orchestrator handles all downstream transformation. Cross-feed dedup belongs to the dispatcher / classifier layer, not this leaf; title-only feeds whose bodies are genuinely absent surface body_sources:{none} and a downstream layer decides whether to fetch the full body from the item link.
Inputs (from §1.0)
Feeds: {feeds[]} # N feed URLs in caller-supplied order
Feed TSV: {RSS_FEEDS_TSV} # {slug}<TAB>{url}<TAB> per feed
Raw output dir: {--raw-output-dir}
Max items per feed: {--max-items-per-feed}
Window: last {--days} day(s)
Engine: {RSS_INGEST_SCRIPT}
Procedure
Execute these steps in order:
1. Ensure {--raw-output-dir} exists. Use Bash `mkdir -p {--raw-output-dir}`.
2. Write the feeds TSV from §1.0 Step 4 to {RSS_FEEDS_TSV}.
3. Run the engine once, capturing the manifest to disk:
python3 {RSS_INGEST_SCRIPT} \
--feeds-file={RSS_FEEDS_TSV} \
--out-dir={--raw-output-dir} \
--days={--days} \
--max-items={--max-items-per-feed} \
--max-body-chars={--max-body-chars} \
> {--raw-output-dir}/manifest.json
The script writes one markdown file per NON-EMPTY feed into {--raw-output-dir}
(filename {slug}.md) and prints the JSON manifest to stdout, redirected to
{--raw-output-dir}/manifest.json. The manifest lists ALL N feeds, including
empty / gated / errored ones, and is the canonical per-source rollup (the
dispatcher's volume-driven auto-scaler keys on manifest counts + est_tokens,
never on file existence). Empty feeds deliberately produce no md file.
Exit codes: 0 = ran (per-feed failures are captured in the manifest, not the
exit code); 2 = bad invocation (neither --feeds-file nor --url) → §6.0
`script-exit-nonzero`; non-zero otherwise (e.g. ModuleNotFoundError for
feedparser) → §6.0 `script-exit-nonzero` / `feedparser-missing`.
4. Read {--raw-output-dir}/manifest.json. For each record in manifest.feeds[],
derive the per-feed crawl_status:
- failed, record.gated is non-null (use the gated label as diagnostic),
OR record.error is a hard failure: an HTTPError (404 etc.),
"not a feed (...)", "unparseable: ...", or "empty body (...)".
- empty, record.error is "no items in window" or "feed has zero
entries" (feed fetched + parsed cleanly; just no recent items).
An empty window is a clean operational outcome, not a failure.
- clean, record.error is null and record.out_file is present
(items_emitted ≥ 1).
5. Build per_source_outputs[], one entry per feed in caller order:
per_source_outputs[i] = {
feed_url: {feeds[i]},
raw_output_path: {record.out_file}, # null for empty / failed feeds
item_count: {record.items_emitted},
char_count: {wc -c on record.out_file, or 0 when no file},
est_tokens: {record.est_tokens},
crawl_status: {"clean" | "empty" | "failed"}, # per-feed
diagnostic: {record.gated or record.error when failed; otherwise omit},
}
6. Compute aggregate fields:
total_char_count = sum(entry.char_count for entry in per_source_outputs)
total_item_count = sum(entry.item_count for entry in per_source_outputs)
overall_crawl_status = (
"clean" if all entries have crawl_status in {clean, empty}
else "failed" if all entries have crawl_status == "failed"
else "partial"
)
Note: `empty` entries do NOT degrade overall status, an empty window is a
clean outcome. Only mixed clean+failed or all-failed produces
`partial`/`failed` aggregate.
7. Read-back: confirm {--raw-output-dir}/manifest.json exists and lists N feed
records. Confirm one md file exists for each entry with crawl_status: clean.
Mismatch → set overall_crawl_status = "failed" with diagnostic =
"post-write reconciliation failed: expected N manifest records / M clean files".
Capture
After the read-back (step 7), capture the structured summary as CRAWL_RETURN and proceed to §4.0:
raw_output_dir: {--raw-output-dir}
manifest_path: {--raw-output-dir}/manifest.json
char_count: <total across all per-feed files> # aggregate
item_count: <total emitted across all feeds> # aggregate
crawl_status: <clean | partial | failed> # aggregate
window_days: {--days}
per_source_outputs: [<one entry per feed, see step 5>]
diagnostic: <one-line note when crawl_status != clean; otherwise omit>
Do NOT surface raw feed content to the caller, the rendered per-feed markdown is on disk at {--raw-output-dir}. §5.0 returns the reference plus summary only.
4.0 Verify
Step 1: File-state verification
Verify the raw output directory, manifest, and per-feed files:
- Directory exists at
--raw-output-dir(use Bash:test -d {DIR}). -
manifest.jsonexists at--raw-output-dirand parses as JSON withfeeds[]length N. - One md file exists at each
CRAWL_RETURN.per_source_outputs[i].raw_output_pathwhosecrawl_status == clean, and that file is non-empty (char_count > 200, a single rendered item plus the metadata comment exceeds 200 bytes). - Entries with
crawl_statusofemptyorfailedcarryraw_output_path: null(no file), confirm no orphan expectation.
If any check fails (entry status clean but file empty/missing, manifest absent or wrong feed count), route to §6.0 (agent-write-mismatch).
5.0 Summary
Return this structured summary to the caller (typically the /run-pulse composer):
raw_output_dir: {CRAWL_RETURN.raw_output_dir}
manifest_path: {CRAWL_RETURN.manifest_path}
char_count: {CRAWL_RETURN.char_count} # aggregate
item_count: {CRAWL_RETURN.item_count} # aggregate
crawl_status: {CRAWL_RETURN.crawl_status} # clean | partial | failed
window_days: {CRAWL_RETURN.window_days}
per_source_outputs: {CRAWL_RETURN.per_source_outputs[]}
# Each entry: { feed_url, raw_output_path, item_count, char_count,
# est_tokens, crawl_status (clean|empty|failed per-feed), diagnostic? }
diagnostic: {CRAWL_RETURN.diagnostic if present}
Caller decides next action based on overall crawl_status (short-circuit on failed; proceed with extraction iteration on clean or partial) and per-feed crawl_status (skip extraction on entries with crawl_status == failed; treat empty as no-content-this-cycle). The manifest carries per-feed est_tokens + counts for the dispatcher's volume-driven auto-scaler. The leaf does not retry, the caller governs retry policy.
6.0 Error Handling
| Error | Response |
|---|---|
--feed-urls, --raw-output-dir, or --max-items-per-feed missing from $ARGUMENTS | STOP. Report which field is absent. |
feed-urls-malformed, --feed-urls does not parse as a JSON array, OR array is empty, OR any entry is not an absolute http(s) URL | STOP. Report the malformed value (the offending entry when per-entry). Caller passes absolute feed URLs. |
--raw-output-dir not an absolute path | STOP. Report the relative path; caller must resolve. |
max-items-zero, --max-items-per-feed == 0 or not a positive integer | STOP. Report the malformed value; pass a positive integer. The cap is the token governor, an uncapped run is unbounded. |
raw-output-parent-missing, parent directory of --raw-output-dir does not exist | STOP. Report the missing parent path. Do not create, caller scaffolds the directory tree before invocation. The --raw-output-dir itself MAY or MAY NOT pre-exist; §3.0 Step 1 creates it. |
A single feed produces crawl_status: failed (HTTP error / 404, non-feed HTML, unparseable XML, empty body, or a gated:cloudflare-waf / gated:cloudflare-js-challenge classification) | The script captures the failure in that feed's manifest record and continues the batch, no md file is written for it. The failure is isolated; aggregate becomes partial (or failed only if all feeds failed). Per-feed diagnostic (the gated label or error string) surfaces in per_source_outputs[]. A js-challenge feed is unreachable via any static fetcher, route it to another lane, do not retry here. |
Crawl produces overall crawl_status: partial (some feeds succeeded, others failed) | Proceed to §5.0 normally. Return surfaces overall partial + per-feed detail in per_source_outputs[]. Caller iterates the array, drops failed entries from downstream extraction, continues with clean entries. |
Crawl produces crawl_status: empty for one or more feeds | Proceed normally. Per-feed entry surfaces item_count: 0 with raw_output_path: null (valid no-recent-items result; the feed was reachable). Empty does NOT degrade aggregate status. |
script-exit-nonzero, the script exits 2 (neither --feeds-file nor --url passed, indicates a leaf bug in TSV construction) or any non-zero code | Surface the script's stderr verbatim. Exit 2 means the TSV was empty or the --feeds-file path was wrong, verify §3.0 Step 2 wrote the TSV. Do not retry without correcting the invocation. |
feedparser-missing, the script aborts with ModuleNotFoundError: feedparser | STOP. The runtime dependency is absent. Report the install command pip install feedparser; the leaf cannot run without it. |
agent-write-mismatch, a record reports crawl_status: clean but the md file is empty/missing, OR manifest.json is absent / lists a feed count ≠ N | Surface the mismatch in the return diagnostic. Set the affected entry's crawl_status: failed; recompute overall status. File-state on disk (manifest + md files) is authoritative. |