本文へ移動
cccskills
無料GitHub で公開

market-sizing

Builds credible TAM/SAM/SOM analysis with external validation and sensitivity testing for startup fundraising: top-down, bottom-up, or both. Use when the user asks to size a market or check market claims and gives something to size: a product or service description, a market segment, or a deck with TAM/SAM/SOM claims. A full run takes about 15–20 minutes and searches the web, 30 or more with a large data room or a revision round, so for a passing question about market size, answer briefly in chat and offer the analysis instead of starting it. When it runs, every figure is externally sourced and stress-tested, never estimated from memory.

インストール方法を見る

含まれるファイル(30)

  • SKILL.md104.7 KB
  • references/artifact-schemas.md35.7 KB
  • references/pitfalls-checklist.md8.1 KB
  • references/tam-sam-som-methodology.md11.0 KB
  • scripts/_compose_result.py11.0 KB
  • scripts/_dispatch_json.py1.9 KB
  • scripts/_ms_gates.py19.9 KB
  • scripts/_params.py7.0 KB
  • scripts/_provenance.py31.3 KB
  • scripts/_quote_match.py4.5 KB
  • scripts/_redteam_copy.py7.8 KB
  • scripts/_redteam_text.py7.8 KB
  • scripts/_revision_answer.py4.2 KB
  • scripts/_run_ref.py7.0 KB
  • scripts/_sizing_redo.py16.5 KB
  • scripts/_theme.py3.0 KB
  • scripts/_thresholds.py2.1 KB
  • scripts/_upload_names.py3.6 KB
  • scripts/_view.py100.6 KB
  • scripts/checklist.py14.1 KB
  • scripts/closing_message.py8.4 KB
  • scripts/compose_report.py190.9 KB
  • scripts/dispatch_prompt.py49.5 KB
  • scripts/market_sizing.py52.6 KB
  • scripts/ocr_uploads.py7.5 KB
  • scripts/pdf_probe.py6.8 KB
  • scripts/record_revision_answer.py8.0 KB
  • scripts/red_team.py10.5 KB
  • scripts/sensitivity.py33.5 KB
  • scripts/visualize.py76.7 KB

SKILL.md(原文)

インストールする前に、エージェントに与えられる指示の中身を確認できます。

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

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.

StepArtifactProducer
1founder contextfounder_context.py read/init
2inputs.jsonAgent (heredoc)
3methodology.jsonAgent (heredoc)
4validation.jsonMain thread (WebFetch/WebSearch research)
5sizing.jsonContext A dispatch: TOP_DOWN_METHODOLOGY + BOTTOM_UP_METHODOLOGY in parallel → market_sizing.py --stdin
6asensitivity.jsonContext A dispatch: SENSITIVITY_TEST → sensitivity.py
6bchecklist.jsonContext A dispatch: CHECKLIST → checklist.py
7Reportcompose_report.py --write-md (writes both report.json and report.md)
8CoachingContext B dispatch: POST_COMPOSE_COACHING

Rules:

  • Deposit each artifact before proceeding to the next step
  • For agent-written artifacts (Steps 2-4), consult ${CLAUDE_PLUGIN_ROOT}/skills/market-sizing/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. Skip it if that path still begins with $.

Run the block below with ${CLAUDE_PLUGIN_ROOT} exactly as it appears.

