# Market Sizing

> Builds credible TAM/SAM/SOM analysis with external validation and sensitivity testing for startup fundraising. Supports top-down, bottom-up, or dual-methodology approaches. Run the sourced, sensitivity-tested analysis rather than estimating a market from memory.

- Skill: `lool-ventures/market-sizing` (Agent Skill, multi-file: 12 files)
- Install (CLI): `npx skillmds@latest add lool-ventures/market-sizing`
- Raw SKILL.md: https://api.skillmd.com/api/skills/lool-ventures/market-sizing/raw
- Safety review: pending (external: skill-scanner PASS, skillspector PASS)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: lool-ventures (https://skillmd.com/u/lool-ventures)
- Updated: 2026-09-10
- Page: https://skillmd.com/skills/lool-ventures/market-sizing

---


# 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 `uv` for script execution.
- **Exports:**
  - `sizing.json` → `financial-model-review`, `ic-sim`, `fundraise-readiness`
  - `sensitivity.json` → `financial-model-review`

## Skill Execution Model (READ FIRST)

> See `founder-skills/references/skill-execution-model.md` for 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 `Task` tool. 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 the `OUTPUT_PATH` given in its prompt (the `handoff/` dir), and returns a small receipt. The main thread gates the file with `check_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-md` has written `report.md`. The sub-agent Reads the staged `coaching_payload.json` from the hand-off dir (Mitigation 2) — it does NOT read the full `report.md` — composes the coaching commentary, WRITES it to the `OUTPUT_PATH` hand-off file, and returns a small receipt. The main thread gates the file (`check_handoff.py`) and inserts it via the shared `insert_coaching.py` script (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:

1. If the message is wrapped in a ` ```json ... ``` ` (or plain ` ``` ... ``` `) fence, strip the fence first.
2. Try to parse the stripped text directly as JSON.
3. If that fails, walk through the text looking for the first `{` character and try `json.JSONDecoder().raw_decode(text[i:])` — this is brace-aware and handles nested objects correctly (unlike regex, which truncates on the first `}`).
4. 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 `--stdin` for JSON piping
- **`sensitivity.py`** — Stress-test assumptions with low/base/high ranges and confidence-based auto-widening
- **`checklist.py`** — Validates 22-item self-check with pass/fail per item
- **`compose_report.py`** — Assembles report with cross-artifact validation; `--write-md` writes report.md; `--strict` exits 1 on high/medium warnings
- **`visualize.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 practices
- **`pitfalls-checklist.md`** — Self-review checklist for common mistakes
- **`artifact-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.md` for 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.

```bash
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:

```bash
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.

```bash
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

```bash
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.

```bash
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:

```bash
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, use `null` if 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, use `null` if 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`:
```bash
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`:
```bash
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)
