Claude Skill

slideops

Use when the user asks for slides, a slide deck, or a presentation about a code repository, one of its subsystems, a feature, an architecture area, or its recent changes. Triggers include "make slides", "build a slide deck", "create a presentation", "HTML slides for this repo", "

LLM Mart · 0 points · 14 views 21 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download glukicov-slideops-skills_slideops-6020266.zip · 804 KB
Part of glukicov/slideops — 2 skills

Install

skills CLI npx skills add https://github.com/glukicov/slideops/tree/main/skills/slideops
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install glukicov-slideops@llmmart
Git git clone https://github.com/glukicov/slideops.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole glukicov/slideops collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

SlideOps: generate a deck from a repository, and keep it true

This skill has two jobs, and the second one is the point.

Build: turn a repository into a deck whose every claim came from the code, not from a model's impression of the code. Keep in sync: make that deck able to prove, months later, whether it still matches the repository, cheaply enough that nobody has to remember to care.

The mechanism joining them is a citation. Every quoted snippet records the file, the line range, and a hash of those source lines at build time, and the deck records the commit it was built from. That turns "is this deck still accurate?" from a question somebody has to answer by reading into a command that answers itself:

python3 scripts/check.py docs/slides/ --repo .

Standard library only. No model, no network, no tokens, milliseconds to run. Two scripts ship with this skill and do all of it: scripts/cite.py writes the citations while you build, and scripts/check.py reads them back afterwards. Writing the citations as you go (Step 3) is therefore not optional decoration: it is the entire reason the deck can be maintained instead of rewritten. See references/freshness.md for the mechanism and references/automation.md for wiring it into pull requests, scheduled refreshes, and agent hooks.

Which job is this?

  • The user wants a new deck: start at Step 0 below.
  • The user wants Markdown documentation instead of slides (a doc that lives in the repo and renders on GitHub, with the same citations and the same freshness check): follow references/markdown.md. It reuses this file's research, confidentiality, and refresh rules and swaps the build and verify steps; references/markdown-pdf.md covers exporting such a doc to PDF. Checking or refreshing an existing .md doc is the same check.py workflow as decks; sweeps pick both up together.
  • The user is asking whether an existing deck is still accurate, wants one refreshed, or wants the check automated: go straight to Refreshing an existing deck near the end. Do not rebuild a deck from scratch because a few slides drifted.

