Skip to content

Latest commit

 

History

History
310 lines (278 loc) · 69.8 KB

File metadata and controls

310 lines (278 loc) · 69.8 KB
name research-wiki
description End-to-end management of a structured markdown research wiki — ingestion of papers into articles/ and concepts/ pages, back-linking, journal regeneration, and Astro static site export with Pagefind search. Two phases: ingestion (write) and export (publish).
category research

Research Wiki

End-to-end management of a structured markdown research wiki that follows a SCHEMA.md convention. Two phases:

  1. Ingestion — fetching papers, creating article/concept pages, back-linking, regenerating journal
  2. Export — building the Astro static site with search, tag filtering, RSS, and agent-ready llms files

Alias / naming note: Some cron and pipeline configs instruct the agent to "follow the research-wiki-ingestion skill." That name is not a separate installed skill — the ingestion workflow it points to is Phase 1 (Ingestion) of this research-wiki skill. If a task references research-wiki-ingestion, load research-wiki and use Phase 1.


Phase 1: Research Wiki Ingestion

Use this phase when the user asks to ingest research papers (arXiv or non-arXiv) into an existing structured markdown wiki.

Trigger Conditions

  • User requests ingestion of a research paper, article, or URL into the wiki
  • Task involves populating a knowledge base with academic sources and synthesized pages
  • Wiki has defined directory structure: articles/ (per-paper pages), concepts/ (topic pages), raw/papers/, index.md, log.md

Core Workflow

