kzinmr

wiki-ingestion-pipelines

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.

kzinmr 8 Updated 4w ago

Resources

1
GitHub

Install

npx skillscat add kzinmr/ai-topics/wiki-ingestion-pipelines

Install via the SkillsCat registry.

SKILL.md

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 in references/newsletter-wiki-ingest.md (with references/newsletter-take-body-fetch-pattern.md and references/newsletter-triage-recovery-2026-08-20.md as supporting references). The standalone newsletter-wiki-ingest skill 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

  1. Orient on wiki: read SCHEMA.md, index.md, recent log.md

  2. Load the checkpoint — filter to recommended_action === "take" decisions

  3. Detect prior batch — scan log.md for same source/newsletter title

  4. 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.
  5. 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).

  6. Create new pages first (write_file), then update existing (patch), then index.md and log.md

  7. Commit and push: cd ~/ai-topics && git add wiki/ && git commit -m "wiki: newsletter ingest ..." && git push

  8. If 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:

  1. Batch 1: Create 3 new pages concurrently (full-page writes — independent)
  2. Batch 2: Enrich 3 existing pages concurrently (use patch, not write_file)
  3. 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:

  1. Read triage_latest.json — note its stale timestamp. Do NOT use its decisions.
  2. Read latest.json (today's ingest) — extract the newsletter candidates
  3. Resolve newsletter post URLs (Substack open.substack.com, beehiiv tracking links) via web_extract or curl + <article> extraction
  4. Perform triage independently: assess AI relevance, check existing wiki coverage, assign star ratings
  5. Save the new triage JSON to triage_latest.json (overwriting the stale data)
  6. Process takes: create entity/concept pages, enrich existing pages
  7. Update index.md, log.md, archive skip/reference items
  8. 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 -3

If 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.

  1. First check: Read triage_latest.json at ${HERMES_HOME}/cron/data/newsletter/triage_latest.json. If it contains a valid decisions array, the triage WORKED — use it directly. No need to re-scrape newsletter URLs.
  2. 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.
  3. 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_ja says "軽微な参照追記" (minor reference addition) or "1-2行" (1-2 lines), it's a clear recommendation to enrich despite the ★★★☆☆ rating. See references/newsletter-wiki-ingest-session-2026-06-13.md for the validated session.
  4. 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 .md output file will fail, but the triage_latest.json saved via execute_code or write_file/terminal dance 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.json looks valid (Takes=0, valid JSON) but is from yesterday. Always check checkpoint_run_id against today's date.
  • Inline triage is expensive but necessary for Case C0. You must resolve newsletter URLs and assess content independently. Use delegate_task batch 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.json for cross-pipeline dedup, that file is still from the previous day. For same-day dedup, check raw/articles/ (sitemap-monitor at 06:00) and log.md instead. See semantic-article-grouping skill 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

  1. Select Topics: Read hot-topics.yaml, extract topics with stale last_crawled (>3 days)
  2. Gap Discovery (optional when few stale topics): Survey major AI domains not in hot-topics.yaml
  3. Research: For each topic, crawl prerequisites, laterals, or deep-dives
  4. Create Wiki Pages: Web search → save raw source → create concept page → update index/log
  5. Update hot-topics.yaml: Set last_crawled: YYYY-MM-DD
  6. 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-files first
  • Verify files exist after delegate_task: explicit file existence check
  • git pull --rebase fails with unstaged changes from sibling processes: If your commit already succeeded but git pull --rebase fails because other agents' modifications remain unstaged:
    1. Check if there's actual remote divergence: git log --oneline HEAD..origin/main — if empty, there is none
    2. If no divergence, skip the --rebase and git push directly — the commit is already clean and can be pushed as-is
    3. If there IS divergence, use git stash && pull --rebase && stash pop (don't rebase on top of unstaged changes)
  • YAML via str.replace fragil — use sed with line numbers for hot-topics.yaml updates
  • Avoid git add -A when sibling agents write to same repo — use selective git add

Section C: OpenAI Blog Ingestion (openai-blog-article-ingestion)

Simple workflow for ingesting openai.com/blog articles.

Workflow

  1. Scrape & Save: web_extract(url) → save to wiki/raw/articles/{date}-{slug}.md
  2. Check existing pages → patch existing concept/entity or create new
  3. Update index.md and log.md
  4. 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: curl bare requests get blocked by Cloudflare. Add -A "Mozilla/5.0..." header. See references/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

  1. Check abstract page for "Published in", "Accepted to"
  2. Search Semantic Scholar for publicationVenue
  3. If no venue found → mark as blocked

Processing Steps

  1. Search arXiv API or Semantic Scholar
  2. For each candidate: fetch metadata → research peer-review → apply triage → save or block
  3. If accepted: save to papers/ → create/update wiki page
  4. 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):

  1. Detect collision early — search_files for the framework name in existing concept slugs and content before creating pages
  2. Create a new concept page with a distinct, descriptive slug (e.g., typed-rlm instead of reusing the conflicting lambda-rlm)
  3. Add frontmatter aliases on the new page to capture the paper's name: aliases: [original-name, Y-Combinator X, etc.]
  4. Add a disambiguation warning to the EXISTING page's top — brief note with wikilink to the new page
  5. Build a comparison table on the new page showing control model, formal proofs, empirical scope, source lineage
  6. Update the parent concept page (e.g., rlm-recursive-language-models) to list both as named variants
  7. 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-ingest

Checkpoint 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.json exists with decisions
  • git log --oneline -3 shows 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 decisions for reference items that have non-empty target_pages. For each such item:
    1. Read the target entity page — does it already contain the article's content?
    2. If the entity page exists but lacks the article's specific contribution (only a URL in sources or References, no body summary), enrich it with a brief entry (3-5 lines)
    3. Update the page's updated frontmatter date and add the source path to sources
    4. 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, updated updated date, 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.json exists with valid decisions
  • git log --oneline -3 shows NO same-day triage commit
  • Take decisions need to be processed from the checkpoint
  • Reference enrichment check: Reference items whose reason_ja recommends enrichment (signals: "追記推奨", "言及する価値あり", timeline update, candidate_wiki_path populated) are actionable even when Takes=0 — enrich via patch only (1-12 lines, update updated+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):

  1. Verify by reading the checkpoint: confirm the decisions array is valid with recommended_action fields
  2. Check git log --oneline -3 — same-day triage commit? If yes, classify by git 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. See references/blog-wiki-ingest-archive-only-commit.md.
    1. Read each take's candidate_wiki_path from the decisions array. The body_excerpt and reason_ja fields provide enough context for enrichment. Also scan reference items — if a reference's reason_ja explicitly recommends enrichment (signals: "追記推奨", "言及する価値あり", candidate_wiki_path populated), treat it as actionable.
    2. 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.
    3. 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.
    4. Update log.md with enrichment summary. Commit all changes.
    5. 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

  1. Run ingest jobs first (blog + newsletter concurrently)
  2. Then triage jobs
  3. 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_collect module → create stub module
  • Wrong DB path → use ~/.blogwatcher/blogwatcher.db
  • Pre-run script timeout → parallelize with ThreadPoolExecutor, write checkpoint before scraping
  • CRITICAL: SQLite is_read dedup pattern — The blogwatcher DB (~/.blogwatcher/blogwatcher.db) has articles table with is_read (boolean) and discovered_date (date) columns. If blog_ingest.py times out at 120s, the most likely cause is query_todays_articles() in daily_inbox_collect.py lacking a date filter, causing it to fetch ALL articles (thousands of rows) instead of just today's. The fix:
    1. Ensure SQL query includes WHERE discovered_date >= date('now', '-1 day') AND is_read = 0
    2. After successful scrape+save in blog_ingest.py, call mark_articles_as_read() to set is_read = 1
    3. This prevents duplicate processing on subsequent cron runs
    4. The daily_inbox_collect module lives at ~/.hermes/scripts/daily_inbox_collect.py
    5. After fixing scripts, re-run the pipeline and cd ~/ai-topics && git add wiki/ && git commit && git push

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.py collects RSS scan articles, newsletters, existing wiki pages
  • Phase 2 (LLM processing): Analyzes, creates/updates wiki pages, commits

Workflow

  1. Duplicate Check: Review what adjacent scheduled jobs already completed (daily inbox update, active crawl, etc.)
  2. Light Sleep (Screening): Review articles not already processed, group by semantic themes
  3. 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
  4. 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
  5. 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 -l

Step 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.json

Read 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
done

This 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>.md

If 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-reference

Case 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:

  1. Read the output file: Typically at /opt/data/.hermes/cron/output/<job-id>/YYYY-MM-DD_HH-MM-SS.md
  2. 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 ## Response heading, followed by a ```json block 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.
  3. Extract the decisions — the JSON has the same structure as a triage checkpoint: decisions[] array with recommended_action, reason_ja, body_excerpt, candidate_wiki_path.
  4. 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.
  5. If no same-day dreaming commit: The agent produced decisions but didn't save or commit. Save the recovered JSON to triage_latest.json yourself:
    • 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

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:

  1. First check: Read triage_latest.json at ~/.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.
  2. Check git log: git log --oneline -3 — look for a same-day dreaming commit (e.g., dreaming: consolidation YYYY-MM-DD — N articles...)
  3. 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.
  4. 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
  5. If Case C2 (triage produced decisions only, no inline commit):
    • Follow the same take processing workflow as Section A (parallel subagents, enrich pages, update index/log)
    • See Section A's Case C recovery for the full workflow

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.json valid, 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.json existed 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 -100 near 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.json had the SAME 6 articles as the fresh latest.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 using read_file output as patch input for index.md, the N| line-number prefix from read_file can embed into your new content. The patch tool then writes +|- instead of - , creating |- pipe-prefix corruption at the start of list items. This happens because read_file wraps each line as NNN|- ... and copying that display into patch's new_string carries the | forward.\n \n Reinforced fix (validated 3× in one session, June 2026): After EVERY patch on index.md that inserts new list items, verify the affected lines with sed -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 use read_file output verbatim as patch input for index files. The line prefix is display-only metadata. Always type the content fresh or extract it with sed '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=0 means 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 -rl with target='files' is NOT a filename lookup: search_files(target='files') searches file content with regex, not filenames. Use find + grep -rl for true filename-based discovery of unprocessed articles.
  • execute_code blocked in cron mode: Write Python scripts to /tmp/ via write_file, then run with terminal python3 /tmp/script.py. Do NOT use cat file | python3 (pipe_to_interpreter blocked).
  • ⚠️ hermes_tools NOT importable in cron-mode subprocess scripts: When running enrichment/enumeration scripts via write_file + terminal python3 in cron mode, from hermes_tools import read_file, patch, terminal will fail with ModuleNotFoundError. hermes_tools is only available inside execute_code blocks. 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). See semantic-article-grouping skill's references/cron-mode-enrichment-execution.md for the full pattern.
  • -mtime window must match: Step 1 (count) and Step 2 (find unprocessed) must use the same -mtime value. 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. See references/dreaming-single-script-enrichment.md for the full workflow and the |- read_file prefix trap that causes silent failure in str.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/@authorname links

