| name | cap-table |
| description | Use for any cap-table number, mechanic, or date before a founder signs — even one SAFE/note/warrant described in chat, a quick 'is this dilution reasonable?' gut-check, or a single QSBS / Israeli §102 eligibility question. Reliable, source-cited deterministic math (YC, NVCA, Cooley GO) for SAFE/note conversion and the post-money 'company capitalization' denominator, priced-round dilution, anti-dilution (BBWA / narrow-based / full-ratchet), option pools, warrants, MFN chains, dual-class voting, and Israeli ↔ Delaware flips. NOT for waterfall modeling, cumulative dividends, RSUs, 83(b), 409A, SPAC, warrant repricing, or pure term-glossary definitions — see scope notes. |
| when_to_use | Use whenever a question turns on a cap-table number, mechanic, or date — conversion math, the post-money denominator, dilution, anti-dilution, MFN chains, warrants, dual-class voting, QSBS eligibility dates, §102 timing, or a flip — INCLUDING a single instrument described in chat, a bare yes/no, or a quick gut-check, and any draft or signed SAFE, note, term sheet, option plan, AoA, Carta XLSX, or spreadsheet. These carry known miscalculation and reliance traps, so run the deterministic math rather than answer from memory. Do NOT use for pure glossary definitions with nothing numeric, dated, or eligibility-related at stake ("what is a SAFE?"), fundraising strategy ("how much should I raise?"), or financial-model review (use `financial-model-review`).
|
| user-invocable | true |
Cap-Table Skill
Model cap-table mechanics for founders so they understand what their term sheets, SAFEs, and convertible notes actually do to their ownership — before they sign. Produce rule-pack-cited math for SAFE conversion, convertible-note conversion, priced-round dilution, option-pool top-ups, anti-dilution, and Israeli ↔ Delaware flips. Every counsel-review item links back to a primary source (YC SAFE primer, NVCA model docs, Israeli Companies Law / Income Tax Ordinance, etc.). Tone is founder-first: a candid coach who's read the documents you can't be expected to read.
Reliance Boundary (mandatory)
For any eligibility, qualification, or status determination that turns on tax or legal facts the cap-table data cannot settle — QSBS (IRC §1202), Israeli §102 track / holding period, IIA obligations, or any rule carrying counsel_review — state the cited fact (the date window, threshold, or clock) and stop there. Never conclude that the founder does or will qualify ("yes, you qualify", "you're eligible", "strong eligibility posture"). The date or threshold is a fact you may assert with its citation; the conclusion is a counsel determination — present it as such and emit a counsel item. This holds whether the engagement runs the full pipeline, fast-assess, or a one-line directional answer: the boundary is about what you may conclude, not how deep the analysis went.
Skill Metadata
- Author: lool-ventures
- Version: managed in
founder-skills/.claude-plugin/plugin.json
- Compatibility: Python 3.10+ and
uv for script execution.
- Rule pack: consumes
cap-table-rules.json at script runtime.
- Exports (full pipeline, in
cap-table-{slug}/):
inputs.json + scenarios.json → financial-model-review (cross-validates revenue/dilution scenarios)
cap_state.json → ic-sim (IC partners ask about dilution exposure)
counsel_packet.json → fundraise-readiness (overall readiness scorecard)
report.json → fundraise-readiness, future cross-document-consistency skill
- Exports (fast-assess mode, in
cap-table-{slug}-fastassess/):
fast_assess_only.json — sentinel marking that fast-assess ran (no canonical artifacts). See references/sentinel-schema.md. Future cross-skill consumers MUST check for this sentinel before treating a missing canonical artifact as "cap-table never ran."
report_fast_assess.md — founder-facing markdown deliverable
- Imports:
market-sizing:sizing.json — sanity-check that the planned raise + cap is consistent with modeled SAM/SOM
financial-model-review:report.json — current revenue scale + runway, to gate scenario plausibility
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): Used ONLY for document-extraction lanes. Cap-table math is fully deterministic and rule-driven — the reference's Context A section carries this as cap-table's dedicated exception (no analytical/judgment work in the math layer requires a sub-agent) — so Context A is reserved for tasks that genuinely need semantic extraction from natural-language documents:
INSTRUMENT_EXTRACTION — extract terms from a PDF/DOCX SAFE, note, term sheet, or option plan
SPREADSHEET_STRUCTURE_DETECTION — identify which cells encode founders / preferred / options / convertibles in a freeform spreadsheet that doesn't match the Carta schema
The sub-agent returns structured JSON. The main thread pipes the JSON through the validation producer (extract_instrument.py / extract_cap_table.py), which enforces the anti-hallucination gate. The sub-agent does NOT write artifacts directly.
-
Context B — Post-compose coaching dispatch (POST_COMPOSE_COACHING): After compose_report.py writes report.md + report.json, 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 A): After dispatching the sub-agent, capture its final assistant message. The sub-agent should return raw JSON, but may wrap it in ```json ... ``` fences or add a prose preamble. Extract JSON 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."
Input Formats — Four Lanes
Each lane produces normalized instruments.json and/or cap_state.json plus an extraction_audit.json trail. The main thread picks the lane from the founder's input type.
-
Lane 1 — Single instrument (PDF / DOCX). Typical: 5–15 page SAFE, term sheet, convertible note, option plan, or Articles of Association. Main thread reads via the Read tool (native PDF support, up to 20 pages per call; longer docs use pages parameter). For SAFEs/notes/term-sheets/option-plans: dispatch Context A INSTRUMENT_EXTRACTION; pipe returned JSON through extract_instrument.py. For AoAs: dispatch Context A ARTICLES_OF_ASSOCIATION_EXTRACTION (see references/lanes/lane-1-pdf-docx.md#dispatch-context-a--articles_of_association_extraction); pipe returned JSON through extract_aoa.py which validates + merges preferred-series terms into inputs.json.preferred_series[]. User confirmation via AskUserQuestion before math runs.
-
Image-only PDF guard (any .pdf source). Before you rely on a cap-table read from a .pdf by the Read tool (vision), run python3 scripts/pdf_probe.py "<path>". Text PDFs (image_only: false) read normally. If image_only: true (no text layer — dense tables are under-read by vision), prefer OCR over vision:
- Try OCR first (the full-parity agent image ships
tesseract + pdftoppm): python3 scripts/extract_pdf_tables.py "<path>" rasterizes + OCRs the pages into a --mode=grid payload (same shape Lane 3 consumes). Paste that grid into the Lane-3 SPREADSHEET_STRUCTURE_DETECTION dispatch and run --mode=freeform-emit (the normal Lane-3 path). Set metadata.extraction_mode = "ocr_image_pdf". A3: if the OCR grid shows a printed grand fully-diluted total (a "Total"/"Fully Diluted" row), copy it into inputs.json stated_totals so cap_state.py cross-foots it (W_FD_RECONCILE_DELTA). OCR is lossy — confirm the cap base with the founder before math.
- If OCR is unavailable or fails (binaries absent /
extract_pdf_tables errors): fall back to vision — set metadata.extraction_mode = "vision_image_pdf" (so cap_state.py emits W_VISION_EXTRACTION_LOW_CONFIDENCE and the artifacts carry the caveat), tell the founder the cap table is LOW-CONFIDENCE / directional, and PROCEED (degraded-but-honest — never silently present vision numbers as authoritative).
-
Tracked-changes DOCX guard (any .docx source). Before relying on a .docx read, run python3 scripts/_docx_text.py "<path>" --detect. If has_tracked_changes: true, the file is a redline / unsigned draft under negotiation — the operative terms are ambiguous (struck vs inserted). Do NOT silently extract. Raise an AskUserQuestion BEFORE extraction with two options: (1) "Upload the clean / final executed version" (preferred); (2) "Proceed on the accepted (final-proposed) terms — I understand it's a draft." This gate message is the PRIMARY draft caveat (it reaches the founder even for a standalone instrument that never builds a cap base). On "proceed": get the accepted-view text with python3 scripts/_docx_text.py "<path>" --extract and (a) paste that output as the document text in the Context-A INSTRUMENT_EXTRACTION dispatch — NOT the raw Read-tool view — so the extractor and evidence_verifier read the SAME accepted-revisions view (else a correct inserted-term extraction can't be verified); and (b) set inputs.metadata.source_markup = "tracked_changes_accepted" so cap_state.py emits W_REDLINE_DRAFT and the report persists the caveat. (_docx_text reads the accepted view stdlib-only — works in the sandbox, which omits office_convert.)
-
Lane 2 — Carta XLSX export. Typical: multi-sheet XLSX (Securities, Convertibles, Stakeholders). extract_cap_table.py --mode=carta reads the sheet-name fingerprint and maps known columns → canonical schema. User confirms ambiguous mappings. See references/carta-pulley-mapping.md for the column-mapping table. Pulley is not yet supported end-to-end (--mode=pulley is a stub that returns a structured blocker pointing to Lane 3 / --mode=freeform-emit); restore when a real Pulley XLSX is available to verify against. Carta exports carry no founder identities or pool structure — Lane 2 writes instruments.json + extraction_audit.json ONLY; always build inputs.json from founder answers (one batched AskUserQuestion: founders + share counts, pool authorized/issued/unallocated). When offering founder candidate options, EXCLUDE obvious investor vehicles — names containing Ventures/Capital/Fund (founders are natural persons or a clearly personal holding entity). A holder also appearing as a SAFE/note investor MAY still be a legit founder co-investor — ASK rather than auto-exclude on that alone. Present an investor vehicle as context labeled "(investor — not a founder)", never as a founder option. (cap_state.py emits W_FOUNDER_LOOKS_LIKE_INVESTOR as a backstop.) Reconciliation: if the carta receipt's summary.fully_diluted is present (Carta's printed grand total — independent of the rows you rebuild), copy it into inputs.json stated_totals { "fully_diluted": <n>, "source": "carta_summary" }. cap_state.py cross-foots the rebuilt cap base against it and emits W_FD_RECONCILE_DELTA if they diverge > 0.1% — catching a holder/class dropped during the manual rebuild.
-
Lane 3 — Freeform spreadsheet (founder's Excel). Arbitrary structure — no fixed schema, unlike Lanes 1/2/4.
extract_cap_table.py --mode=auto confirms the workbook is freeform (prints detected_format + sheet names; exits non-zero for freeform by design).
extract_cap_table.py --mode=grid --xlsx "$XLSX_PATH" dumps all sheets as a cell-value grid (per sheet: dimensions, cell values, merged ranges) to stdout as JSON, compacted under a byte budget so it fits the dispatch control-frame cap (large sheets are trimmed/rounded/row-elided — see references/lanes/lane-3-freeform.md; a grid_too_large blocker means split the workbook per-sheet or fall back to Lane 4).
- The main thread pastes that JSON into the Context A
SPREADSHEET_STRUCTURE_DETECTION dispatch prompt to identify cell semantics (block types + column roles from the closed references/schemas/freeform-role-map.json vocabulary).
- Pipe the returned blocks through
extract_cap_table.py --mode=freeform-emit, which deterministically maps them to schema-valid inputs.json (equity, merged into Step 2's file) + instruments.json and writes both.
- Fields the sheet can't supply (e.g. a note's
interest_rate_type) come back as blockers (a human-in-the-loop gate); resolve them with the founder and re-run with --answer BLOCK.FIELD=VALUE.
See references/lanes/lane-3-freeform.md for the full invocation sequence.
SPREADSHEET_STRUCTURE_DETECTION stated-total requirement: alongside blocks, the structure sub-agent must also report a top-level stated_total field = the sheet's printed grand fully-diluted total for the current/most-recent snapshot column (the cell the sheet itself labels "Total Fully Diluted", "TFD", or equivalent — pool-inclusive, as-converted basis).
- Report it only when such a cell is unambiguously present.
- Omit the field entirely if the sheet's grand total is labeled "Issued"/"Outstanding" (pool-excluded), is as-issued rather than as-converted, or the basis is ambiguous — an issued-only total diverges from the fully-diluted count by the whole unallocated pool, so a wrong-basis stamp would fire a false
W_FD_RECONCILE_DELTA on a correct sheet.
- Use only a total the sheet itself prints; never report a sum the skill computed (non-circularity).
-
Lane 4 — Structured JSON paste / conversational. Founder pastes pre-built JSON or describes their cap-table in chat. Direct heredoc into inputs.json / instruments.json; still flows through extract_cap_table.py --mode=validate for schema enforcement.
Available Scripts
All scripts live at ${CLAUDE_PLUGIN_ROOT}/skills/cap-table/scripts/:
extract_instrument.py — Validates Lane-1 sub-agent output against the per-instrument schema; anti-hallucination gate (per-field confidence; "did you find this verbatim in the document"). Accepts: safe, convertible_note, convertible_loan_agreement (Israeli CLA), convertible_security (YC pre-SAFE form), term_sheet, option_plan, warrant, non_instrument.
extract_aoa.py — Validates Lane-1 sub-agent output for Articles of Association (separate sub-context ARTICLES_OF_ASSOCIATION_EXTRACTION). Per-preferred-series field gates; detects 4 Israeli AoA counsel-review items (israeli_aoa.* rule pack domain): drag-along < 75%, §102 plan absent, liquidation preference > 1x, full-ratchet anti-dilution. With --inputs flag, merges validated preferred_series block into inputs.json.preferred_series[] with extraction provenance stamp.
extract_cap_table.py — Lane-2/3/4 cap-table extraction; modes: carta, pulley, freeform-emit, validate, auto, grid. grid dumps all sheets as a JSON cell-value grid for Lane-3 SPREADSHEET_STRUCTURE_DETECTION dispatch; freeform-emit deterministically maps the detected blocks (via freeform_mapper.py + the freeform-role-map.json contract) into schema-valid inputs.json + instruments.json, with founder-confirmation blockers for fields freeform can't supply. Emits cap_state.json + instruments.json + extraction_audit.json.
cap_state.py — Reads inputs.json + instruments.json; computes as-converted totals; writes cap_state.json. Validates per the §11 schema. Note: the YC Company Capitalization denominator scoping (Gotcha #1) is enforced here — as_converted_totals.* is the pre-financing snapshot.
detect_structure.py — Signal-based coverage detector. Reads inputs.json + instruments.json; emits required_primitives, covered (bool), uncovered_parts, and route.scenario_requests for covered deals. Deterministic (no NLP). Run before any math to determine whether the deterministic pipeline covers the deal. See ## Coverage & Disclosure.
rule_audit.py — Two-phase: --phase=pre_math writes the gating block (per-rule, per-instance status + scope + overlays) BEFORE math runs; --phase=post_math composes watchlist + counsel-review items AFTER math. Math producers consume the gating block.
run_scenario.py — Solver / orchestrator (NOT a fixed chain). Builds a dependency graph; classifies independent vs coupled computations; algebraic resolution first, fixed-point iteration as fallback for non-linear systems (discount-only SAFEs). Convergence threshold + max iterations are parameterized.
safe_conversion.py — SAFE conversion math (cap-only, cap-plus-discount, discount-only, uncapped-MFN). Binds rule-pack inputs per the §5.1 binding table (see design doc).
note_conversion.py — Convertible-note conversion math (cap, discount, both, repay, extend, counsel-review, override branches). Binds rule-pack inputs per the §5.2 binding table.
priced_round.py — Priced-round math (pre-money, new-money, pool top-up, anti-dilution). Coupled with SAFE/note conversion via the solver.
option_pool.py — Option-pool top-up math (rule pack option_pool.pre_money_topup). Uses target_basis denominator.
anti_dilution.py — BBWA / full-ratchet anti-dilution (Gotcha #2 enforced here).
flip_scenario.py — Israeli ↔ Delaware flip mechanics (share-for-share 1:1 only — see Gotcha #7).
counsel_packet.py — Extracts counsel-review items from rule_audit.json into a standalone counsel-handoff packet.
compose_report.py — Assembles all artifacts into report.md + report.json (with embedded coaching_payload block). Cross-artifact validation; emits per-uuid coaching insertion marker.
visualize.py — Generates report.html (self-contained, inline SVG donut + tables; no CDN). The interactive explore.py is the one that uses vendored Chart.js.
explore.py — Generates explorer.html (polished interactive scenario tool; demo/video-friendly).
sweep.py — Generates the optional sweep.json: a pre-money parametric sweep (K real solver frames, new_money held fixed) that powers the explorer's "drag pre-money" slider. No new math — re-runs the priced-round path across a pre_money range. Slider snaps to discrete frames, so every value shown is real.
quick_assess.py — Fast-assess directional review (Step 5-fast); writes the fast_assess_only.json sentinel + report_fast_assess.md, skipping the full pipeline.
verify_one.py — Rule-lookup mode (Step 5-lookup): --rule-lookup <rule_id> returns the cited constant a rule holds (e.g. the QSBS OBBBA window start) + its citations + the reliance boundary, for a bare eligibility/date question. Allowlists by data: rules without a stored constant (e.g. §102 capital-gains) return lookup_status: "escalate" rather than echoing a non-constant field. No solver, no artifact.
concise_report.py — Concise mode (Step 5-concise): renders scenarios.json (the solver's computed_outputs) + optional rule_audit.json flags into a short cited report_concise.md, skipping visualize/explore/counsel_packet/the full compose_report/the coaching sub-agent. Same numbers as the full pipeline (reads the same output); for a single quick math question.
evidence_verifier.py / invariant_checker.py / cross_checker.py / backward_verifier.py — Lane-1 verification stack (Step 3). Forward evidence-quote check, real-world-bounds check, multi-extractor cross-check (demote-only), and fresh-sub-agent backward re-extraction. extract_instrument.py invokes these by default; they are also runnable standalone.
_dispatch_json.py — Tolerant JSON extraction for Context A returns.
Also available from ${CLAUDE_PLUGIN_ROOT}/scripts/ (shared):
founder_context.py — Per-company context management (init/read/merge/validate)
find_artifact.py — Resolves artifact paths by skill name, artifact filename, optional company slug
Run with: python3 ${CLAUDE_PLUGIN_ROOT}/skills/cap-table/scripts/<script>.py --pretty [args]
Available References
Read as needed from ${CLAUDE_PLUGIN_ROOT}/skills/cap-table/references/:
cap-table-reference.md — Domain primer: SAFE mechanics, note mechanics, anti-dilution formulas, §102/3(i)/85A/104H/103K, IIA royalty mechanics, BBWA formula, counsel-review semantics. Read before implementing any math producer.
cap-table-rules.json — The executable reference layer: source-cited rules across the SAFE / convertible-note / option-pool / anti-dilution / Israeli-AoA / Delaware-flip / warrants / dual-class / benchmark domains, each with formulas, inputs, outputs, source citations, date_window semantics, and behavior_target (script_formula / validation_rule / warning_rule / counsel_review_flag / benchmark / source_note). Every math producer loads this at start and stamps its metadata.version into provenance.
cap-table-rules.schema.json — JSON Schema for the rule pack (Draft 2020-12). The schema description on counsel_review is the authoritative definition of "reliance boundary, not confidence score" (see Gotcha #9).
schemas/ — JSON Schemas (Draft 2020-12) for every artifact: inputs.schema.json, instruments.schema.json, cap_state.schema.json, scenarios.schema.json, rule_audit.schema.json, counsel_packet.schema.json, fast_assess_only.schema.json. Each producer script validates against the matching schema.
carta-pulley-mapping.md — Per-vendor column-mapping table for Lane 2 extraction.
Artifact Pipeline
Every cap-table engagement deposits structured JSON artifacts into a working directory. The final step assembles them into a report and validates consistency. This is not optional.
| Step | Artifact | Producer |
|---|
| 1 | founder context | founder_context.py read/init |
| 2 | inputs.json | Agent heredoc or extract_*.py (Lane 4 / Lanes 1–3) |
| 3 | instruments.json | extract_instrument.py (Lane 1) or extract_cap_table.py (Lane 2/3/4) |
| 4 | cap_state.json | cap_state.py |
| 5 | extraction_audit.json | extract_*.py trail |
| 6 | rule_audit.json (gating block) | rule_audit.py --phase=pre_math |
| 7 | scenarios.json | run_scenario.py (solver; consumes gating block from Step 6) |
| 8 | rule_audit.json (watchlist + counsel items) | rule_audit.py --phase=post_math |
| 9 | counsel_packet.json + counsel_packet.md | counsel_packet.py |
| 10 | comparisons.json (when ≥2 scenarios) | compose_report.py |
| 11 | report.md + report.json (with coaching_payload block) | compose_report.py --write-md |
| 12 | report.html | visualize.py |
| 13 | sweep.json (optional — priced rounds only) | sweep.py |
| 14 | explorer.html | explore.py |
| 15 | ## Coaching Commentary appended to report.md | Context B sub-agent (POST_COMPOSE_COACHING) |
Rules:
- Deposit each artifact before proceeding to the next step.
- Math producers consume
rule_audit.json.gating[R][I] for rule-applicability decisions — NOT the rule pack directly. The two-phase split is what makes this work.
- For producer-script artifacts, the agent supplies JSON on stdin where applicable; the script schema-validates against
references/schemas/<artifact>.schema.json. Never write artifacts directly via Write or Edit — always pipe through the producer script so metadata.run_id is injected and schemas are enforced.
compose_report.py enforces that all required artifacts share the same run_id and emits STALE_ARTIFACT warnings on mismatch.
Keep the founder informed with brief, plain-language updates at each step. Never mention file names, scripts, or JSON. After each major step (extraction, scenarios, counsel), share a one-sentence finding before moving on.
Coverage & Disclosure
Before running cap-table math in a full-pipeline engagement, determine whether the deal structure falls within the validated engine's coverage. Never substitute mental arithmetic for a covered script path — the fixed-point solver enforces rule-pack gotchas that hand-rolled estimates miss silently.
Detect coverage after instruments are committed
After instruments.json and cap_state.json are written (Steps 3–4), run:
python3 "$SCRIPTS/detect_structure.py" \
--inputs "$REVIEW_DIR/inputs.json" \
--instruments "$REVIEW_DIR/instruments.json" \
--run-id "$RUN_ID" \
-o "$REVIEW_DIR/coverage_result.json" --pretty
The script emits:
required_primitives — mechanics the deal requires
covered (bool) — whether the deterministic pipeline covers all required primitives
uncovered_parts — any primitives the pipeline cannot handle
route.scenario_requests — a ready-to-run scenario list (populated only when covered: true)
If covered: true — use the deterministic pipeline
Populate scenario_requests.json from route.scenario_requests (the detection output) instead of authoring it by hand. Proceed to Step 4.5 and Step 5 as normal. Never hand-roll or mentally estimate a covered deal — not as a sanity check, not as a directional figure alongside the solver result. The solver is the authoritative source; any hand-rolled estimate will diverge from the fixed-point result.
For covered deals, compose_report.py writes coverage_disclosure.json (with covered: true, computation_method: deterministic_pipeline) and any reconciliation banner automatically — no agent action required.
If covered: false — hand-roll with mandatory disclosure
When uncovered_parts is non-empty, the deal combines primitives the engine does not fully handle. You may produce numbers manually, but:
- Build manually from rule-pack formulas — cite each
rule_id explicitly so the math is traceable.
- Write
coverage_disclosure.json into $REVIEW_DIR/ by heredoc:
{
"schema_version": "v0.1-coverage-disclosure",
"covered": false,
"computation_method": "manual_outside_pipeline",
"required_primitives": ["<from detection output>"],
"uncovered_parts": ["<from detection output>"],
"counsel_review": true
}
- Prepend the provisional banner to the report:
> ⚠️ **Computed outside the validated cap-table engine.** This deal combines primitives the deterministic pipeline does not fully cover; figures are provisional — confirm with counsel.
- Emit a counsel item for every uncovered primitive, regardless of confidence in the manual calculation.
Never present hand-rolled figures without the banner and the coverage_disclosure.json artifact.
Acquisition inputs (inputs.acquisition) — counsel-reviewable
When the deal includes a negotiated-% share acquisition concurrent with or immediately prior to a priced round, populate inputs.acquisition in inputs.json:
"acquisition": {
"acquired_entity": "Target Co",
"consideration_pct": 0.05,
"consideration_form": "shares",
"acquisition_timing": "concurrent_with_round"
}
acquisition_timing enum:
concurrent_with_round — consideration shares are excluded from the YC SAFE Company Capitalization denominator; they dilute the SAFE via post-close fully-diluted only.
pre_round_closed — acquisition stock is outstanding immediately prior; included in Company Capitalization (protects the SAFE from consideration dilution).
All three acquisition rules (acquisition.consideration_shares, acquisition.pool_consideration_basis, acquisition.timing) carry counsel_review: true — the consideration formula, pool-denominator treatment, and timing classification each require counsel confirmation before the founder relies on the computed figures. detect_structure.py automatically detects the acquisition block and includes acquisition_consideration and priced_round in required_primitives.
Priced-round scenario parameters — counsel-reviewable
Two parameters on a priced_round scenario require counsel confirmation before applying:
pre_money_basis — whether converting SAFE shares count toward the pre-money denominator. Enum: includes_safe_conversion (default) | excludes_safe_conversion. A negotiated term; do not silently apply the default and present it as the only valid convention.
pool_consideration_basis — when a concurrent acquisition is present, whether consideration shares count toward the post-money pool-sizing denominator. Enum: include (default) | exclude. Negotiable per Cooley pricing conventions (acquisition.pool_consideration_basis rule).
When neither the term sheet nor the AoA settles these questions, batch them into the founder confirm gate and emit a counsel item for each.
Workflow
Step 0: Path Setup
SCRIPTS="${CLAUDE_PLUGIN_ROOT}/skills/cap-table/scripts"
if [ ! -d "$SCRIPTS" ]; then
SCRIPTS="$(find /sessions -type d -path '*/skills/cap-table/scripts' 2>/dev/null | head -1)"
fi
if [ -z "$SCRIPTS" ] || [ ! -d "$SCRIPTS" ]; then
SCRIPTS="$(find / -type d -path '*/skills/cap-table/scripts' 2>/dev/null | head -1)"
fi
PLUGIN_ROOT="${SCRIPTS%/skills/*}"
REFS="$PLUGIN_ROOT/skills/cap-table/references"
SHARED_SCRIPTS="$PLUGIN_ROOT/scripts"
python3 "$SHARED_SCRIPTS/resolve_artifacts_root.py"
RUN_ID="${RUN_ID:-$(date -u +%Y%m%dT%H%M%SZ)}"
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/cap-table/scripts/cap_state.py' 2>/dev/null | head -5 and derive the variables from it.
After Step 1 (when the company slug is known), derive REVIEW_DIR. Two modes:
- Full pipeline (default — when the founder shared a document, asked for the full review, counsel packet, or interactive explorer, OR when there's no existing full review for this slug):
REVIEW_DIR="$ARTIFACTS_ROOT/cap-table-$SLUG".
- Fast-assess mode (Phase O — short directional answer to a conversational question, no document attached, no explicit "full review" request):
REVIEW_DIR="$ARTIFACTS_ROOT/cap-table-$SLUG-fastassess". Run quick_assess.py (Step 5-fast) instead of Steps 2–11. Total wall-clock under 60 seconds.
- Rule-lookup mode (a bare eligibility/date question — QSBS, Israeli §102, IIA — with no instruments to model and no document): answer straight from the rule pack — no
REVIEW_DIR, no pipeline. Run verify_one.py --rule-lookup <rule_id> (Step 5-lookup) and present its cited constant + the reliance boundary (state the date/threshold; never conclude eligibility — emit a counsel item). If it returns lookup_status: "escalate" (the rule carries no stored constant — e.g. the §102 capital-gains clock, which runs from a plan/trustee-specific date the pack does not hold), ask the founder for the specific fact it names (e.g. the trustee-deposit date) and treat as a counsel determination — never state a default like grant_date.
- Concise mode (a single quick math question that
quick_assess can't shape and that isn't a pure eligibility lookup — a fully-diluted warrant count, an as-converted snapshot, a standalone anti-dilution adjustment, one note/SAFE outside a priced round): run the deterministic math, then render a short cited answer with concise_report.py (Step 5-concise) — skip visualize, explore, counsel_packet, the full compose_report, and the Context-B coaching sub-agent. The numbers are identical to the full pipeline's (it reads the same run_scenario output); only the production weight is dropped. Offer the full review as a follow-up. REVIEW_DIR="$ARTIFACTS_ROOT/cap-table-$SLUG-concise".
Slug discipline: Use the slug returned by founder_context.py VERBATIM in directory names — never invent ad-hoc suffixes (e.g. appending -seed, -round, or any other qualifier). Downstream find_artifact.py lookups resolve by that slug; a mismatched directory is invisible to the cross-skill layer.
REVIEW_DIR="${REVIEW_DIR:-$ARTIFACTS_ROOT/cap-table-$SLUG}"
mkdir -p "$REVIEW_DIR"
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>/cap-table-$SLUG/handoff/$RUN_ID"
STAGING_DIR="$(mktemp -d "${TMPDIR:-/tmp}/cap-table-${SLUG}.staging.XXXXXX")"
Context A hand-off protocol (file transport + gate). Every Context A dispatch is a
Task(subagent_type="founder-skills:cap-table", …) call — the subagent_type is REQUIRED on the
original AND on every corrective dispatch below; omitting it silently downgrades to the wildcard,
shell-capable general-purpose agent (a containment defect). Every Context A dispatch prompt carries an
OUTPUT_PATH: line built from $HANDOFF_AGENT (the lane references show the exact Task(...)
templates). The sub-agent WRITES its extraction 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, or the equivalent
agent-namespace path for any other under-outputs artifact a dispatch prompt reads). 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/cap-table/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). Every corrective redo-dispatch
/ repair-dispatch below is a full Task(subagent_type="founder-skills:cap-table", …) — a "fresh
Task, same prompt" must carry the subagent_type; dropping it on a retry is the same containment defect
as omitting it originally:
- Exit 0 → pipe the file through the validator (
cat "$HANDOFF_DIR/<step>_output.json" | python3 "$SCRIPTS/<validator>.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>; fix and rewrite it; return the receipt."
- Exit 5 (receipt echoes a different path) → repair-dispatch with 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."
- Validator schema rejection (the pipe fails next) → repair-dispatch with the validator's stderr verbatim. (Evidence-verifier rejections keep their OWN lane-specific protocol —
retry_hint re-dispatch — which is a content correction, not a transport correction, and does not consume the transport retry budget.)
- Any other exit (script crash etc.) → STOP with the stderr.
- After ANY corrective dispatch, resume from
check_handoff.py — never pipe unchecked.
Retry budget: max 2 corrective transport dispatches per step, of any kind (max 3 total).
After the second corrective dispatch fails any gate: STOP and report the exact diagnostic. The main
thread MUST NOT author or patch extraction content itself — that is the fabrication failure mode
this architecture exists to prevent. A status: "blocked" return is bounded separately: 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 validator 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: validators consume them only via
the explicit pipe, and compose_report.py never reads handoff/.
Routing heuristics. In order: (1) a bare eligibility/date question (QSBS, §102, IIA) with no instruments and no document → rule-lookup (Step 5-lookup) — a cited fact, not a pipeline run. (2) A single quick math question that quick_assess can't shape (warrant fully-diluted count, as-converted snapshot, standalone anti-dilution, a lone note/SAFE outside a priced round) → concise mode (Step 5-concise) — the real math, short answer, no heavy tail. (3) A priced-round gut-check / first-touch conversational answer (no document, no "full review"/"counsel packet"/"report"/"explorer"/"deep dive") → fast-assess. (4) Otherwise → full pipeline. If an existing cap-table-$SLUG/report.json is present, ask via AskUserQuestion whether to use it or start fresh.
Artifact-worthy boundary (write the sentinel). If your answer presents a founder-facing ownership/dilution table or a post-financing ownership %, you MUST run a script-backed path — quick_assess (fast-assess; writes the fast_assess_only.json sentinel + report_fast_assess.md), concise_report (concise mode; writes the real cap_state.json + scenarios.json + report_concise.md), or the full pipeline — do NOT hand-build such an answer in chat. A one-line directional aside while gathering inputs (e.g. "≈20% to new investors") is fine in chat and writes no artifact. This keeps any quantitative ownership answer backed by the script + sentinel, so downstream consumers can detect that cap-table ran.
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.
W_SECTOR_TYPE_UNKNOWN is benign for cap-table engagements. If founder_context.py emits a W_SECTOR_TYPE_UNKNOWN warning (triggered by free-text sectors such as "technology"), proceed — sector_type is not read by any cap-table rule or script, so this warning has no effect on cap-table math or counsel-review gating. Do not re-prompt the founder just to resolve it.
Exit 1 (not found): This is normal for a first run — do not treat it as an error. Use AskUserQuestion (NOT plain chat) to ask for company name, stage, sector, and geography. Provide at least 2 options. Then create:
python3 "$SHARED_SCRIPTS/founder_context.py" init \
--company-name "Acme Corp" --stage seed --sector "B2B SaaS" \
--geography "US" --artifacts-root "$ARTIFACTS_ROOT" \
--run-id "$RUN_ID"
Exit 2 (multiple): Present the list, ask which company, re-read with --slug.
Step 2: Confirm Engagement Mode + Jurisdiction → inputs.json
Ask the founder via AskUserQuestion (NOT plain chat):
- Mode: "Is this a flip-focused engagement (Israeli → Delaware), or a standard cap-table modeling engagement?" Options:
standard | flip_focused.
- Jurisdiction structure: Options:
delaware | israeli | delaware_with_israeli_sub | mid_flip.
- IIA grant history (Israel-context only): "Has the Israeli entity received any IIA / OCS grants?" Options:
yes | no | not_sure.
Then build inputs.json via heredoc. The skeleton below is the minimal shape (no founders, no pool, no preferred) — useful only for flip-focused engagements that read existing artifacts. For any engagement where someone owns shares, also include founders[] and option_pool blocks. Exception — Lane 3 (freeform spreadsheet): write the minimal shape (company meta only, NO founders[] / option_pool / preferred_series); extract_cap_table.py --mode=freeform-emit fills those equity sections from the sheet and would otherwise conflict with seeded placeholders. See references/inputs-skeleton.md for the full common-case shape, the validator-strictness gotcha (unknown keys are silently dropped), and the plan_type / OIP-OCP-CCP field meanings.
MANDATORY cap-base confirmation gate (S2). Before any FULL-pipeline math on an engagement where someone owns shares (Lanes 1/2/4), the founder cap base — each founder + common share count, and the option pool (authorized / issued / unallocated) — MUST be founder-confirmed via one batched AskUserQuestion (same rule as Lane 2). Raise this gate EVEN when the founder states the full base inline — it confirms your INTERPRETATION of their numbers (which founder owns which class, the pool split), catching mapping errors before downstream math binds to them; do NOT treat inline-stated data as pre-confirmed. NEVER assume founder share counts or pool silently, and never use generic placeholder names like Founder A / Founder B. Set metadata.cap_base_source = "confirmed" once confirmed. cap_state.py defaults to ASSUMED: it emits W_CAP_BASE_ASSUMED on any engagement with an equity base UNLESS cap_base_source = "confirmed" — so you must affirmatively set "confirmed" to suppress the directional caveat (the compliance burden is on the safe side). (Lane 3 is exempt — the freeform emit stamps cap_base_source = "confirmed", since the base is extracted from the founder's own spreadsheet.) Provenance: when you hand-build inputs.json directly (Lanes 1/2/4 — a PDF/Carta read, a manual rebuild, or pasted data the deterministic freeform mapper did not produce), also set metadata.cap_base_provenance = "model_reconstructed". The freeform emit auto-stamps cap_base_provenance = "deterministic_mapped" — do NOT override it. cap_state.py emits W_CAP_BASE_RECONSTRUCTED for any confirmed base that is not deterministic_mapped, so a hand-built base is flagged as not-mechanically-verified even when confirmed.
cat <<INPUTS_EOF > "$REVIEW_DIR/inputs.json"
{
"company_name": "Acme Corp",
"analysis_date": "$(date -u +%Y-%m-%d)",
"mode": "standard",
"jurisdiction": {
"structure": "delaware",
"incorporated_date": "2024-06-01",
"iia_grants_history": {"has_grants": false, "grant_details": []}
},
"event_dates": {
"restructuring_effective_date": null,
"restructuring_approval_date": null,
"filing_date": null,
"tax_position_date": null,
"flip_closing_date": null,
"benchmark_reference_date": null
},
"founders": [
{"name": "Founder A", "founder_id": "founder_a", "common_shares": 10000000}
],
"option_pool": {
"plan_type": "iso",
"authorized": 1500000,
"issued": 0,
"unallocated": 1500000
},
"engagement_questions": [],
"metadata": {"run_id": "$RUN_ID", "schema_version": "v0.5.0-inputs"}
}
INPUTS_EOF
founders[] and option_pool are required for any engagement with shares. The schema marks them optional, but cap_state.py produces an empty pre-financing snapshot if either is missing — with no warning. Read references/inputs-skeleton.md if your scenario has preferred series, common_batches, or non-standard option-plan jurisdictions.
metadata.schema_version is required on inputs.json ("v0.5.0-inputs"), instruments.json ("v0.5.0-instruments"), and cap_state.json ("v0.5.0-cap-state"). Producer scripts inject the value when they write; founder-supplied heredoc inputs must include it explicitly or extract_cap_table.py --mode=validate rejects with E_SCHEMA_VERSION_MISMATCH. Common field-name gotchas: preferred_series[].shares (not shares_outstanding); preferred_series[].series_name (not series_label); preferred_series[].liquidation_preference_type (not participation); founders[].common_shares (not shares). preferred_series[] lives in inputs.json, NOT instruments.json — the instruments schema has no preferred_series key; an unknown top-level key in instruments.json is silently dropped rather than rejected, so there is no schema error to catch the mistake.
option_pool.plan_type enum: iso | nso | section_102_cg | section_102_oi | section_3i | mixed. The Israeli §102 capital-gains track is section_102_cg — there is no "israeli_102" or "102_cg" value; those fail validation. See references/inputs-skeleton.md for the jurisdiction-to-plan_type mapping table.
Omit unknown optional fields rather than writing null, EXCEPT fields the skeleton explicitly shows as null — those are schema-nullable (["string","null"] or ["number","null"]) and keeping them as null is correct. Most other schema fields are typed non-nullable (e.g., jurisdiction.incorporated_date is string, not string | null); writing null for those fails validation. Fields shown in the Step 2 skeleton with values are either required or strongly recommended. jurisdiction.incorporated_date matters for §102/QSBS date math — ask the founder rather than omitting if the engagement touches those.
Validate immediately:
python3 "$SCRIPTS/extract_cap_table.py" --mode=validate --dir "$REVIEW_DIR"
Step 3: Ingest Instruments → instruments.json
Route by input format. Each lane has a dedicated dispatch + validation protocol — read the matching lane reference before executing.
Lane 1 invocation pattern — after gating the sub-agent's hand-off file per the Context A
hand-off protocol (Step 0), pipe it through extract_instrument.py like this:
cat "$HANDOFF_DIR/<doc_slug>_extraction_output.json" | python3 "$SCRIPTS/extract_instrument.py" \
--instruments "$REVIEW_DIR/instruments.json" \
--source-doc "$SOURCE_DOC_PATH" \
--run-id "$RUN_ID" --pretty
extract_instrument.py reads the sub-agent JSON from stdin and updates --instruments in place. The -o/--output flag writes a JSON receipt confirming the write (does not change where instruments are stored). If the id already exists in the target array you'll get E_DUPLICATE_INSTRUMENT_ID; re-run with --replace to overwrite the existing entry instead.
Dispatch independence rule (CRITICAL): the sub-agent dispatch prompt for INSTRUMENT_EXTRACTION contains the document text and the GENERIC extraction rules only. NEVER include per-document hints, expected values, or pre-decided classifications in the dispatch prompt (e.g. "this doc's form is cap_plus_discount", "use issuance_date 2024-01-15") — the sub-agent's reading must be independent. The verification stack (evidence_verifier.py → invariant_checker.py → cross_checker.py) exists to catch divergence; a led witness cannot diverge. Generic normalization rules (e.g. '"Discount Rate is 80%" means multiplier 0.80') are field semantics, not per-document answers — those belong in the dispatch prompt.
Verification stack (Lane 1 and any other lane that piped through extract_instrument.py): forward evidence_verifier.py → invariant_checker.py → cross_checker.py → optional backward_verifier.py. All default-on; see the Lane 1 reference for the receipt schema, attention_needed_fields semantics, and when to run backward verification.
EXTRACTION CONFIRM-GATE (MANDATORY)
Trigger. Extractor warnings that say "confirm with note text" or "confirm with founder" are a QUESTION GATE — not suggestions. After any extraction (Lane 1, 2, or 3), scan all warnings for any field flagged as assumed or left null (e.g. qualified_financing_threshold, maturity_default_treatment, interest_converts_to_shares, interest_rate_type, capitalization_denominator).
Required action. Batch ALL such fields into ONE AskUserQuestion call and get the founder's answers before running any math. NEVER fill them with "standard assumptions" and proceed silently. If the founder cannot confirm a field, you MUST:
- (a) name the assumption explicitly in the final presentation (e.g. "interest rate type assumed fixed simple — confirm with note text");
- (b) emit a counsel item flagging it.
Special cases.
- The denominator field (
capitalization_denominator) requires special care: its definition comes from the note's "Company Capitalization" clause and is note-text-specific — do not default to the cap_state fully-diluted count.
non_qualified_financing_treatment: convert_anyway means convert at cap/discount even though the round missed the qualified financing threshold — there is no separate convert_at_cap enum value for this field.
Exception — blank/template purchase_amount. If a SAFE's purchase_amount is absent because the source is an unfilled template AND the founder cannot supply the amount, proceed-degraded instead of stopping:
- Fill
issuance_date with a clearly-noted stand-in (e.g. the analysis date).
- Leave
purchase_amount absent so the SAFE is recorded terms-only/non-convertible (the pipeline emits W_SAFE_PURCHASE_AMOUNT_MISSING).
- Present the extracted terms in the report, and state that conversion math for that instrument requires the investment amount.
Do not end the turn on the question for a blank template; raise AskUserQuestion as the primary path, but use proceed-degraded when the founder confirms the amount is not yet available. When you present the extracted terms and stop (no math), end with: "If you'd like the full cap-table review — saved artifacts, dilution scenarios, and a counsel packet — just say 'review my cap table' or 'model the round'."
SOURCE-OVERRIDE DECLARATION (MANDATORY)
Trigger. Distinct from a null/assumed field: whenever you treat a value the source actually STATES as wrong, superseded, or a formula artifact — or make any non-obvious judgment that CHANGES the numbers. Examples:
- "the Convertibles sheet's outstanding column is a formula error, so these SAFEs are already converted";
- "ignored row N as a sum/subtotal artifact";
- "reinterpreted negative-price cells".
Required action. You MUST:
- (a) name the override explicitly in the final presentation ("treated X as Y because Z — confirm against the source") AND flag it there as an explicit assumptions/diligence caveat;
- (b) include it in the cap-base confirmation gate so the founder confirms the INTERPRETATION, not just the resulting numbers.
Not a substitute path. Do NOT expect to "add a counsel item" for it — counsel_packet items are rule-pack-generated from matched rules; the model cannot author one. The override's home is the report narrative's assumptions/caveats + the confirm gate, both of which you DO control. A confident reinterpretation baked silently into a confirmed, zero-warning base is the failure this prevents — never let a judgment call disappear into clean-looking output. (This is an instruction, not a script gate; W_CAP_BASE_RECONSTRUCTED already flags a hand-built base as model-reconstructed, but it does not name the specific override — that naming is on you here.)
Freeform discount is NOT a confirm-gate field. A freeform spreadsheet discount column is governed by the deterministic rate convention (references/schemas/freeform-role-map.json discount_convention: a value like 0.20/20 is a 20% RATE → multiplier 0.80; multiplier-form input is unsupported). The producer's resulting conversion warning is a TRANSPARENCY note — surface it in the final presentation (see the Lane-3 reference's ok:true step), but NEVER raise a rate-vs-multiplier AskUserQuestion for it: the convention already decided it is a rate. (Genuinely out-of-range values — > 100 or ≤ 0 — still surface as blockers, which is a different path.)
Freeform multi-snapshot column self-check (Lane 3). A founder's spreadsheet often has several date columns representing successive closing snapshots (e.g. "Seed closing", "Bridge closing", "Current"). When the SPREADSHEET_STRUCTURE_DETECTION dispatch identifies more than one snapshot or closing column, you MUST do both of the following in the confirm-gate before any math runs: (a) name the column you used as the current/outstanding share count — state it explicitly (e.g. "I used the 'Current' column dated 2025-03-31 as the outstanding share count"); and (b) cross-check your per-holder sums against that column's printed subtotal or total cell for each series — if the sheet shows a "Total Preferred" or "Fully Diluted" cell for that column, sum the holders you mapped and compare; surface any mismatch in the confirm-gate (e.g. "My mapped holders sum to 4,800,000 preferred shares; the sheet's total cell shows 5,000,000 — please confirm which holders I may have missed"). This is an LLM-process self-check, not a deterministic guarantee; the goal is to catch a missed holder or misread column before the math binds. If the sheet has no printed total cell for the chosen column, note that the cross-check was not possible. Stated-total stamp (both Lane-3 paths): whenever a same-basis printed grand fully-diluted total is available (pool-inclusive, as-converted — e.g. a "Total Fully Diluted"/"TFD" cell for the chosen column), stamp it into inputs.stated_totals so cap_state.py can cross-foot the rebuilt cap base and emit W_FD_RECONCILE_DELTA if they diverge — mirroring Lane 2 (carta_summary) and the OCR path. Two paths: (a) freeform-emit path — include stated_total: <n> at the top level of the SPREADSHEET_STRUCTURE_DETECTION response JSON (the mapper carries it through --mode=freeform-emit into inputs.stated_totals mechanically); (b) direct-build path (mapper bypassed, inputs.json assembled by hand from the grid) — write "stated_totals": { "fully_diluted": <n>, "source": "freeform_grid" } into inputs.json directly. Basis-match rule (prevents false positives): stamp ONLY a total that is explicitly fully-diluted and pool-inclusive; omit the stamp when the sheet's grand total is labeled "Issued"/"Outstanding" (pool-excluded), is as-issued rather than as-converted, or the basis is ambiguous — a wrong-basis stamp fires W_FD_RECONCILE_DELTA on a correct sheet (a cry-wolf warning is worse than no warning). Better no cross-foot than a false positive. Non-circularity: use only a total the sheet itself prints; never a sum the skill computed.
Image-only PDFs (vision fallback): if the document has no text layer, the verifier can't match values. Dispatch a FRESH sub-agent to transcribe the relevant passages, then feed that text to the verifier via --doc-text <file> --doc-text-source model_vision. The verifier stamps verification_source: "model_vision" and demotes confidence one level — surface that to the founder. (A missing PDF parser is different: it raises E_MISSING_DEPENDENCY and blocks, not a silent image-only pass.)
Lane 4 instruments.json SAFE skeleton (use when authoring by heredoc or conversational reconstruction):
{
"safes": [
{
"id": "safe_1",
"investor_name": "Investor Name",
"purchase_amount": 500000,
"post_money_valuation_cap": 5000000,
"discount_multiplier": null,
"form": "yc_postmoney_cap",
"issuance_date": "2025-06-01",
"extraction_confidence": "high"
}
],
"convertible_notes": [],
"warrants": [],
"option_grants": [],
"metadata": {"run_id": "<RUN_ID>", "schema_version": "v0.5.0-instruments"}
}
Field-name traps: it's purchase_amount (not principal — that's convertible notes), post_money_valuation_cap (not valuation_cap), form (not safe_type/instrument_type), id (not safe_id). The form enum values are: yc_postmoney_cap, yc_postmoney_discount, yc_uncapped_mfn, cap_plus_discount, yc_premoney_cap_only, pre_money_cap_and_discount_legacy, other. For a cap-AND-discount SAFE use form: "cap_plus_discount" with BOTH post_money_valuation_cap and discount_multiplier non-null.
Field-name boundary — id vs safe_id: When authoring or validating instruments.json, each SAFE object uses the field name id (e.g. "id": "safe_seed_1"). The field safe_id appears ONLY in cap_state.json output objects, where cap_state.py renames it for that artifact. Never write safe_id into instruments.json; the schema will reject it as an unknown key, and cap_state.py will raise E_SAFE_MISSING_FIELD.
After Step 3 completes, instruments.json is committed and the run proceeds to Step 4.
Step 4: Compute Cap State → cap_state.json
python3 "$SCRIPTS/cap_state.py" \
--inputs "$REVIEW_DIR/inputs.json" \
--instruments "$REVIEW_DIR/instruments.json" \
--run-id "$RUN_ID" \
-o "$REVIEW_DIR/cap_state.json" --pretty
The script computes the pre-financing as_converted_totals (Gotcha #1 enforced structurally) and validates against references/schemas/cap_state.schema.json.
Scan cap_state.json.warnings[]. A W_ANTI_DILUTION_NONCANONICAL / W_ANTI_DILUTION_UNRECOGNIZED warning means a founder's anti-dilution intent was written under a non-canonical key (e.g. anti_dilution / bbwa) and was recovered (or flagged) rather than silently dropped — the report renderers now surface it, but you MUST also confirm the recovered term with the founder before relying on the down-round math (it changes the conversion).
Step 4.5: Pre-Math Rule Audit → rule_audit.json (gating block)
python3 "$SCRIPTS/rule_audit.py" --phase=pre_math \
--inputs "$REVIEW_DIR/inputs.json" \
--instruments "$REVIEW_DIR/instruments.json" \
--cap-state "$REVIEW_DIR/cap_state.json" \
--run-id "$RUN_ID" \
-o "$REVIEW_DIR/rule_audit.json" --pretty
Math producers in Step 5 consume rule_audit.json.gating[R][I] — they do NOT re-evaluate rule applicability. This is the only place status is computed.
Step 5-fast (FAST-ASSESS MODE ONLY): Run quick_assess.py and exit
When the routing decision in Step 0 picked fast-assess, skip the full Steps 2–11 pipeline (no cap_state / scenarios / rule_audit / counsel_packet / compose). You still author two small input files first — a minimal inputs.json and a bare SAFE array — directly from the founder's conversational answers (see the AskUserQuestion gate below); fast-assess does NOT run the Lane-1/2/3 extractors or the full Step 2 heredoc. Then run:
python3 "$SCRIPTS/quick_assess.py" \
--inputs "$REVIEW_DIR/inputs.json" \
--safes "$REVIEW_DIR/safes.json" \
--pre-money 20000000 --new-money 5000000 \
--review-dir "$REVIEW_DIR" \
--run-id "$RUN_ID" \
--founder-prompt "<the founder's raw prompt>" \
--pretty
--safes takes a BARE JSON ARRAY of SAFE objects (not the instruments.json envelope). Write a file that starts with [ — an array of SAFE instrument objects — not {"safes": [...]}. To author it, write the minimal inputs.json (company_name, founders, option_pool, jurisdiction) and the SAFE array straight from the founder's answers — these two files are the only artifacts fast-assess writes by hand.
Convertible notes need a conversion date. If the founder has notes, pass --event-date YYYY-MM-DD (the date the notes convert). If you omit it when notes are present, fast-assess defaults to today and discloses the assumption (an Assumptions line in the report + a sentinel assumptions[] entry) — the math producer itself never assumes a date.
Never assume a pool top-up. Pass --target-pool-percent X --target-basis post_money ONLY when the founder stated a pool target (or confirmed one when you asked). Otherwise run WITHOUT those flags — the report then carries an explicit "No pool top-up modeled" note, and you offer the 10% what-if as a follow-up re-run. A silently assumed pool target materially changes the founder's headline ownership; it is the founder's negotiation variable, not yours.
Inputs are built from the founder's conversational description via AskUserQuestion (Lane 4 only — fast-assess does NOT invoke Lane-1/2/3 extractors). Do not skip the question gate, and do not split it: batch everything still missing into ONE AskUserQuestion call before running — typically jurisdiction structure (if not obvious), IIA/OCS grant history (Israeli companies), and pool-target intent. If the founder's message already supplied everything, ask nothing and run. When you present the result, state in one line any flag choices that encode an assumption (e.g. "modeled with no pool top-up" / "modeled with the 10% post-money pool you mentioned"). The script writes:
${REVIEW_DIR}/fast_assess_only.json — sentinel for downstream consumers
${REVIEW_DIR}/report_fast_assess.md — 1-page founder-facing markdown
Read report_fast_assess.md and present its numbers verbatim to the founder — never re-derive or reconstruct the ownership table in chat. If you computed preliminary estimates while gathering inputs, discard them in favour of the script output. The script is the authoritative source; hand-reconstructed math will diverge from the fixed-point solver result. This includes the dilution explanation — use the share counts from the report; never re-derive top-up or conversion shares by hand. For what-if follow-ups (e.g. "what if we top up the pool to 10%?"), re-run quick_assess.py with the changed flag and present the new report — never estimate the answer by hand.
Full-pipeline what-ifs (applies to both fast-assess and full reviews): the explorer.html displays only precomputed scenarios (plus, when sweep.json exists, a pre-money slider that scrubs precomputed real solver frames — also not hand-estimated). For any scenario not yet modeled, write a new scenario request and re-run the full pipeline:
- Add the new scenario to
scenario_requests.json
- Re-run
run_scenario.py → rule_audit.py --phase=post_math → compose_report.py
- Present the updated
report.md numbers verbatim
Never hand-estimate a new scenario in chat.
Total wall-clock: under 60 seconds. Then jump to Step 12: Deliver Artifacts with the fast-assess deliverable. Close with a STATEMENT offering the next step, not a trailing question (a final turn ending in a bare "?" with no tool call is a stall) — e.g. "That's the directional answer. If you'd like the full cap-table review — saved artifacts, dilution scenarios, and a counsel packet — just say 'review my cap table' or 'model the round'."
Step 5-lookup (RULE-LOOKUP MODE ONLY): Run verify_one.py and exit
For a bare eligibility/date question (QSBS, Israeli §102, IIA) — no instruments, no document — answer from the rule pack instead of running the pipeline. Pick the rule that holds the relevant fact and run:
python3 "$SCRIPTS/verify_one.py" --rule-lookup delaware_cross_border.qsbs_date_sensitive --pretty
lookup_status: "answered" — present the answer (the cited constant + its primary source) and the reliance_boundary verbatim-in-spirit: state the date/threshold, then stop. Never conclude eligibility; emit a counsel item (the rule carries counsel_review: true).
lookup_status: "escalate" — the rule holds no stored constant (e.g. israel_equity_tax.section_102_capital_gains, whose clock runs from a plan/trustee-specific date the pack does not store). Do not state a default. Ask the founder for the specific fact the payload names (e.g. the trustee-deposit date), and treat it as a counsel determination.
lookup_status: "not_found" — the rule_id was wrong; pick the correct one or fall back to fast-assess / full pipeline.
This writes no artifact and runs in well under a second. End your response with: "If you'd like the full cap-table review — saved artifacts, dilution scenarios, and a counsel packet — just say 'review my cap table' or 'model the round'." If the founder then supplies instruments or asks for the full picture, route to fast-assess or the full pipeline.
Step 5-concise (CONCISE MODE ONLY): run the math, render a short answer, skip the heavy tail
For a single quick math question, do Steps 2–3 (build inputs.json + instruments.json + a one-line scenario_requests.json from the founder's description, Lane 4), then run ONLY the math producers + concise_report.py. Skip counsel_packet, the full compose_report, visualize, explore, and the Context-B coaching sub-agent. The over-production in the full pipeline is the model driving ~14 sequential tool calls plus the coaching sub-agent — not the scripts (each runs in well under 0.1 s); concise mode collapses the tail to one render.
mkdir -p "$REVIEW_DIR"
python3 "$SCRIPTS/cap_state.py" --inputs "$REVIEW_DIR/inputs.json" --instruments "$REVIEW_DIR/instruments.json" --run-id "$RUN_ID" -o "$REVIEW_DIR/cap_state.json"
python3 "$SCRIPTS/rule_audit.py" --phase=pre_math --inputs "$REVIEW_DIR/inputs.json" --instruments "$REVIEW_DIR/instruments.json" --cap-state "$REVIEW_DIR/cap_state.json" --run-id "$RUN_ID" -o "$REVIEW_DIR/rule_audit.json"
python3 "$SCRIPTS/run_scenario.py" --inputs "$REVIEW_DIR/inputs.json" --instruments "$REVIEW_DIR/instruments.json" --cap-state "$REVIEW_DIR/cap_state.json" --scenarios-input "$REVIEW_DIR/scenario_requests.json" --run-id "$RUN_ID" -o "$REVIEW_DIR/scenarios.json"
python3 "$SCRIPTS/rule_audit.py" --phase=post_math --inputs "$REVIEW_DIR/inputs.json" --scenarios "$REVIEW_DIR/scenarios.json" --run-id "$RUN_ID" -o "$REVIEW_DIR/rule_audit.json"
python3 "$SCRIPTS/concise_report.py" --inputs "$REVIEW_DIR/inputs.json" --scenarios "$REVIEW_DIR/scenarios.json" --rule-audit "$REVIEW_DIR/rule_audit.json" --cap-state "$REVIEW_DIR/cap_state.json" --run-id "$RUN_ID" -o "$REVIEW_DIR/report_concise.md"
Pass --cap-state so the concise answer surfaces any anti-dilution recovery warning — a standalone anti-dilution question routes here, and without it the recovery is silently dropped on this route.
Present report_concise.md verbatim — never re-derive its numbers in chat. Concise mode writes the real cap_state.json + scenarios.json (so downstream consumers detect cap-table ran). Then jump to Step 12: Deliver Artifacts and close with a STATEMENT offering the full review as a follow-up (not a trailing question — a final turn ending in a bare "?" with no tool call is a stall), e.g. "If you'd like the full cap-table review — saved artifacts, dilution scenarios, and a counsel packet — just say 'review my cap table' or 'model the round'."
Step 5: Determine Scenarios + Run Math → scenarios.json
Ask the founder via AskUserQuestion which scenarios to model (1–4). Common patterns:
- Standalone SAFE conversion (cap-implied math; no priced round):
{type: "safe_conversion", parameters: {}}
- Series A priced round:
{type: "priced_round", parameters: {pre_money, new_money, target_pool_percent, target_basis}}
- Convertible note conversion at financing:
{type: "note_conversion", parameters: {transaction_event_date, priced_round_new_money, qualified_financing_price}}
- Israeli ↔ Delaware flip (only when mode=flip_focused or explicitly requested):
{type: "flip", parameters: {iia_grants_in_history, section_102_grants_outstanding}}
Stated round terms — PPS and pre-money stamp. When a source document (term sheet, pro-forma cap table, Carta export) prints a round price-per-share and/or a pre-money valuation as a headline term, capture those values into inputs.stated_totals alongside the existing fully_diluted entry, so compose_report.py can include a reconciliation row comparing the document's stated figures against the skill's computed ones:
"stated_totals": {
"price_per_share": 2.50,
"pre_money": 20000000,
"source": "term_sheet"
}
Non-circularity rule — read this carefully. Capture ONLY a figure the source document itself PRINTS as a headline term (e.g., "Series A Share Price: $2.50" or "Pre-Money Valuation: $20,000,000"). NEVER populate price_per_share or pre_money from a value the skill computed — if you do, compose_report.py's reconciliation cross-foot trivially matches and silently hides a real divergence between the document's terms and the skill's math (a false green — the worst failure mode). If the source document does not print a PPS or pre-money figure, omit those fields entirely; do not back-fill them from the model or from scenarios.json. A missing stamp means the reconciliation row is absent (a silent miss — degraded, not wrong); that is far preferable to a circular cross-foot. This is an instruction the agent must follow — there is no deterministic script guard enforcing it.
Founder-only gates must offer an escape. When a recurring gate asks for a fact only the founder knows and the documents don't settle, make the last option an explicit defer — e.g. Not sure / Not sure yet / Don't have the terms handy / Different — I'll correct it in chat — so an unattended or automated answerer can decline rather than fabricate a fact it cannot know. One exception: when a gate's option labels map to a typed downstream enum (the jurisdiction-structure gate below is the only such case), keep the closed option set and accept the residual risk — an out-of-enum "not sure" can't serialize cleanly and would change which downstream rules fire.
Gate Catalog — canonical phrasing (use these exact strings). To keep founder UX consistent and let the regression cassettes anchor on a stable substring, when you raise these recurring AskUserQuestion gates use the question text and option labels below verbatim — do not paraphrase or add per-run suffixes like "(Recommended)" / "(standalone)" / "— no priced round assumed". NEVER append run-specific text to a catalog label — no founder names, share counts, sector, or "(Recommended)"; that context belongs in the question body or an option description, never the option label. (Worked example of the violation to avoid: render the cap-base confirm label as the catalog's Confirmed, NOT Confirmed — Alice, Bob, Carol.) (One-off, document-dependent gates — Lane-1 counsel-review, Lane-2 column mapping, Lane-3 blockers — stay as loose intent; they don't recur in a stable shape.)
| Gate | When | Question | Option labels | Notes |
|---|
| Scenario selection | any run | "Which scenarios should I model? (select all that apply)" | Cap-implied SAFE snapshot (→ safe_conversion), Series A priced round (→ priced_round), Convertible note conversion at financing (→ note_conversion), Israeli ↔ Delaware flip (→ flip) | Offer only the scenarios that apply to this cap table. |
| Option pool | confirming pool existence (e.g. Lane-3 sheet with no pool tab) | "Does the company have an employee option pool?" | No option pool, Yes — I'll provide authorized / issued / unallocated | The "Yes" label MUST keep a free-text/chat path (same affordance as S2, Step 2). Never collapse to bare yes/no. |
| Cap-base confirmation | S2 | "Please confirm [Company]'s cap-table base — I'll use exactly these numbers for all the math:" | Confirmed, Different — I'll correct it in chat | — |
| Note "Company Capitalization" denominator | S3 | "What does the note's 'Company Capitalization' clause define as the conversion denominator?" | Fully-diluted pre-financing (common + options + as-converted SAFEs/notes, before new money), Issued-and-outstanding only (common + issued options, no unallocated pool), I'd need to check the note text | Do NOT append "(Recommended)" — leading Fully-diluted is the stable cassette anchor. |
| Note maturity default | S3 | "If the note reaches maturity before a qualified financing, what happens?" | Convert at cap, Repay principal, Extend maturity, Counsel review / unclear | — |
| Qualified-financing threshold | S3 | "What dollar amount triggers the note's automatic conversion (its 'qualified financing' threshold)?" | free-text (deal-specific — no fixed labels) | — |
| Existing-review routing | S0 | "I found an existing cap-table review for [Company]. Use it, or start fresh?" | Use existing review, Start fresh | — |
| Pool top-up intent | fast-assess / priced round | "Are you planning to top up your option pool as part of this round?" | No top-up planned, Top up to 10% post-money, Top up to 15% post-money, Not sure yet | — |
| Company stage | S0/S2 company-context | "What stage is [Company] at?" | Pre-seed, Seed, Series A, Series B+ | Stage only — never combine with sector (NOT Seed / B2B SaaS); raise sector as a separate question. |
| Jurisdiction structure | S2 company-context | "What is [Company]'s jurisdiction structure?" | Israeli company, Delaware (already flipped), Mid-flip, Delaware with Israeli subsidiary | → israeli | delaware | mid_flip | delaware_with_israeli_sub enum, in that order. Use verbatim — do NOT reword to Israeli company (IL only) etc. |
| IIA / OCS grants | Israeli company-context | "Has [Company] received any IIA (Israel Innovation Authority / OCS) grants?" | No IIA grants, Yes, has IIA grants, Not sure | — |
| SAFE terms | Lane-2/3, sheet shows amounts but not cap/discount | "Do you know the valuation cap and/or discount on the SAFEs? The spreadsheet shows amounts but not the terms." | Yes — I'll share the terms in chat, Uncapped MFN SAFEs, Don't have the terms handy | — |
Write the scenario-request list to a temp file:
cat <<SCENARIOS_EOF > "$REVIEW_DIR/scenario_requests.json"
[
{"scenario_id": "base", "label": "Base case", "type": "safe_conversion", "parameters": {}},
{"scenario_id": "series_a", "label": "Series A at \$20M pre / \$5M raise",
"type": "priced_round",
"parameters": {"pre_money": 20000000, "new_money": 5000000,
"target_pool_percent": 0.10, "target_basis": "pre_money"}}
]
SCENARIOS_EOF
Then run all scenarios:
python3 "$SCRIPTS/run_scenario.py" \
--inputs "$REVIEW_DIR/inputs.json" \
--instruments "$REVIEW_DIR/instruments.json" \
--cap-state "$REVIEW_DIR/cap_state.json" \
--scenarios-input "$REVIEW_DIR/scenario_requests.json" \
--run-id "$RUN_ID" \
-o "$REVIEW_DIR/scenarios.json" --pretty
run_scenario.py dispatches to the right math producer per scenario type and consumes the gating block from Step 4.5. After this completes, share a one-sentence finding per scenario with the founder (e.g., "Series A drops your stake from 87% to 64%; the 10% pool top-up costs you ~3pp").
scenarios.json ownership shape: per-holder ownership percentages live in scenarios.json → scenarios[n].computed_outputs.aggregate_ownership_by_class (an object keyed by class, e.g. {"founders": 0.64, "preferred": 0.26, "option_pool": 0.10}). Per-holder share counts and full ownership tables are always rendered in report.md's Current Cap State section by compose_report.py: individual founders appear by name with share counts and pre-round % in both single-class and dual-class engagements. There is no post_financing_table.rows field in scenarios.json — do not look for one there.
Step 6: Post-Math Rule Audit → rule_audit.json (watchlist + counsel items)
python3 "$SCRIPTS/rule_audit.py" --phase=post_math \
--inputs "$REVIEW_DIR/inputs.json" \
--scenarios "$REVIEW_DIR/scenarios.json" \
--run-id "$RUN_ID" \
-o "$REVIEW_DIR/rule_audit.json" --pretty
The gating block from Step 4.5 is preserved verbatim; this phase adds watchlist + counsel items. --inputs and --scenarios are required for runtime-event counsel-rule gating — the anti_dilution.stale_ccp_detected, anti_dilution.cp2_floor_applied, anti_dilution.pay_to_play_provision_detected, and four anti_dilution.solver_* rules each fire only when their underlying runtime event actually occurred (solver warning emitted, AoA P2P pattern detected, etc.). Without the flags, those rules default to suppressed — safe (no false positives) but produces false negatives instead. Always pass both.
Step 7: Counsel Packet → counsel_packet.json + counsel_packet.md
python3 "$SCRIPTS/counsel_packet.py" \
--rule-audit "$REVIEW_DIR/rule_audit.json" \
--inputs "$REVIEW_DIR/inputs.json" \
--scenarios "$REVIEW_DIR/scenarios.json" \
--run-id "$RUN_ID" \
-o "$REVIEW_DIR/counsel_packet.json" \
--write-md "$REVIEW_DIR/counsel_packet.md" --pretty
Step 8: Compose Report → report.md + report.json
python3 "$SCRIPTS/compose_report.py" \
--dir "$REVIEW_DIR" --run-id "$RUN_ID" \
-o "$REVIEW_DIR/report.json" \
--write-md "$REVIEW_DIR/report.md" --pretty
compose_report.py validates run_id parity (emits STALE_ARTIFACT warning on mismatch) and writes the per-run uuid insertion_marker Context B will use in Step 11.