--- id: iusztinpaul/ai-research-os-workshop/research-render version: "0b2052f9" license: MIT install: manual updated: 2026-06-27 --- # research-render — Research Render transforms wiki pages into multi-form answers: Marp slide decks, matplotlib charts, Obsidian canvases, or social content briefs. Pick one or several formats in a single run; each renders independently and files back into wiki/renders/ where outputs compound alongside your other research artifacts. Publisher: iusztinpaul · Stars: 158 · Updated: 2026-06-27 Install (manual): `git clone https://github.com/iusztinpaul/ai-research-os-workshop` ## SKILL.md # Research Render Wiki pages are the substrate. This skill turns them into **multi-form answers** — slide decks, charts, canvases, social briefs — and files those outputs back into `wiki/renders//.{md,png,canvas}` so they compound just like any other wiki artifact. Four formats are supported: - **marp** — slide deck (Marp markdown) - **chart** — matplotlib chart (PNG + companion `.py` script for reproducibility) - **canvas** — Obsidian Canvas (`.canvas` JSON) - **brief** — social content brief (copy-ready Markdown for a LinkedIn post / Substack note / X thread / Reddit), a reusable idea seed you can paste into a draft A run can produce **several formats at once** — the user picks one or many up front (Step 1), and each chosen format is rendered independently. A fifth notional format ("table") was considered but rejected: tables are best embedded inside `comparison` wiki pages, which `/research` already produces via its `comparison_writer`. If you want a table, ask for a comparison. ## Step 1 — Gather inputs and pick the format(s) You need: - **format(s)** — one or more of `marp` | `chart` | `canvas` | `brief`. Resolve as follows: - If the user's verb pins **exactly one** format unambiguously ("make a slide deck" → `marp`, "chart this" → `chart`, "build a canvas" → `canvas`, "extract a post idea / write a brief" → `brief`), use it without asking. - Otherwise, present a single `AskUserQuestion` with **`multiSelect: true`** offering the four formats, so the user can pick one or render several at once. Capture the result as `selected_formats` (a list). - **source wiki pages** — one or more paths under `/wiki/`. Most renders draw from a single page; canvases, decks, and briefs often draw from multiple (overview + synthesis + a few entity/concept pages). - **prompt** — the user's framing (e.g., "compare BM25 and hybrid retrieval as a 5-slide deck for an internal tech-talk audience"). Verbatim into each render's frontmatter so future agents can reproduce. For a `brief`, the prompt also carries **the body structure the user wants** (the sections to cover) — the brief writer follows it for the body (see Format details → Brief). - **platform** *(brief only, optional)* — LinkedIn / Substack note / X thread / Reddit / generic. Infer from the prompt; default to a platform-agnostic brief. Do not interrupt with a separate question — keep the brief lightly platform-aware and leave true platform-final copy to the user. Locate `research_dir` the same way `/research` (query mode) and `/research-lint` do. Steps 2–4 run **once per selected format**. When several formats are chosen, run the idempotency checks per format and spawn the format writers **in parallel** (Step 4). ## Step 2 — Compute the slug + output paths Slug: - If the user gave a slug (rare): use it. - If sources are a single page: derive from that page's slug. - If sources are multiple: derive from the prompt (≤ 6 words, kebab-case). Output paths by format: | Format | Path | |---|---| | `marp` | `/wiki/renders/marp/.md` | | `chart` | `/wiki/renders/charts/.png` (+ `.py` next to it) | | `canvas` | `/wiki/renders/canvases/.canvas` | | `brief` | `/wiki/renders/briefs/.md` | Create the format directory if it doesn't exist: ```bash mkdir -p "/wiki/renders/" ``` ## Step 3 — Idempotency check Run this check for each selected format (each has its own output path). If the output already exists: - Read the existing file's frontmatter (for marp / brief, and the `.canvas.meta.yaml` sidecar for canvas) or companion `.py` (for chart). - Compare `prompt` and `sources` to the new run. - If identical, **return noop** without writing. - If different, ask the user via `AskUserQuestion` whether to overwrite, suffix-with-timestamp, or skip. ## Step 4 — Spawn the format-specific subagent Each format has a dedicated subagent that knows its output shape. Pass: - `format` - `source_pages` — absolute paths - `research_topic`, `input_summary` (from `index.yaml`) - `prompt` - `output_path` - `research_dir` - `platform` — *(brief only)* the inferred platform, or `generic` | Format | Agent file | |---|---| | `marp` | `agents/marp_writer.md` | | `chart` | `agents/chart_writer.md` | | `canvas` | `agents/canvas_writer.md` | | `brief` | `agents/brief_writer.md` | Each subagent reads its source pages (these are short — entity/concept/comparison/source wiki pages, not raw files), produces the render, and returns a JSON summary on stdout. The orchestrator never reads the source pages itself. When `selected_formats` has more than one entry, spawn all the chosen format writers **in parallel** — one Agent call per format in a single message. They're independent and write to different paths. For `chart` only: after the writer subagent saves the `.py` script, the orchestrator runs it via `uv run --script` to produce the `.png` (the script carries a PEP 723 header declaring matplotlib, so it self-bootstraps). The script is the source of truth; the PNG is regenerable. If execution fails, the script stays on disk and the failure is surfaced — the user can fix it and re-run. For `brief` only — **handle the `needs_guidance` return.** The brief writer is capped at ~1000 words. If covering the prompt's sections faithfully would exceed that, it writes nothing and returns `{"action": "needs_guidance", "estimated_words": N, "reason": "...", "options": [...]}`. When you get this, **do not** force the brief — surface it to the user via `AskUserQuestion`, using the writer's `reason` as context and its `options` as the choices (e.g., drop a section, split into two briefs, headline-level only, or raise the cap). Then re-spawn the brief writer with the tightened prompt (or the agreed higher cap). Don't run Steps 5–6 for a brief that returned `needs_guidance` and wasn't re-rendered. ## Step 5 — Update the index Renders compound. After all selected formats are written, regenerate `index.md` once so the new renders appear in the navigation: ```bash uv run --script ${CLAUDE_PLUGIN_ROOT:-.claude}/skills/research/scripts/build_index_md.py --research-dir "" ``` `index.yaml` does not need to be rebuilt — renders aren't sources, they don't have entries in the `sources:` array. The `total_wiki_pages` count is computed live by `build_index_md.py` from the wiki tree. ## Step 6 — Append log entry One entry per format rendered (share the date when several ran together): ```markdown ## [YYYY-MM-DD] render | | - format: - output: wiki/renders//. - sources: wiki page(s) — - prompt: "" ``` ## Step 7 — Present results Tell the user: - The output path (absolute) and how to view it, for **each** format rendered - **marp**: open in Obsidian with the Marp plugin, or `marp --watch ` from the CLI - **chart**: open the PNG in any viewer; the companion `.py` is alongside for editing - **canvas**: open in Obsidian (Canvas is a native plugin) - **brief**: open the `.md` in Obsidian; copy the body into a post draft, or use it as the idea seed for whatever content workflow you use - For multi-page renders, the list of source pages used - The `prompt` (so they can compare against future renders) ## Format details ### Marp Marp is a markdown-based slide format. The output file has a YAML frontmatter block configuring the deck, then `---` separators between slides: ```yaml --- marp: true theme: default paginate: true backgroundColor: white sources: [] prompt: "" created: --- # Title slide Content --- ## Second slide - bullet - bullet --- ... ``` The Marp Obsidian plugin renders this in-vault. Length: 5–15 slides typical; cap at 25 unless explicitly asked. Each slide ≤ 40 words of body text. ### Chart Charts are matplotlib outputs. The writer subagent produces a `.py` script that: - Imports matplotlib (`matplotlib.use("Agg")` for headless, then `import matplotlib.pyplot as plt`) - Builds the figure - Saves to a path passed in via `sys.argv[1]` (so the orchestrator can wire output paths cleanly) Companion files: ``` wiki/renders/charts/.py # the script (source of truth, editable) wiki/renders/charts/.png # the rendered chart (regenerable) ``` The script must include a frontmatter-equivalent comment block at the top: ```python # -- render metadata -- # format: chart # sources: # prompt: "" # created: # -- ``` Run it via: ```bash uv run --script "/wiki/renders/charts/.py" "/wiki/renders/charts/.png" ``` ### Canvas Obsidian Canvas files are JSON with a fixed schema (see Obsidian docs). The writer subagent produces a `.canvas` file with: - A frontmatter-style metadata block as a separate sidecar (Canvas files don't carry YAML frontmatter natively) — store at `wiki/renders/canvases/.canvas.meta.yaml` for the same `sources` / `prompt` / `created` info - Nodes: text nodes (one per source wiki page or one per claim) AND embed nodes (for any image assets the wiki references) - Edges: directed connections representing relationships (cites, depends-on, contradicts) Use Canvas for visual argument maps, entity-relationship views, and "how these concepts connect" overviews — anything where spatial layout adds meaning. ### Brief A brief is a copy-ready social content seed composed from wiki pages — the kind of "executive summary for a post" that a human can paste into a draft. It is **prose, never code**, and it follows a fixed three-part spine. The body is the only flexible part — the user's prompt dictates its sections. ```markdown --- type: brief format: brief platform: sources: - - prompt: "" created: --- # ## Opening — problem → solution ## ## Open questions 1. 2. 3. --- > Grounding: [[wiki/...]] citations + a one-line `> Synthesis:` note. ``` The spine, in order: 1. **Opening (problem → solution).** Always **start with the problem, told as a story** (a relatable scenario or pain), then continue with **the solution and the transformation** it unlocks (before → after). Across this opening, surface the **6 W's** — *why* (why it matters / why now), *what* (what the thing is), *how* (how it works at a high level), *who* (who it's for / who's involved), *where* (where it fits), *when* (when it applies). Weave them into the narrative; do **not** render them as a labeled checklist. 2. **Body — follow the user's request.** The prompt names the sections to cover; the body follows it beat-for-beat (e.g., "the 3 memory types, their dynamics, the pipeline, the triggers"). This is the flexible middle. 3. **Conclusion — 3 open questions.** Generate exactly **three open questions with the highest signal relative to the brief** — the ones a thoughtful reader (or the author) would most want answered next, and that genuinely extend or stress-test the idea. They double as the post's engagement closer and as research seeds. Generate them from the brief's own content; you may align them with the research dir's `wiki/open-questions.md` if it sharpens them, but never just copy that file wholesale. Conventions: - **Citations live in a footer**, not inline — the body stays paste-able. End with a `> Grounding:` line wikilinking the source pages, plus a `> Synthesis:` line (meta-judgment + what new source would extend the idea). - **Platform-aware, lightly.** Adapt length/voice to `platform` (LinkedIn ≈ 200–400 words, Substack note shorter, X thread = punchier beats, Reddit = plainer); default `generic` ≈ the executive-summary shape. The brief is a seed — leave true platform-final shaping to the user. - **No diagrams.** A brief is the text companion to a diagram the user already has; don't reproduce one in Mermaid. ## Important notes - **All output goes into `wiki/renders/`.** Renders are wiki artifacts; they compound. Never write to `working-dir` or anywhere outside the research dir. - **Source pages, not raw.** Renders read from `wiki/sources/`, `wiki/entities/`, `wiki/concepts/`, `wiki/comparisons/`, `wiki/synthesis.md`, `wiki/overview.md`. They do NOT read `raw/` files. If a render needs a quote that's only in raw, the user should first promote that quote into a source page. - **Idempotent.** Same prompt + same sources → same render. If you overwrite, log it. - **Reproducibility for charts.** The `.py` is mandatory. A PNG without script is not allowed — it forecloses future edits. - **Canvas is best-effort.** Obsidian's Canvas schema evolves; the writer aims for compatibility but visual layout may need touch-up in Obsidian after generation. - **No new dependencies.** matplotlib is already in `pyproject.toml`. Marp viewing is the user's responsibility (Obsidian plugin or CLI). No additional installs. - **Briefs are seeds, not final posts.** The brief is the upstream idea artifact; it deliberately stops short of platform-final copy. Don't over-polish it into a finished post. ## Agent reference - `agents/marp_writer.md` — produces a `.md` Marp deck - `agents/chart_writer.md` — produces a `.py` matplotlib script (the orchestrator runs it to make the PNG) - `agents/canvas_writer.md` — produces a `.canvas` JSON file plus a `.meta.yaml` sidecar - `agents/brief_writer.md` — produces a `.md` social content brief (problem→solution opening with the 6 W's, prompt-driven body, 3 high-signal open questions) [View on SkillFed](https://skillfed.io/iusztinpaul/ai-research-os-workshop/research-render) · [View on GitHub](https://github.com/iusztinpaul/ai-research-os-workshop)