Skip to content

general-counsel-advisor

by Alireza Rezvani · MIT

General Counsel advisory for startups: contract review (MSA, SaaS, NDA, DPA, employment), IP strategy, term sheet decoding, and regulatory landscape mapping. Use when reviewing any contract or term sheet, deciding when to engage outside counsel, defining IP strategy, evaluating regulatory exposure (HIPAA, GDPR, FDA, fintech), or when user mentions general counsel, GC, legal review, contract risk, term sheet, IP assignment, or regulatory exposure. NOT a substitute for licensed counsel — surfaces questions to bring to qualified attorneys.

Runs commands2 scripts1 finding

Labels from its files: runs commands. 6 files, read Oct 23, 2026. Nothing was run.

npx skills add https://github.com/alirezarezvani/claude-skills --skill general-counsel-advisor

What it lets an agent do

Each label, and the line in the skill's files that set it.

  • Runs commands

    Allows a shell or ships scripts the agent runs.

    • It ships 2 scripts the agent can run.

Findings (1)

Matches of fixed checks, with the text they matched.

Not a safety rating, and no finding is not a safety guarantee. These are fixed checks of the files as read on Oct 23, 2026. Nothing was run. How skills are checked.

SKILL.md

As the publisher wrote it. Shown as text, never rendered.

Publisher text
---
name: "general-counsel-advisor"
description: "General Counsel advisory for startups: contract review (MSA, SaaS, NDA, DPA, employment), IP strategy, term sheet decoding, and regulatory landscape mapping. Use when reviewing any contract or term sheet, deciding when to engage outside counsel, defining IP strategy, evaluating regulatory exposure (HIPAA, GDPR, FDA, fintech), or when user mentions general counsel, GC, legal review, contract risk, term sheet, IP assignment, or regulatory exposure. NOT a substitute for licensed counsel — surfaces questions to bring to qualified attorneys."
license: MIT
metadata:
  version: 1.0.0
  author: Alireza Rezvani
  category: c-level
  domain: general-counsel-leadership
  updated: 2026-05-12
  python-tools: contract_risk_scanner.py, term_sheet_analyzer.py
  frameworks: contract-review, ip-strategy, term-sheet-decoding, regulatory-mapping
---

# General Counsel Advisor

Strategic legal frameworks for startup General Counsels and founders without one. Contract risk, IP strategy, term sheet decoding, regulatory landscape.

This is **not legal advice**. It surfaces the right questions to bring to qualified outside counsel and catches the obvious traps before they reach a signature. Treat every output as a starting point for a conversation with a licensed attorney, not as a substitute for one.

## Keywords

general counsel, GC, legal review, contract review, MSA, SaaS agreement, NDA, DPA, employment agreement, contractor agreement, IP assignment, invention assignment, open source license, OSS compliance, term sheet, liquidation preference, anti-dilution, option pool, vesting, acceleration, drag-along, pro-rata, board composition, regulatory, HIPAA, GDPR, CCPA, FDA, MDR, fintech, BSA/AML, money transmitter, AI Act, indemnity, liability cap, force majeure, auto-renewal, choice of law, venue, non-compete, non-solicit

## Quick Start

```bash
# Scan a contract for risky clauses (uses bundled sample if no path given)
python scripts/contract_risk_scanner.py
python scripts/contract_risk_scanner.py path/to/contract.txt

# Analyze a term sheet for founder-friendliness
python scripts/term_sheet_analyzer.py
python scripts/term_sheet_analyzer.py path/to/term_sheet.json
```

## Key Questions (ask these first)

