| name | financial-model-review |
| description | Reviews startup financial models for investor readiness — validates unit economics, stress-tests runway scenarios, and benchmarks metrics against stage-appropriate targets. Accepts Excel, CSV, or text. |
| when_to_use | Use ONLY when the user has provided a financial model file (Excel/CSV) or a structured numerical model in pasted form, AND has asked for review, validation, runway analysis, or unit-economics scoring. Do not auto-invoke on general questions about financial models or fundraising metrics.
|
| user-invocable | true |
Financial Model Review Skill
Help startup founders understand how investors will evaluate their financial model — validating structure, unit economics, runway, and metrics against stage-appropriate standards. Produce a thorough review with actionable improvements. The tone is founder-first: a rigorous but supportive coaching session.
Skill Metadata
- Author: lool-ventures
- Version: managed in
founder-skills/.claude-plugin/plugin.json
- Compatibility: Python 3.10+ and
uv for script execution. openpyxl required for Excel parsing.
- Imports (optional):
market-sizing:sizing.json — validate revenue-to-SOM consistency
deck-review:checklist.json — cross-check model-to-deck number alignment
- Exports:
report.json → ic-sim, fundraise-readiness, dd-readiness
unit_economics.json → metrics-benchmarker, ic-sim
runway.json → fundraise-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): The INPUTS_REVIEW and CHECKLIST steps dispatch the financial-model-review agent via the
Task tool. The 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. (Unit economics and runway are NOT dispatched — those producers consume inputs.json verbatim, so the main thread pipes the file directly.)
- 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 coaching_payload inlined in the dispatch prompt (Mitigation 2) — it does NOT read the full report.md — composes the coaching commentary, WRITES it to the OUTPUT_PATH hand-off file, and returns a small receipt. The main thread gates the file (check_handoff.py) and inserts it via the shared insert_coaching.py script (idempotency matrix, uuid-marker replacement, run_id-parity verification — all deterministic). See the reference above for the full Context B contract.
Tolerant JSON extraction protocol (Context B returns; also the Context A message-channel fallback): capture the sub-agent's final assistant message. It should be raw JSON, but may be wrapped in ```json ... ``` fences or carry a prose preamble. Extract tolerantly:
- If the message is wrapped in a
```json ... ``` (or plain ``` ... ```) fence, strip the fence first.
- Try to parse the stripped text directly as JSON.
- If that fails, walk through the text looking for the first
{ character and try json.JSONDecoder().raw_decode(text[i:]) — this is brace-aware and handles nested objects correctly (unlike regex, which truncates on the first }).
- If extraction fails entirely, re-prompt the sub-agent with: "Your previous reply could not be parsed as JSON. Return ONLY the JSON object — no markdown fences, no prose preamble."
Context A receipts don't need this protocol by hand — check_handoff.py --receipt-json - applies the same tolerant extraction internally; pass the final message verbatim.
If a sub-agent wrote CANONICAL artifact files directly anyway (anything outside its handoff/ OUTPUT_PATH): do not trust them — take its gated hand-off file (or extract the JSON from its final message on the fallback path), then re-pipe through the producer script as specified; the producer overwrites the file with the validated, run_id-stamped version. For INPUTS_REVIEW specifically: if inputs.json contains the {"corrected": ..., "corrections": ...} wrapper, the sub-agent wrote its reply to disk — feed that wrapper through apply_corrections.py as usual.
Context-pressure note: This skill has the highest context budget of the 5 skills. The win from Mitigation 1 is excluding sub-agent reasoning and the 40-60 KB raw extract_model.py output — which flows through the INPUTS_REVIEW dispatch: the sub-agent reads it in its own context window, returns only the corrected inputs.json. The artifacts themselves still accumulate in the main thread (~80-130K total), but that is manageable.
Input Formats
Accept any format: Excel (.xlsx), CSV, Google Sheets exports, financial documents, or conversational input. For Excel files, use extract_model.py to parse. For other formats, extract data manually into the inputs.json schema. If multiple copies of the same file exist (e.g., Financials.xlsx and Financials (1).xlsx), use the most recently modified version and note the duplication to the founder. If timestamps are identical, ask the founder which file to use. If the founder cannot be queried, prefer the file without parenthetical suffixes (e.g., (1), (2)) — these typically indicate browser re-download duplicates.
Available Scripts
All scripts are at ${CLAUDE_PLUGIN_ROOT}/skills/financial-model-review/scripts/:
extract_model.py — Extracts structured data from Excel (.xlsx) and CSV files
validate_extraction.py — Anti-hallucination gate: cross-references model_data.json against inputs.json to catch mismatches (company name, salary, revenue, cash traceability); run after extraction, before review
validate_inputs.py — Four-layer validation of inputs.json (structural, consistency, sanity, completeness); supports --fix to auto-correct sign errors
checklist.py — Scores 46 criteria across 7 categories with profile-based auto-gating
unit_economics.py — Computes and benchmarks 11 unit economics metrics
runway.py — Multi-scenario runway stress-test with decision points
compose_report.py — Assembles report with cross-artifact validation; --strict exits 1 on high-severity warnings (corrupt/missing artifacts)
apply_corrections.py — Processes founder's downloaded corrections file: coerces types, normalizes ILS→USD, merges overrides, writes corrected_inputs.json and extraction_corrections.json
verify_review.py — Review completeness gate: checks artifact existence, content quality, and cross-artifact consistency; --gate 1 for after-compose, --gate 2 (default) for final; exit 0 = publishable, exit 1 = gaps remain
visualize.py — Generates self-contained HTML with SVG charts (not JSON)
explore.py — Generates self-contained interactive HTML explorer from review artifacts; outputs HTML (not JSON)
review_inputs.py — Dual-mode review viewer: HTTP server with live validation (Claude Code) or self-contained static HTML with JS sanity metrics (Cowork); outputs HTML
Also available from ${CLAUDE_PLUGIN_ROOT}/scripts/ (shared):
find_artifact.py — Resolves artifact paths by skill name and filename (used for cross-skill lookups)
Run with: python3 ${CLAUDE_PLUGIN_ROOT}/skills/financial-model-review/scripts/<script>.py --pretty [args]
Available References
Read as needed from ${CLAUDE_PLUGIN_ROOT}/skills/financial-model-review/references/:
checklist-criteria.md — All 46 checklist criteria with gate definitions
schema-inputs.md — JSON schema for inputs.json (the artifact the agent writes)
artifact-schemas.md — JSON schemas for script-produced output artifacts
data-sufficiency.md — Data sufficiency gate and qualitative path
extraction-pitfalls.md — 8 common extraction errors (scale denomination, payroll aggregation, collections vs revenue, etc.)
From ${CLAUDE_PLUGIN_ROOT}/references/ (shared): stage-expectations.md, benchmarks.md, israel-guidance.md, revenue-model-types.md, common-mistakes.md
Artifact Pipeline
Every review 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 | model_data.json | extract_model.py (Excel/CSV in main thread) |
| 3 | inputs.json | Context A dispatch: INPUTS_REVIEW → apply_corrections.py |
| 3.5 | corrected_inputs.json | apply_corrections.py (from INPUTS_REVIEW dispatch) |
| 3.6 | extraction_validation.json | validate_extraction.py (when model_data.json exists) |
| 4 | checklist.json | Context A dispatch: CHECKLIST → checklist.py |
| 5 | unit_economics.json | direct pipe: inputs.json → unit_economics.py |
| 6 | runway.json | direct pipe: inputs.json → runway.py |
| 7 | Report | compose_report.py (writes both report.json and report.md) |
| 7.5 | commentary.json | agent-authored (main thread heredoc) — required by Gate 2 for quantitative reviews |
| 8a | HTML report | visualize.py |
| 8b | Explorer | explore.py |
| 8c | Coaching | Context B dispatch: POST_COMPOSE_COACHING |
Rules:
- Deposit each artifact before proceeding to the next step
- For agent-written artifacts (inputs.json), consult
references/schema-inputs.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 $REVIEW_DIR
Keep the founder informed with brief, plain-language updates at each step. Never mention file names, scripts, or JSON. After each analytical step (3–6), share a one-sentence finding before moving on.
Workflow
Step 0: Path Setup
Every Bash tool call runs in a fresh shell — variables do not persist. Prefix every Bash call that uses these paths with the variable block below, or substitute absolute paths directly:
SCRIPTS="${CLAUDE_PLUGIN_ROOT}/skills/financial-model-review/scripts"
if [ ! -d "$SCRIPTS" ]; then
SCRIPTS="$(find /sessions -type d -path '*/skills/financial-model-review/scripts' 2>/dev/null | head -1)"
fi
if [ -z "$SCRIPTS" ] || [ ! -d "$SCRIPTS" ]; then
SCRIPTS="$(find / -type d -path '*/skills/financial-model-review/scripts' 2>/dev/null | head -1)"
fi
PLUGIN_ROOT="${SCRIPTS%/skills/*}"
REFS="$PLUGIN_ROOT/skills/financial-model-review/references"
SHARED_SCRIPTS="$PLUGIN_ROOT/scripts"
SHARED_REFS="$PLUGIN_ROOT/references"
python3 "$SHARED_SCRIPTS/resolve_artifacts_root.py"
The Step 0 block self-heals when ${CLAUDE_PLUGIN_ROOT} doesn't resolve (Cowork). If it still comes up empty, locate the anchor manually: find / -path '*/skills/financial-model-review/scripts/checklist.py' 2>/dev/null | head -5 and derive the variables from it.
If ARTIFACTS_ROOT resolves to ./artifacts but no artifacts/ directory exists at $(pwd): The workspace may not be mounted yet. 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 and proceed.
After Step 1 (when the slug is known):
REVIEW_DIR="$ARTIFACTS_ROOT/financial-model-review-${SLUG}"
mkdir -p "$REVIEW_DIR"
RUN_ID="$(date -u +%Y%m%dT%H%M%SZ)"
HANDOFF_DIR="$REVIEW_DIR/handoff/$RUN_ID"
mkdir -p "$HANDOFF_DIR"
python3 "$SHARED_SCRIPTS/resolve_artifacts_root.py" --agent
HANDOFF_AGENT="<printed AGENT_ARTIFACTS_ROOT>/financial-model-review-${SLUG}/handoff/$RUN_ID"
REVIEW_DIR_AGENT="<printed AGENT_ARTIFACTS_ROOT>/financial-model-review-${SLUG}"
STAGING_DIR="$(mktemp -d "${TMPDIR:-/tmp}/financial-model-review-${SLUG:-fmr}.staging.XXXXXX")"
Pass RUN_ID to all sub-agents. The four producer artifacts (inputs.json, checklist.json, unit_economics.json, runway.json) must carry "metadata": {"run_id": "$RUN_ID"} at the top level — including skipped stubs, whose stub heredoc carries the same "metadata": {"run_id": "$RUN_ID"} block. The producers propagate it from their stdin payloads; never hand-edit script outputs to add it. (model_data.json and extraction_validation.json have no run_id by design.) compose_report.py checks that all present run IDs match — a mismatch triggers a STALE_ARTIFACT high-severity warning, blocking under --strict. Stub artifacts are exempt from the value comparison but still carry the run_id key so the Context B parity grep finds it.
Overwrite-in-place — do NOT delete prior artifacts under $REVIEW_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
Three cases based on exit code:
Exit 0 (found, single context): Use the company slug and pre-filled fields. Before proceeding to extraction, use AskUserQuestion to ask the founder for current cash balance and date if not already stated in the conversation — this is the #1 cause of incomplete runway analysis. If files are attached, also ask about monthly burn rate unless the conversation already contains it. Batch all questions into a single AskUserQuestion call.
Exit 1 (not found): Use AskUserQuestion (NOT plain chat) to ask the founder for company details AND key financial context. You MUST use the AskUserQuestion tool — do not just list questions in the chat. Gather everything in a single call (one interaction = one chance for the UI to render correctly):
- Company name, stage, sector, geography (required for context creation)
- Current cash balance and date (critical for runway — the #1 cause of incomplete reports)
- Monthly burn rate if not obvious from the provided files
IMPORTANT: Always use the AskUserQuestion tool for founder questions — never ask as plain chat text. The tool provides a structured UI that renders correctly in Cowork. Always provide at least 2 options (the tool requires a minimum of 2). Valid --stage values: pre-seed, seed, series-a, series-b, later (hyphenated, not underscored).
Why everything upfront: Extraction sub-agents run in parallel and cannot pause to ask questions. Asking early prevents pipeline stalls.
If the founder provides files (Excel/CSV), still ask about cash balance — extraction may miss or misinterpret values, and having the founder's stated number lets the agent cross-check later.
Then create:
python3 "$SHARED_SCRIPTS/founder_context.py" init \
--company-name "Acme Corp" --stage seed --sector "B2B SaaS" \
--geography "US" --artifacts-root "$ARTIFACTS_ROOT"
If the script prints a sector_type warning but exits 0, that's non-fatal — proceed without retrying. However, a null sector_type may suppress sector-specific checklist gating downstream. If you know the correct type, re-run with --sector-type (valid values: saas, ai-native, marketplace, hardware, hardware-subscription, consumer-subscription, usage-based, transactional-fintech).
Exit 2 (multiple context files): Present the list to the founder, ask which company, then re-read with --slug.
Step 2: Extract Model Data
When Excel (.xlsx) or CSV files are provided, run extract_model.py directly in the main thread:
python3 "$SCRIPTS/extract_model.py" --file <path> --pretty -o "$REVIEW_DIR/model_data.json"
Check the periodicity_summary and per-sheet periodicity fields. If periodicity is quarterly or annual, all flow metrics (burn, revenue, expenses — anything measured per period) must be divided by 3 or 12 respectively in the next step. Do NOT convert stock metrics (cash balance, headcount, customer count, ARR — point-in-time snapshots). If periodicity is unknown, flag it.
When documents (PDFs, data room dumps, Google Sheets exports) are provided: Extract what you can directly from the documents, consulting ${CLAUDE_PLUGIN_ROOT}/skills/financial-model-review/references/schema-inputs.md for the schema and ${CLAUDE_PLUGIN_ROOT}/skills/financial-model-review/references/data-sufficiency.md for sufficiency assessment. Write a provisional inputs.json.
When conversational input is provided (no files): Gather all needed fields within Step 1 through normal conversation. Consult references/schema-inputs.md for the full schema.
Context A hand-off protocol (file transport + gate)
Every Context A dispatch prompt carries an OUTPUT_PATH: line built from $HANDOFF_AGENT. The
sub-agent WRITES its output JSON to that path with its Write tool and returns only a small receipt:
{"status": "complete", "output_path": "<echo of OUTPUT_PATH>"}. The payload leaves the model
exactly once (into the Write call) — never re-type sub-agent JSON into a heredoc.
Path idiom for dispatch prompts (host-loop path gate): OUTPUT_PATH and any under-outputs artifact
READ path a sub-agent is given are relative to the sub-agent's file-tool cwd (the outputs mount) —
built from the resolve_artifacts_root.py --agent namespace ($HANDOFF_AGENT / $REVIEW_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/financial-model-review/references/...
token (it is pre-resolved to a host-readable path); do NOT substitute a find /sessions-discovered
$REFS (a shell path a file tool can't read).
After EVERY Context A dispatch, gate before piping (<step> = the dispatch's file stem):
printf '%s' '<agent final message verbatim>' | \
python3 "$SHARED_SCRIPTS/check_handoff.py" "$HANDOFF_DIR/<step>_output.json" \
--agent-path "$HANDOFF_AGENT/<step>_output.json" --receipt-json -
Branch on the exit code (complete state machine — do not improvise):
- Exit 0 → pipe the file through the producer:
cat "$HANDOFF_DIR/<step>_output.json" | python3 "$SCRIPTS/<producer>.py" ...
- Exit 3 (missing/empty file — receipt may be fabricated) → redo-dispatch: fresh Task, same prompt plus one line: "your receipt claimed a file at
<path> but none exists; use Write to create exactly that path."
- Exit 4 (file exists, invalid JSON) → repair-dispatch: fresh Task: "Read
<OUTPUT_PATH>; it fails JSON parsing with <verbatim detail from the diagnostic>; fix and rewrite it; return the receipt."
- Exit 5 (receipt echoes a different path) → repair-dispatch telling the agent the exact expected OUTPUT_PATH (it wrote somewhere else).
- Exit 6 (receipt unparseable / no
output_path key) → redo-dispatch with "return ONLY the receipt JSON — no fences, no prose."
- Producer schema rejection (the pipe fails next) → repair-dispatch with the producer's stderr verbatim.
- Any other exit (script crash etc.) → STOP with the stderr.
- After ANY corrective dispatch, resume from
check_handoff.py — never pipe to the producer unchecked.
Retry budget: max 2 corrective dispatches per step, of any kind, in any combination (max 3
total dispatches). After the second corrective dispatch fails any gate: STOP and report the exact
diagnostic to the founder. The main thread MUST NOT author or patch analytical content itself —
filling in the JSON is the fabrication failure mode this architecture exists to prevent. A
status: "blocked" return is not a gate retry, but it is bounded: at most ONE input-fix
re-dispatch per step; a second blocked return STOPs with both reasons quoted.
Graceful degrade (fleet heterogeneity): if the FIRST corrective dispatch also exits 3 while the
agent's receipt claims complete with the correctly echoed path, treat the host's filesystem
topology as hand-off-incompatible: fall back to message-channel transport for the REST of this run
(sub-agent returns full JSON in its final message; 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 $REVIEW_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 under
$REVIEW_DIR, and never delete anything under it.
Step 3: INPUTS_REVIEW Dispatch (Context A)
Dispatch the financial-model-review sub-agent in Context A (INPUTS_REVIEW). Call the Task tool with subagent_type: "founder-skills:financial-model-review" and the prompt below. This is the highest context-pressure dispatch — the sub-agent reads the full model_data.json (40-60 KB) inside its own context window and returns only the corrected inputs.json. This is the primary Mitigation 1 win: the raw extraction output never accumulates in the main thread context.
Before dispatching, substitute placeholders in the template below: replace <HANDOFF_AGENT> and <REVIEW_DIR_AGENT> with the agent-namespace values (from resolve_artifacts_root.py --agent — relative paths the sub-agent's file tools resolve against the outputs mount; NOT absolute /sessions/... paths, which the host-loop gate denies) and <RUN_ID> with $RUN_ID. Leave the ${CLAUDE_PLUGIN_ROOT}/... reference paths literal — they are pre-resolved to a host-readable path. The sub-agent has no access to your shell variables.
Dispatch prompt template:
CONTEXT: INPUTS_REVIEW
OUTPUT_PATH: <HANDOFF_AGENT>/inputs_review_output.json
RUN_ID: <RUN_ID>
You are the financial-model-review agent dispatched in Context A (INPUTS_REVIEW).
Read model_data.json at <REVIEW_DIR_AGENT>/model_data.json (the full extraction output).
Also read:
- ${CLAUDE_PLUGIN_ROOT}/skills/financial-model-review/references/schema-inputs.md
- ${CLAUDE_PLUGIN_ROOT}/skills/financial-model-review/references/extraction-pitfalls.md
- ${CLAUDE_PLUGIN_ROOT}/skills/financial-model-review/references/data-sufficiency.md
Construct a complete, valid inputs.json from the extracted data. Apply all
extraction pitfall checks (scale denomination, ARPU sanity, periodicity
conversion, company name sourcing, payroll aggregation, collections vs revenue).
ARPU sanity check: if drivers.arpu_monthly or unit_economics.ltv.inputs.arpu_monthly
exceeds total MRR, it is probably aggregate revenue, not per-customer ARPU —
divide by customer count.
Use your Write tool to write to OUTPUT_PATH. Shape (do NOT include a "changes"
or "base_hash" key — those belong to the founder browser round-trip, not this
dispatch):
{
"corrected": {<full validated inputs.json contents per schema-inputs.md,
including "metadata": {"run_id": "<RUN_ID>"}>},
"corrections": [
{"path": "cash.current_balance", "old": null, "new": 1500000,
"reason": "<where the value came from / what was fixed>"}
]
}
The "corrections" array is the audit trail written to extraction_corrections.json.
Then return ONLY the receipt JSON in your final assistant message:
{"status": "complete", "output_path": "<echo of OUTPUT_PATH>"}
Do NOT write any file other than OUTPUT_PATH — canonical artifacts are
producer-script-only; anything else you write bypasses schema validation and
run_id stamping.
After the sub-agent returns: gate the hand-off per the Context A hand-off protocol.
INPUTS_REVIEW special handling — file-args-based script: Unlike other dispatch points, apply_corrections.py takes file arguments, not stdin. The hand-off file IS the file argument — no re-typing needed. The main thread must:
- If
inputs.json does not yet exist, write an empty inputs stub first:
echo '{}' > "$REVIEW_DIR/inputs.json"
- Run
apply_corrections.py with the gated hand-off file as its argument:
python3 "$SCRIPTS/apply_corrections.py" "$HANDOFF_DIR/inputs_review_output.json" \
--original "$REVIEW_DIR/inputs.json" \
--output-dir "$REVIEW_DIR"
apply_corrections.py prints an Info: corrected-object payload (dispatch shape) line to
stderr for corrected-shaped payloads — that is expected, not an error.
Read the stdout JSON:
Step 3.5: Validate inputs.json — STOP GATE
Run the validation script:
cat "$REVIEW_DIR/inputs.json" | python3 "$SCRIPTS/validate_inputs.py" --pretty
If valid == false (errors present), run with --fix to auto-correct fixable issues:
python3 "$SCRIPTS/validate_inputs.py" --fix < "$REVIEW_DIR/inputs.json" -o "$REVIEW_DIR/inputs.json"
Then re-validate. If errors persist after --fix, correct inputs.json manually.
Also run the extraction validation script to cross-reference model_data.json against inputs.json (if model_data.json exists):
python3 "$SCRIPTS/validate_extraction.py" --inputs "$REVIEW_DIR/inputs.json" --model-data "$REVIEW_DIR/model_data.json" --fix --pretty -o "$REVIEW_DIR/extraction_validation.json"
Do NOT proceed to Step 4 until valid == true and has_critical_warnings == false.
Step 3.6: Review Extracted Values
Path A — File extraction (model_format is spreadsheet or partial):
Generate the HTML review page for the founder to inspect extracted values. In Cowork (VM, no display), use static mode:
python3 "$SCRIPTS/review_inputs.py" "$REVIEW_DIR/inputs.json" --static "$REVIEW_DIR/review.html" --extraction-warnings "$REVIEW_DIR/extraction_validation.json"
This is a STOP point — do not proceed to Step 4 until the founder responds. Present the review.html path to the founder, then ask via AskUserQuestion (two options): "I reviewed the page — the values look right, proceed" / "I edited values and will upload the corrections file". Generating the page and silently moving on defeats the human verification gate: the founder is the last check on extracted numbers before math runs on them. When they upload corrections.json:
python3 "$SCRIPTS/apply_corrections.py" <uploaded-file> --original "$REVIEW_DIR/inputs.json" --output-dir "$REVIEW_DIR"
Then promote corrected_inputs.json to inputs.json (same as Step 3) and re-run the Step 3.5 validation before proceeding.
In Claude Code (local terminal), use server mode:
python3 "$SCRIPTS/review_inputs.py" "$REVIEW_DIR/inputs.json" --workspace "$REVIEW_DIR" --extraction-warnings "$REVIEW_DIR/extraction_validation.json" &
Wait for the founder to say done, then kill the server and apply corrections.
Path B — Conversational (model_format is conversational or deck): present a confirmation table (stage, MRR, growth rate, burn, cash, customers, CAC, target raise) and use AskUserQuestion to confirm.
Step 4: CHECKLIST Dispatch (Context A)
Dispatch the financial-model-review sub-agent in Context A (CHECKLIST). Call the Task tool with subagent_type: "founder-skills:financial-model-review" and the prompt below. Substitute <HANDOFF_AGENT> / <REVIEW_DIR_AGENT> with the agent-namespace values and <RUN_ID> with $RUN_ID; leave the ${CLAUDE_PLUGIN_ROOT}/... reference path literal (same idiom as INPUTS_REVIEW).
Dispatch prompt template:
CONTEXT: CHECKLIST
OUTPUT_PATH: <HANDOFF_AGENT>/checklist_output.json
RUN_ID: <RUN_ID>
You are the financial-model-review agent dispatched in Context A (CHECKLIST).
Read inputs.json at <REVIEW_DIR_AGENT>/inputs.json.
Also read ${CLAUDE_PLUGIN_ROOT}/skills/financial-model-review/references/checklist-criteria.md.
Assess all 46 checklist items (STRUCT_01..09, UNIT_10..19, CASH_20..32,
METRIC_33..35, BRIDGE_36..38, SECTOR_39..44, OVERALL_45..46).
Profile-based auto-gating is applied BY THE PRODUCER SCRIPT after you return —
assess EVERY item on its merits and never mark an item not_applicable because
of a stage/geography/sector/model_format gate ("partial" models are evaluated
in full; only the script decides gating).
Evidence is MANDATORY for every item: every fail and warn MUST have a non-empty
evidence string citing specific values from the model. Every pass MUST have
evidence noting what was checked.
Use your Write tool to write to OUTPUT_PATH — company + metadata + items
(producer script computes summary):
{
"company": {<the company object copied verbatim from inputs.json — enables profile auto-gating>},
"metadata": {"run_id": "<RUN_ID>"},
"items": [{"id": "STRUCT_01", "status": "pass", "evidence": "...", "notes": null}, ...all 46 items...]
}
Then return ONLY the receipt JSON in your final assistant message:
{"status": "complete", "output_path": "<echo of OUTPUT_PATH>"}
Do NOT write any file other than OUTPUT_PATH — canonical artifacts are
producer-script-only; anything else you write bypasses schema validation and
run_id stamping.
After the sub-agent returns: gate the hand-off per the Context A hand-off protocol, then pipe:
cat "$HANDOFF_DIR/checklist_output.json" | \
python3 "$SCRIPTS/checklist.py" --pretty --run-id "$RUN_ID" -o "$REVIEW_DIR/checklist.json"
Steps 5-6: Unit Economics and Runway (direct — no dispatch)
These two producers consume inputs.json verbatim. Run them directly from the
on-disk file — do NOT round-trip the JSON through a sub-agent (an LLM re-typing
multi-KB financial JSON risks silently corrupting numbers, and it saves no
context since the JSON would land in the main thread anyway):
SCRIPTS="${CLAUDE_PLUGIN_ROOT}/skills/financial-model-review/scripts"
if [ ! -d "$SCRIPTS" ]; then
SCRIPTS="$(find /sessions -type d -path '*/skills/financial-model-review/scripts' 2>/dev/null | head -1)"
[ -n "$SCRIPTS" ] || SCRIPTS="$(find / -type d -path '*/skills/financial-model-review/scripts' 2>/dev/null | head -1)"
fi
cat "$REVIEW_DIR/inputs.json" | python3 "$SCRIPTS/unit_economics.py" --pretty --run-id "$RUN_ID" -o "$REVIEW_DIR/unit_economics.json"
cat "$REVIEW_DIR/inputs.json" | python3 "$SCRIPTS/runway.py" --pretty --run-id "$RUN_ID" -o "$REVIEW_DIR/runway.json"
Both scripts propagate metadata.run_id from inputs.json into their outputs
(required by the Context B run_id-parity check). All metric fields are optional —
missing data yields not_rated / a partial-analysis stub, never a crash.
Step 7: Compose and Validate Report
python3 "$SCRIPTS/compose_report.py" --dir "$REVIEW_DIR" --pretty \
-o "$REVIEW_DIR/report.json" \
--write-md "$REVIEW_DIR/report.md"
compose_report.py writes both report.json and report.md deterministically. Do NOT read report_markdown out of report.json and re-write it via heredoc.
Check validation.warnings: fix high-severity (corrupt/missing artifacts), present medium-severity (checklist failures, runway inconsistencies, metrics gaps) in the report, note low/info. --strict only blocks on high-severity warnings. Fix high-severity warnings, re-deposit, re-compose.
Post-write verification: compose_report.py exits non-zero (code 2) if the declared output files don't exist or are empty after writing. If compose exits non-zero, stop and report the exact stderr — do not proceed.
Verification Gate 1 (after compose)
python3 "$SCRIPTS/verify_review.py" --dir "$REVIEW_DIR" --gate 1 --pretty
If exit code is non-zero: read summary.errors. Fix the issue by re-running the failing step, then re-run verify_review.py --gate 1. Do not proceed until it exits 0.
Step 7.5: Write Commentary (agent-authored, required for quantitative reviews)
verify_review.py --gate 2 requires commentary.json whenever unit_economics.json
and runway.json are real (non-stub) — and explore.py embeds it into the
interactive explorer. Author it now, in the main thread, from the artifacts you
have already seen (checklist summary, unit-economics ratings, runway scenarios).
Schema: ${CLAUDE_PLUGIN_ROOT}/skills/financial-model-review/references/artifact-schemas.md § commentary.json. headline is required;
include only the lens keys whose artifacts exist (valid lens keys: runway,
unit_economics, stress_test, raise_planner).
cat > "$REVIEW_DIR/commentary.json" <<'COMMENTARY_EOF'
{
"headline": "<one-sentence financial health summary>",
"investor_talking_points": [
"<sentence the founder can say out loud during a fundraise conversation>"
],
"lenses": {
"runway": {"callout": "<key insight>", "highlight": "<secondary observation>", "watch_out": "<risk>"},
"unit_economics": {"callout": "<key insight>", "watch_out": "<risk>"}
}
}
COMMENTARY_EOF
Ground every sentence in artifact values — never invent numbers. If both
unit_economics.json and runway.json are skipped stubs (qualitative path),
skip this step; Gate 2 will not require the file.
Steps 8a-8b: Visualize and Generate Explorer (Optional)
python3 "$SCRIPTS/visualize.py" --dir "$REVIEW_DIR" -o "$REVIEW_DIR/report.html"
python3 "$SCRIPTS/explore.py" --dir "$REVIEW_DIR" -o "$REVIEW_DIR/explore.html"
Generate files silently — present paths after Gate 2 passes.
Step 8c: Post-Compose Coaching Commentary (Context B dispatch — POST_COMPOSE_COACHING)
Dispatch the financial-model-review sub-agent in Context B. Call the Task tool with subagent_type: "founder-skills:financial-model-review" after compose_report.py has successfully written both report.json and report.md.
Mitigation 2 protocol: the main thread reads the structured coaching_payload from report.json and inlines it into the dispatch prompt. The sub-agent does NOT Read full report.md — it consumes coaching_payload directly, composes the coaching commentary, and WRITES it to the OUTPUT_PATH hand-off file with its Write tool, returning only a small receipt (the same file transport as Context A — the commentary leaves the model exactly once, into the Write call; the main thread never re-types it). The main thread gates that file with check_handoff.py, then inserts it via the shared insert_coaching.py script (idempotency matrix, uuid-marker replacement, run_id-parity verification — all deterministic). See the financial-model-review agent body's "Context B — Post-compose coaching dispatch (POST_COMPOSE_COACHING)" section for the full procedure.
python3 -c '
import json, sys
data = json.load(open(sys.argv[1]))
print(json.dumps(data["coaching_payload"], indent=2))
' "$REVIEW_DIR/report.json"
The payload prints to stdout — copy it from the tool result into the dispatch
prompt below. (Never capture it into a shell variable: each Bash call runs in a
fresh shell, so the variable would be unreadable and gone.)
Dispatch prompt template (substitute <HANDOFF_AGENT> with the Step-0 agent-namespace value — the same rule as every Context A dispatch; the sub-agent has no shell vars, so paste the printed value):
CONTEXT: POST_COMPOSE_COACHING
OUTPUT_PATH: <HANDOFF_AGENT>/coaching_commentary_output.json
You are dispatched to add coaching commentary to a financial model review.
The compose_report.py script has finished. The structured `coaching_payload`
from report.json is:
<paste the coaching_payload JSON printed by the previous Bash command here verbatim>
Follow your agent body's Context B procedure (POST_COMPOSE_COACHING):
1. Compose commentary from the inlined coaching_payload (failed_items,
warned_items, summary, high_severity_warnings, company_name).
If truncated:true, acknowledge that not all failures are shown.
Do NOT Read the full report.md. Do NOT edit report.md or any canonical artifact.
2. Use your Write tool to write to OUTPUT_PATH exactly:
{"commentary_markdown": "<your coaching commentary — markdown body text, WITHOUT
a '## Coaching Commentary' heading and WITHOUT the insertion_marker string>"}
Do NOT write any file other than OUTPUT_PATH — insertion into report.md is the
main thread's job, via the shared insert_coaching.py script.
3. Return:
{"status": "complete", "output_path": "<echo of OUTPUT_PATH>"}
OR, if the payload is unusable (write no file):
{"status": "blocked", "reason": "<specific gap>"}
Stop after returning the receipt JSON. Do not narrate.
After the sub-agent returns: if its final message is a {"status": "blocked", "reason": ...} object, stop and report the reason to the founder — do not run the gate. Otherwise gate the hand-off, then (on gate exit 0) insert deterministically. The commentary leaves the model exactly once (into the sub-agent's Write call) — NEVER re-type the sub-agent's JSON into a heredoc or a python -c argument.
SHARED_SCRIPTS="${CLAUDE_PLUGIN_ROOT}/scripts"
if [ ! -d "$SHARED_SCRIPTS" ]; then
FMR_SCRIPTS="$(find /sessions -type d -path '*/skills/financial-model-review/scripts' 2>/dev/null | head -1)"
[ -n "$FMR_SCRIPTS" ] || FMR_SCRIPTS="$(find / -type d -path '*/skills/financial-model-review/scripts' 2>/dev/null | head -1)"
SHARED_SCRIPTS="${FMR_SCRIPTS%/skills/*}/scripts"
fi
printf '%s' '<agent final message verbatim>' | \
python3 "$SHARED_SCRIPTS/check_handoff.py" "$HANDOFF_DIR/coaching_commentary_output.json" \
--agent-path "$HANDOFF_AGENT/coaching_commentary_output.json" --receipt-json -
On gate exit 0, insert the gated hand-off FILE (feed the file, never re-type the message):
SHARED_SCRIPTS="${CLAUDE_PLUGIN_ROOT}/scripts"
if [ ! -d "$SHARED_SCRIPTS" ]; then
FMR_SCRIPTS="$(find /sessions -type d -path '*/skills/financial-model-review/scripts' 2>/dev/null | head -1)"
[ -n "$FMR_SCRIPTS" ] || FMR_SCRIPTS="$(find / -type d -path '*/skills/financial-model-review/scripts' 2>/dev/null | head -1)"
SHARED_SCRIPTS="${FMR_SCRIPTS%/skills/*}/scripts"
fi
python3 "$SHARED_SCRIPTS/insert_coaching.py" \
--report "$REVIEW_DIR/report.md" \
--marker '<EXACT insertion_marker string from report.json coaching_payload>' \
--commentary-file "$HANDOFF_DIR/coaching_commentary_output.json" \
--verify-artifact "$REVIEW_DIR/inputs.json" \
--verify-artifact "$REVIEW_DIR/checklist.json" \
--verify-artifact "$REVIEW_DIR/unit_economics.json" \
--verify-artifact "$REVIEW_DIR/runway.json"
The gate (check_handoff.py) verifies the sub-agent's hand-off file exists, parses, and matches the receipt's echoed path; on gate exit 0, insert_coaching.py performs the 6-state idempotency check, replaces the marker with ## Coaching Commentary + the commentary in a single in-place write, and verifies run_id parity across all 4 producer artifacts (skipped stubs for unit_economics.json/runway.json carry a metadata.run_id too and verify identically). 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 Verification Gate 2.
check_handoff.py exit 3 (missing/empty file — receipt may be fabricated) → redo-dispatch: fresh Task, same prompt plus one line: "your receipt claimed a file at <path> but none exists; use Write to create exactly that path."
- Exit 4 (file exists, invalid JSON) → repair-dispatch: fresh Task: "Read
<OUTPUT_PATH>; it fails JSON parsing with <verbatim detail from the diagnostic>; fix and rewrite it; return the receipt."
- Exit 5 (receipt echoes a different path) → repair-dispatch telling the agent the exact expected OUTPUT_PATH.
- Exit 6 (receipt unparseable / no
output_path key) → redo-dispatch with "return ONLY the receipt JSON — no fences, no prose." (A status: "blocked" final message is NOT exit 6 — it was handled before the gate.)
insert_coaching.py exit 1 (blocked; stdout carries {"status": "blocked", "reason": ...}) → stop and report the exact reason. Do NOT hand-edit report.md — if the reason mentions a truncated report or a missing marker, re-run compose_report.py --write-md and retry the chain. If the reason is commentary_markdown missing or empty, treat as a malformed hand-off: repair-dispatch quoting the reason.
- After ANY corrective dispatch, resume from the gate chain — never feed
insert_coaching.py an ungated file.
Retry budget: max 2 corrective dispatches (same rule as Context A). Graceful degrade: if the FIRST corrective dispatch also exits 3 while the receipt claims complete with the correctly echoed path, treat the host topology as hand-off-incompatible — fall back to message-channel transport: apply the tolerant JSON extraction protocol to the sub-agent's final message, stage its commentary_markdown payload to $REVIEW_DIR/coaching_commentary.json via a quoted <<'COACHING_EOF' heredoc (single-quoted → apostrophe-safe; NEVER python -c, NEVER the outputs/ root), and run the same insert_coaching.py --commentary-file "$REVIEW_DIR/coaching_commentary.json" against that staged file.
Step 8d: Cleanup
No cleanup needed: scratch lives in $STAGING_DIR (/tmp, reclaimed by the sandbox). Do not rm
anything under $REVIEW_DIR — it is the promoted outputs/ tree in Cowork, where deleting a
user-visible path is unsafe (and the parity gate flags it).
Verification Gate 2 (final)
python3 "$SCRIPTS/verify_review.py" --dir "$REVIEW_DIR" --pretty
This is the final quality gate. If it exits non-zero, fix the issues before presenting anything to the founder. Once it passes, present everything to the founder:
- Present
$REVIEW_DIR/report.md — the primary deliverable (do NOT inline the markdown in the assistant message; present the file path)
- Present the
report.html file path
- Present the
explore.html file path
Do NOT inline report_markdown in the assistant message. The founder reads the file via the path. (Closing the ~80-130K context accumulation issue.)
Presenting numbers to the founder:
- Present the numbers from
report.md verbatim — do not re-derive or restate them from memory or from intermediate context.
- For what-if questions (e.g., "what if we cut burn by 20%?", "what if revenue grows faster?"), direct the founder to
explore.html for precomputed scenarios, or offer to re-run runway.py with a custom --scenarios block for new scenarios. Never estimate the answer by hand in chat.
- The report's footer line (generated by
compose_report.py) already points the founder to the explorer for what-ifs.
Main-Thread Return
This skill runs inline in the main thread (not as a sub-agent). The final outcome the main thread delivers to the founder is:
- The path to
$REVIEW_DIR/report.md — the primary deliverable.
- The headline outcome fields, sourced from the
coaching_payload you inlined in Step 8c (runway_months, overall_status, high_severity_warnings) plus the insert_coaching.py receipt (status, report_path, run_id). The Context B sub-agent no longer echoes these — do not source them from its return.
- Optionally: the HTML report and explorer paths.
Scoring
- Each of 46 items: pass / fail / warn / not_applicable
score_pct = (pass + 0.5 * warn) / (total - not_applicable) * 100
- Overall: "strong" (>=85%), "solid" (>=70%), "needs_work" (>=50%), "major_revision" (<50%)
Feedback
If a run ends blocked or failed, after you report the reason to the founder, add one line:
If this looks wrong or didn't finish, you can flag it: /founder-skills:feedback.
On unsolicited praise or frustration, you may mention /founder-skills:feedback once — never routinely, never mid-workflow, never more than once per session.