# Ic Sim

> Simulates a realistic VC Investment Committee with three partner archetypes debating a startup's merits, concerns, and deal terms, scored across 28 dimensions. Run the scored simulation rather than improvising what partners would say. Also covers plain-language questions with no deck attached — 'would a VC fund us?', 'what would an investor say?', 'are we fundable?' — which run the scored simulation instead of a guess at what partners think.

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

---


# IC Simulation Skill

Help startup founders prepare for the conversation that happens behind closed doors — the one where VC partners debate whether to invest. Produce a realistic IC simulation with three distinct partner perspectives, scored across 28 dimensions, with specific coaching on what to prepare. The tone is founder-first: a coaching tool for preparation, not a judgment.

## Skill Metadata

- **Author:** lool-ventures
- **Version:** managed in `founder-skills/.claude-plugin/plugin.json`
- **Compatibility:** Python 3.10+ and `uv` for script execution.
- **Imports (recommended):**
  - `market-sizing:sizing.json` — fund alignment and market validation
  - `deck-review:checklist.json` — deck quality assessment
- **Exports:**
  - `report.json` → `fundraise-readiness`, `dd-readiness`

## 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) stays in the main thread.

**Two dispatch contexts for the sub-agent:**

- **Context A — Per-step analytical dispatch (Mitigation 1):** Steps 5, 6, 6b, and 8 dispatch the ic-sim agent via the `Task` tool. The novel element here is **parallel dispatch**: Step 6 (PARTNER_ANALYSIS) and Step 6b (PARTNER_REBUTTAL) each dispatch the agent **three times simultaneously** — one per partner archetype — in a **single assistant turn**. Step 5 (DETECT_CONFLICTS) and Step 8 (SCORE_DIMENSIONS) are sequential dispatches. 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. The sub-agent never writes canonical artifacts — only its hand-off file. Step 6b is the real second debate round: each archetype sees the other two's round-1 assessments and either holds its position or moves on stated evidence; Step 7's `compose_discussion.py` then derives `discussion.json` from Steps 6 and 6b's six artifacts — never authored by the main thread.
- **Context B — Post-compose coaching dispatch:** The final step dispatches the sub-agent after `compose_report.py` writes `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.

**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 combination: pitch deck, financial model, data room contents, text descriptions, prior market-sizing or deck-review artifacts, or just a verbal description of the business.

## Available Scripts

All scripts are at `${CLAUDE_PLUGIN_ROOT}/skills/ic-sim/scripts/`:

- **`fund_profile.py`** — Validates fund profile structure (archetypes, check size, thesis, portfolio)
- **`detect_conflicts.py`** — Validates conflict assessments and computes summary stats
- **`compose_discussion.py`** — Derives `discussion.json` from the 3 round-1 assessments + 3 round-2 rebuttals (majority-vote consensus, debate sections from partners' own responses); rejects (exit 1, no file written) on a structurally invalid rebuttal round
- **`score_dimensions.py`** — Scores 28 dimensions across 7 categories with conviction-based scoring
- **`compose_report.py`** — Assembles report with cross-artifact validation; `--strict` exits 1 on high/medium warnings
- **`visualize.py`** — Generates self-contained HTML with SVG charts (not JSON)

Also available from `${CLAUDE_PLUGIN_ROOT}/scripts/` (shared):

- **`founder_context.py`** — Per-company context management (init/read/merge/validate)

Run with: `python3 ${CLAUDE_PLUGIN_ROOT}/skills/ic-sim/scripts/<script>.py --pretty [args]`

## Available References

Read each when first needed — do NOT load all upfront. At `${CLAUDE_PLUGIN_ROOT}/skills/ic-sim/references/`:

- **`partner-archetypes.md`** — Read before Step 4 (main-thread use ONLY: mapping real partners to archetypes in fund-specific mode). The operative archetype rubric the PARTNER_ANALYSIS sub-agent needs is duplicated in `agents/ic-sim.md` — the sub-agent never reads this file (see "Context A hand-off protocol" below); this is a documented split, not an oversight.
- **`evaluation-criteria.md`** — No longer read by any workflow step. The operative 28-dimension rubric (status values, categories, stage calibration, dealbreaker thresholds, SaaS metrics) now lives in `agents/ic-sim.md`, inlined into the SCORE_DIMENSIONS sub-agent's system prompt. This file is kept as human-readable documentation only; edits to it do NOT propagate to sub-agent behavior — edit `agents/ic-sim.md` directly.
- **`ic-dynamics.md`** — Background on how real VC ICs work: formats, decisions, what kills deals. Not read on a normal run — `discussion.json` is derived by `compose_discussion.py` from the partners' own assessments and rebuttals, with nothing authored by the main thread.
- **`artifact-schemas.md`** — Consult as needed when depositing agent-written artifacts

## Artifact Pipeline

Every simulation deposits structured JSON artifacts into a working directory. The final step assembles all artifacts into a report and validates consistency. This is not optional.

| Step | Artifact | Producer |
|------|----------|----------|
| 1 | founder context | `founder_context.py` read/init |
| 2 | `startup_profile.json` | Agent (heredoc) |
| 3 | `prior_artifacts.json` | Agent (heredoc) |
| 4 | `fund_profile.json` | Agent (heredoc) then `fund_profile.py` validates |
| 5 | `conflict_check.json` | Context A dispatch: DETECT_CONFLICTS → `detect_conflicts.py` |
| 6 | `partner_assessment_{visionary,operator,analyst}.json` | Context A dispatch: PARTNER_ANALYSIS × 3 **in parallel** |
| 6b | `partner_rebuttal_{visionary,operator,analyst}.json` | Context A dispatch: PARTNER_REBUTTAL × 3 **in parallel** |
| 7 | `discussion.json` | `compose_discussion.py` (derives from the 3 assessments + 3 rebuttals — nothing authored) |
| 8 | `score_dimensions.json` | Context A dispatch: SCORE_DIMENSIONS → `score_dimensions.py` |
| 9 | Report | `compose_report.py` (writes both `report.json` and `report.md`) |
| 10 | Coaching | Context B dispatch: POST_COMPOSE_COACHING |

**Rules:**
- Deposit each artifact before proceeding to the next step
- For agent-written artifacts, consult `references/artifact-schemas.md` for the JSON schema
- If a step is not applicable, deposit a stub: `{"skipped": true, "reason": "..."}`
- **Do NOT use `isolation: "worktree"`** for sub-agents — files written in a worktree won't appear in the main `$SIM_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`). **Never surface the bare `pass`/`hard_pass`/`invest`/`more_diligence` verdict enum in a progress update either — `pass`/`hard_pass` mean the IC would DECLINE, and a founder reads a bare "pass" as approval; render the verdict in words (Decline / Invest / More Diligence) per Main-Thread Return, in every founder-facing line, not just the final headline.** After each analytical step (5, 6, 6b, 7), share a one-sentence finding before moving on. **The task tracker is founder-visible too — the same rule governs its labels.** "Gate the inputs review handoff", "Validate inputs.json", "resolve agent namespace paths", "Initialize founder context" are leaks even though each names a real step, and even when the prose around them is clean. Label each task by the founder-visible outcome — "Check your inputs", "Score against the review", "Write up what I found" — never by a file, directory, script, or pipeline stage.

## Workflow

### Step 0: Path Setup

**Every Bash tool call runs in a fresh shell — variables do not persist.** Run the block below exactly **once**: it resolves `$PLUGIN_ROOT` deterministically, and every later block must substitute the printed value as a literal rather than re-running the resolution — repeating the self-heal search can land on a different mount than Step 0 picked when more than one is present (see why in the block's comments).

Optional, best-effort, and via the **Read tool** (not a shell command): before the block below, Read `${CLAUDE_PLUGIN_ROOT}/.claude-plugin/plugin.json` and note its `version` field as `EXPECT_VERSION`. Passing it to `select_plugin_root.py` below lets an exact version match win over an arbitrary first hit. If the Read fails, skip it and omit `--expect-version` — selection is still deterministic without it.

```bash
SCRIPTS="${CLAUDE_PLUGIN_ROOT}/skills/ic-sim/scripts"
if [ ! -d "$SCRIPTS" ]; then
  # In Cowork, CLAUDE_PLUGIN_ROOT substitutes to a host-side path absent inside
  # the session VM — self-heal by collecting EVERY candidate mount (a session can
  # have more than one at once: a stale host-side cache, a test marketplace, even
  # a symlink into a different session's tree) and handing them to
  # select_plugin_root.py, which picks ONE deterministically and names the
  # rejects — never trust `find`'s arbitrary first hit, which can silently mix
  # scripts across plugin versions mid-pipeline.
  CANDIDATES="$(find /sessions -type d -path '*/skills/ic-sim/scripts' 2>/dev/null)"
  [ -n "$CANDIDATES" ] || CANDIDATES="$(find / -type d -path '*/skills/ic-sim/scripts' 2>/dev/null)"
  PROVISIONAL_ROOT="$(printf '%s\n' "$CANDIDATES" | head -1)"
  PROVISIONAL_ROOT="${PROVISIONAL_ROOT%/skills/*}"
  # Bootstrap order: $SHARED_SCRIPTS isn't known until a root is chosen, so use the
  # provisional root's OWN copy of the selector; an older plugin copy without one
  # falls back to the provisional root unchanged.
  SELECTOR="$PROVISIONAL_ROOT/scripts/select_plugin_root.py"
  if [ -f "$SELECTOR" ]; then
    if [ -n "$EXPECT_VERSION" ]; then
      PLUGIN_ROOT="$(printf '%s\n' "$CANDIDATES" | python3 "$SELECTOR" --expect-version "$EXPECT_VERSION")"
    else
      PLUGIN_ROOT="$(printf '%s\n' "$CANDIDATES" | python3 "$SELECTOR")"
    fi
  else
    PLUGIN_ROOT="$PROVISIONAL_ROOT"
  fi
  SCRIPTS="$PLUGIN_ROOT/skills/ic-sim/scripts"