- **Who owns the IP being created or shared?** (Founders forget that contractors don't auto-assign IP without a written clause.)
- **What's the liability cap, and what's carved out?** (Standard: 12 months of fees, with carve-outs for IP infringement, data breach, willful misconduct.)
- **Is there a DPA in place if any personal data flows?** (GDPR, CCPA, state laws — non-negotiable if EU/CA data is touched.)
- **What's the termination right, notice period, and auto-renewal trap?** (5-year auto-renew with 60-day notice is a common founder mistake.)
- **Does this contract or product launch trigger a new regulatory regime?** (Healthcare → HIPAA. Fintech → BSA/AML. Medical device → FDA/MDR.)
- **For term sheets: liquidation preference, pre-money option pool, anti-dilution flavor?** (Three places where 5% of founder economics can quietly disappear.)

## Core Responsibilities

### 1. Contract Review

Standard contracts a startup signs in its first 5 years:

- **Vendor MSA** — Master Service Agreement (cloud, tooling, services)
- **Customer SaaS Agreement** — your standard customer paper + customer redlines
- **NDA** — mutual + one-way, with carve-outs for residuals + independent development
- **DPA** — Data Processing Agreement (required when personal data flows)
- **Employment Agreement** — offer letter, IP assignment, non-compete (where enforceable), arbitration
- **Contractor / 1099 Agreement** — IP assignment is critical; misclassification risk
- **Equity Agreements** — option grants, RSU agreements, advisor grants (FAST template, YC SAFE for advisors)

**Run** `contract_risk_scanner.py` on the text. It flags the 12 most common founder-killer clauses.

### 2. IP Strategy

- **Invention assignment** — every employee and contractor signs one. No exceptions.
- **Open source license compliance** — track every OSS dependency's license; AGPL and GPL trigger copyleft obligations.
- **Trade secrets** — define what's protected and how (clean room dev, access controls, NDAs).
- **Patents** — file provisional within 12 months of disclosure; PCT for international.
- **Trademarks** — register the word mark first, design mark second; clear before launch.
- **Copyright** — automatic on creation, but register for statutory damages eligibility.

See `references/ip_and_regulatory.md`.

### 3. Term Sheet Decoding

When a term sheet arrives, the difference between a founder-friendly and founder-hostile sheet often hides in three clauses:

- **Liquidation preference** — 1x non-participating is standard; 1x participating or 2x is hostile
- **Pre-money vs post-money option pool** — pre-money pool dilutes founders; post-money dilutes everyone proportionally
- **Anti-dilution** — broad-based weighted average is standard; full ratchet is hostile

**Run** `term_sheet_analyzer.py` to get a 0-100 founder-friendliness score with flags.

### 4. Regulatory Landscape

When to engage outside counsel **before** committing:

| Trigger | Regime | First Step |
|---|---|---|
| Healthcare data | HIPAA, HITECH, state breach laws | Specialist health-tech counsel |
| Cardholder data | PCI DSS (industry standard, not law, but contractually required) | QSA + counsel |
| Money movement | BSA/AML, state money-transmitter (50-state patchwork) | Fintech specialist |
| Medical device claims | FDA 510(k) / De Novo / PMA, MDR (EU), ISO 13485 | Medical-device specialist |
| EU residents' personal data | GDPR + EU AI Act if AI is deployed | EU privacy counsel |
| California residents | CCPA / CPRA | Privacy generalist |
| Securities (tokens, equity crowdfunding) | SEC rules (Reg D, Reg A+, Reg CF) | Securities counsel |
| Defense / aerospace customers | ITAR, EAR, DFARS, CMMC | Export-control counsel |
| AI in EU | EU AI Act (risk-tiered) | EU privacy + product counsel |
| AI for hiring (NYC, CO, IL) | Local bias-audit laws | Employment counsel |

See `references/ip_and_regulatory.md` for sequencing.

## Workflows

### Workflow 1: Contract Review
1. Save the contract as plain text
2. Run `contract_risk_scanner.py path/to/contract.txt`
3. For each HIGH risk finding, draft a counter-proposal
4. Bring the redline + counter-proposals to outside counsel
5. Log the decision via `/c-level-agents:decide`

### Workflow 2: Term Sheet Response
1. Save the term sheet as a JSON file matching the schema in `term_sheet_analyzer.py --help`
2. Run `python scripts/term_sheet_analyzer.py path/to/term_sheet.json`
3. Review the founder-friendliness score and per-clause flags
4. Negotiate the worst 3 clauses (don't try to win all 20)
5. Always have a securities/venture attorney review before signing
6. Log via `/c-level-agents:decide` with `/c-level-agents:freeze 30` to prevent regret-driven re-opening

### Workflow 3: IP Hygiene Audit
1. Confirm every employee and contractor (past 12 months) signed invention assignment
2. Run an OSS license inventory (`pip-licenses`, `license-checker` for npm)
3. Map AGPL/GPL dependencies and confirm compliance (or remove)
4. File provisional patents on novel inventions (12-month deadline from disclosure)
5. Register word-mark trademarks for the product name

### Workflow 4: Regulatory Trigger Assessment
1. List planned product features for the next 12 months
2. Map each feature to the trigger table in this document
3. For any HIPAA / FDA / fintech trigger, engage a specialist counsel **before** building
4. Document the regulatory roadmap and budget alongside the product roadmap
5. Pair with `cs-ciso-advisor` for ISO 27001 / SOC 2 sequencing

## Output Standard (when invoked via `/c-level-agents:gc-review`)

```
**Bottom Line:** [sign / negotiate / do not sign]
**The Risks:** [3 highest-severity issues]
**Counter-Proposals:** [specific language]
**Outside Counsel Action Items:** [what to bring to the attorney]
**Your Decision:** [the call only the founder can make]
```

## Adjacent Skills

- `c-level-advisor/skills/ciso-advisor/` — Compliance overlap (SOC 2, ISO 27001, HIPAA technical safeguards)
- `c-level-advisor/skills/cfo-advisor/` — Term sheet → dilution math
- `c-level-advisor/skills/ma-playbook/` — Acquisition agreements, integration playbooks
- `ra-qm-team/` — ISO 13485, MDR, FDA 510(k), GDPR execution
- `c-level-agents/skills/gc-review/SKILL.md` — `/c-level-agents:gc-review` slash command

## References

- [contracts_playbook.md](references/contracts_playbook.md) — Standard contracts, clause checklist, common founder traps
- [ip_and_regulatory.md](references/ip_and_regulatory.md) — IP protection + regulatory landscape mapping
- [term_sheet_decoder.md](references/term_sheet_decoder.md) — Term sheet glossary + founder-friendly defaults + pushback strategies

---

**Version:** 1.0.0
**Status:** Production Ready
**Disclaimer:** Not legal advice. Always engage qualified counsel for binding decisions.

Scripts (2)

Read as text. anlyon.dev never runs them.

scripts/contract_risk_scanner.pyShow
#!/usr/bin/env python3
"""contract_risk_scanner.py — Scan a contract for founder-killer clauses.

Stdlib-only. Outputs human-readable or JSON. Detects 12 common risk patterns:
  1. Unilateral termination favoring the counterparty
  2. Auto-renewal with long notice (60+ days)
  3. Uncapped liability or exclusion of standard caps
  4. Broad indemnification flowing one direction
  5. Non-mutual confidentiality
  6. Missing or vague IP ownership clauses
  7. Aggressive non-compete / non-solicit
  8. Choice of law/venue in counterparty's home jurisdiction (one-sided)
  9. Force majeure favoring only the counterparty
 10. Missing DPA reference when personal data flows
 11. Most-favored-nation pricing clauses
 12. Audit rights without reciprocity

NOT legal advice. Use this to triage; bring findings to qualified counsel.

Usage:
    python contract_risk_scanner.py                       # uses embedded sample
    python contract_risk_scanner.py path/to/contract.txt
    python contract_risk_scanner.py contract.txt --output json
    python contract_risk_scanner.py --help
"""

import argparse
import json
import re
import sys
from dataclasses import dataclass, asdict
from typing import List


SAMPLE_CONTRACT = """\
MASTER SERVICES AGREEMENT

This Agreement shall automatically renew for successive one (1) year terms
unless either party provides ninety (90) days written notice of non-renewal.

LIMITATION OF LIABILITY. In no event shall Provider's aggregate liability
arising out of this Agreement exceed the fees paid by Customer in the
twelve (12) months preceding the claim. Notwithstanding the foregoing,
Customer's indemnification obligations under Section 8 shall be uncapped.

INDEMNIFICATION. Customer shall defend, indemnify and hold harmless
Provider, its affiliates, officers, directors and employees from and against
any and all claims, damages, losses and expenses arising out of or relating
to Customer's use of the Services.

INTELLECTUAL PROPERTY. The parties agree that intellectual property created
during the engagement shall belong to the party who develops it.

NON-COMPETE. For a period of three (3) years following termination, Customer
shall not engage with any competitor of Provider in any capacity, in any
geography.

GOVERNING LAW. This Agreement shall be governed by the laws of Delaware,
and any disputes shall be resolved exclusively in the state and federal
courts located in Wilmington, Delaware.

FORCE MAJEURE. Provider shall not be liable for any failure to perform due
to causes beyond its reasonable control.
"""


@dataclass
class Finding:
    rule_id: str
    severity: str           # CRITICAL | HIGH | MEDIUM | LOW
    title: str
    excerpt: str
    why_it_matters: str
    suggested_redline: str


RULES = [
    {
        "id": "AUTO_RENEW_LONG_NOTICE",
        "severity": "HIGH",
        "title": "Auto-renewal with long notice period",
        "pattern": re.compile(
            r"automatically renew.{0,200}?(\d+|sixty|ninety|one hundred|180)\s*(\(\d+\))?\s*day",
            re.IGNORECASE | re.DOTALL,
        ),
        "why_it_matters": (
            "Auto-renewal with >30 day notice is a classic vendor trap: founders forget the "
            "deadline and get locked into another full term. Especially painful on multi-year contracts."
        ),
        "redline": (
            "Counter: '...unless either party provides thirty (30) days written notice of non-renewal' "
            "OR remove auto-renewal entirely and require affirmative re-signature."
        ),
    },
    {
        "id": "UNCAPPED_CUSTOMER_INDEMNITY",
        "severity": "CRITICAL",
        "title": "Customer indemnity carved out from liability cap (uncapped)",
        "pattern": re.compile(
            r"(customer'?s|your)\s+indemnification.{0,200}?(uncapped|shall be uncapped|excluded from)",
            re.IGNORECASE | re.DOTALL,
        ),
        "why_it_matters": (
            "Uncapped customer indemnity means a single bad claim can exceed all fees ever paid. "
            "Standard practice: mutual indemnity, both sides capped at fees, with narrow carve-outs "
            "(IP infringement, data breach, gross negligence)."
        ),
        "redline": (
            "Counter: cap customer indemnity at 12 months of fees, mutual indemnity, carve-outs only "
            "for willful misconduct and breach of confidentiality."
        ),
    },
    {
        "id": "ONE_SIDED_INDEMNITY",
        "severity": "HIGH",
        "title": "Indemnification flows in one direction only",
        "pattern": re.compile(
            r"(customer|client)\s+shall\s+(defend|indemnify).{0,500}?(provider|company|vendor)",
            re.IGNORECASE | re.DOTALL,
        ),
        "why_it_matters": (
            "One-sided indemnity means you take on risk for the counterparty's actions without reciprocity. "
            "A balanced contract has mutual indemnification with mirrored carve-outs."
        ),
        "redline": (
            "Counter: 'Each party shall defend, indemnify and hold harmless the other party...' with "
            "mirrored scope and equal caps."
        ),
    },
    {
        "id": "VAGUE_IP",
        "severity": "CRITICAL",
        "title": "Vague IP ownership clause",
        "pattern": re.compile(
            r"intellectual property.{0,200}?(belong to the party who develops it|jointly owned|to be determined|as agreed)",
            re.IGNORECASE | re.DOTALL,
        ),
        "why_it_matters": (
            "Vague IP language is the #1 source of post-engagement disputes. Joint ownership often means "
            "neither party can license freely without the other's consent. 'As agreed' is unenforceable."
        ),
        "redline": (
            "Counter: 'All work product, deliverables, and derivative works created under this Agreement "
            "shall be the sole and exclusive property of Customer. Provider hereby assigns all right, title "
            "and interest...' Or explicitly carve out Provider's pre-existing IP and tools with a license back."
        ),
    },
    {
        "id": "AGGRESSIVE_NONCOMPETE",
        "severity": "HIGH",
        "title": "Aggressive non-compete (long duration or broad geography)",
        "pattern": re.compile(
            r"non.compete.{0,300}?(two|three|four|five|2|3|4|5)\s*\(?\d*\)?\s*year",
            re.IGNORECASE | re.DOTALL,
        ),
        "why_it_matters": (
            "Non-competes >12 months or with unbounded geography are often unenforceable (especially in "
            "California, and increasingly federally) but create chilling effects. They also signal the "
            "counterparty's overall negotiation posture."
        ),
        "redline": (
            "Counter: maximum 12 months, specific competitor list (not 'any competitor'), specific "
            "geography. For California-resident counterparties, remove entirely (California labor code "
            "voids most non-competes)."
        ),
    },
    {
        "id": "ONE_SIDED_VENUE",
        "severity": "MEDIUM",
        "title": "Choice of law/venue exclusively in counterparty jurisdiction",
        "pattern": re.compile(
            r"(exclusively in|exclusive jurisdiction).{0,300}?(courts? located in|state and federal courts of)",
            re.IGNORECASE | re.DOTALL,
        ),
        "why_it_matters": (
            "Exclusive venue in counterparty's jurisdiction means you bear travel cost and out-of-state "
            "counsel cost for any dispute. For startups this can effectively prevent enforcement."
        ),
        "redline": (
            "Counter: neutral venue (Delaware is common), or 'venue in the jurisdiction of the defendant' "
            "(forces plaintiff to travel), or arbitration in a neutral location with AAA/JAMS rules."
        ),
    },
    {
        "id": "ONE_SIDED_FORCE_MAJEURE",
        "severity": "MEDIUM",
        "title": "Force majeure clause favors one party",
        "pattern": re.compile(
            r"(provider|company|vendor)\s+shall not be liable.{0,200}?(force majeure|causes beyond)",
            re.IGNORECASE | re.DOTALL,
        ),
        "why_it_matters": (
            "If only the vendor gets force-majeure protection, you pay full price during a pandemic / "
            "outage / supply chain disruption but receive nothing. Mutual force majeure is standard."
        ),
        "redline": (
            "Counter: 'Neither party shall be liable...' with explicit list of qualifying events "
            "(pandemic, war, natural disaster, government action) and a termination right after 30 days."
        ),
    },
    {
        "id": "MISSING_DPA",
        "severity": "HIGH",
        "title": "Personal data appears to flow but no DPA referenced",
        "pattern": re.compile(
            r"(personal data|personally identifiable|user data|customer data|PII)(?!.{0,500}(DPA|data processing agreement|GDPR))",
            re.IGNORECASE | re.DOTALL,
        ),
        "why_it_matters": (
            "If personal data of EU residents (or California residents) flows, a DPA is legally required. "
            "Missing DPA = GDPR Article 28 violation, potential 4%-of-revenue fine, contract unenforceable "
            "with EU customers."
        ),
        "redline": (
            "Counter: 'The parties shall execute a Data Processing Agreement substantially in the form "
            "of Exhibit X prior to any processing of Personal Data.' Use IAPP or Vendor-friendly DPA template."
        ),
    },
    {
        "id": "MOST_FAVORED_NATION",
        "severity": "MEDIUM",
        "title": "Most-favored-nation (MFN) pricing clause",
        "pattern": re.compile(
            r"(most.favored.nation|MFN|best price|lowest price).{0,200}?(offered to|charged to)",
            re.IGNORECASE | re.DOTALL,
        ),
        "why_it_matters": (
            "MFN clauses prevent you from offering volume discounts or strategic pricing to anyone else. "
            "If you sign with one customer, every future customer can demand the same price."
        ),
        "redline": (
            "Counter: remove the MFN entirely. If kept, narrow to 'similarly situated customers, same "
            "tier and volume, excluding strategic / launch / migration discounts.'"
        ),
    },
    {
        "id": "ONE_SIDED_AUDIT",
        "severity": "MEDIUM",
        "title": "Audit rights without reciprocity",
        "pattern": re.compile(
            r"(customer|client).{0,100}?right to audit",
            re.IGNORECASE | re.DOTALL,
        ),
        "why_it_matters": (
            "One-sided audit rights mean the counterparty can demand records on demand, often at your "
            "expense. Reciprocity is standard for B2B agreements."
        ),
        "redline": (
            "Counter: mutual audit rights, max once per year, at requesting party's expense, with "
            "30-day notice, during business hours, narrowed to specific compliance categories."
        ),
    },
    {
        "id": "BROAD_NON_SOLICIT",
        "severity": "MEDIUM",
        "title": "Broad non-solicit (employees AND customers, long duration)",
        "pattern": re.compile(
            r"non.solicit.{0,300}?(employees? and customers?|customers? and employees?)",
            re.IGNORECASE | re.DOTALL,
        ),
        "why_it_matters": (
            "Combined employee + customer non-solicits, especially with long duration, can severely "
            "limit hiring and business development. Many states limit enforceability."
        ),
        "redline": (
            "Counter: split into employee-only (12 months max) and customer-only (12 months max) clauses, "
            "with carve-outs for general advertising / open job postings and for customers who initiate "
            "contact independently."
        ),
    },
    {
        "id": "PERPETUAL_LICENSE_BACK",
        "severity": "HIGH",
        "title": "Perpetual license-back to counterparty of your data or work",
        "pattern": re.compile(
            r"perpetual.{0,100}?(license|right).{0,300}?(customer data|user data|work product|deliverables)",
            re.IGNORECASE | re.DOTALL,
        ),
        "why_it_matters": (
            "A perpetual license-back lets the counterparty use your data or deliverables forever, even "
            "after termination. This is acceptable for usage analytics, NOT for customer data or core IP."
        ),
        "redline": (
            "Counter: time-limited license (for the term of the agreement only), specific purpose "
            "(service delivery only, not training AI models, not sharing with third parties), and "
            "post-termination return-or-destroy obligation."
        ),
    },
]


def scan(text: str) -> List[Finding]:
    findings: List[Finding] = []
    for rule in RULES:
        for match in rule["pattern"].finditer(text):
            excerpt = match.group(0).strip()
            # truncate long excerpts
            if len(excerpt) > 300:
                excerpt = excerpt[:297] + "..."
            findings.append(Finding(
                rule_id=rule["id"],
                severity=rule["severity"],
                title=rule["title"],
                excerpt=excerpt,
                why_it_matters=rule["why_it_matters"],
                suggested_redline=rule["redline"],
            ))
    # rank by severity then rule order
    severity_order = {"CRITICAL": 0, "HIGH": 1, "MEDIUM": 2, "LOW": 3}
    findings.sort(key=lambda f: (severity_order.get(f.severity, 9), f.rule_id))
    return findings


def render_text(findings: List[Finding], source: str) -> str:
    lines = []
    lines.append("=" * 72)
    lines.append("CONTRACT RISK SCAN")
    lines.append(f"Source: {source}")
    lines.append(f"Findings: {len(findings)}")
    lines.append("=" * 72)
    lines.append("")
    if not findings:
        lines.append("No risk patterns matched. (Absence of findings does not mean the contract is safe;")
        lines.append("it means the 12 common patterns this scanner checks did not trigger.)")
        lines.append("")
        lines.append("Always engage qualified counsel before signing.")
        return "\n".join(lines)

    severity_counts = {}
    for f in findings:
        severity_counts[f.severity] = severity_counts.get(f.severity, 0) + 1
    severity_summary = "  ".join(
        f"{sev}: {severity_counts.get(sev, 0)}"
        for sev in ("CRITICAL", "HIGH", "MEDIUM", "LOW")
        if severity_counts.get(sev, 0) > 0
    )
    lines.append(f"Severity: {severity_summary}")
    lines.append("")

    for i, f in enumerate(findings, 1):
        lines.append(f"[{i}] {f.severity} — {f.title}")
        lines.append(f"    Rule: {f.rule_id}")
        lines.append(f"    Excerpt: \"{f.excerpt}\"")
        lines.append("")
        lines.append(f"    Why it matters:")
        for line in _wrap(f.why_it_matters, 4):
            lines.append(line)
        lines.append("")
        lines.append(f"    Suggested redline:")
        for line in _wrap(f.suggested_redline, 4):
            lines.append(line)
        lines.append("")
        lines.append("-" * 72)

    lines.append("")
    lines.append("REMINDER: This scanner triages obvious traps. Always bring redlines to qualified counsel.")
    return "\n".join(lines)


def _wrap(text: str, indent: int, width: int = 68) -> List[str]:
    import textwrap
    return textwrap.wrap(text, width=width, initial_indent=" " * indent, subsequent_indent=" " * indent) or [" " * indent + text]


def main() -> int:
    parser = argparse.ArgumentParser(
        description="Scan a contract for the 12 most common founder-killer clauses.",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog=__doc__,
    )
    parser.add_argument("path", nargs="?", help="Path to contract text file (uses embedded sample if omitted)")
    parser.add_argument("--output", choices=("text", "json"), default="text", help="Output format")
    args = parser.parse_args()

    if args.path:
        try:
            with open(args.path, "r", encoding="utf-8") as f:
                text = f.read()
            source = args.path
        except (IOError, OSError) as e:
            print(f"error: could not read {args.path}: {e}", file=sys.stderr)
            return 1
    else:
        text = SAMPLE_CONTRACT
        source = "<embedded sample MSA>"

    findings = scan(text)

    if args.output == "json":
        payload = {
            "source": source,
            "findings_count": len(findings),
            "findings": [asdict(f) for f in findings],
        }
        print(json.dumps(payload, indent=2))
    else:
        print(render_text(findings, source))

    return 0


if __name__ == "__main__":
    sys.exit(main())
scripts/term_sheet_analyzer.pyShow
#!/usr/bin/env python3
"""term_sheet_analyzer.py — Score a term sheet on founder-friendliness.

Stdlib-only. Computes a 0-100 score across 12 dimensions and flags
hostile clauses. Outputs human-readable or JSON.

NOT legal advice — surfaces questions for venture / securities counsel.

Input schema (JSON):
{
  "round": "Series A",
  "pre_money": 30000000,
  "raise_amount": 8000000,
  "liquidation_preference": {
    "multiple": 1.0,
    "participating": false,
    "cap": null
  },
  "anti_dilution": "broad_based_weighted_average",  // | "narrow_based_weighted_average" | "full_ratchet" | "none"
  "option_pool": {
    "size_pct": 12.0,
    "pre_money": true
  },
  "board_composition": {
    "investor_seats": 1,
    "founder_seats": 2,
    "independent_seats": 1
  },
  "vesting": {
    "standard_years": 4,
    "cliff_months": 12,
    "single_trigger_acceleration": false,
    "double_trigger_acceleration": true
  },
  "pro_rata": true,
  "drag_along": {
    "exists": true,
    "founder_consent_required": true
  },
  "protective_provisions": "standard",  // | "standard" | "aggressive"
  "information_rights": "standard",     // | "standard" | "aggressive"
  "dividends": "none"                   // | "none" | "non_cumulative_when_declared" | "cumulative"
}

Usage:
    python term_sheet_analyzer.py                            # uses embedded sample
    python term_sheet_analyzer.py path/to/term_sheet.json
    python term_sheet_analyzer.py term_sheet.json --output json
    python term_sheet_analyzer.py --help
"""

import argparse
import json
import sys
from typing import Any, Dict, List, Tuple


SAMPLE = {
    "round": "Series A",
    "pre_money": 30_000_000,
    "raise_amount": 8_000_000,
    "liquidation_preference": {"multiple": 1.0, "participating": False, "cap": None},
    "anti_dilution": "broad_based_weighted_average",
    "option_pool": {"size_pct": 12.0, "pre_money": True},
    "board_composition": {"investor_seats": 1, "founder_seats": 2, "independent_seats": 1},
    "vesting": {
        "standard_years": 4,
        "cliff_months": 12,
        "single_trigger_acceleration": False,
        "double_trigger_acceleration": True,
    },
    "pro_rata": True,
    "drag_along": {"exists": True, "founder_consent_required": True},
    "protective_provisions": "standard",
    "information_rights": "standard",
    "dividends": "none",
}


def score(ts: Dict[str, Any]) -> Tuple[int, List[Dict[str, Any]]]:
    """Returns (total_score_0_to_100, list_of_findings).

    Each dimension is scored 0-100, then averaged. Findings list contains
    per-clause analysis with severity.
    """
    findings: List[Dict[str, Any]] = []
    scores: List[int] = []

    # --- 1. Liquidation Preference (high signal) ---
    lp = ts.get("liquidation_preference", {})
    lp_mult = lp.get("multiple", 1.0)
    lp_part = lp.get("participating", False)
    lp_cap = lp.get("cap")
    if lp_mult == 1.0 and not lp_part:
        lp_score = 100
        findings.append(_ok("liquidation_preference", "1x non-participating — founder-friendly standard."))
    elif lp_mult == 1.0 and lp_part and lp_cap and lp_cap <= 3:
        lp_score = 55
        findings.append(_warn("liquidation_preference",
            f"1x participating with {lp_cap}x cap. Investor double-dips up to cap. "
            "Push for non-participating; if accepted, accept cap < 3x."))
    elif lp_mult == 1.0 and lp_part and not lp_cap:
        lp_score = 25
        findings.append(_crit("liquidation_preference",
            "1x PARTICIPATING UNCAPPED. Investor gets their money back AND a pro-rata share of remaining proceeds, "
            "forever. Hostile. Push to non-participating or at minimum cap at 2x."))
    elif lp_mult > 1.0:
        lp_score = 10
        findings.append(_crit("liquidation_preference",
            f"{lp_mult}x preference. Investor gets {lp_mult}x their money back before founders see a dollar. "
            "Hostile; only acceptable in distressed rounds."))
    else:
        lp_score = 80
        findings.append(_ok("liquidation_preference", f"{lp_mult}x configuration acceptable."))
    scores.append(lp_score)

    # --- 2. Anti-Dilution ---
    ad = ts.get("anti_dilution", "broad_based_weighted_average")
    if ad == "broad_based_weighted_average":
        ad_score = 100
        findings.append(_ok("anti_dilution", "Broad-based weighted average — founder-friendly standard."))
    elif ad == "narrow_based_weighted_average":
        ad_score = 70
        findings.append(_warn("anti_dilution",
            "Narrow-based weighted average. More dilutive to founders than broad-based in a down round. "
            "Push to broad-based."))
    elif ad == "full_ratchet":
        ad_score = 10
        findings.append(_crit("anti_dilution",
            "FULL RATCHET. In a down round, investor's price is reset to the new round price entirely, "
            "massively diluting founders. Hostile; reject."))
    elif ad == "none":
        ad_score = 100
        findings.append(_ok("anti_dilution", "No anti-dilution provision. Unusual but founder-friendly."))
    else:
        ad_score = 50
        findings.append(_warn("anti_dilution", f"Unrecognized anti-dilution type: {ad}. Verify with counsel."))
    scores.append(ad_score)

    # --- 3. Option Pool (pre-money vs post-money) ---
    op = ts.get("option_pool", {})
    op_pre = op.get("pre_money", True)
    op_size = op.get("size_pct", 10.0)
    if not op_pre:
        op_score = 100
        findings.append(_ok("option_pool",
            f"Pool of {op_size}% sits post-money — dilutes all shareholders proportionally."))
    elif op_pre and op_size <= 10.0:
        op_score = 70
        findings.append(_warn("option_pool",
            f"Pool of {op_size}% pre-money — comes out of founders' shares. Reasonable size, but consider "
            "negotiating post-money or sharing the pool top-up across the round."))
    elif op_pre and op_size > 10.0:
        op_score = 30
        findings.append(_crit("option_pool",
            f"Pool of {op_size}% PRE-MONEY. This is the 'option pool shuffle' — typically reduces pre-money "
            f"by ~{op_size}%, diluting founders silently. Negotiate hard: justify the size with a hiring plan "
            "or push for post-money."))
    else:
        op_score = 60
        findings.append(_warn("option_pool", "Option pool structure unclear; verify."))
    scores.append(op_score)

    # --- 4. Board Composition ---
    bc = ts.get("board_composition", {})
    inv = bc.get("investor_seats", 0)
    fnd = bc.get("founder_seats", 0)
    ind = bc.get("independent_seats", 0)
    total = inv + fnd + ind
    if total == 0:
        bc_score = 50
        findings.append(_warn("board_composition", "Board composition unspecified."))
    elif fnd > inv and ind >= 1:
        bc_score = 100
        findings.append(_ok("board_composition",
            f"{fnd} founder / {inv} investor / {ind} independent — founder-friendly; founders retain control "
            "with independent tie-breaker."))
    elif fnd == inv and ind >= 1:
        bc_score = 75
        findings.append(_ok("board_composition",
            f"{fnd} founder / {inv} investor / {ind} independent — balanced, independent is critical."))
    elif inv > fnd:
        bc_score = 30
        findings.append(_crit("board_composition",
            f"{fnd} founder / {inv} investor / {ind} independent — investors control the board at Series A. "
            "This is unusually early; investor control typically arrives at Series B or later."))
    else:
        bc_score = 50
        findings.append(_warn("board_composition", f"Composition: {fnd}F/{inv}I/{ind}Ind — verify with counsel."))
    scores.append(bc_score)

    # --- 5. Vesting & Acceleration ---
    vest = ts.get("vesting", {})
    years = vest.get("standard_years", 4)
    cliff = vest.get("cliff_months", 12)
    single = vest.get("single_trigger_acceleration", False)
    double = vest.get("double_trigger_acceleration", False)
    if years == 4 and cliff == 12 and double and not single:
        vest_score = 100
        findings.append(_ok("vesting",
            "4yr/1yr cliff with double-trigger acceleration — founder-friendly standard. "
            "Single-trigger is rare and not recommended by counsel."))
    elif years == 4 and cliff == 12 and not double:
        vest_score = 60
        findings.append(_warn("vesting",
            "4yr/1yr cliff WITHOUT acceleration. Push for double-trigger (change of control + termination "
            "without cause) to protect founder upside in acquisition scenarios."))
    elif years > 4:
        vest_score = 20
        findings.append(_crit("vesting",
            f"{years}-year vesting. Non-standard; reject. 4 years is industry norm."))
    else:
        vest_score = 70
        findings.append(_warn("vesting", f"{years}yr/{cliff}mo cliff — verify acceleration with counsel."))
    scores.append(vest_score)

    # --- 6. Pro-Rata Rights ---
    if ts.get("pro_rata", True):
        pr_score = 100
        findings.append(_ok("pro_rata", "Pro-rata rights — standard for the lead and major investors."))
    else:
        pr_score = 60
        findings.append(_warn("pro_rata",
            "No pro-rata rights. Unusual; if investor is offering this, ask why (signals weak conviction "
            "or competitive pressure). Pro-rata is generally fine for founders to grant."))
    scores.append(pr_score)

    # --- 7. Drag-Along ---
    drag = ts.get("drag_along", {})
    if drag.get("exists") and drag.get("founder_consent_required"):
        drag_score = 100
        findings.append(_ok("drag_along",
            "Drag-along exists but requires founder consent — balanced."))
    elif drag.get("exists") and not drag.get("founder_consent_required"):
        drag_score = 40
        findings.append(_crit("drag_along",
            "Drag-along WITHOUT founder consent. Investors can force a sale over founder objection. "
            "Push for founder consent OR a minimum price threshold (e.g., 3x preference) to trigger drag."))
    else:
        drag_score = 80
        findings.append(_ok("drag_along", "No drag-along — neutral; common at early stages."))
    scores.append(drag_score)

    # --- 8. Protective Provisions ---
    pp = ts.get("protective_provisions", "standard")
    if pp == "standard":
        pp_score = 100
        findings.append(_ok("protective_provisions",
            "Standard protective provisions (NVCA model) — acceptable."))
    elif pp == "aggressive":
        pp_score = 40
        findings.append(_crit("protective_provisions",
            "Aggressive protective provisions can require investor consent for routine operating "
            "decisions (hiring execs, budget changes, vendor contracts). Push back to NVCA standard."))
    else:
        pp_score = 70
        findings.append(_warn("protective_provisions", f"Verify scope with counsel: {pp}"))
    scores.append(pp_score)

    # --- 9. Information Rights ---
    ir = ts.get("information_rights", "standard")
    if ir == "standard":
        ir_score = 100
        findings.append(_ok("information_rights",
            "Standard information rights (quarterly financials, annual audited, budget) — acceptable."))
    elif ir == "aggressive":
        ir_score = 60
        findings.append(_warn("information_rights",
            "Aggressive information rights (monthly financials, board observer rights, inspection rights). "
            "Reasonable for lead at Series B+; at Series A, push to quarterly."))
    else:
        ir_score = 75
        findings.append(_warn("information_rights", f"Verify: {ir}"))
    scores.append(ir_score)

    # --- 10. Dividends ---
    div = ts.get("dividends", "none")
    if div == "none":
        div_score = 100
        findings.append(_ok("dividends", "No dividend obligation — founder-friendly standard."))
    elif div == "non_cumulative_when_declared":
        div_score = 80
        findings.append(_ok("dividends",
            "Non-cumulative when-declared dividends — acceptable; rare to actually be paid."))
    elif div == "cumulative":
        div_score = 30
        findings.append(_crit("dividends",
            "CUMULATIVE dividends accrue every year regardless of declaration and must be paid at exit. "
            "Hostile; push to non-cumulative or none."))
    else:
        div_score = 60
        findings.append(_warn("dividends", f"Verify dividend type: {div}"))
    scores.append(div_score)

    # --- 11. Valuation Sanity ---
    pre = ts.get("pre_money", 0)
    raise_amt = ts.get("raise_amount", 0)
    if pre and raise_amt:
        post = pre + raise_amt
        dilution = (raise_amt / post) * 100
        if dilution > 30:
            val_score = 40
            findings.append(_crit("valuation",
                f"Round dilutes {dilution:.1f}% (raise ${raise_amt:,} on ${pre:,} pre = ${post:,} post). "
                "Over 30% in a single round is heavy; standard is 15-25%."))
        elif dilution > 25:
            val_score = 70
            findings.append(_warn("valuation",
                f"Round dilutes {dilution:.1f}%. Acceptable but on the high end. Standard 15-25%."))
        else:
            val_score = 100
            findings.append(_ok("valuation",
                f"Round dilutes {dilution:.1f}% — within standard 15-25% range."))
        scores.append(val_score)

    # --- 12. Holistic posture ---
    crit_count = sum(1 for f in findings if f["severity"] == "CRITICAL")
    if crit_count >= 3:
        findings.append(_crit("holistic",
            f"{crit_count} CRITICAL flags. This is a hostile term sheet. Either renegotiate the worst clauses "
            "or walk. Do not sign as-is."))
    elif crit_count >= 1:
        findings.append(_warn("holistic",
            f"{crit_count} CRITICAL flag(s). Address before signing; the rest is negotiable but not "
            "disqualifying."))
    else:
        findings.append(_ok("holistic", "No critical flags. Standard founder-friendly term sheet."))

    total_score = round(sum(scores) / len(scores)) if scores else 0
    return total_score, findings


def _ok(clause: str, msg: str) -> Dict[str, Any]:
    return {"clause": clause, "severity": "OK", "message": msg}

def _warn(clause: str, msg: str) -> Dict[str, Any]:
    return {"clause": clause, "severity": "WARN", "message": msg}

def _crit(clause: str, msg: str) -> Dict[str, Any]:
    return {"clause": clause, "severity": "CRITICAL", "message": msg}


def render_text(score_val: int, findings: List[Dict[str, Any]], source: str) -> str:
    lines = []
    lines.append("=" * 72)
    lines.append("TERM SHEET ANALYSIS")
    lines.append(f"Source: {source}")
    lines.append("=" * 72)
    lines.append("")
    grade = (
        "🟢 FOUNDER-FRIENDLY" if score_val >= 85 else
        "🟡 NEGOTIATE"          if score_val >= 65 else
        "🔴 HOSTILE"
    )
    lines.append(f"Founder-friendliness score: {score_val}/100   {grade}")
    lines.append("")
    lines.append("-" * 72)

    for f in findings:
        sev = f["severity"]
        marker = {"OK": "✅", "WARN": "⚠[U+FE0F] ", "CRITICAL": "🚨"}.get(sev, "•")
        lines.append(f"{marker} [{sev:>8}] {f['clause']}")
        lines.append(f"           {f['message']}")
        lines.append("")

    lines.append("-" * 72)
    lines.append("REMINDER: This tool is not legal advice. Always engage venture / securities counsel.")
    return "\n".join(lines)


def main() -> int:
    parser = argparse.ArgumentParser(
        description="Score a term sheet on founder-friendliness across 12 dimensions.",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog=__doc__,
    )
    parser.add_argument("path", nargs="?", help="Path to term sheet JSON file (uses embedded sample if omitted)")
    parser.add_argument("--output", choices=("text", "json"), default="text", help="Output format")
    args = parser.parse_args()

    if args.path:
        try:
            with open(args.path, "r", encoding="utf-8") as f:
                ts = json.load(f)
            source = args.path
        except (IOError, OSError) as e:
            print(f"error: could not read {args.path}: {e}", file=sys.stderr)
            return 1
        except json.JSONDecodeError as e:
            print(f"error: invalid JSON in {args.path}: {e}", file=sys.stderr)
            return 1
    else:
        ts = SAMPLE
        source = "<embedded sample Series A term sheet>"

    score_val, findings = score(ts)

    if args.output == "json":
        print(json.dumps({
            "source": source,
            "score": score_val,
            "grade": "FOUNDER_FRIENDLY" if score_val >= 85 else "NEGOTIATE" if score_val >= 65 else "HOSTILE",
            "findings": findings,
        }, indent=2))
    else:
        print(render_text(score_val, findings, source))

    return 0


if __name__ == "__main__":
    sys.exit(main())