name: write-quarto-notebook
description: Generate a self-contained Quarto notebook (.qmd) from an existing R / Python / Stata blog post + companion script on carlos-mendez.org. Renders the notebook locally to verify it works, packages it with the canonical script + a minimal _quarto.yml + a README.md into a <slug>.zip project bundle, then adds a "Quarto project (.zip)" link button to the post's front matter. Confirms scope before writing.
argument-hint: " [--no-render] [--no-link]"
disable-model-invocation: true
user-invocable: true
Write Quarto Notebook: executable companion for a published post
Produce a single, self-contained Quarto notebook (tutorial.qmd) that a
reader can open in Positron or RStudio, hit Render, and reproduce the entire
tutorial --- prose, code, output, and figures --- with no setup beyond a
working R / Python / Stata installation.
This skill closes a gap left by the other writing skills:
write-scriptproduces a runnable script (analysis.R/script.py/analysis.do) with no prose.write-postproduces a Hugo-rendered post (index.md) with prose but not executable end-to-end.notebook.ipynbis wired for Google Colab/IRkernel, not local Positron/RStudio.
The Quarto notebook is a third artifact: copy the prose from index.md,
copy the clean code blocks from index.md (which are the didactic versions of
the companion script), drop the static output blocks, regenerate every figure
inline, and let Quarto execute everything from a single file.
Supported languages: R, Python, Stata (auto-detected from slug + companion script).
What this skill does NOT do
- Does not write or modify prose, equations, or section structure. The
Hugo
index.mdis the authoritative source of all narrative content; the skill copies it verbatim with a small set of mechanical transforms. - Does not rewrite the companion script.
analysis.R/script.py/analysis.doremain the canonical execution record; the skill never edits them. - Does not commit or push. It leaves the new file (and the modified
index.md) in the working tree and offers a follow-up commit message. The user runs the commit. - Does not render or commit
tutorial.html. The.qmdis the source-of-truth artifact; the reader renders locally on demand. - Does not build a
featured.pngor AI podcast clip. Existing skills handle those.
Example invocations
# R post (companion script is analysis.R)
/project:write-quarto-notebook r_causalpolicy_workshop
/project:write-quarto-notebook r_did
# Python post (companion script is script.py)
/project:write-quarto-notebook python_doubleml
/project:write-quarto-notebook python_dowhy
# Stata post (companion script is analysis.do)
/project:write-quarto-notebook stata_rct
/project:write-quarto-notebook stata_cate2
# Skip the render step (useful for offline / unsupported environment)
/project:write-quarto-notebook r_causalpolicy_workshop --no-render
# Render but do NOT modify index.md's links: block
/project:write-quarto-notebook r_causalpolicy_workshop --no-link
Deliverables
| Language | Output path | Quarto theme | Engine line in YAML |
|---|---|---|---|
| R | content/post/<slug>/tutorial.qmd |
darkly |
(none --- knitr is default) |
| Python | content/post/<slug>/references/tutorial.qmd |
cosmo |
jupyter: python3 |
| Stata | content/post/<slug>/references/tutorial.qmd |
cosmo |
jupyter: nbstata |
These conventions are pinned to existing precedents:
- R:
content/post/r_demeaning_twfe/tutorial.qmd,content/post/r_dynamic_bma/tutorial.qmd - Python:
content/post/python_EconML/references/tutorial-econml-resource-curse.qmd - Stata:
content/post/stata_cate2/references/tutorial-cate-resource-curse.qmd
On success the skill also writes content/post/<slug>/<slug>.zip (the
downloadable Quarto-project bundle produced in Phase 4.5) and modifies
content/post/<slug>/index.md with a new links: entry: icon: file-code, name: "Quarto project (.zip)", url: <slug>.zip. See
Phase 4.5 for the bundle recipe and Phase 5 for the link
placement rule.
Site color palette
The same palette used by write-script and write-post. Apply it when the
skill needs to generate new ggplot/matplotlib code (e.g., a figure chunk
that lives in the source script but is missing from index.md).
| Name | Hex | Use |
|---|---|---|
| Steel blue | #6a9bcc |
Primary data |
| Warm orange | #d97757 |
Reference lines, treated unit |
| Near black | #141413 |
Annotations |
| Teal | #00d4c8 |
Highlights (sparingly) |
| Dark navy | #0f1729 |
Dark-theme figure background |
| Grid line | #1f2b5e |
Dark-theme grid |
| Light text | #c8d0e0 |
Dark-theme axis text |
| White text | #e8ecf2 |
Dark-theme titles |
The R darkly Quarto theme renders the page on a dark background; default
ggplot figures stay on a light panel by default (matching
r_demeaning_twfe/tutorial.qmd). Do not force a dark ggplot theme unless the
companion analysis.R explicitly does so.
Phase 1: Pre-flight
1.1 Parse arguments
Parse $ARGUMENTS into:
- Slug --- the first positional token (e.g.
r_causalpolicy_workshop). Mandatory. --no-render--- skip Phase 4 (render-and-fix). Implies--no-zip(Phase 4.5) and--no-link(Phase 5) — no point packaging or linking a notebook that hasn't been verified. Default: render is mandatory; the skill is not considered successful untilquarto renderexits 0.--no-link--- skip Phase 5 only (add link entry toindex.md). Phase 4.5 still runs — the ZIP is built and left in the bundle for manual review. Default: the link entry is added after a successful ZIP build.
Reject any other argument or flag with a clear error.
1.2 Locate the post
The post directory is content/post/<slug>/. Error out if it does not
exist.
1.3 Detect language
Apply these rules in order:
- If
analysis.Rexists in the post directory → R. - If
script.pyexists → Python. - If
analysis.doexists → Stata. - Otherwise inspect slug prefix:
r_*→ R,python_*→ Python,stata_*→ Stata. - If still ambiguous (e.g. multiple scripts) → stop and ask the user which language to use.
1.4 Verify required inputs
content/post/<slug>/index.md must exist (always required).
The matching companion script must exist (R: analysis.R, Python:
script.py, Stata: analysis.do). If missing, stop and tell the user to
run /project:write-script first.
1.5 Detect prior outputs
Compute the target output path (see the Deliverables table). If the file already exists, ask the user whether to overwrite. Do not silently clobber.
1.6 Check tooling
Run quarto --version. The skill requires Quarto ≥ 1.4. If missing, stop
and tell the user to install Quarto from https://quarto.org/docs/get-started/.
Language-specific tooling checks (only if Phase 4 will run):
- R: no parse-time check required. The setup-packages chunk in the
generated
.qmdwill bootstrappacmanand install missing CRAN packages on first render. - Python: confirm a Python with a Jupyter kernel exists.
quarto check jupyteris the canonical probe. If absent, surface a clear "install jupyter:pip install jupyter" error and stop (unless--no-render). - Stata: confirm the
nbstataJupyter kernel is installed. Probe withjupyter kernelspec list | grep -i stata. If absent, surface "install nbstata:pip install nbstata && python -m nbstata.install" and stop (unless--no-render).
1.7 Read source materials
Read index.md (the authoritative prose source) and the companion script
end-to-end. Count code blocks, figures, mermaid blocks, and math
expressions in index.md so you can report the totals in the scope block.
1.8 Probe installed package versions (for replicability)
The generated .qmd pins exact versions of every top-level package
so the notebook produces the same numbers when re-rendered months or
years later. The ground truth is whatever is installed on the
developer's machine right now --- that, by induction, is the
environment that produced the published post. See
references/language-conventions.md
§ Version pinning policy for the rationale.
Extract the top-level package list from the companion script:
- R: parse
pacman::p_load(...)(orlibrary(...)calls if pacman isn't used) fromanalysis.R. Collect every package name. - Python: parse top-level
import Xandfrom X import ...lines fromscript.py. Resolve PyPI names where they differ from import names (e.g.sklearn→scikit-learn,PIL→Pillow,cv2→opencv-python). - Stata: skip --- Stata SSC packages have no canonical version field. The skill notes this limitation in the scope block but does not try to pin Stata user-contributed packages.
Probe each version on the developer's machine:
- R, single batch call (fast):
Rscript -e 'pkgs <- c("tidyverse","sandwich","lmtest",...); \ for (p in pkgs) cat(sprintf("%s %s\n", p, \ tryCatch(as.character(packageVersion(p)), \ error = function(e) "NOT_INSTALLED")))' - Python, single batch call:
(with proper try/except around eachpython -c 'import importlib.metadata as m; \ pkgs = ["pandas","numpy",...]; \ [print(p, (m.version(p) if True else "NOT_INSTALLED")) \ for p in pkgs]'m.version(p)).
Capture the output as a dictionary {pkg: version}. Anything reporting
NOT_INSTALLED is logged for the scope block.
Handling missing packages. If the probe reports NOT_INSTALLED for
any package, surface it in the scope block under Could not probe.
Do not fabricate a version. Ask the user whether to:
- Install the package now and re-run the skill, OR
- Omit the version pin for that one package (the generated setup
chunk uses
pak::pkg_install("pkg")without@version, letting pak pick the latest version at render time).
Default to option 1 if the user doesn't override.
Phase 2: Confirm scope (MANDATORY)
Before writing anything, print a structured scope block and wait for explicit user confirmation. Use this template literally:
SCOPE
=====
Post slug: <slug>
Detected language: R | Python | Stata
Source files:
- content/post/<slug>/index.md (<N> lines)
- content/post/<slug>/<script> (<M> lines)
Output file: <output path from Deliverables table>
Quarto theme: darkly | cosmo
Engine: knitr (R) | jupyter: python3 | jupyter: nbstata
Render step: will run | SKIPPED (--no-render)
Index.md link: will be added | SKIPPED (--no-link)
Content detected in index.md:
- Sections: <K>
- R code blocks (```r): <a>
- Python code blocks: <b>
- Stata code blocks: <c>
- Mermaid diagrams: <d>
- Figure references (PNG): <e>
- Display math equations: <f>
Tooling check:
- quarto: <version>
- language: <ok | error message>
Pinned versions (top-level, will appear in setup-packages chunk):
- tidyverse@2.0.0
- sandwich@3.1.1
- lmtest@0.9.40
- <... one line per top-level package, with the probed version ...>
Could not probe:
- <list any package the probe reported NOT_INSTALLED, with action plan>
(skipped entirely for Stata — SSC has no canonical version field)
Ambiguities (if any):
- <list anything that needs human judgment>
Proceed? (y / explain change / cancel)
Wait for y before continuing. If the user replies with a change request,
adjust and re-print the scope block.
Phase 3: Generate the .qmd
Five transformation passes on the source index.md. Detailed rules live in
references/transformations.md; the
checklist below is the executable summary.
3.1 Write the YAML front matter
Use the language-specific template from
references/language-conventions.md.
All three languages share these fields:
title: <copy from index.md front-matter `title`>
subtitle: <one-line summary from index.md `summary` field, truncated to ~80 chars>
author: "Carlos Mendez"
date: <copy from index.md `date`, formatted as YYYY-MM-DD>
format:
html:
toc: true
toc-depth: 3
code-fold: true
code-summary: "Show code"
fig-width: 9
fig-height: 5.5
fig-dpi: 300
execute:
warning: false
message: false
Then add language-specific lines:
- R:
theme: darkly(underformat.html). - Python:
theme: cosmo,embed-resources: true, top-leveljupyter: python3. - Stata:
theme: cosmo,embed-resources: true, top-leveljupyter: nbstata.
3.2 Translate the section body
Walk index.md section by section. For each line, apply this dispatch
table:
| Source line type | Action |
|---|---|
| Prose, headings, tables, blockquotes | Copy verbatim |
Display math ($$...$$) |
Apply math-escape transform (see below) |
Inline math ($...$) |
Apply math-escape transform |
```mermaid fence |
Rewrite to ```{mermaid} (Quarto-native) |
```r / ```python / ```stata fence |
Rewrite to ```{r} / ```{python} / ```{stata}, add `# |
```text output block |
Drop entirely --- Quarto re-executes and prints |
 line |
Replace with the chunk that generates the figure (lift from companion script if missing from index.md) |
Internal site link /post/foo/ |
Rewrite to https://carlos-mendez.org/post/foo/ |
Hugo shortcode {{< ... >}} |
Drop or convert (see transformations.md) |
Math-escape transform. Goldmark + Hugo double-escapes backslashes
inside $$...$$; Quarto's MathJax doesn't need that. Apply these
substitutions to every math span:
\\,→\,(thin space)\\*→*(or^*if it follows^)\\{→\{\\}→\}\\^\*→^*\\Big→\Big,\\big→\big
Chunk labels. Every code chunk gets #| label: <slug> where slug is
predictable:
setup-packagesfor the first chunkdata-downloadfor the data-loading chunkdata-<entity>for derived datasets (e.g.data-california)fit-<method>for model fits (fit-did,fit-arima,fit-rdd,fit-naive)fig-<purpose>for figure-producing chunks (fig-raw-series,fig-forest)<topic>-<role>for everything else (its-arima-gap,sc-balance)
Figure chunks also get #| fig-cap: "<alt text from index.md>" and an
optional #| fig-height / #| fig-width override if the figure needs
extra room.
3.3 Synthesize the setup-packages chunk
This is the first executable chunk of the notebook --- positioned before the data-download chunk and after any "Overview / Potential outcomes" prose. It pins the exact versions probed in Phase 1.8 so the notebook is bit-reproducible when re-rendered on a fresh machine months later. Templates by language:
R: use pak::pkg_install("pkg@version") with the versions captured
in Phase 1.8. Substitute the probed versions in the vector below.
(The example shows r_causalpolicy_workshop's actual pin list.)
#| label: setup-packages
# Install pak (the fast modern installer) if missing. pak is the only
# bootstrap dependency; everything else gets pinned-version installs.
if (!requireNamespace("pak", quietly = TRUE)) {
install.packages("pak", repos = "https://cloud.r-project.org")
}
# Install the EXACT versions used when this post was published.
# Pinning fosters replicability: a reader who renders this notebook on
# any machine, any future date, gets the same numbers as the original.
pak::pkg_install(c(
"tidyverse@2.0.0",
"sandwich@3.1.1",
"lmtest@0.9.40",
"tidysynth@0.2.1",
"fpp3@1.0.3",
"mice@3.17.0",
"ranger@0.17.0",
"CausalImpact@1.3.0",
"broom@1.0.8",
"glue@1.8.0",
"forcats@1.0.0"
))
# Attach the packages we use directly.
library(tidyverse); library(sandwich); library(lmtest)
library(tidysynth); library(fpp3); library(mice)
library(ranger); library(CausalImpact)
library(broom); library(glue); library(forcats)
set.seed(42)
Python: probe-then-install pattern. Reinstall only when the installed version differs from the pin (saves time on warm machines).
#| label: setup-packages
import subprocess, sys, importlib.metadata
# {PyPI name: pinned version}. Substitute the versions probed in Phase 1.8.
PINNED = {
"pandas": "2.2.0",
"numpy": "1.26.4",
"scikit-learn": "1.4.1",
# ... one entry per top-level import from script.py ...
}
# Some PyPI names differ from their import names. The probe in Phase 1.8
# resolves these; record them here so version checks use the right key.
IMPORT_NAME = {"scikit-learn": "sklearn", "Pillow": "PIL",
"opencv-python": "cv2"}
for pkg, want in PINNED.items():
try:
have = importlib.metadata.version(pkg)
if have != want:
subprocess.check_call(
[sys.executable, "-m", "pip", "install", f"{pkg}=={want}"])
except importlib.metadata.PackageNotFoundError:
subprocess.check_call(
[sys.executable, "-m", "pip", "install", f"{pkg}=={want}"])
import random, numpy as np
random.seed(42); np.random.seed(42)
Stata: version pinning is not supported (SSC has no canonical
version field). The setup chunk emits the ssc install lines as
comments with a clear note about the limitation, and --- if available
--- the developer's install dates from which <package> as a
best-effort record.
* Version pinning is not supported for Stata user-contributed packages
* via `ssc install` --- SSC has no canonical version field. To replicate
* exactly, note the install dates from `which <package>` after first
* install. The skill records the developer's install dates inline below.
*
* Run these once on a fresh machine:
* ssc install <package>, replace // dev installed YYYY-MM-DD per `which`
3.4 Wire the data-download chunk
Find the data file referenced by the companion script. If the data lives
in the post directory (e.g. proposition99.rds, dataSIM4RCT.dta,
Iris.csv) hard-code the GitHub raw URL of this project:
https://raw.githubusercontent.com/cmg777/starter-academic-v501/master/content/post/<slug>/<filename>
Wrap the download in a if (!file.exists(...)) guard so re-renders skip
the network call. For external dataset URLs, leave the script's existing
download logic intact.
3.5 Add the "Source files" footer
Append a short final section to the .qmd:
## Source files
- Companion script: [`<script>`](https://raw.githubusercontent.com/cmg777/starter-academic-v501/master/content/post/<slug>/<script>)
- Published post: <https://carlos-mendez.org/post/<slug>/>
- GitHub repo: <https://github.com/cmg777/starter-academic-v501>
Write the assembled .qmd to the target path.
Phase 4: Render-and-fix loop (max 3 attempts)
Skip this phase if --no-render was given.
Run from the post directory (so caches and figure outputs land in the right place):
cd content/post/<slug> # (R) the .qmd lives here directly
# OR
cd content/post/<slug>/references # (Python / Stata) .qmd is one level deeper
quarto render tutorial.qmd 2>&1
If the command exits 0 → continue to Phase 5.
If it exits non-zero, classify the stderr against the auto-fix catalog in
references/render-and-fix.md, apply the
matching fix, and retry. The most common patterns are summarised here:
| Error pattern | Auto-fix |
|---|---|
there is no package called 'X' (R) |
Add X to the pak::pkg_install(...) vector with the probed version (re-run the Phase 1.8 probe for that one package), retry. |
ModuleNotFoundError: No module named 'X' (Python) |
Add X to the PINNED dict with the probed version, retry. |
pak: package 'pkg' not available at version 'x.y.z' |
Drop the @version suffix for that package on retry (install latest). Surface the substitution in the verification report as a [~] line: pkg pinned to <latest> instead of <wanted>. |
Could not find a version that satisfies the requirement pkg==x.y.z (pip) |
Drop the ==version for that package on retry. Surface the substitution in the verification report. |
404 Not Found on CRAN install URL |
Fall back to the CRAN archive direct URL: pak::pkg_install("https://cran.r-project.org/src/contrib/Archive/pkg/pkg_x.y.z.tar.gz"). Retry. |
nbstata kernel not found |
Stop; print "install nbstata: pip install nbstata && python -m nbstata.install". |
Duplicate chunk label '<label>' |
Renumber the offending labels, retry. |
Unknown LaTeX command \X or "MathJax could not parse" |
Re-apply math-escape transform aggressively, retry. |
object '<x>' not found inside a figure chunk |
Hoist the data-prep used by an earlier chunk into the failing one, retry. |
Can't select columns that don't exist (R, dplyr) |
Inspect the list-column being unnested; switch to a higher-level grab helper if available (e.g. tidysynth grab_synthetic_control()), retry. |
Mermaid syntax error |
Fall back to a static PNG generated via mermaid-cli if available, otherwise drop the diagram with a <!-- mermaid block omitted: <reason> --> placeholder. |
| Anything else | Stop. Print the raw error to the user. |
After 3 failed attempts (or any unrecognised error) stop and report. Do
not silently leave a broken .qmd behind --- but also do not delete it
(the user will want to inspect).
→ If render exits 0, continue to Phase 4.5 to build the downloadable project ZIP.
Phase 4.5: Build the <slug>.zip project bundle (default: yes)
Skip this phase if --no-render was given (no point bundling code
that hasn't been verified to render).
The reader-facing Quarto deliverable on this site is a ZIP archive
that unzips to a folder named <slug>/ containing the executable
tutorial plus everything needed to render it offline. A bare
tutorial.qmd download forces Positron / RStudio to prompt for a
project directory on first open; the unzipped folder is itself a
recognised Quarto project (thanks to a minimal _quarto.yml), so
there's no prompt.
The pattern was validated on content/post/r_did_ring/ in
2026-05-19; codified in
references/zip-bundle.md.
4.5.1 Files that go in the ZIP
Path inside <slug>.zip |
Source | Notes |
|---|---|---|
<slug>/tutorial.qmd |
Copy from the bundle (R: root, Python/Stata: references/) |
The Phase-3 artefact |
<slug>/<canonical-script> |
Copy from the bundle | R: analysis.R at root; Python: script.py at root; Stata: analysis.do at root |
<slug>/_quarto.yml |
Preserve the bundle's existing _quarto.yml if present, else generate the 2-line stub project:\n type: default\n |
Preserves pre-render hooks (e.g. python_pyfixest's setup_env.py) |
<slug>/README.md |
Generate from the language-appropriate template in references/zip-bundle.md |
Substitute <TITLE>, <SLUG>, <SCRIPT-NAME>, <DATA-NAME>, <METHOD-CITATION>, <DATA-CITATION> |
Not included. Data files (use tryCatch / probe-then-pull
from GitHub raw in tutorial.qmd), render outputs
(tutorial.html / tutorial_files/), CSV outputs, PNGs, the
results report, the infographic, featured.{png,webp}.
4.5.2 Recipe
Use the per-language bash recipe in references/zip-bundle.md §
Recipe. The shape is:
SLUG="<slug>"
WORK=$(mktemp -d)
mkdir -p "$WORK/$SLUG"
cp <bundle>/tutorial.qmd "$WORK/$SLUG/"
cp <bundle>/<canonical-script> "$WORK/$SLUG/"
# _quarto.yml: preserve if present, else generate stub
# README.md: from template
( cd "$WORK" && zip -r "$SLUG.zip" "$SLUG/" )
mv "$WORK/$SLUG.zip" "content/post/$SLUG/$SLUG.zip"
rm -rf "$WORK"
4.5.3 Post-build verification
Run unzip -l content/post/<slug>/<slug>.zip and confirm:
- Exactly 4 entries inside
<slug>/:_quarto.yml,tutorial.qmd,<canonical-script>,README.md(plus the bare<slug>/directory entry). - No
__MACOSX/entries (usemktempstaging, never zip a real working directory). - No
.DS_Storeentries. - README starts with
# <slug> — Quarto project. - ZIP size in the expected range (R 25–40 KB; Python 30–60 KB; Stata 25–40 KB).
If the ZIP is malformed, surface as [✗] in Phase 6 and abort
Phase 5 (no link entry without a valid ZIP to point to).
Phase 5: Add the link to index.md (default: yes)
Skip this phase if --no-link was given OR Phase 4 did not succeed
OR Phase 4.5 produced a malformed ZIP.
Insert a links: entry into index.md's YAML front matter. The link
points at the ZIP produced in Phase 4.5, not at the bare tutorial.qmd
— this lets Positron / RStudio open the unzipped folder as a recognised
Quarto project on first try. Use this exact template, substituting
<slug>:
- icon: file-code
icon_pack: fas
name: "Quarto project (.zip)"
url: <slug>.zip
The URL is bundle-relative and language-agnostic — always just
<slug>.zip at the post-bundle root, regardless of whether
tutorial.qmd lives at root (R) or in references/ (Python / Stata).
Hugo resolves the relative path at build time so the click routes
through Netlify (browsers always download .zip natively).
Placement rule. Scan the existing links: block in this order:
- If a Google Colab entry exists and an AI Podcast entry exists, insert the new entry between them.
- Else if a Google Colab entry exists, insert immediately after it.
- Else if a script entry exists (
R script,Python script,Stata script), insert immediately after it. - Else insert as the first entry of
links:.
Idempotency / upgrade rule. If a previous-version entry already
exists (older runs of this skill produced
name: "Quarto (.qmd)" with a raw.githubusercontent.com/...tutorial.qmd
URL), rewrite both the name: and url: fields in place to the new
ZIP form. Do not duplicate, do not leave the stale entry behind.
Phase 6: Verification report + follow-ups
Print a structured success block:
Verification
============
[✓] tutorial.qmd written to <path> (<N> lines)
[✓] quarto render succeeded (<elapsed>s)
[✓] tutorial.html produced (<K> figures inline)
[✓] <slug>.zip written to <path> (<size> KB, 4 files in <slug>/)
[✓] index.md links: entry inserted ("Quarto project (.zip)" → <slug>.zip)
[~] All pinned versions installed cleanly (or list any pkg that
fell back to latest)
Use [✗] for any step that was skipped (with the reason) or failed.
Use [~] for soft warnings (e.g. one pinned version was unavailable
and got substituted by the render-fix loop --- not a failure, but
worth surfacing).
Then offer 2–3 follow-up actions, written so the user can copy-paste:
Follow-ups
----------
1. Review the published post for consistency with the new notebook:
/project:review-post <slug>
2. Commit and push (Netlify auto-deploys):
git add content/post/<slug>/<output-path> content/post/<slug>/index.md \
logs/<YYYY-MM-DD>-<slug>-quarto.md
git commit -m "<slug>: add Quarto tutorial for local execution"
git push origin master
3. Open the rendered notebook locally:
open content/post/<slug>/<output-path-no-extension>.html
Do not auto-run any follow-up. The skill ends here.
Auto-fix recipes (summary)
Detailed pattern-and-action catalog: see
references/render-and-fix.md.
Language conventions (summary)
Detailed YAML templates, chunk-fence syntax, engine-specific gotchas: see
references/language-conventions.md.
Transformations applied to index.md → tutorial.qmd
Detailed transformation rules with examples: see
references/transformations.md.
ZIP project bundle (Phase 4.5)
Detailed bash recipe (per language), _quarto.yml preserve-rule, and
README.md templates: see
references/zip-bundle.md.
Verification checklist
Detailed go/no-go items (used by Phase 6 to decide success/fail) and
follow-up offer templates: see
references/verification-checklist.md.
Acceptance tests (for the skill itself)
Run after editing this SKILL.md to confirm the contract still works.
R reproduction. Move
content/post/r_causalpolicy_workshop/tutorial.qmdaside totutorial.qmd.before-skill. Invoke/project:write-quarto-notebook r_causalpolicy_workshop. The regenerated file must render cleanly and structurally match the backup (line count within ±10%, same number of chunks, same#| label:slugs).Python smoke. Pick a
python_*post (e.g.python_doublemlif it lacks areferences/tutorial.qmd). Invoke the skill. The generated.qmdmust usejupyter: python3and render in the project's Python environment.Stata smoke. Pick a
stata_*post (e.g.stata_rct). Invoke the skill. The generated.qmdmust usejupyter: nbstata. Ifnbstatais not installed locally, the skill must surface the install hint and stop before writing a broken file.Render-fix loop. Manually inject
library(does_not_exist)into a generated.qmd, re-render via Phase 4. The loop must add the package topacman::p_load(...)(and fail cleanly when the package truly does not exist).--no-renderflag./project:write-quarto-notebook <slug> --no-renderwrites the.qmd, prints[✗] render: skipped (--no-render)in the verification report, and does not modifyindex.md. The ZIP bundle (Phase 4.5) is also skipped because--no-renderimplies--no-zip— no point packaging unverified code.ZIP project bundle round-trip. Invoke the skill on an R post without a pre-existing
_quarto.ymlin the bundle.content/post/<slug>/<slug>.zipmust exist after Phase 4.5. Rununzip -land confirm exactly 4 entries inside the<slug>/folder:tutorial.qmd,analysis.R,_quarto.yml(the 2-line stub), andREADME.md. The Phase-5 link entry must readname: "Quarto project (.zip)"withurl: <slug>.zip. If the bundle already had_quarto.yml(e.g.python_pyfixest), the skill must copy the existing file verbatim, not overwrite it with the stub.