In interactive sessions, prefer execute_code for bulk Python work — use Python's open()/os modules for ALL file I/O (not read_file from hermes_tools, which returns an incompatible dict format in the execute_code sandbox). write_file from hermes_tools works correctly in execute_code. In cron contexts, execute_code availability varies \u2014 use execute_code if it passes the probe, otherwise fall back to terminal() with explicit workdir=~. Always probe first. Primary approach: write complex scripts to /tmp/ via write_file, then run with terminal('python3 /tmp/script.py', workdir='~') or embed in execute_code() when available. Use inline python3 -c "..." only for short (<20 line) one-off logic (e.g. simple parsing, file count checks). For multi-phase ingestion (fetching, ingesting, backlinks, index, journal, static site), break into separate phase-scripts — this avoids command-length truncation and isolates failures. web_extract replaces urllib-based API calls when execute_code is unavailable.

  1. Fetch Metadata
    • arXiv papers: Use arXiv API. RELIABILITY ORDERING (prevailing): (1) web_extract on listing pages (arxiv.org/list/<cat>/recent) as the primary method for daily scans — these are static/cached, bypass rate limits AND HTTP blocks, and show ALL categories' recent submissions grouped by announcement date. The listing pages include cross-listings and can be paged with ?skip=50&show=50 for large categories (cs.AI, cs.CL routinely have 500+ entries). After fetching, read the cached full-text file (footer shows the path) with read_file(path=..., offset=..., limit=200) to see mid-week entries that the head+tail truncation omitted. (2) execute_code with Python urllib for API queries — useful only for narrow keyword/title searches with specific date windows. Beware that execute_code has a 300s hard timeout: a multi-API combined fetch (cs.CY + cs.HC + cs.CL + cs.AI + Semantic Scholar + OpenAlex) will time out. Use execute_code only for single-category API calls or non-arXiv sources. (3) Terminal with curl --retry 5 --retry-delay 2 to https://export.arxiv.org/api/query — only as last resort, and NEVER with HTTP (gets blocked by security scanner) or with * wildcard markers in dates (gets glob-expanded).
    • If API returns 429 rate limit, follow arxiv skill fallback. Preferred first pass in both interactive and cron contexts: use browser_navigate + browser_console with the JavaScript query from references/arxiv-listing-extraction.md (Section 0) — it extracts arXiv IDs and titles directly from listing pages with no HTML parsing. For categories with 100+ entries (cs.AI, cs.CL), supplement with curl-HTML extraction (Section 1) for the full batch.
    • Narrow date-window daily scans (cron): for a tight submittedDate window (last-scan→today), execute_code + urllib against the arXiv API is the clean PRIMARY — date-precise and bypasses the terminal HTTP block. Use listing-page/web_extract only as fallback when the API returns 0 on a weekend or errors (see weekend / HTTP-500 pitfalls). See references/validated-4-source-daily-scan-2026-07-16.md for the exact 4-source recipe.
    • Non-arXiv sources: Try HTML metadata fetch first. If Access Denied, fallback to PDF download via curl, extract text with pdftotext.
  2. Save Raw Source
    • raw/papers/<arxiv_id>.md with frontmatter: source_url, ingested_date, sha256
    • Non-arXiv: raw/papers/<slug>.md with same frontmatter + full extracted text (all raw sources live under raw/papers/)
  3. Create Article Page
    • Path: articles/<slug>.md
    • Frontmatter (required): title, created, updated, type: article, tags, sources, confidence (high/medium/low)
    • created/updated must be full QUOTED date+time timestamps (e.g. "2026-08-16T20:47:13-04:00"), never bare dates — the sidebar and RSS sort by string comparison, and unquoted ISO shifts to UTC (next day). created = ingestion date+time (not paper pub date). Display is date-only; the time is internal for sorting. Bump updated on any significant body edit and rebuild so the sidebar refreshes.
    • Body: synthesis blockquote → Key Findings → Connected Concepts → Connected Articles → Citation (APA, hyperlinked title)
  4. Update/Create Concept Pages
    • Only create a concepts/<slug>.md page when the topic is genuinely new (check existing concepts first — avoid duplicates)
    • Add the new article to related concept pages' Connected Articles lists
    • Ingestion enrichment (mandatory): evaluate whether the new article makes a significant contribution to any connected concept — a novel framing, distinctive finding, or a dimension the concept page lacks. If so, integrate the insight into that concept page's body narrative (a research bullet, subsection, or synthesis paragraph), not just its Connected Articles list. Example: Laidlaw (2026) added a "GenAI as identity work" section to the faculty-development concept page. When you make such a significant body edit, bump the concept's updated timestamp (see frontmatter note below) and rebuild so the right sidebar refreshes.
    • Enrich umbrella concept pages when ingesting theoretical/conceptual papers: when a batch introduces a significant new theoretical approach or framework, update the relevant umbrella page (e.g. learning-theories, feedback, assessment, research-methods-aied) with a "new theoretical directions" subsection that names each new approach, links the article that introduces it, and cross-links related concepts. the wiki maintainer expects the umbrella to reflect these developments, not just list the new article. (Observed 2026-08-16: ingested Ensemble Cognition, human-AI co-regulation, Self-Directed Growth/A2PL into learning-theories.) 4b. Run the inline-link pass (mandatory, HARD GATE) — Load the wiki-inline-links skill and run the full linking pass on every newly created/enriched article and concept page: aggressively link every concept mention in the narrative body (including conceptually-similar phrasing) to the matching concept page, and clean up self-links, links inside ## headings, and same-text links [[slug|slug]]. This is a BLOCKING PREREQUISITE: do NOT build, commit, push, or deploy until every newly created/enriched page in this batch has had the linking pass run AND verified (0 self-links, 0 heading links, balanced brackets, 0 broken links). A green npm run build does NOT substitute for the linking pass — the pass is an editorial step that must happen BEFORE the build. If you skip it and build anyway, the page deploys with missing inline links, which the maintainer will flag. This applies to ALL ingestion paths (interactive AND cron). See the wiki-inline-links skill for the full procedure and term→slug dictionary.
  5. Raw Source Size Management
    • Truncate full text in raw source files to 50k chars maximum
  6. Update Wiki Index
    • Collect all existing entries from index.md Articles and Concepts sections
    • Remove duplicates, add new entries, sort all alphabetically by filename (lowercase)
    • Rewrite the entire sections — never append-only
  7. Update Ingest Log
    • Append to log.md: Date, source, article page, tags, brief summary
  8. Add Back-Links
    • Identify 5+ existing relevant concept pages

    • Add links to their ## Related Pages sections (create section if missing)

    • Verify target pages exist FIRST — before writing any back-link script, stat every target slug with os.path.exists(). For targets that don't exist: substitute with an existing related page, or create a low-confidence stub with sources: []. Never skip silently — the orphan check at the end should report nothing.

    • For referenced pages that don't exist: create low-confidence stubs

    • Check for existing links to avoid duplicates — use if f"[[{target_slug}]" in content: before inserting. This catches both existing direct links and links added in a previous batch.

    • Use per-paper, position-aware insertion — do NOT batch-merge all back-links into a single dict (see Pitfalls: Back-Link Merge Collision). Process each paper's back-links independently. Find the ## Related Pages section with regex, insert before the next ## header (or at end of file if section is at the very end):

      import re
      rp_match = re.search(r'^## Related Pages\n', content, re.MULTILINE)
      if rp_match:
          rest = content[rp_match.end():]
          next_hdr = re.search(r'\n## ', rest)
          insert_pos = rp_match.end() + next_hdr.start() if next_hdr else len(content)
          new_content = content[:insert_pos] + link_line + "\n" + content[insert_pos:]
      else:
          new_content = content.rstrip() + f"\n\n## Related Pages\n{link_line}\n"
    • After the initial pass, do a second-pass verification: read each target file with open() and confirm f"[[{slug}]" appears. This catches silent-write failures, YAML parse errors, or read_file corruption that may have caused a page to be skipped.

  9. Regenerate Journal
    • journal.md must be regenerated after every ingestion batch — it is NOT append-only
    • Primary approach: inline Python. In interactive sessions, use execute_code with inline Python that imports yaml, walks articles/ and concepts/, extracts frontmatter, groups by created, and writes journal.md. In cron, use terminal() with python3 -c "..." and explicit workdir (or execute_code() when available).
    • In cron, yaml may not be available — use regex-based frontmatter parsing as a stdlib-only fallback. Parse each line of the frontmatter directly:
      fm_match = re.match(r'^---\n(.*?)\n---', content, re.DOTALL)
      for line in fm_text.split("\n"):
          if line.startswith("title:"):
              title = line.split(":", 1)[1].strip().strip('"')
          elif line.startswith("created:"):
              created = line.split(":", 1)[1].strip()
          elif line.startswith("confidence:"):
              conf = line.split(":", 1)[1].strip()
          elif line.startswith("tags:"):
              tags_str = line.split(":", 1)[1].strip()
              tags = [t.strip() for t in tags_str.strip("[]").split(",") if t.strip()]
          elif line.startswith("sources:"):
              srcs_str = line.split(":", 1)[1].strip()
              srcs = [s.strip() for s in srcs_str.strip("[]").split(",") if s.strip()]
      This handles all required frontmatter fields without any external dependency. Guard against sources: null (parsed as literal string "null") and sources: [] (empty list) by treating both as "no sources" and skipping the page from the journal.
    • Collect all concept pages from concepts/, extract frontmatter with yaml.safe_load()
    • Skip low-confidence stubs with no sources
    • Include umbrella/high-confidence concept pages that OMIT the sources: field entirely (e.g. umbrella pages like behaviorism, learning-theories have NO sources: key in frontmatter). The journal must list them alongside articles — only skip pages with an EXPLICIT empty/null sources (real low-confidence stubs). If a regen script filters "no sources → skip," it silently drops these umbrella concepts from the journal. Distinguish: has_sources_field vs not sources — include pages with no field, skip only sources: []/sources: null.
    • Group entries by created date, sort newest-first
    • Format: one - {icon} [[slug]] — {source} line per entry, with the full title and tags on the following lines, group by created date with a ## {date} header. (The old pre-Astro pipeline required a strict 3-line format for regenerate-journal-html.py; that parser is retired — the Astro site renders journal.md directly.)
    • Update header (top of file, immediately after # Journal): Last updated: <today> | Total entries: <count> on its own line.
    • Post-regeneration sanity check: verify ALL newly ingested papers appear by searching for their slugs in the output

Pitfalls — Ingestion Phase

  • Terminal Shell Glob Expansion in API URLs: When an arXiv API query URL contains * characters (even as template/draft markers like 202****0000 where *** was meant to be replaced with actual digits), the terminal shell expands them as file globs before the URL reaches the API server. This silently mangles the URL — the API receives a garbled date range like 202606240000+TO+202606240000 (local file matches) instead of 202606240000+TO+202606260000, and returns empty results with no error. Fix: (a) Construct all API URLs with explicit concrete date strings — never leave template markers with * wildcards in terminal() commands. (b) Use execute_code with Python's urllib for ALL arXiv API queries — Python's URL handling avoids both shell glob expansion AND the HTTP security scanner block in one step. (c) If stuck in terminal() with a fragile URL, double-escape the * with \\\\* or use printf '%s' to prevent expansion.

  • web_extract Listing Page Truncation Hides Mid-Week Entries: web_extract on arxiv.org/list/<cat>/recent returns a head+tail window (~50 entries visible) from pages with 100+ entries. The middle entries (typically the previous day's full listings, where the bulk of new AIED-relevant papers appear) are omitted from the inline response. The full text IS saved to disk — the footer tells you the cache path. Fix: After every web_extract on a listing page with 100+ entries, find the "Full text saved to: /path" line in the footer and use read_file(path=..., offset=..., limit=200) to page through the omitted middle section. This is how you discover papers like those submitted on "Wed, 1 Jul" when the initial head window only shows "Thu, 2 Jul" and "Tue, 30 Jun". The cache file preserves the complete listing with all entries in order.

  • Non-arXiv Access Denied — Three-Tier Fallback: (1) Try HTML metadata via web_extract. (2) If blocked, attempt PDF download via curl + pdftotext. (3) If BOTH are blocked by captcha (common with ScienceDirect/Elsevier, which uses aggressive bot detection that returns HTML error pages instead of PDFs), search web_extract on ResearchGate (researchgate.net/publication/...) and Semantic Scholar — these often have author-synthesized summaries with specific study findings and effect sizes sufficient for a high-confidence concept page. The DOI resolver (doi.org/<DOI>) typically returns the same abstract as the publisher page and can serve as a metadata anchor.

  • Non-arXiv PDFs Can Be Landing Pages: Some publisher PDF download links return a tiny HTML landing page (<10KB) instead of the actual paper PDF. pdftotext extracts 0 chars. This is NOT a fatal error — the abstract already captured from web_extract is sufficient. Save the raw file with the abstract only, mark confidence: medium, and note in log.md that the full text was unavailable. Do not retry or attempt alternate download methods — the abstract alone is adequate for a concept page.

  • Orphan Pages: Never skip back-link step — eliminates orphan pages in the wiki graph

  • Overly Narrow Ingestion: Ingest papers with conceptual connection to the wiki's core domain without asking for permission. Synthesize connections rather than seeking approval.

  • Index Sorting: Appending to index.md without re-sorting leads to disordered indexes. Always collect, sort, rewrite.

  • Index Rebuild vs Daily Digest Ordering: The daily digest (concepts/daily-digest-YYYY-MM-DD.md) is a real wiki page and must appear in index.md. If you rebuild index.md (step 5) BEFORE creating the digest (step 4g), the index silently excludes it and the count drifts by exactly one (observed 2026-07-14: 452 index lines vs 453 actual concept files — the digest was written after the rebuild, forcing a second rebuild). Fix: create the daily digest BEFORE the index rebuild, OR make the index rebuild the final write step, executed after the digest exists. Detect by diffing the set of concepts/*.md slugs against the set of [[slug]] entries in index.md — the missing slug will be the daily digest.

  • Index Regeneration — Full Rebuild Required: Do NOT regex-parse existing index.md to extract entries; the regex will miss entries with non-standard formatting, causing data loss. Instead, scan all files in concepts/, parse each file's YAML frontmatter, and rebuild the entire Concepts section from scratch. This is the only reliable method. CRITICAL: include ALL page types (concept, digest, comparison, summary, entity) — do NOT filter to type: concept only. The index is a comprehensive map of all wiki pages, not just concept pages. Filtering by type silently drops digests, entities, comparisons, and summaries (typically 30–40 pages), producing an index count that is lower than expected. If the new index count is ~30 lower than the previous day's count, the most likely cause is a type filter in the rebuild logic. Concrete silent-failure pattern observed 2026-07: globbing concepts/*.md then skipping with if fn.startswith("daily-digest"): continue dropped all 42 daily-digest pages (count fell 418→380 before the fix). Do NOT exclude digests from the index — they are real wiki pages and must be listed. The correct count includes them. Before rebuilding, check index.md for line-number corruption (same pattern as concept-page corruption: first 20 bytes contain |--- after spaces+digits). If corrupted, strip prefixes with re.match(r'\\s*\\d+\\|(.*)', line) before extracting sections — otherwise the ## Concepts section marker won't be found and the rebuild may append rather than replace.

  • hermes_tools read_file in execute_code: In execute_code, read_file from hermes_tools may return a dict with different key structure than the standalone read_file tool. Avoid using it in execute_code scripts — use Python's open() and os modules for all file I/O instead. The write_file from hermes_tools in execute_code works correctly and is safe to use.

  • OpenAlex Future-Dated Entries: OpenAlex API may return papers with placeholder publication dates far in the future (e.g., December 2026). Always add an explicit date-range filter in your processing code — check that publication_date is within the actual search window, not just matching the year. Discard entries with future dates. CONFIRMED 2026-07-13: a full OpenAlex publication_year:2026 query returned 10 results ALL dated 2026-12-xx (today was 2026-07-13) — every one was a future-dated placeholder, so OpenAlex yielded ZERO in-window papers that day. RECONFIRMED 2026-07-16: identical — 10/10 results dated 2026-12-xx, 0 in-window. Treat as the default OpenAlex behavior for publication_year:<current_year> in cron; never report OpenAlex as a failure when it returns only future-dated placeholders. This is the expected failure mode, not a connection error. Report ✓ N results (0 in-window — all future-dated Dec YYYY placeholders, discarded) so the user distinguishes "source worked but returned nothing relevant" from "source failed."

  • Memory Unavailable in Cron: The memory tool may be unavailable in cron/scheduled job contexts. NOTE (2026-08-03): this wiki no longer creates daily-digest pages — the daily scan cron was updated to record ## [YYYY-MM-DD] meta | scan-complete anchors in log.md instead, and 59 existing digest pages were removed. The log.md scan-complete anchor is therefore now the PRIMARY last-scan-date signal. Cross-check with os.path.exists('raw/papers/<id>.md') for any candidate to confirm prior ingestion. If legacy digest files still exist in a fresh clone, the newest daily-digest-YYYY-MM-DD.md filename in concepts/ may still be used as a secondary signal, but do not expect it — prefer the log anchor.

  • Homepage tag cloud (2026-08-03): the homepage template (templates/index-template.html) has a __TAG_CLOUD__ placeholder; the generator fills it with the top 40 tags by usage as a clickable word cloud (font-size 0.8–2.3rem, sqrt-scaled, linking to tags/<tag>.html). Do not remove the placeholder when editing the template.

  • RSS feed (2026-08-03): the generator emits feed.xml (RSS 2.0) at the output root on EVERY regen — 40 most recently added concept pages (sorted by created desc, items with title/link/guid/pubDate/CDATA description). Base URL comes from --site-url (default https://edtechdev.github.io/aied). The feed is committed + deployed with the wiki, so it updates automatically whenever new articles are ingested and the site is regenerated. RSS links (alternate link in <head> + 📡 RSS nav item) exist on the homepage template, the page template, and full tag-index pages — merged/redirect tag pages intentionally have no nav. Do not remove feed.xml from the repo; the generator rewrites it each run. If the site domain ever changes, update --site-url (and the hardcoded alternate-link hrefs in the templates) together.

  • Block-style frontmatter (2026-08-03): generate-static-site.py's parse_frontmatter now handles block-style tags:/sources: lists (tags:\n - a) as well as inline [a, b] — previously block-style pages were silently invisible to tag pages and tag indices. All wiki concept pages were normalized to inline tags: [...] (reconciliation round 2), but the generator fix means either style works going forward. New pages should still use inline form for consistency.

  • Top-of-page summaries (2026-08-03): every concept page should open with a > **…** synthesis blockquote right after the frontmatter (before the body # H1), with [[wikilinks]] to related pages where appropriate — 590/590 pages now follow this. The generator also emits a summary block on the 43 standalone tag pages (tags without a same-named concept): "N article(s) on X" + SCHEMA taxonomy category + related-tag chips (co-occurring tags) + representative article links. When adding new pages, include the blockquote summary; when editing the generator, don't remove the tag-page __SUMMARY__ wiring or the TAG_CATEGORIES map.

  • Blockquote rendering (2026-08-03): md_to_html now converts > ... lines to <blockquote> (Pico-styled). Previously blockquotes rendered with a literal > prefix. New pages should keep the > **…** synthesis blockquote form — it now renders properly and feeds the RSS description.

  • Tag case sensitivity (2026-08-03): tags are lowercase; RCT was normalized to rct (SCHEMA + all pages). The tag→concept redirect now falls back to a lowercase concept slug case-insensitively (tags/<tag>.htmlpages/<lower>.html), so a future uppercase tag won't produce a broken redirect to a nonexistent pages/<Tag>.html. Never create a concept file with an uppercase slug — it collides with the lowercase one (case-sensitive fs) and produces duplicate index entries.

  • Summary-insert scripts must never rewrite the body from a stale variable (2026-08-03, second near-miss): a script that inserted blockquote summaries AND removed stub markers separately lost 55 summaries — insert_summary() wrote the file, then the stub-marker pass rewrote it from the pre-insert body variable. Rule: any script that modifies frontmatter OR body must read the file fresh immediately before writing, and each write must be based on the latest content. Verify after any batch edit: count > blockquotes across concepts before/after (was 217 → after fix all stub pages have them).

  • Frontmatter YAML pitfalls (2026-08-03): the original ingestion pipeline emitted frontmatter with yaml.safe_dump, which (a) wraps scalars >80 chars across lines and (b) appends a stray ... document-end marker — 328 frontmatter blocks contained ... and ~368 titles were multi-line, so the line-based parse_frontmatter read only the first title line → truncated titles, and yaml parsing failed. Fix: emit single-line scalars with json.dumps(value, ensure_ascii=False) (always one line, valid YAML) and strip stray ... lines. All 589 frontmatter blocks now parse as valid YAML. The generator's _clean_title() strips YAML quoting/escaping at pages-build time. Titles that legitimately contain quoted phrases (e.g. "It Felt a Bit Eerie": …) keep their quotes.

  • Case-duplicate hygiene (2026-08-03): tags must be lowercase; deleting/renaming a tag leaves stale <Tag>.html files (the generator never deletes stale output — remove manually, like tags/automated-assessment.html and tags/RCT.html). Check with git ls-files | sort -f | uniq -di after any tag/topic merge. raw/articles/ was merged into raw/papers/ — ALL raw source files now live under raw/papers/ (concepts reference sources: [raw/papers/<slug>.md]). Bodies must NEVER reference raw/ paths (2026-08-11): the legacy static generator used to ../-prefix [local](raw/...) links so they resolved from pages/, but the Astro renderer does not — ^[raw/papers/x.md] footnotes render as literal text and [local](raw/papers/x.md) links are broken because raw/ isn't deployed. Keep raw files referenced only via the sources: frontmatter field; 27 articles were cleaned of body references on 2026-08-11.

  • Frontmatter-rewrite scripts must preserve the body (2026-08-03, near-miss): a tag-reconciliation script that rebuilt frontmatter with re.sub returned ONLY the frontmatter block (m.group(1) + fm_text + m.group(3)) and silently truncated 8 concept pages to just their frontmatter — the site was regenerated and pushed before the truncation was noticed. Fix: when rewriting frontmatter, always append content[m.end():] (the body). Recovery: restore from the previous git commit (git show <commit>:concepts/<slug>.md > concepts/<slug>.md), re-apply the fix with the corrected script, regenerate, and push an amended commit. Always diff page sizes before/after a frontmatter-rewrite pass.

  • Imports must be INSIDE the --- frontmatter fence (2026-08-11, silent deploy outage): src/pages/use-with-ai.astro had import BaseLayout from ... placed AFTER the closing ---. Astro ignores it → BaseLayout is not defined → BOTH astro check and astro build fail. Every GitHub Actions deploy from 2026-08-10 17:45 until 2026-08-11 08:51 failed silently (site frozen at the last good deploy). Rule: import statements go inside the frontmatter block, and after any src/pages/*.astro edit run npm run build locally (or npx astro build) — a green git push does NOT mean a green deploy. Check gh run list after pushing.

  • Astro upgraded to v7.2 (2026-08-12): upgraded from v5.18 via npx @astrojs/upgrade (also bumped @astrojs/sitemap→3.7.3, @astrojs/rss→4.0.19, @astrojs/check→0.9.10). Two required code changes: (1) content config moved from src/content/config.tssrc/content.config.ts (v6 removed the legacy location); (2) v7 content validation is STRICT — every article's frontmatter MUST include the sources: field (10 older articles lacked it and broke the build; fixed by adding sources: []). Rule: any new/edited article must include sources: (either sources: ['raw/papers/<file>.md'] or sources: [] if no raw file) or the build fails. Build still npm run build (astro check + astro build + pagefind + sitemap).

  • Tag pages merged into topic pages (2026-08-03): tags/<tag>.html for any tag that has a same-named concept page (concepts/<tag>.md exists) is now a meta-refresh redirect to pages/<tag>.html — the concept page is canonical and auto-embeds a "📎 N other pages tagged " index section (generator appends it in the page loop when page['slug'] in tag_to_pages). Tags WITHOUT a same-named concept (43 of 122) keep full index pages. Concept pages exclude their own slug from tag chips (rendered as a non-link <span> instead). tag_to_pages is built EARLY (before the page loop) — do not move it back. When editing the generator, single-backslash regex escapes (r'\[\[...') are correct; the patch tool may double them — always verify with python3 /tmp/count_bs.py-style byte counting or ast.parse + re.compile after patching. Window reconciliation when a task also gives a fallback: many scan configs say "if no last-scan date in memory, search the last N days" (e.g. 3 days). When both apply (memory unavailable and a task-provided N-day fallback), take the UNION / wider window — start from today − N days, NOT from the newest digest date. The wider window can only re-surface already-ingested papers, which the raw/papers/<id>.md dedup check correctly skips and reports as "already in wiki" — it will NOT cause duplicate ingestion. This is safer than the narrower digest-based window because it catches anything the prior narrow run may have missed. CRITICAL — the scan-complete anchor in log.md can LAG behind actual runs (observed frozen at 2026-07-03 while digests continued to 2026-07-08): never trust it over the digest filename, or you will start the window too early and re-scan already-ingested papers. Save the scan date to log.md as a searchable anchor for the next run.

  • execute_code and Memory in Cron — Availability Varies: execute_code and memory availability is NOT uniform across cron environments. In some cron setups (e.g. this Hermes instance, June 2026), both execute_code and memory are fully available — execute_code runs all Python logic including arXiv API queries (which avoids terminal HTTP blocks), and memory persists scan dates. In other cron environments, execute_code returns BLOCKED and memory returns "Memory is not available". Always check-and-adapt at the start of a cron run: call memory(action='read', target='memory') (or any simple memory call) to probe availability. If it fails, fall back to log.md or the most recent daily-digest-*.md filename for the last scan date. If execute_code is available (no block response), use it as the primary Python runtime — it handles arXiv API queries with urllib cleanly, avoiding terminal HTTP security blocks. If execute_code is blocked: two terminal patterns: (a) inline Python via python3 -c "..." for journal regeneration, YAML parsing, and file I/O; (b) python3 /tmp/script.py for complex scripts written via write_file. For arXiv API queries when execute_code is blocked and terminal blocks HTTP, fall back to listing-page extraction via web_extract. Always pass workdir=~ explicitly on every terminal() call in cron — cron may set a bogus default working directory.

  • Terminal Default Workdir in Cron: The terminal() tool in cron contexts may have a broken default working directory (e.g., ~/Doccuments/hermes-telegram that doesn't exist), causing all commands to fail with exit code 126. Always pass workdir=~ explicitly on every terminal() call in cron. Never rely on the default.

  • HTTP arXiv API Blocked by terminal(): The terminal tool's security scanner blocks HTTP URLs (arXiv API is HTTP-only). In interactive sessions, use execute_code with Python's urllib for arXiv API queries. In cron here, execute_code + urllib ALSO reaches the HTTP arXiv API successfully — only the terminal() tool's scanner blocks HTTP, not Python's urllib inside execute_code. Verified 2026-07: all four categories (cs.CY/cs.HC/cs.CL/cs.AI) queried via urllib.request with urllib.parse.urlencode returned authoritative, date-precise results. Therefore the arXiv API is the PRIMARY scan method even in cron — prefer it over listing pages. Use listing-page extraction via urllib on arxiv.org/list/<category>/recent only when execute_code itself is blocked OR the API returns 0 on a weekend (see weekend pitfall). See references/arxiv-api-query-pattern.md for the proven execute_code snippet.

  • Terminal "SQL TRUNCATE" Filter: The terminal tool may block commands containing the word "truncate" (e.g. text.truncate(50000)) due to SQL injection heuristics. Fix: Use "slice", "truncate_", or split the word in logic (e.g., text[:50000]) when writing Python scripts to be executed via terminal().

  • arXiv Search API Total Rate Limit OR Timeout: If arXiv search API returns 429 on every request even after backoff (both HTTP and HTTPS endpoints can rate-limit simultaneously), OR if the execute_code times out at 300s without an explicit error code (slow API response, not rate-limiting), abandon the API entirely and switch to listing-page extraction as the primary fallback. Use web_extract on arxiv.org/list/cs.CY/recent and arxiv.org/list/cs.HC/recent — these cached/static pages bypass API rate limits and show ALL recent submissions with exact arXiv IDs, submission dates, titles, and cross-listing categories. For large listing pages (100+ entries, common for cs.CY/cs.HC which reach 127-144 entries), also fetch ?skip=50&show=50 and ?skip=100&show=50 to get the second and third pages. June 9 submissions routinely span all three pages. Scan the listing for education-relevant titles, then use web_extract on individual arxiv.org/abs/ID pages for full metadata (authors, abstract, venue). This is more reliable than web_search for narrow date windows because listing pages are comprehensive per category and include exact submission dates. If listing pages are also unavailable, fall back to web_search with date-anchored queries (see references/web-search-fallback.md for proven patterns). Report ✓ N papers (via listing pages) in source status to distinguish from API results. Validated across multiple consecutive daily scans.

  • Stub Dependencies: Backlinks may reference pages that do not exist. Create stubs with required frontmatter.

  • File Size: Raw source full text over 50k chars causes unmanageable files. Truncate to 50k max.

  • Terminal Fallback: If terminal/write_file fail, try execute_code with Python (if available). In cron, fix the terminal issue directly or simplify the script — execute_code may be unavailable.

  • Terminal -c Command Length Limit: When execute_code is unavailable and you fall back to terminal() with inline python3 -c "...", large scripts (e.g. embedding full raw-paper text, multi-step ingestion logic) can exceed the terminal tool's command-length limit and get silently truncated — the -c argument simply vanishes, producing Argument expected for the -c option. Fix: write the Python script to a temp file (/tmp/ingest_<arxiv_id>.py) via write_file, then run it with terminal('python3 /tmp/ingest_<arxiv_id>.py', workdir='~'). This is a reliable two-step pattern for any complex ingestion step that can't fit in a -c string.

  • Unicode Curly Quotes in Scraped Text Causing Syntax Errors: When web_extract returns paper titles or abstracts that contain Unicode curly/smart quotes (\u2018/\u2019, \u201c/\u201d), embedding that text directly into a Python f-string can produce a SyntaxError — Python's parser confuses \u2019 (RIGHT SINGLE QUOTATION MARK) with an ASCII single-quote string delimiter, breaking the string. This is especially common with titles that use scare quotes (e.g., 'Cruel optimism'). Fix: Use the JSON-embedding pattern (see next pitfall) to isolate complex string data from Python source code. JSON parsers handle Unicode escapes correctly regardless of quote style. If you must embed inline, scrub curly quotes with .replace('\u2019', "'").replace('\u2018', "'").replace('\u201c', '"').replace('\u201d', '"').

  • JSON-Embedding Pattern for Complex Paper Data: When writing a multi-paper ingestion script to /tmp for cron execution, define paper metadata as a JSON array inside a raw Python string, then parse with json.loads(). This avoids three problems at once: (a) command-length limits (JSON is compact), (b) Unicode curly-quote syntax errors (JSON handles all Unicode via \uXXXX escapes), and (c) escaped double-quote hell in titles with inner quotes or colons:

    import json
    new_papers = json.loads(r'''
    [
        {"slug": "my-paper", "title": "Paper with \"Quotes\" and Special Chars",
         "arxiv_id": "2606.12345", "tags": ["llm", "higher-ed"],
         "findings": "Key finding here",
         "backlink_pages": ["student-experience", "equity"]}
    ]
    ''')

    This pattern scales to 12+ papers per batch with no quoting issues. Paper data stays cleanly separated from Python logic, and each paper gets one JSON object with exactly the fields the script needs. Run python3 /tmp/script.py (not -c) to execute.

  • Single-Script vs Phase-Script Threshold: The skill generally recommends breaking into separate phase-scripts, but for small batches (≤6 papers) a single "everything-but-fetch" script that creates concept pages, back-links, index, log, and digest in one pass is faster and has no command-length issues. Use this guideline:

    • ≤6 papers → 2 scripts (fetch-raw + everything-else)
    • 7–10 papers → 3 scripts (fetch-raw + concept-pages + index-log-digest)
    • ≥11 papers → 4+ scripts (fetch-raw; concept-pages; backlinks; index-log-digest) The reference references/validated-6-paper-batch-2026-07-03.md demonstrates the two-script pattern with exact timings for a 6-paper batch where all APIs responded on first attempt (no rate limiting, no listing-page fallback needed).
  • arXiv API URL Encoding — Use urlencode, not quote: Complex arXiv queries with boolean operators need proper URL encoding. Use urllib.parse.urlencode(), not urllib.parse.quote(query, safe=''). The urlencode approach constructs the full query parameter (e.g. {"search_query": "cat:cs.CY AND (ti:education OR ti:learning) AND submittedDate:[START TO END]"}) and encodes it correctly — crucially, it preserves + as the arXiv query language's space/AND separator. The quote(query, safe='') approach encodes + as %2B, which the arXiv API interprets differently and causes empty results. Example:

    params = urllib.parse.urlencode({
        "search_query": "cat:cs.CY AND (ti:education OR ti:learning) AND submittedDate:[202606250000 TO 202606262359]",
        "sortBy": "submittedDate",
        "sortOrder": "descending",
        "max_results": 20
    })
    url = f"http://export.arxiv.org/api/query?{params}"
  • arXiv XML Namespace — opensearch Required for totalResults: When parsing arXiv API XML responses with xml.etree.ElementTree, the namespace dict MUST include opensearch in addition to atom and arxiv. The totalResults, startIndex, and itemsPerPage elements use the opensearch namespace (http://a9.com/-/spec/opensearch/1.1/). Without it, root.find('.//opensearch:totalResults', ns) returns None and .text access raises AttributeError. Complete namespace dict:

    ns = {
        "atom": "http://www.w3.org/2005/Atom",
        "arxiv": "http://arxiv.org/schemas/atom",
        "opensearch": "http://a9.com/-/spec/opensearch/1.1/"
    }
    total = int(root.find(".//opensearch:totalResults", ns).text)

    Initialize this dict ABOVE all arXiv query try-blocks to avoid cascading scope failures (see Cascading Scope Failure pitfall).

  • Terminal Security Scanner Blocks Pipe-to-Interpreter: When the terminal tool detects curl ... | python3 -c "..." (pipe to an interpreter), it rejects the command with a HIGH-severity alert: "Pipe to interpreter: Command pipes output from 'curl' directly to interpreter 'python3'". Fix: Use the two-step file pattern instead: (a) save the response to a temp file first (curl -sL ... -o /tmp/results.json), then (b) run the Python processing as a separate invocation (python3 -c "import json; ..." or python3 /tmp/script.py). This bypasses the security heuristic because the pipe is eliminated. Both steps can be chained with && if the Python is short: curl -o /tmp/data.json ... && python3 -c "...". For complex processing, write a script to /tmp with write_file then execute.

  • Semantic Scholar Rate Limiting: API frequently returns 429. Treat as non-fatal; continue with other sources. Additionally, the S2 bulk search with narrow queries may return only 1-2 results total even across 20 slots — this is not a rate-limit failure but a query sparsity issue. When S2 results are sparse, note ✓ N results (query-specific sparsity) in the source status rather than treating it as a source failure. RECONFIRMED 2026-07-16: S2 returned exactly 1 result (Codify, 2026-05-06) — out-of-window vs a July scan and no arXiv ID, so not ingested. The year=2026 filter does not respect the rolling window; always window-filter by publicationDate.

  • arXiv submittedDate Returning 0 on Weekdays: The submittedDate filter operates on actual submission date, not arXiv ID prefix month. arXiv processes submissions only Mon–Fri; papers submitted Friday evening appear in Monday's listing. A Sat–Mon scan window will return 0 from the API (no submissions on Sat/Sun, Monday's haven't been indexed yet), while listing pages (arxiv.org/list/<cat>/recent) correctly show Friday's submissions under Monday's announcement-date heading. Fix for weekend scans: skip the arXiv API entirely and use listing-page extraction as the primary method for Sat+Sun+Mon windows. Report the API result as ✓ 0 papers via API (weekend — submissions not processed) and the listing-page yield as ✓ N papers (via listing pages). The listing page groups by announcement date, not submission date — papers submitted Friday appear under Monday's date heading.

  • Missing Related Pages Section: Check for ## Related Pages existence before adding back-links. If missing, add the section at end of page.

  • grep Verification via subprocess: grep -r "[[slug]]" via subprocess.run() returns empty — shell interprets [[...]] as globs. Verify by reading files with Python open() and string search.

  • Journal Count Drift from Regex Frontmatter Parsing in Cron: When regenerating journal.md with regex-based frontmatter (cron fallback where yaml is unavailable), the journal entry count may differ from the yaml-based count by 1-5 entries. Root causes: (a) sources: with YAML block-list format (sources:\\n - raw/papers/foo.md) — the regex parser grabs only the first line and gets an empty bracket-pair or just -; (b) sources: null parsed as literal string 'null' which passes an if srcs: guard but fails a startswith('[') check; (c) multi-line tags: or source: values that span lines. Detection: compare the journal count header against the previous session's log entry; a drop of 1-5 is normal. A drop of 10+ means pages with sources: null or block-list format may be silently excluded. Fix: run a separate verification pass that counts concept pages with type: concept and sources: (non-null, non-empty-list) using os.listdir + regex — the two counts should agree.

  • YAML sources/tags Coercion to Float: When using yaml.safe_load() to parse frontmatter (e.g. for journal regeneration), the sources and tags fields can contain non-string values — notably type float (e.g. a YAML value 2026 in a position that should be a string can parse as 2026.0). This causes one of two errors: TypeError: sequence item 0: expected str instance, float found (when joining with ', '.join()) or TypeError: expected string or bytes-like object, got 'float' (when passing a sources list element directly to re.search()). Fix: explicitly convert elements to string when iterating: sources = [str(s) for s in metadata.get('sources', [])] and tags = [str(t) for t in metadata.get('tags', [])]. For re.search code paths, coerce at access time: m2 = re.search(r'raw/papers/', str(src)). Add these guards in any code path that calls yaml.safe_load on frontmatter. Does not apply to regex-based frontmatter parsing (which always produces strings from line-splitting).

  • Colon in Title Breaks YAML: Unquoted colons in titles break yaml.safe_load(). Always wrap title values in double quotes when title contains :.

  • Inner Double Quotes in Title Break YAML: When a paper title contains literal double quotes (e.g., "It Felt a Bit Eerie"), writing title: ""It Felt..." causes YAML to parse the opening "" as an empty string followed by unparseable text. The yaml.safe_load() fails silently, excluding the page from journal regeneration with no error. Fix: escape inner double quotes with backslashes — title: "\"It Felt a Bit Eerie\": Exploring Humanlike Interactions...". Automated generation fix: when building the title string for a concept page frontmatter in Python, check for both colons AND double quotes, and use .replace('"', '\\"') BEFORE wrapping in outer quotes:

if ':' in title_raw or '"' in title_raw:
    escaped = title_raw.replace('"', '\\"')
    title_yaml = f'"{escaped}"'
else:
    title_yaml = title_raw

This is distinct from the colon pitfall above and happens most often with papers that use scare quotes in their titles. After fixing, re-run journal regeneration — the previously hidden page will reappear and the journal entry count will increase.

  • Firecrawl PDF Timeout (arXiv Full-Text): web_extract on arXiv PDF URLs (e.g., https://arxiv.org/pdf/ID) may timeout with HTTP 504 when Firecrawl's upstream scraper can't extract the PDF in time — especially for larger papers or when Firecrawl is under load. This is common in cron runs where multiple PDFs are fetched in parallel. Fix: (a) First, try web_extract on HTML versions (https://arxiv.org/html/IDv1) — these are lighter-weight and rarely timeout; use the resulting abstract/summary for metadata. (b) For full-text extraction, fall back to terminal('curl -sL --retry 3 --retry-delay 2 "https://arxiv.org/pdf/ID" -o /tmp/ID.pdf && pdftotext /tmp/ID.pdf /tmp/ID.txt', workdir='~') — HTTPS arXiv URLs work in terminal and pdftotext reliably extracts text even from large PDFs. Truncate the resulting text to 50k chars before saving. Both steps can run in parallel across papers. The HTML extract alone is often sufficient for concept-page synthesis; the full text is a nice-to-have for the raw source file.
  • execute_code Cascading Scope Failure in Multi-API Scripts: When multiple API calls share a variable (e.g., XML namespace ns) inside sequential try blocks, and the first call raises an exception, the variable is never defined. The second call then fails with NameError even if that API is healthy — a cascading failure. Fix: initialize shared variables (namespace dicts, headers, etc.) above ALL try blocks, not inside one. This prevents one source failure from sabotaging subsequent sources.
  • Back-Link Target Does Not Exist: When linking 5+ new papers in a batch, some target slugs in the back-link plan may not exist as files (e.g., open-source.md is not in the wiki). If the script silently skips them, those back-links are lost with no record. Fix: stat every target slug with os.path.exists() before scripting the insertions. For missing targets, (a) substitute an existing related page from the same tag domain, or (b) create a low-confidence stub. Log which substitution was made in the concept page body so provenance is traceable. Then add the back-link verification step (second pass with open().read()) as a catch-all.
  • Subagent-ingested articles often SKIP the back-link step (2026-08-16): When you delegate article creation to delegate_task subagents, each child reliably creates the article + raw files but routinely does NOT add the back-links to the Connected Articles sections of the target concept pages (the subagents' task prompts focus on creating the article, not on the reverse links). Fix: after any subagent wave, run an independent back-link pass yourself (as the orchestrator): for each new article, confirm every slug it links in its ## Connected Concepts appears in that concept page's ## Connected Articles (or ## Connected Concepts) section; insert any missing ones. Do NOT assume the subagents completed cross-linking — verify with open().read() and add what's absent.
  • arXiv API HTTP 500 (Server Error): Unlike 429 rate-limiting, HTTP 500 means the API server is down — retries with backoff are unlikely to help. After 2 failed attempts, abandon the API entirely and switch to listing-page extraction (web_extract on arxiv.org/list/cs.HC/recent and arxiv.org/list/cs.CY/recent). These pages are cached/static and work even when the API is down. Report ✗ HTTP 500 (API down, used listing pages) in source status — this is different from a rate-limit failure and should be noted distinctly.
  • Back-Link Merge Collision When Multiple New Papers Link to Same Target: When ingesting multiple papers in a single batch, two or more papers may link to the same existing concept page (e.g., both a study-time paper and an overreliance paper pointing to over-reliance.md). Using Python's {**dict1, **dict2, **dict3} merge for backlinks silently drops entries when target slugs collide — later dicts overwrite earlier ones. Fix: Process backlinks per-paper rather than merging into a single dict. After the initial pass, do a second pass to verify every new paper's slugs appear in all their intended target pages by reading each target with open() and checking for the new slug. If missing, insert it.
  • Journal Script Path Resolution: scripts/regenerate-journal.py uses relative paths (../../../../wiki/) that don't resolve correctly from cron contexts or when __file__ differs. Do not invoke it directly via terminal() — use execute_code with inline Python that walks concepts/ with absolute paths (<WIKI_PATH>/concepts/). The script is a reference, not a reliable runner.
  • Semantic Scholar Bulk Search JSON: The bulk search endpoint (/paper/search/bulk) returns a top-level data key containing a list of papers: {"data": [{"paperId": "...", "title": "...", "abstract": "..."}]}. Note that abstract may be null or a disclaimer string in some results; check openAccessPdf status if full text is required. CRITICAL — year is year-level ONLY, not a date range: the year=2026 filter returns results from any month in 2026. In a July scan, S2 returned a May paper (publicationDate 2026-05-06) that was OUTSIDE the scan window. Always window-filter by publicationDate after fetching. Treat S2 as a cross-reference source, not a primary date-bound scanner — its year filter will not respect your rolling window.
  • browser_console extraction only shows first 50 entries: The browser_navigate snapshot only loads the first 50 <dt>/<dd> pairs. For categories with 100+ entries (cs.AI, cs.CL), use browser_scroll(direction="down") followed by another browser_console(...) call to get the next batch, or fall back to the curl-HTML approach in references/arxiv-listing-extraction.md Section 1 for full extraction.
  • ArXiv Subject Logic for AIED: Always scan ALL FOUR categories (cs.CY, cs.HC, cs.CL, cs.AI). AIED papers regularly appear in cs.AI and cs.CL even when cs.CY/cs.HC have 0 education-relevant entries. Do NOT wait for cs.CY/cs.HC to yield < 5 before broadening — scan all four listing pages (arxiv.org/list/<cat>/recent) from the start. They are the most reliable way to find recent cross-listed papers that the API search may miss.
  • Cron Summary Consistency: Use the Source status block in every summary to communicate pipeline health: Source: [Status Icon] [Count] [Method/Notes]. This allows the user to distinguish between "Zero results because the API failed" and "Zero results because nothing new was published."
  • Ingestion Log Anchors: Since memory is unavailable in cron, use a level-2 header in log.md with the date and scan-complete or meta tag (e.g., ## [YYYY-MM-DD] meta | scan-complete) to store the last_scan_date. The next agent can read_file(path='log.md', limit=50) and regex for this anchor to determine their search window. Caveat: this anchor can lag if a later run writes a digest but not a matching anchor — always cross-check against the newest daily-digest-*.md filename, which is the reliable last-ingestion date (see Memory Unavailable in Cron pitfall above).
  • Stale curl temp files parsed as fresh API data: A failed/blocked curl (e.g. HTTP→HTTPS exit code 3, or the security scanner blocking the command) often leaves the PREVIOUS run's response file on disk under the same /tmp/*.xml / /tmp/*.json path. If you then parse that file, you silently ingest STALE data (e.g. a prior day's archive window instead of today's). Fix: (a) Prefer execute_code + Python urllib for API queries so each run fetches fresh with no shared temp file. (b) If you must use curl, either write to a uniquely-dated temp path per run, or verify freshness by checking the updated/published timestamps in the parsed response fall inside your scan window BEFORE ingestion. In the 2026-07-08 run, an HTTPS arXiv curl exited 3 but left a prior run's /tmp/arxiv_*.xml dated 2026-07-03 on disk; the window header showed the wrong dates yet the file was valid XML and nearly parsed as fresh. Always sanity-check parsed entry dates are within [START, END].
  • Pre-flight dedup BEFORE downloading PDFs: Stat raw/papers/<arxiv_id>.md for every candidate BEFORE fetching any PDF. If it exists, the paper was already ingested in a prior run — skip the download and classify it as already in wiki (reference the prior daily-digest/log.md entry). This avoids wasted bandwidth and accidental duplicate raw files that then force a reclassification. A prior-day daily-digest referencing the arXiv ID is a strong "already ingested" signal. In the 2026-07-08 run, 2 of 8 in-window arXiv papers had already been ingested the prior day; downloading all 8 first, then discovering 2 existing raw files, required reclassifying them as skipped — pre-flight dedup would have flagged them before any download.
  • Refreshing an already-ingested paper to a NEWER version (v1 → v3, etc.): "Already in wiki" is NOT the end of the story when the user explicitly asks to update to the latest version. This is the deliberate opposite of the dedup skip — rewrite the raw file in place. Procedure (validated 2026-07-14, arXiv:2605.21629 v1→v3): (1) Detect current version via grep "arXiv:<id>v\d" in the raw file; find latest on https://arxiv.org/abs/<id> (submission-history block shows vN [last revised <date>]). (2) Fetch NEW-version full text from the PDF, NOT the /html/<id>vN page — web_extract on the HTML returns duplicated/garbled Unicode (3.23.2 million, 26.9%26.9\\%, p<0.001p<0.001) that pollutes the raw file. Use curl -sL --retry 3 --retry-delay 2 "https://arxiv.org/pdf/<id>" -o /tmp/<id>.pdf && pdftotext /tmp/<id>.pdf /tmp/<id>.txt. (3) Cap body at 50k chars with a slice (body[:50000]), never truncate — the terminal SQL filter blocks that word. (4) Rewrite raw frontmatter: keep source_url + ingested, ADD updated: <today> and version: vN (last revised <date>), and RECOMPUTE sha256 over the new body. Old vs new SHA will differ — that's exactly the drift signal the SCHEMA sha256 field exists to catch. (5) Bump the concept page's updated date and add a visible "updated to vN" note next to the source link. Do NOT rewrite the synthesis body unless the headline findings actually changed — version bumps usually add methodology/appendix material, not new results (in the 2605.21629 case all headline numbers were identical v1→v3). (6) Regenerate the static site and restart the http.server if it's down (HTTP 000 = server not running, not a content bug). See references/refresh-paper-version.md for the exact script.
  • read_file Line-Number Corruption in Concept Pages: The standalone read_file tool returns content with line-number prefixes ( 1|content). If this annotated output is ever written back to disk via write_file or patch, the line numbers become baked into the file content. The file no longer starts with ---, so YAML frontmatter parsing silently fails, and the page is excluded from index regeneration and journal rebuilds with no error. Prevention: NEVER pipe read_file output into write_file or patch. Always use Python open() for reading files in execute_code scripts, and read with open() before writing. Detection: check for files whose first 20 bytes contain |--- after spaces+digits (if raw.startswith(b' ') and b'|---' in raw[:20]). Recovery: run scripts/detect-readfile-corruption.py which strips line-number prefixes with re.match(r'\s*\d+\|(.*)', line). This corrupted 30 pages (May 2026) — always verify index count against actual file count after regeneration.

Phase 2: Wiki Static Export (Astro)

Use this phase when the user asks to build the wiki's static site, or when a cron job requires site regeneration.

Trigger Conditions

  • User asks to build/publish the wiki site
  • Cron job daily/weekly scan requires site regeneration
  • New article/concept pages ingested and journal.md updated

Workflow

  1. Regenerate journal.md first — Use inline Python to regenerate journal (see Phase 1, step 8).
  2. Regenerate agent-ready files — Run:
    python3 tooling/scripts/generate-llms-files.py
    This rebuilds public/llms.txt and public/llms-full.txt from the current articles/ and concepts/ markdown.
  3. Build the Astro site — Run:
    cd [WIKI_PATH]
    npm run build
    This produces dist/ with Pagefind search index, sitemap, RSS, and all pages.
  4. Commit and push — GitHub Actions deploys dist/ to GitHub Pages:
    git add -A && git commit -m "..." && git push

Pitfalls — Export Phase

  • The old generate-static-site.py pipeline is obsolete. The wiki is now an Astro 5 site. Do not regenerate static-site/ or journal.html — those artifacts belong to the retired pre-Astro pipeline. Any docs referencing scripts/generate-static-site.py, scripts/regenerate-journal-html.py, or templates/index-template.html are historical.
  • Script path mismatches: scripts/generate-static-site.py was removed from tooling/scripts/. The current scripts are fetch-rss-feeds.py, generate-llms-files.py, add-backlinks.py, detect-readfile-corruption.py.
  • Wiki path variations: Some wikis use <WIKI_PATH>, others relative paths. Resolve correct base path from structure.
  • Pagefind must rebuild: search index lives in dist/pagefind/ — always run npm run build after content changes; a stale dist/ shows old search results.
  • Astro strips whitespace before inline <a> tags placed on a new line (2026-08-16): In .astro templates, if an inline link is put on its OWN line right after preceding text — e.g. in a\n<a href=...>... — the Astro compiler collapses the newline and emits in a<a href=...> with NO space between the text and the link (the rendered page reads in aGoogle... / thisvideo). Fix: keep the <a> tag on the SAME line as the preceding text, with an explicit space before it (...in a <a href=...>...</a>,). Applies to any inline anchor in .astro files whose anchor text must not touch the preceding word. Verify the rendered dist/ HTML (grep for text <a) before committing — a green build does not catch this.
  • Frontmatter-rewrite scripts must preserve the body: when rewriting frontmatter, always append content[m.end():] (the body). Diff page sizes before/after any batch edit.

Cron Stall Recovery

When a daily ingestion cron job stalls mid-pipeline (typically during an execute_code call — the agent's response is "Stream stalled mid tool-call"), the raw papers and concept pages are usually saved but index.md, log.md, journal.md, daily digest, and static site may not be updated. Follow references/cron-recovery.md for the full procedure. High-level steps:

  1. Find the stalled sessionsession_search(query="<topic>", sort="newest"), scroll to the stall point
  2. Verify what was saved — check concepts/<slug>.md and raw/papers/<arxiv_id>.md exist with correct sizes
  3. Complete remaining steps in order: daily digest (create concepts/daily-digest-YYYY-MM-DD.md) FIRST, then index.md (alphabetical rebuild — must include the just-created digest), then log.md (append), journal.md (prepend to header), static site HTML (individual pages + patch index.html + patch journal.html). Order matters: create the daily digest BEFORE rebuilding index.md, or the index will silently exclude it and drift by one (see Ingestion Phase: Index Rebuild vs Daily Digest Ordering).
  4. Restart static site server if down
  5. Update last_arxiv_scan_date in memory so the next cron run picks up from today

User Preferences (Embedded)

  • Broad Domain Interpretation: Ingest papers on general AI concepts that connect conceptually to the wiki's core domain without hesitation.
  • Action over deliberation: Ingest borderline papers first, explain connection later.
  • Synthesis > summarization: Concept pages must connect to existing wiki content.
  • Compact wiki: No duplicate content between raw sources and concept pages.
  • Full quoted timestamps (mandatory): created/updated on articles AND concepts must carry full quoted date+time (e.g. "2026-08-16T20:47:13-04:00"), used internally for accurate reverse-chron sorting (sidebar, RSS, journal) but displayed as date-only everywhere. Never bare dates; never unquoted (UTC shift bug).
  • Concept narrative enrichment: integrate significant article insights into connected concept page BODIES (research bullets/subsections), not just Connected Articles lists.
  • Significant-edit → bump updated + refresh sidebar: after any substantive body edit to a concept/article page, bump updated to now and rebuild so the "Recently Updated Concepts"/"Recently Added Articles" sidebar reflects it.
  • Inline hyperlink rule (wiki-style): whenever a concept is mentioned by name in the BODY of a concept or article page, hyperlink that mention to the concept's page (plain [[slug]], or piped [[slug|display]] when display text differs from the slug). Do this for every concept mention in body prose — in addition to the Connected Concepts/Articles lists at the bottom. Use the most specific slug matching the mention's meaning, and the dedicated umbrella page for generic terms (e.g. plain "feedback" → [[feedback]], not [[feedback-loop]]). Aggressive linking applies — link conceptually-similar phrases too (critical analysis→[[critical-thinking]], AI tutors→[[intelligent-tutoring]], human oversight→[[human-in-the-loop-ai]], teachers/educators→[[teacher-role]]). Consult tooling/concept-index.md (the canonical concept manifest) before linking — it lists every concept with its related/similar phrases and the absorbed→canonical merge map (e.g. "over-reliance"→[[cognitive-offloading]], "gamification"→[[game-based-learning]], "feedback-loop"→[[feedback]], "ai-tutoring"→[[intelligent-tutoring]], "automated grading"→[[automated-assessment]]). Removed concepts (cognitive-load-theory, dual-process-theory) are mentioned as plain text, never linked. Load the wiki-inline-links skill for the full linking procedure, the term→slug dictionary, and the self-link/heading/same-text cleanup + verification steps. This is an editorial convention the wiki maintainer expects on all pages — including pages added via cron ingestion.

Support Files

  • tooling/concept-index.md — Canonical concept manifest: every concept with its related/similar phrases + absorbed→canonical merge map. Consult before inserting inline [[slug]] links.
  • wiki-inline-links skill — Dedicated aggressive inline-link pass: term→slug dictionary, conceptually-similar phrase mapping, self-link/heading/same-text cleanup, and verification. Load after every page creation/enrichment (manual and cron).
  • scripts/add-backlinks.py — Re-runnable back-link addition script
  • scripts/fetch-rss-feeds.py — Journal RSS feed fetcher (CAEAI, BJET; output JSON for the weekly ingestion cron)
  • scripts/generate-llms-files.py — Regenerates public/llms.txt and public/llms-full.txt from articles/ + concepts/
  • scripts/detect-readfile-corruption.py — Detect and repair wiki pages corrupted by read_file line-number prefixes
  • cron/daily-scan-prompt.md — Daily arXiv/EdArXiv scan cron prompt
  • cron/weekly-rss-scan-prompt.md — Weekly journal RSS ingestion cron prompt (open-access check included)
  • references/web-search-fallback.md — Proven web_search query patterns for arXiv discovery when API is rate-limited
  • references/arxiv-listing-extraction.md — Deterministic arXiv ID extraction from listing pages to bypass API blocks
  • references/arxiv-api-query-pattern.md — Proven execute_code + urllib pattern for date-window arXiv API queries (works in cron; bypasses terminal HTTP block)
  • references/daily-scan-pipeline.md — Full daily AIED scan pipeline architecture and error handling
  • references/aied-relevance-filtering.md — Two-stage title-scan + abstract-verification filtering pattern
  • references/cron-recovery.md — Step-by-step recovery when a daily ingestion cron job stalls mid-pipeline
  • references/validated-10-paper-batch-2026-06-30.md — Concrete end-to-end benchmark: timings, hybrid fetch pattern, phase scripts, and memory fallback for a 10-paper batch
  • references/validated-6-paper-batch-2026-07-03.md — Medium-volume benchmark: 6-paper batch with all APIs responsive, two-script phase architecture
  • references/cs-cl-ai-fallback-validation.md — Validation notes on the cs.CL+cs.AI fallback search (added 2026-06-15)
  • references/refresh-paper-version.md — Refresh an already-ingested paper to a newer arXiv version (v1→v3): detection, PDF fetch, frontmatter + sha256 update
  • references/validated-4-source-daily-scan-2026-07-16.md — Exact 4-source daily scan (arXiv cs.CY/cs.HC via execute_code urllib + Semantic Scholar bulk + OpenAlex via terminal curl): hybrid fetch split, dedup/filter, 2-script ingest, confirmed S2/OpenAlex outcomes

Verification

  • Inline-link gate (HARD): before declaring any ingestion batch complete (manual or cron), confirm the wiki-inline-links pass ran on EVERY newly created/enriched article + concept page AND passed verification (0 self-links, 0 heading links, balanced brackets, 0 broken links). A green npm run build + green deploy does NOT substitute. If any page in the batch lacks inline links, go back and run the pass before reporting done.
  • List-formatting gate (HARD): ordered/bulleted lists whose consecutive items are separated by a blank line render broken — each item restarts at 1. (CommonMark splits them into separate lists). Run python3 skills/research/wiki-inline-links/scripts/check_list_formatting.py <WIKI> --all before build and fix every reported page by removing the blank line between consecutive list items so each list is ONE contiguous block. This catches a recurring maintainer-flagged rendering bug that a green build does NOT detect.
  • Check index.md counts increment correctly
  • Understand index count vs journal count: the index counts ALL page types (concepts, digests, entities, comparisons, summaries — including stubs with sources: [] or sources: null), while the journal only counts concept pages with actual source references. A gap of 50-80 between index and journal counts is normal — it reflects digests and stub pages that act as placeholders for future cross-linking. If the index count drops by 30+ after a rebuild, the rebuild likely filtered by type: concept only — rebuild without the type filter.
  • Confirm back-links by reading target files with Python (avoid grep for [[slug]] because the shell interprets [[ as glob patterns)
  • Validate frontmatter with SCHEMA.md requirements
  • Confirm journal.md regenerated with correct entry count, newest entry at top
  • Post-regeneration sanity check: verify ALL newly ingested slugs appear in both index.md and journal.md by searching for them with Python open().read(). Silent YAML parse failures (unquoted colons, sources: null, read_file corruption) can drop entries with no error.
  • Three-Way Count Reconciliation (Astro-era): After rebuild, these counts MUST agree: (a) len([f for f in os.listdir('articles') if f.endswith('.md')]) + len([f for f in os.listdir('concepts') if f.endswith('.md')]), (b) the number of - [[slug]] lines in index.md, and (c) the number of article+concept pages in the built site. npm run build prints the page count — trust it as authoritative. Reconcile before declaring the run complete. Note: this wiki's index.md uses ONE ## Concepts section that alphabetically mixes both articles AND concepts (no separate ## Articles section). The header **Total pages:** N reflects the actual file count (articles + concepts), which is NOT the same as the number of - [[slug]] index lines (the index omits some pages). When updating the header, set it to the real os.listdir file count (articles + concepts), not the index line count — they differ (e.g. 786 pages vs 702 index lines).
  • Count HTML files against concept page count after export