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", "
Install
npx skills add https://github.com/glukicov/slideops/tree/main/skills/slideops
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install glukicov-slideops@llmmart
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.mdcovers exporting such a doc to PDF. Checking or refreshing an existing.mddoc is the samecheck.pyworkflow 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:
- 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. - Audience and venue. New joiners (onboarding) · team sprint/standup · stakeholder or exec review · conference/meetup. This drives jargon level, pacing, and the Sizing row.
- 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").
- 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 perreferences/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:rootblock, screenshot the title slide of each (the recipe inreferences/verification.md), and present the images side by side before they choose. - 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.
- 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
npxnetwork access, seereferences/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(andgit log -pfor 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.tomlhistory) 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
Copy
assets/template.htmlto 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:roottheme block if the user chose a non-default theme (references/themes.md).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.Cite every snippet as you write it, with the script, never by hand:
python3 scripts/cite.py app/main.py:40-58 --repo <repo> --snippetIt prints the
data-src/data-sha256pair to paste onto the<pre>, warns when the lines are too wide for the pattern you chose, and with--snippetprints 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.HTML-escape every verbatim snippet:
&→&,<→<,>→>inside<pre>/<code>. Unescaped source code (generics, arrows, includes) silently corrupts the markup downstream. Mind snippet line width too: seereferences/style-guide.md§ Code snippets.Follow every rule in
references/style-guide.mdas 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.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 atdeck.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.
- 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. - 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
fillclass 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). - 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 withscripts/cite.py. - 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))) " - 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. Vendorcheck.pyinto 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.
Detect, for free. Sweep the folder and read the result:
python3 scripts/check.py docs/slides/ --repo . --jsonThis 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.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 withscripts/cite.pyso it is checkable from now on.
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.Re-stamp and re-verify.
python3 scripts/cite.py --stamp <deck> --repo ., thencheck.pyuntil 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.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. -->  <!-- 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 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**: `&` → `&`, `<` → `<`, `>` → `>` 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**: `&` → `&`, `<` → `<`, `>` → `>` 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.
Reviews (0)
No reviews yet.
No comments yet.