| name | market-sizing |
| description | Builds credible TAM/SAM/SOM analysis with external validation and sensitivity testing for startup fundraising. Supports top-down, bottom-up, or dual-methodology approaches. |
| when_to_use | Use ONLY when the user has asked to size a market or validate market claims, AND has provided enough context (a product/service description, a market segment, or a deck with TAM/SAM/SOM claims). Do not auto-invoke on general fundraising or strategy questions.
|
| user-invocable | true |
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
checklist.json → ic-sim
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 coaching_payload inlined in the dispatch prompt (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:
- 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 try json.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 --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. Never mention file names, scripts, or JSON. After each analytical step (5–6), share a one-sentence finding before moving on.
Workflow
Step 0: Path Setup
Every Bash tool call runs in a fresh shell — variables do not persist. Prefix every Bash call that uses these paths with the variable block below, or substitute absolute paths directly:
SCRIPTS="${CLAUDE_PLUGIN_ROOT}/skills/market-sizing/scripts"
if [ ! -d "$SCRIPTS" ]; then
SCRIPTS="$(find /sessions -type d -path '*/skills/market-sizing/scripts' 2>/dev/null | head -1)"
fi
if [ -z "$SCRIPTS" ] || [ ! -d "$SCRIPTS" ]; then
SCRIPTS="$(find / -type d -path '*/skills/market-sizing/scripts' 2>/dev/null | head -1)"
fi
PLUGIN_ROOT="${SCRIPTS%/skills/*}"
REFS="$PLUGIN_ROOT/skills/market-sizing/references"
SHARED_SCRIPTS="$PLUGIN_ROOT/scripts"
SHARED_REFS="$PLUGIN_ROOT/references"
python3 "$SHARED_SCRIPTS/resolve_artifacts_root.py"
The Step 0 block self-heals when ${CLAUDE_PLUGIN_ROOT} doesn't resolve (Cowork). If it still comes up empty, locate the anchor manually: find / -path '*/skills/market-sizing/scripts/market_sizing.py' 2>/dev/null | head -5 and derive the variables from it.
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):
ANALYSIS_DIR="$ARTIFACTS_ROOT/market-sizing-${SLUG}"
mkdir -p "$ANALYSIS_DIR"
RUN_ID="$(date -u +%Y%m%dT%H%M%SZ)"
HANDOFF_DIR="$ANALYSIS_DIR/handoff/$RUN_ID"
mkdir -p "$HANDOFF_DIR"
python3 "$SHARED_SCRIPTS/resolve_artifacts_root.py" --agent
HANDOFF_AGENT="<printed AGENT_ARTIFACTS_ROOT>/market-sizing-${SLUG}/handoff/$RUN_ID"
ANALYSIS_DIR_AGENT="<printed AGENT_ARTIFACTS_ROOT>/market-sizing-${SLUG}"
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): This is normal for a first run — do not treat it as an error. Use AskUserQuestion (NOT plain chat) to ask for company name, stage, sector, and geography. Provide at least 2 options. Then create:
python3 "$SHARED_SCRIPTS/founder_context.py" init \
--company-name "Acme Corp" --stage seed --sector "B2B SaaS" \
--geography "US" --artifacts-root "$ARTIFACTS_ROOT"
Exit 2 (multiple): Present the list, ask which company, re-read with --slug.
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.
Extract all market-relevant data. If the deck includes explicit TAM/SAM/SOM claims, record them in inputs.json under existing_claims.
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.
Write inputs.json:
cat <<'INPUTS_EOF' > "$ANALYSIS_DIR/inputs.json"
{
"company_name": "...",
"analysis_date": "YYYY-MM-DD",
"stage": "seed",
"sector": "...",
"geography": "...",
"product_description": "...",
"target_segments": ["..."],
"pricing_model": "...",
"revenue_model": "...",
"existing_claims": {"tam": null, "sam": null, "som": null},
"existing_claims_detail": null,
"materials_provided": ["..."],
"metadata": {"run_id": "<RUN_ID>"}
}
INPUTS_EOF
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 (top-down / bottom-up / both) and why. Update methodology.json and repeat Steps A+B.
If "Correct or add data": Ask which values are wrong or missing, 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.
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 tools. Perform all web research calls yourself.
When methodology is "both": Research both approaches in parallel (two WebSearch calls in one assistant turn — one for top-down market data, one for bottom-up customer/ARPU data).
- Top-down research: WebSearch for industry reports, government statistics, analyst estimates for total market size, segment percentages, market growth rates.
- Bottom-up research: WebSearch for customer counts, pricing/ARPU benchmarks, competitor data, serviceable segment data.
When methodology is single: Perform one research pass for the chosen approach.
When pure calculation (user provides all numbers): Skip research. Write a stub validation.json with {"skipped": true, "reason": "User-provided inputs, no external validation required"}.
Source quality hierarchy: Government/regulatory > Established analysts > Industry associations > Academic > Business press > Company blogs (product facts only).
Triangulate key numbers with 2+ independent sources. Every assumption must appear in the assumptions array with a name matching script parameter names and a category of sourced, derived, or agent_estimate.
Write validation.json directly:
cat <<'VAL_EOF' > "$ANALYSIS_DIR/validation.json"
{
"assumptions": [
{"name": "industry_total", "value": 50000000000, "category": "sourced", "label": "Global RegTech market", "source_url": "...", "source_title": "...", "confidence": "high"},
...
],
"figure_validations": [
{"figure": "TAM", "label": "Global RegTech TAM", "status": "validated", "source_count": 2}
],
"sources": [
{"title": "...", "url": "...", "publisher": "...", "date_accessed": "YYYY-MM-DD", "quality_tier": "analyst_firm", "segment_match": "exact", "supported": "industry_total"}
],
"metadata": {"run_id": "<RUN_ID>"}
}
VAL_EOF
Context A hand-off protocol (file transport + gate)
Every Context A dispatch prompt carries an OUTPUT_PATH: line built from $HANDOFF_AGENT. The
sub-agent WRITES its output JSON to that path with its Write tool and returns only a small receipt:
{"status": "complete", "output_path": "<echo of OUTPUT_PATH>"}. The payload leaves the model
exactly once (into the Write call) — never re-type sub-agent JSON into a heredoc.
Path idiom for dispatch prompts (host-loop path gate): OUTPUT_PATH and any under-outputs artifact
READ path a sub-agent is given are relative to the sub-agent's file-tool cwd (the outputs mount) —
built from the resolve_artifacts_root.py --agent namespace ($HANDOFF_AGENT / $ANALYSIS_DIR_AGENT).
Never hand a sub-agent an absolute /sessions/... path for a file-tool Read/Write — the host-loop path
gate denies it (steering shell work to the bash tool instead). Bundled references/*.md are the one
exception: pass them as the literal ${CLAUDE_PLUGIN_ROOT}/skills/market-sizing/references/... token (it is
pre-resolved to a host-readable path); do NOT substitute a find /sessions-discovered $REFS (a shell
path a file tool can't read).
After EVERY Context A dispatch, gate before piping (<step> = the dispatch's file stem):
printf '%s' '<agent final message verbatim>' | \
python3 "$SHARED_SCRIPTS/check_handoff.py" "$HANDOFF_DIR/<step>_output.json" \
--agent-path "$HANDOFF_AGENT/<step>_output.json" --receipt-json -
Branch on the exit code (complete state machine — do not improvise):
- Exit 0 → pipe the file through the producer:
cat "$HANDOFF_DIR/<step>_output.json" | python3 "$SCRIPTS/<producer>.py" ...
- Exit 3 (missing/empty file — receipt may be fabricated) → redo-dispatch: fresh Task, same prompt plus one line: "your receipt claimed a file at
<path> but none exists; use Write to create exactly that path."
- Exit 4 (file exists, invalid JSON) → repair-dispatch: fresh Task: "Read
<OUTPUT_PATH>; it fails JSON parsing with <verbatim detail from the diagnostic>; fix and rewrite it; return the receipt."
- Exit 5 (receipt echoes a different path) → repair-dispatch telling the agent the exact expected OUTPUT_PATH (it wrote somewhere else).
- Exit 6 (receipt unparseable / no
output_path key) → redo-dispatch with "return ONLY the receipt JSON — no fences, no prose."
- Producer schema rejection (the pipe fails next) → repair-dispatch with the producer's stderr verbatim.
- Any other exit (script crash etc.) → STOP with the stderr.
- After ANY corrective dispatch, resume from
check_handoff.py — never pipe to the producer unchecked.
Retry budget: max 2 corrective dispatches per step, of any kind, in any combination (max 3
total dispatches). After the second corrective dispatch fails any gate: STOP and report the exact
diagnostic to the founder. The main thread MUST NOT author or patch analytical content itself —
filling in the JSON is the fabrication failure mode this architecture exists to prevent. A
status: "blocked" return is not a gate retry, but it is bounded: at most ONE input-fix
re-dispatch per step; a second blocked return STOPs with both reasons quoted.
Graceful degrade (fleet heterogeneity): if the FIRST corrective dispatch also exits 3 while the
agent's receipt claims complete with the correctly echoed path, treat the host's filesystem
topology as hand-off-incompatible: fall back to message-channel transport for the REST of this run
(sub-agent returns full JSON in its final message; stage to $STAGING_DIR/<step>_input.json; same
producer pipe), and note the fallback in your final summary.
Retries overwrite the same OUTPUT_PATH (the mount is write-allowed / delete-denied — never rm
under $ANALYSIS_DIR). Hand-off files are not canonical artifacts: producers ignore them except
via the explicit pipe, and compose_report.py never reads handoff/.
Ad-hoc scratch (NOT sub-agent hand-off) still goes to $STAGING_DIR in /tmp — see the reference
(founder-skills/references/skill-execution-model.md). Hard rule: never stage scratch under
$ANALYSIS_DIR, and never delete anything under it.
Step 5: Calculate TAM/SAM/SOM -> sizing.json (Context A dispatch)
The dispatch pattern depends on methodology:
Parallel dispatch recipe (methodology = "both")
Dispatch the market-sizing agent TWICE in parallel via the Task tool — one for top-down, one for bottom-up. Call the Task tool with subagent_type: "founder-skills:market-sizing" for both calls, so the analysis runs in the scoped agent (its tools: allowlist binds; a type-less dispatch falls back to the wildcard general-purpose agent). Use a SINGLE assistant turn with 2 Task tool calls (NOT two sequential turns). The Claude Code harness runs both Task calls in parallel when they appear in the same assistant response.
Pseudocode for the dispatch (executed as 2 parallel Task tool_use blocks):
[
Task(description="Market sizing: top-down methodology",
prompt="CONTEXT: TOP_DOWN_METHODOLOGY\nOUTPUT_PATH: <HANDOFF_AGENT>/top_down_output.json\nRUN_ID: <id>\n<research data from validation.json>"),
Task(description="Market sizing: bottom-up methodology",
prompt="CONTEXT: BOTTOM_UP_METHODOLOGY\nOUTPUT_PATH: <HANDOFF_AGENT>/bottom_up_output.json\nRUN_ID: <id>\n<research data from validation.json>"),
]
Full dispatch prompt template (TOP_DOWN_METHODOLOGY):
CONTEXT: TOP_DOWN_METHODOLOGY
OUTPUT_PATH: <HANDOFF_AGENT>/top_down_output.json
RUN_ID: <RUN_ID>
You are the market-sizing agent dispatched in Context A (TOP_DOWN_METHODOLOGY).
Read inputs.json at <ANALYSIS_DIR_AGENT>/inputs.json and validation.json at
<ANALYSIS_DIR_AGENT>/validation.json.
Using the top-down approach, compute TAM/SAM/SOM. Pre-fetched research data:
<inline the relevant assumptions from validation.json — industry_total, segment_pct, share_pct>
Use your Write tool to write to OUTPUT_PATH exactly this JSON — the shape
expected by market_sizing.py --stdin for approach "top_down":
{
"approach": "top_down",
"industry_total": <number>,
"segment_pct": <number>,
"share_pct": <number>
}
Then return ONLY the receipt JSON in your final assistant message:
{"status": "complete", "output_path": "<echo of OUTPUT_PATH>"}
Do NOT write any file other than OUTPUT_PATH — canonical artifacts are
producer-script-only; anything else you write bypasses schema validation and
run_id stamping.
Full dispatch prompt template (BOTTOM_UP_METHODOLOGY):
CONTEXT: BOTTOM_UP_METHODOLOGY
OUTPUT_PATH: <HANDOFF_AGENT>/bottom_up_output.json
RUN_ID: <RUN_ID>
You are the market-sizing agent dispatched in Context A (BOTTOM_UP_METHODOLOGY).
Read inputs.json at <ANALYSIS_DIR_AGENT>/inputs.json and validation.json at
<ANALYSIS_DIR_AGENT>/validation.json.
Using the bottom-up approach, compute TAM/SAM/SOM. Pre-fetched research data:
<inline the relevant assumptions from validation.json — customer_count, arpu, serviceable_pct, target_pct>
Use your Write tool to write to OUTPUT_PATH exactly this JSON — the shape
expected by market_sizing.py --stdin for approach "bottom_up":
{
"approach": "bottom_up",
"customer_count": <integer>,
"arpu": <number>,
"serviceable_pct": <number>,
"target_pct": <number>
}
Then return ONLY the receipt JSON in your final assistant message:
{"status": "complete", "output_path": "<echo of OUTPUT_PATH>"}
Do NOT write any file other than OUTPUT_PATH — canonical artifacts are
producer-script-only; anything else you write bypasses schema validation and
run_id stamping.
After both sub-agents return: gate EACH hand-off per the Context A hand-off protocol (run
check_handoff.py per file, branch on exit codes). Then merge the two files deterministically and
pipe — never re-type the values:
python3 "$SHARED_SCRIPTS/merge_json.py" \
"$HANDOFF_DIR/top_down_output.json" "$HANDOFF_DIR/bottom_up_output.json" \
--set approach=both | \
python3 "$SCRIPTS/market_sizing.py" --stdin --pretty --run-id "$RUN_ID" -o "$ANALYSIS_DIR/sizing.json"
Single methodology dispatch
When methodology is "top_down" only: dispatch one TOP_DOWN_METHODOLOGY task, gate the hand-off, then
cat "$HANDOFF_DIR/top_down_output.json" | python3 "$SCRIPTS/market_sizing.py" --stdin --pretty --run-id "$RUN_ID" -o "$ANALYSIS_DIR/sizing.json".
When methodology is "bottom_up" only: same with bottom_up_output.json.
For "both" mode, check the comparison section — a >30% TAM discrepancy means investigating which assumptions are flawed. TAM must match the product's actual target universe (not inflated industry totals).
Multi-vertical / platform companies: If inputs.json lists applications in 2+ distinct industries:
- Identify verticals — classify as
commercial (revenue/pilots), r_and_d (demonstrated feasibility, 2-3yr commercialization path), or future (conceptual/early).
- Include
commercial and r_and_d in TAM. If top-down only covers one vertical, use bottom-up as primary. When verticals have different ARPUs, compute weighted blended ARPU. Future verticals go in coaching commentary as upside, not in the calculated TAM.
- Narrow SAM and SOM — SAM = traction + active R&D segments. SOM = beachhead only.
- Document scope in
methodology.json rationale.
Default to full-scope TAM. Only narrow to beachhead if the user explicitly requests it.
Step 5.5: Reality Check
Before proceeding, answer:
- Laugh test: Would an experienced VC nod or raise an eyebrow? Seed + <5 pilots + >$1B TAM = explain yourself.
- Scope match: Does TAM cover all
commercial and r_and_d verticals from inputs.json?
- Customer count sanity: Can you name a representative sample of the customers in your count?
- Convergence integrity: Were top-down and bottom-up parameters set independently? If you adjusted one after seeing the other, revert and accept the delta.
This step produces no artifact. If it reveals problems, fix them before proceeding.
Steps 6a & 6b: Parallel Analysis (Sensitivity + Checklist) (Context A dispatch)
Dispatch the market-sizing agent TWICE in parallel via the Task tool — one for SENSITIVITY_TEST, one for CHECKLIST. Call the Task tool with subagent_type: "founder-skills:market-sizing" for both calls. Use a SINGLE assistant turn with 2 Task tool calls. The Claude Code harness runs both Task calls in parallel.
SENSITIVITY_TEST dispatch prompt template
CONTEXT: SENSITIVITY_TEST
OUTPUT_PATH: <HANDOFF_AGENT>/sensitivity_output.json
RUN_ID: <RUN_ID>
You are the market-sizing agent dispatched in Context A (SENSITIVITY_TEST).
Read:
- <ANALYSIS_DIR_AGENT>/validation.json — for confidence tiers
- <ANALYSIS_DIR_AGENT>/sizing.json — for base values and approach
Construct sensitivity input with confidence-based ranges. Tag each parameter
with confidence from validation: `sourced` (range stands), `derived`
(min +/-30%), `agent_estimate` (min +/-50%). Include EVERY `agent_estimate`
parameter — compose_report.py flags missing ones as UNSOURCED_ASSUMPTIONS.
Use your Write tool to write to OUTPUT_PATH exactly the shape expected by
sensitivity.py. Each range MUST include a `confidence` of `sourced`,
`derived`, or `agent_estimate` — without it, sensitivity.py defaults to
`sourced` and the auto-widening for derived/agent_estimate parameters never
fires:
{
"approach": "bottom_up|top_down|both",
"base": {<parameter: value pairs from sizing.json>},
"ranges": {
"<parameter>": {"low_pct": <negative number>, "high_pct": <positive number>, "confidence": "sourced|derived|agent_estimate"}
}
}
Then return ONLY the receipt JSON in your final assistant message:
{"status": "complete", "output_path": "<echo of OUTPUT_PATH>"}
Do NOT write any file other than OUTPUT_PATH — canonical artifacts are
producer-script-only; anything else you write bypasses schema validation and
run_id stamping.
After the sub-agent returns: gate the hand-off per the Context A hand-off protocol, then pipe:
cat "$HANDOFF_DIR/sensitivity_output.json" | \
python3 "$SCRIPTS/sensitivity.py" --pretty --run-id "$RUN_ID" -o "$ANALYSIS_DIR/sensitivity.json"
CHECKLIST dispatch prompt template
CONTEXT: CHECKLIST
OUTPUT_PATH: <HANDOFF_AGENT>/checklist_output.json
RUN_ID: <RUN_ID>
You are the market-sizing agent dispatched in Context A (CHECKLIST). Read:
- ${CLAUDE_PLUGIN_ROOT}/skills/market-sizing/references/pitfalls-checklist.md
- ${CLAUDE_PLUGIN_ROOT}/skills/market-sizing/references/artifact-schemas.md
(read the "Canonical 22 checklist IDs" section)
- <ANALYSIS_DIR_AGENT>/inputs.json
- <ANALYSIS_DIR_AGENT>/methodology.json
- <ANALYSIS_DIR_AGENT>/validation.json
- <ANALYSIS_DIR_AGENT>/sizing.json
Assess all 22 items with status (pass/fail/not_applicable) and notes.
Use your Write tool to write to OUTPUT_PATH the items array without a summary
(the producer script computes the summary). Each item has this shape:
{
"items": [
{"id": "structural_tam_gt_sam_gt_som", "status": "pass", "notes": null}
]
}
status is one of: pass, fail, not_applicable.
Assess every one of these 22 items — one item per id, no omissions, no invented
ids. The 22 ids, grouped by category:
Structural Checks:
{"id": "structural_tam_gt_sam_gt_som"}
{"id": "structural_definitions_correct"}
TAM Scoping:
{"id": "tam_matches_product_scope"}
{"id": "source_segments_match"}
SOM Realism:
{"id": "som_share_defensible"}
{"id": "som_backed_by_gtm"}
{"id": "som_consistent_with_projections"}
Data Quality:
{"id": "data_current"}
{"id": "sources_reputable"}
{"id": "figures_triangulated"}
{"id": "unsupported_figures_flagged"}
{"id": "validated_used_precisely"}
{"id": "assumptions_categorized"}
Methodology:
{"id": "both_approaches_used"}
{"id": "approaches_reconciled"}
{"id": "growth_dynamics_considered"}
Market Understanding:
{"id": "market_properly_segmented"}
{"id": "competitive_landscape_acknowledged"}
{"id": "sam_expansion_path_noted"}
Presentation:
{"id": "assumptions_explicit"}
{"id": "formulas_shown"}
{"id": "sources_cited"}
Then return ONLY the receipt JSON in your final assistant message:
{"status": "complete", "output_path": "<echo of OUTPUT_PATH>"}
Do NOT write any file other than OUTPUT_PATH — canonical artifacts are
producer-script-only; anything else you write bypasses schema validation and
run_id stamping.
After the sub-agent returns: gate the hand-off per the Context A hand-off protocol, then pipe:
cat "$HANDOFF_DIR/checklist_output.json" | \
python3 "$SCRIPTS/checklist.py" --pretty --run-id "$RUN_ID" -o "$ANALYSIS_DIR/checklist.json"
Verify after both sub-agents return: check that $ANALYSIS_DIR contains fresh sensitivity.json and checklist.json. If either is missing, re-run the failed dispatch before proceeding. Share a coaching update with the founder.
Step 7: Compose and Validate Report
python3 "$SCRIPTS/compose_report.py" --dir "$ANALYSIS_DIR" --pretty \
-o "$ANALYSIS_DIR/report.json" \
--write-md "$ANALYSIS_DIR/report.md"
Fix high-severity warnings and re-run. Use --strict to enforce a clean report.
Post-write verification: compose_report.py exits non-zero (code 2) if the declared output files don't exist or are empty after writing. If compose exits non-zero, stop and report the exact stderr — do not proceed to Step 8.
Step 8: Post-Compose Coaching Commentary (Context B dispatch, POST_COMPOSE_COACHING)
Dispatch the market-sizing sub-agent in Context B (Mitigation 2). Call the Task tool with
subagent_type: "founder-skills:market-sizing" after compose_report.py has successfully written
report.md.
Mitigation 2 protocol: the main thread reads the structured coaching_payload from report.json and inlines it into the dispatch prompt. The sub-agent does NOT Read full report.md — it consumes coaching_payload directly, composes the coaching commentary, and WRITES it to the OUTPUT_PATH hand-off file with its Write tool, returning only a small receipt (the same file transport as Context A — the commentary leaves the model exactly once, into the Write call; the main thread never re-types it). The main thread gates that file with check_handoff.py, then inserts it via the shared insert_coaching.py script (idempotency matrix, uuid-marker replacement, run_id-parity verification — all deterministic). See the market-sizing agent body's "Context B — Post-compose coaching dispatch (POST_COMPOSE_COACHING)" section for the full procedure.
Before dispatching, construct the coaching_payload from the compose
output. Read report.json to extract the checklist summary, failed items, and
high-severity warnings. Read sizing.json for tam, sam, som.
Read methodology.json for methodology. Read confidence directly from
report.json's coaching_payload.confidence (compose derives it from the
checklist score_pct: ≥85 high / ≥60 medium / else low). Extract the
insertion_marker from report.json (the compose script emits it alongside
report_markdown). Do NOT pass warned_items from a warn status —
market-sizing checklist has no warn status, so warned_items is always [].
The compose script also emits coaching_payload.deck_coverage directly in
report.json — copy this field verbatim into the dispatch prompt (it is
null when the founder's existing_claims had no canonical figures stated;
otherwise {"deck_reviewed": true, "stated": [...], "missing": [...]}).
Coaching framing for deck_coverage: when missing is non-empty, frame
as "deck stated {stated} but should also show {missing}" — NOT understatement.
If the warnings list contains EXISTING_CLAIMS_SHAPE, do not trust
deck_coverage = null as "deck wasn't reviewed"; frame the coaching around
the warning and the "Deck Claims (Narrative)" section instead.
Dispatch prompt template:
CONTEXT: POST_COMPOSE_COACHING
OUTPUT_PATH: <HANDOFF_AGENT>/coaching_commentary_output.json
ANALYSIS_DIR: <absolute path to ANALYSIS_DIR>
RUN_ID: <RUN_ID>
You are the market-sizing agent dispatched in Context B (POST_COMPOSE_COACHING).
The main thread has already run all producer scripts and composed the final
report using the Mitigation 2 protocol. Do NOT read the full report.md.
coaching_payload:
{
"schema_version": "v0.4.2-market-sizing",
"summary": {
"score_pct": <number from checklist.json summary>,
"total": <number>,
"pass": <number>,
"fail": <number>,
"not_applicable": <number>
},
"failed_items": [<array of failed checklist item objects with id and notes>],
"warned_items": [],
"high_severity_warnings": [<codes from report.json validation.warnings where severity=="high">],
"methodology": "<top_down|bottom_up|both from methodology.json>",
"confidence": "<high|medium|low — copy from report.json coaching_payload.confidence>",
"tam": <tam value from sizing.json>,
"sam": <sam value from sizing.json>,
"som": <som value from sizing.json>,
"company_name": "<from inputs.json>",
"deck_coverage": <null OR {"deck_reviewed": true, "stated": [<canonical keys with values>], "missing": [<canonical keys with null>]} — copy verbatim from coaching_payload emitted by compose_report.py>,
"review_dir": "<ANALYSIS_DIR absolute path>",
"report_path": "<ANALYSIS_DIR>/report.md",
"insertion_marker": "<EXACT marker string from report.json, e.g. <!-- COACHING_INSERTION_POINT_a1b2c3d4 -->"
}
Follow the POST_COMPOSE_COACHING procedure in your agent body exactly:
1. Compose commentary from coaching_payload (failed_items only; warned_items is always [])
2. Use your Write tool to write to OUTPUT_PATH exactly:
{"commentary_markdown": "<your coaching commentary — markdown body text, WITHOUT
a '## Coaching Commentary' heading and WITHOUT the insertion_marker string>"}
Do NOT write any file other than OUTPUT_PATH — insertion into report.md is the
main thread's job, via the shared insert_coaching.py script.
3. Return the receipt — do NOT write any file other than OUTPUT_PATH
Return:
{"status": "complete", "output_path": "<echo of OUTPUT_PATH>"}
OR if the payload is unusable (write no file):
{"status": "blocked", "reason": "<specific gap>"}
After the sub-agent returns: if its final message is a {"status": "blocked", "reason": ...} object, stop and report the reason to the founder — do not run the gate. Otherwise gate the hand-off, then (on gate exit 0) insert deterministically. The commentary leaves the model exactly once (into the sub-agent's Write call) — NEVER re-type the sub-agent's JSON into a heredoc or a python -c argument.
SHARED_SCRIPTS="${CLAUDE_PLUGIN_ROOT}/scripts"
if [ ! -d "$SHARED_SCRIPTS" ]; then
MS_SCRIPTS="$(find /sessions -type d -path '*/skills/market-sizing/scripts' 2>/dev/null | head -1)"
[ -n "$MS_SCRIPTS" ] || MS_SCRIPTS="$(find / -type d -path '*/skills/market-sizing/scripts' 2>/dev/null | head -1)"
SHARED_SCRIPTS="${MS_SCRIPTS%/skills/*}/scripts"
fi
printf '%s' '<agent final message verbatim>' | \
python3 "$SHARED_SCRIPTS/check_handoff.py" "$HANDOFF_DIR/coaching_commentary_output.json" \
--agent-path "$HANDOFF_AGENT/coaching_commentary_output.json" --receipt-json -
On gate exit 0, insert the gated hand-off FILE (feed the file, never re-type the message):
SHARED_SCRIPTS="${CLAUDE_PLUGIN_ROOT}/scripts"
if [ ! -d "$SHARED_SCRIPTS" ]; then
MS_SCRIPTS="$(find /sessions -type d -path '*/skills/market-sizing/scripts' 2>/dev/null | head -1)"
[ -n "$MS_SCRIPTS" ] || MS_SCRIPTS="$(find / -type d -path '*/skills/market-sizing/scripts' 2>/dev/null | head -1)"
SHARED_SCRIPTS="${MS_SCRIPTS%/skills/*}/scripts"
fi
python3 "$SHARED_SCRIPTS/insert_coaching.py" \
--report "$ANALYSIS_DIR/report.md" \
--marker '<EXACT insertion_marker string from report.json>' \
--commentary-file "$HANDOFF_DIR/coaching_commentary_output.json" \
--verify-artifact "$ANALYSIS_DIR/inputs.json" \
--verify-artifact "$ANALYSIS_DIR/methodology.json" \
--verify-artifact "$ANALYSIS_DIR/validation.json" \
--verify-artifact "$ANALYSIS_DIR/sizing.json" \
--verify-artifact "$ANALYSIS_DIR/sensitivity.json" \
--verify-artifact "$ANALYSIS_DIR/checklist.json"
The gate (check_handoff.py) verifies the sub-agent's hand-off file exists, parses, and matches the receipt's echoed path; on gate exit 0, insert_coaching.py performs the 6-state idempotency check, replaces the marker with ## Coaching Commentary + the commentary in a single in-place write, and verifies run_id parity across all 6 producer artifacts. Branch on the exit code (complete state machine — do not improvise):
- Exit 0 from the chain —
insert_coaching.py's receipt on stdout says inserted (or already_inserted on a resume). Proceed to Step 9.
check_handoff.py exit 3 (missing/empty file — receipt may be fabricated) → redo-dispatch: fresh Task, same prompt plus one line: "your receipt claimed a file at <path> but none exists; use Write to create exactly that path."
- Exit 4 (file exists, invalid JSON) → repair-dispatch: fresh Task: "Read
<OUTPUT_PATH>; it fails JSON parsing with <verbatim detail from the diagnostic>; fix and rewrite it; return the receipt."
- Exit 5 (receipt echoes a different path) → repair-dispatch telling the agent the exact expected OUTPUT_PATH.
- Exit 6 (receipt unparseable / no
output_path key) → redo-dispatch with "return ONLY the receipt JSON — no fences, no prose." (A status: "blocked" final message is NOT exit 6 — it was handled before the gate.)
insert_coaching.py exit 1 (blocked; stdout carries {"status": "blocked", "reason": ...}) → stop and report the exact reason. Do NOT hand-edit report.md — if the reason mentions a truncated report or a missing marker, re-run compose_report.py --write-md and retry the chain. If the reason is commentary_markdown missing or empty, treat as a malformed hand-off: repair-dispatch quoting the reason.
- After ANY corrective dispatch, resume from the gate chain — never feed
insert_coaching.py an ungated file.
Retry budget: max 2 corrective dispatches (same rule as Context A). Graceful degrade: if the FIRST corrective dispatch also exits 3 while the receipt claims complete with the correctly echoed path, treat the host topology as hand-off-incompatible — fall back to message-channel transport: apply the tolerant JSON extraction protocol to the sub-agent's final message, stage its commentary_markdown payload to $ANALYSIS_DIR/coaching_commentary.json via a quoted <<'COACHING_EOF' heredoc (single-quoted → apostrophe-safe; NEVER python -c, NEVER the outputs/ root), and run the same insert_coaching.py --commentary-file "$ANALYSIS_DIR/coaching_commentary.json" against that staged file.
Verify the receipt before presenting (Step 10 must not deliver a report whose marker was never consumed): the chain's exit 0 IS that verification — do not skip the gate/insert chain and present report.md directly after the dispatch.
Step 9 (Optional): Generate Visual Report
python3 "$SCRIPTS/visualize.py" --dir "$ANALYSIS_DIR" -o "$ANALYSIS_DIR/report.html"
Present the HTML file path to the user so they can open the visual report.
Step 10: Deliver Artifacts
Copy final deliverables to workspace root: {Company}_Market_Sizing.md, .html (if generated), .json (optional).
No cleanup needed: scratch lives in $STAGING_DIR (/tmp, reclaimed by the sandbox). Do not rm
anything under $ANALYSIS_DIR — it is the promoted outputs/ tree in Cowork, where deleting a
user-visible path is unsafe (and the parity gate flags it).
Edge Cases
- Founder provided text, not a file: When the founder describes their market in conversation rather than uploading a file, adapt: write
inputs.json from the conversation and note reduced confidence in data-backed assertions. Set materials_provided: ["text"].
- Cross-skill context: If
founder_context.py returned prior deck-review or financial-model-review runs, mention relevant findings in coaching commentary (e.g., "Your deck claims $X TAM — our analysis calculates $Y"). Do not hard-fail on discrepancies; flag them for the founder.
Main-Thread Return
This skill runs inline in the main thread (not as a sub-agent). The final outcome the main thread delivers to the founder is:
- The path to
$ANALYSIS_DIR/report.md — the primary deliverable.
- The headline outcome fields, sourced from the
coaching_payload you constructed in Step 8 (tam, sam, som, methodology, confidence, high_severity_warnings) plus the insert_coaching.py receipt (status, report_path, run_id). The Context B sub-agent no longer echoes these — do not source them from its return.
- Optionally: the HTML report path from Step 9.
Do NOT inline report_markdown in the assistant message. The founder reads the file via the path. Inlining round-trips ~25 KB of markdown through the parent context unnecessarily.
Scoring
- Each of 22 items: pass / fail / not_applicable
score_pct = pass / (total - not_applicable) x 100
- compose_report.py validates cross-artifact consistency (assumption coverage, sensitivity ranges)
What-If Recomputation Rule
If the founder asks "what if [parameter] were [value]": re-run market_sizing.py and/or sensitivity.py with the modified input and present the script's output. Never recompute TAM/SAM/SOM by hand — the compound formula (customer count × ARPU × serviceable % × target %) makes mental arithmetic error-prone and the script output is the authoritative source.
Feedback
If a run ends blocked or failed, after you report the reason to the founder, add one line:
If this looks wrong or didn't finish, you can flag it: /founder-skills:feedback.
On unsolicited praise or frustration, you may mention /founder-skills:feedback once — never routinely, never mid-workflow, never more than once per session.