From 3d2ece41bd2a999b5d32a4903f0209ee2ed390e0 Mon Sep 17 00:00:00 2001 From: Douglas Ezra Morrison Date: Tue, 26 May 2026 19:08:25 -0700 Subject: [PATCH 1/8] feat: add reprexes skill (.claude/skills/reprexes) --- .claude/skills/reprexes/SKILL.md | 128 +++++++++++++++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 .claude/skills/reprexes/SKILL.md diff --git a/.claude/skills/reprexes/SKILL.md b/.claude/skills/reprexes/SKILL.md new file mode 100644 index 0000000..a669c03 --- /dev/null +++ b/.claude/skills/reprexes/SKILL.md @@ -0,0 +1,128 @@ +--- +name: reprexes +description: Isolate a technical problem into a minimal reproducible example ("reprex") and iterate fixes on that instead of inside the full application. Use when debugging a bug whose cause isn't obvious after a first look, when a failure only surfaces deep in a large pipeline / app / render, when the full-context test loop is slow, or before filing an upstream issue. Invoke explicitly with /reprexes. +user-invocable: true +allowed-tools: + - Bash + - Read + - Write + - Edit +--- + +# reprexes + +When a technical problem is hard to pin down, **don't debug it in the full +context of the motivating application.** Extract a minimal, self-contained +reproduction of the phenomenon, iterate candidate fixes on *that* (a fast, +clean loop), then port the working fix back to the real code. + +Reference: . The +payoff is real: ~80% of the time, the act of building a thorough reprex +surfaces the cause on its own — the noise you strip away was hiding it. + +## When this fires + +- A bug whose cause isn't obvious after a first read of the code. +- A failure that only shows up deep inside a large pipeline, app, or a full + `quarto render` — where each reproduction attempt is slow. +- Iterating on a fix that's expensive to test in the full context. +- Before asking a human for help or filing an upstream/package issue — a + reprex is what they'll ask for anyway. + +Don't bother for a one-line obvious fix, or a problem you can already see and +test cheaply in place. + +## The two requirements + +A reprex is only useful if it is BOTH: + +1. **Reproducible** — it captures *everything* needed to trigger the + phenomenon: every `library()` call, and code that creates every object it + uses. Someone (or a fresh session) can run it cold and see the same thing. +2. **Minimal** — everything unrelated to the phenomenon is stripped out: + small or built-in data instead of the real dataset, only the lines that + matter. + +The tension between these two is the whole game: include enough to reproduce, +but nothing more. + +## Procedure + +1. **Hypothesize the minimal trigger.** What is the smallest combination of + data + operation you believe causes the phenomenon? +2. **Create a standalone scratch file** outside the repo tree (e.g. + `/tmp/reprex.R`, or a tiny `/tmp/reprex.qmd` for a render bug). Put in it, + in order: + - the package loads (`library(...)`), + - the minimal data (see tactics below), + - the minimal code that triggers the phenomenon, with a comment marking + where it goes wrong. +3. **Run it in a clean session** and confirm it reproduces. For R, run with + `Rscript /tmp/reprex.R` (a fresh process — no stale `globalenv()` state + masking or faking the bug). For a Quarto page, render just that file: + `quarto render /tmp/reprex.qmd --to html`, not the whole site. +4. **Minimize.** Remove pieces until the phenomenon disappears — the last + removal that "fixes" it implicates that piece. (Or build up from nothing + until it appears.) Keep the data as small as it can be while still failing. +5. **Iterate fixes on the reprex**, not the full app. This is the fast loop + the whole technique exists to create. +6. **Port the fix back** to the real code and verify it there. +7. **Clean up.** Delete the scratch file (it lives in `/tmp`, so it never + touches the repo). If the bug was subtle, consider promoting the reprex + into a real regression test (`testthat`) instead of discarding it. + +## Minimizing the data + +- Prefer a **built-in dataset** (`mtcars`, `iris`, `mpg`) or a hand-built + tiny frame over the real data. +- If you must use a slice of real data, serialize the minimal slice with + `dput()` so the reprex recreates it inline — no external file dependency. +- Shrink to the fewest rows/columns that still show the phenomenon. + +## R / Quarto specifics + +- In R packages and Quarto projects, reprexes are usually short R snippets or + a single standalone `.qmd` page. Respect the repo's lint config + (`.lintr` / `.lintr.R`) if the reprex code will be ported back. +- The **`reprex` package** (tidyverse, ) + formats a reprex for sharing: it runs your code in a clean, separate R + session via `rmarkdown::render()` and emits code **plus actual output**. + Copy the code and call `reprex::reprex()` (reads the clipboard by default), + or point it at a file with `reprex(input = "/tmp/reprex.R")` — handy from a + non-interactive CLI session where there's no clipboard. Useful arguments: + - `venue =` — output format: `"gh"` (GitHub-flavored Markdown, default), + `"so"`/`"ds"` (Stack Overflow / Discourse), `"slack"`, `"R"` (runnable + script with commented output), `"html"`, `"rtf"`. + - `session_info = TRUE` — append `sessionInfo()` / `sessioninfo::session_info()`, + so versions travel with the reprex (set this when the bug may be + version-dependent). + - `std_out_err = TRUE` — capture stdout/stderr too (for output that doesn't + come back as normal R results). + - `wd =` — set the working directory when the code needs one. + - Use this when the reprex is destined for a PR comment or an upstream + issue. Companion helpers clean up "wild-caught" reprexes: `reprex_clean()` + (strip a GitHub/SO paste), `reprex_rescue()` (from console output), and + `reprex_invert()` (recover clean code from a rendered reprex). + - Validation bonus: because `reprex()` runs in a fresh session, if it errors + on a missing object or package, your example wasn't actually + self-contained — fix that before sharing. +- When the bug might be **version-dependent**, capture `sessionInfo()` (or + run `tidyverse_update()`) in the reprex so versions are part of the record. +- Build artifacts (`_site/`, `_freeze/`, `.quarto/`) are common confounders + for "it renders differently" bugs — a clean standalone render sidesteps + stale freeze caches. + +## Before declaring the reprex good + +- A fresh session runs it top-to-bottom and shows the phenomenon (and nothing + else breaks first). +- It references no object, file, or option it didn't create itself. +- It's as small as you can make it and still reproduce. + +## Don't + +- Don't paste the entire app/module — that's the opposite of a reprex. +- Don't commit scratch reprex files; keep them in `/tmp` or a gitignored + scratch path. +- Don't iterate fixes in the slow full-context loop once you have a reprex + that reproduces. From f9629c26f4cc4d0d77b598a9fee5ceaa384659b5 Mon Sep 17 00:00:00 2001 From: Douglas Ezra Morrison Date: Tue, 26 May 2026 21:05:44 -0700 Subject: [PATCH 2/8] reprexes skill: sync canonical (fold description; callr/tidyverse_update/lintr accuracy fixes) --- .claude/skills/reprexes/SKILL.md | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/.claude/skills/reprexes/SKILL.md b/.claude/skills/reprexes/SKILL.md index a669c03..91eb513 100644 --- a/.claude/skills/reprexes/SKILL.md +++ b/.claude/skills/reprexes/SKILL.md @@ -1,6 +1,12 @@ --- name: reprexes -description: Isolate a technical problem into a minimal reproducible example ("reprex") and iterate fixes on that instead of inside the full application. Use when debugging a bug whose cause isn't obvious after a first look, when a failure only surfaces deep in a large pipeline / app / render, when the full-context test loop is slow, or before filing an upstream issue. Invoke explicitly with /reprexes. +description: > + Isolate a technical problem into a minimal reproducible example ("reprex") + and iterate fixes on that instead of inside the full application. Use when + debugging a bug whose cause isn't obvious after a first look, when a failure + only surfaces deep in a large pipeline / app / render, when the full-context + test loop is slow, or before filing an upstream issue. Invoke explicitly + with /reprexes. user-invocable: true allowed-tools: - Bash @@ -17,8 +23,8 @@ reproduction of the phenomenon, iterate candidate fixes on *that* (a fast, clean loop), then port the working fix back to the real code. Reference: . The -payoff is real: ~80% of the time, the act of building a thorough reprex -surfaces the cause on its own — the noise you strip away was hiding it. +payoff is real: often, the act of building a thorough reprex surfaces the +cause on its own — the noise you strip away was hiding it. ## When this fires @@ -73,7 +79,7 @@ but nothing more. ## Minimizing the data -- Prefer a **built-in dataset** (`mtcars`, `iris`, `mpg`) or a hand-built +- Prefer a **built-in dataset** (`mtcars`, `mpg`) or a hand-built tiny frame over the real data. - If you must use a slice of real data, serialize the minimal slice with `dput()` so the reprex recreates it inline — no external file dependency. @@ -82,11 +88,12 @@ but nothing more. ## R / Quarto specifics - In R packages and Quarto projects, reprexes are usually short R snippets or - a single standalone `.qmd` page. Respect the repo's lint config - (`.lintr` / `.lintr.R`) if the reprex code will be ported back. + a single standalone `.qmd` page. Respect the repo's lint config if the + reprex code will be ported back. - The **`reprex` package** (tidyverse, ) formats a reprex for sharing: it runs your code in a clean, separate R - session via `rmarkdown::render()` and emits code **plus actual output**. + session (via `callr` since reprex 2.0) and emits code **plus actual + output**. Copy the code and call `reprex::reprex()` (reads the clipboard by default), or point it at a file with `reprex(input = "/tmp/reprex.R")` — handy from a non-interactive CLI session where there's no clipboard. Useful arguments: @@ -106,8 +113,11 @@ but nothing more. - Validation bonus: because `reprex()` runs in a fresh session, if it errors on a missing object or package, your example wasn't actually self-contained — fix that before sharing. -- When the bug might be **version-dependent**, capture `sessionInfo()` (or - run `tidyverse_update()`) in the reprex so versions are part of the record. +- When the bug might be **version-dependent**, capture `sessionInfo()` (or set + `session_info = TRUE` above) in the reprex so versions are part of the + record. If you suspect *stale* packages are the cause, `tidyverse_update()` + outside the reprex can rule that out — but it updates packages, it doesn't + record versions, so don't put it in the reprex itself. - Build artifacts (`_site/`, `_freeze/`, `.quarto/`) are common confounders for "it renders differently" bugs — a clean standalone render sidesteps stale freeze caches. From aa9d455f118f1fdb368600a4a65af7deaa683ca5 Mon Sep 17 00:00:00 2001 From: Douglas Ezra Morrison Date: Tue, 26 May 2026 21:27:11 -0700 Subject: [PATCH 3/8] reprexes skill: sync canonical (venue bullets/r, helper+std_out_err wording, tempfile note) --- .claude/skills/reprexes/SKILL.md | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/.claude/skills/reprexes/SKILL.md b/.claude/skills/reprexes/SKILL.md index 91eb513..c04acb4 100644 --- a/.claude/skills/reprexes/SKILL.md +++ b/.claude/skills/reprexes/SKILL.md @@ -57,8 +57,9 @@ but nothing more. 1. **Hypothesize the minimal trigger.** What is the smallest combination of data + operation you believe causes the phenomenon? 2. **Create a standalone scratch file** outside the repo tree (e.g. - `/tmp/reprex.R`, or a tiny `/tmp/reprex.qmd` for a render bug). Put in it, - in order: + `/tmp/reprex.R`, or a tiny `/tmp/reprex.qmd` for a render bug; on a + non-Unix machine use `tempfile(fileext = ".R")` / `tempdir()` for a + portable path). Put in it, in order: - the package loads (`library(...)`), - the minimal data (see tactics below), - the minimal code that triggers the phenomenon, with a comment marking @@ -97,19 +98,24 @@ but nothing more. Copy the code and call `reprex::reprex()` (reads the clipboard by default), or point it at a file with `reprex(input = "/tmp/reprex.R")` — handy from a non-interactive CLI session where there's no clipboard. Useful arguments: - - `venue =` — output format: `"gh"` (GitHub-flavored Markdown, default), - `"so"`/`"ds"` (Stack Overflow / Discourse), `"slack"`, `"R"` (runnable - script with commented output), `"html"`, `"rtf"`. + - `venue =` — output format: + - `"gh"` — GitHub-flavored Markdown (default) + - `"so"` / `"ds"` — Stack Overflow / Discourse + - `"slack"` — Slack message + - `"r"` — runnable R script with commented output + - `"html"` — HTML + - `"rtf"` — rich text for presentations - `session_info = TRUE` — append `sessionInfo()` / `sessioninfo::session_info()`, so versions travel with the reprex (set this when the bug may be version-dependent). - - `std_out_err = TRUE` — capture stdout/stderr too (for output that doesn't - come back as normal R results). + - `std_out_err = TRUE` — capture stdout/stderr too (e.g. `system()` / + subprocess or C-level output that doesn't come back as normal R results). - `wd =` — set the working directory when the code needs one. - Use this when the reprex is destined for a PR comment or an upstream - issue. Companion helpers clean up "wild-caught" reprexes: `reprex_clean()` - (strip a GitHub/SO paste), `reprex_rescue()` (from console output), and - `reprex_invert()` (recover clean code from a rendered reprex). + issue. Companion helpers handle "wild-caught" reprexes: `reprex_clean()` + (strip the prompts/output from a copied reprex), `reprex_rescue()` + (recover code from R-console output with `>`/`+` prompts), and + `reprex_invert()` (turn a rendered reprex back into plain code). - Validation bonus: because `reprex()` runs in a fresh session, if it errors on a missing object or package, your example wasn't actually self-contained — fix that before sharing. From 4ec0419993cb227493f928165ed5cc94deeb68d8 Mon Sep 17 00:00:00 2001 From: Douglas Ezra Morrison Date: Tue, 26 May 2026 21:50:48 -0700 Subject: [PATCH 4/8] reprexes skill: sync canonical (one-sentence-per-line intro; tidyverse::tidyverse_update()) --- .claude/skills/reprexes/SKILL.md | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/.claude/skills/reprexes/SKILL.md b/.claude/skills/reprexes/SKILL.md index c04acb4..eee6314 100644 --- a/.claude/skills/reprexes/SKILL.md +++ b/.claude/skills/reprexes/SKILL.md @@ -22,8 +22,8 @@ context of the motivating application.** Extract a minimal, self-contained reproduction of the phenomenon, iterate candidate fixes on *that* (a fast, clean loop), then port the working fix back to the real code. -Reference: . The -payoff is real: often, the act of building a thorough reprex surfaces the +Reference: . +The payoff is real: often, the act of building a thorough reprex surfaces the cause on its own — the noise you strip away was hiding it. ## When this fires @@ -96,7 +96,8 @@ but nothing more. session (via `callr` since reprex 2.0) and emits code **plus actual output**. Copy the code and call `reprex::reprex()` (reads the clipboard by default), - or point it at a file with `reprex(input = "/tmp/reprex.R")` — handy from a + or point it at a file with `reprex(input = "/tmp/reprex.R")` (or a + `tempfile(fileext = ".R")` path on non-Unix machines) — handy from a non-interactive CLI session where there's no clipboard. Useful arguments: - `venue =` — output format: - `"gh"` — GitHub-flavored Markdown (default) @@ -112,17 +113,20 @@ but nothing more. subprocess or C-level output that doesn't come back as normal R results). - `wd =` — set the working directory when the code needs one. - Use this when the reprex is destined for a PR comment or an upstream - issue. Companion helpers handle "wild-caught" reprexes: `reprex_clean()` - (strip the prompts/output from a copied reprex), `reprex_rescue()` + issue. Companion helpers handle "wild-caught" reprexes (all exported in + reprex 2.x): `reprex_clean()` (strip the `#>` output markers from a + rendered/pasted reprex, leaving runnable code), `reprex_rescue()` (recover code from R-console output with `>`/`+` prompts), and - `reprex_invert()` (turn a rendered reprex back into plain code). + `reprex_invert()` (the inverse of `reprex()` — recover the input code + from a rendered reprex). - Validation bonus: because `reprex()` runs in a fresh session, if it errors on a missing object or package, your example wasn't actually self-contained — fix that before sharing. - When the bug might be **version-dependent**, capture `sessionInfo()` (or set `session_info = TRUE` above) in the reprex so versions are part of the - record. If you suspect *stale* packages are the cause, `tidyverse_update()` - outside the reprex can rule that out — but it updates packages, it doesn't + record. If you suspect *stale* packages are the cause, + `tidyverse::tidyverse_update()` outside the reprex can rule that out — but it + updates packages, it doesn't record versions, so don't put it in the reprex itself. - Build artifacts (`_site/`, `_freeze/`, `.quarto/`) are common confounders for "it renders differently" bugs — a clean standalone render sidesteps @@ -138,7 +142,7 @@ but nothing more. ## Don't - Don't paste the entire app/module — that's the opposite of a reprex. -- Don't commit scratch reprex files; keep them in `/tmp` or a gitignored - scratch path. +- Don't commit scratch reprex files; keep them in `/tmp` (or `tempdir()` on + non-Unix machines) or a gitignored scratch path. - Don't iterate fixes in the slow full-context loop once you have a reprex that reproduces. From 4622bcc3c38f42f9920d35fa9d26c9acae608534 Mon Sep 17 00:00:00 2001 From: Douglas Ezra Morrison Date: Tue, 26 May 2026 21:58:30 -0700 Subject: [PATCH 5/8] reprexes skill: sync canonical (usage note + helpers out of args list) --- .claude/skills/reprexes/SKILL.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.claude/skills/reprexes/SKILL.md b/.claude/skills/reprexes/SKILL.md index eee6314..b376fb3 100644 --- a/.claude/skills/reprexes/SKILL.md +++ b/.claude/skills/reprexes/SKILL.md @@ -98,7 +98,8 @@ but nothing more. Copy the code and call `reprex::reprex()` (reads the clipboard by default), or point it at a file with `reprex(input = "/tmp/reprex.R")` (or a `tempfile(fileext = ".R")` path on non-Unix machines) — handy from a - non-interactive CLI session where there's no clipboard. Useful arguments: + non-interactive CLI session where there's no clipboard. Use it when the + output is destined for a PR comment or an upstream issue. Useful arguments: - `venue =` — output format: - `"gh"` — GitHub-flavored Markdown (default) - `"so"` / `"ds"` — Stack Overflow / Discourse @@ -112,16 +113,15 @@ but nothing more. - `std_out_err = TRUE` — capture stdout/stderr too (e.g. `system()` / subprocess or C-level output that doesn't come back as normal R results). - `wd =` — set the working directory when the code needs one. - - Use this when the reprex is destined for a PR comment or an upstream - issue. Companion helpers handle "wild-caught" reprexes (all exported in - reprex 2.x): `reprex_clean()` (strip the `#>` output markers from a - rendered/pasted reprex, leaving runnable code), `reprex_rescue()` - (recover code from R-console output with `>`/`+` prompts), and - `reprex_invert()` (the inverse of `reprex()` — recover the input code - from a rendered reprex). - Validation bonus: because `reprex()` runs in a fresh session, if it errors on a missing object or package, your example wasn't actually self-contained — fix that before sharing. + + Companion helpers handle "wild-caught" reprexes (all exported in reprex + 2.x): `reprex_clean()` (strip the `#>` output markers from a rendered/pasted + reprex, leaving runnable code), `reprex_rescue()` (recover code from + R-console output with `>`/`+` prompts), and `reprex_invert()` (the inverse + of `reprex()` — recover the input code from a rendered reprex). - When the bug might be **version-dependent**, capture `sessionInfo()` (or set `session_info = TRUE` above) in the reprex so versions are part of the record. If you suspect *stale* packages are the cause, From 735687530db8425cc6e38e869cc3c9b6a164b215 Mon Sep 17 00:00:00 2001 From: Douglas Ezra Morrison Date: Wed, 3 Jun 2026 15:16:12 -0700 Subject: [PATCH 6/8] Address review nits on reprexes skill - Add Glob to allowed-tools so the skill can locate files to port fixes back - Trim redundant /tmp deletion claim in step 7 (don't-commit is the real action) - Surface the self-containment check as a named sub-bullet Co-Authored-By: Claude Opus 4.8 --- .claude/skills/reprexes/SKILL.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/.claude/skills/reprexes/SKILL.md b/.claude/skills/reprexes/SKILL.md index b376fb3..2881768 100644 --- a/.claude/skills/reprexes/SKILL.md +++ b/.claude/skills/reprexes/SKILL.md @@ -13,6 +13,7 @@ allowed-tools: - Read - Write - Edit + - Glob --- # reprexes @@ -74,9 +75,9 @@ but nothing more. 5. **Iterate fixes on the reprex**, not the full app. This is the fast loop the whole technique exists to create. 6. **Port the fix back** to the real code and verify it there. -7. **Clean up.** Delete the scratch file (it lives in `/tmp`, so it never - touches the repo). If the bug was subtle, consider promoting the reprex - into a real regression test (`testthat`) instead of discarding it. +7. **Clean up.** Don't commit the scratch file — keep it in `/tmp` (or a + gitignored scratch path). If the bug was subtle, consider promoting the + reprex into a real regression test (`testthat`) instead of discarding it. ## Minimizing the data @@ -113,8 +114,8 @@ but nothing more. - `std_out_err = TRUE` — capture stdout/stderr too (e.g. `system()` / subprocess or C-level output that doesn't come back as normal R results). - `wd =` — set the working directory when the code needs one. - - Validation bonus: because `reprex()` runs in a fresh session, if it errors - on a missing object or package, your example wasn't actually + - **Self-containment check:** because `reprex()` runs in a fresh session, if + it errors on a missing object or package, your example wasn't actually self-contained — fix that before sharing. Companion helpers handle "wild-caught" reprexes (all exported in reprex From c669f4c1223f5659b8723c96ad72d6a8c1f1b41f Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 20 Jun 2026 09:26:09 +0000 Subject: [PATCH 7/8] fix: add Grep to reprexes allowed-tools Round-2 review nit: Grep was absent from allowed-tools, the same class of observation as the Glob finding addressed in round 1. Adding it makes the skill fully self-contained for symbol searches when porting a fix back. Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_01SNxbcZHy5tYHWAzeJ9XL4J --- .claude/skills/reprexes/SKILL.md | 1 + 1 file changed, 1 insertion(+) diff --git a/.claude/skills/reprexes/SKILL.md b/.claude/skills/reprexes/SKILL.md index 2881768..a70c9f6 100644 --- a/.claude/skills/reprexes/SKILL.md +++ b/.claude/skills/reprexes/SKILL.md @@ -14,6 +14,7 @@ allowed-tools: - Write - Edit - Glob + - Grep --- # reprexes From 13c706166dbddbe99748d9734233e6d8fed3b868 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 20 Jun 2026 09:36:02 +0000 Subject: [PATCH 8/8] ci: re-trigger build-deploy (transient Pages CDN failure) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previous run failed because the gh-pages branch was being updated by two PR preview deployments simultaneously, causing Pages CDN to error. No code change — re-triggering CI. Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_01SNxbcZHy5tYHWAzeJ9XL4J