Cap-Table Skill
SkillDocs & knowledgeUse 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.
No other account needed.
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
uvfor script execution. - Rule pack: consumes
data/cap-table-rules.jsonat 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, futurecross-document-consistencyskill
- Exports (fast-assess mode, in
cap-table-{slug}-fastassess/):fast_assess_only.json— sentinel marking that fast-assess ran (no canonical artifacts). Seereferences/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/SOMfinancial-model-review:report.json— current revenue scale + runway, to gate scenario plausibility
Skill Execution Model (READ FIRST)
See
founder-skills/references/skill-execution-model.mdfor the full inline-skill execution model (3 dispatch contexts, Mitigation 1+2, producer contract, Cowork quirks, per-symptom triage).
This skill runs inline in the main thread, not as a sub-agent — see the reference above ("Why Inline (Not Forked Sub-Agent)") for the rationale. Sub-agents are deliberately shell-free, so orchestration (producer scripts, artifact persistence) stays in the main thread.
Two dispatch contexts for the sub-agent:
-
Context A — Per-step analytical dispatch (Mitigation 1): 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 planSPREADSHEET_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.pywritesreport.md+report.json, the sub-agent Reads the stagedcoaching_payload.jsonfrom the hand-off dir (Mitigation 2) — it does NOT read the fullreport.md— composes the coaching commentary, WRITES it to theOUTPUT_PATHhand-off file, and returns a small receipt. The main thread gates the file (check_handoff.py) and inserts it via the sharedinsert_coaching.pyscript (idempotency matrix, uuid-marker replacement, run_id-parity verification — all deterministic). See the reference above for the full Context B contract.
Tolerant JSON extraction protocol (Context 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 tryjson.JSONDecoder().raw_decode(text[i:])— this is brace-aware and handles nested objects correctly (unlike regex, which truncates on the first}). - If extraction fails entirely, re-prompt the sub-agent with: "Your previous reply could not be parsed as JSON. Return ONLY the JSON object — no markdown fences, no prose preamble."
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
pagesparameter). For SAFEs/notes/term-sheets/option-plans: dispatch Context AINSTRUMENT_EXTRACTION; pipe returned JSON throughextract_instrument.py. For AoAs: dispatch Context AARTICLES_OF_ASSOCIATION_EXTRACTION(seereferences/lanes/lane-1-pdf-docx.md#dispatch-context-a--articles_of_association_extraction); pipe returned JSON throughextract_aoa.pywhich validates + merges preferred-series terms intoinputs.json.preferred_series[]. User confirmation viaAskUserQuestionbefore 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
.pdfsource). Before you rely on a cap-table read from a.pdfby the Read tool (vision), runpython3 scripts/pdf_probe.py "<path>". Text PDFs (image_only: false) read normally. Ifimage_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=gridpayload (same shape Lane 3 consumes). Paste that grid into the Lane-3SPREADSHEET_STRUCTURE_DETECTIONdispatch and run--mode=freeform-emit(the normal Lane-3 path). Setmetadata.extraction_mode = "ocr_image_pdf". A3: if the OCR grid shows a printed grand fully-diluted total (a "Total"/"Fully Diluted" row), copy it intoinputs.jsonstated_totalssocap_state.pycross-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_tableserrors): fall back to vision — setmetadata.extraction_mode = "vision_image_pdf"(socap_state.pyemitsW_VISION_EXTRACTION_LOW_CONFIDENCEand 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.pyalso reports anrtlblock; whenrtl.rtl_suspectis 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 ifrtl.rtl_reversed_likelyreverse each line before reading labels (digits inside an RTL line usually stay LTR — verify against a printed total /stated_totalsbefore math). Warning only; lane routing is unchanged.
- Try OCR first (the full-parity agent image ships
-
Tracked-changes DOCX guard (any
.docxsource). Before relying on a.docxread, runpython3 scripts/_docx_text.py "<path>" --detect. Ifhas_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 anAskUserQuestionBEFORE 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 withpython3 scripts/_docx_text.py "<path>" --extractand (a) paste that output as the document text in the Context-AINSTRUMENT_EXTRACTIONdispatch — NOT the raw Read-tool view — so the extractor andevidence_verifierread the SAME accepted-revisions view (else a correct inserted-term extraction can't be verified); and (b) setinputs.metadata.source_markup = "tracked_changes_accepted"socap_state.pyemitsW_REDLINE_DRAFTand the report persists the caveat. (_docx_textreads the accepted view stdlib-only — works in the sandbox, which omitsoffice_convert.) -
Lane 2 — Carta XLSX export. Typical: multi-sheet XLSX (Securities, Convertibles, Stakeholders).
extract_cap_table.py --mode=cartareads the sheet-name fingerprint and maps known columns → canonical schema. User confirms ambiguous mappings. Seereferences/carta-pulley-mapping.mdfor the column-mapping table. Pulley is not yet supported end-to-end (--mode=pulleyis 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 writesinstruments.json+extraction_audit.jsonONLY; always buildinputs.jsonfrom founder answers (one batchedAskUserQuestion: founders + share counts, pool authorized/issued/unallocated). When offering founder candidate options, EXCLUDE obvious investor vehicles — names containingVentures/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.pyemitsW_FOUNDER_LOOKS_LIKE_INVESTORas a backstop.) Reconciliation: if the carta receipt'ssummary.fully_dilutedis present (Carta's printed grand total — independent of the rows you rebuild), copy it intoinputs.jsonstated_totals{ "fully_diluted": <n>, "source": "carta_summary" }.cap_state.pycross-foots the rebuilt cap base against it and emitsW_FD_RECONCILE_DELTAif 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=autoconfirms the workbook is freeform (printsdetected_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 — seereferences/lanes/lane-3-freeform.md; agrid_too_largeblocker means split the workbook per-sheet or fall back to Lane 4).- The main thread pastes that JSON into the Context A
SPREADSHEET_STRUCTURE_DETECTIONdispatch prompt to identify cell semantics (block types + column roles from the closedreferences/schemas/freeform-role-map.jsonvocabulary). - Pipe the returned blocks through
extract_cap_table.py --mode=freeform-emit, which deterministically maps them to schema-validinputs.json(equity, merged into Step 2's file) +instruments.jsonand 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.mdfor the full invocation sequence.SPREADSHEET_STRUCTURE_DETECTIONstated-total requirement: alongsideblocks, the structure sub-agent must also report a top-levelstated_totalfield = 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_DELTAon 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 throughextract_cap_table.py --mode=validatefor 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", anambiguitiesentry,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_datethe 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 oneAskUserQuestionaway, 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 — stampmetadata.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; anamendmentrestates one clause of an existing instrument (its other terms legitimately absent), so its clause deltas surface from the receiptambiguitiesrather than being forced through the note gate.term_sheet/option_planare terms-docs: no strict field schema, not persisted to a math array — their extractedfieldsride in the receipt'sterms_doc. Both cases render from the receipt: write it toextraction_audit.jsonand passcompose_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 throughextract_instrument.pyand saving the--auditreceipt is mandatory; never hand-composereport_extraction_only.md. Terms docs never block on a verifier/invariant finding (those surface as per-field to-confirm markers), but a missing--source-docor 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"), andcompose_report.pyreads it directly — no--auditflag there, sincecompose_extraction_report.pyis the no-cap-base fork's renderer.extract_aoa.py— Validates Lane-1 sub-agent output for Articles of Association (separate sub-contextARTICLES_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--inputsflag, merges validated preferred_series block intoinputs.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.griddumps all sheets as a JSON cell-value grid for Lane-3SPREADSHEET_STRUCTURE_DETECTIONdispatch;freeform-emitdeterministically maps the detected blocks (viafreeform_mapper.py+ thefreeform-role-map.jsoncontract) into schema-validinputs.json+instruments.json, with founder-confirmation blockers for fields freeform can't supply. Emitsinstruments.json+extraction_audit.json(plusinputs.jsonon the freeform-emit path). NOTcap_state.json— that iscap_state.py's output at Step 4.cap_state.py— Readsinputs.json+instruments.json; computes as-converted totals; writescap_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. Readsinputs.json+instruments.json; emitsrequired_primitives,covered(bool),uncovered_parts, androute.scenario_requestsfor 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_mathwrites the gating block (per-rule, per-instance status + scope + overlays) BEFORE math runs;--phase=post_mathcomposes 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 packoption_pool.pre_money_topup). Usestarget_basisdenominator.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 fromrule_audit.jsoninto a standalone counsel-handoff packet.compose_report.py— Assembles all artifacts intoreport.md+report.json(with embeddedcoaching_payloadblock). Cross-artifact validation; emits per-uuid coaching insertion marker.visualize.py— Generatesreport.html(self-contained, inline SVG donut + tables; no CDN). The interactiveexplore.pyis the one that uses vendored Chart.js.explore.py— Generatesexplorer.html(polished interactive scenario tool; demo/video-friendly).sweep.py— Generates the optionalsweep.json: a pre-money parametric sweep (K real solver frames,new_moneyheld fixed) that powers the explorer's "drag pre-money" slider. No new math — re-runs the priced-round path across apre_moneyrange. Slider snaps to discrete frames, so every value shown is real.quick_assess.py— Fast-assess directional review (Step 5-fast); writes thefast_assess_only.jsonsentinel +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) returnlookup_status: "escalate"rather than echoing a non-constant field. No solver, no artifact.concise_report.py— Concise mode (Step 5-concise): rendersscenarios.json(the solver'scomputed_outputs) + optionalrule_audit.jsonflags into a short citedreport_concise.md, skippingvisualize/explore/counsel_packet/the fullcompose_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.pyinvokes 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