fi
PLUGIN_ROOT="${SCRIPTS%/skills/*}"
echo "PLUGIN_ROOT=$PLUGIN_ROOT"   # resolved ONCE, here — paste this literal into every later block; never re-run this resolution
REFS="$PLUGIN_ROOT/skills/ic-sim/references"
SHARED_SCRIPTS="$PLUGIN_ROOT/scripts"
# Resolve the canonical artifacts root via a SCRIPT, not inline bash (the agent paraphrases inline
# path computations → outputs/ vs outputs/artifacts/ drift across runs). Deterministic + creates it.
python3 "$SHARED_SCRIPTS/resolve_artifacts_root.py"   # prints ARTIFACTS_ROOT — use the printed path verbatim as ARTIFACTS_ROOT in every later block (a captured var dies in the next fresh shell)
```

Reaching the self-heal branch is normal in Cowork — `${CLAUDE_PLUGIN_ROOT}` resolves to a HOST path that does not exist inside the VM, so the `[ ! -d "$SCRIPTS" ]` test fails by design rather than by misconfiguration. It is not a sign anything is wrong, and it is not worth narrating to the founder.

**Outputs mount is append-only.** Everything under the promoted outputs mount (`.../mnt/outputs/`, not just `$SIM_DIR`) is write-allowed and delete-denied by the platform: never `rm`, move away, or empty anything under it — **including files you created yourself**. Never create ad-hoc scratch anywhere under the outputs mount (no `_src/` copies, no run-state note files); scratch belongs in `$STAGING_DIR` (a `/tmp` dir, defined below). Do not "clean up" the outputs folder before delivering — extra working files there are expected and harmless.

**If `ARTIFACTS_ROOT` resolves to `$(pwd)/artifacts` but no `artifacts/` directory exists at `$(pwd)`:** Use `Glob` with pattern `**/artifacts/founder_context.json` to locate existing artifacts, and derive `ARTIFACTS_ROOT` from the result. If nothing is found, `mkdir -p "$ARTIFACTS_ROOT"` and proceed.

**There is no quick-check lane here, and that is deliberate.** The verdict is the product of three partner analyses plus 28 scored dimensions; any subset fast enough to be a "quick check" would produce a verdict from a fraction of the evidence, and there is no honest way to label that. So when the founder asks a small
conversational question, do not improvise an answer from your own reasoning under this skill's name —
an unproduced verdict is exactly the output a founder over-trusts. Instead, say up front what the
full run costs and let them choose: "Answering that properly means running the full IC simulation — it takes
several minutes and produces a scored report with the partner debate and the conflict check. I can run it now, or if you just want my read without the
scoring, say so and I'll answer outside the IC simulation." Naming the trade-off is honest; quietly
substituting the cheap version is not.

After Step 1 (when the slug is known):

```bash
SIM_DIR="$ARTIFACTS_ROOT/ic-sim-${SLUG}"
mkdir -p "$SIM_DIR"
RUN_ID="$(date -u +%Y%m%dT%H%M%SZ)"
# Context A hand-off dir — PER RUN: sub-agents WRITE their raw output JSON here (the audit trail —
# raw sub-agent output as returned, before producer validation). Permanent by platform design
# (outputs/ mounts are write-allowed / delete-denied); nothing in it is ever a canonical artifact.
# The $RUN_ID segment is load-bearing: it prevents a stale prior-run file from silently passing
# the hand-off gate when a dispatch fails to write.
HANDOFF_DIR="$SIM_DIR/handoff/$RUN_ID"
mkdir -p "$HANDOFF_DIR"
# Sub-agents address the SAME dir by a different path (their file tools are rooted at the outputs
# mount in Cowork). Resolve the FULL agent-namespace path 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 "ic-sim-${SLUG}" --run-id "$RUN_ID"   # prints HANDOFF_AGENT verbatim
HANDOFF_AGENT="<printed value>"   # use verbatim in OUTPUT_PATH lines
# Ad-hoc scratch (NOT sub-agent hand-off) lives OUTSIDE the promoted outputs/ tree, in a temp dir
# that is safe to both create and reclaim. Use the printed path verbatim in later steps.
STAGING_DIR="$(mktemp -d "${TMPDIR:-/tmp}/ic-sim-${SLUG:-co}.staging.XXXXXX")"
```

Pass `RUN_ID` to all sub-agents. Every artifact written to `$SIM_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 `$SIM_DIR`.** It is the promoted `outputs/`
tree in Cowork, where deleting a user-visible path is unsafe (Cowork can deny it; the parity gate flags
it). Each producer writes its artifact fresh via `-o` every run, and `RUN_ID` is minted fresh per run —
so if a prior run left an artifact a later step doesn't regenerate, `compose_report.py`'s `STALE_ARTIFACT`
check (run_ids must match) catches the mismatch. No bulk `rm` is needed or wanted.

