Umbrella for all wiki ingestion pipelines — newsletter ingest, blog ingest, active knowledge crawl, arXiv paper pipeline, OpenAI blog ingestion, dreaming knowledge consolidation, and pipeline troubleshooting.
Resources
1Install
npx skillscat add kzinmr/ai-topics/wiki-ingestion-pipelines Install via the SkillsCat registry.
Wiki Ingestion Pipelines (umbrella)
This umbrella skill covers all automated wiki ingestion pipelines — from external source to wiki page. Each section below covers one pipeline end-to-end, including cron configuration, checkpoint handling, and failure recovery.
All pipelines follow the same pattern:
fetch external content → checkpoint → triage → wiki-ingest (create/update pages) → commit📖 X-scan:
references/x-accounts-scan-cron.md. Cron-mode:references/cron-mode-constraints.md.
📖 The newsletter wiki-ingest execution details (triage parse-failure recovery, take body-fetch patterns, enrichment + archive + commit/push specifics) live inreferences/newsletter-wiki-ingest.md(withreferences/newsletter-take-body-fetch-pattern.mdandreferences/newsletter-triage-recovery-2026-08-20.mdas supporting references). The standalonenewsletter-wiki-ingestskill was consolidated into this umbrella on 2026-08-21.
Section A: Newsletter Pipeline (newsletter-wiki-ingest)
📖 For URL resolution patterns (Substack, beehiiv, Cloudflare blocks) and classification heuristics, see
references/newsletter-triage-url-resolution.md
Consume a pre-triaged checkpoint JSON from the newsletter-triage cron job and create/update wiki pages autonomously.
Pipeline Chain
newsletter-ingest (07:10 UTC) → newsletter-triage (07:20 UTC) → newsletter-wiki-ingest (07:40 UTC)Input Format
Triage checkpoint JSON is injected via context_from cron chaining, or available at:
${HERMES_HOME}/cron/data/newsletter/triage_latest.json
⚠️ Recovery:
references/newsletter-triage-checkpoint-recovery.md+references/git-push-concurrent-pipeline-pitfall.md.
Workflow
Orient on wiki: read SCHEMA.md, index.md, recent log.md
Load the checkpoint — filter to
recommended_action === "take"decisionsDetect prior batch — scan log.md for same source/newsletter title
Process each take decision:
- ★★★★★ → New concept page
- ★★★★☆ → Update existing page
- ★★★☆☆ → Entity page update
- ⚠️ Independently verify mid-star ratings: A triage agent may rate ★★★☆☆ (Reference) content that genuinely fills a wiki gap and should be ★★★★☆ (Take). Before accepting Reference decisions, check whether the article body contains content not present in any existing entity/concept page. If it does, upgrade to Take. Key signals: (a) the page exists but lacks the article's specific methodology/claim/data, (b) the article introduces a novel conceptual angle not covered by any existing page, (c) the article is paywalled/truncated but the free preview reveals substantive technical claims.
Check reference items for triage-recommended enrichment — Before falling through to [SILENT], scan the decisions array for reference items whose reason_ja explicitly recommends enrichment (signals: timeline update, minor reference addition). These are actionable even at the reference level. Execute the recommended minor enrichment (1-5 line addition).
Create new pages first (write_file), then update existing (patch), then index.md and log.md
Commit and push:
cd ~/ai-topics && git add wiki/ && git commit -m "wiki: newsletter ingest ..." && git pushIf no take decisions AND no triage-recommended reference enrichments were executed, respond [SILENT]
Log.md prepend pitfall — header displacement
When writing a log entry to log.md in the newsletter-wiki-ingest step, use new_entry + old prepend. But the log.md starts with a multi-line header (# Wiki Log\n\n_Log of all wiki changes...) that MUST stay at the top. The naive approach:
with open(LOG_PATH) as f: old = f.read()
with open(LOG_PATH, 'w') as f: f.write(new_entry + old)pushes the header down — the new entry appears before the title. Always verify that the header is at line 1 after writing. If displaced, use the validated strip-all-variants rebuild + 3-invariant assertion in references/log-prepend-header-repair.md — the old inline re-order snippet is fragile (duplicate # Wiki Log headers on spacing mismatch; drops just-prepended entry when a prior pipeline already displaced the header). Never chain two mutating log scripts — one script, assert header@line1 + exactly-1-header + entry-present. Reusable script: scripts/prepend-log-entry.py handles this correctly (may be absent on disk — then use the reference pattern above).
Parallel enrichment via delegate_task (Batch Pattern)
When the newsletter-wiki-ingest step has 8+ takes (creates + enrichments), parallelize using delegate_task with the tasks array:
- Batch 1: Create 3 new pages concurrently (full-page writes — independent)
- Batch 2: Enrich 3 existing pages concurrently (use
patch, notwrite_file) - Batch 3: Enrich remaining 2 + references (3 tasks max per batch)
Each subagent must get explicit context with the article body, current page content, and the instruction to use patch not write_file for existing pages >40 lines. This pattern completes ~8 enrichment operations in ~70 seconds wall-clock time.
Failure Recovery
Case A: Checkpoint missing entirely (no file at triage_latest.json).
Read the triage failure output file — it contains the embedded newsletter-ingest checkpoint with candidates array. Parse candidates, resolve URLs, assign star ratings, process.
Case B: Checkpoint file ok: false (file exists but ingest failed).
Read the triage failure output file for the embedded candidates array. Follow the same recovery path as Case A.
Case C: Checkpoint EXISTS with valid data, but cron output parse failed (most insidious — the triage agent saved JSON correctly to triage_latest.json but its final response was wrapped in markdown by the cron runner, causing downstream JSON extraction to fail).
⚠️ Case C0 — Stale triage_latest.json (newsletter variant): The file exists and has a valid decisions array, but it's from YESTERDAY'S batch. The newsletter-triage agent processed the previous day's checkpoint (e.g., checkpoint_run_id: 20260617T...) while today's newsletters sit untriaged in latest.json.
Detection: Always check checkpoint_run_id in the triage JSON against today's ingest run_id:
import json
triage = json.load(open("/opt/data/.hermes/cron/data/newsletter/triage_latest.json"))
ingest = json.load(open("/opt/data/.hermes/cron/data/newsletter/latest.json"))
# If checkpoint_run_id is yesterday's date, the triage is stale
if triage.get("checkpoint_run_id", "").startswith("20260617"): # yesterday
print("STALE TRIAGE — must re-triage from today's latest.json")Recovery (Case C0 — stale checkpoint): Perform combined triage+wiki-ingest inline:
- Read
triage_latest.json— note its stale timestamp. Do NOT use its decisions. - Read
latest.json(today's ingest) — extract the newsletter candidates - Resolve newsletter post URLs (Substack open.substack.com, beehiiv tracking links) via
web_extractorcurl+<article>extraction - Perform triage independently: assess AI relevance, check existing wiki coverage, assign star ratings
- Save the new triage JSON to
triage_latest.json(overwriting the stale data) - Process takes: create entity/concept pages, enrich existing pages
- Update index.md, log.md, archive skip/reference items
- Commit and push:
cd ~/ai-topics && git add wiki/ && git commit -m 'wiki: newsletter ingest ...' && git push
Concrete example (June 18, 2026): Newsletter-triage processed yesterday's 6 historical newsletters (Takes=0) while today's 4 newsletters were untriaged. Newsletter-wiki-ingest detected stale checkpoint_run_id: 20260617T071109Z, independently resolved all 4 URLs, found 2 takes (Midjourney Medical → entities/midjourney.md, Radical AI → entities/radical-ai.md) and 1 reference (State of blog → entities/nathan-lambert.md enrichment). Archive: 2 items, total 872 URLs. See references/newsletter-wiki-ingest-session-2026-06-18.md.
Case C1 — Triage committed wiki changes inline: commit message ≠ proof of page edits — run git show --stat <commit>; archive-only commit → proceed as Case C (archive already committed, skip archive_triage.py). See references/newsletter-wiki-ingest-2026-08-04-archive-only-commit.md (validated 2026-08-04).
Combined triage+wiki-ingest pattern (same as blog pipeline): The newsletter-triage agent may perform wiki-ingest inline — creating/updating/committing entity and concept pages in the same session. When the downstream newsletter-wiki-ingest job (07:40 UTC) recovers from a Case C checkpoint, check git log first:
git log --oneline -3If the triage agent already committed wiki changes (same-day commit matching the batch), skip wiki page creation and add only a log.md recovery note. The archive step will run as dedup.
- First check: Read
triage_latest.jsonat${HERMES_HOME}/cron/data/newsletter/triage_latest.json. If it contains a validdecisionsarray, the triage WORKED — use it directly. No need to re-scrape newsletter URLs. - Independent verification: The triage agent may have under-rated content (e.g., Reference for genuinely novel content that should be Take). Independently read the newsletter post body or existing wiki pages to confirm rating accuracy before accepting Reference/Skip decisions.
- Upgrade if warranted: If independent verification shows the content fills a genuine wiki gap, upgrade the decision from Reference to Take and proceed with wiki ingestion.
- Nuance — reference enrichment without upgrading: Even when a reference (★★★☆☆) does not warrant full ★★★★☆ upgrade, it may still justify a minor enrichment (1-5 line addition to an existing page). This applies when the triage notes "fable-5.mdへの軽微な参照追記(1-2行)に値する" — the triage explicitly recommends the addition but rates it ★★★☆☆ because the content is a perspective framing or supplementary detail, not a new fact. In this case, execute the minor enrichment without changing the star rating. The triage was correct; you're just following its recommendation.
- Concrete signal: If the triage's
reason_jasays "軽微な参照追記" (minor reference addition) or "1-2行" (1-2 lines), it's a clear recommendation to enrich despite the ★★★☆☆ rating. Seereferences/newsletter-wiki-ingest-session-2026-06-13.mdfor the validated session.
- Log differently: Use
newsletter-triage output parse failed but checkpoint valid; wiki-ingest verified and processed independently. If 0 takes but references were enriched, log each enrichment separately.
Why Case C matters: The triage agent's cron output is always wrapped in markdown by the scheduler. A downstream JSON parser that reads the
.mdoutput file will fail, but the triage_latest.json saved viaexecute_codeorwrite_file/terminaldance is clean. Always check the checkpoint file before assuming the triage failed.
See references/newsletter-wiki-ingest.md for full Substack URL resolution patterns and State A/B/C handling.
Case C Differentiators (Newsletter Pipeline)
| Scenario | triage_latest.json |
Decision |
|---|---|---|
| Case A | Missing entirely | Re-ingest from scratch |
| Case B | File exists but ok: false |
Re-ingest from embedded candidates |
| Case C | Exists, today's date, cron parse failed | Use decisions, independent verification |
| Case C0 | Exists but stale (yesterday's data) | Must re-triage from today's latest.json |
| Case C1 | Exists, today's date, agent committed inline | Skip wiki work, add log recovery note |
Key Pitfalls
- ⚠️ Case C0 is the most insidious —
triage_latest.jsonlooks valid (Takes=0, valid JSON) but is from yesterday. Always checkcheckpoint_run_idagainst today's date. - Inline triage is expensive but necessary for Case C0. You must resolve newsletter URLs and assess content independently. Use
delegate_taskbatch mode for parallel URL resolution (max 3 concurrent). - Detect follow-up batches before creating pages
- Subagents need explicit absolute paths (
/opt/data/ai-topics/wiki/...) - Japanese output is mandatory for cron reports; see
references/cron-terminal-gotchas.md - Commit early for large batches to prevent data loss from tool call limits
- blog-triage JSON is from yesterday at newsletter-triage time: Newsletter-triage runs at 07:20 UTC, but blog-triage runs at 07:30 UTC. When checking
${HERMES_HOME}/cron/data/blog_ingest/triage_latest.jsonfor cross-pipeline dedup, that file is still from the previous day. For same-day dedup, checkraw/articles/(sitemap-monitor at 06:00) andlog.mdinstead. Seesemantic-article-groupingskill for full race condition documentation.
See references/newsletter-wiki-ingest.md for full workflow details.
Section B: Active Knowledge Crawl (active-knowledge-crawl)
Daily cron job that proactively researches and ingests new concepts based on config/hot-topics.yaml.
Trigger
Scheduled cron job, or manual invocation by user.
Workflow
- Select Topics: Read hot-topics.yaml, extract topics with stale
last_crawled(>3 days) - Gap Discovery (optional when few stale topics): Survey major AI domains not in hot-topics.yaml
- Research: For each topic, crawl prerequisites, laterals, or deep-dives
- Create Wiki Pages: Web search → save raw source → create concept page → update index/log
- Update hot-topics.yaml: Set
last_crawled: YYYY-MM-DD - Commit:
cd ~/ai-topics && git pull --rebase && git add wiki/ config/hot-topics.yaml && git commit && git push
Constraints
- Max 2 concepts per topic, max 6 total per run
- Source file in raw/articles/ REQUIRED before creating concept page
- Depth-1 only (grandchildren out of scope)
- arXiv-only (not peer-reviewed) papers FORBIDDEN as sources
- Git push may fail in cron — report status clearly
Critical Lessons
- Files may already be committed (duplicate run detection): check
git ls-filesfirst - Verify files exist after
delegate_task: explicit file existence check git pull --rebasefails with unstaged changes from sibling processes: If your commit already succeeded butgit pull --rebasefails because other agents' modifications remain unstaged:- Check if there's actual remote divergence:
git log --oneline HEAD..origin/main— if empty, there is none - If no divergence, skip the
--rebaseandgit pushdirectly — the commit is already clean and can be pushed as-is - If there IS divergence, use
git stash && pull --rebase && stash pop(don't rebase on top of unstaged changes)
- Check if there's actual remote divergence:
- YAML via str.replace fragil — use sed with line numbers for hot-topics.yaml updates
- Avoid
git add -Awhen sibling agents write to same repo — use selectivegit add
Section C: OpenAI Blog Ingestion (openai-blog-article-ingestion)
Simple workflow for ingesting openai.com/blog articles.
Workflow
- Scrape & Save:
web_extract(url)→ save towiki/raw/articles/{date}-{slug}.md - Check existing pages → patch existing concept/entity or create new
- Update index.md and log.md
- Commit:
cd ~/ai-topics && git add wiki/ && git commit -m "wiki: ingest OpenAI blog article - {topic}" && git push
Pitfalls
- OpenAI blog URLs may have
/index/path prefix - openai.com returns 403 without User-Agent header:
curlbare requests get blocked by Cloudflare. Add-A "Mozilla/5.0..."header. Seereferences/site-specific-fetchability.md→ openai.com section for the exact command. - Don't create duplicate pages
- Create minimal stub entity pages for newly mentioned people/organizations
Section D: arXiv Paper Pipeline (arxiv-paper-pipeline)
Workflow for pulling arXiv papers, triaging by peer-review status, and ingesting into wiki.
Save Path
Always save to ~/wiki/raw/papers/ (NOT ~/wiki/raw/articles/).
Naming: {YYYY-MM-DD}_{arxiv_id}_{short-title}.md
Triage Decision Matrix
| Paper Type | Action |
|---|---|
| Peer-reviewed conf/journal (NeurIPS, ICML, ICLR, ACL, CVPR, JMLR, TACL, Nature, Science) | ✅ Wiki-ingest OK |
| Tech company/industry research lab tech report (OpenAI, Meta, Google, MS, Anthropic, Huawei, Apple, Amazon, NVIDIA, and similar) | ✅ Wiki-ingest OK |
| arXiv-only (no venue) | ❌ BLOCK |
| User explicitly requests blocked paper | ✅ User override — ingest with blocked_reason note |
Peer-Review Detection
- Check abstract page for "Published in", "Accepted to"
- Search Semantic Scholar for
publicationVenue - If no venue found → mark as blocked
Processing Steps
- Search arXiv API or Semantic Scholar
- For each candidate: fetch metadata → research peer-review → apply triage → save or block
- If accepted: save to papers/ → create/update wiki page
- Integrate user-provided context (tweets, discussions) alongside paper content
Name Collision Handling (RLM and similar proliferating frameworks)
When a paper's framework name collides with an existing concept page (e.g., Huawei's lambda-RLM vs Galanos's Lambda-RLM):
- Detect collision early —
search_filesfor the framework name in existing concept slugs and content before creating pages - Create a new concept page with a distinct, descriptive slug (e.g.,
typed-rlminstead of reusing the conflictinglambda-rlm) - Add frontmatter aliases on the new page to capture the paper's name:
aliases: [original-name, Y-Combinator X, etc.] - Add a disambiguation warning to the EXISTING page's top — brief note with wikilink to the new page
- Build a comparison table on the new page showing control model, formal proofs, empirical scope, source lineage
- Update the parent concept page (e.g.,
rlm-recursive-language-models) to list both as named variants - Update log.md — explain the collision, the resolution, and the comparison
See references/arxiv-paper-pipeline.md for further detail on blocked paper handling and JSON format.
Section D2: Non-arXiv PDF Paper Ingestion (pdf-paper-ingestion)
Ingest papers from non-arXiv sources (company CDN PDFs, whitepapers, system cards) into wiki/raw/papers/. Covers: PDF download → PyMuPDF text extraction → frontmatter → "Raw Papers" index section → commit.
See references/pdf-paper-ingestion.md for the full workflow, naming conventions, and pitfalls.
When the user asks to compare two papers or system cards, see references/paper-comparison-workflow.md for the end-to-end workflow (download → extract → compare → commit).
Section E: Blog Pipeline Troubleshooting (blog-ingest-troubleshooting) — references/blog-triage-checkpoint-recovery.md
Debug and fix the full blog/newsletter cron pipeline chain.
Pipeline Architecture
ingest ──checkpoint──▶ triage ──checkpoint──▶ wiki-ingestCheckpoint File Locations
| Pipeline | Ingest checkpoint | Triage checkpoint |
|---|---|---|
| Blog | ~/.hermes/cron/data/blog_ingest/latest.json |
~/.hermes/cron/data/blog_ingest/triage_latest.json |
| Newsletter | ~/.hermes/cron/data/newsletter/latest.json |
~/.hermes/cron/data/newsletter/triage_latest.json |
| Dreaming | ~/.hermes/cron/data/dreaming/latest.json |
~/.hermes/cron/data/dreaming/triage_latest.json |
Most Common Failure: "Checkpoint Cascade"
- Ingest script times out → checkpoint stays stale
- Triage reads old checkpoint → sees 0 articles → nothing output
- Wiki-ingest reads empty triage →
[SILENT]
Combined Triage+Wiki-Ingest Pattern (blog pipeline) — Two Case C Sub-Patterns
The blog-triage cron agent may either (a) perform wiki-ingest inline (committing page changes during its run), or (b) produce a clean decisions array in triage_latest.json for downstream processing. The downstream blog-wiki-ingest (07:50 UTC) must handle both.
Case C1 — Triage committed wiki changes inline (triage agent's prompt includes wiki-ingest steps):
triage_latest.jsonexists with decisionsgit log --oneline -3shows a same-day triage commit (e.g., "wiki: blog ingest ...")- Take decisions are already in the wiki — no page work needed for takes
- ⚠️ Check reference items for unenriched targets: The triage agent may have processed takes but MISSED reference (★★★☆☆) enrichments. Scan the triage
decisionsfor reference items that have non-emptytarget_pages. For each such item:- Read the target entity page — does it already contain the article's content?
- If the entity page exists but lacks the article's specific contribution (only a URL in
sourcesorReferences, no body summary), enrich it with a brief entry (3-5 lines) - Update the page's
updatedfrontmatter date and add the source path tosources - Concrete example (June 2026): george-hotz reference for "Summoning the Demon" (Jun 17) was missed by the triage agent's inline commit despite having
target_pages: [entities/george-hotz]. Enrichment added one notable-posts bullet line, updatedupdateddate, and added the raw article source path.
- Recovery: add log.md recovery note (including reference enrichments), archive runs as dedup
Case C2 — Triage produced decisions only, no inline commit (this session's pattern):
triage_latest.jsonexists with valid decisionsgit log --oneline -3shows NO same-day triage commit- Take decisions need to be processed from the checkpoint
- Reference enrichment check: Reference items whose
reason_jarecommends enrichment (signals: "追記推奨", "言及する価値あり", timeline update,candidate_wiki_pathpopulated) are actionable even when Takes=0 — enrich via patch only (1-12 lines, updateupdated+sources). References CAN parallelize with takes on DIFFERENT files (validated Aug 2026); sequential only for same-file targets. - Recovery: read
triage_latest.json, process takes via parallel subagents (batch 3+2 for 5 takes), then reference enrichments (see Case C2 reference-check bullet re parallelization), update log.md, commit
Common recovery procedure for blog-wiki-ingest on Case C (both sub-patterns start with the same first step):
- Verify by reading the checkpoint: confirm the decisions array is valid with recommended_action fields
- Check
git log --oneline -3— same-day triage commit? If yes, classify bygit show --stat <commit>:- Inline wiki edits (C1): commit touches entity/concept/event pages → skip wiki page creation; add only a log.md recovery note; archive runs as dedup.
- Archive-only (still C2): commit touches only
wiki/raw/archived/triage/+archive_index.json(message like "archive skip/reference items (N takes flagged for wiki-ingest)") → takes are flagged but NOT processed; proceed to step 3. Seereferences/blog-wiki-ingest-archive-only-commit.md.
- Read each take's
candidate_wiki_pathfrom the decisions array. Thebody_excerptandreason_jafields provide enough context for enrichment. Also scan reference items — if a reference'sreason_jaexplicitly recommends enrichment (signals: "追記推奨", "言及する価値あり",candidate_wiki_pathpopulated), treat it as actionable. - Process takes via parallel subagents in 2 batches (batch 1: 3 tasks, batch 2: remaining). Each subagent receives raw article path + existing page content + exact insertion points.
- Process reference enrichments — sequential by default, but references CAN batch in parallel with takes when targeting DIFFERENT files (validated Aug 2026: cory-doctorow take + lcamtuf reference, zero conflicts). Real constraint is file-conflict avoidance. Update frontmatter (
updated,sources) per page; on "Found 2 matches" for sources, use the preceding source path as context. - Update log.md with enrichment summary. Commit all changes.
- Archive: runs as dedup (triage agent already archived skip/reference items).
Concrete example (June 11, 2026): 5 takes (simonwillison.net ×3, garymarcus, johndcook) → enriched mythos/simon-willison/gary-marcus/john-d-cook/gemma-family in 2 batches (~80s).
See references/blog-ingest-troubleshooting.md section "Triage cron output parse failed (but checkpoint IS valid)" for the full Case C recovery procedure.
Re-executing a Pipeline
- Run ingest jobs first (blog + newsletter concurrently)
- Then triage jobs
- Then wiki-ingest jobs
⚠️ cronjob(run) is async — run ingest scripts directly from terminal if cron scheduler fails:
| Pipeline | Script path |
|---|---|
| Blog ingest | python3 ~/.hermes/scripts/blog_ingest.py |
| Newsletter ingest | python3 ~/scripts/process_email.py |
Stage-Specific Issues
Ingest:
- Missing
daily_inbox_collectmodule → create stub module - Wrong DB path → use
~/.blogwatcher/blogwatcher.db - Pre-run script timeout → parallelize with ThreadPoolExecutor, write checkpoint before scraping
- CRITICAL: SQLite
is_readdedup pattern — The blogwatcher DB (~/.blogwatcher/blogwatcher.db) hasarticlestable withis_read(boolean) anddiscovered_date(date) columns. Ifblog_ingest.pytimes out at 120s, the most likely cause isquery_todays_articles()indaily_inbox_collect.pylacking a date filter, causing it to fetch ALL articles (thousands of rows) instead of just today's. The fix:- Ensure SQL query includes
WHERE discovered_date >= date('now', '-1 day') AND is_read = 0 - After successful scrape+save in
blog_ingest.py, callmark_articles_as_read()to setis_read = 1 - This prevents duplicate processing on subsequent cron runs
- The
daily_inbox_collectmodule lives at~/.hermes/scripts/daily_inbox_collect.py - After fixing scripts, re-run the pipeline and
cd ~/ai-topics && git add wiki/ && git commit && git push
- Ensure SQL query includes
Triage: Reads from ingest checkpoint. Empty checkpoint → no output. May also produce markdown report instead of valid JSON — downstream wiki-ingest will fail with parse error. See references/blog-ingest-troubleshooting.md for recovery.
Wiki-ingest: Reads from triage checkpoint. No take decisions → [SILENT].
Script Dual Location
Scripts live in TWO locations that must be kept in sync:
| Location | Purpose | Git-tracked? |
|---|---|---|
~/ai-topics/scripts/ |
Source of truth | ✅ Yes |
~/.hermes/scripts/ |
Cron execution copy | ❌ No |
When fixing: edit ai-topics/scripts, then cp to .hermes/scripts.
Section F: Dreaming — Knowledge Consolidation Cycle (dreaming)
Automated consolidation process analyzing recently collected articles and folding significant findings into the wiki.
Pipeline
- Phase 1 (pre-run script):
~/ai-topics/scripts/dreaming.pycollects RSS scan articles, newsletters, existing wiki pages - Phase 2 (LLM processing): Analyzes, creates/updates wiki pages, commits
Workflow
- Duplicate Check: Review what adjacent scheduled jobs already completed (daily inbox update, active crawl, etc.)
- Light Sleep (Screening): Review articles not already processed, group by semantic themes
- REM (Flat Synthesis): Score each theme using weighted signals (relevance 0.30, frequency 0.25, query_diversity 0.15, recency 0.15, consolidation 0.10, conceptual_richness 0.05)
- Score ≥ 0.65: Create or update wiki page
- Score 0.45-0.65: Add to existing page or log for review
- NJ Delivery Filter: Apply Newsjacking lens (0-5) to select what to deliver
- NJ ≥ 4: Lead story; NJ = 3: Secondary; NJ ≤ 1: Omit from delivery
- Deep Sleep (Replay-safe integration): Create/update wiki pages, cross-references, index/log, commit
Sub-Patterns
- Pattern A: Existing coverage depth check — don't auto-update, check if page already covers the insight
- Pattern B: Newsletter noise filtering (Substack UI elements, redirect chains)
- Pattern C: Batch entity discovery — create missing entity pages for recurring people/companies
- Pattern D: Duplicate detection matrix (filename, index entry, content grep, session_search)
0-Article Recovery Workflow (Shell Commands)
When the dreaming checkpoint reports collected_articles=0, raw articles may still exist that other pipelines didn't consume. Use this concrete workflow:
Step 1: Count recent raw articles
find ~/wiki/raw/articles -name "*.md" -mtime -3 -size +500c | wc -lStep 1.5: Cross-pipeline dedup check (FIRST — saves the most time)
Before scanning raw articles, check the latest blog triage JSON. This immediately rules out the entire blog-ingest batch (typically 15-20 articles already decided as skip/reference), catching ~70% of raw articles from the blog pipeline.
# Check blog triage exists
ls -la ~/.hermes/cron/data/blog_ingest/triage_latest.json
# Also check newsletter triage
ls -la ~/.hermes/cron/data/newsletter/triage_latest.jsonRead the triage JSON with a Python script (pipe_to_interpreter blocked in cron mode — use write_file to /tmp/ then terminal python3):
import json, os
blog_path = os.path.expanduser("~/.hermes/cron/data/blog_ingest/triage_latest.json")
with open(blog_path) as f:
d = json.load(f)
for x in d.get("decisions", []):
print(f"{x['recommended_action']}: {x.get('source_name','')} - {x.get('title','')[:60]}")Articles already decided in blog/newsletter triage should be marked as skip (already captured by blog pipeline) before proceeding to full analysis. This is the single most time-saving step in the recovery workflow.
Step 2: Find genuinely unprocessed articles
find ~/wiki/raw/articles -name "*.md" -size +500c -mtime -3 | while read f; do
base=$(basename "$f" .md)
count=$(grep -rl "$base" ~/ai-topics/wiki/entities/ ~/ai-topics/wiki/concepts/ ~/ai-topics/wiki/log.md 2>/dev/null | wc -l)
if [ "$count" -eq 0 ]; then
size=$(stat -c%s "$f")
echo "UNPROCESSED: $base ($size bytes)"
fi
doneThis checks each article filename against entity pages, concept pages, AND log.md. An article is "unprocessed" only if zero references exist anywhere.
Step 3: Filter by AI relevance
Read each unprocessed article's first 50+ lines. Skip:
- Vintage computing, math, F1, politics, general security (non-AI)
- Event announcements, marketing promos (low wiki value)
- Link blog posts already covered by another source (check krebsonsecurity, simonwillison references)
Step 4: Check existing entity page coverage
First verify entity page exists, then check content depth:
# Quick existence check (faster than grep)
ls ~/ai-topics/wiki/entities/<entity>.md 2>/dev/null && echo "EXISTS" || echo "MISSING"
# Content depth check
grep -E "^##" ~/ai-topics/wiki/entities/<entity>.md
# Also check for article-specific keywords
grep -i "keyword-from-article" ~/ai-topics/wiki/entities/<entity>.mdIf the entity page exists but lacks the article's specific content → enrichment candidate (TAKE/REFERENCE).
6. Build triage JSON
Since execute_code is blocked in cron mode, use write_file to /tmp/dreaming_triage.py then terminal python3 /tmp/dreaming_triage.py. Key: use None (Python) not null (JS) for optional fields.
7. Process takes via parallel subagents
When the triage has 3+ take decisions, process them in parallel using delegate_task with batch mode (up to 3 concurrent tasks):
Batch breakdown strategy (proven pattern, 6 June 2026):
- Batch 1: New concept pages (write_file) + 1 entity enrichment (patch) — 3 tasks
- Batch 2: Remaining entity enrichments (patch) — remaining tasks
- Each subagent receives full article content + existing page content + exact insertion points
- After batch completion, verify all files exist, then update index.md and log.md
Pitfall: Subagent wikilink format drift. Subagents may use non-standard wikilink formats like [[Entity(name)]] instead of the canonical [[entities/name]] or [[concepts/name]]. Always verify new page content after creation and fix any malformed wikilinks. Specific pitfall (June 2026): subagent created [[Entity(elevenlabs)]] instead of the correct format — this was caught during index.md insertion but could have gone unnoticed in log.md.
Pitfall: Unclosed brackets. Subagents may create wikilinks without closing the ]] — always grep for [[ in newly created pages and verify each one has a matching ]].
Pitfall: Language policy in subagent-created pages. Subagents may include Japanese summary sections (## 日本語まとめ) in concept pages even though the wiki language policy bans non-English content in pages outside raw/. After creating new pages, verify no Japanese characters exist before committing. Use grep -Pn '[\x{3000}-\x{9FFF}]' to scan for CJK characters in non-raw pages.
Step 6: Archive skip/reference items
After saving the triage JSON, archive skip and reference decisions for later re-evaluation:
cd ~/ai-topics && python3 scripts/archive_triage.py dreaming --keep-referenceCase C Recovery (Cron Output Parse Failed)
The dreaming pipeline shares the same Case C recovery pattern documented in Section A (Newsletter) and Section E (Blog). When the dreaming-group agent's cron output is wrapped in markdown by the scheduler, downstream JSON extraction fails — but triage_latest.json may or may not have been saved to disk correctly.
⚠️ Case C0 — Stale triage_latest.json (NEW sub-pattern)
The most insidious variant: triage_latest.json exists and has a valid decisions array, but it's from YESTERDAY. The dreaming-group agent never saved today's output to checkpoint. A quick read shows "6 decisions, Takes=0" and looks fine — but it's stale data.
Detection: Always check the triage_timestamp or checkpoint_run_id field in the triage JSON. If it's not today's date, the file is stale:
import json
d = json.load(open("/opt/data/.hermes/cron/data/dreaming/triage_latest.json"))
ts = d.get("triage_timestamp", d.get("checkpoint_run_id", "none"))
# Check against today: "2026-06-14" should appear in the timestamp
print(f"Triage timestamp: {ts}")If stale, proceed to the cron output file recovery path (Case C3 below).
Differentiators from other cases:
| Scenario | triage_latest.json |
Decision |
|---|---|---|
| Case A | Missing entirely | Re-ingest from scratch |
| Case B | File exists but ok: false |
Re-ingest from embedded candidates |
| Case C1 | Exists, today's date, agent committed inline | Use decisions, skip wiki work, add log |
| Case C2 | Exists, today's date, agent did NOT commit | Process takes from decisions |
| Case C0 (new) | Exists but stale (yesterday's data) | Must recover from cron output |
| Case C3 | Missing — triage never saved anything | Recover from cron output file |
Checkpoint Paths
| Pipeline | Ingest checkpoint | Triage checkpoint |
|---|---|---|
| Dreaming | ~/.hermes/cron/data/dreaming/latest.json |
~/.hermes/cron/data/dreaming/triage_latest.json |
Case C3 — Recovery from Cron Output File (when triage_latest.json is stale or missing)
When the dreaming-group agent's cron output is known to exist (from ls on the output directory) but triage_latest.json is stale or missing:
- Read the output file: Typically at
/opt/data/.hermes/cron/output/<job-id>/YYYY-MM-DD_HH-MM-SS.md - Find the embedded JSON: The cron output file is ~3500 lines — the first ~3480 lines are the cron prompt and skill instructions. The dreaming-group agent's response starts at the
## Responseheading, followed by a```jsonblock near the very end of the file. **Skip to the last 100 lines**:tail -100 /opt/data/.hermes/cron/output/<job-id>/YYYY-MM-DD_HH-MM-SS.md. Search for"checkpoint_run_id"or"summary_ja"to locate the JSON. - Extract the decisions — the JSON has the same structure as a triage checkpoint:
decisions[]array withrecommended_action,reason_ja,body_excerpt,candidate_wiki_path. - Check git log:
git log --oneline -3— look for a same-day dreaming commit. The dreaming-group agent may have committed inline even though it didn't save checkpoint. If same-day commit exists → Case C1, skip wiki work, add log recovery note. - If no same-day dreaming commit: The agent produced decisions but didn't save or commit. Save the recovered JSON to
triage_latest.jsonyourself:- Reconstruct the full JSON with
ensure_ascii=False(contains Japanese text) - Save to
/opt/data/.hermes/cron/data/dreaming/triage_latest.json - Then proceed as normal: verify decisions, process takes, archive, commit
- Reconstruct the full JSON with
Case C1 — Triage agent committed wiki changes inline
The dreaming-group agent's prompt includes wiki-ingest steps. When it runs, it may create/enrich/commit wiki pages during the triage session. The downstream dreaming-wiki-ingest (18:20 UTC) must detect this:
- First check: Read
triage_latest.jsonat~/.hermes/cron/data/dreaming/triage_latest.json. Check its timestamp — if it's not today's date, the file is stale (see Case C0 above). Use the cron output file recovery path (C3) instead. - Check git log:
git log --oneline -3— look for a same-day dreaming commit (e.g.,dreaming: consolidation YYYY-MM-DD — N articles...) - Independent verification: Verify each decision by checking the target wiki page's content depth (line count, keyword grep for article-specific claims). Do not assume skip decisions are correct — confirm that the existing page genuinely covers the article.
- If Case C1 (triage committed inline):
- Skip wiki page creation (already done by triage agent)
- Add a log.md recovery note recording the Case C recovery
- Run
archive_triage.py dreaming --keep-reference(will return dedup — all items already archived by triage agent) - Commit and push the recovery note
- If Case C2 (triage produced decisions only, no inline commit):
- Follow the same
takeprocessing workflow as Section A (parallel subagents, enrich pages, update index/log) - See Section A's Case C recovery for the full workflow
- Follow the same
Differentiator (C1 vs C2): The dreaming-group agent's prompt typically includes wiki-ingest steps (it's a single combined triage+ingest agent). C1 is the EXPECTED case — the triage agent almost always commits inline. C2 only occurs if the agent's prompt separates triage from ingest, or if the agent times out before committing.
Concrete Example (June 13, 2026)
- Dreaming-group collected 6 RSS articles from 2026-06-09
- Triaged all 6 as skip (already covered by existing wiki pages: sample-efficiency.md 127 lines, siri-ai.md 149 lines, ed-zitron.md 470 lines, martin-alderson.md 232 lines, entropicthoughts-com.md 214 lines, gary-marcus.md 262 lines)
- Triaged agent committed inline:
3b2e578d dreaming: consolidation 2026-06-13 — 5/6 articles already covered, 1 entity gap filled (entropicthoughts) - Downstream dreaming-wiki-ingest found
triage_latest.jsonvalid, verified all 6 decisions independently, confirmed Case C1 via git log, added recovery note, archive was dedup - Total time: ~2 minutes for full recovery cycle
Concrete Example (June 14, 2026) — Case C0 + C3 Recovery
- Dreaming-group collected 6 RSS articles from 2026-06-09 via
latest.json triage_latest.jsonexisted but was stale (from June 13 — same 6 articles already triaged yesterday!) — Case C0- No same-day dreaming commit in git log
- Cron output file at
/opt/data/.hermes/cron/output/<job-id>/2026-06-14_18-12-10.md(3567 lines) contained the dreaming-group response starting at line 3492 - Found by
tail -100near the end: clean JSON with"checkpoint_run_id": "20260614T181000Z", Takes=0, all 6 skip - Recovered JSON showed same articles as yesterday — already processed. Pipeline saturation
- Saved recovered JSON to
triage_latest.json, verified skip decisions independently (all confirmed), archive returned "All items already archived (dedup)" - Added log.md recovery note, committed
- Key learning: The stale
triage_latest.jsonhad the SAME 6 articles as the freshlatest.json— the ingest checkpoint (latest.json) overwrites with fresh data nightly, but old 2026-06-09 RSS articles persist across days until they drop off. Check the article DATE in the ingest checkpoint, not just the checkpoint file date.
Pitfalls
- Duplicate detection is MANDATORY
- Always check existing pages first (don't trust 0.65 threshold alone)
- Log.md corruption via patch (accidental
|prefix) — When usingread_fileoutput aspatchinput forindex.md, theN|line-number prefix fromread_filecan embed into your new content. The patch tool then writes+|-instead of-, creating|-pipe-prefix corruption at the start of list items. This happens becauseread_filewraps each line asNNN|- ...and copying that display intopatch'snew_stringcarries the|forward.\n \n Reinforced fix (validated 3× in one session, June 2026): After EVERY patch onindex.mdthat inserts new list items, verify the affected lines withsed -n 'N,Mp' wiki/index.md | cat -A. The correct format is- [[...]](no leading pipe, no+prefix). If you see|-at line start, immediately re-patch with:\n\n old_string = \"+|- \" (the corrupted prefix)\n new_string = \"- \" (the correct prefix)\n\n Root cause: Never useread_fileoutput verbatim aspatchinput for index files. The line prefix is display-only metadata. Always type the content fresh or extract it withsed 'Np'(without line numbers). - Pre-run script timeout → fallback file at
/opt/data/.hermes/cron/data/dreaming/grouped_themes_latest.json - Stale dreaming themes (2-3 days old) may already be processed by daily pipelines
- 0-article doesn't mean nothing to do:
collected_articles=0means other pipelines consumed sources, but raw articles may have arrived AFTER those pipelines ran. Always run the 0-article recovery workflow. - Cross-pipeline dedup order matters: Check blog triage JSON FIRST (
~/.hermes/cron/data/blog_ingest/triage_latest.json) — it instantly rules out 70%+ of raw articles. Then check log.md, then wiki pages. Reading articles should be the LAST step, not the first. grep -rlwithtarget='files'is NOT a filename lookup:search_files(target='files')searches file content with regex, not filenames. Usefind+grep -rlfor true filename-based discovery of unprocessed articles.execute_codeblocked in cron mode: Write Python scripts to/tmp/viawrite_file, then run withterminal python3 /tmp/script.py. Do NOT usecat file | python3(pipe_to_interpreter blocked).- ⚠️
hermes_toolsNOT importable in cron-mode subprocess scripts: When running enrichment/enumeration scripts viawrite_file+terminal python3in cron mode,from hermes_tools import read_file, patch, terminalwill fail withModuleNotFoundError.hermes_toolsis only available insideexecute_codeblocks. Fix: Use direct agent tool calls (patch(),read_file(),terminal()) from the agent's own API instead of from subprocess scripts. Restrict/tmp/scripts to pure-Python operations (JSON manipulation, file prepend, string replacement, git commands). Seesemantic-article-groupingskill'sreferences/cron-mode-enrichment-execution.mdfor the full pattern. -mtimewindow must match: Step 1 (count) and Step 2 (find unprocessed) must use the same-mtimevalue. Step 1 uses-mtime -3; Step 2 must also use-mtime -3, not-mtime -1.- Same-entity multi-article enrichment requires a single script: When 2+ articles enrich the SAME entity page, do NOT use parallel subagents (they conflict on the same file). Use the single-Python-script pattern with ordered
str.replace()calls. Seereferences/dreaming-single-script-enrichment.mdfor the full workflow and the|-read_file prefix trap that causes silent failure instr.replace().
Section G: Newsletter Triage (newsletter-triage)
URL Resolution Patterns (CRITICAL — raw files contain tracking URLs, not canonical)
Substack newsletters:
- Look for
open.substack.com/pub/{publication}/p/{slug}(usually Link 7 or 9) - Canonical form:
https://www.{publication}.com/p/{slug}or use the open.substack URL directly with web_extract - IGNORE:
substack.com/redirect/2/...(resolves to app download),substack.com/app-link/post?...(email tracking) - For author attribution: extract from the raw file's
substack.com/@authornamelinks
Beehiiv newsletters:
- URLs are wrapped as
link.mail.beehiiv.com/v1/c/... - Resolution via
curl(preferred — fast, no JS needed): Usecurl -sS -L -o /dev/null -w "%{url_effective}" --max-time 8 -H "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" -H "Accept: text/html,application/xhtml+xml" URL. This resolves beehiiv tracking redirects to canonical URLs (e.g.,getsuperintel.site/p/...). Without browser-like headers, barecurlgets blocked by Cloudflare (HTTP 403) — the headers are essential. - Fallback —
web_extract: Useweb_extracton the tracking URL (handles JS redirect chain). Orweb_searchwith subject line + date. - Fallback — Newsletter web version (when Cloudflare blocks ALL tracking links): As of July 2026, Cloudflare escalated protection — even full browser headers may get
HTTP 403 + cf-mitigated: challenge. When this happens, find thehp.beehiiv.com/{uuid}link in the raw email (it's the newsletter's public web version). Scrape that instead — it contains the full newsletter text with article references and author analysis. Also check for{publication}.site/p/{slug}URLs. Pattern:grep -oP 'https://hp\.beehiiv\.com/[a-f0-9-]+' /path/to/raw/newsletter.md curl -sL 'https://hp.beehiiv.com/{uuid}' -o /tmp/page.html # Strip HTML → extract text for triage classification - If all links unresolvable even with headers AND no web version found, mark as
manual_review_beehiiv_links_unresolvablein triage output
Cron pipeline context: The newsletter-ingest cron job saves raw digests to wiki/raw/newsletters/ — this is the source data. Each digest contains 16-20 links, most of which are tracking/redirect URLs requiring resolution.
Classification Criteria
| Level | Criteria | Action |
|---|---|---|
| Critical | Direct AI agent/LLM relevance, comprehensive landscape updates, major product launches | Create new concept/entity pages, major enrichments |
| High | Specific tooling/workflow coverage, industry context with wiki actionability | Enrich existing entities, create concept pages |
| Medium | Weekly roundups with 1-2 relevant items | Selective entity enrichment |
| Low | No wiki actionability | Skip |
Triage Output Format
Save JSON with: triage_timestamp, run_id, newsletters[] (each with message_id, subject, source, date, canonical_url, classification, summary, wiki_relevance, recommended_action), and summary (counts, key_themes[], recommended_wiki_updates[]).
Save Locations
/opt/data/.hermes/cron/data/triage/newsletter-triage-{timestamp}.json(for downstreamnewsletter-wiki-ingest)/opt/data/ai-topics/wiki/raw/inbox/newsletter-ingest/{timestamp}.json(wiki inbox copy)
Key Pitfalls
- Raw newsletter files in wiki/raw/newsletters/ contain ONLY tracking/redirect URLs — you MUST resolve to canonical URLs before content extraction
- The beehiiv newsletter digest is saved but the source file may not appear in the raw directory listing (it IS there, just needs reading)
- Substack redirect chains:
substack.com/redirect/...→ app download page, NOT the article. Always useopen.substack.com/pub/...pattern - Multiple newsletters can arrive in the same batch — classify each independently
Enrichment Pre-Check (recommended step)
Before finalizing classification, check which wiki_targets pages already exist:
# For each identified target entity/concept:
search_files(pattern="entity-or-concept-name", path="~/wiki/entities", target="files")
search_files(pattern="entity-or-concept-name", path="~/wiki/concepts", target="files")- If the target page exists and is rich (>50 lines), the article is an enrichment candidate (update existing)
- If the target page exists but is a skeleton (<20 lines), the article is a major enrichment candidate
- If no target page exists, the article is a page creation candidate
- Note existing page status in the triage output — this saves the downstream wiki-ingest agent from re-checking
See references/newsletter-triage.md for detailed URL resolution patterns, classification criteria, and output format.
Section Z: Trending Topics Reporting (trending-topics)
See research/trending-topics-reporting skill for the end-to-end trending topics research/reporting workflow. This is NOT an ingestion pipeline — it produces a Japanese-language trending report saved to inbox/rss-scans/ — but it runs after all morning ingestion pipelines (12:00 UTC) and uses their output as input.
Quick Reference
# Run the trend detector
python3 ~/ai-topics/scripts/trending_topics.py --days 3
# Query DB for recent AI articles
python3 -c "import sqlite3; c=sqlite3.connect('/opt/data/.blogwatcher/blogwatcher.db').execute('''SELECT b.name, a.title, a.url FROM articles a JOIN blogs b ON a.blog_id=b.id WHERE DATE(a.discovered_date)>=date('now','-2 days') AND (a.title LIKE '%AI%' OR a.title LIKE '%agent%' OR a.title LIKE '%LLM%' OR a.title LIKE '%model%') ORDER BY b.name'''); [print(f' [{r[0]}] {r[1]}') for r in c.fetchall()]"Key Pitfall: Dual Article Storage
Articles may be in EITHER /opt/data/ai-topics/wiki/raw/articles/ (canonical) OR /opt/data/.hermes/home/wiki/raw/articles/ (cron HOME). Always check both with find.
Section H: Daily RSS Triage (daily-rss-triage)
See references/daily-rss-triage-reference.md for extended workflow patterns and cron mode notes.
⚠️ Skill name collision (RESOLVED): The reference file was renamed from
daily-rss-triage.mdtodaily-rss-triage-reference.md. Useskill_view(name='wiki-ingestion-pipelines', file_path='references/daily-rss-triage-reference.md')to access it.
End-to-end pipeline for processing daily RSS scans: scan blogs → triage → ingest → commit.
Pipeline Position
Pre-run script executes blogwatcher scan, queries DB, reads newsletter, lists existing topics.
The daily RSS triage is the triage + ingest stage of the blog pipeline.
Blog triage decision framework: See
references/blog-triage-decision-framework.mdfor the practical article evaluation process — AI-relevance scoring, existing wiki coverage checks, star ratings, triage checkpoint JSON format, and parallel subagent batch processing pattern.
Workflow
- Parse script JSON output for scan results
- Generate Japanese summary report → save to
~/ai-topics/inbox/rss-scans/daily-scan-YYYY-MM-DD.md - If article_total == 0 AND no newsletter →
[SILENT] - Apply Newsjacking Triage Filter (0-5 score):
- Trend Surfing, Polarizing Promise, Contrarian Insight, Pattern Interrupt, In-Group Signal
- Score ≥ 3: Priority triage; 1-2: Standard; 0: Low priority
- For each article: check existing wiki topics, evaluate relevance, scrape content
- Create/update wiki pages, update index/log, commit
- All reports in Japanese
Key Pitfalls
search_filesunreliable for wiki directory discovery — use Pythonos.walk()- RSS 429 rate limits — log failures, don't retry immediately
- Reddit URLs fail with web_extract — use browser tools as fallback
- Pre-staged files from previous runs — check
git diff --stagedbefore committing
Section H: Raw Article Curation (wiki-raw-article-curation)
See references/wiki-raw-article-curation.md for full workflow.
Systematically reduce the "unprocessed raw articles" count reported by wiki_health.py.
Series Registration Pattern
When multiple raw articles form a coherent series (e.g., slide decks from the same course, multi-part blog series), register them as a group in index.md rather than individually scattered across sections.
Workflow:
- Verify all series articles exist in
raw/articles/(they may already be saved but unregistered in index.md) - Add a dedicated section header in index.md:
## Raw Articles — {Series Name} (N pages) - Each entry includes: wikilink, brief description, and cross-references to companion materials (lecture transcripts, concept pages)
- If companion lecture transcripts exist in
raw/transcripts/, update the "Raw Transcripts" section count and add entries - Update the author's entity project page to link directly to raw slide articles (not just concept pages)
Example — Cheat at Search slide series:
## Raw Articles — Cheat at Search Slide Series (7 pages)
- [[raw/articles/YYYY-MM-DD_author_part-1]] — Part 1 title. Brief description. Companion: [[concepts/relevant-concept]]
- [[raw/articles/YYYY-MM-DD_author_part-2]] — Part 2 title. ...Key pitfall: Entity project pages may link to concept pages (e.g., [[concepts/llm-search-judge]]) instead of raw slide articles. When registering a series, update entity pages to link directly to raw articles with concept pages as secondary references.
Detection
python3 ~/ai-topics/scripts/wiki_health.py | grep -A 3 "Unprocessed Raw Articles"Mixed-Strategy Approach (< 100 unprocessed)
- "Already Consumed but Unlinked" check: Search unique phrase from article in L2 pages
- Tier 1 (High-Value): Deep-read and enrich existing wiki pages
- Tier 2 (Bulk-Associate): Add filename to existing page's References section
Association Targets
| Article Type | Best Target |
|---|---|
| Author blog | Their entity page |
| Technical concept | Relevant concept page |
| Newsletter tracking pixel | wiki/concepts/blogwatcher.md |
| Metadata-only artifacts | wiki/concepts/blogwatcher.md |
Bulk-Associate Workflow (>100 unprocessed)
- Domain analysis — group by domain/author
- Keyword-to-entity mapping
- Batch update entity pages
- Handle remaining unmatched articles
Pitfalls
- Substring matching quirk: filename stem must appear verbatim in L2 content
- Escape-drift on YAML frontmatter patches — use markdown References section instead
Section I: X Bookmarks Ingest (x-bookmarks-ingest)
Cron pipeline triggered by fetch_x_bookmarks.py that processes incoming X/Twitter bookmarks and ingests external articles into the wiki.
Pipeline Chain
fetch_x_bookmarks.py (pre-run script, every 6h) → x-bookmarks-ingest (agent cron)Input Format
The agent cron job receives a JSON payload with new_bookmarks[] array. Each bookmark contains:
id,author_id,created_at,text,public_metrics(bookmark_count, like_count, etc.)entities.urls[]— each URL hasexpanded_url,display_url,status(HTTP), and optionallytitle/descriptionexternal_urls[]— URLs withstatus: 200that are NOT X article linksarticle— for X Articles (x.com/i/article/...), containstitlefield
Workflow
Extract actionable URLs: Filter bookmarks for
external_urls[]withstatus: 200. These are direct article links (OpenAI blog, Substack, arXiv, etc.). X Articles (x.com/i/article/...) withstatus: 500require the fallback path.Scrape external articles:
web_extract()each external URL. Save towiki/raw/articles/{YYYY-MM-DD}_{source}_{slug}.md. The OpenAI blog and Meta/FAIR research blogs typically return full content.X Article content extraction (in priority order):
a. Check
article.plain_textFIRST — The bookmark/tweet metadata frequently contains the FULL article body inarticle.plain_texteven when the URL returns HTTP 500. Ifarticle.plain_texthas substantial content (>2KB), save it directly as the raw article and skip all API/mirror fallbacks. Both the article body AND inline code blocks (article.entities.code[]) are available. Seewiki-entity-enrichment-from-articleskill'sreferences/x-article-plain-text-content.mdfor the full pattern, decision logic table, and content preservation notes.b. Identify the author (CRITICAL — do this BEFORE scraping mirrors): X Articles don't have an
author_namefield in bookmark metadata. Theauthor_idin the tweet is the bookmarker, not the article author. To find the actual author:- Check
article.plain_textfor cross-post URLs: Look for "Cross-posted from https://..." or explicit blog URLs in the article body. Curl the canonical blog URL and grep for<meta name="author",<script type="application/ld+json">, and<meta name="twitter:creator"to extract author identity, affiliation, and handle. This is the MOST RELIABLE method — static HTML meta tags don't need JS rendering or API auth. - Check
article.entities.mentions[]: The mentions array lists X handles cited in the article. The first mention may be the author or a key reference. - Check
article.entities.urls[]: URLs in the article body often point to the canonical blog post, the author's homepage, or related resources with author metadata. - See
references/xurl-author-identification-x-bookmarks.mdfor the full author identification workflow, including cross-post meta tag extraction, failure modes, and session patterns.
c. Try GetXAPI (if
$GETXAPI_KEYis set) — Structured JSON with headings and lists. Use the parent tweet's ID.c. Mirror search — For articles where
article.plain_textis empty or too short:- Extract
article.titlefrom bookmark metadata - Run
web_searchwith:"<article title>" 2026(add author name or domain keywords) - Common mirrors: LangChain blog (
blog.langchain.com/...), Substack, arXiv, personal blogs - If found → scrape and save. If not → mark as metadata-only, skip wiki creation.
- Notable authors often cross-post: check
blog.langchain.com,substack.com, author's personal site.
- Check
Check for existing entity pages BEFORE creating new ones: This is the most common pitfall. Before creating any person/org entity page:
search_files(pattern="firstname.*lastname|@handle", path="~/wiki/entities", target="files")- Also check
search_files(target="content")for aliases referencing the same person under different slugs (e.g., Varun Trivedy = Vivek Trivedy = @Vtrivedy10) - If an existing page is found (even with a slightly different slug), update THAT page — do NOT create a duplicate.
Prioritize by engagement: Process highest-bookmark-count articles first (signal of importance).
Create/update wiki pages: Follow
wiki-entity-enrichment-from-articleskill for entity/concept creation. For multi-article batches by the same author (e.g., two LangChain blog posts by Vivek Trivedy), use the Multi-Source Same-Author Sequential Enrichment pattern. For a single comprehensive article that touches many pages (entity + concept + methodology + org + anti-patterns), use the Comprehensive Article Multi-Page Cascade pattern — seereferences/comprehensive-article-multi-page-cascade.md.Update index.md and log.md: One log entry summarizing the entire batch. Update index.md entry count. Patch existing concept page descriptions if significantly changed.
Commit and push:
cd ~/ai-topics && git add wiki/ && git commit -m "wiki: X bookmarks ingest — <summary>" && git push
Key Pitfalls
- Duplicate entity detection is MANDATORY: Before creating any person entity page, search for existing pages under different slugs (e.g.,
vtrivedy10.mdvsvarun-trivedy.mdvsvivek-trivedy.md). Many tracked people already have pages created bybuild_x_wiki.py. - X Articles behind auth wall:
web_extract()onx.com/i/article/...returns JavaScript wall or login page. Checkarticle.plain_textfirst — it often contains the full article body. Only useweb_searchfor mirrors as a last resort whenarticle.plain_textis insufficient or empty. Seereferences/x-article-plain-text-content.mdinwiki-entity-enrichment-from-articleskill. - Image-only bookmarks: Bookmarks where the only URLs are
pic.x.com/...media links have no scrapable content. Skip them. - Thread-only bookmarks (trivial): Bookmarks with a single short tweet, no external URL, no thread, and <50 bookmarks. Skip for article scraping (save as metadata-only).
- Truncated thread bookmarks with a substantive claim (★★☆☆☆): Bookmarks where X API truncation cuts the tweet text mid-sentence (leaving a fragment like "The first is a"), but the available fragment contains ONE clear, substantive claim. The full thread is unrecoverable via
xurl search "conversation_id:..."(API returns 0 results for single-tweet conversations). Recovery: (1) Save the available fragment as a raw article withstatus: TRUNCATEDand a note in the frontmatter. (2) Check if the fragment has a standalone claim worth enriching an existing entity page (e.g., an analyst's enterprise adoption pattern, a benchmark result, a product announcement). (3) If the claim is substantive AND the author already has an entity page, add a brief contribution entry to their Notable Contributions section with the raw article wikilink. (4) Do NOT try to chase mirrors or reconstruct the full thread — X API truncation is permanent for these tweets. Example (Jun 2026): Jaya Gupta's "OpenAI vs Anthropic enterprise" thread — fragment carried "ChatGPT as org-wide default, Claude ring-fenced for power users" before truncation. Enrichedjaya-gupta.mdwith a 4-line contribution entry. Engagement metrics (139 bookmarks, 334 likes) confirmed the fragment had signal value despite truncation. - Rich thread-only bookmarks (★★★☆☆ to ★★★★★): Bookmarks where the tweet text ITSELF is substantive, with no external article URL but the thread contains detailed technical claims, benchmarks, paper/code/model links, and high engagement (>100 bookmarks). The thread IS the article. Do NOT skip these. Process the full thread via
xurl search "conversation_id:<id>"to reconstruct all author tweets, save the combined thread as a raw article inwiki/raw/articles/, create entity pages for the author and collaborators, and create/enrich concept pages for the announced technology. Seereferences/rich-thread-bookmark-processing.md. - LangChain blog mirror pattern (Webflow migration — IMPORTANT): LangChain migrated their blog from
blog.langchain.comtowww.langchain.com/blog/(Webflow-hosted). When searching for X Article mirrors of LangChain posts, trywww.langchain.com/blog/<slug>FIRST —blog.langchain.com/<slug>often returns 404 for articles published after the migration. The Webflow pages return full HTML content that can be extracted by stripping<script>/<style>tags. Example:www.langchain.com/blog/the-anatomy-of-an-agent-harness(200) vsblog.langchain.com/the-anatomy-of-an-agent-harness(404). - Don't create duplicate Vivek Trivedy pages: He already has
vtrivedy10.md(188 lines, canonical) +varun-trivedy.md(173 lines, duplicate waiting for dedup). Use[[vtrivedy10]]as the wikilink target. - Old thread retrieval failure (pre-2025 threads): Threads older than ~6 months may be completely irretrievable via
xurl search "conversation_id:..."(API returns 0 results). Nitter mirrors (nitter.net, nitter.privacydev.net, xcancel.com) are frequently down. For old threads, save the available tweet fragment as a raw article withstatus: TRUNCATEDand note that the full thread is inaccessible. Only enrich wiki pages if the available fragment contains a substantive standalone insight that isn't already captured. - Already-processed bookmark detection: Before processing any bookmark, check if the article already exists in the wiki: (a)
grep -l "<article title>" wiki/raw/articles/*.mdfor existing raw articles, (b)grep -l "<article title>" wiki/log.mdfor prior ingestion, (c) check the author's entity page Timeline/Blog sections for the article. If fully ingested by a prior pipeline, skip with a log note — don't duplicate work. - Entity file exists but missing from index.md (wiki drift): When enriching an existing entity page, verify it appears in
wiki/index.md. Rich pages (100+ lines) can be absent from the index due to prior pipeline gaps — searchingindex.mdwithsearch_filesmay return 0 results even though the.mdfile exists. If missing, add the entity entry to the recently-updated Entities section at the same time as the enrichment. Concrete example (June 2026):entities/openai-codex.md(335 lines) was absent from index.md; discovered during X bookmarks enrichment and fixed in the same commit.
Deliverable Format (cron)
The final response is auto-delivered. Report findings concisely:
- ✅ Processed articles with wiki actions
- 🆕 New pages created
- ✏️ Updated pages
- ⏭️ Skipped/auth-walled articles
- 🔍 Notable discoveries (duplicates found, new entities identified)
If nothing was scrapable (all bookmarks are image-only, thread-only, or X Articles with no mirrors found), respond [SILENT].
Section J: X Accounts Scan (x-accounts-scan)
Cron pipeline triggered by fetch_x_accounts.py that scans tracked X/Twitter accounts for new posts and ingests linked articles into the wiki.
Pipeline Chain
fetch_x_accounts.py (pre-run script, daily 22:30 UTC) → x-accounts-scan (agent cron)Input Format
The agent cron job receives a JSON payload with new_posts[] array. Each post contains:
id,created_at,account_handle,account_name,textexternal_urls[]— URLs withstatus: 200that are external article links (GitHub, blogs, docs)links[]— each link hasurl,domain,title,description,unwound_url,statusreferenced_tweet_types[]—["quoted"],["replied_to"], or[]
Workflow
Categorize posts by AI relevance: Scan each post's text and linked URLs. Skip non-AI posts (DIY bio, music projects, general politics, vintage computing). Keep: LLM tools, agent frameworks, model training, prompt engineering, AI safety, coding agents, ML infrastructure.
Group posts by contributor: Posts from the same account within the same scan batch often form a coherent story (e.g., a thread about a new tool release split across 2-3 tweets). Process these together.
Check existing entity pages: Before scraping, verify entity pages exist for each account.
find wiki/entities -name "*.md" | grep -i "<handle>". Entity pages created bybuild_x_wiki.pyor prior enrichment should be enriched, not replaced.Scrape linked articles in parallel: Use
delegate_taskwith batch mode (up to 3 concurrent leaf subagents) to scrape external URLs and enrich wiki in parallel.Two-batch scraping strategy (proven pattern, Jun 2026): Split articles by content type for efficient parallel subagent work:
- Batch 1 — Web articles: Blogs, essays, policy posts, independent sites. Subagent uses
web_extract()/curl+ meta tag parsing for publication dates. Source:wiki/raw/articles/with YYYY-MM-DD_source-slug_content-slug.md naming per raw-article-filename-policy. - Batch 2 — GitHub/API content: PRs, releases, API-driven sources. Subagent uses GitHub REST API (
/pulls/:id,/releases/tags/:tag) or arxiv API for papers. Bonus: detect arxiv papers behind SPA summary pages (ChapterPal, etc.) and save to bothwiki/raw/articles/ANDwiki/raw/papers/. - Each subagent receives: post text, external URLs, existing entity page paths, naming policy context
- Subagents: scrape → save raw article → return summary of files created
- ⚠️ In subagent
context, include tag hygiene instructions: "Use ONLY canonical tags from SCHEMA.md. Check/opt/data/ai-topics/wiki/SCHEMA.md."
- Batch 1 — Web articles: Blogs, essays, policy posts, independent sites. Subagent uses
Create/enrich wiki pages in parallel: After raw articles are saved, use a second
delegate_taskbatch for wiki page creation:- Batch 1 — New concept/entity pages + existing page updates: Create new pages (
write_file), enrich existing rich pages (patchonly, neverwrite_fileon >40-line pages). Each subagent handles 3-4 wiki targets. - Batch 2 — Remaining entity/concept updates: Entity pages for authors/organizations mentioned in articles.
- Each subagent: read raw article → read existing wiki page (if any) → create or patch → update index.md and log.md → run pre-commit validation
- Subagents report which pages they created or updated for reconciliation
- Batch 1 — New concept/entity pages + existing page updates: Create new pages (
Reconcile parallel subagent changes: After all subagents complete, index.md and log.md will have been modified by each. Read the final state and fix:
- Index counts: Recalculate total page counts — each subagent may have independently bumped them
- Entity entry concatenation: Subagent patches to entity index entries may have concatenated new descriptions onto old ones.
grepfor each enriched entity and verify the description line is clean - Alphabetical order: New concept entries may be misplaced; verify with
grep -n "concepts/<new-page>" wiki/index.md - log.md ordering: Multiple subagents may prepend entries out of order — re-read and verify timestamp sequence
Fix tag violations before commit: After all subagent work, run pre-commit validation or manually verify tags:
grep -h "^tags:" wiki/concepts/*.md wiki/entities/*.md | grep -v SCHEMAFix any non-canonical tags using the patterns in General Pipeline Pitfalls → Subagent tag hygiene.
Commit and push:
cd ~/ai-topics && git add wiki/ && git commit -m 'wiki: X accounts scan — <summary>' && git pushGenerate Japanese report: Final response is the Discord-delivered Japanese report. Format:
- Scan summary (accounts scanned, posts found, posts processed)
- Per-contributor sections with 🔴🟠🟡🟢 priority markers
- Each section: post text excerpt, linked URL(s), key findings, wiki changes (🆕 created / ✏️ enriched)
- Skipped posts with reason
- Statistics table (new concepts, enriched concepts, enriched entities, raw articles saved, total page count delta)
Key Pitfalls
- Non-AI posts in scan: The scan captures ALL posts from tracked accounts, including non-AI content. Accounts like Jonathan Whitaker (
@johnowhitaker) post about DIY bio, music projects, and science communication alongside AI content. Filter aggressively. - Same-project multiple posts: Daniel van Strien posted 3 times about 2 projects (polars-hf + uv-scripts-for-ai) — group by project, not by post. Lance Martin's 2 posts were both about Claude Opus 4.8 tips — process together.
- JS-rendered doc sites:
platform.claude.compages are Next.js SPAs —curlreturns partial server-rendered content. Extract visible text by stripping<script>/<style>tags; the server-side content between JS chunks is usually sufficient for raw articles. - Subagent index.md merge conflicts: Three parallel subagents all modify index.md. After they complete, read the file fresh and reconcile counts. The section header count (
## Concepts (N pages)) frequently drifts from the header count. - Entity index entry concatenation (specific pattern): When subagents
patchentity descriptions in index.md, they may only match the prefix of the existing description line. Result:- [[entities/name]] — new description — old description still appended. Fix by reading the line after patching and using a secondpatchto remove the concatenated portion. execute_codeblocked in cron mode: Usewrite_file+terminal python3for batch operations.- Pipe-to-interpreter security scanner blocks
cmd | python3in cron mode: Thecurl | python3 -c "..."pattern is flagged as[HIGH] Pipe to interpreterand blocked withstatus: pending_approval. In cron mode (no user present to approve), this causes silent failure. Always use two-step pattern: (1)curl -sL "https://..." -o /tmp/file.html, (2)read_fileorwrite_filea Python script to/tmp/script.py, thenterminal python3 /tmp/script.py. This applies to ALL content extraction from URLs in cron jobs — never pipe curl directly to any interpreter. filecommand may be unavailable: Some minimal Linux environments lack thefileutility. Usewc -cfor file size, check HTTP status codes from curl, or read file headers withread_fileinstead.- Invisible Unicode in X API responses triggers cron injection scanner: X Article
plain_textand other X API response fields may contain invisible Unicode (U+200B zero-width space, etc.) that blocks the entire cron run.fetch_x_bookmarks.pysanitizes recursively via_sanitize_dict(). If a cron job showsBLOCKED: prompt contains invisible unicode, seereferences/cron-injection-unicode-block.mdfor diagnostics.
Deliverable Format (cron)
Japanese-language report auto-delivered to Discord. See references/x-accounts-scan-report.md for the report template and formatting standards.
ℹ️
wiki-daily-reportcollision RESOLVED (2026-09-11 curator pass): The duplicate local copy at~/.hermes/skills/wiki-daily-report/was archived (the override atconfig/hermes/skills/_overrides/wiki-daily-report/is now the single canonical copy), so bareskill_view(name='wiki-daily-report')resolves again. For report formatting guidance preferskill_view(name='wiki-daily-report')orskill_view(name='wiki-ingestion-pipelines', file_path='references/x-accounts-scan-report.md'). Thedreaminganddaily-rss-triagecollisions were likewise resolved earlier (local duplicates archived, repo copy canonical).
⚠️ Cross-Pipeline Same-Day Enrichment Collision
The x-accounts-scan pipeline runs at 22:30 UTC — well after the morning pipelines (blog-ingest 07:50, newsletter-ingest 07:40, active-crawl 11:00, trending-topics 12:00). By the time x-accounts-scan runs, concept pages touched by those pipelines may already contain content from the same day's news cycle.
Detection: Before creating a new concept page from an x-accounts-scan article, check if a related concept page was already enriched today:
# Check git log for same-day commits touching related concept pages
git log --oneline --since="YYYY-MM-DD" -- wiki/concepts/<related-slug>.md
# Check index.md for same-day enrichment notes
grep "(June XX)" wiki/index.md | grep -i "<topic keyword>"Resolution patterns:
Concept page already exists + same-day enrichment → sub-topic page: If the existing concept page already covers the general topic (e.g.,
concepts/computer-use.mdenriched with Gemini announcement on same day), create a more specific sub-topic page (e.g.,concepts/gemini-computer-use.mdfor Android-specific implementation) and add bidirectional cross-links:- From existing page: "See [[concepts/gemini-computer-use]] for the Android-specific implementation guide..."
- From new page: "See [[concepts/computer-use]] for the general computer use agent landscape..."
Concept page already exists + fully covered → entity enrichment only: If the existing concept page already has comprehensive coverage and the x-accounts-scan article adds no new conceptual angle, enrich the contributor's entity page instead (add to "Key Work", "Core Ideas", or "Blog / Recent Posts").
No existing concept page → normal creation: Proceed with new concept page creation as usual.
Concrete example (June 25, 2026): concepts/computer-use.md was enriched by blog-ingest with "Gemini 3.5 Flash Computer Use (June 2026)" during the morning pipeline. When x-accounts-scan found Philipp Schmid's Android-specific computer use guide at 22:30 UTC, pattern 1 was applied: concepts/gemini-computer-use.md was created as an Android-specific sub-topic with bidirectional links.
---\n\n## JS-Rendered Site Workaround (companion GitHub repo)
Many modern doc sites (Next.js SPAs) render sub-page content client-side, so web_extract() returns empty results on all pages except the SSR'd landing page.
Solution: Check the main landing page for a companion GitHub repo link (usually a "View on GitHub" badge or footer link). Clone the repo — it typically contains markdown READMEs and source code for each module/lesson.
See references/cron-safe-web-scraping.md for the two-stage fetch-then-process pattern.
See references/js-rendered-docs-workarounds.md for the full workflow, detection patterns, and the Braintrust Evals 101 case study.
See references/delegate-task-web-extraction-fallback.md for the delegate_task + web toolset pattern when terminal/execute_code network access is blocked.
Opposite pattern — sites that APPEAR complex but ARE SSR-capable: Some sites (e.g., alphaxiv.org) were misclassified by triage as JS-rendered or "no article" but actually return full HTML via curl. See references/site-specific-fetchability.md for confirmed SSR-capable sites and the triage misclassification recovery workflow.
Manual Article Ingest Patterns
See references/reddit-url-resolution-pattern.md for blocked Reddit URLs; references/candidate-viability-gate.md for the active-crawl gate.
See references/manual-article-ingest-patterns.md for:
- Author identification via secondary search when
web_extract()omits the byline - Related-concept detection — checking existing pages before creating new ones
- Author/org/product mapping — which entities to create from a single article
- MCP tool identification — extracting tool names from article data source mentions
- Pattern 6: Substack multi-part series batch discovery — checking
/archiveto find all parts when given part 1 only
Section K: Course / Lecture Series Portal Page Creation
When ingesting a multi-lecture course (e.g., Maven courses, workshop series), create a structured portal page that spans multiple wiki directories (concepts/, transcripts/, raw/articles/, entities/). The portal page must include an Ecosystem Context section explaining what platforms/services the course serves as onboarding for, and bidirectional links to instructor entities, platform entities, and related concept pages.
See references/course-portal-page-creation.md for the full architecture, portal page template, bidirectional linking checklist, and per-lecture ingestion workflow.
Key pattern: The portal page is type: concept (not entity). Each transcript ingestion updates the portal's lecture table with wikilinks. Related concept pages (e.g., agentic-search.md) get a "Related Course Materials" section linking back to the portal and transcripts.
Cross-Cutting Concept Page Creation
When multiple entities converge on a shared theme (e.g., multi-model synthesis across 3 providers), create a cross-cutting concept page that ties them together. See references/cross-cutting-concept-page-pattern.md for the full workflow: research → create concept page → cascade entity updates → index/log.
General Pipeline Pitfalls
Always orient first — read SCHEMA.md + index + recent log before any operation
Language policy: Triage JSON
reason_ja/summary_jamay be Japanese (checkpoint files in.hermes/cron/data/), but ALL wiki body content (entities/,concepts/,comparisons/,queries/,events/,index.md,log.md) must be in English. The pre-commit hook blocks Japanese text in wiki pages. Real failure (2026-06-04): dreaming-wiki-ingest wrote Japanese headings and body text intoconcepts/legal-agent-benchmark.mdandentities/cohere.md, requiring re-patch before commit. When working from triage JSON, translate all wiki content to English even though the triage reasoning is in Japanese.- ⚠️ CJK proper nouns also blocked: Even legitimate proper nouns with CJK characters (company names like 智谱AI, person names or places with kanji/hanzi) are blocked by the pre-commit hook's CJK scanner. Always use the Latin/English transcription instead (e.g.,
Zhipu AInot智谱AI;Tokyonot東京). This applies to entity descriptions, index.md entries, and any non-raw/ wiki page. Confirmed June 2026:智谱AIin index.md blocked the newsletter-ingest commit.
- ⚠️ CJK proper nouns also blocked: Even legitimate proper nouns with CJK characters (company names like 智谱AI, person names or places with kanji/hanzi) are blocked by the pre-commit hook's CJK scanner. Always use the Latin/English transcription instead (e.g.,
Detect follow-up batches — check log.md for same source before creating pages
Escape-drift on YAML frontmatter patches: Add to markdown References section instead
Partial-match corruption on patch — When
old_stringmatches only a PREFIX of a target line (not the full content), the patch tool replaces only the matched portion and appends the REMAINING original text onto your new content. Fix: Always include enough trailing context inold_stringto uniquely identify the ENTIRE line — preferably the full text of the line from the file. Verify by reading the file first withread_file(offset, limit)and using the exact bytes shown. After every patch on index files, immediately re-read the affected lines to check for appended garbage text. If corruption occurred, fix with a second patch that replaces the corrupted substring.Markdown table leading-pipe corruption (specific variant) — When patching markdown table rows in entity pages (e.g.,
| **May 2026** | Published ...), the leading|pipe character is part of the file content but can easily be omitted fromold_string. When the patch tool finds a partial match without the leading pipe, it inserts the new content at the match boundary, producing||double-pipe corruption at the line start. Fix: (1) Read the file first to confirm the exact content including the leading|. (2) Include the FULL line in bothold_stringandnew_string, starting with|. (3) After patching, immediately re-read the affected lines to check for||corruption. (4) If||is present at the beginning of a line, use a second patch fixing||→|at those lines. This is most likely to happen when adding new rows to career timeline tables, sources lists, or comparison tables in entity pages.Context compaction can mask prior work — review compaction summary for already-completed tasks
Commit message
&trap: The terminal tool interprets&as shell backgrounding. If your commit message contains&(e.g.,set_to_none=True,agents & tools), use single quotes:git commit -m 'wiki: safe message here'. Double quotes fail silently. Also,&&chaining triggers the tool's backgrounding detection — split into separategit add,git commit,git pushcalls when&&chaining fails.Tag validation blocks commits: The pre-commit hook (
~/.githooks/pre-commit-tag-validator.py) checks every YAML frontmattertags:entry against SCHEMA.md's canonical taxonomy. If a tag isn't in SCHEMA.md, the commit is blocked with a violation message. Fix: (a) check SCHEMA.md for an existing canonical tag that matches your intent (e.g., usesandboxinstead of inventingagent-sandboxing), or (b) add the new tag to SCHEMA.md before committing. Do NOT use--no-verifyto bypass — the curator workflow expects tag hygiene.- ⚠️ Tag proximity trap: The pre-commit hook catches tags that don't exist in SCHEMA.md, but it's easy to invent a tag that's SIMILAR to the canonical one but not identical. Example: using
rl-trainingwhen SCHEMA.md hasreinforcement-learning. Before adding ANY new tag, search SCHEMA.md (grep -i "keyword" wiki/SCHEMA.md) for near-matches. In June 2026,rl-trainingwas blocked and had to be replaced with the existingreinforcement-learning— saving a round-trip. This also applies to entity tags: check forcompany(notstartup),open-source(notos),ai-safety(notalignment),ml-research(notml-researcher).\n - ⚠️ SCHEMA.md bold marker requirement — entire category silently excluded (CRITICAL): The pre-commit tag validator (pre-commit-tag-validator.py) extracts valid tags using TWO mechanisms: (a) backtick-quoted tags (`tag-name`), and (b) bold-prefixed comma-separated categories (- **Category**: tag1, tag2). If a SCHEMA category line lacks**bold markers around the category name (e.g.,- AI Agents:instead of- **AI Agents**:), ALL tags on that line are silently excluded from the valid tag set. The validator won't warn about the formatting — it just won't see any of those tags, and your commit will be blocked with violations for tags that appear to be IN SCHEMA.md but aren't recognized. Detection: ifgrep 'tag-name' wiki/SCHEMA.mdfinds the tag but the pre-commit hook reports it missing, check that the category line has**markers. Fix: add**around the category name on that line. Confirmed June 2026: x-accounts-scan commit blocked — 10 tag violations includingagent-skills,agent-workflows,agent-toolingwhich were all present in SCHEMA line 37 but invisible because- AI Agents:lacked bold markers. Fix: changed to- **AI Agents**:— instantly resolved all 10 violations. - ⚠️ Course/lecture content tag mappings (common traps): When ingesting lecture transcripts and summaries, these non-obvious mappings are frequent pitfalls:
Invented tag Correct SCHEMA.md tag Notes async-processingasync-agentsUnder "AI Agents" taxonomy, not "Engineering" loggingobservabilityUnder "Engineering"; monitoringalso existsagent-sandboxingsandboxUnder "Models" taxonomy prompt-engineeringpromptingShorter canonical form data-processingdata-scienceBroader canonical category toolingtoolordeveloper-toolsContext-dependent open-weights(plural)open-weight(singular)SCHEMA.md has singular form; subagents pluralize by default life-sciencesbiology+biotechUnder "Domain Concepts"; use both or pick one based on focus drug-discoverybiotechorevaluationUnder "Domain Concepts" or "Models" taxonomy llm-evaluationevaluationSCHEMA.md uses bare evaluation; subagents invent compoundllm-prefix - ⚠️ Pre-existing tag violations block unrelated commits: The pre-commit hook scans ALL staged files, not just your changes. If a pre-existing file (e.g.,
gpt-4-system-card.md) has a tag violation (hallucinations,disinformation), your commit of completely unrelated files will be blocked. Fix: Use selectivegit addwith explicit file paths instead ofgit add wiki/. Example:git add wiki/transcripts/my-file.md wiki/raw/articles/my-other-file.md wiki/index.md wiki/log.md. This stages ONLY your files, bypassing the pre-existing violation. Verify withgit diff --cached --name-onlybefore committing to confirm only intended files are staged. Never use--no-verifyto bypass — the pre-existing violation should be fixed separately.
- ⚠️ Tag proximity trap: The pre-commit hook catches tags that don't exist in SCHEMA.md, but it's easy to invent a tag that's SIMILAR to the canonical one but not identical. Example: using
⚠️ Content regression blocks commits (CRITICAL): The pre-commit hook
.githooks/pre-commit-content-regression.pydetects when entity/concept pages shrink by >50 lines AND >50%. This catches the #1 recurring wiki data-loss pattern: an ingestion pipeline overwriting a rich curated page with a skeleton/stub. 58 documented regression events across 9 destructive commits (worst:7b69b67dwith 15 pages,383eff68with 14 pages). Prevention: Before ANYwrite_filetowiki/entities/orwiki/concepts/,read_filethe existing page first. If it has >40 lines, usepatchto add content — NEVERwrite_fileto replace it. Recovery: When enriching a damaged page, always checkgit logfor a richer historical version first — restore the richest version as base, merge any genuinely new content, thenpatchnew info on top. Seewiki-entity-enrichment-from-articleskill'sreferences/pre-write-verification.mdfor the full protocol, git history enrichment 4-step pattern, andreferences/content-regression-scanner.shfor scanning the commit history. Cron prompt enforcement: All ingestion cron jobs (raw-backlog-ingest, x-bookmarks-ingest, skeleton-enrich-daily, newsletter-wiki-ingest, blog-wiki-ingest) must include an explicit anti-overwrite warning in their prompt preamble.Patch tool Unicode escape-drift in cron mode: When enriching wiki pages from articles with smart quotes, em-dashes, or CJK characters,
patchfrequently fails with "Escape-drift detected". The cron-safe workaround iswrite_filea Python script to/tmp/and run it withterminal. This also handles multi-section insertions that would otherwise require multiple sequential patch calls. Seereferences/comprehensive-article-multi-page-cascade.mdfor the Python script template.⚠️
write_fileon append-only files destroys ALL history (CRITICAL):write_fileOVERWRITES the entire file — it does NOT prepend or append. Usingwrite_fileonlog.mdor any append-only file destroys all prior content irrecoverably (unless git-tracked). Always use the Python prepend pattern described below, even outside cron mode. If you accidentally overwrite, recover immediately withgit checkout HEAD -- <path>. Confirmed June 2026:write_fileon log.md destroyed 20KB/229 lines of history; recovered viagit checkout HEAD -- wiki/log.md. The correct pattern is: (1)write_filea Python prepend script to/tmp/prepend_log.py, (2)terminal python3 /tmp/prepend_log.py.Log.md append in cron mode: Use
echo '...' >> /opt/data/wiki/log.mdfor appending entries. Heredoc (cat << EOF >> file) may be blocked by the pipe-to-interpreter scanner. Plainecho >>works reliably for single-line and multi-line entries (use single quotes to avoid shell interpolation of&,!,$). For complex entries with special characters, use the Python script pattern:write_fileto/tmp/append_log.pythenterminal python3 /tmp/append_log.py. — Log.md is append-only with newest entries at top. In cron mode,execute_codeis blocked, so you cannot prepend programmatically. Two reliable workarounds:Option A — Python script (preferred): Write a complete Python script to
/tmp/viawrite_file, then execute withterminal python3. This is cleaner thancatmerge — single step, handles large files (8200+ lines) reliably, works with Unicode and special characters:# /tmp/prepend_log.py new_entry = \"\"\"## [YYYY-MM-DD] Title Body... \"\"\" log_path = "/opt/data/ai-topics/wiki/log.md" with open(log_path) as f: current = f.read() with open(log_path, 'w') as f: f.write(new_entry + current)Verified June 2026: 8200-line log.md (659KB) handled without issue in a single terminal call.
Option B —
catmerge pattern: (1)write_filethe new log entry to/tmp/log_new_entry.md, (2)cat /tmp/log_new_entry.md /opt/data/ai-topics/wiki/log.md > /tmp/log_merged.md && mv /tmp/log_merged.md /opt/data/ai-topics/wiki/log.md. This avoids both theexecute_codeblock and pipe-to-interpreter detection.Do NOT use
sed -i '1i...'— it fails on large files with embedded special characters. Do NOT trytail -ror similar pipe chains — blocked by the scanner.Total page count in index.md header AND section header must be correct — After creating a new page, update TWO counts in index.md: (a) the header count at the top (
Total pages: N | Indexed entries: M | Concepts: X | Entities: Y), and (b) the section header count (e.g.,## Concepts (N pages)). Both must match. Forgetting the section header count causes drift where the index says 946 but the section header says 945.
Concrete pattern from blog-wiki-ingest (Jun 2026): Created 1 new concept → Concepts went 945→946 in both the header AND## Concepts (946 pages). The top-level Total pages and Indexed entries also increment by 1 each.concepts/_index.md drift: Many wikis have a separate
concepts/_index.mdlisting concepts by category. Creating or enriching concept pages without updating this causes index drift. Always update BOTHwiki/index.md(main index) and any sub-index files (concepts/_index.md,entities/_index.mdif they exist) when adding new pages.New files invisible to
git addafter auto-commit: If a cron job or auto-sync mechanism committed your new wiki files before your batch commit,git statuswon't show them andgit addwon't stage them. Verify withgit ls-files | grep <new-file-name>. If the file shows as tracked butgit diff HEAD --statdoesn't include it, it was already committed — just commit the remaining modifications.Subagents need explicit absolute paths — don't rely on HOME resolution
⚠️ Sibling subagent
/tmp/race condition on write_file (CRITICAL): When multiple subagents (or a sibling pipeline subagent running in the same 07:00-07:50 UTC window) write to the same/tmp/<name>.pypath,write_fileemits a warning but still overwrites the file. The subsequentterminal python3then runs the WRONG content. Observed June 2026: sibling subagent wrote its tag-fix script to/tmp/fix_tags.pywhile the main pipeline agent was about to run the same filename. Result:wiki/concepts/hornet.mdwas shrunk from 132→47 lines (content regression), caught only by the pre-commit hook. Prevention: Always use unique filenames per pipeline:/tmp/<pipeline>_triage_<date>.pyor/tmp/<pipeline>_fix_<date>.py. If you see awrite_filewarning about sibling modification, re-read the file withread_filebefore executing to confirm content integrity. If a page was corrupted by this pattern, restore withgit checkout HEAD -- <path>and re-apply only the intended change viapatch.⚠️ Subagent tag hygiene (CRITICAL — 25 violations in one batch, June 2026): Parallel subagents creating concept/entity pages routinely use non-canonical tags not in SCHEMA.md. The pre-commit hook will block the entire commit for ALL violations across ALL files — you can't partially commit. Prevention workflow:
- In subagent task
contextfields, include the full content quality rules fromreferences/subagent-content-quality-guardrails.md. This covers tag hygiene, pipe-prefix prevention, CJK blocking, and wikilink format — all in one copy-pasteable block. - After all subagents complete, run the post-subagent verification commands from
references/subagent-content-quality-guardrails.md(pipe-prefix scan, tag validation, CJK scan). - If violations exist, fix them before attempting
git commit. Usepatchfor inline-format tags (tags: [a, b, c]) and the Python script pattern for multi-line format. - Never use
--no-verifyto bypass — fix the tags properly.
- In subagent task
⚠️ Wikilink target validation before creating links (CRITICAL): Before creating
[[entities/name]]or[[concepts/name]]wikilinks in ANY wiki page, verify the target exists. Preferred method:search_files(pattern="page-slug", path="wiki/index.md", target="content")— index.md is a single file without symlink ambiguity. Fallback:search_files(pattern="page-slug", path="wiki/entities", target="content")(searches file content, NOT filenames —target="files"searches content too, it does NOT search filenames). ⚠️search_files(target='files')withpath="~/wiki"can return false negatives due to symlink resolution issues (documented inpre-write-verification.md). Always cross-check with index.md content search orgit log -- 'wiki/entities/<slug>.md'. Transcripts, summaries, and enrichment outputs frequently reference projects, tools, and organizations that lack wiki pages. Two resolution strategies: (1) Create a stub entity/concept page, or (2) use a raw article link instead (e.g.,[[raw/articles/2025-04-14_corbt_art-trainer-new-rl-trainer]]instead of[[concepts/art-agent-reinforcement-trainer]]). Existing pages may use different slugs than expected — search broadly (e.g.,search_files(pattern="*corbitt*", target="content")to find bothentities/kyle-corbitt.mdANDconcepts/corbett-kyle-corbitt.md).⚠️ Patch tool wikilink escaping in markdown tables: When using
patchto edit markdown tables with wikilinks containing|(pipe) characters, the patch tool may double-escape them (|→\\|), corrupting wikilink rendering. Fix: After patching, immediatelyread_filethe affected lines and check for escaped pipes. If present, do a secondpatchto restore correct formatting.Patch duplicate-section creation from non-unique old_string: When using
patchwithreplace_all=false(default), the tool requires a uniqueold_string. If yourold_stringmatches multiple locations (e.g.,## X Activity Themesor---), the patch will fail. If you used a non-unique match that succeeded, you may get unexpected insertions. Always read the file first withread_fileto find unique context (2-3 surrounding lines) for yourold_string. After patching, verify with anotherread_fileto catch unintended duplications. Concrete case (Jun 2026): patching ethan-mollick.md duplicated the Claude Fable paragraph because the trailing newline boundary matched twice.YAML tag format duality — inline vs multi-line: Wiki pages use TWO different YAML tag formats. Some have inline:
tags: [concept, claude-code, model]. Others have multi-line:tags:\n - tool\n - open-source. When writing fix scripts or patching tags, you MUST handle both formats. A script that only looks for- oldtag\n(multi-line) will miss inline[oldtag]violations and the commit will still be blocked. Detection:grep -n "^tags:" <file>⚠️ YAML sources list duplication trap: When patching the YAML frontmatter
sources:field to add a new raw article/newsletter path, the path frequently appears in BOTH the frontmatter AND the page body text (as aSource: [[raw/...]]wikilink or markdown reference). Thepatchtool then fails withFound 2 matches. This happens because many wiki pages list sources in both places — frontmatter for machine readability, body text for human readability.Symptom:
patchreturnserror: "Found 2 matches"when you use the last existing source line as context for insertion.Fix: Provide more context in
old_stringthat includes the YAML closing|---delimiter AND the next heading line to make the match unique. Example — when adding to the end of the sources list, include the heading that follows|---:# Bad — matches both frontmatter and body text: old_string = " - raw/articles/some-path.md\n|---" # Good — includes page heading to force frontmatter-only match: old_string = " - raw/articles/some-path.md\n---\n\n# Page Title"Detection: Before patching sources, grep for the nearest unique raw path to confirm it appears only once in the file. If it appears twice, use the heading context technique above.
Observed June 2026:
concepts/ai-regulation-2026.md— the apple raw article path appeared in both thesources:frontmatter and the DMA section body. Workaround: included the page heading# AI Regin the match context. — if the line contains[, it's inline format; if the next line starts with-, it's multi-line. Fix inline format withpatch(old_string = exacttags: [...]line). Fix multi-line format with a Python script or individualpatchcalls per tag. Seereferences/parallel-enrichment-pitfalls.mdfor parallel enrichment log.md race fixes,\nliteral trap, andreplace_all=truesource list hazard.