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
uvfor script execution. - Imports (recommended):
market-sizing:sizing.json— fund alignment and market validationdeck-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.mdfor the full inline-skill execution model (3 dispatch contexts, Mitigation 1+2, producer contract, Cowork quirks, per-symptom triage).
This skill runs inline in the main thread, not as a sub-agent — see the reference above ("Why Inline (Not Forked Sub-Agent)") for the rationale. Sub-agents are deliberately shell-free, so orchestration (producer scripts, artifact persistence) 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
Tasktool. 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 theOUTPUT_PATHgiven in its prompt (thehandoff/dir), and returns a small receipt. The main thread gates the file withcheck_handoff.py, then pipes it through the producer script. 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'scompose_discussion.pythen derivesdiscussion.jsonfrom 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.pywritesreport.md. The sub-agent Reads the stagedcoaching_payload.jsonfrom the hand-off dir (Mitigation 2) — it does NOT read the fullreport.md— composes the coaching commentary, WRITES it to theOUTPUT_PATHhand-off file, and returns a small receipt. The main thread gates the file (check_handoff.py) and inserts it via the sharedinsert_coaching.pyscript (idempotency matrix, uuid-marker replacement, run_id-parity verification — all deterministic). See the reference above for the full Context B contract.
Tolerant JSON extraction protocol (Context B returns; also the Context A message-channel fallback): capture the sub-agent's final assistant message. It should be raw JSON, but may be wrapped in ```json ... ``` fences or carry a prose preamble. Extract tolerantly:
- If the message is wrapped in a
```json ... ```(or plain``` ... ```) fence, strip the fence first. - Try to parse the stripped text directly as JSON.
- If that fails, walk through the text looking for the first
{character and tryjson.JSONDecoder().raw_decode(text[i:])— this is brace-aware and handles nested objects correctly (unlike regex, which truncates on the first}). - If extraction fails entirely, re-prompt the sub-agent with: "Your previous reply could not be parsed as JSON. Return ONLY the JSON object — no markdown fences, no prose preamble."
Context A receipts don't need this protocol by hand — check_handoff.py --receipt-json - applies the same tolerant extraction internally; pass the final message verbatim.
Input Formats
Accept any 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 statscompose_discussion.py— Derivesdiscussion.jsonfrom 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 roundscore_dimensions.py— Scores 28 dimensions across 7 categories with conviction-based scoringcompose_report.py— Assembles report with cross-artifact validation;--strictexits 1 on high/medium warningsvisualize.py— Generates self-contained HTML with SVG charts (not JSON)
Also available from ${CLAUDE_PLUGIN_ROOT}/scripts/ (shared):
founder_context.py— Per-company context management (init/read/merge/validate)
Run with: python3 ${CLAUDE_PLUGIN_ROOT}/skills/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 inagents/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 inagents/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 — editagents/ic-sim.mddirectly.ic-dynamics.md— Background on how real VC ICs work: formats, decisions, what kills deals. Not read on a normal run —discussion.jsonis derived bycompose_discussion.pyfrom 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.mdfor the JSON schema - If a step is not applicable, deposit a stub:
{"skipped": true, "reason": "..."} - Do NOT use
isolation: "worktree"for sub-agents — files written in a worktree won't appear in the main$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.
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):
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
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.
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):
- Interactive — Pause between partner positions for founder input
- Auto-pilot — Run all sections without pausing
- 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:
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):
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):
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):
printf '%s' '<agent final message verbatim>' | \
python3 "$SHARED_SCRIPTS/check_handoff.py" "$HANDOFF_DIR/<step>_output.json" \
--agent-path "$HANDOFF_AGENT/<step>_output.json" --receipt-json -
Branch on the exit code (complete state machine — do not improvise):
- Exit 0 → pipe the file through the producer:
cat "$HANDOFF_DIR/<step>_output.json" | python3 "$SCRIPTS/<producer>.py" ... - Exit 3 (missing/empty file — receipt may be fabricated) → redo-dispatch: fresh Task, same prompt plus one line: "your receipt claimed a file at
<path>but none exists; use Write to create exactly that path." - Exit 4 (file exists, invalid JSON) → repair-dispatch: fresh Task: "Read
<OUTPUT_PATH>; it fails JSON parsing with<verbatim detail from the diagnostic>; fix and rewrite it; return the receipt." - Exit 5 (receipt echoes a different path) → repair-dispatch telling the agent the exact expected OUTPUT_PATH (it wrote somewhere else).
- Exit 6 (receipt unparseable / no
output_pathkey) → 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 relativeOUTPUT_PATHresolved against the outputs mount instead of the session root, so the file landed at the doubled path reported infound_at. Do NOT treat this as a fabricated receipt (that is exit 3), and do NOT read the hand-off fromfound_at— it is diagnostic only. Re-runresolve_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):
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.
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)