Fetch a URL, summarize it, and save as a structured research note in the current Obsidian Research/Library taxonomy. Use the master Research Library MOC as the source of truth for tags, folder routing, and freshness updates.
Take a URL → fetch its content → generate a structured summary → check for
thesis drift against existing notes → write a standalone research note into
the active Research/Library/ taxonomy in the Obsidian vault.
The library is no longer flat. Notes should be routed into the existing numbered subfolders and tagged using the vocabulary established by the master Research Library MOC and the vault tag index.
Tweet/X URLs get rich content via xAI's x_search tool (engagement stats,
thread context, media descriptions). All other URLs get plain-text extraction
via HTTP fetch. The VS Code model (you, the agent) does all summarization —
no external LLM API call needed for the intelligence step.
Vault writes go through the obsidian skill (composable CLI wrapper).
The following sources bypass quality filters and are always captured as research notes when encountered — in email, X, or any other ingestion pipeline. These are high-signal sources that publish in digest/roundup format but contain original analysis worth preserving.
| Source | Email / Handle | Why |
|---|---|---|
| AINews / Latent Space | swyx@substack.com, latentspace, @swyx | Daily AI industry analysis with original commentary, curated signal |
When a calling skill (e.g., gmail-daily-briefing) encounters content from an always-capture source, it should invoke this skill regardless of whether the content looks like a "newsletter digest" or "roundup." The quality filter exclusion for digests does NOT apply to always-capture sources.
To add more sources, append rows to the table above.
| Trigger | Example |
|---|---|
| User shares a URL and wants it saved | "research this: https://..." |
| User says "save this article" | "save this article to obsidian" |
| User says "obsidian research" | "/obsidian-linked-research https://x.com/..." |
| User shares a tweet to capture | "save this tweet: https://x.com/..." |
| User references a link for later | "I want to read this later, save it" |
automation/api, env var, or ~/.config/last30days/.env)Before fetching or summarizing, inspect the live research taxonomy so you do not invent a second folder or tag scheme.
Read the master library MOC first:
python .github/skills/obsidian/scripts/obsidian.py read --path "Research/Library/00 MOC/🗺️ MOC - Research Library.md"Read any relevant topic MOC only after the master MOC, and only for extra domain context. Topic MOCs are secondary maps, not the source of truth for canonical tags.
Read the current vault tag index:
python -c "import sys; sys.path.insert(0,'.github/skills/obsidian/scripts'); from obsidian import Obsidian; print(Obsidian().tags().text)"List the live research-library note paths so you can see the active buckets:
python -c "import sys; sys.stdout.reconfigure(encoding='utf-8'); sys.path.insert(0,'.github/skills/obsidian/scripts'); from obsidian import Obsidian; print(Obsidian().files(folder='Research/Library', ext='md').text)"Windows UTF-8 note: On Windows, subprocess pipes default to the system code page (cp1252), which cannot encode emoji or non-ASCII vault paths. Always add
sys.stdout.reconfigure(encoding='utf-8')before anyprint()call in one-liner subprocess commands, or prefer calling the Obsidian Python API directly.
URL-based dedup (mandatory before fetching): Search for an existing note
whose url: frontmatter matches the input URL. This catches duplicates even
when the slug or folder differ. Use the Obsidian search API:
python -c "import sys; sys.stdout.reconfigure(encoding='utf-8'); sys.path.insert(0,'.github/skills/obsidian/scripts'); from obsidian import Obsidian; r=Obsidian().search(query='<INPUT_URL>'); print(r.text if hasattr(r,'text') else r)"Note: Obsidian's local search does not support
url:field-operator syntax —url: <value>throws "Operator not recognized". Use a plain-text query with the URL string or a distinctive fragment of it (e.g., the tweet ID or domain path).
If a match is found, stop — tell the user the note already exists, give them the path and an Obsidian link, and ask whether they want to update the existing note instead. Do NOT proceed to fetch or create a new note.
Use this routing table unless the live vault has changed again:
| Folder | Use for |
|---|---|
Research/Library/01 Agent Harnesses & Architecture/ | harnesses, orchestration, control planes, architecture, model/runtime design |
Research/Library/02 Skills, IDEs & Agent Tooling/ | skills, Copilot, Claude Code, IDEs, hooks, MCP, tooling |
Research/Library/03 Evals, Reliability & Control/ | evals, reliability, governance, testing, review, controls |
Research/Library/04 SDLC, Workflow & Strategy/ | SDLC, workflow redesign, consulting POV, product/API strategy |
Research/Library/05 Knowledge, RAG & Memory/ | RAG, retrieval, knowledge systems, Obsidian, memory, second brain |
Research/Library/06 Cryptography/ | cryptography, post-quantum, blockchain security, zero-knowledge proofs |
Research/Library/07 Macrotrends & Futures/ | space, off-world, megatrends, long-horizon futures, civilizational shifts |
Research/Library/08 Org Design & AI Transformation/ | organizational design, corporate transformation via AI, hierarchy-vs-intelligence, management theory |
Tagging rules before you write anything:
Canonical Tag Guidance first: agents, ai-agents, agent-harnesses, skills, claude-code, copilot, mcp, evals, rag, sdlc, workflow-design, researchAIagents, MachineLearning, or mixed singular/plural duplicates if a canonical form already existsRun the fetch script to retrieve URL content:
python .github/skills/obsidian-linked-research/scripts/fetch.py "<url>"The script outputs JSON to stdout:
For tweets:
{
"type": "tweet",
"url": "https://x.com/...",
"text": "Full tweet/thread text",
"author_handle": "username",
"author_name": "Display Name",
"date": "2026-03-01",
"engagement": {"likes": 150, "reposts": 30, "replies": 12, "quotes": 5},
"thread_context": "...",
"is_thread": false,
"media_descriptions": ["..."],
"image_urls": ["https://pbs.twimg.com/media/..."],
"article_content": "Full article text if X Article detected",
"article_title": "Article title if X Article"
}For web pages:
{
"type": "web",
"url": "https://...",
"title": "Page Title",
"content": "Plain text content (up to 8000 chars)",
"image_urls": ["https://example.com/hero.jpg", "https://example.com/diagram.png"]
}The image_urls array includes OG images and meaningful <img> tags found in the
page (tracking pixels, favicons, and tiny icons are filtered out). Use Step 3b to
download them into the vault.
If the result contains an "error" key, report it to the user and stop.
Transient retry: If the error is
"No JSON in response"for a tweet URL, retry once — the xAI Responses API occasionally returns a top-level array[{...}]instead of a plain{...}object; a second call usually returns the plain object. If it fails twice, report to the user.
X/Twitter fallback dead-ends: Do NOT try Playwright for x.com URLs — it returns a login wall for unauthenticated sessions. Do NOT try Firecrawl — it explicitly does not support x.com. The only supported path for tweet content is
fetch.pyvia the xAIx_searchtool.
If a non-X web page is truncated or loses useful structure: fetch.py is
still the required first pass, but long articles may come back clipped (for
example around the first ~8000 chars) or flattened enough that headings, quotes,
or diagram captions are hard to reconstruct. In that case, supplement the fetch
result with a secondary enrichment pass:
WebFetch (Claude Code tool) or fetch_webpage (if available) on the same URL to recover richer article structureWebFetch and ask it to extract all major sections in fullfetch.py; it remains the canonical fetch step and the source of image_urls and metadatafetch.py metadata (title, image_urls, url) as the source of truth unless the secondary fetch clearly corrects itIf "needs_browser": true: The tweet contains an X Article that requires
JavaScript rendering. Do NOT use fetch_webpage — it cannot render X Article
pages. Instead, use the Playwright browser tools:
browser_navigate(url=tweet_url)browser_wait_for(time=5000)wait_for) contains the full article text
in the accessibility tree. Extract the article content from the snapshot YAML.Use the returned content as the article body for summarization. Combine it with the tweet metadata (author, engagement) already in the fetch result.
Extracting images from browser content: Use browser_evaluate to extract
pbs.twimg.com image URLs from the DOM (the snapshot YAML won't include src
attributes):
browser_evaluate(function="() => { return JSON.stringify(Array.from(document.querySelectorAll('img')).filter(i => i.src && i.src.includes('pbs.twimg.com') && !i.src.includes('_bigger')).map(i => i.src)); }")Then download the images using:
python -c "import sys; sys.path.insert(0,'.github/skills/obsidian-linked-research/scripts'); from fetch import download_images; import json; r=download_images(['url1','url2'], '<vault_path>/Research/Library/attachments', '{slug}'); print(json.dumps(r))"Close the browser when done: browser_close()
Using the fetched content, generate a deep, structured analysis — not a shallow summary. Do NOT call an external API — use your own reasoning.
Quality bar: Study the existing notes in the target Research/Library/
subfolder for reference.
Good notes are section-by-section breakdowns with tables, code examples, specific
details, and "Relevance to This Repo" sections. They read like comprehensive
technical references, not tweet-length summaries.
Think through the content and produce this structure internally:
{
"title": "Clear, descriptive title for the note",
"slug": "kebab-case-filename-slug (3-6 words, no special chars)",
"library_bucket": "01 Agent Harnesses & Architecture | 02 Skills, IDEs & Agent Tooling | 03 Evals, Reliability & Control | 04 SDLC, Workflow & Strategy | 05 Knowledge, RAG & Memory",
"library_path": "Research/Library/<bucket>/<slug>.md",
"author": "@handle or Author Name",
"source": "x|reddit|blog|article|github",
"core_thesis": "1-2 sentences capturing the central argument or claim",
"sections": [
{
"heading": "Section heading from the article",
"content": "Detailed breakdown — preserve key arguments, lists, code, tables, quotes"
}
],
"key_takeaways_table": [
{"lesson": "Short label", "detail": "Specific explanation"}
],
"relevance_to_repo": "2-4 sentences on how this connects to Eric Cartman / the user's workflow",
"related_notes": ["[[Note Title 1]]", "[[Note Title 2]]"],
"master_moc_section": "The existing section in the master Research Library MOC this note belongs under",
"topic_moc": "Optional topic MOC to update secondarily, if one clearly applies",
"tag_rationale": "Short note on which tags were reused from the master MOC/tag index and whether any new canonical tag is truly needed",
"tags": ["tag1", "tag2", "tag3"]
}Summarization rules:
slug: lowercase, hyphens only, 3-6 words, no special charslibrary_bucket: choose exactly one existing library folder based on the dominant themelibrary_path: always point at Research/Library/<bucket>/<slug>.mdtags: lowercase, 4-7 tags, topically relevant, hyphens in multi-word, with existing vault tags preferred over new inventionssections: Preserve the article's own structure. Include code blocks, tables,
numbered lists, and blockquotes from the original. DO NOT flatten rich content
into bullet points.key_takeaways_table: 4-8 rows. Each "lesson" is a short label; "detail" is
the specific, non-obvious insight. Avoid generic platitudes.relevance_to_repo: Connect the content to this repo's architecture, skills,
or workflow patterns. Be specific.related_notes: Use [[wiki-link]] format to connect to other Library notes.master_moc_section: prefer an existing folder section in 🗺️ MOC - Research Library; also use Recently Addedtopic_moc: optional; only set this if the note clearly belongs to a subordinate topic MOC such as 🤖 MOC - AI Agent Developmenttag_rationale: explicitly check the master MOC and Obsidian().tags().text output before deciding to introduce any new canonical tagsource: detect from URL (x.com→x, reddit.com→reddit, github.com→github, else→article)author: extract from content or URL. "Unknown" if not identifiableAfter generating the summary (Step 2), check whether the new note's core thesis conflicts with, supersedes, or complements existing vault notes. This step uses your own reasoning (the VS Code agent model) — no external API call.
Search the vault for existing notes that share tags or are in the same library bucket as the new note:
# Search by primary tags (run for each of the top 2-3 tags)
python -c "import sys; sys.stdout.reconfigure(encoding='utf-8'); sys.path.insert(0,'.github/skills/obsidian/scripts'); from obsidian import Obsidian; ob=Obsidian(); r=ob.search(query='<primary_tag>', path='Research/Library/<bucket>'); print(r.text if hasattr(r,'text') else r)"Also check the related_notes identified in Step 2 — read any that exist.
From the search results plus related notes, select the top 3-5 most related existing notes by this priority:
related_notes from Step 2For each of the top 3-5 related notes:
python -c "import sys; sys.stdout.reconfigure(encoding='utf-8'); sys.path.insert(0,'.github/skills/obsidian/scripts'); from obsidian import Obsidian; ob=Obsidian(); content=ob.read(path='<existing_note_path>'); print(content[:2000])"Extract the ## Core Thesis section (or the first substantive paragraph if no
Core Thesis heading exists). Compare it against the new note's core_thesis
from Step 2.
For each comparison, make a judgment call. This is semantic analysis, not keyword matching. Apply these definitions:
| Relationship | Definition | Action |
|---|---|---|
| Supersedes | The new source covers the same ground but with updated data, revised conclusions, or a more complete framework. The old note is no longer the best reference on its specific claim. | Add supersedes: [[old-note-slug]] to new note frontmatter. After writing (Step 4), update old note's frontmatter with status: superseded and superseded_by: [[new-note-slug]]. |
| Conflicts | The two sources genuinely disagree on a meaningful claim — not just emphasis or scope differences, but contradictory conclusions or incompatible frameworks. | Flag in output. Create a synthesis note (see 2.5d). Add both notes to each other's ## Related with a conflict annotation. |
| Complements | The new source adds a different angle on the same topic without contradicting. Different scope, different data, or different perspective that enriches understanding. | No special action — just ensure it appears in related_notes for Step 3. |
Judgment guidance:
If Supersedes:
supersedes: "[[old-note-slug]]"python -c "
import sys
sys.path.insert(0, '.github/skills/obsidian/scripts')
from obsidian import Obsidian
ob = Obsidian()
content = ob.read(path='<old_note_path>')
# Add superseded status to frontmatter
content = content.replace('status: unread', 'status: superseded', 1)
# If no status field, add one after the last frontmatter field before ---
# Also add superseded_by field
import re
if 'superseded_by:' not in content:
content = re.sub(r'^(---\s*$)', r'superseded_by: \"[[<new-note-slug>]]\"\n\1', content, count=1, flags=re.MULTILINE)
ob.create(path='<old_note_path>', content=content, overwrite=True)
print('Updated old note with superseded status')
"If Conflicts (genuinely important disagreement):
Flag in Step 7 output: "THESIS CONFLICT: [[new-note]] vs [[old-note]]: [explanation]"
Create a synthesis note that captures both sides of the disagreement:
---
type: research-note
source: synthesis
date_saved: {today YYYY-MM-DD}
tags: [synthesis, {shared-tags}]
status: unread
resolves:
- "[[note-slug-1]]"
- "[[note-slug-2]]"
---
# Conflict: {Topic} — {Slug1} vs {Slug2}
## The Disagreement
**[[{slug1}]]** argues: {thesis 1}
**[[{slug2}]]** argues: {thesis 2}
## Why It Matters
{2-3 sentences on why this tension is meaningful for the research library}
## Possible Resolution
{Your analysis of which position has stronger evidence, or whether they're
actually addressing different aspects of the same problem}
## Related
- [[{slug1}]] · [[{slug2}]]Write this synthesis note to:
Research/Library/<bucket>/conflict-<slug1>-vs-<slug2>.md
Add the synthesis note to both original notes' ## Related sections.
If Complements:
No special action. Ensure the existing note appears in related_notes (Step 2
output) so the ## Related section in the new note will include it.
Before proceeding to Step 3, briefly report what the drift check found:
Assemble the markdown. The note should be comprehensive enough to replace reading the original — a full technical reference, not a summary card.
If Step 2.5 found a supersession, include supersedes: "[[old-note-slug]]" in
the frontmatter. If a conflict was found, the synthesis note will be created
after this note is written (in Step 4).
Use this template as a starting point, but adapt sections to match the content:
---
type: research-note
source: {source}
author: "{author}"
url: {url}
date_found: {today YYYY-MM-DD}
date_saved: {today YYYY-MM-DD}
tags: [{comma-separated tags}]
status: unread
supersedes: "[[old-note-slug]]" # only if Step 2.5 found a supersession
---
**Author**: {author}
**Published**: {date}
**Views**: {views if known}
**Source**: {url}
**Engagement**: ❤️ {likes} 🔁 {reposts} 💬 {replies} 📝 {quotes}
---
## Core Thesis
{1-2 sentences: the central argument or finding}
---
## {Section 1 Heading from Article}
{Detailed content — preserve tables, code blocks, lists, quotes from original.
Include specific numbers, tool names, people. Don't flatten into generic bullets.}
## {Section 2 Heading}
{Continue for each major section of the source material...}
---
## Key Takeaways
| Lesson | Detail |
|--------|--------|
| {short label} | {specific, non-obvious insight} |
| ... | ... |
---
## Relevance to This Repo
{2-4 sentences connecting this to Eric Cartman's architecture, skills, or
the user's workflow. Be specific about what to keep, change, or investigate.}
---
## Related
- [[Note Title 1]] · [[Note Title 2]]
- External source: {url}
## My NotesFormatting rules:
--- horizontal rules between major sections for visual scanabilitybash, json, etc.)>) for direct quotes from the source**bold** for key terms and emphasis as in the original## Related should use [[wiki-links]] to connect to other Library notesAnd if it's a thread, add a Thread Context section before Summary:
## Thread Context
{thread_context}If images are present, embed them inline within the section they belong to
— right after the paragraph or heading they illustrate. Do NOT collect images into
a separate ## Images section at the end. Place each image where a reader would
naturally expect to see it, using this format:
![[{slug}-1.jpg]]
*{media_description_1}*Use ![[filename]] (Obsidian wiki-link embed) for each image. If
media_descriptions are available, add them as italic captions below each image.
If an image doesn't clearly belong to a specific section (e.g., a generic hero
image or author avatar), place it just below the metadata header block.
If the fetch result contains image_urls, download them to the vault's
attachment folder. First, discover the vault path:
python -c "import sys; sys.path.insert(0,'.github/skills/obsidian/scripts'); from obsidian import Obsidian; print(Obsidian().vault_info().text)"Parse the vault path from the output, then download images:
python .github/skills/obsidian-linked-research/scripts/fetch.py "<url>" --download-images "<vault_path>/Research/Library/attachments" --slug "{slug}"Or if you already have the fetch result and just need to download, call Python directly:
python -c "import sys; sys.path.insert(0,'.github/skills/obsidian-linked-research/scripts'); from fetch import download_images; download_images({image_urls_list}, '<vault_path>/Research/Library/attachments', '{slug}')"Images will be saved as {slug}-1.jpg, {slug}-2.png, etc.
Use ![[{slug}-1.jpg]] in the note to embed them.
PowerShell
$gotcha (Substack/Cloudinary CDN URLs):substackcdn.comimage URLs contain a signature segment like$s_!5LAd!,w_1456,.... PowerShell interpolates$s_as a variable and mangles the URL, producing a 404 even though the URL is valid. Do not pass these URLs inside apython -c "..."one-liner from PowerShell. Two fixes, in order of preference:
- Use the underlying origin URL instead — strip the
https://substackcdn.com/image/fetch/<sig>/prefix and URL-decode the remainder to get the plainhttps://substack-post-media.s3.amazonaws.com/public/images/<uuid>_<WxH>.pngURL. No$, no resizing params, full resolution.- If you must keep the CDN URL, write the URL list into a temp
.pyfile with theWritetool and runpython _tmp_dl.py— never inline it in a shell argument.Same rule applies to any one-liner: prefer a temp script over
python -cwhenever the payload contains$, backticks, or nested quotes.
Primary pattern (works in all environments, including Claude Code on Windows):
Use the Write tool to write the note content to a temp file, then call the Obsidian Python API directly:
# 1. Write content to temp file using the Write tool:
# file_path: Z:\Projects\Eric-Cartman\_tmp_note.md
# content: {full markdown content}
# 2. Call the Obsidian Python API (in a Bash tool):
python -c "
import sys
sys.path.insert(0, '.github/skills/obsidian/scripts')
from obsidian import Obsidian
with open('_tmp_note.md', 'r', encoding='utf-8') as f:
content = f.read()
ob = Obsidian()
result = ob.create(path='Research/Library/{library_bucket}/{slug}.md', content=content)
print(result)
"
# 3. Delete the temp file:
rm _tmp_note.mdIMPORTANT — keyword arguments required:
ob.create()does not accept positional arguments. Always usepath=andcontent=as keyword arguments.ob.create(path, content)will fail.
WARNING — do not use heredoc pipes from bash: The
@'...'@heredoc pipe pattern only works in a native PowerShell shell. When Claude Code runs bash on Windows, attemptingbash -c "powershell -Command \"@'...'@\""fails due to irreparably lossy quote escaping. Any note containing apostrophes, contractions, or code will break the heredoc approach. Use the Write-tool + Python API pattern above instead.
Verify the write:
python .github/skills/obsidian/scripts/obsidian.py read --path "Research/Library/{library_bucket}/{slug}.md" 2>&1 | head -6If the first few lines match your frontmatter, the write succeeded.
Check for existing notes before creating (deduplicate):
The primary dedup gate is the URL-based search in Step 0. This slug check is a secondary guard for the rare case where two different URLs produce the same slug:
python -c "
import sys
sys.path.insert(0, '.github/skills/obsidian/scripts')
from obsidian import Obsidian
ob = Obsidian()
result = ob.read(path='Research/Library/{library_bucket}/{slug}.md')
print('EXISTS' if result and not result.startswith('Error') else 'NOT_FOUND')
"If the note already exists, append -2, -3, etc. to the slug before calling ob.create().
Post-write drift actions (from Step 2.5):
After the new note is successfully written, apply any pending drift actions:
## Related sectionsAfter the note exists, refresh the master Research Library MOC as part of the same run unless doing so would require a major restructure.
At minimum:
Recently AddedRecently Added entries older than 7 days (see pruning logic below)If the master MOC would need a larger restructure, stop after creating the note and
suggest a separate obsidian-vault-linker refresh.
If you need to insert the note into an existing section (for example the correct
library bucket list plus Recently Added), prefer a clean overwrite instead of
stacking append-only blocks at the end of the file:
Recently Added entries (older than 7 days)Recently Addedcreate --overwriteExample pattern (use the Python API directly — do not use heredoc pipes from bash):
python -c "
import sys
sys.path.insert(0, '.github/skills/obsidian/scripts')
from obsidian import Obsidian
ob = Obsidian()
# Read current MOC
content = ob.read(path='Research/Library/00 MOC/🗺️ MOC - Research Library.md')
# Make in-memory edits (string replacement)
updated = content.replace(old_text, new_text)
# Overwrite — always use keyword args
ob.create(path='Research/Library/00 MOC/🗺️ MOC - Research Library.md', content=updated, overwrite=True)
print('MOC updated')
"Windows UTF-8:
ob.read()andob.create()handle encoding correctly. Do not go through subprocess pipes for MOC reads/writes — encoding errors will occur with emoji paths.
When writing a new Recently Added entry, always include an inline date tag so
the pruning step can identify stale entries without reading every linked note:
- [[{slug}|{title}]] — {one-line why this note matters} `{YYYY-MM-DD}`Example:
- [[five-ai-paradigm-shifts|Five AI Paradigm Shifts]] — Miessler's framework for agent-era mental models `2026-03-30`Whenever Step 5 runs (i.e., on every successful note save), prune Recently Added
entries whose inline date is more than 7 days before today. Entries without an
inline date are left untouched (they predate this feature).
Use this Python snippet as part of the in-memory MOC edit before rewriting:
import re
from datetime import date, timedelta
cutoff = date.today() - timedelta(days=7)
def prune_recently_added(moc_content: str) -> tuple[str, int]:
"""Remove Recently Added list items whose `YYYY-MM-DD` date is older than cutoff.
Returns (updated_content, number_pruned).
Items without a date tag are kept unchanged.
"""
# Match list items that end with a backtick-wrapped date: `2026-03-22`
date_pattern = re.compile(r'`(\d{4}-\d{2}-\d{2})`')
pruned = 0
output_lines = []
in_recently_added = False
for line in moc_content.splitlines(keepends=True):
# Detect entry into / exit from the Recently Added section
if re.match(r'^##\s+Recently Added', line):
in_recently_added = True
output_lines.append(line)
continue
if in_recently_added and re.match(r'^##\s+', line):
in_recently_added = False
if in_recently_added and line.startswith('- '):
m = date_pattern.search(line)
if m:
entry_date = date.fromisoformat(m.group(1))
if entry_date < cutoff:
pruned += 1
continue # drop this line
output_lines.append(line)
return ''.join(output_lines), prunedApply it before inserting the new entry:
updated_content, n_pruned = prune_recently_added(moc_content)
if n_pruned:
print(f"Pruned {n_pruned} stale Recently Added entries")
# then insert new entry and rewriteUse the lighter append approach only when a small footer-style refresh is
good enough and the file does not need an in-place insertion. Even in that case,
still run prune_recently_added on the current content first, rewrite with
pruning applied, then append the new entry.
If a topic MOC clearly applies, update it secondarily after the master MOC is current. The master MOC remains authoritative for canonical tags and library-wide freshness.
python -c "import sys; sys.path.insert(0,'.github/skills/obsidian/scripts'); from obsidian import Obsidian; ob=Obsidian(); ob.open('Research/Library/{library_bucket}/{slug}')"After writing the note and updating MOCs, run the connection detector to classify relationships between the new note and existing vault content:
python .github/skills/obsidian-connection-detector/scripts/detect.py --note "Research/Library/{library_bucket}/{slug}.md" --no-sectionThis is best-effort — if it fails or is unavailable, continue to Step 7. The detector
will classify relationships (supports, contradicts, extends, bridges) and write to
Research/connections.json. The thesis tracker will pick up new connections on its
next run and flag emerging theses.
Report to the user:
obsidian://open?vault=Obsidian%20Vault&file=Research%2FLibrary%2F{library_bucket_urlencoded}%2F{slug}Invoke the skill-reflection skill with the following context:
obsidian-linked-research.github/skills/obsidian-linked-research/SKILL.mdThe reflection skill will analyze the run and produce improvement recommendations.
The output is a Research/Library/<bucket>/{slug}.md file in the Obsidian vault,
following the enriched note template above. The format matches the existing
library pages and respects the live MOC/tag taxonomy already in the vault.
If thesis drift was detected, additional outputs may include:
status: superseded, superseded_by:)Research/Library/<bucket>/conflict-<slug1>-vs-<slug2>.mdfetch.py even if you think you know the contentob.create(path=, content=) with keyword args; never use positional arguments and never write vault files directly via filesystem toolsob.create(path=..., content=...), then delete the temp file; do not use @'...'@ heredocs from bash on Windows (they break on apostrophes and single quotes in note content)bash -c "powershell -Command \"...\"" for heredocs — quote escaping through that shell boundary is irreparably lossy; the Write-tool + Python API pattern avoids this entirelysys.stdout.reconfigure(encoding='utf-8') to one-liner subprocess commands, or call the Obsidian Python API directly (preferred)Research/Library/ frontmatter for the input URL in Step 0 before fetching; check slug in Step 4 as a secondary guardfetch.py uses only Python stdlibResearch/Library/00 MOC/🗺️ MOC - Research Library.md| Skill | Relationship |
|---|---|
obsidian | Composable vault wrapper — used for all vault writes |
obsidian-daily-research | Automated daily pipeline — produces Research/Dailies/ notes with #keep tags that get promoted to Research/Library/ |
obsidian-vault-lint | Weekly maintenance — Phase 3 taxonomy repair detects tag and folder drift; thesis drift check complements it at ingest time |
last30days | Research skill — shares xAI API patterns and keyring/env/config key cascade |
content-research-writer | Long-form writing partner — can use Library notes as sources |
User: "research this: <url>"
│
▼
┌─────────────────────────┐
│ Read master MOC │ ← Determine folder + canonical tags
│ + tag index + library │
└──────────┬──────────────┘
│
▼
┌─────────────────────────┐
│ fetch.py <url> │ ← Python stdlib only
│ ├─ Tweet? → xAI API │ (x_search tool, grok-4-1-fast)
│ └─ Web? → urllib │ (HTML strip, plain text)
└──────────┬──────────────┘
│ JSON stdout
▼
┌─────────────────────────┐
│ Agent (VS Code model) │ ← Summarize, structure, compose
│ ├─ Generate summary │
│ ├─ Thesis drift check │ ← NEW: compare against existing notes
│ │ ├─ Supersedes? │ Update old note frontmatter
│ │ ├─ Conflicts? │ Create synthesis note
│ │ └─ Complements? │ Add to Related
│ ├─ Compose markdown │
│ └─ Write → Python API │
└──────────┬──────────────┘
│ Write tool → _tmp_note.md → ob.create(path=, content=)
▼
┌─────────────────────────┐
│ obsidian.py create │ ← CLI wrapper (composable)
│ └─ Research/Library/ │
│ <bucket>/{slug}.md │
└──────────┬──────────────┘
│
▼
┌─────────────────────────┐
│ Post-write drift ops │ ← Update superseded notes, create synthesis
└──────────┬──────────────┘
│
▼
┌─────────────────────────┐
│ Update master MOC │ ← Keep library map and tags current
│ (+ topic MOC if needed)│
└─────────────────────────┘aa16c9e
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.