Cap-Table Skill

SkillDocs & knowledge

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.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the Cap-Table Skill skill

What this skill tells your AI

The instructions your AI receives, as published by lool-ventures/founder-skills in founder-skills/skills/cap-table/SKILL.md and read by ahel’s review.

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 data/cap-table-rules.json at script runtime.
  • Exports (full pipeline, in cap-table-{slug}/):
    • inputs.json + scenarios.jsonfinancial-model-review (cross-validates revenue/dilution scenarios)
    • cap_state.jsonic-sim (IC partners ask about dilution exposure)
    • counsel_packet.jsonfundraise-readiness (overall readiness scorecard)
    • report.jsonfundraise-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 staged coaching_payload.json from the hand-off dir (Mitigation 2) — it does NOT read the full report.md — composes the coaching commentary, WRITES it to the OUTPUT_PATH hand-off file, and returns a small receipt. The main thread gates the file (check_handoff.py) and inserts it via the shared insert_coaching.py script (idempotency matrix, uuid-marker replacement, run_id-parity verification — all deterministic). See the reference above for the full Context B contract.

Tolerant JSON extraction protocol (Context 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:

  1. If the message is wrapped in a ```json ... ``` (or plain ``` ... ```) fence, strip the fence first.
  2. Try to parse the stripped text directly as JSON.
  3. If that fails, walk through the text looking for the first { character and try json.JSONDecoder().raw_decode(text[i:]) — this is brace-aware and handles nested objects correctly (unlike regex, which truncates on the first }).
  4. If extraction fails entirely, re-prompt the sub-agent with: "Your previous reply could not be parsed as JSON. Return ONLY the JSON object — no markdown fences, no prose preamble."

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. Founder-facing: lanes, grids, and structure-detection are internal — to the founder this is just "reading your cap table" (or "reading your spreadsheet"); never announce the lane number, "the grid", "structure detection", or a script/flag as you work through the steps below.

  • 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 — present the extracted terms in the question body and confirm with the Gate Catalog's Cap-base confirmation labels (the same confirm-or-correct pair); never put document values in an option label.

  • 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).
    • RTL / reversed-Hebrew text layer. pdf_probe.py also reports an rtl block; when rtl.rtl_suspect is true (a Hebrew-locale export — common in this corpus — even one that HAS a text layer), do not transcribe tables from a vision read alone: extract the raw text (pdfplumber), check line direction, and if rtl.rtl_reversed_likely reverse each line before reading labels (digits inside an RTL line usually stay LTR — verify against a printed total / stated_totals before math). Warning only; lane routing is unchanged.
  • 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: "This document has tracked changes — it's a redline / unsigned draft, not a final executed version. How should I proceed?" Options: Upload the clean / final executed version / 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.

    1. extract_cap_table.py --mode=auto confirms the workbook is freeform (prints detected_format + sheet names; exits non-zero for freeform by design).
    2. 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).
    3. 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).
    4. 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.
    5. 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.

    Lane 4 has no document to fall back on, so a field the founder did not state must NOT be invented. Lanes 1–3 have the absent-field protocol (leave null, confidence.level: "absent", an ambiguities entry, completeness: "partial") precisely because a document can be silent on a field. A conversation can be silent too, and the same rule applies with more force: there is no source to re-read later.

    Concretely — a SAFE or note issuance_date the founder never gave. Do not fill in a plausible date: it flows into the date-sensitive watchlist and a founder-facing status table, where it is indistinguishable from one they supplied. Instead ask — the date is one AskUserQuestion away, shaped per the Gate Catalog's Founder-only fact gates row (a stated-value option plus an explicit defer), and it changes QSBS, §102 and maturity math — or record it absent per the protocol above and say in the report that the timing rows are unresolved pending that date. The same holds for any relaxable field: purchase_amount / principal / investor_name / maturity_date.

    A fabricated date is worse than a missing one. A missing one stops the founder; a fabricated one gives them a QSBS clock that is wrong by however far you guessed.

  • Anything not matching a lane above (e.g. a multi-page PDF export of a pro-forma cap table, a scanned ledger, an unfamiliar tool export). No dedicated lane exists yet — reconstruct via Lane 4 from the document + founder confirmation: read/transcribe what you can, hand-build inputs.json / instruments.json, and flow through --mode=validate. This is a sanctioned fallback (see Coverage & Disclosure), not an improvisation — stamp metadata.cap_base_provenance = "model_reconstructed" and apply the cap-base confirmation gate as normal.

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, amendment. The last three are non-extractable — classified and surfaced but not persisted to an instrument array; an amendment restates one clause of an existing instrument (its other terms legitimately absent), so its clause deltas surface from the receipt ambiguities rather than being forced through the note gate. term_sheet / option_plan are terms-docs: no strict field schema, not persisted to a math array — their extracted fields ride in the receipt's terms_doc. Both cases render from the receipt: write it to extraction_audit.json and pass compose_extraction_report.py --audit, which emits an "Amendments (terms modified)" section and a "Term sheet terms (as extracted)" table. Piping a term sheet / option plan through extract_instrument.py and saving the --audit receipt is mandatory; never hand-compose report_extraction_only.md. Terms docs never block on a verifier/invariant finding (those surface as per-field to-confirm markers), but a missing --source-doc or other input-integrity error still fails loud. The full pipeline saves the receipt the same way (Step 3's Lane-1 invocation always passes -o "$REVIEW_DIR/extraction_audit.json"), and compose_report.py reads it directly — no --audit flag there, since compose_extraction_report.py is the no-cap-base fork's renderer.
  • 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 instruments.json + extraction_audit.json (plus inputs.json on the freeform-emit path). NOT cap_state.json — that is cap_state.py's output at Step 4.
  • 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):

Shortened here. Read the whole file on GitHub.

Signals

GitHub stars
34
Forks
3
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
cap-table
Source
github.com/lool-ventures/founder-skills