Deck Review Skill
SkillDocs & knowledgeScores and strengthens startup pitch decks (pre-seed through Series A) against 35 investor-grade criteria grounded in Sequoia, DocSend, YC, a16z, and Carta data. Run the scored rubric rather than giving deck advice from memory.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Deck Review Skill skill
What this skill tells your AI
The instructions your AI receives, as published by lool-ventures/founder-skills in founder-skills/skills/deck-review/SKILL.md and read by ahel’s review.
Help startup founders strengthen their pitch decks before sending them to investors. Produce a structured, scored review with specific, actionable recommendations grounded in current best practices from Sequoia, DocSend, YC, a16z, and Carta data. The tone is founder-first: a candid coaching session, not a VC evaluation.
Skill Metadata
- Author: lool-ventures
- Version: managed in
founder-skills/.claude-plugin/plugin.json - Compatibility: Python 3.10+ and
uvfor script execution. - Exports:
checklist.json→financial-model-review,ic-sim,fundraise-readiness(future)
Skill Execution Model (READ FIRST)
See
founder-skills/references/skill-execution-model.mdfor the full inline-skill execution model (3 dispatch contexts, Mitigation 1+2, producer contract, Cowork quirks, per-symptom triage).
This skill runs inline in the main thread, not as a sub-agent — see the reference above ("Why Inline (Not Forked Sub-Agent)") for the rationale. Sub-agents are deliberately shell-free, so orchestration (producer scripts, artifact persistence) stays in the main thread.
Two dispatch contexts for the sub-agent:
- Context A — Per-step analytical dispatch (Mitigation 1): Steps 4 and 5 dispatch the deck-review agent via the
Tasktool. The agent does deep analysis, WRITES its output JSON to theOUTPUT_PATHgiven in its prompt (thehandoff/dir), and returns a small receipt. The main thread gates the file withcheck_handoff.py, then pipes it through the producer script (slide_reviews.pyorchecklist.py). The sub-agent never writes canonical artifacts — only its hand-off file. - Context B — Post-compose coaching dispatch: Step 7 dispatches the sub-agent after
compose_report.pywritesreport.md. The sub-agent Reads the stagedcoaching_payload.jsonfrom the hand-off dir (Mitigation 2) — it does NOT Read the fullreport.md— composes the coaching commentary, WRITES it to theOUTPUT_PATHhand-off file, and returns a small receipt. The main thread gates the file (check_handoff.py) and inserts it via the sharedinsert_coaching.pyscript (idempotency matrix, uuid-marker replacement, run_id-parity verification — all deterministic). See the reference above for the full Context B contract.
Tolerant JSON extraction protocol (Context B returns; also the Context A message-channel fallback): capture the sub-agent's final assistant message. It should be raw JSON, but may be wrapped in ```json ... ``` fences or carry a prose preamble. Extract tolerantly:
- If the message is wrapped in a
```json ... ```(or plain``` ... ```) fence, strip the fence first. - Try to parse the stripped text directly as JSON.
- If that fails, walk through the text looking for the first
{character and tryjson.JSONDecoder().raw_decode(text[i:])— this is brace-aware and handles nested objects correctly (unlike regex, which truncates on the first}). - If extraction fails entirely, re-prompt the sub-agent with: "Your previous reply could not be parsed as JSON. Return ONLY the JSON object — no markdown fences, no prose preamble."
Context A receipts don't need this protocol by hand — check_handoff.py --receipt-json - applies the same tolerant extraction internally; pass the final message verbatim.
Input Formats
Accept any format: PDF, PowerPoint (PPTX/PPT), markdown, or text descriptions of slides. PowerPoint is converted to PDF at ingestion (Step 2) so the slides can actually be seen; without a converter the review degrades to text-only and says so.
Available Scripts
All scripts are at ${CLAUDE_PLUGIN_ROOT}/skills/deck-review/scripts/:
setup_run.py— ResolvesREVIEW_DIR, detects resume vs. fresh run, cleans stale artifacts (--clean)deck_inventory.py— Producer fordeck_inventory.json(agent provides JSON via stdin; schema-validated)stage_profile.py— Producer forstage_profile.json;--rebuild-stage+--confidence {high,low}for founder-corrected stagesgate_state.py— Producer (emit) + answer-writer (answer) for the stage-confirmation gateledger.py— Producer forledger.json; refuses a figure whosevaluedisagrees with its ownrawstringreconcile.py— Producer forreconciliation.json; corroborates each figure's quote against the second read, computes the proposed relations, and decides which reach the founderslide_reviews.py— Producer forslide_reviews.json(agent provides JSON via stdin; schema-validated).--reconciliationis required: the numeric chain must have run for this run_idchecklist.py— Scores 35 criteria across 7 categories (pass/fail/warn/not_applicable)compose_report.py— Assembles artifacts into final report with cross-artifact validation;--strictexits 1 on high/medium warningsvisualize.py— Generates self-contained HTML with SVG charts (not JSON)
Also available from ${CLAUDE_PLUGIN_ROOT}/scripts/ (shared):
founder_context.py— Per-company context management (init/read/merge/validate)
Run with: python3 ${CLAUDE_PLUGIN_ROOT}/skills/deck-review/scripts/<script>.py --pretty [args]
Available References
Read as needed from ${CLAUDE_PLUGIN_ROOT}/skills/deck-review/references/:
deck-best-practices.md— Full best practices: slide frameworks, stage-specific guidelines, design rules, AI-company requirementschecklist-criteria.md— Definitions for all 35 criteria with pass/fail/warn thresholdsartifact-schemas.md— JSON schemas for all artifacts
Artifact Pipeline
Every review deposits structured JSON artifacts into a working directory. The final step assembles all artifacts into a report and validates consistency. This is not optional.
| Step | Artifact | Producer |
|---|---|---|
| 1 | founder context | founder_context.py read/init |
| 2 | deck_inventory.json | deck_inventory.py (agent provides JSON via stdin) |
| 3 | stage_profile.json | stage_profile.py (agent provides JSON via stdin) |
| 3.5 | ledger.json | ledger.py (agent provides JSON via stdin) |
| 3.6 | second_read.json | a ledger-blind re-read of the figure-bearing slide text |
| 3.8 | reconciliation.json | reconcile.py (agent proposes relations via stdin) |
| 4 | slide_reviews.json | slide_reviews.py (agent provides JSON via stdin) |
| 5 | checklist.json | checklist.py |
| 6 | Report | compose_report.py (writes both report.json and report.md) |
Rules:
- Deposit each artifact before proceeding to the next step
- For producer-script artifacts (Steps 2-4, including 3.5 and 3.8), the agent supplies JSON on stdin and the script schema-validates against
references/schemas/<artifact>.schema.json. Never write artifacts directly viaWriteorEdit— always pipe through the producer script sometadata.run_idis injected and the schema is enforced. - If a step is not applicable, deposit a stub:
{"skipped": true, "reason": "..."}
Keep the founder informed with brief, plain-language updates at each step. Narrate the founder-visible OUTCOME, never the internal step. That is the test, and it catches more than a word list can: the forbidden thing is not a syntax, it is talking about the machinery. Bad — "Gating and piping the extraction through the producer, then staging the coaching hand-off"; good — "I've checked your numbers and I'm writing up what stood out." Never name an internal artifact, field, or token (a payload key, a marker name, an artifact filename, a hand-off dir) even in plain prose with no backticks — a detector keyed on syntax cannot see "gated", "hand-off" or "canonical artifacts", but the founder still reads them and they mean nothing to them. The between-step progress lines are the primary leak vector, not the final summary. Say the outcome of each transition: "Reading your deck slide by slide", "Your figures line up — moving on to the slide review", "Finishing up and putting the report together". Also excluded: file/script names, paths, *.py, --flags, $vars, exit codes ("Exit N", "not found"), W_/E_ codes, JSON, and step/route labels ("Lane N", "Context A/B", "Phase N", "structure detection", "the grid", any ALL_CAPS_TOKEN). After each analytical step (4–5), share a one-sentence finding before moving on. The task tracker is founder-visible too — the same rule governs its labels. "Gate the slide-review handoff" is a leak even though it names a real step, and even when the prose around it is clean. Label each task by the founder-visible outcome — "Check your inputs", "Score against the review", "Write up what I found" — never by a file, directory, script, or pipeline stage.
Workflow
Step 0: Path Setup
Every Bash tool call runs in a fresh shell — variables do not persist. A stale reference does not error, it silently expands to empty (a path quietly becomes /inputs.json). Run the block below exactly once: it resolves $PLUGIN_ROOT deterministically, and every later block must substitute the printed value as a literal rather than re-running the resolution — repeating the self-heal search can land on a different mount than Step 0 picked when more than one is present (see why in the block's comments). $RUN_ID is minted once below, then re-established authoritatively by setup_run.py's printed run_id (Step 1, which decides resume-vs-fresh) — never re-run the mint line below in a later block. Read the printed values out of each Bash call's output (PLUGIN_ROOT and ARTIFACTS_ROOT here, then review_dir/run_id/resume/gate_answer after Step 1) and paste them as literals into every subsequent block; do not carry a variable forward and assume it survived.
Optional, best-effort, and via the Read tool (not a shell command): before the block below, Read ${CLAUDE_PLUGIN_ROOT}/.claude-plugin/plugin.json and note its version field as EXPECT_VERSION. Passing it to select_plugin_root.py below lets an exact version match win over an arbitrary first hit. If the Read fails, skip it and omit --expect-version — selection is still deterministic without it.
SCRIPTS="${CLAUDE_PLUGIN_ROOT}/skills/deck-review/scripts"
if [ ! -d "$SCRIPTS" ]; then
# In Cowork, CLAUDE_PLUGIN_ROOT is a host path absent inside the VM. Collect EVERY
# candidate mount (a session can have several) and let select_plugin_root.py pick one
# deterministically — never trust `find`'s first hit, which mixes plugin versions.
CANDIDATES="$(find /sessions -type d -path '*/skills/deck-review/scripts' 2>/dev/null)"
[ -n "$CANDIDATES" ] || CANDIDATES="$(find / -type d -path '*/skills/deck-review/scripts' 2>/dev/null)"
PROVISIONAL_ROOT="$(printf '%s\n' "$CANDIDATES" | head -1)"
PROVISIONAL_ROOT="${PROVISIONAL_ROOT%/skills/*}"
# Bootstrap order: $SHARED_SCRIPTS isn't known until a root is chosen, so use the
# provisional root's OWN copy of the selector; an older plugin copy without one
# falls back to the provisional root unchanged.
SELECTOR="$PROVISIONAL_ROOT/scripts/select_plugin_root.py"
if [ -f "$SELECTOR" ]; then
if [ -n "$EXPECT_VERSION" ]; then
PLUGIN_ROOT="$(printf '%s\n' "$CANDIDATES" | python3 "$SELECTOR" --expect-version "$EXPECT_VERSION")"
else
PLUGIN_ROOT="$(printf '%s\n' "$CANDIDATES" | python3 "$SELECTOR")"
fi
else
PLUGIN_ROOT="$PROVISIONAL_ROOT"
fi
SCRIPTS="$PLUGIN_ROOT/skills/deck-review/scripts"
fi
PLUGIN_ROOT="${SCRIPTS%/skills/*}"
echo "PLUGIN_ROOT=$PLUGIN_ROOT" # resolved ONCE, here — paste this literal into every later block; never re-run this resolution
REFS="$PLUGIN_ROOT/skills/deck-review/references"
SHARED_SCRIPTS="$PLUGIN_ROOT/scripts"
# Resolve the artifacts root via the SCRIPT, never inline bash: an inline computation gets
# paraphrased into outputs/ one run and outputs/artifacts/ the next, desyncing find_artifact.py.
python3 "$SHARED_SCRIPTS/resolve_artifacts_root.py" # prints ARTIFACTS_ROOT — use the printed path verbatim as ARTIFACTS_ROOT in every later block (a captured var dies in the next fresh shell)
# RUN_ID — used by Step 1 (founder_context init) before slug-aware setup_run.py
# runs, then passed to setup_run via --run-id. If the caller's task prompt
# supplied a RUN_ID (resume), keep it; otherwise mint a fresh one.
RUN_ID="${RUN_ID:-$(date -u +%Y%m%dT%H%M%SZ)}"
Reaching the self-heal branch is normal in Cowork — ${CLAUDE_PLUGIN_ROOT} resolves to a HOST path that does not exist inside the VM, so the [ ! -d "$SCRIPTS" ] test fails by design rather than by misconfiguration. It is not a sign anything is wrong, and it is not worth narrating to the founder.
Outputs mount is append-only. Everything under the promoted outputs mount (.../mnt/outputs/, not just $REVIEW_DIR) is write-allowed and delete-denied by the platform: never rm, move away, or empty anything under it — including files you created yourself. Never create ad-hoc scratch anywhere under the outputs mount (no _src/ copies, no run-state note files); scratch belongs in $STAGING_DIR (a /tmp dir, defined below). Do not "clean up" the outputs folder before delivering — extra working files there are expected and harmless. The uploaded deck is already readable in place from the uploads mount; never copy it under outputs to make it readable.
There is no quick-check lane here, and that is deliberate. The 35 criteria are scored from per-slide sub-agent reviews — those reviews ARE the work, so dropping them leaves only the checklist scaffolding. So when the founder asks a small conversational question, do not improvise an answer from your own reasoning under this skill's name — an unproduced score is exactly the output a founder over-trusts. Instead, say up front what the full run costs and let them choose: "Answering that properly means running the full deck review — it takes several minutes and produces a scored report across all 35 criteria. I can run it now, or if you just want my read without the scoring, say so and I'll answer outside the deck review." Naming the trade-off is honest; quietly substituting the cheap version is not.
After Step 1 (when the slug is known) — substitute $SLUG below with the company slug from Step 1's printed JSON, then call setup_run.py to resolve REVIEW_DIR, detect whether this is a resume, and clean stale state in one atomic step. Always call setup_run.py with --clean and --run-id "$RUN_ID"; do not pre-read gate_state.json yourself. setup_run.py decides resume vs. fresh by comparing the answered gate_state.json's run_id against --run-id, and on a fresh (non-resume) run it deletes a stale answered gate_state.json so a prior completed run cannot be misread as a resume:
python3 "$SCRIPTS/setup_run.py" \
--artifacts-root "$ARTIFACTS_ROOT" \
--slug "$SLUG" \
--run-id "$RUN_ID" \
--clean \
--pretty
Read review_dir, run_id, resume, reuse_checkpoints, gate_id, gate_action, and gate_answer from the JSON printed by the previous Bash command. gate_action is what to do next — branch on it, not on the answer string. It is one of:
gate_action | what it means |
|---|---|
continue | the founder confirmed; proceed |
continue_if_rebuilt | they said "proceed anyway"; rebuild the profile at low confidence FIRST, then proceed |
rebuild | an intermediate answer (Different stage, or a stage_choice pick): rebuild and re-emit the confirmation gate — this run is not finished asking |
stop | the founder declined the review. Stop. Produce nothing. |
reask | no usable answer; emit the gate |
Read gate_id too when you act on the answer: "Seed" means one thing on stage_choice and nothing at all on the others, and the answer string alone cannot tell you which gate you are resuming. Substitute REVIEW_DIR with the review_dir value, RUN_ID with the run_id value, and IS_RESUMING with 1 if resume is true, else empty, in every subsequent bash block. Then:
# Context A hand-off dir — PER RUN: sub-agents WRITE their raw output JSON here (audit trail,
# pre-validation; never a canonical artifact). The $RUN_ID segment is load-bearing — it stops a
# stale prior-run file passing the hand-off gate when a dispatch fails to write.
HANDOFF_DIR="$REVIEW_DIR/handoff/$RUN_ID"
mkdir -p "$HANDOFF_DIR"
# Sub-agents address the SAME dir by a different path (their file tools are rooted at the outputs
# mount in Cowork). Resolve agent-namespace paths via the script — never hand-splice the printed
# root with a literal skill/slug/run-id string, which is the non-determinism it exists to remove:
python3 "$SHARED_SCRIPTS/resolve_artifacts_root.py" --handoff-dir-agent \
--dir-name "deck-review-${SLUG}" --run-id "$RUN_ID" # prints HANDOFF_AGENT verbatim
HANDOFF_AGENT="<printed value>" # use verbatim in OUTPUT_PATH lines
# Sub-agent READ paths for under-outputs artifacts use the SAME agent namespace (relative — the
# sub-agent's file-tool cwd IS the outputs mount on host-loop; an absolute /sessions/... read is denied):
python3 "$SHARED_SCRIPTS/resolve_artifacts_root.py" --analysis-dir-agent \
--dir-name "deck-review-${SLUG}" # prints the dir in the agent namespace
REVIEW_DIR_AGENT="<printed value>" # e.g. stage_profile.json, deck_inventory.json reads
# Ad-hoc scratch (NOT hand-off) lives OUTSIDE the outputs/ tree, where it is safe to create and
# reclaim. Use the printed path verbatim in later steps.
STAGING_DIR="$(mktemp -d "${TMPDIR:-/tmp}/deck-review-${SLUG:-deck}.staging.XXXXXX")"
To resume across a gate round-trip, the caller's task prompt must supply the prior RUN_ID (so RUN_ID above is set before this block runs). Then setup_run.py sees the answered gate_state.json whose run_id matches and returns resume: true — and because resume is true, --clean leaves gate_state.json, deck_inventory.json, and stage_profile.json in place (they are same-run checkpoints for this RUN_ID).
Pass RUN_ID to every producer script via --run-id. Producer scripts inject it into metadata.run_id automatically. compose_report.py enforces that all required artifacts share the same run_id and emits a MISSING_METADATA (high) warning for any artifact without one. Keeping RUN_ID stable across the gate is what prevents a STALE_ARTIFACT mismatch with the pre-gate artifacts.
When reuse_checkpoints is true: gate_state.json, deck_inventory.json, and stage_profile.json survived --clean because they belong to THIS run. Skip Steps 2 and 3 if both deck_inventory.json and stage_profile.json exist and their metadata.run_id matches $RUN_ID; otherwise re-run them with the same RUN_ID.
Read reuse_checkpoints, not resume, for this decision — they are different questions and they do come apart. resume says the gate may be skipped; reuse_checkpoints says the artifacts on disk are this run's. A same-run gate answered without a recorded source yields resume: false with reuse_checkpoints: true: ask the founder again, but keep what Steps 2-3 already produced. Keying the skip on resume re-runs and overwrites them, spending the three dispatches the preservation exists to protect. Apply the same rule to Steps 3.5-3.8: skip them when reconciliation.json exists with a matching metadata.run_id. It is the most expensive stretch of the pipeline — three dispatches, two of which read the deck — and re-running it on a gate round-trip spends that twice for an identical result.
Step 1: Read or Create Founder Context
python3 "$SHARED_SCRIPTS/founder_context.py" read --artifacts-root "$ARTIFACTS_ROOT" --pretty
Exit 0 (found): Use the company slug and pre-filled fields. Proceed to Step 2.
Exit 1 (not found): Expected on a first run — do NOT mention this check or its exit status to the founder; if you narrate anything first, say only "Let me grab a few basics about the company." First skim the attached deck (title slide, footer, contact block — a Read of the file is available now; do NOT write any artifact yet) to derive candidate values. Then use AskUserQuestion (NOT plain chat) to ask for company name, stage, sector, and geography, pre-filling each question's first option with the deck-derived value (company name from the title slide; sector/geography from deck signals such as customer names, currency, phone country codes), labeled as read from the deck; keep free-form options for correction. If AskUserQuestion is genuinely unavailable in the host, do NOT skip the ask and do NOT assume the answer: ask the same question in plain chat, state the options explicitly, and wait for an answer before continuing. The ban above is on asking casually WHILE the tool is available — it is not a reason to stall a host that lacks it. If the deck yields no signal for a field, ask as normal. Deriving some of the four does not license skipping the ask. Treat them independently: a deck that evidences company, stage and sector but says nothing about geography leaves you asking for geography — not filling it in and moving on. Never record a value the materials do not evidence, and in particular never read geography off a currency symbol ($ is also CAD, AUD and SGD, and founders everywhere price in USD) or off where the team used to work (an ex-Stripe engineer is not a US company). Geography selects which regulatory and benchmark guidance the whole review is graded against, so a silent guess there is not a small one.
Stage is the exception to deck-derived pre-filling — it has a real fixed label set, not a value to read off a slide.
Options: Pre-seed / Seed / Series A / Series B+
→ pre-seed | seed | series-a | series-b (founder_context.py's VALID_STAGES has 7 values including series-c/series-d/later; on a Series B+ pick, ask a plain-text follow-up rather than defaulting to series-b). This is founder_context.py's company-stage field, distinct from the deck-scope --rebuild-stage enum the later Gate uses (pre_seed/seed/series_a/series_b/growth, at :377 below) — the two do not share a value set. Provide at least 2 options. Stage is re-confirmed later by the Gate, so Step 1's stage answer is a prior, not a commitment. Then create:
python3 "$SHARED_SCRIPTS/founder_context.py" init \
--company-name "Acme Corp" --stage seed --sector "B2B SaaS" \
--geography "US" --artifacts-root "$ARTIFACTS_ROOT" \
--run-id "$RUN_ID"
Exit 2 (multiple): Present the list, ask which company, re-read with --slug.
Execution checkpoint — END OF STEP 1, READ BEFORE CONTINUING
You now have enough to run. Invoking this skill is not the same as running it. From here, every number that reaches the founder must come out of a producer script. Concretely:
Shortened here. Read the whole file on GitHub.
Signals
- GitHub stars
- 34
- Forks
- 3
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
deck-review- Source
- github.com/lool-ventures/founder-skills