Slide review
Skill ericluo04/claude-academic-workflow/skills/slide-review
Academic research workflow for Claude Code: 14 skills (paper reading, lit review, bib auditing, preregistration, referee reports, replication packages, LaTeX, TikZ) plus a Quarto reveal.js slide system with render-time quality gates. Entirely AI-generated with Claude Code as a proof of concept.
npx -y skills add ericluo04/claude-academic-workflow --skill slide-reviewAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
2 things to look at
- no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
- 5 stars5 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Render a Quarto reveal.js deck, screenshot every slide in a real browser, and review the pictures for overflow, unreadable type, low contrast, broken figures, failed math, and a weak argument, on a light talk deck or a dark lecture deck, and check that an offline-variant deck still works with no network. TRIGGER on "review my slides", "check my deck", "audit my slides", "pre-talk check", "pre-flight my deck", "am I ready to present", "are my slides readable", "will this deck work offline", "does my math render", "is anything cut off", "did my slides overflow", or before a seminar, job talk, conference talk, or lecture. Works on .qmd or an already-rendered .html; handles Yale talk and lecture themes. macOS toolchain: quarto, deck-check.mjs on node 22, Playwright MCP, pdfread.py.
SKILL.md
33.7 KB, as published. Nobody here has run it
slide-review
Render the deck, gate it on a headless-Chrome fit check, screenshot every slide, then hand those PNGs to
reviewer subagents that judge from pixels. The predecessor skill read .tex source and guessed at
overflow by counting characters, which is how a deck reaches the podium with its last bullets below the
bottom edge. Nothing here infers layout from source. A browser measures it and the reviewers look at it.
Same loop as tikz-iterate: render, rasterize, let an agent with eyes judge the image, report exact
fixes. This skill reports and does not edit, because a deck revision is the author's call.
Descends from the slide-excellence orchestrator, the create-lecture workflow, and its four review
agents in pedrohcgs/claude-code-my-workflow.
Where things live
The stages stay here; the detail sits in files this skill reads on demand. Paths are relative to
this skill's directory. $ASSETS is ~/.claude/assets/quarto-yale, and its README.md is the
toolchain's own manual: gate internals, filter behaviour, theme classes, the offline recipe. Facts
that README owns are cited below, never restated.
| File | What it holds |
|---|---|
scripts/probe.js | The stage 4 probe. Read the file and paste the whole function into browser_evaluate. |
scripts/figure-ground.js | The stage 4 figure-ground pass. Same usage. |
references/probe-reading.md | The theme ink tables with measured ratios, the font-size and contrast thresholds, the four math-failure kinds, the dark-deck defect classes. |
references/reviewer-prompts.md | The four reviewer prompts, the settled-decisions paragraph reviewers receive, and the reply contract. |
references/failure-taxonomy.md | The full failure table. The eight commonest rows are inline below. |
style/house.md | This author's expectations: the closing-slide contact block, density calibration by deck type, settled design decisions, the author line. A public fork swaps this file. |
One browser, so serialize it
There is a single shared Playwright instance on this machine. Two agents driving it will hijack each other's page mid-capture. So all Playwright work happens here, in one place, sequentially. Never spawn parallel subagents that navigate or screenshot. Fan out only after the PNGs are on disk, where the reviewers read image files and touch no browser.
Playwright also blocks file://, so the deck has to be served over HTTP.
The stage 3 gate is exempt from all of this. deck-check.mjs launches its own headless Chrome over CDP,
so it cannot be hijacked and needs no server. That is why the measurements live there and Playwright is
kept for the things only it can do: the deck's ground colour, contrast against composited backgrounds,
the math-engine signals, and the screenshots.
Inputs
| Argument | Default | Meaning |
|---|---|---|
| positional | required | A .qmd to render, or an already-rendered .html to review as-is. |
--preflight | off | Stages 0 to 4 only (type, render, fit, probe, and the offline check if the deck asked for it), no reviewer fanout. Use in the hour before a talk. |
--type=talk|lecture | detected | Which lenses to run and which contrast expectations apply. Detection in stage 0. |
--slides=1-12 | all | Capture and review a range only. 1-based, matches the on-slide number. |
--goal="..." | none | What the deck should accomplish. Passed to the argument reviewer verbatim. |
If the positional is a bare filename, Glob for it under the current project before asking. No config
file is read.
Setup
export PATH="$HOME/.local/bin:/usr/local/bin:$PATH"
ASSETS="$HOME/.claude/assets/quarto-yale"
RUN="$HOME/.claude/state/slide-review/$(date +%Y%m%d-%H%M%S)"
mkdir -p "$RUN/shots"
PORT=$(python3 -c "import socket;s=socket.socket();s.bind(('127.0.0.1',0));print(s.getsockname()[1]);s.close()")
Preflight quarto, python3, and the Playwright MCP tools. Missing quarto or python3, surface
SETUP_MISSING:<tool> and stop. Missing the browser, fall back to the capture path in stage 5 rather
than reading the source, which is the failure this skill replaces. node 22 and Chrome are
needed for the stage 3 gate, which is not optional.
There is no Homebrew on this machine, so no pdftoppm, no pdftotext, no ImageMagick. Do not reach for
them. Every PDF goes through ~/.claude/assets/bin/pdfread.py (see stage 5), including any
PDF you need to look at, because Read cannot open one here.
mkdir the shots directory before any screenshot. Playwright returns a bare ENOENT if the parent
directory does not exist, and it does not create it.
Stage 0: deck type and ground
Settle this before running anything, because the whole review branches on it. yale-talk is dark ink
on white. yale-lecture is a designed dark theme, so the ground every contrast number is taken
against flips, and ink that is correct in a talk deck is invisible in a lecture deck. Deck type picks
the lens list (a lecture gets the pedagogy reviewer, a talk does not), the text-density standard
(deliberately opposite targets per type; style/house.md), and the closing-slide expectation
(stage 3c).
Read the theme and the other front matter facts in one pass. In a Quarto website the format block
usually sits in a _metadata.yml beside the .qmd, so search both:
grep -nE 'yale-(talk|lecture)\.scss|html-math-method|embed-resources|self-contained-math|highlight-style|stage-slide' \
deck.qmd _metadata.yml "$(dirname deck.qmd)/_metadata.yml" 2>/dev/null
For a rendered .html with no source, fingerprint the class names, since the two themes define
mostly distinct sets:
grep -coE 'class="[^"]*\b(assumption|proposition|lemma|takeaway|result|thanks-slide|ymid)\b' deck.html # talk
grep -coE 'class="[^"]*\b(keyidea|definition|question|prompt|agenda|steps|demo-tag|hero|ypale)\b' deck.html # lecture
Whichever count is higher wins. A tie, or zero both ways, means ask. The root font size is a
second signal: 30px is yale-talk, 34px is yale-lecture. The stage 4 probe reports the root
size and the deck's ground colour, so confirm the answer there before writing the report. A
lecture deck reporting a white ground means the theme did not load, which is CRITICAL on its own.
What each theme's ink should measure, hex by hex, is the first section of
references/probe-reading.md. The class vocabulary both themes define is the README's
Theme classes section; the cheapest single fingerprint is that the talk theme has .ymid where
the lecture theme has .ypale.
.section-break is where the two themes diverge, so the old advice about it is theme specific, and
the README's Things that will silently break the deck section has the current rule: no
background-color attribute on either theme. On a lecture divider the attribute is also a defect
the probe can see, because reveal's has-dark-background then forces that slide's body text to pure
white (verification in references/probe-reading.md, dark defects). Do not carry the old
background-color="#00356b" fix forward from memory.
Stage 1: render
cd "$(dirname deck.qmd)" && quarto render deck.qmd --to revealjs 2>&1 | tee "$RUN/render.log"
On non-zero exit, extract the error from the log, surface RENDER_FAILED:<message>, and stop. Never
review a stale HTML from a previous render.
Then mine the log, because pandoc reports two classes of defect that no amount of looking at pixels will reveal:
grep -nE '^\[WARNING\]|^ERROR|Could not fetch|not found' "$RUN/render.log"
Verified output on a deck citing a key absent from the .bib and embedding a missing figure:
[WARNING] Citeproc: citation ghostcite2019 not found
[WARNING] Could not fetch resource figures/does-not-exist.png
Both are CRITICAL. An unresolved citation prints the raw bibtex key on the wall.
There is no post-processing step. A correctly configured deck comes out of quarto render ready to
review, and any offline claim gets confirmed in stage 2. Skip stage 1 entirely when handed an .html.
Stage 2: offline check, only when the deck asked for it
This used to be a mandatory gate and is not one any more. Decks now default to MathJax loaded from a
CDN, so an external MathJax reference is the expected state of a correct deck, and reporting it as a
defect sends the author after a bug that is not there. Offline is an opt-in variant, declared in
front matter as embed-resources: true together with html-math-method: katex. Run the check only
in that case, using the facts stage 0 already read:
python3 "$ASSETS/check-offline.py" deck.html; echo "exit=$?" # opt-in decks only
Say which branch ran, in the report, on its own line. A default deck gets
Offline-safe: not checked (CDN MathJax variant, offline not requested). An opt-in deck gets the
output quoted verbatim near the top, because a deck that loses its math on a conference room
projector with no wifi is the worst outcome this skill exists to prevent. A pass lists external
hosts and both runtime loader fields as none and ends OFFLINE-SAFE : YES (the field is
padded, so grep the label and read the verdict, per the README).
On an opt-in deck, exit 1 with OFFLINE-SAFE : NO is CRITICAL, and the report names the specific
cause. Two causes account for nearly all of them, and the README's Things that will silently break the deck section owns the mechanics of both:
- Any
cdn.jsdelivrhit or leftover katex runtime loader meanshtml-math-methodwas written in the object form, which Quarto's katexPostProcessor silently ignores. The fix is the README's offline recipe: the bare stringkatexplusself-contained-math: trueunderembed-resources: true. With that, Quarto embeds KaTeX and its fonts itself; verified at 4.95 MB with no external hosts, no runtime loaders, and no post-processing of any kind. - The base theme was swapped off
defaultonto one of the eight built-in reveal themes that@importGoogle Fonts, which no Quarto setting can make offline-safe; the README names the eight and the four clean ones. CRITICAL, with the fix being a return to[default, yale-talk.scss]or[default, yale-lecture.scss].
Run this grep on every deck, offline variant or not, because a swapped base theme also changes the variables the Yale SCSS layers onto, and on a lecture deck it can quietly undo the dark ground the rest of this review measures against:
grep -nE '^\s*theme:|fonts\.googleapis|fonts\.gstatic' deck.qmd deck.html
Ignore the unconverted math count. Quarto's katex mode leaves <span class="math"> in the file on
purpose and converts it in the browser, so a non-zero count there is normal and not a finding.
Stage 3: fit gate
Run this on every deck, before touching the browser. It is the one mandatory gate, whatever the theme and whatever the math engine. It owns every geometric finding in the report.
node "$ASSETS/deck-check.mjs" fit deck.html; echo "exit=$?"
node "$ASSETS/deck-check.mjs" fit deck.html --json > "$RUN/fit.json"
node "$ASSETS/stage-check.mjs" deck.html; echo "exit=$?"
The second gate is the step-0 check: every content slide must open as its heading alone, and every
press must change visible ink. A clean deck prints STEP-0-CLEAN: YES and exits 0. It fails with
LEAKS AT STEP 0 on content visible before the first press, and with
DEAD STEP: press 2 changes nothing or DEAD STEPS: presses 2, 4 change nothing on a press the
room cannot see, presses counted from 1. Both are CRITICAL. Name every slide it lists, and quote the
press numbers: the fix is per press, not per slide. Nothing else in this review can find either, and
the walk adds about two seconds. Mechanics, slide-kind classification, and what produces a dead
step are the README's staging section, under Checking it.
deck-check.mjs fit prints the canvas, one line per slide, then a verdict, and exits 1 whenever the
verdict is NO, so the gate composes: node "$ASSETS/deck-check.mjs" fit deck.html || <report>.
Verified output on a deck whose third slide is overstuffed, where quarto render had exited 0 in
silence:
canvas 1050x700, 5 slides
1 ok Deliberately Broken Test Deck
2 ok Motivation
3 OVERFLOW +547px Results
4 ok A formal result
5 ok Conclusion
DECK-FITS: NO
The status tokens, and what each one is worth in the report:
| Token | Meaning | Severity |
|---|---|---|
ok | Slide fits. | none |
OVERFLOW +Npx | Content past the bottom edge. Gone from the screen. | CRITICAL |
TOO WIDE +Npx | Content past the right edge. | CRITICAL |
N MISSING IMG | An <img> that resolved to nothing. | CRITICAL |
N CRUSHED FIG | A figure scaled down far enough to be unreadable. | MAJOR |
FIG SHRUNK TO Npx | A figure authored at 1200px or wider that auto-stretch scaled under 600px. | MAJOR |
N UNRENDERED MATH | A span.math the engine never touched. Raw TeX on the wall. | CRITICAL |
NEARLY EMPTY | Effectively blank slide. Prints "nearly empty (warning, not a failure)" and does not fail the gate. | MAJOR |
DANGLING TARGET #id | A .jump button whose target id resolves to no slide. Reveal lands on the title slide instead, silently. Fails the gate. | CRITICAL |
UNPAGINATED BIBLIOGRAPHY | div#refs holds entries, which means the deck set a bibliography: and forgot citeproc: false, so pandoc rendered a second copy of the whole reference list onto one slide. Fails the gate. | CRITICAL |
no [.jump-back] on #id | A jump target with no way back. Warning; leaves the exit code alone. | MAJOR |
The exact measurement condition behind each token is the README's fit-gate table. UNRENDERED MATH
works under either engine and is what catches broken math delimiters, which under MathJax leave the
literal \(...\) in the paragraph and produce no container at all for the stage 4 probe to find.
The tokens are separate and a slide can carry more than one (4 1 MISSING IMG and
5 TOO WIDE +308px on the same run, verified). Carry each token and its number into the report
verbatim; never re-derive either.
NEARLY EMPTY is the one token that leaves the exit code at 0. A blank slide is still a defect the
audience sees, so report it, but do not describe the gate as failing on it. The common cause is a
bare macro block before the first ##; the fix is ## Notation {visibility="hidden"}, per the
README's Things that will silently break the deck.
deck-check.mjs drives its own headless Chrome over CDP on node 22, needs no npm install, honours
CHROME_BIN, and navigates slide by slide with fragments forced visible, which is the only way the
numbers come out right (README, fit-gate section). Do not write your own overflow probe: the obvious
one-pass snippet reads scrollHeight 0 off every non-present slide and reports a broken deck as
clean, as the README explains. deck-check.mjs handout deck.html out.pdf exports a PDF; offer it
when the user wants a handout, and use it as the capture fallback in stage 5.
Stage 3b: jump buttons and the progress bar
Both themes carry two behavioural features no static screenshot can check, and both fail silently.
Run this only on a deck that has an appendix or .jump spans. It needs its own headless Chrome
rather than the shared Playwright browser, because it navigates and clicks:
grep -c 'class="jump-btn' deck.html # 0 means skip the jump half
grep -c 'appendix-break\|section .*appendix' deck.html
What has to be true, and what each failure means:
| Check | Failure means |
|---|---|
.progress carries metered when the deck has an appendix | The filter's script did not run, so the appendix is back in the denominator. MAJOR. |
.progress carries segmented when the deck has two or more .section-break dividers | Same cause. MINOR on its own. |
--section-cut-N values equal 100 * i / (main - 1) for each divider at flattened index i, where main is the index of the first appendix slide | A cut in the wrong place, which is worse than no cuts. MAJOR. |
The fill span's scaleX is 1 on the last main slide and on every appendix and references slide | The exclusion is not working. MAJOR, and the symptom the feature exists to prevent. |
Every a.jump-btn[data-jump] target resolves to a slide | A dangling target lands the presenter on the title slide mid-question. CRITICAL. |
Every appendix slide reachable by a jump carries a .jump-back | A one-way trip. MAJOR. |
A jump then a back returns to the origin's {indexh, indexv, indexf} exactly | The handler is not capturing the origin before reveal navigates. CRITICAL. |
Stage 3 already ran the dangling-target check: deck-check.mjs fit fails on
DANGLING TARGET #id "label" and warns on a jump target with no .jump-back, so read its
jump-button block before doing this by hand, and carry its label and id into the report verbatim.
The equivalent in the browser, if you need it on an HTML you were handed:
browser_evaluate: () => [...document.querySelectorAll('a.jump-btn[data-jump]')].map(a => {
const id = a.getAttribute('data-jump'), el = document.getElementById(id);
const sec = el && el.closest('.slides section');
return { label: a.textContent.trim(), target: id,
resolves: !!sec, isAppendix: !!(sec && sec.classList.contains('appendix')),
hasBack: !!(sec && sec.querySelector('a.jump-btn.jump-back')) };
})
Report resolves: false as CRITICAL with the label and the target id. hasBack: false is MAJOR:
the presenter can get there and not back. isAppendix: false is not a finding; a jump to a main
slide is legitimate.
The round-trip check needs interaction, so it belongs in one evaluate that drives reveal itself:
put the origin slide at a known fragment step, read Reveal.getState(), dispatch a bubbling click
on the button, dispatch one on the .jump-back it lands next to, and compare Reveal.getState() to
what you stored. A synthetic bubbling click is the right tool here: the handler is delegated, and a
real mouse click cannot reach a button whose fragment is still hidden.
Four behaviours are correct and must not be reported as defects. A jump lands the target with
all its fragments visible, on purpose. The bar stays full while you are inside the appendix or
the references, also on purpose, because the main deck really is over. A references slide has zero
fragments, because a reference list is not an argument being built. And an empty <div id="refs">
on the references divider is the marker that keeps citation links alive, not leftover markup: what
fails the gate is a #refs with entries in it.
Stage 3c: the closing slide
How a deck ends is settled per type, so this check branches the way stage 0 does. Run it on every
deck. A research talk ends on a closing slide carrying the contact block; a teaching lecture ends on
content, with no closing slide. The expected contents, their position relative to the appendix and
references dividers, and the concrete presence greps are in style/house.md; state each finding
against that file's values. Handed a rendered .html with no source, take the ordered title list
from fit.json and the per-slide classes from probe.json after stage 4 has run, and read the
same positions off those.
| Deck type | State | Severity |
|---|---|---|
| talk | No closing slide anywhere | MAJOR. The talk stops instead of closing, and the room leaves with no address to write to. |
| talk | Closing slide sits inside the appendix, or after it, or after the references | MAJOR. The audience meets the appendix before the invitation. The fix is to move it ahead of the .appendix-break divider. |
| talk | Closing slide with no contact line and no invitation to get in touch | MINOR. Name the line that is missing. |
| talk | No QR slot on the closing slide | MINOR. |
| talk | QR slot present, image resolves to nothing | CRITICAL, and stage 3 has already printed MISSING IMG for that slide. Carry its token. |
| lecture | A thank-you slide is present | MINOR. It belongs on a talk; the lecture's own closing slide is the one idea to leave with. |
The bar reads full on the closing slide, since it is the last main-body slide. That is stage 3b's fill check and not a second finding here.
Stage 4: serve and probe
The browser handles what only it can: the deck's real ground colour, contrast against composited backgrounds, the math-engine failure signals, unresolved citations, and the PNGs the reviewers look at. Geometry is already settled by stage 3, so this probe no longer measures it, which keeps the hold on the shared browser short.
cd "$(dirname deck.html)" && nohup python3 -m http.server "$PORT" --bind 127.0.0.1 >/dev/null 2>&1 &
sleep 2 && curl -s -o /dev/null -w "%{http_code}\n" "http://127.0.0.1:$PORT/deck.html"
Anything but 200 means the server did not come up; check the port and retry once. Kill it at the end
of the run, on every exit path including failure:
lsof -ti tcp:$PORT | xargs -r kill
Navigate with browser_navigate to http://127.0.0.1:$PORT/deck.html, then size the viewport so the
render scale is exactly 2.0, which makes every later measurement trivial to convert:
browser_resize width=2334 height=1556
Reveal computes scale = min(0.9 * innerW / 1050, 0.9 * innerH / 700) at the Quarto default margin of
0.1, so 2334x1556 gives scale 2.000 against the 1050x700 logical canvas. Verified. One screenshot
pixel is half a deck pixel, and the reviewers are told so.
Collapse animation so one capture per slide is the whole slide:
browser_evaluate: () => { Reveal.configure({fragments: false, transition: 'none', autoAnimate: false}); Reveal.layout(); return {total: Reveal.getTotalSlides(), scale: Reveal.getScale()}; }
fragments: false is what makes staged content measurable, and it has to stay. Decks run
stage-slide.lua, which wraps nearly every top-level block on a content slide in a .fragment so
the slide opens as its heading alone. A staged block is present in the DOM from the start and only
hidden, so it is content, not missing content. Never report a slide as empty or a point as absent
because it arrives on a later keypress, and tell the reviewers the same, since the screenshots are
taken with fragments off and show the fully revealed slide.
Then run the probe: read scripts/probe.js and paste the whole function into one
browser_evaluate, and save the returned JSON to $RUN/probe.json. It forces each section visible
in turn and restores the inline style afterwards, because Reveal sets display: none on off-screen
sections and a hidden element reports no computed geometry and no client rects. It measures font
size and contrast against the composited background, reads the deck's own ground colour instead of
assuming white, and re-bases per slide on a data-background-color, because reveal paints that
backdrop on a separate .slide-background element that is nowhere in the text's ancestor chain;
without the re-base, the probe measures a divider slide's text against the deck ground while the
audience sees it on the attribute's colour.
Images cannot be measured in that pass: reveal lazy-loads them from data-src, so off-screen
<img> elements report naturalWidth 0 and a zero-size box, and forcing the section visible does
not change that (verification in references/probe-reading.md). So run scripts/figure-ground.js
the same way: it loads each image into a detached Image and samples the border band. Append the
result to $RUN/probe.json as figures.
Read both against references/probe-reading.md: the font-size and contrast thresholds, the
figures[].lightbox rule with its verified calibrations, the four math-failure kinds and how each
engine shows them, the unresolved-citation shape, and the four dark-deck defect classes, which apply
only when deck.dark is true and are all skipped on a talk deck.
Stage 5: capture
For each slide index n from 0 to total - 1, two calls, in order:
browser_evaluate: () => { Reveal.slide(N); Reveal.layout(); return Reveal.getIndices(); }
browser_take_screenshot: filename=$RUN/shots/slide-NN.png type=png scale=css
Reveal.slide(n) is the only navigation that works reliably. URL fragments do not, and arrow keys
drift on decks with vertical stacks. Zero-pad NN so the files sort.
The capture is a viewport screenshot, so content past the canvas edge is cut off in the image exactly as the projector will cut it. A bullet sliced through its x-height in the PNG is the defect, not an artifact of the capture.
Above about 40 slides this is 80 tool calls. Say so and offer --slides= before starting a long deck.
In --preflight mode, capture only the slides the probe flagged, plus the title slide.
Fallback when the browser is contended
If another agent holds the browser, or the page keeps changing under you mid-capture, export the deck to PDF and rasterize from there. Stage 3 has already run, so the geometry is in hand either way; what this route gives up is stage 4's contrast and KaTeX signals, so get those from the browser first if you can hold it even briefly.
node "$ASSETS/deck-check.mjs" handout deck.html "$RUN/deck.pdf"
That reuses the same headless Chrome the gate uses and needs nothing installed. decktape is the second
option if the handout export disagrees with what you saw on screen; the README's PDF sections carry
the exact npx -y decktape@latest reveal invocation. Both drive their own browser, so neither is
blocked by the shared Playwright instance, and file:// is fine here because that restriction is
Playwright's. Chrome's --headless --print-to-pdf is not an alternative; it produces a blank PDF on
reveal decks.
Reading any PDF
The Read tool cannot open PDFs on this machine. There is no poppler and so no pdftoppm, which is
what Read needs, and it fails on every .pdf. Route through pdfread.py, then Read the PNG:
~/.claude/assets/bin/pdfread.py png "$RUN/deck.pdf" --pages 3 --dpi 110 --out "$RUN/shots/s"
That writes $RUN/shots/s-3.png. --pages takes 3, 1-5, or 1,4,9, 1-based. Raise --dpi to 150
when a reviewer needs to judge small type in a figure. This applies anywhere a PDF shows up, including
a deck the user hands over directly.
If handed a PDF instead of a deck
Legacy Beamer, or a deck someone exported. No probe, no offline check, no browser, so the image reviewers run alone and the report says which lenses were unavailable. Rasterize every page:
~/.claude/assets/bin/pdfread.py pages deck.pdf # page count first
~/.claude/assets/bin/pdfread.py png deck.pdf --pages 1-24 --dpi 110 --out "$RUN/shots/slide"
Check head -c 5 deck.pdf is %PDF- first. A download that was actually an HTML error page will die
with Unrecoverable error, exit code 1, which reads like a rasterizer bug and is not one. Confirm the
pixel size with sips -g pixelWidth -g pixelHeight; under 1400 wide, re-render at --dpi 150 so the
reviewers can judge small type. Correct once, do not loop. gs at /usr/local/bin/gs does the same job
if PyMuPDF is unavailable, and pdfread.py --help prints the equivalent invocation.
Stage 6: fan out
Now the browser is idle and the PNGs are on disk, so this part parallelizes safely. One Agent call
per lens, all in one message, subagent_type: "general-purpose". Every reviewer gets the deck facts
(canvas 1050x700, screenshots at scale 2.0, root font size, deck type, and the ground colour from
deck.ground) and absolute PNG paths, and every reviewer is told to read the images.
Two facts every reviewer needs, or they will report the theme as a bug. A lecture deck is dark on
purpose, so light text on a near-black ground is the design and not a rendering failure. And the deck
is staged by stage-slide.lua, so the screenshots show the fully revealed slide even though the
audience meets it one block at a time; a point that arrives on a later keypress is present, not
missing.
The lenses: layout, argument, and copy on every deck, pedagogy on a lecture only. The prompts, the
settled-decisions paragraph that opens each of them, and the reply contract are in
references/reviewer-prompts.md; send them as written. The density calibration and the settled
decisions they encode are style/house.md. Inputs per lens: layout gets all PNGs plus fit.json
and probe.json; argument gets all PNGs plus the ordered title list and --goal if provided;
pedagogy gets all PNGs; copy gets all PNGs plus the .qmd source path so fixes can be exact. Every
reviewer replies in the numbered contract, or NONE at its severity floor.
Stage 7: synthesize
Do this in the main thread. Merge, drop duplicates (layout and copy will both find raw TeX in an
equation; keep the one with better evidence), and rank on two keys: severity first, then whether the
audience sees it. An unresolved citation printing (smith2020?) on the wall outranks an inconsistent
symbol in the appendix at the same severity. Drop reviewer findings that contradict the settled
decisions or the density calibration in style/house.md; the full drop rules are rows in
references/failure-taxonomy.md.
# Slide review: deck.qmd
Type: research talk (light, ground #ffffff) | Slides: 24 | Captured: $RUN/shots/
Render: clean | 2 pandoc warnings
Math: MathJax from CDN | Offline-safe: not checked (offline not requested)
DECK-FITS: NO (2 slides)
Closing: thank-you slide at 22, ahead of the appendix divider
## Blocking (fix before presenting)
1. [CRITICAL] Slide 3 "Results" - OVERFLOW +547px.
Bullets 9 to 14 and the specification table are off screen. Split the slide,
or move the table to its own.
2. [CRITICAL] Slide 5 "A wide table" - TOO WIDE +308px.
The rightmost three columns are off screen. Drop columns or mark the table `.spec`.
3. [CRITICAL] Slide 4 "A formal result" - `\frobenius` renders as red literal text.
KaTeX has no such macro; it was a Beamer preamble definition. Replace with `\|X\|_F`.
## Worth fixing
4. [MAJOR] Slide 1 is blank (NEARLY EMPTY), counted in c/t and printed in the handout.
A macro block sits before the first `##`. Retitle it `## Notation {visibility="hidden"}`.
The gate passes this one, so it will not block a release.
## Minor
## Clean
Slides 2, 5 through 24 pass all mechanical checks.
Lead with the fit verdict and the count of blocking issues. If nothing blocks, say the deck is ready to present in the first line, then list the rest. The offline line still appears, saying either the verdict or that the deck did not ask for the check, so nobody reads its absence as a pass.
On a lecture deck the header line reads Type: teaching lecture (dark, ground #1a1c1e), the closing
line reads Closing: no thank-you slide (correct for a lecture), and the report
says which dark checks ran and what each found, including the ones that found nothing. A figure whose
pixels could not be read is reported as sent for human review, with the slide numbers.
Finish with the run directory, the PNG paths, and the agent count.
Failure taxonomy, the short form
The eight commonest rows. The full table is references/failure-taxonomy.md.
| Symptom | Response |
|---|---|
quarto render non-zero | RENDER_FAILED:<msg> from the log, stop. Do not review the previous HTML. |
An external cdn.jsdelivr MathJax reference | Expected on every default deck. Not a finding, and not a reason to run the offline check. |
deck-check.mjs fit exit 1 | Working as intended. Every slide it names is a finding; carry its numbers through unaltered. |
stage-check.mjs exit 1 with LEAKS AT STEP 0 | Content is visible before the first keypress. CRITICAL. Name every slide it lists; the usual causes are a figure cell, an .r-stack base layer, or a top-level .nonincremental list. |
stage-check.mjs exit 1 with DEAD STEP or DEAD STEPS | A press advances the deck and changes nothing on the wall. CRITICAL. Quote the slide and the press numbers it names. The usual cause is a container div wrapped around content that already stages itself, so the fix is to drop the wrapper or mark it .together if the beat was meant. |
browser_navigate on a file:// path | Playwright blocks it. Serve over HTTP; there is no workaround. |
| Page changes under you mid-capture | Another agent grabbed the browser. Abandon the capture and switch to the decktape path; do not retry browser work in parallel. |
Read fails on a .pdf | Expected, there is no poppler. Convert with pdfread.py png and read the PNG. |
Out of scope
Editing the deck. This produces a report; the author decides.
Prescribing how the author should write. Legibility, correctness, and whether the argument is
followable are in bounds. Sentence length, tone, and diction are not. Text density is in bounds on
the terms style/house.md sets, which are per deck type and about what the audience is asked to
absorb.
Judging whether the research is right. That is /review-paper.
Authoring a deck from scratch. That is /research-talk for a seminar deck and /teaching-lecture for
classroom material. This skill audits a deck that already exists.
Beamer .tex source. A PDF exported from Beamer goes through the rasterize path for image review only.