IFS= read -r TEXT_ROOT_RAW <<'EOF'
${CLAUDE_PLUGIN_ROOT}
EOF
SCRIPTS="${CLAUDE_PLUGIN_ROOT}/skills/market-sizing/scripts"
case "$TEXT_ROOT_RAW" in *'$'*|'') SCRIPTS="" ;; esac   # text arrived unfilled: ignore any exported value
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.
  ours() { python3 -c 'import json, sys
for c in sys.stdin.read().splitlines():
    try: n = json.load(open(c.rsplit("/skills/", 1)[0] + "/.claude-plugin/plugin.json"))["name"]
    except Exception: n = "founder-skills"
    if n == "founder-skills": print(c)'; }
  CANDIDATES="$(find /sessions -type d -path '*/skills/market-sizing/scripts' 2>/dev/null | ours)"
  [ -n "$CANDIDATES" ] || CANDIDATES="$(find /root/.claude/plugins -type d -path '*/skills/market-sizing/scripts' 2>/dev/null | ours)"
  [ -n "$CANDIDATES" ] || CANDIDATES="$(find / -type d -path '*/skills/market-sizing/scripts' 2>/dev/null | ours)"
  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. PLUGIN_ROOT is the shell's path: never Read from it or put it in a sub-agent prompt. Exception: a READ_ROOT= printed below goes in Reads and prompts.
SHARED_SCRIPTS="$PLUGIN_ROOT/scripts"
# PREFLIGHT: the run STOPS if this prints (the skill was served without its plugin). Test for
# check_handoff.py, never a /mnt/skills path: a working first-message session shows that path too.
[ -f "$SHARED_SCRIPTS/check_handoff.py" ] || echo "UNSUPPORTED_ENVIRONMENT: the plugin's shared scripts are not reachable from here"
[ -f "$SHARED_SCRIPTS/check_handoff.py" ] && case "$TEXT_ROOT_RAW" in
  *'$'*|'') echo "PATH_STATE=literal"; echo "READ_ROOT=$PLUGIN_ROOT" ;;
  /sessions/*) echo "PATH_STATE=local" ;;
  *) if [ -d "$TEXT_ROOT_RAW" ]; then echo "PATH_STATE=substituted"; else echo "PATH_STATE=local"; fi ;;
esac
# 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)

If the preflight line printed UNSUPPORTED_ENVIRONMENT, stop here. This environment serves the skill without the rest of its plugin: the shared scripts are missing and sub-agents cannot be dispatched, so the gates, the hand-off checks and the independent review steps below cannot run. Tell the founder, in one sentence, that this skill needs Claude Cowork or Claude Code and that you have not run it — then stop. Do not improvise the missing steps: an analysis that grades itself and reviews itself reads exactly like one that was checked, which is the failure this stop exists to prevent.

Then start the run's record, once, copying every FS_HOST_ line of the request between the markers. RUN_ID is the run_id it prints. Any non-zero exit here or from bind below: say in one sentence that the analysis could not start, and stop. Exit 12 from any script: ask nothing; say what the run is waiting for, and stop.

python3 "<printed PLUGIN_ROOT>/scripts/run_status.py" start --skill market-sizing --artifacts-root "<printed ARTIFACTS_ROOT>" <<'FS_HOST_EOF'
<each FS_HOST_ line of the request>
FS_HOST_EOF

Plugin folder for Reads and prompts (as loaded): ${CLAUDE_PLUGIN_ROOT} — this skill's references are in ${CLAUDE_PLUGIN_ROOT}/skills/market-sizing/references/. If Step 0 printed READ_ROOT=, use that value instead.

Plugin paths. If Step 0 printed READ_ROOT=, this skill's text arrived without its plugin folder filled in. Use that value in place of ${CLAUDE_PLUGIN_ROOT} in every Read and sub-agent prompt, including where a later step says to leave that path literal. If it did not print READ_ROOT=, use the paths as shown. The folder comes from Step 0's filesystem search, not from a skill file or the "Base directory" line, which can name a folder that does not exist. These are setup details: updates to the founder are about their company, not file locations, printed paths or plugin versions.

Outputs mount is append-only. Everything under the promoted outputs mount (.../mnt/outputs/, not just $ANALYSIS_DIR) is write-allowed; by this skill's rule, not a platform limit, nothing there is deleted, since a removed file may be one the founder or a later step still needs: 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). One exception: a PowerPoint deck's PDF rendering goes in the run's hand-off folder, because on a local session the Read tool cannot reach $STAGING_DIR. 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 path set to the printed ARTIFACTS_ROOT and pattern founder-context-*.json to find earlier artifacts (always pass path: on a cloud session the working folder is the home directory). 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 it after the slug block below has created ANALYSIS_DIR and bound the run. Run the same producer the full pipeline uses, with only the inputs the founder gave you:

python3 "$SCRIPTS/market_sizing.py" --stdin --pretty \
  --run-id "$RUN_ID" --currency "$CURRENCY" -o "$ANALYSIS_DIR/sizing.json" <<'JSON'
<the founder's inputs, as the JSON market_sizing.py reads on stdin>
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.

Then close the run's record: python3 "$SHARED_SCRIPTS/run_status.py" finish --mode quick_check --run-id "$RUN_ID" --artifacts-root "$ARTIFACTS_ROOT" --output "$ANALYSIS_DIR/sizing.json".

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
RUN_ID="<the run_id start printed>"
mkdir -p "$ANALYSIS_DIR" && python3 "$SHARED_SCRIPTS/run_status.py" bind --run-id "$RUN_ID" \
  --artifacts-root "$ARTIFACTS_ROOT" --run-dir "$ANALYSIS_DIR" --slug "$SLUG"
# 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 rule
# (nothing under outputs/ is ever deleted, by this skill's rule); 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"
# FILE-TOOL PATHS (Cowork only; skip when ARTIFACTS_ROOT does not start with /sessions/). File tools
# refuse a RELATIVE path and cannot open /sessions/... paths. FIRST, with the Write tool, write
# "<HOST_OUTPUTS>/artifacts/.host-outputs-probe" containing exactly <HOST_OUTPUTS>: the file-tool path
# your context maps to /sessions/<id>/mnt/outputs/ (the "Paths in bash differ from what file tools
# see" list; no such list -> that /sessions/.../mnt/outputs path itself). Then prove it (quoted; it may
# contain spaces). Exit 0 = proven. 2/4/5 -> fix per its JSON, retry once, then STOP:
python3 "$SHARED_SCRIPTS/resolve_artifacts_root.py" --set-host-outputs-dir "<HOST_OUTPUTS>"
# Sub-agents address the SAME dir by a different path (the absolute file-tool path proven above).
# 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 (absolute once proven;
# a relative or /sessions/... read is refused):
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.
# Scratch output (a redirect or a temp file) goes in $STAGING_DIR, never a fixed /tmp/<name>: /tmp is shared across sessions.
STAGING_DIR="$(mktemp -d "${TMPDIR:-/tmp}/market-sizing-${SLUG:-co}.staging.XXXXXX")"
# Every later command runs in a fresh shell: copy these printed values verbatim into it.
printf 'RUN_ID=%s\nSTAGING_DIR=%s\nHANDOFF_DIR=%s  # shell\nHANDOFF_AGENT=%s  # same dir, for Read/Write\n' "$RUN_ID" "$STAGING_DIR" "$HANDOFF_DIR" "$HANDOFF_AGENT"

A resumed run (start printed resume: 1) runs Step 1 and this block again, then continues at the question waiting_on names; the files before it are this run's and stay. For ms_methodology that is the Gate after Steps 2-3: do not redo Steps 2-3 or rewrite inputs.json or methodology.json.

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 (our rule forbids it, and older hosts refused it; the parity gate flags it). Each producer writes its artifact fresh via -o every run, and every run has its own RUN_ID — 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

If the founder's message or the attached materials name the company, add --expect-company "<that name>" to this read (never a file name); a context stored for another company then asks which (Exit 10).

python3 "$SHARED_SCRIPTS/founder_context.py" read --artifacts-root "$ARTIFACTS_ROOT" --run-id "$RUN_ID" \
  --skill market-sizing --pretty

Exit 0 (found): Use the company slug and pre-filled fields. Proceed to Step 2.

Exit 1 (not found: no code, or CONTEXT_NOT_FOUND): Open the four questions (the block below) first. 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.

Each question is a recorded gate: open, ask, answer. Ask from the printed needs_input and record each reply with its answer_command. A field the materials give is recorded unasked: record_gate_answer.py default --gate ctx_basics.<field> --reason derived_from_materials (or inferred) --answer-id use_derived --value "<value>"; a stage: its id, no --value. A Series B+ reply opens ctx_stage_detail, recorded the same way. <!-- gate: ctx_stage_detail -->

python3 "$SHARED_SCRIPTS/record_gate_answer.py" open --run-id "$RUN_ID" --artifacts-root "$ARTIFACTS_ROOT" --gate ctx_basics.company_name \
  --gate ctx_basics.stage --gate ctx_basics.sector --gate ctx_basics.geography

Then create (exit 10 names a question not yet recorded: ask it, record it, run init again):

--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" --run-id "$RUN_ID" --skill market-sizing
  # 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 10 (which company): ask which from the printed needs_input, record it with its answer_command, then re-read with --slug; A different company → as Exit 1. <!-- gate: ctx_select_company --> Exit 1 with another code: report it and stop.

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:

DECK_SRC="<deck path>"
HANDOFF_DIR="<printed HANDOFF_DIR>"; HANDOFF_AGENT="<printed HANDOFF_AGENT>"
B="$(basename "$DECK_SRC")"
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.
  LO_PROFILE="$(mktemp -d "${TMPDIR:-/tmp}/lo-profile.XXXXXX")"   # this block's own: a fresh shell
  "$c" --headless -env:UserInstallation="file://$LO_PROFILE" \
    --convert-to pdf --outdir "$HANDOFF_DIR" "$DECK_SRC" 2>&1 | tail -3
  break
done
# Printed as the Read tool's path: the hand-off folder is the one it reaches (a temp folder is not, on a local session).
if [ -s "$HANDOFF_DIR/${B%.*}.pdf" ]; then echo "$HANDOFF_AGENT/${B%.*}.pdf"; else echo "no pdf — use the text fallback below"; fi

Read the PDF at the path it printed (in this run's hand-off folder: working data, not a deliverable). 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 is a flat object with lowercase keys tam, sam, som (null if not stated); a stated range puts its low end here and its high end in existing_claims_high. Custom keys (SAM_Israel_only) are ignored and raise EXISTING_CLAIMS_SHAPE.

When the materials state more than one figure for a metric, existing_claims holds the one dated to the year your sizing describes (today, unless sizing_basis says otherwise), and among those the one on the market-size slide (else the first stated). Another figure for the same metric and date goes in existing_claims_alternatives as {value, slide, label}; the founder is told both. A figure for another date (today's TAM beside a 2030 one) is not a rival: it goes in existing_claims_detail, unless it is the metric's only figure, which stays in existing_claims (a SOM with its period in existing_claims_horizon_months). Other figures outside the flat shape (regional sub-SAMs) go in existing_claims_detail too (any structure; shown as narrative, not reconciled).

Record the period each SOM covers. existing_claims_horizon_months.som = the deck SOM's period in months (a plan-year run-rate is 12; "by 2028" is months from analysis_date); capture_horizon_months = the period YOUR share_pct/target_pct describe (typically 36/60). Differing periods are reported as such, not as a gap — an 18-month plan held against a 5-year figure once read as a 5.6x understatement.

founder_stated_inputs — facts the founder stated about their own business. A flat object holding arpu (what they charge or collect per customer) when the founder or their materials stated it outright (not researched, not inferred, not your estimate). A figure about the MARKET — a population or customer_count, an industry_total, a share or capture rate — is a claim even when the deck states it: it goes in existing_claims / existing_claims_detail to be compared against, never here, or the build that consumes it restates the deck instead of testing it (compose flags that at high). 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. A discrepancy is a question for the founder, never an edit: a figure the founder did not confirm is not founder-stated. If the founder quoted a rate per month or per quarter, say so in founder_stated_inputs_period ({"arpu": "month"}) and the check normalises it to the annual figure the math uses — a founder-stated $157/month against a computed $1,884 is agreement, not an override.

Two figures for one input. When the founder's materials state more than one figure for the same input (a rate typed in chat and a blended rate in the deck, or two rates in the deck), ask before Step A via AskUserQuestion which one the sizing uses: write the figures as below, open --gate ms_two_figures.<input> (e.g. ms_two_figures.arpu), ask from its needs_input (one option per figure, the typed one first) and record the reply before changing which figure founder_stated_inputs holds. Record the chosen figure in founder_stated_inputs with founder_stated_inputs_source ({"arpu": "chat"} or "document:<file>#page=<n>"), their answer in founder_stated_choice ({"arpu": "<their words>"}; unasked, the report says so), and every other one in founder_stated_alternatives ({"arpu": [{"value": 261, "period": "month", "source": "document:<file>#page=<n>", "label": "<the deck's words>"}]}). The report shows both; never drop one. label is copied exactly from that page, a sentence of six words or more: the report names the page only when those words are found on it, and otherwise says "in your materials". A "chat" source is never shown, since nothing on disk can confirm what was typed.

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 (open --gate ms_fx_rate first, then record the reply) <!-- gate: ms_fx_rate -->; 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 ${CLAUDE_PLUGIN_ROOT}/skills/market-sizing/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:

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_horizon_months": {"tam": null, "sam": null, "som": null},
  "capture_horizon_months": 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 ${CLAUDE_PLUGIN_ROOT}/skills/market-sizing/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. It is a recorded gate: open it first. If it printed "applied": "pre_answer", the request answered it: post Step A, skip Step B, go to Step 4.

python3 "$SHARED_SCRIPTS/record_gate_answer.py" open --run-id "$RUN_ID" --artifacts-root "$ARTIFACTS_ROOT" --gate ms_methodology

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 (annual) | $48K | Deck slide 8 |
| 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

Record the reply with the answer_command open printed.

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 Record it (--gate ms_methodology_change), 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. Once they pick a field, open --gate ms_correct_data.<field> (.other for Something else), then record the reply there (--answer-id set --value "<new value>", or something_else). 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 (after Step 6b has run, and before the adversarial review in Step 6c): 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 the same RUN_ID, then re-run compose_report.py to recompose the report. Never mint a fresh id for a partial refresh: the run is the id most of its files carry, so the refreshed files and the review after them would belong to another run, and compose refuses it. Do not hand-patch checklist.json or report.md directly — that bypasses the sub-agent scoring this architecture exists to preserve, and under one run id nothing downstream can tell a skipped re-dispatch from a done one.

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. For the industry total, prefer a figure for the product's own geography and category; label a broader one as broader.
  • 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"}, and size with the founder's numbers as they gave them (no --validation). Record each in inputs.json (founder_stated_inputs or existing_claims): the report then says the figures are theirs and unchecked. A figure that is not theirs makes it an unchecked sizing, reported as such.

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 figure goes in the assumptions array under a name of its own, with a category of sourced, derived, or agent_estimate, and a unit saying what it measures: money_total_per_year or money_per_customer (both with currency; per-customer also with period: month, quarter or year), count, fraction (0-1), ratio, percent_points (35 = 35%). Record a figure as its source states it: a head-count is a count, even if the market total is built from it. A sourced figure names a sources[] entry in source_title/source_url, or it is graded as an estimate. A figure in another currency needs its rate here too: {"name": "fx_usd_ils", "unit": "fx_rate", "from": "USD", "to": "ILS", "value": 3.72, "as_of": "YYYY-MM-DD", "category": "sourced", ...}, looked up now, never from memory.

Every figure_validations[] entry's status MUST be one of exactly 4 canonical values — do not invent others (e.g. validated_with_caveat, unverified are NOT valid and will misrepresent the figure's validation state):

  • validated — 2+ sources confirm the figure
  • partially_supported — only 1 source
  • unsupported — not investigated / no sources found
  • refuted — investigated and disproved (include a refutation string explaining why)

Write validation.json directly:

cat <<'VAL_EOF' > "$ANALYSIS_DIR/validation.json"
{
  "assumptions": [
    {"name": "regtech_market", "value": 50000000000, "unit": "money_total_per_year", "currency": "USD", "category": "sourced", "label": "Global RegTech market", "source_url": "...", "source_title": "...", "confidence": "high"},
    {"name": "regulated_smb_share", "value": 16, "unit": "percent_points", "category": "derived", "label": "Regulated SMB share", "factors": [{"factor_id": "regulated_share", "value": 0.40, "source_id": "Regulator register 2025"}, {"factor_id": "smb_share", "value": 0.40, "source_id": "company_stated"}]},
    ...
  ],
  "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

factors lists every multiplicand of a narrowing step (two or more); a ratio's denominator carries "role": "divisor". One entry is the value under another name, and a sum is not a chain — describe a sum in label and leave factors out.

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.

$HANDOFF_AGENT and $HANDOFF_DIR name the SAME directory by two different paths — they are not interchangeable. $HANDOFF_DIR is the absolute VM path your shell uses (python3, check_handoff.py, producer pipes). $HANDOFF_AGENT is the absolute file-tool path of the same directory (proven at Step 0), and it is the ONLY one that goes in a dispatch prompt. Putting $HANDOFF_DIR in an OUTPUT_PATH line hands the sub-agent an absolute /sessions/... path the host-loop gate denies; putting $HANDOFF_AGENT in a shell command resolves it against the wrong cwd. Rule of thumb: agent namespace in prompts, shell namespace in bash.

The receipt is the ONE exemption from the never-re-type rule. "Never re-type" governs the payload — the extraction JSON, the coaching commentary, anything the founder's numbers pass through. The receipt is a two-field acknowledgement the sub-agent returns in its final message, and reading output_path out of it to pass to check_handoff.py --agent-path is expected, not a violation. If it were forbidden, the hand-off could not be gated at all.

Path idiom for dispatch prompts (host-loop path gate): OUTPUT_PATH and any under-outputs artifact READ path a sub-agent is given are absolute file-tool paths, never relative ones (a relative file-tool path is refused) — 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), never the shell's path to the plugin folder.

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 -
<!-- skill-quality-ci: bash-after-subagent-ok -->

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." For a printed prompt (RED_TEAM, CHECKLIST, a sizing build: a redo keeps --redo comparison), re-run dispatch_prompt.py with the same arguments plus --correction missing-file and send that output instead.
  • 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." For a generated prompt, use --correction receipt-only.
  • Producer schema rejection (the pipe fails next) → repair-dispatch with the producer's stderr verbatim, unless the message names another remedy: (remedy: record_research) is NOT a sub-agent fault (a missing exchange rate, currency or period that only you can look up): record it and re-run the same pipe. Re-dispatching there burns the retry budget and invites the sub-agent to invent the figure. For a printed prompt (RED_TEAM, CHECKLIST, a sizing build: a redo keeps --redo comparison), re-run the failed pipe with 2> "$HANDOFF_DIR/producer_rejected.txt", then dispatch_prompt.py with the same arguments plus --correction producer-rejected --detail-file "$HANDOFF_DIR/producer_rejected.txt", and send that output.
  • Exit 8 (path_namespace_mismatch) → the sub-agent complied; the agent-namespace prefix was wrong. Its relative OUTPUT_PATH resolved against the outputs mount instead of the session root, so the file landed at the doubled path reported in found_at. Only a RELATIVE agent path can do this, so the Step 0 proof did not run: run it. Do NOT treat this as a fabricated receipt (that is exit 3), and do NOT read the hand-off from found_at — it is diagnostic only. Re-run resolve_artifacts_root.py --agent, rebuild the agent-namespace prefix from the printed value, and re-dispatch. Counts against the same 2-dispatch retry budget.
  • Blocked write_refused (the sub-agent's Write was refused — its path was relative or a /sessions/... path) → run the Step 0 proof if it did not run, rebuild the agent-namespace paths from the resolver, re-dispatch ONCE. A second write_refused STOPs with both details quoted.
  • 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 tell the founder in one plain sentence that this run's working files were passed directly instead of through outputs/, so its audit trail is incomplete (the results are unaffected). A refused write is NOT this case: it returns write_refused (above).

Retries overwrite the same OUTPUT_PATH (nothing under the outputs mount is deleted, by this skill's rule — 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 anywhere under the outputs mount (which includes $ANALYSIS_DIR), and never delete anything under it — see the append-only rule in Step 0.

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.

Each prompt is printed, not written: run the generator once per approach, each in a shell call of its own — nothing before it but variable assignments, nothing after it — and send what it prints unchanged:

<!-- skill-quality-ci: bash-after-subagent-ok -->
python3 "$SCRIPTS/dispatch_prompt.py" top_down_methodology --run-id "$RUN_ID" \
  --analysis-dir "$ANALYSIS_DIR" --analysis-dir-agent "$ANALYSIS_DIR_AGENT" \
  --handoff-dir "$HANDOFF_DIR" --handoff-agent "$HANDOFF_AGENT"

dispatch_prompt.py bottom_up_methodology takes the same arguments. Exit 10 is a question with no record for this run (blocked_by_gate): for ms_methodology ask the Gate's own question above; otherwise ask from the printed needs_input. Record it, run the generator again. Then Task(subagent_type="founder-skills:market-sizing", description="Market sizing: top-down methodology", prompt=<the printed text, whole>) and the same for bottom-up, in one turn.

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:

CURRENCY=$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1])).get("currency") or "USD")' "$ANALYSIS_DIR/inputs.json")
SIZING_BASIS=$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1])).get("sizing_basis") or "")' "$ANALYSIS_DIR/inputs.json")
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" \
    --validation "$ANALYSIS_DIR/validation.json" --inputs "$ANALYSIS_DIR/inputs.json" \
    --currency "$CURRENCY" --sizing-basis "$SIZING_BASIS" -o "$ANALYSIS_DIR/sizing.json"
<!-- skill-quality-ci: bash-after-subagent-ok -->

The calculator reads each input's value from the research record, checks it measures what the input needs, and records in sizing.json what it used and where from. --currency labels the analysis; --sizing-basis carries inputs.json's convention (an empty value means "not declared").

When it refuses, its message ends with the remedy. (remedy: redispatch): repair-dispatch the sizing step with the message verbatim (a head-count referenced as the market total is fixed by deriving the total from the head-count and a price, never by relabelling the figure). (remedy: record_research): the record lacks something only you can look up (a currency, a period, an exchange rate with its source and date); record it in validation.json and re-run the SAME pipe; this is not a sub-agent fault, so do not re-dispatch. (remedy: founder_question): ask the founder (e.g. whether their price is per month or per year); it exits 10 with that question's needs_input (ms_input_period.<input>): record the reply and the period, then re-run the same pipe. Tell the founder what you are doing in their terms, never the code.

If the founder's own figures or the deck's claims are in another currency, record which in inputs.json (founder_stated_inputs_currency, existing_claims_currency).

Single methodology dispatch

When methodology is "top_down" only: print and 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" --validation "$ANALYSIS_DIR/validation.json" --inputs "$ANALYSIS_DIR/inputs.json" --currency "$CURRENCY" --sizing-basis "$SIZING_BASIS" -o "$ANALYSIS_DIR/sizing.json" (deriving $CURRENCY and $SIZING_BASIS from inputs.json exactly as above). When methodology is "bottom_up" only: same with bottom_up_output.json.

For "both", read sizing.json's comparison: each of TAM, SAM and SOM is gated at >30% (tam_delta_pct, sam_delta_pct, som_delta_pct); TAM agreeing says nothing about SAM/SOM. A TAM/SAM/SOM discrepancy over 30% means an input may be wrong, or the two builds may measure different markets (say, a global all-category total against a count of the product's buyers); record a better-fitting figure in validation.json if research finds one. Then, once per run, re-run that build's generator with the same arguments plus --redo comparison (--suspect <input> for inputs you doubt), send its output in your next call, gate it, and re-run the same pipe. Never print that build's plain prompt again or edit a sizing prompt by hand. That redo, with the hand-off corrections above for it, is the only sizing dispatch a gap allows; a redo refused as SIZING_REDO_SPENT or SIZING_REDO_TOO_LATE is not retried. A gap that remains is reported, not removed: no later step re-dispatches a build for it unless the founder asks at Step 6d.

Multi-vertical / platform companies: If inputs.json lists applications in 2+ distinct industries:

  1. Identify verticals — classify as commercial (revenue/pilots), r_and_d (demonstrated feasibility, 2-3yr commercialization path), or future (conceptual/early).
  2. 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.
  3. Narrow SAM and SOM — SAM = traction + active R&D segments. SOM = beachhead only.
  4. 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:

  1. Laugh test: Would an experienced VC nod or raise an eyebrow? Seed + <5 pilots + >$1B TAM = explain yourself.
  2. Scope match: Does TAM cover all commercial and r_and_d verticals from inputs.json?
  3. Customer count sanity: Can you name a representative sample of the customers in your count?
  4. Convergence integrity: Were top-down and bottom-up parameters set independently? If you adjusted one after seeing the other, revert and accept the delta. Check TAM, SAM, and SOM delta separately — a converged TAM does not guarantee converged SAM/SOM.
  5. Percent scale: if market_sizing.py printed IMPLAUSIBLE_PCT_SCALE, ask the founder now, via AskUserQuestion, whether they meant the value as given (option 1) or that many percent (open --gate ms_pct_scale.<input> first; record the reply) — the review in Step 6c must see the answer, and its prompt is not printed without it.

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>/sizing.json: `input_provenance` gives, for each input, the value the sizing
used (`value_consumed`) and its grade (`category`). sensitivity.py takes both from there itself, so
you choose only the ranges. Tag each with a confidence: `sourced`, `derived` (min +/-30%),
`agent_estimate` (min +/-50%). Include EVERY `agent_estimate` input — 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",
  "ranges": {
    "<parameter>": {"low_pct": <negative number>, "high_pct": <positive number>, "confidence": "sourced|derived|agent_estimate"}
  }
}
**`sourced` splits on the assumption's own `confidence`; omission is the narrow case, not the default.**
Source states a range → `sourced`. `sourced` + `confidence: high` + no stated range → omitting is
acceptable. **`sourced` + `confidence` medium or low → include at `derived` (±30%)**: `sourced` means
corroborated, not precise, and that cell is often the least certain input in the model. Any consumed
parameter with no range is reported as `SENSITIVITY_OMITS_PARAM` unless it is `sourced`/`high`.

**A declared `confidence` cannot narrow the recorded grade**: sensitivity.py applies the stricter
of the two, so tagging an estimate `sourced` does not avoid widening. Tag honestly.
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 — you never write a canonical
artifact; 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" --sizing "$ANALYSIS_DIR/sizing.json" \
    --inputs "$ANALYSIS_DIR/inputs.json" -o "$ANALYSIS_DIR/sensitivity.json"
<!-- skill-quality-ci: bash-after-subagent-ok -->

CHECKLIST dispatch prompt template

The prompt is printed, not written: run this, then dispatch the CHECKLIST sub-agent with the printed text as its prompt, unchanged — nothing added, removed or reworded, in either round. It also writes the copy of methodology.json the grader reads, which leaves out the revision record.

Run the generator in a shell call of its own — nothing before it but variable assignments, nothing after it — and send what it prints unchanged.

python3 "$SCRIPTS/dispatch_prompt.py" checklist --run-id "$RUN_ID" \
  --analysis-dir "$ANALYSIS_DIR" --analysis-dir-agent "$ANALYSIS_DIR_AGENT" \
  --handoff-dir "$HANDOFF_DIR" --handoff-agent "$HANDOFF_AGENT"

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" --sizing "$ANALYSIS_DIR/sizing.json" \
    --inputs "$ANALYSIS_DIR/inputs.json" -o "$ANALYSIS_DIR/checklist.json"
<!-- skill-quality-ci: bash-after-subagent-ok -->

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 6c: Adversarial Review -> redteam.json (Context A dispatch, RED_TEAM)

Dispatch the red-team agent ONCE via the Task tool, with subagent_type: "founder-skills:market-sizing-redteam". This is a different agent from the one every other step in this skill dispatches, and the difference is its tool allowlist: it has WebSearch and the others do not. Allowlists are per-agent, so putting web search on the shared agent would hand the network to dispatches that state in writing that they have none — including the one whose FX rule depends on that being true.

RUN THIS STEP. Its SUCCESS is optional; attempting it is not. If the dispatch fails, or the agent returns BLOCKED, or the producer rejects the hand-off, continue to Step 7 — the report discloses that no adversarial review ran rather than presenting an unchallenged analysis as a checked one. Do NOT abort the run over it.

Do not skip it because the founder supplied no documents: most of what this step finds comes from published figures, not from the founder's deck, and a conversational run is the case with the LEAST corroboration and the most to gain. The only reason to skip is an explicit instruction from the founder not to.

You cannot silently skip it. compose_report.py in Step 7 REFUSES to produce a report unless one of two things is true: a fresh redteam.json exists for this run, or the decision not to run one is recorded in methodology.json as red_team_skipped, with red_team_skipped_run_id set to this run's RUN_ID (compose accepts only a skip of the run it composes), whose value must be one of exactly three:

ValueWhen
founder_declinedthe founder asked you not to run one
dispatch_failedyou dispatched it and it returned BLOCKED, or the producer rejected the hand-off
no_network_availablethis run has no access to outside sources
no_subagent_dispatchthis environment runs the analysis as ONE agent and cannot dispatch a reviewer at all — nothing was attempted, so "worth re-running" would be false here

There is deliberately no value meaning "it did not seem necessary". Each value maps to a different sentence the founder reads, because "you asked us not to" and "we tried and could not" are different disclosures.

(Measured, and the reason this is a refusal rather than a warning: on a live run an earlier draft that called the step "optional" was skipped outright. Its only downstream consumer was a low-severity disclosure, and this repo had already learned once — in deck-review's numeric chain — that a step whose only consumer is a warning gets skipped in silence. Severity could not fix it either: Step 7 runs compose without --strict, so even a high warning halts nothing.)

The founder's uploads live outside outputs/, where a sub-agent cannot reach them, so mirror them into the hand-off dir first (exit 3 from the resolver = no uploads folder: nothing was attached, or this host keeps uploads elsewhere; then copy into $HANDOFF_DIR/docs the founder documents you read earlier, from the path you read them at, and ask for a path only if you never had one: open --gate ms_upload_path first) <!-- gate: ms_upload_path -->, machine-read every scanned page so a citation to one can be checked, then generate the dispatch prompt and pass it unchanged — the lane that tests this regenerates it and compares the two:

python3 "$SHARED_SCRIPTS/resolve_artifacts_root.py" --uploads   # prints UPLOADS_DIR, or exits 3
mkdir -p "$HANDOFF_DIR/docs" && cp "<printed UPLOADS_DIR>"/* "$HANDOFF_DIR/docs/"

Machine-reading is its own call — minutes on a large data room, and a timeout kills a bundled call mid-way. Re-run it after a timeout; it resumes. The generator refuses until the receipt covers every scanned PDF:

python3 "$SCRIPTS/ocr_uploads.py" --uploads-dir "$HANDOFF_DIR/docs" --out "$HANDOFF_DIR/ocr"   # exit 0 always

Run the generator in a shell call of its own — nothing before it but variable assignments, nothing after it — and send what it prints unchanged.

python3 "$SCRIPTS/dispatch_prompt.py" red_team --run-id "$RUN_ID" \
  --analysis-dir "$ANALYSIS_DIR" --handoff-dir "$HANDOFF_DIR" \
  --analysis-dir-agent "<ANALYSIS_DIR_AGENT>" --handoff-agent "<HANDOFF_AGENT>"

Task(subagent_type="founder-skills:market-sizing-redteam", prompt=<the printed text, whole>).

After the sub-agent returns: gate the hand-off per the Context A hand-off protocol, then pipe:

cat "$HANDOFF_DIR/redteam_output.json" | \
  python3 "$SCRIPTS/red_team.py" --pretty --run-id "$RUN_ID" \
    --uploads-dir "$HANDOFF_DIR/docs" --ocr-dir "$HANDOFF_DIR/ocr" -o "$ANALYSIS_DIR/redteam.json"
<!-- skill-quality-ci: bash-after-subagent-ok -->

A finding that arrives without a source is set aside with its reason and the rest are kept — the producer never discards the whole hand-off over one weak finding, and the count of what was set aside reaches the founder. An empty findings list is a valid result: report it as "the review ran and found nothing", never as a failure.

Step 6d: One Revision, Only If the Founder Asks for It

A review is final. Only if an accepted finding in redteam.json is high and names a parameter: open the question first, record_revision_answer.py --dir "$ANALYSIS_DIR" --from-pre-answer (applied or answered: settled, ask nothing), then ask via AskUserQuestion: option 1 "Deliver with the challenges shown", option 2 "Revise the challenged inputs and have it reviewed once more (about 10 minutes)". Otherwise go to Step 7. Record the answer (without it, compose refuses): python3 "$SCRIPTS/record_revision_answer.py" --dir "$ANALYSIS_DIR" --answer deliver|revise --source founder

On "Revise", ask which changes to make (ms_revision_changes, opened by that answer; record the reply with --answer-id, one id per challenged input, comma-joined): one option per change you propose, written <input>: <from> → <to> (<its source>), plus "None of these". A figure the founder stated changes only to one of their own figures or one they type. Make exactly the edits they confirmed; if a change's reason needs recording, it goes in methodology.json "red_team_revision": {"changes": [{"field": "<input>", "reason": "<why>"}]}, which nothing reads or prints. The report states what changed between the reviews from the reviews themselves. Change those figures in validation.json, then re-run the sizing with market_sizing.py --replay "$ANALYSIS_DIR/sizing.json" --validation "$ANALYSIS_DIR/validation.json" --inputs "$ANALYSIS_DIR/inputs.json" -o "$ANALYSIS_DIR/sizing.json" (a change that swaps what an input refers to, such as the founder's own ARPU for the researched one, re-dispatches that sizing step instead). Then re-run 6a/6b and 6c once, with the same RUN_ID, on round-2 hand-off paths, reusing round 1's documents and their OCR:

HANDOFF_DIR="$ANALYSIS_DIR/handoff/$RUN_ID/r2"; mkdir -p "$HANDOFF_DIR"
python3 "$SHARED_SCRIPTS/resolve_artifacts_root.py" --handoff-dir-agent \
  --dir-name "market-sizing-${SLUG}" --run-id "$RUN_ID/r2"   # the round-2 HANDOFF_AGENT
# Step 6c in round 2: no mirror, no OCR. The generator and the producer read round 1's documents:
#   dispatch_prompt.py ... --review-docs-dir "$ANALYSIS_DIR/handoff/$RUN_ID" --review-docs-agent "<round-1 HANDOFF_AGENT>"
#   red_team.py ... --uploads-dir "$ANALYSIS_DIR/handoff/$RUN_ID/docs" --ocr-dir "$ANALYSIS_DIR/handoff/$RUN_ID/ocr"

The review shown is the first one of the analysis as delivered; any later review of the same analysis is listed beneath it, so a re-run neither replaces nor hides one. Labels, prompts and chat describe figures, never their history — the report states the reviews and what changed between them. Why a figure changed goes in that change's reason, which no page prints and the review never reads; a label names only what the figure is ("Reseller-channel share of qualifying accounts", not "set independently"). A founder answer after Step 6c that would change an input is this round while it is unused; once it is used, record the answer in methodology.json founder_notes and deliver.

If the founder asked you not to ask questions, take option 1 here (recorded with --source no_questions), at Step 5.5's percent-scale question and at the two-figures question (Steps 2–3): record each with record_gate_answer.py default --gate <gate> --reason asked_not_to_be_asked, and continue. For these questions only, this overrides the plain-chat fallback: do not wait.

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"

Warnings split into two kinds — treat them differently. The discriminating test is: can re-running fix it?

  • Pipeline-integrity (SIZING_INVALID, ARTIFACT_INVALID, CORRUPT_ARTIFACT, MISSING_ARTIFACT, STALE_ARTIFACT, OVERCLAIMED_VALIDATION) mean the run itself is broken. Fix the underlying issue and re-run compose.
  • Content findings (CHECKLIST_FAILURES, CHECKLIST_FAILURES_CRITICAL, RED_TEAM_FINDINGS) are the analysis's honest verdict about the sizing. Report them to the founder as-is — never re-score, re-dispatch, reword, or edit the analysis or the review to make them disappear. Re-running cannot fix a finding that is true.
  • About the review itself (REDTEAM_ALTERED, REVIEW_COPY_MISSING, RED_TEAM_SKIP_CONTRADICTED): each message states its own remedy. REVISION_NOT_OFFERED: ask Step 6d's question now, record the answer, re-run compose. ANALYSIS_CHANGED_BETWEEN_REVIEWS is a disclosure: deliver.
  • About the sizing's inputs. SIZING_STALE (research changed before the review): re-run with --replay as in Step 6d. SENSITIVITY_STALE: re-run the Step 6a pipe. CHECKLIST_STALE: re-dispatch the checklist, or accept it with the reason. SIZING_UNRESOLVABLE: restore the research figure the sizing named. SIZING_NOT_CHECKED: re-run Step 5 with references. RECORD_CHANGED_AFTER_REVIEW: restore what the review saw, or ask the founder (Step 6d); a replay does not clear it. SIZING_ALTERED and UNIT_CHANGED_AFTER_REJECTION are disclosures: deliver. Never edit sizing.json, the review, or a research figure's unit to make any of these agree.

Two codes sit in neither class:

  • IMPLAUSIBLE_PCT_SCALE fires for any share between 0 and 1, because 0.35 meaning 35% and a legitimate 0.35% are indistinguishable from the number alone — the producer says so itself. Treating it as pipeline-integrity would tell you to "fix and re-run" a figure that may be correct, which is an instruction to change a right number. Ask the founder which they meant at Step 5.5, before the review; here it is already answered.
  • UNVALIDATED_CLAIMS cannot distinguish not investigated from searched and no support found. The first is a gap in the run, the second is a finding about the market. Say which one it is, from what the run actually did.

Use --strict to enforce a clean report. Note it blocks on high and medium, so it cannot be used as a pipeline-only gate — a content finding stops it too.

A warning code you do not recognise is still real. Treat it by what it is, never by silence: fix it and re-run if the run itself is broken, otherwise say what it means for the founder in plain language. A FOUNDER_TEXT_TOKEN message says where the token is and what to do about it.

Post-write verification: compose_report.py exits 2 if an output is missing or empty. On a non-zero exit, read compose_result.json beside the report for the codes. Exit 10 is a question, not a failure: ask the gate its JSON names (blocked_by_gate; ms_revision is Step 6d; ms_methodology is the Gate, then Steps 5–6 again), record it, compose again; never re-run compose without the answer. Any other non-zero exit: 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 STAGES it as a file in the hand-off dir; the sub-agent Reads it from the agent namespace (a functionally required read, so a wrong prefix fails loudly before anything is written). The sub-agent does NOT Read full report.md — it consumes the staged coaching_payload.json directly, composes the coaching commentary, and WRITES it as plain markdown to the OUTPUT_PATH hand-off file (a .md file) with its Write tool — no JSON, no escaping — 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 --format=markdown, transforms it into the JSON transport envelope with md_to_commentary.py (deterministic escaping — json.dumps cannot emit malformed JSON), then pipes it into the shared insert_coaching.py script (idempotency matrix, uuid-marker replacement, run_id-parity verification — all deterministic, unchanged). See the market-sizing agent body's "Context B — Post-compose coaching dispatch (POST_COMPOSE_COACHING)" section for the full procedure.

Before dispatching, STAGE the coaching_payload as a file the sub-agent Reads — do not paste it into the prompt. The Read is what makes a wrong agent-namespace prefix fail loudly BEFORE the sub-agent writes anything:

<!-- skill-quality-ci: bash-after-subagent-ok -->
python3 -c '
import json, sys
data = json.load(open(sys.argv[1]))
json.dump(data["coaching_payload"], open(sys.argv[2], "w"), indent=2)
print(json.dumps({"staged": sys.argv[2]}))
' "$ANALYSIS_DIR/report.json" "$HANDOFF_DIR/coaching_payload.json"

Never capture it into a shell variable — each Bash call runs in a fresh shell.

If a field the payload needs is missing from report.json, construct it 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.md
ANALYSIS_DIR_AGENT: <ANALYSIS_DIR_AGENT — the agent-namespace value from Step 0, NOT an absolute path>
# Context only. A Context-B sub-agent opens neither report.md nor any artifact here; it Reads
# only the staged coaching_payload.json. Handing it a VM-absolute /sessions/... path would be an
# invariant break (the host-side containment hook denies gated file ops on that namespace) and a
# live invitation to try one.
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.

Read the coaching payload at <HANDOFF_AGENT>/coaching_payload.json.

If that Read FAILS, write NO file and return exactly:
{"status": "blocked", "reason": "handoff_path_unresolvable", "attempted": "<the path you tried>"}
Do not Glob for it, do not guess a different prefix, do not proceed from memory —
a failed Read here means the hand-off prefix is wrong and the main thread must
re-issue the dispatch. Reporting it is the correct outcome.

Its shape (for reference — read the file, do not reconstruct it):
{
  "schema_version": "v0.7.0-market-sizing",
  "summary": {
    "score_pct": <number from checklist.json summary>,
    "overall_status": "<strong | solid | needs_work | major_revision — how good the sizing is>",
    "all_pass": <true only when nothing failed — a `strong` sizing can still have an open item>,
    "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": [<objects {code, label, message} — copy verbatim from coaching_payload; write the label, never the code>],
  "comparison_blocked": <copy from report.json coaching_payload.comparison_blocked — when `any` is
                         true those figures were never cross-checked; do not coach as if they were>,
  "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>,
  "market_size_approach": "<bottom_up | top_down | null — which build produced the tam/sam/som above; null means no sizing.json was resolvable, so say the figures are unsourced rather than naming a build>",
  "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>,
  "approach_comparison": <null on a single-approach run, else {"tam_gap": ..., "sam_gap": ..., "som_gap": ..., "shared_inputs": [{"metric": ..., "detail": ...}], "caveat": ...} — copy verbatim>,
  "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 [])
   The commentary is appended to the founder's report, so write it in their
   language: never a checklist item id (`figures_triangulated`), a warning code
   (`UNVALIDATED_CLAIMS`) or one of our filenames. Say what the finding IS.
     Instead of: "The `figures_triangulated` failure points to thin sourcing"
     Write:      "Three key assumptions rest on a single source"
2. Use your Write tool to write to OUTPUT_PATH exactly the coaching commentary
   as plain markdown — do NOT wrap it in JSON, do NOT escape anything (your
   Write tool handles newlines and quotes). 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 md_to_commentary.py + insert_coaching.py scripts.
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. The one exception is write_refused: run the Step 0 proof if it did not run, rebuild <HANDOFF_AGENT> from the resolver, and re-dispatch once; a second write_refused stops. Otherwise gate the hand-off, then (on gate exit 0) transform and insert deterministically. The commentary leaves the model exactly once (into the sub-agent's Write call) — NEVER re-type the sub-agent's markdown into a heredoc or a python -c argument.

<!-- skill-quality-ci: bash-after-subagent-ok -->
# Step 0 already resolved PLUGIN_ROOT once, deterministically — reuse its printed
# value here rather than re-running the self-heal search in this fresh shell.
SHARED_SCRIPTS="<printed PLUGIN_ROOT>/scripts"
printf '%s' '<agent final message verbatim>' | \
  python3 "$SHARED_SCRIPTS/check_handoff.py" "$HANDOFF_DIR/coaching.md" \
    --format=markdown --agent-path "$HANDOFF_AGENT/coaching.md" --receipt-json - \
    --marker '<EXACT insertion_marker string from report.json>'

On gate exit 0, transform the gated hand-off FILE into the JSON transport envelope and insert (feed the file, never re-type the message):

<!-- skill-quality-ci: bash-after-subagent-ok -->
SHARED_SCRIPTS="<printed PLUGIN_ROOT>/scripts"
python3 "$SHARED_SCRIPTS/md_to_commentary.py" "$HANDOFF_DIR/coaching.md" | \
  python3 "$SHARED_SCRIPTS/insert_coaching.py" \
    --report "$ANALYSIS_DIR/report.md" \
    --report-json "$ANALYSIS_DIR/report.json" \
    --marker '<EXACT insertion_marker string from report.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 --format=markdown) verifies the sub-agent's hand-off file exists, is non-empty, matches the receipt's echoed path, and passes the content-shape gate (not receipt-shaped, no marker collision); md_to_commentary.py wraps the raw markdown in the {"commentary_markdown": ...} envelope (escaping by construction via json.dumps); insert_coaching.py then 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 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.)
  • Exit 7 (content-shape gate failed — receipt-shaped or marker-bearing file) → repair-dispatch: "your file wasn't the coaching commentary — write the coaching markdown, nothing else, to <OUTPUT_PATH>."
  • Exit 8 (path_namespace_mismatch) → the sub-agent complied; the agent-namespace prefix was wrong. Its relative OUTPUT_PATH resolved against the outputs mount instead of the session root, so the file landed at the doubled path reported in found_at (only a relative path can do this: run the Step 0 proof). Do NOT treat this as a fabricated receipt, and do NOT read the hand-off from found_at — re-dispatch with the corrected agent-namespace prefix (re-run resolve_artifacts_root.py --agent and rebuild <HANDOFF_AGENT> from the printed value). Counts against the same 2-dispatch retry budget.
  • Any other exit (script crash, unreadable file, invalid UTF-8) → STOP with the stderr. check_handoff.py exit 4 is reachable here too: gate 1 already confirmed the file exists and is non-empty, so a failure opening it afterwards is an IO/permission fault, not a malformed hand-off. A decode error raises before any typed exit and surfaces as a traceback.
  • 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 the transform+insert pipe 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 and fall back to message-channel transport, and tell the founder in one plain sentence (see Context A's degrade rule). The corrective dispatch MUST ask for the commentary inline for this to be reachable — add: "the file hand-off is not working in this environment; return the coaching commentary itself as your final message, as raw markdown, with no receipt JSON and no fences." Without that line the fallback is unreachable: the normal Context B prompt instructs the agent to return ONLY the receipt and not to narrate, so its final message contains no markdown to stage. Then stage that returned markdown to $STAGING_DIR/coaching.md via a single-quoted <<'COACHING_EOF' heredoc (apostrophe-safe; NEVER python -c, NEVER the outputs/ root — $STAGING_DIR is the /tmp scratch dir from Step 0, never the promoted outputs mount), and run the same md_to_commentary.py "$STAGING_DIR/coaching.md" | insert_coaching.py chain 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" --run-id "$RUN_ID" -o "$ANALYSIS_DIR/report.html"

Do not hand this over here — the Deliver step below is the only place work reaches the founder, and it sends the complete set as files. A path presented here is the partial-delivery bug.

Step 10: Deliver Artifacts

Copy final deliverables to the workspace root — $ARTIFACTS_ROOT/.., i.e. the promoted outputs mount itself, NOT $ARTIFACTS_ROOT and NOT $ANALYSIS_DIR: {Company}_Market_Sizing.md, .html (if generated), .json (optional). Concretely, if $ARTIFACTS_ROOT is <mount>/artifacts then these go to <mount>/. That is the level the founder sees as deliverable cards; artifacts/ below it is working state. Do not infer the level by elimination — dirname "$ARTIFACTS_ROOT" is the answer.

Then close the run's file list (for a host; nothing to tell the founder):

python3 "<printed PLUGIN_ROOT>/scripts/run_status.py" deliverables --run-id "$RUN_ID" --artifacts-root "$ARTIFACTS_ROOT" --final || :

Send the finished work to the founder — the complete set, as files. Not a path, and not a subset. A path is not a deliverable in Cowork — whether the workspace it names outlives the task depends on how that task was started, so a founder who was handed only a path may end up with nothing. (That is your reason for sending files; it is not something to tell the founder — see the no-claims rule below.) Send every finished document you produced for them, and frame them as results you generated rather than something they asked to look at.

Then hand them over by name — one named entry per document. Sending the files and handing them over are different acts: a founder looking at a row of cards cannot tell which document is which. Write each deliverable into your message as its own named entry — a link where this surface renders one that opens (on a /sessions session tree, computer:// + the absolute path you just copied it to), otherwise the label with the path stated beside it — labelled by what the document IS, in the founder's words (the written report, the charts), not by its filename. "The files are above" is not a hand-over. Where a step prints the hand-over, the printed entries are these. This does not conflict with the never-name-a-file rule: the founder reads your label, never the path. Never paste a report's body into the message — link or name it.

Then offer the working data — once, in one sentence. For example: "If you want to keep the working data behind this — to pick it up later, or feed it into another analysis — say so and I'll send it as a single archive." Make no claim about whether anything persists, in either direction: that depends on how the task was started, and it is not something to assert. If a folder is connected to the task, offer to write the full set there instead.

If the founder says yes, assemble the archive in your working scratch but write the finished file into the same directory as the deliverables, and send it from there. (Your reason, not something to tell the founder: a scratch path cannot be handed over at all, and attempting it fails the entire delivery — taking the real deliverables down with it. The scratch dir stays where it is; only the finished archive moves.) Include only the reusable inputs — the validated figures and extractions this analysis was built from, plus the composed report data. Never include pipeline hand-off files, receipts, coaching payloads, or gate state: they mean nothing outside the run that made them.

In this skill the hand-over message is printed, not written. It is the links, the report's opening verdict paragraph (the same words, its marks explained), and the offer. Send the files first; then run this as your LAST tool call:

python3 "$SCRIPTS/closing_message.py" --report "$ANALYSIS_DIR/report.json" \
  --deliverable "the written report=<absolute path you copied the .md to>" \
  --deliverable "the charts=<absolute path you copied the .html to, if generated>"

Its printed output is your entire final message, character for character — every dash, line and link as printed — with no summary, sources or note before or after it. It already answers whether the founder's figures hold up, so there is nothing to add.

Here the printed hand-over replaces the named entries and the offer described above: its archive offer is the only offer made, and a connected folder is written to only if the founder asks.

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). What reaches the founder is the delivered files and the printed hand-over message (Step 10) — nothing else. The headline fields in the staged coaching_payload (tam, sam, som, methodology, confidence, high_severity_warnings, comparison_blocked) and the insert_coaching.py receipt (status, report_path, run_id) are what the main thread HAS for its own bookkeeping; they are already on the page, and the Context B sub-agent does not echo them. In Claude Code the report path is the deliverable; in Cowork the files are.

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 (plain numbers, no --validation; the result says it was not checked against the research) and/or sensitivity.py with the modified input, -o into $STAGING_DIR (never $ANALYSIS_DIR — a what-if presents, it does not revise the reviewed analysis), 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: type /founder-skills:feedback.

On unsolicited praise or frustration, you may tell the founder they can type /founder-skills:feedback once — never routinely, never mid-workflow, never more than once per session.

レビュー

まだレビューはありません。使ってみた感想をお寄せください。

同じリポジトリのスキル

概要と使いどころ

cap-table

無料

Use for any cap-table number, mechanic, or date before a founder signs, from a single SAFE, note or warrant described in chat or a quick 'is this dilution reasonable?' check to any draft or signed SAFE, note, term sheet, option plan, AoA, Carta XLSX, or spreadsheet. A single question gets a short cited answer in about 2–3 minutes; the full report, about 5–10 minutes, runs only when the founder shares documents or asks for it. Source-cited deterministic math (YC, NVCA, Cooley GO) for SAFE/note conversion, priced-round dilution, anti-dilution, option pools, warrants, MFN chains, dual-class voting, QSBS and Israeli §102 dates, and Israeli ↔ Delaware flips. NOT for waterfall modeling, cumulative dividends, RSUs, 83(b), 409A, SPAC, warrant repricing, or pure term definitions.

日本語の概要は準備中です。原文の説明を表示しています。

lool-ventures/founder-skills442026年10月12日 更新

Maps a startup's competitive landscape, scores moat strength across 6+ dimensions, and writes an investor-ready competition narrative with a positioning map. Use when the user asks for a competitive landscape, moat analysis, or positioning review and gives enough to work from: a deck, a list of competitors, or a clearly named startup. A full run takes about 30–40 minutes and searches the web, so for a passing question such as 'who are my competitors?' or 'is that a real moat?', answer briefly in chat and offer the analysis instead of starting it. When it runs, the competitor set and moat scores are checked adversarially, never recalled from memory.

日本語の概要は準備中です。原文の説明を表示しています。

lool-ventures/founder-skills442026年10月12日 更新

Scores and strengthens startup pitch decks (pre-seed through Series A) against 35 investor-grade criteria grounded in Sequoia, DocSend, YC, a16z, and Carta data. Use when the user has shared a pitch deck (PDF, PPTX, markdown, pasted slide text, or a link to one) and asks for a review, score, feedback, or critique. A full review takes about 15–25 minutes, 40 or more for a long investor deck, so for a general pitch question with no deck, answer briefly in chat and offer the review instead of starting it. When it runs, score with the rubric, never with deck advice from memory.

日本語の概要は準備中です。原文の説明を表示しています。

lool-ventures/founder-skills442026年10月12日 更新

feedback

無料

Send feedback about founder-skills — report a bug, suggest an idea, ask for help, or share a win. Drafts your message and gives you a link to submit; nothing is sent automatically.

日本語の概要は準備中です。原文の説明を表示しています。

lool-ventures/founder-skills442026年10月12日 更新

Reviews startup financial models for investor readiness: validates unit economics, stress-tests runway scenarios, and benchmarks metrics against stage-appropriate targets. Use the full review, about 10–15 minutes, when the user provides a financial model (Excel, CSV, or a model pasted as text) and asks for review, validation, runway analysis, or unit-economics scoring. For a single money question with figures to compute, such as 'how long do I have?' or 'is a 4x burn multiple bad?', use its shorter quick check, so the answer comes from the calculator and dated benchmarks, not mental arithmetic.

日本語の概要は準備中です。原文の説明を表示しています。

lool-ventures/founder-skills442026年10月12日 更新

ic-sim

無料

Simulates a realistic VC Investment Committee: three partner archetypes debate a startup's merits, concerns, and deal terms, scored across 28 dimensions. Use when the user asks to simulate an IC, or how partners would debate their startup, and gives a deck, a company description, or a specific fund. A full run takes about 15–20 minutes, so for a passing question such as 'would a VC fund us?' or 'are we fundable?', answer briefly in chat and offer the simulation instead of starting it. When it runs, partners' views come from the scored simulation, never improvised.

日本語の概要は準備中です。原文の説明を表示しています。

lool-ventures/founder-skills442026年10月12日 更新

lool-ventures のスキルをすべて見る

このスキルの問題を報告する