Market Sizing Skill
Help startup founders build credible, defensible TAM/SAM/SOM analysis — the kind that earns investor trust rather than raising eyebrows. Produce a structured, validated market sizing with external sources, sensitivity testing, and a self-check against common pitfalls. The tone is founder-first: a rigorous but supportive coaching session.
Skill Metadata
- Author: lool-ventures
- Version: managed in
founder-skills/.claude-plugin/plugin.json - Compatibility: Python 3.10+ and
uvfor script execution. - Exports:
sizing.json→financial-model-review,ic-sim,fundraise-readinesssensitivity.json→financial-model-review
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, web research) stays in the main thread.
Two dispatch contexts for the sub-agent:
- Context A — Per-step analytical dispatch (Mitigation 1): Steps 5 and 6 dispatch the market-sizing agent via the
Tasktool. The key element here is parallel dispatch: Step 5 (methodology calculation) dispatches the agent twice simultaneously — one for TOP_DOWN_METHODOLOGY and one for BOTTOM_UP_METHODOLOGY — in a single assistant turn when the methodology is "both". The sub-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 (market_sizing.py --stdin). The sub-agent never writes canonical artifacts — only its hand-off file. - Context B — Post-compose coaching dispatch: The final step dispatches the sub-agent after
compose_report.py --write-mdhas writtenreport.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.
Research-before-dispatch pattern: The main thread performs web research (WebFetch/WebSearch, or the host's equivalents) BEFORE dispatching sub-agents. Research data is passed inline in the sub-agent prompts. This skill's sub-agent tools: allowlist deliberately includes no network tools (a design choice, not a platform limit — see the reference), so research runs in the main thread and is passed inline.
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: pitch deck (PDF, PPTX, markdown), financial model, market data, text descriptions, or verbal description of the business.
Available Scripts
All scripts are at ${CLAUDE_PLUGIN_ROOT}/skills/market-sizing/scripts/:
market_sizing.py— TAM/SAM/SOM calculator (top-down, bottom-up, or both); accepts--stdinfor JSON pipingsensitivity.py— Stress-test assumptions with low/base/high ranges and confidence-based auto-wideningchecklist.py— Validates 22-item self-check with pass/fail per itemcompose_report.py— Assembles report with cross-artifact validation;--write-mdwrites report.md;--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/market-sizing/scripts/<script>.py --pretty [args]
Available References
Read as needed from ${CLAUDE_PLUGIN_ROOT}/skills/market-sizing/references/:
tam-sam-som-methodology.md— Definitions, calculation methods, industry examples, best practicespitfalls-checklist.md— Self-review checklist for common mistakesartifact-schemas.md— JSON schemas for all analysis artifacts
Artifact Pipeline
Every analysis 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 | inputs.json |
Agent (heredoc) |
| 3 | methodology.json |
Agent (heredoc) |
| 4 | validation.json |
Main thread (WebFetch/WebSearch research) |
| 5 | sizing.json |
Context A dispatch: TOP_DOWN_METHODOLOGY + BOTTOM_UP_METHODOLOGY in parallel → market_sizing.py --stdin |
| 6a | sensitivity.json |
Context A dispatch: SENSITIVITY_TEST → sensitivity.py |
| 6b | checklist.json |
Context A dispatch: CHECKLIST → checklist.py |
| 7 | Report | compose_report.py --write-md (writes both report.json and report.md) |
| 8 | Coaching | Context B dispatch: POST_COMPOSE_COACHING |
Rules:
- Deposit each artifact before proceeding to the next step
- For agent-written artifacts (Steps 2-4), consult
references/artifact-schemas.mdfor the JSON schema - If a step is not applicable, deposit a stub:
{"skipped": true, "reason": "..."} - Do NOT use
isolation: "worktree"for sub-agents — files written in a worktree won't appear in the main$ANALYSIS_DIR
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 to apply, 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." Bad — "schema-drift warning on coaching_payload"; good — nothing, because the founder has no stake in it. 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 still mean nothing to them. The between-step progress lines are the primary leak vector, not the final summary. They feel internal — you are narrating what you are about to do — but the founder reads every one of them, and this is where the leaks actually appear: "Now gating the hand-off before piping through the checklist producer", "Gate 1 passes", "Running the final verification gate". Rewrite each pipeline transition as the founder-visible outcome: "Checking your numbers against the 46-point review", "Your inputs look consistent — moving on to unit economics", "Finishing up and putting the report together". If a progress line would mean nothing to someone who has never seen this skill's internals, it does not belong in the channel. Also excluded, as before: 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 (5–6), share a one-sentence finding before moving on. The task tracker is founder-visible too — the same rule governs its labels. "Gate the inputs review handoff", "Validate inputs.json", "resolve agent namespace paths", "Initialize founder context" are leaks even though each names a real step, and even when the prose around them 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. 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).
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/market-sizing/scripts"
if [ ! -d "$SCRIPTS" ]; then
# In Cowork, CLAUDE_PLUGIN_ROOT substitutes to a host-side path absent inside
# the session VM — self-heal by collecting EVERY candidate mount (a session can
# have more than one at once: a stale host-side cache, a test marketplace, even
# a symlink into a different session's tree) and handing them to
# select_plugin_root.py, which picks ONE deterministically and names the
# rejects — never trust `find`'s arbitrary first hit, which can silently mix
# scripts across plugin versions mid-pipeline.
CANDIDATES="$(find /sessions -type d -path '*/skills/market-sizing/scripts' 2>/dev/null)"
[ -n "$CANDIDATES" ] || CANDIDATES="$(find / -type d -path '*/skills/market-sizing/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/market-sizing/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/market-sizing/references"
SHARED_SCRIPTS="$PLUGIN_ROOT/scripts"
SHARED_REFS="$PLUGIN_ROOT/references"
# Resolve the canonical artifacts root via a SCRIPT, not inline bash (the agent paraphrases inline
# path computations → outputs/ vs outputs/artifacts/ drift across runs). Deterministic + creates it.
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)
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 $ANALYSIS_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.
If ARTIFACTS_ROOT resolves to $(pwd)/artifacts but no artifacts/ directory exists at $(pwd): Use Glob with pattern **/artifacts/founder_context.json to locate existing artifacts, and derive ARTIFACTS_ROOT from the result. If nothing is found, mkdir -p "$ARTIFACTS_ROOT" and proceed.
After Step 1 (when the slug is known), derive ANALYSIS_DIR. Two modes — pick exactly one:
- Full analysis (default — the founder shared materials, asked for a TAM/SAM/SOM analysis or a
report, OR there is no existing full analysis for this slug): run Steps 2–10.
ANALYSIS_DIR="$ARTIFACTS_ROOT/market-sizing-${SLUG}". - Quick-check mode — a single directional sizing question in conversation, with no materials
attached and no request for an analysis or report ("roughly how big is this market if we charge
$15k to 18,000 pharmacies?", "does a €2B TAM sound plausible for X?"). Run Step 5-quick instead of
Steps 2–10.
ANALYSIS_DIR="$ARTIFACTS_ROOT/market-sizing-${SLUG}-quickcheck".
Tie-breaker when both bullets seem to fit — and they often will. A founder who supplies complete inputs conversationally ("size the market: 18,000 pharmacies at €15k, 35% serviceable, 2% capture") matches the full-analysis bullet on what they asked for and the quick-check bullet on how they asked. Decide on the verb, not the inputs:
- "size the market", "analyze", "build me a TAM", "I need this for a deck" ⇒ full analysis, even when every number is already in hand. They asked for the work product, and the sourcing, sensitivity and 22-item check are the work product.
- "roughly", "ballpark", "sanity-check", "does X sound right", "how big is" ⇒ quick-check, even when materials are attached.
Complete inputs are not a signal for quick-check. They make the full analysis faster, not less wanted. When the verb is genuinely absent — a bare list of numbers with no request — default to full analysis and say you did: an unwanted full run costs the founder time, an unwanted quick check costs them the analysis they came for.
Never answer a sizing question from your own arithmetic. Quick-check exists because the alternative a model reaches for — computing the number in its head and offering the real analysis as an opt-in — produces a figure with no provenance, no sensitivity range, and no record, under this skill's name. Running fewer producers is fine; running none is not.
Step 5-quick: the quick-check path
Run the same producer the full pipeline uses, with only the inputs the founder gave you:
printf '%s' "$QUICK_JSON" | python3 "$SCRIPTS/market_sizing.py" --stdin --pretty \
--run-id "$RUN_ID" --currency "$CURRENCY" -o "$ANALYSIS_DIR/sizing.json"
Producers deliberately NOT run: external validation (Step 4), sensitivity.py, checklist.py,
compose_report.py, visualize.py, and the Context-B coaching dispatch. No report.md is written.
Same-numbers guarantee. The TAM/SAM/SOM figures are identical to what the full analysis would compute from the same inputs — it is the same script reading the same shape. Only the production weight is dropped. What you do not get is what those skipped producers add: sourced assumptions, a low/base/high range, the 22-item quality check, and the deck-claim reconciliation.
Presenting it. Label it a quick check, not an analysis. State the figures, name the inputs they came from, and say plainly that the assumptions are unsourced and unstressed. Then close with a statement, never a question: "The full analysis sources each assumption, stress-tests the range, and produces a report you can put in front of an investor — say the word and I'll run it." A question invites a "no" to something the founder would have wanted.
ANALYSIS_DIR="${ANALYSIS_DIR:-$ARTIFACTS_ROOT/market-sizing-${SLUG}}" # full analysis
# ANALYSIS_DIR="${ANALYSIS_DIR:-$ARTIFACTS_ROOT/market-sizing-${SLUG}-quickcheck}" # quick check
mkdir -p "$ANALYSIS_DIR"
RUN_ID="$(date -u +%Y%m%dT%H%M%SZ)"
# Context A hand-off dir — PER RUN: sub-agents WRITE their raw output JSON here (the audit trail —
# raw sub-agent output as returned, before producer validation). Permanent by platform design
# (outputs/ mounts are write-allowed / delete-denied); nothing in it is ever a canonical artifact.
# The $RUN_ID segment is load-bearing: it prevents a stale prior-run file from silently passing
# the hand-off gate when a dispatch fails to write.
HANDOFF_DIR="$ANALYSIS_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 the FULL agent-namespace paths via the script — never hand-splice the
# printed root with a literal skill-name/slug/run-id string yourself (that string-splicing is
# exactly the non-determinism the resolver script exists to remove):
python3 "$SHARED_SCRIPTS/resolve_artifacts_root.py" --handoff-dir-agent \
--dir-name "market-sizing-${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 "market-sizing-${SLUG}" # prints the dir in the agent namespace
ANALYSIS_DIR_AGENT="<printed value>" # e.g. inputs.json, validation.json, sizing.json reads
# Ad-hoc scratch (NOT sub-agent hand-off) lives OUTSIDE the promoted outputs/ tree, in a temp dir
# that is safe to both create and reclaim. Use the printed path verbatim in later steps.
STAGING_DIR="$(mktemp -d "${TMPDIR:-/tmp}/market-sizing-${SLUG:-co}.staging.XXXXXX")"
Pass RUN_ID to all sub-agents. Every artifact written to $ANALYSIS_DIR must include "metadata": {"run_id": "$RUN_ID"} at the top level. compose_report.py checks that all artifact run IDs match — a mismatch triggers a STALE_ARTIFACT high-severity warning, blocking under --strict.
Overwrite-in-place — do NOT delete prior artifacts under $ANALYSIS_DIR. It is the promoted outputs/
tree in Cowork, where deleting a user-visible path is unsafe (Cowork can deny it; the parity gate flags
it). Each producer writes its artifact fresh via -o every run, and RUN_ID is minted fresh per run —
so if a prior run left an artifact a later step doesn't regenerate, compose_report.py's STALE_ARTIFACT
check (run_ids must match) catches the mismatch. No bulk rm is needed or wanted.
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." Deck/materials carve-out — derive field-by-field, never all-or-nothing (do not ask for what you were already given): if the founder provided materials (a deck, financial model, or a sufficiently detailed description), derive each of the four basics — company name, stage, sector, geography — that the materials state, and skip the gate entirely when all four are in hand. Treat the four independently: deriving three and missing one does NOT send you back to asking for all four. Before gating on a still-missing field, try to infer it from a clear signal in the materials and proceed (noting it as inferred, not founder-stated, so it isn't presented as confirmed): geography from a phone country code or an office address (e.g. a +972 number → Israel), but never from currency alone — $ is also CAD, AUD and SGD, and founders everywhere price in USD, so a currency symbol is not a country; stage from an ambiguous fundraise signal (a named round, round size, or "raising our seed" language → the matching --stage value); sector from the product category and ICP. Use AskUserQuestion (NOT plain chat) only for the specific field(s) that genuinely have no derivable or inferable signal — and ask for only those, stating the values you already derived so the founder confirms or corrects rather than re-supplying everything. 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 none of the four can be derived at all, that reduces to asking for all four.)
Stage is the one field with a real fixed label set — use it verbatim if asking.
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 for the specific stage rather than defaulting to series-b). Company name, sector and geography cannot take fixed labels — shape each as an affirmative option carrying the inferred/derived value plus a stated-value fallback. Provide at least 2 options. Then create:
--stage is enum-validated (hyphenated, lowercase) — one of: pre-seed, seed, series-a,
series-b, series-c, series-d, later. Passing a non-canonical token (e.g. seriesa,
pre_seed) is an argparse error and forces a retry — map the founder's answer to one of these
7 values before calling init.
--sector-type is an optional override (also enum-validated, hyphenated): one of saas,
ai-native, marketplace, hardware, hardware-subscription, consumer-subscription,
usage-based, transactional-fintech, retail. When omitted, founder_context.py auto-derives
it from --sector via a small alias table (e.g. "B2B SaaS" -> saas); if the sector doesn't match
a known alias, the script emits a runtime warning asking you to set --sector-type explicitly —
pick the closest value from the enum above rather than waiting for that warning.
python3 "$SHARED_SCRIPTS/founder_context.py" init \
--company-name "Acme Corp" --stage seed --sector "B2B SaaS" \
--geography "US" --artifacts-root "$ARTIFACTS_ROOT"
# Add --sector-type <value> if the auto-derivation warning fires or the sector
# doesn't map cleanly to one of the 9 canonical sector-type values above.
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:
- Never compute a figure in chat. Not TAM, not runway, not a ratio, not a benchmark comparison — not even one you are confident about. An in-chat number has no provenance, no range, no artifact, and nothing downstream can contradict it. That is worse than a slow answer and worse than no answer.
- Never benchmark against a figure you recalled. Benchmarks live in the reference files and the producers read them. If you find yourself writing "typically around X for this stage", stop: either a producer sourced it or it does not go in front of the founder.
- A what-if, a sensitivity illustration, or "roughly what would X give" is NOT an exemption. This is the exemption a live run invented: having correctly produced the real figure, it then wrote "using the current count would shave TAM to roughly €249M rather than €270M" — a second number, computed in chat, from an input the founder never gave. An illustrative figure is read exactly as confidently as a computed one, and the founder cannot tell which came from the pipeline. Two ways to answer a what-if: re-run the producer with the alternate input and quote its output, or give no number and say which direction it moves. Never arithmetic in prose.
- Never offer the real run as an opt-in after answering. "Here's a rough estimate — I can run the full analysis if you want" is the failure. The founder cannot tell that what they just read was not the analysis, so they will not ask for it.
- Two ways to finish, and only two: run the full pipeline to completion, or run the quick-check path (Step 5-quick), which still runs
market_sizing.py. Both end with real artifacts on disk. Anything else is not a finished run. - If you are blocked, say BLOCKED and say why. A missing input, a failed hand-off, an unreadable document — name it and stop. Do not substitute your own reasoning for the pipeline and present the result as its output.
Artifact existence is the proof of execution: if no canonical artifact was written, the skill did not run, whatever the transcript says.
Steps 2-3: Extract Inputs & Choose Methodology
When files are provided (deck, model, market data), read the provided file(s) directly and extract market-relevant data. Read ${CLAUDE_PLUGIN_ROOT}/skills/market-sizing/references/tam-sam-som-methodology.md and ${CLAUDE_PLUGIN_ROOT}/skills/market-sizing/references/artifact-schemas.md.
A .pptx/.ppt deck cannot be read directly — it is binary and Read refuses it, so the market figures inside it are invisible unless you do one of these first. Prefer rendering, since TAM/SAM/SOM claims frequently live in a chart rather than in a sentence:
for c in libreoffice soffice /Applications/LibreOffice.app/Contents/MacOS/soffice; do
command -v "$c" >/dev/null 2>&1 || continue
# -env:UserInstallation is required: LibreOffice writes a first-run profile under
# $HOME, which is read-only in the sandbox, and otherwise exits 77 having converted
# nothing. Errors are shown, not suppressed — a silent failure looks exactly like
# having no converter and sends you down the wrong branch.
"$c" --headless -env:UserInstallation="file://$STAGING_DIR/.lo" \
--convert-to pdf --outdir "$STAGING_DIR" "$DECK_SRC" 2>&1 | tail -3
break
done
ls -1 "$STAGING_DIR"/*.pdf 2>/dev/null || echo "no pdf — use the text fallback below"
Read the resulting PDF from $STAGING_DIR. If no converter is available, fall back to
python3 "$SHARED_SCRIPTS/pptx_to_text.py" "$DECK_SRC" --pretty, which recovers slide text, table cells and speaker notes — enough for stated market claims, though any figure that exists only inside a chart image is lost. Say so rather than treating the extraction as complete: a market claim you could not read is not a market claim the deck failed to make.
Extract all market-relevant data. If the deck includes explicit TAM/SAM/SOM claims, record them in inputs.json under existing_claims.
Competitive landscape: if the deck names competitors, describes a competitive positioning
slide, or otherwise addresses competition, summarize that content into inputs.json's
competitive_landscape_notes field (a short string; use null if the deck says nothing about
competition). This field exists because the CHECKLIST sub-agent (Step 6b) scores
competitive_landscape_acknowledged from inputs.json/methodology.json/validation.json/
sizing.json only — it never reads the deck itself. If competitive content from the deck isn't
carried into this field, the checklist item scores blind to what the deck actually said.
existing_claims must be a flat object with lowercase keys tam, sam, som. Use null for any figure the deck does not state. Custom keys (e.g., SAM_Israel_only) are silently ignored by reconciliation and will trigger an EXISTING_CLAIMS_SHAPE warning.
If the deck states figures that don't fit the flat shape — regional sub-SAMs, time-anchored SOM projections, alternative TAM frames — put them in the optional existing_claims_detail field (any structure). This field does NOT participate in deck-vs-computed reconciliation, but it is rendered as a "Deck Claims (Narrative)" sub-section in the report.
founder_stated_inputs — record the numbers the founder actually gave you. A flat object holding
any of customer_count, arpu, serviceable_pct, target_pct, industry_total, segment_pct,
share_pct that the founder or their materials stated outright (not researched, not inferred,
not your estimate). Leave it {} when the founder gave no quantitative inputs — this is opt-in and
an empty object disables the check rather than failing it.
Its purpose is enforcement, not documentation: compose_report.py compares these against the values
the sizing math actually consumed and raises FOUNDER_VALUE_OVERRIDDEN if they diverge by more than
0.5%. A better-sourced researched figure may be presented as a cross-check — it must never
silently replace what the founder said. If the founder reviews the discrepancy and agrees to the
researched figure, update this field and record the reason via accepted_warnings in methodology.json, so the change is
disclosed rather than invisible. (A unit normalization — "18k" → 18000 — is within tolerance and
does not trip it.)
Currency — set it, do not assume dollars. currency is the ISO code every money figure in this
analysis is denominated in ("USD", "EUR", "ILS", …). Derive it from the materials: an explicitly
stated currency, the symbol on a pricing page or revenue line (€, ₪, £), or the market the
company sells into. Only default to "USD" when the materials genuinely give no signal. This is a
target, not an assumption — a wrong code puts a wrong unit on
the headline TAM, and a wrong unit in a TAM travels into the founder's deck unchallenged.
If the materials mix currencies — a EUR price list against an industry total sourced in USD, which is
the common case since industry totals are almost always quoted in USD — do not silently pick one,
and do not do the arithmetic in your head. Set currency to the one currency the analysis will be
denominated in, then let the sizing step convert: it takes a rate you supply and records it in the
report, so the founder can see what was converted and at what rate. You are the one who looks the
rate up — you have web access and the sizing sub-agent does not. If you cannot establish a rate from a
real source, ask the founder rather than guessing; a rate you half-remember is the one failure mode
nothing downstream can catch.
Sizing basis — declare current-year vs. forecast-year, don't leave it implicit. Industry reports
routinely quote both a current-year figure and a 3-5 year forecast figure for the same market, often
2-3x apart. sizing_basis records which one this analysis used: "current_year" (default — use
unless there's a specific reason to size the market as a report projects it will be, not as it is
today), "forecast_year" (every headline figure is a stated future-year projection — use only when
the founder's materials or chosen sources are themselves forecast-anchored), or "mixed" (inputs
knowingly combine both horizons — state which input uses which in methodology.json's rationale
when you pick this). See references/tam-sam-som-methodology.md §5 for the full rationale. Set it
explicitly in inputs.json on every run — an unset sizing_basis renders as "not declared" in the
report rather than silently defaulting, so leaving it out is a visible gap, not a safe skip.
GTM and projections evidence — two more fields the CHECKLIST sub-agent cannot see without you.
Same problem as competitive_landscape_notes above: the CHECKLIST sub-agent (Step 6b) never reads
the deck or financial model, so if go-to-market and financial-alignment evidence isn't carried into
inputs.json, the som_backed_by_gtm and som_consistent_with_projections checklist items score
blind. These are two different kinds of evidence, so they get two different fields:
gtm_evidence_notes(string, usenullif absent): a short summary of any customer-acquisition strategy, sales funnel metrics, or comparable-company benchmark the materials give for how SOM gets captured (e.g. "Deck slide 11: outbound to 40 target accounts/quarter via 2 AEs, citing a 15% demo-to-close rate from a named competitor's public S-1").projections_alignment_notes(string, usenullif absent): a short summary of whether the materials show the SOM revenue figure lining up with the hiring plan, sales capacity, or burn rate (e.g. "Financial model shows 3 AEs hired by Q3, consistent with the SOM ramp; burn rate does not fund a 4th until Y2").
Write inputs.json:
cat <<'INPUTS_EOF' > "$ANALYSIS_DIR/inputs.json"
{
"company_name": "...",
"analysis_date": "YYYY-MM-DD",
"stage": "seed",
"sector": "...",
"geography": "...",
"currency": "USD",
"sizing_basis": "current_year",
"product_description": "...",
"target_segments": ["..."],
"pricing_model": "...",
"revenue_model": "...",
"existing_claims": {"tam": null, "sam": null, "som": null},
"existing_claims_detail": null,
"founder_stated_inputs": {},
"competitive_landscape_notes": "...",
"gtm_evidence_notes": "...",
"projections_alignment_notes": "...",
"materials_provided": ["..."],
"metadata": {"run_id": "<RUN_ID>"}
}
INPUTS_EOF
Heredoc guardrail: every templated heredoc in this file uses a single-quoted delimiter (<<'INPUTS_EOF', <<'METH_EOF', etc.) on purpose. An UNQUOTED delimiter (<<EOF) lets the shell expand $-bearing values inside the body, so a literal dollar amount like $8M silently shell-expands away ($8 is read as a variable, M is left dangling) before it ever reaches the file. This applies to any heredoc you improvise too, not just the templates above: always single-quote the delimiter when the body may contain a $.
Write methodology.json:
cat <<'METH_EOF' > "$ANALYSIS_DIR/methodology.json"
{
"approach_chosen": "both",
"rationale": "...",
"metadata": {"run_id": "<RUN_ID>"}
}
METH_EOF
When conversational input (no files): Extract directly from the conversation. Read references/tam-sam-som-methodology.md, choose the approach, and write both artifacts directly.
After writing, verify that $ANALYSIS_DIR contains both inputs.json and methodology.json.
Gate: Confirm Methodology and Inputs
MANDATORY STOP — TWO SEPARATE STEPS. DO NOT COMBINE THEM.
Step A: Output a chat message with the methodology choice and key inputs. Use a formatted summary. This is a normal assistant message — NOT an AskUserQuestion call. Example:
Here's what I've extracted and how I plan to approach the sizing:
**Company:** Acme Corp — AI-powered compliance for fintechs
**Geography:** US
**Target segments:** Mid-market fintechs ($10M-$500M revenue)
**Methodology:** Both top-down and bottom-up
- Top-down: Global RegTech market → US share → fintech compliance segment
- Bottom-up: ~2,400 target fintechs × $48K ARPU
**Key inputs found:**
| Input | Value | Source |
|-------|-------|--------|
| Current ARR | $850K | Deck slide 7 |
| Customers | 12 | Deck slide 8 |
| ARPU (monthly) | $4,000 | Derived from ARR/customers |
| Growth rate | 15% MoM | Deck slide 9 |
**Missing / needs clarification:**
- Geographic expansion plans (US only or international?)
- Enterprise vs SMB customer split
If existing_claims were found in the deck, include them: "Your deck claims TAM of $X — I'll validate this against external sources."
Step B: AFTER the chat message, call AskUserQuestion with a short question that names the methodology so the founder isn't confirming blind. The question field is plain text — one sentence, NO markdown/tables/bullets.
Question (substitute the chosen approach): I'll size this <top-down / bottom-up / both top-down and bottom-up> — does this approach look right?
Options: Looks good / Change methodology / Correct or add data
CRITICAL: the question must name the methodology as ONE plain-text sentence. The full inputs/rationale stay in the Step-A chat message — do NOT put a table or markdown in the question.
This two-step pattern (chat message then AskUserQuestion) is required because AskUserQuestion renders as plain text. Detailed content goes in the chat message; only the gate question goes in AskUserQuestion.
If the founder selects "Looks good": Proceed to Step 4 (External Validation).
If "Change methodology": Ask which approach they prefer, via AskUserQuestion:
Options: Top-down / Bottom-up / Both top-down and bottom-up
Then ask why (plain text — the reason isn't a fixed choice). Update methodology.json and repeat Steps A+B.
If "Correct or add data": Ask which values are wrong or missing via AskUserQuestion. The labels are runtime data — the specific inputs at stake differ every run — so build them from what is actually on screen: one option per input you just showed in the Step-A message, each naming that input and its current value (e.g. Paying accounts: 4,200 — so the founder is correcting a number they can see, not recalling one), capped at three, plus a final Something else — I'll say which in chat so nothing is unreachable. Never emit a bare free-text prompt with no options. Then correct/patch inputs.json, and check whether the updated inputs change what methodology is viable. If so, update methodology.json too. Repeat Steps A+B.
Late edits to inputs.json (any point after Step 6b has already run): checklist.json and
report.md are snapshots of inputs.json at the time their producing step ran — patching
inputs.json alone does NOT retroactively update them. If you edit inputs.json after CHECKLIST
has already been dispatched (e.g. adding competitive_landscape_notes found later in the deck),
you must re-dispatch the CHECKLIST step (and any other downstream step whose scoring depends on
the changed field) with a fresh RUN_ID, then re-run compose_report.py to recompose the report.
Do not hand-patch checklist.json or report.md directly — that bypasses the sub-agent scoring
this architecture exists to preserve, and compose_report.py's STALE_ARTIFACT check exists
precisely to catch a skipped re-dispatch (mismatched run_id across artifacts).
Step 4: External Validation -> validation.json
The main thread performs the web research. Do NOT dispatch a sub-agent for this step — the main thread has web-research capability (WebSearch/WebFetch, or the host's equivalents), and this skill's sub-agent allowlist deliberately includes no network to
…(truncated)