Beehiiv newsletters:

  • URLs are wrapped as link.mail.beehiiv.com/v1/c/...
  • Resolution via curl (preferred — fast, no JS needed): Use curl -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, bare curl gets blocked by Cloudflare (HTTP 403) — the headers are essential.
  • Fallback — web_extract: Use web_extract on the tracking URL (handles JS redirect chain). Or web_search with 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 the hp.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_unresolvable in 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 downstream newsletter-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 use open.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.md to daily-rss-triage-reference.md. Use skill_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.md for 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

  1. Parse script JSON output for scan results
  2. Generate Japanese summary report → save to ~/ai-topics/inbox/rss-scans/daily-scan-YYYY-MM-DD.md
  3. If article_total == 0 AND no newsletter → [SILENT]
  4. 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
  5. For each article: check existing wiki topics, evaluate relevance, scrape content
  6. Create/update wiki pages, update index/log, commit
  7. All reports in Japanese

Key Pitfalls

  • search_files unreliable for wiki directory discovery — use Python os.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 --staged before 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:

  1. Verify all series articles exist in raw/articles/ (they may already be saved but unregistered in index.md)
  2. Add a dedicated section header in index.md: ## Raw Articles — {Series Name} (N pages)
  3. Each entry includes: wikilink, brief description, and cross-references to companion materials (lecture transcripts, concept pages)
  4. If companion lecture transcripts exist in raw/transcripts/, update the "Raw Transcripts" section count and add entries
  5. 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)

  1. "Already Consumed but Unlinked" check: Search unique phrase from article in L2 pages
  2. Tier 1 (High-Value): Deep-read and enrich existing wiki pages
  3. 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)

  1. Domain analysis — group by domain/author
  2. Keyword-to-entity mapping
  3. Batch update entity pages
  4. 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 has expanded_url, display_url, status (HTTP), and optionally title/description
  • external_urls[] — URLs with status: 200 that are NOT X article links
  • article — for X Articles (x.com/i/article/...), contains title field

