From bf3cc1ce8d17b463643bd25206567eb939fa5fc4 Mon Sep 17 00:00:00 2001 From: Hong Zhang Date: Mon, 25 May 2026 20:31:33 -0700 Subject: [PATCH] update docs --- .claude/skills/release-deckmark/SKILL.md | 170 ++++++++++++ .gitignore | 3 - CLAUDE.md | 100 +++++++ docs/ROADMAP.md | 138 ++++++++++ docs/troubleshooting.md | 325 +++++++++++++++++++++++ 5 files changed, 733 insertions(+), 3 deletions(-) create mode 100644 .claude/skills/release-deckmark/SKILL.md create mode 100644 CLAUDE.md create mode 100644 docs/ROADMAP.md create mode 100644 docs/troubleshooting.md diff --git a/.claude/skills/release-deckmark/SKILL.md b/.claude/skills/release-deckmark/SKILL.md new file mode 100644 index 0000000..d1b6ce9 --- /dev/null +++ b/.claude/skills/release-deckmark/SKILL.md @@ -0,0 +1,170 @@ +--- +name: release-deckmark +description: Use when releasing a new version of deckmark or re-tagging an existing version. Covers the bump-or-retag decision, three-file version sync, CI wait, post-release npx-cache wipe, and install verification — everything needed to ship a release that real users will actually receive. +--- + +# release-deckmark + +End-to-end checklist for cutting a deckmark release. Follow top-to-bottom; +skip nothing. Every step here exists because forgetting it has bitten us +at least once. + +## Pre-flight (1 minute) + +Run these in the repo root before touching versions or tags. + +``` +git checkout main && git pull --ff-only +npm test +npm run build +``` + +All three must succeed. If any fails, fix before continuing — a broken +release is worse than no release. + +Then decide: + +- **Bumping to a new version** (e.g., 1.0.0 → 1.0.1)? Use the "Bump" + path below. **Default to this.** Semver says published artifacts under + a given version shouldn't change. +- **Re-tagging the same version**? Use the "Re-tag" path. Only do this + if the release happened minutes ago and you're confident no one has + cached the bad bytes yet. Tell the user the trade-off and confirm + before proceeding. + +## Path A: Bump (default) + +1. **Sync three version files**. CI will reject the release if any + disagree with the tag: + - `package.json` → `"version"` + - `.claude-plugin/plugin.json` → `"version"` + - `.claude-plugin/marketplace.json` → the entry's `"version"` + +2. **Commit and tag**: + ``` + git add package.json .claude-plugin/plugin.json .claude-plugin/marketplace.json + git commit -m "chore(release): X.Y.Z" + git tag vX.Y.Z + ``` + +3. **Push commit AND tag** (separately — pushing the branch doesn't + push tags): + ``` + git push origin main + git push origin vX.Y.Z + ``` + +The tag push triggers `.github/workflows/release.yml`. + +## Path B: Re-tag same version + +Only when re-publishing within minutes of the original release. + +1. **Delete the existing GitHub Release in the browser**. The `gh` CLI + isn't installed locally — navigate to the release URL on GitHub and + click the trash icon → "Delete this release". This removes the + Release but leaves the tag. + +2. **Delete the tag remotely, then locally**: + ``` + git push origin :refs/tags/vX.Y.Z + git tag -d vX.Y.Z + ``` + +3. **Sync main and re-tag at HEAD**: + ``` + git checkout main && git pull --ff-only + git tag vX.Y.Z + git push origin vX.Y.Z + ``` + +## Wait for CI (~2 minutes) + +The workflow runs install → build → test → pack → upload tarball. It +will fail loudly if: + +- The tag doesn't match `package.json#version`. +- Tests fail on Ubuntu Node 22 (we don't have Windows CI yet — watch + for path-separator bugs that pass locally but fail there). + +Verify CI passed before continuing. The release is live when +`https://github.com/sowenzhang/deckmark/releases/latest/download/deckmark.tgz` +serves the new tarball. + +## Post-release verification (3 minutes — DO NOT SKIP) + +The release being on GitHub doesn't mean users will see it. `npx` keys +its cache by URL, not contents, so the same URL with new bytes is still +served from cache. + +1. **Confirm the tarball URL resolves** to the new release: + ```powershell + curl -sIL https://github.com/sowenzhang/deckmark/releases/latest/download/deckmark.tgz | + Select-String -Pattern '(?i)^(location|content-length):' | Select-Object -First 6 + ``` + Expect a `302` redirecting to `releases/download/vX.Y.Z/deckmark.tgz`. + +2. **Wipe the npx cache**: + ```powershell + Remove-Item -Recurse -Force "$env:LOCALAPPDATA\npm-cache\_npx" + ``` + (macOS/Linux: `rm -rf ~/.npm/_npx`.) + +3. **Refresh the marketplace mirror inside Claude Code**: + ``` + /plugin marketplace update deckmark-marketplace + ``` + +4. **Restart Claude Code** so the MCP server re-spawns and re-fetches + the tarball. + +5. **Verify the installed code matches** what you shipped. Find the + npx-extracted package and grep for an identifier unique to this + release: + ```powershell + $deck = Get-ChildItem "$env:LOCALAPPDATA\npm-cache\_npx" -Directory | + Where-Object { Test-Path "$($_.FullName)\node_modules\deckmark" } | + Select-Object -First 1 -ExpandProperty FullName + Select-String -Path "$deck\node_modules\deckmark\dist\runtime\engines\reveal.js" ` + -Pattern '' + ``` + For v1.0.0 the load-bearing identifiers are `rejectSymlink`, + `deckmark-build`, `isUnder`, `assertDirOrAbsent`. Pick one that's + new in *your* release. + +6. **End-to-end smoke test**. In Claude Code: + - `/mcp` → confirm deckmark shows **connected**. + - Ask the agent: *"use deckmark to make a one-slide deck about + anything, build it, and publish multi-file."* + - Double-click the resulting `published/index.html` and confirm it + renders without manual edits. (This is the file:// regression + check — relative `vendor/reveal/...` paths must work.) + +## When something goes wrong + +| Symptom | First check | +|---|---| +| CI fails with "tag does not match version" | All three version files in sync? Did you push the tag at the same HEAD as the version-bump commit? | +| Release exists on GitHub but install runs old code | npx cache wasn't wiped. Repeat step 2 in "Post-release verification". | +| MCP server disconnects after release | Marketplace mirror is stale. `/plugin marketplace update`, restart Claude Code. | +| Published HTML opens blank via file:// | Some `vendor/reveal/` reference still has a leading `/`. Grep the released `dist/runtime/engines/reveal.js` for `REVEAL_PREFIX = '/vendor/reveal'` — if found, the wrong code shipped. | + +For deeper issues, see `docs/troubleshooting.md`. + +## Pitfalls (things that have bitten us) + +- **Pushing the branch but not the tag.** `git push origin main` does + NOT push tags. Push the tag explicitly. +- **Re-tagging without deleting the GitHub Release first.** The + `softprops/action-gh-release@v2` action will update an existing + release, but the experience is cleaner if you delete and let it + recreate. Also keeps release notes regenerating from the right + boundary. +- **Forgetting one of the three version files.** CI catches this, but + you'll have wasted a CI run. +- **Trusting "the release is live" without wiping the npx cache.** The + cache is URL-keyed, and the URL doesn't change between versions — + this is the #1 reason "my fix shipped but I'm still seeing the bug." +- **Skipping the file:// smoke test.** The dev server hides this class + of bug because it has its own route. Multi-file publish + double-click + is the only honest check. diff --git a/.gitignore b/.gitignore index aa12dc5..c2921b6 100644 --- a/.gitignore +++ b/.gitignore @@ -36,9 +36,6 @@ Thumbs.db # Dogfood artifacts — generated when invoking init_deck on this repo for manual testing # Anchored to repo root so runtime/templates/ versions are unaffected -/AGENTS.md -/CLAUDE.md -/GEMINI.md /content.md /deckmark.config.json /annotations/ diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..9a95a2a --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,100 @@ +# CLAUDE.md + +Working notes for an AI agent (Claude / Codex / Gemini / Copilot) picking +this repo up cold. README.md is for users installing the plugin; this +file is for you contributing to it. + +## What this project is + +deckmark is an MCP-first plugin: an AI agent that lets users build a +slide deck, opens it in the browser with an annotation overlay, the user +clicks elements to leave comments, and the agent reads those annotations +back and applies them. AI-agnostic (Claude Code, Gemini CLI, Codex, +Copilot, Cursor) because the surface is the MCP tool list, not a CLI. + +## Architecture map + +| Path | Role | +|---|---| +| `mcp/server.ts` | MCP stdio server entry. Exposes the seven tools. | +| `mcp/tools/` | One file per tool: `init`, `build`, `review`, `annotations`, `publish`. | +| `runtime/engines/reveal.ts` | Markdown → reveal.js HTML. Owns `build_deck`, user-asset sync, the `.deckmark-build` marker. | +| `runtime/server/` | Fastify review server: static file routes, overlay injection, session API. | +| `runtime/overlay/` | Browser-side overlay (TS, bundled by esbuild to `dist/overlay/overlay.js`). | +| `runtime/publish/inline-html.ts` | Single-file publish: inlines reveal CSS/JS + base64 images. | +| `runtime/publish/multi-file.ts` | Multi-file publish: copies buildDir + overlays reveal dist. | +| `runtime/store/` | Annotation session JSON store. | +| `skills/deckmark/SKILL.md` | Agent-facing prompt explaining the workflow. | +| `commands/` | Claude-Code slash commands (e.g. `/deckmark:use-deckmark`). | +| `test/unit/`, `test/integration/` | `node:test`-based suites. Currently 44 tests. | + +reveal.js is **vendored at runtime via `require.resolve('reveal.js/dist/reveal.js')`**, not hard-coded. This is load-bearing: when deckmark is installed via npx, reveal.js gets hoisted to a parent `node_modules/`, so any hard-coded path would break. + +## Build / test / run + +``` +npm run build # build:src + typecheck:overlay + build:overlay +npm run build:src # tsc + copy templates/themes — fastest for MCP/engine changes +npm run build:overlay # esbuild bundle for the browser overlay only +npm test # unit + integration; must be green before commit +npm run mcp # spawn the MCP server locally for manual testing +``` + +There's no watch mode. After each change, rebuild + restart whichever +agent is hosting the MCP server. + +## Distribution + +- Released via **GitHub Releases**, not npm. The release workflow on + `tags: ['v*']` packs `deckmark.tgz` and uploads it. +- Marketplace + non-Claude agents fetch it via + `npx -y --package https://github.com/sowenzhang/deckmark/releases/latest/download/deckmark.tgz deckmark-mcp`. +- Three files carry the version and must move together: + `package.json`, `.claude-plugin/plugin.json`, `.claude-plugin/marketplace.json`. + The CI workflow refuses to release if they disagree with the tag. +- `npx` keys its cache by URL. Re-releasing the same version with new + bytes does NOT invalidate it. See `docs/troubleshooting.md` for the + cache-wipe procedure. + +## Conventions + +- **No emojis** anywhere unless the user explicitly asks. Same for the + docs the agent writes. +- **No new doc files unless asked.** ROADMAP, README, troubleshooting + already exist; resist adding more. +- **Comments**: default to none. Only write one when *why* is + non-obvious (a hidden constraint, a workaround for a specific bug, + surprising behavior). Don't restate what the code does. +- **Tests**: TDD friendly. Every security-relevant fix in v1.0 ships + with a regression test (see `test/unit/engines-reveal.test.ts` for + the symlink, traversal, and marker-guard tests as a pattern). +- **Security defaults**: anything that resolves a path from user input + goes through a containment check (`isUnder(root, candidate)` in + `inline-html.ts`). Anything that copies user files rejects symlinks + via the `cp({ filter })` pattern. +- **Destructive ops** (`rm`, force-push, etc.): always guarded. The + `.deckmark-build` marker-file ratchet in `runtime/engines/reveal.ts` + is the canonical example — copy that pattern. +- **Paths in docs**: never hard-code absolute paths like + `C:\projects\...`. Use placeholders or env-var-based + discovery (`$env:USERPROFILE`, `$LOCALAPPDATA`, etc.). + +## Where to look first + +- A new user task → `README.md`. +- A new release → `docs/troubleshooting.md` § "Release-time issues". +- A reported bug → `docs/troubleshooting.md` matches symptoms to fixes. +- Long-term direction → `docs/ROADMAP.md`. +- Agent workflow when running the seven tools → `skills/deckmark/SKILL.md`. + +## What NOT to do + +- Don't `npm publish`. The plugin is shipped via GitHub Releases only. +- Don't add a CLI surface. The MCP tool list is the public API. +- Don't change emitted HTML paths back to absolute (`/vendor/reveal/...`). + Relative paths (`vendor/reveal/...`) are load-bearing for the + multi-file publish to work via plain `file://` double-click. +- Don't introduce telemetry, accounts, or hosted state. The plugin is + local-first by design (see "Not planned" in `docs/ROADMAP.md`). +- Don't skip the regression test when patching a security finding. The + v1.0 review cycle added one test per fix; keep that ratio. diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md new file mode 100644 index 0000000..2a9dab0 --- /dev/null +++ b/docs/ROADMAP.md @@ -0,0 +1,138 @@ +# Roadmap + +This document captures where deckmark is, what's planned, and what's +explicitly out of scope. It's a living doc — open an issue if you want +to push or pull on any item. + +## Where we are: v1.0.0 (released) + +Shipped and verified end-to-end on Claude Code: + +- **Plugin shape**: AI-agnostic MCP server, distributed as a GitHub + Release tarball, fetched on demand via `npx`. No npm publish required. +- **Seven MCP tools**: `init_deck`, `build_deck`, `start_review`, + `wait_for_close`, `get_annotations`, `stop_review`, `publish_deck`. +- **Design system**: three orthogonal axes — `style × mode × motion` — + with 5 styles (`professional`, `academic`, `fashion`, `technical`, + `fun`), 2 modes (`light`, `dark`), 3 motion flags. +- **Engine**: vendored reveal.js 5.2.0 resolved at runtime via + `require.resolve`. +- **Overlay**: vanilla TypeScript bundled with esbuild, injected by the + review server. CSS selectors via `unique-selector`. +- **Review server**: Fastify on `127.0.0.1`, ephemeral port, + auto-shutdown 5 minutes after Done. +- **Publish**: single-file (everything inlined, ~1–2 MB self-contained + HTML) and multi-file (folder with `vendor/reveal/` overlay) modes. +- **Security hardening**: symlink rejection at every depth, path + traversal containment in all four inliner sinks, marker-file ratchet + on the build-clean, descriptive errors on vendor-path collisions. +- **Tests**: 44 unit + integration tests, green on Node 22. + +## Next up (v1.1, near-term) + +Small, observable gaps that won't change the public API. + +- **MCP `serverInfo.version` is hard-coded `0.1.0`**. Read it from + `package.json` so the advertise version matches the package version. + Cosmetic, but it shows in `/mcp` output and looks wrong. +- **Battle-test on non-Claude agents**. The README claims AI-agnostic + support for Gemini CLI, Codex, Copilot CLI, and Cursor, but only + Claude Code has been driven end-to-end through a real review loop. + Verify each install path, fix anything that surfaces. +- **Windows-only CI matrix**. The release workflow only runs Ubuntu / + Node 22. Windows is the primary dev environment and several recent + bugs were Windows-specific (path separators, symlink permissions); + add a Windows job to catch the next one before release. +- **Troubleshooting docs**. Common install failures (stale npx cache, + marketplace mirror, MCP server didn't load) all have known fixes + buried in chat history. Lift them into `docs/troubleshooting.md`. +- **Better error surfaces in the agent UI**. When the static server + can't find an asset, the agent sees a generic 404; emit a structured + hint in the MCP response so the agent can suggest a fix (e.g., + "rebuild — images/ wasn't synced"). +- **Stop-clean idempotence**. `stop_review` is already idempotent on + the server, but the session JSON could leak if the process dies + between `start_review` and `wait_for_close`. Add a startup sweep + that prunes orphaned session files older than N minutes. + +## Mid-term (v1.x, 3–6 months) + +Bigger features, still backward-compatible. + +- **Annotation depth**: + - Threaded comments (reply to a comment without losing the thread). + - "Resolve with note" so the agent's response shows up in the same + UI the user wrote the comment in. + - Batch resolve / batch dismiss from the overlay. +- **Diff view between annotation rounds**. Show the user what changed + since they last reviewed — visually highlight affected slides so they + can focus their re-review. +- **Custom themes**. Today the 5 styles are baked in. Let users drop a + `theme.css` next to `content.md` and reference it from + `deckmark.config.json`. Keep the 5 built-ins as defaults / starting + points. +- **More built-in styles**. Likely candidates based on common requests: + `minimalist` (Swiss / grid-heavy), `editorial` (long-form magazine), + `pitch` (VC-deck style). Each new style still passes through the + same three-axis system. +- **Slide templates**. Today every slide is generated from markdown. + Add named templates (`---template: comparison`, `---template: stat-grid`) + so the agent can reach for a known shape when the content fits. +- **Speaker-notes annotation**. Notes are second-class today (markdown + comments below a slide aren't surfaced). Render them in a notes pane + in presenter mode and let users annotate them too. +- **Export formats**: + - **PDF**: print-to-PDF via headless Chromium, one slide per page. + - **PowerPoint**: round-trip to `.pptx` for users whose downstream + workflow demands it. Higher effort — requires mapping reveal.js + layouts to PowerPoint shapes; may end up as a separate package. + +## Aspirational (v2.0+, year+) + +These would be material architecture shifts. Not committed. + +- **Multiple engines**. Today reveal.js is the only renderer. A + pluggable `engine` field in `deckmark.config.json` could pick + between reveal.js, Marp, or a from-scratch minimal renderer. +- **Real-time collaboration**. Multiple reviewers annotating + simultaneously, with a presence layer. Needs server-side state, a + hosted version, or a peer-to-peer overlay. +- **Hosted SaaS**. A managed instance that runs the review server in + the cloud so reviewers don't need the agent or `npx`. Out of scope + for the OSS plugin but a natural extension. +- **Video render**. Walking slides through with timed narration — + outputs an `.mp4`. Requires TTS pipeline + ffmpeg orchestration. +- **Accessibility certification**. Run a WCAG 2.2 AA audit on each of + the 5 styles; track and fix findings. Already pays attention + (semantic HTML, keyboard nav for reveal.js), but no formal audit + yet. +- **Internationalization**. RTL languages, CJK font stacks bundled, + fallback handling in the overlay UI strings. + +## Not planned + +Explicitly off the roadmap so users don't ask why these aren't shipping. + +- **A WYSIWYG editor**. Markdown source-of-truth is intentional — the + agent edits it, the user reviews it, both stay coherent. An editor + would split that loop. +- **Telemetry / analytics**. No opt-in, no opt-out — just none. + Reviews happen on `127.0.0.1`; nothing leaves the user's machine. +- **An online identity / accounts system**. The plugin is per-user, + per-machine. No login. +- **Monetization features in the OSS plugin**. Paywalled styles, + watermarks on free decks, etc. — not happening here. + +## How items move on this list + +- **Next-up** items are committed; if you don't see them in a release + within ~3 months, file an issue. +- **Mid-term** items are likely-but-not-guaranteed; ordering depends on + what real users hit. +- **Aspirational** items live here so they're not lost, but they need + a champion (in-repo contributor or downstream user with a real use + case) to graduate. + +To advocate for an item: open an issue describing the *use case* +(not the implementation). Use cases shape priority; implementation +sketches are bonus. diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md new file mode 100644 index 0000000..85cfedd --- /dev/null +++ b/docs/troubleshooting.md @@ -0,0 +1,325 @@ +# Troubleshooting + +Symptoms and fixes for the install / release / local-dev issues we've hit. +Every command is real — copy-paste should work. + +Path conventions used below: +- Windows npx cache: `%LOCALAPPDATA%\npm-cache\_npx\` +- macOS / Linux npx cache: `~/.npm/_npx/` +- Claude Code plugin home: `~/.claude/plugins/` + +--- + +## Health check — is the install actually working? + +Run these three checks in order. If all three pass, the install is fine. + +1. **MCP server connected**. In Claude Code, type `/mcp`. `deckmark` must + show as **connected**. If it says "disconnected" or doesn't appear, + jump to "MCP server doesn't load" below. +2. **Tools listed**. The tool list (visible during a tool call, or + ask the agent "list your MCP tools") must include the seven + `mcp__deckmark__*` entries: `init_deck`, `build_deck`, `start_review`, + `wait_for_close`, `get_annotations`, `stop_review`, `publish_deck`. +3. **Trivial build works**. In any directory, ask the agent: + *"use deckmark to create a one-slide deck about anything"*. If + `build_deck` and `start_review` complete and a browser opens, you're + good. + +--- + +## A new release isn't being picked up + +**Symptom**: you tagged a new release (or re-tagged v1.0.0), but the +installed plugin still runs old code. + +**Cause**: two layers of cache, both URL-keyed. + +1. Claude Code's marketplace mirror at + `~/.claude/plugins/cache/deckmark-marketplace/` is the marketplace + manifest cache. It's refreshed by `/plugin marketplace update`. +2. `npx`'s cache at `%LOCALAPPDATA%\npm-cache\_npx\\node_modules\deckmark\` + is keyed by the URL, **not** the tarball contents. Re-uploading the + same tarball URL with new bytes does not invalidate it. + +**Fix**: + +```powershell +# 1. Refresh the marketplace mirror (the slash command runs inside Claude Code) +/plugin marketplace update deckmark-marketplace + +# 2. Wipe the npx cache entirely +Remove-Item -Recurse -Force "$env:LOCALAPPDATA\npm-cache\_npx" + +# 3. Restart Claude Code so the MCP server re-spawns and re-fetches the tarball +``` + +On macOS / Linux step 2 becomes `rm -rf ~/.npm/_npx`. + +To verify the new code actually landed before restarting, inspect the +extracted package: + +```powershell +# Find the npx bucket containing deckmark +Get-ChildItem "$env:LOCALAPPDATA\npm-cache\_npx" -Directory | + Where-Object { Test-Path "$($_.FullName)\node_modules\deckmark" } | + Select-Object -ExpandProperty FullName +``` + +Grep that path's `dist/runtime/engines/reveal.js` for any identifier +unique to your new code (e.g. `rejectSymlink`, `deckmark-build`, +`REVEAL_PREFIX = 'vendor/reveal'`). + +--- + +## MCP server doesn't load (`/mcp` shows it disconnected, or it's missing) + +**Symptom**: `/mcp` lists deckmark as disconnected, or you see +`-32000 Failed to reconnect to plugin:deckmark:deckmark` in the agent +output. + +This is a catch-all — there are five known causes, in rough order of +likelihood. Work the list top-to-bottom. + +1. **Stale npx cache**. See the section above. This was the most common + cause during v1.0 development. +2. **Stale marketplace mirror**. `/plugin marketplace update deckmark-marketplace`, + then restart Claude Code. +3. **Plugin not enabled in the plugin manager**. Open `/plugin`, find + `deckmark@deckmark-marketplace`, confirm it's enabled (not just + installed). Toggle off → on if uncertain. +4. **Malformed `.mcp.json` inside the plugin**. The plugin's `.mcp.json` + must be a flat shape with each server at the top level — *not* + wrapped in `{ "mcpServers": {...} }`. Example of correct shape: + ```json + { + "deckmark": { + "command": "npx", + "args": ["-y", "--package", + "https://github.com/sowenzhang/deckmark/releases/latest/download/deckmark.tgz", + "deckmark-mcp"] + } + } + ``` + If you forked the plugin and wrapped it in `mcpServers`, Claude Code + silently skips it. +5. **Relative `dist/mcp/server.js` path**. If the plugin's `.mcp.json` + references a path inside the plugin install directory, it must use + `${CLAUDE_PLUGIN_ROOT}` — not a relative path. (The released plugin + uses `npx` and avoids this entirely; only relevant if you're forking + or hand-rolling a local plugin.) + +If all five check out and it's *still* broken, nuke it all: + +```powershell +# Nuclear option — uninstall everything and start fresh +/plugin uninstall deckmark +/plugin marketplace remove deckmark-marketplace +Remove-Item -Recurse -Force "$env:USERPROFILE\.claude\plugins\cache\deckmark-marketplace" +Remove-Item -Recurse -Force "$env:LOCALAPPDATA\npm-cache\_npx" +# Restart Claude Code, then reinstall: +/plugin marketplace add sowenzhang/deckmark +/plugin install deckmark@deckmark-marketplace +``` + +--- + +## Build / runtime errors + +### `Reveal is not defined` + 404s on `/vendor/reveal/*` + +**Cause**: reveal.js wasn't found on disk. The release uses +`require.resolve('reveal.js/dist/reveal.js')` so it works whether reveal +is hoisted (npx-extracted tarball) or nested (local `node_modules`). +If you forked and hard-coded a path like `/node_modules/reveal.js/dist`, +npx's hoisting will break it. + +**Fix**: use `require.resolve`, never a hard-coded path. See +`runtime/server/static-overlay.ts` and `runtime/publish/multi-file.ts` +for the pattern. + +### Images 404 from the review server + +**Cause** (pre-v1.0): the engine didn't sync user assets from the deck +folder into `build/`. The dev server only serves from `build/`. + +**Fix**: this is handled in v1.0+. If you see it post-v1.0, you're on a +stale install — wipe the npx cache and restart. + +### Published `index.html` opens blank when double-clicked + +**Cause**: the HTML references reveal.js using absolute paths like +`/vendor/reveal/reveal.js`. Under `file://`, that resolves to the +filesystem drive root (`file:///vendor/reveal/reveal.js`), not the +folder containing `index.html`. + +**Fix**: v1.0+ emits relative paths (`vendor/reveal/reveal.js`). If you +have an older published folder, either republish (after upgrading) or +manually edit `index.html` and remove the leading `/` from **all +three** references (two `` tags + one `