### Step 1: Read or Create Founder Context

```bash
python3 "$SHARED_SCRIPTS/founder_context.py" read --artifacts-root "$ARTIFACTS_ROOT" --pretty
```

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

**Exit 1 (not found):** Expected on a first run — do NOT mention this check or its exit status to the founder; if you narrate anything first, say only "Let me grab a few basics about the company." Use `AskUserQuestion` (NOT plain chat) to ask for company name, stage, sector, and geography. **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.

**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 any derived value plus a stated-value fallback. Provide at least 2 options. Then create:

**Auto-pilot cross-reference — derive field-by-field, never all-or-nothing (do not stall an unattended run on a question the materials already answer):** if the founder has selected Auto-pilot (see Mode Selection below) and provided materials (a deck, financial model, data room, or a sufficiently detailed description), derive each of the four basics — company name, stage, sector, geography — that the materials state, instead of gating on `AskUserQuestion`; a true unattended run should not stop and wait on a prompt whose answer is already in hand. Treat the four **independently**: deriving three and missing one does NOT re-gate all four. Before treating a field as missing, try to **infer** it from a clear signal in the materials (noting it as inferred, not founder-stated): geography from a phone country code, office address, or currency (e.g. a `+972` number → Israel); 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. When running interactively, fall back to `AskUserQuestion` for **only** the specific field(s) with no derivable or inferable signal (stating what you already derived). Under Auto-pilot — where you cannot ask — mark any field that still has no signal as `to_confirm` and proceed rather than stalling.