Produces a single self-contained HTML slide deck (no build step, no CDN dependencies): one 1280×720 slide per screen, click/arrow-key/URL-hash navigation, a progress bar and slide counter, an Esc-toggled overview grid (deep-linkable as #overview), optional per-slide speaker notes (<aside class="notes">, toggled with N, invisible in exports), and, on request, a paginated PDF export. Every slide is grounded in the real repository: real code snippets, real commands, real screenshots, real numbers. Nothing is invented to fill space.

A worked example ships with the skill: examples/skill-demo.html is a 17-slide deck about this skill, built by this skill, showing the patterns, all four theme presets on real decks, and an inlined Mermaid diagram.

Two starting points, chosen with the user up front:

  • General overview: what the project is, how to install/set it up, how to run it, its main features, its architecture at a glance. Mirrors a README/onboarding walkthrough.
  • Focused topic: one subsystem, one recent change, one architecture decision, one workflow. Goes deep rather than wide.

Step 0: Orient, then ask

Spend ~2 minutes scanning the repo before asking anything, so every question you ask is concrete and every option you offer actually exists: read the README and the manifest(s) that define the product (per-app in a monorepo), run git log --oneline -15 and glance at the top-level tree and docs/, and check whether docs/slides/ already has decks (open one: its title slide, theme and build stamp show the local conventions). Do not start slide research yet; this pass is only to ask good questions. The scan itself may hit stale doc pointers; note them and move on (Step 1's code-wins rule deals with drift later).

What you find shapes the candidates:

  • Existing decks remove or reframe candidates. A topic an existing deck already covers is off the proposal list, unless meaningful commits have landed since that deck's date, in which case offer "update the existing X deck" as its own candidate. Disclose the existing decks in one line at the top of the intake.
  • Skip meta noise in "why now". Recent commits about tooling, docs, or slide decks themselves don't justify a deck; reach back to the most recent product activity, and don't re-justify a candidate with work an existing deck already presents.

Then ask everything in one compact intake (use your environment's structured-question tool if it has one; otherwise a single message with lettered options). Mark the default in each list, skip any item the user already answered, and never exceed these six:

  1. Topic. Propose 3-4 concrete candidates you found in the scan, each with a one-line "why now" (e.g. "v2.3 shipped last week: a what's-new deck", "the sync/ subsystem is the largest and undocumented: a deep dive", "no onboarding doc exists: a general overview"), plus "something else: tell me". Never ask a bare "what should the deck be about?": the scan is what makes this question answerable in one click. Mark as default the candidate you would genuinely bet the user wants given the repo's core domain and its deck history (a core subsystem with zero coverage usually beats a recently-changed-but-already-presented area), not mechanically the most recent change.
  2. Audience and venue. New joiners (onboarding) · team sprint/standup · stakeholder or exec review · conference/meetup. This drives jargon level, pacing, and the Sizing row.
  3. Length. Offer the Sizing table rows as time slots ("5-min lightning ≈ 8-15 slides", "20-min deep dive ≈ 12-20", "45-min onboarding ≈ 20-35").
  4. Design. Theme menu from references/themes.md: Ledger Light (default) · Ledger Dark · midnight · graphite · match a brand (ask for the URL/style guide; fetch it for real per references/style-guide.md § Theming, never invent colors from a description). Optionally a font choice (themes.md § Font options) if the user signals caring about typography. If the user hesitates between themes or asks to see them, show rather than tell: copy the template, swap in each candidate preset's :root block, screenshot the title slide of each (the recipe in references/verification.md), and present the images side by side before they choose.
  5. Scope and sensitive data. Confirm what the deck may draw on and who will see it: "internal team" and "conference talk" are different redaction bars. Name anything off-limits up front (unannounced features, customer names, internal hostnames). The defaults in "The confidentiality rule" below apply regardless of the answer.
  6. Extras. Three independent toggles, so don't letter them as alternatives: state the defaults and ask the user to object to any ("PDF export: no · Mermaid diagrams: built-in flow boxes only (Mermaid needs one-time npx network access, see references/diagrams.md) · output: <repo>/docs/slides/<topic-slug>-<date>.html"). The deck (plus a PDF if asked for) is the only file this produces, so there is nothing else to agree on.

For "what changed" style decks, also pin the recency window (a date range or "since the last release") and resolve it with git log before writing anything, not from memory.

The outline checkpoint. After the first research pass, show the user a proposed topic list and slide flow (like a table of contents) and get a thumbs-up before writing any HTML: this is the single highest-leverage checkpoint. Building 25 slides around the wrong 6 topics wastes far more time than a 30-second review would have. Skip the checkpoint only when one of these observable conditions holds:

  • the user already gave you an explicit topic list or outline (not just scope/audience), or
  • you are running non-interactively (no user available to answer): proceed, and say in your final summary that the outline was not reviewed.

The confidentiality rule (applies to every step)

A deck is a document that leaves the repository: it gets emailed, screen-shared, and posted. Treat everything you put on a slide as public from the moment it is written.

Never read for slide content, and never quote: .env and any *.env*, key/certificate files (*.pem, *.key, id_rsa*, *.p12, service-account JSON), secrets/, credentials*, .npmrc/.pypirc/.netrc, CI secret definitions, production logs, database dumps, fixtures containing real customer data, and any path the repository's own ignore files exclude. If a file you need is on this list, describe its shape ("a service account JSON, mounted at runtime") instead of its content.

Redact even from files that are safe to quote: credentials, tokens, API keys, private hostnames and internal URLs, IP addresses, account and customer identifiers, personal names and emails that are not public contributors, and precise infrastructure paths where a generic description carries the same meaning. Replace with a clear placeholder (<project-id>, db.internal.example), never a plausible-looking fake.

Ask when it is the user's call, not yours: if a slide would be materially weaker without a detail that looks sensitive (an internal hostname in a diagram, a real customer count, an unannounced feature name), ask the user before including it, and say what you are about to expose. Silence is not consent.

Screenshots carry more than you think. A screenshot of a terminal, dashboard, or editor also captures window titles, file trees, branch names, ticket numbers, other tabs, and notifications. Crop to the region that makes the point, and read the image back before embedding it.

Verification includes a redaction scan of the finished artifacts: see references/verification.md § Redaction scan.

Step 1: Research (never skip, never approximate)

The deck is only as credible as its weakest verified claim. For every slide you plan to write:

  • Respect the confidentiality rule below when choosing what to open and what to quote: secrets, credentials, production logs, and real customer data are out of scope for slide content even when they would be interesting.
  • Read the real files. README(s), package manifest (pyproject.toml, package.json, go.mod, …), the actual source of anything you plan to quote or diagram, existing architecture docs, existing agent-instruction files (CLAUDE.md, .agents/skills/, .claude/skills/, and similar: these often already describe real user-facing capabilities accurately, a goldmine for "say to your assistant" style bubbles).
  • Prefer code intelligence / grep+view over guessing. If you're about to describe a class, a config schema, a CLI flag, or a directory layout, go find it and read it. If you can't find where a claim comes from, don't make the claim.
  • When the repo's own docs contradict its code, the code wins. Docs drift; verify any doc-sourced claim against the current source before putting it on a slide, and if the drift is itself notable, say so on the slide rather than repeating the stale claim.
  • For "what's new" / recent-changes decks: use git log --oneline --since=... -- path (and git log -p for real diff content) to ground every "shipped this month" claim in an actual commit, not a vague impression. Cross-check version bumps (e.g. pyproject.toml history) against the commits that produced them. A file added inside your window may already be deleted again by a later commit: git show <commit>:<path> recovers it, and the deletion may itself be worth a slide note.
  • For architecture/flow diagrams: trace the real call path (imports, function calls, the actual sequence a request follows) rather than inferring structure from the file tree alone. A plausible-looking but wrong diagram is worse than a smaller, correct one. For rendering (built-in flow boxes vs pre-rendered Mermaid SVG), see references/diagrams.md.
  • For screenshots/plots: use only real artifacts: a real chart the repo's own tooling produced, a real trace/log screenshot, a real terminal output. If a genuinely useful image doesn't exist yet, either generate it by actually running the repo's own code (reuse its existing plotting/export/reporting functions against real data on disk rather than writing new ad hoc plotting code) or fall back to a non-image pattern; never fabricate a fake chart.
  • Delegate research for large/unfamiliar repos. If the repo is large or you're unfamiliar with it, a read-only exploration sub-agent pass ("find the CLI entry points, the README, the test/dataset conventions, and any existing architecture docs") is worth it before drafting the topic list, if your environment provides sub-agents. Don't delegate the actual slide writing: that needs your own judgment about pacing and what's genuinely interesting.

Step 2: Draft the topic list

Before writing HTML, sketch the flow as a short outline (title → agenda → N sections, each with 2-6 slides → close) and share it per the outline checkpoint above. A well-paced deck opens with a title slide, an agenda, and 2-3 "the project in one picture" slides (what it is, one or two real results/screenshots), then either a section-divider-led deep dive per topic (for a general/multi-topic deck) or straight into content (for a focused deck), and ends on a closing/roadmap slide. Typical density: 1 idea per slide; resist cramming two ideas onto one slide just to save a slide.

Sizing

Section dividers count toward the slide budget.

Deck type Rough slide count Section dividers?
Lightning update (one topic, one meeting) 8-15 No, straight into content
Focused deep dive (one subsystem/feature) 12-20 Optional, only if it has 2+ sub-topics
General overview / onboarding 20-35 Yes, one per major section

Step 3: Build

  1. Copy assets/template.html to the output path. It already has the full verified CSS + navigation JS; do not rewrite either from scratch. All slide markup lives between the <!-- SLIDES START --> and <!-- SLIDES END --> markers; everything outside them stays untouched (a scripted splice against those markers is the easiest way to replace the body in one pass), with two exceptions: set the <head>'s <title> to the real deck title, and swap the :root theme block if the user chose a non-default theme (references/themes.md).

  2. Work through your topic list slide by slide, copying the matching PATTERN: block from the template for each slide (title, agenda, section-divider, prose+cards, image+caption, table, before/after code, single annotated snippet, flow diagram, chat-bubble examples, lane comparison, closing: see the template's own comments for when to use which) and replacing every bracketed placeholder with real, verified content. Delete every pattern block you didn't end up using; delete the illustrative comments too once real content replaces them.

  3. Cite every snippet as you write it, with the script, never by hand:

    python3 scripts/cite.py app/main.py:40-58 --repo <repo> --snippet
    

    It prints the data-src / data-sha256 pair to paste onto the <pre>, warns when the lines are too wide for the pattern you chose, and with --snippet prints the source already HTML-escaped. Stamp the build commit once, when the deck is otherwise finished:

    python3 scripts/cite.py --stamp <deck.html> --repo <repo>
    

    A hand-computed hash is worse than no hash: it silently reports CHANGED months later and nobody can tell whether the code moved or the build was sloppy. Same for the date, which is easy to invent and impossible to verify afterwards. Cite anything you quote or assert from one place; prose summarising a whole subsystem needs no citation. See references/freshness.md.

  4. HTML-escape every verbatim snippet: & → &amp;, < → &lt;, > → &gt; inside <pre>/<code>. Unescaped source code (generics, arrows, includes) silently corrupts the markup downstream. Mind snippet line width too: see references/style-guide.md § Code snippets.

  5. Follow every rule in references/style-guide.md as you write: sentence case, no em dashes in prose, tag accuracy, real-content-only, rather than fixing it all in a pass at the end.

  6. Keep slide numbering visible to yourself: an HTML comment <!-- N: LABEL --> above each <section class="slide">. Comments are 0-indexed; the URL hash and the on-screen counter are 1-indexed. Worked example: <!-- 6: ARCHITECTURE --> is display slide 7, reached at deck.html#7. Renumber the comments any time you insert or delete a slide, and when the user later says "slide 12", re-derive which section that is from the file itself (grep -n '<!-- [0-9]*:' deck.html) rather than trusting a remembered count: a stale comment index is a fast path to editing the wrong slide.

Step 4: Verify (every single slide, every single edit)

Do not consider the deck done until you've actually looked at it. This is not optional polish: run for the first time on any deck, this step reliably catches real bugs: broken image paths, text overlapping the nav pill, tables overflowing their card, stale slide numbering, wrong aggregate numbers.

  1. Render every slide to an image with headless Chrome and look at each one. Chrome discovery is platform-dependent: use the cross-platform recipe in references/verification.md, which also covers staging directories (always a fresh per-deck directory, never fixed shared paths: parallel deck builds collide) and a faster batched screenshot loop.
  2. View each screenshot. Check specifically for: text clipped by or overlapping the bottom nav pill, cards/boxes stretching to fill unexpected empty space (add the fill class only where stretching is wanted; the grids default to content height), tables or code blocks overflowing their container, and images that failed to load (a small broken-image icon with visible alt text, almost always a relative-path problem: see the PDF workflow note in verification.md).
  3. Check citations resolve before shipping: python3 scripts/check.py <deck> --repo <repo> should report every citation CURRENT. Anything else means the deck is already stale on the day it was built: CHANGED means you quoted something and then it moved under you (or the hash was hand-computed), UNVERIFIED means a snippet has no hash at all. Both are build defects, not future problems. Fix them now with scripts/cite.py.
  4. Check structural balance after every edit: a stray unclosed <div> breaks everything downstream silently:
    python3 -c "
    import re
    c = open('deck.html').read()
    print('section:', len(re.findall(r'<section class=\"slide', c)), len(re.findall(r'</section>', c)))
    print('div:', len(re.findall(r'<div', c)), len(re.findall(r'</div>', c)))
    "
    
  5. Fix what you find, re-screenshot those slides, confirm the fix. Clean up every temp screenshot/scratch file when you're done: nothing but the deck (+ optional PDF) should remain.

Step 5: Optional PDF export

If asked for a PDF, use the companion slides-to-pdf skill (distributed alongside this one; its SKILL.md is the full self-contained recipe if it isn't installed as a skill). It screenshots every slide at 2x, prints a page-per-slide PDF with a centred page number on every page, and verifies the result by rendering the PDF back to images, which is required because headless Chrome cannot rasterize a local PDF for a visual check and image pages can be silently blank.

Step 6: Ship it, and say how to keep it honest

The deck is the only file you leave behind. Do not write a companion README.md, index, summary or notes file next to it, and do not add the deck to an existing one unless the user asks: the deck already states what it covers, the build stamp already records where it came from, and a hand-written sidecar is one more thing to go stale. If the repo already keeps such an index and the user wants it updated, that is their call to make, not a default.

Everything that would have gone in that file belongs in your final message instead: what the deck covers, its slide count, where it was written, how to view and navigate it (click, arrow keys, Esc for the overview, N for notes) and, since decks get edited slide-by-slide over many follow-up requests, one paragraph on how the file is structured for future edits (the pattern-block/comment-numbering conventions above).

Tell the user, in that same message, how to find out when the deck has gone stale:

python3 scripts/check.py <deck-or-folder> --repo <repo> --suggest

Then ask whether this deck should be kept in sync, and make it concrete rather than leaving it as a suggestion. The answer depends on what kind of document it is, so say so:

  • Evergreen (onboarding, architecture, anything linked from a README): offer to add the pull-request check from references/automation.md. Report-only first (--exit-zero), so it annotates a PR without blocking anyone. Vendor check.py into the repo (tools/slideops-check.py), because it is one dependency-free standard-library file and the deck's repo should not depend on a skill being installed.
  • A snapshot (sprint update, "what shipped in March", a conference talk): recommend not automating it. It describes a moment and is supposed to freeze. Say this out loud rather than silently skipping it.

Do not propose blocking every commit. A docs check on the fast path trains people to pass --no-verify, and drift is a review-time concern. The reasoning, the workflow files, the advisory hook variants, and the delegated-refresh recipe are all in references/automation.md.

Refreshing an existing deck

When the user asks whether a deck is still accurate, or wants one brought back in line, repair it; do not rebuild it. A rebuild throws away the pacing, the narrative and the review that went into the original, and costs far more than fixing three slides.

  1. Detect, for free. Sweep the folder and read the result:

    python3 scripts/check.py docs/slides/ --repo . --json
    

    This costs no tokens and no model call. The JSON is a complete repair brief: per stale citation it carries the status, the unified diff, the commits that caused it, the corrected data-src/data-sha256, and the current source. Read that instead of re-reading the repository. If nothing is stale, say so and stop: that is the common case and it should be cheap.

  2. Triage by status, because they need different work:

    • MOVED: the code is identical, only the line numbers shifted. Update the two attributes. Do not touch the slide's prose, and do not re-verify visually: nothing rendered changed.
    • CHANGED: read the diff and the commit subjects. A rename needs a re-quote; a deleted branch of logic may have killed the claim the slide makes. Decide about the claim first, then the snippet.
    • MISSING: the file is gone. The slide is probably obsolete. Find where it went (git log --diff-filter=D -- <path>) and ask the user before deleting a slide.
    • UNVERIFIED: no hash was recorded. Re-cite it with scripts/cite.py so it is checkable from now on.
  3. Repair only what drifted. Edit those slides, re-trim snippets to the width budget (references/style-guide.md § Code snippets), and leave every other slide alone.

  4. Re-stamp and re-verify. python3 scripts/cite.py --stamp <deck> --repo ., then check.py until clean, then re-screenshot only the slides you touched (Step 4): a longer snippet can push content under the nav pill. Re-export the PDF only if one exists.

  5. Report what changed and why. Name the slides you edited, the commits that caused the drift, and anything you judged still-true-despite-the-diff. That last category is where a human may disagree with you, so surface it rather than burying it.

Files (slideops)
  • assets
    • template.html 34.4 KB · in bundle
    • template.md 1.9 KB
      <!-- Copy this file to the output path, keep the section shapes you need, delete the
           rest, and replace every [BRACKETED] placeholder with real, verified content.
           Every fenced snippet quoted from the repository gets a slideops comment directly
           above its fence; print it with:  python3 scripts/cite.py path:start-end --repo . --md
           Stamp the finished doc (writes line 1):  python3 scripts/cite.py --stamp doc.md --repo .
           Full contract: references/markdown.md -->
      
      # [DOC TITLE]
      
      [One-paragraph summary: what this document covers and who it is for. Real claims only,
      each traceable to the repository.]
      
      ## [Section name]
      
      [Prose grounded in the code. Cite anything quoted or asserted from one specific place;
      prose summarising a whole subsystem needs no citation.]
      
      <!-- PATTERN: cited snippet. The comment comes from cite.py --md, never typed by hand. -->
      <!-- slideops data-src="[path/to/file.py:START-END]" data-sha256="[FROM cite.py]" -->
      ```python
      [verbatim source lines, unescaped, exactly as cite.py --md --snippet prints them]
      ```
      
      <!-- PATTERN: diagram. GitHub renders mermaid fences natively; no download needed.
           Trace the real call path before drawing it; a plausible wrong diagram is worse
           than a smaller true one. -->
      ```mermaid
      flowchart LR
          A[[real module]] --> B[[real module]]
      ```
      
      <!-- PATTERN: image. Real artifacts only (a chart the repo's tooling produced, a real
           screenshot); repo-relative path from the doc's location; never fabricate. -->
      ![What the image actually shows](relative/path/to/real-image.png)
      
      <!-- PATTERN: table. Build rows from a real source file and cite it when the numbers
           come from one place. -->
      | [Column] | [Column] |
      |---|---|
      | [real value] | [real value] |
      
      ## [Next section name]
      
      [Repeat the patterns above as needed. One idea per section; headings are what the
      freshness report names when a citation drifts, so keep them specific.]
      
  • examples
    • skill-demo.html 1 MB · in bundle
    • skill-demo.md 4.8 KB
      <!-- slideops-build commit=9dcc206 date=2026-08-31 repo=slideops -->
      # SlideOps Markdown docs, demonstrated
      
      This document is the worked example for the skill's Markdown mode: a doc about the
      mechanism, built by the mechanism. Every fenced snippet below carries a citation comment
      written by `cite.py --md`, the file carries a build stamp on line 1, and this
      repository's CI runs `check.py` against this very file. If an edit to the shipped
      scripts drifts a snippet, the build says so. The deck twin of this example is
      [`skill-demo.html`](skill-demo.html).
      
      ![The same slide rendered in Ledger Light and Ledger Dark, split diagonally](../../../docs/hero.png)
      
      ## The carrier format
      
      A doc carries the same two facts a deck carries, as comments that GitHub never renders.
      The stamp owns line 1 of the file, and each citation sits directly above the fence it
      vouches for:
      
      ````markdown
      <!-- slideops-build commit=a8bde99 date=2026-08-31 repo=my-service -->
      
      <!-- slideops data-src="app/main.py:40-58" data-sha256="a1b2c3d4e5f6" -->
      ```python
      def main() -> int:
          return 0
      ```
      ````
      
      ## How the checker reads a doc
      
      One regular expression finds the citation comments. It is deliberately narrow: a
      `slideops` comment with a `data-src`, an optional lowercase hex `data-sha256`, nothing
      else:
      
      <!-- slideops data-src="skills/slideops/scripts/check.py:49-52" data-sha256="31c21278b704" -->
      ```python
      MD_CITATION_RE = re.compile(
          r'<!--\s*slideops\s+data-src="(?P<src>[^"]+)"(?:\s+data-sha256="(?P<sha>[0-9a-f]{6,64})")?\s*-->',
          re.I,
      )
      ```
      
      Fenced content is masked before scanning, which is why the quoted example in the
      previous section is not miscounted as a real citation, and each surviving match is
      attributed to the nearest preceding heading. That heading is what a freshness report
      names when the snippet drifts:
      
      <!-- slideops data-src="skills/slideops/scripts/check.py:213-219" data-sha256="16901bd2c8c9" -->
      ```python
      def find_citations_md(doc_text: str) -> list[Citation]:
          """Attach each citation comment to the nearest preceding Markdown heading."""
          masked = mask_fences(doc_text)
          headings: list[tuple[int, int, str]] = [
              (m.start(), index, m.group("text").strip()) for index, m in enumerate(MD_HEADING_RE.finditer(masked))
          ]
          return [_citation(match, headings) for match in MD_CITATION_RE.finditer(masked)]
      ```
      
      ## How the writer builds a doc
      
      `cite.py --md --snippet` prints the comment and the source in a language-tagged fence.
      Markdown needs no HTML escaping, but a snippet can contain backtick fences of its own,
      so the fence grows until it cannot collide:
      
      <!-- slideops data-src="skills/slideops/scripts/cite.py:80-83" data-sha256="8bca63483abd" -->
      ```python
      def fence_for(lines: list[str]) -> str:
          """A backtick fence one longer than any backtick run in the content, minimum three."""
          longest_run = max((len(m) for line in lines for m in re.findall(r"`+", line)), default=0)
          return "`" * max(3, longest_run + 1)
      ```
      
      Stamping is position-based rather than search-based: line 1 belongs to the stamp, so a
      stamp-shaped example inside a fence (like the one in this document) is never rewritten:
      
      <!-- slideops data-src="skills/slideops/scripts/cite.py:149-159" data-sha256="87cb2a112b78" -->
      ```python
          if deck.suffix.lower() in MD_SUFFIXES:
              # The stamp owns line 1 of a Markdown doc; a stamp-shaped example deeper in the
              # file (say, inside a code fence) is content and must never be rewritten.
              meta = f"<!-- slideops-build {payload} -->"
              if MD_BUILD_META_RE.match(text):
                  text, action = MD_BUILD_META_RE.sub(meta + "\n", text, count=1), "updated"
              else:
                  text, action = meta + "\n" + text, "inserted"
              deck.write_text(text)
              print(f"{action}: {meta}")
              return 0
      ```
      
      ## The loop this enables
      
      ```mermaid
      flowchart LR
          build["cite.py --md<br>writes citations"] --> doc["doc.md<br>stamped, committed"]
          doc --> checker["check.py<br>milliseconds, no tokens"]
          checker -->|CURRENT| done["nothing to do"]
          checker -->|"stale (--json brief)"| repair["agent repairs only<br>the drifted sections"]
          repair --> build
      ```
      
      The statuses are the same five a deck gets, and a directory sweep picks up `.md` docs
      and `.html` decks together:
      
      | Status | Meaning for a doc |
      |---|---|
      | `CURRENT` | the cited lines are byte-identical to the source |
      | `MOVED` | same content, new line numbers; update the comment, leave the prose |
      | `CHANGED` | the cited lines differ; the section's claim needs a decision |
      | `MISSING` | the file is gone; the section is probably obsolete |
      | `UNVERIFIED` | no hash recorded; re-cite with `cite.py --md` |
      
      ## Try it on this file
      
      From the repository root:
      
      ```bash
      python3 skills/slideops/scripts/check.py skills/slideops/examples/skill-demo.md --repo .
      ```
      
      Every citation reports CURRENT, or this repository's CI would have failed the commit
      that broke it.
      
  • references
    • automation.md 6.5 KB
      # Keeping a deck in sync
      
      A deck is generated from code once. Keeping it true after that is the part every
      documentation effort loses, and it is the part this skill automates.
      
      Two costs, and they are nothing alike:
      
      | | What it costs | How often to run it |
      |---|---|---|
      | **Detect** (`scripts/check.py`) | standard library only, no model, no network, no tokens. Milliseconds. | As often as you like |
      | **Repair** (an agent rewrites slides) | tokens, and a review | Only when detect says so, and only for evergreen docs |
      
      That asymmetry is the whole design. Detection is free, so it can be everywhere. Repair is
      not, so it is a decision someone makes on purpose, scoped by `check` to the slides that
      actually drifted rather than a re-read of the whole deck.
      
      ## The loop
      
      ```
      build once  ->  check whenever (free)  ->  refresh on purpose (scoped)
      ```
      
      ## Level 1: on demand
      
      The default, and for most decks the only level you need:
      
      ```bash
      python3 scripts/check.py docs/slides/ --repo .            # sweep every deck in the folder
      python3 scripts/check.py docs/slides/ --repo . --quiet    # silent unless something drifted
      python3 scripts/check.py docs/slides/ --repo . --suggest  # + diff, commits, corrected citation
      ```
      
      A directory sweeps recursively and skips HTML that carries no citations, so pointing it at
      `docs/` is safe. Exit code is 1 when anything is stale, 0 when clean.
      
      In a session, "are the decks still true?" is enough: the agent runs the sweep and reads
      the result.
      
      ## Level 2: on a pull request
      
      The right gate for a deck people rely on. Not every commit: a docs check that blocks the
      fast path teaches everyone to pass `--no-verify`, and drift is a review-time concern, not
      a keystroke-time one.
      
      `check.py` is one dependency-free file, so a repo that owns decks should vendor it rather
      than depend on every contributor having the skill installed:
      
      ```bash
      curl -o tools/slideops-check.py \
        https://raw.githubusercontent.com/glukicov/slideops/main/skills/slideops/scripts/check.py
      ```
      
      (Or copy it out of your own checkout: `cp skills/slideops/scripts/check.py tools/`. Where
      the installed copy lives depends on how the skill was installed, so the URL is the portable
      instruction.)
      
      ```yaml
      # .github/workflows/deck-freshness.yml
      name: Deck freshness
      on:
        pull_request:
          branches: [main]
      
      jobs:
        check:
          runs-on: ubuntu-latest
          steps:
            - uses: actions/checkout@v4
              with:
                fetch-depth: 0
            - uses: actions/setup-python@v5
              with:
                python-version: "3.14"
            - name: Are the decks still true?
              run: |
                python3 tools/slideops-check.py docs/slides/ --repo . --suggest --exit-zero \
                  | tee -a "$GITHUB_STEP_SUMMARY"
      ```
      
      `--exit-zero` makes this **report-only**: the drift and its fix land in the PR summary,
      and nobody is blocked. Start here. Drop `--exit-zero` later, for the one or two decks the
      team actually relies on, once the reports have been quiet for a while.
      
      ## Level 3: delegated refresh
      
      This is where the tokens go, and the reason detection is worth wiring up at all.
      
      `--json` is a complete repair brief. For every stale citation it carries the status, the
      unified diff, **the commits that caused it**, the corrected `data-src` and `data-sha256`,
      and the current source. Current citations stay one line each, so the payload stays small:
      
      ```bash
      python3 scripts/check.py docs/slides/ --repo . --json > /tmp/drift.json
      ```
      
      The commit subjects are the part a diff cannot give you. "rename helper for clarity" and
      "drop the retry branch" produce similar diffs and need completely different slide edits:
      one is a re-quote, the other may invalidate the claim the slide is making.
      
      Hand that to the agent (headless, in CI, or in a session):
      
      ```bash
      claude -p "Refresh the stale slides in docs/slides/.
      $(cat /tmp/drift.json)
      
      For each stale citation: read the diff and the commit subjects, decide whether the slide's
      CLAIM is still true, then either re-quote (MOVED: attributes only, leave the prose alone)
      or rewrite the slide (CHANGED: the claim may be dead). Update data-src and data-sha256 from
      suggested_src/suggested_sha256, re-stamp the build meta with scripts/cite.py --stamp, then
      re-run check.py until clean and re-screenshot only the slides you touched." \
        --allowedTools "Read,Edit,Bash"
      ```
      
      Then open it as a pull request rather than a push, because a refresh is a content change
      that deserves the same review as the code that caused it. Monthly on a schedule works well
      for an onboarding deck:
      
      ```yaml
      on:
        schedule:
          - cron: "0 9 1 * *"     # 09:00 on the 1st
        workflow_dispatch:
      ```
      
      Guard the token spend with the free half first, so a quiet month costs nothing:
      
      ```bash
      python3 tools/slideops-check.py docs/slides/ --repo . --quiet || refresh_decks
      ```
      
      ## Level 4: agent hooks, advisory only
      
      If you want the agent itself to notice, keep it to a notification. A hook that blocks is a
      hook that gets disabled.
      
      `SessionStart` is the low-noise option: one line, once, at the point where acting on it is
      cheap. With `--quiet`, a clean repo prints nothing at all.
      
      ```json
      {
        "hooks": {
          "SessionStart": [
            {
              "hooks": [
                {
                  "type": "command",
                  "command": "python3 tools/slideops-check.py docs/slides/ --repo . --quiet --exit-zero"
                }
              ]
            }
          ]
        }
      }
      ```
      
      A git `pre-commit` hook is the same idea, and the same rule applies: warn, exit 0, never
      block.
      
      ```bash
      #!/bin/sh
      # .git/hooks/pre-commit: advisory, never blocking
      python3 tools/slideops-check.py docs/slides/ --repo . --quiet --exit-zero
      exit 0
      ```
      
      ## What to automate, and what to let rot
      
      Not every deck deserves any of this. Automating a snapshot is pure waste:
      
      | Deck | Automate? |
      |---|---|
      | Onboarding, architecture, anything linked from the README | Yes: PR check, and a scheduled refresh |
      | A subsystem deep dive the team keeps returning to | PR check, report-only |
      | Sprint update, "what shipped in March" | No. It is a record of a moment and is *supposed* to freeze |
      | A conference talk you gave once | No |
      
      A deck that describes a point in time should keep describing that point in time. Freshness
      checking is for documents that claim to describe *now*.
      
      ## What a clean check does not prove
      
      `check` verifies that quoted source still matches the code. It cannot see that the prose
      around a snippet has become wrong, that a slide is missing a subsystem added last quarter,
      or that the architecture diagram is now a lie. It tells you what has **definitely** drifted,
      which is the part humans reliably miss, and it makes the rest a smaller problem.
      
    • diagrams.md 4.2 KB
      # Diagrams: built-in flow boxes vs pre-rendered Mermaid
      
      Two ways to draw architecture on a slide. Both must depict the **real, verified** call
      path (SKILL.md Step 1); the choice is only about rendering.
      
      | Situation | Use |
      |---|---|
      | Linear pipeline, ≤6 boxes, one or two rows | Template's `.flow`/`.flow-box` pattern (no tooling, styles itself with the theme) |
      | Sequence diagram (request/response between components) | Mermaid `sequenceDiagram` |
      | Branching graph, fan-in/fan-out, back-edges | Mermaid `flowchart` |
      | ER / schema relationships | Mermaid `erDiagram` |
      | Git branching story | Mermaid `gitGraph` |
      
      Never load mermaid.js at runtime: the deck must stay a single self-contained file with no
      network access. Instead **pre-render to SVG at build time and inline the SVG** into the
      slide.
      
      This is the one step that downloads code. It fetches a pinned `@mermaid-js/mermaid-cli`
      unless the binary is already installed, so treat it as an opt-in the user agreed to in the
      intake: if the environment has no network, or the user did not opt in, use the flow-box
      pattern instead and say why. Diagram sources you write are your own text, but the labels
      in them land in a shipped artifact, so the confidentiality rule applies to them too.
      
      ## Render recipe
      
      1. Write the diagram source to `$STAGE/<name>.mmd`.
      2. Write a theme config deriving from the deck's current `:root` tokens (open the deck and
         copy the literal values; mermaid can't read CSS variables):
      
      ```json
      // $STAGE/mermaid-config.json: values from the DECK'S CURRENT THEME, not hardcoded olive
      {
        "theme": "base",
        "themeVariables": {
          "background": "transparent",
          "primaryColor": "<--card>",
          "primaryTextColor": "<--fg>",
          "primaryBorderColor": "<--accent>",
          "lineColor": "<--muted>",
          "secondaryColor": "<--bg2>",
          "tertiaryColor": "<--bg>",
          "fontFamily": "-apple-system, Segoe UI, Roboto, sans-serif",
          "fontSize": "16px"
        }
      }
      ```
      
      3. Point puppeteer at the same Chrome that verification.md's `find_chrome` found (no
         second browser download):
      
      ```bash
      printf '{ "executablePath": "%s" }\n' "$CHROME" > "$STAGE/puppeteer-config.json"
      # Pinned: an unpinned `npx --yes` runs whatever the registry serves today, unreviewed.
      # If mmdc is already installed, use it and skip the download entirely.
      MMDC=$(command -v mmdc || echo "npx --yes @mermaid-js/mermaid-cli@11.4.2")
      $MMDC \
        -i "$STAGE/<name>.mmd" -o "$STAGE/<name>.svg" \
        -c "$STAGE/mermaid-config.json" -p "$STAGE/puppeteer-config.json" \
        -b transparent -I "mmd-<name>"
      ```
      
      4. Inline the SVG into the slide inside `.img-wrap` (the template constrains inline SVG to
         the slide body):
      
      ```html
      <section class="slide">
        <div class="kicker">[Section name]</div>
        <h2>[Slide title]</h2>
        <div class="img-wrap"><!-- paste the full <svg …>…</svg> here --></div>
        <p class="caption">[What the diagram shows; verified against the real call path.]</p>
      </section>
      ```
      
      ## Rules that matter
      
      - **`-I "mmd-<name>"` (unique per diagram) is mandatory.** The SVG's internal `<style>` is
        scoped to its root id; two inlined diagrams with the default `my-svg` id restyle each
        other.
      - **Theme by config, verify by screenshot.** Some mermaid internals (e.g. sequence-diagram
        activation bars) ignore `themeVariables` and keep light-grey fills; that's usually fine
        on dark themes, but check the slide screenshot for unreadable text or glaring fills and
        simplify the diagram (or switch diagram type) if theming fights you.
      - **A rethemed deck does not retheme inlined SVGs.** Their colors are baked at render
        time; re-run this recipe for every Mermaid slide after a theme swap.
      - **Size**: mermaid emits `width="100%"` with a `viewBox`; the slide's `.img-wrap` caps it
        to the available body. If a diagram renders too small to read at 1280×720, it has too
        many nodes for one slide: split it, don't shrink the font.
      - **Keep the `.mmd` sources** in `$STAGE` until the deck ships, then delete with the rest
        of staging; the inlined SVG is the artifact. If the user wants editable sources kept,
        put them next to the deck as `<deck>-diagrams/<name>.mmd`.
      - Escape rules still apply to `.mmd` content on slides only if you *show* the source in a
        `<pre>`; the inlined SVG itself is already valid markup: never HTML-escape it.
      
    • freshness.md 5.9 KB
      # Citations and freshness
      
      A deck starts rotting the moment it is exported. Citations make that measurable: each
      quoted snippet records where it came from and what the source looked like at the time, so
      `scripts/check.py` can tell you later which slides are still true.
      
      This is the difference between a deck that *claims* to be verified and one that can prove
      it. Write citations as you build, not as a pass at the end.
      
      ## Citing a snippet
      
      Two attributes on the `<pre>` (or on any element that carries quoted content):
      
      ```html
      <pre class="code small"
           data-src="backend/app/main.py:40-58"
           data-sha256="a1b2c3d4e5f6">…HTML-escaped snippet…</pre>
      ```
      
      - **`data-src`** is a repository-relative path, optionally with a 1-indexed inclusive line
        range: `path/to/file.py`, `path/to/file.py:42`, or `path/to/file.py:40-58`. Without a
        range, the whole file is the citation.
      - **`data-sha256`** is the first 12 hex characters of the SHA-256 of the **source lines as
        they were when you built the deck**, joined with `\n`. Hash the source, not the slide:
        snippets are usually trimmed with `…`, so they never byte-match the file.
      
      Never compute it by hand. `scripts/cite.py` prints the pair, warns when the lines are too
      wide for the slide pattern, and with `--snippet` prints the source already HTML-escaped:
      
      ```console
      $ python3 scripts/cite.py backend/app/main.py:40-58 --repo .
      data-src="backend/app/main.py:40-58" data-sha256="a1b2c3d4e5f6"
      ```
      
      A wrong hash is worse than a missing one: it surfaces months later as CHANGED, and by then
      nobody can tell whether the code moved or the build was sloppy.
      
      Cite anything you quote or assert from a specific place: code snippets, config blocks,
      tables built from a source file, a diagram's underlying module. Prose that summarises a
      whole subsystem does not need a citation; a number lifted from one line does.
      
      ## The Markdown carrier
      
      A Markdown doc (see [`markdown.md`](markdown.md)) carries the same two attributes in an
      HTML comment directly above the fence it vouches for, invisible when rendered:
      
      ````markdown
      <!-- slideops data-src="backend/app/main.py:40-58" data-sha256="a1b2c3d4e5f6" -->
      ```python
      ...verbatim source...
      ```
      ````
      
      `cite.py --md` prints the comment (and with `--snippet` the fenced source, no HTML
      escaping needed). Citations attach to the nearest preceding heading, which is what the
      report names instead of a slide number. The build stamp is a comment on line 1:
      
      ```markdown
      <!-- slideops-build commit=a9c9c0d date=2026-08-24 repo=my-service -->
      ```
      
      `cite.py --stamp doc.md` writes it, and `check.py` treats `.md` and `.html` targets
      identically from there on: same statuses, same `--suggest`, same `--json` repair brief.
      
      ## Recording the build point
      
      Once per deck, in the `<head>`:
      
      ```html
      <meta name="slideops-build" content="commit=a9c9c0d date=2026-08-24 repo=my-service">
      ```
      
      The commit is what lets `check` explain *how* a snippet changed rather than only that it
      did: it reads the old lines from that commit and diffs them against today's, and lists the
      commits responsible. Without it, you still get CURRENT/CHANGED, just no diff, no move
      detection and no "who changed this". Write it with the script, which uses the repository's
      real HEAD and today's real date rather than a remembered one:
      
      ```bash
      python3 scripts/cite.py --stamp docs/slides/architecture.html --repo .
      ```
      
      ## Checking a deck later
      
      ```bash
      python3 scripts/check.py docs/slides/architecture.html --repo .   # one deck
      python3 scripts/check.py docs/slides/ --repo .                    # sweep a folder
      python3 scripts/check.py docs/slides/ --repo . --quiet            # silent unless stale
      python3 scripts/check.py docs/slides/ --repo . --suggest          # + diff, commits, the fix
      python3 scripts/check.py docs/slides/ --repo . --json             # repair brief for an agent
      ```
      
      A directory is swept recursively, skipping HTML that carries no citations. The run is
      standard library only: no model, no network, no tokens.
      
      | Status | Meaning | What to do |
      |---|---|---|
      | `CURRENT` | cited lines are byte-identical | nothing |
      | `MOVED` | same content, new line numbers | update `data-src`, leave the slide's text alone |
      | `CHANGED` | the cited lines differ | read the diff, decide whether the slide's claim still holds, re-quote |
      | `MISSING` | the file is gone | the slide is probably obsolete; `git log --diff-filter=D -- <path>` finds where it went |
      | `UNVERIFIED` | no hash, or the build commit is not in this repo | re-cite the snippet |
      
      Exit code is 1 when anything is stale, and `--exit-zero` forces 0 for report-only jobs. How
      to wire that into a pull request, a scheduled refresh, or an advisory agent hook (and which
      decks deserve it at all) is in [`automation.md`](automation.md).
      
      ## Updating a stale deck
      
      Repair the slides that drifted; do not rebuild the deck. The full workflow, including how
      to triage each status, is in SKILL.md § Refreshing an existing deck. In short:
      
      1. `--suggest` (for a human) or `--json` (for an agent) prints, per stale citation, what
         changed, **which commits changed it**, the corrected `data-src` and `data-sha256`, and
         the current source HTML-escaped and ready to paste.
      2. Read the diff and the commit subjects, then decide whether the slide's **claim** is
         still true. A renamed variable usually just needs a re-quote; a deleted branch of logic
         may invalidate the whole slide. `MOVED` never needs a prose change at all.
      3. Replace the snippet, update both attributes, and re-trim to the width budget
         (style-guide.md § Code snippets). Both scripts warn when lines are too wide for the
         pattern you used.
      4. Re-stamp and re-verify: `python3 scripts/cite.py --stamp <deck> --repo .`, then `check`
         until clean, then re-screenshot the slides you touched (a longer snippet can push
         content under the nav pill).
      
      A deck whose citations all pass is not automatically correct: the prose around a snippet
      can still be wrong, and `check` cannot see that. It tells you what has *definitely* drifted,
      which is the part humans reliably miss.
      
    • markdown-pdf.md 5.2 KB
      # Markdown doc to PDF
      
      Exports a doc built by [`markdown.md`](markdown.md) as a paginated PDF. The pipeline
      differs from the slides-to-pdf skill in one structural way: a doc is a flowing document,
      so there are no per-page screenshots. Convert the Markdown to HTML, wrap it in print
      CSS, and let Chrome's own print pagination break the pages. The verification step is the
      same, because headless Chrome cannot rasterize a local PDF to check its own output.
      
      Inputs to establish up front: the doc path, the output path (default: next to the `.md`,
      same basename, `.pdf`), and whether the doc contains `mermaid` fences (they need
      pre-rendering; see step 2).
      
      ## 1. Find Chrome and make a staging directory
      
      Chrome discovery and the staging rules are in
      [`verification.md`](verification.md): always a fresh per-export directory, never a
      fixed shared path, and do not add `--no-sandbox` (you may be printing a doc you did not
      author).
      
      ```bash
      DOC="/absolute/path/to/doc.md"
      STAGE=$(mktemp -d -t mdpdf)
      ```
      
      ## 2. Convert the Markdown to HTML
      
      Use any GitHub-flavored converter already available (`pandoc`, `marked`, a Python
      `markdown-it` install). If none is installed, one npx download does it, the same network
      opt-in the skill uses for Mermaid:
      
      ```bash
      npx --yes marked --gfm -i "$DOC" -o "$STAGE/body.html"
      ```
      
      Two fix-ups on the produced body, both mechanical:
      
      - **Relative image paths** break the moment the HTML lives in `$STAGE`. Rewrite each
        `src` to an absolute `file://` path resolved from the doc's own directory.
      - **Mermaid fences** come out as plain `<code class="language-mermaid">` blocks, which
        print as source text. Pre-render each one to SVG with mermaid-cli exactly as
        [`diagrams.md`](diagrams.md) § Render recipe does for decks, and replace the
        code block with the inline SVG. If the user declined network access, say so and leave
        the fence as a code block rather than dropping it.
      
      The citation comments need no handling: HTML comments are invisible in print.
      
      ## 3. Wrap in print CSS and print
      
      ```bash
      python3 - "$STAGE" <<'EOF'
      from pathlib import Path
      import sys
      
      stage = Path(sys.argv[1])
      body = (stage / "body.html").read_text()
      (stage / "print.html").write_text(f"""<!doctype html><meta charset="utf-8"><style>
        @page {{ size: A4; margin: 18mm 16mm 20mm;
          @bottom-center {{ content: counter(page) " / " counter(pages);
                           font: 9pt/1 -apple-system, "Segoe UI", Roboto, sans-serif;
                           color: #666; }} }}
        body {{ font: 11pt/1.55 -apple-system, "Segoe UI", Roboto, sans-serif;
                max-width: 100%; margin: 0; }}
        h1, h2, h3 {{ line-height: 1.25; page-break-after: avoid; }}
        pre {{ font: 8.5pt/1.45 ui-monospace, "SF Mono", Consolas, monospace;
               padding: 8pt 10pt; border: 0.5pt solid #ccc; border-radius: 4pt;
               white-space: pre-wrap; page-break-inside: avoid; }}
        img, svg {{ max-width: 100%; }}
        table {{ border-collapse: collapse; }}
        td, th {{ border: 0.5pt solid #999; padding: 3pt 8pt; }}
      </style>
      {body}""")
      EOF
      
      "$CHROME" --headless=new --disable-gpu \
        --print-to-pdf="$STAGE/output.pdf" --no-pdf-header-footer \
        "file://$STAGE/print.html"
      ```
      
      The `@bottom-center` block is what numbers the pages: a CSS paged-media margin box, so
      Chrome paints `1 / 7` centred in the bottom margin of every page and keeps counting
      correctly however the content happens to break. The wider `20mm` bottom margin is the
      room it prints into. Keep `--no-pdf-header-footer`: Chrome's own footer is a right-hand
      `file://` URL and a date, which is not what was asked for and would sit beside this one.
      
      Margin boxes are a recent Chrome feature. An older binary drops the rule silently and
      prints unnumbered pages that otherwise look correct, which is why step 4 reads the
      numbers back rather than assuming them.
      
      ## 4. Verify by rendering the PDF back to images
      
      Identical to the slides-to-pdf verification, and just as mandatory: render every page
      back to an image with pypdfium2 (throwaway venv if it is not installed), then actually
      view the first page, the last page, and every page that should contain an image or a
      diagram. The classic silent failure is a broken image path producing a right-looking
      page count with a blank page in the middle.
      
      The same pass reads the footer back. `page.get_textpage().get_text_range()` returns the
      margin box text along with the body, so assert that every page contains its own
      `"{i} / {n}"` before shipping; a Chrome too old for margin boxes fails here and nowhere
      else.
      
      ```bash
      python3 -m venv "$STAGE/venv" && "$STAGE/venv/bin/pip" install --quiet pypdfium2 Pillow
      "$STAGE/venv/bin/python" - "$STAGE" <<'EOF'
      import sys
      import pypdfium2 as pdfium
      
      stage = sys.argv[1]
      pdf = pdfium.PdfDocument(f"{stage}/output.pdf")
      n = len(pdf)
      print("page count:", n)
      for i in range(n):
          pdf[i].render(scale=1.5).to_pil().save(f"{stage}/check-{i + 1:02d}.png")
          footer = f"{i + 1} / {n}"
          assert footer in pdf[i].get_textpage().get_text_range(), f"page {i + 1} has no footer"
      print("every page is numbered")
      EOF
      ```
      
      ## 5. Ship and clean up
      
      Copy the verified `output.pdf` to the output path, delete `$STAGE` entirely, and note in
      your summary that the PDF is a snapshot: `check.py` verifies the `.md`, and a re-export
      after a repair is how the PDF catches up.
      
    • markdown.md 6.2 KB
      # Markdown documentation mode
      
      Sometimes the right artifact is not a deck: the user wants a document that lives in the
      repository, renders on GitHub, and gets read top to bottom. This mode produces a single
      self-contained Markdown file with the same guarantee as a deck: every quoted snippet
      carries a citation, so `scripts/check.py` can prove later which sections still match the
      code and hand an agent a repair brief for only the parts that drifted.
      
      Choose this mode when the user says "markdown docs", "a document, not slides",
      "README-style documentation", or asks for docs "that know when they go stale". Everything
      about research rigor, confidentiality, and real-content-only carries over from SKILL.md
      unchanged; this file covers only what differs.
      
      ## The carrier format
      
      Two comment forms carry the same facts the HTML attributes carry in a deck. Both are
      invisible in rendered Markdown.
      
      The build stamp owns line 1 of the file:
      
      ```markdown
      <!-- slideops-build commit=a8bde99 date=2026-08-31 repo=my-service -->
      ```
      
      Each citation sits directly above the fence it vouches for:
      
      ````markdown
      <!-- slideops data-src="app/main.py:40-58" data-sha256="a1b2c3d4e5f6" -->
      ```python
      def main() -> int:
          ...
      ```
      ````
      
      `data-src` and `data-sha256` mean exactly what they mean in decks (see
      [`freshness.md`](freshness.md)): repository-relative path with an optional 1-indexed
      inclusive line range, and the first 12 hex chars of the SHA-256 of the source lines at
      build time. Hash the source, never a trimmed snippet, and never by hand:
      
      ```bash
      python3 scripts/cite.py app/main.py:40-58 --repo . --md --snippet
      ```
      
      prints the ready-to-paste comment plus the source in a language-tagged fence, unescaped
      (Markdown needs no HTML escaping, and the fence grows automatically when the source
      itself contains backtick fences). Stamp once, when the doc is otherwise finished:
      
      ```bash
      python3 scripts/cite.py --stamp docs/architecture.md --repo .
      ```
      
      Citations attach to the nearest preceding heading, the way deck citations attach to
      slide comments. The freshness report names that heading when a citation drifts, so keep
      headings specific: "The request path" locates a repair; "Details" does not.
      
      ## Intake differences
      
      The Step 0 orientation pass and the compact intake carry over, with these swaps:
      
      - **No theme, no length, no PDF questions.** Markdown has no slide budget and GitHub
        renders it; drop intake items 3 and 4, and the PDF toggle.
      - **Output path**: default `<repo>/docs/<topic-slug>.md`. Ask only if the repo has an
        established docs layout that suggests otherwise.
      - **Diagrams are free.** GitHub renders `mermaid` fences natively, so Mermaid needs no
        download and no opt-in here. Still trace the real call path before drawing one.
      - The outline checkpoint still applies: propose the heading structure before writing.
      
      ## Build
      
      1. Copy [`assets/template.md`](../assets/template.md) to the output path. It shows the
         pattern for cited snippets, Mermaid fences, images, and tables. Replace every
         bracketed placeholder with real content and delete the guidance comments.
      2. **Cite every quoted snippet as you write it** with `cite.py --md --snippet`, never by
         hand. Cite anything quoted or asserted from one specific place; prose summarising a
         whole subsystem needs no citation.
      3. **Images must be real artifacts** (a chart the repo's own tooling produced, a real
         screenshot), linked by a path that resolves from the doc's location. Crop and re-read
         screenshots per the confidentiality rule. Never fabricate an image.
      4. Prose rules from [`style-guide.md`](style-guide.md) that are not slide-layout rules
         still apply: sentence case headings, no em dashes, no invented numbers, tag accuracy.
      5. Stamp the doc with `cite.py --stamp` once content is final.
      
      ## Verify
      
      1. `python3 scripts/check.py docs/<doc>.md --repo .` must report every citation CURRENT.
         CHANGED or UNVERIFIED on build day is a build defect: fix it with `cite.py` now.
      2. Fences must balance and every `slideops` comment must sit directly above its fence;
         a preview render (GitHub, or any Markdown viewer) catches a broken fence instantly.
      3. Confirm every image link resolves from the doc's committed location, not from your
         working directory.
      
      ## Ship, and keep it honest
      
      **The doc is the only file you leave behind.** Do not write a companion `README.md`, index
      or summary file beside it, and do not link it into an existing one unless the user asks:
      the doc's own title and build stamp already say what it covers and where it came from, and
      an uncited sidecar is the one file `check.py` cannot keep honest. Report what the doc
      covers, and where you wrote it, in your final message instead.
      
      Tell the user how to find out when the doc goes stale, exactly as with a deck:
      
      ```bash
      python3 scripts/check.py docs/ --repo . --suggest
      ```
      
      Directory sweeps pick up `.md` docs and `.html` decks together, skipping files that
      carry no citations. The automation recipes in [`automation.md`](automation.md) apply
      verbatim: the same vendored `check.py`, the same report-only pull request check, the
      same evergreen-vs-snapshot decision.
      
      ## Optional PDF export
      
      If the user asks for a PDF of the doc, follow [`markdown-pdf.md`](markdown-pdf.md): a
      Markdown-to-HTML conversion, a print-CSS wrap, Chrome's native print pagination, a centred
      `page / total` footer on every page, and the same render-back verification the
      slides-to-pdf skill uses. Offer it the way decks offer PDF export; do not produce one
      unasked.
      
      ## Refreshing a stale doc
      
      The deck workflow in SKILL.md § Refreshing an existing deck applies with one word
      swapped: repair sections, not slides.
      
      1. `check.py <doc-or-folder> --repo . --json` is the complete repair brief: per stale
         citation it names the section heading, the status, the diff, the causing commits, and
         the corrected attributes.
      2. Triage by status exactly as for decks: MOVED updates two attributes and touches no
         prose; CHANGED means read the diff and decide whether the section's claim still
         holds; MISSING usually means the section is obsolete (ask before deleting);
         UNVERIFIED means re-cite.
      3. Rewrite only the sections that drifted, re-stamp, and run `check.py` until clean. No
         screenshots to redo: the rendered doc is the artifact.
      
    • style-guide.md 8.5 KB
      # Style guide
      
      Apply these rules as you write each slide, not as a cleanup pass afterward.
      
      ## Language
      
      **Sentence case, always.** Slide titles, kickers, tags, table headers, section-divider
      labels: all sentence case (`Six orchestrator modes`, not `SIX ORCHESTRATOR MODES`). The
      template's CSS has no forced `text-transform: uppercase` anywhere; don't add it back.
      
      **No em dashes ( — ) in prose, anywhere.** Replace them contextually, not with a single
      blanket substitution:
      
      | Context | Original em-dash pattern | Replacement |
      |---|---|---|
      | Label, then elaboration | `Pushed — every run` | `Pushed: every run` |
      | Parenthetical aside | `a bundle — local or hosted — can be onboarded` | `a bundle, local or hosted, can be onboarded` |
      | Short badge/tag text | `Live — last 100 calls` | `Live · last 100 calls` |
      | Joining two short nouns in a heading | `Talk to the assistant — and the loop` | `Talk to the assistant & the improvement loop` |
      | Mid-sentence emphasis break | `stays the default — legacy` | `stays the default: legacy` |
      
      Verbatim code snippets are the one place an em dash may legitimately survive: if a real
      source line you are quoting contains one, prefer trimming that line (comments are usually
      the culprit and usually trimmable) over rewording the snippet; never edit quoted code just
      to satisfy the character rule. Practical check: every `grep -n "—" deck.html` hit must be
      inside a `<pre>` snippet copied verbatim from source; prose hits are bugs.
      
      **En dashes ( – ) for genuine numeric/date ranges are fine and expected**:
      `Aug 17–20`, `140–165`. Don't touch these; they aren't the same character or the same
      problem.
      
      **Concise, technical, no marketing language.** Short declarative sentences. Numbers and file
      paths over adjectives. If you can cut a word without losing meaning, cut it.
      
      **Prefer assertion titles.** When a slide makes a claim, the title states the claim
      (`Every write is replicated off-box`, not `Replication overview`); the audience should get
      the point from titles alone. Plain topic labels are fine for reference slides (tables,
      pattern lists) that don't argue anything.
      
      **No emoji in slide prose.** The only emoji in a deck is the one the chat-bubble pattern
      ships (💬). If source material you're summarizing contains emoji, paraphrase around them;
      in verbatim snippets, prefer quoting lines without them.
      
      ## Content accuracy: the rule that matters most
      
      **Every code snippet, file path, command, config field, and number on a slide must be
      copy-verified against the real repository before it's typed onto a slide.** Concretely,
      that means before you write a snippet:
      
      - `view`/`grep` the real file and copy from it; don't reconstruct a class or config from
        memory or from a similar-looking example elsewhere.
      - If you reference a field name (e.g. a dataclass attribute), confirm it exists in the
        actual class definition, not just in a doc that describes it (docs drift; code wins).
      - If you claim a skill/CLI does something ("say to the assistant: ... → does X"), find the
        actual skill file or `--help` text that makes that true, and match your bubble's
        arrow-text to what it actually says.
      - If you build a chart, generate it from the tool's own real plotting/reporting code against
        real data on disk, not a hand-drawn approximation. If you can't get real data, use a
        different slide pattern instead of a fake chart.
      - If you report an aggregate stat (a mean, a percentile, a count of "successful" items),
        recompute it yourself from the real underlying data and state your filter explicitly (e.g.
        "the N most recent **successful** calls", and if asked to double check, actually
        cross-verify every item's status field against the raw source, not just trust the
        aggregation code once).
      
      A wrong or invented technical detail is far worse than a slide with less detail on it.
      
      ## Code snippets on slides
      
      - **Cite it**: every snippet carries `data-src` and `data-sha256` (references/freshness.md).
        An uncited snippet cannot be checked later, which defeats the point of quoting it.
      - **Escape first**: `&` → `&amp;`, `<` → `&lt;`, `>` → `&gt;` in every snippet. Unescaped
        generics/arrows/includes corrupt the surrounding markup silently.
      - **Line width budget**: `pre.code` clips overflow silently (`overflow:hidden`). At the
        `small` size, a half-width column (the two-column patterns) fits roughly **65
        characters** per line; a full-width `pre` fits roughly **95**. Measure your longest line
        before choosing a pattern; if real lines exceed the half-width budget, use a full-width
        `pre` with explanation cards above/below instead of beside.
      - **Trim with `…`**, never reword. Dropping whole lines (imports, long comments) and
        dedenting a nested snippet are fine; rewrapping a long line, truncating within a line,
        dropping type annotations, or any other edit inside a line counts as rewording and is
        not. When the load-bearing line itself exceeds every width budget, don't quote it at
        all: cite it as `path:line`, quote the shorter surrounding lines verbatim, and explain
        the long one in a card beside the snippet.
      
      ## Tags and severity: don't editorialize
      
      The template ships five tag classes: `tag-new`, `tag-major`, `tag-existing`, `tag-optional`,
      `tag-critical`. Use them to state a fact, never to dramatize one:
      
      - Don't call something "breaking" unless it truly breaks a **shipped, relied-upon**
        contract. A schema/format change with no prior external consumers is a new version, not a
        breaking one: label it with the version number (`tag-major`, text like `v1.0.0`), not the
        word "breaking".
      - On a roadmap/closing slide, distinguish **shipped** from **planned** unmistakably: shipped
        items use `.list-check` (✓), unshipped/planned items use `.list-todo` (→) with an explicit
        `(not yet shipped)` qualifier next to the heading. Mixing these (e.g. checkmarking planned
        work) reads as a false claim of completion.
      
      ## Images: real artifacts only
      
      Every image on a slide must be a genuine artifact: a real screenshot, a real chart the
      project's own tooling produced from real data, a real terminal/editor capture. Caption it
      with real numbers pulled from that same image or its underlying data; never a plausible
      guess. If a useful visual doesn't exist yet, generate it by actually invoking the project's
      own real code path (reusing its existing plotting/export functions is strongly preferred
      over writing new ad hoc plotting code), or pick a non-image slide pattern instead.
      
      Images normally reference files by relative path. A deck that will be distributed
      standalone, away from the folder its images live in, should embed them as base64
      `data:image/png;base64,…` URIs instead, so the single HTML file stays self-contained
      (at roughly +33% of the image bytes; keep such decks to a handful of images).
      
      ## Density and pacing
      
      Roughly **one idea per slide**. A table, a diagram, a code comparison, and a prose point
      each deserve their own slide rather than being stacked together to save slide count. Use
      section-divider slides (the `section-slide` pattern) as breathing room between major topics
      in a multi-topic deck. In a focused single-topic deck, kickers are usually landmark enough:
      add dividers only when its sub-topics each run 3+ slides; never in a lightning update.
      
      ## Theming
      
      The template is fully tokenized: every color derives (via `color-mix`) from the single
      `:root` block at the top, so **switching theme = replacing that one block**. Preset
      palettes and font options live in `references/themes.md`; offer them in the intake.
      
      If the user names a brand or reference site instead: **fetch it for real** (use your
      web-fetch tool to pull the site and read its actual CSS) and extract its literal hex/rgba
      values; don't approximate a palette from a verbal description or a vague memory of the
      brand. Map the extracted values onto the token roles per themes.md § Mapping a brand.
      
      Two invariants, checked with the same grep:
      
      ```bash
      grep -nE "rgba?\(|hsla?\(|oklch\(|hwb\(|#[0-9a-fA-F]{3,6}\b" deck.html
      ```
      
      1. Every hit must be inside the `:root` block, inside an inlined SVG (whose colors are
         baked at render time; re-render Mermaid diagrams after any theme swap, see
         `references/diagrams.md`), or inside a verbatim `<pre>` snippet whose real source
         contains the literal (quoted code is never edited to satisfy this rule, same as the
         em-dash carve-out). A literal anywhere else is a bug: replace it with a token or a
         `color-mix` of one, in the template too if that's where it came from.
      2. After a swap, zero old-palette values remain outside inlined SVGs and verbatim
         snippets.
      
    • themes.md 6.6 KB
      # Theme presets
      
      The template's entire look derives from the single `:root` token block at the top of
      `assets/template.html` (every tint and border is `color-mix`-derived from these tokens).
      **Switching theme = replacing that one block wholesale.** Each preset below is a complete
      drop-in replacement; after pasting it, the only line you might still touch is `--font`
      (and `--mono`) if the user chose a font from § Font options. The default block's role
      comments are optional and the presets omit them.
      
      After swapping, confirm no literals escaped the block:
      `grep -nE "rgba?\(|#[0-9a-fA-F]{3,6}\b" deck.html` must only hit lines inside `:root`
      (plus any inline SVG you embedded, whose colors are baked at render time).
      
      Offer these in the intake as one-liners, default first:
      
      - **Ledger Light** (default): warm paper surfaces, deep olive accent, dark code blocks;
        reads well in bright rooms, on projectors, and on paper.
      - **Ledger Dark**: the default after dark. Warm espresso surfaces and a soft gold accent,
        the same warm family as the light theme rather than a separate design. Pick this when
        the deck is presented in a dim room or embedded in dark docs.
      - **Midnight**: deep navy + sky-blue accent; calm, corporate-friendly dark.
      - **Graphite**: near-black neutral + amber accent; highest contrast, projector-safe.
      - **Match a brand**: user gives a site/style guide; extract real values per
        style-guide.md § Theming and map them onto the token roles below.
      
      Ledger Light and Ledger Dark are a matched pair: one warm family, with the surface/ink
      roles inverted and the accent moved from deep olive to soft gold so it holds up on dark
      surfaces. Offering "the same deck, dark" is therefore a one-block swap with no redesign.
      
      ## Ledger Light (default)
      
      Already in the template; shown here for reference when mapping a brand.
      
      ```css
      :root{
        /* surfaces */
        --bg:#faf6ee; --bg2:#f2ecdf; --card:#ffffff; --code-bg:#2b2820;
        --stage-1:#e8e0cf; --stage-2:#cfc6b2;
        --section-1:#f4eede; --section-2:#e6dcc6;
        /* text */
        --fg:#2b2620; --text:#3d372e; --strong:#171310; --muted:#8a8069;
        /* roles */
        --accent:#7a7f1f; --accent2:#5f6419; --warn:#a34a3f; --bad:#b5544a;
        /* code block */
        --code-fg:#e8dfc0; --code-comment:#948b74; --code-keyword:#d3c87a; --code-string:#c9b98a;
        /* title gradient: dark ink to accent */
        --grad-1:#2b2620; --grad-2:#7a7f1f;
        /* misc */
        --card-border:color-mix(in srgb, var(--muted) 30%, transparent);
        --img-bg:#ffffff; --shadow:rgba(60,50,30,.25); --radius:14px;
        --font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
        --mono: "SFMono-Regular", Consolas, Menlo, "Liberation Mono", monospace;
      }
      ```
      
      Light-theme check: tinted tags and cards derive from the accent, so after building,
      screenshot a slide of each pattern you used and confirm tag text stays readable. If one
      is too faint, darken that role token (not the tint percentages).
      
      ## Ledger Dark
      
      The default's night-time counterpart: the same warm family, with espresso surfaces in
      place of paper and a soft gold accent where the light theme uses deep olive. Warm all the
      way through, including the code block.
      
      ```css
      :root{
        /* surfaces */
        --bg:#1b1814; --bg2:#241f19; --card:#2b251e; --code-bg:#141110;
        --stage-1:#312a21; --stage-2:#14110e;
        --section-1:#282219; --section-2:#17130f;
        /* text */
        --fg:#f7f3ea; --text:#e7dfd0; --strong:#ffffff; --muted:#a4977e;
        /* roles */
        --accent:#c3a94a; --accent2:#9d8636; --warn:#c96a4f; --bad:#d1735c;
        /* code block */
        --code-fg:#e8dfc0; --code-comment:#8b7f68; --code-keyword:#e0c274; --code-string:#c9b98a;
        /* title gradient: white to accent */
        --grad-1:#ffffff; --grad-2:#e0c274;
        /* misc */
        --card-border:color-mix(in srgb, var(--muted) 22%, transparent);
        --img-bg:#ffffff; --shadow:rgba(0,0,0,.55); --radius:14px;
        --font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
        --mono: "SFMono-Regular", Consolas, Menlo, "Liberation Mono", monospace;
      }
      ```
      
      ## Midnight
      
      ```css
      :root{
        --bg:#12161f; --bg2:#1a2130; --card:#1e2636; --code-bg:#0d111a;
        --stage-1:#233047; --stage-2:#0d1017;
        --section-1:#1f2940; --section-2:#10141d;
        --fg:#f2f5fa; --text:#d8dfeb; --strong:#ffffff; --muted:#8b98b0;
        --accent:#5aa9e6; --accent2:#4a90c2; --warn:#e6a15a; --bad:#e05c6e;
        --code-fg:#a8c7e8; --code-comment:#5d6b84; --code-keyword:#7ab8f0; --code-string:#9fd0a8;
        --grad-1:#ffffff; --grad-2:#a8d4f5;
        --card-border:color-mix(in srgb, var(--muted) 18%, transparent);
        --img-bg:#ffffff; --shadow:rgba(0,0,0,.55); --radius:14px;
        --font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
        --mono: "SFMono-Regular", Consolas, Menlo, "Liberation Mono", monospace;
      }
      ```
      
      ## Graphite
      
      ```css
      :root{
        --bg:#161616; --bg2:#1f1f1f; --card:#242424; --code-bg:#101010;
        --stage-1:#2c2c2c; --stage-2:#0e0e0e;
        --section-1:#262626; --section-2:#121212;
        --fg:#fafafa; --text:#e4e4e4; --strong:#ffffff; --muted:#9a9a9a;
        --accent:#e8c547; --accent2:#c2a63c; --warn:#e07b39; --bad:#e05c5c;
        --code-fg:#e8dfc0; --code-comment:#7d7d6a; --code-keyword:#f0d47a; --code-string:#d8c68f;
        --grad-1:#ffffff; --grad-2:#f0dc8f;
        --card-border:color-mix(in srgb, var(--muted) 18%, transparent);
        --img-bg:#ffffff; --shadow:rgba(0,0,0,.55); --radius:14px;
        --font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
        --mono: "SFMono-Regular", Consolas, Menlo, "Liberation Mono", monospace;
      }
      ```
      
      ## Font options
      
      Swap `--font` (and `--mono` if asked). All stacks are system fonts: no webfont downloads,
      decks stay self-contained and identical offline.
      
      | Choice | `--font` value |
      |---|---|
      | Modern sans (default) | `-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif` |
      | Humanist | `"Gill Sans", "Segoe UI", Verdana, sans-serif` |
      | Editorial serif | `"Iowan Old Style", "Palatino Linotype", Georgia, serif` |
      | Mono-flavoured | keep default `--font`, and set headings only: add `h1,h2,.kicker{font-family:var(--mono);letter-spacing:0;}` |
      
      If the user picks a serif or mono-flavoured look, re-run the visual pass on text-heavy
      slides: metrics differ and previously-fitting lines may wrap.
      
      ## Mapping a brand onto the tokens
      
      Extract real values (style-guide.md § Theming), then fill roles in this order:
      `--accent` (the brand's primary), `--bg/--bg2/--card` (its dark surface family, or a
      neutral dark if the brand is light-only and the user wants dark), `--warn/--bad` (its
      warning/danger colors if defined, else keep the preset's), text tokens by contrast against
      your chosen surfaces. Leave the `color-mix` tints alone: they follow the roles
      automatically.
      
    • verification.md 6 KB
      # Verification & PDF export
      
      ## Setup: headless Chrome + a staging directory
      
      Headless Chrome is the workhorse for this whole skill: it's how you look at a slide before
      telling the user it's done, and it's how you produce a PDF at the end.
      
      **Chrome discovery is platform-dependent.** The Playwright browser cache lives at
      `~/Library/Caches/ms-playwright` on macOS and `~/.cache/ms-playwright` on Linux, and the
      binary path inside a `chromium-*` bundle differs per platform, so discover it by searching
      for the binary itself rather than hardcoding a subpath:
      
      ```bash
      find_chrome() {
        local base c
        for base in "$HOME/Library/Caches/ms-playwright" "$HOME/.cache/ms-playwright"; do
          [ -d "$base" ] || continue
          c=$(find "$base" -maxdepth 6 -type f \
                \( -name chrome -o -name Chromium -o -name "Google Chrome for Testing" \) \
                2>/dev/null | grep -v headless_shell | sort -V | tail -1)
          [ -n "$c" ] && { printf '%s\n' "$c"; return; }
        done
        # System installs as a last resort
        for c in "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" \
                 "$(command -v google-chrome || true)" "$(command -v chromium || true)"; do
          [ -x "$c" ] && { printf '%s\n' "$c"; return; }
        done
      }
      CHROME=$(find_chrome)
      [ -x "$CHROME" ] || { npx --yes playwright install chromium; CHROME=$(find_chrome); }
      "$CHROME" --version   # sanity-check before the loop
      ```
      
      (Windows is untested; adapt the cache path, `%LOCALAPPDATA%\ms-playwright`, and binary
      name if you're there.) Quote `"$CHROME"` everywhere: the macOS binary path contains spaces.
      
      **Keep the Chrome sandbox on.** The commands below deliberately omit `--no-sandbox`. You
      are rendering a page built from repository content, so the sandbox is the layer that
      contains a malicious payload if one ever reaches a slide. Add `--no-sandbox` only if
      Chrome refuses to start as root inside a container that cannot grant user namespaces, and
      say so when you do; the better fix is to run as a non-root user.
      
      **Staging directory: always fresh, always per-deck.** Never use fixed shared paths like
      `/tmp/slide-01.png`: parallel deck builds (or a second deck in the same session) collide
      on them. If your environment provides a session scratchpad directory, create a
      subdirectory of it per deck; otherwise:
      
      ```bash
      STAGE=$(mktemp -d -t slides)
      DECK="deck.html"   # your deck's actual filename; the loops below use it
      ```
      
      All commands below write into `$STAGE`. Delete the whole directory when the deck ships.
      
      ## Visual verification (do this for every slide, every edit: see SKILL.md Step 4)
      
      Screenshot a single slide (fast, for spot-checking one edit; the URL hash is 1-indexed,
      `#7` is display slide 7, i.e. the section with comment `<!-- 6: ... -->`):
      
      ```bash
      "$CHROME" --headless=new --disable-gpu --hide-scrollbars \
        --window-size=1280,720 --screenshot="$STAGE/slide.png" "file://$(pwd)/$DECK#7"
      ```
      
      Screenshot every slide (do this at least once before calling a deck "done", and again after
      any batch of structural edits: insertions/deletions/reflows, not just wording tweaks).
      Each cold Chrome launch takes ~2-3 s; batching a few in parallel keeps a 25-slide deck
      under ~30 s:
      
      ```bash
      N=$(grep -c '<section class="slide' "$DECK")
      for i in $(seq 1 $N); do
        "$CHROME" --headless=new --disable-gpu --hide-scrollbars \
          --window-size=1280,720 --screenshot="$STAGE/slide-$(printf "%02d" $i).png" \
          "file://$(pwd)/$DECK#${i}" &
        [ $((i % 4)) -eq 0 ] && wait
      done; wait
      ```
      
      Then actually view each PNG. Things this catches in practice, every time it's run:
      
      - Text or an image overlapping the bottom nav pill (fix: tighten vertical spacing, reduce a
        `margin-top`, or shorten the offending copy).
      - A grid of cards stretching to fill leftover vertical space. The template's `.cols-2` /
        `.cols-3` default to content height; if you added the `fill` class (or a `flex:1`
        inline style) and the content doesn't need the room, remove it, or pin the stretching
        children with `flex:0 0 auto`.
      - A relative image path (`../img/foo.png`) that resolves fine when the deck lives in its
        original folder but breaks the moment you copy the HTML somewhere else to screenshot it
        (see "PDF export" below: this is the single most common failure mode when generating a
        PDF export copy).
      - Stale slide-number comments after an insertion/deletion: re-derive with
        `grep -n '<!-- [0-9]*:' "$DECK"` and renumber whenever the count changes.
      
      ## Redaction scan (before you hand anything over)
      
      Run this on the finished artifacts, not on the source, and read every hit:
      
      ```bash
      grep -nEi "api[_-]?key|secret|token|password|passwd|BEGIN [A-Z ]*PRIVATE KEY|\bAKIA[0-9A-Z]{16}\b|xox[baprs]-|ghp_[A-Za-z0-9]{20,}|[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}|\b(10|127|172|192)\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\b" "$DECK"
      ```
      
      The regex is a net, not a verdict: a hit inside the word "tokenized" is fine, a hit on a
      real key is a stop-everything. Also check by eye, because these do not grep:
      
      - **Every embedded image**, including ones you generated. Look at window titles, file
        trees, branch names, ticket ids, notification banners, and neighbouring tabs.
      - **Speaker notes** (`<aside class="notes">`): they ship inside the HTML even though they
        are hidden, so anyone who opens the file can read them.
      - **HTML comments**, including the slide-index comments, for anything pasted while drafting.
      - **The PDF**, separately from the HTML: it is a different artifact, and text you cropped
        out of a slide can still sit in the page it was rendered from.
      
      If the deck is going outside the organisation, have the user confirm the artifact before
      it is sent. You cannot un-share a deck.
      
      ## PDF export
      
      PDF export lives in the companion **slides-to-pdf** skill (self-contained; distributed
      alongside this one). In short: build an export copy with absolute image paths and hidden
      nav chrome, screenshot every slide at 2x, print a page-per-slide wrapper to PDF, and
      verify by rendering the PDF back to images with pypdfium2, since headless Chrome cannot
      rasterize a local PDF and image pages can be silently blank. Use that skill's recipe as
      written; do not improvise a shortcut here.
      
  • scripts
    • check.py 21 KB
      #!/usr/bin/env python3
      """slideops check: report which slides cite code that has changed since the deck was built.
      
      A deck rots the moment it is exported. This turns that from a hope into a check: every
      quoted snippet carries the file and line range it came from, plus a hash of those source
      lines at build time. This re-reads the repository at its current state and tells you,
      slide by slide, what is still true.
      
          python3 check.py <deck.html|dir> [more...] [--repo PATH] [--suggest] [--json]
                           [--quiet] [--exit-zero]
      
      Deterministic and dependency-free: standard library only, no model, no network, no tokens.
      Running it costs milliseconds, so run it often. Spending tokens is a separate, later
      decision: `--json` hands an agent the complete repair brief (what changed, the diff, the
      commits that did it, the corrected citation) so a refresh reads only the slides that
      actually drifted.
      
      Statuses:
          CURRENT       the cited lines are byte-identical to what they were
          MOVED         the same lines still exist, at different line numbers (update data-src)
          CHANGED       the cited lines differ; the slide may now be wrong
          MISSING       the file is gone
          UNVERIFIED    the citation has no hash, or the build commit is not in this repo
      
      Exit codes: 0 nothing stale, 1 stale citations found, 2 a deck or the repo could not be
      read. `--exit-zero` forces 0 whenever the run completed, for report-only CI jobs that
      should annotate a pull request without failing it.
      """
      
      from __future__ import annotations
      
      import argparse
      import difflib
      import hashlib
      import html
      import json
      import re
      import subprocess
      import sys
      from collections.abc import Sequence
      from dataclasses import dataclass, field
      from pathlib import Path
      
      CITATION_RE = re.compile(
          r'<[a-zA-Z][^<>]*?data-src="(?P<src>[^"]+)"'
          r'(?:[^<>]*?data-sha256="(?P<sha>[0-9a-f]{6,64})")?[^<>]*>',
          re.I | re.S,
      )
      MD_CITATION_RE = re.compile(
          r'<!--\s*slideops\s+data-src="(?P<src>[^"]+)"(?:\s+data-sha256="(?P<sha>[0-9a-f]{6,64})")?\s*-->',
          re.I,
      )
      BUILD_META_RE = re.compile(r'<meta name="slideops-build" content="(?P<content>[^"]*)"', re.I)
      MD_BUILD_META_RE = re.compile(r"<!--\s*slideops-build\s+(?P<content>[^>]*?)\s*-->", re.I)
      SLIDE_COMMENT_RE = re.compile(r"<!--\s*(?P<index>\d+):\s*(?P<label>.+?)\s*-->")
      MD_HEADING_RE = re.compile(r"^#{1,6}\s+(?P<text>.+?)\s*$", re.M)
      MD_SUFFIXES = {".md", ".markdown"}
      HASH_LENGTH = 12
      MAX_COMMITS = 10
      
      
      @dataclass
      class Citation:
          src: str
          path: str
          start: int | None
          end: int | None
          recorded_sha: str | None
          slide_index: int | None
          slide_label: str
          status: str = "UNVERIFIED"
          detail: str = ""
          new_range: tuple[int, int] | None = None
          diff: list[str] = field(default_factory=list)
          current_lines: list[str] = field(default_factory=list)
          commits: list[str] = field(default_factory=list)
      
          @property
          def display_slide(self) -> str:
              return str(self.slide_index + 1) if self.slide_index is not None else "?"
      
          @property
          def is_stale(self) -> bool:
              return self.status in {"CHANGED", "MISSING", "MOVED"}
      
          @property
          def suggested_src(self) -> str:
              if self.new_range:
                  return f"{self.path}:{self.new_range[0]}-{self.new_range[1]}"
              return self.src
      
      
      @dataclass
      class DeckReport:
          """One deck's worth of results, so a run can span a whole docs/slides/ folder."""
      
          deck: Path
          kind: str = "deck"
          build: dict[str, str] = field(default_factory=dict)
          citations: list[Citation] = field(default_factory=list)
          error: str = ""
      
          @property
          def noun(self) -> str:
              """What a citation's location is called in this document: a slide or a section."""
              return "section" if self.kind == "doc" else "slide"
      
          @property
          def stale(self) -> list[Citation]:
              return [c for c in self.citations if c.is_stale]
      
          @property
          def unverified(self) -> list[Citation]:
              return [c for c in self.citations if c.status == "UNVERIFIED"]
      
      
      def hash_lines(lines: Sequence[str]) -> str:
          return hashlib.sha256("\n".join(lines).encode("utf-8")).hexdigest()[:HASH_LENGTH]
      
      
      def parse_src(src: str) -> tuple[str, int | None, int | None]:
          """'path/to/file.py:40-58' -> ('path/to/file.py', 40, 58). Range is optional."""
          match = re.match(r"^(?P<path>.+?)(?::(?P<start>\d+)(?:-(?P<end>\d+))?)?$", src.strip())
          if not match:
              return src.strip(), None, None
          path = match.group("path")
          start = int(match.group("start")) if match.group("start") else None
          end = int(match.group("end")) if match.group("end") else start
          return path, start, end
      
      
      def slice_lines(text: str, start: int | None, end: int | None) -> list[str] | None:
          lines = text.splitlines()
          if start is None:
              return lines
          if start < 1 or end is None or end > len(lines) or start > end:
              return None
          return lines[start - 1 : end]
      
      
      def git(repo: Path, *args: str) -> str | None:
          result = subprocess.run(["git", "-C", str(repo), *args], capture_output=True, text=True, timeout=30)
          return result.stdout if result.returncode == 0 else None
      
      
      def is_markdown(path: Path) -> bool:
          return path.suffix.lower() in MD_SUFFIXES
      
      
      def mask_fences(text: str) -> str:
          """Blank fenced code blocks (delimiters included), keeping every offset and newline.
      
          A doc that *teaches* the citation syntax quotes it inside a fence; masking first is
          what keeps those examples from being counted as citations, headings, or build stamps.
          """
      
          def blank(line: str) -> str:
              body, newline = (line[:-1], "\n") if line.endswith("\n") else (line, "")
              return " " * len(body) + newline
      
          out: list[str] = []
          fence = ""
          for line in text.splitlines(keepends=True):
              stripped = line.lstrip()
              marker = ""
              if stripped[:3] in ("```", "~~~"):
                  marker = stripped[0] * (len(stripped) - len(stripped.lstrip(stripped[0])))
              if fence:
                  if marker and marker[0] == fence[0] and len(marker) >= len(fence):
                      fence = ""
                  out.append(blank(line))
              elif marker:
                  fence = marker
                  out.append(blank(line))
              else:
                  out.append(line)
          return "".join(out)
      
      
      def _attach(anchors: list[tuple[int, int, str]], position: int) -> tuple[int | None, str]:
          """The nearest preceding anchor (slide comment or heading) owns the citation."""
          index, label = None, ""
          for anchor_position, anchor_index, anchor_label in anchors:
              if anchor_position <= position:
                  index, label = anchor_index, anchor_label
              else:
                  break
          return index, label
      
      
      def _citation(match: re.Match[str], anchors: list[tuple[int, int, str]]) -> Citation:
          index, label = _attach(anchors, match.start())
          path, start, end = parse_src(match.group("src"))
          return Citation(
              src=match.group("src"),
              path=path,
              start=start,
              end=end,
              recorded_sha=(match.group("sha") or "").lower() or None,
              slide_index=index,
              slide_label=label,
          )
      
      
      def find_citations(deck_html: str) -> list[Citation]:
          """Attach each citation to the slide it sits on, using the slide-index comments."""
          slides: list[tuple[int, int, str]] = [
              (m.start(), int(m.group("index")), m.group("label").strip()) for m in SLIDE_COMMENT_RE.finditer(deck_html)
          ]
          return [_citation(match, slides) for match in CITATION_RE.finditer(deck_html)]
      
      
      def find_citations_md(doc_text: str) -> list[Citation]:
          """Attach each citation comment to the nearest preceding Markdown heading."""
          masked = mask_fences(doc_text)
          headings: list[tuple[int, int, str]] = [
              (m.start(), index, m.group("text").strip()) for index, m in enumerate(MD_HEADING_RE.finditer(masked))
          ]
          return [_citation(match, headings) for match in MD_CITATION_RE.finditer(masked)]
      
      
      def locate_moved(current_text: str, original: list[str]) -> tuple[int, int] | None:
          """If the cited block still exists verbatim elsewhere in the file, find where."""
          current = current_text.splitlines()
          if not original or len(original) > len(current):
              return None
          first = original[0]
          for offset, line in enumerate(current):
              if line == first and current[offset : offset + len(original)] == original:
                  return offset + 1, offset + len(original)
          return None
      
      
      def commits_since(repo: Path, path: str, build_commit: str | None) -> list[str]:
          """The commits that touched this file since the deck was built, newest first.
      
          This is the 'why' a diff cannot give you: a rename reads very differently from a
          behaviour change, and the subject line is usually enough to tell them apart before
          anyone reads a single line of code.
          """
          if not build_commit:
              return []
          log = git(repo, "log", "--oneline", f"-{MAX_COMMITS}", f"{build_commit}..HEAD", "--", path)
          if not log:
              return []
          return [line.strip() for line in log.splitlines() if line.strip()]
      
      
      def check_citation(citation: Citation, repo: Path, build_commit: str | None) -> None:
          file_path = repo / citation.path
          if not file_path.is_file():
              citation.status = "MISSING"
              citation.detail = "file no longer exists"
              citation.commits = commits_since(repo, citation.path, build_commit)
              return
      
          current_text = file_path.read_text(errors="replace")
          current = slice_lines(current_text, citation.start, citation.end)
          if current is None:
              citation.status = "CHANGED"
              citation.detail = f"file is now {len(current_text.splitlines())} lines; cited range is past the end"
              citation.commits = commits_since(repo, citation.path, build_commit)
              return
      
          citation.current_lines = current
          if not citation.recorded_sha:
              citation.status = "UNVERIFIED"
              citation.detail = "no data-sha256 recorded at build time"
              return
      
          if hash_lines(current).startswith(citation.recorded_sha[:HASH_LENGTH]):
              citation.status = "CURRENT"
              return
      
          original: list[str] | None = None
          if build_commit:
              blob = git(repo, "show", f"{build_commit}:{citation.path}")
              if blob is not None:
                  original = slice_lines(blob, citation.start, citation.end)
      
          citation.commits = commits_since(repo, citation.path, build_commit)
      
          if original is not None:
              moved_to = locate_moved(current_text, original)
              if moved_to:
                  citation.status = "MOVED"
                  citation.new_range = moved_to
                  citation.current_lines = original
                  citation.detail = f"same content, now at lines {moved_to[0]}-{moved_to[1]}"
                  return
              citation.status = "CHANGED"
              citation.diff = list(
                  difflib.unified_diff(
                      original, current, fromfile=f"{citation.src} @ build", tofile=f"{citation.src} @ now", lineterm="", n=1
                  )
              )
              changed = sum(
                  1 for line in citation.diff if line.startswith(("+", "-")) and not line.startswith(("+++", "---"))
              )
              citation.detail = f"{changed} line(s) differ"
          else:
              citation.status = "CHANGED"
              citation.detail = "content differs (build commit unavailable, cannot diff)"
      
      
      def parse_build_meta(deck_html: str, *, markdown: bool = False) -> dict[str, str]:
          build: dict[str, str] = {}
          meta = MD_BUILD_META_RE.search(mask_fences(deck_html)) if markdown else BUILD_META_RE.search(deck_html)
          if meta:
              for part in meta.group("content").split():
                  key, _, value = part.partition("=")
                  if value:
                      build[key] = value
          return build
      
      
      def check_deck(deck: Path, repo: Path) -> DeckReport:
          markdown = is_markdown(deck)
          report = DeckReport(deck=deck, kind="doc" if markdown else "deck")
          try:
              deck_html = deck.read_text(errors="replace")
          except OSError as exc:
              report.error = f"could not read: {exc}"
              return report
      
          report.build = parse_build_meta(deck_html, markdown=markdown)
          build_commit = report.build.get("commit")
          if build_commit and git(repo, "cat-file", "-e", f"{build_commit}^{{commit}}") is None:
              build_commit = None
      
          report.citations = find_citations_md(deck_html) if markdown else find_citations(deck_html)
          for citation in report.citations:
              check_citation(citation, repo, build_commit)
          return report
      
      
      def looks_like_deck(path: Path) -> bool:
          """Used only when expanding a directory, so pointing at docs/ does not flood output."""
          try:
              head = path.read_text(errors="replace")
          except OSError:
              return False
          if is_markdown(path):
              masked = mask_fences(head)
              return bool(MD_BUILD_META_RE.search(masked) or MD_CITATION_RE.search(masked))
          return bool(BUILD_META_RE.search(head) or CITATION_RE.search(head))
      
      
      def collect_decks(targets: list[Path]) -> tuple[list[Path], list[str]]:
          """Expand files and directories into a deduplicated, sorted list of decks."""
          decks: list[Path] = []
          problems: list[str] = []
          for target in targets:
              if target.is_dir():
                  candidates = sorted([*target.rglob("*.html"), *target.rglob("*.md"), *target.rglob("*.markdown")])
                  found = [p for p in candidates if looks_like_deck(p)]
                  if not found:
                      problems.append(f"{target}: no decks with citations found")
                  decks.extend(found)
              elif target.is_file():
                  decks.append(target)
              else:
                  problems.append(f"{target}: not found")
      
          seen: set[Path] = set()
          unique: list[Path] = []
          for deck in decks:
              resolved = deck.resolve()
              if resolved not in seen:
                  seen.add(resolved)
                  unique.append(deck)
          return unique, problems
      
      
      def print_deck(report: DeckReport, repo: Path, quiet: bool, multi: bool) -> None:
          shown = report.stale if quiet else report.citations
          if quiet and not shown:
              return
      
          if multi:
              print(f"{report.deck}")
          else:
              print(f"Deck: {report.deck.name}")
              if report.build:
                  print("Built: " + " ".join(f"{k}={v}" for k, v in report.build.items()))
              head = (git(repo, "rev-parse", "--short", "HEAD") or "unknown").strip()
              print(f"Repo: {repo} @ {head}")
              print()
      
          if report.error:
              print(f"  {report.error}")
              return
      
          if not report.citations:
              print("  No citations found in this deck.")
              if not multi:
                  print("  Add data-src (and data-sha256) to quoted snippets so freshness can be checked;")
                  print("  see references/freshness.md.")
              return
      
          width = max(len(c.src) for c in shown)
          for citation in shown:
              line = (
                  f"  {report.noun} {citation.display_slide:>3}  {citation.slide_label:<14} "
                  f"{citation.src:<{width}}  {citation.status:<10} {citation.detail}"
              )
              print(line.rstrip())
          if multi:
              print()
      
      
      def print_suggestions(report: DeckReport, multi: bool) -> None:
          for citation in report.stale:
              print()
              print("=" * 78)
              where = f"{report.deck}: " if multi else ""
              headline = f"{report.noun} {citation.display_slide} ({citation.slide_label})"
              print(f"{where}{headline} — {citation.src} — {citation.status}")
              print("=" * 78)
              if citation.commits:
                  print("Commits that touched it since the deck was built:")
                  for commit in citation.commits:
                      print(f"  {commit}")
                  print()
              if citation.status == "MISSING":
                  print("The file is gone. Either the slide is obsolete, or the code moved:")
                  print(f"  git log --diff-filter=D -- {citation.path}")
                  continue
              if citation.status == "MOVED" and citation.new_range:
                  start, end = citation.new_range
                  print("Content is unchanged; only the line numbers moved. Update the citation:")
                  print(f'  data-src="{citation.path}:{start}-{end}"')
                  print(f'  data-sha256="{hash_lines(citation.current_lines)}"')
                  continue
              if citation.diff:
                  print("What changed:")
                  for line in citation.diff:
                      print(f"  {line}")
                  print()
              print("Current source, HTML-escaped and ready to paste into the <pre>:")
              print(f'  data-src="{citation.src}" data-sha256="{hash_lines(citation.current_lines)}"')
              print()
              for line in citation.current_lines:
                  print(f"  {html.escape(line)}")
              longest = max((len(line) for line in citation.current_lines), default=0)
              if longest > 65:
                  print()
                  print(
                      f"  Note: longest line is {longest} chars. Half-width columns fit ~65 and"
                      " full-width ~95, so this may need a full-width pre or a different pattern."
                  )
      
      
      def citation_json(citation: Citation) -> dict[str, object]:
          """Stale citations carry the full repair brief; current ones stay one line each.
      
          An agent asked to refresh a deck should need this payload and nothing else: what
          drifted, how, who changed it, and the exact attributes to write back.
          """
          payload: dict[str, object] = {
              "slide": citation.display_slide,
              "label": citation.slide_label,
              "src": citation.src,
              "status": citation.status,
              "detail": citation.detail,
          }
          if not citation.is_stale:
              return payload
          payload.update(
              {
                  "suggested_src": citation.suggested_src,
                  "suggested_sha256": hash_lines(citation.current_lines) if citation.current_lines else None,
                  "new_range": list(citation.new_range) if citation.new_range else None,
                  "diff": citation.diff,
                  "commits": citation.commits,
                  "current_source": citation.current_lines,
              }
          )
          return payload
      
      
      def emit_json(reports: list[DeckReport], repo: Path) -> None:
          head = (git(repo, "rev-parse", "--short", "HEAD") or "unknown").strip()
          print(
              json.dumps(
                  {
                      "repo": str(repo),
                      "head": head,
                      "checked": len(reports),
                      "stale": sum(len(r.stale) for r in reports),
                      "unverified": sum(len(r.unverified) for r in reports),
                      "decks": [
                          {
                              "deck": str(r.deck),
                              "kind": r.kind,
                              "build": r.build,
                              "error": r.error or None,
                              "stale": len(r.stale),
                              "citations": [citation_json(c) for c in r.citations],
                          }
                          for r in reports
                      ],
                  },
                  indent=2,
              )
          )
      
      
      def main(argv: list[str]) -> int:
          parser = argparse.ArgumentParser(description="Report slides whose cited code has changed.")
          parser.add_argument("targets", type=Path, nargs="+", help="deck HTML files, or directories to sweep")
          parser.add_argument("--repo", type=Path, default=Path(), help="repository the decks cite (default: cwd)")
          parser.add_argument("--suggest", action="store_true", help="print current source and updated citations")
          parser.add_argument("--json", action="store_true", help="machine-readable repair brief, for CI or an agent")
          parser.add_argument("--quiet", action="store_true", help="list only stale citations; silent when all are current")
          parser.add_argument("--exit-zero", action="store_true", help="always exit 0 when the run completed (report-only)")
          args = parser.parse_args(argv[1:])
      
          repo: Path = args.repo.resolve()
          if not repo.is_dir():
              print(f"Repo not found: {repo}", file=sys.stderr)
              return 2
      
          decks, problems = collect_decks(args.targets)
          for problem in problems:
              print(problem, file=sys.stderr)
          if not decks:
              return 2
      
          reports = [check_deck(deck, repo) for deck in decks]
          multi = len(reports) > 1
      
          if args.json:
              emit_json(reports, repo)
          else:
              for report in reports:
                  print_deck(report, repo, args.quiet, multi)
      
              total = sum(len(r.citations) for r in reports)
              stale = sum(len(r.stale) for r in reports)
              unverified = sum(len(r.unverified) for r in reports)
              if not (args.quiet and not stale):
                  if not multi:
                      print()
                  deck_count = f" across {len(reports)} decks" if multi else ""
                  print(
                      f"{total - stale - unverified} current, {stale} stale"
                      + (f", {unverified} unverified" if unverified else "")
                      + f", {total} cited in total{deck_count}."
                  )
                  if stale and not args.suggest:
                      print("Run again with --suggest for the current source and a ready-to-paste snippet,")
                      print("or --json to hand an agent the whole repair brief.")
      
              if args.suggest:
                  for report in reports:
                      print_suggestions(report, multi)
      
          if any(r.error for r in reports):
              return 2
          if args.exit_zero:
              return 0
          return 1 if any(r.stale for r in reports) else 0
      
      
      if __name__ == "__main__":
          sys.exit(main(sys.argv))
      
    • cite.py 7.7 KB
      #!/usr/bin/env python3
      """slideops cite: produce the citation attributes for a snippet, and stamp the build commit.
      
      The companion to check.py. `check` tells you what drifted; `cite` is what you use while
      building or repairing a slide, so the two attributes that make a snippet checkable are
      never typed by hand:
      
          python3 cite.py app/main.py:40-58 [more...] [--repo PATH] [--snippet] [--md]
          python3 cite.py --stamp docs/slides/deck.html [--repo PATH]
      
      With --md the citation prints as the comment form used in Markdown docs, --snippet prints
      a fenced block instead of escaped HTML, and --stamp on a .md file writes the comment stamp
      onto line 1.
      
      A hand-computed hash is worse than no hash: it reports UNVERIFIED or CHANGED months later
      and nobody can tell whether the code moved or the build was sloppy. Same for the build
      date, which an agent will otherwise cheerfully invent.
      
      Exit codes: 0 fine, 1 a reference could not be resolved, 2 the repo or deck is unreadable.
      """
      
      from __future__ import annotations
      
      import argparse
      import datetime
      import hashlib
      import html
      import re
      import subprocess
      import sys
      from pathlib import Path
      
      HASH_LENGTH = 12
      BUILD_META_RE = re.compile(r'<meta name="slideops-build" content="[^"]*">', re.I)
      MD_BUILD_META_RE = re.compile(r"\A<!--\s*slideops-build[^>]*-->[ \t]*\n?", re.I)
      HEAD_RE = re.compile(r"<head[^>]*>", re.I)
      HALF_WIDTH, FULL_WIDTH = 65, 95
      MD_SUFFIXES = {".md", ".markdown"}
      FENCE_LANGS = {
          ".py": "python",
          ".js": "javascript",
          ".ts": "typescript",
          ".tsx": "tsx",
          ".sh": "bash",
          ".bash": "bash",
          ".rb": "ruby",
          ".go": "go",
          ".rs": "rust",
          ".java": "java",
          ".c": "c",
          ".h": "c",
          ".cpp": "cpp",
          ".yml": "yaml",
          ".yaml": "yaml",
          ".json": "json",
          ".toml": "toml",
          ".html": "html",
          ".css": "css",
          ".sql": "sql",
          ".md": "markdown",
          ".markdown": "markdown",
      }
      
      
      def git(repo: Path, *args: str) -> str | None:
          result = subprocess.run(["git", "-C", str(repo), *args], capture_output=True, text=True, timeout=30)
          return result.stdout.strip() if result.returncode == 0 else None
      
      
      def parse_ref(ref: str) -> tuple[str, int | None, int | None]:
          """'app/main.py:40-58' -> ('app/main.py', 40, 58). Range is optional."""
          match = re.match(r"^(?P<path>.+?)(?::(?P<start>\d+)(?:-(?P<end>\d+))?)?$", ref.strip())
          if not match:
              return ref.strip(), None, None
          start = int(match.group("start")) if match.group("start") else None
          end = int(match.group("end")) if match.group("end") else start
          return match.group("path"), start, end
      
      
      def fence_for(lines: list[str]) -> str:
          """A backtick fence one longer than any backtick run in the content, minimum three."""
          longest_run = max((len(m) for line in lines for m in re.findall(r"`+", line)), default=0)
          return "`" * max(3, longest_run + 1)
      
      
      def cite_one(ref: str, repo: Path, show_snippet: bool, markdown: bool = False) -> bool:
          path, start, end = parse_ref(ref)
          file_path = repo / path
          if not file_path.is_file():
              print(f"{ref}: no such file under {repo}", file=sys.stderr)
              return False
      
          lines = file_path.read_text(errors="replace").splitlines()
          if start is None:
              selected, src = lines, path
          else:
              if end is None or start < 1 or end > len(lines) or start > end:
                  print(f"{ref}: file has {len(lines)} lines; that range does not exist", file=sys.stderr)
                  return False
              selected, src = lines[start - 1 : end], f"{path}:{start}-{end}"
      
          digest = hashlib.sha256("\n".join(selected).encode("utf-8")).hexdigest()[:HASH_LENGTH]
          attributes = f'data-src="{src}" data-sha256="{digest}"'
          print(f"<!-- slideops {attributes} -->" if markdown else attributes)
      
          if not markdown:
              # The width budgets are slide-pattern constraints; fenced Markdown scrolls instead.
              longest = max((len(line) for line in selected), default=0)
              if longest > FULL_WIDTH:
                  print(
                      f"  warning: longest line is {longest} chars; over the ~{FULL_WIDTH} full-width budget",
                      file=sys.stderr,
                  )
              elif longest > HALF_WIDTH:
                  print(f"  note: longest line is {longest} chars; needs a full-width pre (~{FULL_WIDTH})", file=sys.stderr)
      
          if show_snippet:
              if markdown:
                  fence = fence_for(selected)
                  print(fence + FENCE_LANGS.get(file_path.suffix.lower(), ""))
                  for line in selected:
                      print(line)
                  print(fence)
              else:
                  print()
                  for line in selected:
                      print(html.escape(line))
          return True
      
      
      def stamp(deck: Path, repo: Path) -> int:
          """Write the build provenance meta, which is what lets check.py diff against the past."""
          if not deck.is_file():
              print(f"Deck not found: {deck}", file=sys.stderr)
              return 2
      
          commit = git(repo, "rev-parse", "--short", "HEAD")
          if not commit:
              print(f"Not a git repository (or no commits): {repo}", file=sys.stderr)
              return 2
          if git(repo, "status", "--porcelain"):
              print("  note: the repo has uncommitted changes; check.py can only diff against the commit", file=sys.stderr)
      
          name = (git(repo, "rev-parse", "--show-toplevel") or str(repo)).rsplit("/", 1)[-1]
          today = datetime.date.today().isoformat()
          payload = f"commit={commit} date={today} repo={name}"
      
          text = deck.read_text()
          if deck.suffix.lower() in MD_SUFFIXES:
              # The stamp owns line 1 of a Markdown doc; a stamp-shaped example deeper in the
              # file (say, inside a code fence) is content and must never be rewritten.
              meta = f"<!-- slideops-build {payload} -->"
              if MD_BUILD_META_RE.match(text):
                  text, action = MD_BUILD_META_RE.sub(meta + "\n", text, count=1), "updated"
              else:
                  text, action = meta + "\n" + text, "inserted"
              deck.write_text(text)
              print(f"{action}: {meta}")
              return 0
      
          meta = f'<meta name="slideops-build" content="{payload}">'
          if BUILD_META_RE.search(text):
              text = BUILD_META_RE.sub(meta, text, count=1)
              action = "updated"
          else:
              head = HEAD_RE.search(text)
              if not head:
                  print(f"{deck}: no <head> to insert the build meta into", file=sys.stderr)
                  return 1
              text = text[: head.end()] + "\n" + meta + text[head.end() :]
              action = "inserted"
      
          deck.write_text(text)
          print(f"{action}: {meta}")
          return 0
      
      
      def main(argv: list[str]) -> int:
          parser = argparse.ArgumentParser(description="Print citation attributes, or stamp a deck's build commit.")
          parser.add_argument("refs", nargs="*", help="path:start-end references to cite")
          parser.add_argument("--repo", type=Path, default=Path(), help="repository the deck cites (default: cwd)")
          parser.add_argument("--snippet", action="store_true", help="also print the HTML-escaped source, ready to paste")
          parser.add_argument("--md", action="store_true", help="print Markdown citation comments (and fenced snippets)")
          parser.add_argument("--stamp", type=Path, metavar="DECK", help="write the build commit meta into this deck")
          args = parser.parse_args(argv[1:])
      
          repo: Path = args.repo.resolve()
          if not repo.is_dir():
              print(f"Repo not found: {repo}", file=sys.stderr)
              return 2
      
          if args.stamp:
              code = stamp(args.stamp, repo)
              if code or not args.refs:
                  return code
      
          if not args.refs:
              parser.print_usage(sys.stderr)
              print("Give at least one path:start-end reference, or --stamp DECK.", file=sys.stderr)
              return 2
      
          return 0 if all([cite_one(ref, repo, args.snippet, markdown=args.md) for ref in args.refs]) else 1
      
      
      if __name__ == "__main__":
          sys.exit(main(sys.argv))
      
  • SKILL.md 25.8 KB
    ---
    name: slideops
    description: 'Use when the user asks for slides, a slide deck, or a presentation about a code repository, one of its subsystems, a feature, an architecture area, or its recent changes. Triggers include "make slides", "build a slide deck", "create a presentation", "HTML slides for this repo", "overview deck", "team update slides", "slides for our latest changes", or naming a topic and asking for a deck about it. Also use when the user asks whether an existing deck still matches the code, or wants one rechecked, refreshed, or kept in sync automatically: "is this deck still accurate", "check the slides against the code", "did anything we documented change", "refresh the architecture deck", "these docs are stale", "fail the build when the deck stops matching the code", or asks to wire that check into CI, a pull request, or an agent hook. Also use for Markdown documentation instead of slides ("write markdown docs", "a design doc with citations", "docs that know when they go stale"), including PDF export of a doc.'
    license: MIT
    compatibility: Needs a headless Chrome or Chromium binary (Playwright cache or system install) and Python 3 for the verification pass. Reads the target repository with git. Network access only if you opt into Mermaid diagrams (one npx download) or brand-colour extraction; everything else works offline.
    metadata:
      author: Gleb Lukicov
      version: 1.1.2
    ---
    
    # SlideOps: generate a deck from a repository, and keep it true
    
    This skill has two jobs, and the second one is the point.
    
    **Build**: turn a repository into a deck whose every claim came from the code, not from a
    model's impression of the code. **Keep in sync**: make that deck able to prove, months
    later, whether it still matches the repository, cheaply enough that nobody has to
    remember to care.
    
    The mechanism joining them is a citation. Every quoted snippet records the file, the line
    range, and a hash of those source lines at build time, and the deck records the commit it
    was built from. That turns "is this deck still accurate?" from a question somebody has to
    answer by reading into a command that answers itself:
    
    ```bash
    python3 scripts/check.py docs/slides/ --repo .
    ```
    
    Standard library only. No model, no network, no tokens, milliseconds to run. Two scripts
    ship with this skill and do all of it: [`scripts/cite.py`](scripts/cite.py) writes the
    citations while you build, and [`scripts/check.py`](scripts/check.py) reads them back
    afterwards. Writing the citations as you go (Step 3) is therefore not optional decoration:
    it is the entire reason the deck can be maintained instead of rewritten. See
    [`references/freshness.md`](references/freshness.md) for the mechanism and
    [`references/automation.md`](references/automation.md) for wiring it into pull requests,
    scheduled refreshes, and agent hooks.
    
    ## Which job is this?
    
    - The user wants a **new deck**: start at Step 0 below.
    - The user wants **Markdown documentation instead of slides** (a doc that lives in the
      repo and renders on GitHub, with the same citations and the same freshness check):
      follow [`references/markdown.md`](references/markdown.md). It reuses this file's
      research, confidentiality, and refresh rules and swaps the build and verify steps;
      [`references/markdown-pdf.md`](references/markdown-pdf.md) covers exporting such a doc
      to PDF. Checking or refreshing an existing `.md` doc is the same `check.py` workflow
      as decks; sweeps pick both up together.
    - The user is asking whether an existing deck is **still accurate**, wants one
      **refreshed**, or wants the check **automated**: go straight to
      [Refreshing an existing deck](#refreshing-an-existing-deck) near the end. Do not rebuild
      a deck from scratch because a few slides drifted.
    
    ---
    
    Produces a single self-contained HTML slide deck (no build step, no CDN dependencies):
    one 1280×720 slide per screen, click/arrow-key/URL-hash navigation, a progress bar and
    slide counter, an Esc-toggled overview grid (deep-linkable as `#overview`), optional
    per-slide speaker notes (`<aside class="notes">`, toggled with N, invisible in exports),
    and, on request, a paginated PDF export. Every slide is grounded in the real repository:
    real code snippets, real commands, real screenshots, real numbers. Nothing is invented
    to fill space.
    
    A worked example ships with the skill: [`examples/skill-demo.html`](examples/skill-demo.html)
    is a 17-slide deck about this skill, built by this skill, showing the patterns, all four
    theme presets on real decks, and an inlined Mermaid diagram.
    
    Two starting points, chosen with the user up front:
    
    - **General overview**: what the project is, how to install/set it up, how to run it,
      its main features, its architecture at a glance. Mirrors a README/onboarding walkthrough.
    - **Focused topic**: one subsystem, one recent change, one architecture decision, one
      workflow. Goes deep rather than wide.
    
    ---
    
    ## Step 0: Orient, then ask
    
    Spend ~2 minutes scanning the repo **before asking anything**, so every question you ask
    is concrete and every option you offer actually exists: read the README and the
    manifest(s) that define the product (per-app in a monorepo), run `git log --oneline -15`
    and glance at the top-level tree and `docs/`, and check whether `docs/slides/` already has
    decks (open one: its title slide, theme and build stamp show the local conventions). Do
    not start slide research yet; this pass is only to ask good questions. The scan itself may hit stale doc pointers; note them
    and move on (Step 1's code-wins rule deals with drift later).
    
    What you find shapes the candidates:
    
    - **Existing decks remove or reframe candidates.** A topic an existing deck already covers
      is off the proposal list, unless meaningful commits have landed since that deck's date,
      in which case offer "update the existing X deck" as its own candidate. Disclose the
      existing decks in one line at the top of the intake.
    - **Skip meta noise in "why now".** Recent commits about tooling, docs, or slide decks
      themselves don't justify a deck; reach back to the most recent *product* activity, and
      don't re-justify a candidate with work an existing deck already presents.
    
    Then ask everything in **one compact intake** (use your environment's structured-question
    tool if it has one; otherwise a single message with lettered options). Mark the default
    in each list, skip any item the user already answered, and never exceed these six:
    
    1. **Topic.** Propose 3-4 concrete candidates you found in the scan, each with a one-line
       "why now" (e.g. "v2.3 shipped last week: a what's-new deck", "the `sync/` subsystem is
       the largest and undocumented: a deep dive", "no onboarding doc exists: a general
       overview"), plus "something else: tell me". Never ask a bare "what should the deck be
       about?": the scan is what makes this question answerable in one click. Mark as default
       the candidate you would genuinely bet the user wants given the repo's core domain and
       its deck history (a core subsystem with zero coverage usually beats a
       recently-changed-but-already-presented area), not mechanically the most recent change.
    2. **Audience and venue.** New joiners (onboarding) · team sprint/standup · stakeholder or
       exec review · conference/meetup. This drives jargon level, pacing, and the Sizing row.
    3. **Length.** Offer the Sizing table rows as time slots ("5-min lightning ≈ 8-15 slides",
       "20-min deep dive ≈ 12-20", "45-min onboarding ≈ 20-35").
    4. **Design.** Theme menu from [`references/themes.md`](references/themes.md): Ledger
       Light (default) · Ledger Dark · midnight · graphite · match a brand (ask for the URL/style
       guide; fetch it for real per [`references/style-guide.md`](references/style-guide.md)
       § Theming, never invent colors from a description). Optionally a font choice
       (themes.md § Font options) if the user signals caring about typography. If the user
       hesitates between themes or asks to see them, show rather than tell: copy the
       template, swap in each candidate preset's `:root` block, screenshot the title slide
       of each (the recipe in [`references/verification.md`](references/verification.md)),
       and present the images side by side before they choose.
    5. **Scope and sensitive data.** Confirm what the deck may draw on and who will see it:
       "internal team" and "conference talk" are different redaction bars. Name anything
       off-limits up front (unannounced features, customer names, internal hostnames). The
       defaults in "The confidentiality rule" below apply regardless of the answer.
    6. **Extras.** Three independent toggles, so don't letter them as alternatives: state the
       defaults and ask the user to object to any ("PDF export: no · Mermaid diagrams: built-in
       flow boxes only (Mermaid needs one-time `npx` network access, see
       [`references/diagrams.md`](references/diagrams.md)) · output:
       `<repo>/docs/slides/<topic-slug>-<date>.html`"). The deck (plus a PDF if asked for) is
       the only file this produces, so there is nothing else to agree on.
    
    For "what changed" style decks, also pin the **recency window** (a date range or "since
    the last release") and resolve it with `git log` before writing anything, not from memory.
    
    **The outline checkpoint.** After the first research pass, show the user a proposed
    **topic list and slide flow** (like a table of contents) and get a thumbs-up *before*
    writing any HTML: this is the single highest-leverage checkpoint. Building 25 slides
    around the wrong 6 topics wastes far more time than a 30-second review would have.
    Skip the checkpoint only when one of these observable conditions holds:
    
    - the user already gave you an explicit topic list or outline (not just scope/audience), or
    - you are running non-interactively (no user available to answer): proceed, and say in
      your final summary that the outline was not reviewed.
    
    ## The confidentiality rule (applies to every step)
    
    A deck is a document that leaves the repository: it gets emailed, screen-shared, and
    posted. Treat everything you put on a slide as public from the moment it is written.
    
    **Never read for slide content, and never quote:** `.env` and any `*.env*`, key/certificate
    files (`*.pem`, `*.key`, `id_rsa*`, `*.p12`, service-account JSON), `secrets/`,
    `credentials*`, `.npmrc`/`.pypirc`/`.netrc`, CI secret definitions, production logs,
    database dumps, fixtures containing real customer data, and any path the repository's own
    ignore files exclude. If a file you need is on this list, describe its *shape* ("a service
    account JSON, mounted at runtime") instead of its content.
    
    **Redact even from files that are safe to quote:** credentials, tokens, API keys, private
    hostnames and internal URLs, IP addresses, account and customer identifiers, personal
    names and emails that are not public contributors, and precise infrastructure paths where
    a generic description carries the same meaning. Replace with a clear placeholder
    (`<project-id>`, `db.internal.example`), never a plausible-looking fake.
    
    **Ask when it is the user's call, not yours:** if a slide would be materially weaker
    without a detail that looks sensitive (an internal hostname in a diagram, a real customer
    count, an unannounced feature name), ask the user before including it, and say what you
    are about to expose. Silence is not consent.
    
    **Screenshots carry more than you think.** A screenshot of a terminal, dashboard, or
    editor also captures window titles, file trees, branch names, ticket numbers, other
    tabs, and notifications. Crop to the region that makes the point, and read the image back
    before embedding it.
    
    Verification includes a redaction scan of the finished artifacts: see
    [`references/verification.md`](references/verification.md) § Redaction scan.
    
    ## Step 1: Research (never skip, never approximate)
    
    The deck is only as credible as its weakest verified claim. For every slide you plan to
    write:
    
    - **Respect the confidentiality rule below** when choosing what to open and what to quote:
      secrets, credentials, production logs, and real customer data are out of scope for slide
      content even when they would be interesting.
    - **Read the real files.** README(s), package manifest (`pyproject.toml`, `package.json`,
      `go.mod`, …), the actual source of anything you plan to quote or diagram, existing
      architecture docs, existing agent-instruction files (`CLAUDE.md`, `.agents/skills/`,
      `.claude/skills/`, and similar: these often already describe real user-facing
      capabilities accurately, a goldmine for "say to your assistant" style bubbles).
    - **Prefer code intelligence / grep+view over guessing.** If you're about to describe a
      class, a config schema, a CLI flag, or a directory layout, go find it and read it. If you
      can't find where a claim comes from, don't make the claim.
    - **When the repo's own docs contradict its code, the code wins.** Docs drift; verify any
      doc-sourced claim against the current source before putting it on a slide, and if the
      drift is itself notable, say so on the slide rather than repeating the stale claim.
    - **For "what's new" / recent-changes decks:** use `git log --oneline --since=... -- path`
      (and `git log -p` for real diff content) to ground every "shipped this month" claim in an
      actual commit, not a vague impression. Cross-check version bumps (e.g. `pyproject.toml`
      history) against the commits that produced them. A file added inside your window may
      already be deleted again by a later commit: `git show <commit>:<path>` recovers it, and
      the deletion may itself be worth a slide note.
    - **For architecture/flow diagrams:** trace the real call path (imports, function calls,
      the actual sequence a request follows) rather than inferring structure from the file tree
      alone. A plausible-looking but wrong diagram is worse than a smaller, correct one. For
      rendering (built-in flow boxes vs pre-rendered Mermaid SVG), see
      [`references/diagrams.md`](references/diagrams.md).
    - **For screenshots/plots:** use only real artifacts: a real chart the repo's own tooling
      produced, a real trace/log screenshot, a real terminal output. If a genuinely useful image
      doesn't exist yet, either generate it by actually running the repo's own code (reuse its
      existing plotting/export/reporting functions against real data on disk rather than
      writing new ad hoc plotting code) or fall back to a non-image pattern; never fabricate
      a fake chart.
    - **Delegate research for large/unfamiliar repos.** If the repo is large or you're
      unfamiliar with it, a read-only exploration sub-agent pass ("find the CLI entry points,
      the README, the test/dataset conventions, and any existing architecture docs") is worth
      it before drafting the topic list, if your environment provides sub-agents. Don't
      delegate the actual slide writing: that needs your own judgment about pacing and what's
      genuinely interesting.
    
    ## Step 2: Draft the topic list
    
    Before writing HTML, sketch the flow as a short outline (title → agenda → N sections, each
    with 2-6 slides → close) and share it per the outline checkpoint above. A well-paced deck
    opens with a title slide, an agenda, and 2-3 "the project in one picture" slides (what it
    is, one or two real results/screenshots), then either a section-divider-led deep dive per
    topic (for a general/multi-topic deck) or straight into content (for a focused deck), and
    ends on a closing/roadmap slide. Typical density: **1 idea per slide**; resist cramming two
    ideas onto one slide just to save a slide.
    
    ### Sizing
    
    Section dividers count toward the slide budget.
    
    | Deck type | Rough slide count | Section dividers? |
    |---|---|---|
    | Lightning update (one topic, one meeting) | 8-15 | No, straight into content |
    | Focused deep dive (one subsystem/feature) | 12-20 | Optional, only if it has 2+ sub-topics |
    | General overview / onboarding | 20-35 | Yes, one per major section |
    
    ## Step 3: Build
    
    1. Copy [`assets/template.html`](assets/template.html) to the output path. It already has
       the full verified CSS + navigation JS; do not rewrite either from scratch. All slide
       markup lives between the `<!-- SLIDES START -->` and `<!-- SLIDES END -->` markers;
       everything outside them stays untouched (a scripted splice against those markers is the
       easiest way to replace the body in one pass), with two exceptions: set the `<head>`'s
       `<title>` to the real deck title, and swap the `:root` theme block if the user chose a
       non-default theme ([`references/themes.md`](references/themes.md)).
    2. Work through your topic list slide by slide, copying the matching `PATTERN:` block from
       the template for each slide (title, agenda, section-divider, prose+cards, image+caption,
       table, before/after code, single annotated snippet, flow diagram, chat-bubble examples,
       lane comparison, closing: see the template's own comments for when to use which) and
       replacing every bracketed placeholder with real, verified content. Delete every pattern
       block you didn't end up using; delete the illustrative comments too once real content
       replaces them.
    3. **Cite every snippet as you write it**, with the script, never by hand:
    
       ```bash
       python3 scripts/cite.py app/main.py:40-58 --repo <repo> --snippet
       ```
    
       It prints the `data-src` / `data-sha256` pair to paste onto the `<pre>`, warns when the
       lines are too wide for the pattern you chose, and with `--snippet` prints the source
       already HTML-escaped. Stamp the build commit once, when the deck is otherwise finished:
    
       ```bash
       python3 scripts/cite.py --stamp <deck.html> --repo <repo>
       ```
    
       A hand-computed hash is worse than no hash: it silently reports CHANGED months later
       and nobody can tell whether the code moved or the build was sloppy. Same for the date,
       which is easy to invent and impossible to verify afterwards. Cite anything you quote or
       assert from one place; prose summarising a whole subsystem needs no citation. See
       [`references/freshness.md`](references/freshness.md).
    4. **HTML-escape every verbatim snippet**: `&` → `&amp;`, `<` → `&lt;`, `>` → `&gt;`
       inside `<pre>`/`<code>`. Unescaped source code (generics, arrows, includes) silently
       corrupts the markup downstream. Mind snippet line width too: see
       [`references/style-guide.md`](references/style-guide.md) § Code snippets.
    5. Follow every rule in [`references/style-guide.md`](references/style-guide.md) as you
       write: sentence case, no em dashes in prose, tag accuracy, real-content-only, rather
       than fixing it all in a pass at the end.
    6. Keep slide numbering visible to yourself: an HTML comment `<!-- N: LABEL -->` above each
       `<section class="slide">`. Comments are 0-indexed; the URL hash and the on-screen
       counter are 1-indexed. Worked example: `<!-- 6: ARCHITECTURE -->` is display slide 7,
       reached at `deck.html#7`. **Renumber the comments any time you insert or delete a
       slide**, and when the user later says "slide 12", re-derive which section that is from
       the file itself (`grep -n '<!-- [0-9]*:' deck.html`) rather than trusting a remembered
       count: a stale comment index is a fast path to editing the wrong slide.
    
    ## Step 4: Verify (every single slide, every single edit)
    
    Do not consider the deck done until you've actually looked at it. This is not optional
    polish: run for the first time on any deck, this step reliably catches real bugs: broken
    image paths, text overlapping the nav pill, tables overflowing their card, stale slide
    numbering, wrong aggregate numbers.
    
    1. **Render every slide to an image** with headless Chrome and look at each one. Chrome
       discovery is platform-dependent: use the cross-platform recipe in
       [`references/verification.md`](references/verification.md), which also covers staging
       directories (always a fresh per-deck directory, never fixed shared paths: parallel deck
       builds collide) and a faster batched screenshot loop.
    2. View each screenshot. Check specifically for: text clipped by or overlapping the bottom
       nav pill, cards/boxes stretching to fill unexpected empty space (add the `fill` class
       only where stretching is wanted; the grids default to content height), tables or code
       blocks overflowing their container, and images that failed to load (a small
       broken-image icon with visible alt text, almost always a relative-path problem: see the
       PDF workflow note in verification.md).
    3. **Check citations resolve** before shipping: `python3 scripts/check.py <deck> --repo <repo>`
       should report every citation CURRENT. Anything else means the deck is already stale on
       the day it was built: CHANGED means you quoted something and then it moved under you (or
       the hash was hand-computed), UNVERIFIED means a snippet has no hash at all. Both are
       build defects, not future problems. Fix them now with `scripts/cite.py`.
    4. **Check structural balance** after every edit: a stray unclosed `<div>` breaks
       everything downstream silently:
       ```bash
       python3 -c "
       import re
       c = open('deck.html').read()
       print('section:', len(re.findall(r'<section class=\"slide', c)), len(re.findall(r'</section>', c)))
       print('div:', len(re.findall(r'<div', c)), len(re.findall(r'</div>', c)))
       "
       ```
    5. Fix what you find, re-screenshot *those* slides, confirm the fix. Clean up every temp
       screenshot/scratch file when you're done: nothing but the deck (+ optional PDF)
       should remain.
    
    ## Step 5: Optional PDF export
    
    If asked for a PDF, use the companion **slides-to-pdf** skill (distributed alongside this
    one; its SKILL.md is the full self-contained recipe if it isn't installed as a skill).
    It screenshots every slide at 2x, prints a page-per-slide PDF with a centred page number
    on every page, and verifies the result by rendering the PDF back to images, which is
    required because headless Chrome cannot rasterize a local PDF for a visual check and image
    pages can be silently blank.
    
    ## Step 6: Ship it, and say how to keep it honest
    
    **The deck is the only file you leave behind.** Do not write a companion `README.md`,
    index, summary or notes file next to it, and do not add the deck to an existing one unless
    the user asks: the deck already states what it covers, the build stamp already records
    where it came from, and a hand-written sidecar is one more thing to go stale. If the repo
    already keeps such an index and the user wants it updated, that is their call to make, not
    a default.
    
    Everything that would have gone in that file belongs in your final message instead: what
    the deck covers, its slide count, where it was written, how to view and navigate it
    (click, arrow keys, `Esc` for the overview, `N` for notes) and, since decks get edited
    slide-by-slide over many follow-up requests, one paragraph on how the file is structured
    for future edits (the pattern-block/comment-numbering conventions above).
    
    Tell the user, in that same message, how to find out when the deck has gone stale:
    
    ```bash
    python3 scripts/check.py <deck-or-folder> --repo <repo> --suggest
    ```
    
    Then **ask whether this deck should be kept in sync**, and make it concrete rather than
    leaving it as a suggestion. The answer depends on what kind of document it is, so say so:
    
    - **Evergreen** (onboarding, architecture, anything linked from a README): offer to add
      the pull-request check from [`references/automation.md`](references/automation.md).
      Report-only first (`--exit-zero`), so it annotates a PR without blocking anyone. Vendor
      `check.py` into the repo (`tools/slideops-check.py`), because it is one dependency-free
      standard-library file and the deck's repo should not depend on a skill being installed.
    - **A snapshot** (sprint update, "what shipped in March", a conference talk): recommend
      *not* automating it. It describes a moment and is supposed to freeze. Say this out loud
      rather than silently skipping it.
    
    Do not propose blocking every commit. A docs check on the fast path trains people to pass
    `--no-verify`, and drift is a review-time concern. The reasoning, the workflow files, the
    advisory hook variants, and the delegated-refresh recipe are all in
    [`references/automation.md`](references/automation.md).
    
    ## Refreshing an existing deck
    
    When the user asks whether a deck is still accurate, or wants one brought back in line,
    **repair it; do not rebuild it**. A rebuild throws away the pacing, the narrative and the
    review that went into the original, and costs far more than fixing three slides.
    
    1. **Detect, for free.** Sweep the folder and read the result:
    
       ```bash
       python3 scripts/check.py docs/slides/ --repo . --json
       ```
    
       This costs no tokens and no model call. The JSON is a complete repair brief: per stale
       citation it carries the status, the unified diff, the commits that caused it, the
       corrected `data-src`/`data-sha256`, and the current source. Read that instead of
       re-reading the repository. If nothing is stale, say so and stop: that is the common
       case and it should be cheap.
    
    2. **Triage by status**, because they need different work:
       - `MOVED`: the code is identical, only the line numbers shifted. Update the two
         attributes. **Do not touch the slide's prose**, and do not re-verify visually: nothing
         rendered changed.
       - `CHANGED`: read the diff *and the commit subjects*. A rename needs a re-quote; a
         deleted branch of logic may have killed the claim the slide makes. Decide about the
         claim first, then the snippet.
       - `MISSING`: the file is gone. The slide is probably obsolete. Find where it went
         (`git log --diff-filter=D -- <path>`) and ask the user before deleting a slide.
       - `UNVERIFIED`: no hash was recorded. Re-cite it with `scripts/cite.py` so it is
         checkable from now on.
    
    3. **Repair only what drifted.** Edit those slides, re-trim snippets to the width budget
       ([`references/style-guide.md`](references/style-guide.md) § Code snippets), and leave
       every other slide alone.
    
    4. **Re-stamp and re-verify.** `python3 scripts/cite.py --stamp <deck> --repo .`, then
       `check.py` until clean, then re-screenshot **only the slides you touched** (Step 4): a
       longer snippet can push content under the nav pill. Re-export the PDF only if one exists.
    
    5. **Report what changed and why.** Name the slides you edited, the commits that caused
       the drift, and anything you judged still-true-despite-the-diff. That last category is
       where a human may disagree with you, so surface it rather than burying it.
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related