Workflow

  1. Extract actionable URLs: Filter bookmarks for external_urls[] with status: 200. These are direct article links (OpenAI blog, Substack, arXiv, etc.). X Articles (x.com/i/article/...) with status: 500 require the fallback path.

  2. Scrape external articles: web_extract() each external URL. Save to wiki/raw/articles/{YYYY-MM-DD}_{source}_{slug}.md. The OpenAI blog and Meta/FAIR research blogs typically return full content.

  3. X Article content extraction (in priority order):

    a. Check article.plain_text FIRST — The bookmark/tweet metadata frequently contains the FULL article body in article.plain_text even when the URL returns HTTP 500. If article.plain_text has 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. See wiki-entity-enrichment-from-article skill's references/x-article-plain-text-content.md for 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_name field in bookmark metadata. The author_id in the tweet is the bookmarker, not the article author. To find the actual author:

    • Check article.plain_text for 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.md for the full author identification workflow, including cross-post meta tag extraction, failure modes, and session patterns.

    c. Try GetXAPI (if $GETXAPI_KEY is set) — Structured JSON with headings and lists. Use the parent tweet's ID.

    c. Mirror search — For articles where article.plain_text is empty or too short:

    • Extract article.title from bookmark metadata
    • Run web_search with: "<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.
  4. 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.
  5. Prioritize by engagement: Process highest-bookmark-count articles first (signal of importance).

  6. Create/update wiki pages: Follow wiki-entity-enrichment-from-article skill 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 — see references/comprehensive-article-multi-page-cascade.md.

  7. 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.

  8. 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.md vs varun-trivedy.md vs vivek-trivedy.md). Many tracked people already have pages created by build_x_wiki.py.
  • X Articles behind auth wall: web_extract() on x.com/i/article/... returns JavaScript wall or login page. Check article.plain_text first — it often contains the full article body. Only use web_search for mirrors as a last resort when article.plain_text is insufficient or empty. See references/x-article-plain-text-content.md in wiki-entity-enrichment-from-article skill.
  • 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 with status: TRUNCATED and 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. Enriched jaya-gupta.md with 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 in wiki/raw/articles/, create entity pages for the author and collaborators, and create/enrich concept pages for the announced technology. See references/rich-thread-bookmark-processing.md.
  • LangChain blog mirror pattern (Webflow migration — IMPORTANT): LangChain migrated their blog from blog.langchain.com to www.langchain.com/blog/ (Webflow-hosted). When searching for X Article mirrors of LangChain posts, try www.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) vs blog.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 with status: TRUNCATED and 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/*.md for existing raw articles, (b) grep -l "<article title>" wiki/log.md for 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 — searching index.md with search_files may return 0 results even though the .md file 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, text
  • external_urls[] — URLs with status: 200 that are external article links (GitHub, blogs, docs)
  • links[] — each link has url, domain, title, description, unwound_url, status
  • referenced_tweet_types[] — ["quoted"], ["replied_to"], or []

Workflow

  1. 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.

  2. 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.

  3. Check existing entity pages: Before scraping, verify entity pages exist for each account. find wiki/entities -name "*.md" | grep -i "<handle>". Entity pages created by build_x_wiki.py or prior enrichment should be enriched, not replaced.

  4. Scrape linked articles in parallel: Use delegate_task with 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 both wiki/raw/articles/ AND wiki/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."
  5. Create/enrich wiki pages in parallel: After raw articles are saved, use a second delegate_task batch for wiki page creation:

    • Batch 1 — New concept/entity pages + existing page updates: Create new pages (write_file), enrich existing rich pages (patch only, never write_file on >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
  6. 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. grep for 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
  7. 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 SCHEMA

    Fix any non-canonical tags using the patterns in General Pipeline Pitfalls → Subagent tag hygiene.

  8. Commit and push: cd ~/ai-topics && git add wiki/ && git commit -m 'wiki: X accounts scan — <summary>' && git push

  9. Generate 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.com pages are Next.js SPAs — curl returns 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 patch entity 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 second patch to remove the concatenated portion.
  • execute_code blocked in cron mode: Use write_file + terminal python3 for batch operations.
  • Pipe-to-interpreter security scanner blocks cmd | python3 in cron mode: The curl | python3 -c "..." pattern is flagged as [HIGH] Pipe to interpreter and blocked with status: 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_file or write_file a Python script to /tmp/script.py, then terminal python3 /tmp/script.py. This applies to ALL content extraction from URLs in cron jobs — never pipe curl directly to any interpreter.
  • file command may be unavailable: Some minimal Linux environments lack the file utility. Use wc -c for file size, check HTTP status codes from curl, or read file headers with read_file instead.
  • Invisible Unicode in X API responses triggers cron injection scanner: X Article plain_text and other X API response fields may contain invisible Unicode (U+200B zero-width space, etc.) that blocks the entire cron run. fetch_x_bookmarks.py sanitizes recursively via _sanitize_dict(). If a cron job shows BLOCKED: prompt contains invisible unicode, see references/cron-injection-unicode-block.md for 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-report collision RESOLVED (2026-09-11 curator pass): The duplicate local copy at ~/.hermes/skills/wiki-daily-report/ was archived (the override at config/hermes/skills/_overrides/wiki-daily-report/ is now the single canonical copy), so bare skill_view(name='wiki-daily-report') resolves again. For report formatting guidance prefer skill_view(name='wiki-daily-report') or skill_view(name='wiki-ingestion-pipelines', file_path='references/x-accounts-scan-report.md'). The dreaming and daily-rss-triage collisions 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:

  1. 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.md enriched with Gemini announcement on same day), create a more specific sub-topic page (e.g., concepts/gemini-computer-use.md for 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..."
  2. 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").

  3. 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 /archive to 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_ja may 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 into concepts/legal-agent-benchmark.md and entities/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 AI not 智谱AI; Tokyo not 東京). This applies to entity descriptions, index.md entries, and any non-raw/ wiki page. Confirmed June 2026: 智谱AI in index.md blocked the newsletter-ingest commit.
  • 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_string matches 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 in old_string to uniquely identify the ENTIRE line — preferably the full text of the line from the file. Verify by reading the file first with read_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 from old_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 both old_string and new_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 separate git add, git commit, git push calls when && chaining fails.

  • Tag validation blocks commits: The pre-commit hook (~/.githooks/pre-commit-tag-validator.py) checks every YAML frontmatter tags: 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., use sandbox instead of inventing agent-sandboxing), or (b) add the new tag to SCHEMA.md before committing. Do NOT use --no-verify to 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-training when SCHEMA.md has reinforcement-learning. Before adding ANY new tag, search SCHEMA.md (grep -i "keyword" wiki/SCHEMA.md) for near-matches. In June 2026, rl-training was blocked and had to be replaced with the existing reinforcement-learning — saving a round-trip. This also applies to entity tags: check for company (not startup), open-source (not os), ai-safety (not alignment), ml-research (not ml-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: if grep 'tag-name' wiki/SCHEMA.md finds 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 including agent-skills, agent-workflows, agent-tooling which 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-processing async-agents Under "AI Agents" taxonomy, not "Engineering"
      logging observability Under "Engineering"; monitoring also exists
      agent-sandboxing sandbox Under "Models" taxonomy
      prompt-engineering prompting Shorter canonical form
      data-processing data-science Broader canonical category
      tooling tool or developer-tools Context-dependent
      open-weights (plural) open-weight (singular) SCHEMA.md has singular form; subagents pluralize by default
      life-sciences biology + biotech Under "Domain Concepts"; use both or pick one based on focus
      drug-discovery biotech or evaluation Under "Domain Concepts" or "Models" taxonomy
      llm-evaluation evaluation SCHEMA.md uses bare evaluation; subagents invent compound llm- 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 selective git add with explicit file paths instead of git 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 with git diff --cached --name-only before committing to confirm only intended files are staged. Never use --no-verify to bypass — the pre-existing violation should be fixed separately.
  • ⚠️ Content regression blocks commits (CRITICAL): The pre-commit hook .githooks/pre-commit-content-regression.py detects 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: 7b69b67d with 15 pages, 383eff68 with 14 pages). Prevention: Before ANY write_file to wiki/entities/ or wiki/concepts/, read_file the existing page first. If it has >40 lines, use patch to add content — NEVER write_file to replace it. Recovery: When enriching a damaged page, always check git log for a richer historical version first — restore the richest version as base, merge any genuinely new content, then patch new info on top. See wiki-entity-enrichment-from-article skill's references/pre-write-verification.md for the full protocol, git history enrichment 4-step pattern, and references/content-regression-scanner.sh for 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, patch frequently fails with "Escape-drift detected". The cron-safe workaround is write_file a Python script to /tmp/ and run it with terminal. This also handles multi-section insertions that would otherwise require multiple sequential patch calls. See references/comprehensive-article-multi-page-cascade.md for the Python script template.

  • ⚠️ write_file on append-only files destroys ALL history (CRITICAL): write_file OVERWRITES the entire file — it does NOT prepend or append. Using write_file on log.md or 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 with git checkout HEAD -- <path>. Confirmed June 2026: write_file on log.md destroyed 20KB/229 lines of history; recovered via git checkout HEAD -- wiki/log.md. The correct pattern is: (1) write_file a 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.md for appending entries. Heredoc (cat << EOF >> file) may be blocked by the pipe-to-interpreter scanner. Plain echo >> 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_file to /tmp/append_log.py then terminal python3 /tmp/append_log.py. — Log.md is append-only with newest entries at top. In cron mode, execute_code is blocked, so you cannot prepend programmatically. Two reliable workarounds:

    Option A — Python script (preferred): Write a complete Python script to /tmp/ via write_file, then execute with terminal python3. This is cleaner than cat merge — 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 — cat merge pattern: (1) write_file the 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 the execute_code block and pipe-to-interpreter detection.

    Do NOT use sed -i '1i...' — it fails on large files with embedded special characters. Do NOT try tail -r or 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.md listing concepts by category. Creating or enriching concept pages without updating this causes index drift. Always update BOTH wiki/index.md (main index) and any sub-index files (concepts/_index.md, entities/_index.md if they exist) when adding new pages.

  • New files invisible to git add after auto-commit: If a cron job or auto-sync mechanism committed your new wiki files before your batch commit, git status won't show them and git add won't stage them. Verify with git ls-files | grep <new-file-name>. If the file shows as tracked but git diff HEAD --stat doesn'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>.py path, write_file emits a warning but still overwrites the file. The subsequent terminal python3 then runs the WRONG content. Observed June 2026: sibling subagent wrote its tag-fix script to /tmp/fix_tags.py while the main pipeline agent was about to run the same filename. Result: wiki/concepts/hornet.md was 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>.py or /tmp/<pipeline>_fix_<date>.py. If you see a write_file warning about sibling modification, re-read the file with read_file before executing to confirm content integrity. If a page was corrupted by this pattern, restore with git checkout HEAD -- <path> and re-apply only the intended change via patch.

  • ⚠️ 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:

    1. In subagent task context fields, include the full content quality rules from references/subagent-content-quality-guardrails.md. This covers tag hygiene, pipe-prefix prevention, CJK blocking, and wikilink format — all in one copy-pasteable block.
    2. After all subagents complete, run the post-subagent verification commands from references/subagent-content-quality-guardrails.md (pipe-prefix scan, tag validation, CJK scan).
    3. If violations exist, fix them before attempting git commit. Use patch for inline-format tags (tags: [a, b, c]) and the Python script pattern for multi-line format.
    4. Never use --no-verify to bypass — fix the tags properly.
  • ⚠️ 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') with path="~/wiki" can return false negatives due to symlink resolution issues (documented in pre-write-verification.md). Always cross-check with index.md content search or git 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 both entities/kyle-corbitt.md AND concepts/corbett-kyle-corbitt.md).

  • ⚠️ Patch tool wikilink escaping in markdown tables: When using patch to edit markdown tables with wikilinks containing | (pipe) characters, the patch tool may double-escape them (| → \\|), corrupting wikilink rendering. Fix: After patching, immediately read_file the affected lines and check for escaped pipes. If present, do a second patch to restore correct formatting.

  • Patch duplicate-section creation from non-unique old_string: When using patch with replace_all=false (default), the tool requires a unique old_string. If your old_string matches multiple locations (e.g., ## X Activity Themes or ---), the patch will fail. If you used a non-unique match that succeeded, you may get unexpected insertions. Always read the file first with read_file to find unique context (2-3 surrounding lines) for your old_string. After patching, verify with another read_file to 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 a Source: [[raw/...]] wikilink or markdown reference). The patch tool then fails with Found 2 matches. This happens because many wiki pages list sources in both places — frontmatter for machine readability, body text for human readability.

    Symptom: patch returns error: "Found 2 matches" when you use the last existing source line as context for insertion.

    Fix: Provide more context in old_string that 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 the sources: frontmatter and the DMA section body. Workaround: included the page heading # AI Reg in the match context. — if the line contains [, it's inline format; if the next line starts with -, it's multi-line. Fix inline format with patch (old_string = exact tags: [...] line). Fix multi-line format with a Python script or individual patch calls per tag. See references/parallel-enrichment-pitfalls.md for parallel enrichment log.md race fixes, \n literal trap, and replace_all=true source list hazard.