`--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 (or the
deck-derived stage) 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; 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.

**When no enum value fits (e.g. logistics, physical goods, industrials).** Do not silently pick
`ai-native` because the company mentions AI — that selects AI-native benchmarks for a business whose
economics are not AI-native, and nothing downstream flags it. Pick the value matching the **revenue
mechanics** (a logistics marketplace ⇒ `marketplace`; a freight SaaS ⇒ `saas`), state in the run that the
sector has no exact enum value and which one you substituted, and treat the resulting benchmark comparisons
as directional. If nothing matches on mechanics either, say so rather than choosing the least-wrong label
silently.

```bash
python3 "$SHARED_SCRIPTS/founder_context.py" init \
  --company-name "Acme Corp" --stage seed --sector "B2B SaaS" \
  --geography "US" --artifacts-root "$ARTIFACTS_ROOT"
  # Add --sector-type <value> if the auto-derivation warning fires or the sector
  # doesn't map cleanly to one of the 9 canonical sector-type values above.
```

**Exit 2 (multiple):** Present the list, ask which company, re-read with `--slug`.

#### Execution checkpoint — END OF STEP 1, READ BEFORE CONTINUING

You now have enough to run. **Invoking this skill is not the same as running it.** From here, every
number that reaches the founder must come out of a producer script. Concretely:

- **Never compute a figure in chat.** Not TAM, not runway, not a ratio, not a benchmark comparison —
  not even one you are confident about. An in-chat number has no provenance, no range, no artifact, and
  nothing downstream can contradict it. That is worse than a slow answer and worse than no answer.
- **Never benchmark against a figure you recalled.** Benchmarks live in the reference files and the
  producers read them. If you find yourself writing "typically around X for this stage", stop: either a
  producer sourced it or it does not go in front of the founder.
- **A what-if, a sensitivity illustration, or "roughly what would X give" is NOT an exemption.** This is
  the exemption a live run invented: having correctly produced the real figure, it then wrote *"using the
  current count would shave TAM to roughly €249M rather than €270M"* — a second number, computed in chat,
  from an input the founder never gave. An illustrative figure is read exactly as confidently as a
  computed one, and the founder cannot tell which came from the pipeline. Two ways to answer a what-if:
  **re-run the producer with the alternate input** and quote its output, or **give no number** and say
  which direction it moves. Never arithmetic in prose.
- **Never offer the real run as an opt-in after answering.** "Here's a rough estimate — I can run the
  full analysis if you want" *is* the failure. The founder cannot tell that what they just read was not
  the analysis, so they will not ask for it.
- **Two ways to finish, and only two:** run the full pipeline to completion, or run the full pipeline after stating its cost up front (there is no quick lane here). 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.

### Mode Selection

Ask the user (or infer from context):

1. **Interactive** — Pause between partner positions for founder input
2. **Auto-pilot** — Run all sections without pausing
3. **Fund-specific** — Research a real fund first. Combines with either mode.

### Steps 2-3: Extract Startup Profile and Import Prior Artifacts

Read the provided materials and extract the startup profile directly. Import any prior market-sizing or deck-review artifacts from `$ARTIFACTS_ROOT`. Deposit both artifacts to `$SIM_DIR`.

**Read `${CLAUDE_PLUGIN_ROOT}/skills/ic-sim/references/artifact-schemas.md` before writing artifacts** to ensure JSON schema compliance. (Use the literal `${CLAUDE_PLUGIN_ROOT}` token for a file-tool Read — it is pre-resolved to a host-readable path; do NOT read the `find /sessions`-derived `$REFS` value, which a host-native file tool cannot reach.)

**Stage-token reconciliation (do not copy Step 1's stage token verbatim):** `founder_context.py`'s
`--stage` enum is hyphenated (`pre-seed`, `seed`, `series-a`, `series-b`, `series-c`, `series-d`,
`later`), but `startup_profile.json`'s `stage` field — and the `KNOWN_STAGES` set
`compose_report.py` actually checks against — uses UNDERSCORED tokens (`pre_seed`, `seed`,
`series_a` are in calibrated scope; anything else, including `series_b`, is flagged
`STAGE_OUT_OF_SCOPE`). These are two different enum namespaces for the same concept. Convert
Step 1's hyphenated stage to the underscored form when writing `startup_profile.json`
(`pre-seed` -> `pre_seed`, `series-a` -> `series_a`, `seed` -> `seed` unchanged) — do not paste the
hyphenated value straight through, and do not add commentary/caveats inside the `stage` field
itself (an inline caveat there also trips `STAGE_OUT_OF_SCOPE` even when the underlying stage is
in scope).

Write `startup_profile.json`:
```bash
cat <<'PROFILE_EOF' > "$SIM_DIR/startup_profile.json"
{
  "company_name": "...",
  "simulation_date": "YYYY-MM-DD",
  "stage": "seed",
  "one_liner": "...",
  "sector": "...",
  "geography": "...",
  "business_model": "...",
  "funding_history": "...",
  "current_raise": "...",
  "key_metrics": "...",
  "materials_provided": ["..."],
  "metadata": {"run_id": "<RUN_ID>"}
}
PROFILE_EOF
```

Write `prior_artifacts.json` (stub if no prior artifacts):
```bash
cat <<'PRIOR_EOF' > "$SIM_DIR/prior_artifacts.json"
{"imported": [], "skipped": true, "reason": "No prior artifacts available", "metadata": {"run_id": "<RUN_ID>"}}
PRIOR_EOF
```

### Step 4: Build Fund Profile -> `fund_profile.json`

**Fund-specific mode only — read `${CLAUDE_PLUGIN_ROOT}/skills/ic-sim/references/partner-archetypes.md` now.** You need it to map a real fund's partners to the three archetype roles. **In generic mode, skip this read** — generic mode uses the three canonical archetypes (visionary, operator, analyst) verbatim and does no real-partner mapping, so the file adds nothing. (Literal token, not `$REFS` — a file-tool Read of the `find /sessions` path is denied on host-loop.)

**Generic mode:** Build a standard early-stage fund profile with the three canonical archetypes (visionary, operator, analyst). **OMIT the `portfolio` field entirely — do not fabricate holdings.** A generic fund is a synthesized/illustrative persona with no real portfolio; inventing companies here manufactures fictional conflicts against them downstream (Step 5), and those fabricated conflicts can distort the verdict. `portfolio` is optional in generic mode precisely so it can be left out. Use the example below verbatim as the shape, adapting only thesis/stage/check-size to the startup's sector — but keep `portfolio` absent.

**Fund-specific mode:** Use WebSearch to research fund thesis, portfolio, partner backgrounds, check size range, and stage preference. Map real partners to archetype roles. Include the researched `portfolio` array and a `sources` array (each source needs `url` or `title`).

**Validation constraints:** `check_size_range` must be a dict (not a string), `stage_focus` must be a non-empty array, each source must have `url` or `title`.

Generic-mode example (note: no `portfolio` key):

```bash
cat <<'FUND_EOF' | python3 "$SCRIPTS/fund_profile.py" --pretty --run-id "$RUN_ID" -o "$SIM_DIR/fund_profile.json"
{
  "fund_name": "Generic Early-Stage Fund",
  "mode": "generic",
  "thesis_areas": ["B2B SaaS", "AI-native tooling"],
  "check_size_range": {"min": 500000, "max": 3000000, "currency": "USD"},
  "stage_focus": ["pre-seed", "seed"],
  "archetypes": [
    {"role": "visionary", "name": "The Visionary", "background": "Repeat founder; pattern-matches on market timing and 10x outcomes", "focus_areas": ["market size", "timing", "founder ambition"]},
    {"role": "operator", "name": "The Operator", "background": "Former VP of Sales; scaled GTM at two startups", "focus_areas": ["go-to-market", "unit economics", "execution risk"]},
    {"role": "analyst", "name": "The Analyst", "background": "Ex-growth-equity; underwrites metrics and defensibility", "focus_areas": ["retention", "margins", "competitive moat"]}
  ]
}
FUND_EOF
```

**Accepted warnings:** Add `accepted_warnings` array with `code`, `match` (case-insensitive), and `reason`. Compose downgrades matching warnings to `"acknowledged"`.

**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` naming an internal FILE is
the one to watch — that text is still in the report and must be removed before you hand
anything over.


