snappy-course skill
module module-idreadmodulesread/images/generated/img-1773735051000.png$ npx snappy-skills install snappy-course
Orchestrator for the Skool classroom portfolio at skool.com/snappy. The classroom is the top-of-funnel for snappy.ai -- it is a growing portfolio of courses mined from the corpus (not a single course). This skill ties together five surfaces: Skool classroom (many courses, each with modules + lessons), course Slack workspace (T0AQPFMQEKB / snappyclaudec-qdk8925.slack.com), blog mirrors at snappy.ai/blog, newsletter at snappy.ai/newsletter, and Skool DM outreach. It does not write lesson copy or post directly -- it tracks the content map, enforces cross-link rules, and coordinates the skills that do the actual work. Write as a teacher, not a performer. Downstream reuse to other surfaces happens separately and must NEVER influence the writing. Robert reviews, never auto-publish.
typescriptimport { getModules, getCourseModule, getSlackInfo } from "../snappy-course/api.ts";
Or CLI:
bashnpx tsx ~/.claude/skills/snappy-course/api.ts modules
npx tsx ~/.claude/skills/snappy-course/api.ts module <id>
| Function | Purpose |
|---|---|
getModules() |
Returns live modules and planned module titles |
getCourseModule(id) |
Get a specific module by ID |
getSlackInfo() |
Returns course Slack workspace info (team ID, channels) |
| Script | Purpose | Notes |
|---|---|---|
scripts/upload-lesson.sh |
Ship a draft to Skool end-to-end | Idempotent (skips if title exists). Handles missing images gracefully. Use --force to bypass dedup. Preferred over manual DOM automation. |
scripts/create-course.sh |
Create a new Skool course | Idempotent (skips if exact title exists). Returns course URL. |
scripts/lint-draft.sh |
Lint a draft against D1-D6 rules | Exit 0 = clean, exit 1 = violations. Run before claiming done. |
scripts/skool-browser-start.sh |
Start an authenticated Skool browser session | MANDATORY before any Skool browser work. Exits non-zero on failure. |
scripts/skool-auth-restore.sh |
Restore auth from backup | Recovery when auth gets corrupted. |
scripts/add-cover-image.sh |
Add or refresh a cover image on an existing Skool lesson (via draft frontmatter) | Reads skool_url + image_url from draft, downloads image, enters edit mode, removes any existing first image-bearing node via ProseMirror transaction, moves cursor to editor top, uploads at position 0, saves via structural-DOM-change (no title-dirty hack), verifies via navigate-away-and-back. Idempotent — works whether lesson has an existing cover or not. |
scripts/replace-cover.sh |
Swap an existing cover with a new image file on disk | Takes <lesson-md-url> <new-image-path> directly, same ProseMirror transaction pattern as add-cover-image. Use when you already have the image file and don't want the frontmatter round-trip. |
scripts/hoist-cover.sh |
Move an already-present buried body image to position 0 (cover banner) | Takes <lesson-md-url>. Does NOT change the image — only relocates it. Use when a lesson has the right image but it's wrapped in a <p> and not rendering as a cover. |
scripts/backfill-ship-state.sh |
Patch skool_url/shipped_at into draft files |
Reads pass-log ship lines. For legacy ships only. |
scripts/prep-drafts.mjs |
Convert draft markdown to Skool-ready HTML | Emits inline_images[{landmark,src}] for multi-image lessons. |
scripts/classroom-check.sh |
Audit the live classroom tile surface against data/classroom-spec.md and emit a verification cert |
MANDATORY after any run that touches a course tile, cover, lesson push, or reorder. Writes ~/.claude/logs/classroom-certs/<session>-<ts>.cert. Rules C1–C7: every tile has a cover, covers are unique, no placeholder titles, aspect ratio ~1.94:1, count matches data/classroom-roster.json. A classroom-touching task is NOT done until this script returns PASS or PASS_WITH_WARNINGS on a fresh run. |
Classroom enforcement (kernel rule, 2026-04-15): the spec at data/classroom-spec.md and the check at scripts/classroom-check.sh are how the classroom surface stays honest. If an agent pushes a lesson, replaces a cover, creates a course, or reorders tiles, the run is not finished until classroom-check.sh writes a fresh PASS cert. The roster at data/classroom-roster.json is the canonical list of courses that must exist — update it intentionally when adding or removing a course, and the check's C7 rule will catch drift automatically. This rule exists because every audit sweep prior to 2026-04-15 scoped itself to lesson covers inside a course and missed the missing course-tile cover on Build with Snappy. The check closes that blind spot.
Lesson cover scripts — decision tree:
image_url in frontmatter → add-cover-image.shreplace-cover.shhoist-cover.shSection/folder reorder pattern (Wave 7A 2026-04-13): moving a lesson into a section uses Skool's "Change folder" modal, scriptable via:
[data-rbd-draggable-id="module-*"] (sections are set-*)… menu: <row> [class*=MenuItemDropdownMenu] button → menu items are plain <div>s with text (Change folder, Edit page, etc)[class*=ModalContent]; folder picker = [class*=DropdownButton] inside it[data-testid^="dropdown-item-"] — dropdown-item-0 = "None", others = section titles without (Draft) prefix/save/i inside [class*=ModalContent]Within-section drag reorder (P-fix 2026-04-14): if you must reorder AFTER the fact, agent-browser drag does NOT trigger Skool's react-beautiful-dnd. You need real mouse events: agent-browser mouse move to the row, mouse down, then several move steps (first move >5px to start the drag, subsequent ~45px increments), then mouse up. Drop zones land just AFTER the hovered row, so reaching idx=0 requires two passes — first drag to idx=1, then a second drag up to the top edge of the current idx=0 row.
Cover-attach auth precheck (P-fix 2026-04-14): add-cover-image.sh now fails fast with a clear message if the module title is not visible (logged-out / 404 / stale session). Before running the cover loop, start the session with bash ~/.claude/skills/snappy-course/scripts/skool-browser-start.sh <session-name>.
bash# Ship a draft (preferred path for all lesson shipping)
bash ~/.claude/skills/snappy-course/scripts/upload-lesson.sh \
[--force] <path/to/draft.md> "<Course Title>" "<Short Title <=50 chars>"
# Create a new course
bash ~/.claude/skills/snappy-course/scripts/create-course.sh \
--title "Course Title" --description "Optional one-liner"
# Lint before shipping
bash ~/.claude/skills/snappy-course/scripts/lint-draft.sh <path/to/draft.md>
bash ~/.claude/skills/snappy-course/scripts/lint-draft.sh --all
Every lesson draft at data/drafts/<course>/<slug>.md carries YAML frontmatter. These keys are the canonical contract -- sensor, lint, and ship scripts all read them.
| Key | When set | Purpose |
|---|---|---|
title |
draft creation | Display title (Skool page title) |
course |
draft creation | Human course name |
slug |
draft creation | Filename-matching slug |
status |
draft creation | draft until shipped |
word_count |
after body write | Integer for sensor bucket |
source_files |
during mining | List of corpus paths used as grounding |
source_quotes |
during mining | Structured speaker:/date:/quote: objects -- D2 lint reads these. Must be structured objects, NOT flat strings. |
image_brief |
image planning | 6-layer Ink Journal brief |
image_cdn_url |
after image ship | CDN URL of uploaded lesson cover. Note: add-cover-image.sh reads image_url only — when generating covers, write BOTH image_url: and image_cdn_url: to the same value, otherwise the cover-attach pod will skip the lesson. |
image_generated_at |
after image gen | UTC timestamp |
skool_url |
after Skool ship | Full Skool lesson URL. Presence = ground-truth that lesson shipped. |
shipped_at |
after Skool ship | UTC timestamp. New ship pods SHOULD write both fields directly. |
The linter (scripts/lint-draft.sh) enforces these. All must pass before a draft ships.
| Rule | What it checks |
|---|---|
| D1 | No Mined from: in body. Provenance lives in frontmatter only. |
| D2 | Named corpus people in body must appear inside actual quote: text in source_quotes:. Speaker fields do NOT count as grounding. |
| D3 | No banned phrases from snappy-positioning (unlocks, leverage, transform, supercharge, 10x, seamless, etc.) in body or title. |
| D4 | "LinkedIn" is a hard fail in body or title. Classroom tone is teacher, never performer. |
| D5 | Repetitive sentence openers capped at 2 per draft ("There is a", "There are", "The reason", "Here is", "This is", "The thing is"). |
| D6 | No em dashes or en dashes. Dead AI tell. |
D1-D6 is the mechanical floor. A lesson can pass every D rule and still be generic AI slop Robert would not put his name on. Q1-Q8 is the substance gate. Every lesson must score PASS on all eight before shipping. Score WARN = revise. FAIL on any single rule = do not ship.
Auditors: for each rule, paste a specific quote from the lesson that triggers the verdict. If you can't cite a quote, you didn't audit — re-read.
| Rule | Name | One-line definition | Violation signal (what a fail looks like) | How to raise the score |
|---|---|---|---|---|
| Q1 | Voice fidelity | Reads like the 3 reference lessons (ralph-long-running-loop, skills-install-from-root, remote-terminus-tmux-mac-mini): first person, direct, builder-to-builder, concrete nouns. | Abstract SaaS tone, second-person cheerleading ("you'll love..."), parallel-structure mic-drops, three-bullet "Here's why" framings, em-dash sandwiches, eyebrow words ("Approach", "What you get"), "welcome to" preambles. | Read a reference lesson aloud, then read the draft aloud. If the volume doesn't match, rewrite the weakest paragraph using the reference's sentence shapes. |
| Q2 | Depth (named example) | Contains at least ONE worked example with real stack names, real commands, real file paths, or a real number-about-the-world (not author-scoreboard). | Only generic framing: "agents can do things", "skills help you". No concrete artifact a reader could copy. | Replace one abstract sentence with a named tool + real path (~/.claude/skills/...), a bash one-liner, or a named workflow from Robert's setup (Mac Mini + tmux + Terminus, Ralph loop, 6-layer image template, etc). |
| Q3 | Concrete artifact | At least one command, code block, file path, URL, or named config the reader can actually run or inspect on their machine. | Lesson is pure prose. Nothing the reader could paste into a terminal. Nothing they could grep for. | Add a bash block, an absolute path, a command invocation, or a canonical URL from the stack. One is enough; two is better. |
| Q4 | Arc (hook → load-bearing middle → next action) | Opens with a concrete promise or failure mode (NOT "welcome to"), the middle carries a load-bearing idea, the ending gives one specific next action the reader can do today. | Opens with "welcome to / in this lesson". Middle is three parallel bullets with no idea underneath. Ends with "let's dive in" or no action at all. | First sentence must be a claim or a failure mode the reader recognizes. Last paragraph must start with "Before the next lesson:" or equivalent and name one thing to do. |
| Q5 | Opinion (hot take) | Contains at least one sentence that is Robert's named opinion — a take a generic AI would not write because it requires judgment or conviction. | Every sentence is neutral information. No "I think / I've found / the mistake most people make / this matters more than X". Everything is defensible and nothing is interesting. | Name a trade-off, a failure mode you've seen, or a contrarian position. Prefer "the mistake most people make is X" or "this is worth more than it sounds" to bland restatements. |
| Q6 | Citation honesty | Every named person / quote / stat is either grounded in source_quotes: frontmatter OR is generic pattern language ("the pattern I see in client work"). No fabricated quotes. No author-scoreboard numbers. |
Body names Jordan/Daniel/Andy/etc without a matching structured quote: payload. Uses counts-of-my-own-output ("70 skills shipped", "620 endpoints"). |
Strip names you cannot ground. Strip counts of Robert's own output (decoy signal per feedback_numbers_decoy_rule). Numbers-about-the-world and generic patterns are fine. |
| Q7 | Not-yours smell test | Could a generic AI have produced this lesson from the title alone? If yes, FAIL. | The lesson contains no detail that reveals Robert actually built/ran this. No idiosyncratic setup, no specific tool versions, no "I tried X and it broke, so I do Y" moment. Reads like a Wikipedia summary of the topic. | Add something that only someone who ran this would know: a gotcha, a version caveat, a config flag that matters, the order of operations that actually works, the default that bit you. |
| Q8 | Shape over credential | Teaches a piece of the SHAPE of the tech — names components, shows how they connect, gives the reader a mental model they can hold and manipulate. Never frames value as a certificate, badge, or completion credential. | Lesson promises a credential, certificate, badge, or "claim your X". Lesson tells the reader what to do without revealing how the parts fit together. Generic completion gamification. Dumbed-down "prompt better" tips. | Replace credential framing with a recap of the architecture. Name the parts, draw the connections, give the reader something they can mentally manipulate. Practical at our level (Mac Mini + tmux + Terminus, Ralph loop, skill auto-injection, 6-layer image template) — the stuff Robert actually does. |
Auditor output format (required for every pilot and sweep):
Q1 voice: PASS | WARN | FAIL — "<verbatim quote from lesson>"
Q2 depth: PASS | WARN | FAIL — "<verbatim quote>"
...
Q7 smell: PASS | WARN | FAIL — "<verbatim quote>"
Q8 shape: PASS | WARN | FAIL — "<verbatim quote>"
VERDICT: SHIP | REVISE | RESTART
Robert's standard: a lesson he would put his name on scores PASS on all 8. One FAIL = REVISE. Two or more FAIL = RESTART from a different angle.
Source file paths go in source_files:. Verbatim corpus excerpts go in source_quotes: (structured speaker:/date:/quote: objects). The rendered body must not contain any Mined from: line, source-file path, meeting-date citation, or "based on the transcript from..." phrasing.
Any sentence naming a real corpus contributor (Daniel, Dimitrius, Benoit, Andy, Jordan, Ray, etc.) must trace back to a verbatim quote passed as corpus context. That quote must appear in source_quotes:. If you cannot ground it, rewrite using generic framing: "the pattern I keep seeing is...", "one thing that comes up in client work...".
Do not mine data/krisp-teaching-by-person.json -- see warning header in that file. Its verbatim_quote fields contain host (Robert) first-person speech mis-attributed to each guest. Root cause of the Jordan fabrication pattern in pass-log 2026-04-13. Use data/quote-bank.json for speaker-attributed quotes instead.
bashbash ~/.claude/skills/snappy-course/scripts/lint-draft.sh <path/to/draft.md>
Exit 0 = clean. Exit 1 = violations listed. Fix the draft, do not override the lint.
upload-lesson.sh handles this. Bottom-placed images are a recurring bug.FAHGsRxfKoU).bash# 1. Download ref image
curl -o /tmp/ref-1x1.png "https://robert-storage.tor1.digitaloceanspaces.com/images/generated/img-1773735051000.png"
# 2. Generate with ref (6-layer prompt)
npx tsx ~/.claude/skills/snappy-gemini/api.ts image "FULL 6-LAYER PROMPT" --ref /tmp/ref-1x1.png --out /tmp/lesson-img.png
# 3. Resize (HEIGHT then WIDTH)
sips -z 1080 1080 /tmp/lesson-img.png
# 4. Upload to CDN
bash ~/.claude/skills/snappy-image/scripts/cdn-upload.sh --file /tmp/lesson-img.png --context skool --slug lesson-name
Course cover spec: 1460 x 752 px (sips -z 752 1460), use 16:9 ref (img-1773735074000.png).
Known quirk: First generation sometimes produces inverted colors (black background). Regenerate with explicit "background MUST be warm cream/egg-white paper, NOT dark or black."
snappy-mine, snappy-corpus, snappy-knowledge BEFORE refining workflow descriptions. A slightly rougher true sentence beats a beautiful made-up one.MANDATORY: start every Skool session via the guard script.
bashbash ~/.claude/skills/snappy-course/scripts/skool-browser-start.sh <session-name>
The guard script kills stale daemons, backs up auth, verifies auth_token cookie, opens the classroom, and verifies logged-in state. Exits non-zero on failure -- STOP and ask Robert.
Hard rules:
--headed.agent-browser state save for Skool. Prior incident (2026-04-09): agents saved state while logged-out, clobbered working auth, blocked the swarm for hours.--session <name> only. Never pkill agent-browser.~/.openclaw/workspace/skool-auth.json. Restore: bash ~/.claude/skills/snappy-course/scripts/skool-auth-restore.sh.--session name.Programmatic .value= + dispatchEvent does NOT enable Save buttons on Skool inputs. Real keystrokes are required:
bash# Tag the input first
agent-browser --session "$S" eval "document.querySelector('SELECTOR').id = 'my-input'"
# Real keystrokes via Playwright
agent-browser --session "$S" fill "#my-input" "New value"
The _valueTracker hack, Object.getOwnPropertyDescriptor(...).set.call(), and toggling other fields all fail. Only agent-browser fill (Playwright real keystrokes) works.
The Skool lesson editor is Tiptap/ProseMirror. Key facts:
.iuLEXd) via dispatchEvent(new MouseEvent('click',{bubbles:true})).Array.from(document.querySelectorAll('.styled__SingleLineInput-sc-1saiqqb-1')).find(i=>i.placeholder==='Title'). There are 3 visible SingleLineInputs (2 Search + 1 Title) -- never use "non-empty value" heuristic..tiptap.ProseMirror.skool-editor2 (contenteditable).editor.commands.setContent(html) (Tiptap API). innerHTML also works with dispatchEvent(new Event('input',{bubbles:true})).agent-browser upload "#upload-image-input" /path/to.png (auto-inserts at cursor). This is the ONLY path that triggers Skool's CDN handler and persists assets.skool.com/f/... URLs. Data URLs (data:image/...;base64,...) injected via setContent() render in-editor but get stripped to null on save — the rendered page shows a broken classroom/null image with naturalWidth=0. Confirmed recovery 2026-04-14 on lesson 1.1: prior pod used data-URL injection, save silently nulled src; running replace-cover.sh with the native #upload-image-input path produced a clean assets.skool.com/f/f40396.../946f54....png URL that survived independent reload. Never inject images via setContent(html) — always go through the hidden file input. To set alt text on a persisted image, use editor.view.dispatch(state.tr.setNodeMarkup(imgPos, null, {...node.attrs, alt: '...', title: '...'})) after upload, then dirty the form via editor.chain().focus('end').insertContent(' ').run() + a subsequent deleteRange to re-enable SAVE.setContent(), image uploads, ProseMirror transactions do NOT enable SAVE. Fill title LAST via agent-browser fill -- this is the only step that enables SAVE.upload-lesson.sh handles this automatically..styled__ModuleBody-sc-cgnv0g-3 .tiptap (NOT .styled__ModuleBody-sc-cgnv0g-3 which always has exactly 1 child wrapper).agent-browser --session "$S" screenshot /tmp/foo.png (path is positional, NOT --output). Full-page: agent-browser --session "$S" screenshot --full /tmp/foo.png.agent-browser press: Takes ONLY a key name, not a selector. Focus first, then press.Save-enable trap recovery:
bashagent-browser --session "$S" eval "document.getElementById('t-title').focus()"
agent-browser --session "$S" type "#t-title" " " # append space -- SAVE enables
agent-browser --session "$S" eval "document.getElementById('t-title').focus()"
agent-browser --session "$S" press Backspace # remove space, SAVE stays enabled
Save-enable trap — when title trick fails (Wave 9, 2026-04-14): Skool normalizes trailing whitespace on the title field, so the space+backspace trick can leave SAVE disabled even though the title input shows 35 chars. editor.commands.setContent() also does NOT dirty the form. The reliable path when you need to replace body content is to dirty through the Tiptap editor API directly:
bash# After setContent or any programmatic body change:
agent-browser --session "$S" eval "const e=document.querySelector('.tiptap.ProseMirror.skool-editor2').editor; e.chain().focus('end').insertContent(' ').run()"
# SAVE now enabled. Optionally delete the inserted space via a ProseMirror transaction —
# the form stays dirty because Skool tracks dirty-flag, not deep equality.
agent-browser --session "$S" eval "const e=document.querySelector('.tiptap.ProseMirror.skool-editor2').editor; e.chain().focus('end').deleteRange({from: e.state.selection.from - 1, to: e.state.selection.from}).run()"
Also: if you use setContent(coverHtml + bodyHtml) to preserve an existing cover image, the image can get dropped when saving — safer to re-insert the cover image via a ProseMirror transaction (tr.insert(0, schema.nodes.image.create({src}))) AFTER the body replace, before saving.
Tiptap capabilities: H1-H4, bold, italic, strike, code, inline code, ordered/unordered lists, blockquotes, code blocks with language, horizontal rules, links, images, video embeds (YouTube/Loom/Vimeo/Wistia). Does NOT support: tables, callouts, toggles, task lists, text color, alignment, underline, emoji, slash commands.
Tiptap does NOT render <table> via innerHTML. HTML table elements are flattened to inline text. Use bold paragraph lists instead.
ProseMirror image deletion (DOM removal does not work):
javascriptconst editor = document.querySelector('.tiptap.ProseMirror.skool-editor2').editor;
const view = editor.view;
const {state} = view;
let tr = state.tr;
const toDelete = [];
state.doc.descendants((node, pos) => {
if (node.type.name === 'image') toDelete.push({pos, size: node.nodeSize});
});
toDelete.reverse().forEach(({pos, size}) => { tr = tr.delete(pos, pos + size); });
view.dispatch(tr);
ProseMirror inline image insertion at specific position:
javascript// Map positions first
const positions = [];
editor.state.doc.forEach((n,o) => positions.push({offset:o, type:n.type.name, text:n.textContent.slice(0,60)}));
// Set cursor then upload
editor.chain().focus().setTextSelection(targetOffset).run();
// Then: agent-browser upload "#upload-image-input" <path>
insertAdjacentHTML silently drops <img> tags (not ProseMirror schema nodes). Use editor.schema.nodes.image.create + tr.insert instead.
| What | Selector | Notes |
|---|---|---|
| Title text | .styled__MenuItemTitle-sc-1wvgzj7-8 |
Shared by sections and pages |
| Row wrapper | .styled__MenuItemTitleWrapper-sc-1wvgzj7-6 |
Section: .kOFVaB. Page: .jKMFqx/.kqLDxL |
| Kebab/dropdown trigger | .styled__DropdownButton-sc-1c1jt59-9 |
Parent has display:none (CSS hover). Must force-show: btn.parentElement.style.display='flex', tag with id, then dispatchEvent(new MouseEvent('click',{bubbles:true})) |
| Course tile wrapper | .styled__CourseLinkWrapper-sc-ugqan8-6 |
Navigate into course: tag wrapper, then agent-browser click "#id" (dispatchEvent does NOT navigate) |
| Course tile dropdown | .styled__DropdownButton-sc-1c1jt59-9 inside .styled__CourseDropdownMenu-sc-ugqan8-8 |
dispatchEvent click |
Click-pattern rule: Sidebar dropdown buttons require dispatchEvent(new MouseEvent('click',{bubbles:true})). Plain .click() does nothing. agent-browser click may navigate to the page instead of opening the menu.
Always tag-then-click: Find row by text, query its dropdown child, set .id, then dispatchEvent.
Classroom index → tile kebab → Edit course → description textarea → Save. The tile kebab parent is hidden (hover-only); force-show before clicking. Full recipe:
bashS="skool-tile-desc-pod"
# 1. Open dropdown on tile (force-show + dispatchEvent, NOT agent-browser click)
agent-browser --session "$S" eval "
const w=[...document.querySelectorAll('.styled__CourseLinkWrapper-sc-ugqan8-6')].find(x=>x.innerText.startsWith('DSPy'));
const dd=w.querySelector('.styled__CourseDropdownMenu-sc-ugqan8-8');
dd.style.display='flex';
dd.querySelector('.styled__DropdownButton-sc-1c1jt59-9').dispatchEvent(new MouseEvent('click',{bubbles:true}));
"
# 2. Click Edit course (first dropdown option)
agent-browser --session "$S" eval "document.querySelector('.skool-ui-dropdown-option-0').dispatchEvent(new MouseEvent('click',{bubbles:true}))"
# 3. Tag textarea and fill via agent-browser fill (NOT .value=)
agent-browser --session "$S" eval "document.querySelector('.skool-ui-base-modal .styled__MultiLineInput-sc-1saiqqb-2').id='course-desc-input'"
agent-browser --session "$S" fill "#course-desc-input" "New description here"
# 4. Click Save in modal
agent-browser --session "$S" eval "
const modal=document.querySelector('.skool-ui-base-modal');
[...modal.querySelectorAll('button')].find(b=>b.textContent.trim()==='Save').dispatchEvent(new MouseEvent('click',{bubbles:true}))
"
Course tile dropdown options (order): -0 Edit course, -1 Move ←, -2 Move →, -3 Duplicate course, -4 View as member, -5 Share course key. Delete course is .styled__DeleteDropdownItem-sc-ugqan8-18 (separate).
| Item | Selector |
|---|---|
| Edit folder | .skool-ui-dropdown-option-0 |
| Add page in folder | .skool-ui-dropdown-option-1 |
| Duplicate folder | .skool-ui-dropdown-option-2 |
| Delete folder | .styled__DeleteDropdownItem-sc-1wvgzj7-3 (NOT a dropdown-option) |
Edit page, Revert to draft, Change folder, Duplicate, Drip status, Delete (red, shows confirmation modal with CANCEL and DELETE buttons).
| What | Selector |
|---|---|
| Modal root | .skool-ui-base-modal |
| Name input | .styled__SingleLineInput-sc-1saiqqb-1 |
| Description textarea | .styled__MultiLineInput-sc-1saiqqb-2 |
| Save button | Array.from(modal.querySelectorAll('button')).find(b=>b.textContent.trim()==='Save') |
Folder name limit: 50 chars. Effective sidebar clip is ~44 chars.
Course description limit: 500 chars. Counter goes red, Save disabled when exceeded.
React-dropzone intercepts setInputFiles on its hidden input. The fix:
bashS="session-name"
# 1. Inject bridge input
agent-browser --session "$S" eval "
const tmp = document.createElement('input');
tmp.type = 'file'; tmp.accept = 'image/*'; tmp.id = 'cover-bridge';
tmp.style.cssText = 'position:fixed;top:0;left:0;z-index:99999';
document.body.appendChild(tmp);
tmp.addEventListener('change', function() {
if (!tmp.files.length) return;
const dzInput = document.querySelector('[class*=DropzoneRoot] input[type=file]');
const rp = Object.keys(dzInput).find(k => k.startsWith('__reactProps'));
dzInput[rp].onChange({ target: { files: [tmp.files[0]] } });
});
'bridge ready';
"
# 2. Upload via bridge
agent-browser --session "$S" upload "#cover-bridge" /tmp/cover.png
# 3. Click Save on crop modal (last Save on page)
agent-browser --session "$S" eval "
const saves = Array.from(document.querySelectorAll('button')).filter(b => b.textContent.trim() === 'Save');
saves[saves.length - 1].dispatchEvent(new MouseEvent('click', {bubbles: true}));
"
# 4. Click Save on edit course modal
agent-browser --session "$S" eval "
const save = Array.from(document.querySelectorAll('button')).find(b => b.textContent.trim() === 'Save');
save.dispatchEvent(new MouseEvent('click', {bubbles: true}));
"
# 5. Clean up
agent-browser --session "$S" eval "document.getElementById('cover-bridge')?.remove()"
Cover image spec: 1460 x 752 px.
agent-browser eval returns JSON-stringified results wrapped in another layer of quoting: "{\"hit\":true}". Use grep -qE '(\"hit\":true|\"hit\":true)' or grep -qF "$TITLE" for substring matches.
Full browser automation recipes for resource links, resource files, transcripts, pinned community posts, and resource management (edit/reorder/delete) are documented in:
~/.claude/skills/snappy-course/data/skool-feature-recipes.md
Key facts from that reference:
Use these to make lessons more than flat paragraphs:
javascripteditor.commands.setContent(`
<h2>Section Title</h2>
<p>Intro with <strong>bold</strong> and <code>inline code</code>.</p>
<blockquote><p>Key takeaway here.</p></blockquote>
<ol><li><strong>Step 1:</strong> Install the tool</li></ol>
<pre><code class="language-bash">npm install @anthropic-ai/sdk</code></pre>
<hr>
<h3>Subsection</h3>
`);
upload-lesson.sh is create-only (idempotency check skips existing). To update: navigate to lesson, enter edit mode, extract cover image HTML first (child.querySelector('img')), replace innerHTML with coverHtml + newBody, dirty title via fill/type+backspace, save.
Audits run BEFORE push pods, never in parallel. Skool writes are irreversible from the orchestrator's POV — by the time a lint/dashboard pod's report returns, a parallel push pod has already landed defective bytes into a live lesson.
Why: On 2026-04-14 during the Claude Code Mastery M1 push, a dashboard pod and a push pod were dispatched in the same wave. The dashboard flagged a §4a banned word (unlocks) in the M1.2 draft. By the time its report arrived, the push pod had already created the Skool lesson carrying that defect live. Required a separate patch-pod round-trip.
How to apply:
PUT, post publish, DM send): sequence in two waves.snappy-positioning AGENTS.md → "Push pods: re-grep payload in-line"). Defense in depth.A toast is not evidence. An action is not done until verified.
After EVERY action (edit, upload, rename, delete):
<img> existence. Missing = upload silently failed.Cover-at-top pass state: imgIdx in {0, 1} with contentBefore === 0. Either is valid. imgIdx >= 2 or contentBefore > 0 = cover is buried, must be hoisted.
Image count verification:
javascriptdocument.querySelectorAll('.ProseMirror img, [class*=Module] img, [class*=Content] img').length
// 0 = images did not persist
Certificate format:
certificate:
premises: [what was supposed to change]
action: [what the agent did]
trace: [session name, timestamps]
evidence: post-navigation screenshot showing [title/body/images] rendered correctly
image_count: [expected] actual [observed]
conclusion: VERIFIED | FAILED -- [detail]
Hard rule: if image_count expected > 0 and actual = 0, the action FAILED. Re-upload.
| Course | Lessons | Notes |
|---|---|---|
| Agentic Development Playbook | 12 | All have images |
| Skills as the OS | 6 | All have images |
| Xano + Agents | 5 | All have images |
| Ray's Mental Models | 5 | All have images |
| MCP Deep Dive | 8 | All have images |
| Tech Content | 7 | All have images |
| MCP Wednesday Vault | 1 | Intro page only, no session archives |
Getting Started with Snappy, Business Resources, Core Implementation Library, AG Grid/WeWeb/Webflow boilerplates, Meet Your Mentor: Daniel Migizi.
#lessons thread is anchored to the blog URL.?utm_source=<surface>&utm_medium=<format>&utm_campaign=course-launch.snappy-knowledge.snappy-positioning and read section 4a before drafting any copy.Priority order: active Skool members (14 days) > joined-but-never-opened > newsletter subscribers > past call no-shows > recent commenters. Channel: Skool DM primary, email secondary. Max 1 course-specific DM per member per week.
Team ID: T0AQPFMQEKB. Subdomain: snappyclaudec-qdk8925.slack.com. Channels: #welcome, #lessons, #wins, #help, #live, #general. All output goes through snappy-slack as drafts.
| Method | Endpoint | Notes |
|---|---|---|
| POST | /groups/{groupId}/update-landing-page-description |
Body: {lpDescription}. 1000 char limit. |
| GET/POST | /groups/{groupName}/links |
Sidebar links. 3 maximum. |
| POST | /groups/{groupName}/move-link |
old is 0-indexed, new is 1-indexed. |
| Skill | Role |
|---|---|
snappy-skool |
Skool classroom ops, lesson posting, invite DMs |
snappy-content |
Voice rules, anti-AI checklist |
snappy-positioning |
Voice constitution, section 4a banned phrases (canonical source) |
snappy-image / snappy-gemini |
Ink Journal image pipeline |
snappy-mine / snappy-corpus / snappy-knowledge |
Corpus grounding, member records |
snappy-publish |
Blog mirrors via git to Vercel |
snappy-email |
Newsletter capture + welcome sequence |
snappy-slack |
Course Slack workspace (drafts only) |
snappy-dom-cartographer |
DOM mapping for new Skool features |
skool-browser-start.sh fails -> STOP. Ask Robert.image_cdn_url in frontmatter -> upload-lesson.sh skips image upload gracefully (body via innerHTML, coverOffset=0).skool-auth-restore.sh. If that fails, STOP.<!-- SKILL-INDEX-START -->
[snappy-course Index]|root: ~/.claude/skills/snappy-course|IMPORTANT: Prefer these files over pre-training assumptions for this domain. Read the relevant file when the AGENTS.md summary is insufficient.|root:{SKILL.md}|data:{classroom-spec.md,course-audit-20260411.md,drafts/MODULE-WAVE-1-DRAFTS.md,drafts/MODULE-WAVE-1-IMAGES.md,drafts/MODULE-WAVE-2-DRAFTS.md,drafts/adp-control-loop/01-pid-older-than-you-think.md,drafts/adp-control-loop/02-certificates-over-toasts.md,drafts/adp-control-loop/03-the-promotion-threshold.md,drafts/adp-control-loop/04-handoffs-are-physical.md,drafts/adp-control-loop/05-manual-agent-cron.md,drafts/adp-control-loop/06-the-skill-is-the-controller.md,drafts/adp-control-loop/ENHANCEMENT-DASHBOARD.md,drafts/adp/ENHANCEMENT-DASHBOARD.md,drafts/adp/carbonite-capture-skills-at-the-peak.md,drafts/adp/code-as-depreciating-asset.md,drafts/adp/cookies-are-the-difference.md,drafts/adp/decouple-qa-from-enrichment.md,drafts/adp/docs-as-two-way-street.md,drafts/adp/playbooks-that-rewrite-themselves.md,drafts/adp/probability-compounding-pipelines.md,drafts/agent-browser/ENHANCEMENT-DASHBOARD.md,drafts/agent-browser/parallel-weekend-coverage.md,drafts/agent-browser/vs-playwright-mcp.md,drafts/browser-automation/ENHANCEMENT-DASHBOARD.md,drafts/browser-automation/browser-01-the-problem.md,drafts/browser-automation/browser-02-what-agent-browser-does.md,drafts/browser-automation/browser-03-dom-cartographer.md,drafts/browser-automation/browser-04-real-patterns.md,drafts/browser-automation/browser-05-install-and-ship.md,drafts/build-with-snappy/00-snappy-kernel.md,drafts/build-with-snappy/01-ralph.md,drafts/build-with-snappy/02-snappy-dispatch.md,drafts/build-with-snappy/03-snappy-shell.md,drafts/build-with-snappy/04-snappy-skill.md,drafts/build-with-snappy/05-skill-builder.md,drafts/build-with-snappy/06-snappy-hooks.md,drafts/build-with-snappy/07-find-skills.md,drafts/build-with-snappy/08-skills-install.md,drafts/build-with-snappy/09-claude-code-surfaces.md,drafts/build-with-snappy/ENHANCEMENT-DASHBOARD.md,drafts/build-with-snappy/REVIVAL-DASHBOARD.md,drafts/build-with-snappy/_OUTLINE.md,drafts/build-with-snappy/_TEMPLATE.md,drafts/build-with-snappy/_live-snapshot-M2L2.md,drafts/build-with-snappy/course-cover-spec.md,drafts/build-with-snappy/module-02/01-snappy-openrouter.md,drafts/build-with-snappy/module-02/02-snappy-ai-models.md,drafts/build-with-snappy/module-02/03-snappy-gemini.md,drafts/build-with-snappy/module-02/04-research.md,drafts/build-with-snappy/module-02/05-dspy-brain.md,drafts/build-with-snappy/module-03/01-agent-browser.md,drafts/build-with-snappy/module-03/02-snappy-browse.md,drafts/build-with-snappy/module-03/03-snappy-dom-cartographer.md,drafts/build-with-snappy/module-03/04-snappy-api-sniffer.md,drafts/build-with-snappy/module-03/05-snappy-desktop.md,drafts/build-with-snappy/module-03/06-mac-mini-server.md,drafts/build-with-snappy/module-04/01-snappy-channel-contract.md,drafts/build-with-snappy/module-04/02-snappy-telegram.md,drafts/build-with-snappy/module-04/03-snappy-whatsapp.md,drafts/build-with-snappy/module-04/04-snappy-imessage.md,drafts/build-with-snappy/module-04/05-snappy-sync.md,drafts/build-with-snappy/module-04/06-snappy-walkthrough.md,drafts/build-with-snappy/module-05/01-snappy-ffmpeg.md,drafts/build-with-snappy/module-05/02-snappy-video.md,drafts/build-with-snappy/module-05/03-video-pipeline.md,drafts/build-with-snappy/module-05/04-remotion-best-practices.md,drafts/build-with-snappy/module-05/05-snappy-sensors.md,drafts/build-with-snappy/module-06/01-universal-backend.md,drafts/build-with-snappy/module-06/02-xanoscript-builder.md,drafts/build-with-snappy/module-06/03-snappy-xano-mcp.md,drafts/build-with-snappy/module-06/04-mcp-server-builder.md,drafts/build-with-snappy/module-06/05-xano-mcp-tool-developer.md,drafts/build-with-snappy/module-06/06-chatgpt-mcp-apps.md,drafts/build-with-snappy/module-06/07-snappy-box.md,drafts/build-with-snappy/module-06/08-notion-api.md,drafts/build-with-snappy/module-07/01-vercel-agents.md,drafts/build-with-snappy/module-07/02-vercel-react-best-practices.md,drafts/build-with-snappy/module-07/03-vercel-composition-patterns.md,drafts/build-with-snappy/module-07/04-vercel-react-native-skills.md,drafts/build-with-snappy/module-07/05-seymour-papert.md,drafts/build-with-snappy/module-07/06-web-design-guidelines.md,drafts/build-with-snappy/module-07/07-crayonchat.md,drafts/build-with-snappy/module-07/08-dev-terminal.md,drafts/claude-code-bridges/completion-claim-your-certificate.md,drafts/claude-code-bridges/foundation-agent-skills.md,drafts/claude-code-bridges/foundation-building-with-claude-api.md,drafts/claude-code-bridges/foundation-claude-code-in-action.md,drafts/claude-code-bridges/foundation-intro-to-mcp.md,drafts/claude-code-mastery/DASHBOARD.md,drafts/claude-code-mastery/ENHANCEMENT-DASHBOARD.md,drafts/claude-code-mastery/m1/1.1-body-revised.md,drafts/claude-code-mastery/m1/1.1-hero-spec.md,drafts/claude-code-mastery/m1/1.2-body-new.md,drafts/claude-code-mastery/m1/alt-text-1.1.md,drafts/claude-code-mastery/m1/completion-stakes.md,drafts/claude-code-mastery/m1/cta-block.md,drafts/claude-code-mastery/m1/loom-script-1.1.md,drafts/claude-code-mastery/m1/module-description.md,drafts/claude-code-mastery/m1/resources-1.1.md,drafts/claude-code-mastery/m1/resources-1.2.md,drafts/claude-code-mastery/m2/2.1-hero-spec.md,drafts/claude-code-mastery/m2/2.2-body-revised.md,drafts/claude-code-mastery/m2/2.2-inline-spec.md,drafts/claude-code-mastery/m2/inspection-report.md,drafts/claude-code-mastery/m3/3.1-hero-spec.md,drafts/claude-code-mastery/m3/3.2-body-revised.md,drafts/claude-code-mastery/m3/3.2-hero-spec.md,drafts/claude-code-mastery/m3/inspection-report.md,drafts/claude-code-mastery/m4/4.1-hero-spec.md,drafts/claude-code-mastery/m4/4.2-body-revised.md,drafts/claude-code-mastery/m4/4.2-hero-spec.md,drafts/claude-code-mastery/m4/inspection-report.md,drafts/claude-code-mastery/m5/5.1-hero-spec.md,drafts/claude-code-mastery/m5/5.2-body-revised.md,drafts/claude-code-mastery/m5/inspection-report.md,drafts/claude-code-mastery/m6/6.1-body-revised.md,drafts/claude-code-mastery/m6/6.1-hero-spec.md,drafts/claude-code-mastery/m6/inspection-report.md,drafts/claude-code/ENHANCEMENT-DASHBOARD.md,drafts/claude-code/bug-report-widget.md,drafts/claude-code/ralph-02-the-loop-pattern.md,drafts/claude-code/ralph-03-exit-criteria.md,drafts/claude-code/ralph-05-install-and-run.md,drafts/claude-code/ralph-long-running-loop.md,drafts/claude-code/remote-terminus-tmux-mac-mini.md,drafts/claude-code/skills-install-from-root.md,drafts/claude-code/terminal-vs-vscode-extension.md,drafts/claude-code/welcome-what-youll-build.md,drafts/crayonchat/ENHANCEMENT-DASHBOARD.md,drafts/crayonchat/crayonchat-01-what-it-is.md,drafts/crayonchat/crayonchat-02-wiring-backend.md,drafts/crayonchat/crayonchat-03-customization.md,drafts/crayonchat/crayonchat-04-install-and-ship.md,drafts/dspy/00-welcome-dspy.md,drafts/dspy/ENHANCEMENT-DASHBOARD.md,drafts/dspy/measuring-agent-output-in-background.md,drafts/dspy/ship-dspy-single-session.md,drafts/fly/00-welcome-fly.md,drafts/fly/ENHANCEMENT-DASHBOARD.md,drafts/fly/mcp-hot-deploy-wrap.md,drafts/fly/never-split-logic-supabase-edge.md,drafts/fly/suspended-machine-mental-model.md,drafts/fly/two-instances-dev-prod.md,drafts/gemini/00-welcome-gemini.md,drafts/gemini/ENHANCEMENT-DASHBOARD.md,drafts/gemini/claude-code-fallback.md,drafts/gemini/direct-api-for-nano-banana.md,drafts/hookdeck/00-welcome-hookdeck.md,drafts/hookdeck/ENHANCEMENT-DASHBOARD.md,drafts/hookdeck/every-webhook-day-one.md,drafts/hookdeck/migration-insurance.md,drafts/knowledge-graphs/ENHANCEMENT-DASHBOARD.md,drafts/knowledge-graphs/falkordb-01-what-is-a-knowledge-graph.md,drafts/knowledge-graphs/falkordb-02-why-falkordb.md,drafts/knowledge-graphs/falkordb-03-entity-vs-edge.md,drafts/knowledge-graphs/falkordb-04-ingestion.md,drafts/knowledge-graphs/falkordb-05-querying.md,drafts/krisp/00-welcome-krisp.md,drafts/krisp/ENHANCEMENT-DASHBOARD.md,drafts/krisp/mine-pipeline-transcript-to-lesson.md,drafts/krisp/queryable-meeting-retrospection.md,drafts/mac-mini-stack/ENHANCEMENT-DASHBOARD.md,drafts/mac-mini-stack/mac-mini-01-why-a-mac-mini.md,drafts/mac-mini-stack/mac-mini-02-hardware-and-base-install.md,drafts/mac-mini-stack/mac-mini-03-terminal-tmux-remote-access.md,drafts/mac-mini-stack/mac-mini-04-claude-code-remote.md,drafts/mac-mini-stack/mac-mini-05-one-pass-install.md,drafts/mcp-cloudflare/ENHANCEMENT-DASHBOARD.md,drafts/mcp-cloudflare/mcp-cloudflare-01-what-mcp-is-practically.md,drafts/mcp-cloudflare/mcp-cloudflare-02-why-meta-tools-beat-one-per-endpoint.md,drafts/mcp-cloudflare/mcp-cloudflare-03-cloudflare-workers-as-your-mcp-host.md,drafts/mcp-cloudflare/mcp-cloudflare-04-snappy-xano-mcp-case-study.md,drafts/mcp-cloudflare/mcp-cloudflare-05-build-your-own.md,drafts/mcp-cloudflare/mcp-cloudflare-06-when-not-to-use-mcp.md,drafts/mcp/ENHANCEMENT-DASHBOARD.md,drafts/mcp/authentication-with-oauth.md,drafts/mcp/building-mcp-from-existing-api.md,drafts/mcp/cli-vs-mcp-tradeoff.md,drafts/mcp/deploying-on-cloudflare-workers.md,drafts/mcp/designing-your-tools.md,drafts/mcp/interactive-ui-in-chat.md,drafts/mcp/mcp-as-feedback-channel-not-wrapper.md,drafts/mcp/mcp-vs-skills-vs-cli.md,drafts/mcp/nine-meta-tools-cover-four-hundred-endpoints.md,drafts/mcp/plug-your-mcp-into-your-dev-loop.md,drafts/mcp/sharing-installing-controlling-mcp.md,drafts/mcp/tool-descriptions-context-window.md,drafts/mcp/two-tier-judgment-tools.md,drafts/mcp/what-an-mcp-actually-is.md,drafts/mcp/xano-mcp-as-cli-mistake-insurance.md,drafts/ray/ENHANCEMENT-DASHBOARD.md,drafts/ray/ar-aging-the-business-line.md,drafts/ray/four-drivers-of-rate.md,drafts/ray/hours-and-minutes.md,drafts/ray/multiplication-vs-addition.md,drafts/ray/the-harness-has-opinions.md,drafts/ray/the-kitchen-sink-demo.md,drafts/ray/the-lackey-trap.md,drafts/ray/the-rewrite-trap.md,drafts/ray/the-transcript-data-asset.md,drafts/ray/you-create-the-data.md,drafts/skills-os/ENHANCEMENT-DASHBOARD.md,drafts/skills-os/cli-first-everything-else-second.md,drafts/skills-os/composition-over-orchestration.md,drafts/skills-os/half-the-code-is-explanation.md,drafts/skills-os/let-the-api-absorb-complexity.md,drafts/skills-os/pr-review-loop-skill.md,drafts/skills-os/skill-system-05-pid-loop.md,drafts/skills-os/skill-system-06-write-your-first.md,drafts/skills-os/skills-describe-capability-agents-md-orchestrates.md,drafts/skills-os/the-self-correcting-loop.md,drafts/skills-os/the-three-file-contract.md,drafts/skills-os/turn-the-skill-into-a-script.md,drafts/skills-os/what-is-a-skill-really.md,drafts/snappy-kernel/01-what-claude-md-actually-does.md,drafts/snappy-kernel/02-the-six-kernel-concerns.md,drafts/snappy-kernel/03-api-ts-direct-calls-env-cli-mode.md,drafts/snappy-kernel/04-the-passive-context-index.md,drafts/snappy-kernel/05-preload-skill-context.md,drafts/snappy-kernel/06-always-inject-and-the-footer.md,drafts/snappy-kernel/07-posttooluse-and-stop-the-regen-queue.md,drafts/snappy-kernel/08-env-cache-and-load-ts.md,drafts/snappy-kernel/09-the-bitwarden-incident.md,drafts/snappy-kernel/10-p-proportional-and-i-integral.md,drafts/snappy-kernel/11-signal-logs-consumers-and-graduation.md,drafts/snappy-kernel/12-the-five-pipelines.md,drafts/snappy-kernel/13-snappy-ops-the-front-door.md,drafts/supabase/00-welcome-supabase.md,drafts/supabase/ENHANCEMENT-DASHBOARD.md,drafts/supabase/branching-dev-prod-diffs.md,drafts/supabase/files-table-junction-pattern.md,drafts/supabase/never-put-logic-in-edge-functions.md,drafts/supabase/supabase-plus-falkordb-entity-vs-graph.md,drafts/tech-content/ENHANCEMENT-DASHBOARD.md,drafts/tech-content/claude-code-on-phone-termius-tmux.md,drafts/tech-content/claude-code-terminal-vs-vscode.md,drafts/tech-content/file-upload-pass-the-url.md,drafts/tech-content/fly-behave-through-mcp.md,drafts/tech-content/fly-neon-xano-replacement-stack.md,drafts/tech-content/flyio-mental-model-suspended-machine.md,drafts/tech-content/handy-computer-local-voice.md,drafts/tech-content/leaving-xano-prototype-fast.md,drafts/tech-content/model-swapping-is-engineering.md,drafts/tech-content/never-split-backend-supabase-fly.md,drafts/tech-content/picking-your-ai-coding-tool.md,drafts/tech-content/ralph-skill-keeps-claude-going.md,drafts/tech-content/setting-up-termius.md,drafts/tech-content/skills-install-from-root-claude.md,drafts/tech-content/snippets-sftp-port-forwarding.md,drafts/tech-content/style-references-not-descriptions.md,drafts/tech-content/termius-in-agent-dev-workflow.md,drafts/tech-content/thin-frontend-rule-for-agentic-ui.md,drafts/tech-content/tool-not-android.md,drafts/tech-content/two-fly-instances-dev-prod.md,drafts/tech-content/waterfall-enrichment.md,drafts/tech-content/webflow-to-xano-sockets-skool.md,drafts/tech-content/what-termius-is.md,drafts/tech-content/xano-hot-deploy-agentic-dev-skool.md,drafts/tech-content/xano-shopify-server-side-skool.md,drafts/tech-content/xano-to-fly-one-line-swap-skool.md,drafts/tech-content/xano-unified-payment-layer-skool.md,drafts/xano-system/ENHANCEMENT-DASHBOARD.md,drafts/xano-system/xano-01-what-xanoscript-is.md,drafts/xano-system/xano-02-the-builder-skill.md,drafts/xano-system/xano-03-common-patterns.md,drafts/xano-system/xano-04-the-three-skill-stack.md,drafts/xano-system/xano-05-install-and-first-endpoint.md,drafts/xano/ENHANCEMENT-DASHBOARD.md,drafts/xano/consent-architecture-blockchain-ledger.md,drafts/xano/hot-deploy-feedback-loop.md,drafts/xano/migration-one-line-swap.md,drafts/xano/provisioning-your-stack.md,drafts/xano/shopify-server-side-backend.md,drafts/xano/the-mcp-bridge-pattern.md,drafts/xano/the-xano-mental-model.md,drafts/xano/tiptap-r2-asset-pipeline.md,drafts/xano/unified-payment-api.md,drafts/xano/webflow-sockets-no-webhooks.md,drafts/xano/why-agents-build-faster-fly-neon.md,drafts/xano/why-you-need-a-backend.md,image-upload-audit.md,killed/20260414T060504Z/claude-code-mastery/01-welcome-claude-code-mastery.md,killed/20260414T060504Z/claude-code-mastery/02-bridge-agent-skills.md,killed/20260414T060504Z/claude-code-mastery/04-bridge-claude-api.md,killed/20260414T060504Z/claude-code-mastery/06-bridge-mcp.md,killed/20260414T060504Z/claude-code-mastery/08-bridge-claude-code-in-action.md,killed/20260414T060504Z/claude-code-mastery/10-completion-claim-certificate.md,killed/20260414T060504Z/ralph-loops/ralph-01-the-quit-early-problem.md,killed/20260414T060504Z/ralph-loops/ralph-02-the-loop-pattern.md,killed/20260414T060504Z/ralph-loops/ralph-03-exit-criteria.md,killed/20260414T060504Z/ralph-loops/ralph-04-real-use-cases.md,killed/20260414T060504Z/ralph-loops/ralph-05-install-and-run.md,killed/20260414T060504Z/skill-system/skill-system-01-what-a-skill-is.md,killed/20260414T060504Z/skill-system/skill-system-02-finding-and-loading.md,killed/20260414T060504Z/skill-system/skill-system-03-the-gateway.md,killed/20260414T060504Z/skill-system/skill-system-04-composition.md,killed/20260414T060504Z/skill-system/skill-system-05-pid-loop.md,killed/20260414T060504Z/skill-system/skill-system-06-write-your-first.md,killed/20260414T060504Z/snappy-kernel/01-what-claude-md-actually-does.md,killed/20260414T060504Z/snappy-kernel/02-the-six-kernel-concerns.md,killed/20260414T060504Z/snappy-kernel/03-skill-md-agents-md-api-ts.md,killed/20260414T060504Z/snappy-kernel/04-api-ts-direct-calls-env-cli-mode.md,killed/20260414T060504Z/snappy-kernel/05-the-passive-context-index.md,killed/20260414T060504Z/snappy-kernel/06-preload-skill-context.md,killed/20260414T060504Z/snappy-kernel/07-always-inject-and-the-footer.md,killed/20260414T060504Z/snappy-kernel/08-posttooluse-and-stop-the-regen-queue.md,killed/20260414T060504Z/snappy-kernel/09-env-cache-and-load-ts.md,killed/20260414T060504Z/snappy-kernel/10-the-bitwarden-incident.md,killed/20260414T060504Z/snappy-kernel/11-p-proportional-and-i-integral.md,killed/20260414T060504Z/snappy-kernel/12-d-derivative-trend-detection.md,killed/20260414T060504Z/snappy-kernel/13-signal-logs-consumers-and-graduation.md,killed/20260414T060504Z/snappy-kernel/14-the-five-pipelines.md,killed/20260414T060504Z/snappy-kernel/15-snappy-ops-the-front-door.md,pass-log-entry-falkordb-knowledge-graphs-2026-04-13.md,pass-log.md,polish-pod-d-2026-04-11.md,polish-queue-20260411.md,ray-content/MANIFEST.md,ray-content/consulting-economics/running-yourself-as-a-business.md,ray-content/consulting-economics/the-5k-retainer-model.md,ray-content/consulting-economics/the-rate-increase.md,ray-content/consulting-economics/vendor-angle.md,ray-content/mental-models/durable-pipeline-illusion.md,ray-content/mental-models/earned-brag.md,ray-content/mental-models/premium-vs-totality.md,ray-content/mental-models/the-hidden-gem-problem.md,ray-content/mental-models/two-names.md,ray-content/mentorship-to-content/extract-dont-rewrite.md,ray-content/mentorship-to-content/inspire-dont-inform.md,ray-content/mentorship-to-content/seeds-you-dont-know.md,ray-content/mentorship-to-content/the-101-course.md,ray-content/mentorship-to-content/the-vj-story.md,ray-content/weekly-rhythm/sell-ship-study.md,ray-content/weekly-rhythm/short-and-sharp.md,ray-content/weekly-rhythm/the-weekly-report.md,refinement-queue.md,skool-feature-recipes.md,tech-index-summary.md,upload-log-20260410.md,upload-log-20260411.md,wave2-learnings.md,wave3-ray-candidates.md,wave3-tech-candidates.md}
<!-- SKILL-INDEX-END -->
snappy-imagesnappy-remotion<!-- SNAPPY-CONTRACT-VERBS-START -->
Generated from api.ts HAND_CONTRACT. Do not hand-edit this block.
| Verb | Contract arguments | Effect | First call |
|---|---|---|---|
module |
module-id |
read |
npx tsx ~/.claude/skills/snappy-course/api.ts module <module-id> |
modules |
— | read |
npx tsx ~/.claude/skills/snappy-course/api.ts modules |
When an answer carries face_hint, show it with one snappy_present(<answer>) call.
See /snappy-faces for face selection. Human-facing images must crop to the
element, render at 2x on Retina, and fill the destination channel instead of
placing a small card in a full-page screenshot.
<!-- SNAPPY-CONTRACT-VERBS-END -->
---
name: snappy-course
role: Orchestrator for the Skool classroom portfolio at skool.com/snappy. Tracks the content map, enforces cross-link and voice rules, coordinates draft-to-ship pipeline, and owns all browser automation for Skool lesson/course management.
loaded-by: PreToolUse hook (auto-injected when "snappy-course" is mentioned)
---
# snappy-course -- Agent Loader
Orchestrator for the **Skool classroom portfolio** at skool.com/snappy. The classroom is the top-of-funnel for snappy.ai -- it is a growing portfolio of courses mined from the corpus (not a single course). This skill ties together five surfaces: Skool classroom (many courses, each with modules + lessons), course Slack workspace (T0AQPFMQEKB / snappyclaudec-qdk8925.slack.com), blog mirrors at snappy.ai/blog, newsletter at snappy.ai/newsletter, and Skool DM outreach. It does not write lesson copy or post directly -- it tracks the content map, enforces cross-link rules, and coordinates the skills that do the actual work. Write as a teacher, not a performer. Downstream reuse to other surfaces happens separately and must NEVER influence the writing. Robert reviews, never auto-publish.
## API module
```typescript
import { getModules, getCourseModule, getSlackInfo } from "../snappy-course/api.ts";
```
Or CLI:
```bash
npx tsx ~/.claude/skills/snappy-course/api.ts modules
npx tsx ~/.claude/skills/snappy-course/api.ts module <id>
```
## API functions
| Function | Purpose |
|----------|---------|
| `getModules()` | Returns live modules and planned module titles |
| `getCourseModule(id)` | Get a specific module by ID |
| `getSlackInfo()` | Returns course Slack workspace info (team ID, channels) |
## Scripts (the primary operational interface)
| Script | Purpose | Notes |
|--------|---------|-------|
| `scripts/upload-lesson.sh` | Ship a draft to Skool end-to-end | Idempotent (skips if title exists). Handles missing images gracefully. Use `--force` to bypass dedup. **Preferred over manual DOM automation.** |
| `scripts/create-course.sh` | Create a new Skool course | Idempotent (skips if exact title exists). Returns course URL. |
| `scripts/lint-draft.sh` | Lint a draft against D1-D6 rules | Exit 0 = clean, exit 1 = violations. Run before claiming done. |
| `scripts/skool-browser-start.sh` | Start an authenticated Skool browser session | MANDATORY before any Skool browser work. Exits non-zero on failure. |
| `scripts/skool-auth-restore.sh` | Restore auth from backup | Recovery when auth gets corrupted. |
| `scripts/add-cover-image.sh` | Add or refresh a cover image on an existing Skool lesson (via draft frontmatter) | Reads `skool_url` + `image_url` from draft, downloads image, enters edit mode, removes any existing first image-bearing node via ProseMirror transaction, moves cursor to editor top, uploads at position 0, saves via structural-DOM-change (no title-dirty hack), verifies via navigate-away-and-back. Idempotent — works whether lesson has an existing cover or not. |
| `scripts/replace-cover.sh` | Swap an existing cover with a new image file on disk | Takes `<lesson-md-url> <new-image-path>` directly, same ProseMirror transaction pattern as add-cover-image. Use when you already have the image file and don't want the frontmatter round-trip. |
| `scripts/hoist-cover.sh` | Move an already-present buried body image to position 0 (cover banner) | Takes `<lesson-md-url>`. Does NOT change the image — only relocates it. Use when a lesson has the right image but it's wrapped in a `<p>` and not rendering as a cover. |
| `scripts/backfill-ship-state.sh` | Patch `skool_url`/`shipped_at` into draft files | Reads pass-log ship lines. For legacy ships only. |
| `scripts/prep-drafts.mjs` | Convert draft markdown to Skool-ready HTML | Emits `inline_images[{landmark,src}]` for multi-image lessons. |
| `scripts/classroom-check.sh` | Audit the live classroom tile surface against `data/classroom-spec.md` and emit a verification cert | **MANDATORY after any run that touches a course tile, cover, lesson push, or reorder.** Writes `~/.claude/logs/classroom-certs/<session>-<ts>.cert`. Rules C1–C7: every tile has a cover, covers are unique, no placeholder titles, aspect ratio ~1.94:1, count matches `data/classroom-roster.json`. A classroom-touching task is NOT done until this script returns PASS or PASS_WITH_WARNINGS on a fresh run. |
**Classroom enforcement (kernel rule, 2026-04-15):** the spec at `data/classroom-spec.md` and the check at `scripts/classroom-check.sh` are how the classroom surface stays honest. If an agent pushes a lesson, replaces a cover, creates a course, or reorders tiles, the run is not finished until `classroom-check.sh` writes a fresh PASS cert. The roster at `data/classroom-roster.json` is the canonical list of courses that must exist — update it intentionally when adding or removing a course, and the check's C7 rule will catch drift automatically. **This rule exists because every audit sweep prior to 2026-04-15 scoped itself to lesson covers inside a course and missed the missing course-tile cover on Build with Snappy. The check closes that blind spot.**
**Lesson cover scripts — decision tree:**
- Lesson has no cover, you have a draft with `image_url` in frontmatter → `add-cover-image.sh`
- Lesson has no cover (or wrong cover), you have an image file on disk → `replace-cover.sh`
- Lesson already has the right image but it's buried in the body → `hoist-cover.sh`
**Section/folder reorder pattern (Wave 7A 2026-04-13):** moving a lesson into a section uses Skool's "Change folder" modal, scriptable via:
- Lesson row: `[data-rbd-draggable-id="module-*"]` (sections are `set-*`)
- Row `…` menu: `<row> [class*=MenuItemDropdownMenu] button` → menu items are plain `<div>`s with text (`Change folder`, `Edit page`, etc)
- Modal: `[class*=ModalContent]`; folder picker = `[class*=DropdownButton]` inside it
- Folder dropdown items: `[data-testid^="dropdown-item-"]` — `dropdown-item-0` = "None", others = section titles **without** `(Draft)` prefix
- Save: button matched `/save/i` inside `[class*=ModalContent]`
- Timing: modal open ~600ms, dropdown ~500ms, save settle ~1200ms
- **Within-section order**: Skool appends moved lessons to the END of the destination section, so move lessons in the desired final sequence — no separate reorder pass needed.
**Within-section drag reorder (P-fix 2026-04-14):** if you must reorder AFTER the fact, `agent-browser drag` does NOT trigger Skool's `react-beautiful-dnd`. You need real mouse events: `agent-browser mouse move` to the row, `mouse down`, then several `move` steps (first move >5px to start the drag, subsequent ~45px increments), then `mouse up`. Drop zones land just AFTER the hovered row, so reaching idx=0 requires two passes — first drag to idx=1, then a second drag up to the top edge of the current idx=0 row.
**Cover-attach auth precheck (P-fix 2026-04-14):** `add-cover-image.sh` now fails fast with a clear message if the module title is not visible (logged-out / 404 / stale session). Before running the cover loop, start the session with `bash ~/.claude/skills/snappy-course/scripts/skool-browser-start.sh <session-name>`.
### Script usage
```bash
# Ship a draft (preferred path for all lesson shipping)
bash ~/.claude/skills/snappy-course/scripts/upload-lesson.sh \
[--force] <path/to/draft.md> "<Course Title>" "<Short Title <=50 chars>"
# Create a new course
bash ~/.claude/skills/snappy-course/scripts/create-course.sh \
--title "Course Title" --description "Optional one-liner"
# Lint before shipping
bash ~/.claude/skills/snappy-course/scripts/lint-draft.sh <path/to/draft.md>
bash ~/.claude/skills/snappy-course/scripts/lint-draft.sh --all
```
## Draft frontmatter contract
Every lesson draft at `data/drafts/<course>/<slug>.md` carries YAML frontmatter. These keys are the canonical contract -- sensor, lint, and ship scripts all read them.
| Key | When set | Purpose |
|-----|----------|---------|
| `title` | draft creation | Display title (Skool page title) |
| `course` | draft creation | Human course name |
| `slug` | draft creation | Filename-matching slug |
| `status` | draft creation | `draft` until shipped |
| `word_count` | after body write | Integer for sensor bucket |
| `source_files` | during mining | List of corpus paths used as grounding |
| `source_quotes` | during mining | Structured `speaker:/date:/quote:` objects -- D2 lint reads these. **Must be structured objects, NOT flat strings.** |
| `image_brief` | image planning | 6-layer Ink Journal brief |
| `image_cdn_url` | after image ship | CDN URL of uploaded lesson cover. **Note**: `add-cover-image.sh` reads `image_url` only — when generating covers, write BOTH `image_url:` and `image_cdn_url:` to the same value, otherwise the cover-attach pod will skip the lesson. |
| `image_generated_at` | after image gen | UTC timestamp |
| `skool_url` | **after Skool ship** | Full Skool lesson URL. **Presence = ground-truth that lesson shipped.** |
| `shipped_at` | **after Skool ship** | UTC timestamp. New ship pods SHOULD write both fields directly. |
## Lint rules (D1-D6)
The linter (`scripts/lint-draft.sh`) enforces these. All must pass before a draft ships.
| Rule | What it checks |
|------|----------------|
| **D1** | No `Mined from:` in body. Provenance lives in frontmatter only. |
| **D2** | Named corpus people in body must appear inside actual `quote:` text in `source_quotes:`. Speaker fields do NOT count as grounding. |
| **D3** | No banned phrases from snappy-positioning (unlocks, leverage, transform, supercharge, 10x, seamless, etc.) in body or title. |
| **D4** | "LinkedIn" is a hard fail in body or title. Classroom tone is teacher, never performer. |
| **D5** | Repetitive sentence openers capped at 2 per draft ("There is a", "There are", "The reason", "Here is", "This is", "The thing is"). |
| **D6** | No em dashes or en dashes. Dead AI tell. |
## Q1-Q8: Lesson Quality Rubric (substance gate, beyond D1-D6 mechanical lint)
D1-D6 is the mechanical floor. A lesson can pass every D rule and still be generic AI slop Robert would not put his name on. Q1-Q8 is the substance gate. Every lesson must score PASS on all eight before shipping. Score WARN = revise. FAIL on any single rule = do not ship.
Auditors: for each rule, paste a specific quote from the lesson that triggers the verdict. If you can't cite a quote, you didn't audit — re-read.
| Rule | Name | One-line definition | Violation signal (what a fail looks like) | How to raise the score |
|------|------|---------------------|--------------------------------------------|------------------------|
| **Q1** | Voice fidelity | Reads like the 3 reference lessons (ralph-long-running-loop, skills-install-from-root, remote-terminus-tmux-mac-mini): first person, direct, builder-to-builder, concrete nouns. | Abstract SaaS tone, second-person cheerleading ("you'll love..."), parallel-structure mic-drops, three-bullet "Here's why" framings, em-dash sandwiches, eyebrow words ("Approach", "What you get"), "welcome to" preambles. | Read a reference lesson aloud, then read the draft aloud. If the volume doesn't match, rewrite the weakest paragraph using the reference's sentence shapes. |
| **Q2** | Depth (named example) | Contains at least ONE worked example with real stack names, real commands, real file paths, or a real number-about-the-world (not author-scoreboard). | Only generic framing: "agents can do things", "skills help you". No concrete artifact a reader could copy. | Replace one abstract sentence with a named tool + real path (`~/.claude/skills/...`), a bash one-liner, or a named workflow from Robert's setup (Mac Mini + tmux + Terminus, Ralph loop, 6-layer image template, etc). |
| **Q3** | Concrete artifact | At least one command, code block, file path, URL, or named config the reader can actually run or inspect on their machine. | Lesson is pure prose. Nothing the reader could paste into a terminal. Nothing they could grep for. | Add a `bash` block, an absolute path, a command invocation, or a canonical URL from the stack. One is enough; two is better. |
| **Q4** | Arc (hook → load-bearing middle → next action) | Opens with a concrete promise or failure mode (NOT "welcome to"), the middle carries a load-bearing idea, the ending gives one specific next action the reader can do today. | Opens with "welcome to / in this lesson". Middle is three parallel bullets with no idea underneath. Ends with "let's dive in" or no action at all. | First sentence must be a claim or a failure mode the reader recognizes. Last paragraph must start with "**Before the next lesson:**" or equivalent and name one thing to do. |
| **Q5** | Opinion (hot take) | Contains at least one sentence that is Robert's named opinion — a take a generic AI would not write because it requires judgment or conviction. | Every sentence is neutral information. No "I think / I've found / the mistake most people make / this matters more than X". Everything is defensible and nothing is interesting. | Name a trade-off, a failure mode you've seen, or a contrarian position. Prefer "the mistake most people make is X" or "this is worth more than it sounds" to bland restatements. |
| **Q6** | Citation honesty | Every named person / quote / stat is either grounded in `source_quotes:` frontmatter OR is generic pattern language ("the pattern I see in client work"). No fabricated quotes. No author-scoreboard numbers. | Body names Jordan/Daniel/Andy/etc without a matching structured `quote:` payload. Uses counts-of-my-own-output ("70 skills shipped", "620 endpoints"). | Strip names you cannot ground. Strip counts of Robert's own output (decoy signal per feedback_numbers_decoy_rule). Numbers-about-the-world and generic patterns are fine. |
| **Q7** | Not-yours smell test | Could a generic AI have produced this lesson from the title alone? If yes, FAIL. | The lesson contains no detail that reveals Robert actually built/ran this. No idiosyncratic setup, no specific tool versions, no "I tried X and it broke, so I do Y" moment. Reads like a Wikipedia summary of the topic. | Add something that only someone who ran this would know: a gotcha, a version caveat, a config flag that matters, the order of operations that actually works, the default that bit you. |
| **Q8** | Shape over credential | Teaches a piece of the SHAPE of the tech — names components, shows how they connect, gives the reader a mental model they can hold and manipulate. Never frames value as a certificate, badge, or completion credential. | Lesson promises a credential, certificate, badge, or "claim your X". Lesson tells the reader what to do without revealing how the parts fit together. Generic completion gamification. Dumbed-down "prompt better" tips. | Replace credential framing with a recap of the architecture. Name the parts, draw the connections, give the reader something they can mentally manipulate. Practical at our level (Mac Mini + tmux + Terminus, Ralph loop, skill auto-injection, 6-layer image template) — the stuff Robert actually does. |
**Auditor output format (required for every pilot and sweep):**
```
Q1 voice: PASS | WARN | FAIL — "<verbatim quote from lesson>"
Q2 depth: PASS | WARN | FAIL — "<verbatim quote>"
...
Q7 smell: PASS | WARN | FAIL — "<verbatim quote>"
Q8 shape: PASS | WARN | FAIL — "<verbatim quote>"
VERDICT: SHIP | REVISE | RESTART
```
Robert's standard: a lesson he would put his name on scores PASS on all 8. One FAIL = REVISE. Two or more FAIL = RESTART from a different angle.
## Grounding rules (G1-G3) -- MANDATORY for every lesson generation
### G1 -- provenance lives in frontmatter, NEVER in the body
Source file paths go in `source_files:`. Verbatim corpus excerpts go in `source_quotes:` (structured `speaker:/date:/quote:` objects). The rendered body must not contain any `Mined from:` line, source-file path, meeting-date citation, or "based on the transcript from..." phrasing.
### G2 -- named-person anecdotes must be grounded
Any sentence naming a real corpus contributor (Daniel, Dimitrius, Benoit, Andy, Jordan, Ray, etc.) must trace back to a verbatim quote passed as corpus context. That quote must appear in `source_quotes:`. If you cannot ground it, rewrite using generic framing: "the pattern I keep seeing is...", "one thing that comes up in client work...".
**Do not mine `data/krisp-teaching-by-person.json`** -- see warning header in that file. Its `verbatim_quote` fields contain host (Robert) first-person speech mis-attributed to each guest. Root cause of the Jordan fabrication pattern in pass-log 2026-04-13. Use `data/quote-bank.json` for speaker-attributed quotes instead.
### G3 -- run the lint before claiming done
```bash
bash ~/.claude/skills/snappy-course/scripts/lint-draft.sh <path/to/draft.md>
```
Exit 0 = clean. Exit 1 = violations listed. Fix the draft, do not override the lint.
## Operational rules -- MANDATORY for every lesson generation pod
### Image rules
1. **Title rendered INTO the image.** The Ink Journal base ref has an empty lower text slot -- fill it with the lesson title in clean black serif. Blank = incomplete.
2. **Lorem-ipsum gate:** mentally render the ref with lorem ipsum in the text slot. If it looks right, the ref is good. If empty/awkward, fix the ref.
3. **Cover image goes at the TOP** of the lesson body, above the first heading/paragraph. `upload-lesson.sh` handles this. Bottom-placed images are a recurring bug.
4. **Lessons > 500 words need 2-4 inline illustrations**, one per major section break. Each must visualize a RELATIONSHIP (cause/effect, hierarchy, loop, tradeoff), not just a noun.
5. **Canva sync:** every lesson image auto-imports to Canva (Course Images folder `FAHGsRxfKoU`).
### Image generation pipeline
```bash
# 1. Download ref image
curl -o /tmp/ref-1x1.png "https://robert-storage.tor1.digitaloceanspaces.com/images/generated/img-1773735051000.png"
# 2. Generate with ref (6-layer prompt)
npx tsx ~/.claude/skills/snappy-gemini/api.ts image "FULL 6-LAYER PROMPT" --ref /tmp/ref-1x1.png --out /tmp/lesson-img.png
# 3. Resize (HEIGHT then WIDTH)
sips -z 1080 1080 /tmp/lesson-img.png
# 4. Upload to CDN
bash ~/.claude/skills/snappy-image/scripts/cdn-upload.sh --file /tmp/lesson-img.png --context skool --slug lesson-name
```
Course cover spec: 1460 x 752 px (`sips -z 752 1460`), use 16:9 ref (`img-1773735074000.png`).
**Known quirk:** First generation sometimes produces inverted colors (black background). Regenerate with explicit "background MUST be warm cream/egg-white paper, NOT dark or black."
### Writing voice
- **Classroom tone is primary.** Never mention LinkedIn, carousels, social media, or "multi-surface reuse" in any prompt that writes lesson content. The word "LinkedIn" is cursed -- it produces performative writing.
- **Write as a teacher, not a performer.** The tone test: does this read like explaining to a student, or performing for an audience? If the latter, rewrite.
- **Sentence variety.** Mix short punchy lines with longer flowing ones. If three consecutive sentences have the same length/shape, rework. Read aloud -- if it sounds like a list of declarations, break the pattern.
- **Accuracy before polish.** Consult `snappy-mine`, `snappy-corpus`, `snappy-knowledge` BEFORE refining workflow descriptions. A slightly rougher true sentence beats a beautiful made-up one.
- **Multi-pass strategy.** Courses need many waves (write, critique, image, resources, cross-link). Polish what exists before shipping more. One world-class lesson beats five mediocre ones.
## Skool browser automation
### Auth and session setup
**MANDATORY: start every Skool session via the guard script.**
```bash
bash ~/.claude/skills/snappy-course/scripts/skool-browser-start.sh <session-name>
```
The guard script kills stale daemons, backs up auth, verifies `auth_token` cookie, opens the classroom, and verifies logged-in state. Exits non-zero on failure -- STOP and ask Robert.
**Hard rules:**
- Skool WAF blocks headless browsers. Always use `--headed`.
- NEVER call `agent-browser state save` for Skool. Prior incident (2026-04-09): agents saved state while logged-out, clobbered working auth, blocked the swarm for hours.
- Session isolation by `--session <name>` only. Never `pkill agent-browser`.
- Auth file: `~/.openclaw/workspace/skool-auth.json`. Restore: `bash ~/.claude/skills/snappy-course/scripts/skool-auth-restore.sh`.
- Concurrent sessions work if each uses a unique `--session` name.
- Headed session may need close+reopen to get past blank page on first launch.
### React-form rule (the trap)
Programmatic `.value=` + `dispatchEvent` does NOT enable Save buttons on Skool inputs. Real keystrokes are required:
```bash
# Tag the input first
agent-browser --session "$S" eval "document.querySelector('SELECTOR').id = 'my-input'"
# Real keystrokes via Playwright
agent-browser --session "$S" fill "#my-input" "New value"
```
The `_valueTracker` hack, `Object.getOwnPropertyDescriptor(...).set.call()`, and toggling other fields all fail. Only `agent-browser fill` (Playwright real keystrokes) works.
### Page editor (Tiptap)
The Skool lesson editor is Tiptap/ProseMirror. Key facts:
- **Enter edit mode:** click the pencil button (`.iuLEXd`) via `dispatchEvent(new MouseEvent('click',{bubbles:true}))`.
- **Title input:** Find by placeholder: `Array.from(document.querySelectorAll('.styled__SingleLineInput-sc-1saiqqb-1')).find(i=>i.placeholder==='Title')`. There are 3 visible SingleLineInputs (2 Search + 1 Title) -- never use "non-empty value" heuristic.
- **Body editor:** `.tiptap.ProseMirror.skool-editor2` (contenteditable).
- **Set content:** `editor.commands.setContent(html)` (Tiptap API). `innerHTML` also works with `dispatchEvent(new Event('input',{bubbles:true}))`.
- **Image upload:** `agent-browser upload "#upload-image-input" /path/to.png` (auto-inserts at cursor). This is the ONLY path that triggers Skool's CDN handler and persists `assets.skool.com/f/...` URLs. **Data URLs (`data:image/...;base64,...`) injected via `setContent()` render in-editor but get stripped to `null` on save** — the rendered page shows a broken `classroom/null` image with naturalWidth=0. Confirmed recovery 2026-04-14 on lesson 1.1: prior pod used data-URL injection, save silently nulled src; running `replace-cover.sh` with the native `#upload-image-input` path produced a clean `assets.skool.com/f/f40396.../946f54....png` URL that survived independent reload. Never inject images via `setContent(html)` — always go through the hidden file input. To set alt text on a persisted image, use `editor.view.dispatch(state.tr.setNodeMarkup(imgPos, null, {...node.attrs, alt: '...', title: '...'}))` after upload, then dirty the form via `editor.chain().focus('end').insertContent(' ').run()` + a subsequent `deleteRange` to re-enable SAVE.
- **SAVE requires title dirty.** `setContent()`, image uploads, ProseMirror transactions do NOT enable SAVE. Fill title LAST via `agent-browser fill` -- this is the only step that enables SAVE.
- **Title limit:** 50 characters. Counter goes red when exceeded.
- **Published/Draft toggle:** New pages default to Published. You MUST toggle before SAVE or the lesson ships live. `upload-lesson.sh` handles this automatically.
- **Read-mode body selector for audits:** `.styled__ModuleBody-sc-cgnv0g-3 .tiptap` (NOT `.styled__ModuleBody-sc-cgnv0g-3` which always has exactly 1 child wrapper).
- **Screenshot:** `agent-browser --session "$S" screenshot /tmp/foo.png` (path is positional, NOT `--output`). Full-page: `agent-browser --session "$S" screenshot --full /tmp/foo.png`.
- **`agent-browser press`:** Takes ONLY a key name, not a selector. Focus first, then press.
**Save-enable trap recovery:**
```bash
agent-browser --session "$S" eval "document.getElementById('t-title').focus()"
agent-browser --session "$S" type "#t-title" " " # append space -- SAVE enables
agent-browser --session "$S" eval "document.getElementById('t-title').focus()"
agent-browser --session "$S" press Backspace # remove space, SAVE stays enabled
```
**Save-enable trap — when title trick fails (Wave 9, 2026-04-14):** Skool normalizes trailing whitespace on the title field, so the space+backspace trick can leave SAVE disabled even though the title input shows 35 chars. `editor.commands.setContent()` also does NOT dirty the form. The reliable path when you need to replace body content is to dirty through the Tiptap editor API directly:
```bash
# After setContent or any programmatic body change:
agent-browser --session "$S" eval "const e=document.querySelector('.tiptap.ProseMirror.skool-editor2').editor; e.chain().focus('end').insertContent(' ').run()"
# SAVE now enabled. Optionally delete the inserted space via a ProseMirror transaction —
# the form stays dirty because Skool tracks dirty-flag, not deep equality.
agent-browser --session "$S" eval "const e=document.querySelector('.tiptap.ProseMirror.skool-editor2').editor; e.chain().focus('end').deleteRange({from: e.state.selection.from - 1, to: e.state.selection.from}).run()"
```
Also: if you use `setContent(coverHtml + bodyHtml)` to preserve an existing cover image, the image can get dropped when saving — safer to re-insert the cover image via a ProseMirror transaction (`tr.insert(0, schema.nodes.image.create({src}))`) AFTER the body replace, before saving.
**Tiptap capabilities:** H1-H4, bold, italic, strike, code, inline code, ordered/unordered lists, blockquotes, code blocks with language, horizontal rules, links, images, video embeds (YouTube/Loom/Vimeo/Wistia). Does NOT support: tables, callouts, toggles, task lists, text color, alignment, underline, emoji, slash commands.
**Tiptap does NOT render `<table>` via innerHTML.** HTML table elements are flattened to inline text. Use bold paragraph lists instead.
**ProseMirror image deletion (DOM removal does not work):**
```javascript
const editor = document.querySelector('.tiptap.ProseMirror.skool-editor2').editor;
const view = editor.view;
const {state} = view;
let tr = state.tr;
const toDelete = [];
state.doc.descendants((node, pos) => {
if (node.type.name === 'image') toDelete.push({pos, size: node.nodeSize});
});
toDelete.reverse().forEach(({pos, size}) => { tr = tr.delete(pos, pos + size); });
view.dispatch(tr);
```
**ProseMirror inline image insertion at specific position:**
```javascript
// Map positions first
const positions = [];
editor.state.doc.forEach((n,o) => positions.push({offset:o, type:n.type.name, text:n.textContent.slice(0,60)}));
// Set cursor then upload
editor.chain().focus().setTextSelection(targetOffset).run();
// Then: agent-browser upload "#upload-image-input" <path>
```
**`insertAdjacentHTML` silently drops `<img>` tags** (not ProseMirror schema nodes). Use `editor.schema.nodes.image.create` + `tr.insert` instead.
### Sidebar navigation and dropdown menus
| What | Selector | Notes |
|------|----------|-------|
| Title text | `.styled__MenuItemTitle-sc-1wvgzj7-8` | Shared by sections and pages |
| Row wrapper | `.styled__MenuItemTitleWrapper-sc-1wvgzj7-6` | Section: `.kOFVaB`. Page: `.jKMFqx`/`.kqLDxL` |
| Kebab/dropdown trigger | `.styled__DropdownButton-sc-1c1jt59-9` | Parent has `display:none` (CSS hover). Must force-show: `btn.parentElement.style.display='flex'`, tag with id, then `dispatchEvent(new MouseEvent('click',{bubbles:true}))` |
| Course tile wrapper | `.styled__CourseLinkWrapper-sc-ugqan8-6` | Navigate into course: tag wrapper, then `agent-browser click "#id"` (dispatchEvent does NOT navigate) |
| Course tile dropdown | `.styled__DropdownButton-sc-1c1jt59-9` inside `.styled__CourseDropdownMenu-sc-ugqan8-8` | dispatchEvent click |
**Click-pattern rule:** Sidebar dropdown buttons require `dispatchEvent(new MouseEvent('click',{bubbles:true}))`. Plain `.click()` does nothing. `agent-browser click` may navigate to the page instead of opening the menu.
**Always tag-then-click:** Find row by text, query its dropdown child, set `.id`, then dispatchEvent.
### Update course tile description (Edit course modal)
Classroom index → tile kebab → Edit course → description textarea → Save. The tile kebab parent is hidden (hover-only); force-show before clicking. Full recipe:
```bash
S="skool-tile-desc-pod"
# 1. Open dropdown on tile (force-show + dispatchEvent, NOT agent-browser click)
agent-browser --session "$S" eval "
const w=[...document.querySelectorAll('.styled__CourseLinkWrapper-sc-ugqan8-6')].find(x=>x.innerText.startsWith('DSPy'));
const dd=w.querySelector('.styled__CourseDropdownMenu-sc-ugqan8-8');
dd.style.display='flex';
dd.querySelector('.styled__DropdownButton-sc-1c1jt59-9').dispatchEvent(new MouseEvent('click',{bubbles:true}));
"
# 2. Click Edit course (first dropdown option)
agent-browser --session "$S" eval "document.querySelector('.skool-ui-dropdown-option-0').dispatchEvent(new MouseEvent('click',{bubbles:true}))"
# 3. Tag textarea and fill via agent-browser fill (NOT .value=)
agent-browser --session "$S" eval "document.querySelector('.skool-ui-base-modal .styled__MultiLineInput-sc-1saiqqb-2').id='course-desc-input'"
agent-browser --session "$S" fill "#course-desc-input" "New description here"
# 4. Click Save in modal
agent-browser --session "$S" eval "
const modal=document.querySelector('.skool-ui-base-modal');
[...modal.querySelectorAll('button')].find(b=>b.textContent.trim()==='Save').dispatchEvent(new MouseEvent('click',{bubbles:true}))
"
```
Course tile dropdown options (order): `-0` Edit course, `-1` Move ←, `-2` Move →, `-3` Duplicate course, `-4` View as member, `-5` Share course key. Delete course is `.styled__DeleteDropdownItem-sc-ugqan8-18` (separate).
### Folder dropdown menu items
| Item | Selector |
|------|----------|
| Edit folder | `.skool-ui-dropdown-option-0` |
| Add page in folder | `.skool-ui-dropdown-option-1` |
| Duplicate folder | `.skool-ui-dropdown-option-2` |
| Delete folder | `.styled__DeleteDropdownItem-sc-1wvgzj7-3` (NOT a dropdown-option) |
### Page dropdown menu items (force-show kebab parent first)
Edit page, Revert to draft, Change folder, Duplicate, Drip status, **Delete** (red, shows confirmation modal with CANCEL and DELETE buttons).
### Edit modal (folder/course)
| What | Selector |
|------|----------|
| Modal root | `.skool-ui-base-modal` |
| Name input | `.styled__SingleLineInput-sc-1saiqqb-1` |
| Description textarea | `.styled__MultiLineInput-sc-1saiqqb-2` |
| Save button | `Array.from(modal.querySelectorAll('button')).find(b=>b.textContent.trim()==='Save')` |
**Folder name limit: 50 chars.** Effective sidebar clip is ~44 chars.
**Course description limit: 500 chars.** Counter goes red, Save disabled when exceeded.
### Course cover upload (bridge input pattern)
React-dropzone intercepts `setInputFiles` on its hidden input. The fix:
```bash
S="session-name"
# 1. Inject bridge input
agent-browser --session "$S" eval "
const tmp = document.createElement('input');
tmp.type = 'file'; tmp.accept = 'image/*'; tmp.id = 'cover-bridge';
tmp.style.cssText = 'position:fixed;top:0;left:0;z-index:99999';
document.body.appendChild(tmp);
tmp.addEventListener('change', function() {
if (!tmp.files.length) return;
const dzInput = document.querySelector('[class*=DropzoneRoot] input[type=file]');
const rp = Object.keys(dzInput).find(k => k.startsWith('__reactProps'));
dzInput[rp].onChange({ target: { files: [tmp.files[0]] } });
});
'bridge ready';
"
# 2. Upload via bridge
agent-browser --session "$S" upload "#cover-bridge" /tmp/cover.png
# 3. Click Save on crop modal (last Save on page)
agent-browser --session "$S" eval "
const saves = Array.from(document.querySelectorAll('button')).filter(b => b.textContent.trim() === 'Save');
saves[saves.length - 1].dispatchEvent(new MouseEvent('click', {bubbles: true}));
"
# 4. Click Save on edit course modal
agent-browser --session "$S" eval "
const save = Array.from(document.querySelectorAll('button')).find(b => b.textContent.trim() === 'Save');
save.dispatchEvent(new MouseEvent('click', {bubbles: true}));
"
# 5. Clean up
agent-browser --session "$S" eval "document.getElementById('cover-bridge')?.remove()"
```
Cover image spec: 1460 x 752 px.
### Eval-grep gotcha
`agent-browser eval` returns JSON-stringified results wrapped in another layer of quoting: `"{\"hit\":true}"`. Use `grep -qE '(\"hit\":true|\\"hit\\":true)'` or `grep -qF "$TITLE"` for substring matches.
### Additional Skool features (resources, transcripts, pinned posts)
Full browser automation recipes for resource links, resource files, transcripts, pinned community posts, and resource management (edit/reorder/delete) are documented in:
**`~/.claude/skills/snappy-course/data/skool-feature-recipes.md`**
Key facts from that reference:
- Resources and transcripts save immediately when added via ADD dropdown. They persist independently of the page SAVE button.
- CANCEL does NOT revert resource/transcript additions.
- Adding a resource link enables the page SAVE button as a side effect (useful for force-enabling SAVE).
- Resource label limit: 34 characters.
- Pinning community posts is done FROM the community feed, not the lesson editor.
### Rich formatting for lessons (available in Tiptap)
Use these to make lessons more than flat paragraphs:
```javascript
editor.commands.setContent(`
<h2>Section Title</h2>
<p>Intro with <strong>bold</strong> and <code>inline code</code>.</p>
<blockquote><p>Key takeaway here.</p></blockquote>
<ol><li><strong>Step 1:</strong> Install the tool</li></ol>
<pre><code class="language-bash">npm install @anthropic-ai/sdk</code></pre>
<hr>
<h3>Subsection</h3>
`);
```
### Updating existing lesson content
`upload-lesson.sh` is create-only (idempotency check skips existing). To update: navigate to lesson, enter edit mode, extract cover image HTML first (`child.querySelector('img')`), replace innerHTML with `coverHtml + newBody`, dirty title via fill/type+backspace, save.
## Push wave discipline -- MANDATORY sequencing
**Audits run BEFORE push pods, never in parallel.** Skool writes are irreversible from the orchestrator's POV — by the time a lint/dashboard pod's report returns, a parallel push pod has already landed defective bytes into a live lesson.
**Why:** On 2026-04-14 during the Claude Code Mastery M1 push, a dashboard pod and a push pod were dispatched in the same wave. The dashboard flagged a §4a banned word (`unlocks`) in the M1.2 draft. By the time its report arrived, the push pod had already created the Skool lesson carrying that defect live. Required a separate patch-pod round-trip.
**How to apply:**
- **Local work** (file inspections, drafts, image generation, dry-run greps): parallelize freely.
- **External-surface writes** (Skool save, API `PUT`, post publish, DM send): sequence in two waves.
- Wave 1: audit/dashboard/lint pods. Await results. Inspect output.
- Wave 2: push pods, only after Wave 1 returned clean.
- Push pods ALSO re-grep their own payload before saving (see `snappy-positioning` AGENTS.md → "Push pods: re-grep payload in-line"). Defense in depth.
- A push pod's verification certificate must show the pre-save grep came back empty for all §4a phrases.
## Ship verification protocol -- MANDATORY
**A toast is not evidence. An action is not done until verified.**
After EVERY action (edit, upload, rename, delete):
1. Click SAVE and wait for toast.
2. Navigate AWAY from the page.
3. Navigate BACK to the edited page.
4. Screenshot the rendered page in read mode.
5. Verify each change in the screenshot.
6. If images expected but not visible, enter edit mode and check `<img>` existence. Missing = upload silently failed.
**Cover-at-top pass state:** `imgIdx` in `{0, 1}` with `contentBefore === 0`. Either is valid. `imgIdx >= 2` or `contentBefore > 0` = cover is buried, must be hoisted.
**Image count verification:**
```javascript
document.querySelectorAll('.ProseMirror img, [class*=Module] img, [class*=Content] img').length
// 0 = images did not persist
```
**Certificate format:**
```
certificate:
premises: [what was supposed to change]
action: [what the agent did]
trace: [session name, timestamps]
evidence: post-navigation screenshot showing [title/body/images] rendered correctly
image_count: [expected] actual [observed]
conclusion: VERIFIED | FAILED -- [detail]
```
Hard rule: if `image_count` expected > 0 and actual = 0, the action FAILED. Re-upload.
## Classroom state (as of 2026-04-12)
### Active courses (all Draft)
| Course | Lessons | Notes |
|--------|---------|-------|
| Agentic Development Playbook | 12 | All have images |
| Skills as the OS | 6 | All have images |
| Xano + Agents | 5 | All have images |
| Ray's Mental Models | 5 | All have images |
| MCP Deep Dive | 8 | All have images |
| Tech Content | 7 | All have images |
| MCP Wednesday Vault | 1 | Intro page only, no session archives |
### Other modules (not course portfolio)
Getting Started with Snappy, Business Resources, Core Implementation Library, AG Grid/WeWeb/Webflow boilerplates, Meet Your Mentor: Daniel Migizi.
## Cross-link rules
1. Every Skool lesson links to its blog mirror at the bottom.
2. Every blog mirror links to the Skool classroom and the Slack invite.
3. Every blog mirror ends with one CTA: newsletter signup OR book a call. Pick one.
4. Every Slack `#lessons` thread is anchored to the blog URL.
5. UTMs on every outbound link: `?utm_source=<surface>&utm_medium=<format>&utm_campaign=course-launch`.
6. Skool posts referencing external resources link to canonical snappy.ai page, never third party.
## Rules
1. No LinkedIn for course. Robert said no.
2. Slack output is drafts only. Review before send.
3. Skool classroom is source of truth. Blog mirrors track Skool, not the reverse.
4. One CTA per surface. Never two equal CTAs.
5. Never invite cold. Every invite traces to a prior touchpoint in `snappy-knowledge`.
6. Voice is non-negotiable. Load `snappy-positioning` and read section 4a before drafting any copy.
## Invite flow
Priority order: active Skool members (14 days) > joined-but-never-opened > newsletter subscribers > past call no-shows > recent commenters. Channel: Skool DM primary, email secondary. Max 1 course-specific DM per member per week.
## Slack workspace
Team ID: `T0AQPFMQEKB`. Subdomain: `snappyclaudec-qdk8925.slack.com`. Channels: #welcome, #lessons, #wins, #help, #live, #general. All output goes through `snappy-slack` as drafts.
## Skool API endpoints
| Method | Endpoint | Notes |
|--------|----------|-------|
| POST | `/groups/{groupId}/update-landing-page-description` | Body: `{lpDescription}`. 1000 char limit. |
| GET/POST | `/groups/{groupName}/links` | Sidebar links. 3 maximum. |
| POST | `/groups/{groupName}/move-link` | `old` is 0-indexed, `new` is 1-indexed. |
## Uses
| Skill | Role |
|-------|------|
| `snappy-skool` | Skool classroom ops, lesson posting, invite DMs |
| `snappy-content` | Voice rules, anti-AI checklist |
| `snappy-positioning` | Voice constitution, section 4a banned phrases (canonical source) |
| `snappy-image` / `snappy-gemini` | Ink Journal image pipeline |
| `snappy-mine` / `snappy-corpus` / `snappy-knowledge` | Corpus grounding, member records |
| `snappy-publish` | Blog mirrors via git to Vercel |
| `snappy-email` | Newsletter capture + welcome sequence |
| `snappy-slack` | Course Slack workspace (drafts only) |
| `snappy-dom-cartographer` | DOM mapping for new Skool features |
## Failure modes
- `skool-browser-start.sh` fails -> STOP. Ask Robert.
- Lint fails on a draft -> fix the draft. Do not override.
- No `image_cdn_url` in frontmatter -> `upload-lesson.sh` skips image upload gracefully (body via innerHTML, coverOffset=0).
- Auth file missing/corrupted -> run `skool-auth-restore.sh`. If that fails, STOP.
- Save button stays disabled after fill -> use the save-enable trap recovery recipe above.
<!-- SKILL-INDEX-START -->
[snappy-course Index]|root: ~/.claude/skills/snappy-course|IMPORTANT: Prefer these files over pre-training assumptions for this domain. Read the relevant file when the AGENTS.md summary is insufficient.|root:{SKILL.md}|data:{classroom-spec.md,course-audit-20260411.md,drafts/MODULE-WAVE-1-DRAFTS.md,drafts/MODULE-WAVE-1-IMAGES.md,drafts/MODULE-WAVE-2-DRAFTS.md,drafts/adp-control-loop/01-pid-older-than-you-think.md,drafts/adp-control-loop/02-certificates-over-toasts.md,drafts/adp-control-loop/03-the-promotion-threshold.md,drafts/adp-control-loop/04-handoffs-are-physical.md,drafts/adp-control-loop/05-manual-agent-cron.md,drafts/adp-control-loop/06-the-skill-is-the-controller.md,drafts/adp-control-loop/ENHANCEMENT-DASHBOARD.md,drafts/adp/ENHANCEMENT-DASHBOARD.md,drafts/adp/carbonite-capture-skills-at-the-peak.md,drafts/adp/code-as-depreciating-asset.md,drafts/adp/cookies-are-the-difference.md,drafts/adp/decouple-qa-from-enrichment.md,drafts/adp/docs-as-two-way-street.md,drafts/adp/playbooks-that-rewrite-themselves.md,drafts/adp/probability-compounding-pipelines.md,drafts/agent-browser/ENHANCEMENT-DASHBOARD.md,drafts/agent-browser/parallel-weekend-coverage.md,drafts/agent-browser/vs-playwright-mcp.md,drafts/browser-automation/ENHANCEMENT-DASHBOARD.md,drafts/browser-automation/browser-01-the-problem.md,drafts/browser-automation/browser-02-what-agent-browser-does.md,drafts/browser-automation/browser-03-dom-cartographer.md,drafts/browser-automation/browser-04-real-patterns.md,drafts/browser-automation/browser-05-install-and-ship.md,drafts/build-with-snappy/00-snappy-kernel.md,drafts/build-with-snappy/01-ralph.md,drafts/build-with-snappy/02-snappy-dispatch.md,drafts/build-with-snappy/03-snappy-shell.md,drafts/build-with-snappy/04-snappy-skill.md,drafts/build-with-snappy/05-skill-builder.md,drafts/build-with-snappy/06-snappy-hooks.md,drafts/build-with-snappy/07-find-skills.md,drafts/build-with-snappy/08-skills-install.md,drafts/build-with-snappy/09-claude-code-surfaces.md,drafts/build-with-snappy/ENHANCEMENT-DASHBOARD.md,drafts/build-with-snappy/REVIVAL-DASHBOARD.md,drafts/build-with-snappy/_OUTLINE.md,drafts/build-with-snappy/_TEMPLATE.md,drafts/build-with-snappy/_live-snapshot-M2L2.md,drafts/build-with-snappy/course-cover-spec.md,drafts/build-with-snappy/module-02/01-snappy-openrouter.md,drafts/build-with-snappy/module-02/02-snappy-ai-models.md,drafts/build-with-snappy/module-02/03-snappy-gemini.md,drafts/build-with-snappy/module-02/04-research.md,drafts/build-with-snappy/module-02/05-dspy-brain.md,drafts/build-with-snappy/module-03/01-agent-browser.md,drafts/build-with-snappy/module-03/02-snappy-browse.md,drafts/build-with-snappy/module-03/03-snappy-dom-cartographer.md,drafts/build-with-snappy/module-03/04-snappy-api-sniffer.md,drafts/build-with-snappy/module-03/05-snappy-desktop.md,drafts/build-with-snappy/module-03/06-mac-mini-server.md,drafts/build-with-snappy/module-04/01-snappy-channel-contract.md,drafts/build-with-snappy/module-04/02-snappy-telegram.md,drafts/build-with-snappy/module-04/03-snappy-whatsapp.md,drafts/build-with-snappy/module-04/04-snappy-imessage.md,drafts/build-with-snappy/module-04/05-snappy-sync.md,drafts/build-with-snappy/module-04/06-snappy-walkthrough.md,drafts/build-with-snappy/module-05/01-snappy-ffmpeg.md,drafts/build-with-snappy/module-05/02-snappy-video.md,drafts/build-with-snappy/module-05/03-video-pipeline.md,drafts/build-with-snappy/module-05/04-remotion-best-practices.md,drafts/build-with-snappy/module-05/05-snappy-sensors.md,drafts/build-with-snappy/module-06/01-universal-backend.md,drafts/build-with-snappy/module-06/02-xanoscript-builder.md,drafts/build-with-snappy/module-06/03-snappy-xano-mcp.md,drafts/build-with-snappy/module-06/04-mcp-server-builder.md,drafts/build-with-snappy/module-06/05-xano-mcp-tool-developer.md,drafts/build-with-snappy/module-06/06-chatgpt-mcp-apps.md,drafts/build-with-snappy/module-06/07-snappy-box.md,drafts/build-with-snappy/module-06/08-notion-api.md,drafts/build-with-snappy/module-07/01-vercel-agents.md,drafts/build-with-snappy/module-07/02-vercel-react-best-practices.md,drafts/build-with-snappy/module-07/03-vercel-composition-patterns.md,drafts/build-with-snappy/module-07/04-vercel-react-native-skills.md,drafts/build-with-snappy/module-07/05-seymour-papert.md,drafts/build-with-snappy/module-07/06-web-design-guidelines.md,drafts/build-with-snappy/module-07/07-crayonchat.md,drafts/build-with-snappy/module-07/08-dev-terminal.md,drafts/claude-code-bridges/completion-claim-your-certificate.md,drafts/claude-code-bridges/foundation-agent-skills.md,drafts/claude-code-bridges/foundation-building-with-claude-api.md,drafts/claude-code-bridges/foundation-claude-code-in-action.md,drafts/claude-code-bridges/foundation-intro-to-mcp.md,drafts/claude-code-mastery/DASHBOARD.md,drafts/claude-code-mastery/ENHANCEMENT-DASHBOARD.md,drafts/claude-code-mastery/m1/1.1-body-revised.md,drafts/claude-code-mastery/m1/1.1-hero-spec.md,drafts/claude-code-mastery/m1/1.2-body-new.md,drafts/claude-code-mastery/m1/alt-text-1.1.md,drafts/claude-code-mastery/m1/completion-stakes.md,drafts/claude-code-mastery/m1/cta-block.md,drafts/claude-code-mastery/m1/loom-script-1.1.md,drafts/claude-code-mastery/m1/module-description.md,drafts/claude-code-mastery/m1/resources-1.1.md,drafts/claude-code-mastery/m1/resources-1.2.md,drafts/claude-code-mastery/m2/2.1-hero-spec.md,drafts/claude-code-mastery/m2/2.2-body-revised.md,drafts/claude-code-mastery/m2/2.2-inline-spec.md,drafts/claude-code-mastery/m2/inspection-report.md,drafts/claude-code-mastery/m3/3.1-hero-spec.md,drafts/claude-code-mastery/m3/3.2-body-revised.md,drafts/claude-code-mastery/m3/3.2-hero-spec.md,drafts/claude-code-mastery/m3/inspection-report.md,drafts/claude-code-mastery/m4/4.1-hero-spec.md,drafts/claude-code-mastery/m4/4.2-body-revised.md,drafts/claude-code-mastery/m4/4.2-hero-spec.md,drafts/claude-code-mastery/m4/inspection-report.md,drafts/claude-code-mastery/m5/5.1-hero-spec.md,drafts/claude-code-mastery/m5/5.2-body-revised.md,drafts/claude-code-mastery/m5/inspection-report.md,drafts/claude-code-mastery/m6/6.1-body-revised.md,drafts/claude-code-mastery/m6/6.1-hero-spec.md,drafts/claude-code-mastery/m6/inspection-report.md,drafts/claude-code/ENHANCEMENT-DASHBOARD.md,drafts/claude-code/bug-report-widget.md,drafts/claude-code/ralph-02-the-loop-pattern.md,drafts/claude-code/ralph-03-exit-criteria.md,drafts/claude-code/ralph-05-install-and-run.md,drafts/claude-code/ralph-long-running-loop.md,drafts/claude-code/remote-terminus-tmux-mac-mini.md,drafts/claude-code/skills-install-from-root.md,drafts/claude-code/terminal-vs-vscode-extension.md,drafts/claude-code/welcome-what-youll-build.md,drafts/crayonchat/ENHANCEMENT-DASHBOARD.md,drafts/crayonchat/crayonchat-01-what-it-is.md,drafts/crayonchat/crayonchat-02-wiring-backend.md,drafts/crayonchat/crayonchat-03-customization.md,drafts/crayonchat/crayonchat-04-install-and-ship.md,drafts/dspy/00-welcome-dspy.md,drafts/dspy/ENHANCEMENT-DASHBOARD.md,drafts/dspy/measuring-agent-output-in-background.md,drafts/dspy/ship-dspy-single-session.md,drafts/fly/00-welcome-fly.md,drafts/fly/ENHANCEMENT-DASHBOARD.md,drafts/fly/mcp-hot-deploy-wrap.md,drafts/fly/never-split-logic-supabase-edge.md,drafts/fly/suspended-machine-mental-model.md,drafts/fly/two-instances-dev-prod.md,drafts/gemini/00-welcome-gemini.md,drafts/gemini/ENHANCEMENT-DASHBOARD.md,drafts/gemini/claude-code-fallback.md,drafts/gemini/direct-api-for-nano-banana.md,drafts/hookdeck/00-welcome-hookdeck.md,drafts/hookdeck/ENHANCEMENT-DASHBOARD.md,drafts/hookdeck/every-webhook-day-one.md,drafts/hookdeck/migration-insurance.md,drafts/knowledge-graphs/ENHANCEMENT-DASHBOARD.md,drafts/knowledge-graphs/falkordb-01-what-is-a-knowledge-graph.md,drafts/knowledge-graphs/falkordb-02-why-falkordb.md,drafts/knowledge-graphs/falkordb-03-entity-vs-edge.md,drafts/knowledge-graphs/falkordb-04-ingestion.md,drafts/knowledge-graphs/falkordb-05-querying.md,drafts/krisp/00-welcome-krisp.md,drafts/krisp/ENHANCEMENT-DASHBOARD.md,drafts/krisp/mine-pipeline-transcript-to-lesson.md,drafts/krisp/queryable-meeting-retrospection.md,drafts/mac-mini-stack/ENHANCEMENT-DASHBOARD.md,drafts/mac-mini-stack/mac-mini-01-why-a-mac-mini.md,drafts/mac-mini-stack/mac-mini-02-hardware-and-base-install.md,drafts/mac-mini-stack/mac-mini-03-terminal-tmux-remote-access.md,drafts/mac-mini-stack/mac-mini-04-claude-code-remote.md,drafts/mac-mini-stack/mac-mini-05-one-pass-install.md,drafts/mcp-cloudflare/ENHANCEMENT-DASHBOARD.md,drafts/mcp-cloudflare/mcp-cloudflare-01-what-mcp-is-practically.md,drafts/mcp-cloudflare/mcp-cloudflare-02-why-meta-tools-beat-one-per-endpoint.md,drafts/mcp-cloudflare/mcp-cloudflare-03-cloudflare-workers-as-your-mcp-host.md,drafts/mcp-cloudflare/mcp-cloudflare-04-snappy-xano-mcp-case-study.md,drafts/mcp-cloudflare/mcp-cloudflare-05-build-your-own.md,drafts/mcp-cloudflare/mcp-cloudflare-06-when-not-to-use-mcp.md,drafts/mcp/ENHANCEMENT-DASHBOARD.md,drafts/mcp/authentication-with-oauth.md,drafts/mcp/building-mcp-from-existing-api.md,drafts/mcp/cli-vs-mcp-tradeoff.md,drafts/mcp/deploying-on-cloudflare-workers.md,drafts/mcp/designing-your-tools.md,drafts/mcp/interactive-ui-in-chat.md,drafts/mcp/mcp-as-feedback-channel-not-wrapper.md,drafts/mcp/mcp-vs-skills-vs-cli.md,drafts/mcp/nine-meta-tools-cover-four-hundred-endpoints.md,drafts/mcp/plug-your-mcp-into-your-dev-loop.md,drafts/mcp/sharing-installing-controlling-mcp.md,drafts/mcp/tool-descriptions-context-window.md,drafts/mcp/two-tier-judgment-tools.md,drafts/mcp/what-an-mcp-actually-is.md,drafts/mcp/xano-mcp-as-cli-mistake-insurance.md,drafts/ray/ENHANCEMENT-DASHBOARD.md,drafts/ray/ar-aging-the-business-line.md,drafts/ray/four-drivers-of-rate.md,drafts/ray/hours-and-minutes.md,drafts/ray/multiplication-vs-addition.md,drafts/ray/the-harness-has-opinions.md,drafts/ray/the-kitchen-sink-demo.md,drafts/ray/the-lackey-trap.md,drafts/ray/the-rewrite-trap.md,drafts/ray/the-transcript-data-asset.md,drafts/ray/you-create-the-data.md,drafts/skills-os/ENHANCEMENT-DASHBOARD.md,drafts/skills-os/cli-first-everything-else-second.md,drafts/skills-os/composition-over-orchestration.md,drafts/skills-os/half-the-code-is-explanation.md,drafts/skills-os/let-the-api-absorb-complexity.md,drafts/skills-os/pr-review-loop-skill.md,drafts/skills-os/skill-system-05-pid-loop.md,drafts/skills-os/skill-system-06-write-your-first.md,drafts/skills-os/skills-describe-capability-agents-md-orchestrates.md,drafts/skills-os/the-self-correcting-loop.md,drafts/skills-os/the-three-file-contract.md,drafts/skills-os/turn-the-skill-into-a-script.md,drafts/skills-os/what-is-a-skill-really.md,drafts/snappy-kernel/01-what-claude-md-actually-does.md,drafts/snappy-kernel/02-the-six-kernel-concerns.md,drafts/snappy-kernel/03-api-ts-direct-calls-env-cli-mode.md,drafts/snappy-kernel/04-the-passive-context-index.md,drafts/snappy-kernel/05-preload-skill-context.md,drafts/snappy-kernel/06-always-inject-and-the-footer.md,drafts/snappy-kernel/07-posttooluse-and-stop-the-regen-queue.md,drafts/snappy-kernel/08-env-cache-and-load-ts.md,drafts/snappy-kernel/09-the-bitwarden-incident.md,drafts/snappy-kernel/10-p-proportional-and-i-integral.md,drafts/snappy-kernel/11-signal-logs-consumers-and-graduation.md,drafts/snappy-kernel/12-the-five-pipelines.md,drafts/snappy-kernel/13-snappy-ops-the-front-door.md,drafts/supabase/00-welcome-supabase.md,drafts/supabase/ENHANCEMENT-DASHBOARD.md,drafts/supabase/branching-dev-prod-diffs.md,drafts/supabase/files-table-junction-pattern.md,drafts/supabase/never-put-logic-in-edge-functions.md,drafts/supabase/supabase-plus-falkordb-entity-vs-graph.md,drafts/tech-content/ENHANCEMENT-DASHBOARD.md,drafts/tech-content/claude-code-on-phone-termius-tmux.md,drafts/tech-content/claude-code-terminal-vs-vscode.md,drafts/tech-content/file-upload-pass-the-url.md,drafts/tech-content/fly-behave-through-mcp.md,drafts/tech-content/fly-neon-xano-replacement-stack.md,drafts/tech-content/flyio-mental-model-suspended-machine.md,drafts/tech-content/handy-computer-local-voice.md,drafts/tech-content/leaving-xano-prototype-fast.md,drafts/tech-content/model-swapping-is-engineering.md,drafts/tech-content/never-split-backend-supabase-fly.md,drafts/tech-content/picking-your-ai-coding-tool.md,drafts/tech-content/ralph-skill-keeps-claude-going.md,drafts/tech-content/setting-up-termius.md,drafts/tech-content/skills-install-from-root-claude.md,drafts/tech-content/snippets-sftp-port-forwarding.md,drafts/tech-content/style-references-not-descriptions.md,drafts/tech-content/termius-in-agent-dev-workflow.md,drafts/tech-content/thin-frontend-rule-for-agentic-ui.md,drafts/tech-content/tool-not-android.md,drafts/tech-content/two-fly-instances-dev-prod.md,drafts/tech-content/waterfall-enrichment.md,drafts/tech-content/webflow-to-xano-sockets-skool.md,drafts/tech-content/what-termius-is.md,drafts/tech-content/xano-hot-deploy-agentic-dev-skool.md,drafts/tech-content/xano-shopify-server-side-skool.md,drafts/tech-content/xano-to-fly-one-line-swap-skool.md,drafts/tech-content/xano-unified-payment-layer-skool.md,drafts/xano-system/ENHANCEMENT-DASHBOARD.md,drafts/xano-system/xano-01-what-xanoscript-is.md,drafts/xano-system/xano-02-the-builder-skill.md,drafts/xano-system/xano-03-common-patterns.md,drafts/xano-system/xano-04-the-three-skill-stack.md,drafts/xano-system/xano-05-install-and-first-endpoint.md,drafts/xano/ENHANCEMENT-DASHBOARD.md,drafts/xano/consent-architecture-blockchain-ledger.md,drafts/xano/hot-deploy-feedback-loop.md,drafts/xano/migration-one-line-swap.md,drafts/xano/provisioning-your-stack.md,drafts/xano/shopify-server-side-backend.md,drafts/xano/the-mcp-bridge-pattern.md,drafts/xano/the-xano-mental-model.md,drafts/xano/tiptap-r2-asset-pipeline.md,drafts/xano/unified-payment-api.md,drafts/xano/webflow-sockets-no-webhooks.md,drafts/xano/why-agents-build-faster-fly-neon.md,drafts/xano/why-you-need-a-backend.md,image-upload-audit.md,killed/20260414T060504Z/claude-code-mastery/01-welcome-claude-code-mastery.md,killed/20260414T060504Z/claude-code-mastery/02-bridge-agent-skills.md,killed/20260414T060504Z/claude-code-mastery/04-bridge-claude-api.md,killed/20260414T060504Z/claude-code-mastery/06-bridge-mcp.md,killed/20260414T060504Z/claude-code-mastery/08-bridge-claude-code-in-action.md,killed/20260414T060504Z/claude-code-mastery/10-completion-claim-certificate.md,killed/20260414T060504Z/ralph-loops/ralph-01-the-quit-early-problem.md,killed/20260414T060504Z/ralph-loops/ralph-02-the-loop-pattern.md,killed/20260414T060504Z/ralph-loops/ralph-03-exit-criteria.md,killed/20260414T060504Z/ralph-loops/ralph-04-real-use-cases.md,killed/20260414T060504Z/ralph-loops/ralph-05-install-and-run.md,killed/20260414T060504Z/skill-system/skill-system-01-what-a-skill-is.md,killed/20260414T060504Z/skill-system/skill-system-02-finding-and-loading.md,killed/20260414T060504Z/skill-system/skill-system-03-the-gateway.md,killed/20260414T060504Z/skill-system/skill-system-04-composition.md,killed/20260414T060504Z/skill-system/skill-system-05-pid-loop.md,killed/20260414T060504Z/skill-system/skill-system-06-write-your-first.md,killed/20260414T060504Z/snappy-kernel/01-what-claude-md-actually-does.md,killed/20260414T060504Z/snappy-kernel/02-the-six-kernel-concerns.md,killed/20260414T060504Z/snappy-kernel/03-skill-md-agents-md-api-ts.md,killed/20260414T060504Z/snappy-kernel/04-api-ts-direct-calls-env-cli-mode.md,killed/20260414T060504Z/snappy-kernel/05-the-passive-context-index.md,killed/20260414T060504Z/snappy-kernel/06-preload-skill-context.md,killed/20260414T060504Z/snappy-kernel/07-always-inject-and-the-footer.md,killed/20260414T060504Z/snappy-kernel/08-posttooluse-and-stop-the-regen-queue.md,killed/20260414T060504Z/snappy-kernel/09-env-cache-and-load-ts.md,killed/20260414T060504Z/snappy-kernel/10-the-bitwarden-incident.md,killed/20260414T060504Z/snappy-kernel/11-p-proportional-and-i-integral.md,killed/20260414T060504Z/snappy-kernel/12-d-derivative-trend-detection.md,killed/20260414T060504Z/snappy-kernel/13-signal-logs-consumers-and-graduation.md,killed/20260414T060504Z/snappy-kernel/14-the-five-pipelines.md,killed/20260414T060504Z/snappy-kernel/15-snappy-ops-the-front-door.md,pass-log-entry-falkordb-knowledge-graphs-2026-04-13.md,pass-log.md,polish-pod-d-2026-04-11.md,polish-queue-20260411.md,ray-content/MANIFEST.md,ray-content/consulting-economics/running-yourself-as-a-business.md,ray-content/consulting-economics/the-5k-retainer-model.md,ray-content/consulting-economics/the-rate-increase.md,ray-content/consulting-economics/vendor-angle.md,ray-content/mental-models/durable-pipeline-illusion.md,ray-content/mental-models/earned-brag.md,ray-content/mental-models/premium-vs-totality.md,ray-content/mental-models/the-hidden-gem-problem.md,ray-content/mental-models/two-names.md,ray-content/mentorship-to-content/extract-dont-rewrite.md,ray-content/mentorship-to-content/inspire-dont-inform.md,ray-content/mentorship-to-content/seeds-you-dont-know.md,ray-content/mentorship-to-content/the-101-course.md,ray-content/mentorship-to-content/the-vj-story.md,ray-content/weekly-rhythm/sell-ship-study.md,ray-content/weekly-rhythm/short-and-sharp.md,ray-content/weekly-rhythm/the-weekly-report.md,refinement-queue.md,skool-feature-recipes.md,tech-index-summary.md,upload-log-20260410.md,upload-log-20260411.md,wave2-learnings.md,wave3-ray-candidates.md,wave3-tech-candidates.md}
<!-- SKILL-INDEX-END -->
## Used by
- `snappy-image`
- `snappy-remotion`
<!-- SNAPPY-CONTRACT-VERBS-START -->
## Contract verbs
Generated from `api.ts` `HAND_CONTRACT`. Do not hand-edit this block.
| Verb | Contract arguments | Effect | First call |
|---|---|---|---|
| `module` | `module-id` | `read` | `npx tsx ~/.claude/skills/snappy-course/api.ts module <module-id>` |
| `modules` | — | `read` | `npx tsx ~/.claude/skills/snappy-course/api.ts modules` |
## Show the result
When an answer carries `face_hint`, show it with one `snappy_present(<answer>)` call.
See `/snappy-faces` for face selection. Human-facing images must crop to the
element, render at 2x on Retina, and fill the destination channel instead of
placing a small card in a full-page screenshot.
<!-- SNAPPY-CONTRACT-VERBS-END -->