### 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 relative path a sub-agent's file tools resolve against the
outputs mount, 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` is **relative to the sub-agent's
file-tool cwd** (the outputs mount) — built from the `resolve_artifacts_root.py --agent` namespace
(`$HANDOFF_AGENT`). Never hand a sub-agent an absolute `/sessions/...` path for a file-tool Read/Write —
the host-loop path gate denies it. ic-sim sub-agents perform **zero file reads** (all inputs are inlined
into the prompt; the archetype/28-dimension rubric lives in `agents/ic-sim.md`), so only `OUTPUT_PATH`
(a write) needs the agent namespace. A bundled `references/*.md` a MAIN-THREAD step reads is passed as the
literal `${CLAUDE_PLUGIN_ROOT}/skills/ic-sim/references/...` token (pre-resolved to a host-readable path);
never a `find /sessions`-discovered `$REFS` (a shell path a file tool can't read).

**After EVERY Context A dispatch, gate before piping** (`<step>` = the dispatch's file stem):

```bash
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."
- **Exit 4** (file exists, invalid JSON) → **repair-dispatch**: fresh Task: "Read `<OUTPUT_PATH>`; it fails JSON parsing with `<verbatim detail from the diagnostic>`; fix and rewrite it; return the receipt."
- **Exit 5** (receipt echoes a different path) → **repair-dispatch** telling the agent the exact expected OUTPUT_PATH (it wrote somewhere else).
- **Exit 6** (receipt unparseable / no `output_path` key) → **redo-dispatch** with "return ONLY the receipt JSON — no fences, no prose."
- **Producer schema rejection** (the pipe fails next) → **repair-dispatch** with the producer's stderr verbatim.
- **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`. 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.
- **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; apply the tolerant JSON extraction protocol;
stage to `$STAGING_DIR/<step>_input.json`; same producer pipe), and note the fallback in your
final summary.

Retries overwrite the same OUTPUT_PATH (the mount is write-allowed / delete-denied — never `rm`
under `$SIM_DIR`). Hand-off files are not canonical artifacts: producers consume them only 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 `$SIM_DIR`), and never delete anything under it — see the
append-only rule in Step 0.

**General heredoc guardrail:** every templated heredoc in this file already uses a single-quoted
delimiter (`<<'PROFILE_EOF'`, `<<'FUND_EOF'`, etc.) — this is deliberate, not incidental. An
UNQUOTED heredoc delimiter (`<<EOF` without quotes) lets the shell perform variable/parameter
expansion inside the body, so a literal dollar amount like `$8M` silently shell-expands away (`$8`
is read as a variable reference, `M` is left dangling) before it ever reaches the file. This applies
to ad-hoc/improvised writes too, not just the provided templates: if you ever compose a heredoc that
isn't one of the templates above, always single-quote its delimiter when the body may contain a `$`.

### Step 5: Check Portfolio Conflicts -> `conflict_check.json` (Context A dispatch)

**This step branches on the fund `mode`** (from `fund_profile.json`): a **generic** fund skips the sub-agent; a **fund-specific** fund dispatches it.

#### Generic mode

A generic fund is a synthesized/illustrative persona with **no real portfolio** (Step 4 omits the `portfolio` field). There are no holdings to check, and assessing conflicts against invented companies would be circular — so **do NOT run a sub-agent**. Read the mode inline and, if generic, produce the deterministic empty ("clear") conflict check directly — all in ONE Bash call (each Bash call is a fresh shell, so never capture the mode in one call and test it in another):

```bash
if [ "$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1])).get("mode","fund_specific"))' "$SIM_DIR/fund_profile.json")" = "generic" ]; then
  python3 "$SCRIPTS/detect_conflicts.py" --generic-stub --run-id "$RUN_ID" -o "$SIM_DIR/conflict_check.json"
  echo "generic mode: wrote clear-stub conflict_check.json — skip to Step 6"
fi
```

If that printed the "skip to Step 6" line (generic mode), **skip the rest of Step 5** and go to Step 6. Otherwise the fund is fund-specific — continue below.

#### Fund-specific mode

**The sub-agent performs ZERO file reads.** Read the two inputs it needs in the main thread and paste their content into the dispatch prompt below — do not send it a path to Read.

```bash
cat "$SIM_DIR/fund_profile.json"
cat "$SIM_DIR/startup_profile.json"
```

The two JSON files print to stdout — copy each verbatim into the matching `FUND_PROFILE:` / `STARTUP_PROFILE:` block below. (Never capture into a shell variable: each Bash call runs in a fresh shell.)

**Dispatch the ic-sim sub-agent in Context A (DETECT_CONFLICTS).** Call the `Task` tool with `subagent_type: "founder-skills:ic-sim"` (a type-less dispatch falls back to the wildcard `general-purpose` agent).

**Dispatch prompt template:**

```
CONTEXT: DETECT_CONFLICTS
SIM_DIR: <absolute path to SIM_DIR>
OUTPUT_PATH: <HANDOFF_AGENT>/detect_conflicts

…(truncated)
